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
+}