@cryptotaxi247 / CoPilot / commits / e8426570

Velo sigma exclusion (#445)

* Add VeloSigma exclusion table and model definition * Add Velociraptor Sigma exclusion management endpoints and schemas * Refactor Velociraptor Sigma exclusion management: improve code formatting and structure * Refactor Velociraptor Sigma exclusion responses: update response models and improve API consistency * Migrate Sigma job to Velo: comment out Sigma queries collection job * Add endpoint to retrieve Wazuh agents for Grafana dashboard by customer code * dev build * back to main * dev build * back to main * Add SOCFortress DEFENDER input configuration template for Graylog * Add Defender for Endpoint dashboard and summary configuration * defender for endpoint integration addition * dev build * back to main * refactor: update Incident Sources page * chore: update frontend dependencies * fix: update asset payload to use agent hostname in VelociraptorSigmaService * feat: enhance alert creation for Velociraptor Sigma by checking for existing alerts and adding assets/IOCs * fix: update agent name to use hostname for consistency with Wazuh events * feat: implement MITRE ATT&CK tactics and techniques endpoints in Wazuh Manager * fix: update references from Crowdstrike to DefenderForEndpoint in provisioning logic * dev build * back to main * fix: update tenant ID replacement in filebeat configuration * dev build * back to main * feat: add pagination and total count to exclusion rules retrieval * feat: implement Atomic Red Team markdown retrieval and caching * feat: enhance artifact collection with optional parameters support * feat: add exclusion rules list * chore: update frontend dependencies * feat: add status toggler * refactor: incident Management api * feat: overview component * feat: add delete button * feat: add form * feat: update form * lint * feat: add customer field * feat: responsiveness * lint * feat: enhance delete_exclusion response to include success status * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Apr 27, 2025 at 16:19 UTC e842657046e107ceeee604d17c4e7fe3f0c3ae7e
95 files changed +5728 -1211
backend/alembic/alembic.ini
+1 -1
@@ -60,7 +60,7 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne
60 # are written from script.py.mako
61 # output_encoding = utf-8
62
63 -sqlalchemy.url = mysql+pymysql://copilot:H7U3AHsXWSGvE5L123B7$GQdLQz@10.255.254.2/copilot
63 +sqlalchemy.url = mysql+pymysql://copilot:REPLACE_ME@copilot-mysql/copilot
64
65
66 [post_write_hooks]
backend/alembic/versions/53ca91e8a196_add_velo_sigma_rule_exclusion_table.py new
+46
@@ -0,0 +1,46 @@
1 +"""Add velo sigma rule exclusion table
2 +
3 +Revision ID: 53ca91e8a196
4 +Revises: 6ac2c9c193a8
5 +Create Date: 2025-04-21 12:50:23.321400
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +
13 +from alembic import op
14 +
15 +# revision identifiers, used by Alembic.
16 +revision: str = "53ca91e8a196"
17 +down_revision: Union[str, None] = "6ac2c9c193a8"
18 +branch_labels: Union[str, Sequence[str], None] = None
19 +depends_on: Union[str, Sequence[str], None] = None
20 +
21 +
22 +def upgrade() -> None:
23 + # ### commands auto generated by Alembic - please adjust! ###
24 + op.create_table(
25 + "incident_management_velo_sigma_exclusion",
26 + sa.Column("field_matches", sa.JSON(), nullable=True),
27 + sa.Column("id", sa.Integer(), nullable=False),
28 + sa.Column("name", sa.String(length=255), nullable=False),
29 + sa.Column("description", sa.String(length=255), nullable=True),
30 + sa.Column("channel", sa.String(length=255), nullable=True),
31 + sa.Column("title", sa.String(length=255), nullable=True),
32 + sa.Column("customer_code", sa.String(length=50), nullable=True),
33 + sa.Column("created_by", sa.String(length=100), nullable=False),
34 + sa.Column("created_at", sa.DateTime(), nullable=False),
35 + sa.Column("last_matched_at", sa.DateTime(), nullable=True),
36 + sa.Column("match_count", sa.Integer(), nullable=False),
37 + sa.Column("enabled", sa.Boolean(), nullable=False),
38 + sa.PrimaryKeyConstraint("id"),
39 + )
40 + # ### end Alembic commands ###
41 +
42 +
43 +def downgrade() -> None:
44 + # ### commands auto generated by Alembic - please adjust! ###
45 + op.drop_table("incident_management_velo_sigma_exclusion")
46 + # ### end Alembic commands ###
backend/app/agents/routes/agents.py
+50
@@ -1,11 +1,13 @@
1 import asyncio
2 import csv
3 import io
4 +from typing import Optional
5
6 # from fastapi import BackgroundTasks
7 from fastapi import APIRouter
8 from fastapi import BackgroundTasks
9 from fastapi import Depends
10 +from fastapi import Header
11 from fastapi import HTTPException
12 from fastapi import Path
13 from fastapi import Security
@@ -178,6 +180,54 @@ async def get_agents(db: AsyncSession = Depends(get_db)) -> AgentsResponse:
180 raise HTTPException(status_code=500, detail=f"Failed to fetch agents: {e}")
181
182
183 +@agents_router.get(
184 + "/dashboard/agents",
185 + response_model=AgentsResponse,
186 + description="Get all Wazuh agents for a specific customer (Grafana dashboard use)",
187 +)
188 +async def get_customer_agents_for_dashboard(
189 + customer_code: Optional[str] = Header(None, description="Customer code to filter agents by"),
190 + db: AsyncSession = Depends(get_db),
191 +) -> AgentsResponse:
192 + """
193 + Retrieve all agents for a specific customer for dashboard use.
194 + This endpoint is designed specifically for integration with Grafana dashboards.
195 +
196 + Args:
197 + customer_code (str, optional): The customer code from the request header.
198 + db (AsyncSession): The database session.
199 +
200 + Returns:
201 + AgentsResponse: The response containing the list of agents for the specified customer.
202 +
203 + Raises:
204 + HTTPException: If the customer_code is not provided or if there's an error fetching the agents.
205 + """
206 + if not customer_code:
207 + logger.warning("Dashboard agent request made with no customer_code header")
208 + return AgentsResponse(
209 + agents=[],
210 + success=False,
211 + message="No customer_code header provided",
212 + )
213 +
214 + logger.info(f"Fetching agents for customer_code: {customer_code} (dashboard request)")
215 + try:
216 + # Query agents with the specified customer code
217 + result = await db.execute(select(Agents).filter(Agents.customer_code == customer_code))
218 + agents = result.scalars().all()
219 +
220 + logger.info(f"Found {len(agents)} agents for customer_code: {customer_code}")
221 + return AgentsResponse(
222 + agents=agents,
223 + success=True,
224 + message=f"Agents for customer {customer_code} fetched successfully",
225 + )
226 + except Exception as e:
227 + logger.error(f"Failed to fetch agents for customer {customer_code}: {e}")
228 + raise HTTPException(status_code=500, detail=f"Failed to fetch agents for customer {customer_code}")
229 +
230 +
231 @agents_router.get(
232 "/{agent_id}",
233 response_model=AgentsResponse,
backend/app/connectors/grafana/dashboards/DefenderForEndpoint/summary.json new
+953
@@ -0,0 +1,953 @@
1 +{
2 + "annotations": {
3 + "list": [
4 + {
5 + "builtIn": 1,
6 + "datasource": {
7 + "type": "grafana",
8 + "uid": "-- Grafana --"
9 + },
10 + "enable": true,
11 + "hide": true,
12 + "iconColor": "rgba(0, 211, 255, 1)",
13 + "name": "Annotations & Alerts",
14 + "type": "dashboard"
15 + }
16 + ]
17 + },
18 + "editable": false,
19 + "fiscalYearStartMonth": 0,
20 + "graphTooltip": 0,
21 + "id": null,
22 + "links": [],
23 + "panels": [
24 + {
25 + "datasource": {
26 + "type": "grafana-opensearch-datasource",
27 + "uid": "replace_datasource_uid"
28 + },
29 + "fieldConfig": {
30 + "defaults": {
31 + "mappings": [
32 + {
33 + "options": {
34 + "match": "null",
35 + "result": {
36 + "text": "N/A"
37 + }
38 + },
39 + "type": "special"
40 + }
41 + ],
42 + "thresholds": {
43 + "mode": "absolute",
44 + "steps": [
45 + {
46 + "color": "orange",
47 + "value": null
48 + }
49 + ]
50 + },
51 + "unit": "locale"
52 + },
53 + "overrides": []
54 + },
55 + "gridPos": {
56 + "h": 7,
57 + "w": 3,
58 + "x": 0,
59 + "y": 0
60 + },
61 + "id": 2,
62 + "options": {
63 + "colorMode": "value",
64 + "graphMode": "area",
65 + "justifyMode": "auto",
66 + "orientation": "horizontal",
67 + "percentChangeColorMode": "standard",
68 + "reduceOptions": {
69 + "calcs": [
70 + "sum"
71 + ],
72 + "fields": "",
73 + "values": false
74 + },
75 + "showPercentChange": false,
76 + "text": {},
77 + "textMode": "auto",
78 + "wideLayout": true
79 + },
80 + "pluginVersion": "11.4.0",
81 + "targets": [
82 + {
83 + "bucketAggs": [
84 + {
85 + "field": "timestamp",
86 + "id": "2",
87 + "settings": {
88 + "interval": "auto",
89 + "min_doc_count": 0,
90 + "trimEdges": 0
91 + },
92 + "type": "date_histogram"
93 + }
94 + ],
95 + "datasource": {
96 + "type": "grafana-opensearch-datasource",
97 + "uid": "replace_datasource_uid"
98 + },
99 + "metrics": [
100 + {
101 + "field": "select field",
102 + "id": "1",
103 + "type": "count"
104 + }
105 + ],
106 + "query": "_exists_:json_description",
107 + "refId": "A",
108 + "timeField": "timestamp"
109 + }
110 + ],
111 + "title": "ALERTS",
112 + "type": "stat"
113 + },
114 + {
115 + "datasource": {
116 + "type": "grafana-opensearch-datasource",
117 + "uid": "replace_datasource_uid"
118 + },
119 + "fieldConfig": {
120 + "defaults": {
121 + "color": {
122 + "mode": "thresholds"
123 + },
124 + "custom": {
125 + "align": "auto",
126 + "cellOptions": {
127 + "type": "auto"
128 + },
129 + "inspect": false
130 + },
131 + "mappings": [],
132 + "thresholds": {
133 + "mode": "absolute",
134 + "steps": [
135 + {
136 + "color": "dark-orange",
137 + "value": null
138 + }
139 + ]
140 + }
141 + },
142 + "overrides": [
143 + {
144 + "matcher": {
145 + "id": "byName",
146 + "options": "Time"
147 + },
148 + "properties": [
149 + {
150 + "id": "displayName",
151 + "value": "Time"
152 + },
153 + {
154 + "id": "unit",
155 + "value": "time: YYYY-MM-DD HH:mm:ss"
156 + },
157 + {
158 + "id": "custom.align"
159 + }
160 + ]
161 + },
162 + {
163 + "matcher": {
164 + "id": "byName",
165 + "options": "Count"
166 + },
167 + "properties": [
168 + {
169 + "id": "displayName",
170 + "value": "EVENTS"
171 + },
172 + {
173 + "id": "unit",
174 + "value": "short"
175 + },
176 + {
177 + "id": "decimals",
178 + "value": -1
179 + },
180 + {
181 + "id": "custom.cellOptions",
182 + "value": {
183 + "mode": "gradient",
184 + "type": "color-background"
185 + }
186 + },
187 + {
188 + "id": "custom.align"
189 + },
190 + {
191 + "id": "thresholds",
192 + "value": {
193 + "mode": "absolute",
194 + "steps": [
195 + {
196 + "color": "dark-orange",
197 + "value": null
198 + }
199 + ]
200 + }
201 + }
202 + ]
203 + },
204 + {
205 + "matcher": {
206 + "id": "byName",
207 + "options": "agent_name"
208 + },
209 + "properties": [
210 + {
211 + "id": "displayName",
212 + "value": "AGENT"
213 + },
214 + {
215 + "id": "custom.cellOptions",
216 + "value": {
217 + "mode": "gradient",
218 + "type": "color-background"
219 + }
220 + },
221 + {
222 + "id": "custom.align"
223 + },
224 + {
225 + "id": "links",
226 + "value": [
227 + {
228 + "targetBlank": true,
229 + "title": "VIEW EVENTS",
230 + "url": "https://grafana.morcan.sec/explore?left=%5B%22now-1h%22,%22now%22,%22WAZUH%22,%7B%22refId%22:%22A%22,%22query%22:%22agent_name:${__value.text}%22,%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D"
231 + }
232 + ]
233 + }
234 + ]
235 + },
236 + {
237 + "matcher": {
238 + "id": "byName",
239 + "options": "Count"
240 + },
241 + "properties": [
242 + {
243 + "id": "displayName",
244 + "value": "ALERTS"
245 + },
246 + {
247 + "id": "unit",
248 + "value": "short"
249 + },
250 + {
251 + "id": "decimals",
252 + "value": 0
253 + },
254 + {
255 + "id": "custom.align"
256 + }
257 + ]
258 + },
259 + {
260 + "matcher": {
261 + "id": "byName",
262 + "options": "COMPUTER"
263 + },
264 + "properties": [
265 + {
266 + "id": "custom.width",
267 + "value": 276
268 + }
269 + ]
270 + }
271 + ]
272 + },
273 + "gridPos": {
274 + "h": 7,
275 + "w": 5,
276 + "x": 3,
277 + "y": 0
278 + },
279 + "id": 3,
280 + "options": {
281 + "cellHeight": "sm",
282 + "footer": {
283 + "countRows": false,
284 + "fields": "",
285 + "reducer": [
286 + "sum"
287 + ],
288 + "show": false
289 + },
290 + "showHeader": true,
291 + "sortBy": []
292 + },
293 + "pluginVersion": "11.4.0",
294 + "targets": [
295 + {
296 + "bucketAggs": [
297 + {
298 + "fake": true,
299 + "field": "json_computerDnsName",
300 + "id": "4",
301 + "settings": {
302 + "min_doc_count": 1,
303 + "order": "desc",
304 + "orderBy": "_term",
305 + "size": "0"
306 + },
307 + "type": "terms"
308 + }
309 + ],
310 + "datasource": {
311 + "type": "grafana-opensearch-datasource",
312 + "uid": "replace_datasource_uid"
313 + },
314 + "metrics": [
315 + {
316 + "field": "select field",
317 + "id": "1",
318 + "type": "count"
319 + }
320 + ],
321 + "query": "_exists_:json_description",
322 + "refId": "A",
323 + "timeField": "timestamp"
324 + }
325 + ],
326 + "title": "ALERTS BY AGENT",
327 + "transformations": [
328 + {
329 + "id": "organize",
330 + "options": {
331 + "excludeByName": {},
332 + "includeByName": {},
333 + "indexByName": {},
334 + "renameByName": {
335 + "json_computerDnsName": "COMPUTER"
336 + }
337 + }
338 + }
339 + ],
340 + "type": "table"
341 + },
342 + {
343 + "datasource": {
344 + "type": "grafana-opensearch-datasource",
345 + "uid": "replace_datasource_uid"
346 + },
347 + "fieldConfig": {
348 + "defaults": {
349 + "color": {
350 + "mode": "palette-classic"
351 + },
352 + "custom": {
353 + "axisBorderShow": false,
354 + "axisCenteredZero": false,
355 + "axisColorMode": "text",
356 + "axisLabel": "",
357 + "axisPlacement": "auto",
358 + "barAlignment": 0,
359 + "barWidthFactor": 0.6,
360 + "drawStyle": "bars",
361 + "fillOpacity": 0,
362 + "gradientMode": "none",
363 + "hideFrom": {
364 + "legend": false,
365 + "tooltip": false,
366 + "viz": false
367 + },
368 + "insertNulls": false,
369 + "lineInterpolation": "linear",
370 + "lineWidth": 1,
371 + "pointSize": 5,
372 + "scaleDistribution": {
373 + "type": "linear"
374 + },
375 + "showPoints": "auto",
376 + "spanNulls": false,
377 + "stacking": {
378 + "group": "A",
379 + "mode": "normal"
380 + },
381 + "thresholdsStyle": {
382 + "mode": "off"
383 + }
384 + },
385 + "mappings": [],
386 + "thresholds": {
387 + "mode": "absolute",
388 + "steps": [
389 + {
390 + "color": "green",
391 + "value": null
392 + },
393 + {
394 + "color": "red",
395 + "value": 80
396 + }
397 + ]
398 + }
399 + },
400 + "overrides": []
401 + },
402 + "gridPos": {
403 + "h": 17,
404 + "w": 16,
405 + "x": 8,
406 + "y": 0
407 + },
408 + "id": 4,
409 + "options": {
410 + "legend": {
411 + "calcs": [],
412 + "displayMode": "table",
413 + "placement": "right",
414 + "showLegend": true
415 + },
416 + "tooltip": {
417 + "mode": "single",
418 + "sort": "none"
419 + }
420 + },
421 + "pluginVersion": "11.4.0",
422 + "targets": [
423 + {
424 + "alias": "",
425 + "bucketAggs": [
426 + {
427 + "field": "json_computerDnsName",
428 + "id": "3",
429 + "settings": {
430 + "min_doc_count": "1",
431 + "order": "desc",
432 + "orderBy": "_term",
433 + "size": "10"
434 + },
435 + "type": "terms"
436 + },
437 + {
438 + "field": "timestamp",
439 + "id": "2",
440 + "settings": {
441 + "interval": "5m"
442 + },
443 + "type": "date_histogram"
444 + }
445 + ],
446 + "datasource": {
447 + "type": "grafana-opensearch-datasource",
448 + "uid": "replace_datasource_uid"
449 + },
450 + "metrics": [
451 + {
452 + "id": "1",
453 + "type": "count"
454 + }
455 + ],
456 + "query": "_exists_:json_description",
457 + "refId": "A",
458 + "timeField": "timestamp"
459 + }
460 + ],
461 + "title": "TOP 10 AGENTS - HISTOGRAM",
462 + "transparent": true,
463 + "type": "timeseries"
464 + },
465 + {
466 + "datasource": {
467 + "type": "grafana-opensearch-datasource",
468 + "uid": "replace_datasource_uid"
469 + },
470 + "fieldConfig": {
471 + "defaults": {
472 + "color": {
473 + "mode": "palette-classic"
474 + },
475 + "custom": {
476 + "hideFrom": {
477 + "legend": false,
478 + "tooltip": false,
479 + "viz": false
480 + }
481 + },
482 + "decimals": 0,
483 + "mappings": [],
484 + "unit": "short"
485 + },
486 + "overrides": [
487 + {
488 + "matcher": {
489 + "id": "byName",
490 + "options": "1"
491 + },
492 + "properties": [
493 + {
494 + "id": "color",
495 + "value": {
496 + "fixedColor": "#C8F2C2",
497 + "mode": "fixed"
498 + }
499 + }
500 + ]
501 + },
502 + {
503 + "matcher": {
504 + "id": "byName",
505 + "options": "2"
506 + },
507 + "properties": [
508 + {
509 + "id": "color",
510 + "value": {
511 + "fixedColor": "#96D98D",
512 + "mode": "fixed"
513 + }
514 + }
515 + ]
516 + },
517 + {
518 + "matcher": {
519 + "id": "byName",
520 + "options": "3"
521 + },
522 + "properties": [
523 + {
524 + "id": "color",
525 + "value": {
526 + "fixedColor": "#56A64B",
527 + "mode": "fixed"
528 + }
529 + }
530 + ]
531 + },
532 + {
533 + "matcher": {
534 + "id": "byName",
535 + "options": "4"
536 + },
537 + "properties": [
538 + {
539 + "id": "color",
540 + "value": {
541 + "fixedColor": "#37872D",
542 + "mode": "fixed"
543 + }
544 + }
545 + ]
546 + },
547 + {
548 + "matcher": {
549 + "id": "byName",
550 + "options": "5"
551 + },
552 + "properties": [
553 + {
554 + "id": "color",
555 + "value": {
556 + "fixedColor": "#FFF899",
557 + "mode": "fixed"
558 + }
559 + }
560 + ]
561 + },
562 + {
563 + "matcher": {
564 + "id": "byName",
565 + "options": "7"
566 + },
567 + "properties": [
568 + {
569 + "id": "color",
570 + "value": {
571 + "fixedColor": "#F2CC0C",
572 + "mode": "fixed"
573 + }
574 + }
575 + ]
576 + },
577 + {
578 + "matcher": {
579 + "id": "byName",
580 + "options": "9"
581 + },
582 + "properties": [
583 + {
584 + "id": "color",
585 + "value": {
586 + "fixedColor": "#FF9830",
587 + "mode": "fixed"
588 + }
589 + }
590 + ]
591 + },
592 + {
593 + "matcher": {
594 + "id": "byName",
595 + "options": "10"
596 + },
597 + "properties": [
598 + {
599 + "id": "color",
600 + "value": {
601 + "fixedColor": "#FF9830",
602 + "mode": "fixed"
603 + }
604 + }
605 + ]
606 + },
607 + {
608 + "matcher": {
609 + "id": "byName",
610 + "options": "12"
611 + },
612 + "properties": [
613 + {
614 + "id": "color",
615 + "value": {
616 + "fixedColor": "#F2495C",
617 + "mode": "fixed"
618 + }
619 + }
620 + ]
621 + },
622 + {
623 + "matcher": {
624 + "id": "byName",
625 + "options": "13"
626 + },
627 + "properties": [
628 + {
629 + "id": "color",
630 + "value": {
631 + "fixedColor": "#FF7383",
632 + "mode": "fixed"
633 + }
634 + }
635 + ]
636 + }
637 + ]
638 + },
639 + "gridPos": {
640 + "h": 10,
641 + "w": 8,
642 + "x": 0,
643 + "y": 7
644 + },
645 + "id": 5,
646 + "maxDataPoints": 3,
647 + "options": {
648 + "displayLabels": [],
649 + "legend": {
650 + "calcs": [],
651 + "displayMode": "table",
652 + "placement": "right",
653 + "showLegend": true,
654 + "values": [
655 + "value",
656 + "percent"
657 + ]
658 + },
659 + "pieType": "donut",
660 + "reduceOptions": {
661 + "calcs": [
662 + "sum"
663 + ],
664 + "fields": "",
665 + "values": false
666 + },
667 + "text": {},
668 + "tooltip": {
669 + "mode": "single",
670 + "sort": "none"
671 + }
672 + },
673 + "pluginVersion": "11.4.0",
674 + "targets": [
675 + {
676 + "bucketAggs": [
677 + {
678 + "$$hashKey": "object:235",
679 + "fake": true,
680 + "field": "json_evidence_entityType",
681 + "id": "3",
682 + "settings": {
683 + "min_doc_count": 1,
684 + "order": "desc",
685 + "orderBy": "_count",
686 + "size": "10"
687 + },
688 + "type": "terms"
689 + },
690 + {
691 + "$$hashKey": "object:236",
692 + "field": "timestamp",
693 + "id": "2",
694 + "settings": {
695 + "interval": "auto",
696 + "min_doc_count": 0,
697 + "trimEdges": 0
698 + },
699 + "type": "date_histogram"
700 + }
701 + ],
702 + "datasource": {
703 + "type": "grafana-opensearch-datasource",
704 + "uid": "replace_datasource_uid"
705 + },
706 + "metrics": [
707 + {
708 + "$$hashKey": "object:233",
709 + "field": "select field",
710 + "id": "1",
711 + "meta": {},
712 + "settings": {},
713 + "type": "count"
714 + }
715 + ],
716 + "query": "_exists_:json_description",
717 + "refId": "A",
718 + "timeField": "timestamp"
719 + }
720 + ],
721 + "title": "EVENTS BY TYPE",
722 + "type": "piechart"
723 + },
724 + {
725 + "datasource": {
726 + "type": "grafana-opensearch-datasource",
727 + "uid": "replace_datasource_uid"
728 + },
729 + "fieldConfig": {
730 + "defaults": {
731 + "color": {
732 + "mode": "thresholds"
733 + },
734 + "custom": {
735 + "align": "auto",
736 + "cellOptions": {
737 + "type": "auto"
738 + },
739 + "filterable": true,
740 + "inspect": false
741 + },
742 + "mappings": [],
743 + "thresholds": {
744 + "mode": "absolute",
745 + "steps": [
746 + {
747 + "color": "green",
748 + "value": null
749 + },
750 + {
751 + "color": "red",
752 + "value": 80
753 + }
754 + ]
755 + }
756 + },
757 + "overrides": [
758 + {
759 + "matcher": {
760 + "id": "byName",
761 + "options": "EVENT ID"
762 + },
763 + "properties": [
764 + {
765 + "id": "links",
766 + "value": [
767 + {
768 + "targetBlank": true,
769 + "title": "EVENT DETAILS",
770 + "url": "https://grafana.morcan.sec/explore?left=%7B%22datasource%22:%22DFE%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22query%22:%22_id:${__value.text}%22,%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D"
771 + }
772 + ]
773 + }
774 + ]
775 + },
776 + {
777 + "matcher": {
778 + "id": "byName",
779 + "options": "COMPUTER"
780 + },
781 + "properties": [
782 + {
783 + "id": "custom.width",
784 + "value": 284
785 + }
786 + ]
787 + },
788 + {
789 + "matcher": {
790 + "id": "byName",
791 + "options": "USER"
792 + },
793 + "properties": [
794 + {
795 + "id": "custom.width",
796 + "value": 129
797 + }
798 + ]
799 + },
800 + {
801 + "matcher": {
802 + "id": "byName",
803 + "options": "ENTITY TYPE"
804 + },
805 + "properties": [
806 + {
807 + "id": "custom.width",
808 + "value": 137
809 + }
810 + ]
811 + }
812 + ]
813 + },
814 + "gridPos": {
815 + "h": 17,
816 + "w": 24,
817 + "x": 0,
818 + "y": 17
819 + },
820 + "id": 1,
821 + "options": {
822 + "cellHeight": "sm",
823 + "footer": {
824 + "countRows": false,
825 + "enablePagination": true,
826 + "fields": "",
827 + "reducer": [
828 + "sum"
829 + ],
830 + "show": false
831 + },
832 + "showHeader": true,
833 + "sortBy": []
834 + },
835 + "pluginVersion": "11.4.0",
836 + "targets": [
837 + {
838 + "alias": "",
839 + "bucketAggs": [],
840 + "datasource": {
841 + "type": "grafana-opensearch-datasource",
842 + "uid": "replace_datasource_uid"
843 + },
844 + "format": "table",
845 + "metrics": [
846 + {
847 + "id": "1",
848 + "settings": {
849 + "order": "desc",
850 + "size": "500",
851 + "useTimeRange": true
852 + },
853 + "type": "raw_data"
854 + }
855 + ],
856 + "query": "_exists_:json_description",
857 + "queryType": "lucene",
858 + "refId": "A",
859 + "timeField": "timestamp"
860 + }
861 + ],
862 + "title": "EVENTS",
863 + "transformations": [
864 + {
865 + "id": "filterFieldsByName",
866 + "options": {
867 + "include": {
868 + "names": [
869 + "timestamp",
870 + "_id",
871 + "json_computerDnsName",
872 + "json_description",
873 + "json_detectionSource",
874 + "json_evidence_detectionStatus",
875 + "json_evidence_entityType",
876 + "json_evidence_fileName",
877 + "json_evidence_filePath",
878 + "json_investigationState",
879 + "json_mitreTechniques",
880 + "json_severity",
881 + "json_threatFamilyName",
882 + "json_threatName",
883 + "json_title",
884 + "json_loggedOnUsers_0_accountName"
885 + ]
886 + }
887 + }
888 + },
889 + {
890 + "id": "organize",
891 + "options": {
892 + "excludeByName": {},
893 + "includeByName": {},
894 + "indexByName": {
895 + "_id": 1,
896 + "json_computerDnsName": 2,
897 + "json_description": 4,
898 + "json_detectionSource": 8,
899 + "json_evidence_detectionStatus": 9,
900 + "json_evidence_entityType": 10,
901 + "json_evidence_fileName": 11,
902 + "json_evidence_filePath": 12,
903 + "json_investigationState": 13,
904 + "json_loggedOnUsers_0_accountName": 3,
905 + "json_mitreTechniques": 14,
906 + "json_severity": 15,
907 + "json_threatFamilyName": 6,
908 + "json_threatName": 5,
909 + "json_title": 7,
910 + "timestamp": 0
911 + },
912 + "renameByName": {
913 + "_id": "EVENT ID",
914 + "json_computerDnsName": "COMPUTER",
915 + "json_description": "DESCRIPTION",
916 + "json_detectionSource": "DETECTION SOURCE",
917 + "json_evidence_detectionStatus": "STATUS",
918 + "json_evidence_entityType": "ENTITY TYPE",
919 + "json_evidence_fileName": "FILE NAME",
920 + "json_evidence_filePath": "PATH",
921 + "json_evidence_url": "",
922 + "json_investigationState": "STATE",
923 + "json_loggedOnUsers": "USER",
924 + "json_loggedOnUsers_0_accountName": "USER",
925 + "json_mitreTechniques": "TTPs",
926 + "json_severity": "SEVERITY",
927 + "json_threatFamilyName": "FAMILY",
928 + "json_threatName": "THREAT",
929 + "json_title": "TITLE",
930 + "timestamp": "DATE/TIME"
931 + }
932 + }
933 + }
934 + ],
935 + "type": "table"
936 + }
937 + ],
938 + "preload": false,
939 + "schemaVersion": 40,
940 + "tags": [],
941 + "templating": {
942 + "list": []
943 + },
944 + "time": {
945 + "from": "now-7d",
946 + "to": "now"
947 + },
948 + "timepicker": {},
949 + "timezone": "browser",
950 + "title": "EDR - MS DFE _SUMMARY",
951 + "version": 2,
952 + "weekStart": ""
953 +}
backend/app/connectors/grafana/schema/dashboards.py
+5
@@ -114,6 +114,10 @@ class CatoDashboard(Enum):
114 CATO_SUMMARY = ("Cato", "summary.json")
115
116
117 +class DefenderForEndpointDashboard(Enum):
118 + DEFENDERFORENDPOINT_SUMMARY = ("DefenderForEndpoint", "summary.json")
119 +
120 +
121 class DashboardProvisionRequest(BaseModel):
122 dashboards: List[str] = Field(
123 ...,
@@ -149,6 +153,7 @@ class DashboardProvisionRequest(BaseModel):
153 + list(DarktraceDashboard)
154 + list(BitdefenderDashboard)
155 + list(CatoDashboard)
156 + + list(DefenderForEndpointDashboard)
157 }
158 if e not in valid_dashboards:
159 raise ValueError(f'Dashboard identifier "{e}" is not recognized.')
backend/app/connectors/grafana/services/dashboards.py
+2
@@ -10,6 +10,7 @@ from app.connectors.grafana.schema.dashboards import CatoDashboard
10 from app.connectors.grafana.schema.dashboards import CrowdstrikeDashboard
11 from app.connectors.grafana.schema.dashboards import DarktraceDashboard
12 from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
13 +from app.connectors.grafana.schema.dashboards import DefenderForEndpointDashboard
14 from app.connectors.grafana.schema.dashboards import DuoDashboard
15 from app.connectors.grafana.schema.dashboards import FortinetDashboard
16 from app.connectors.grafana.schema.dashboards import GrafanaDashboard
@@ -189,6 +190,7 @@ async def provision_dashboards(
190 + list(DarktraceDashboard)
191 + list(BitdefenderDashboard)
192 + list(CatoDashboard)
193 + + list(DefenderForEndpointDashboard)
194 }
195
196 for dashboard_name in dashboard_request.dashboards:
backend/app/connectors/velociraptor/schema/artifacts.py
+25
@@ -3,6 +3,7 @@ from typing import Any
3 from typing import Dict
4 from typing import List
5 from typing import Optional
6 +from typing import Union
7
8 from fastapi import HTTPException
9 from pydantic import BaseModel
@@ -72,6 +73,13 @@ class QuarantineArtifactsEnum(str, Enum):
73 linux_quarantine = "Linux.Remediation.Quarantine"
74
75
76 +class ParameterKeyValue(BaseModel):
77 + """Represents a key-value pair for artifact parameters."""
78 +
79 + key: str = Field(..., description="Parameter key/name")
80 + value: str = Field(..., description="Parameter value")
81 +
82 +
83 class BaseBody(BaseModel):
84 hostname: str = Field(..., description="Name of the client")
85 velociraptor_id: Optional[str] = Field(None, description="Client ID of the client")
@@ -79,10 +87,27 @@ class BaseBody(BaseModel):
87
88
89 class CollectArtifactBody(BaseBody):
90 + """Request body for collecting artifacts with optional parameters."""
91 +
92 artifact_name: Optional[str] = Field(
93 None,
94 description="Name of the artifact for collection or command running",
95 )
96 + parameters: Optional[Dict[str, Union[str, List[ParameterKeyValue]]]] = Field(
97 + None,
98 + description="Optional parameters for the artifact, such as environment variables",
99 + )
100 +
101 + class Config:
102 + schema_extra = {
103 + "example": {
104 + "hostname": "WIN-HFOU106TD7K",
105 + "velociraptor_id": "C.475df76785008b04",
106 + "velociraptor_org": "root",
107 + "artifact_name": "Windows.AttackSimulation.AtomicRedTeam",
108 + "parameters": {"env": [{"key": "InstallART", "value": "N"}, {"key": "T1552.001 - 3", "value": "Y"}]},
109 + },
110 + }
111
112
113 class CollectFileBody(BaseBody):
backend/app/connectors/velociraptor/services/artifacts.py
+88 -15
@@ -34,16 +34,14 @@ def get_artifact_key(analyzer_body: CollectArtifactBody) -> str:
34 Construct the artifact key.
35
36 Args:
37 - client_id (str): The ID of the client.
38 - artifact (str): The name of the artifact.
39 - command (str): The command that was run, if applicable.
40 - quarantined (bool): Whether the client is quarantined or not.
37 + analyzer_body: The collector body with artifact details
38
39 Returns:
40 str: The constructed artifact key.
41 """
42 action = getattr(analyzer_body, "action", None)
43 command = getattr(analyzer_body, "command", None)
44 + parameters = getattr(analyzer_body, "parameters", None)
45
46 if action == "quarantine":
47 return (
@@ -63,6 +61,12 @@ def get_artifact_key(analyzer_body: CollectArtifactBody) -> str:
61 f"urgent=true, artifacts=['{analyzer_body.artifact_name}'], "
62 f"env=dict(Command='{analyzer_body.command}'))"
63 )
64 + elif parameters is not None:
65 + # Parameters are provided, will be included in the query
66 + return (
67 + f"collect_client(org_id='{analyzer_body.velociraptor_org}', client_id='{analyzer_body.velociraptor_id}', "
68 + f"artifacts=['{analyzer_body.artifact_name}'])"
69 + )
70 else:
71 return (
72 f"collect_client(org_id='{analyzer_body.velociraptor_org}', client_id='{analyzer_body.velociraptor_id}', "
@@ -106,32 +110,101 @@ async def run_artifact_collection(
110 collect_artifact_body: CollectArtifactBody,
111 ) -> CollectArtifactResponse:
112 """
109 - Run an artifact collection on a client.
113 + Run an artifact collection on a client with optional parameters.
114
115 Args:
112 - run_analyzer_body (RunAnalyzerBody): The body of the request.
116 + collect_artifact_body: The body of the request with optional parameters.
117
118 Returns:
115 - RunAnalyzerResponse: A dictionary containing the success status and a message.
119 + CollectArtifactResponse: A dictionary containing the success status, message and results.
120 """
121 velociraptor_service = await UniversalService.create("Velociraptor")
122 try:
119 - # ! Can specify org_id with org_id='OL680' ! #
120 - query = create_query(
121 - (
123 + # Build the query dynamically based on whether parameters are provided
124 + parameters = getattr(collect_artifact_body, "parameters", None)
125 +
126 + if parameters:
127 + # Velociraptor expects parameters in a very specific format
128 + # For the "env" parameter, we need to construct a dict
129 + if "env" in parameters and isinstance(parameters["env"], list):
130 + env_dict = {}
131 + for item in parameters["env"]:
132 + env_dict[item.key] = item.value
133 +
134 + # Format the query with proper VQL syntax
135 + query = create_query(
136 + f"SELECT collect_client("
137 + f"org_id='{collect_artifact_body.velociraptor_org}', "
138 + f"client_id='{collect_artifact_body.velociraptor_id}', "
139 + f"artifacts=['{collect_artifact_body.artifact_name}'], "
140 + f"env=dict(",
141 + )
142 +
143 + # Add each environment variable as a key-value pair
144 + env_parts = []
145 + for key, value in env_dict.items():
146 + # Escape any single quotes in the values
147 + escaped_value = value.replace("'", "\\'")
148 + env_parts.append(f"`{key}`='{escaped_value}'")
149 +
150 + query += ", ".join(env_parts)
151 + query += ")) FROM scope()"
152 + else:
153 + # Handle other types of parameters
154 + query = create_query(
155 + f"SELECT collect_client("
156 + f"org_id='{collect_artifact_body.velociraptor_org}', "
157 + f"client_id='{collect_artifact_body.velociraptor_id}', "
158 + f"artifacts=['{collect_artifact_body.artifact_name}']",
159 + )
160 +
161 + # Add other parameters if needed
162 + for param_key, param_value in parameters.items():
163 + if isinstance(param_value, str):
164 + query += f", `{param_key}`='{param_value}'"
165 +
166 + query += ") FROM scope()"
167 + else:
168 + # Original query without parameters
169 + query = create_query(
170 f"SELECT collect_client("
171 f"org_id='{collect_artifact_body.velociraptor_org}', "
172 f"client_id='{collect_artifact_body.velociraptor_id}', "
173 f"artifacts=['{collect_artifact_body.artifact_name}']) "
126 - f"FROM scope()"
127 - ),
128 - )
174 + f"FROM scope()",
175 + )
176 +
177 + logger.info(f"Running artifact collection with query: {query}")
178 flow = velociraptor_service.execute_query(query, org_id=collect_artifact_body.velociraptor_org)
179 logger.info(f"Successfully ran artifact collection on {flow}")
180
132 - artifact_key = get_artifact_key(analyzer_body=collect_artifact_body)
181 + # Check if results are available
182 + if not flow.get("results") or len(flow["results"]) == 0:
183 + logger.error("No results returned from query execution")
184 + raise HTTPException(
185 + status_code=500,
186 + detail="Query execution did not return any results",
187 + )
188 +
189 + # Instead of relying on get_artifact_key, extract the flow_id directly from results
190 + # by checking all keys in the first result for a flow_id
191 + result_dict = flow["results"][0]
192 + flow_id = None
193 +
194 + # Look for any key that has a flow_id in its value
195 + for key, value in result_dict.items():
196 + if isinstance(value, dict) and "flow_id" in value:
197 + flow_id = value["flow_id"]
198 + logger.debug(f"Found flow_id {flow_id} in key: {key}")
199 + break
200 +
201 + if not flow_id:
202 + logger.error(f"Could not find flow_id in results: {result_dict}")
203 + raise HTTPException(
204 + status_code=500,
205 + detail="Failed to extract flow ID from results",
206 + )
207
134 - flow_id = flow["results"][0][artifact_key]["flow_id"]
208 logger.info(f"Extracted flow_id: {flow_id}")
209
210 completed = velociraptor_service.watch_flow_completion(flow_id, org_id=collect_artifact_body.velociraptor_org)
backend/app/connectors/wazuh_manager/routes/mitre.py new
+122
@@ -0,0 +1,122 @@
1 +# App specific imports
2 +from typing import List
3 +from typing import Optional
4 +
5 +from fastapi import APIRouter
6 +from fastapi import Path
7 +from fastapi import Query
8 +from fastapi import Security
9 +from loguru import logger
10 +
11 +from app.auth.routes.auth import AuthHandler
12 +from app.connectors.wazuh_manager.schema.mitre import AtomicRedTeamMarkdownResponse
13 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTacticsResponse
14 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTechniquesResponse
15 +from app.connectors.wazuh_manager.services.mitre import AtomicRedTeamService
16 +from app.connectors.wazuh_manager.services.mitre import get_mitre_tactics
17 +from app.connectors.wazuh_manager.services.mitre import get_mitre_techniques
18 +
19 +# Initialize router and auth handler
20 +wazuh_manager_mitre_router = APIRouter()
21 +auth_handler = AuthHandler()
22 +
23 +
24 +@wazuh_manager_mitre_router.get(
25 + "/tactics",
26 + response_model=WazuhMitreTacticsResponse,
27 + description="List MITRE ATT&CK tactics",
28 + dependencies=[Security(auth_handler.require_any_scope("admin", "analyst"))],
29 +)
30 +async def list_mitre_tactics(
31 + limit: Optional[int] = Query(None, description="Maximum number of items to return"),
32 + offset: Optional[int] = Query(None, description="First item to return"),
33 + select: Optional[List[str]] = Query(None, description="List of fields to return"),
34 + sort: Optional[str] = Query(None, description="Fields to sort by"),
35 + search: Optional[str] = Query(None, description="Text to search in fields"),
36 + q: Optional[str] = Query(None, description="Query to filter results"),
37 +):
38 + """
39 + List MITRE ATT&CK tactics with optional filtering parameters.
40 +
41 + Args:
42 + limit: Maximum number of items to return
43 + offset: First item to return
44 + select: List of fields to return
45 + sort: Fields to sort by
46 + search: Text to search in fields
47 + q: Query to filter results
48 +
49 + Returns:
50 + WazuhMitreTacticsResponse: A list of MITRE ATT&CK tactics matching the criteria.
51 + """
52 + return await get_mitre_tactics(limit=limit, offset=offset, select=select, sort=sort, search=search, q=q)
53 +
54 +
55 +@wazuh_manager_mitre_router.get(
56 + "/techniques",
57 + response_model=WazuhMitreTechniquesResponse,
58 + description="List MITRE ATT&CK techniques",
59 + dependencies=[Security(auth_handler.require_any_scope("admin", "analyst"))],
60 +)
61 +async def list_mitre_techniques(
62 + limit: Optional[int] = Query(None, description="Maximum number of items to return"),
63 + offset: Optional[int] = Query(None, description="First item to return"),
64 + select: Optional[List[str]] = Query(None, description="List of fields to return"),
65 + sort: Optional[str] = Query(None, description="Fields to sort by"),
66 + search: Optional[str] = Query(None, description="Text to search in fields"),
67 + q: Optional[str] = Query(None, description="Query to filter results"),
68 +):
69 + """
70 + List MITRE ATT&CK techniques with optional filtering parameters.
71 +
72 + Args:
73 + limit: Maximum number of items to return
74 + offset: First item to return
75 + select: List of fields to return
76 + sort: Fields to sort by
77 + search: Text to search in fields
78 + q: Query to filter results
79 +
80 + Returns:
81 + WazuhMitreTechniquesResponse: A list of MITRE ATT&CK techniques matching the criteria.
82 + """
83 + return await get_mitre_techniques(limit=limit, offset=offset, select=select, sort=sort, search=search, q=q)
84 +
85 +
86 +@wazuh_manager_mitre_router.get(
87 + "/techniques/{technique_id}/atomic-tests",
88 + response_model=AtomicRedTeamMarkdownResponse,
89 + description="Get Atomic Red Team tests for a MITRE ATT&CK technique",
90 + dependencies=[Security(auth_handler.require_any_scope("admin", "analyst"))],
91 +)
92 +async def get_technique_atomic_tests(technique_id: str = Path(..., description="MITRE ATT&CK technique ID (e.g., T1003, T1003.004)")):
93 + """
94 + Get Atomic Red Team tests for a specific MITRE ATT&CK technique.
95 +
96 + Args:
97 + technique_id: The MITRE ATT&CK technique ID
98 +
99 + Returns:
100 + AtomicRedTeamMarkdownResponse: The Atomic Red Team tests for the technique
101 + """
102 + logger.info(f"Request for Atomic Red Team tests for technique {technique_id}")
103 +
104 + # Extract the technique ID from the full ID if needed (e.g., "T1003.004" -> "T1003.004")
105 + clean_technique_id = technique_id.split("-")[-1] if "-" in technique_id else technique_id
106 +
107 + # Fetch the markdown content
108 + markdown_content = await AtomicRedTeamService.get_technique_markdown(clean_technique_id)
109 +
110 + if markdown_content is None:
111 + return AtomicRedTeamMarkdownResponse(
112 + success=False,
113 + message=f"No Atomic Red Team tests found for technique {technique_id}",
114 + technique_id=clean_technique_id,
115 + )
116 +
117 + return AtomicRedTeamMarkdownResponse(
118 + success=True,
119 + message=f"Atomic Red Team tests retrieved for technique {technique_id}",
120 + technique_id=clean_technique_id,
121 + markdown_content=markdown_content,
122 + )
backend/app/connectors/wazuh_manager/schema/mitre.py new
+121
@@ -0,0 +1,121 @@
1 +from typing import Any
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +
6 +from pydantic import BaseModel
7 +
8 +
9 +class MitreTacticItem(BaseModel):
10 + """Represents a single MITRE ATT&CK tactic from Wazuh's API."""
11 +
12 + description: str
13 + name: str
14 + id: str
15 + modified_time: str
16 + created_time: str
17 + short_name: str
18 + techniques: List[str]
19 + references: List[str] = []
20 + url: str
21 + source: str
22 + external_id: str
23 +
24 +
25 +class MitreFailedItem(BaseModel):
26 + """Represents a failed item in the Wazuh API response."""
27 +
28 + error: Dict[str, Any]
29 + id: str
30 +
31 +
32 +class MitreResponseData(BaseModel):
33 + """Represents the data section of the Wazuh MITRE response."""
34 +
35 + affected_items: List[MitreTacticItem]
36 + total_affected_items: int
37 + total_failed_items: int
38 + failed_items: List[MitreFailedItem] = []
39 +
40 +
41 +class MitreAPIResponse(BaseModel):
42 + """Base response model for Wazuh API responses related to MITRE data."""
43 +
44 + data: MitreResponseData
45 + message: str
46 + error: int
47 +
48 +
49 +# Response models for API endpoints
50 +class WazuhMitreTacticsResponse(BaseModel):
51 + """Response model for the MITRE tactics endpoint."""
52 +
53 + success: bool
54 + message: str
55 + results: List[MitreTacticItem] = []
56 +
57 +
58 +# First, add a model for references
59 +class MitreReference(BaseModel):
60 + """Represents a reference in MITRE ATT&CK data."""
61 +
62 + url: str
63 + description: Optional[str] = None
64 + source: str
65 +
66 +
67 +# Then update the MitreTechniqueItem model
68 +class MitreTechniqueItem(BaseModel):
69 + """Represents a single MITRE ATT&CK technique from Wazuh's API."""
70 +
71 + description: str
72 + name: str
73 + id: str
74 + modified_time: str
75 + created_time: str
76 + tactics: List[str]
77 + url: str
78 + source: str
79 + external_id: str
80 +
81 + # Fields that might have different structure
82 + references: List[MitreReference] = []
83 + mitigations: Optional[List[str]] = None
84 + subtechnique_of: Optional[str] = None
85 +
86 + # Optional fields from the API response
87 + techniques: Optional[List[str]] = None # For sub-techniques
88 + groups: Optional[List[str]] = []
89 + software: Optional[List[str]] = []
90 + mitre_detection: Optional[str] = None
91 + mitre_version: Optional[str] = None
92 + deprecated: Optional[int] = 0
93 + remote_support: Optional[int] = 0
94 + network_requirements: Optional[int] = 0
95 +
96 + # Fields that we standardize in our model but might not be in the response
97 + platforms: List[str] = []
98 + data_sources: List[str] = []
99 + is_subtechnique: bool = False
100 +
101 + class Config:
102 + """Configuration for the model."""
103 +
104 + extra = "ignore" # Ignore extra fields from the API
105 +
106 +
107 +class WazuhMitreTechniquesResponse(BaseModel):
108 + """Response model for the MITRE techniques endpoint."""
109 +
110 + success: bool
111 + message: str
112 + results: List[MitreTechniqueItem] = []
113 +
114 +
115 +class AtomicRedTeamMarkdownResponse(BaseModel):
116 + """Response model for Atomic Red Team markdown content."""
117 +
118 + success: bool
119 + message: str
120 + technique_id: str
121 + markdown_content: Optional[str] = None
backend/app/connectors/wazuh_manager/services/mitre.py new
+212
@@ -0,0 +1,212 @@
1 +import time
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +from typing import Tuple
6 +
7 +import aiohttp
8 +from fastapi import HTTPException
9 +from loguru import logger
10 +from pydantic import ValidationError
11 +
12 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTacticsResponse
13 +from app.connectors.wazuh_manager.schema.mitre import WazuhMitreTechniquesResponse
14 +from app.connectors.wazuh_manager.utils.universal import send_get_request
15 +
16 +# Constants for the Atomic Red Team GitHub repository
17 +GITHUB_RAW_URL = "https://raw.githubusercontent.com/redcanaryco/atomic-red-team/refs/heads/master/atomics"
18 +CACHE_EXPIRY = 86400 # Cache expiry time in seconds (24 hours)
19 +
20 +
21 +class AtomicRedTeamService:
22 + """Service for fetching Atomic Red Team markdown content."""
23 +
24 + # Cache to store the markdown content with timestamp
25 + # Format: {technique_id: (markdown_content, timestamp)}
26 + _cache: Dict[str, Tuple[str, float]] = {}
27 +
28 + @classmethod
29 + async def get_technique_markdown(cls, technique_id: str) -> Optional[str]:
30 + """
31 + Get the markdown content for a given MITRE ATT&CK technique ID.
32 +
33 + Args:
34 + technique_id: The MITRE ATT&CK technique ID (e.g., T1003, T1003.004)
35 +
36 + Returns:
37 + The markdown content or None if not found
38 + """
39 + # Check the cache first
40 + if technique_id in cls._cache:
41 + content, timestamp = cls._cache[technique_id]
42 + if time.time() - timestamp < CACHE_EXPIRY:
43 + logger.debug(f"Returning cached markdown for {technique_id}")
44 + return content
45 +
46 + # Construct the URL for the raw markdown file
47 + url = f"{GITHUB_RAW_URL}/{technique_id}/{technique_id}.md"
48 +
49 + logger.info(f"Fetching Atomic Red Team markdown from {url}")
50 +
51 + try:
52 + async with aiohttp.ClientSession() as session:
53 + async with session.get(url) as response:
54 + if response.status == 200:
55 + content = await response.text()
56 + # Store in cache with current timestamp
57 + cls._cache[technique_id] = (content, time.time())
58 + return content
59 + elif response.status == 404:
60 + logger.warning(f"Atomic Red Team markdown not found for technique {technique_id}")
61 + return None
62 + else:
63 + logger.error(f"Failed to fetch markdown for {technique_id}: {response.status}")
64 + return None
65 + except aiohttp.ClientError as e:
66 + logger.error(f"Error fetching markdown for {technique_id}: {str(e)}")
67 + return None
68 + except Exception as e:
69 + logger.error(f"Unexpected error fetching markdown for {technique_id}: {str(e)}")
70 + return None
71 +
72 + @classmethod
73 + def clear_cache(cls, technique_id: Optional[str] = None) -> None:
74 + """
75 + Clear the cache for a specific technique or all techniques.
76 +
77 + Args:
78 + technique_id: The MITRE ATT&CK technique ID to clear, or None to clear all
79 + """
80 + if technique_id:
81 + if technique_id in cls._cache:
82 + del cls._cache[technique_id]
83 + logger.info(f"Cleared cache for {technique_id}")
84 + else:
85 + cls._cache.clear()
86 + logger.info("Cleared all cached Atomic Red Team markdown content")
87 +
88 +
89 +async def get_mitre_tactics(
90 + limit: Optional[int] = None,
91 + offset: Optional[int] = None,
92 + select: Optional[List[str]] = None,
93 + sort: Optional[str] = None,
94 + search: Optional[str] = None,
95 + q: Optional[str] = None,
96 +) -> WazuhMitreTacticsResponse:
97 + """
98 + Fetch MITRE ATT&CK tactics from Wazuh API.
99 +
100 + Args:
101 + limit: Maximum number of items to return
102 + offset: First item to return
103 + select: List of fields to return
104 + sort: Fields to sort by
105 + search: Text to search in fields
106 + q: Query to filter results
107 +
108 + Returns:
109 + WazuhMitreTacticsResponse: A list of all MITRE ATT&CK tactics.
110 + """
111 + # Build parameters dictionary, excluding None values
112 + params = {"limit": limit, "offset": offset, "sort": sort, "search": search, "q": q}
113 +
114 + # Add select parameter if provided
115 + if select:
116 + params["select"] = ",".join(select)
117 +
118 + # Remove None values
119 + params = {k: v for k, v in params.items() if v is not None}
120 +
121 + response = await send_get_request(endpoint="/mitre/tactics", params=params)
122 +
123 + logger.debug(f"Response from Wazuh MITRE tactics endpoint with params {params}")
124 +
125 + try:
126 + # Extract data from response
127 + if "data" in response and "data" in response["data"]:
128 + wazuh_data = response["data"]["data"]
129 + mitre_tactics = wazuh_data.get("affected_items", [])
130 + total_items = wazuh_data.get("total_affected_items", len(mitre_tactics))
131 +
132 + logger.debug(f"Retrieved {len(mitre_tactics)} of {total_items} MITRE tactics from Wazuh")
133 +
134 + return WazuhMitreTacticsResponse(
135 + success=True,
136 + message=f"Successfully retrieved {len(mitre_tactics)} MITRE tactics",
137 + results=mitre_tactics,
138 + )
139 + else:
140 + logger.error("Unexpected response structure from Wazuh API")
141 + raise HTTPException(status_code=500, detail="Unexpected response structure from Wazuh API")
142 +
143 + except Exception as e:
144 + logger.error(f"Error parsing Wazuh MITRE tactics response: {e}")
145 + raise HTTPException(status_code=500, detail=f"Error processing MITRE data: {str(e)}")
146 +
147 +
148 +async def get_mitre_techniques(
149 + limit: Optional[int] = None,
150 + offset: Optional[int] = None,
151 + select: Optional[List[str]] = None,
152 + sort: Optional[str] = None,
153 + search: Optional[str] = None,
154 + q: Optional[str] = None,
155 +) -> WazuhMitreTechniquesResponse:
156 + """
157 + Fetch MITRE ATT&CK techniques from Wazuh API.
158 +
159 + Args:
160 + limit: Maximum number of items to return
161 + offset: First item to return
162 + select: List of fields to return
163 + sort: Fields to sort by
164 + search: Text to search in fields
165 + q: Query to filter results
166 +
167 + Returns:
168 + WazuhMitreTechniquesResponse: A list of all MITRE ATT&CK techniques.
169 + """
170 + # Build parameters dictionary, excluding None values
171 + params = {"limit": limit, "offset": offset, "sort": sort, "search": search, "q": q}
172 +
173 + # Add select parameter if provided
174 + if select:
175 + params["select"] = ",".join(select)
176 +
177 + # Remove None values
178 + params = {k: v for k, v in params.items() if v is not None}
179 +
180 + response = await send_get_request(endpoint="/mitre/techniques", params=params)
181 +
182 + logger.debug(f"Response from Wazuh MITRE techniques endpoint with params {params}")
183 +
184 + try:
185 + # Extract data from response
186 + if "data" in response and "data" in response["data"]:
187 + wazuh_data = response["data"]["data"]
188 + mitre_techniques = wazuh_data.get("affected_items", [])
189 + total_items = wazuh_data.get("total_affected_items", len(mitre_techniques))
190 +
191 + # Process each technique to set is_subtechnique based on subtechnique_of
192 + for technique in mitre_techniques:
193 + if "subtechnique_of" in technique and technique["subtechnique_of"]:
194 + technique["is_subtechnique"] = True
195 +
196 + logger.debug(f"Retrieved {len(mitre_techniques)} of {total_items} MITRE techniques from Wazuh")
197 +
198 + return WazuhMitreTechniquesResponse(
199 + success=True,
200 + message=f"Successfully retrieved {len(mitre_techniques)} MITRE techniques",
201 + results=mitre_techniques,
202 + )
203 + else:
204 + logger.error("Unexpected response structure from Wazuh API")
205 + raise HTTPException(status_code=500, detail="Unexpected response structure from Wazuh API")
206 +
207 + except ValidationError as e:
208 + logger.error(f"Validation error parsing Wazuh MITRE techniques response: {e}")
209 + raise HTTPException(status_code=500, detail=f"Data validation error: {str(e)}")
210 + except Exception as e:
211 + logger.error(f"Error parsing Wazuh MITRE techniques response: {e}")
212 + raise HTTPException(status_code=500, detail=f"Error processing MITRE data: {str(e)}")
backend/app/db/db_populate.py
+5
@@ -332,6 +332,7 @@ def get_available_integrations_list():
332 ("Darktrace", "Integrate Darktrace with SOCFortress."),
333 ("BitDefender", "Integrate BitDefender with SOCFortress."),
334 ("CATO", "Integrate CATO NETWORKS with SOCFortress."),
335 + ("DefenderForEndpoint", "Integrate DefenderForEndpoint with SOCFortress."),
336 # ... Add more available integrations as needed ...
337 ]
338
@@ -470,6 +471,10 @@ async def get_available_integrations_auth_keys_list(session: AsyncSession):
471 ("CATO", "ACCOUNT_ID"),
472 ("CATO", "EVENT_TYPES"),
473 ("CATO", "EVENT_SUB_TYPES"),
474 + ("DefenderForEndpoint", "TENANT_ID"),
475 + ("DefenderForEndpoint", "CLIENT_ID"),
476 + ("DefenderForEndpoint", "CLIENT_SECRET"),
477 + ("DefenderForEndpoint", "SYSLOG_PORT"),
478 # ... Add more available integrations auth keys as needed ...
479 ]
480 logger.info("Getting available integrations auth keys.")
backend/app/incidents/models.py
+33
@@ -207,3 +207,36 @@ class CaseReportTemplateDataStore(SQLModel, table=True):
207 file_size: Optional[int] = Field(nullable=True) # File size in bytes
208 upload_time: datetime = Field(default_factory=datetime.utcnow) # Time of upload
209 file_hash: str = Field(max_length=128, nullable=False) # Hash of the file (e.g., SHA-256)
210 +
211 +
212 +class VeloSigmaExclusion(SQLModel, table=True):
213 + """Exclusion rules for Velociraptor Sigma alerts."""
214 +
215 + __tablename__ = "incident_management_velo_sigma_exclusion"
216 +
217 + id: Optional[int] = Field(default=None, primary_key=True)
218 + name: str = Field(max_length=255, nullable=False, description="Friendly name for this exclusion rule")
219 + description: Optional[str] = Field(sa_column=Text, nullable=True, description="Description of why this exclusion exists")
220 +
221 + # Core matching criteria
222 + channel: Optional[str] = Field(max_length=255, nullable=True, description="Windows event channel to match (exact match)")
223 + title: Optional[str] = Field(max_length=255, nullable=True, description="Sigma rule title to match (exact match)")
224 +
225 + # Field matching data - stored as JSON to allow flexible field matching
226 + field_matches: Optional[Dict] = Field(
227 + sa_column=Column(JSON),
228 + nullable=True,
229 + description="JSON of field names and values to match in the event data",
230 + )
231 +
232 + # Metadata
233 + customer_code: Optional[str] = Field(
234 + max_length=50,
235 + nullable=True,
236 + description="Customer code this exclusion applies to (null means all customers)",
237 + )
238 + created_by: str = Field(max_length=100, nullable=False, description="User who created this exclusion")
239 + created_at: datetime = Field(default_factory=datetime.utcnow, description="When this exclusion was created")
240 + last_matched_at: Optional[datetime] = Field(nullable=True, description="When this exclusion last matched an alert")
241 + match_count: int = Field(default=0, description="How many times this exclusion has matched")
242 + enabled: bool = Field(default=True, description="Whether this exclusion is active")
backend/app/incidents/routes/incident_alert.py
+156
@@ -4,6 +4,7 @@ from fastapi import APIRouter
4 from fastapi import Depends
5 from fastapi import Header
6 from fastapi import HTTPException
7 +from fastapi import Query
8 from fastapi import Security
9 from loguru import logger
10 from sqlalchemy.ext.asyncio import AsyncSession
@@ -23,6 +24,10 @@ from app.incidents.schema.incident_alert import CreatedAlertPayload
24 from app.incidents.schema.incident_alert import IndexNamesResponse
25 from app.incidents.schema.velo_sigma import VelociraptorSigmaAlert
26 from app.incidents.schema.velo_sigma import VelociraptorSigmaAlertResponse
27 +from app.incidents.schema.velo_sigma import VeloSigmaExclusionCreate
28 +from app.incidents.schema.velo_sigma import VeloSigmaExclusionListResponse
29 +from app.incidents.schema.velo_sigma import VeloSigmaExclusionUpdate
30 +from app.incidents.schema.velo_sigma import VeloSigmaExlcusionRouteResponse
31 from app.incidents.services.alert_collection import add_copilot_alert_id
32 from app.incidents.services.alert_collection import get_alerts_not_created_in_copilot
33 from app.incidents.services.alert_collection import get_graylog_event_indices
@@ -32,6 +37,7 @@ from app.incidents.services.incident_alert import create_alert
37 from app.incidents.services.incident_alert import create_alert_full
38 from app.incidents.services.incident_alert import get_single_alert_details
39 from app.incidents.services.incident_alert import retrieve_alert_timeline
40 +from app.incidents.services.velo_sigma import VeloSigmaExclusionService
41 from app.incidents.services.velo_sigma import create_velo_sigma_alert
42
43 incidents_alerts_router = APIRouter()
@@ -268,3 +274,153 @@ async def process_sigma_alert(alert: VelociraptorSigmaAlert, session: AsyncSessi
274 """
275 logger.info(f"Processing Velociraptor Sigma alert: {alert}")
276 return await create_velo_sigma_alert(alert, session)
277 +
278 +
279 +@incidents_alerts_router.post(
280 + "/create/velo-sigma/exclusion",
281 + response_model=VeloSigmaExlcusionRouteResponse,
282 + summary="Create a new Velociraptor Sigma exclusion rule",
283 +)
284 +async def create_exclusion(
285 + exclusion: VeloSigmaExclusionCreate,
286 + current_user: str = Depends(AuthHandler().return_username_for_logging),
287 + db: AsyncSession = Depends(get_db),
288 +):
289 + """Create a new exclusion rule for Velociraptor Sigma alerts."""
290 + # Set the created_by field to the current user
291 + logger.info(f"Current user: {current_user}")
292 +
293 + # Take only needed fields from exclusion, excluding created_by
294 + exclusion_dict = exclusion.dict(exclude={"created_by"})
295 + # Create a new exclusion with the current user
296 + updated_exclusion = VeloSigmaExclusionCreate(**exclusion_dict, created_by=current_user)
297 +
298 + # Log the exclusion data for debugging
299 + logger.info(f"Exclusion data: {updated_exclusion.dict()}")
300 +
301 + service = VeloSigmaExclusionService(db)
302 + # return await service.create_exclusion(updated_exclusion)
303 + return VeloSigmaExlcusionRouteResponse(
304 + success=True,
305 + message="Exclusion rule created successfully",
306 + exclusion_response=await service.create_exclusion(updated_exclusion),
307 + )
308 +
309 +
310 +@incidents_alerts_router.get(
311 + "/create/velo-sigma/exclusion/{exclusion_id}",
312 + response_model=VeloSigmaExlcusionRouteResponse,
313 + summary="Get an exclusion rule by ID",
314 +)
315 +async def get_exclusion(
316 + exclusion_id: int,
317 + db: AsyncSession = Depends(get_db),
318 + current_user: str = Depends(AuthHandler().get_current_user),
319 +):
320 + """Retrieve details of a specific exclusion rule."""
321 + service = VeloSigmaExclusionService(db)
322 + exclusion = await service.get_exclusion(exclusion_id)
323 +
324 + if not exclusion:
325 + raise HTTPException(status_code=404, detail="Exclusion rule not found")
326 +
327 + return VeloSigmaExlcusionRouteResponse(
328 + success=True,
329 + message="Exclusion rule retrieved successfully",
330 + exclusion_response=exclusion,
331 + )
332 +
333 +
334 +@incidents_alerts_router.get(
335 + "/create/velo-sigma/exclusion",
336 + response_model=VeloSigmaExclusionListResponse,
337 + summary="List all exclusion rules",
338 +)
339 +async def list_exclusions(
340 + skip: int = Query(0, description="Number of items to skip for pagination"),
341 + limit: int = Query(100, description="Maximum number of items to return"),
342 + enabled_only: bool = Query(False, description="Only return enabled exclusions"),
343 + db: AsyncSession = Depends(get_db),
344 + current_user: str = Depends(AuthHandler().get_current_user),
345 +):
346 + """List all exclusion rules with pagination."""
347 + service = VeloSigmaExclusionService(db)
348 +
349 + # Get exclusions and total count
350 + exclusions, total_count = await service.list_exclusions_with_count(skip=skip, limit=limit, enabled_only=enabled_only)
351 +
352 + return VeloSigmaExclusionListResponse(
353 + success=True,
354 + message="Exclusion rules retrieved successfully",
355 + exclusions=exclusions,
356 + pagination={"total": total_count, "skip": skip, "limit": limit},
357 + )
358 +
359 +
360 +@incidents_alerts_router.patch(
361 + "/create/velo-sigma/exclusion/{exclusion_id}",
362 + response_model=VeloSigmaExlcusionRouteResponse,
363 + summary="Update an exclusion rule",
364 +)
365 +async def update_exclusion(
366 + exclusion_id: int,
367 + exclusion: VeloSigmaExclusionUpdate,
368 + db: AsyncSession = Depends(get_db),
369 + current_user: str = Depends(AuthHandler().get_current_user),
370 +):
371 + """Update an existing exclusion rule."""
372 + service = VeloSigmaExclusionService(db)
373 + updated = await service.update_exclusion(exclusion_id, exclusion.dict(exclude_unset=True))
374 +
375 + if not updated:
376 + raise HTTPException(status_code=404, detail="Exclusion rule not found")
377 +
378 + # return updated
379 + return VeloSigmaExlcusionRouteResponse(
380 + success=True,
381 + message="Exclusion rule updated successfully",
382 + exclusion_response=updated,
383 + )
384 +
385 +
386 +@incidents_alerts_router.delete("/create/velo-sigma/exclusion/{exclusion_id}", summary="Delete an exclusion rule")
387 +async def delete_exclusion(
388 + exclusion_id: int,
389 + db: AsyncSession = Depends(get_db),
390 + current_user: str = Depends(AuthHandler().get_current_user),
391 +):
392 + """Delete an exclusion rule."""
393 + service = VeloSigmaExclusionService(db)
394 + deleted = await service.delete_exclusion(exclusion_id)
395 +
396 + if not deleted:
397 + raise HTTPException(status_code=404, detail="Exclusion rule not found")
398 +
399 + return {"message": "Exclusion rule deleted successfully", "success": True}
400 +
401 +
402 +@incidents_alerts_router.post(
403 + "/velo-sigma/exclusion/{exclusion_id}/toggle",
404 + response_model=VeloSigmaExlcusionRouteResponse,
405 + summary="Toggle an exclusion rule's enabled status",
406 +)
407 +async def toggle_exclusion(
408 + exclusion_id: int,
409 + db: AsyncSession = Depends(get_db),
410 + current_user: str = Depends(AuthHandler().get_current_user),
411 +):
412 + """Enable or disable an exclusion rule."""
413 + service = VeloSigmaExclusionService(db)
414 + exclusion = await service.get_exclusion(exclusion_id)
415 +
416 + if not exclusion:
417 + raise HTTPException(status_code=404, detail="Exclusion rule not found")
418 +
419 + # Toggle the enabled status
420 + updated = await service.update_exclusion(exclusion_id, {"enabled": not exclusion.enabled})
421 + # return updated
422 + return VeloSigmaExlcusionRouteResponse(
423 + success=True,
424 + message="Exclusion rule toggled successfully",
425 + exclusion_response=updated,
426 + )
backend/app/incidents/schema/velo_sigma.py
+75
@@ -1,6 +1,8 @@
1 import json
2 +from datetime import datetime
3 from typing import Any
4 from typing import Dict
5 +from typing import List
6 from typing import Optional
7 from typing import Union
8
@@ -382,3 +384,76 @@ class VelociraptorSigmaAlertResponse(BaseModel):
384 success: bool
385 message: str
386 alert_id: Optional[str] = None
387 +
388 +
389 +class VeloSigmaExclusionBase(BaseModel):
390 + """Base class for Velociraptor Sigma exclusion rules."""
391 +
392 + name: str = Field(..., description="Friendly name for this exclusion rule")
393 + description: Optional[str] = Field(None, description="Description of why this exclusion exists")
394 + channel: Optional[str] = Field(None, description="Windows event channel to match (exact match)")
395 + title: Optional[str] = Field(None, description="Sigma rule title to match (exact match)")
396 + field_matches: Optional[Dict] = Field(None, description="Field names and values to match in the event data")
397 + customer_code: Optional[str] = Field(None, description="Customer code this exclusion applies to (null means all customers)")
398 + enabled: bool = Field(True, description="Whether this exclusion is active")
399 +
400 +
401 +class VeloSigmaExclusionCreate(VeloSigmaExclusionBase):
402 + """Schema for creating a new exclusion rule."""
403 +
404 + # Make created_by optional so it can be set by the server
405 + created_by: Optional[str] = Field(None, description="User who created this exclusion rule")
406 +
407 + class Config:
408 + # Example showing the expected request format
409 + schema_extra = {
410 + "example": {
411 + "name": "Chainsaw Batch Script Exclusion",
412 + "description": "Exclude alerts from chainsaw batch scripts in Windows Temp folder",
413 + "channel": "Microsoft-Windows-Sysmon/Operational",
414 + "title": "HackTool - Powerup Write Hijack DLL",
415 + "field_matches": {"TargetFilename": "C:\\Windows\\Temp\\chainsaw_batch.bat"},
416 + "customer_code": None, # Optional, NULL means apply to all customers
417 + "enabled": True,
418 + },
419 + }
420 +
421 +
422 +class VeloSigmaExclusionUpdate(BaseModel):
423 + """Schema for updating an exclusion rule."""
424 +
425 + name: Optional[str] = None
426 + description: Optional[str] = None
427 + channel: Optional[str] = None
428 + title: Optional[str] = None
429 + field_matches: Optional[Dict] = None
430 + customer_code: Optional[str] = None
431 + enabled: Optional[bool] = None
432 +
433 +
434 +class VeloSigmaExclusionResponse(VeloSigmaExclusionBase):
435 + """Response schema for exclusion rules."""
436 +
437 + id: int
438 + created_by: str
439 + created_at: datetime
440 + last_matched_at: Optional[datetime] = None
441 + match_count: int
442 +
443 + class Config:
444 + orm_mode = True
445 +
446 +
447 +class VeloSigmaExlcusionRouteResponse(BaseModel):
448 + """Response schema for exclusion rules."""
449 +
450 + exclusion_response: VeloSigmaExclusionResponse
451 + success: bool
452 + message: str
453 +
454 +
455 +class VeloSigmaExclusionListResponse(BaseModel):
456 + success: bool
457 + message: str
458 + exclusions: List[VeloSigmaExclusionResponse]
459 + pagination: dict = {"total": 0, "skip": 0, "limit": 0}
backend/app/incidents/services/incident_alert.py
+140 -2
@@ -554,11 +554,85 @@ async def handle_customer_notifications(
554 )
555
556
557 +# async def create_alert_full(
558 +# alert_payload: CreatedAlertPayload,
559 +# customer_code: str,
560 +# session: AsyncSession,
561 +# threshold_alert: bool = False,
562 +# ) -> Alert:
563 +# """
564 +# Create an alert in CoPilot.
565 +
566 +# Args:
567 +# alert_payload (dict): The alert payload.
568 +# customer_code (str): The customer code.
569 +# session (AsyncSession): The database session.
570 +
571 +# Returns:
572 +# CreateAlertResponse: The response object containing the created alert details.
573 +
574 +# Raises:
575 +# HTTPException: If there is an error creating the alert.
576 +# """
577 +# alert_id = (await create_alert_in_copilot(alert_payload=alert_payload, customer_code=customer_code, session=session)).id
578 +# alert_context_id = (
579 +# await create_alert_context_payload(source=alert_payload.source, alert_payload=alert_payload.alert_context_payload, session=session)
580 +# ).id
581 +# asset = await create_asset_context_payload(
582 +# customer_code=customer_code,
583 +# asset_payload=alert_payload,
584 +# alert_context_id=alert_context_id,
585 +# alert_id=alert_id,
586 +# session=session,
587 +# )
588 +# if alert_payload.ioc_payload is not None:
589 +# ioc_id = (
590 +# await create_ioc_payload(
591 +# ioc_payload=AlertIoCCreate(
592 +# alert_id=alert_id,
593 +# ioc_value=alert_payload.ioc_payload["ioc_value"],
594 +# ioc_type=alert_payload.ioc_payload["ioc_type"],
595 +# ioc_description=alert_payload.ioc_payload["ioc_description"],
596 +# ),
597 +# alert_id=alert_id,
598 +# session=session,
599 +# )
600 +# ).id
601 +# logger.info(
602 +# f"Creating alert for customer code {customer_code} with alert context ID {alert_context_id} and asset ID {asset.id} and ioc ID {ioc_id}",
603 +# )
604 +# logger.info(f"Creating alert for customer code {customer_code} with alert context ID {alert_context_id} and asset ID {asset.id}")
605 +# alert_payload.alert_id = alert_id
606 +# if asset is not None:
607 +# await handle_customer_notifications(
608 +# customer_code=customer_code,
609 +# asset_name=asset.asset_name,
610 +# alert_payload=alert_payload,
611 +# session=session,
612 +# )
613 +# else:
614 +# await handle_customer_notifications(
615 +# customer_code=customer_code,
616 +# asset_name="No asset found",
617 +# alert_payload=alert_payload,
618 +# session=session,
619 +# )
620 +
621 +# if threshold_alert is True:
622 +# logger.info(f"Threshold alert created for customer code {customer_code} with alert ID {alert_id}")
623 +# return alert_id
624 +
625 +# await add_alert_to_document(CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id), alert_id)
626 +
627 +# return alert_id
628 +
629 +
630 async def create_alert_full(
631 alert_payload: CreatedAlertPayload,
632 customer_code: str,
633 session: AsyncSession,
634 threshold_alert: bool = False,
635 + velo_sigma_alert: bool = False,
636 ) -> Alert:
637 """
638 Create an alert in CoPilot.
@@ -567,6 +641,8 @@ async def create_alert_full(
641 alert_payload (dict): The alert payload.
642 customer_code (str): The customer code.
643 session (AsyncSession): The database session.
644 + threshold_alert (bool, optional): Whether this is a threshold alert. Defaults to False.
645 + velo_sigma_alert (bool, optional): Whether this is a Velociraptor Sigma alert. Defaults to False.
646
647 Returns:
648 CreateAlertResponse: The response object containing the created alert details.
@@ -574,6 +650,66 @@ async def create_alert_full(
650 Raises:
651 HTTPException: If there is an error creating the alert.
652 """
653 + # For velo_sigma_alert, check if an open alert with the same title already exists
654 + if velo_sigma_alert:
655 + existing_alert_id = await open_alert_exists(alert_payload, customer_code, session)
656 + if existing_alert_id:
657 + logger.info(
658 + f"Found existing open alert ID {existing_alert_id} for Velociraptor Sigma alert with title {alert_payload.alert_title_payload}",
659 + )
660 +
661 + # Add the asset to the existing alert if it doesn't already exist
662 + asset_exists = await does_assit_exist(alert_payload, existing_alert_id, session)
663 + if not asset_exists and alert_payload.asset_payload:
664 + logger.info(f"Adding asset {alert_payload.asset_payload} to existing alert ID {existing_alert_id}")
665 + await add_asset_to_copilot_alert(
666 + alert_payload=alert_payload,
667 + alert_id=existing_alert_id,
668 + customer_code=customer_code,
669 + session=session,
670 + )
671 +
672 + # Add IOC if present and doesn't already exist
673 + if alert_payload.ioc_payload is not None:
674 + ioc_exists = await does_ioc_exist(alert_payload, existing_alert_id, session)
675 + if not ioc_exists:
676 + logger.info(f"Adding IOC {alert_payload.ioc_payload['ioc_value']} to existing alert ID {existing_alert_id}")
677 + await add_ioc_to_copilot_alert(
678 + alert_payload=alert_payload,
679 + alert_id=existing_alert_id,
680 + customer_code=customer_code,
681 + session=session,
682 + )
683 +
684 + # Update the document reference if needed
685 + if alert_payload.index_name and alert_payload.index_id:
686 + await add_alert_to_document(
687 + CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id),
688 + existing_alert_id,
689 + )
690 +
691 + # Set alert ID for notifications
692 + alert_payload.alert_id = existing_alert_id
693 +
694 + # Handle customer notifications
695 + if alert_payload.asset_payload:
696 + await handle_customer_notifications(
697 + customer_code=customer_code,
698 + asset_name=alert_payload.asset_payload,
699 + alert_payload=alert_payload,
700 + session=session,
701 + )
702 + else:
703 + await handle_customer_notifications(
704 + customer_code=customer_code,
705 + asset_name="No asset found",
706 + alert_payload=alert_payload,
707 + session=session,
708 + )
709 +
710 + return existing_alert_id
711 +
712 + # If not velo_sigma_alert or no existing alert found, proceed with normal alert creation
713 alert_id = (await create_alert_in_copilot(alert_payload=alert_payload, customer_code=customer_code, session=session)).id
714 alert_context_id = (
715 await create_alert_context_payload(source=alert_payload.source, alert_payload=alert_payload.alert_context_payload, session=session)
@@ -618,8 +754,10 @@ async def create_alert_full(
754 session=session,
755 )
756
621 - if threshold_alert is True:
622 - logger.info(f"Threshold alert created for customer code {customer_code} with alert ID {alert_id}")
757 + if threshold_alert is True or velo_sigma_alert is True:
758 + logger.info(
759 + f"{'Threshold' if threshold_alert else 'Velociraptor Sigma'} alert created for customer code {customer_code} with alert ID {alert_id}",
760 + )
761 return alert_id
762
763 await add_alert_to_document(CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id), alert_id)
backend/app/incidents/services/velo_sigma.py
+300 -2
@@ -1,18 +1,23 @@
1 +import re
2 from datetime import datetime
3 from datetime import timedelta
4 from typing import Any
5 from typing import Dict
6 +from typing import List
7 from typing import Optional
8 from typing import Union
9
10 from loguru import logger
11 +from sqlalchemy import func
12 from sqlalchemy import select
13 +from sqlalchemy import update
14 from sqlalchemy.ext.asyncio import AsyncSession
15
16 from app.connectors.wazuh_indexer.utils.universal import (
17 create_wazuh_indexer_client_async,
18 )
19 from app.db.universal_models import Agents
20 +from app.incidents.models import VeloSigmaExclusion
21 from app.incidents.schema.db_operations import AlertTagCreate
22 from app.incidents.schema.db_operations import CommentCreate
23 from app.incidents.schema.incident_alert import CreateAlertRequest
@@ -23,12 +28,284 @@ from app.incidents.schema.velo_sigma import PowerShellEvent
28 from app.incidents.schema.velo_sigma import SysmonEvent
29 from app.incidents.schema.velo_sigma import VelociraptorSigmaAlert
30 from app.incidents.schema.velo_sigma import VelociraptorSigmaAlertResponse
31 +from app.incidents.schema.velo_sigma import VeloSigmaExclusionCreate
32 from app.incidents.services.db_operations import create_alert_tag
33 from app.incidents.services.db_operations import create_comment
34 from app.incidents.services.incident_alert import create_alert
35 from app.incidents.services.incident_alert import create_alert_full
36
37
38 +class VeloSigmaExclusionService:
39 + """Service for managing and checking Velociraptor Sigma exclusions."""
40 +
41 + def __init__(self, session: AsyncSession):
42 + self.session = session
43 +
44 + async def check_exclusions(self, alert: VelociraptorSigmaAlert) -> Optional[VeloSigmaExclusion]:
45 + """
46 + Check if the given alert matches any exclusion rules.
47 +
48 + Args:
49 + alert: The Velociraptor Sigma alert to check
50 +
51 + Returns:
52 + The first matching exclusion rule, or None if no match is found
53 + """
54 + # Get all enabled exclusions
55 + stmt = select(VeloSigmaExclusion).where(VeloSigmaExclusion.enabled == True)
56 + result = await self.session.execute(stmt)
57 + exclusions = result.scalars().all()
58 +
59 + if not exclusions:
60 + return None
61 +
62 + # Parse the event data
63 + try:
64 + parsed_event = alert.get_parsed_event()
65 + logger.debug(f"Checking exclusions for alert | Channel: {alert.channel} | Type: {type(parsed_event).__name__}")
66 + logger.debug(f"Parsed event: {parsed_event}")
67 + event_data = {}
68 +
69 + # Extract event data fields from different event types
70 + if hasattr(parsed_event, "EventData"):
71 + # First add all top-level fields
72 + for attr_name in dir(parsed_event.EventData):
73 + if not attr_name.startswith("_") and not callable(getattr(parsed_event.EventData, attr_name)):
74 + try:
75 + value = getattr(parsed_event.EventData, attr_name)
76 + if not callable(value):
77 + event_data[attr_name] = str(value)
78 + except Exception:
79 + pass
80 +
81 + # Special handling for PowerShell ContextInfo which contains Host Application
82 + if hasattr(parsed_event.EventData, "ContextInfo") and parsed_event.EventData.ContextInfo:
83 + # Parse the ContextInfo string which contains multiple lines of key-value pairs
84 + context_info = parsed_event.EventData.ContextInfo
85 + logger.debug(f"Found ContextInfo in PowerShell event: {context_info}")
86 +
87 + for line in context_info.splitlines():
88 + line = line.strip()
89 + if line and " = " in line:
90 + key, value = line.split(" = ", 1)
91 + key = key.strip()
92 + # Add these fields with their original names for direct matching
93 + event_data[key] = value.strip()
94 + # Also add common CamelCase variations to make matching more flexible
95 + if " " in key:
96 + # Convert "Host Application" to "HostApplication"
97 + camel_key = "".join(word.capitalize() for word in key.split())
98 + camel_key = camel_key[0].lower() + camel_key[1:] # lowerCamelCase
99 + event_data[camel_key] = value.strip()
100 +
101 + # Log the extracted field names and values for debugging
102 + for key, value in event_data.items():
103 + logger.debug(f"Extracted field: {key} = {value}")
104 +
105 + except Exception as e:
106 + logger.error(f"Error parsing event data: {str(e)}")
107 + # If we can't parse the event, we won't exclude it
108 + return None
109 +
110 + # Check each exclusion against this alert
111 + for exclusion in exclusions:
112 + logger.debug(f"Checking exclusion: {exclusion.name} (ID: {exclusion.id})")
113 + if await self._matches_exclusion(alert, event_data, exclusion):
114 + # Update match statistics
115 + await self._update_exclusion_stats(exclusion.id)
116 + return exclusion
117 +
118 + return None
119 +
120 + async def _matches_exclusion(self, alert: VelociraptorSigmaAlert, event_data: Dict[str, str], exclusion: VeloSigmaExclusion) -> bool:
121 + """Check if the alert matches the given exclusion rule."""
122 + # Check customer code if specified
123 + if exclusion.customer_code and exclusion.customer_code != self._get_customer_code(alert):
124 + logger.debug(f"Customer code mismatch: rule={exclusion.customer_code}, alert={self._get_customer_code(alert)}")
125 + return False
126 +
127 + # Check channel if specified
128 + if exclusion.channel and exclusion.channel != alert.channel:
129 + logger.debug(f"Channel mismatch: rule={exclusion.channel}, alert={alert.channel}")
130 + return False
131 +
132 + # Check title if specified
133 + if exclusion.title and exclusion.title != alert.title:
134 + logger.debug(f"Title mismatch: rule={exclusion.title}, alert={alert.title}")
135 + return False
136 +
137 + # Check field matches if specified
138 + if exclusion.field_matches:
139 + for field_name, field_value in exclusion.field_matches.items():
140 + # Special handling for Host Application field
141 + if field_name.lower() == "hostapplication" and field_name not in event_data:
142 + # Try common variations
143 + alternate_keys = ["Host Application", "HostApplication", "hostApplication"]
144 + found = False
145 + for alt_key in alternate_keys:
146 + if alt_key in event_data:
147 + field_name = alt_key # Use the key that exists in the data
148 + found = True
149 + break
150 + if not found:
151 + logger.debug(f"Field '{field_name}' and its variations not found in event data")
152 + return False
153 +
154 + # Direct match
155 + if field_name in event_data:
156 + event_value = event_data[field_name]
157 + logger.debug(f"Checking field match: {field_name}={field_value} against {event_value}")
158 +
159 + # Support exact match or regex match
160 + if isinstance(field_value, str):
161 + if field_value.startswith("regex:"):
162 + # Remove the regex: prefix and try to match
163 + regex_pattern = field_value[6:]
164 + try:
165 + if not re.search(regex_pattern, event_value, re.IGNORECASE):
166 + logger.debug(f"Regex pattern '{regex_pattern}' did not match '{event_value}'")
167 + return False
168 + else:
169 + logger.debug(f"Regex pattern '{regex_pattern}' matched '{event_value}'")
170 + except re.error:
171 + logger.error(f"Invalid regex pattern in exclusion {exclusion.id}: {regex_pattern}")
172 + return False
173 + else:
174 + # Case-insensitive path comparison for Windows paths
175 + if "path" in field_name.lower() or "file" in field_name.lower() or "\\" in field_value:
176 + norm_field_value = field_value.lower().replace("\\\\", "\\")
177 + norm_event_value = event_value.lower().replace("\\\\", "\\")
178 + if norm_field_value != norm_event_value:
179 + logger.debug(f"Path mismatch: rule='{norm_field_value}' event='{norm_event_value}'")
180 + return False
181 + else:
182 + # Standard case-insensitive match for other fields
183 + if field_value.lower() != event_value.lower():
184 + logger.debug(f"Case-insensitive match failed: rule='{field_value}' event='{event_value}'")
185 + return False
186 + else:
187 + # For non-string values (like lists or objects), convert to string for comparison
188 + if str(field_value) != event_value:
189 + logger.debug(f"String conversion match failed: rule='{str(field_value)}' event='{event_value}'")
190 + return False
191 + else:
192 + # If field doesn't exist in the event and we're looking for it, no match
193 + logger.debug(f"Field '{field_name}' not found in event data")
194 + logger.debug(f"Available fields: {', '.join(event_data.keys())}")
195 + return False
196 +
197 + # If we passed all checks, this is a match
198 + logger.info(f"Alert matched exclusion rule '{exclusion.name}' (ID: {exclusion.id})")
199 + return True
200 +
201 + async def _update_exclusion_stats(self, exclusion_id: int) -> None:
202 + """Update the statistics for an exclusion after it matches."""
203 + try:
204 + stmt = (
205 + update(VeloSigmaExclusion)
206 + .where(VeloSigmaExclusion.id == exclusion_id)
207 + .values(last_matched_at=datetime.utcnow(), match_count=VeloSigmaExclusion.match_count + 1)
208 + )
209 + await self.session.execute(stmt)
210 + await self.session.commit()
211 + except Exception as e:
212 + logger.error(f"Error updating exclusion stats: {str(e)}")
213 + # Don't raise the error, just log it
214 +
215 + def _get_customer_code(self, alert: VelociraptorSigmaAlert) -> str:
216 + """Extract or determine customer code from the alert."""
217 + # This will depend on where customer code is stored in your alerts
218 + # You might need to use your agent lookup logic here
219 + # For now, we'll return a placeholder
220 + return "unknown"
221 +
222 + async def create_exclusion(self, exclusion: VeloSigmaExclusionCreate) -> VeloSigmaExclusion:
223 + """Create a new exclusion rule."""
224 + exclusion_data = exclusion.dict()
225 +
226 + # Ensure created_by is set to something non-null
227 + if not exclusion_data.get("created_by"):
228 + exclusion_data["created_by"] = "system" # Default fallback
229 +
230 + db_exclusion = VeloSigmaExclusion(**exclusion_data)
231 + self.session.add(db_exclusion)
232 + await self.session.commit()
233 + await self.session.refresh(db_exclusion)
234 + return db_exclusion
235 +
236 + async def get_exclusion(self, exclusion_id: int) -> Optional[VeloSigmaExclusion]:
237 + """Retrieve an exclusion by ID."""
238 + stmt = select(VeloSigmaExclusion).where(VeloSigmaExclusion.id == exclusion_id)
239 + result = await self.session.execute(stmt)
240 + return result.scalar_one_or_none()
241 +
242 + async def list_exclusions(self, skip: int = 0, limit: int = 100, enabled_only: bool = False) -> List[VeloSigmaExclusion]:
243 + """List all exclusion rules with pagination."""
244 + query = select(VeloSigmaExclusion)
245 + if enabled_only:
246 + query = query.where(VeloSigmaExclusion.enabled == True)
247 + query = query.offset(skip).limit(limit)
248 + result = await self.session.execute(query)
249 + return result.scalars().all()
250 +
251 + async def update_exclusion(self, exclusion_id: int, exclusion_data: Dict[str, Any]) -> Optional[VeloSigmaExclusion]:
252 + """Update an existing exclusion rule."""
253 + db_exclusion = await self.get_exclusion(exclusion_id)
254 + if not db_exclusion:
255 + return None
256 +
257 + # Update only provided fields
258 + for key, value in exclusion_data.items():
259 + if hasattr(db_exclusion, key):
260 + setattr(db_exclusion, key, value)
261 +
262 + await self.session.commit()
263 + await self.session.refresh(db_exclusion)
264 + return db_exclusion
265 +
266 + async def delete_exclusion(self, exclusion_id: int) -> bool:
267 + """Delete an exclusion rule."""
268 + db_exclusion = await self.get_exclusion(exclusion_id)
269 + if not db_exclusion:
270 + return False
271 +
272 + await self.session.delete(db_exclusion)
273 + await self.session.commit()
274 + return True
275 +
276 + async def list_exclusions_with_count(self, skip: int = 0, limit: int = 100, enabled_only: bool = False) -> tuple[list, int]:
277 + """
278 + List all exclusion rules with pagination and return total count.
279 +
280 + Args:
281 + skip: Number of items to skip
282 + limit: Maximum number of items to return
283 + enabled_only: If True, only return enabled exclusions
284 +
285 + Returns:
286 + Tuple of (list of exclusions, total count)
287 + """
288 + query = select(VeloSigmaExclusion)
289 +
290 + if enabled_only:
291 + query = query.where(VeloSigmaExclusion.enabled == True)
292 +
293 + # Get total count first
294 + count_query = select(func.count()).select_from(query.subquery())
295 + # Change self.db to self.session
296 + total_count = await self.session.scalar(count_query)
297 +
298 + # Then get the paginated results
299 + query = query.order_by(VeloSigmaExclusion.id.desc())
300 + query = query.offset(skip).limit(limit)
301 +
302 + # Change self.db to self.session
303 + result = await self.session.execute(query)
304 + exclusions = result.scalars().all()
305 +
306 + return list(exclusions), total_count
307 +
308 +
309 class VelociraptorSigmaService:
310 """Service for handling Velociraptor Sigma alerts and their integration with Wazuh."""
311
@@ -88,7 +365,12 @@ class VelociraptorSigmaService:
365 agent = agent_result.scalar_one_or_none()
366
367 if agent and agent.customer_code:
368 + logger.info(f"Found agent details {agent}")
369 customer_code = agent.customer_code
370 + # ! SOMETIMES VELOCIRAPTOR AND WAZUH ENUMERATE DIFFERENT HOSTNAMES ! #
371 + # ! Due to this, we will use the agent.hostname as the asset name as this is what ! #
372 + # ! used in the Wazuh events.!#
373 + agent_name = agent.hostname
374 logger.info(f"Found customer code '{customer_code}' for clientID {alert.clientID}")
375 else:
376 logger.warning(f"No agent found with velociraptor_id '{alert.clientID}', using default customer code")
@@ -99,7 +381,7 @@ class VelociraptorSigmaService:
381 alert_id = await create_alert_full(
382 alert_payload=CreatedAlertPayload(
383 alert_context_payload=event_context,
102 - asset_payload=alert.computer,
384 + asset_payload=agent_name,
385 timefield_payload=timestamp,
386 alert_title_payload=alert.title,
387 source=alert.source,
@@ -108,7 +390,8 @@ class VelociraptorSigmaService:
390 ),
391 customer_code=customer_code, # Use the looked up customer code
392 session=self.session,
111 - threshold_alert=True,
393 + threshold_alert=False,
394 + velo_sigma_alert=True,
395 )
396 result["alert_id"] = alert_id
397
@@ -169,6 +452,21 @@ class VelociraptorSigmaService:
452 Response indicating the success or failure of the processing
453 """
454 try:
455 + # Check exclusions first
456 + exclusion_service = VeloSigmaExclusionService(self.session)
457 + matching_exclusion = await exclusion_service.check_exclusions(alert)
458 +
459 + if matching_exclusion:
460 + # Alert is excluded, return a response indicating this
461 + logger.info(f"Skipping alert processing: matched exclusion rule '{matching_exclusion.name}' (ID: {matching_exclusion.id})")
462 + return VelociraptorSigmaAlertResponse(
463 + success=True,
464 + message=f"Alert excluded by rule: {matching_exclusion.name}",
465 + alert_id=None,
466 + excluded=True,
467 + exclusion_id=matching_exclusion.id,
468 + )
469 +
470 # Parse event and determine event type
471 result = await self._process_event_by_type(alert)
472
backend/app/integrations/defender_for_endpoint/routes/provision.py new
+137
@@ -0,0 +1,137 @@
1 +from typing import Dict
2 +
3 +from fastapi import APIRouter
4 +from fastapi import Depends
5 +from fastapi import HTTPException
6 +from fastapi import Security
7 +from sqlalchemy.ext.asyncio import AsyncSession
8 +
9 +from app.auth.utils import AuthHandler
10 +from app.db.db_session import get_db
11 +from app.integrations.defender_for_endpoint.schema.provision import (
12 + DefenderForEndpointCustomerDetails,
13 +)
14 +from app.integrations.defender_for_endpoint.schema.provision import (
15 + ProvisionDefenderForEndpointAuthKeys,
16 +)
17 +from app.integrations.defender_for_endpoint.schema.provision import (
18 + ProvisionDefenderForEndpointRequest,
19 +)
20 +from app.integrations.defender_for_endpoint.schema.provision import (
21 + ProvisionDefenderForEndpointResponse,
22 +)
23 +from app.integrations.defender_for_endpoint.services.provision import (
24 + provision_defender_for_endpoint,
25 +)
26 +from app.integrations.routes import find_customer_integration
27 +from app.integrations.routes import get_customer_integrations_by_customer_code
28 +from app.integrations.schema import CustomerIntegrations
29 +from app.integrations.schema import CustomerIntegrationsResponse
30 +
31 +integration_defender_for_endpoint_router = APIRouter()
32 +
33 +
34 +async def get_customer_integration_response(
35 + customer_code: str,
36 + session: AsyncSession,
37 +) -> CustomerIntegrationsResponse:
38 + """
39 + Retrieves the integration response for a customer.
40 +
41 + Args:
42 + customer_code (str): The code of the customer.
43 + session (AsyncSession): The async session object for database operations.
44 +
45 + Returns:
46 + CustomerIntegrationsResponse: The integration response for the customer.
47 +
48 + Raises:
49 + HTTPException: If the customer integration settings are not found.
50 + """
51 + customer_integration_response = await get_customer_integrations_by_customer_code(
52 + customer_code,
53 + session,
54 + )
55 + if customer_integration_response.available_integrations == []:
56 + raise HTTPException(
57 + status_code=404,
58 + detail="Customer integration settings not found.",
59 + )
60 + return customer_integration_response
61 +
62 +
63 +def extract_defender_for_endpoint_auth_keys(
64 + customer_integration: CustomerIntegrations,
65 +) -> Dict[str, str]:
66 + """
67 + Extracts the authentication keys for defender_for_endpoint integration from the given customer integration.
68 +
69 + Args:
70 + customer_integration (CustomerIntegrations): The customer integration object.
71 +
72 + Returns:
73 + Dict[str, str]: A dictionary containing the authentication keys for defender_for_endpoint integration.
74 +
75 + Raises:
76 + HTTPException: If no authentication keys are found for defender_for_endpoint integration.
77 + """
78 + defender_for_endpoint_auth_keys = {}
79 + for subscription in customer_integration.integration_subscriptions:
80 + if subscription.integration_service.service_name == "DefenderForEndpoint":
81 + for auth_key in subscription.integration_auth_keys:
82 + defender_for_endpoint_auth_keys[auth_key.auth_key_name] = auth_key.auth_value
83 + if not defender_for_endpoint_auth_keys:
84 + raise HTTPException(
85 + status_code=404,
86 + detail="No auth keys found for defender_for_endpoint integration. Please create auth keys for defender_for_endpoint integration.",
87 + )
88 + return defender_for_endpoint_auth_keys
89 +
90 +
91 +@integration_defender_for_endpoint_router.post(
92 + "/provision",
93 + response_model=ProvisionDefenderForEndpointResponse,
94 + description="Provision DefenderForEndpoint integration for a customer.",
95 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
96 +)
97 +async def provision_defender_for_endpoint_route(
98 + provision_defender_for_endpoint_request: ProvisionDefenderForEndpointRequest,
99 + session: AsyncSession = Depends(get_db),
100 +) -> ProvisionDefenderForEndpointResponse:
101 + """
102 + Provisions DefenderForEndpoint integration for a customer.
103 +
104 + Args:
105 + provision_defender_for_endpoint_request (ProvisionDefenderForEndpointRequest): The request object containing the necessary information for provisioning.
106 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
107 +
108 + Returns:
109 + ProvisionDefenderForEndpointResponse: The response object containing the result of the provisioning.
110 + """
111 + customer_integration_response = await get_customer_integration_response(
112 + provision_defender_for_endpoint_request.customer_code,
113 + session,
114 + )
115 +
116 + customer_integration = await find_customer_integration(
117 + provision_defender_for_endpoint_request.customer_code,
118 + provision_defender_for_endpoint_request.integration_name,
119 + customer_integration_response,
120 + )
121 +
122 + defender_for_endpoint_auth_keys = extract_defender_for_endpoint_auth_keys(customer_integration)
123 +
124 + auth_keys = ProvisionDefenderForEndpointAuthKeys(**defender_for_endpoint_auth_keys)
125 +
126 + return await provision_defender_for_endpoint(
127 + customer_details=DefenderForEndpointCustomerDetails(
128 + customer_code=provision_defender_for_endpoint_request.customer_code,
129 + customer_name=customer_integration.customer_name,
130 + protocal_type="TCP",
131 + syslog_port=int(auth_keys.SYSLOG_PORT),
132 + hot_data_retention=provision_defender_for_endpoint_request.hot_data_retention,
133 + index_replicas=provision_defender_for_endpoint_request.index_replicas,
134 + ),
135 + keys=auth_keys,
136 + session=session,
137 + )
backend/app/integrations/defender_for_endpoint/schema/provision.py new
+97
@@ -0,0 +1,97 @@
1 +from typing import Any
2 +from typing import Dict
3 +from typing import Optional
4 +
5 +from pydantic import BaseModel
6 +from pydantic import Field
7 +from pydantic import root_validator
8 +
9 +
10 +class ProvisionDefenderForEndpointRequest(BaseModel):
11 + customer_code: str = Field(
12 + ...,
13 + description="The customer code.",
14 + examples=["00001"],
15 + )
16 + integration_name: str = Field(
17 + "DefenderForEndpoint",
18 + description="The integration name.",
19 + examples=["DefenderForEndpoint"],
20 + )
21 + hot_data_retention: Optional[int] = Field(
22 + 30,
23 + example=30,
24 + description="Number of days to retain hot data",
25 + )
26 + index_replicas: Optional[int] = Field(
27 + 0,
28 + example=1,
29 + description="Number of replicas for the customer's Graylog instance",
30 + )
31 +
32 + # ensure the `integration_name` is always set to "DefenderForEndpoint"
33 + @root_validator(pre=True)
34 + def set_integration_name(cls, values: Dict[str, Any]) -> Dict[str, Any]:
35 + values["integration_name"] = "DefenderForEndpoint"
36 + return values
37 +
38 +
39 +class ProvisionDefenderForEndpointResponse(BaseModel):
40 + success: bool
41 + message: str
42 +
43 +
44 +class ProvisionDefenderForEndpointAuthKeys(BaseModel):
45 + CLIENT_ID: str = Field(
46 + ...,
47 + description="The client id.",
48 + examples=["00002"],
49 + )
50 + CLIENT_SECRET: str = Field(
51 + ...,
52 + description="The client secret.",
53 + examples=["00002"],
54 + )
55 + TENANT_ID: str = Field(
56 + ...,
57 + description="The tenant id.",
58 + examples=["00002"],
59 + )
60 + SYSLOG_PORT: str = Field(
61 + ...,
62 + description="The syslog port.",
63 + examples=["5556"],
64 + )
65 +
66 +
67 +class DefenderForEndpointCustomerDetails(BaseModel):
68 + customer_name: str = Field(
69 + ...,
70 + description="The customer name.",
71 + examples=["Customer 1"],
72 + )
73 + customer_code: str = Field(
74 + ...,
75 + description="The customer code.",
76 + examples=["00002"],
77 + )
78 + protocal_type: str = Field(
79 + ...,
80 + description="The protocal type.",
81 + examples=["TCP"],
82 + )
83 + syslog_port: int = Field(
84 + ...,
85 + description="The syslog port.",
86 + examples=[514],
87 + )
88 + hot_data_retention: int = Field(
89 + ...,
90 + example=30,
91 + description="Number of days to retain hot data",
92 + )
93 + index_replicas: int = Field(
94 + ...,
95 + example=1,
96 + description="Number of replicas for the customer's Graylog instance",
97 + )
backend/app/integrations/defender_for_endpoint/services/provision.py new
+586
@@ -0,0 +1,586 @@
1 +import json
2 +import os
3 +from datetime import datetime
4 +
5 +import aiofiles
6 +from fastapi import HTTPException
7 +from loguru import logger
8 +from sqlalchemy import and_
9 +from sqlalchemy import update
10 +from sqlalchemy.ext.asyncio import AsyncSession
11 +
12 +from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
13 +from app.connectors.grafana.schema.dashboards import DefenderForEndpointDashboard
14 +from app.connectors.grafana.services.dashboards import provision_dashboards
15 +from app.connectors.grafana.utils.universal import create_grafana_client
16 +from app.connectors.graylog.services.collector import (
17 + get_content_pack_id_by_content_pack_name,
18 +)
19 +from app.connectors.graylog.services.collector import get_input_id_by_input_name
20 +from app.connectors.graylog.services.collector import get_stream_id_by_stream_name
21 +from app.connectors.graylog.services.streams import assign_stream_to_index
22 +from app.connectors.graylog.utils.universal import send_post_request
23 +from app.connectors.wazuh_indexer.services.monitoring import (
24 + output_shard_number_to_be_set_based_on_nodes,
25 +)
26 +from app.customer_provisioning.schema.grafana import GrafanaDatasource
27 +from app.customer_provisioning.schema.grafana import GrafanaDataSourceCreationResponse
28 +from app.customer_provisioning.schema.graylog import GraylogIndexSetCreationResponse
29 +from app.customer_provisioning.schema.graylog import TimeBasedIndexSet
30 +from app.customer_provisioning.schema.provision import ProvisionNewCustomer
31 +from app.customer_provisioning.services.grafana import create_grafana_folder
32 +from app.customer_provisioning.services.grafana import get_opensearch_version
33 +from app.customers.routes.customers import get_customer_meta
34 +from app.integrations.defender_for_endpoint.schema.provision import (
35 + DefenderForEndpointCustomerDetails,
36 +)
37 +from app.integrations.defender_for_endpoint.schema.provision import (
38 + ProvisionDefenderForEndpointAuthKeys,
39 +)
40 +from app.integrations.defender_for_endpoint.schema.provision import (
41 + ProvisionDefenderForEndpointResponse,
42 +)
43 +from app.integrations.models.customer_integration_settings import CustomerIntegrations
44 +from app.network_connectors.models.network_connectors import (
45 + CustomerNetworkConnectorsMeta,
46 +)
47 +from app.stack_provisioning.graylog.schema.provision import ContentPackKeywords
48 +from app.stack_provisioning.graylog.schema.provision import (
49 + ProvisionNetworkContentPackRequest,
50 +)
51 +from app.stack_provisioning.graylog.services.provision import (
52 + provision_content_pack_network_connector,
53 +)
54 +from app.utils import get_connector_attribute
55 +from app.utils import get_customer_meta_attribute
56 +
57 +
58 +#### ! GRAYLOG ! ####
59 +async def build_index_set_config(request: DefenderForEndpointCustomerDetails) -> TimeBasedIndexSet:
60 + """
61 + Build the configuration for a time-based index set.
62 +
63 + Args:
64 + request (DefenderForEndpointCustomerDetails): The request object containing customer information.
65 +
66 + Returns:
67 + TimeBasedIndexSet: The configured time-based index set.
68 + """
69 + return TimeBasedIndexSet(
70 + title=f"{request.customer_name} - DEFENDER FOR ENDPOINT EVENTS",
71 + description=f"{request.customer_name} - DEFENDER FOR ENDPOINT EVENTS",
72 + index_prefix=f"defender-for-endpoint-{request.customer_code}",
73 + rotation_strategy_class="org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategy",
74 + rotation_strategy={
75 + "type": "org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategyConfig",
76 + "rotation_period": "P1D",
77 + "rotate_empty_index_set": False,
78 + "max_rotation_period": None,
79 + },
80 + retention_strategy_class="org.graylog2.indexer.retention.strategies.DeletionRetentionStrategy",
81 + retention_strategy={
82 + "type": "org.graylog2.indexer.retention.strategies.DeletionRetentionStrategyConfig",
83 + "max_number_of_indices": request.hot_data_retention,
84 + },
85 + creation_date=datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ"),
86 + index_analyzer="standard",
87 + shards=await output_shard_number_to_be_set_based_on_nodes(),
88 + replicas=request.index_replicas,
89 + index_optimization_max_num_segments=1,
90 + index_optimization_disabled=False,
91 + writable=True,
92 + field_type_refresh_interval=5000,
93 + )
94 +
95 +
96 +# Function to send the POST request and handle the response
97 +async def send_index_set_creation_request(
98 + index_set: TimeBasedIndexSet,
99 +) -> GraylogIndexSetCreationResponse:
100 + """
101 + Sends a request to create an index set in Graylog.
102 +
103 + Args:
104 + index_set (TimeBasedIndexSet): The index set to be created.
105 +
106 + Returns:
107 + GraylogIndexSetCreationResponse: The response from Graylog after creating the index set.
108 + """
109 + json_index_set = json.dumps(index_set.dict())
110 + logger.info(f"json_index_set set: {json_index_set}")
111 + response_json = await send_post_request(
112 + endpoint="/api/system/indices/index_sets",
113 + data=index_set.dict(),
114 + )
115 + return GraylogIndexSetCreationResponse(**response_json)
116 +
117 +
118 +# Refactored create_index_set function
119 +async def create_index_set(
120 + request: ProvisionNewCustomer,
121 +) -> GraylogIndexSetCreationResponse:
122 + """
123 + Creates an index set for a new customer.
124 +
125 + Args:
126 + request (ProvisionNewCustomer): The request object containing the customer information.
127 +
128 + Returns:
129 + GraylogIndexSetCreationResponse: The response object containing the result of the index set creation.
130 + """
131 + logger.info(f"Creating index set for customer {request.customer_name}")
132 + index_set_config = await build_index_set_config(request)
133 + return await send_index_set_creation_request(index_set_config)
134 +
135 +
136 +async def provision_content_pack(customer_details):
137 + """
138 + Provisions a content pack for a customer.
139 +
140 + Args:
141 + customer_details (CustomerDetails): The details of the customer.
142 +
143 + Returns:
144 + ContentPack: The provisioned content pack.
145 + """
146 + return await provision_content_pack_network_connector(
147 + content_pack_request=ProvisionNetworkContentPackRequest(
148 + content_pack_name="DEFENDER_FOR_ENDPOINT",
149 + keywords=ContentPackKeywords(
150 + customer_name=customer_details.customer_name,
151 + customer_code=customer_details.customer_code,
152 + protocol_type=customer_details.protocal_type,
153 + syslog_port=customer_details.syslog_port,
154 + ),
155 + ),
156 + )
157 +
158 +
159 +async def get_stream_and_index_ids(customer_details):
160 + """
161 + Retrieves the stream ID and index ID for a given customer.
162 +
163 + Args:
164 + customer_details (CustomerDetails): The details of the customer.
165 +
166 + Returns:
167 + tuple: A tuple containing the stream ID and index ID.
168 + """
169 + stream_id = await get_stream_id_by_stream_name(stream_name=f"{customer_details.customer_name} - DEFENDER FOR ENDPOINT LOGS AND EVENTS")
170 + index_id = (await create_index_set(request=customer_details)).data.id
171 + content_pack_stream_id = await get_content_pack_id_by_content_pack_name(
172 + content_pack_name=f"{customer_details.customer_name}_DEFENDER_FOR_ENDPOINT_STREAM",
173 + )
174 + if customer_details.protocal_type == "TCP":
175 + content_pack_input_id = await get_content_pack_id_by_content_pack_name(
176 + content_pack_name=f"{customer_details.customer_name}_DEFENDER_FOR_ENDPOINT_INPUT_TCP",
177 + )
178 + return stream_id, index_id, content_pack_stream_id, content_pack_input_id
179 +
180 +
181 +#### ! GRAFANA ! ####
182 +async def create_grafana_datasource(
183 + customer_code: str,
184 + session: AsyncSession,
185 +) -> GrafanaDataSourceCreationResponse:
186 + """
187 + Creates a Grafana datasource for the specified customer.
188 +
189 + Args:
190 + customer_code (str): The customer code.
191 + session (AsyncSession): The async session.
192 +
193 + Returns:
194 + GrafanaDataSourceCreationResponse: The response containing the created datasource details.
195 + """
196 + logger.info("Creating Grafana datasource")
197 + grafana_client = await create_grafana_client("Grafana")
198 + # Switch to the newly created organization
199 + grafana_client.user.switch_actual_user_organisation(
200 + (await get_customer_meta(customer_code, session)).customer_meta.customer_meta_grafana_org_id,
201 + )
202 + datasource_payload = GrafanaDatasource(
203 + name="DEFENDER FOR ENDPOINT",
204 + type="grafana-opensearch-datasource",
205 + typeName="OpenSearch",
206 + access="proxy",
207 + url=await get_connector_attribute(
208 + connector_id=1,
209 + column_name="connector_url",
210 + session=session,
211 + ),
212 + database=f"defender-for-endpoint-{customer_code}*",
213 + basicAuth=True,
214 + basicAuthUser=await get_connector_attribute(
215 + connector_id=1,
216 + column_name="connector_username",
217 + session=session,
218 + ),
219 + secureJsonData={
220 + "basicAuthPassword": await get_connector_attribute(
221 + connector_id=1,
222 + column_name="connector_password",
223 + session=session,
224 + ),
225 + },
226 + isDefault=False,
227 + jsonData={
228 + "database": f"defender-for-endpoint-{customer_code}*",
229 + "flavor": "opensearch",
230 + "includeFrozen": False,
231 + "logLevelField": "severity",
232 + "logMessageField": "summary",
233 + "maxConcurrentShardRequests": 5,
234 + "pplEnabled": True,
235 + "timeField": "timestamp",
236 + "tlsSkipVerify": True,
237 + "version": await get_opensearch_version(),
238 + },
239 + readOnly=True,
240 + )
241 + results = grafana_client.datasource.create_datasource(
242 + datasource=datasource_payload.dict(),
243 + )
244 + return GrafanaDataSourceCreationResponse(**results)
245 +
246 +
247 +async def create_customer_network_connector_meta(
248 + customer_details,
249 + stream_id,
250 + index_id,
251 + content_pack_stream_id,
252 + content_pack_input_id,
253 + session,
254 +):
255 + """
256 + Create a CustomerNetworkConnectorsMeta object with the provided details.
257 +
258 + Args:
259 + customer_details (CustomerDetails): Details of the customer.
260 + stream_id (int): ID of the Graylog stream.
261 + index_id (int): ID of the Graylog index.
262 + session (Session): Database session.
263 +
264 + Returns:
265 + CustomerNetworkConnectorsMeta: The created CustomerNetworkConnectorsMeta object.
266 + """
267 + return CustomerNetworkConnectorsMeta(
268 + customer_code=customer_details.customer_code,
269 + network_connector_name="DefenderForEndpoint",
270 + graylog_stream_id=stream_id,
271 + graylog_input_id=(await get_input_id_by_input_name(input_name=f"{customer_details.customer_name} - DEFENDER FOR ENDPOINT")),
272 + graylog_pipeline_id="not_set",
273 + graylog_content_pack_input_id=content_pack_input_id,
274 + graylog_content_pack_stream_id=content_pack_stream_id,
275 + grafana_org_id=(
276 + await get_customer_meta_attribute(
277 + session=session,
278 + customer_code=customer_details.customer_code,
279 + column_name="customer_meta_grafana_org_id",
280 + )
281 + ),
282 + graylog_index_id=index_id,
283 + grafana_dashboard_folder_id=None,
284 + grafana_datasource_uid=None,
285 + )
286 +
287 +
288 +async def validate_grafana_organization_id(customer_code, session):
289 + """
290 + Validate the Grafana organization ID for the customer.
291 +
292 + Args:
293 + customer_code (str): The customer code.
294 + session (Session): Database session.
295 +
296 + Returns:
297 + int: The Grafana organization ID.
298 + """
299 + return await get_customer_meta_attribute(session=session, customer_code=customer_code, column_name="customer_meta_grafana_org_id")
300 +
301 +
302 +async def provision_defender_for_endpoint(
303 + customer_details: DefenderForEndpointCustomerDetails,
304 + keys: ProvisionDefenderForEndpointAuthKeys,
305 + session: AsyncSession,
306 +) -> ProvisionDefenderForEndpointResponse:
307 + """
308 + Provisions a Defender For Endpoint customer by performing the following steps:
309 + 1. Provisions the content pack for the customer.
310 + 2. Retrieves the stream and index IDs for the customer.
311 + 3. Creates customer network connector metadata.
312 + 4. Assigns the stream to the index.
313 + 5. Inserts the customer network connector metadata into the database.
314 + 6. Creates a directory for the customer to store the docker compose and falconhose cfg.
315 +
316 + Args:
317 + customer_details (DefenderForEndpointCustomerDetails): The details of the DefenderForEndpoint customer.
318 + keys (ProvisionDefenderForEndpointAuthKeys): The keys required for provisioning.
319 + session (AsyncSession): The database session.
320 +
321 + Returns:
322 + None
323 + """
324 + # If customer name contains a space, replace it with a _
325 + if " " in customer_details.customer_name:
326 + customer_details.customer_name = customer_details.customer_name.replace(" ", "_")
327 + if await validate_grafana_organization_id(customer_details.customer_code, session) is None:
328 + raise HTTPException(status_code=404, detail="Grafana organization ID not found. Please provision Grafana for the customer first.")
329 + logger.info(f"Provisioning Defender For Endpoint for customer {customer_details.customer_name}")
330 + await provision_content_pack(customer_details)
331 + stream_id, index_id, content_pack_stream_id, content_pack_input_id = await get_stream_and_index_ids(customer_details)
332 + customer_network_connector_meta = await create_customer_network_connector_meta(
333 + customer_details,
334 + stream_id,
335 + index_id,
336 + content_pack_stream_id,
337 + content_pack_input_id,
338 + session,
339 + )
340 + await assign_stream_to_index(stream_id=stream_id, index_id=index_id)
341 + # Grafana Deployment
342 + customer_network_connector_meta.grafana_datasource_uid = (
343 + await create_grafana_datasource(
344 + customer_code=customer_details.customer_code,
345 + session=session,
346 + )
347 + ).datasource.uid
348 + grafana_folder = await create_grafana_folder(
349 + organization_id=(
350 + await get_customer_meta(
351 + customer_details.customer_code,
352 + session,
353 + )
354 + ).customer_meta.customer_meta_grafana_org_id,
355 + folder_title="DEFENDER FOR ENDPOINT",
356 + )
357 + await provision_dashboards(
358 + DashboardProvisionRequest(
359 + dashboards=[dashboard.name for dashboard in DefenderForEndpointDashboard],
360 + organizationId=(
361 + await get_customer_meta(
362 + customer_details.customer_code,
363 + session,
364 + )
365 + ).customer_meta.customer_meta_grafana_org_id,
366 + folderId=grafana_folder.id,
367 + datasourceUid=customer_network_connector_meta.grafana_datasource_uid,
368 + ),
369 + )
370 + customer_network_connector_meta.grafana_dashboard_folder_id = grafana_folder.uid
371 + await insert_into_customer_network_connectors_meta_table(
372 + customer_network_connectors_meta=customer_network_connector_meta,
373 + session=session,
374 + )
375 + await create_customer_directory_if_needed(customer_name=customer_details.customer_name)
376 + await create_customer_data_directory(customer_name=customer_details.customer_name)
377 + file = await load_and_replace_docker_compose(customer_name=customer_details.customer_name)
378 + await save_uploaded_file(
379 + file=file,
380 + filename=f"{customer_details.customer_name}_docker-compose-defender-for-endpoint.yml",
381 + customer_name=customer_details.customer_name,
382 + )
383 + await load_and_replace_filebeat_cfg(customer_details=customer_details, keys=keys, session=session)
384 +
385 + await update_customer_integration_table(
386 + customer_code=customer_details.customer_code,
387 + session=session,
388 + )
389 +
390 + return ProvisionDefenderForEndpointResponse(
391 + message="Defender For Endpoint for customer provisioned successfully",
392 + success=True,
393 + )
394 +
395 +
396 +async def insert_into_customer_network_connectors_meta_table(
397 + customer_network_connectors_meta: CustomerNetworkConnectorsMeta,
398 + session: AsyncSession,
399 +) -> None:
400 + """
401 + Insert the customer network connectors meta into the database.
402 +
403 + Args:
404 + customer_network_connectors_meta (CustomerNetworkConnectorsMeta): The customer network connectors meta to insert.
405 + session (AsyncSession): The async session object for database operations.
406 +
407 + Returns:
408 + None
409 + """
410 + logger.info("Inserting customer network connectors meta into the database")
411 + session.add(customer_network_connectors_meta)
412 + await session.commit()
413 + logger.info("Customer network connectors meta inserted successfully")
414 + return None
415 +
416 +
417 +# ! Add the docker-compose.yml file to the `data` folder
418 +project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
419 +UPLOAD_FOLDER = os.path.join(project_root, "data")
420 +
421 +
422 +async def create_customer_directory_if_needed(customer_name: str):
423 + """
424 + Create a directory for the customer in the UPLOAD_FOLDER if it doesn't exist.
425 +
426 + Args:
427 + customer_name (str): The name of the customer.
428 + """
429 + # Create the path to the customer's directory
430 + # If customer name contains a space, replace it with a _
431 + if " " in customer_name:
432 + customer_name = customer_name.replace(" ", "_")
433 + customer_directory = os.path.join(UPLOAD_FOLDER, customer_name)
434 + # Check if the directory exists
435 + if not os.path.exists(customer_directory):
436 + # If it doesn't exist, create it
437 + os.makedirs(customer_directory)
438 +
439 +
440 +async def create_customer_data_directory(customer_name: str):
441 + """
442 + Create a data directory within the customer's directory in the UPLOAD_FOLDER
443 +
444 + This function ensures there's a dedicated data directory for customer-specific files.
445 + For example, if the customer directory is /opt/CoPilot/data/Customer1,
446 + this will create /opt/CoPilot/data/Customer1/data.
447 +
448 + Args:
449 + customer_name (str): The name of the customer.
450 +
451 + Returns:
452 + str: The path to the created data directory
453 + """
454 + # Normalize customer name (replace spaces with underscores)
455 + if " " in customer_name:
456 + customer_name = customer_name.replace(" ", "_")
457 +
458 + # Create the path to the customer's directory
459 + customer_directory = os.path.join(UPLOAD_FOLDER, customer_name)
460 +
461 + # Create the path to the data directory within the customer's directory
462 + customer_data_directory = os.path.join(customer_directory, "data")
463 +
464 + # Check if the customer directory exists, if not create it
465 + if not os.path.exists(customer_directory):
466 + os.makedirs(customer_directory)
467 +
468 + # Check if the data directory exists, if not create it
469 + if not os.path.exists(customer_data_directory):
470 + os.makedirs(customer_data_directory)
471 + logger.info(f"Created data directory for customer {customer_name}: {customer_data_directory}")
472 + else:
473 + logger.info(f"Data directory for customer {customer_name} already exists: {customer_data_directory}")
474 +
475 + return customer_data_directory
476 +
477 +
478 +async def load_and_replace_docker_compose(customer_name: str):
479 + """
480 + Load the docker-compose.yml file and replace the placeholder with the customer name.
481 +
482 + Args:
483 + customer_name (str): The name of the customer.
484 +
485 + Returns:
486 + str: The content of the docker-compose.yml file with the placeholder replaced.
487 + """
488 + # Get the current directory:
489 + current_directory = os.path.dirname(os.path.abspath(__file__))
490 + # Go up one level
491 + parent_directory = os.path.dirname(current_directory)
492 + # If customer name contains a space, replace it with a _
493 + if " " in customer_name:
494 + customer_name = customer_name.replace(" ", "_")
495 + # Open the docker-compose.yml file and read the content
496 + with open(os.path.join(parent_directory, "templates", "docker-compose.yml"), "r") as file:
497 + data = file.read()
498 + data = data.replace("CUSTOMER_NAME", customer_name)
499 + return data
500 +
501 +
502 +async def save_uploaded_file(file, filename, customer_name):
503 + """
504 + Save the uploaded file to the server.
505 +
506 + Args:
507 + file: The file to save.
508 + filename: The name of the file.
509 +
510 + Returns:
511 + str: The path to the saved file.
512 + """
513 + # If customer name contains a space, replace it with a _
514 + if " " in customer_name:
515 + customer_name = customer_name.replace(" ", "_")
516 + customer_upload_folder = os.path.join(UPLOAD_FOLDER, customer_name)
517 + async with aiofiles.open(os.path.join(customer_upload_folder, filename), "wb") as f:
518 + await f.write(file.encode())
519 + return os.path.join(customer_upload_folder, filename)
520 +
521 +
522 +async def load_and_replace_filebeat_cfg(
523 + customer_details: DefenderForEndpointCustomerDetails,
524 + keys: ProvisionDefenderForEndpointAuthKeys,
525 + session: AsyncSession,
526 +):
527 + """
528 + Load the filebeat.yml file and replace the placeholders with the customer details.
529 +
530 + Args:
531 + customer_details (DefenderForEndpointCustomerDetails): The details of the customer.
532 + keys (ProvisionDefenderForEndpointAuthKeys): The authentication keys for DefenderForEndpoint.
533 +
534 + Returns:
535 + str: The content of the filebeat.yml file with the placeholders replaced.
536 + """
537 + # Get the current directory:
538 + current_directory = os.path.dirname(os.path.abspath(__file__))
539 + # Go up one level
540 + parent_directory = os.path.dirname(current_directory)
541 + connector_url = str(await get_connector_attribute(connector_id=3, column_name="connector_url", session=session))
542 + connector_url = connector_url.replace("https://", "").replace("http://", "").replace(":9000", "")
543 + # Open the filebeat.yml file and read the content
544 + with open(os.path.join(parent_directory, "templates", "filebeat.yml"), "r") as file:
545 + data = file.read()
546 + data = data.replace("REPLACE_TENANT_ID", keys.TENANT_ID)
547 + data = data.replace("REPLACE_CLIENT_ID", keys.CLIENT_ID)
548 + data = data.replace("REPLACE_CLIENT_SECRET", keys.CLIENT_SECRET)
549 + data = data.replace("REPLACE_SYSLOG_HOST", connector_url)
550 + data = data.replace("REPLACE_SYSLOG_PORT", keys.SYSLOG_PORT)
551 + # Save the file
552 + # If customer name contains a space, replace it with a _
553 + if " " in customer_details.customer_name:
554 + customer_details.customer_name = customer_details.customer_name.replace(" ", "_")
555 + customer_upload_folder = os.path.join(UPLOAD_FOLDER, customer_details.customer_name)
556 + async with aiofiles.open(os.path.join(customer_upload_folder, "filebeat.yml"), "w") as f:
557 + await f.write(data)
558 + return os.path.join(customer_upload_folder, "filebeat.yml")
559 +
560 +
561 +async def update_customer_integration_table(
562 + customer_code: str,
563 + session: AsyncSession,
564 +) -> None:
565 + """
566 + Updates the `customer_integrations` table to set the `deployed` column to True where the `customer_code`
567 + matches the given customer code and the `integration_service_name` is "DefenderForEndpoint".
568 +
569 + Args:
570 + customer_code (str): The customer code.
571 + session (AsyncSession): The async session object for making HTTP requests.
572 + """
573 + logger.info(f"Updating customer integrations table for customer {customer_code}")
574 + await session.execute(
575 + update(CustomerIntegrations)
576 + .where(
577 + and_(
578 + CustomerIntegrations.customer_code == customer_code,
579 + CustomerIntegrations.integration_service_name == "DefenderForEndpoint",
580 + ),
581 + )
582 + .values(deployed=True),
583 + )
584 + await session.commit()
585 +
586 + return None
backend/app/integrations/defender_for_endpoint/templates/docker-compose.yml new
+13
@@ -0,0 +1,13 @@
1 +version: '3.8'
2 +services:
3 + defender-for-endpoint-connector-CUSTOMER_NAME:
4 + image: docker.elastic.co/beats/filebeat:8.17.2
5 + container_name: defender-for-endpoint-CUSTOMER_NAME
6 + user: root
7 + volumes:
8 + - /opt/CoPilot/data/data/CUSTOMER_NAME/filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
9 + - /opt/CoPilot/data/data/CUSTOMER_NAME/data:/usr/share/filebeat/data
10 + - /var/run/docker.sock:/var/run/docker.sock
11 + - /var/lib/docker/containers/:/var/lib/docker/containers/:ro
12 + - /var/log/:/var/log/:ro
13 + - /var/log/audit/:/var/log/audit/:ro
backend/app/integrations/defender_for_endpoint/templates/filebeat.yml new
+16
@@ -0,0 +1,16 @@
1 +filebeat.modules:
2 +- module: microsoft
3 + defender_atp:
4 + enabled: true
5 + var.oauth2.client.id: "REPLACE_CLIENT_ID"
6 + var.oauth2.client.secret: "REPLACE_CLIENT_SECRET"
7 + var.oauth2.token_url: "https://login.microsoftonline.com/REPLACE_TENANT_ID/oauth2/token"
8 +
9 +filebeat.inputs:
10 +- type: log
11 + enabled: false
12 + paths:
13 + - /var/log/*.log
14 +
15 +output.logstash:
16 + hosts: ["REPLACE_SYSLOG_HOST:REPLACE_SYSLOG_PORT"]
backend/app/integrations/markdown/defenderforendpoint.md new
+57
@@ -0,0 +1,57 @@
1 +# [Defender For Endpoint Integration](https://learn.microsoft.com/en-us/defender-endpoint/api/get-alerts)
2 +
3 +## Prerequisites
4 +
5 +Before using the Defender For Endpoint SIEM Connector, you’ll want to first define the API client and set its scope. Refer to this guide (https://learn.microsoft.com/en-us/defender-endpoint/api/get-alerts) to getting access to the Defender For Endpoint API for setting up a new API client key. For the new API client, make sure the scope includes access for `Alert.Read.All` and `Alert.ReadWrite.All`.
6 +
7 +### IMPORTANT: Make sure your API has the below configred roles:
8 +
9 +`Alert.Read.All, Alert.ReadWrite.All`
10 +
11 +![Defender For Endpoint API Settings](/images/defenderforendpoint/permissions.png)
12 +
13 +## Configuration
14 +
15 +The configuration for our API creds and syslog forwarder settings are stored within `/usr/share/filebeat/filebeat.yml`. Adjust to make your changes. **NOTE that the `tenant_id` , `cliend_id` , `client_secret` , `syslog_port`, and `syslog_host` will need to be updated.** Below is an example, CoPilot will take care of this for you.
16 +
17 +```yaml
18 +filebeat.modules:
19 +- module: microsoft
20 + defender_atp:
21 + enabled: true
22 + var.oauth2.client.id: "CLIENT_ID"
23 + var.oauth2.client.secret: "CLIENT_SECRET"
24 + var.oauth2.token_url: "https://login.microsoftonline.com/TENANT_ID/oauth2/token"
25 +
26 +filebeat.inputs:
27 +- type: log
28 + enabled: false
29 + paths:
30 + - /var/log/*.log
31 +
32 +output.logstash:
33 + hosts: ["REPLACE_SYSLOG_HOST:REPLACE_SYSLOG_PORT"]
34 +```
35 +
36 +## Provisioning
37 +
38 +Once you have saved the Defender For Endpoint configuration for the customer, you are ready to deploy the integration. Navigate to the `Customers` tab and select the appropriate customer. The provisiong creates the necessary:
39 +
40 +- Graylog CEF Input
41 +- Graylog Stream
42 +- Graylog Index
43 +- Grafana Datasource
44 +- Grafana Dashboards
45 +- Defender For Endpoint Docker-Compose File
46 +
47 +## Deployment of Defender For Endpoint Container
48 +
49 +The Defender For Endpoint integration runs via a docker container. During provisioning, the following directory is created `/opt/CoPilot/data/data/CUSTOMER_NAME`. Within this directory will reside the `CUSTOMER_NAME_docker-compose-defender-for-endpoint.yml` and the `filebeat.yml` files. These can be modified if desired but should already contain the details needed to collect logs for their Defender For Endpoint environment.
50 +
51 +Start the container with the below command:
52 +
53 +```bash
54 +docker compose -f /opt/CoPilot/data/data/CUSTOMER_NAME/CUSTOMER_NAME_docker-compose-defender-for-endpoint.yml up -d
55 +```
56 +
57 +You should now see the container running.
backend/app/routers/defenderforendpoint.py new
+15
@@ -0,0 +1,15 @@
1 +from fastapi import APIRouter
2 +
3 +from app.integrations.defender_for_endpoint.routes.provision import (
4 + integration_defender_for_endpoint_router,
5 +)
6 +
7 +# Instantiate the APIRouter
8 +router = APIRouter()
9 +
10 +# Include the Defender For Endpoint related routes
11 +router.include_router(
12 + integration_defender_for_endpoint_router,
13 + prefix="/defender_for_endpoint",
14 + tags=["Defender For Endpoint"],
15 +)
backend/app/routers/wazuh_manager.py
+7
@@ -1,5 +1,6 @@
1 from fastapi import APIRouter
2
3 +from app.connectors.wazuh_manager.routes.mitre import wazuh_manager_mitre_router
4 from app.connectors.wazuh_manager.routes.rules import wazuh_manager_rules_router
5
6 # Instantiate the APIRouter
@@ -11,3 +12,9 @@ router.include_router(
12 prefix="/wazuh_manager",
13 tags=["wazuh-manager"],
14 )
15 +
16 +router.include_router(
17 + wazuh_manager_mitre_router,
18 + prefix="/wazuh_manager/mitre",
19 + tags=["wazuh-manager"],
20 +)
backend/app/schedulers/scheduler.py
+8 -6
@@ -145,12 +145,13 @@ async def initialize_job_metadata():
145 "function": invoke_alert_creation_collect,
146 "description": "Invokes alert creation collection.",
147 },
148 - {
149 - "job_id": "invoke_sigma_queries_collect",
150 - "time_interval": 5,
151 - "function": invoke_sigma_queries_collect,
152 - "description": "Invokes Sigma queries collection.",
153 - },
148 + # ! Mirgrated SIGMA to VELO ! #
149 + # {
150 + # "job_id": "invoke_sigma_queries_collect",
151 + # "time_interval": 5,
152 + # "function": invoke_sigma_queries_collect,
153 + # "description": "Invokes Sigma queries collection.",
154 + # },
155 # {"job_id": "invoke_mimecast_integration", "time_interval": 5, "function": invoke_mimecast_integration}
156 ]
157 for job in known_jobs:
@@ -204,6 +205,7 @@ async def schedule_enabled_jobs(scheduler):
205 "invoke_office365_threat_intel_alert",
206 "wazuh_index_fields_resize",
207 "invoke_huntress_integration_collection",
208 + "invoke_cato_integration_collect",
209 ]
210
211 # Disable each job in the list
backend/app/stack_provisioning/graylog/schema/provision.py
+2
@@ -29,6 +29,8 @@ class AvailableContentPacks(str, Enum):
29 SOCFORTRESS_CROWDSTRIKE_PROCESSING_PIPELINE = "The Crowdstrike Processing Pipeline content pack"
30 SOCFORTRESS_BITDEFENDER_INPUT_TCP = "The Bitdefender Input TCP content pack"
31 SOCFORTRESS_BITDEFENDER_STREAM = "The Bitdefender Stream content pack"
32 + SOCFORTRESS_DEFENDER_FOR_ENDPOINT_INPUT_TCP = "The Defender for Endpoint Input TCP content pack"
33 + SOCFORTRESS_DEFENDER_FOR_ENDPOINT_STREAM = "The Defender for Endpoint Stream content pack"
34
35
36 class ContentPackKeywords(BaseModel):
backend/app/stack_provisioning/graylog/templates/SOCFORTRESS_DEFENDER_FOR_ENDPOINT_INPUT_TCP.json new
+106
@@ -0,0 +1,106 @@
1 +{
2 + "v": 1,
3 + "id": "REPLACE_UUID_GLOBAL",
4 + "rev": 1,
5 + "name": "customer_name_DEFENDER_FOR_ENDPOINT_INPUT_TCP",
6 + "summary": "customer_name_DEFENDER_FOR_ENDPOINT_INPUT_TCP",
7 + "description": "",
8 + "vendor": "SOCFortress",
9 + "url": "",
10 + "parameters": [],
11 + "entities": [
12 + {
13 + "v": "1",
14 + "type": {
15 + "name": "input",
16 + "version": "1"
17 + },
18 + "id": "REPLACE_UUID_SPECIFIC",
19 + "data": {
20 + "title": {
21 + "@type": "string",
22 + "@value": "customer_name - DEFENDER FOR ENDPOINT"
23 + },
24 + "configuration": {
25 + "tls_key_file": {
26 + "@type": "string",
27 + "@value": ""
28 + },
29 + "port": {
30 + "@type": "integer",
31 + "@value": "SYSLOG_PORT"
32 + },
33 + "tls_enable": {
34 + "@type": "boolean",
35 + "@value": false
36 + },
37 + "recv_buffer_size": {
38 + "@type": "integer",
39 + "@value": 1048576
40 + },
41 + "tcp_keepalive": {
42 + "@type": "boolean",
43 + "@value": false
44 + },
45 + "tls_client_auth_cert_file": {
46 + "@type": "string",
47 + "@value": ""
48 + },
49 + "bind_address": {
50 + "@type": "string",
51 + "@value": "0.0.0.0"
52 + },
53 + "no_beats_prefix": {
54 + "@type": "boolean",
55 + "@value": true
56 + },
57 + "tls_cert_file": {
58 + "@type": "string",
59 + "@value": ""
60 + },
61 + "tls_client_auth": {
62 + "@type": "string",
63 + "@value": "disabled"
64 + },
65 + "charset_name": {
66 + "@type": "string",
67 + "@value": "UTF-8"
68 + },
69 + "number_worker_threads": {
70 + "@type": "integer",
71 + "@value": 4
72 + },
73 + "tls_key_password": {
74 + "@type": "string",
75 + "@value": ""
76 + }
77 + },
78 + "static_fields": {
79 + "syslog_type": {
80 + "@type": "string",
81 + "@value": "defender-atp"
82 + },
83 + "syslog_customer": {
84 + "@type": "string",
85 + "@value": "customer_code"
86 + }
87 + },
88 + "type": {
89 + "@type": "string",
90 + "@value": "org.graylog.plugins.beats.Beats2Input"
91 + },
92 + "global": {
93 + "@type": "boolean",
94 + "@value": true
95 + },
96 + "extractors": []
97 + },
98 + "constraints": [
99 + {
100 + "type": "server-version",
101 + "version": ">=5.0.13+083613e"
102 + }
103 + ]
104 + }
105 + ]
106 +}
backend/app/stack_provisioning/graylog/templates/SOCFORTRESS_DEFENDER_FOR_ENDPOINT_STREAM.json new
+102
@@ -0,0 +1,102 @@
1 +{
2 + "v": 1,
3 + "id": "REPLACE_UUID_GLOBAL",
4 + "rev": 1,
5 + "name": "customer_name_DEFENDER_FOR_ENDPOINT_STREAM",
6 + "summary": "customer_name_DEFENDER_FOR_ENDPOINT_STREAM",
7 + "description": "",
8 + "vendor": "SOCFortress",
9 + "url": "",
10 + "parameters": [],
11 + "entities": [
12 + {
13 + "v": "1",
14 + "type": {
15 + "name": "stream",
16 + "version": "1"
17 + },
18 + "id": "REPLACE_UUID_SPECIFIC",
19 + "data": {
20 + "alarm_callbacks": [],
21 + "outputs": [],
22 + "remove_matches": {
23 + "@type": "boolean",
24 + "@value": true
25 + },
26 + "title": {
27 + "@type": "string",
28 + "@value": "customer_name - DEFENDER FOR ENDPOINT LOGS AND EVENTS"
29 + },
30 + "stream_rules": [
31 + {
32 + "type": {
33 + "@type": "string",
34 + "@value": "EXACT"
35 + },
36 + "field": {
37 + "@type": "string",
38 + "@value": "syslog_type"
39 + },
40 + "value": {
41 + "@type": "string",
42 + "@value": "defender-atp"
43 + },
44 + "inverted": {
45 + "@type": "boolean",
46 + "@value": false
47 + },
48 + "description": {
49 + "@type": "string",
50 + "@value": ""
51 + }
52 + },
53 + {
54 + "type": {
55 + "@type": "string",
56 + "@value": "EXACT"
57 + },
58 + "field": {
59 + "@type": "string",
60 + "@value": "syslog_customer"
61 + },
62 + "value": {
63 + "@type": "string",
64 + "@value": "customer_code"
65 + },
66 + "inverted": {
67 + "@type": "boolean",
68 + "@value": false
69 + },
70 + "description": {
71 + "@type": "string",
72 + "@value": ""
73 + }
74 + }
75 + ],
76 + "alert_conditions": [],
77 + "matching_type": {
78 + "@type": "string",
79 + "@value": "AND"
80 + },
81 + "disabled": {
82 + "@type": "boolean",
83 + "@value": false
84 + },
85 + "description": {
86 + "@type": "string",
87 + "@value": "customer_name - DEFENDER FOR ENDPOINT LOGS AND EVENTS"
88 + },
89 + "default_stream": {
90 + "@type": "boolean",
91 + "@value": false
92 + }
93 + },
94 + "constraints": [
95 + {
96 + "type": "server-version",
97 + "version": ">=5.0.13+083613e"
98 + }
99 + ]
100 + }
101 + ]
102 +}
backend/copilot.py
+2
@@ -43,6 +43,7 @@ from app.routers import crowdstrike
43 from app.routers import customer_provisioning
44 from app.routers import customers
45 from app.routers import darktrace
46 +from app.routers import defenderforendpoint
47 from app.routers import dfir_iris
48 from app.routers import dnstwist
49 from app.routers import duo
@@ -162,6 +163,7 @@ api_router.include_router(duo.router)
163 api_router.include_router(portainer.router)
164 api_router.include_router(incidents.router)
165 api_router.include_router(darktrace.router)
166 +api_router.include_router(defenderforendpoint.router)
167
168 # Include the APIRouter in the FastAPI app
169 app.include_router(api_router)
frontend/package.json
+10 -10
@@ -45,9 +45,9 @@
45 "@fontsource/jetbrains-mono": "^5.2.5",
46 "@fontsource/lexend": "^5.2.6",
47 "@fontsource/public-sans": "^5.2.5",
48 - "@shikijs/markdown-it": "^3.2.2",
48 + "@shikijs/markdown-it": "^3.3.0",
49 "@vueuse/core": "^13.1.0",
50 - "axios": "^1.8.4",
50 + "axios": "^1.9.0",
51 "bytes": "^3.1.2",
52 "codemirror": "~6.0.1",
53 "colord": "^2.9.3",
@@ -66,7 +66,7 @@
66 "pinia": "^3.0.2",
67 "pinia-plugin-persistedstate": "^4.2.0",
68 "secure-ls": "^2.0.0",
69 - "shiki": "^3.2.2",
69 + "shiki": "^3.3.0",
70 "thememirror": "^2.0.1",
71 "validator": "^13.15.0",
72 "vue": "^3.5.13",
@@ -74,7 +74,7 @@
74 "vue-codemirror": "^6.1.1",
75 "vue-highlight-words": "^3.0.1",
76 "vue-i18n": "^11.1.3",
77 - "vue-router": "^4.5.0",
77 + "vue-router": "^4.5.1",
78 "vue-sjv": "^0.0.6",
79 "vue3-apexcharts": "^1.8.0",
80 "vue3-marquee": "^4.2.2",
@@ -96,33 +96,33 @@
96 "@types/fs-extra": "^11.0.4",
97 "@types/jsdom": "^21.1.7",
98 "@types/lodash": "^4.17.16",
99 - "@types/node": "^22.14.1",
99 + "@types/node": "^22.15.2",
100 "@types/validator": "^13.15.0",
101 "@vitejs/plugin-vue": "^5.2.3",
102 "@vitejs/plugin-vue-jsx": "^4.1.2",
103 "@vue/test-utils": "^2.4.6",
104 "@vue/tsconfig": "^0.7.0",
105 - "cypress": "^14.3.1",
105 + "cypress": "^14.3.2",
106 "depcheck": "^1.4.7",
107 - "eslint": "^9.25.0",
107 + "eslint": "^9.25.1",
108 "flourite": "^1.3.0",
109 "fs-extra": "^11.3.0",
110 "jsdom": "^26.1.0",
111 "npm-run-all2": "^7.0.2",
112 "prettier": "^3.5.3",
113 "prettier-plugin-tailwindcss": "^0.6.11",
114 - "sass": "^1.86.3",
114 + "sass": "^1.87.0",
115 "start-server-and-test": "^2.0.11",
116 "tailwindcss": "^4.1.4",
117 "taze": "^19.0.4",
118 "type-fest": "^4.40.0",
119 "typescript": "~5.8.3",
120 - "vite": "^6.3.2",
120 + "vite": "^6.3.3",
121 "vite-bundle-visualizer": "^1.2.1",
122 "vite-plugin-vue-devtools": "^7.7.5",
123 "vite-svg-loader": "^5.1.0",
124 "vitest": "^3.1.2",
125 - "vue-tsc": "^2.2.8"
125 + "vue-tsc": "^2.2.10"
126 },
127 "pnpm": {
128 "onlyBuiltDependencies": [
frontend/playground.excalidraw
+95 -337
@@ -4,407 +4,169 @@
4 "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
5 "elements": [
6 {
7 - "id": "7Fo6axMzOAl0YNLNXgEYP",
8 - "type": "rectangle",
9 - "x": 440,
10 - "y": 420,
11 - "width": 840,
12 - "height": 160,
13 - "angle": 0,
14 - "strokeColor": "#1e1e1e",
15 - "backgroundColor": "transparent",
16 - "fillStyle": "solid",
17 - "strokeWidth": 2,
18 - "strokeStyle": "solid",
19 - "roughness": 0,
20 - "opacity": 100,
21 - "groupIds": [],
22 - "frameId": null,
23 - "roundness": {
24 - "type": 3
25 - },
26 - "seed": 1942671124,
27 - "version": 167,
28 - "versionNonce": 528672660,
29 - "isDeleted": false,
30 - "boundElements": null,
31 - "updated": 1726739423410,
32 - "link": null,
33 - "locked": false
34 - },
35 - {
36 - "id": "36I5MO5QmLj0aEBGyca_L",
7 + "id": "mQVpxTBZA5YGRUGbjJ6Mn",
8 "type": "text",
38 - "x": 440,
39 - "y": 160,
40 - "width": 778.125,
41 - "height": 211.2,
9 + "x": 319.17488221177166,
10 + "y": 149.2828766834112,
11 + "width": 935,
12 + "height": 400,
13 "angle": 0,
14 "strokeColor": "#1e1e1e",
15 "backgroundColor": "transparent",
16 "fillStyle": "solid",
17 "strokeWidth": 2,
18 "strokeStyle": "solid",
48 - "roughness": 0,
19 + "roughness": 1,
20 "opacity": 100,
21 "groupIds": [],
22 "frameId": null,
23 + "index": "aC",
24 "roundness": null,
53 - "seed": 676167980,
54 - "version": 54,
55 - "versionNonce": 382397460,
25 + "seed": 45475197,
26 + "version": 37,
27 + "versionNonce": 1854235507,
28 "isDeleted": false,
29 "boundElements": null,
58 - "updated": 1726739651262,
30 + "updated": 1745571308557,
31 "link": null,
32 "locked": false,
61 - "text": "{\n \"id\": 3,\n \"case_id\": 19,\n \"bucket_name\": \"copilot-cases\",\n \"object_key\": \"19/085_sca_policy_results (1) (1).csv\",\n \"file_name\": \"085_sca_policy_results (1) (1).csv\",\n \"content_type\": \"text/csv\",\n \"file_size\": 401516,\n \"upload_time\": \"2024-09-16T14:10:45\",\n \"file_hash\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"\n}",
62 - "fontSize": 16,
63 - "fontFamily": 3,
33 + "text": "{\n \"name\": \"Chainsaw Batch Script Exclusion\",\n \"description\": \"Exclude alerts from chainsaw batch scripts in Windows Temp folder\",\n \"channel\": \"Microsoft-Windows-Sysmon/Operational\",\n \"title\": \"HackTool - Powerup Write Hijack DLL\",\n \"field_matches\": {\n \"TargetFilename\": \"C:\\\\Windows\\\\Temp\\\\chainsaw_batch.bat\"\n },\n \"customer_code\": null,\n \"enabled\": true,\n \"id\": 2,\n \"created_by\": \"taylor\",\n \"created_at\": \"2025-04-21T20:54:21\",\n \"last_matched_at\": \"2025-04-21T21:07:16\",\n \"match_count\": 4\n}",
34 + "fontSize": 20,
35 + "fontFamily": 8,
36 "textAlign": "left",
37 "verticalAlign": "top",
66 - "baseline": 207,
38 "containerId": null,
68 - "originalText": "{\n \"id\": 3,\n \"case_id\": 19,\n \"bucket_name\": \"copilot-cases\",\n \"object_key\": \"19/085_sca_policy_results (1) (1).csv\",\n \"file_name\": \"085_sca_policy_results (1) (1).csv\",\n \"content_type\": \"text/csv\",\n \"file_size\": 401516,\n \"upload_time\": \"2024-09-16T14:10:45\",\n \"file_hash\": \"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"\n}",
69 - "lineHeight": 1.2
39 + "originalText": "{\n \"name\": \"Chainsaw Batch Script Exclusion\",\n \"description\": \"Exclude alerts from chainsaw batch scripts in Windows Temp folder\",\n \"channel\": \"Microsoft-Windows-Sysmon/Operational\",\n \"title\": \"HackTool - Powerup Write Hijack DLL\",\n \"field_matches\": {\n \"TargetFilename\": \"C:\\\\Windows\\\\Temp\\\\chainsaw_batch.bat\"\n },\n \"customer_code\": null,\n \"enabled\": true,\n \"id\": 2,\n \"created_by\": \"taylor\",\n \"created_at\": \"2025-04-21T20:54:21\",\n \"last_matched_at\": \"2025-04-21T21:07:16\",\n \"match_count\": 4\n}",
40 + "autoResize": true,
41 + "lineHeight": 1.25
42 },
43 {
72 - "id": "i2MHZShrXG15f8_KpJHUa",
73 - "type": "text",
74 - "x": 460,
75 - "y": 440,
76 - "width": 28.125,
77 - "height": 19.2,
78 - "angle": 0,
79 - "strokeColor": "#1e1e1e",
80 - "backgroundColor": "transparent",
81 - "fillStyle": "solid",
82 - "strokeWidth": 2,
83 - "strokeStyle": "solid",
84 - "roughness": 0,
85 - "opacity": 100,
86 - "groupIds": [],
87 - "frameId": null,
88 - "roundness": null,
89 - "seed": 1252874900,
90 - "version": 8,
91 - "versionNonce": 1692366228,
92 - "isDeleted": false,
93 - "boundElements": null,
94 - "updated": 1726739399346,
95 - "link": null,
96 - "locked": false,
97 - "text": "#id",
98 - "fontSize": 16,
99 - "fontFamily": 3,
100 - "textAlign": "left",
101 - "verticalAlign": "top",
102 - "baseline": 15,
103 - "containerId": null,
104 - "originalText": "#id",
105 - "lineHeight": 1.2
106 - },
107 - {
108 - "id": "RV9oLEMrjlWKdxG7pwgAe",
109 - "type": "text",
110 - "x": 520,
111 - "y": 440,
112 - "width": 56.25,
113 - "height": 19.2,
114 - "angle": 0,
115 - "strokeColor": "#1e1e1e",
116 - "backgroundColor": "transparent",
117 - "fillStyle": "solid",
118 - "strokeWidth": 2,
119 - "strokeStyle": "solid",
120 - "roughness": 0,
121 - "opacity": 100,
122 - "groupIds": [],
123 - "frameId": null,
124 - "roundness": null,
125 - "seed": 2094536620,
126 - "version": 28,
127 - "versionNonce": 1125859092,
128 - "isDeleted": false,
129 - "boundElements": null,
130 - "updated": 1726739611746,
131 - "link": null,
132 - "locked": false,
133 - "text": "bucket",
134 - "fontSize": 16,
135 - "fontFamily": 3,
136 - "textAlign": "left",
137 - "verticalAlign": "top",
138 - "baseline": 15,
139 - "containerId": null,
140 - "originalText": "bucket",
141 - "lineHeight": 1.2
142 - },
143 - {
144 - "id": "oGzleD2uOanLNkXxoMEPo",
145 - "type": "text",
146 - "x": 460,
147 - "y": 480,
148 - "width": 84.375,
149 - "height": 19.2,
44 + "id": "JL5Np7xAzu_YrV1Ab3c9d",
45 + "type": "rectangle",
46 + "x": 326.692207056782,
47 + "y": 172.4386048973996,
48 + "width": 978.6013532223594,
49 + "height": 22.999014025039685,
50 "angle": 0,
151 - "strokeColor": "#1e1e1e",
51 + "strokeColor": "#e03131",
52 "backgroundColor": "transparent",
53 "fillStyle": "solid",
54 "strokeWidth": 2,
55 "strokeStyle": "solid",
156 - "roughness": 0,
157 - "opacity": 100,
158 - "groupIds": [],
159 - "frameId": null,
160 - "roundness": null,
161 - "seed": 1364832940,
162 - "version": 25,
163 - "versionNonce": 683650348,
164 - "isDeleted": false,
165 - "boundElements": null,
166 - "updated": 1726739512763,
167 - "link": null,
168 - "locked": false,
169 - "text": "file_name",
170 - "fontSize": 16,
171 - "fontFamily": 3,
172 - "textAlign": "left",
173 - "verticalAlign": "top",
174 - "baseline": 15,
175 - "containerId": null,
176 - "originalText": "file_name",
177 - "lineHeight": 1.2
178 - },
179 - {
180 - "id": "rDKnphsatsGrmf3fpf1cV",
181 - "type": "line",
182 - "x": 440,
183 - "y": 520,
184 - "width": 840,
185 - "height": 0,
186 - "angle": 0,
187 - "strokeColor": "#1e1e1e",
188 - "backgroundColor": "transparent",
189 - "fillStyle": "solid",
190 - "strokeWidth": 2,
191 - "strokeStyle": "solid",
192 - "roughness": 0,
56 + "roughness": 1,
57 "opacity": 100,
58 "groupIds": [],
59 "frameId": null,
60 + "index": "aD",
61 "roundness": {
197 - "type": 2
62 + "type": 3
63 },
199 - "seed": 1266635308,
200 - "version": 28,
201 - "versionNonce": 543171988,
64 + "seed": 946660285,
65 + "version": 130,
66 + "versionNonce": 172096541,
67 "isDeleted": false,
68 "boundElements": null,
204 - "updated": 1726739407013,
69 + "updated": 1745571302740,
70 "link": null,
206 - "locked": false,
207 - "points": [
208 - [
209 - 0,
210 - 0
211 - ],
212 - [
213 - 840,
214 - 0
215 - ]
216 - ],
217 - "lastCommittedPoint": null,
218 - "startBinding": null,
219 - "endBinding": null,
220 - "startArrowhead": null,
221 - "endArrowhead": null
71 + "locked": false
72 },
73 {
224 - "id": "wI1C-_mmlekQJDgJXUWh-",
74 + "id": "uUWn4ah6Xwwodr2_dnxLk",
75 "type": "rectangle",
226 - "x": 1140,
227 - "y": 540,
228 - "width": 120,
229 - "height": 29.2,
76 + "x": 334.18897530708597,
77 + "y": 248.84138303692623,
78 + "width": 544.6616327224424,
79 + "height": 22.40998223394439,
80 "angle": 0,
231 - "strokeColor": "#1e1e1e",
81 + "strokeColor": "#e03131",
82 "backgroundColor": "transparent",
83 "fillStyle": "solid",
84 "strokeWidth": 2,
85 "strokeStyle": "solid",
236 - "roughness": 0,
86 + "roughness": 1,
87 "opacity": 100,
88 "groupIds": [],
89 "frameId": null,
90 + "index": "aE",
91 "roundness": {
92 "type": 3
93 },
243 - "seed": 1839046676,
244 - "version": 22,
245 - "versionNonce": 634539436,
246 - "isDeleted": false,
247 - "boundElements": [
248 - {
249 - "type": "text",
250 - "id": "10oLnLmiy98yqT-l5-bnP"
251 - }
252 - ],
253 - "updated": 1726739458908,
254 - "link": null,
255 - "locked": false
256 - },
257 - {
258 - "id": "10oLnLmiy98yqT-l5-bnP",
259 - "type": "text",
260 - "x": 1171.875,
261 - "y": 545,
262 - "width": 56.25,
263 - "height": 19.2,
264 - "angle": 0,
265 - "strokeColor": "#1e1e1e",
266 - "backgroundColor": "transparent",
267 - "fillStyle": "solid",
268 - "strokeWidth": 2,
269 - "strokeStyle": "solid",
270 - "roughness": 0,
271 - "opacity": 100,
272 - "groupIds": [],
273 - "frameId": null,
274 - "roundness": null,
275 - "seed": 1412661524,
276 - "version": 22,
277 - "versionNonce": 1292332,
278 - "isDeleted": false,
279 - "boundElements": null,
280 - "updated": 1726739458908,
281 - "link": null,
282 - "locked": false,
283 - "text": "delete",
284 - "fontSize": 16,
285 - "fontFamily": 3,
286 - "textAlign": "center",
287 - "verticalAlign": "middle",
288 - "baseline": 15,
289 - "containerId": "wI1C-_mmlekQJDgJXUWh-",
290 - "originalText": "delete",
291 - "lineHeight": 1.2
292 - },
293 - {
294 - "id": "v0F2yt8m1v5viu5ZRzoJ8",
295 - "type": "text",
296 - "x": 1140,
297 - "y": 440,
298 - "width": 103.125,
299 - "height": 19.2,
300 - "angle": 0,
301 - "strokeColor": "#1e1e1e",
302 - "backgroundColor": "transparent",
303 - "fillStyle": "solid",
304 - "strokeWidth": 2,
305 - "strokeStyle": "solid",
306 - "roughness": 0,
307 - "opacity": 100,
308 - "groupIds": [],
309 - "frameId": null,
310 - "roundness": null,
311 - "seed": 435175444,
312 - "version": 42,
313 - "versionNonce": 483035156,
94 + "seed": 1114755485,
95 + "version": 74,
96 + "versionNonce": 344193917,
97 "isDeleted": false,
98 "boundElements": null,
316 - "updated": 1726739608363,
99 + "updated": 1745571332457,
100 "link": null,
318 - "locked": false,
319 - "text": "upload_time",
320 - "fontSize": 16,
321 - "fontFamily": 3,
322 - "textAlign": "left",
323 - "verticalAlign": "top",
324 - "baseline": 15,
325 - "containerId": null,
326 - "originalText": "upload_time",
327 - "lineHeight": 1.2
101 + "locked": false
102 },
103 {
330 - "id": "bEDavW5JTgpbaW0h7ISKK",
331 - "type": "text",
332 - "x": 560,
333 - "y": 480,
334 - "width": 84.375,
335 - "height": 19.2,
104 + "id": "QfOm3UONPP8AubL5tz7IT",
105 + "type": "rectangle",
106 + "x": 334.4245880235241,
107 + "y": 346.8776924130443,
108 + "width": 363.62538914653015,
109 + "height": 50.75312105455794,
110 "angle": 0,
337 - "strokeColor": "#1e1e1e",
111 + "strokeColor": "#e03131",
112 "backgroundColor": "transparent",
113 "fillStyle": "solid",
114 "strokeWidth": 2,
115 "strokeStyle": "solid",
342 - "roughness": 0,
116 + "roughness": 1,
117 "opacity": 100,
118 "groupIds": [],
119 "frameId": null,
346 - "roundness": null,
347 - "seed": 259937556,
348 - "version": 31,
349 - "versionNonce": 1471891116,
120 + "index": "aF",
121 + "roundness": {
122 + "type": 3
123 + },
124 + "seed": 1221690333,
125 + "version": 99,
126 + "versionNonce": 1467603987,
127 "isDeleted": false,
128 "boundElements": null,
352 - "updated": 1726739517230,
129 + "updated": 1745571341923,
130 "link": null,
354 - "locked": false,
355 - "text": "file_hash",
356 - "fontSize": 16,
357 - "fontFamily": 3,
358 - "textAlign": "left",
359 - "verticalAlign": "top",
360 - "baseline": 15,
361 - "containerId": null,
362 - "originalText": "file_hash",
363 - "lineHeight": 1.2
131 + "locked": false
132 },
133 {
366 - "id": "JyCZfLdDkRbsznPGG0hIm",
367 - "type": "text",
368 - "x": 600,
369 - "y": 540,
370 - "width": 84.375,
371 - "height": 19.2,
134 + "id": "E4CutLyujjhCTGN7K4zmr",
135 + "type": "rectangle",
136 + "x": 341.5250985234549,
137 + "y": 473.8193982285487,
138 + "width": 459.412668047557,
139 + "height": 50.06770224310162,
140 "angle": 0,
373 - "strokeColor": "#1e1e1e",
141 + "strokeColor": "#e03131",
142 "backgroundColor": "transparent",
143 "fillStyle": "solid",
144 "strokeWidth": 2,
145 "strokeStyle": "solid",
378 - "roughness": 0,
146 + "roughness": 1,
147 "opacity": 100,
148 "groupIds": [],
149 "frameId": null,
382 - "roundness": null,
383 - "seed": 235304980,
384 - "version": 68,
385 - "versionNonce": 1453589396,
150 + "index": "aG",
151 + "roundness": {
152 + "type": 3
153 + },
154 + "seed": 1787711731,
155 + "version": 94,
156 + "versionNonce": 1717477715,
157 "isDeleted": false,
158 "boundElements": null,
388 - "updated": 1726739620062,
159 + "updated": 1745612918221,
160 "link": null,
390 - "locked": false,
391 - "text": "file_size",
392 - "fontSize": 16,
393 - "fontFamily": 3,
394 - "textAlign": "left",
395 - "verticalAlign": "top",
396 - "baseline": 15,
397 - "containerId": null,
398 - "originalText": "file_size",
399 - "lineHeight": 1.2
161 + "locked": false
162 },
163 {
402 - "id": "2q1BqKBXk22KA1zzteQpE",
403 - "type": "text",
404 - "x": 460,
405 - "y": 540,
406 - "width": 112.5,
407 - "height": 19.2,
164 + "id": "P_rBULtWGP_rRJYremkiV",
165 + "type": "rectangle",
166 + "x": 322.0977590862386,
167 + "y": 619.310250629091,
168 + "width": 849.3677782560479,
169 + "height": 241.18174428120813,
170 "angle": 0,
171 "strokeColor": "#1e1e1e",
172 "backgroundColor": "transparent",
@@ -415,28 +177,24 @@
177 "opacity": 100,
178 "groupIds": [],
179 "frameId": null,
418 - "roundness": null,
419 - "seed": 1377385492,
180 + "index": "aH",
181 + "roundness": {
182 + "type": 3
183 + },
184 + "seed": 718186643,
185 "version": 65,
421 - "versionNonce": 1912696212,
186 + "versionNonce": 1091791389,
187 "isDeleted": false,
188 "boundElements": null,
424 - "updated": 1726739618846,
189 + "updated": 1745612941858,
190 "link": null,
426 - "locked": false,
427 - "text": "content_type",
428 - "fontSize": 16,
429 - "fontFamily": 3,
430 - "textAlign": "left",
431 - "verticalAlign": "top",
432 - "baseline": 15,
433 - "containerId": null,
434 - "originalText": "content_type",
435 - "lineHeight": 1.2
191 + "locked": false
192 }
193 ],
194 "appState": {
195 "gridSize": 20,
196 + "gridStep": 5,
197 + "gridModeEnabled": false,
198 "viewBackgroundColor": "#ffffff"
199 },
200 "files": {}
frontend/pnpm-lock.yaml
+255 -255
@@ -36,14 +36,14 @@ importers:
36 specifier: ^5.2.5
37 version: 5.2.5
38 '@shikijs/markdown-it':
39 - specifier: ^3.2.2
40 - version: 3.2.2
39 + specifier: ^3.3.0
40 + version: 3.3.0
41 '@vueuse/core':
42 specifier: ^13.1.0
43 version: 13.1.0(vue@3.5.13(typescript@5.8.3))
44 axios:
45 - specifier: ^1.8.4
46 - version: 1.8.4(debug@4.4.0)
45 + specifier: ^1.9.0
46 + version: 1.9.0(debug@4.4.0)
47 bytes:
48 specifier: ^3.1.2
49 version: 3.1.2
@@ -99,8 +99,8 @@ importers:
99 specifier: ^2.0.0
100 version: 2.0.0
101 shiki:
102 - specifier: ^3.2.2
103 - version: 3.2.2
102 + specifier: ^3.3.0
103 + version: 3.3.0
104 thememirror:
105 specifier: ^2.0.1
106 version: 2.0.1(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.5)
@@ -123,8 +123,8 @@ importers:
123 specifier: ^11.1.3
124 version: 11.1.3(vue@3.5.13(typescript@5.8.3))
125 vue-router:
126 - specifier: ^4.5.0
127 - version: 4.5.0(vue@3.5.13(typescript@5.8.3))
126 + specifier: ^4.5.1
127 + version: 4.5.1(vue@3.5.13(typescript@5.8.3))
128 vue-sjv:
129 specifier: ^0.0.6
130 version: 0.0.6(vue@3.5.13(typescript@5.8.3))
@@ -140,7 +140,7 @@ importers:
140 devDependencies:
141 '@antfu/eslint-config':
142 specifier: ^4.12.0
143 - version: 4.12.0(@typescript-eslint/utils@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))
143 + version: 4.12.0(@typescript-eslint/utils@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))
144 '@clack/prompts':
145 specifier: ^0.10.1
146 version: 0.10.1
@@ -149,7 +149,7 @@ importers:
149 version: 4.3.0(vue@3.5.13(typescript@5.8.3))
150 '@tailwindcss/vite':
151 specifier: ^4.1.4
152 - version: 4.1.4(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))
152 + version: 4.1.4(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))
153 '@tsconfig/node20':
154 specifier: ^20.1.5
155 version: 20.1.5
@@ -169,17 +169,17 @@ importers:
169 specifier: ^4.17.16
170 version: 4.17.16
171 '@types/node':
172 - specifier: ^22.14.1
173 - version: 22.14.1
172 + specifier: ^22.15.2
173 + version: 22.15.2
174 '@types/validator':
175 specifier: ^13.15.0
176 version: 13.15.0
177 '@vitejs/plugin-vue':
178 specifier: ^5.2.3
179 - version: 5.2.3(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))
179 + version: 5.2.3(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))
180 '@vitejs/plugin-vue-jsx':
181 specifier: ^4.1.2
182 - version: 4.1.2(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))
182 + version: 4.1.2(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))
183 '@vue/test-utils':
184 specifier: ^2.4.6
185 version: 2.4.6
@@ -187,14 +187,14 @@ importers:
187 specifier: ^0.7.0
188 version: 0.7.0(typescript@5.8.3)(vue@3.5.13(typescript@5.8.3))
189 cypress:
190 - specifier: ^14.3.1
191 - version: 14.3.1
190 + specifier: ^14.3.2
191 + version: 14.3.2
192 depcheck:
193 specifier: ^1.4.7
194 version: 1.4.7
195 eslint:
196 - specifier: ^9.25.0
197 - version: 9.25.0(jiti@2.4.2)
196 + specifier: ^9.25.1
197 + version: 9.25.1(jiti@2.4.2)
198 flourite:
199 specifier: ^1.3.0
200 version: 1.3.0
@@ -214,8 +214,8 @@ importers:
214 specifier: ^0.6.11
215 version: 0.6.11(prettier@3.5.3)
216 sass:
217 - specifier: ^1.86.3
218 - version: 1.86.3
217 + specifier: ^1.87.0
218 + version: 1.87.0
219 start-server-and-test:
220 specifier: ^2.0.11
221 version: 2.0.11
@@ -232,23 +232,23 @@ importers:
232 specifier: ~5.8.3
233 version: 5.8.3
234 vite:
235 - specifier: ^6.3.2
236 - version: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
235 + specifier: ^6.3.3
236 + version: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
237 vite-bundle-visualizer:
238 specifier: ^1.2.1
239 version: 1.2.1(rollup@4.39.0)
240 vite-plugin-vue-devtools:
241 specifier: ^7.7.5
242 - version: 7.7.5(rollup@4.39.0)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))
242 + version: 7.7.5(rollup@4.39.0)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))
243 vite-svg-loader:
244 specifier: ^5.1.0
245 version: 5.1.0(vue@3.5.13(typescript@5.8.3))
246 vitest:
247 specifier: ^3.1.2
248 - version: 3.1.2(@types/debug@4.1.12)(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
248 + version: 3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
249 vue-tsc:
250 - specifier: ^2.2.8
251 - version: 2.2.8(typescript@5.8.3)
250 + specifier: ^2.2.10
251 + version: 2.2.10(typescript@5.8.3)
252 optionalDependencies:
253 '@rollup/rollup-linux-x64-gnu':
254 specifier: ^4.40.0
@@ -767,8 +767,8 @@ packages:
767 resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
768 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
769
770 - '@eslint/js@9.25.0':
771 - resolution: {integrity: sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==}
770 + '@eslint/js@9.25.1':
771 + resolution: {integrity: sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==}
772 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
773
774 '@eslint/markdown@6.3.0':
@@ -1130,31 +1130,31 @@ packages:
1130 '@sec-ant/readable-stream@0.4.1':
1131 resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
1132
1133 - '@shikijs/core@3.2.2':
1134 - resolution: {integrity: sha512-yvlSKVMLjddAGBa2Yu+vUZxuu3sClOWW1AG+UtJkvejYuGM5BVL35s6Ijiwb75O9QdEx6IkMxinHZSi8ZyrBaA==}
1133 + '@shikijs/core@3.3.0':
1134 + resolution: {integrity: sha512-CovkFL2WVaHk6PCrwv6ctlmD4SS1qtIfN8yEyDXDYWh4ONvomdM9MaFw20qHuqJOcb8/xrkqoWQRJ//X10phOQ==}
1135
1136 - '@shikijs/engine-javascript@3.2.2':
1137 - resolution: {integrity: sha512-tlDKfhWpF4jKLUyVAnmL+ggIC+0VyteNsUpBzh1iwWLZu4i+PelIRr0TNur6pRRo5UZIv3ss/PLMuwahg9S2hg==}
1136 + '@shikijs/engine-javascript@3.3.0':
1137 + resolution: {integrity: sha512-XlhnFGv0glq7pfsoN0KyBCz9FJU678LZdQ2LqlIdAj6JKsg5xpYKay3DkazXWExp3DTJJK9rMOuGzU2911pg7Q==}
1138
1139 - '@shikijs/engine-oniguruma@3.2.2':
1140 - resolution: {integrity: sha512-vyXRnWVCSvokwbaUD/8uPn6Gqsf5Hv7XwcW4AgiU4Z2qwy19sdr6VGzMdheKKN58tJOOe5MIKiNb901bgcUXYQ==}
1139 + '@shikijs/engine-oniguruma@3.3.0':
1140 + resolution: {integrity: sha512-l0vIw+GxeNU7uGnsu6B+Crpeqf+WTQ2Va71cHb5ZYWEVEPdfYwY5kXwYqRJwHrxz9WH+pjSpXQz+TJgAsrkA5A==}
1141
1142 - '@shikijs/langs@3.2.2':
1143 - resolution: {integrity: sha512-NY0Urg2dV9ETt3JIOWoMPuoDNwte3geLZ4M1nrPHbkDS8dWMpKcEwlqiEIGqtwZNmt5gKyWpR26ln2Bg2ecPgw==}
1142 + '@shikijs/langs@3.3.0':
1143 + resolution: {integrity: sha512-zt6Kf/7XpBQKSI9eqku+arLkAcDQ3NHJO6zFjiChI8w0Oz6Jjjay7pToottjQGjSDCFk++R85643WbyINcuL+g==}
1144
1145 - '@shikijs/markdown-it@3.2.2':
1146 - resolution: {integrity: sha512-abxppHBxksFKhAHn/nM/VAktZVMOtigPgWFuokENJ0jPAoqMs4Xn7zMCjizftgld0B+JbM7IGGJsC2qaP4j0OQ==}
1145 + '@shikijs/markdown-it@3.3.0':
1146 + resolution: {integrity: sha512-8cBI+tmDwIOAL+mSI3nU0rhyyvf4Qy3WoPIyZXVnRm1UJNyybxK+h+b0Zwa58UylBGXlw/eMLhKaYVztlgvkYw==}
1147 peerDependencies:
1148 markdown-it-async: ^2.2.0
1149 peerDependenciesMeta:
1150 markdown-it-async:
1151 optional: true
1152
1153 - '@shikijs/themes@3.2.2':
1154 - resolution: {integrity: sha512-Zuq4lgAxVKkb0FFdhHSdDkALuRpsj1so1JdihjKNQfgM78EHxV2JhO10qPsMrm01FkE3mDRTdF68wfmsqjt6HA==}
1153 + '@shikijs/themes@3.3.0':
1154 + resolution: {integrity: sha512-tXeCvLXBnqq34B0YZUEaAD1lD4lmN6TOHAhnHacj4Owh7Ptb/rf5XCDeROZt2rEOk5yuka3OOW2zLqClV7/SOg==}
1155
1156 - '@shikijs/types@3.2.2':
1157 - resolution: {integrity: sha512-a5TiHk7EH5Lso8sHcLHbVNNhWKP0Wi3yVnXnu73g86n3WoDgEra7n3KszyeCGuyoagspQ2fzvy4cpSc8pKhb0A==}
1156 + '@shikijs/types@3.3.0':
1157 + resolution: {integrity: sha512-KPCGnHG6k06QG/2pnYGbFtFvpVJmC3uIpXrAiPrawETifujPBv0Se2oUxm5qYgjCvGJS9InKvjytOdN+bGuX+Q==}
1158
1159 '@shikijs/vscode-textmate@10.0.2':
1160 resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
@@ -1358,8 +1358,8 @@ packages:
1358 '@types/ms@2.1.0':
1359 resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
1360
1361 - '@types/node@22.14.1':
1362 - resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==}
1361 + '@types/node@22.15.2':
1362 + resolution: {integrity: sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==}
1363
1364 '@types/normalize-package-data@2.4.4':
1365 resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
@@ -1662,8 +1662,8 @@ packages:
1662 '@vue/devtools-shared@7.7.5':
1663 resolution: {integrity: sha512-QBjG72RfpM0DKtpns2RZOxBltO226kOAls9e4Lri6YxS2gWTgL0H+wj1R2K76lxxIeOrqo4+2Ty6RQnzv+WSTQ==}
1664
1665 - '@vue/language-core@2.2.8':
1666 - resolution: {integrity: sha512-rrzB0wPGBvcwaSNRriVWdNAbHQWSf0NlGqgKHK5mEkXpefjUlVRP62u03KvwZpvKVjRnBIQ/Lwre+Mx9N6juUQ==}
1665 + '@vue/language-core@2.2.10':
1666 + resolution: {integrity: sha512-+yNoYx6XIKuAO8Mqh1vGytu8jkFEOH5C8iOv3i8Z/65A7x9iAOXA97Q+PqZ3nlm2lxf5rOJuIGI/wDtx/riNYw==}
1667 peerDependencies:
1668 typescript: '*'
1669 peerDependenciesMeta:
@@ -1838,8 +1838,8 @@ packages:
1838 aws4@1.13.2:
1839 resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==}
1840
1841 - axios@1.8.4:
1842 - resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
1841 + axios@1.9.0:
1842 + resolution: {integrity: sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==}
1843
1844 balanced-match@1.0.2:
1845 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@@ -2131,8 +2131,8 @@ packages:
2131 csstype@3.1.3:
2132 resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
2133
2134 - cypress@14.3.1:
2135 - resolution: {integrity: sha512-/2q06qvHMK3PNiadnRW1Je0lJ43gAFPQJUAK2zIxjr22kugtWxVQznTBLVu1AvRH+RP3oWZhCdWqiEi+0NuqCg==}
2134 + cypress@14.3.2:
2135 + resolution: {integrity: sha512-n+yGD2ZFFKgy7I3YtVpZ7BcFYrrDMcKj713eOZdtxPttpBjCyw/R8dLlFSsJPouneGN7A/HOSRyPJ5+3/gKDoA==}
2136 engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
2137 hasBin: true
2138
@@ -2524,8 +2524,8 @@ packages:
2524 resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
2525 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
2526
2527 - eslint@9.25.0:
2528 - resolution: {integrity: sha512-MsBdObhM4cEwkzCiraDv7A6txFXEqtNXOb877TsSp2FCkBNl8JfVQrmiuDqC1IkejT6JLPzYBXx/xAiYhyzgGA==}
2527 + eslint@9.25.1:
2528 + resolution: {integrity: sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==}
2529 engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
2530 hasBin: true
2531 peerDependencies:
@@ -3631,11 +3631,11 @@ packages:
3631 resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
3632 engines: {node: '>=18'}
3633
3634 - oniguruma-parser@0.5.4:
3635 - resolution: {integrity: sha512-yNxcQ8sKvURiTwP0mV6bLQCYE7NKfKRRWunhbZnXgxSmB1OXa1lHrN3o4DZd+0Si0kU5blidK7BcROO8qv5TZA==}
3634 + oniguruma-parser@0.11.2:
3635 + resolution: {integrity: sha512-F7Ld4oDZJCI5/wCZ8AOffQbqjSzIRpKH7I/iuSs1SkhZeCj0wS6PMZ4W6VA16TWHrAo0Y9bBKEJOe7tvwcTXnw==}
3636
3637 - oniguruma-to-es@4.1.0:
3638 - resolution: {integrity: sha512-SNwG909cSLo4vPyyPbU/VJkEc9WOXqu2ycBlfd1UCXLqk1IijcQktSBb2yRQ2UFPsDhpkaf+C1dtT3PkLK/yWA==}
3637 + oniguruma-to-es@4.2.0:
3638 + resolution: {integrity: sha512-MDPs6KSOLS0tKQ7joqg44dRIRZUyotfTy0r+7oEEs6VwWWP0+E2PPDYWMFN0aqOjRyWHBYq7RfKw9GQk2S2z5g==}
3639
3640 open@10.1.0:
3641 resolution: {integrity: sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==}
@@ -4065,8 +4065,8 @@ packages:
4065 safer-buffer@2.1.2:
4066 resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
4067
4068 - sass@1.86.3:
4069 - resolution: {integrity: sha512-iGtg8kus4GrsGLRDLRBRHY9dNVA78ZaS7xr01cWnS7PEMQyFtTqBiyCrfpTYTZXRWM94akzckYjh8oADfFNTzw==}
4068 + sass@1.87.0:
4069 + resolution: {integrity: sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==}
4070 engines: {node: '>=14.0.0'}
4071 hasBin: true
4072
@@ -4112,8 +4112,8 @@ packages:
4112 resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==}
4113 engines: {node: '>= 0.4'}
4114
4115 - shiki@3.2.2:
4116 - resolution: {integrity: sha512-0qWBkM2t/0NXPRcVgtLhtHv6Ak3Q5yI4K/ggMqcgLRKm4+pCs3namgZlhlat/7u2CuqNtlShNs9lENOG6n7UaQ==}
4115 + shiki@3.3.0:
4116 + resolution: {integrity: sha512-j0Z1tG5vlOFGW8JVj0Cpuatzvshes7VJy5ncDmmMaYcmnGW0Js1N81TOW98ivTFNZfKRn9uwEg/aIm638o368g==}
4117
4118 side-channel-list@1.0.0:
4119 resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
@@ -4573,8 +4573,8 @@ packages:
4573 peerDependencies:
4574 vue: '>=3.2.13'
4575
4576 - vite@6.3.2:
4577 - resolution: {integrity: sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg==}
4576 + vite@6.3.3:
4577 + resolution: {integrity: sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==}
4578 engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
4579 hasBin: true
4580 peerDependencies:
@@ -4681,8 +4681,8 @@ packages:
4681 peerDependencies:
4682 vue: ^3.0.0
4683
4684 - vue-router@4.5.0:
4685 - resolution: {integrity: sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==}
4684 + vue-router@4.5.1:
4685 + resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==}
4686 peerDependencies:
4687 vue: ^3.2.0
4688
@@ -4691,8 +4691,8 @@ packages:
4691 peerDependencies:
4692 vue: ^3.3.4
4693
4694 - vue-tsc@2.2.8:
4695 - resolution: {integrity: sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ==}
4694 + vue-tsc@2.2.10:
4695 + resolution: {integrity: sha512-jWZ1xSaNbabEV3whpIDMbjVSVawjAyW+x1n3JeGQo7S0uv2n9F/JMgWW90tGWNFRKya4YwKMZgCtr0vRAM7DeQ==}
4696 hasBin: true
4697 peerDependencies:
4698 typescript: '>=5.0.0'
@@ -4883,44 +4883,44 @@ snapshots:
4883 '@jridgewell/gen-mapping': 0.3.8
4884 '@jridgewell/trace-mapping': 0.3.25
4885
4886 - '@antfu/eslint-config@4.12.0(@typescript-eslint/utils@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))':
4886 + '@antfu/eslint-config@4.12.0(@typescript-eslint/utils@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(@vue/compiler-sfc@3.5.13)(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))':
4887 dependencies:
4888 '@antfu/install-pkg': 1.0.0
4889 '@clack/prompts': 0.10.1
4890 - '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.25.0(jiti@2.4.2))
4890 + '@eslint-community/eslint-plugin-eslint-comments': 4.4.1(eslint@9.25.1(jiti@2.4.2))
4891 '@eslint/markdown': 6.3.0
4892 - '@stylistic/eslint-plugin': 4.2.0(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
4893 - '@typescript-eslint/eslint-plugin': 8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
4894 - '@typescript-eslint/parser': 8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
4895 - '@vitest/eslint-plugin': 1.1.43(@typescript-eslint/utils@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))
4892 + '@stylistic/eslint-plugin': 4.2.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
4893 + '@typescript-eslint/eslint-plugin': 8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
4894 + '@typescript-eslint/parser': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
4895 + '@vitest/eslint-plugin': 1.1.43(@typescript-eslint/utils@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))
4896 ansis: 3.17.0
4897 cac: 6.7.14
4898 - eslint: 9.25.0(jiti@2.4.2)
4899 - eslint-config-flat-gitignore: 2.1.0(eslint@9.25.0(jiti@2.4.2))
4898 + eslint: 9.25.1(jiti@2.4.2)
4899 + eslint-config-flat-gitignore: 2.1.0(eslint@9.25.1(jiti@2.4.2))
4900 eslint-flat-config-utils: 2.0.1
4901 - eslint-merge-processors: 2.0.0(eslint@9.25.0(jiti@2.4.2))
4902 - eslint-plugin-antfu: 3.1.1(eslint@9.25.0(jiti@2.4.2))
4903 - eslint-plugin-command: 3.2.0(eslint@9.25.0(jiti@2.4.2))
4904 - eslint-plugin-import-x: 4.10.6(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
4905 - eslint-plugin-jsdoc: 50.6.9(eslint@9.25.0(jiti@2.4.2))
4906 - eslint-plugin-jsonc: 2.20.0(eslint@9.25.0(jiti@2.4.2))
4907 - eslint-plugin-n: 17.17.0(eslint@9.25.0(jiti@2.4.2))
4901 + eslint-merge-processors: 2.0.0(eslint@9.25.1(jiti@2.4.2))
4902 + eslint-plugin-antfu: 3.1.1(eslint@9.25.1(jiti@2.4.2))
4903 + eslint-plugin-command: 3.2.0(eslint@9.25.1(jiti@2.4.2))
4904 + eslint-plugin-import-x: 4.10.6(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
4905 + eslint-plugin-jsdoc: 50.6.9(eslint@9.25.1(jiti@2.4.2))
4906 + eslint-plugin-jsonc: 2.20.0(eslint@9.25.1(jiti@2.4.2))
4907 + eslint-plugin-n: 17.17.0(eslint@9.25.1(jiti@2.4.2))
4908 eslint-plugin-no-only-tests: 3.3.0
4909 - eslint-plugin-perfectionist: 4.11.0(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
4910 - eslint-plugin-pnpm: 0.3.1(eslint@9.25.0(jiti@2.4.2))
4911 - eslint-plugin-regexp: 2.7.0(eslint@9.25.0(jiti@2.4.2))
4912 - eslint-plugin-toml: 0.12.0(eslint@9.25.0(jiti@2.4.2))
4913 - eslint-plugin-unicorn: 58.0.0(eslint@9.25.0(jiti@2.4.2))
4914 - eslint-plugin-unused-imports: 4.1.4(@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))
4915 - eslint-plugin-vue: 10.0.0(eslint@9.25.0(jiti@2.4.2))(vue-eslint-parser@10.1.3(eslint@9.25.0(jiti@2.4.2)))
4916 - eslint-plugin-yml: 1.17.0(eslint@9.25.0(jiti@2.4.2))
4917 - eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.13)(eslint@9.25.0(jiti@2.4.2))
4909 + eslint-plugin-perfectionist: 4.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
4910 + eslint-plugin-pnpm: 0.3.1(eslint@9.25.1(jiti@2.4.2))
4911 + eslint-plugin-regexp: 2.7.0(eslint@9.25.1(jiti@2.4.2))
4912 + eslint-plugin-toml: 0.12.0(eslint@9.25.1(jiti@2.4.2))
4913 + eslint-plugin-unicorn: 58.0.0(eslint@9.25.1(jiti@2.4.2))
4914 + eslint-plugin-unused-imports: 4.1.4(@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))
4915 + eslint-plugin-vue: 10.0.0(eslint@9.25.1(jiti@2.4.2))(vue-eslint-parser@10.1.3(eslint@9.25.1(jiti@2.4.2)))
4916 + eslint-plugin-yml: 1.17.0(eslint@9.25.1(jiti@2.4.2))
4917 + eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.13)(eslint@9.25.1(jiti@2.4.2))
4918 globals: 16.0.0
4919 jsonc-eslint-parser: 2.4.0
4920 local-pkg: 1.1.1
4921 parse-gitignore: 2.0.0
4922 toml-eslint-parser: 0.10.0
4923 - vue-eslint-parser: 10.1.3(eslint@9.25.0(jiti@2.4.2))
4923 + vue-eslint-parser: 10.1.3(eslint@9.25.1(jiti@2.4.2))
4924 yaml-eslint-parser: 1.3.0
4925 transitivePeerDependencies:
4926 - '@eslint/json'
@@ -5390,22 +5390,22 @@ snapshots:
5390 '@esbuild/win32-x64@0.25.2':
5391 optional: true
5392
5393 - '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.25.0(jiti@2.4.2))':
5393 + '@eslint-community/eslint-plugin-eslint-comments@4.4.1(eslint@9.25.1(jiti@2.4.2))':
5394 dependencies:
5395 escape-string-regexp: 4.0.0
5396 - eslint: 9.25.0(jiti@2.4.2)
5396 + eslint: 9.25.1(jiti@2.4.2)
5397 ignore: 5.3.2
5398
5399 - '@eslint-community/eslint-utils@4.5.1(eslint@9.25.0(jiti@2.4.2))':
5399 + '@eslint-community/eslint-utils@4.5.1(eslint@9.25.1(jiti@2.4.2))':
5400 dependencies:
5401 - eslint: 9.25.0(jiti@2.4.2)
5401 + eslint: 9.25.1(jiti@2.4.2)
5402 eslint-visitor-keys: 3.4.3
5403
5404 '@eslint-community/regexpp@4.12.1': {}
5405
5406 - '@eslint/compat@1.2.8(eslint@9.25.0(jiti@2.4.2))':
5406 + '@eslint/compat@1.2.8(eslint@9.25.1(jiti@2.4.2))':
5407 optionalDependencies:
5408 - eslint: 9.25.0(jiti@2.4.2)
5408 + eslint: 9.25.1(jiti@2.4.2)
5409
5410 '@eslint/config-array@0.20.0':
5411 dependencies:
@@ -5439,7 +5439,7 @@ snapshots:
5439 transitivePeerDependencies:
5440 - supports-color
5441
5442 - '@eslint/js@9.25.0': {}
5442 + '@eslint/js@9.25.1': {}
5443
5444 '@eslint/markdown@6.3.0':
5445 dependencies:
@@ -5756,38 +5756,38 @@ snapshots:
5756
5757 '@sec-ant/readable-stream@0.4.1': {}
5758
5759 - '@shikijs/core@3.2.2':
5759 + '@shikijs/core@3.3.0':
5760 dependencies:
5761 - '@shikijs/types': 3.2.2
5761 + '@shikijs/types': 3.3.0
5762 '@shikijs/vscode-textmate': 10.0.2
5763 '@types/hast': 3.0.4
5764 hast-util-to-html: 9.0.5
5765
5766 - '@shikijs/engine-javascript@3.2.2':
5766 + '@shikijs/engine-javascript@3.3.0':
5767 dependencies:
5768 - '@shikijs/types': 3.2.2
5768 + '@shikijs/types': 3.3.0
5769 '@shikijs/vscode-textmate': 10.0.2
5770 - oniguruma-to-es: 4.1.0
5770 + oniguruma-to-es: 4.2.0
5771
5772 - '@shikijs/engine-oniguruma@3.2.2':
5772 + '@shikijs/engine-oniguruma@3.3.0':
5773 dependencies:
5774 - '@shikijs/types': 3.2.2
5774 + '@shikijs/types': 3.3.0
5775 '@shikijs/vscode-textmate': 10.0.2
5776
5777 - '@shikijs/langs@3.2.2':
5777 + '@shikijs/langs@3.3.0':
5778 dependencies:
5779 - '@shikijs/types': 3.2.2
5779 + '@shikijs/types': 3.3.0
5780
5781 - '@shikijs/markdown-it@3.2.2':
5781 + '@shikijs/markdown-it@3.3.0':
5782 dependencies:
5783 markdown-it: 14.1.0
5784 - shiki: 3.2.2
5784 + shiki: 3.3.0
5785
5786 - '@shikijs/themes@3.2.2':
5786 + '@shikijs/themes@3.3.0':
5787 dependencies:
5788 - '@shikijs/types': 3.2.2
5788 + '@shikijs/types': 3.3.0
5789
5790 - '@shikijs/types@3.2.2':
5790 + '@shikijs/types@3.3.0':
5791 dependencies:
5792 '@shikijs/vscode-textmate': 10.0.2
5793 '@types/hast': 3.0.4
@@ -5806,10 +5806,10 @@ snapshots:
5806
5807 '@sindresorhus/merge-streams@4.0.0': {}
5808
5809 - '@stylistic/eslint-plugin@4.2.0(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
5809 + '@stylistic/eslint-plugin@4.2.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
5810 dependencies:
5811 - '@typescript-eslint/utils': 8.29.0(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
5812 - eslint: 9.25.0(jiti@2.4.2)
5811 + '@typescript-eslint/utils': 8.29.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
5812 + eslint: 9.25.1(jiti@2.4.2)
5813 eslint-visitor-keys: 4.2.0
5814 espree: 10.3.0
5815 estraverse: 5.3.0
@@ -5895,12 +5895,12 @@ snapshots:
5895 '@tailwindcss/oxide-win32-arm64-msvc': 4.1.4
5896 '@tailwindcss/oxide-win32-x64-msvc': 4.1.4
5897
5898 - '@tailwindcss/vite@4.1.4(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))':
5898 + '@tailwindcss/vite@4.1.4(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))':
5899 dependencies:
5900 '@tailwindcss/node': 4.1.4
5901 '@tailwindcss/oxide': 4.1.4
5902 tailwindcss: 4.1.4
5903 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
5903 + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
5904
5905 '@trysound/sax@0.2.0': {}
5906
@@ -5931,7 +5931,7 @@ snapshots:
5931 '@types/fs-extra@11.0.4':
5932 dependencies:
5933 '@types/jsonfile': 6.1.4
5934 - '@types/node': 22.14.1
5934 + '@types/node': 22.15.2
5935
5936 '@types/hast@3.0.4':
5937 dependencies:
@@ -5939,7 +5939,7 @@ snapshots:
5939
5940 '@types/jsdom@21.1.7':
5941 dependencies:
5942 - '@types/node': 22.14.1
5942 + '@types/node': 22.15.2
5943 '@types/tough-cookie': 4.0.5
5944 parse5: 7.2.1
5945
@@ -5947,7 +5947,7 @@ snapshots:
5947
5948 '@types/jsonfile@6.1.4':
5949 dependencies:
5950 - '@types/node': 22.14.1
5950 + '@types/node': 22.15.2
5951
5952 '@types/katex@0.16.7': {}
5953
@@ -5965,7 +5965,7 @@ snapshots:
5965
5966 '@types/ms@2.1.0': {}
5967
5968 - '@types/node@22.14.1':
5968 + '@types/node@22.15.2':
5969 dependencies:
5970 undici-types: 6.21.0
5971
@@ -5987,18 +5987,18 @@ snapshots:
5987
5988 '@types/yauzl@2.10.3':
5989 dependencies:
5990 - '@types/node': 22.14.1
5990 + '@types/node': 22.15.2
5991 optional: true
5992
5993 - '@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
5993 + '@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
5994 dependencies:
5995 '@eslint-community/regexpp': 4.12.1
5996 - '@typescript-eslint/parser': 8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
5996 + '@typescript-eslint/parser': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
5997 '@typescript-eslint/scope-manager': 8.30.1
5998 - '@typescript-eslint/type-utils': 8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
5999 - '@typescript-eslint/utils': 8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
5998 + '@typescript-eslint/type-utils': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
5999 + '@typescript-eslint/utils': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
6000 '@typescript-eslint/visitor-keys': 8.30.1
6001 - eslint: 9.25.0(jiti@2.4.2)
6001 + eslint: 9.25.1(jiti@2.4.2)
6002 graphemer: 1.4.0
6003 ignore: 5.3.2
6004 natural-compare: 1.4.0
@@ -6007,14 +6007,14 @@ snapshots:
6007 transitivePeerDependencies:
6008 - supports-color
6009
6010 - '@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
6010 + '@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
6011 dependencies:
6012 '@typescript-eslint/scope-manager': 8.30.1
6013 '@typescript-eslint/types': 8.30.1
6014 '@typescript-eslint/typescript-estree': 8.30.1(typescript@5.8.3)
6015 '@typescript-eslint/visitor-keys': 8.30.1
6016 debug: 4.4.0(supports-color@8.1.1)
6017 - eslint: 9.25.0(jiti@2.4.2)
6017 + eslint: 9.25.1(jiti@2.4.2)
6018 typescript: 5.8.3
6019 transitivePeerDependencies:
6020 - supports-color
@@ -6029,12 +6029,12 @@ snapshots:
6029 '@typescript-eslint/types': 8.30.1
6030 '@typescript-eslint/visitor-keys': 8.30.1
6031
6032 - '@typescript-eslint/type-utils@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
6032 + '@typescript-eslint/type-utils@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
6033 dependencies:
6034 '@typescript-eslint/typescript-estree': 8.30.1(typescript@5.8.3)
6035 - '@typescript-eslint/utils': 8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
6035 + '@typescript-eslint/utils': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
6036 debug: 4.4.0(supports-color@8.1.1)
6037 - eslint: 9.25.0(jiti@2.4.2)
6037 + eslint: 9.25.1(jiti@2.4.2)
6038 ts-api-utils: 2.1.0(typescript@5.8.3)
6039 typescript: 5.8.3
6040 transitivePeerDependencies:
@@ -6072,24 +6072,24 @@ snapshots:
6072 transitivePeerDependencies:
6073 - supports-color
6074
6075 - '@typescript-eslint/utils@8.29.0(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
6075 + '@typescript-eslint/utils@8.29.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
6076 dependencies:
6077 - '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.0(jiti@2.4.2))
6077 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1(jiti@2.4.2))
6078 '@typescript-eslint/scope-manager': 8.29.0
6079 '@typescript-eslint/types': 8.29.0
6080 '@typescript-eslint/typescript-estree': 8.29.0(typescript@5.8.3)
6081 - eslint: 9.25.0(jiti@2.4.2)
6081 + eslint: 9.25.1(jiti@2.4.2)
6082 typescript: 5.8.3
6083 transitivePeerDependencies:
6084 - supports-color
6085
6086 - '@typescript-eslint/utils@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)':
6086 + '@typescript-eslint/utils@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)':
6087 dependencies:
6088 - '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.0(jiti@2.4.2))
6088 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1(jiti@2.4.2))
6089 '@typescript-eslint/scope-manager': 8.30.1
6090 '@typescript-eslint/types': 8.30.1
6091 '@typescript-eslint/typescript-estree': 8.30.1(typescript@5.8.3)
6092 - eslint: 9.25.0(jiti@2.4.2)
6092 + eslint: 9.25.1(jiti@2.4.2)
6093 typescript: 5.8.3
6094 transitivePeerDependencies:
6095 - supports-color
@@ -6156,28 +6156,28 @@ snapshots:
6156 '@unrs/resolver-binding-win32-x64-msvc@1.6.3':
6157 optional: true
6158
6159 - '@vitejs/plugin-vue-jsx@4.1.2(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))':
6159 + '@vitejs/plugin-vue-jsx@4.1.2(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))':
6160 dependencies:
6161 '@babel/core': 7.26.10
6162 '@babel/plugin-transform-typescript': 7.27.0(@babel/core@7.26.10)
6163 '@vue/babel-plugin-jsx': 1.4.0(@babel/core@7.26.10)
6164 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
6164 + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
6165 vue: 3.5.13(typescript@5.8.3)
6166 transitivePeerDependencies:
6167 - supports-color
6168
6169 - '@vitejs/plugin-vue@5.2.3(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))':
6169 + '@vitejs/plugin-vue@5.2.3(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))':
6170 dependencies:
6171 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
6171 + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
6172 vue: 3.5.13(typescript@5.8.3)
6173
6174 - '@vitest/eslint-plugin@1.1.43(@typescript-eslint/utils@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))':
6174 + '@vitest/eslint-plugin@1.1.43(@typescript-eslint/utils@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))':
6175 dependencies:
6176 - '@typescript-eslint/utils': 8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
6177 - eslint: 9.25.0(jiti@2.4.2)
6176 + '@typescript-eslint/utils': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
6177 + eslint: 9.25.1(jiti@2.4.2)
6178 optionalDependencies:
6179 typescript: 5.8.3
6180 - vitest: 3.1.2(@types/debug@4.1.12)(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
6180 + vitest: 3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
6181
6182 '@vitest/expect@3.1.2':
6183 dependencies:
@@ -6186,13 +6186,13 @@ snapshots:
6186 chai: 5.2.0
6187 tinyrainbow: 2.0.0
6188
6189 - '@vitest/mocker@3.1.2(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))':
6189 + '@vitest/mocker@3.1.2(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))':
6190 dependencies:
6191 '@vitest/spy': 3.1.2
6192 estree-walker: 3.0.3
6193 magic-string: 0.30.17
6194 optionalDependencies:
6195 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
6195 + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
6196
6197 '@vitest/pretty-format@3.1.2':
6198 dependencies:
@@ -6301,14 +6301,14 @@ snapshots:
6301 dependencies:
6302 '@vue/devtools-kit': 7.7.2
6303
6304 - '@vue/devtools-core@7.7.5(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))':
6304 + '@vue/devtools-core@7.7.5(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))':
6305 dependencies:
6306 '@vue/devtools-kit': 7.7.5
6307 '@vue/devtools-shared': 7.7.5
6308 mitt: 3.0.1
6309 nanoid: 5.1.5
6310 pathe: 2.0.3
6311 - vite-hot-client: 2.0.4(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))
6311 + vite-hot-client: 2.0.4(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))
6312 vue: 3.5.13(typescript@5.8.3)
6313 transitivePeerDependencies:
6314 - vite
@@ -6341,7 +6341,7 @@ snapshots:
6341 dependencies:
6342 rfdc: 1.4.1
6343
6344 - '@vue/language-core@2.2.8(typescript@5.8.3)':
6344 + '@vue/language-core@2.2.10(typescript@5.8.3)':
6345 dependencies:
6346 '@volar/language-core': 2.4.12
6347 '@vue/compiler-dom': 3.5.13
@@ -6494,7 +6494,7 @@ snapshots:
6494
6495 aws4@1.13.2: {}
6496
6497 - axios@1.8.4(debug@4.4.0):
6497 + axios@1.9.0(debug@4.4.0):
6498 dependencies:
6499 follow-redirects: 1.15.9(debug@4.4.0)
6500 form-data: 4.0.2
@@ -6783,7 +6783,7 @@ snapshots:
6783
6784 csstype@3.1.3: {}
6785
6786 - cypress@14.3.1:
6786 + cypress@14.3.2:
6787 dependencies:
6788 '@cypress/request': 3.0.8
6789 '@cypress/xvfb': 1.2.4(supports-color@8.1.1)
@@ -7074,20 +7074,20 @@ snapshots:
7074
7075 escape-string-regexp@5.0.0: {}
7076
7077 - eslint-compat-utils@0.5.1(eslint@9.25.0(jiti@2.4.2)):
7077 + eslint-compat-utils@0.5.1(eslint@9.25.1(jiti@2.4.2)):
7078 dependencies:
7079 - eslint: 9.25.0(jiti@2.4.2)
7079 + eslint: 9.25.1(jiti@2.4.2)
7080 semver: 7.7.1
7081
7082 - eslint-compat-utils@0.6.5(eslint@9.25.0(jiti@2.4.2)):
7082 + eslint-compat-utils@0.6.5(eslint@9.25.1(jiti@2.4.2)):
7083 dependencies:
7084 - eslint: 9.25.0(jiti@2.4.2)
7084 + eslint: 9.25.1(jiti@2.4.2)
7085 semver: 7.7.1
7086
7087 - eslint-config-flat-gitignore@2.1.0(eslint@9.25.0(jiti@2.4.2)):
7087 + eslint-config-flat-gitignore@2.1.0(eslint@9.25.1(jiti@2.4.2)):
7088 dependencies:
7089 - '@eslint/compat': 1.2.8(eslint@9.25.0(jiti@2.4.2))
7090 - eslint: 9.25.0(jiti@2.4.2)
7089 + '@eslint/compat': 1.2.8(eslint@9.25.1(jiti@2.4.2))
7090 + eslint: 9.25.1(jiti@2.4.2)
7091
7092 eslint-flat-config-utils@2.0.1:
7093 dependencies:
@@ -7101,40 +7101,40 @@ snapshots:
7101 transitivePeerDependencies:
7102 - supports-color
7103
7104 - eslint-json-compat-utils@0.2.1(eslint@9.25.0(jiti@2.4.2))(jsonc-eslint-parser@2.4.0):
7104 + eslint-json-compat-utils@0.2.1(eslint@9.25.1(jiti@2.4.2))(jsonc-eslint-parser@2.4.0):
7105 dependencies:
7106 - eslint: 9.25.0(jiti@2.4.2)
7106 + eslint: 9.25.1(jiti@2.4.2)
7107 esquery: 1.6.0
7108 jsonc-eslint-parser: 2.4.0
7109
7110 - eslint-merge-processors@2.0.0(eslint@9.25.0(jiti@2.4.2)):
7110 + eslint-merge-processors@2.0.0(eslint@9.25.1(jiti@2.4.2)):
7111 dependencies:
7112 - eslint: 9.25.0(jiti@2.4.2)
7112 + eslint: 9.25.1(jiti@2.4.2)
7113
7114 - eslint-plugin-antfu@3.1.1(eslint@9.25.0(jiti@2.4.2)):
7114 + eslint-plugin-antfu@3.1.1(eslint@9.25.1(jiti@2.4.2)):
7115 dependencies:
7116 - eslint: 9.25.0(jiti@2.4.2)
7116 + eslint: 9.25.1(jiti@2.4.2)
7117
7118 - eslint-plugin-command@3.2.0(eslint@9.25.0(jiti@2.4.2)):
7118 + eslint-plugin-command@3.2.0(eslint@9.25.1(jiti@2.4.2)):
7119 dependencies:
7120 '@es-joy/jsdoccomment': 0.50.0
7121 - eslint: 9.25.0(jiti@2.4.2)
7121 + eslint: 9.25.1(jiti@2.4.2)
7122
7123 - eslint-plugin-es-x@7.8.0(eslint@9.25.0(jiti@2.4.2)):
7123 + eslint-plugin-es-x@7.8.0(eslint@9.25.1(jiti@2.4.2)):
7124 dependencies:
7125 - '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.0(jiti@2.4.2))
7125 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1(jiti@2.4.2))
7126 '@eslint-community/regexpp': 4.12.1
7127 - eslint: 9.25.0(jiti@2.4.2)
7128 - eslint-compat-utils: 0.5.1(eslint@9.25.0(jiti@2.4.2))
7127 + eslint: 9.25.1(jiti@2.4.2)
7128 + eslint-compat-utils: 0.5.1(eslint@9.25.1(jiti@2.4.2))
7129
7130 - eslint-plugin-import-x@4.10.6(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3):
7130 + eslint-plugin-import-x@4.10.6(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3):
7131 dependencies:
7132 '@pkgr/core': 0.2.4
7133 '@types/doctrine': 0.0.9
7134 - '@typescript-eslint/utils': 8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
7134 + '@typescript-eslint/utils': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
7135 debug: 4.4.0(supports-color@8.1.1)
7136 doctrine: 3.0.0
7137 - eslint: 9.25.0(jiti@2.4.2)
7137 + eslint: 9.25.1(jiti@2.4.2)
7138 eslint-import-resolver-node: 0.3.9
7139 get-tsconfig: 4.10.0
7140 is-glob: 4.0.3
@@ -7147,14 +7147,14 @@ snapshots:
7147 - supports-color
7148 - typescript
7149
7150 - eslint-plugin-jsdoc@50.6.9(eslint@9.25.0(jiti@2.4.2)):
7150 + eslint-plugin-jsdoc@50.6.9(eslint@9.25.1(jiti@2.4.2)):
7151 dependencies:
7152 '@es-joy/jsdoccomment': 0.49.0
7153 are-docs-informative: 0.0.2
7154 comment-parser: 1.4.1
7155 debug: 4.4.0(supports-color@8.1.1)
7156 escape-string-regexp: 4.0.0
7157 - eslint: 9.25.0(jiti@2.4.2)
7157 + eslint: 9.25.1(jiti@2.4.2)
7158 espree: 10.3.0
7159 esquery: 1.6.0
7160 parse-imports: 2.2.1
@@ -7164,12 +7164,12 @@ snapshots:
7164 transitivePeerDependencies:
7165 - supports-color
7166
7167 - eslint-plugin-jsonc@2.20.0(eslint@9.25.0(jiti@2.4.2)):
7167 + eslint-plugin-jsonc@2.20.0(eslint@9.25.1(jiti@2.4.2)):
7168 dependencies:
7169 - '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.0(jiti@2.4.2))
7170 - eslint: 9.25.0(jiti@2.4.2)
7171 - eslint-compat-utils: 0.6.5(eslint@9.25.0(jiti@2.4.2))
7172 - eslint-json-compat-utils: 0.2.1(eslint@9.25.0(jiti@2.4.2))(jsonc-eslint-parser@2.4.0)
7169 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1(jiti@2.4.2))
7170 + eslint: 9.25.1(jiti@2.4.2)
7171 + eslint-compat-utils: 0.6.5(eslint@9.25.1(jiti@2.4.2))
7172 + eslint-json-compat-utils: 0.2.1(eslint@9.25.1(jiti@2.4.2))(jsonc-eslint-parser@2.4.0)
7173 espree: 10.3.0
7174 graphemer: 1.4.0
7175 jsonc-eslint-parser: 2.4.0
@@ -7178,12 +7178,12 @@ snapshots:
7178 transitivePeerDependencies:
7179 - '@eslint/json'
7180
7181 - eslint-plugin-n@17.17.0(eslint@9.25.0(jiti@2.4.2)):
7181 + eslint-plugin-n@17.17.0(eslint@9.25.1(jiti@2.4.2)):
7182 dependencies:
7183 - '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.0(jiti@2.4.2))
7183 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1(jiti@2.4.2))
7184 enhanced-resolve: 5.18.1
7185 - eslint: 9.25.0(jiti@2.4.2)
7186 - eslint-plugin-es-x: 7.8.0(eslint@9.25.0(jiti@2.4.2))
7185 + eslint: 9.25.1(jiti@2.4.2)
7186 + eslint-plugin-es-x: 7.8.0(eslint@9.25.1(jiti@2.4.2))
7187 get-tsconfig: 4.10.0
7188 globals: 15.15.0
7189 ignore: 5.3.2
@@ -7192,19 +7192,19 @@ snapshots:
7192
7193 eslint-plugin-no-only-tests@3.3.0: {}
7194
7195 - eslint-plugin-perfectionist@4.11.0(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3):
7195 + eslint-plugin-perfectionist@4.11.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3):
7196 dependencies:
7197 '@typescript-eslint/types': 8.29.0
7198 - '@typescript-eslint/utils': 8.29.0(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
7199 - eslint: 9.25.0(jiti@2.4.2)
7198 + '@typescript-eslint/utils': 8.29.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
7199 + eslint: 9.25.1(jiti@2.4.2)
7200 natural-orderby: 5.0.0
7201 transitivePeerDependencies:
7202 - supports-color
7203 - typescript
7204
7205 - eslint-plugin-pnpm@0.3.1(eslint@9.25.0(jiti@2.4.2)):
7205 + eslint-plugin-pnpm@0.3.1(eslint@9.25.1(jiti@2.4.2)):
7206 dependencies:
7207 - eslint: 9.25.0(jiti@2.4.2)
7207 + eslint: 9.25.1(jiti@2.4.2)
7208 find-up-simple: 1.0.1
7209 jsonc-eslint-parser: 2.4.0
7210 pathe: 2.0.3
@@ -7212,36 +7212,36 @@ snapshots:
7212 tinyglobby: 0.2.12
7213 yaml-eslint-parser: 1.3.0
7214
7215 - eslint-plugin-regexp@2.7.0(eslint@9.25.0(jiti@2.4.2)):
7215 + eslint-plugin-regexp@2.7.0(eslint@9.25.1(jiti@2.4.2)):
7216 dependencies:
7217 - '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.0(jiti@2.4.2))
7217 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1(jiti@2.4.2))
7218 '@eslint-community/regexpp': 4.12.1
7219 comment-parser: 1.4.1
7220 - eslint: 9.25.0(jiti@2.4.2)
7220 + eslint: 9.25.1(jiti@2.4.2)
7221 jsdoc-type-pratt-parser: 4.1.0
7222 refa: 0.12.1
7223 regexp-ast-analysis: 0.7.1
7224 scslre: 0.3.0
7225
7226 - eslint-plugin-toml@0.12.0(eslint@9.25.0(jiti@2.4.2)):
7226 + eslint-plugin-toml@0.12.0(eslint@9.25.1(jiti@2.4.2)):
7227 dependencies:
7228 debug: 4.4.0(supports-color@8.1.1)
7229 - eslint: 9.25.0(jiti@2.4.2)
7230 - eslint-compat-utils: 0.6.5(eslint@9.25.0(jiti@2.4.2))
7229 + eslint: 9.25.1(jiti@2.4.2)
7230 + eslint-compat-utils: 0.6.5(eslint@9.25.1(jiti@2.4.2))
7231 lodash: 4.17.21
7232 toml-eslint-parser: 0.10.0
7233 transitivePeerDependencies:
7234 - supports-color
7235
7236 - eslint-plugin-unicorn@58.0.0(eslint@9.25.0(jiti@2.4.2)):
7236 + eslint-plugin-unicorn@58.0.0(eslint@9.25.1(jiti@2.4.2)):
7237 dependencies:
7238 '@babel/helper-validator-identifier': 7.25.9
7239 - '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.0(jiti@2.4.2))
7239 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1(jiti@2.4.2))
7240 '@eslint/plugin-kit': 0.2.8
7241 ci-info: 4.2.0
7242 clean-regexp: 1.0.0
7243 core-js-compat: 3.41.0
7244 - eslint: 9.25.0(jiti@2.4.2)
7244 + eslint: 9.25.1(jiti@2.4.2)
7245 esquery: 1.6.0
7246 globals: 16.0.0
7247 indent-string: 5.0.0
@@ -7254,38 +7254,38 @@ snapshots:
7254 semver: 7.7.1
7255 strip-indent: 4.0.0
7256
7257 - eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2)):
7257 + eslint-plugin-unused-imports@4.1.4(@typescript-eslint/eslint-plugin@8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)):
7258 dependencies:
7259 - eslint: 9.25.0(jiti@2.4.2)
7259 + eslint: 9.25.1(jiti@2.4.2)
7260 optionalDependencies:
7261 - '@typescript-eslint/eslint-plugin': 8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.0(jiti@2.4.2))(typescript@5.8.3)
7261 + '@typescript-eslint/eslint-plugin': 8.30.1(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
7262
7263 - eslint-plugin-vue@10.0.0(eslint@9.25.0(jiti@2.4.2))(vue-eslint-parser@10.1.3(eslint@9.25.0(jiti@2.4.2))):
7263 + eslint-plugin-vue@10.0.0(eslint@9.25.1(jiti@2.4.2))(vue-eslint-parser@10.1.3(eslint@9.25.1(jiti@2.4.2))):
7264 dependencies:
7265 - '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.0(jiti@2.4.2))
7266 - eslint: 9.25.0(jiti@2.4.2)
7265 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1(jiti@2.4.2))
7266 + eslint: 9.25.1(jiti@2.4.2)
7267 natural-compare: 1.4.0
7268 nth-check: 2.1.1
7269 postcss-selector-parser: 6.1.2
7270 semver: 7.7.1
7271 - vue-eslint-parser: 10.1.3(eslint@9.25.0(jiti@2.4.2))
7271 + vue-eslint-parser: 10.1.3(eslint@9.25.1(jiti@2.4.2))
7272 xml-name-validator: 4.0.0
7273
7274 - eslint-plugin-yml@1.17.0(eslint@9.25.0(jiti@2.4.2)):
7274 + eslint-plugin-yml@1.17.0(eslint@9.25.1(jiti@2.4.2)):
7275 dependencies:
7276 debug: 4.4.0(supports-color@8.1.1)
7277 escape-string-regexp: 4.0.0
7278 - eslint: 9.25.0(jiti@2.4.2)
7279 - eslint-compat-utils: 0.6.5(eslint@9.25.0(jiti@2.4.2))
7278 + eslint: 9.25.1(jiti@2.4.2)
7279 + eslint-compat-utils: 0.6.5(eslint@9.25.1(jiti@2.4.2))
7280 natural-compare: 1.4.0
7281 yaml-eslint-parser: 1.3.0
7282 transitivePeerDependencies:
7283 - supports-color
7284
7285 - eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.13)(eslint@9.25.0(jiti@2.4.2)):
7285 + eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.13)(eslint@9.25.1(jiti@2.4.2)):
7286 dependencies:
7287 '@vue/compiler-sfc': 3.5.13
7288 - eslint: 9.25.0(jiti@2.4.2)
7288 + eslint: 9.25.1(jiti@2.4.2)
7289
7290 eslint-scope@8.3.0:
7291 dependencies:
@@ -7296,15 +7296,15 @@ snapshots:
7296
7297 eslint-visitor-keys@4.2.0: {}
7298
7299 - eslint@9.25.0(jiti@2.4.2):
7299 + eslint@9.25.1(jiti@2.4.2):
7300 dependencies:
7301 - '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.0(jiti@2.4.2))
7301 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.25.1(jiti@2.4.2))
7302 '@eslint-community/regexpp': 4.12.1
7303 '@eslint/config-array': 0.20.0
7304 '@eslint/config-helpers': 0.2.1
7305 '@eslint/core': 0.13.0
7306 '@eslint/eslintrc': 3.3.1
7307 - '@eslint/js': 9.25.0
7307 + '@eslint/js': 9.25.1
7308 '@eslint/plugin-kit': 0.2.8
7309 '@humanfs/node': 0.16.6
7310 '@humanwhocodes/module-importer': 1.0.1
@@ -8602,12 +8602,12 @@ snapshots:
8602 dependencies:
8603 mimic-function: 5.0.1
8604
8605 - oniguruma-parser@0.5.4: {}
8605 + oniguruma-parser@0.11.2: {}
8606
8607 - oniguruma-to-es@4.1.0:
8607 + oniguruma-to-es@4.2.0:
8608 dependencies:
8609 emoji-regex-xs: 1.0.0
8610 - oniguruma-parser: 0.5.4
8610 + oniguruma-parser: 0.11.2
8611 regex: 6.0.1
8612 regex-recursion: 6.0.2
8613
@@ -8973,7 +8973,7 @@ snapshots:
8973
8974 safer-buffer@2.1.2: {}
8975
8976 - sass@1.86.3:
8976 + sass@1.87.0:
8977 dependencies:
8978 chokidar: 4.0.3
8979 immutable: 5.1.1
@@ -9014,14 +9014,14 @@ snapshots:
9014
9015 shell-quote@1.8.2: {}
9016
9017 - shiki@3.2.2:
9017 + shiki@3.3.0:
9018 dependencies:
9019 - '@shikijs/core': 3.2.2
9020 - '@shikijs/engine-javascript': 3.2.2
9021 - '@shikijs/engine-oniguruma': 3.2.2
9022 - '@shikijs/langs': 3.2.2
9023 - '@shikijs/themes': 3.2.2
9024 - '@shikijs/types': 3.2.2
9019 + '@shikijs/core': 3.3.0
9020 + '@shikijs/engine-javascript': 3.3.0
9021 + '@shikijs/engine-oniguruma': 3.3.0
9022 + '@shikijs/langs': 3.3.0
9023 + '@shikijs/themes': 3.3.0
9024 + '@shikijs/types': 3.3.0
9025 '@shikijs/vscode-textmate': 10.0.2
9026 '@types/hast': 3.0.4
9027
@@ -9499,17 +9499,17 @@ snapshots:
9499 - rollup
9500 - supports-color
9501
9502 - vite-hot-client@2.0.4(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)):
9502 + vite-hot-client@2.0.4(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)):
9503 dependencies:
9504 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
9504 + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
9505
9506 - vite-node@3.1.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1):
9506 + vite-node@3.1.2(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1):
9507 dependencies:
9508 cac: 6.7.14
9509 debug: 4.4.0(supports-color@8.1.1)
9510 es-module-lexer: 1.6.0
9511 pathe: 2.0.3
9512 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
9512 + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
9513 transitivePeerDependencies:
9514 - '@types/node'
9515 - jiti
@@ -9524,7 +9524,7 @@ snapshots:
9524 - tsx
9525 - yaml
9526
9527 - vite-plugin-inspect@0.8.9(rollup@4.39.0)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)):
9527 + vite-plugin-inspect@0.8.9(rollup@4.39.0)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)):
9528 dependencies:
9529 '@antfu/utils': 0.7.10
9530 '@rollup/pluginutils': 5.1.4(rollup@4.39.0)
@@ -9535,28 +9535,28 @@ snapshots:
9535 perfect-debounce: 1.0.0
9536 picocolors: 1.1.1
9537 sirv: 3.0.1
9538 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
9538 + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
9539 transitivePeerDependencies:
9540 - rollup
9541 - supports-color
9542
9543 - vite-plugin-vue-devtools@7.7.5(rollup@4.39.0)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3)):
9543 + vite-plugin-vue-devtools@7.7.5(rollup@4.39.0)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3)):
9544 dependencies:
9545 - '@vue/devtools-core': 7.7.5(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))
9545 + '@vue/devtools-core': 7.7.5(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))(vue@3.5.13(typescript@5.8.3))
9546 '@vue/devtools-kit': 7.7.5
9547 '@vue/devtools-shared': 7.7.5
9548 execa: 9.5.2
9549 sirv: 3.0.1
9550 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
9551 - vite-plugin-inspect: 0.8.9(rollup@4.39.0)(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))
9552 - vite-plugin-vue-inspector: 5.3.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))
9550 + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
9551 + vite-plugin-inspect: 0.8.9(rollup@4.39.0)(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))
9552 + vite-plugin-vue-inspector: 5.3.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))
9553 transitivePeerDependencies:
9554 - '@nuxt/kit'
9555 - rollup
9556 - supports-color
9557 - vue
9558
9559 - vite-plugin-vue-inspector@5.3.1(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)):
9559 + vite-plugin-vue-inspector@5.3.1(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)):
9560 dependencies:
9561 '@babel/core': 7.26.10
9562 '@babel/plugin-proposal-decorators': 7.25.9(@babel/core@7.26.10)
@@ -9567,7 +9567,7 @@ snapshots:
9567 '@vue/compiler-dom': 3.5.13
9568 kolorist: 1.8.0
9569 magic-string: 0.30.17
9570 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
9570 + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
9571 transitivePeerDependencies:
9572 - supports-color
9573
@@ -9576,26 +9576,26 @@ snapshots:
9576 svgo: 3.3.2
9577 vue: 3.5.13(typescript@5.8.3)
9578
9579 - vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1):
9579 + vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1):
9580 dependencies:
9581 esbuild: 0.25.2
9582 - fdir: 6.4.3(picomatch@4.0.2)
9582 + fdir: 6.4.4(picomatch@4.0.2)
9583 picomatch: 4.0.2
9584 postcss: 8.5.3
9585 rollup: 4.39.0
9586 - tinyglobby: 0.2.12
9586 + tinyglobby: 0.2.13
9587 optionalDependencies:
9588 - '@types/node': 22.14.1
9588 + '@types/node': 22.15.2
9589 fsevents: 2.3.3
9590 jiti: 2.4.2
9591 lightningcss: 1.29.2
9592 - sass: 1.86.3
9592 + sass: 1.87.0
9593 yaml: 2.7.1
9594
9595 - vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1):
9595 + vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.15.2)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1):
9596 dependencies:
9597 '@vitest/expect': 3.1.2
9598 - '@vitest/mocker': 3.1.2(vite@6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1))
9598 + '@vitest/mocker': 3.1.2(vite@6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1))
9599 '@vitest/pretty-format': 3.1.2
9600 '@vitest/runner': 3.1.2
9601 '@vitest/snapshot': 3.1.2
@@ -9612,12 +9612,12 @@ snapshots:
9612 tinyglobby: 0.2.13
9613 tinypool: 1.0.2
9614 tinyrainbow: 2.0.0
9615 - vite: 6.3.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
9616 - vite-node: 3.1.2(@types/node@22.14.1)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.86.3)(yaml@2.7.1)
9615 + vite: 6.3.3(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
9616 + vite-node: 3.1.2(@types/node@22.15.2)(jiti@2.4.2)(lightningcss@1.29.2)(sass@1.87.0)(yaml@2.7.1)
9617 why-is-node-running: 2.3.0
9618 optionalDependencies:
9619 '@types/debug': 4.1.12
9620 - '@types/node': 22.14.1
9620 + '@types/node': 22.15.2
9621 jsdom: 26.1.0
9622 transitivePeerDependencies:
9623 - jiti
@@ -9658,10 +9658,10 @@ snapshots:
9658
9659 vue-component-type-helpers@2.2.8: {}
9660
9661 - vue-eslint-parser@10.1.3(eslint@9.25.0(jiti@2.4.2)):
9661 + vue-eslint-parser@10.1.3(eslint@9.25.1(jiti@2.4.2)):
9662 dependencies:
9663 debug: 4.4.0(supports-color@8.1.1)
9664 - eslint: 9.25.0(jiti@2.4.2)
9664 + eslint: 9.25.1(jiti@2.4.2)
9665 eslint-scope: 8.3.0
9666 eslint-visitor-keys: 4.2.0
9667 espree: 10.3.0
@@ -9683,7 +9683,7 @@ snapshots:
9683 '@vue/devtools-api': 6.6.4
9684 vue: 3.5.13(typescript@5.8.3)
9685
9686 - vue-router@4.5.0(vue@3.5.13(typescript@5.8.3)):
9686 + vue-router@4.5.1(vue@3.5.13(typescript@5.8.3)):
9687 dependencies:
9688 '@vue/devtools-api': 6.6.4
9689 vue: 3.5.13(typescript@5.8.3)
@@ -9692,10 +9692,10 @@ snapshots:
9692 dependencies:
9693 vue: 3.5.13(typescript@5.8.3)
9694
9695 - vue-tsc@2.2.8(typescript@5.8.3):
9695 + vue-tsc@2.2.10(typescript@5.8.3):
9696 dependencies:
9697 '@volar/typescript': 2.4.12
9698 - '@vue/language-core': 2.2.8(typescript@5.8.3)
9698 + '@vue/language-core': 2.2.10(typescript@5.8.3)
9699 typescript: 5.8.3
9700
9701 vue3-apexcharts@1.8.0(apexcharts@4.5.0)(vue@3.5.13(typescript@5.8.3)):
@@ -9741,7 +9741,7 @@ snapshots:
9741
9742 wait-on@8.0.3(debug@4.4.0):
9743 dependencies:
9744 - axios: 1.8.4(debug@4.4.0)
9744 + axios: 1.9.0(debug@4.4.0)
9745 joi: 17.13.3
9746 lodash: 4.17.21
9747 minimist: 1.2.8
frontend/public/images/defenderforendpoint/permissions.png
Binary files /dev/null and b/frontend/public/images/defenderforendpoint/permissions.png differ
frontend/src/api/endpoints/incidentManagement.ts deleted
-462
@@ -1,462 +0,0 @@
1 -import type { FlaskBaseResponse } from "@/types/flask.d"
2 -import type {
3 - Alert,
4 - AlertComment,
5 - AlertContext,
6 - AlertDetails,
7 - AlertsFilter,
8 - AlertStatus,
9 - AlertTag,
10 - AlertTimeline
11 -} from "@/types/incidentManagement/alerts.d"
12 -import type {
13 - Case,
14 - CaseDataStore,
15 - CasePayload,
16 - CaseReportTemplateDataStore,
17 - CaseStatus
18 -} from "@/types/incidentManagement/cases.d"
19 -import type { IncidentNotification, IncidentNotificationPayload } from "@/types/incidentManagement/notifications.d"
20 -import type { SourceConfiguration, SourceName } from "@/types/incidentManagement/sources.d"
21 -import type { KeysOfUnion, UnionToIntersection } from "type-fest"
22 -import _castArray from "lodash/castArray"
23 -import { HttpClient } from "../httpClient"
24 -
25 -export type AlertsListFilterValue = string | string[] | AlertStatus | null
26 -export type AlertsFilterTypes = KeysOfUnion<AlertsFilter>
27 -
28 -export interface AlertsQuery {
29 - page: number
30 - pageSize: number
31 - sort: "asc" | "desc"
32 - filter: Partial<UnionToIntersection<AlertsFilter>>
33 - filters: {
34 - type: AlertsFilterTypes
35 - value: AlertsListFilterValue
36 - }[]
37 -}
38 -
39 -export type CasesFilter =
40 - | { status: CaseStatus }
41 - | { assignedTo: string }
42 - | { hostname: string }
43 - | { customerCode: string }
44 -
45 -export type CasesFilterTypes = KeysOfUnion<CasesFilter>
46 -
47 -export type AlertCommentPayload = Omit<AlertComment, "id">
48 -
49 -export type AlertCommentUpdatePayload = Omit<AlertComment, "id"> & { comment_id: number }
50 -
51 -export interface AlertIocPayload {
52 - alert_id: number
53 - ioc_value: string
54 - ioc_type: string
55 - ioc_description: string
56 -}
57 -
58 -export interface CaseReportPayload {
59 - case_id: number
60 - file_name: string
61 - template_name: string
62 -}
63 -
64 -export default {
65 - // #region Sources
66 - getConfiguredSources() {
67 - return HttpClient.get<FlaskBaseResponse & { sources: SourceName[] }>(
68 - `/incidents/db_operations/configured/sources`
69 - )
70 - },
71 - getAvailableMappings(indexName: string) {
72 - return HttpClient.get<FlaskBaseResponse & { available_mappings: string[] }>(
73 - `/incidents/db_operations/mappings/fields-assets-title-and-timefield`,
74 - {
75 - params: { index_name: indexName }
76 - }
77 - )
78 - },
79 - getSourceByIndex(indexName: string) {
80 - return HttpClient.get<FlaskBaseResponse & { source: SourceName }>(
81 - `/incidents/db_operations/available-source/${indexName}`
82 - )
83 - },
84 - getAvailableIndices(source: SourceName) {
85 - return HttpClient.get<FlaskBaseResponse & { indices: string[] }>(
86 - `/incidents/db_operations/available-indices/${source}`
87 - )
88 - },
89 - createSourceConfiguration(payload: SourceConfiguration) {
90 - return HttpClient.post<FlaskBaseResponse>(`/incidents/db_operations/fields-assets-title-and-timefield`, payload)
91 - },
92 - updateSourceConfiguration(payload: SourceConfiguration) {
93 - return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/fields-assets-title-and-timefield`, payload)
94 - },
95 - getSourceConfiguration(source: SourceName) {
96 - return HttpClient.get<FlaskBaseResponse & SourceConfiguration>(
97 - `/incidents/db_operations/fields-assets-title-and-timefield`,
98 - {
99 - params: { source }
100 - }
101 - )
102 - },
103 - getSocfortressRecommendsWazuh() {
104 - return HttpClient.get<FlaskBaseResponse & SourceConfiguration>(
105 - `/incidents/db_operations/socfortress/recommends/wazuh`
106 - )
107 - },
108 - deleteSourceConfiguration(source: SourceName) {
109 - return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/configured/sources/${source}`)
110 - },
111 - // #endregion
112 -
113 - // #region Alerts
114 - getAlertsList(args: Partial<AlertsQuery>, signal?: AbortSignal) {
115 - let url = `/incidents/db_operations/alerts`
116 -
117 - if (args?.filter?.status) {
118 - url = `/incidents/db_operations/alerts/status/${args.filter.status}`
119 - }
120 - if (args?.filter?.assetName) {
121 - url = `/incidents/db_operations/alerts/asset/${args.filter.assetName}`
122 - }
123 - if (args?.filter?.assignedTo) {
124 - url = `/incidents/db_operations/alerts/assigned-to/${args.filter.assignedTo}`
125 - }
126 - if (args?.filter?.tag) {
127 - url = `/incidents/db_operations/alert/tag/${_castArray(args.filter.tag).join(",")}`
128 - }
129 - if (args?.filter?.title) {
130 - url = `/incidents/db_operations/alerts/title/${args.filter.title}`
131 - }
132 - if (args?.filter?.customerCode) {
133 - url = `/incidents/db_operations/alerts/customer/${args.filter.customerCode}`
134 - }
135 - if (args?.filter?.source) {
136 - url = `/incidents/db_operations/alerts/source/${args.filter.source}`
137 - }
138 -
139 - const params: any = {
140 - page: args.page || 1,
141 - page_size: args.pageSize || 25,
142 - order: args.sort || "desc"
143 - }
144 -
145 - if (args.filters?.length) {
146 - for (const filter of args.filters) {
147 - if (filter.value?.length) {
148 - switch (filter.type) {
149 - case "assignedTo":
150 - params.assigned_to = filter.value
151 - break
152 - case "title":
153 - params.alert_title = filter.value
154 - break
155 - case "customerCode":
156 - params.customer_code = filter.value
157 - break
158 - case "source":
159 - params.source = filter.value
160 - break
161 - case "assetName":
162 - params.asset_name = filter.value
163 - break
164 - case "iocValue":
165 - params.ioc_value = filter.value
166 - break
167 - case "status":
168 - params.status = filter.value
169 - break
170 - case "tag":
171 - params.tags = _castArray(filter.value)
172 - break
173 - default:
174 - params[filter.type] = filter.value
175 - break
176 - }
177 - }
178 - }
179 -
180 - url = `/incidents/db_operations/alerts/filter`
181 - }
182 -
183 - return HttpClient.get<
184 - FlaskBaseResponse & {
185 - alerts: Alert[]
186 - closed: number
187 - in_progress: number
188 - open: number
189 - total: number
190 - total_filtered: number
191 - }
192 - >(url, {
193 - params,
194 - paramsSerializer: {
195 - indexes: null // remove brackets in array types
196 - },
197 - signal
198 - })
199 - },
200 - getAlert(alertId: number) {
201 - return HttpClient.get<FlaskBaseResponse & { alerts: Alert[] }>(`/incidents/db_operations/alert/${alertId}`)
202 - },
203 - getAlertDetails(indexId: string, indexName: string) {
204 - return HttpClient.post<FlaskBaseResponse & { alert_details: AlertDetails }>(`/incidents/alerts/alert/details`, {
205 - index_id: indexId,
206 - index_name: indexName
207 - })
208 - },
209 - getAlertTimeline(indexId: string, indexName: string) {
210 - return HttpClient.post<FlaskBaseResponse & { alert_timeline: AlertTimeline[] }>(
211 - `/incidents/alerts/alert/timeline`,
212 - {
213 - index_id: indexId,
214 - index_name: indexName
215 - }
216 - )
217 - },
218 - getAvailableUsers() {
219 - return HttpClient.get<FlaskBaseResponse & { available_users: string[] }>(
220 - `/incidents/db_operations/alert/available-users`
221 - )
222 - },
223 - updateAlertStatus(alertId: number, status: AlertStatus) {
224 - return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/alert/status`, {
225 - alert_id: alertId,
226 - status
227 - })
228 - },
229 - updateAlertAssignedUser(alertId: number, user: string) {
230 - return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/alert/assigned-to`, {
231 - alert_id: alertId,
232 - assigned_to: user
233 - })
234 - },
235 - deleteAlertTag(alertId: number, tagId: number) {
236 - return HttpClient.delete<FlaskBaseResponse & { alert_tag: AlertTag }>(`/incidents/db_operations/alert/tag`, {
237 - data: { alert_id: alertId, tag_id: tagId }
238 - })
239 - },
240 - deleteAlert(alertId: number) {
241 - return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/alert/${alertId}`)
242 - },
243 - deleteAlerts(alertIds: number[]) {
244 - return HttpClient.delete<FlaskBaseResponse & { deleted_alert_ids: number[]; not_deleted_alert_ids: [] }>(
245 - `/incidents/db_operations/alerts`,
246 - {
247 - data: { alert_ids: alertIds }
248 - }
249 - )
250 - },
251 - getAlertContext(alertContextId: number) {
252 - return HttpClient.get<FlaskBaseResponse & { alert_context: AlertContext }>(
253 - `/incidents/db_operations/alert/context/${alertContextId}`
254 - )
255 - },
256 - newAlertComment(payload: AlertCommentPayload) {
257 - return HttpClient.post<FlaskBaseResponse & { comment: AlertComment }>(
258 - `/incidents/db_operations/alert/comment`,
259 - payload
260 - )
261 - },
262 - updateAlertComment(payload: AlertCommentUpdatePayload) {
263 - return HttpClient.put<FlaskBaseResponse & { comment: AlertComment }>(
264 - `/incidents/db_operations/alert/comment`,
265 - payload
266 - )
267 - },
268 - deleteAlertComment(commentId: number) {
269 - return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/alert/comment/${commentId}`)
270 - },
271 - newAlertTag(alertId: number, tag: string) {
272 - return HttpClient.post<FlaskBaseResponse & { alert_tag: AlertTag }>(`/incidents/db_operations/alert/tag`, {
273 - alert_id: alertId,
274 - tag
275 - })
276 - },
277 - createAlertIoc(payload: AlertIocPayload) {
278 - return HttpClient.post<FlaskBaseResponse & { alert_ioc: { alert_id: number; ioc_id: number } }>(
279 - `/incidents/db_operations/alert/ioc`,
280 - payload
281 - )
282 - },
283 - deleteAlertIoc(alertId: number, iocId: number) {
284 - return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/alert/ioc`, {
285 - data: { alert_id: alertId, ioc_id: iocId }
286 - })
287 - },
288 - // #endregion
289 -
290 - // #region Cases
291 - getCasesList(filters?: Partial<UnionToIntersection<CasesFilter>>) {
292 - let url = `/incidents/db_operations/cases`
293 -
294 - if (filters?.status) {
295 - url = `/incidents/db_operations/case/status/${filters.status}`
296 - }
297 - if (filters?.assignedTo) {
298 - url = `/incidents/db_operations/case/assigned-to/${filters.assignedTo}`
299 - }
300 - if (filters?.customerCode) {
301 - url = `/incidents/db_operations/case/customer/${filters.customerCode}`
302 - }
303 - if (filters?.hostname) {
304 - url = `/agents/${filters.hostname}/cases`
305 - }
306 -
307 - return HttpClient.get<FlaskBaseResponse & { cases: Case[] }>(url)
308 - },
309 - getCase(caseId: number) {
310 - return HttpClient.get<FlaskBaseResponse & { cases: Case[] }>(`/incidents/db_operations/case/${caseId}`)
311 - },
312 - createCase(payload: CasePayload) {
313 - return HttpClient.post<FlaskBaseResponse & { case: Case }>(`/incidents/db_operations/case/create`, payload)
314 - },
315 - createCaseFromAlert(alertId: number) {
316 - return HttpClient.post<FlaskBaseResponse & { case_alert_link: { case_id: number; alert_id: number } }>(
317 - `/incidents/db_operations/case/from-alert`,
318 - {
319 - alert_id: alertId
320 - }
321 - )
322 - },
323 - /** @deprecated in favor of multiLinkCase */
324 - linkCase(alertId: number, caseId: number) {
325 - return HttpClient.post<FlaskBaseResponse & { case_alert_link: { case_id: number; alert_id: number } }>(
326 - `/incidents/db_operations/case/alert-link`,
327 - {
328 - alert_id: alertId,
329 - case_id: caseId
330 - }
331 - )
332 - },
333 - multiLinkCase(alertIds: number[], caseId: number) {
334 - return HttpClient.post<FlaskBaseResponse & { case_alert_links: { case_id: number; alert_id: number }[] }>(
335 - `/incidents/db_operations/case/alert-links`,
336 - {
337 - alert_ids: alertIds,
338 - case_id: caseId
339 - }
340 - )
341 - },
342 - unlinkCase(alertId: number, caseId: number) {
343 - return HttpClient.post<FlaskBaseResponse>(`/incidents/db_operations/case/alert-unlink`, {
344 - alert_id: alertId,
345 - case_id: caseId
346 - })
347 - },
348 - updateCaseStatus(caseId: number, status: CaseStatus) {
349 - return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/case/status`, {
350 - case_id: caseId,
351 - status
352 - })
353 - },
354 - updateCaseAssignedUser(caseId: number, user: string) {
355 - return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/case/assigned-to`, {
356 - case_id: caseId,
357 - assigned_to: user
358 - })
359 - },
360 - deleteCase(caseId: number) {
361 - return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/case/${caseId}`)
362 - },
363 - exportCases(customerCode?: string) {
364 - let url = `/incidents/report/generate-report-csv`
365 -
366 - if (customerCode) {
367 - url = `/incidents/report/generate-report-csv/${customerCode}`
368 - }
369 -
370 - return HttpClient.post<Blob>(url, {
371 - responseType: "blob"
372 - })
373 - },
374 - getCaseDataStoreFiles(caseId: number) {
375 - return HttpClient.get<FlaskBaseResponse & { case_data_store: CaseDataStore[] }>(
376 - `/incidents/db_operations/case/data-store/${caseId}`
377 - )
378 - },
379 - downloadCaseDataStoreFile(caseId: number, fileName: string) {
380 - return HttpClient.get<Blob>(`/incidents/db_operations/case/data-store/download/${caseId}/${fileName}`, {
381 - responseType: "blob"
382 - })
383 - },
384 - uploadCaseDataStoreFile(caseId: number, file: File) {
385 - const form = new FormData()
386 - form.append("file", new Blob([file], { type: file.type }), file.name)
387 -
388 - return HttpClient.post<FlaskBaseResponse & { case_data_store: CaseDataStore }>(
389 - `/incidents/db_operations/case/data-store/upload`,
390 - form,
391 - {
392 - params: {
393 - case_id: caseId
394 - }
395 - }
396 - )
397 - },
398 - deleteCaseDataStoreFile(caseId: number, fileName: string) {
399 - return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/case/data-store/${caseId}/${fileName}`)
400 - },
401 - getCaseReportTemplate() {
402 - return HttpClient.get<FlaskBaseResponse & { case_report_template_data_store: string[] }>(
403 - `/incidents/db_operations/case-report-template`
404 - )
405 - },
406 - uploadDefaultCaseReportTemplate() {
407 - return HttpClient.post<FlaskBaseResponse & { case_report_template_data_store: string[] }>(
408 - `/incidents/db_operations/case-report-template/default-template`
409 - )
410 - },
411 - uploadCustomCaseReportTemplate(file: File) {
412 - const form = new FormData()
413 - form.append("file", new Blob([file], { type: file.type }), file.name)
414 -
415 - return HttpClient.post<FlaskBaseResponse & { case_report_template_data_store: CaseReportTemplateDataStore }>(
416 - `/incidents/db_operations/case-report-template/upload`,
417 - form
418 - )
419 - },
420 - downloadCaseReportTemplate(fileName: string) {
421 - return HttpClient.get<Blob>(`/incidents/db_operations/case-report-template/download/${fileName}`, {
422 - responseType: "blob"
423 - })
424 - },
425 - deleteCaseReportTemplate(fileName: string) {
426 - return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/case-report-template/${fileName}`)
427 - },
428 - checkDefaultCaseReportTemplateExists() {
429 - return HttpClient.get<FlaskBaseResponse & { default_template_exists: boolean }>(
430 - `/incidents/db_operations/case-report-template/do-default-template-exists`
431 - )
432 - },
433 - generateCaseReport(payload: CaseReportPayload, type: "docx" | "pdf") {
434 - const url = type === "docx" ? `/incidents/report/generate-report-docx` : `/incidents/report/generate-report-pdf`
435 - return HttpClient.post<Blob>(url, payload, {
436 - responseType: "blob"
437 - })
438 - },
439 - createCaseNotification(caseId: number) {
440 - return HttpClient.post<FlaskBaseResponse>(`/incidents/db_operations/case/notification`, { case_id: caseId })
441 - },
442 - // #endregion
443 -
444 - // #region Notification
445 - getNotifications(customerCode: string) {
446 - return HttpClient.get<FlaskBaseResponse & { notifications: IncidentNotification[] }>(
447 - `/incidents/db_operations/notification/${customerCode}`
448 - )
449 - },
450 - setNotification(notification: IncidentNotificationPayload) {
451 - return HttpClient.put<FlaskBaseResponse & { notifications: IncidentNotification[] }>(
452 - `/incidents/db_operations/notification`,
453 - notification,
454 - {
455 - params: {
456 - customer_code: notification.customer_code
457 - }
458 - }
459 - )
460 - }
461 - // #endregion
462 -}
frontend/src/api/endpoints/incidentManagement/alerts.ts new
+216
@@ -0,0 +1,216 @@
1 +import type { FlaskBaseResponse } from "@/types/flask.d"
2 +import type {
3 + Alert,
4 + AlertComment,
5 + AlertContext,
6 + AlertDetails,
7 + AlertsFilter,
8 + AlertStatus,
9 + AlertTag,
10 + AlertTimeline
11 +} from "@/types/incidentManagement/alerts.d"
12 +import type { KeysOfUnion, UnionToIntersection } from "type-fest"
13 +import _castArray from "lodash/castArray"
14 +import { HttpClient } from "../../httpClient"
15 +
16 +export type AlertsListFilterValue = string | string[] | AlertStatus | null
17 +export type AlertsFilterTypes = KeysOfUnion<AlertsFilter>
18 +
19 +export interface AlertsQuery {
20 + page: number
21 + pageSize: number
22 + sort: "asc" | "desc"
23 + filter: Partial<UnionToIntersection<AlertsFilter>>
24 + filters: {
25 + type: AlertsFilterTypes
26 + value: AlertsListFilterValue
27 + }[]
28 +}
29 +
30 +export type AlertCommentPayload = Omit<AlertComment, "id">
31 +
32 +export type AlertCommentUpdatePayload = Omit<AlertComment, "id"> & { comment_id: number }
33 +
34 +export interface AlertIocPayload {
35 + alert_id: number
36 + ioc_value: string
37 + ioc_type: string
38 + ioc_description: string
39 +}
40 +
41 +export default {
42 + getAlertsList(args: Partial<AlertsQuery>, signal?: AbortSignal) {
43 + let url = `/incidents/db_operations/alerts`
44 +
45 + if (args?.filter?.status) {
46 + url = `/incidents/db_operations/alerts/status/${args.filter.status}`
47 + }
48 + if (args?.filter?.assetName) {
49 + url = `/incidents/db_operations/alerts/asset/${args.filter.assetName}`
50 + }
51 + if (args?.filter?.assignedTo) {
52 + url = `/incidents/db_operations/alerts/assigned-to/${args.filter.assignedTo}`
53 + }
54 + if (args?.filter?.tag) {
55 + url = `/incidents/db_operations/alert/tag/${_castArray(args.filter.tag).join(",")}`
56 + }
57 + if (args?.filter?.title) {
58 + url = `/incidents/db_operations/alerts/title/${args.filter.title}`
59 + }
60 + if (args?.filter?.customerCode) {
61 + url = `/incidents/db_operations/alerts/customer/${args.filter.customerCode}`
62 + }
63 + if (args?.filter?.source) {
64 + url = `/incidents/db_operations/alerts/source/${args.filter.source}`
65 + }
66 +
67 + const params: any = {
68 + page: args.page || 1,
69 + page_size: args.pageSize || 25,
70 + order: args.sort || "desc"
71 + }
72 +
73 + if (args.filters?.length) {
74 + for (const filter of args.filters) {
75 + if (filter.value?.length) {
76 + switch (filter.type) {
77 + case "assignedTo":
78 + params.assigned_to = filter.value
79 + break
80 + case "title":
81 + params.alert_title = filter.value
82 + break
83 + case "customerCode":
84 + params.customer_code = filter.value
85 + break
86 + case "source":
87 + params.source = filter.value
88 + break
89 + case "assetName":
90 + params.asset_name = filter.value
91 + break
92 + case "iocValue":
93 + params.ioc_value = filter.value
94 + break
95 + case "status":
96 + params.status = filter.value
97 + break
98 + case "tag":
99 + params.tags = _castArray(filter.value)
100 + break
101 + default:
102 + params[filter.type] = filter.value
103 + break
104 + }
105 + }
106 + }
107 +
108 + url = `/incidents/db_operations/alerts/filter`
109 + }
110 +
111 + return HttpClient.get<
112 + FlaskBaseResponse & {
113 + alerts: Alert[]
114 + closed: number
115 + in_progress: number
116 + open: number
117 + total: number
118 + total_filtered: number
119 + }
120 + >(url, {
121 + params,
122 + paramsSerializer: {
123 + indexes: null // remove brackets in array types
124 + },
125 + signal
126 + })
127 + },
128 + getAlert(alertId: number) {
129 + return HttpClient.get<FlaskBaseResponse & { alerts: Alert[] }>(`/incidents/db_operations/alert/${alertId}`)
130 + },
131 + getAlertDetails(indexId: string, indexName: string) {
132 + return HttpClient.post<FlaskBaseResponse & { alert_details: AlertDetails }>(`/incidents/alerts/alert/details`, {
133 + index_id: indexId,
134 + index_name: indexName
135 + })
136 + },
137 + getAlertTimeline(indexId: string, indexName: string) {
138 + return HttpClient.post<FlaskBaseResponse & { alert_timeline: AlertTimeline[] }>(
139 + `/incidents/alerts/alert/timeline`,
140 + {
141 + index_id: indexId,
142 + index_name: indexName
143 + }
144 + )
145 + },
146 + getAvailableUsers() {
147 + return HttpClient.get<FlaskBaseResponse & { available_users: string[] }>(
148 + `/incidents/db_operations/alert/available-users`
149 + )
150 + },
151 + updateAlertStatus(alertId: number, status: AlertStatus) {
152 + return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/alert/status`, {
153 + alert_id: alertId,
154 + status
155 + })
156 + },
157 + updateAlertAssignedUser(alertId: number, user: string) {
158 + return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/alert/assigned-to`, {
159 + alert_id: alertId,
160 + assigned_to: user
161 + })
162 + },
163 + deleteAlertTag(alertId: number, tagId: number) {
164 + return HttpClient.delete<FlaskBaseResponse & { alert_tag: AlertTag }>(`/incidents/db_operations/alert/tag`, {
165 + data: { alert_id: alertId, tag_id: tagId }
166 + })
167 + },
168 + deleteAlert(alertId: number) {
169 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/alert/${alertId}`)
170 + },
171 + deleteAlerts(alertIds: number[]) {
172 + return HttpClient.delete<FlaskBaseResponse & { deleted_alert_ids: number[]; not_deleted_alert_ids: [] }>(
173 + `/incidents/db_operations/alerts`,
174 + {
175 + data: { alert_ids: alertIds }
176 + }
177 + )
178 + },
179 + getAlertContext(alertContextId: number) {
180 + return HttpClient.get<FlaskBaseResponse & { alert_context: AlertContext }>(
181 + `/incidents/db_operations/alert/context/${alertContextId}`
182 + )
183 + },
184 + newAlertComment(payload: AlertCommentPayload) {
185 + return HttpClient.post<FlaskBaseResponse & { comment: AlertComment }>(
186 + `/incidents/db_operations/alert/comment`,
187 + payload
188 + )
189 + },
190 + updateAlertComment(payload: AlertCommentUpdatePayload) {
191 + return HttpClient.put<FlaskBaseResponse & { comment: AlertComment }>(
192 + `/incidents/db_operations/alert/comment`,
193 + payload
194 + )
195 + },
196 + deleteAlertComment(commentId: number) {
197 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/alert/comment/${commentId}`)
198 + },
199 + newAlertTag(alertId: number, tag: string) {
200 + return HttpClient.post<FlaskBaseResponse & { alert_tag: AlertTag }>(`/incidents/db_operations/alert/tag`, {
201 + alert_id: alertId,
202 + tag
203 + })
204 + },
205 + createAlertIoc(payload: AlertIocPayload) {
206 + return HttpClient.post<FlaskBaseResponse & { alert_ioc: { alert_id: number; ioc_id: number } }>(
207 + `/incidents/db_operations/alert/ioc`,
208 + payload
209 + )
210 + },
211 + deleteAlertIoc(alertId: number, iocId: number) {
212 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/alert/ioc`, {
213 + data: { alert_id: alertId, ioc_id: iocId }
214 + })
215 + }
216 +}
frontend/src/api/endpoints/incidentManagement/cases.ts new
+178
@@ -0,0 +1,178 @@
1 +import type { FlaskBaseResponse } from "@/types/flask.d"
2 +import type {
3 + Case,
4 + CaseDataStore,
5 + CasePayload,
6 + CaseReportTemplateDataStore,
7 + CaseStatus
8 +} from "@/types/incidentManagement/cases.d"
9 +import type { KeysOfUnion, UnionToIntersection } from "type-fest"
10 +import { HttpClient } from "../../httpClient"
11 +
12 +export type CasesFilter =
13 + | { status: CaseStatus }
14 + | { assignedTo: string }
15 + | { hostname: string }
16 + | { customerCode: string }
17 +
18 +export type CasesFilterTypes = KeysOfUnion<CasesFilter>
19 +
20 +export interface CaseReportPayload {
21 + case_id: number
22 + file_name: string
23 + template_name: string
24 +}
25 +
26 +export default {
27 + getCasesList(filters?: Partial<UnionToIntersection<CasesFilter>>) {
28 + let url = `/incidents/db_operations/cases`
29 +
30 + if (filters?.status) {
31 + url = `/incidents/db_operations/case/status/${filters.status}`
32 + }
33 + if (filters?.assignedTo) {
34 + url = `/incidents/db_operations/case/assigned-to/${filters.assignedTo}`
35 + }
36 + if (filters?.customerCode) {
37 + url = `/incidents/db_operations/case/customer/${filters.customerCode}`
38 + }
39 + if (filters?.hostname) {
40 + url = `/agents/${filters.hostname}/cases`
41 + }
42 +
43 + return HttpClient.get<FlaskBaseResponse & { cases: Case[] }>(url)
44 + },
45 + getCase(caseId: number) {
46 + return HttpClient.get<FlaskBaseResponse & { cases: Case[] }>(`/incidents/db_operations/case/${caseId}`)
47 + },
48 + createCase(payload: CasePayload) {
49 + return HttpClient.post<FlaskBaseResponse & { case: Case }>(`/incidents/db_operations/case/create`, payload)
50 + },
51 + createCaseFromAlert(alertId: number) {
52 + return HttpClient.post<FlaskBaseResponse & { case_alert_link: { case_id: number; alert_id: number } }>(
53 + `/incidents/db_operations/case/from-alert`,
54 + {
55 + alert_id: alertId
56 + }
57 + )
58 + },
59 + /** @deprecated in favor of multiLinkCase */
60 + linkCase(alertId: number, caseId: number) {
61 + return HttpClient.post<FlaskBaseResponse & { case_alert_link: { case_id: number; alert_id: number } }>(
62 + `/incidents/db_operations/case/alert-link`,
63 + {
64 + alert_id: alertId,
65 + case_id: caseId
66 + }
67 + )
68 + },
69 + multiLinkCase(alertIds: number[], caseId: number) {
70 + return HttpClient.post<FlaskBaseResponse & { case_alert_links: { case_id: number; alert_id: number }[] }>(
71 + `/incidents/db_operations/case/alert-links`,
72 + {
73 + alert_ids: alertIds,
74 + case_id: caseId
75 + }
76 + )
77 + },
78 + unlinkCase(alertId: number, caseId: number) {
79 + return HttpClient.post<FlaskBaseResponse>(`/incidents/db_operations/case/alert-unlink`, {
80 + alert_id: alertId,
81 + case_id: caseId
82 + })
83 + },
84 + updateCaseStatus(caseId: number, status: CaseStatus) {
85 + return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/case/status`, {
86 + case_id: caseId,
87 + status
88 + })
89 + },
90 + updateCaseAssignedUser(caseId: number, user: string) {
91 + return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/case/assigned-to`, {
92 + case_id: caseId,
93 + assigned_to: user
94 + })
95 + },
96 + deleteCase(caseId: number) {
97 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/case/${caseId}`)
98 + },
99 + exportCases(customerCode?: string) {
100 + let url = `/incidents/report/generate-report-csv`
101 +
102 + if (customerCode) {
103 + url = `/incidents/report/generate-report-csv/${customerCode}`
104 + }
105 +
106 + return HttpClient.post<Blob>(url, {
107 + responseType: "blob"
108 + })
109 + },
110 + getCaseDataStoreFiles(caseId: number) {
111 + return HttpClient.get<FlaskBaseResponse & { case_data_store: CaseDataStore[] }>(
112 + `/incidents/db_operations/case/data-store/${caseId}`
113 + )
114 + },
115 + downloadCaseDataStoreFile(caseId: number, fileName: string) {
116 + return HttpClient.get<Blob>(`/incidents/db_operations/case/data-store/download/${caseId}/${fileName}`, {
117 + responseType: "blob"
118 + })
119 + },
120 + uploadCaseDataStoreFile(caseId: number, file: File) {
121 + const form = new FormData()
122 + form.append("file", new Blob([file], { type: file.type }), file.name)
123 +
124 + return HttpClient.post<FlaskBaseResponse & { case_data_store: CaseDataStore }>(
125 + `/incidents/db_operations/case/data-store/upload`,
126 + form,
127 + {
128 + params: {
129 + case_id: caseId
130 + }
131 + }
132 + )
133 + },
134 + deleteCaseDataStoreFile(caseId: number, fileName: string) {
135 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/case/data-store/${caseId}/${fileName}`)
136 + },
137 + getCaseReportTemplate() {
138 + return HttpClient.get<FlaskBaseResponse & { case_report_template_data_store: string[] }>(
139 + `/incidents/db_operations/case-report-template`
140 + )
141 + },
142 + uploadDefaultCaseReportTemplate() {
143 + return HttpClient.post<FlaskBaseResponse & { case_report_template_data_store: string[] }>(
144 + `/incidents/db_operations/case-report-template/default-template`
145 + )
146 + },
147 + uploadCustomCaseReportTemplate(file: File) {
148 + const form = new FormData()
149 + form.append("file", new Blob([file], { type: file.type }), file.name)
150 +
151 + return HttpClient.post<FlaskBaseResponse & { case_report_template_data_store: CaseReportTemplateDataStore }>(
152 + `/incidents/db_operations/case-report-template/upload`,
153 + form
154 + )
155 + },
156 + downloadCaseReportTemplate(fileName: string) {
157 + return HttpClient.get<Blob>(`/incidents/db_operations/case-report-template/download/${fileName}`, {
158 + responseType: "blob"
159 + })
160 + },
161 + deleteCaseReportTemplate(fileName: string) {
162 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/case-report-template/${fileName}`)
163 + },
164 + checkDefaultCaseReportTemplateExists() {
165 + return HttpClient.get<FlaskBaseResponse & { default_template_exists: boolean }>(
166 + `/incidents/db_operations/case-report-template/do-default-template-exists`
167 + )
168 + },
169 + generateCaseReport(payload: CaseReportPayload, type: "docx" | "pdf") {
170 + const url = type === "docx" ? `/incidents/report/generate-report-docx` : `/incidents/report/generate-report-pdf`
171 + return HttpClient.post<Blob>(url, payload, {
172 + responseType: "blob"
173 + })
174 + },
175 + createCaseNotification(caseId: number) {
176 + return HttpClient.post<FlaskBaseResponse>(`/incidents/db_operations/case/notification`, { case_id: caseId })
177 + }
178 +}
frontend/src/api/endpoints/incidentManagement/exclusionRules.ts new
+67
@@ -0,0 +1,67 @@
1 +import type { FlaskBaseResponse } from "@/types/flask.d"
2 +import type { ExclusionRule } from "@/types/incidentManagement/exclusionRules.d"
3 +import { HttpClient } from "../../httpClient"
4 +
5 +export interface ExclusionRulesQuery {
6 + pagination: {
7 + skip?: number
8 + limit?: number
9 + }
10 + filters: {
11 + enabledOnly?: boolean
12 + }
13 +}
14 +
15 +export interface ExclusionRulePayload {
16 + name: string
17 + description: string
18 + channel: string
19 + title: string
20 + field_matches: { [key: string]: string }
21 + enabled: boolean
22 + customer_code?: string
23 +}
24 +
25 +export default {
26 + getExclusionRulesList(args: Partial<ExclusionRulesQuery>, signal?: AbortSignal) {
27 + const params: any = {
28 + skip: args.pagination?.skip || 0,
29 + limit: args.pagination?.limit || 25
30 + }
31 +
32 + if (args.filters?.enabledOnly !== undefined) {
33 + params.enabled_only = args.filters.enabledOnly
34 + }
35 +
36 + return HttpClient.get<
37 + FlaskBaseResponse & {
38 + exclusions: ExclusionRule[]
39 + pagination: {
40 + total: number
41 + skip: number
42 + limit: number
43 + }
44 + }
45 + >(`/incidents/alerts/create/velo-sigma/exclusion`, { params, signal })
46 + },
47 + createExclusionRule(payload: ExclusionRulePayload) {
48 + return HttpClient.post<FlaskBaseResponse & { exclusion_response: ExclusionRule }>(
49 + `/incidents/alerts/create/velo-sigma/exclusion`,
50 + payload
51 + )
52 + },
53 + updateExclusionRule(exclusionId: number, payload: ExclusionRulePayload) {
54 + return HttpClient.patch<FlaskBaseResponse & { exclusion_response: ExclusionRule }>(
55 + `/incidents/alerts/create/velo-sigma/exclusion/${exclusionId}`,
56 + payload
57 + )
58 + },
59 + toggleExclusionRuleStatus(exclusionId: number) {
60 + return HttpClient.post<FlaskBaseResponse & { exclusion_response: ExclusionRule }>(
61 + `/incidents/alerts/velo-sigma/exclusion/${exclusionId}/toggle`
62 + )
63 + },
64 + deleteExclusionRules(exclusionId: number) {
65 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/alerts/create/velo-sigma/exclusion/${exclusionId}`)
66 + }
67 +}
frontend/src/api/endpoints/incidentManagement/index.ts new
+13
@@ -0,0 +1,13 @@
1 +import alerts from "./alerts"
2 +import cases from "./cases"
3 +import exclusionRules from "./exclusionRules"
4 +import notification from "./notification"
5 +import sources from "./sources"
6 +
7 +export default {
8 + alerts,
9 + cases,
10 + exclusionRules,
11 + notification,
12 + sources
13 +}
frontend/src/api/endpoints/incidentManagement/notification.ts new
+28
@@ -0,0 +1,28 @@
1 +import type { FlaskBaseResponse } from "@/types/flask.d"
2 +import type { IncidentNotification } from "@/types/incidentManagement/notifications.d"
3 +import { HttpClient } from "../../httpClient"
4 +
5 +export interface IncidentNotificationPayload {
6 + customer_code: string
7 + shuffle_workflow_id: string
8 + enabled: boolean
9 +}
10 +
11 +export default {
12 + getNotifications(customerCode: string) {
13 + return HttpClient.get<FlaskBaseResponse & { notifications: IncidentNotification[] }>(
14 + `/incidents/db_operations/notification/${customerCode}`
15 + )
16 + },
17 + setNotification(notification: IncidentNotificationPayload) {
18 + return HttpClient.put<FlaskBaseResponse & { notifications: IncidentNotification[] }>(
19 + `/incidents/db_operations/notification`,
20 + notification,
21 + {
22 + params: {
23 + customer_code: notification.customer_code
24 + }
25 + }
26 + )
27 + }
28 +}
frontend/src/api/endpoints/incidentManagement/sources.ts new
+51
@@ -0,0 +1,51 @@
1 +import type { FlaskBaseResponse } from "@/types/flask.d"
2 +import type { SourceConfiguration, SourceName } from "@/types/incidentManagement/sources.d"
3 +import { HttpClient } from "../../httpClient"
4 +
5 +export default {
6 + getConfiguredSources() {
7 + return HttpClient.get<FlaskBaseResponse & { sources: SourceName[] }>(
8 + `/incidents/db_operations/configured/sources`
9 + )
10 + },
11 + getAvailableMappings(indexName: string) {
12 + return HttpClient.get<FlaskBaseResponse & { available_mappings: string[] }>(
13 + `/incidents/db_operations/mappings/fields-assets-title-and-timefield`,
14 + {
15 + params: { index_name: indexName }
16 + }
17 + )
18 + },
19 + getSourceByIndex(indexName: string) {
20 + return HttpClient.get<FlaskBaseResponse & { source: SourceName }>(
21 + `/incidents/db_operations/available-source/${indexName}`
22 + )
23 + },
24 + getAvailableIndices(source: SourceName) {
25 + return HttpClient.get<FlaskBaseResponse & { indices: string[] }>(
26 + `/incidents/db_operations/available-indices/${source}`
27 + )
28 + },
29 + createSourceConfiguration(payload: SourceConfiguration) {
30 + return HttpClient.post<FlaskBaseResponse>(`/incidents/db_operations/fields-assets-title-and-timefield`, payload)
31 + },
32 + updateSourceConfiguration(payload: SourceConfiguration) {
33 + return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/fields-assets-title-and-timefield`, payload)
34 + },
35 + getSourceConfiguration(source: SourceName) {
36 + return HttpClient.get<FlaskBaseResponse & SourceConfiguration>(
37 + `/incidents/db_operations/fields-assets-title-and-timefield`,
38 + {
39 + params: { source }
40 + }
41 + )
42 + },
43 + getSocfortressRecommendsWazuh() {
44 + return HttpClient.get<FlaskBaseResponse & SourceConfiguration>(
45 + `/incidents/db_operations/socfortress/recommends/wazuh`
46 + )
47 + },
48 + deleteSourceConfiguration(source: SourceName) {
49 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/configured/sources/${source}`)
50 + }
51 +}
frontend/src/components/customers/notifications/CustomerNotificationsWorkflows.vue
+1 -1
@@ -74,7 +74,7 @@ const formCTX = ref<{ reset: (incidentNotification?: IncidentNotification) => vo
74 function getCustomerNetworkConnectors() {
75 loading.value = true
76
77 - Api.incidentManagement
77 + Api.incidentManagement.notification
78 .getNotifications(customerCode)
79 .then(res => {
80 if (res.data.success) {
frontend/src/components/customers/notifications/CustomerNotificationsWorkflowsForm.vue
+3 -2
@@ -29,7 +29,8 @@
29 </template>
30
31 <script setup lang="ts">
32 -import type { IncidentNotification, IncidentNotificationPayload } from "@/types/incidentManagement/notifications.d"
32 +import type { IncidentNotificationPayload } from "@/api/endpoints/incidentManagement/notification"
33 +import type { IncidentNotification } from "@/types/incidentManagement/notifications.d"
34 import type { FormInst, FormRules, FormValidationError } from "naive-ui"
35 import Api from "@/api"
36 import { NButton, NForm, NFormItem, NInput, NSpin, NSwitch, useMessage } from "naive-ui"
@@ -122,7 +123,7 @@ function submit() {
123 enabled: form.value.enabled
124 }
125
125 - Api.incidentManagement
126 + Api.incidentManagement.notification
127 .setNotification(payload)
128 .then(res => {
129 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertAsset.vue
+1 -1
@@ -209,7 +209,7 @@ watch(showDetails, val => {
209 function getAlertContext(alertContextId: number) {
210 loading.value = true
211
212 - Api.incidentManagement
212 + Api.incidentManagement.alerts
213 .getAlertContext(alertContextId)
214 .then(res => {
215 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertAssetInfo.vue
+1 -1
@@ -116,7 +116,7 @@ watch(showAlertDetails, val => {
116 function getAlertDetails(indexId: string, indexName: string) {
117 loading.value = true
118
119 - Api.incidentManagement
119 + Api.incidentManagement.alerts
120 .getAlertDetails(indexId, indexName)
121 .then(res => {
122 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertAssignUser.vue
+2 -2
@@ -44,7 +44,7 @@ const userSelected = ref<string | null>(null)
44 function getUsers() {
45 loadingUsers.value = true
46
47 - Api.incidentManagement
47 + Api.incidentManagement.alerts
48 .getAvailableUsers()
49 .then(res => {
50 if (res.data.success) {
@@ -66,7 +66,7 @@ function assignUser() {
66 if (userSelected.value && userSelected.value !== assignedTo.value) {
67 loadingUsers.value = true
68
69 - Api.incidentManagement
69 + Api.incidentManagement.alerts
70 .updateAlertAssignedUser(alert.value.id, userSelected.value)
71 .then(res => {
72 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertComment.vue
+2 -2
@@ -124,7 +124,7 @@ function editComment() {
124 function updateAlertComment() {
125 saving.value = true
126
127 - Api.incidentManagement
127 + Api.incidentManagement.alerts
128 .updateAlertComment({
129 alert_id: comment.value.alert_id,
130 comment_id: comment.value.id,
@@ -152,7 +152,7 @@ function updateAlertComment() {
152 function deleteAlertComment() {
153 canceling.value = true
154
155 - Api.incidentManagement
155 + Api.incidentManagement.alerts
156 .deleteAlertComment(comment.value.id)
157 .then(res => {
158 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertCommentsList.vue
+1 -1
@@ -93,7 +93,7 @@ function submit() {
93 if (trimmedValue.value) {
94 submitting.value = true
95
96 - Api.incidentManagement
96 + Api.incidentManagement.alerts
97 .newAlertComment({
98 alert_id: alertId.value,
99 comment: trimmedValue.value,
frontend/src/components/incidentManagement/alerts/AlertCreateCaseButton.vue
+1 -1
@@ -33,7 +33,7 @@ function updateAlert(updatedAlert: Alert) {
33 function createCase() {
34 creating.value = true
35
36 - Api.incidentManagement
36 + Api.incidentManagement.cases
37 .createCaseFromAlert(alert.value.id)
38 .then(res => {
39 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertDetailTimeline.vue
+1 -1
@@ -38,7 +38,7 @@ function formatDateTime(timestamp: Date | string): string {
38 function getAlertTimeline() {
39 loading.value = true
40
41 - Api.incidentManagement
41 + Api.incidentManagement.alerts
42 .getAlertTimeline(asset.index_id, asset.index_name)
43 .then(res => {
44 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertDetails.vue
+1 -1
@@ -89,7 +89,7 @@ function updateIos(iocs: AlertIOC[]) {
89 function getAlert(alertId: number) {
90 loading.value = true
91
92 - Api.incidentManagement
92 + Api.incidentManagement.alerts
93 .getAlert(alertId)
94 .then(res => {
95 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertIoCItem.vue
+1 -1
@@ -55,7 +55,7 @@ const showDeleteConfirm = ref(false)
55 function deleteIoc() {
56 canceling.value = true
57
58 - Api.incidentManagement
58 + Api.incidentManagement.alerts
59 .deleteAlertIoc(alertId, ioc.id)
60 .then(res => {
61 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertIoCsForm.vue
+2 -2
@@ -54,7 +54,7 @@
54 </template>
55
56 <script setup lang="ts">
57 -import type { AlertIocPayload } from "@/api/endpoints/incidentManagement"
57 +import type { AlertIocPayload } from "@/api/endpoints/incidentManagement/alerts"
58 import type { DeepNullable } from "@/types/common"
59 import type { AlertIOC } from "@/types/incidentManagement/alerts.d"
60 import type { FormInst, FormItemRule, FormRules, FormValidationError } from "naive-ui"
@@ -188,7 +188,7 @@ function resetForm() {
188 function submit() {
189 submitting.value = true
190
191 - Api.incidentManagement
191 + Api.incidentManagement.alerts
192 .createAlertIoc(form.value as AlertIocPayload)
193 .then(res => {
194 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertItem.vue
+2 -2
@@ -181,7 +181,7 @@
181 </template>
182
183 <template v-if="alert && !compact" #footerMain>
184 - <div class="flex items-center gap-3">
184 + <div class="flex flex-wrap items-center gap-3">
185 <Badge v-if="alert.alert_creation_time" type="splitted" :class="{ 'flex sm:!hidden': !compact }">
186 <template #iconLeft>
187 <Icon :name="TimeIcon" :size="16" />
@@ -338,7 +338,7 @@ function updateAlert(updatedAlert: Alert) {
338 function getAlert(alertId: number) {
339 loading.value = true
340
341 - Api.incidentManagement
341 + Api.incidentManagement.alerts
342 .getAlert(alertId)
343 .then(res => {
344 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertLinkedCases.vue
+1 -1
@@ -66,7 +66,7 @@ function unlink(caseId: number) {
66
67 loadingId.value = caseId
68
69 - Api.incidentManagement
69 + Api.incidentManagement.cases
70 .unlinkCase(alert.value.id, caseId)
71 .then(res => {
72 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertMergeCaseButton.vue
+2 -2
@@ -109,7 +109,7 @@ function toggleSelectedCase(caseEntity: Case) {
109 function getCasesList() {
110 loadingCases.value = true
111
112 - Api.incidentManagement
112 + Api.incidentManagement.cases
113 .getCasesList()
114 .then(res => {
115 if (res.data.success) {
@@ -130,7 +130,7 @@ function linkCase() {
130 if (selectedCase.value?.id) {
131 merging.value = true
132
133 - Api.incidentManagement
133 + Api.incidentManagement.cases
134 .multiLinkCase(
135 alerts.map(o => o.id),
136 selectedCase.value.id
frontend/src/components/incidentManagement/alerts/AlertStatusSwitch.vue
+1 -1
@@ -47,7 +47,7 @@ function updateStatus() {
47 if (statusSelected.value && statusSelected.value !== status.value) {
48 loading.value = true
49
50 - Api.incidentManagement
50 + Api.incidentManagement.alerts
51 .updateAlertStatus(alert.value.id, statusSelected.value)
52 .then(res => {
53 if (res.data.success && statusSelected.value) {
frontend/src/components/incidentManagement/alerts/AlertTags.vue
+2 -2
@@ -52,7 +52,7 @@ function updateAlert(updatedAlert: Alert) {
52 function deleteTag(tagId: number) {
53 deletingTag.value = true
54
55 - Api.incidentManagement
55 + Api.incidentManagement.alerts
56 .deleteAlertTag(alert.value.id, tagId)
57 .then(res => {
58 if (res.data.success) {
@@ -80,7 +80,7 @@ function newAlertTag(text: string): string | { label: string; value: string } {
80 if (tag && alert.value.tags.filter(o => o.tag.toLowerCase() === tag.toLowerCase()).length === 0) {
81 creatingTag.value = true
82
83 - Api.incidentManagement
83 + Api.incidentManagement.alerts
84 .newAlertTag(alert.value.id, tag)
85 .then(res => {
86 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertsFilters.vue
+3 -3
@@ -153,7 +153,7 @@
153 </template>
154
155 <script setup lang="ts">
156 -import type { AlertsFilterTypes, AlertsListFilterValue } from "@/api/endpoints/incidentManagement"
156 +import type { AlertsFilterTypes, AlertsListFilterValue } from "@/api/endpoints/incidentManagement/alerts"
157 import type { Customer } from "@/types/customers.d"
158 import type { AlertStatus } from "@/types/incidentManagement/alerts.d"
159 import type { SourceName } from "@/types/incidentManagement/sources.d"
@@ -291,7 +291,7 @@ function getQueryString() {
291 function getAvailableUsers() {
292 loadingAvailableUsers.value = true
293
294 - Api.incidentManagement
294 + Api.incidentManagement.alerts
295 .getAvailableUsers()
296 .then(res => {
297 if (res.data.success) {
@@ -331,7 +331,7 @@ function getCustomers() {
331 function getConfiguredSources() {
332 loadingConfiguredSources.value = true
333
334 - Api.incidentManagement
334 + Api.incidentManagement.sources
335 .getConfiguredSources()
336 .then(res => {
337 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/AlertsList.vue
+5 -5
@@ -220,7 +220,7 @@
220 </template>
221
222 <script setup lang="ts">
223 -import type { AlertsQuery } from "@/api/endpoints/incidentManagement"
223 +import type { AlertsQuery } from "@/api/endpoints/incidentManagement/alerts"
224 import type { Alert } from "@/types/incidentManagement/alerts.d"
225 import type { Case } from "@/types/incidentManagement/cases.d"
226 import type { AlertsListFilter } from "./types.d"
@@ -408,7 +408,7 @@ function getData() {
408 query.filters = filters.value
409 }
410
411 - Api.incidentManagement
411 + Api.incidentManagement.alerts
412 .getAlertsList(query, abortController.signal)
413 .then(res => {
414 if (res.data.success) {
@@ -434,7 +434,7 @@ function getData() {
434 }
435
436 function getAvailableUsers() {
437 - Api.incidentManagement
437 + Api.incidentManagement.alerts
438 .getAvailableUsers()
439 .then(res => {
440 if (res.data.success) {
@@ -449,7 +449,7 @@ function getAvailableUsers() {
449 }
450
451 function getCases() {
452 - Api.incidentManagement
452 + Api.incidentManagement.cases
453 .getCasesList()
454 .then(res => {
455 if (res.data.success) {
@@ -466,7 +466,7 @@ function getCases() {
466 function deleteAlerts() {
467 deleting.value = true
468
469 - Api.incidentManagement
469 + Api.incidentManagement.alerts
470 .deleteAlerts(checkedAlerts.value.map(o => o.id))
471 .then(res => {
472 if (res.data.success) {
frontend/src/components/incidentManagement/alerts/utils.ts
+1 -1
@@ -45,7 +45,7 @@ export function deleteAlert({ alert, cbBefore, cbSuccess, cbAfter, cbError, mess
45 cbBefore()
46 }
47
48 - Api.incidentManagement
48 + Api.incidentManagement.alerts
49 .deleteAlert(alert.id)
50 .then(res => {
51 if (res.data.success) {
frontend/src/components/incidentManagement/cases/CaseAssignUser.vue
+2 -2
@@ -44,7 +44,7 @@ const userSelected = ref<string | null>(null)
44 function getUsers() {
45 loadingUsers.value = true
46
47 - Api.incidentManagement
47 + Api.incidentManagement.alerts
48 .getAvailableUsers()
49 .then(res => {
50 if (res.data.success) {
@@ -66,7 +66,7 @@ function assignUser() {
66 if (userSelected.value && userSelected.value !== assignedTo.value) {
67 loadingUsers.value = true
68
69 - Api.incidentManagement
69 + Api.incidentManagement.cases
70 .updateCaseAssignedUser(caseData.value.id, userSelected.value)
71 .then(res => {
72 if (res.data.success) {
frontend/src/components/incidentManagement/cases/CaseCreationForm.vue
+2 -2
@@ -193,7 +193,7 @@ function resetForm() {
193 function submit() {
194 submitting.value = true
195
196 - Api.incidentManagement
196 + Api.incidentManagement.cases
197 .createCase(form.value)
198 .then(res => {
199 if (res.data.success) {
@@ -215,7 +215,7 @@ function submit() {
215 function getAvailableUsers() {
216 loadingAvailableUsers.value = true
217
218 - Api.incidentManagement
218 + Api.incidentManagement.alerts
219 .getAvailableUsers()
220 .then(res => {
221 if (res.data.success) {
frontend/src/components/incidentManagement/cases/CaseDataStore.vue
+2 -2
@@ -121,7 +121,7 @@ function closeUploadForm() {
121 function getCaseDataStore(caseId: number) {
122 loading.value = true
123
124 - Api.incidentManagement
124 + Api.incidentManagement.cases
125 .getCaseDataStoreFiles(caseId)
126 .then(res => {
127 if (res.data.success) {
@@ -143,7 +143,7 @@ function uploadDataStoreFile() {
143
144 uploading.value = true
145
146 - Api.incidentManagement
146 + Api.incidentManagement.cases
147 .uploadCaseDataStoreFile(caseId, newFile.value)
148 .then(res => {
149 if (res.data.success) {
frontend/src/components/incidentManagement/cases/CaseDataStoreItem.vue
+2 -2
@@ -100,7 +100,7 @@ const prettyBytes = computed(() => bytes(dataStoreFile.file_size))
100 function downloadFile() {
101 downloading.value = true
102
103 - Api.incidentManagement
103 + Api.incidentManagement.cases
104 .downloadCaseDataStoreFile(dataStoreFile.case_id, dataStoreFile.file_name)
105 .then(res => {
106 if (res.data) {
@@ -120,7 +120,7 @@ function downloadFile() {
120 function deleteDataStoreFile() {
121 canceling.value = true
122
123 - Api.incidentManagement
123 + Api.incidentManagement.cases
124 .deleteCaseDataStoreFile(dataStoreFile.case_id, dataStoreFile.file_name)
125 .then(res => {
126 if (res.data.success) {
frontend/src/components/incidentManagement/cases/CaseDetails.vue
+1 -1
@@ -72,7 +72,7 @@ function updateCase(updatedCase: Case) {
72 function getCase(caseId: number) {
73 loading.value = true
74
75 - Api.incidentManagement
75 + Api.incidentManagement.cases
76 .getCase(caseId)
77 .then(res => {
78 if (res.data.success) {
frontend/src/components/incidentManagement/cases/CaseItem.vue
+1 -1
@@ -282,7 +282,7 @@ function updateCase(updatedCase: Case) {
282 function getCase(caseId: number) {
283 loading.value = true
284
285 - Api.incidentManagement
285 + Api.incidentManagement.cases
286 .getCase(caseId)
287 .then(res => {
288 if (res.data.success) {
frontend/src/components/incidentManagement/cases/CaseNotificationButton.vue
+1 -1
@@ -40,7 +40,7 @@ const message = useMessage()
40 function invoke() {
41 invoking.value = true
42
43 - Api.incidentManagement
43 + Api.incidentManagement.cases
44 .createCaseNotification(caseId)
45 .then(res => {
46 if (res.data) {
frontend/src/components/incidentManagement/cases/CaseReportButton.vue
+2 -2
@@ -52,7 +52,7 @@
52 </template>
53
54 <script setup lang="ts">
55 -import type { CaseReportPayload } from "@/api/endpoints/incidentManagement"
55 +import type { CaseReportPayload } from "@/api/endpoints/incidentManagement/cases"
56 import type { DeepNullable } from "@/types/common"
57 import type { FormInst, FormRules, FormValidationError } from "naive-ui"
58 import type { Size } from "naive-ui/es/button/src/interface"
@@ -168,7 +168,7 @@ function exportCases() {
168 ? `${form.value.file_name}.${extension}`
169 : `case:${caseId}_report_${formatDate(new Date(), dFormats.datetimesec)}.${extension}`
170
171 - Api.incidentManagement
171 + Api.incidentManagement.cases
172 .generateCaseReport(
173 {
174 case_id: caseId,
frontend/src/components/incidentManagement/cases/CaseReportTemplateManager.vue
+3 -3
@@ -205,7 +205,7 @@ function uploadCustomTemplate() {
205 function uploadDefaultTemplate() {
206 uploading.value = true
207
208 - Api.incidentManagement
208 + Api.incidentManagement.cases
209 .uploadDefaultCaseReportTemplate()
210 .then(res => {
211 if (res.data.success) {
@@ -226,7 +226,7 @@ function uploadDefaultTemplate() {
226 function checkDefaultCaseReportTemplateExists() {
227 checkingDefaultTemplate.value = true
228
229 - Api.incidentManagement
229 + Api.incidentManagement.cases
230 .checkDefaultCaseReportTemplateExists()
231 .then(res => {
232 if (res.data.success) {
@@ -246,7 +246,7 @@ function checkDefaultCaseReportTemplateExists() {
246 function downloadTemplate(templateName: string) {
247 downloading.value = templateName
248
249 - Api.incidentManagement
249 + Api.incidentManagement.cases
250 .downloadCaseReportTemplate(templateName)
251 .then(res => {
252 if (res.data) {
frontend/src/components/incidentManagement/cases/CaseStatusSwitch.vue
+1 -1
@@ -47,7 +47,7 @@ function updateStatus() {
47 if (statusSelected.value && statusSelected.value !== status.value) {
48 loading.value = true
49
50 - Api.incidentManagement
50 + Api.incidentManagement.cases
51 .updateCaseStatus(caseData.value.id, statusSelected.value)
52 .then(res => {
53 if (res.data.success && statusSelected.value) {
frontend/src/components/incidentManagement/cases/CasesExport.vue
+1 -1
@@ -81,7 +81,7 @@ function exportCases(key: string) {
81 ? `cases_${formatDate(new Date(), dFormats.datetimesec)}.csv`
82 : `cases_customer:${key}_${formatDate(new Date(), dFormats.datetimesec)}.csv`
83
84 - Api.incidentManagement
84 + Api.incidentManagement.cases
85 .exportCases(key === "--all--" ? undefined : key)
86 .then(res => {
87 if (res.data) {
frontend/src/components/incidentManagement/cases/CasesList.vue
+3 -3
@@ -179,7 +179,7 @@
179 </template>
180
181 <script setup lang="ts">
182 -import type { CasesFilter, CasesFilterTypes } from "@/api/endpoints/incidentManagement"
182 +import type { CasesFilter, CasesFilterTypes } from "@/api/endpoints/incidentManagement/cases"
183 import type { Customer } from "@/types/customers.d"
184 import type { Case, CaseStatus } from "@/types/incidentManagement/cases.d"
185 import Api from "@/api"
@@ -359,7 +359,7 @@ function getData() {
359 query = { [filters.value.type]: filters.value.value }
360 }
361
362 - Api.incidentManagement
362 + Api.incidentManagement.cases
363 .getCasesList(query)
364 .then(res => {
365 if (res.data.success) {
@@ -379,7 +379,7 @@ function getData() {
379 }
380
381 function getAvailableUsers() {
382 - Api.incidentManagement
382 + Api.incidentManagement.alerts
383 .getAvailableUsers()
384 .then(res => {
385 if (res.data.success) {
frontend/src/components/incidentManagement/cases/utils.ts
+1 -1
@@ -45,7 +45,7 @@ export function deleteAlert({ caseData, cbBefore, cbSuccess, cbAfter, cbError, m
45 cbBefore()
46 }
47
48 - Api.incidentManagement
48 + Api.incidentManagement.cases
49 .deleteCase(caseData.id)
50 .then(res => {
51 if (res.data.success) {
frontend/src/components/incidentManagement/exclusionRules/ExclusionRuleDetails.vue new
+132
@@ -0,0 +1,132 @@
1 +<template>
2 + <n-tabs type="line" animated :tabs-padding="24" class="grow" pane-wrapper-class="flex grow flex-col">
3 + <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy" class="flex grow flex-col">
4 + <n-spin :show="loading" class="flex grow flex-col" content-class="flex grow flex-col">
5 + <div class="flex flex-col gap-4 p-5 pt-3">
6 + <div class="grid grid-cols-6 gap-4">
7 + <CardKV class="col-span-6 md:col-span-2">
8 + <template #key>name</template>
9 + <template #value>{{ entity.name }}</template>
10 + </CardKV>
11 + <CardKV class="col-span-3 md:col-span-2">
12 + <template #key>creator</template>
13 + <template #value>
14 + <div class="flex flex-col gap-2 py-1">
15 + <div>
16 + <span class="text-secondary">by:</span>
17 + {{ entity.created_by }}
18 + </div>
19 + <div>
20 + <span class="text-secondary">at:</span>
21 + {{ formatDate(entity.created_at, dFormats.datetimesec) }}
22 + </div>
23 + </div>
24 + </template>
25 + </CardKV>
26 + <CardKV class="col-span-3 md:col-span-2">
27 + <template #key>status</template>
28 + <template #value>
29 + <div class="flex h-full w-full items-center justify-center font-sans">
30 + <ExclusionRuleStatusToggler
31 + :entity
32 + @loading="updatingStatus = $event"
33 + @updated="setStatus($event)"
34 + />
35 + </div>
36 + </template>
37 + </CardKV>
38 + </div>
39 +
40 + <div class="border-default bg-secondary flex flex-wrap items-center gap-3 rounded-lg border p-3">
41 + <Badge type="splitted">
42 + <template #label># ID</template>
43 + <template #value>{{ entity.id }}</template>
44 + </Badge>
45 +
46 + <Badge type="splitted" color="primary">
47 + <template #iconLeft>
48 + <Icon :name="TargetIcon" />
49 + </template>
50 + <template #label>Match count</template>
51 + <template #value>
52 + {{ entity.match_count }}
53 + </template>
54 + </Badge>
55 +
56 + <Badge v-if="entity.last_matched_at" type="splitted" color="primary">
57 + <template #iconLeft>
58 + <Icon :name="TimeIcon" />
59 + </template>
60 + <template #label>Last match</template>
61 + <template #value>
62 + {{ formatDate(entity.last_matched_at, dFormats.datetimesec) }}
63 + </template>
64 + </Badge>
65 +
66 + <Badge v-if="entity.customer_code" type="splitted">
67 + <template #label>Customer</template>
68 + <template #value>
69 + <code
70 + class="text-primary cursor-pointer leading-none"
71 + @click.stop="gotoCustomer({ code: entity.customer_code })"
72 + >
73 + #{{ entity.customer_code }}
74 + <Icon :name="LinkIcon" :size="14" class="relative top-0.5" />
75 + </code>
76 + </template>
77 + </Badge>
78 + </div>
79 +
80 + <CardKV v-for="(val, key) in properties" :key>
81 + <template #key>{{ key }}</template>
82 + <template #value>{{ val }}</template>
83 + </CardKV>
84 + </div>
85 + </n-spin>
86 + </n-tab-pane>
87 + <n-tab-pane name="Fields" tab="Fields" display-directive="show:lazy">
88 + <div class="p-7 pt-4">
89 + <CardKV v-for="(val, key) in entity.field_matches" :key size="lg">
90 + <template #key>{{ key }}</template>
91 + <template #value>
92 + <CodeSource :code="val" lang="shell" />
93 + </template>
94 + </CardKV>
95 + </div>
96 + </n-tab-pane>
97 + </n-tabs>
98 +</template>
99 +
100 +<script setup lang="ts">
101 +import type { ExclusionRule } from "@/types/incidentManagement/exclusionRules.d"
102 +import Badge from "@/components/common/Badge.vue"
103 +import CardKV from "@/components/common/cards/CardKV.vue"
104 +import CodeSource from "@/components/common/CodeSource.vue"
105 +import Icon from "@/components/common/Icon.vue"
106 +import { useGoto } from "@/composables/useGoto"
107 +import { useSettingsStore } from "@/stores/settings"
108 +import { formatDate } from "@/utils"
109 +import _pick from "lodash/pick"
110 +import { NSpin, NTabPane, NTabs } from "naive-ui"
111 +import { computed, ref, toRefs } from "vue"
112 +import ExclusionRuleStatusToggler from "./ExclusionRuleStatusToggler.vue"
113 +
114 +const props = defineProps<{
115 + entity: ExclusionRule
116 +}>()
117 +
118 +const { entity } = toRefs(props)
119 +
120 +const TimeIcon = "carbon:time"
121 +const LinkIcon = "carbon:launch"
122 +const TargetIcon = "zondicons:target"
123 +const dFormats = useSettingsStore().dateFormat
124 +const { gotoCustomer } = useGoto()
125 +const updatingStatus = ref(false)
126 +const loading = computed(() => updatingStatus.value)
127 +const properties = computed(() => _pick(entity.value, ["description", "channel", "title"]))
128 +
129 +function setStatus(value: ExclusionRule) {
130 + entity.value.enabled = value.enabled
131 +}
132 +</script>
frontend/src/components/incidentManagement/exclusionRules/ExclusionRuleForm.vue new
+372
@@ -0,0 +1,372 @@
1 +<template>
2 + <n-spin :show="loading" class="customer-form">
3 + <n-form ref="formRef" :label-width="80" :model :rules>
4 + <div class="flex flex-col gap-0">
5 + <n-form-item label="Name" path="name">
6 + <n-input v-model:value.trim="model.name" placeholder="Exclusion rule name" clearable />
7 + </n-form-item>
8 +
9 + <n-form-item label="Description" path="description">
10 + <n-input
11 + v-model:value.trim="model.description"
12 + placeholder="Exclusion rule description"
13 + clearable
14 + />
15 + </n-form-item>
16 +
17 + <n-form-item label="Channel" path="channel">
18 + <n-input v-model:value.trim="model.channel" placeholder="Exclusion rule channel" clearable />
19 + </n-form-item>
20 +
21 + <n-form-item label="Title" path="title">
22 + <n-input v-model:value.trim="model.title" placeholder="Exclusion rule title" clearable />
23 + </n-form-item>
24 +
25 + <div class="mb-6 flex flex-col gap-2">
26 + <n-form-item path="field_matches" required label="Field matches" :show-feedback="false">
27 + <div class="flex w-full flex-col gap-4">
28 + <div
29 + v-for="(field, index) of model.field_matches"
30 + :key="field.id"
31 + class="border-default relative flex w-full flex-col gap-2 rounded-xl border p-2"
32 + >
33 + <n-input v-model:value.trim="field.key" placeholder="Field name" clearable />
34 + <n-input
35 + v-model:value.trim="field.value"
36 + placeholder="Field match"
37 + clearable
38 + type="textarea"
39 + :autosize="{ minRows: 3 }"
40 + />
41 + <div class="absolute -right-2.5 -top-2.5">
42 + <n-button
43 + v-if="model.field_matches.length > 1"
44 + circle
45 + secondary
46 + size="tiny"
47 + type="error"
48 + @click="delField(index)"
49 + >
50 + <template #icon>
51 + <Icon :name="DelIcon" />
52 + </template>
53 + </n-button>
54 + </div>
55 + </div>
56 + </div>
57 + </n-form-item>
58 +
59 + <div class="flex justify-end">
60 + <n-button @click="addField()">
61 + <template #icon>
62 + <Icon :name="AddIcon" />
63 + </template>
64 + Add field
65 + </n-button>
66 + </div>
67 +
68 + <n-alert v-if="!areFieldsFilled" type="warning">
69 + <span class="text-sm">Please fill in all fields</span>
70 + </n-alert>
71 + <n-alert v-if="!areFieldsUniques && model.field_matches.length > 1" type="warning">
72 + <span class="text-sm">Attention, there are duplicate fields</span>
73 + </n-alert>
74 + </div>
75 +
76 + <n-form-item label="Customer" path="customer_code">
77 + <n-select
78 + v-model:value="model.customer_code"
79 + :options="customersOptions"
80 + placeholder="Select Customer..."
81 + to="body"
82 + filterable
83 + :loading="loadingCustomers || !customersOptions.length"
84 + />
85 + </n-form-item>
86 +
87 + <n-form-item path="enabled" label="Status">
88 + <n-checkbox v-model:checked="model.enabled" size="large">Enabled</n-checkbox>
89 + </n-form-item>
90 +
91 + <div class="flex justify-between gap-4">
92 + <div class="flex gap-4">
93 + <slot name="additionalActions"></slot>
94 + </div>
95 + <div class="flex gap-4">
96 + <n-button :disabled="loading" @click="reset()">Reset</n-button>
97 + <n-button type="primary" :disabled="!isValid" :loading="loading" @click="validate()">
98 + Submit
99 + </n-button>
100 + </div>
101 + </div>
102 + </div>
103 + </n-form>
104 + </n-spin>
105 +</template>
106 +
107 +<script setup lang="ts">
108 +import type { ExclusionRulePayload } from "@/api/endpoints/incidentManagement/exclusionRules"
109 +import type { Customer } from "@/types/customers.d"
110 +import type { ExclusionRule } from "@/types/incidentManagement/exclusionRules"
111 +import type { FormInst, FormItemRule, FormRules, FormValidationError } from "naive-ui"
112 +import Api from "@/api"
113 +import Icon from "@/components/common/Icon.vue"
114 +import _get from "lodash/get"
115 +import _trim from "lodash/trim"
116 +import { NAlert, NButton, NCheckbox, NForm, NFormItem, NInput, NSelect, NSpin, useMessage } from "naive-ui"
117 +import { computed, onBeforeMount, onMounted, ref, toRefs, watch } from "vue"
118 +
119 +interface FieldMatch {
120 + id: string
121 + key: string | null
122 + value: string | null
123 +}
124 +
125 +interface Model extends Omit<ExclusionRulePayload, "field_matches"> {
126 + field_matches: FieldMatch[]
127 +}
128 +
129 +const props = defineProps<{
130 + entity?: ExclusionRule
131 + resetOnSubmit?: boolean
132 +}>()
133 +
134 +const emit = defineEmits<{
135 + (e: "update:loading", value: boolean): void
136 + (e: "submitted", value: ExclusionRule): void
137 + (
138 + e: "mounted",
139 + value: {
140 + reset: () => void
141 + }
142 + ): void
143 +}>()
144 +
145 +const { entity, resetOnSubmit } = toRefs(props)
146 +
147 +const DelIcon = "carbon:close-filled"
148 +const AddIcon = "carbon:add"
149 +const loading = ref(false)
150 +const loadingCustomers = ref(false)
151 +const message = useMessage()
152 +const model = ref<Model>(getDefaultModel())
153 +const formRef = ref<FormInst | null>(null)
154 +const customersList = ref<Customer[]>([])
155 +
156 +const customersOptions = computed(() =>
157 + customersList.value.map(o => ({ label: `#${o.customer_code} - ${o.customer_name}`, value: o.customer_code }))
158 +)
159 +
160 +const areFieldsPresent = computed(() => {
161 + return !!model.value.field_matches.length
162 +})
163 +
164 +const areFieldsFilled = computed(() => {
165 + const fieldsFilled = model.value.field_matches.filter(o => (!!o.key && !o.value) || (!o.key && !!o.value))
166 +
167 + return fieldsFilled.length === 0
168 +})
169 +
170 +const areFieldsUniques = computed(() => {
171 + const fieldsFilled = model.value.field_matches.filter(o => !!o.key).map(o => o.key)
172 +
173 + const uniques: (string | null)[] = fieldsFilled.filter((value, index, self) => self.indexOf(value) === index)
174 +
175 + return uniques.length === fieldsFilled.length
176 +})
177 +
178 +const rules: FormRules = {
179 + name: {
180 + required: true,
181 + message: "Please input name",
182 + trigger: ["input", "blur"]
183 + },
184 + description: {
185 + required: true,
186 + message: "Please input description",
187 + trigger: ["input", "blur"]
188 + },
189 + channel: {
190 + required: true,
191 + message: "Please input channel",
192 + trigger: ["input", "blur"]
193 + },
194 + title: {
195 + required: true,
196 + message: "Please input title",
197 + trigger: ["input", "blur"]
198 + },
199 + field_matches: {
200 + required: false,
201 +
202 + validator(_rule: FormItemRule, _value: string) {
203 + if (!areFieldsPresent.value) {
204 + return new Error(`Please fill least one fields`)
205 + }
206 +
207 + if (!areFieldsFilled.value) {
208 + return new Error(`Please fill all fields`)
209 + }
210 +
211 + if (!areFieldsUniques.value) {
212 + return new Error(`There are duplicated fields`)
213 + }
214 +
215 + return true
216 + },
217 + trigger: ["input", "blur"]
218 + }
219 +}
220 +
221 +const isValid = computed(() => {
222 + let valid = true
223 +
224 + for (const key in rules) {
225 + const rule = rules[key] as FormRules
226 +
227 + if (rule.required && !_trim(_get(model.value, key))) {
228 + valid = false
229 + }
230 + }
231 +
232 + if (!areFieldsFilled.value || !areFieldsPresent.value || !areFieldsUniques.value) {
233 + valid = false
234 + }
235 +
236 + return valid
237 +})
238 +
239 +function validate() {
240 + if (!formRef.value) return
241 +
242 + formRef.value.validate((errors?: Array<FormValidationError>) => {
243 + if (!errors) {
244 + submit()
245 + } else {
246 + message.warning("You must fill in the required fields correctly.")
247 + return false
248 + }
249 + })
250 +}
251 +
252 +function getDefaultModel(entity?: Partial<ExclusionRule>): Model {
253 + return {
254 + name: entity?.name || "",
255 + description: entity?.description || "",
256 + channel: entity?.channel || "",
257 + title: entity?.title || "",
258 + field_matches: entity?.field_matches
259 + ? Object.entries(entity.field_matches).map(o => ({ key: o[0], value: o[1], id: o[0] }))
260 + : [{ id: `${new Date().getTime()}`, key: null, value: null }],
261 + customer_code: entity?.customer_code || undefined,
262 + enabled: entity?.enabled || false
263 + }
264 +}
265 +
266 +function reset(force?: boolean) {
267 + if (!loading.value || force) {
268 + setModel()
269 + formRef.value?.restoreValidation()
270 + }
271 +}
272 +
273 +function addField() {
274 + model.value.field_matches.push({
275 + id: `${new Date().getTime()}`,
276 + key: null,
277 + value: null
278 + })
279 +}
280 +
281 +function delField(index: number) {
282 + model.value.field_matches.splice(index, 1)
283 +
284 + if (!model.value.field_matches.length) {
285 + addField()
286 + }
287 +}
288 +
289 +function getCustomers() {
290 + loadingCustomers.value = true
291 +
292 + Api.customers
293 + .getCustomers()
294 + .then(res => {
295 + if (res.data.success) {
296 + customersList.value = res.data?.customers || []
297 + } else {
298 + message.warning(res.data?.message || "An error occurred. Please try again later.")
299 + }
300 + })
301 + .catch(err => {
302 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
303 + })
304 + .finally(() => {
305 + loadingCustomers.value = false
306 + })
307 +}
308 +
309 +function submit() {
310 + loading.value = true
311 +
312 + const payload: ExclusionRulePayload = {
313 + ...model.value,
314 + field_matches: model.value.field_matches
315 + .filter(o => !!o.key && !!o.value)
316 + .reduce((acc: Record<string, string>, cur: FieldMatch) => {
317 + acc[`${cur.key}`] = `${cur.value}`
318 + return acc
319 + }, {})
320 + }
321 +
322 + const method = entity.value?.id
323 + ? Api.incidentManagement.exclusionRules.updateExclusionRule(entity.value.id, payload)
324 + : Api.incidentManagement.exclusionRules.createExclusionRule(payload)
325 +
326 + method
327 + .then(res => {
328 + if (res.data.success) {
329 + emit("submitted", res.data.exclusion_response)
330 + if (resetOnSubmit.value) {
331 + reset(true)
332 + }
333 + } else {
334 + message.warning(res.data?.message || "An error occurred. Please try again later.")
335 + }
336 + })
337 + .catch(err => {
338 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
339 + })
340 + .finally(() => {
341 + loading.value = false
342 + })
343 +}
344 +
345 +function setModel() {
346 + model.value = getDefaultModel(entity.value)
347 +}
348 +
349 +watch(loading, val => {
350 + emit("update:loading", val)
351 +})
352 +
353 +watch(
354 + entity,
355 + val => {
356 + if (val) {
357 + setModel()
358 + }
359 + },
360 + { immediate: true }
361 +)
362 +
363 +onBeforeMount(() => {
364 + getCustomers()
365 +})
366 +
367 +onMounted(() => {
368 + emit("mounted", {
369 + reset
370 + })
371 +})
372 +</script>
frontend/src/components/incidentManagement/exclusionRules/ExclusionRuleItem.vue new
+223
@@ -0,0 +1,223 @@
1 +<template>
2 + <div>
3 + <CardEntity :loading :embedded hoverable>
4 + <template #headerMain>{{ entity.name }}</template>
5 + <template #headerExtra>
6 + <div class="hidden font-sans sm:block">
7 + <ExclusionRuleStatusToggler
8 + :entity
9 + @loading="updatingStatus = $event"
10 + @updated="setStatus($event)"
11 + />
12 + </div>
13 + </template>
14 + <template #default>
15 + {{ entity.title }}
16 + </template>
17 + <template #footerMain>
18 + <div class="hidden flex-wrap items-center gap-3 sm:flex">
19 + <Badge type="splitted" color="primary">
20 + <template #iconLeft>
21 + <Icon :name="TargetIcon" />
22 + </template>
23 + <template #label>Match count</template>
24 + <template #value>{{ entity.match_count }}</template>
25 + </Badge>
26 +
27 + <Badge v-if="entity.last_matched_at" type="splitted" color="primary">
28 + <template #iconLeft>
29 + <Icon :name="TimeIcon" />
30 + </template>
31 + <template #label>Last match</template>
32 + <template #value>
33 + {{ formatDate(entity.last_matched_at, dFormats.datetimesec) }}
34 + </template>
35 + </Badge>
36 +
37 + <Badge v-if="entity.customer_code" type="splitted">
38 + <template #label>Customer</template>
39 + <template #value>
40 + <code
41 + class="text-primary cursor-pointer leading-none"
42 + @click.stop="gotoCustomer({ code: entity.customer_code })"
43 + >
44 + #{{ entity.customer_code }}
45 + <Icon :name="LinkIcon" :size="14" class="relative top-0.5" />
46 + </code>
47 + </template>
48 + </Badge>
49 + </div>
50 + </template>
51 +
52 + <template #footerExtra>
53 + <div class="flex items-center gap-3">
54 + <div class="block sm:hidden">
55 + <ExclusionRuleStatusToggler
56 + :entity
57 + @loading="updatingStatus = $event"
58 + @updated="setStatus($event)"
59 + />
60 + </div>
61 +
62 + <n-button size="small" @click.stop="openDetails()">
63 + <template #icon>
64 + <Icon :name="DetailsIcon"></Icon>
65 + </template>
66 + Details
67 + </n-button>
68 + </div>
69 + </template>
70 + </CardEntity>
71 +
72 + <n-modal
73 + v-model:show="showDetails"
74 + :style="{ maxWidth: 'min(850px, 90vw)', minHeight: 'min(480px, 90vh)', overflow: 'hidden' }"
75 + display-directive="show"
76 + >
77 + <n-card
78 + content-class="flex flex-col !p-0"
79 + :title="`#${entity.id} • ${entity.name}`"
80 + closable
81 + :bordered="false"
82 + segmented
83 + role="modal"
84 + @close="closeDetails()"
85 + >
86 + <n-spin :show="loadingDelete">
87 + <ExclusionRuleForm v-if="editing" :entity class="p-6" @submitted="updateEntity($event)">
88 + <template #additionalActions>
89 + <n-button v-if="editing" @click="editing = false">Close</n-button>
90 + </template>
91 + </ExclusionRuleForm>
92 + <ExclusionRuleDetails v-else :entity />
93 + </n-spin>
94 +
95 + <template #footer>
96 + <div v-if="!editing" class="flex items-center justify-end gap-4">
97 + <n-button text type="error" ghost :loading="loadingDelete" @click="handleDelete">
98 + <template #icon>
99 + <Icon :name="DeleteIcon" :size="15"></Icon>
100 + </template>
101 + Delete
102 + </n-button>
103 + <n-button :disabled="loadingDelete" @click="editing = true">
104 + <template #icon>
105 + <Icon :name="EditIcon" :size="14"></Icon>
106 + </template>
107 + Edit
108 + </n-button>
109 + </div>
110 + </template>
111 + </n-card>
112 + </n-modal>
113 + </div>
114 +</template>
115 +
116 +<script setup lang="ts">
117 +import type { ExclusionRule } from "@/types/incidentManagement/exclusionRules.d"
118 +import Api from "@/api"
119 +import Badge from "@/components/common/Badge.vue"
120 +import CardEntity from "@/components/common/cards/CardEntity.vue"
121 +import Icon from "@/components/common/Icon.vue"
122 +import { useGoto } from "@/composables/useGoto"
123 +import { useSettingsStore } from "@/stores/settings"
124 +import { formatDate } from "@/utils"
125 +import { NButton, NCard, NModal, NSpin, useDialog, useMessage } from "naive-ui"
126 +import { computed, h, ref, toRefs } from "vue"
127 +import ExclusionRuleDetails from "./ExclusionRuleDetails.vue"
128 +import ExclusionRuleForm from "./ExclusionRuleForm.vue"
129 +import ExclusionRuleStatusToggler from "./ExclusionRuleStatusToggler.vue"
130 +
131 +const props = defineProps<{
132 + entity: ExclusionRule
133 + embedded?: boolean
134 +}>()
135 +
136 +const emit = defineEmits<{
137 + (e: "deleted"): void
138 + (e: "updated"): void
139 +}>()
140 +
141 +const { entity, embedded } = toRefs(props)
142 +
143 +const TimeIcon = "carbon:time"
144 +const LinkIcon = "carbon:launch"
145 +const DetailsIcon = "carbon:settings-adjust"
146 +const DeleteIcon = "ph:trash"
147 +const EditIcon = "uil:edit-alt"
148 +const TargetIcon = "zondicons:target"
149 +
150 +const message = useMessage()
151 +const dialog = useDialog()
152 +const updatingStatus = ref(false)
153 +const loadingDelete = ref(false)
154 +const loading = computed(() => updatingStatus.value || loadingDelete.value)
155 +const editing = ref(false)
156 +const showDetails = ref(false)
157 +const { gotoCustomer } = useGoto()
158 +const dFormats = useSettingsStore().dateFormat
159 +
160 +function openDetails() {
161 + showDetails.value = true
162 +}
163 +
164 +function closeDetails() {
165 + showDetails.value = false
166 +}
167 +
168 +function setStatus(value: ExclusionRule) {
169 + entity.value.enabled = value.enabled
170 +}
171 +
172 +function updateEntity(value: ExclusionRule) {
173 + entity.value.name = value.name
174 + entity.value.description = value.description
175 + entity.value.channel = value.channel
176 + entity.value.title = value.title
177 + entity.value.field_matches = value.field_matches
178 + entity.value.enabled = value.enabled
179 + entity.value.customer_code = value.customer_code
180 +
181 + editing.value = false
182 + emit("updated")
183 +}
184 +
185 +function deleteExclusionRules() {
186 + loadingDelete.value = true
187 +
188 + Api.incidentManagement.exclusionRules
189 + .deleteExclusionRules(entity.value.id)
190 + .then(res => {
191 + if (res.data.success) {
192 + showDetails.value = false
193 + emit("deleted")
194 + } else {
195 + message.warning(res.data?.message || "An error occurred. Please try again later.")
196 + }
197 + })
198 + .catch(err => {
199 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
200 + })
201 + .finally(() => {
202 + loadingDelete.value = false
203 + })
204 +}
205 +
206 +function handleDelete() {
207 + dialog.warning({
208 + title: "Confirm",
209 + content: () =>
210 + h("div", {
211 + innerHTML: `Are you sure you want to delete the Exclusion Rule: <strong>${entity.value.name}</strong> ?`
212 + }),
213 + positiveText: "Yes I'm sure",
214 + negativeText: "Cancel",
215 + onPositiveClick: () => {
216 + deleteExclusionRules()
217 + },
218 + onNegativeClick: () => {
219 + message.info("Delete canceled")
220 + }
221 + })
222 +}
223 +</script>
frontend/src/components/incidentManagement/exclusionRules/ExclusionRuleStatusToggler.vue new
+76
@@ -0,0 +1,76 @@
1 +<template>
2 + <n-switch
3 + v-model:value="entity.enabled"
4 + :rail-style
5 + :loading="updatingStatus"
6 + @update:value="toggleExclusionRuleStatus()"
7 + >
8 + <template #checked>Enabled</template>
9 + <template #unchecked>Disabled</template>
10 + </n-switch>
11 +</template>
12 +
13 +<script setup lang="ts">
14 +import type { ExclusionRule } from "@/types/incidentManagement/exclusionRules.d"
15 +import type { CSSProperties } from "vue"
16 +import Api from "@/api"
17 +import { useThemeStore } from "@/stores/theme"
18 +import { NSwitch, useMessage } from "naive-ui"
19 +import { computed, ref, toRefs, watch } from "vue"
20 +
21 +const props = defineProps<{
22 + entity: ExclusionRule
23 +}>()
24 +
25 +const emit = defineEmits<{
26 + (e: "loading", value: boolean): void
27 + (e: "updated", value: ExclusionRule): void
28 +}>()
29 +
30 +const { entity } = toRefs(props)
31 +
32 +const message = useMessage()
33 +const themeStore = useThemeStore()
34 +const checkedColor = computed(() => themeStore.style["success-color-rgb"])
35 +const uncheckedColor = computed(() => themeStore.style["border-color-rgb"])
36 +const updatingStatus = ref(false)
37 +
38 +function railStyle({ focused, checked }: { focused: boolean; checked: boolean }) {
39 + const style: CSSProperties = {}
40 + if (checked) {
41 + style.background = `rgb(${checkedColor.value} / 40%)`
42 + if (focused) {
43 + style.boxShadow = `0 0 0 2px rgb(${checkedColor.value} / 30%)`
44 + }
45 + } else {
46 + style.background = `rgb(${uncheckedColor.value})`
47 + if (focused) {
48 + style.boxShadow = `0 0 0 2px rgb(${uncheckedColor.value} / 10%)`
49 + }
50 + }
51 + return style
52 +}
53 +
54 +function toggleExclusionRuleStatus() {
55 + updatingStatus.value = true
56 +
57 + Api.incidentManagement.exclusionRules
58 + .toggleExclusionRuleStatus(entity.value.id)
59 + .then(res => {
60 + if (res.data.success) {
61 + entity.value.enabled = res.data.exclusion_response.enabled
62 + emit("updated", res.data.exclusion_response)
63 + }
64 + })
65 + .catch(err => {
66 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
67 + })
68 + .finally(() => {
69 + updatingStatus.value = false
70 + })
71 +}
72 +
73 +watch(updatingStatus, val => {
74 + emit("loading", val)
75 +})
76 +</script>
frontend/src/components/incidentManagement/exclusionRules/ExclusionRulesList.vue new
+193
@@ -0,0 +1,193 @@
1 +<template>
2 + <div class="sigma-queries-list">
3 + <div ref="header" class="header @container flex items-center justify-end gap-2">
4 + <div class="info flex grow gap-2">
5 + <n-popover v-if="showInfoPopover" overlap placement="bottom-start">
6 + <template #trigger>
7 + <div class="bg-default rounded-lg">
8 + <n-button size="small" class="!cursor-help">
9 + <template #icon>
10 + <Icon :name="InfoIcon"></Icon>
11 + </template>
12 + </n-button>
13 + </div>
14 + </template>
15 + <div class="flex flex-col gap-2">
16 + <div class="box">
17 + Total :
18 + <code>{{ total }}</code>
19 + </div>
20 + </div>
21 + </n-popover>
22 +
23 + <NewExclusionRuleButton
24 + v-if="showCreationButton"
25 + :hide-button-extended-label="simpleMode"
26 + @success="getData()"
27 + />
28 + </div>
29 +
30 + <div class="@md:block hidden">
31 + <n-checkbox v-model:checked="filters.enabledOnly">Enabled only</n-checkbox>
32 + </div>
33 + <div class="@md:hidden block">
34 + <n-popover overlap placement="right" class="!px-0">
35 + <template #trigger>
36 + <div class="bg-default rounded-lg">
37 + <n-badge :show="filters.enabledOnly" dot type="success" :offset="[-3, 4]">
38 + <n-button size="small">
39 + <template #icon>
40 + <Icon :name="FilterIcon"></Icon>
41 + </template>
42 + </n-button>
43 + </n-badge>
44 + </div>
45 + </template>
46 + <div class="py-1">
47 + <div class="px-4">
48 + <n-checkbox v-model:checked="filters.enabledOnly">Enabled only</n-checkbox>
49 + </div>
50 + </div>
51 + </n-popover>
52 + </div>
53 +
54 + <n-pagination
55 + v-model:page="currentPage"
56 + v-model:page-size="pageSize"
57 + :page-slot="pageSlot"
58 + :show-size-picker="showSizePicker"
59 + :page-sizes="pageSizes"
60 + :item-count="total"
61 + :simple="simpleMode"
62 + />
63 + </div>
64 +
65 + <n-spin :show="loading">
66 + <div class="my-3 flex min-h-52 flex-col gap-2">
67 + <template v-if="list.length">
68 + <ExclusionRuleItem
69 + v-for="item of list"
70 + :key="item.id"
71 + :entity="item"
72 + class="item-appear item-appear-bottom item-appear-005"
73 + @deleted="getData()"
74 + @updated="getData()"
75 + />
76 + </template>
77 + <template v-else>
78 + <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" />
79 + </template>
80 + </div>
81 + </n-spin>
82 +
83 + <div class="footer flex justify-end">
84 + <n-pagination
85 + v-if="list.length > 3"
86 + v-model:page="currentPage"
87 + :page-size="pageSize"
88 + :item-count="total"
89 + :page-slot="6"
90 + />
91 + </div>
92 + </div>
93 +</template>
94 +
95 +<script setup lang="ts">
96 +import type { ExclusionRulesQuery } from "@/api/endpoints/incidentManagement/exclusionRules"
97 +import type { ExclusionRule } from "@/types/incidentManagement/exclusionRules.d"
98 +import Api from "@/api"
99 +import Icon from "@/components/common/Icon.vue"
100 +import { useResizeObserver } from "@vueuse/core"
101 +import { NBadge, NButton, NCheckbox, NEmpty, NPagination, NPopover, NSpin, useMessage } from "naive-ui"
102 +import { onBeforeMount, ref, watch } from "vue"
103 +import ExclusionRuleItem from "./ExclusionRuleItem.vue"
104 +import NewExclusionRuleButton from "./NewExclusionRuleButton.vue"
105 +
106 +const { showCreationButton = true, showInfoPopover = true } = defineProps<{
107 + showCreationButton?: boolean
108 + showInfoPopover?: boolean
109 +}>()
110 +
111 +const emit = defineEmits<{
112 + (
113 + e: "mounted",
114 + value: {
115 + reload: () => void
116 + }
117 + ): void
118 + (e: "loaded", value: number): void
119 +}>()
120 +
121 +const FilterIcon = "carbon:filter-edit"
122 +const InfoIcon = "carbon:information"
123 +const message = useMessage()
124 +const filters = ref({ enabledOnly: false })
125 +const loading = ref(false)
126 +const list = ref<ExclusionRule[]>([])
127 +const total = ref(0)
128 +
129 +const currentPage = ref(1)
130 +const pageSizes = [10, 25, 50, 100]
131 +const pageSize = ref(pageSizes[1])
132 +const header = ref()
133 +const pageSlot = ref(8)
134 +const simpleMode = ref(false)
135 +const showSizePicker = ref(true)
136 +
137 +function getData() {
138 + loading.value = true
139 +
140 + const query: Partial<ExclusionRulesQuery> = {
141 + pagination: {
142 + limit: pageSize.value,
143 + skip: (currentPage.value - 1) * pageSize.value
144 + }
145 + }
146 +
147 + if (filters.value.enabledOnly) {
148 + query.filters = { enabledOnly: filters.value.enabledOnly }
149 + }
150 +
151 + Api.incidentManagement.exclusionRules
152 + .getExclusionRulesList(query)
153 + .then(res => {
154 + if (res.data.success) {
155 + list.value = res.data.exclusions || []
156 + total.value = res.data.pagination.total || 0
157 + } else {
158 + message.warning(res.data?.message || "An error occurred. Please try again later.")
159 + }
160 + })
161 + .catch(err => {
162 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
163 + })
164 + .finally(() => {
165 + loading.value = false
166 + emit("loaded", total.value)
167 + })
168 +}
169 +
170 +useResizeObserver(header, entries => {
171 + const entry = entries[0]
172 + const { width } = entry.contentRect
173 +
174 + pageSlot.value = width < 700 ? 5 : 8
175 + simpleMode.value = width < 550
176 +})
177 +
178 +watch(pageSize, () => {
179 + currentPage.value = 1
180 +})
181 +
182 +watch([currentPage, pageSize, () => filters.value.enabledOnly], () => {
183 + getData()
184 +})
185 +
186 +onBeforeMount(() => {
187 + getData()
188 +
189 + emit("mounted", {
190 + reload: getData
191 + })
192 +})
193 +</script>
frontend/src/components/incidentManagement/exclusionRules/NewExclusionRuleButton.vue new
+53
@@ -0,0 +1,53 @@
1 +<template>
2 + <div>
3 + <n-button size="small" type="primary" @click="showForm = true">
4 + <template #icon>
5 + <Icon :name="NewNewExclusionRuleIcon" :size="16"></Icon>
6 + </template>
7 + {{ hideButtonExtendedLabel ? "Create" : " Create Exclusion Rule" }}
8 + </n-button>
9 +
10 + <n-modal
11 + v-model:show="showForm"
12 + display-directive="show"
13 + preset="card"
14 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(200px, 90vh)', overflow: 'hidden' }"
15 + title="Create Exclusion Rule"
16 + :bordered="false"
17 + content-class="flex flex-col"
18 + segmented
19 + >
20 + <ExclusionRuleForm reset-on-submit @submitted="submitted()" @mounted="formCTX = $event" />
21 + </n-modal>
22 + </div>
23 +</template>
24 +
25 +<script setup lang="ts">
26 +import Icon from "@/components/common/Icon.vue"
27 +import { NButton, NModal } from "naive-ui"
28 +import { ref, toRefs, watch } from "vue"
29 +import ExclusionRuleForm from "./ExclusionRuleForm.vue"
30 +
31 +const props = defineProps<{ hideButtonExtendedLabel?: boolean }>()
32 +
33 +const emit = defineEmits<{
34 + (e: "success"): void
35 +}>()
36 +
37 +const { hideButtonExtendedLabel } = toRefs(props)
38 +
39 +const NewNewExclusionRuleIcon = "ic:outline-do-not-disturb-on"
40 +const showForm = ref(false)
41 +const formCTX = ref<{ reset: () => void } | null>(null)
42 +
43 +function submitted() {
44 + emit("success")
45 + showForm.value = false
46 +}
47 +
48 +watch(showForm, val => {
49 + if (val) {
50 + formCTX.value?.reset()
51 + }
52 +})
53 +</script>
frontend/src/components/incidentManagement/sources/ConfiguredSourceItem.vue
+1 -1
@@ -55,7 +55,7 @@ const showConfirm = ref(false)
55 function deleteSourceConfiguration() {
56 canceling.value = true
57
58 - Api.incidentManagement
58 + Api.incidentManagement.sources
59 .deleteSourceConfiguration(source)
60 .then(res => {
61 if (res.data.success) {
frontend/src/components/incidentManagement/sources/ConfiguredSourcesList.vue
+26 -33
@@ -1,6 +1,6 @@
1 <template>
2 <div class="configured-sources-list">
3 - <div class="header mb-3 flex items-center justify-end gap-2">
3 + <div v-if="showToolbar" class="mb-3 flex items-center justify-end gap-2">
4 <div class="info flex grow gap-5">
5 <n-popover overlap placement="bottom-start">
6 <template #trigger>
@@ -21,12 +21,10 @@
21 </n-popover>
22 </div>
23 <div class="actions flex items-center gap-2">
24 - <n-button size="small" type="primary" @click="showWizard = true">
25 - <template #icon>
26 - <Icon :name="NewSourceConfigurationIcon" :size="15"></Icon>
27 - </template>
28 - Create Source Configuration
29 - </n-button>
24 + <NewConfiguredSourceButton
25 + :disabled-sources="configuredSourcesList"
26 + @success="getConfiguredSources()"
27 + />
28 </div>
29 </div>
30 <n-spin :show="loading" class="min-h-32">
@@ -43,19 +41,6 @@
41 <n-empty v-if="!loading" description="No items found" class="h-48 justify-center" />
42 </template>
43 </n-spin>
46 -
47 - <n-modal
48 - v-model:show="showWizard"
49 - display-directive="show"
50 - preset="card"
51 - :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(200px, 90vh)', overflow: 'hidden' }"
52 - title="Create Source Configuration"
53 - :bordered="false"
54 - content-class="flex flex-col !p-0"
55 - segmented
56 - >
57 - <SourceConfigurationWizard :disabled-sources="configuredSourcesList" @submitted="getConfiguredSources()" />
58 - </n-modal>
44 </div>
45 </template>
46
@@ -63,24 +48,33 @@
48 import type { SourceName } from "@/types/incidentManagement/sources.d"
49 import Api from "@/api"
50 import Icon from "@/components/common/Icon.vue"
66 -import { NButton, NEmpty, NModal, NPopover, NSpin, useMessage } from "naive-ui"
67 -import { computed, onBeforeMount, ref, watch } from "vue"
51 +import { NButton, NEmpty, NPopover, NSpin, useMessage } from "naive-ui"
52 +import { computed, onBeforeMount, ref } from "vue"
53 import ConfiguredSourceItem from "./ConfiguredSourceItem.vue"
69 -import SourceConfigurationWizard from "./SourceConfigurationWizard.vue"
54 +import NewConfiguredSourceButton from "./NewConfiguredSourceButton.vue"
55 +
56 +const { showToolbar = true } = defineProps<{ showToolbar?: boolean }>()
57 +
58 +const emit = defineEmits<{
59 + (
60 + e: "mounted",
61 + value: {
62 + reload: () => void
63 + }
64 + ): void
65 + (e: "loaded", value: number): void
66 +}>()
67
68 const InfoIcon = "carbon:information"
72 -const NewSourceConfigurationIcon = "carbon:fetch-upload-cloud"
69 const message = useMessage()
74 -const showWizard = ref(false)
70 const loading = ref(false)
71 const configuredSourcesList = ref<SourceName[]>([])
72 const totalConfiguredSources = computed(() => configuredSourcesList.value.length)
78 -const formCTX = ref<{ reset: () => void } | null>(null)
73
74 function getConfiguredSources() {
75 loading.value = true
76
83 - Api.incidentManagement
77 + Api.incidentManagement.sources
78 .getConfiguredSources()
79 .then(res => {
80 if (res.data.success) {
@@ -94,16 +88,15 @@ function getConfiguredSources() {
88 })
89 .finally(() => {
90 loading.value = false
91 + emit("loaded", configuredSourcesList.value.length)
92 })
93 }
94
100 -watch(showWizard, val => {
101 - if (val) {
102 - formCTX.value?.reset()
103 - }
104 -})
105 -
95 onBeforeMount(() => {
96 getConfiguredSources()
97 +
98 + emit("mounted", {
99 + reload: getConfiguredSources
100 + })
101 })
102 </script>
frontend/src/components/incidentManagement/sources/NewConfiguredSourceButton.vue new
+88
@@ -0,0 +1,88 @@
1 +<template>
2 + <div>
3 + <n-button size="small" type="primary" @click="showWizard = true">
4 + <template #icon>
5 + <Icon :name="NewSourceConfigurationIcon" :size="15"></Icon>
6 + </template>
7 + Create Source Configuration
8 + </n-button>
9 +
10 + <n-modal
11 + v-model:show="showWizard"
12 + display-directive="show"
13 + preset="card"
14 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(200px, 90vh)', overflow: 'hidden' }"
15 + title="Create Source Configuration"
16 + :bordered="false"
17 + content-class="flex flex-col !p-0"
18 + segmented
19 + >
20 + <SourceConfigurationWizard
21 + :disabled-sources="configuredSourcesList"
22 + @submitted="submitted()"
23 + @mounted="formCTX = $event"
24 + />
25 + </n-modal>
26 + </div>
27 +</template>
28 +
29 +<script setup lang="ts">
30 +import type { SourceName } from "@/types/incidentManagement/sources.d"
31 +import Api from "@/api"
32 +import Icon from "@/components/common/Icon.vue"
33 +import { NButton, NModal, useMessage } from "naive-ui"
34 +import { onBeforeMount, ref, watch } from "vue"
35 +import SourceConfigurationWizard from "./SourceConfigurationWizard.vue"
36 +
37 +const { disabledSources } = defineProps<{ disabledSources?: SourceName[] }>()
38 +
39 +const emit = defineEmits<{
40 + (e: "success"): void
41 +}>()
42 +
43 +const NewSourceConfigurationIcon = "carbon:fetch-upload-cloud"
44 +const message = useMessage()
45 +const showWizard = ref(false)
46 +const loading = ref(false)
47 +const configuredSourcesList = ref<SourceName[]>([])
48 +const formCTX = ref<{ reset: () => void } | null>(null)
49 +
50 +function getConfiguredSources() {
51 + loading.value = true
52 +
53 + Api.incidentManagement.sources
54 + .getConfiguredSources()
55 + .then(res => {
56 + if (res.data.success) {
57 + configuredSourcesList.value = res.data?.sources || []
58 + } else {
59 + message.warning(res.data?.message || "An error occurred. Please try again later.")
60 + }
61 + })
62 + .catch(err => {
63 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
64 + })
65 + .finally(() => {
66 + loading.value = false
67 + })
68 +}
69 +
70 +function submitted() {
71 + getConfiguredSources()
72 + emit("success")
73 +}
74 +
75 +watch(showWizard, val => {
76 + if (val) {
77 + formCTX.value?.reset()
78 + }
79 +})
80 +
81 +onBeforeMount(() => {
82 + if (disabledSources?.length) {
83 + configuredSourcesList.value = disabledSources
84 + } else {
85 + getConfiguredSources()
86 + }
87 +})
88 +</script>
frontend/src/components/incidentManagement/sources/SourceConfigurationDetails.vue
+2 -2
@@ -55,7 +55,7 @@ const formCTX = ref<{ reset: () => void; toggleSubmittingFlag: () => boolean } |
55 function getSourceConfiguration() {
56 loading.value = true
57
58 - Api.incidentManagement
58 + Api.incidentManagement.sources
59 .getSourceConfiguration(source)
60 .then(res => {
61 if (res.data.success) {
@@ -90,7 +90,7 @@ function setViewMode() {
90 function updateSourceConfiguration(payload: SourceConfiguration) {
91 submitting.value = formCTX.value?.toggleSubmittingFlag() || true
92
93 - Api.incidentManagement
93 + Api.incidentManagement.sources
94 .updateSourceConfiguration(payload)
95 .then(res => {
96 if (res.data.success) {
frontend/src/components/incidentManagement/sources/SourceConfigurationForm.vue
+4 -4
@@ -408,7 +408,7 @@ function getSocfortressRecommendsWazuh() {
408
409 loadingSocfortressRecommendsWazuh.value = true
410
411 - Api.incidentManagement
411 + Api.incidentManagement.sources
412 .getSocfortressRecommendsWazuh()
413 .then(res => {
414 if (res.data.success) {
@@ -437,7 +437,7 @@ function getSocfortressRecommendsWazuh() {
437 function getAvailableMappings(indexName: string) {
438 loadingAvailableMappings.value = true
439
440 - Api.incidentManagement
440 + Api.incidentManagement.sources
441 .getAvailableMappings(indexName)
442 .then(res => {
443 if (res.data.success) {
@@ -461,7 +461,7 @@ function getAvailableMappings(indexName: string) {
461 function getAvailableIndices(source: SourceName) {
462 loadingIndexNames.value = true
463
464 - Api.incidentManagement
464 + Api.incidentManagement.sources
465 .getAvailableIndices(source)
466 .then(res => {
467 if (res.data.success) {
@@ -486,7 +486,7 @@ function getAvailableIndices(source: SourceName) {
486 function getSourceByIndex(indexName: string) {
487 loadingSource.value = true
488
489 - Api.incidentManagement
489 + Api.incidentManagement.sources
490 .getSourceByIndex(indexName)
491 .then(res => {
492 if (res.data.success) {
frontend/src/components/incidentManagement/sources/SourceConfigurationWizard.vue
+1 -1
@@ -207,7 +207,7 @@ function getIndices() {
207 function createSourceConfiguration(payload: SourceConfiguration) {
208 submitting.value = formCTX.value?.toggleSubmittingFlag() || true
209
210 - Api.incidentManagement
210 + Api.incidentManagement.sources
211 .createSourceConfiguration(payload)
212 .then(res => {
213 if (res.data.success) {
frontend/src/components/overview/IncidentAlerts.vue
+1 -1
@@ -41,7 +41,7 @@ const values = computed<ItemProps[]>(() => [
41 function getData() {
42 loading.value = true
43
44 - Api.incidentManagement
44 + Api.incidentManagement.alerts
45 .getAlertsList({
46 page: 0,
47 pageSize: 0
frontend/src/components/overview/IncidentCases.vue
+1 -1
@@ -57,7 +57,7 @@ const values = computed<ItemProps[]>(() => [
57 function getData() {
58 loading.value = true
59
60 - Api.incidentManagement
60 + Api.incidentManagement.cases
61 .getCasesList()
62 .then(res => {
63 if (res.data.success) {
frontend/src/components/reportCreation/Panels.vue
+1
@@ -314,6 +314,7 @@ function removeRow(row: Row) {
314 1
315 )
316 }
317 +
318 function removePanel(row: Row, panel: PanelData) {
319 row.panels.splice(
320 row.panels.findIndex(o => o.panelId === panel.panelId),
frontend/src/components/scheduler/Item.vue
+8 -9
@@ -2,16 +2,15 @@
2 <CardEntity>
3 <template #headerMain>{{ job.id }}</template>
4 <template #headerExtra>
5 - <div class="flex items-center gap-2">
6 - {{ formatDate(job.last_success, dFormats.datetimesec) }}
7 -
8 - <n-tooltip>
9 - <template #trigger>
5 + <n-tooltip placement="top-end">
6 + <template #trigger>
7 + <div class="flex items-center gap-2">
8 + {{ formatDate(job.last_success, dFormats.datetimesec) }}
9 <Icon :name="TimeIcon"></Icon>
11 - </template>
12 - Last success time
13 - </n-tooltip>
14 - </div>
10 + </div>
11 + </template>
12 + Last success time
13 + </n-tooltip>
14 </template>
15
16 <template #default>
frontend/src/components/sigma/QueriesList.vue
+3 -3
@@ -72,7 +72,7 @@
72 <n-select
73 v-model:value="filters.active"
74 :options="activeOptions"
75 - placeholder="Active Status"
75 + placeholder="Select a status"
76 clearable
77 class="!w-56"
78 />
@@ -154,11 +154,11 @@ const showFilters = ref(false)
154 const showActionsView = useStorage<boolean>("sigma-queries-list-actions-view-state", false, localStorage)
155 const queriesList = ref<SigmaQuery[]>([])
156
157 -const pageSize = ref(25)
157 +const pageSizes = [10, 25, 50, 100]
158 +const pageSize = ref(pageSizes[1])
159 const currentPage = ref(1)
160 const simpleMode = ref(false)
161 const showSizePicker = ref(true)
161 -const pageSizes = [10, 25, 50, 100]
162 const header = ref()
163 const pageSlot = ref(8)
164
frontend/src/stores/caseReportTemplate.ts
+3 -3
@@ -34,7 +34,7 @@ export const useCaseReportTemplateStore = defineStore("caseReportTemplate", {
34 return new Promise((resolve, reject) => {
35 this.setLoading(true)
36
37 - Api.incidentManagement
37 + Api.incidentManagement.cases
38 .getCaseReportTemplate()
39 .then(res => {
40 if (res.data.success) {
@@ -54,7 +54,7 @@ export const useCaseReportTemplateStore = defineStore("caseReportTemplate", {
54 },
55 uploadCustomTemplate(file: File): Promise<AxiosResponse<FlaskBaseResponse>> {
56 return new Promise((resolve, reject) => {
57 - Api.incidentManagement
57 + Api.incidentManagement.cases
58 .uploadCustomCaseReportTemplate(file)
59 .then(res => {
60 if (res.data.success) {
@@ -71,7 +71,7 @@ export const useCaseReportTemplateStore = defineStore("caseReportTemplate", {
71 },
72 deleteTemplate(templateName: string): Promise<AxiosResponse<FlaskBaseResponse>> {
73 return new Promise((resolve, reject) => {
74 - Api.incidentManagement
74 + Api.incidentManagement.cases
75 .deleteCaseReportTemplate(templateName)
76 .then(res => {
77 if (res.data.success) {
frontend/src/types/incidentManagement/exclusionRules.d.ts new
+14
@@ -0,0 +1,14 @@
1 +export interface ExclusionRule {
2 + name: string
3 + description: string
4 + channel: string
5 + title: string
6 + field_matches: { [key: string]: string }
7 + customer_code: null | string
8 + enabled: boolean
9 + id: number
10 + created_by: string
11 + created_at: Date
12 + last_matched_at: Date | null
13 + match_count: number
14 +}
frontend/src/types/incidentManagement/notifications.d.ts
-6
@@ -1,9 +1,3 @@
1 -export interface IncidentNotificationPayload {
2 - customer_code: string
3 - shuffle_workflow_id: string
4 - enabled: boolean
5 -}
6 -
1 export interface IncidentNotification {
2 customer_code: string
3 enabled: boolean
frontend/src/views/incidentManagement/Sources.vue
+75 -2
@@ -1,9 +1,82 @@
1 <template>
2 - <div class="page">
3 - <ConfiguredSourcesList />
2 + <div ref="page" class="page">
3 + <n-tabs type="line" animated :tabs-padding="24">
4 + <n-tab-pane
5 + name="ConfiguredSources"
6 + :tab="
7 + showConfiguredSourcesListToolbar
8 + ? 'Configured Sources'
9 + : `Configured Sources (${configuredSourcesListTotal})`
10 + "
11 + display-directive="show"
12 + >
13 + <ConfiguredSourcesList
14 + :show-toolbar="showConfiguredSourcesListToolbar"
15 + @mounted="configuredSourcesListCTX = $event"
16 + @loaded="configuredSourcesListTotal = $event"
17 + />
18 + </n-tab-pane>
19 + <n-tab-pane name="ExclusionRules" tab="Exclusion Rules" display-directive="show">
20 + <ExclusionRulesList
21 + :show-creation-button="showExclusionRulesListCreationButton"
22 + :show-info-popover="showExclusionRulesListInfoPopover"
23 + @mounted="exclusionRulesListCTX = $event"
24 + />
25 + </n-tab-pane>
26 +
27 + <template #suffix>
28 + <div class="flex items-center gap-2">
29 + <NewConfiguredSourceButton
30 + v-if="!showConfiguredSourcesListToolbar"
31 + @success="reloadConfiguredSourcesList()"
32 + />
33 + <NewExclusionRuleButton
34 + v-if="!showExclusionRulesListCreationButton"
35 + @success="reloadExclusionRulesList()"
36 + />
37 + </div>
38 + </template>
39 + </n-tabs>
40 </div>
41 </template>
42
43 <script setup lang="ts">
44 +import ExclusionRulesList from "@/components/incidentManagement/exclusionRules/ExclusionRulesList.vue"
45 +import NewExclusionRuleButton from "@/components/incidentManagement/exclusionRules/NewExclusionRuleButton.vue"
46 import ConfiguredSourcesList from "@/components/incidentManagement/sources/ConfiguredSourcesList.vue"
47 +import NewConfiguredSourceButton from "@/components/incidentManagement/sources/NewConfiguredSourceButton.vue"
48 +
49 +import { useResizeObserver } from "@vueuse/core"
50 +import { NTabPane, NTabs } from "naive-ui"
51 +import { ref } from "vue"
52 +
53 +const configuredSourcesListTotal = ref(0)
54 +const configuredSourcesListCTX = ref<{ reload: () => void } | null>(null)
55 +const exclusionRulesListCTX = ref<{ reload: () => void } | null>(null)
56 +const page = ref()
57 +
58 +const showConfiguredSourcesListToolbar = ref(false)
59 +const showExclusionRulesListCreationButton = ref(false)
60 +const showExclusionRulesListInfoPopover = ref(false)
61 +
62 +function reloadConfiguredSourcesList() {
63 + if (configuredSourcesListCTX.value) {
64 + configuredSourcesListCTX.value.reload()
65 + }
66 +}
67 +
68 +function reloadExclusionRulesList() {
69 + if (exclusionRulesListCTX.value) {
70 + exclusionRulesListCTX.value.reload()
71 + }
72 +}
73 +
74 +useResizeObserver(page, entries => {
75 + const entry = entries[0]
76 + const { width } = entry.contentRect
77 +
78 + showConfiguredSourcesListToolbar.value = width < 600
79 + showExclusionRulesListCreationButton.value = width < 800
80 + showExclusionRulesListInfoPopover.value = width > 600
81 +})
82 </script>