@cryptotaxi247 / CoPilot / commits / 5ebad050

Atomic invoke (#450)

* some precommit fixes * Fix message formatting in MITRE response for alerts and techniques * Fix message formatting in list_atomic_tests response for accuracy * Add artifact retrieval and parameter filtering endpoints * Update Singul integration to use correct authentication and include org_id in message fields * chore: update frontend dependencies * feat: add Atomic test in technique details * fix: update authentication placeholders in Singul integration * feat: update artifacts api/types * feat: add WindowsAttackSimulator components * feat: update WindowsAttackSimulator components * feat: update WindowsAttackSimulator components * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Jun 1, 2025 at 12:52 UTC 5ebad050b4c6fb7a790c3399c56211d730c796b9
26 files changed +1322 -393
backend/app/connectors/graylog/schema/streams.py
+1 -1
@@ -19,7 +19,7 @@ class Stream(BaseModel):
19 content_pack: Optional[str]
20 created_at: str
21 creator_user_id: str
22 - description: Optional[str] = Field('No description provided')
22 + description: Optional[str] = Field("No description provided")
23 disabled: bool
24 id: str
25 index_set_id: str
backend/app/connectors/shuffle/schema/singul.py
+1 -8
@@ -1,14 +1,7 @@
1 -from typing import Any
2 -from typing import Dict
3 -from typing import List
4 -from typing import Optional
5 -
6 -from fastapi import HTTPException
1 from pydantic import BaseModel
2 from pydantic import Field
9 -from pydantic import root_validator
3
4
5 class SingulRequest(BaseModel):
6 app: str = Field(..., description="The name of the application", example="outlook_office365")
14 -
7 + org_id: str = Field(..., description="The organization ID", example="org_12345")
backend/app/connectors/shuffle/services/singul.py
+6 -5
@@ -1,9 +1,10 @@
1 from loguru import logger
2 from shufflepy import Singul
3 +
4 from app.connectors.shuffle.schema.singul import SingulRequest
4 -from app.connectors.shuffle.utils.universal import send_post_request
5
6 -singul = Singul(auth="TEMP", url="https://singul.io")
6 +singul = Singul(auth="REPLACE", url="https://shuffler.io")
7 +
8
9 async def execute_singul(
10 request: SingulRequest,
@@ -20,15 +21,15 @@ async def execute_singul(
21 logger.info("Executing Singul integration")
22 response = singul.communication.send_message(
23 app=request.app,
24 + auth_id="REPLACE",
25 fields=[
24 - {"key": "to", "value": "walton.taylor23@gmail.com"},
26 + {"key": "to", "value": "REPLACE"},
27 {"key": "subject", "value": "Test Email from Singul"},
28 {"key": "body", "value": "This is a test email sent from Singul."},
27 - ]
29 + ],
30 )
31 logger.info(f"Singul response: {response}")
32 return {
33 "executionId": response.get("id", "unknown"),
34 "message": "Singul integration executed successfully",
35 }
34 -
backend/app/connectors/velociraptor/routes/artifacts.py
+46
@@ -9,6 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
9 from sqlalchemy.future import select
10
11 from app.auth.utils import AuthHandler
12 +from app.connectors.velociraptor.schema.artifacts import ArtifactParametersResponse
13 from app.connectors.velociraptor.schema.artifacts import ArtifactReccomendationAIRequest
14 from app.connectors.velociraptor.schema.artifacts import ArtifactReccomendationRequest
15 from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
@@ -21,6 +22,10 @@ from app.connectors.velociraptor.schema.artifacts import QuarantineBody
22 from app.connectors.velociraptor.schema.artifacts import QuarantineResponse
23 from app.connectors.velociraptor.schema.artifacts import RunCommandBody
24 from app.connectors.velociraptor.schema.artifacts import RunCommandResponse
25 +from app.connectors.velociraptor.services.artifacts import get_artifact_by_name
26 +from app.connectors.velociraptor.services.artifacts import (
27 + get_artifact_parameters_by_prefix_service,
28 +)
29 from app.connectors.velociraptor.services.artifacts import get_artifacts
30 from app.connectors.velociraptor.services.artifacts import post_to_copilot_ai_module
31 from app.connectors.velociraptor.services.artifacts import quarantine_host
@@ -268,6 +273,47 @@ async def get_all_artifacts_for_os_prefix(
273 )
274
275
276 +@velociraptor_artifacts_router.get(
277 + "/artifact/{artifact_name}",
278 + response_model=ArtifactsResponse,
279 + description="Get a specific artifact by name",
280 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
281 +)
282 +async def get_artifact_by_name_route(artifact_name: str) -> ArtifactsResponse:
283 + """
284 + Retrieve a specific artifact by its name.
285 +
286 + Args:
287 + artifact_name (str): The name of the artifact to retrieve.
288 +
289 + Returns:
290 + ArtifactsResponse: The response containing the specific artifact.
291 + """
292 + logger.info(f"Fetching artifact by name: {artifact_name}")
293 + return await get_artifact_by_name(artifact_name)
294 +
295 +
296 +@velociraptor_artifacts_router.get(
297 + "/artifact/{artifact_name}/parameters/{parameter_prefix}",
298 + response_model=ArtifactParametersResponse,
299 + description="Get parameters from an artifact that match a specific prefix",
300 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
301 +)
302 +async def get_artifact_parameters_by_prefix(artifact_name: str, parameter_prefix: str) -> ArtifactParametersResponse:
303 + """
304 + Retrieve parameters from a specific artifact that start with the given prefix.
305 +
306 + Args:
307 + artifact_name (str): The name of the artifact to retrieve parameters from.
308 + parameter_prefix (str): The prefix to filter parameters by (e.g., "T1552.001").
309 +
310 + Returns:
311 + ArtifactParametersResponse: The response containing matching parameters.
312 + """
313 + logger.info(f"Fetching parameters with prefix '{parameter_prefix}' from artifact '{artifact_name}'")
314 + return await get_artifact_parameters_by_prefix_service(artifact_name, parameter_prefix)
315 +
316 +
317 @velociraptor_artifacts_router.get(
318 "/hostname/{hostname}",
319 response_model=ArtifactsResponse,
backend/app/connectors/velociraptor/schema/artifacts.py
+41
@@ -11,9 +11,24 @@ from pydantic import Field
11 from pydantic import validator
12
13
14 +class ArtifactParameter(BaseModel):
15 + """Represents a parameter definition from Velociraptor artifact."""
16 +
17 + name: str = Field(..., description="Parameter name")
18 + description: Optional[str] = Field(None, description="Parameter description")
19 + type: Optional[str] = Field(None, description="Parameter type (e.g., 'bool', 'string')")
20 + default: Optional[Union[str, bool]] = Field(None, description="Default value for the parameter")
21 +
22 +
23 class Artifacts(BaseModel):
24 description: str = Field(..., description="Description of the artifact.")
25 name: str = Field(..., description="Name of the artifact.")
26 + author: Optional[str] = Field(None, description="Author of the artifact.")
27 + precondition: Optional[str] = Field(None, description="Precondition for running the artifact.")
28 + parameters: Optional[List[ArtifactParameter]] = Field(
29 + None,
30 + description="List of parameters that can be configured for this artifact.",
31 + )
32
33
34 class ArtifactsResponse(BaseModel):
@@ -23,6 +38,32 @@ class ArtifactsResponse(BaseModel):
38 success: str = Field(...)
39
40
41 +class ArtifactParametersResponse(BaseModel):
42 + """Response containing filtered artifact parameters."""
43 +
44 + success: bool = Field(..., description="Whether the request was successful")
45 + message: str = Field(..., description="Response message")
46 + artifact_name: str = Field(..., description="Name of the artifact")
47 + parameter_prefix: str = Field(..., description="The prefix used for filtering")
48 + matching_parameters: List[ArtifactParameter] = Field(default_factory=list, description="List of parameters that match the prefix")
49 + total_matches: int = Field(..., description="Total number of matching parameters")
50 +
51 + class Config:
52 + schema_extra = {
53 + "example": {
54 + "success": True,
55 + "message": "Found 2 parameters matching prefix 'T1552.001'",
56 + "artifact_name": "Windows.AttackSimulation.AtomicRedTeam",
57 + "parameter_prefix": "T1552.001",
58 + "matching_parameters": [
59 + {"name": "T1552.001 - 3", "description": "Credentials In Files - Extracting passwords with findstr", "type": "bool"},
60 + {"name": "T1552.001 - 4", "description": "Credentials In Files - Access unattend.xml", "type": "bool"},
61 + ],
62 + "total_matches": 2,
63 + },
64 + }
65 +
66 +
67 class OSPrefixEnum(Enum):
68 LINUX = "Linux."
69 WINDOWS = "Windows."
backend/app/connectors/velociraptor/services/artifacts.py
+117
@@ -2,6 +2,7 @@ import httpx
2 from fastapi import HTTPException
3 from loguru import logger
4
5 +from app.connectors.velociraptor.schema.artifacts import ArtifactParametersResponse
6 from app.connectors.velociraptor.schema.artifacts import ArtifactReccomendationRequest
7 from app.connectors.velociraptor.schema.artifacts import ArtifactReccomendationResponse
8 from app.connectors.velociraptor.schema.artifacts import Artifacts
@@ -106,6 +107,122 @@ async def get_artifacts() -> ArtifactsResponse:
107 )
108
109
110 +async def get_artifact_by_name(artifact_name: str) -> ArtifactsResponse:
111 + """
112 + Get a specific artifact by name from Velociraptor.
113 +
114 + Args:
115 + artifact_name (str): The name of the artifact to retrieve.
116 +
117 + Returns:
118 + ArtifactsResponse: A response containing the specific artifact.
119 + """
120 + logger.info(f"Fetching artifact '{artifact_name}' from Velociraptor")
121 + velociraptor_service = await UniversalService.create("Velociraptor")
122 +
123 + # Query for a specific artifact by name
124 + query = create_query(f"SELECT name,description,parameters FROM artifact_definitions() WHERE name = '{artifact_name}'")
125 + artifact_result = velociraptor_service.execute_query(query)
126 +
127 + try:
128 + if artifact_result["success"]:
129 + if artifact_result["results"]:
130 + artifacts = [Artifacts(**artifact) for artifact in artifact_result["results"]]
131 + return ArtifactsResponse(
132 + success=True,
133 + message=f"Artifact '{artifact_name}' retrieved successfully",
134 + artifacts=artifacts,
135 + )
136 + else:
137 + return ArtifactsResponse(
138 + success=True,
139 + message=f"Artifact '{artifact_name}' not found",
140 + artifacts=[],
141 + )
142 + else:
143 + raise HTTPException(
144 + status_code=500,
145 + detail=f"Failed to get artifact '{artifact_name}': {artifact_result['message']}",
146 + )
147 + except Exception as err:
148 + logger.error(f"Failed to get artifact '{artifact_name}': {err}")
149 + raise HTTPException(
150 + status_code=500,
151 + detail=f"Failed to get artifact '{artifact_name}': {err}",
152 + )
153 +
154 +
155 +async def get_artifact_parameters_by_prefix_service(
156 + artifact_name: str,
157 + parameter_prefix: str,
158 +) -> ArtifactParametersResponse:
159 + """
160 + Get parameters from a specific artifact that match a given prefix.
161 +
162 + Args:
163 + artifact_name (str): The name of the artifact to retrieve parameters from.
164 + parameter_prefix (str): The prefix to filter parameters by.
165 +
166 + Returns:
167 + ArtifactParametersResponse: A response containing matching parameters.
168 + """
169 + logger.info(f"Fetching parameters with prefix '{parameter_prefix}' from artifact '{artifact_name}'")
170 +
171 + try:
172 + # First, get the artifact with its parameters
173 + artifact_response = await get_artifact_by_name(artifact_name)
174 +
175 + if not artifact_response.success or not artifact_response.artifacts:
176 + return ArtifactParametersResponse(
177 + success=False,
178 + message=f"Artifact '{artifact_name}' not found",
179 + artifact_name=artifact_name,
180 + parameter_prefix=parameter_prefix,
181 + matching_parameters=[],
182 + total_matches=0,
183 + )
184 +
185 + artifact = artifact_response.artifacts[0]
186 +
187 + # Filter parameters by prefix
188 + matching_parameters = []
189 + if artifact.parameters:
190 + for param in artifact.parameters:
191 + if param.name.startswith(parameter_prefix):
192 + matching_parameters.append(param)
193 +
194 + # Sort the matching parameters for consistent ordering
195 + matching_parameters.sort(key=lambda x: x.name)
196 +
197 + total_matches = len(matching_parameters)
198 +
199 + if total_matches == 0:
200 + message = f"No parameters found matching prefix '{parameter_prefix}' in artifact '{artifact_name}'"
201 + elif total_matches == 1:
202 + message = f"Found 1 parameter matching prefix '{parameter_prefix}' in artifact '{artifact_name}'"
203 + else:
204 + message = f"Found {total_matches} parameters matching prefix '{parameter_prefix}' in artifact '{artifact_name}'"
205 +
206 + logger.info(message)
207 +
208 + return ArtifactParametersResponse(
209 + success=True,
210 + message=message,
211 + artifact_name=artifact_name,
212 + parameter_prefix=parameter_prefix,
213 + matching_parameters=matching_parameters,
214 + total_matches=total_matches,
215 + )
216 +
217 + except Exception as err:
218 + error_message = f"Failed to get parameters with prefix '{parameter_prefix}' from artifact '{artifact_name}': {err}"
219 + logger.error(error_message)
220 + raise HTTPException(
221 + status_code=500,
222 + detail=error_message,
223 + )
224 +
225 +
226 async def run_artifact_collection(
227 collect_artifact_body: CollectArtifactBody,
228 ) -> CollectArtifactResponse:
backend/app/connectors/wazuh_manager/routes/mitre.py
+51 -47
@@ -3,20 +3,34 @@ from typing import List
3 from typing import Optional
4
5 from fastapi import APIRouter
6 +from fastapi import HTTPException
7 from fastapi import Path
8 from fastapi import Query
9 from fastapi import Security
10 from loguru import logger
10 -from fastapi import HTTPException
11
12 from app.auth.routes.auth import AuthHandler
13 from app.connectors.wazuh_manager.schema.mitre import AtomicRedTeamMarkdownResponse
14 -from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTacticsResponse, AtomicTestsListResponse
15 -from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTechniquesResponse, MitreTechniquesInAlertsResponse, MitreTechniqueInAlert, MitreTechniqueAlertsResponse, WazuhMitreSoftwareResponse, WazuhMitreReferencesResponse, WazuhMitreMitigationsResponse, WazuhMitreGroupsResponse
16 -from app.connectors.wazuh_manager.services.mitre import get_alerts_by_mitre_id, get_mitre_references, get_mitre_groups
14 +from app.connectors.wazuh_manager.schema.mitre import AtomicTestsListResponse
15 +from app.connectors.wazuh_manager.schema.mitre import MitreTechniqueAlertsResponse
16 +from app.connectors.wazuh_manager.schema.mitre import MitreTechniquesInAlertsResponse
17 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreGroupsResponse
18 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreMitigationsResponse
19 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreReferencesResponse
20 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreSoftwareResponse
21 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTacticsResponse
22 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTechniquesResponse
23 from app.connectors.wazuh_manager.services.mitre import AtomicRedTeamService
18 -from app.connectors.wazuh_manager.services.mitre import get_mitre_tactics, get_mitre_software, get_mitre_mitigations
19 -from app.connectors.wazuh_manager.services.mitre import get_mitre_techniques, search_mitre_techniques_in_alerts
24 +from app.connectors.wazuh_manager.services.mitre import get_alerts_by_mitre_id
25 +from app.connectors.wazuh_manager.services.mitre import get_mitre_groups
26 +from app.connectors.wazuh_manager.services.mitre import get_mitre_mitigations
27 +from app.connectors.wazuh_manager.services.mitre import get_mitre_references
28 +from app.connectors.wazuh_manager.services.mitre import get_mitre_software
29 +from app.connectors.wazuh_manager.services.mitre import get_mitre_tactics
30 +from app.connectors.wazuh_manager.services.mitre import get_mitre_techniques
31 +from app.connectors.wazuh_manager.services.mitre import (
32 + search_mitre_techniques_in_alerts,
33 +)
34
35 # Initialize router and auth handler
36 wazuh_manager_mitre_router = APIRouter()
@@ -51,9 +65,8 @@ async def list_mitre_groups(
65 Returns:
66 WazuhMitreGroupsResponse: A list of MITRE ATT&CK groups matching the criteria.
67 """
54 - return await get_mitre_groups(
55 - limit=limit, offset=offset, select=select, sort=sort, search=search, q=q
56 - )
68 + return await get_mitre_groups(limit=limit, offset=offset, select=select, sort=sort, search=search, q=q)
69 +
70
71 @wazuh_manager_mitre_router.get(
72 "/mitigations",
@@ -83,9 +96,8 @@ async def list_mitre_mitigations(
96 Returns:
97 WazuhMitreMitigationsResponse: A list of MITRE ATT&CK mitigations matching the criteria.
98 """
86 - return await get_mitre_mitigations(
87 - limit=limit, offset=offset, select=select, sort=sort, search=search, q=q
88 - )
99 + return await get_mitre_mitigations(limit=limit, offset=offset, select=select, sort=sort, search=search, q=q)
100 +
101
102 @wazuh_manager_mitre_router.get(
103 "/references",
@@ -113,9 +125,8 @@ async def list_mitre_references(
125 Returns:
126 WazuhMitreReferencesResponse: A list of MITRE ATT&CK references matching the criteria.
127 """
116 - return await get_mitre_references(
117 - limit=limit, offset=offset, sort=sort, search=search, q=q
118 - )
128 + return await get_mitre_references(limit=limit, offset=offset, sort=sort, search=search, q=q)
129 +
130
131 @wazuh_manager_mitre_router.get(
132 "/software",
@@ -145,9 +156,8 @@ async def list_mitre_software(
156 Returns:
157 WazuhMitreSoftwareResponse: A list of MITRE ATT&CK software matching the criteria.
158 """
148 - return await get_mitre_software(
149 - limit=limit, offset=offset, select=select, sort=sort, search=search, q=q
150 - )
159 + return await get_mitre_software(limit=limit, offset=offset, select=select, sort=sort, search=search, q=q)
160 +
161
162 @wazuh_manager_mitre_router.get(
163 "/tactics",
@@ -251,19 +261,20 @@ async def list_atomic_tests(
261
262 return AtomicTestsListResponse(
263 success=True,
254 - message=f"Found {total_techniques} techniques with {result.get('total_tests', 'many')} atomic tests (page {page} of {total_pages})",
264 + message=f"Found {total_techniques} MITRE techniques in {result['total_techniques']} alerts (page {page} of {total_pages},)",
265 total_techniques=total_techniques,
266 total_tests=result.get("total_tests"),
267 tests=paginated_tests,
268 last_updated=result["last_updated"],
269 page=page,
270 page_size=size,
261 - total_pages=total_pages
271 + total_pages=total_pages,
272 )
273 except Exception as e:
274 logger.error(f"Error retrieving atomic tests: {str(e)}")
275 raise HTTPException(status_code=500, detail=f"Error retrieving atomic tests: {str(e)}")
276
277 +
278 @wazuh_manager_mitre_router.get(
279 "/techniques/{technique_id}/atomic-tests",
280 response_model=AtomicRedTeamMarkdownResponse,
@@ -302,6 +313,7 @@ async def get_technique_atomic_tests(technique_id: str = Path(..., description="
313 markdown_content=markdown_content,
314 )
315
316 +
317 @wazuh_manager_mitre_router.get(
318 "/techniques/alerts",
319 response_model=MitreTechniquesInAlertsResponse,
@@ -327,14 +339,10 @@ async def list_mitre_techniques_in_alerts(
339 additional_filters = []
340
341 if rule_level is not None:
330 - additional_filters.append({
331 - "match_phrase": {"rule_level": {"query": str(rule_level)}}
332 - })
342 + additional_filters.append({"match_phrase": {"rule_level": {"query": str(rule_level)}}})
343
344 if rule_group is not None:
335 - additional_filters.append({
336 - "match_phrase": {"rule_groups": {"query": rule_group}}
337 - })
345 + additional_filters.append({"match_phrase": {"rule_groups": {"query": rule_group}}})
346
347 # Execute the search with the specified parameters
348 results = await search_mitre_techniques_in_alerts(
@@ -343,26 +351,26 @@ async def list_mitre_techniques_in_alerts(
351 offset=offset,
352 additional_filters=additional_filters,
353 index_pattern=index_pattern,
346 - mitre_field=mitre_field
354 + mitre_field=mitre_field,
355 )
356
357 # Get the total number of techniques (from all pages)
350 - total_techniques = results.get('total_techniques_count', results['techniques_count'])
358 + total_techniques = results.get("total_techniques_count", results["techniques_count"])
359
360 # Calculate total pages based on the total number of techniques
361 total_pages = (total_techniques + size - 1) // size if total_techniques > 0 else 1
362
363 return MitreTechniquesInAlertsResponse(
364 success=True,
357 - message=f"Found {total_techniques} MITRE techniques in {results['total_alerts']} alerts (page {page} of {total_pages})",
358 - total_alerts=results['total_alerts'],
365 + message=f"Found {total_techniques} MITRE techniques in {results['total_alerts']} alerts (page {page} of {total_pages},)",
366 + total_alerts=results["total_alerts"],
367 techniques_count=total_techniques, # Use the total count for all pages
360 - techniques=results['techniques'], # Use current page techniques
368 + techniques=results["techniques"], # Use current page techniques
369 time_range=time_range,
362 - field_used=results.get('field_used', 'unknown'),
370 + field_used=results.get("field_used", "unknown"),
371 page=page,
372 page_size=size,
365 - total_pages=total_pages
373 + total_pages=total_pages,
374 )
375
376
@@ -395,14 +403,10 @@ async def get_mitre_technique_alerts(
403 additional_filters = []
404
405 if rule_level is not None:
398 - additional_filters.append({
399 - "match_phrase": {"rule_level": {"query": str(rule_level)}}
400 - })
406 + additional_filters.append({"match_phrase": {"rule_level": {"query": str(rule_level)}}})
407
408 if rule_group is not None:
403 - additional_filters.append({
404 - "match_phrase": {"rule_groups": {"query": rule_group}}
405 - })
409 + additional_filters.append({"match_phrase": {"rule_groups": {"query": rule_group}}})
410
411 # Get the alerts
412 results = await get_alerts_by_mitre_id(
@@ -412,19 +416,19 @@ async def get_mitre_technique_alerts(
416 offset=offset,
417 additional_filters=additional_filters,
418 index_pattern=index_pattern,
415 - mitre_field=mitre_field
419 + mitre_field=mitre_field,
420 )
421
422 return MitreTechniqueAlertsResponse(
423 success=True,
420 - message=f"Found {results['total_alerts']} alerts for MITRE technique {clean_technique_id} (page {page} of {(results['total_alerts'] + size - 1) // size})",
421 - technique_id=results['technique_id'],
422 - technique_name=results['technique_name'],
423 - total_alerts=results['total_alerts'],
424 - alerts=results['alerts'],
425 - field_used=results.get('field_used', 'unknown'),
424 + message=f"Found {results['total_alerts']} alerts for MITRE technique {clean_technique_id} (page {page} of {(results['total_alerts'] + size - 1) // size},)",
425 + technique_id=results["technique_id"],
426 + technique_name=results["technique_name"],
427 + total_alerts=results["total_alerts"],
428 + alerts=results["alerts"],
429 + field_used=results.get("field_used", "unknown"),
430 time_range=time_range,
431 page=page,
432 page_size=size,
429 - total_pages=(results['total_alerts'] + size - 1) // size
433 + total_pages=(results["total_alerts"] + size - 1) // size,
434 )
backend/app/connectors/wazuh_manager/schema/mitre.py
+23 -1
@@ -3,7 +3,8 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 -from pydantic import BaseModel, Field
6 +from pydantic import BaseModel
7 +from pydantic import Field
8
9
10 class MitreTacticItem(BaseModel):
@@ -123,14 +124,17 @@ class AtomicRedTeamMarkdownResponse(BaseModel):
124
125 class AtomicTestSummary(BaseModel):
126 """Summary information about an Atomic Red Team test."""
127 +
128 technique_id: str = Field(..., description="MITRE ATT&CK technique ID")
129 technique_name: str = Field(..., description="MITRE ATT&CK technique name")
130 test_count: int = Field(..., description="Number of atomic tests available for this technique")
131 categories: List[str] = Field(default_factory=list, description="Categories/platforms the tests cover")
132 has_prerequisites: bool = Field(False, description="Whether the tests have prerequisites")
133
134 +
135 class AtomicTestsListResponse(BaseModel):
136 """Response model for listing all available Atomic Red Team tests."""
137 +
138 success: bool = Field(True, description="Whether the request was successful")
139 message: str = Field(..., description="Response message")
140 total_techniques: int = Field(..., description="Total number of techniques with atomic tests")
@@ -141,8 +145,10 @@ class AtomicTestsListResponse(BaseModel):
145 page_size: int = Field(..., description="Number of items per page")
146 total_pages: int = Field(..., description="Total number of pages available")
147
148 +
149 class MitreTechniqueInAlert(BaseModel):
150 """Schema for a MITRE technique found in alerts."""
151 +
152 technique_id: str = Field(..., description="MITRE ATT&CK technique ID")
153 technique_name: str = Field(..., description="MITRE ATT&CK technique name")
154 count: int = Field(..., description="Number of alerts containing this technique")
@@ -152,6 +158,7 @@ class MitreTechniqueInAlert(BaseModel):
158
159 class MitreTechniquesInAlertsResponse(BaseModel):
160 """Response schema for MITRE techniques found in alerts."""
161 +
162 success: bool = Field(True, description="Whether the request was successful")
163 message: str = Field(..., description="Description of the response")
164 total_alerts: int = Field(..., description="Total number of alerts matching the query")
@@ -166,6 +173,7 @@ class MitreTechniquesInAlertsResponse(BaseModel):
173
174 class MitreTechniqueAlertsResponse(BaseModel):
175 """Response schema for detailed alerts associated with a specific MITRE technique."""
176 +
177 success: bool = Field(True, description="Whether the request was successful")
178 message: str = Field(..., description="Description of the response")
179 technique_id: str = Field(..., description="The MITRE technique ID that was searched for")
@@ -203,6 +211,7 @@ class MitreSoftwareItem(BaseModel):
211
212 class Config:
213 """Configuration for the model."""
214 +
215 extra = "ignore" # Ignore extra fields from the API
216
217
@@ -213,8 +222,10 @@ class WazuhMitreSoftwareResponse(BaseModel):
222 message: str
223 results: List[MitreSoftwareItem] = []
224
225 +
226 class MitreReferenceItem(BaseModel):
227 """Represents a single MITRE ATT&CK reference from Wazuh's API."""
228 +
229 url: str
230 description: Optional[str] = None
231 source: str
@@ -223,17 +234,22 @@ class MitreReferenceItem(BaseModel):
234
235 class Config:
236 """Configuration for the model."""
237 +
238 extra = "ignore" # Ignore extra fields from the API
239
240 +
241 class WazuhMitreReferencesResponse(BaseModel):
242 """Response model for the MITRE references endpoint."""
243 +
244 success: bool
245 message: str
246 results: List[MitreReferenceItem] = []
247 total: int = 0
248
249 +
250 class MitreMitigationItem(BaseModel):
251 """Represents a single MITRE ATT&CK mitigation from Wazuh's API."""
252 +
253 mitre_version: Optional[str] = None
254 deprecated: int = 0
255 description: str
@@ -249,18 +265,22 @@ class MitreMitigationItem(BaseModel):
265
266 class Config:
267 """Configuration for the model."""
268 +
269 extra = "ignore" # Ignore extra fields from the API
270
271
272 class WazuhMitreMitigationsResponse(BaseModel):
273 """Response model for the MITRE mitigations endpoint."""
274 +
275 success: bool
276 message: str
277 results: List[MitreMitigationItem] = []
278 total: int = 0
279
280 +
281 class MitreGroupItem(BaseModel):
282 """Represents a single MITRE ATT&CK group from Wazuh's API."""
283 +
284 mitre_version: Optional[str] = None
285 deprecated: int = 0
286 description: Optional[str] = None
@@ -281,11 +301,13 @@ class MitreGroupItem(BaseModel):
301
302 class Config:
303 """Configuration for the model."""
304 +
305 extra = "ignore" # Ignore extra fields from the API
306
307
308 class WazuhMitreGroupsResponse(BaseModel):
309 """Response model for the MITRE groups endpoint."""
310 +
311 success: bool
312 message: str
313 results: List[MitreGroupItem] = []
backend/app/connectors/wazuh_manager/services/mitre.py
+105 -136
@@ -1,25 +1,30 @@
1 +import asyncio
2 +import re
3 import time
4 +from datetime import datetime
5 from typing import Dict
6 from typing import List
7 from typing import Optional
5 -from typing import Tuple, Union
6 -from datetime import datetime
7 -import yaml
8 +from typing import Tuple
9 +from typing import Union
10 +
11 import aiohttp
9 -import asyncio
12 +import yaml
13 +from elasticsearch7 import AsyncElasticsearch
14 from fastapi import HTTPException
15 from loguru import logger
12 -import re
13 -import json
16 from pydantic import ValidationError
17
16 -from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTacticsResponse
17 -from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTechniquesResponse, WazuhMitreSoftwareResponse, WazuhMitreReferencesResponse, WazuhMitreMitigationsResponse, WazuhMitreGroupsResponse
18 -from app.connectors.wazuh_manager.utils.universal import send_get_request
18 from app.connectors.wazuh_indexer.utils.universal import (
19 create_wazuh_indexer_client_async,
20 )
22 -from elasticsearch7 import AsyncElasticsearch
21 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreGroupsResponse
22 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreMitigationsResponse
23 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreReferencesResponse
24 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreSoftwareResponse
25 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTacticsResponse
26 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTechniquesResponse
27 +from app.connectors.wazuh_manager.utils.universal import send_get_request
28
29 # Constants for the Atomic Red Team GitHub repository
30 GITHUB_RAW_URL = "https://raw.githubusercontent.com/redcanaryco/atomic-red-team/refs/heads/master/atomics"
@@ -47,11 +52,7 @@ class AtomicRedTeamService:
52 tests, timestamp = cls._tests_cache["all_tests"]
53 if time.time() - timestamp < CACHE_EXPIRY:
54 logger.debug("Returning cached list of all atomic tests")
50 - return {
51 - "total_techniques": len(tests),
52 - "tests": tests,
53 - "last_updated": datetime.fromtimestamp(timestamp).isoformat()
54 - }
55 + return {"total_techniques": len(tests), "tests": tests, "last_updated": datetime.fromtimestamp(timestamp).isoformat()}
56
57 # Fetch the list of all techniques with atomic tests
58 try:
@@ -68,11 +69,12 @@ class AtomicRedTeamService:
69 except Exception as e:
70 logger.error(f"Error listing atomic tests: {str(e)}")
71 raise HTTPException(status_code=500, detail=f"Error listing atomic tests: {str(e)}")
72 +
73 @classmethod
74 async def _parse_atomic_index_markdown(cls, content: str) -> Dict:
75 """Parse the atomic-red-team-index.md file to extract test information."""
76 techniques = []
75 - technique_pattern = r'\|\s*\[([^]]+)\]\([^)]+\)\s*\|\s*([T\d\.]+)\s*\|\s*(\d+)\s*\|'
77 + technique_pattern = r"\|\s*\[([^]]+)\]\([^)]+\)\s*\|\s*([T\d\.]+)\s*\|\s*(\d+)\s*\|"
78
79 matches = re.findall(technique_pattern, content)
80 total_tests = 0
@@ -81,13 +83,15 @@ class AtomicRedTeamService:
83 try:
84 count = int(test_count)
85 total_tests += count
84 - techniques.append({
85 - "technique_id": technique_id,
86 - "technique_name": name,
87 - "test_count": count,
88 - "categories": [], # Would require additional requests to determine
89 - "has_prerequisites": False # Would require additional requests to determine
90 - })
86 + techniques.append(
87 + {
88 + "technique_id": technique_id,
89 + "technique_name": name,
90 + "test_count": count,
91 + "categories": [], # Would require additional requests to determine
92 + "has_prerequisites": False, # Would require additional requests to determine
93 + },
94 + )
95 except ValueError:
96 continue # Skip if test_count isn't a valid integer
97
@@ -95,7 +99,7 @@ class AtomicRedTeamService:
99 "total_techniques": len(techniques),
100 "total_tests": total_tests,
101 "tests": techniques,
98 - "last_updated": datetime.utcnow().isoformat()
102 + "last_updated": datetime.utcnow().isoformat(),
103 }
104
105 # Cache the result
@@ -115,8 +119,7 @@ class AtomicRedTeamService:
119 async with session.get(url, headers=headers) as response:
120 if response.status != 200:
121 logger.error(f"GitHub API error: {response.status}")
118 - raise HTTPException(status_code=response.status,
119 - detail="Could not access Atomic Red Team repository")
122 + raise HTTPException(status_code=response.status, detail="Could not access Atomic Red Team repository")
123
124 folders = await response.json()
125
@@ -144,23 +147,23 @@ class AtomicRedTeamService:
147 yaml_content = await yaml_resp.text()
148 try:
149 data = yaml.safe_load(yaml_content)
147 - test_count = len(data.get('atomic_tests', []))
150 + test_count = len(data.get("atomic_tests", []))
151 total_tests += test_count
152 platforms = set()
153 has_prereqs = False
154
152 - for test in data.get('atomic_tests', []):
153 - if test.get('supported_platforms'):
154 - platforms.update(test.get('supported_platforms', []))
155 - if test.get('dependencies'):
155 + for test in data.get("atomic_tests", []):
156 + if test.get("supported_platforms"):
157 + platforms.update(test.get("supported_platforms", []))
158 + if test.get("dependencies"):
159 has_prereqs = True
160
161 return {
162 "technique_id": technique_id,
160 - "technique_name": data.get('display_name', technique_id),
163 + "technique_name": data.get("display_name", technique_id),
164 "test_count": test_count,
165 "categories": list(platforms),
163 - "has_prerequisites": has_prereqs
166 + "has_prerequisites": has_prereqs,
167 }
168 except Exception as e:
169 logger.warning(f"Error parsing YAML for {technique_id}: {e}")
@@ -171,29 +174,29 @@ class AtomicRedTeamService:
174 md_content = await md_resp.text()
175
176 # Extract name from markdown header
174 - name_match = re.search(r'# ([^\n]+)', md_content)
177 + name_match = re.search(r"# ([^\n]+)", md_content)
178 name = name_match.group(1) if name_match else technique_id
179
180 # Count atomic tests by headers
178 - test_headers = re.findall(r'## Atomic Test #\d+', md_content)
181 + test_headers = re.findall(r"## Atomic Test #\d+", md_content)
182 test_count = len(test_headers)
183 total_tests += test_count
184
185 # Look for platform indicators
186 platforms = []
184 - if 'windows' in md_content.lower():
185 - platforms.append('windows')
186 - if 'macos' in md_content.lower() or 'darwin' in md_content.lower():
187 - platforms.append('macos')
188 - if 'linux' in md_content.lower():
189 - platforms.append('linux')
187 + if "windows" in md_content.lower():
188 + platforms.append("windows")
189 + if "macos" in md_content.lower() or "darwin" in md_content.lower():
190 + platforms.append("macos")
191 + if "linux" in md_content.lower():
192 + platforms.append("linux")
193
194 return {
195 "technique_id": technique_id,
196 "technique_name": name.replace(f"- {technique_id}", "").strip(),
197 "test_count": test_count,
198 "categories": platforms,
196 - "has_prerequisites": 'dependency' in md_content.lower() or 'dependencies' in md_content.lower()
199 + "has_prerequisites": "dependency" in md_content.lower() or "dependencies" in md_content.lower(),
200 }
201
202 # If both methods fail, return basic info
@@ -202,7 +205,7 @@ class AtomicRedTeamService:
205 "technique_name": technique_id,
206 "test_count": 0,
207 "categories": [],
205 - "has_prerequisites": False
208 + "has_prerequisites": False,
209 }
210
211 # Process all techniques concurrently but with rate limiting
@@ -213,7 +216,7 @@ class AtomicRedTeamService:
216 "total_techniques": len(techniques),
217 "total_tests": total_tests,
218 "tests": techniques,
216 - "last_updated": datetime.utcnow().isoformat()
219 + "last_updated": datetime.utcnow().isoformat(),
220 }
221
222 # Cache the result
@@ -412,7 +415,6 @@ async def get_mitre_techniques(
415 raise HTTPException(status_code=500, detail=f"Error processing MITRE data: {str(e)}")
416
417
415 -
418 async def search_mitre_techniques_in_alerts(
419 time_range: str = "now-24h",
420 size: int = 1000,
@@ -473,7 +475,7 @@ async def search_mitre_techniques_in_alerts(
475 additional_filters=additional_filters,
476 index_pattern=index_pattern,
477 mitre_field=field,
476 - name_field=name_field
478 + name_field=name_field,
479 )
480
481 # Set size to 0 to just get counts
@@ -483,10 +485,11 @@ async def search_mitre_techniques_in_alerts(
485 count_response = await client.search(**count_query)
486
487 # If we get aggregations with buckets, we found the right field
486 - if (count_response.get("aggregations") and
487 - count_response["aggregations"].get("techniques") and
488 - count_response["aggregations"]["techniques"].get("buckets")):
489 -
488 + if (
489 + count_response.get("aggregations")
490 + and count_response["aggregations"].get("techniques")
491 + and count_response["aggregations"]["techniques"].get("buckets")
492 + ):
493 # Get total count of techniques
494 total_techniques = len(count_response["aggregations"]["techniques"]["buckets"])
495
@@ -498,7 +501,7 @@ async def search_mitre_techniques_in_alerts(
501 additional_filters=additional_filters,
502 index_pattern=index_pattern,
503 mitre_field=field,
501 - name_field=name_field
504 + name_field=name_field,
505 )
506
507 # Log the query for debugging
@@ -513,14 +516,16 @@ async def search_mitre_techniques_in_alerts(
516 mitre_field=field,
517 technique_mapping=technique_mapping,
518 name_field=name_field,
516 - technique_tactic_mapping=technique_tactic_mapping
519 + technique_tactic_mapping=technique_tactic_mapping,
520 )
521
522 # Update with the full count
523 results = page_results
524 results["total_techniques_count"] = total_techniques
525
523 - logger.info(f"Found {results['techniques_count']} MITRE techniques on this page, {total_techniques} total with field '{field}'")
526 + logger.info(
527 + f"Found {results['techniques_count']} MITRE techniques on this page, {total_techniques} total with field '{field}'",
528 + )
529 break
530 else:
531 logger.warning(f"No results found with field '{field}', trying next option")
@@ -539,7 +544,7 @@ async def search_mitre_techniques_in_alerts(
544 "techniques": [],
545 "field_used": None,
546 "attempted_fields": field_options,
542 - "errors": errors
547 + "errors": errors,
548 }
549
550 return results
@@ -563,11 +568,11 @@ async def _build_technique_id_name_mapping() -> Dict[str, str]:
568
569 # Create mapping from ID to name
570 technique_mapping = {}
566 - if techniques_response and hasattr(techniques_response, 'success') and techniques_response.success:
571 + if techniques_response and hasattr(techniques_response, "success") and techniques_response.success:
572 # Debug the response structure
573 logger.debug(f"Techniques response type: {type(techniques_response)}")
574
570 - if hasattr(techniques_response, 'results'):
575 + if hasattr(techniques_response, "results"):
576 techniques = techniques_response.results
577 logger.debug(f"Got {len(techniques)} techniques, first item type: {type(techniques[0]) if techniques else 'None'}")
578
@@ -585,7 +590,7 @@ async def _build_technique_id_name_mapping() -> Dict[str, str]:
590 technique_mapping[technique_id] = technique_name
591
592 # Sometimes the ID might be referenced without the 'T' prefix
588 - if technique_id.startswith('T'):
593 + if technique_id.startswith("T"):
594 technique_mapping[technique_id[1:]] = technique_name
595
596 logger.info(f"Built mapping for {len(technique_mapping)} MITRE techniques")
@@ -595,6 +600,7 @@ async def _build_technique_id_name_mapping() -> Dict[str, str]:
600 logger.exception(f"Error building technique mapping: {str(e)}")
601 return {} # Return empty mapping if error occurs
602
603 +
604 async def _build_technique_tactic_mapping() -> Dict[str, List[Dict[str, str]]]:
605 """
606 Build a mapping of MITRE technique IDs to their associated tactics.
@@ -608,14 +614,14 @@ async def _build_technique_tactic_mapping() -> Dict[str, List[Dict[str, str]]]:
614
615 # Create mapping from ID to tactics
616 technique_tactic_mapping = {}
611 - if hasattr(techniques_response, 'success') and techniques_response.success:
617 + if hasattr(techniques_response, "success") and techniques_response.success:
618 techniques = techniques_response.results
619
620 # Get all tactics for name lookup
621 tactics_response = await get_mitre_tactics(limit=1000)
622 tactic_name_mapping = {}
623
618 - if hasattr(tactics_response, 'success') and tactics_response.success:
624 + if hasattr(tactics_response, "success") and tactics_response.success:
625 for tactic in tactics_response.results:
626 if isinstance(tactic, dict):
627 tactic_id = tactic.get("id", "")
@@ -627,10 +633,7 @@ async def _build_technique_tactic_mapping() -> Dict[str, List[Dict[str, str]]]:
633 short_name = getattr(tactic, "short_name", "")
634
635 if tactic_id:
630 - tactic_name_mapping[tactic_id] = {
631 - "name": tactic_name,
632 - "short_name": short_name
633 - }
636 + tactic_name_mapping[tactic_id] = {"name": tactic_name, "short_name": short_name}
637
638 logger.debug(f"Built tactic name mapping with {len(tactic_name_mapping)} tactics")
639
@@ -651,7 +654,7 @@ async def _build_technique_tactic_mapping() -> Dict[str, List[Dict[str, str]]]:
654 tactic_info = {
655 "id": tactic_id,
656 "name": tactic_name_mapping.get(tactic_id, {}).get("name", "Unknown"),
654 - "short_name": tactic_name_mapping.get(tactic_id, {}).get("short_name", "")
657 + "short_name": tactic_name_mapping.get(tactic_id, {}).get("short_name", ""),
658 }
659 tactics.append(tactic_info)
660
@@ -660,7 +663,7 @@ async def _build_technique_tactic_mapping() -> Dict[str, List[Dict[str, str]]]:
663 # Store as "T1234"
664 technique_tactic_mapping[technique_external_id] = tactics
665 # Store as "1234" (without T prefix)
663 - if technique_external_id.startswith('T'):
666 + if technique_external_id.startswith("T"):
667 technique_tactic_mapping[technique_external_id[1:]] = tactics
668
669 technique_tactic_mapping[technique_id] = tactics
@@ -676,12 +679,13 @@ async def _build_technique_tactic_mapping() -> Dict[str, List[Dict[str, str]]]:
679 logger.exception(f"Error building technique-tactic mapping: {str(e)}")
680 return {}
681
682 +
683 def _process_mitre_search_results(
684 response: Dict,
685 mitre_field: str,
686 technique_mapping: Dict[str, str],
687 name_field: Optional[str] = None,
684 - technique_tactic_mapping: Optional[Dict[str, List[Dict[str, str]]]] = None
688 + technique_tactic_mapping: Optional[Dict[str, List[Dict[str, str]]]] = None,
689 ) -> Dict:
690 """
691 Process the Wazuh Indexer response to extract MITRE technique information.
@@ -740,11 +744,11 @@ def _process_mitre_search_results(
744 tactics = technique_tactic_mapping[technique_id]
745 logger.debug(f"Found tactics for technique ID: {technique_id} (exact match)")
746 # Try with 'T' prefix if it doesn't have one
743 - elif not technique_id.startswith('T') and f"T{technique_id}" in technique_tactic_mapping:
747 + elif not technique_id.startswith("T") and f"T{technique_id}" in technique_tactic_mapping:
748 tactics = technique_tactic_mapping[f"T{technique_id}"]
749 logger.debug(f"Found tactics for technique ID: {technique_id} (added T prefix)")
750 # Try without 'T' prefix if it has one
747 - elif technique_id.startswith('T') and technique_id[1:] in technique_tactic_mapping:
751 + elif technique_id.startswith("T") and technique_id[1:] in technique_tactic_mapping:
752 tactics = technique_tactic_mapping[technique_id[1:]]
753 logger.debug(f"Found tactics for technique ID: {technique_id} (removed T prefix)")
754 else:
@@ -759,13 +763,15 @@ def _process_mitre_search_results(
763 if technique_name == "Unknown Technique":
764 logger.debug(f"Could not find name for technique {technique_id} in document or mapping")
765
762 - techniques.append({
763 - "technique_id": technique_id,
764 - "technique_name": technique_name,
765 - "count": bucket["doc_count"],
766 - "last_seen": datetime.utcnow().isoformat() + "Z",
767 - "tactics": tactics
768 - })
766 + techniques.append(
767 + {
768 + "technique_id": technique_id,
769 + "technique_name": technique_name,
770 + "count": bucket["doc_count"],
771 + "last_seen": datetime.utcnow().isoformat() + "Z",
772 + "tactics": tactics,
773 + },
774 + )
775
776 # Add debugging information
777 debug_info = {
@@ -773,7 +779,7 @@ def _process_mitre_search_results(
779 "mapping_size": len(technique_mapping),
780 "tactic_mapping_size": len(technique_tactic_mapping) if technique_tactic_mapping else 0,
781 "timestamp": datetime.utcnow().isoformat(),
776 - "sample_technique_ids": [t["technique_id"] for t in techniques[:3]] if techniques else []
782 + "sample_technique_ids": [t["technique_id"] for t in techniques[:3]] if techniques else [],
783 }
784
785 # Compile the final result
@@ -793,10 +799,7 @@ async def _get_wazuh_indexer_client() -> AsyncElasticsearch:
799 return await create_wazuh_indexer_client_async()
800 except Exception as e:
801 logger.error(f"Failed to create OpenSearch client: {str(e)}")
796 - raise HTTPException(
797 - status_code=503,
798 - detail=f"Unable to connect to Wazuh Indexer: {str(e)}"
799 - )
802 + raise HTTPException(status_code=503, detail=f"Unable to connect to Wazuh Indexer: {str(e)}")
803
804
805 def _build_mitre_search_query(
@@ -810,10 +813,7 @@ def _build_mitre_search_query(
813 ) -> Dict:
814 """Build the Wazuh Indexer query for MITRE technique aggregation."""
815 # Build the base filters
813 - query_filters = [
814 - {"match_all": {}},
815 - {"range": {"timestamp": {"from": time_range, "to": "now"}}}
816 - ]
816 + query_filters = [{"match_all": {}}, {"range": {"timestamp": {"from": time_range, "to": "now"}}}]
817
818 # Add filters for mitre field (required)
819 query_filters.append({"exists": {"field": mitre_field}})
@@ -828,42 +828,21 @@ def _build_mitre_search_query(
828 "body": {
829 "size": 0,
830 "from": offset,
831 - "query": {
832 - "bool": {
833 - "must": [],
834 - "filter": query_filters,
835 - "should": [],
836 - "must_not": []
837 - }
838 - },
839 - "aggs": {
840 - "techniques": {
841 - "terms": {
842 - "field": mitre_field,
843 - "size": size,
844 - "order": {"_count": "desc"}
845 - }
846 - }
847 - }
848 - }
831 + "query": {"bool": {"must": [], "filter": query_filters, "should": [], "must_not": []}},
832 + "aggs": {"techniques": {"terms": {"field": mitre_field, "size": size, "order": {"_count": "desc"}}}},
833 + },
834 }
835
836 # If we have a separate name field, add a sub-aggregation to collect technique names
837 if name_field:
838 # Add filter for name field (optional)
839 query["body"]["aggs"]["techniques"]["aggs"] = {
855 - "technique_name": {
856 - "terms": {
857 - "field": name_field,
858 - "size": 1 # Just need the first/most common name
859 - }
860 - }
840 + "technique_name": {"terms": {"field": name_field, "size": 1}}, # Just need the first/most common name
841 }
842
843 return query
844
845
866 -
846 async def get_alerts_by_mitre_id(
847 technique_id: str,
848 time_range: str = "now-24h",
@@ -897,9 +876,9 @@ async def get_alerts_by_mitre_id(
876 technique_name = "Unknown Technique"
877 if technique_id in technique_mapping:
878 technique_name = technique_mapping[technique_id]
900 - elif technique_id.startswith('T') and technique_id[1:] in technique_mapping:
879 + elif technique_id.startswith("T") and technique_id[1:] in technique_mapping:
880 technique_name = technique_mapping[technique_id[1:]]
902 - elif not technique_id.startswith('T') and f"T{technique_id}" in technique_mapping:
881 + elif not technique_id.startswith("T") and f"T{technique_id}" in technique_mapping:
882 technique_name = technique_mapping[f"T{technique_id}"]
883
884 # Get OpenSearch client
@@ -926,7 +905,7 @@ async def get_alerts_by_mitre_id(
905 offset=offset,
906 additional_filters=additional_filters,
907 index_pattern=index_pattern,
929 - mitre_field=field
908 + mitre_field=field,
909 )
910
911 # Execute the search
@@ -949,7 +928,7 @@ async def get_alerts_by_mitre_id(
928 "technique_name": technique_name,
929 "total_alerts": total_hits,
930 "alerts": documents,
952 - "field_used": field
931 + "field_used": field,
932 }
933
934 logger.info(f"Found {len(documents)} of {total_hits} alerts for technique {technique_id} using field '{field}'")
@@ -970,7 +949,7 @@ async def get_alerts_by_mitre_id(
949 "total_alerts": 0,
950 "alerts": [],
951 "field_used": None,
973 - "errors": errors
952 + "errors": errors,
953 }
954
955 return results
@@ -996,17 +975,10 @@ def _build_mitre_alerts_query(
975 ) -> Dict:
976 """Build the OpenSearch query to fetch alerts for a specific MITRE technique."""
977 # Build the base filters
999 - query_filters = [
1000 - {"range": {"timestamp": {"from": time_range, "to": "now"}}}
1001 - ]
978 + query_filters = [{"range": {"timestamp": {"from": time_range, "to": "now"}}}]
979
980 # Add MITRE ID filter with support for array fields
1004 - query_filters.append({
1005 - "query_string": {
1006 - "query": f"{mitre_field}:\"{technique_id}\"",
1007 - "analyze_wildcard": True
1008 - }
1009 - })
981 + query_filters.append({"query_string": {"query": f'{mitre_field}:"{technique_id}"', "analyze_wildcard": True}})
982
983 # Add any additional filters provided
984 if additional_filters:
@@ -1018,17 +990,11 @@ def _build_mitre_alerts_query(
990 "body": {
991 "size": size,
992 "from": offset,
1021 - "query": {
1022 - "bool": {
1023 - "filter": query_filters
1024 - }
1025 - },
993 + "query": {"bool": {"filter": query_filters}},
994 "_source": True,
1027 - "sort": [
1028 - {"timestamp": {"order": "desc"}}
1029 - ],
1030 - "track_total_hits": True
1031 - }
995 + "sort": [{"timestamp": {"order": "desc"}}],
996 + "track_total_hits": True,
997 + },
998 }
999
1000 return query
@@ -1095,6 +1061,7 @@ async def get_mitre_software(
1061 logger.error(f"Error parsing Wazuh MITRE software response: {e}")
1062 raise HTTPException(status_code=500, detail=f"Error processing MITRE data: {str(e)}")
1063
1064 +
1065 async def get_mitre_references(
1066 limit: Optional[int] = None,
1067 offset: Optional[int] = None,
@@ -1138,7 +1105,7 @@ async def get_mitre_references(
1105 success=True,
1106 message=f"Successfully retrieved {len(mitre_references)} MITRE references",
1107 results=mitre_references,
1141 - total=total_items
1108 + total=total_items,
1109 )
1110 else:
1111 logger.error("Unexpected response structure from Wazuh API")
@@ -1151,6 +1118,7 @@ async def get_mitre_references(
1118 logger.error(f"Error parsing Wazuh MITRE references response: {e}")
1119 raise HTTPException(status_code=500, detail=f"Error processing MITRE data: {str(e)}")
1120
1121 +
1122 async def get_mitre_mitigations(
1123 limit: Optional[int] = None,
1124 offset: Optional[int] = None,
@@ -1200,7 +1168,7 @@ async def get_mitre_mitigations(
1168 success=True,
1169 message=f"Successfully retrieved {len(mitre_mitigations)} MITRE mitigations",
1170 results=mitre_mitigations,
1203 - total=total_items
1171 + total=total_items,
1172 )
1173 else:
1174 logger.error("Unexpected response structure from Wazuh API")
@@ -1213,6 +1181,7 @@ async def get_mitre_mitigations(
1181 logger.error(f"Error parsing Wazuh MITRE mitigations response: {e}")
1182 raise HTTPException(status_code=500, detail=f"Error processing MITRE data: {str(e)}")
1183
1184 +
1185 async def get_mitre_groups(
1186 limit: Optional[int] = None,
1187 offset: Optional[int] = None,
@@ -1262,7 +1231,7 @@ async def get_mitre_groups(
1231 success=True,
1232 message=f"Successfully retrieved {len(mitre_groups)} MITRE groups",
1233 results=mitre_groups,
1265 - total=total_items
1234 + total=total_items,
1235 )
1236 else:
1237 logger.error("Unexpected response structure from Wazuh API")
backend/app/db/db_setup.py
+4 -1
@@ -107,6 +107,7 @@ async def create_copilot_user_if_not_exists(db_url: str, db_user_name: str):
107 # logger.error(f"Error applying migrations: {e}")
108 # raise e
109
110 +
111 def apply_migrations():
112 """
113 Applies Alembic migrations to ensure the database schema is up to date.
@@ -127,13 +128,15 @@ def apply_migrations():
128 # Check current revision first
129 logger.info("Checking current database revision...")
130 try:
130 - from alembic.script import ScriptDirectory
131 from sqlalchemy import create_engine
132
133 + from alembic.script import ScriptDirectory
134 +
135 # Get current revision
136 engine = create_engine(SQLALCHEMY_DATABASE_URI.replace("+aiomysql", "+pymysql"))
137 with engine.connect() as connection:
138 from alembic.runtime.migration import MigrationContext
139 +
140 context = MigrationContext.configure(connection)
141 current_rev = context.get_current_revision()
142 logger.info(f"Current database revision: {current_rev}")
backend/app/incidents/services/db_operations.py
+1
@@ -939,6 +939,7 @@ async def create_alert_tag(alert_tag: AlertTagCreate, db: AsyncSession) -> Alert
939 raise HTTPException(status_code=400, detail="Alert tag already exists")
940 return db_alert_tag
941
942 +
943 async def add_alert_tag_if_not_exists(alert_tag: AlertTagCreate, db: AsyncSession) -> AlertTag:
944 # Check if the tag already exists
945 result = await db.execute(select(AlertTag).where(AlertTag.tag == alert_tag.tag))
backend/app/incidents/services/velo_sigma.py
+42 -14
@@ -1,5 +1,5 @@
1 -import re
1 import json
2 +import re
3 from datetime import datetime
4 from datetime import timedelta
5 from typing import Any
@@ -30,7 +30,7 @@ from app.incidents.schema.velo_sigma import SysmonEvent
30 from app.incidents.schema.velo_sigma import VelociraptorSigmaAlert
31 from app.incidents.schema.velo_sigma import VelociraptorSigmaAlertResponse
32 from app.incidents.schema.velo_sigma import VeloSigmaExclusionCreate
33 -from app.incidents.services.db_operations import create_alert_tag, add_alert_tag_if_not_exists
33 +from app.incidents.services.db_operations import add_alert_tag_if_not_exists
34 from app.incidents.services.db_operations import create_comment
35 from app.incidents.services.incident_alert import create_alert
36 from app.incidents.services.incident_alert import create_alert_full
@@ -163,7 +163,12 @@ class VeloSigmaExclusionService:
163 # Remove the regex: prefix and try to match
164 regex_pattern = field_value[6:]
165 # Special handling for path-based regex patterns
166 - if "path" in field_name.lower() or "file" in field_name.lower() or "\\" in regex_pattern or "/" in regex_pattern:
166 + if (
167 + "path" in field_name.lower()
168 + or "file" in field_name.lower()
169 + or "\\" in regex_pattern
170 + or "/" in regex_pattern
171 + ):
172 try:
173 # Normalize paths for comparison by converting all to lowercase and standardizing backslashes
174 pattern = regex_pattern.lower().replace("\\\\", "\\")
@@ -178,7 +183,7 @@ class VeloSigmaExclusionService:
183
184 # Convert the wildcard pattern to proper regex format
185 # Escape special regex characters except for the wildcards we want to keep
181 - pattern_parts = re.split(r'(\.\*)', pattern)
186 + pattern_parts = re.split(r"(\.\*)", pattern)
187 regex_parts = []
188
189 for i, part in enumerate(pattern_parts):
@@ -203,7 +208,7 @@ class VeloSigmaExclusionService:
208 logger.debug(f"Path regex match succeeded! Match: {match_result.group(0)}")
209 return True
210 else:
206 - logger.debug(f"Path regex match failed")
211 + logger.debug("Path regex match failed")
212 return False
213
214 except Exception as e:
@@ -280,7 +285,7 @@ class VeloSigmaExclusionService:
285 placeholders = {}
286
287 # Find all character classes like [^\\] or [\\w] and preserve them
283 - char_class_pattern = r'(\[\^?[^\]]*\])'
288 + char_class_pattern = r"(\[\^?[^\]]*\])"
289 char_classes = re.finditer(char_class_pattern, normalized)
290
291 for i, match in enumerate(char_classes):
@@ -290,7 +295,7 @@ class VeloSigmaExclusionService:
295
296 # Use regex to replace any sequence of one or more backslashes with a single backslash
297 # This handles \, \\, \\\, \\\\, etc.
293 - normalized = re.sub(r'\\+', r'\\', normalized)
298 + normalized = re.sub(r"\\+", r"\\", normalized)
299
300 # Handle escaped special characters in paths
301 normalized = normalized.replace("\\(", "(").replace("\\)", ")")
@@ -539,7 +544,11 @@ class VelociraptorSigmaService:
544 if not isinstance(event_payload, str):
545 # Try to serialize using json
546 try:
542 - event_payload = json.dumps(event_payload, default=lambda o: o.__dict__ if hasattr(o, "__dict__") else str(o), indent=2)
547 + event_payload = json.dumps(
548 + event_payload,
549 + default=lambda o: o.__dict__ if hasattr(o, "__dict__") else str(o),
550 + indent=2,
551 + )
552 except TypeError:
553 # If JSON serialization fails, use string representation
554 event_payload = str(event_payload)
@@ -560,18 +569,33 @@ class VelociraptorSigmaService:
569
570 # Add tags
571 await add_alert_tag_if_not_exists(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag=f"{alert.type}"), db=self.session)
563 - await add_alert_tag_if_not_exists(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="velociraptor-direct"), db=self.session)
572 + await add_alert_tag_if_not_exists(
573 + alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="velociraptor-direct"),
574 + db=self.session,
575 + )
576
577 # Add event-specific tags
578 if "Sysmon" in alert.channel:
567 - await add_alert_tag_if_not_exists(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:sysmon"), db=self.session)
579 + await add_alert_tag_if_not_exists(
580 + alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:sysmon"),
581 + db=self.session,
582 + )
583 elif "Defender" in alert.channel:
569 - await add_alert_tag_if_not_exists(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:defender"), db=self.session)
584 + await add_alert_tag_if_not_exists(
585 + alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:defender"),
586 + db=self.session,
587 + )
588 elif "PowerShell" in alert.channel:
571 - await add_alert_tag_if_not_exists(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:powershell"), db=self.session)
589 + await add_alert_tag_if_not_exists(
590 + alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:powershell"),
591 + db=self.session,
592 + )
593 else:
594 # Generic event type
574 - await add_alert_tag_if_not_exists(alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:generic"), db=self.session)
595 + await add_alert_tag_if_not_exists(
596 + alert_tag=AlertTagCreate(alert_id=result["alert_id"], tag="event_type:generic"),
597 + db=self.session,
598 + )
599
600 logger.info(f"Created fallback CoPilot alert with ID: {result['alert_id']} for customer {customer_code}")
601 result["success"] = True
@@ -944,7 +968,11 @@ class VelociraptorSigmaService:
968 if not isinstance(event_payload, str):
969 # Try to serialize using json
970 try:
947 - event_payload = json.dumps(event_payload, default=lambda o: o.__dict__ if hasattr(o, "__dict__") else str(o), indent=2)
971 + event_payload = json.dumps(
972 + event_payload,
973 + default=lambda o: o.__dict__ if hasattr(o, "__dict__") else str(o),
974 + indent=2,
975 + )
976 except TypeError:
977 # If JSON serialization fails, use string representation
978 event_payload = str(event_payload)
backend/app/integrations/monitoring_alert/routes/provision.py
+17 -1
@@ -25,13 +25,25 @@ from app.integrations.monitoring_alert.schema.provision import (
25 from app.integrations.monitoring_alert.schema.provision import (
26 ProvisionWazuhMonitoringAlertResponse,
27 )
28 -from app.integrations.monitoring_alert.services.provision import provision_custom_alert, provision_crowdstrike_monitoring_alert, provision_fortinet_system_monitoring_alert, provision_fortinet_utm_monitoring_alert, provision_paloalto_monitoring_alert
28 +from app.integrations.monitoring_alert.services.provision import (
29 + provision_crowdstrike_monitoring_alert,
30 +)
31 +from app.integrations.monitoring_alert.services.provision import provision_custom_alert
32 +from app.integrations.monitoring_alert.services.provision import (
33 + provision_fortinet_system_monitoring_alert,
34 +)
35 +from app.integrations.monitoring_alert.services.provision import (
36 + provision_fortinet_utm_monitoring_alert,
37 +)
38 from app.integrations.monitoring_alert.services.provision import (
39 provision_office365_exchange_online_alert,
40 )
41 from app.integrations.monitoring_alert.services.provision import (
42 provision_office365_threat_intel_alert,
43 )
44 +from app.integrations.monitoring_alert.services.provision import (
45 + provision_paloalto_monitoring_alert,
46 +)
47 from app.integrations.monitoring_alert.services.provision import (
48 provision_suricata_monitoring_alert,
49 )
@@ -120,24 +132,28 @@ async def invoke_provision_office365_threat_intel_alert(
132 # Provision the Office365 Threat Intel monitoring alert
133 await provision_office365_threat_intel_alert(request)
134
135 +
136 async def invoke_provision_crowdstrike_monitoring_alert(
137 request: ProvisionMonitoringAlertRequest,
138 ):
139 # Provision the CrowdStrike monitoring alert
140 await provision_crowdstrike_monitoring_alert(request)
141
142 +
143 async def invoke_provision_fortinet_system_monitoring_alert(
144 request: ProvisionMonitoringAlertRequest,
145 ):
146 # Provision the Fortinet System monitoring alert
147 await provision_fortinet_system_monitoring_alert(request)
148
149 +
150 async def invoke_provision_fortinet_utm_monitoring_alert(
151 request: ProvisionMonitoringAlertRequest,
152 ):
153 # Provision the Fortinet UTM monitoring alert
154 await provision_fortinet_utm_monitoring_alert(request)
155
156 +
157 async def invoke_provision_paloalto_monitoring_alert(
158 request: ProvisionMonitoringAlertRequest,
159 ):
backend/app/integrations/monitoring_alert/services/provision.py
+5
@@ -667,6 +667,7 @@ async def provision_office365_threat_intel_alert(
667 message="Office365 Threat Intel monitoring alerts provisioned successfully",
668 )
669
670 +
671 async def provision_crowdstrike_monitoring_alert(
672 request: ProvisionMonitoringAlertRequest,
673 ) -> ProvisionWazuhMonitoringAlertResponse:
@@ -759,6 +760,7 @@ async def provision_crowdstrike_monitoring_alert(
760 message="Crowdstrike monitoring alerts provisioned successfully",
761 )
762
763 +
764 async def provision_fortinet_system_monitoring_alert(
765 request: ProvisionMonitoringAlertRequest,
766 ) -> ProvisionWazuhMonitoringAlertResponse:
@@ -851,6 +853,7 @@ async def provision_fortinet_system_monitoring_alert(
853 message="Fortinet system monitoring alerts provisioned successfully",
854 )
855
856 +
857 async def provision_fortinet_utm_monitoring_alert(
858 request: ProvisionMonitoringAlertRequest,
859 ) -> ProvisionWazuhMonitoringAlertResponse:
@@ -943,6 +946,7 @@ async def provision_fortinet_utm_monitoring_alert(
946 message="Fortinet UTM monitoring alerts provisioned successfully",
947 )
948
949 +
950 async def provision_paloalto_monitoring_alert(
951 request: ProvisionMonitoringAlertRequest,
952 ) -> ProvisionWazuhMonitoringAlertResponse:
@@ -1035,6 +1039,7 @@ async def provision_paloalto_monitoring_alert(
1039 message="Palo Alto monitoring alerts provisioned successfully",
1040 )
1041
1042 +
1043 async def provision_custom_alert(request: CustomMonitoringAlertProvisionModel) -> ProvisionWazuhMonitoringAlertResponse:
1044 """
1045 Provisions custom monitoring alerts.
backend/app/routers/shuffle.py
+1 -1
@@ -1,8 +1,8 @@
1 from fastapi import APIRouter
2
3 from app.connectors.shuffle.routes.integrations import shuffle_integrations_router
4 -from app.connectors.shuffle.routes.workflows import shuffle_workflows_router
4 from app.connectors.shuffle.routes.singul import shuffle_singul_router
5 +from app.connectors.shuffle.routes.workflows import shuffle_workflows_router
6
7 # Instantiate the APIRouter
8 router = APIRouter()
backend/requirements.txt
+1 -1
@@ -145,6 +145,7 @@ rich==13.6.0
145 rsa==4.9
146 ScoutSuite==5.14.0
147 setuptools==65.5.0
148 +shufflepy==0.1.6
149 simplejson==3.19.1
150 six==1.16.0
151 sniffio==1.3.0
@@ -155,7 +156,6 @@ starlette==0.27.0
156 stix==1.2.0.11
157 stix2==3.0.1
158 stix2-elevator==4.1.7
158 -shufflepy==0.1.6
159 stix2-patterns==2.0.0
160 stix2-validator==3.1.4
161 stixmarx==1.0.8
frontend/package.json
+5 -5
@@ -70,7 +70,7 @@
70 "shiki": "^3.4.2",
71 "thememirror": "^2.0.1",
72 "validator": "^13.15.15",
73 - "vue": "^3.5.15",
73 + "vue": "^3.5.16",
74 "vue-advanced-cropper": "^2.8.9",
75 "vue-codemirror": "^6.1.1",
76 "vue-highlight-words": "^3.0.1",
@@ -90,7 +90,7 @@
90 "@antfu/eslint-config": "^4.13.2",
91 "@clack/prompts": "^0.11.0",
92 "@iconify/vue": "^5.0.0",
93 - "@tailwindcss/vite": "^4.1.7",
93 + "@tailwindcss/vite": "^4.1.8",
94 "@tsconfig/node20": "^20.1.5",
95 "@types/bytes": "^3.1.5",
96 "@types/file-saver": "^2.0.7",
@@ -98,7 +98,7 @@
98 "@types/jsdom": "^21.1.7",
99 "@types/lodash": "^4.17.17",
100 "@types/markdown-it": "^14.1.2",
101 - "@types/node": "^22.15.23",
101 + "@types/node": "^22.15.28",
102 "@types/validator": "^13.15.1",
103 "@vitejs/plugin-vue": "^5.2.4",
104 "@vitejs/plugin-vue-jsx": "^4.2.0",
@@ -112,10 +112,10 @@
112 "jsdom": "^26.1.0",
113 "npm-run-all2": "^8.0.4",
114 "prettier": "^3.5.3",
115 - "prettier-plugin-tailwindcss": "^0.6.11",
115 + "prettier-plugin-tailwindcss": "^0.6.12",
116 "sass": "^1.89.0",
117 "start-server-and-test": "^2.0.12",
118 - "tailwindcss": "^4.1.7",
118 + "tailwindcss": "^4.1.8",
119 "taze": "^19.1.0",
120 "type-fest": "^4.41.0",
121 "typescript": "~5.8.3",
frontend/pnpm-lock.yaml
+217 -170
@@ -25,7 +25,7 @@ importers:
25 version: 6.1.2
26 '@f3ve/vue-markdown-it':
27 specifier: ^0.2.3
28 - version: 0.2.3(vue@3.5.15(typescript@5.8.3))
28 + version: 0.2.3(vue@3.5.16(typescript@5.8.3))
29 '@fontsource/jetbrains-mono':
30 specifier: ^5.2.5
31 version: 5.2.5
@@ -40,10 +40,10 @@ importers:
40 version: 3.4.2
41 '@vueuse/core':
42 specifier: ^13.3.0
43 - version: 13.3.0(vue@3.5.15(typescript@5.8.3))
43 + version: 13.3.0(vue@3.5.16(typescript@5.8.3))
44 '@vueuse/motion':
45 specifier: ^3.0.3
46 - version: 3.0.3(vue@3.5.15(typescript@5.8.3))
46 + version: 3.0.3(vue@3.5.16(typescript@5.8.3))
47 axios:
48 specifier: ^1.9.0
49 version: 1.9.0(debug@4.4.1)
@@ -85,7 +85,7 @@ importers:
85 version: 3.0.1
86 naive-ui:
87 specifier: ^2.41.0
88 - version: 2.41.0(vue@3.5.15(typescript@5.8.3))
88 + version: 2.41.0(vue@3.5.16(typescript@5.8.3))
89 nanoid:
90 specifier: ^5.1.5
91 version: 5.1.5
@@ -94,10 +94,10 @@ importers:
94 version: 5.3.0
95 pinia:
96 specifier: ^3.0.2
97 - version: 3.0.2(typescript@5.8.3)(vue@3.5.15(typescript@5.8.3))
97 + version: 3.0.2(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))
98 pinia-plugin-persistedstate:
99 specifier: ^4.3.0
100 - version: 4.3.0(pinia@3.0.2(typescript@5.8.3)(vue@3.5.15(typescript@5.8.3)))
100 + version: 4.3.0(pinia@3.0.2(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3)))
101 secure-ls:
102 specifier: ^2.0.0
103 version: 2.0.0
@@ -111,48 +111,48 @@ importers:
111 specifier: ^13.15.15
112 version: 13.15.15
113 vue:
114 - specifier: ^3.5.15
115 - version: 3.5.15(typescript@5.8.3)
114 + specifier: ^3.5.16
115 + version: 3.5.16(typescript@5.8.3)
116 vue-advanced-cropper:
117 specifier: ^2.8.9
118 - version: 2.8.9(vue@3.5.15(typescript@5.8.3))
118 + version: 2.8.9(vue@3.5.16(typescript@5.8.3))
119 vue-codemirror:
120 specifier: ^6.1.1
121 - version: 6.1.1(codemirror@6.0.1)(vue@3.5.15(typescript@5.8.3))
121 + version: 6.1.1(codemirror@6.0.1)(vue@3.5.16(typescript@5.8.3))
122 vue-highlight-words:
123 specifier: ^3.0.1
124 - version: 3.0.1(vue@3.5.15(typescript@5.8.3))
124 + version: 3.0.1(vue@3.5.16(typescript@5.8.3))
125 vue-i18n:
126 specifier: ^11.1.5
127 - version: 11.1.5(vue@3.5.15(typescript@5.8.3))
127 + version: 11.1.5(vue@3.5.16(typescript@5.8.3))
128 vue-router:
129 specifier: ^4.5.1
130 - version: 4.5.1(vue@3.5.15(typescript@5.8.3))
130 + version: 4.5.1(vue@3.5.16(typescript@5.8.3))
131 vue-sjv:
132 specifier: ^0.0.6
133 - version: 0.0.6(vue@3.5.15(typescript@5.8.3))
133 + version: 0.0.6(vue@3.5.16(typescript@5.8.3))
134 vue3-apexcharts:
135 specifier: ^1.8.0
136 - version: 1.8.0(apexcharts@4.7.0)(vue@3.5.15(typescript@5.8.3))
136 + version: 1.8.0(apexcharts@4.7.0)(vue@3.5.16(typescript@5.8.3))
137 vue3-marquee:
138 specifier: ^4.2.2
139 - version: 4.2.2(vue@3.5.15(typescript@5.8.3))
139 + version: 4.2.2(vue@3.5.16(typescript@5.8.3))
140 vuedraggable:
141 specifier: ^4.1.0
142 - version: 4.1.0(vue@3.5.15(typescript@5.8.3))
142 + version: 4.1.0(vue@3.5.16(typescript@5.8.3))
143 devDependencies:
144 '@antfu/eslint-config':
145 specifier: ^4.13.2
146 - version: 4.13.2(@vue/compiler-sfc@3.5.15)(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.23)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
146 + version: 4.13.2(@vue/compiler-sfc@3.5.16)(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.28)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
147 '@clack/prompts':
148 specifier: ^0.11.0
149 version: 0.11.0
150 '@iconify/vue':
151 specifier: ^5.0.0
152 - version: 5.0.0(vue@3.5.15(typescript@5.8.3))
152 + version: 5.0.0(vue@3.5.16(typescript@5.8.3))
153 '@tailwindcss/vite':
154 - specifier: ^4.1.7
155 - version: 4.1.8(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
154 + specifier: ^4.1.8
155 + version: 4.1.8(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
156 '@tsconfig/node20':
157 specifier: ^20.1.5
158 version: 20.1.5
@@ -175,23 +175,23 @@ importers:
175 specifier: ^14.1.2
176 version: 14.1.2
177 '@types/node':
178 - specifier: ^22.15.23
179 - version: 22.15.23
178 + specifier: ^22.15.28
179 + version: 22.15.28
180 '@types/validator':
181 specifier: ^13.15.1
182 version: 13.15.1
183 '@vitejs/plugin-vue':
184 specifier: ^5.2.4
185 - version: 5.2.4(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.15(typescript@5.8.3))
185 + version: 5.2.4(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3))
186 '@vitejs/plugin-vue-jsx':
187 specifier: ^4.2.0
188 - version: 4.2.0(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.15(typescript@5.8.3))
188 + version: 4.2.0(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3))
189 '@vue/test-utils':
190 specifier: ^2.4.6
191 version: 2.4.6
192 '@vue/tsconfig':
193 specifier: ^0.7.0
194 - version: 0.7.0(typescript@5.8.3)(vue@3.5.15(typescript@5.8.3))
194 + version: 0.7.0(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))
195 cypress:
196 specifier: ^14.4.0
197 version: 14.4.0
@@ -217,8 +217,8 @@ importers:
217 specifier: ^3.5.3
218 version: 3.5.3
219 prettier-plugin-tailwindcss:
220 - specifier: ^0.6.11
221 - version: 0.6.11(prettier@3.5.3)
220 + specifier: ^0.6.12
221 + version: 0.6.12(prettier@3.5.3)
222 sass:
223 specifier: ^1.89.0
224 version: 1.89.0
@@ -226,7 +226,7 @@ importers:
226 specifier: ^2.0.12
227 version: 2.0.12
228 tailwindcss:
229 - specifier: ^4.1.7
229 + specifier: ^4.1.8
230 version: 4.1.8
231 taze:
232 specifier: ^19.1.0
@@ -239,19 +239,19 @@ importers:
239 version: 5.8.3
240 vite:
241 specifier: ^6.3.5
242 - version: 6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
242 + version: 6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
243 vite-bundle-visualizer:
244 specifier: ^1.2.1
245 version: 1.2.1(rollup@4.41.1)
246 vite-plugin-vue-devtools:
247 specifier: ^7.7.6
248 - version: 7.7.6(rollup@4.41.1)(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.15(typescript@5.8.3))
248 + version: 7.7.6(rollup@4.41.1)(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3))
249 vite-svg-loader:
250 specifier: ^5.1.0
251 - version: 5.1.0(vue@3.5.15(typescript@5.8.3))
251 + version: 5.1.0(vue@3.5.16(typescript@5.8.3))
252 vitest:
253 specifier: ^3.1.4
254 - version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.23)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
254 + version: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.28)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
255 vue-tsc:
256 specifier: ^2.2.10
257 version: 2.2.10(typescript@5.8.3)
@@ -264,7 +264,7 @@ importers:
264 version: 0.3.11
265 vueuc:
266 specifier: ^0.4.64
267 - version: 0.4.64(vue@3.5.15(typescript@5.8.3))
267 + version: 0.4.64(vue@3.5.16(typescript@5.8.3))
268
269 packages:
270
@@ -1363,8 +1363,8 @@ packages:
1363 '@types/ms@2.1.0':
1364 resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
1365
1366 - '@types/node@22.15.23':
1367 - resolution: {integrity: sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw==}
1366 + '@types/node@22.15.28':
1367 + resolution: {integrity: sha512-I0okKVDmyKR281I0UIFV7EWAWRnR0gkuSKob5wVcByyyhr7Px/slhkQapcYX4u00ekzNWaS1gznKZnuzxwo4pw==}
1368
1369 '@types/parse-json@4.0.2':
1370 resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
@@ -1618,15 +1618,27 @@ packages:
1618 '@vue/compiler-core@3.5.15':
1619 resolution: {integrity: sha512-nGRc6YJg/kxNqbv/7Tg4juirPnjHvuVdhcmDvQWVZXlLHjouq7VsKmV1hIxM/8yKM0VUfwT/Uzc0lO510ltZqw==}
1620
1621 + '@vue/compiler-core@3.5.16':
1622 + resolution: {integrity: sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==}
1623 +
1624 '@vue/compiler-dom@3.5.15':
1625 resolution: {integrity: sha512-ZelQd9n+O/UCBdL00rlwCrsArSak+YLZpBVuNDio1hN3+wrCshYZEDUO3khSLAzPbF1oQS2duEoMDUHScUlYjA==}
1626
1627 + '@vue/compiler-dom@3.5.16':
1628 + resolution: {integrity: sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==}
1629 +
1630 '@vue/compiler-sfc@3.5.15':
1631 resolution: {integrity: sha512-3zndKbxMsOU6afQWer75Zot/aydjtxNj0T2KLg033rAFaQUn2PGuE32ZRe4iMhflbTcAxL0yEYsRWFxtPro8RQ==}
1632
1633 + '@vue/compiler-sfc@3.5.16':
1634 + resolution: {integrity: sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==}
1635 +
1636 '@vue/compiler-ssr@3.5.15':
1637 resolution: {integrity: sha512-gShn8zRREZbrXqTtmLSCffgZXDWv8nHc/GhsW+mbwBfNZL5pI96e7IWcIq8XGQe1TLtVbu7EV9gFIVSmfyarPg==}
1638
1639 + '@vue/compiler-ssr@3.5.16':
1640 + resolution: {integrity: sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==}
1641 +
1642 '@vue/compiler-vue2@2.7.16':
1643 resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
1644
@@ -1655,23 +1667,26 @@ packages:
1667 typescript:
1668 optional: true
1669
1658 - '@vue/reactivity@3.5.15':
1659 - resolution: {integrity: sha512-GaA5VUm30YWobCwpvcs9nvFKf27EdSLKDo2jA0IXzGS344oNpFNbEQ9z+Pp5ESDaxyS8FcH0vFN/XSe95BZtHQ==}
1670 + '@vue/reactivity@3.5.16':
1671 + resolution: {integrity: sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==}
1672
1661 - '@vue/runtime-core@3.5.15':
1662 - resolution: {integrity: sha512-CZAlIOQ93nj0OPpWWOx4+QDLCMzBNY85IQR4Voe6vIID149yF8g9WQaWnw042f/6JfvLttK7dnyWlC1EVCRK8Q==}
1673 + '@vue/runtime-core@3.5.16':
1674 + resolution: {integrity: sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ==}
1675
1664 - '@vue/runtime-dom@3.5.15':
1665 - resolution: {integrity: sha512-wFplHKzKO/v998up2iCW3RN9TNUeDMhdBcNYZgs5LOokHntrB48dyuZHspcahKZczKKh3v6i164gapMPxBTKNw==}
1676 + '@vue/runtime-dom@3.5.16':
1677 + resolution: {integrity: sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww==}
1678
1667 - '@vue/server-renderer@3.5.15':
1668 - resolution: {integrity: sha512-Gehc693kVTYkLt6QSYEjGvqvdK2zZ/gf/D5zkgmvBdeB30dNnVZS8yY7+IlBmHRd1rR/zwaqeu06Ij04ZxBscg==}
1679 + '@vue/server-renderer@3.5.16':
1680 + resolution: {integrity: sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg==}
1681 peerDependencies:
1670 - vue: 3.5.15
1682 + vue: 3.5.16
1683
1684 '@vue/shared@3.5.15':
1685 resolution: {integrity: sha512-bKvgFJJL1ZX9KxMCTQY6xD9Dhe3nusd1OhyOb1cJYGqvAr0Vg8FIjHPMOEVbJ9GDT9HG+Bjdn4oS8ohKP8EvoA==}
1686
1687 + '@vue/shared@3.5.16':
1688 + resolution: {integrity: sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==}
1689 +
1690 '@vue/test-utils@2.4.6':
1691 resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==}
1692
@@ -3824,8 +3839,8 @@ packages:
3839 resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
3840 engines: {node: '>= 0.8.0'}
3841
3827 - prettier-plugin-tailwindcss@0.6.11:
3828 - resolution: {integrity: sha512-YxaYSIvZPAqhrrEpRtonnrXdghZg1irNg4qrjboCXrpybLWVs55cW2N3juhspVJiO0JBvYJT8SYsJpc8OQSnsA==}
3842 + prettier-plugin-tailwindcss@0.6.12:
3843 + resolution: {integrity: sha512-OuTQKoqNwV7RnxTPwXWzOFXy6Jc4z8oeRZYGuMpRyG3WbuR3jjXdQFK8qFBMBx8UHWdHrddARz2fgUenild6aw==}
3844 engines: {node: '>=14.21.3'}
3845 peerDependencies:
3846 '@ianvs/prettier-plugin-sort-imports': '*'
@@ -4679,8 +4694,8 @@ packages:
4694 peerDependencies:
4695 vue: ^3.2
4696
4682 - vue@3.5.15:
4683 - resolution: {integrity: sha512-aD9zK4rB43JAMK/5BmS4LdPiEp8Fdh8P1Ve/XNuMF5YRf78fCyPE6FUbQwcaWQ5oZ1R2CD9NKE0FFOVpMR7gEQ==}
4697 + vue@3.5.16:
4698 + resolution: {integrity: sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==}
4699 peerDependencies:
4700 typescript: '*'
4701 peerDependenciesMeta:
@@ -4857,7 +4872,7 @@ snapshots:
4872 '@jridgewell/gen-mapping': 0.3.8
4873 '@jridgewell/trace-mapping': 0.3.25
4874
4860 - '@antfu/eslint-config@4.13.2(@vue/compiler-sfc@3.5.15)(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.23)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))':
4875 + '@antfu/eslint-config@4.13.2(@vue/compiler-sfc@3.5.16)(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.28)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))':
4876 dependencies:
4877 '@antfu/install-pkg': 1.1.0
4878 '@clack/prompts': 0.10.1
@@ -4866,7 +4881,7 @@ snapshots:
4881 '@stylistic/eslint-plugin': 4.4.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)
4882 '@typescript-eslint/eslint-plugin': 8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)
4883 '@typescript-eslint/parser': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)
4869 - '@vitest/eslint-plugin': 1.2.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.23)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
4884 + '@vitest/eslint-plugin': 1.2.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.28)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
4885 ansis: 4.0.0
4886 cac: 6.7.14
4887 eslint: 9.27.0(jiti@2.4.2)
@@ -4888,7 +4903,7 @@ snapshots:
4903 eslint-plugin-unused-imports: 4.1.4(@typescript-eslint/eslint-plugin@8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))
4904 eslint-plugin-vue: 10.1.0(eslint@9.27.0(jiti@2.4.2))(vue-eslint-parser@10.1.3(eslint@9.27.0(jiti@2.4.2)))
4905 eslint-plugin-yml: 1.18.0(eslint@9.27.0(jiti@2.4.2))
4891 - eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.15)(eslint@9.27.0(jiti@2.4.2))
4906 + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.16)(eslint@9.27.0(jiti@2.4.2))
4907 globals: 16.2.0
4908 jsonc-eslint-parser: 2.4.0
4909 local-pkg: 1.1.1
@@ -5211,9 +5226,9 @@ snapshots:
5226 dependencies:
5227 css-render: 0.15.14
5228
5214 - '@css-render/vue3-ssr@0.15.14(vue@3.5.15(typescript@5.8.3))':
5229 + '@css-render/vue3-ssr@0.15.14(vue@3.5.16(typescript@5.8.3))':
5230 dependencies:
5216 - vue: 3.5.15(typescript@5.8.3)
5231 + vue: 3.5.16(typescript@5.8.3)
5232
5233 '@csstools/color-helpers@5.0.2': {}
5234
@@ -5443,10 +5458,10 @@ snapshots:
5458 '@eslint/core': 0.14.0
5459 levn: 0.4.1
5460
5446 - '@f3ve/vue-markdown-it@0.2.3(vue@3.5.15(typescript@5.8.3))':
5461 + '@f3ve/vue-markdown-it@0.2.3(vue@3.5.16(typescript@5.8.3))':
5462 dependencies:
5463 markdown-it: 14.1.0
5449 - vue: 3.5.15(typescript@5.8.3)
5464 + vue: 3.5.16(typescript@5.8.3)
5465
5466 '@fontsource/jetbrains-mono@5.2.5': {}
5467
@@ -5475,10 +5490,10 @@ snapshots:
5490
5491 '@iconify/types@2.0.0': {}
5492
5478 - '@iconify/vue@5.0.0(vue@3.5.15(typescript@5.8.3))':
5493 + '@iconify/vue@5.0.0(vue@3.5.16(typescript@5.8.3))':
5494 dependencies:
5495 '@iconify/types': 2.0.0
5481 - vue: 3.5.15(typescript@5.8.3)
5496 + vue: 3.5.16(typescript@5.8.3)
5497
5498 '@intlify/core-base@11.1.5':
5499 dependencies:
@@ -5883,12 +5898,12 @@ snapshots:
5898 '@tailwindcss/oxide-win32-arm64-msvc': 4.1.8
5899 '@tailwindcss/oxide-win32-x64-msvc': 4.1.8
5900
5886 - '@tailwindcss/vite@4.1.8(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))':
5901 + '@tailwindcss/vite@4.1.8(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))':
5902 dependencies:
5903 '@tailwindcss/node': 4.1.8
5904 '@tailwindcss/oxide': 4.1.8
5905 tailwindcss: 4.1.8
5891 - vite: 6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
5906 + vite: 6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
5907
5908 '@trysound/sax@0.2.0': {}
5909
@@ -5912,7 +5927,7 @@ snapshots:
5927 '@types/fs-extra@11.0.4':
5928 dependencies:
5929 '@types/jsonfile': 6.1.4
5915 - '@types/node': 22.15.23
5930 + '@types/node': 22.15.28
5931
5932 '@types/hast@3.0.4':
5933 dependencies:
@@ -5920,7 +5935,7 @@ snapshots:
5935
5936 '@types/jsdom@21.1.7':
5937 dependencies:
5923 - '@types/node': 22.15.23
5938 + '@types/node': 22.15.28
5939 '@types/tough-cookie': 4.0.5
5940 parse5: 7.3.0
5941
@@ -5928,7 +5943,7 @@ snapshots:
5943
5944 '@types/jsonfile@6.1.4':
5945 dependencies:
5931 - '@types/node': 22.15.23
5946 + '@types/node': 22.15.28
5947
5948 '@types/katex@0.16.7': {}
5949
@@ -5955,7 +5970,7 @@ snapshots:
5970
5971 '@types/ms@2.1.0': {}
5972
5958 - '@types/node@22.15.23':
5973 + '@types/node@22.15.28':
5974 dependencies:
5975 undici-types: 6.21.0
5976
@@ -5975,7 +5990,7 @@ snapshots:
5990
5991 '@types/yauzl@2.10.3':
5992 dependencies:
5978 - '@types/node': 22.15.23
5993 + '@types/node': 22.15.28
5994 optional: true
5995
5996 '@typescript-eslint/eslint-plugin@8.33.0(@typescript-eslint/parser@8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)':
@@ -6125,29 +6140,29 @@ snapshots:
6140 '@unrs/resolver-binding-win32-x64-msvc@1.7.5':
6141 optional: true
6142
6128 - '@vitejs/plugin-vue-jsx@4.2.0(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.15(typescript@5.8.3))':
6143 + '@vitejs/plugin-vue-jsx@4.2.0(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3))':
6144 dependencies:
6145 '@babel/core': 7.27.3
6146 '@babel/plugin-transform-typescript': 7.27.1(@babel/core@7.27.3)
6147 '@rolldown/pluginutils': 1.0.0-beta.10
6148 '@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.27.3)
6134 - vite: 6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
6135 - vue: 3.5.15(typescript@5.8.3)
6149 + vite: 6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
6150 + vue: 3.5.16(typescript@5.8.3)
6151 transitivePeerDependencies:
6152 - supports-color
6153
6139 - '@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.15(typescript@5.8.3))':
6154 + '@vitejs/plugin-vue@5.2.4(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3))':
6155 dependencies:
6141 - vite: 6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
6142 - vue: 3.5.15(typescript@5.8.3)
6156 + vite: 6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
6157 + vue: 3.5.16(typescript@5.8.3)
6158
6144 - '@vitest/eslint-plugin@1.2.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.23)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))':
6159 + '@vitest/eslint-plugin@1.2.1(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.28)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))':
6160 dependencies:
6161 '@typescript-eslint/utils': 8.33.0(eslint@9.27.0(jiti@2.4.2))(typescript@5.8.3)
6162 eslint: 9.27.0(jiti@2.4.2)
6163 optionalDependencies:
6164 typescript: 5.8.3
6150 - vitest: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.23)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
6165 + vitest: 3.1.4(@types/debug@4.1.12)(@types/node@22.15.28)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
6166 transitivePeerDependencies:
6167 - supports-color
6168
@@ -6158,13 +6173,13 @@ snapshots:
6173 chai: 5.2.0
6174 tinyrainbow: 2.0.0
6175
6161 - '@vitest/mocker@3.1.4(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))':
6176 + '@vitest/mocker@3.1.4(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))':
6177 dependencies:
6178 '@vitest/spy': 3.1.4
6179 estree-walker: 3.0.3
6180 magic-string: 0.30.17
6181 optionalDependencies:
6167 - vite: 6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
6182 + vite: 6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
6183
6184 '@vitest/pretty-format@3.1.4':
6185 dependencies:
@@ -6240,11 +6255,24 @@ snapshots:
6255 estree-walker: 2.0.2
6256 source-map-js: 1.2.1
6257
6258 + '@vue/compiler-core@3.5.16':
6259 + dependencies:
6260 + '@babel/parser': 7.27.3
6261 + '@vue/shared': 3.5.16
6262 + entities: 4.5.0
6263 + estree-walker: 2.0.2
6264 + source-map-js: 1.2.1
6265 +
6266 '@vue/compiler-dom@3.5.15':
6267 dependencies:
6268 '@vue/compiler-core': 3.5.15
6269 '@vue/shared': 3.5.15
6270
6271 + '@vue/compiler-dom@3.5.16':
6272 + dependencies:
6273 + '@vue/compiler-core': 3.5.16
6274 + '@vue/shared': 3.5.16
6275 +
6276 '@vue/compiler-sfc@3.5.15':
6277 dependencies:
6278 '@babel/parser': 7.27.3
@@ -6257,11 +6285,28 @@ snapshots:
6285 postcss: 8.5.3
6286 source-map-js: 1.2.1
6287
6288 + '@vue/compiler-sfc@3.5.16':
6289 + dependencies:
6290 + '@babel/parser': 7.27.3
6291 + '@vue/compiler-core': 3.5.16
6292 + '@vue/compiler-dom': 3.5.16
6293 + '@vue/compiler-ssr': 3.5.16
6294 + '@vue/shared': 3.5.16
6295 + estree-walker: 2.0.2
6296 + magic-string: 0.30.17
6297 + postcss: 8.5.3
6298 + source-map-js: 1.2.1
6299 +
6300 '@vue/compiler-ssr@3.5.15':
6301 dependencies:
6302 '@vue/compiler-dom': 3.5.15
6303 '@vue/shared': 3.5.15
6304
6305 + '@vue/compiler-ssr@3.5.16':
6306 + dependencies:
6307 + '@vue/compiler-dom': 3.5.16
6308 + '@vue/shared': 3.5.16
6309 +
6310 '@vue/compiler-vue2@2.7.16':
6311 dependencies:
6312 de-indent: 1.0.2
@@ -6273,15 +6318,15 @@ snapshots:
6318 dependencies:
6319 '@vue/devtools-kit': 7.7.6
6320
6276 - '@vue/devtools-core@7.7.6(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.15(typescript@5.8.3))':
6321 + '@vue/devtools-core@7.7.6(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3))':
6322 dependencies:
6323 '@vue/devtools-kit': 7.7.6
6324 '@vue/devtools-shared': 7.7.6
6325 mitt: 3.0.1
6326 nanoid: 5.1.5
6327 pathe: 2.0.3
6283 - vite-hot-client: 2.0.4(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
6284 - vue: 3.5.15(typescript@5.8.3)
6328 + vite-hot-client: 2.0.4(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
6329 + vue: 3.5.16(typescript@5.8.3)
6330 transitivePeerDependencies:
6331 - vite
6332
@@ -6312,66 +6357,68 @@ snapshots:
6357 optionalDependencies:
6358 typescript: 5.8.3
6359
6315 - '@vue/reactivity@3.5.15':
6360 + '@vue/reactivity@3.5.16':
6361 dependencies:
6317 - '@vue/shared': 3.5.15
6362 + '@vue/shared': 3.5.16
6363
6319 - '@vue/runtime-core@3.5.15':
6364 + '@vue/runtime-core@3.5.16':
6365 dependencies:
6321 - '@vue/reactivity': 3.5.15
6322 - '@vue/shared': 3.5.15
6366 + '@vue/reactivity': 3.5.16
6367 + '@vue/shared': 3.5.16
6368
6324 - '@vue/runtime-dom@3.5.15':
6369 + '@vue/runtime-dom@3.5.16':
6370 dependencies:
6326 - '@vue/reactivity': 3.5.15
6327 - '@vue/runtime-core': 3.5.15
6328 - '@vue/shared': 3.5.15
6371 + '@vue/reactivity': 3.5.16
6372 + '@vue/runtime-core': 3.5.16
6373 + '@vue/shared': 3.5.16
6374 csstype: 3.1.3
6375
6331 - '@vue/server-renderer@3.5.15(vue@3.5.15(typescript@5.8.3))':
6376 + '@vue/server-renderer@3.5.16(vue@3.5.16(typescript@5.8.3))':
6377 dependencies:
6333 - '@vue/compiler-ssr': 3.5.15
6334 - '@vue/shared': 3.5.15
6335 - vue: 3.5.15(typescript@5.8.3)
6378 + '@vue/compiler-ssr': 3.5.16
6379 + '@vue/shared': 3.5.16
6380 + vue: 3.5.16(typescript@5.8.3)
6381
6382 '@vue/shared@3.5.15': {}
6383
6384 + '@vue/shared@3.5.16': {}
6385 +
6386 '@vue/test-utils@2.4.6':
6387 dependencies:
6388 js-beautify: 1.15.4
6389 vue-component-type-helpers: 2.2.10
6390
6344 - '@vue/tsconfig@0.7.0(typescript@5.8.3)(vue@3.5.15(typescript@5.8.3))':
6391 + '@vue/tsconfig@0.7.0(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))':
6392 optionalDependencies:
6393 typescript: 5.8.3
6347 - vue: 3.5.15(typescript@5.8.3)
6394 + vue: 3.5.16(typescript@5.8.3)
6395
6349 - '@vueuse/core@13.3.0(vue@3.5.15(typescript@5.8.3))':
6396 + '@vueuse/core@13.3.0(vue@3.5.16(typescript@5.8.3))':
6397 dependencies:
6398 '@types/web-bluetooth': 0.0.21
6399 '@vueuse/metadata': 13.3.0
6353 - '@vueuse/shared': 13.3.0(vue@3.5.15(typescript@5.8.3))
6354 - vue: 3.5.15(typescript@5.8.3)
6400 + '@vueuse/shared': 13.3.0(vue@3.5.16(typescript@5.8.3))
6401 + vue: 3.5.16(typescript@5.8.3)
6402
6403 '@vueuse/metadata@13.3.0': {}
6404
6358 - '@vueuse/motion@3.0.3(vue@3.5.15(typescript@5.8.3))':
6405 + '@vueuse/motion@3.0.3(vue@3.5.16(typescript@5.8.3))':
6406 dependencies:
6360 - '@vueuse/core': 13.3.0(vue@3.5.15(typescript@5.8.3))
6361 - '@vueuse/shared': 13.3.0(vue@3.5.15(typescript@5.8.3))
6407 + '@vueuse/core': 13.3.0(vue@3.5.16(typescript@5.8.3))
6408 + '@vueuse/shared': 13.3.0(vue@3.5.16(typescript@5.8.3))
6409 defu: 6.1.4
6410 framesync: 6.1.2
6411 popmotion: 11.0.5
6412 style-value-types: 5.1.2
6366 - vue: 3.5.15(typescript@5.8.3)
6413 + vue: 3.5.16(typescript@5.8.3)
6414 optionalDependencies:
6415 '@nuxt/kit': 3.17.4
6416 transitivePeerDependencies:
6417 - magicast
6418
6372 - '@vueuse/shared@13.3.0(vue@3.5.15(typescript@5.8.3))':
6419 + '@vueuse/shared@13.3.0(vue@3.5.16(typescript@5.8.3))':
6420 dependencies:
6374 - vue: 3.5.15(typescript@5.8.3)
6421 + vue: 3.5.16(typescript@5.8.3)
6422
6423 '@yr/monotone-cubic-spline@1.0.3': {}
6424
@@ -7257,9 +7304,9 @@ snapshots:
7304 transitivePeerDependencies:
7305 - supports-color
7306
7260 - eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.15)(eslint@9.27.0(jiti@2.4.2)):
7307 + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.16)(eslint@9.27.0(jiti@2.4.2)):
7308 dependencies:
7262 - '@vue/compiler-sfc': 3.5.15
7309 + '@vue/compiler-sfc': 3.5.16
7310 eslint: 9.27.0(jiti@2.4.2)
7311
7312 eslint-scope@8.3.0:
@@ -8486,10 +8533,10 @@ snapshots:
8533 arrify: 2.0.1
8534 minimatch: 3.1.2
8535
8489 - naive-ui@2.41.0(vue@3.5.15(typescript@5.8.3)):
8536 + naive-ui@2.41.0(vue@3.5.16(typescript@5.8.3)):
8537 dependencies:
8538 '@css-render/plugin-bem': 0.15.14(css-render@0.15.14)
8492 - '@css-render/vue3-ssr': 0.15.14(vue@3.5.15(typescript@5.8.3))
8539 + '@css-render/vue3-ssr': 0.15.14(vue@3.5.16(typescript@5.8.3))
8540 '@types/katex': 0.16.7
8541 '@types/lodash': 4.17.17
8542 '@types/lodash-es': 4.17.12
@@ -8504,10 +8551,10 @@ snapshots:
8551 lodash-es: 4.17.21
8552 seemly: 0.3.10
8553 treemate: 0.3.11
8507 - vdirs: 0.1.8(vue@3.5.15(typescript@5.8.3))
8508 - vooks: 0.2.12(vue@3.5.15(typescript@5.8.3))
8509 - vue: 3.5.15(typescript@5.8.3)
8510 - vueuc: 0.4.64(vue@3.5.15(typescript@5.8.3))
8554 + vdirs: 0.1.8(vue@3.5.16(typescript@5.8.3))
8555 + vooks: 0.2.12(vue@3.5.16(typescript@5.8.3))
8556 + vue: 3.5.16(typescript@5.8.3)
8557 + vueuc: 0.4.64(vue@3.5.16(typescript@5.8.3))
8558
8559 nanoid@3.3.11: {}
8560
@@ -8706,21 +8753,21 @@ snapshots:
8753
8754 pify@2.3.0: {}
8755
8709 - pinia-plugin-persistedstate@4.3.0(pinia@3.0.2(typescript@5.8.3)(vue@3.5.15(typescript@5.8.3))):
8756 + pinia-plugin-persistedstate@4.3.0(pinia@3.0.2(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))):
8757 dependencies:
8758 '@nuxt/kit': 3.17.4
8759 deep-pick-omit: 1.2.1
8760 defu: 6.1.4
8761 destr: 2.0.5
8762 optionalDependencies:
8716 - pinia: 3.0.2(typescript@5.8.3)(vue@3.5.15(typescript@5.8.3))
8763 + pinia: 3.0.2(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3))
8764 transitivePeerDependencies:
8765 - magicast
8766
8720 - pinia@3.0.2(typescript@5.8.3)(vue@3.5.15(typescript@5.8.3)):
8767 + pinia@3.0.2(typescript@5.8.3)(vue@3.5.16(typescript@5.8.3)):
8768 dependencies:
8769 '@vue/devtools-api': 7.7.6
8723 - vue: 3.5.15(typescript@5.8.3)
8770 + vue: 3.5.16(typescript@5.8.3)
8771 optionalDependencies:
8772 typescript: 5.8.3
8773
@@ -8766,7 +8813,7 @@ snapshots:
8813
8814 prelude-ls@1.2.1: {}
8815
8769 - prettier-plugin-tailwindcss@0.6.11(prettier@3.5.3):
8816 + prettier-plugin-tailwindcss@0.6.12(prettier@3.5.3):
8817 dependencies:
8818 prettier: 3.5.3
8819
@@ -9418,10 +9465,10 @@ snapshots:
9465
9466 validator@13.15.15: {}
9467
9421 - vdirs@0.1.8(vue@3.5.15(typescript@5.8.3)):
9468 + vdirs@0.1.8(vue@3.5.16(typescript@5.8.3)):
9469 dependencies:
9470 evtd: 0.2.4
9424 - vue: 3.5.15(typescript@5.8.3)
9471 + vue: 3.5.16(typescript@5.8.3)
9472
9473 verror@1.10.0:
9474 dependencies:
@@ -9450,17 +9497,17 @@ snapshots:
9497 - rollup
9498 - supports-color
9499
9453 - vite-hot-client@2.0.4(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)):
9500 + vite-hot-client@2.0.4(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)):
9501 dependencies:
9455 - vite: 6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9502 + vite: 6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9503
9457 - vite-node@3.1.4(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0):
9504 + vite-node@3.1.4(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0):
9505 dependencies:
9506 cac: 6.7.14
9507 debug: 4.4.1(supports-color@8.1.1)
9508 es-module-lexer: 1.7.0
9509 pathe: 2.0.3
9463 - vite: 6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9510 + vite: 6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9511 transitivePeerDependencies:
9512 - '@types/node'
9513 - jiti
@@ -9475,7 +9522,7 @@ snapshots:
9522 - tsx
9523 - yaml
9524
9478 - vite-plugin-inspect@0.8.9(rollup@4.41.1)(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)):
9525 + vite-plugin-inspect@0.8.9(rollup@4.41.1)(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)):
9526 dependencies:
9527 '@antfu/utils': 0.7.10
9528 '@rollup/pluginutils': 5.1.4(rollup@4.41.1)
@@ -9486,28 +9533,28 @@ snapshots:
9533 perfect-debounce: 1.0.0
9534 picocolors: 1.1.1
9535 sirv: 3.0.1
9489 - vite: 6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9536 + vite: 6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9537 transitivePeerDependencies:
9538 - rollup
9539 - supports-color
9540
9494 - vite-plugin-vue-devtools@7.7.6(rollup@4.41.1)(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.15(typescript@5.8.3)):
9541 + vite-plugin-vue-devtools@7.7.6(rollup@4.41.1)(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3)):
9542 dependencies:
9496 - '@vue/devtools-core': 7.7.6(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.15(typescript@5.8.3))
9543 + '@vue/devtools-core': 7.7.6(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))(vue@3.5.16(typescript@5.8.3))
9544 '@vue/devtools-kit': 7.7.6
9545 '@vue/devtools-shared': 7.7.6
9546 execa: 9.6.0
9547 sirv: 3.0.1
9501 - vite: 6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9502 - vite-plugin-inspect: 0.8.9(rollup@4.41.1)(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
9503 - vite-plugin-vue-inspector: 5.3.1(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
9548 + vite: 6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9549 + vite-plugin-inspect: 0.8.9(rollup@4.41.1)(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
9550 + vite-plugin-vue-inspector: 5.3.1(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
9551 transitivePeerDependencies:
9552 - '@nuxt/kit'
9553 - rollup
9554 - supports-color
9555 - vue
9556
9510 - vite-plugin-vue-inspector@5.3.1(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)):
9557 + vite-plugin-vue-inspector@5.3.1(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)):
9558 dependencies:
9559 '@babel/core': 7.27.3
9560 '@babel/plugin-proposal-decorators': 7.27.1(@babel/core@7.27.3)
@@ -9518,16 +9565,16 @@ snapshots:
9565 '@vue/compiler-dom': 3.5.15
9566 kolorist: 1.8.0
9567 magic-string: 0.30.17
9521 - vite: 6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9568 + vite: 6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9569 transitivePeerDependencies:
9570 - supports-color
9571
9525 - vite-svg-loader@5.1.0(vue@3.5.15(typescript@5.8.3)):
9572 + vite-svg-loader@5.1.0(vue@3.5.16(typescript@5.8.3)):
9573 dependencies:
9574 svgo: 3.3.2
9528 - vue: 3.5.15(typescript@5.8.3)
9575 + vue: 3.5.16(typescript@5.8.3)
9576
9530 - vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0):
9577 + vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0):
9578 dependencies:
9579 esbuild: 0.25.5
9580 fdir: 6.4.5(picomatch@4.0.2)
@@ -9536,17 +9583,17 @@ snapshots:
9583 rollup: 4.41.1
9584 tinyglobby: 0.2.14
9585 optionalDependencies:
9539 - '@types/node': 22.15.23
9586 + '@types/node': 22.15.28
9587 fsevents: 2.3.3
9588 jiti: 2.4.2
9589 lightningcss: 1.30.1
9590 sass: 1.89.0
9591 yaml: 2.8.0
9592
9546 - vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.23)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0):
9593 + vitest@3.1.4(@types/debug@4.1.12)(@types/node@22.15.28)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0):
9594 dependencies:
9595 '@vitest/expect': 3.1.4
9549 - '@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
9596 + '@vitest/mocker': 3.1.4(vite@6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0))
9597 '@vitest/pretty-format': 3.1.4
9598 '@vitest/runner': 3.1.4
9599 '@vitest/snapshot': 3.1.4
@@ -9563,12 +9610,12 @@ snapshots:
9610 tinyglobby: 0.2.14
9611 tinypool: 1.0.2
9612 tinyrainbow: 2.0.0
9566 - vite: 6.3.5(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9567 - vite-node: 3.1.4(@types/node@22.15.23)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9613 + vite: 6.3.5(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9614 + vite-node: 3.1.4(@types/node@22.15.28)(jiti@2.4.2)(lightningcss@1.30.1)(sass@1.89.0)(yaml@2.8.0)
9615 why-is-node-running: 2.3.0
9616 optionalDependencies:
9617 '@types/debug': 4.1.12
9571 - '@types/node': 22.15.23
9618 + '@types/node': 22.15.28
9619 jsdom: 26.1.0
9620 transitivePeerDependencies:
9621 - jiti
@@ -9584,28 +9631,28 @@ snapshots:
9631 - tsx
9632 - yaml
9633
9587 - vooks@0.2.12(vue@3.5.15(typescript@5.8.3)):
9634 + vooks@0.2.12(vue@3.5.16(typescript@5.8.3)):
9635 dependencies:
9636 evtd: 0.2.4
9590 - vue: 3.5.15(typescript@5.8.3)
9637 + vue: 3.5.16(typescript@5.8.3)
9638
9639 vscode-uri@3.1.0: {}
9640
9594 - vue-advanced-cropper@2.8.9(vue@3.5.15(typescript@5.8.3)):
9641 + vue-advanced-cropper@2.8.9(vue@3.5.16(typescript@5.8.3)):
9642 dependencies:
9643 classnames: 2.5.1
9644 debounce: 1.2.1
9645 easy-bem: 1.1.1
9599 - vue: 3.5.15(typescript@5.8.3)
9646 + vue: 3.5.16(typescript@5.8.3)
9647
9601 - vue-codemirror@6.1.1(codemirror@6.0.1)(vue@3.5.15(typescript@5.8.3)):
9648 + vue-codemirror@6.1.1(codemirror@6.0.1)(vue@3.5.16(typescript@5.8.3)):
9649 dependencies:
9650 '@codemirror/commands': 6.8.1
9651 '@codemirror/language': 6.11.0
9652 '@codemirror/state': 6.5.2
9653 '@codemirror/view': 6.36.8
9654 codemirror: 6.0.1
9608 - vue: 3.5.15(typescript@5.8.3)
9655 + vue: 3.5.16(typescript@5.8.3)
9656
9657 vue-component-type-helpers@2.2.10: {}
9658
@@ -9622,26 +9669,26 @@ snapshots:
9669 transitivePeerDependencies:
9670 - supports-color
9671
9625 - vue-highlight-words@3.0.1(vue@3.5.15(typescript@5.8.3)):
9672 + vue-highlight-words@3.0.1(vue@3.5.16(typescript@5.8.3)):
9673 dependencies:
9674 highlight-words-core: 1.2.3
9628 - vue: 3.5.15(typescript@5.8.3)
9675 + vue: 3.5.16(typescript@5.8.3)
9676
9630 - vue-i18n@11.1.5(vue@3.5.15(typescript@5.8.3)):
9677 + vue-i18n@11.1.5(vue@3.5.16(typescript@5.8.3)):
9678 dependencies:
9679 '@intlify/core-base': 11.1.5
9680 '@intlify/shared': 11.1.5
9681 '@vue/devtools-api': 6.6.4
9635 - vue: 3.5.15(typescript@5.8.3)
9682 + vue: 3.5.16(typescript@5.8.3)
9683
9637 - vue-router@4.5.1(vue@3.5.15(typescript@5.8.3)):
9684 + vue-router@4.5.1(vue@3.5.16(typescript@5.8.3)):
9685 dependencies:
9686 '@vue/devtools-api': 6.6.4
9640 - vue: 3.5.15(typescript@5.8.3)
9687 + vue: 3.5.16(typescript@5.8.3)
9688
9642 - vue-sjv@0.0.6(vue@3.5.15(typescript@5.8.3)):
9689 + vue-sjv@0.0.6(vue@3.5.16(typescript@5.8.3)):
9690 dependencies:
9644 - vue: 3.5.15(typescript@5.8.3)
9691 + vue: 3.5.16(typescript@5.8.3)
9692
9693 vue-tsc@2.2.10(typescript@5.8.3):
9694 dependencies:
@@ -9649,40 +9696,40 @@ snapshots:
9696 '@vue/language-core': 2.2.10(typescript@5.8.3)
9697 typescript: 5.8.3
9698
9652 - vue3-apexcharts@1.8.0(apexcharts@4.7.0)(vue@3.5.15(typescript@5.8.3)):
9699 + vue3-apexcharts@1.8.0(apexcharts@4.7.0)(vue@3.5.16(typescript@5.8.3)):
9700 dependencies:
9701 apexcharts: 4.7.0
9655 - vue: 3.5.15(typescript@5.8.3)
9702 + vue: 3.5.16(typescript@5.8.3)
9703
9657 - vue3-marquee@4.2.2(vue@3.5.15(typescript@5.8.3)):
9704 + vue3-marquee@4.2.2(vue@3.5.16(typescript@5.8.3)):
9705 dependencies:
9659 - vue: 3.5.15(typescript@5.8.3)
9706 + vue: 3.5.16(typescript@5.8.3)
9707
9661 - vue@3.5.15(typescript@5.8.3):
9708 + vue@3.5.16(typescript@5.8.3):
9709 dependencies:
9663 - '@vue/compiler-dom': 3.5.15
9664 - '@vue/compiler-sfc': 3.5.15
9665 - '@vue/runtime-dom': 3.5.15
9666 - '@vue/server-renderer': 3.5.15(vue@3.5.15(typescript@5.8.3))
9667 - '@vue/shared': 3.5.15
9710 + '@vue/compiler-dom': 3.5.16
9711 + '@vue/compiler-sfc': 3.5.16
9712 + '@vue/runtime-dom': 3.5.16
9713 + '@vue/server-renderer': 3.5.16(vue@3.5.16(typescript@5.8.3))
9714 + '@vue/shared': 3.5.16
9715 optionalDependencies:
9716 typescript: 5.8.3
9717
9671 - vuedraggable@4.1.0(vue@3.5.15(typescript@5.8.3)):
9718 + vuedraggable@4.1.0(vue@3.5.16(typescript@5.8.3)):
9719 dependencies:
9720 sortablejs: 1.14.0
9674 - vue: 3.5.15(typescript@5.8.3)
9721 + vue: 3.5.16(typescript@5.8.3)
9722
9676 - vueuc@0.4.64(vue@3.5.15(typescript@5.8.3)):
9723 + vueuc@0.4.64(vue@3.5.16(typescript@5.8.3)):
9724 dependencies:
9678 - '@css-render/vue3-ssr': 0.15.14(vue@3.5.15(typescript@5.8.3))
9725 + '@css-render/vue3-ssr': 0.15.14(vue@3.5.16(typescript@5.8.3))
9726 '@juggle/resize-observer': 3.4.0
9727 css-render: 0.15.14
9728 evtd: 0.2.4
9729 seemly: 0.3.10
9683 - vdirs: 0.1.8(vue@3.5.15(typescript@5.8.3))
9684 - vooks: 0.2.12(vue@3.5.15(typescript@5.8.3))
9685 - vue: 3.5.15(typescript@5.8.3)
9730 + vdirs: 0.1.8(vue@3.5.16(typescript@5.8.3))
9731 + vooks: 0.2.12(vue@3.5.16(typescript@5.8.3))
9732 + vue: 3.5.16(typescript@5.8.3)
9733
9734 w3c-keyname@2.2.8: {}
9735
frontend/src/api/endpoints/artifacts.ts
+24 -1
@@ -1,4 +1,11 @@
1 -import type { Artifact, CollectResult, CommandResult, QuarantineResult, Recommendation } from "@/types/artifacts.d"
1 +import type {
2 + Artifact,
3 + CollectResult,
4 + CommandResult,
5 + MatchingParameter,
6 + QuarantineResult,
7 + Recommendation
8 +} from "@/types/artifacts.d"
9 import type { OsTypesFull, OsTypesLower } from "@/types/common.d"
10 import type { FlaskBaseResponse } from "@/types/flask.d"
11 import { HttpClient } from "../httpClient"
@@ -12,6 +19,12 @@ export interface CollectRequest {
19 hostname: string
20 velociraptor_id?: string
21 artifact_name: string
22 + parameters?: {
23 + env?: {
24 + key: string
25 + value: string
26 + }[]
27 + }
28 }
29
30 export interface CommandRequest {
@@ -60,5 +73,15 @@ export default {
73 `/artifacts/velociraptor-artifact-recommendation`,
74 payload
75 )
76 + },
77 + getParameters(artifactName: string, parameterPrefix: string) {
78 + return HttpClient.get<
79 + FlaskBaseResponse & {
80 + artifact_name: string
81 + parameter_prefix: string
82 + matching_parameters: MatchingParameter[]
83 + total_matches: number
84 + }
85 + >(`/artifacts/artifact/${artifactName}/parameters/${parameterPrefix}`)
86 }
87 }
frontend/src/components/mitre/AtomicTests/TechniqueCard.vue
+5 -1
@@ -7,7 +7,7 @@
7 <code>{{ entity.test_count }}</code>
8 </template>
9 <template #default>{{ entity.technique_name }}</template>
10 - <template #footer>
10 + <template #footerMain>
11 <div class="flex flex-wrap items-center gap-3">
12 <Badge v-if="entity.has_prerequisites" color="primary" type="splitted">
13 <template #label>has prerequisites</template>
@@ -19,6 +19,9 @@
19 </Badge>
20 </div>
21 </template>
22 + <template #footerExtra>
23 + <SimulatorButton :technique-id="entity.technique_id" size="small" />
24 + </template>
25 </CardEntity>
26
27 <n-modal
@@ -42,6 +45,7 @@ import Badge from "@/components/common/Badge.vue"
45 import CardEntity from "@/components/common/cards/CardEntity.vue"
46 import Icon from "@/components/common/Icon.vue"
47 import { iconFromOs } from "@/utils"
48 +import SimulatorButton from "../WindowsAttackSimulator/SimulatorButton.vue"
49 import TechniqueCardContent from "./TechniqueCardContent.vue"
50
51 const { entity } = defineProps<{ entity: MitreAtomicTest; embedded?: boolean }>()
frontend/src/components/mitre/TechniqueAlert/TechniqueAlertOverview.vue
+6
@@ -47,6 +47,11 @@
47 <TechniqueEventsList v-if="techniqueDetails" :external-id />
48 </div>
49 </n-tab-pane>
50 + <n-tab-pane name="Atomic test" tab="Atomic test" display-directive="show:lazy">
51 + <div class="px-7 pb-7 pt-4">
52 + <TechniqueCardContent :technique-id="externalId" />
53 + </div>
54 + </n-tab-pane>
55 </n-tabs>
56 </n-spin>
57 </template>
@@ -56,6 +61,7 @@ import type { MitreTechniqueDetails } from "@/types/mitre.d"
61 import { NSpin, NTabPane, NTabs, useMessage } from "naive-ui"
62 import { onBeforeMount, ref } from "vue"
63 import Api from "@/api"
64 +import TechniqueCardContent from "../AtomicTests/TechniqueCardContent.vue"
65 import GroupsList from "../Group/GroupsList.vue"
66 import MitigationsList from "../Mitigation/MitigationsList.vue"
67 import SoftwareList from "../Software/SoftwareList.vue"
frontend/src/components/mitre/WindowsAttackSimulator/AgentsList.vue new
+100
@@ -0,0 +1,100 @@
1 +<template>
2 + <n-spin :show="loading">
3 + <div class="flex min-h-52 flex-col gap-2 py-0.5">
4 + <template v-if="list.length">
5 + <CardEntity
6 + v-for="item of list"
7 + :key="item.hostname"
8 + embedded
9 + clickable
10 + hoverable
11 + :highlighted="item.hostname === selected?.hostname"
12 + size="small"
13 + @click="setItem(item)"
14 + >
15 + <template #headerMain>
16 + {{ item.hostname }}
17 + </template>
18 + <template #headerExtra>
19 + <code class="text-primary cursor-pointer" @click.stop="gotoAgent(item.agent_id)">
20 + {{ item.agent_id }}
21 + <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
22 + </code>
23 + </template>
24 + <template #default>
25 + {{ item.ip_address }}
26 + <code>{{ item.label }}</code>
27 + </template>
28 + <template #footer>
29 + {{ item.os }}
30 + </template>
31 + </CardEntity>
32 + </template>
33 + <template v-else>
34 + <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" />
35 + </template>
36 + </div>
37 + </n-spin>
38 +</template>
39 +
40 +<script setup lang="ts">
41 +import type { Agent } from "@/types/agents.d"
42 +import { NEmpty, NSpin, useMessage } from "naive-ui"
43 +import { onBeforeMount, ref } from "vue"
44 +import Api from "@/api"
45 +import CardEntity from "@/components/common/cards/CardEntity.vue"
46 +import Icon from "@/components/common/Icon.vue"
47 +import { useGoto } from "@/composables/useGoto"
48 +
49 +const { agentsList, filter } = defineProps<{
50 + agentsList?: Agent[] | null
51 + filter?: (agent: Agent) => boolean
52 +}>()
53 +
54 +const emit = defineEmits<{
55 + (e: "loaded", value: Agent[]): void
56 +}>()
57 +
58 +const selected = defineModel<Agent | null>("selected", { default: null })
59 +
60 +const LinkIcon = "carbon:launch"
61 +
62 +const { gotoAgent } = useGoto()
63 +const message = useMessage()
64 +const loading = ref(false)
65 +const list = ref<Agent[]>([])
66 +
67 +function getList() {
68 + loading.value = true
69 +
70 + Api.agents
71 + .getAgents()
72 + .then(res => {
73 + if (res.data.success) {
74 + const tmpList = res.data?.agents || []
75 + list.value = filter ? tmpList.filter(filter) : tmpList
76 + emit("loaded", list.value)
77 + } else {
78 + message.warning(res.data?.message || "An error occurred. Please try again later.")
79 + }
80 + })
81 + .catch(err => {
82 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
83 + })
84 + .finally(() => {
85 + loading.value = false
86 + })
87 +}
88 +
89 +function setItem(item: Agent) {
90 + selected.value = selected.value?.hostname === item.hostname ? null : item
91 +}
92 +
93 +onBeforeMount(() => {
94 + if (agentsList?.length) {
95 + list.value = agentsList
96 + } else {
97 + getList()
98 + }
99 +})
100 +</script>
frontend/src/components/mitre/WindowsAttackSimulator/ParametersList.vue new
+84
@@ -0,0 +1,84 @@
1 +<template>
2 + <n-spin :show="loading">
3 + <div class="flex min-h-52 flex-col gap-2 py-0.5">
4 + <template v-if="list.length">
5 + <CardEntity
6 + v-for="item of list"
7 + :key="item.name"
8 + embedded
9 + clickable
10 + hoverable
11 + :highlighted="item.name === selected?.name"
12 + size="small"
13 + @click="setItem(item)"
14 + >
15 + <template #header>
16 + {{ item.name }}
17 + </template>
18 + <template #default>
19 + {{ item.description }}
20 + </template>
21 + </CardEntity>
22 + </template>
23 + <template v-else>
24 + <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" />
25 + </template>
26 + </div>
27 + </n-spin>
28 +</template>
29 +
30 +<script setup lang="ts">
31 +import type { MatchingParameter } from "@/types/artifacts"
32 +import { NEmpty, NSpin, useMessage } from "naive-ui"
33 +import { onBeforeMount, ref } from "vue"
34 +import Api from "@/api"
35 +import CardEntity from "@/components/common/cards/CardEntity.vue"
36 +
37 +const { techniqueId, parametersList } = defineProps<{
38 + techniqueId: string
39 + parametersList?: MatchingParameter[] | null
40 +}>()
41 +
42 +const emit = defineEmits<{
43 + (e: "loaded", value: MatchingParameter[]): void
44 +}>()
45 +
46 +const selected = defineModel<MatchingParameter | null>("selected", { default: null })
47 +
48 +const message = useMessage()
49 +const loading = ref(false)
50 +const list = ref<MatchingParameter[]>([])
51 +
52 +function getList() {
53 + loading.value = true
54 +
55 + Api.artifacts
56 + .getParameters("Windows.AttackSimulation.AtomicRedTeam", techniqueId)
57 + .then(res => {
58 + if (res.data.success) {
59 + list.value = res.data?.matching_parameters || []
60 + emit("loaded", list.value)
61 + } else {
62 + message.warning(res.data?.message || "An error occurred. Please try again later.")
63 + }
64 + })
65 + .catch(err => {
66 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
67 + })
68 + .finally(() => {
69 + loading.value = false
70 + })
71 +}
72 +
73 +function setItem(item: MatchingParameter) {
74 + selected.value = selected.value?.name === item.name ? null : item
75 +}
76 +
77 +onBeforeMount(() => {
78 + if (parametersList?.length) {
79 + list.value = parametersList
80 + } else {
81 + getList()
82 + }
83 +})
84 +</script>
frontend/src/components/mitre/WindowsAttackSimulator/SimulatorButton.vue new
+34
@@ -0,0 +1,34 @@
1 +<template>
2 + <n-button :size type="primary" secondary @click.stop="showForm = true">
3 + <template #icon>
4 + <Icon :name="AttackIcon" />
5 + </template>
6 + Simulate Windows Attack
7 + </n-button>
8 +
9 + <n-modal
10 + v-model:show="showForm"
11 + display-directive="show"
12 + preset="card"
13 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(250px, 90vh)', overflow: 'hidden' }"
14 + :title="`${techniqueId}: Simulate Windows Attack`"
15 + :bordered="false"
16 + segmented
17 + content-class="p-0!"
18 + >
19 + <SimulatorWizard :technique-id />
20 + </n-modal>
21 +</template>
22 +
23 +<script setup lang="ts">
24 +import type { Size } from "naive-ui/es/button/src/interface"
25 +import { NButton, NModal } from "naive-ui"
26 +import { ref } from "vue"
27 +import Icon from "@/components/common/Icon.vue"
28 +import SimulatorWizard from "./SimulatorWizard.vue"
29 +
30 +const { size, techniqueId } = defineProps<{ size?: Size; techniqueId: string }>()
31 +
32 +const AttackIcon = "mdi:target"
33 +const showForm = ref(false)
34 +</script>
frontend/src/components/mitre/WindowsAttackSimulator/SimulatorWizard.vue new
+378
@@ -0,0 +1,378 @@
1 +<template>
2 + <div class="simulator-windows-attack-wizard min-h-120 flex flex-col gap-4 overflow-hidden">
3 + <div>
4 + <n-scrollbar x-scrollable trigger="none">
5 + <div class="px-7 pb-2 pt-4">
6 + <n-steps :current="current" size="small" :status="currentStatus">
7 + <n-step title="Attack" />
8 + <n-step title="Endpoint agent" />
9 + <n-step title="Simulate" />
10 + </n-steps>
11 + </div>
12 + </n-scrollbar>
13 + </div>
14 +
15 + <div class="flex grow flex-col overflow-hidden">
16 + <Transition :name="`slide-form-${slideFormDirection}`">
17 + <div v-if="current === 1" class="grow overflow-hidden">
18 + <n-scrollbar ref="parametersScroll" style="max-height: 350px" trigger="none">
19 + <ParametersList
20 + v-model:selected="selectedAttack"
21 + :technique-id
22 + class="px-7"
23 + :parameters-list
24 + @loaded="parametersList = $event"
25 + />
26 + </n-scrollbar>
27 + </div>
28 + <div v-else-if="current === 2" class="grow overflow-hidden">
29 + <n-scrollbar ref="agentsScroll" style="max-height: 350px" trigger="none">
30 + <AgentsList
31 + v-model:selected="selectedAgent"
32 + class="px-7"
33 + :agents-list
34 + :filter="agentsListFilter"
35 + @loaded="agentsList = $event"
36 + />
37 + </n-scrollbar>
38 + </div>
39 + <div v-else class="flex flex-col gap-6 px-7">
40 + <CardEntity size="small" embedded>
41 + <template #header>selected</template>
42 + <template #footer>
43 + <div class="flex flex-col gap-2">
44 + <CardEntity v-if="selectedAttack" embedded size="small">
45 + <template #header>
46 + {{ selectedAttack.name }}
47 + </template>
48 + <template #default>
49 + {{ selectedAttack.description }}
50 + </template>
51 + </CardEntity>
52 + <CardEntity v-if="selectedAgent" embedded size="small">
53 + <template #headerMain>
54 + {{ selectedAgent.hostname }}
55 + </template>
56 + <template #headerExtra>
57 + <code
58 + class="text-primary cursor-pointer"
59 + @click.stop="gotoAgent(selectedAgent.agent_id)"
60 + >
61 + {{ selectedAgent.agent_id }}
62 + <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
63 + </code>
64 + </template>
65 + <template #default>
66 + {{ selectedAgent.ip_address }}
67 + <code>{{ selectedAgent.label }}</code>
68 + </template>
69 + <template #footer>
70 + {{ selectedAgent.os }}
71 + </template>
72 + </CardEntity>
73 + </div>
74 + </template>
75 + </CardEntity>
76 +
77 + <div v-if="collectResponse" class="flex flex-col gap-2">
78 + <div v-if="collectTime" class="text-xs">
79 + <span class="text-sm">last simulation:</span>
80 + <code>{{ formatDate(collectTime, dFormats.datetimesec) }}</code>
81 + </div>
82 + <CardEntity v-for="report of collectResponse" :key="`${report.GUID}`" size="small">
83 + <template #default>
84 + <table>
85 + <tbody class="text-xs">
86 + <tr v-for="(value, key) in report" :key="`${key}`">
87 + <td class="text-secondary whitespace-nowrap p-1 pr-4">{{ key }}</td>
88 + <td class="p-1">{{ value }}</td>
89 + </tr>
90 + </tbody>
91 + </table>
92 + </template>
93 + </CardEntity>
94 + </div>
95 + <template v-else>
96 + <div v-if="!loading" class="p-6 text-center">
97 + Run “
98 + <strong>Simulate attack</strong>
99 + ” to view the report here.
100 + </div>
101 + <div v-else class="flex flex-col gap-2">
102 + <n-skeleton height="20px" width="50%" :sharp="false" />
103 + <n-skeleton height="100px" width="100%" :sharp="false" />
104 + </div>
105 + </template>
106 + </div>
107 + </Transition>
108 + </div>
109 +
110 + <div class="flex justify-between gap-4 px-7 pb-4">
111 + <div class="flex items-center gap-4">
112 + <n-button v-if="isPrevStepEnabled" :disabled="loading" @click="prev()">
113 + <template #icon>
114 + <Icon :name="ArrowLeftIcon"></Icon>
115 + </template>
116 + Prev
117 + </n-button>
118 + <n-button v-if="isNextStepEnabled" icon-placement="right" :disabled="loading" @click="next()">
119 + <template #icon>
120 + <Icon :name="ArrowRightIcon"></Icon>
121 + </template>
122 + Next
123 + </n-button>
124 + </div>
125 + <n-button v-if="isSubmitEnabled" type="primary" :disabled="!isSubmitValid" :loading @click="submit()">
126 + <template #icon>
127 + <Icon :name="AttackIcon"></Icon>
128 + </template>
129 + Simulate attack
130 + </n-button>
131 + </div>
132 + </div>
133 +</template>
134 +
135 +<script setup lang="ts">
136 +import type { ScrollbarInst, StepsProps } from "naive-ui"
137 +import type { CollectRequest } from "@/api/endpoints/artifacts"
138 +import type { Agent } from "@/types/agents.d"
139 +import type { MatchingParameter } from "@/types/artifacts.d"
140 +import { NButton, NScrollbar, NSkeleton, NStep, NSteps, useMessage } from "naive-ui"
141 +import { computed, ref, watch } from "vue"
142 +import Api from "@/api"
143 +import CardEntity from "@/components/common/cards/CardEntity.vue"
144 +import Icon from "@/components/common/Icon.vue"
145 +import { useGoto } from "@/composables/useGoto"
146 +import { useSettingsStore } from "@/stores/settings"
147 +import { formatDate } from "@/utils"
148 +import AgentsList from "./AgentsList.vue"
149 +import ParametersList from "./ParametersList.vue"
150 +
151 +export interface Report {
152 + "Execution Time (UTC)": Date
153 + "Execution Time (Local)": Date
154 + Technique: string
155 + "Test Number": number
156 + "Test Name": string
157 + Hostname: string
158 + Username: string
159 + GUID: string
160 +}
161 +
162 +const { techniqueId } = defineProps<{
163 + techniqueId: string
164 +}>()
165 +
166 +const emit = defineEmits<{
167 + (e: "update:loading", value: boolean): void
168 + (e: "close"): void
169 + (e: "submitted"): void
170 +}>()
171 +
172 +const ArrowRightIcon = "carbon:arrow-right"
173 +const ArrowLeftIcon = "carbon:arrow-left"
174 +const AttackIcon = "mdi:target"
175 +const LinkIcon = "carbon:launch"
176 +
177 +const { gotoAgent } = useGoto()
178 +const dFormats = useSettingsStore().dateFormat
179 +const message = useMessage()
180 +const current = ref<number>(1)
181 +const currentStatus = ref<StepsProps["status"]>("process")
182 +const slideFormDirection = ref<"right" | "left">("right")
183 +
184 +const parametersList = ref<MatchingParameter[] | null>(null)
185 +const agentsList = ref<Agent[] | null>(null)
186 +const selectedAttack = ref<MatchingParameter | null>(null)
187 +const selectedAgent = ref<Agent | null>(null)
188 +const loading = ref(false)
189 +const collectResponse = ref<Report[] | null>(null)
190 +const collectTime = ref<Date | null>(null)
191 +const parametersScroll = ref<ScrollbarInst | null>(null)
192 +const agentsScroll = ref<ScrollbarInst | null>(null)
193 +
194 +const isNextStepEnabled = computed(
195 + () => (current.value === 1 && selectedAttack.value) || (current.value === 2 && selectedAgent.value)
196 +)
197 +const isPrevStepEnabled = computed(() => current.value > 1)
198 +const isSubmitEnabled = computed(() => current.value === 3)
199 +const isSubmitValid = computed(() => {
200 + if (!selectedAttack.value) {
201 + return false
202 + }
203 +
204 + if (!selectedAgent.value) {
205 + return false
206 + }
207 +
208 + return true
209 +})
210 +
211 +function submit() {
212 + if (selectedAttack.value && selectedAgent.value) {
213 + currentStatus.value = "finish"
214 + loading.value = true
215 +
216 + const payload: CollectRequest = {
217 + hostname: selectedAgent.value.hostname,
218 + artifact_name: "Windows.AttackSimulation.AtomicRedTeam",
219 + parameters: {
220 + env: [
221 + {
222 + key: "InstallART",
223 + value: "N"
224 + },
225 + {
226 + key: selectedAttack.value.name,
227 + value: "Y"
228 + }
229 + ]
230 + }
231 + }
232 +
233 + Api.artifacts
234 + .collect(payload)
235 + .then(res => {
236 + if (res.data.success) {
237 + emit("submitted")
238 + collectResponse.value = (res.data.results as unknown as Report[]) || []
239 + collectTime.value = new Date()
240 + message.success(res.data?.message || "Successfully executed query.")
241 + } else {
242 + message.warning(res.data?.message || "An error occurred. Please try again later.")
243 + }
244 + })
245 + .catch(err => {
246 + // MOCK
247 + /*
248 + const res = {
249 + message: "Successfully executed query",
250 + success: true,
251 + results: [
252 + {
253 + "Execution Time (UTC)": "2025-05-31T20:43:02Z",
254 + "Execution Time (Local)": "2025-05-31T13:43:02Z",
255 + Technique:
256 + "[T1003.001](https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1003.001/T1003.001.md)",
257 + "Test Number": 10,
258 + "Test Name": "Powershell Mimikatz",
259 + Hostname: "WIN-HFOU106TD7K",
260 + Username: "nt authority\\system",
261 + GUID: "66fb0bc1-3c3f-47e9-a298-550ecfefacbc"
262 + }
263 + ]
264 + }
265 +
266 + emit("submitted")
267 + collectResponse.value = (res.results as unknown as Report[]) || []
268 + collectTime.value = new Date()
269 + message.success(res?.message || "Successfully executed query.")
270 +
271 + throw err
272 + */
273 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
274 + })
275 + .finally(() => {
276 + loading.value = false
277 + })
278 + }
279 +}
280 +
281 +function _reset() {
282 + currentStatus.value = "process"
283 + slideFormDirection.value = "right"
284 + current.value = 1
285 +
286 + selectedAttack.value = null
287 + selectedAgent.value = null
288 + collectResponse.value = null
289 + collectTime.value = null
290 +}
291 +
292 +function next() {
293 + currentStatus.value = "process"
294 + slideFormDirection.value = "right"
295 + current.value++
296 +}
297 +
298 +function prev() {
299 + currentStatus.value = "process"
300 + slideFormDirection.value = "left"
301 + current.value--
302 +}
303 +
304 +function agentsListFilter(agent: Agent) {
305 + return agent.os.toLowerCase().includes("window")
306 +}
307 +
308 +function scrollInView(scrollContainer: ScrollbarInst) {
309 + // @ts-expect-error $el property not mapped
310 + const wrap = (scrollContainer.$el.nextSibling || scrollContainer.$el.nextElementSibling) as HTMLElement
311 +
312 + const element = wrap.querySelector(".highlighted") as HTMLElement
313 +
314 + if (element) {
315 + const middle = element.offsetTop - wrap.offsetHeight / 2 + element.offsetHeight / 2
316 +
317 + scrollContainer.scrollTo({ top: middle, behavior: "smooth" })
318 + }
319 +}
320 +
321 +watch(selectedAttack, val => {
322 + if (val) {
323 + next()
324 + }
325 +})
326 +
327 +watch(selectedAgent, val => {
328 + if (val) {
329 + next()
330 + }
331 +})
332 +
333 +watch([current, parametersList, agentsList], () => {
334 + if (current.value === 1 && parametersList.value?.length && selectedAttack.value) {
335 + setTimeout(() => {
336 + if (parametersScroll.value) {
337 + scrollInView(parametersScroll.value)
338 + }
339 + }, 200)
340 + }
341 + if (current.value === 2 && agentsList.value?.length && selectedAgent.value) {
342 + setTimeout(() => {
343 + if (agentsScroll.value) {
344 + scrollInView(agentsScroll.value)
345 + }
346 + }, 200)
347 + }
348 +})
349 +</script>
350 +
351 +<style lang="scss" scoped>
352 +.simulator-windows-attack-wizard {
353 + .slide-form-right-enter-active,
354 + .slide-form-right-leave-active,
355 + .slide-form-left-enter-active,
356 + .slide-form-left-leave-active {
357 + transition: all 0.2s ease-out;
358 + position: absolute;
359 + width: 100%;
360 + }
361 +
362 + .slide-form-left-enter-from {
363 + transform: translateX(-100%);
364 + }
365 +
366 + .slide-form-left-leave-to {
367 + transform: translateX(100%);
368 + }
369 +
370 + .slide-form-right-enter-from {
371 + transform: translateX(100%);
372 + }
373 +
374 + .slide-form-right-leave-to {
375 + transform: translateX(-100%);
376 + }
377 +}
378 +</style>
frontend/src/types/artifacts.d.ts
+7
@@ -24,3 +24,10 @@ export interface Recommendation {
24 description: string
25 explanation: string
26 }
27 +
28 +export interface MatchingParameter {
29 + name: string
30 + description: string
31 + type: string
32 + default?: string
33 +}