@cryptotaxi247 / CoPilot / commits / eae77458

Ioc source (#337)

* feat: Add IoCFieldName model and corresponding database table * feat: Introduce IoC field names handling in database operations * fix: Update get_ioc_names to return all distinct IoC field names instead of the first one * feat: Implement IOC handling in alert creation process and add IOC type determination * feat: Update IOC payload handling to support optional IOC data and enhance alert creation process * feat: Add IoC existence check and functionality to add IoC to CoPilot alerts * feat: Include alert_id in IoC payload creation for CoPilot alerts * chore: update dependencies in frontend * feat: improved Breadcrumb component * feat: add ioc-fields in incident source forms * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Nov 19, 2024 at 11:26 UTC eae7745896720f8275f518604930e61f100c9c20
16 files changed +566 -183
backend/alembic/env.py
+1
@@ -38,6 +38,7 @@ from app.incidents.models import Comment
38 from app.incidents.models import CustomerCodeFieldName
39 from app.incidents.models import FieldName
40 from app.incidents.models import IoC
41 +from app.incidents.models import IoCFieldName
42 from app.incidents.models import Notification
43 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
44 AlertCreationSettings,
backend/alembic/versions/21a945c2982b_add_ioc_table_for_sources.py new
+37
@@ -0,0 +1,37 @@
1 +"""Add ioc table for sources
2 +
3 +Revision ID: 21a945c2982b
4 +Revises: 33e4754d845b
5 +Create Date: 2024-11-18 13:39:23.302575
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 = "21a945c2982b"
17 +down_revision: Union[str, None] = "33e4754d845b"
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_iocfieldname",
26 + sa.Column("id", sa.Integer(), nullable=False),
27 + sa.Column("source", sa.String(length=50), nullable=False),
28 + sa.Column("field_name", sa.String(length=100), nullable=False),
29 + sa.PrimaryKeyConstraint("id"),
30 + )
31 + # ### end Alembic commands ###
32 +
33 +
34 +def downgrade() -> None:
35 + # ### commands auto generated by Alembic - please adjust! ###
36 + op.drop_table("incident_management_iocfieldname")
37 + # ### end Alembic commands ###
backend/app/incidents/models.py
+7
@@ -131,6 +131,13 @@ class AlertTitleFieldName(SQLModel, table=True):
131 field_name: str = Field(max_length=100, nullable=False)
132
133
134 +class IoCFieldName(SQLModel, table=True):
135 + __tablename__ = "incident_management_iocfieldname"
136 + id: Optional[int] = Field(default=None, primary_key=True)
137 + source: str = Field(max_length=50, nullable=False)
138 + field_name: str = Field(max_length=100, nullable=False)
139 +
140 +
141 class CustomerCodeFieldName(SQLModel, table=True):
142 __tablename__ = "incident_management_customercodefieldname"
143 id: Optional[int] = Field(default=None, primary_key=True)
backend/app/incidents/routes/db_operations.py
+23
@@ -69,6 +69,7 @@ from app.incidents.schema.db_operations import PutNotification
69 from app.incidents.schema.db_operations import SocfortressRecommendsWazuhAlertTitleName
70 from app.incidents.schema.db_operations import SocfortressRecommendsWazuhAssetName
71 from app.incidents.schema.db_operations import SocfortressRecommendsWazuhFieldNames
72 +from app.incidents.schema.db_operations import SocfortressRecommendsWazuhIoCFieldNames
73 from app.incidents.schema.db_operations import SocfortressRecommendsWazuhResponse
74 from app.incidents.schema.db_operations import SocfortressRecommendsWazuhTimeFieldName
75 from app.incidents.schema.db_operations import UpdateAlertStatus
@@ -76,6 +77,7 @@ from app.incidents.schema.db_operations import UpdateCaseStatus
77 from app.incidents.services.db_operations import add_alert_title_name
78 from app.incidents.services.db_operations import add_asset_name
79 from app.incidents.services.db_operations import add_field_name
80 +from app.incidents.services.db_operations import add_ioc_name
81 from app.incidents.services.db_operations import add_timefield_name
82 from app.incidents.services.db_operations import alert_total
83 from app.incidents.services.db_operations import alert_total_by_alert_title
@@ -130,6 +132,7 @@ from app.incidents.services.db_operations import delete_asset_name
132 from app.incidents.services.db_operations import delete_case
133 from app.incidents.services.db_operations import delete_field_name
134 from app.incidents.services.db_operations import delete_file_from_case
135 +from app.incidents.services.db_operations import delete_ioc_name
136 from app.incidents.services.db_operations import delete_report_template
137 from app.incidents.services.db_operations import delete_timefield_name
138 from app.incidents.services.db_operations import download_file_from_case
@@ -142,6 +145,7 @@ from app.incidents.services.db_operations import get_asset_names
145 from app.incidents.services.db_operations import get_case_by_id
146 from app.incidents.services.db_operations import get_customer_notification
147 from app.incidents.services.db_operations import get_field_names
148 +from app.incidents.services.db_operations import get_ioc_names
149 from app.incidents.services.db_operations import get_timefield_names
150 from app.incidents.services.db_operations import is_alert_linked_to_case
151 from app.incidents.services.db_operations import list_alert_by_assigned_to
@@ -165,6 +169,7 @@ from app.incidents.services.db_operations import put_customer_notification
169 from app.incidents.services.db_operations import replace_alert_title_name
170 from app.incidents.services.db_operations import replace_asset_name
171 from app.incidents.services.db_operations import replace_field_name
172 +from app.incidents.services.db_operations import replace_ioc_name
173 from app.incidents.services.db_operations import replace_timefield_name
174 from app.incidents.services.db_operations import report_template_exists
175 from app.incidents.services.db_operations import update_alert_assigned_to
@@ -228,6 +233,7 @@ async def get_socfortress_recommends_wazuh(session: AsyncSession = Depends(get_d
233 asset_name=SocfortressRecommendsWazuhAssetName.agent_name.value,
234 timefield_name=SocfortressRecommendsWazuhTimeFieldName.timestamp_utc.value,
235 alert_title_name=SocfortressRecommendsWazuhAlertTitleName.rule_description.value,
236 + ioc_field_names=[ioc.value for ioc in SocfortressRecommendsWazuhIoCFieldNames],
237 source="wazuh",
238 success=True,
239 message="Field names and asset names retrieved successfully",
@@ -248,6 +254,7 @@ async def delete_configured_source(source: str, session: AsyncSession = Depends(
254 asset_name = await get_asset_names(source, session)
255 timefield_name = await get_timefield_names(source, session)
256 alert_title_name = await get_alert_title_names(source, session)
257 + ioc_names = await get_ioc_names(source, session)
258
259 logger.info(
260 f"Field names found: {field_names}, Asset name found: {asset_name}, Timefield name found: {timefield_name}, Alert title name found: {alert_title_name}",
@@ -262,6 +269,10 @@ async def delete_configured_source(source: str, session: AsyncSession = Depends(
269
270 await delete_alert_title_name(source, alert_title_name, session)
271
272 + if ioc_names:
273 + for ioc_name in ioc_names:
274 + await delete_ioc_name(ioc_value=ioc_name, source=source, session=session)
275 +
276 logger.info(f"Field names and asset names deleted successfully for source {source}. Committing changes to the database")
277
278 await session.commit()
@@ -283,6 +294,7 @@ async def get_source_fields_and_assets(source: str, session: AsyncSession = Depe
294 asset_name=await get_asset_names(source, session),
295 timefield_name=await get_timefield_names(source, session),
296 alert_title_name=await get_alert_title_names(source, session),
297 + ioc_field_names=await get_ioc_names(source, session),
298 source=source,
299 success=True,
300 message="Field names and asset names retrieved successfully",
@@ -300,6 +312,10 @@ async def create_wazuh_fields_and_assets(names: FieldAndAssetNames, session: Asy
312
313 await add_alert_title_name(names.source, names.alert_title_name, session)
314
315 + if names.ioc_field_names:
316 + for ioc_name in names.ioc_field_names:
317 + await add_ioc_name(names.source, ioc_name, session)
318 +
319 logger.info(f"Field names and asset names created successfully for source {names.source}")
320
321 await session.commit()
@@ -317,6 +333,8 @@ async def update_fields_and_assets(names: FieldAndAssetNames, session: AsyncSess
333
334 await replace_alert_title_name(names.source, names.alert_title_name, session)
335
336 + await replace_ioc_name(names.source, names.ioc_field_names, session)
337 +
338 return {"message": "Field names and asset names created successfully", "success": True}
339
340
@@ -331,6 +349,11 @@ async def delete_wazuh_fields_and_assets(names: FieldAndAssetNames, session: Asy
349
350 await delete_alert_title_name(names.source, names.alert_title_name, session)
351
352 + if names.ioc_field_names:
353 + logger.info(f"Deleting IoC field names: {names.ioc_field_names}")
354 + for ioc_name in names.ioc_field_names:
355 + await delete_ioc_name(ioc_value=ioc_name, source=names.source, session=session)
356 + logger.info(f"Field names and asset names deleted successfully for source {names.source}. Committing changes to the database")
357 await session.commit()
358
359 return {"message": "Field names and asset names deleted successfully", "success": True}
backend/app/incidents/schema/db_operations.py
+7
@@ -54,11 +54,16 @@ class SocfortressRecommendsWazuhAlertTitleName(Enum):
54 rule_description = "rule_description"
55
56
57 +class SocfortressRecommendsWazuhIoCFieldNames(Enum):
58 + threat_intel_value = "threat_intel_value"
59 +
60 +
61 class SocfortressRecommendsWazuhResponse(BaseModel):
62 field_names: List[str]
63 asset_name: str
64 timefield_name: str
65 alert_title_name: str
66 + ioc_field_names: Optional[List[str]] = None
67 source: str
68 success: bool
69 message: str
@@ -190,6 +195,7 @@ class FieldAndAssetNames(BaseModel):
195 asset_name: str
196 timefield_name: str
197 alert_title_name: str
198 + ioc_field_names: Optional[List[str]] = None
199 source: str
200
201
@@ -198,6 +204,7 @@ class FieldAndAssetNamesResponse(BaseModel):
204 asset_name: str
205 timefield_name: str
206 alert_title_name: str
207 + ioc_field_names: Optional[List[str]] = None
208 source: str
209 success: bool
210 message: str
backend/app/incidents/schema/incident_alert.py
+2
@@ -51,6 +51,7 @@ class FieldNames(BaseModel):
51 asset_name: str
52 timefield_name: str
53 alert_title_name: str
54 + ioc_field_names: Optional[List[str]] = None
55
56
57 class GenericSourceModel(BaseModel):
@@ -128,6 +129,7 @@ class CreatedAlertPayload(BaseModel):
129 asset_payload: str
130 timefield_payload: str
131 alert_title_payload: str
132 + ioc_payload: Optional[dict] = None
133 source: str
134 index_name: Optional[str] = None
135 index_id: Optional[str] = None
backend/app/incidents/services/db_operations.py
+59 -1
@@ -42,6 +42,7 @@ from app.incidents.models import Comment
42 from app.incidents.models import CustomerCodeFieldName
43 from app.incidents.models import FieldName
44 from app.incidents.models import IoC
45 +from app.incidents.models import IoCFieldName
46 from app.incidents.models import Notification
47 from app.incidents.models import TimestampFieldName
48 from app.incidents.schema.db_operations import AlertContextCreate
@@ -492,6 +493,11 @@ async def get_timefield_names(source: str, session: AsyncSession):
493 return result.scalars().first()
494
495
496 +async def get_ioc_names(source: str, session: AsyncSession):
497 + result = await session.execute(select(IoCFieldName.field_name).where(IoCFieldName.source == source).distinct())
498 + return result.scalars().all()
499 +
500 +
501 async def get_alert_title_names(source: str, session: AsyncSession):
502 result = await session.execute(select(AlertTitleFieldName.field_name).where(AlertTitleFieldName.source == source).distinct())
503 return result.scalars().first()
@@ -561,6 +567,16 @@ async def add_alert_title_name(source: str, alert_title_name: str, session: Asyn
567 session.add(alert_title)
568
569
570 +async def add_ioc_name(source: str, ioc_name: str, session: AsyncSession):
571 + result = await session.execute(
572 + select(IoCFieldName).where((IoCFieldName.source == source) & (IoCFieldName.field_name == ioc_name)),
573 + )
574 + existing_ioc = result.scalars().first()
575 + if existing_ioc is None:
576 + ioc = IoCFieldName(source=source, field_name=ioc_name)
577 + session.add(ioc)
578 +
579 +
580 # ! NOT USING FOR NOW. GETTING THE CUSTOMER CODE FROM THE ALERTS SOURCE FIELD INSTEAD ! #
581 async def add_customer_code_name(source: str, customer_code_name: str, session: AsyncSession):
582 result = await session.execute(
@@ -591,6 +607,23 @@ async def replace_field_name(source: str, field_names: List[str], session: Async
607 await session.commit()
608
609
610 +async def replace_ioc_name(source: str, ioc_names: List[str], session: AsyncSession):
611 + # First delete all the ioc names for this source, then add the new ioc names
612 + result = await session.execute(select(IoCFieldName).where(IoCFieldName.source == source))
613 + iocs = result.scalars().all()
614 +
615 + # Delete all the ioc names for this source
616 + for ioc in iocs:
617 + await session.delete(ioc)
618 +
619 + # Add the new ioc names
620 + for ioc_name in ioc_names:
621 + await add_ioc_name(source, ioc_name, session)
622 +
623 + # Commit the changes
624 + await session.commit()
625 +
626 +
627 async def replace_asset_name(source: str, asset_name: str, session: AsyncSession):
628 # Load the current asset for this source from the DB, then delete it and replace it with `asset_name`
629 result = await session.execute(select(AssetFieldName).where(AssetFieldName.source == source))
@@ -652,6 +685,16 @@ async def delete_field_name(source: str, field_name: str, session: AsyncSession)
685 await session.delete(field)
686
687
688 +async def delete_ioc_name(source: str, ioc_name: str, session: AsyncSession):
689 + logger.info(f"Deleting ioc name {ioc_name} for source {source}")
690 + ioc = await session.execute(
691 + select(IoCFieldName).where((IoCFieldName.source == source) & (IoCFieldName.field_name == ioc_name)),
692 + )
693 + ioc = ioc.scalar_one_or_none()
694 + if ioc:
695 + await session.delete(ioc)
696 +
697 +
698 async def delete_asset_name(source: str, asset_name: str, session: AsyncSession):
699 logger.info(f"Deleting asset name {asset_name} for source {source}")
700 asset = await session.execute(
@@ -1820,6 +1863,15 @@ async def delete_tags(alert_id: int, db: AsyncSession):
1863 )
1864
1865
1866 +async def delete_iocs(alert_id: int, db: AsyncSession):
1867 + result = await db.execute(select(AlertToIoC).where(AlertToIoC.alert_id == alert_id))
1868 + alert_to_iocs = result.scalars().all()
1869 + for alert_to_ioc in alert_to_iocs:
1870 + await db.execute(
1871 + delete(AlertToIoC).where((AlertToIoC.alert_id == alert_to_ioc.alert_id) & (AlertToIoC.ioc_id == alert_to_ioc.ioc_id)),
1872 + )
1873 +
1874 +
1875 async def is_alert_linked_to_case(alert_id: int, db: AsyncSession) -> bool:
1876 result = await db.execute(select(CaseAlertLink).where(CaseAlertLink.alert_id == alert_id))
1877 linked_cases = result.scalars().all()
@@ -1844,7 +1896,12 @@ async def delete_alert(alert_id: int, db: AsyncSession):
1896 logger.info(f"Deleting alert {alert_id}")
1897 result = await db.execute(
1898 select(Alert)
1847 - .options(selectinload(Alert.comments), selectinload(Alert.assets).selectinload(Asset.alert_context), selectinload(Alert.tags))
1899 + .options(
1900 + selectinload(Alert.comments),
1901 + selectinload(Alert.assets).selectinload(Asset.alert_context),
1902 + selectinload(Alert.tags),
1903 + selectinload(Alert.iocs),
1904 + )
1905 .where(Alert.id == alert_id),
1906 )
1907 alert = result.scalars().first()
@@ -1854,6 +1911,7 @@ async def delete_alert(alert_id: int, db: AsyncSession):
1911 await delete_comments(alert_id, db)
1912 await delete_assets(alert_id, db)
1913 await delete_tags(alert_id, db)
1914 + await delete_iocs(alert_id, db)
1915
1916 await db.execute(delete(Alert).where(Alert.id == alert.id))
1917
backend/app/incidents/services/incident_alert.py
+178
@@ -1,4 +1,5 @@
1 import os
2 +import re
3 from datetime import datetime
4 from datetime import timedelta
5 from typing import Any
@@ -17,8 +18,12 @@ from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_cl
18 from app.db.universal_models import Agents
19 from app.incidents.models import Alert
20 from app.incidents.models import AlertContext
21 +from app.incidents.models import AlertToIoC
22 from app.incidents.models import Asset
23 +from app.incidents.models import IoC
24 from app.incidents.routes.db_operations import get_configured_sources
25 +from app.incidents.schema.db_operations import AlertIoCCreate
26 +from app.incidents.schema.db_operations import AlertIocValue
27 from app.incidents.schema.incident_alert import CreateAlertRequest
28 from app.incidents.schema.incident_alert import CreateAlertRequestRoute
29 from app.incidents.schema.incident_alert import CreateAlertResponse
@@ -30,6 +35,7 @@ from app.incidents.services.db_operations import get_alert_title_names
35 from app.incidents.services.db_operations import get_asset_names
36 from app.incidents.services.db_operations import get_customer_notification
37 from app.incidents.services.db_operations import get_field_names
38 +from app.incidents.services.db_operations import get_ioc_names
39 from app.incidents.services.db_operations import get_timefield_names
40 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
41 AlertCreationSettings,
@@ -327,6 +333,7 @@ async def get_all_field_names(syslog_type: str, session: AsyncSession) -> FieldN
333 asset_name=await get_asset_names(syslog_type, session),
334 timefield_name=await get_timefield_names(syslog_type, session),
335 alert_title_name=await get_alert_title_names(syslog_type, session),
336 + ioc_field_names=await get_ioc_names(syslog_type, session),
337 )
338
339
@@ -363,6 +370,60 @@ async def build_alert_context_payload(alert_payload: dict, field_names: Any) ->
370 return alert_context_payload
371
372
373 +def get_ioc_type(ioc_value: str) -> Optional[AlertIocValue]:
374 + """
375 + Determine the IOC type based on the value.
376 +
377 + Args:
378 + ioc_value (str): The IOC value.
379 +
380 + Returns:
381 + AlertIocValue: The IOC type (IP, DOMAIN, HASH, or URL), or None if the type cannot be determined.
382 + """
383 + # Regular expression patterns for IP, domain, and hash
384 + ip_pattern = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$")
385 + domain_pattern = re.compile(r"^(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$")
386 + hash_pattern = re.compile(r"^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{40}$|^[a-fA-F0-9]{64}$")
387 +
388 + if ip_pattern.match(ioc_value):
389 + return AlertIocValue.IP
390 + elif domain_pattern.match(ioc_value):
391 + return AlertIocValue.DOMAIN
392 + elif hash_pattern.match(ioc_value):
393 + return AlertIocValue.HASH
394 + else:
395 + return None
396 +
397 +
398 +async def build_ioc_payload(alert_payload: dict, field_names: Any) -> Optional[Dict[str, Any]]:
399 + """
400 + Build the alert context payload.
401 +
402 + Args:
403 + alert_payload (dict): The alert payload.
404 + field_names (Any): The field names.
405 +
406 + Returns:
407 + Optional[dict]: The alert context payload or None if no ioc_value is found.
408 + """
409 + logger.info(f"Building IOC payload for alert {alert_payload}")
410 + ioc_payload = {field: alert_payload[field] for field in field_names.ioc_field_names if field in alert_payload}
411 +
412 + # Determine the IOC value
413 + ioc_value = next(iter(ioc_payload.values()), None)
414 + if not ioc_value:
415 + logger.info("No IOC value found, returning None")
416 + return None
417 +
418 + # Determine the IOC type
419 + ioc_payload["ioc_value"] = ioc_value
420 + ioc_payload["ioc_type"] = get_ioc_type(ioc_value)
421 +
422 + ioc_payload["ioc_description"] = "IOC Auto-Generated From SOCFortress CoPilot"
423 + logger.info(f"IOC Payload: {ioc_payload}")
424 + return ioc_payload
425 +
426 +
427 async def build_alert_payload(
428 syslog_type: str,
429 index_name: str,
@@ -395,6 +456,7 @@ async def build_alert_payload(
456 asset_payload=alert_payload[field_names.asset_name] if field_names.asset_name in alert_payload else None,
457 timefield_payload=alert_payload[field_names.timefield_name] if field_names.timefield_name in alert_payload else None,
458 alert_title_payload=alert_payload[field_names.alert_title_name] if field_names.alert_title_name in alert_payload else None,
459 + ioc_payload=await build_ioc_payload(alert_payload, field_names),
460 source=syslog_type,
461 index_name=index_name,
462 index_id=index_id,
@@ -446,6 +508,22 @@ async def create_alert_full(alert_payload: CreatedAlertPayload, customer_code: s
508 session=session,
509 )
510 ).id
511 + if alert_payload.ioc_payload is not None:
512 + ioc_id = (
513 + await create_ioc_payload(
514 + ioc_payload=AlertIoCCreate(
515 + alert_id=alert_id,
516 + ioc_value=alert_payload.ioc_payload["ioc_value"],
517 + ioc_type=alert_payload.ioc_payload["ioc_type"],
518 + ioc_description=alert_payload.ioc_payload["ioc_description"],
519 + ),
520 + alert_id=alert_id,
521 + session=session,
522 + )
523 + ).id
524 + logger.info(
525 + 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}",
526 + )
527 logger.info(f"Creating alert for customer code {customer_code} with alert context ID {alert_context_id} and asset ID {asset_id}")
528 await handle_customer_notifications(customer_code, alert_payload, session)
529
@@ -514,6 +592,73 @@ async def add_asset_to_copilot_alert(alert_payload: CreatedAlertPayload, alert_i
592 return asset_context
593
594
595 +async def does_ioc_exist(alert_payload: CreatedAlertPayload, alert_id: int, session: AsyncSession) -> bool:
596 + """
597 + Check if the IoC exists for the given alert payload.
598 +
599 + Args:
600 + alert_payload (dict): The alert payload.
601 + alert_id (int): The alert ID.
602 + session (AsyncSession): The database session.
603 +
604 + Returns:
605 + bool: True if the IoC exists, None otherwise.
606 + """
607 + logger.info(f"Checking if an IoC exists for alert ID {alert_id} with IoC value {alert_payload.ioc_payload['ioc_value']}")
608 + result = await session.execute(
609 + select(IoC)
610 + .join(AlertToIoC, AlertToIoC.ioc_id == IoC.id)
611 + .where(AlertToIoC.alert_id == alert_id, IoC.value == alert_payload.ioc_payload["ioc_value"]),
612 + )
613 + ioc = result.scalars().first()
614 + if ioc:
615 + logger.info(f"IoC exists for alert ID {alert_id} with IoC value {alert_payload.ioc_payload['ioc_value']}")
616 + return True
617 + logger.info(f"No IoC exists for alert ID {alert_id} with IoC value {alert_payload.ioc_payload['ioc_value']}")
618 + return False
619 +
620 +
621 +async def add_ioc_to_copilot_alert(alert_payload: CreatedAlertPayload, alert_id: int, customer_code: str, session: AsyncSession) -> None:
622 + """
623 + Add the IoC to the alert in CoPilot.
624 +
625 + Args:
626 + alert_payload (dict): The alert payload.
627 + alert_id (int): The alert ID.
628 + customer_code (str): The customer code.
629 + session (AsyncSession): The database session.
630 + """
631 + if await does_ioc_exist(alert_payload, alert_id, session):
632 + return None
633 +
634 + ioc_payload = AlertIoCCreate(
635 + alert_id=alert_id,
636 + ioc_value=alert_payload.ioc_payload["ioc_value"],
637 + ioc_type=alert_payload.ioc_payload["ioc_type"],
638 + ioc_description=alert_payload.ioc_payload["ioc_description"],
639 + )
640 +
641 + ioc_context = IoC(
642 + value=ioc_payload.ioc_value,
643 + type=ioc_payload.ioc_type,
644 + description=ioc_payload.ioc_description,
645 + )
646 + # Add the IoC context to the session
647 + session.add(ioc_context)
648 + await session.commit()
649 + await session.refresh(ioc_context)
650 +
651 + # Create the AlertToIoC relationship
652 + alert_to_ioc = AlertToIoC(
653 + alert_id=alert_id,
654 + ioc_id=ioc_context.id,
655 + )
656 + # Add the AlertToIoC relationship to the session
657 + session.add(alert_to_ioc)
658 + await session.commit()
659 + return ioc_context
660 +
661 +
662 async def create_alert_in_copilot(alert_payload: CreatedAlertPayload, customer_code: str, session: AsyncSession) -> Alert:
663 """
664 Create an alert in CoPilot.
@@ -591,6 +736,38 @@ async def create_asset_context_payload(
736 return asset_context
737
738
739 +async def create_ioc_payload(
740 + ioc_payload: AlertIoCCreate,
741 + alert_id: int,
742 + session: AsyncSession,
743 +) -> IoC:
744 + """
745 + Build the ioc context payload based on the valid field names and the ioc payload. Then
746 + create the ioc context in the database.
747 + """
748 + logger.info(f"Creating IoC context for alert ID {alert_id} with payload {ioc_payload}")
749 +
750 + ioc_context = IoC(
751 + value=ioc_payload.ioc_value,
752 + type=ioc_payload.ioc_type,
753 + description=ioc_payload.ioc_description,
754 + )
755 + # Add the IoC context to the session
756 + session.add(ioc_context)
757 + await session.flush()
758 +
759 + # Create the AlertToIoC relationship
760 + alert_to_ioc = AlertToIoC(
761 + alert_id=alert_id,
762 + ioc_id=ioc_context.id,
763 + )
764 + # Add the AlertToIoC relationship to the session
765 + session.add(alert_to_ioc)
766 + await session.commit()
767 +
768 + return ioc_context
769 +
770 +
771 async def open_alert_exists(alert_payload: CreatedAlertPayload, customer_code: str, session: AsyncSession) -> bool:
772 """
773 Check if an open alert exists for the given alert payload.
@@ -663,6 +840,7 @@ async def create_alert(
840 existing_alert,
841 )
842 await add_asset_to_copilot_alert(alert_payload, existing_alert, customer_code, session)
843 + await add_ioc_to_copilot_alert(alert_payload, existing_alert, customer_code, session)
844 return existing_alert
845 return await create_alert_full(alert_payload, customer_code, session)
846
frontend/package-lock.json
+185 -157
@@ -13,7 +13,7 @@
13 "@fontsource/jetbrains-mono": "^5.1.1",
14 "@fontsource/lexend": "^5.1.1",
15 "@fontsource/public-sans": "^5.1.1",
16 - "@shikijs/markdown-it": "^1.23.0",
16 + "@shikijs/markdown-it": "^1.23.1",
17 "@tailwindcss/container-queries": "^0.1.1",
18 "@vueuse/core": "^11.2.0",
19 "axios": "^1.7.7",
@@ -34,7 +34,7 @@
34 "pinia": "^2.2.6",
35 "pinia-plugin-persistedstate": "^4.1.3",
36 "secure-ls": "^2.0.0",
37 - "shiki": "^1.23.0",
37 + "shiki": "^1.23.1",
38 "validator": "^13.12.0",
39 "vue": "^3.5.13",
40 "vue-advanced-cropper": "^2.8.9",
@@ -47,7 +47,7 @@
47 "vuedraggable": "^4.1.0"
48 },
49 "devDependencies": {
50 - "@antfu/eslint-config": "^3.9.1",
50 + "@antfu/eslint-config": "^3.9.2",
51 "@clack/prompts": "^0.8.1",
52 "@iconify/vue": "^4.1.2",
53 "@tsconfig/node20": "^20.1.4",
@@ -72,7 +72,7 @@
72 "npm-run-all2": "^7.0.1",
73 "postcss": "^8.4.49",
74 "prettier": "^3.3.3",
75 - "prettier-plugin-tailwindcss": "^0.6.8",
75 + "prettier-plugin-tailwindcss": "^0.6.9",
76 "sass": "^1.81.0",
77 "start-server-and-test": "^2.0.8",
78 "tailwind-config-viewer": "^2.0.4",
@@ -92,7 +92,7 @@
92 "node": ">=18.0.0"
93 },
94 "optionalDependencies": {
95 - "@rollup/rollup-linux-x64-gnu": "^4.27.2"
95 + "@rollup/rollup-linux-x64-gnu": "^4.27.3"
96 }
97 },
98 "node_modules/@ajoelp/json-to-formdata": {
@@ -127,18 +127,19 @@
127 }
128 },
129 "node_modules/@antfu/eslint-config": {
130 - "version": "3.9.1",
131 - "resolved": "https://registry.npmjs.org/@antfu/eslint-config/-/eslint-config-3.9.1.tgz",
132 - "integrity": "sha512-a/xubkbJ9i6U6jX5ZUB3GeXahhorpMWgDRwdga297ilmadcJFrepBRjGf8SnA+RlPrVRI4cqPdQeQZZKR+Mjiw==",
130 + "version": "3.9.2",
131 + "resolved": "https://registry.npmjs.org/@antfu/eslint-config/-/eslint-config-3.9.2.tgz",
132 + "integrity": "sha512-a1I1CXmtQdTL9jxcb2RzKjuYYAzjdKK3ktVpQGd/1S/aUdhKgcEEi3DRXYgnB8xdpYLqracETxEMDf9PQlmyBg==",
133 "dev": true,
134 + "license": "MIT",
135 "dependencies": {
136 "@antfu/install-pkg": "^0.4.1",
136 - "@clack/prompts": "^0.7.0",
137 + "@clack/prompts": "^0.8.1",
138 "@eslint-community/eslint-plugin-eslint-comments": "^4.4.1",
139 "@eslint/markdown": "^6.2.1",
140 "@stylistic/eslint-plugin": "^2.10.1",
140 - "@typescript-eslint/eslint-plugin": "^8.14.0",
141 - "@typescript-eslint/parser": "^8.14.0",
141 + "@typescript-eslint/eslint-plugin": "^8.15.0",
142 + "@typescript-eslint/parser": "^8.15.0",
143 "@vitest/eslint-plugin": "^1.1.10",
144 "eslint-config-flat-gitignore": "^0.3.0",
145 "eslint-flat-config-utils": "^0.4.0",
@@ -147,11 +148,11 @@
148 "eslint-plugin-command": "^0.2.6",
149 "eslint-plugin-import-x": "^4.4.2",
150 "eslint-plugin-jsdoc": "^50.5.0",
150 - "eslint-plugin-jsonc": "^2.18.1",
151 - "eslint-plugin-n": "^17.13.1",
151 + "eslint-plugin-jsonc": "^2.18.2",
152 + "eslint-plugin-n": "^17.13.2",
153 "eslint-plugin-no-only-tests": "^3.3.0",
154 "eslint-plugin-perfectionist": "^3.9.1",
154 - "eslint-plugin-regexp": "^2.6.0",
155 + "eslint-plugin-regexp": "^2.7.0",
156 "eslint-plugin-toml": "^0.11.1",
157 "eslint-plugin-unicorn": "^56.0.0",
158 "eslint-plugin-unused-imports": "^4.1.4",
@@ -232,33 +233,6 @@
233 }
234 }
235 },
235 - "node_modules/@antfu/eslint-config/node_modules/@clack/prompts": {
236 - "version": "0.7.0",
237 - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.7.0.tgz",
238 - "integrity": "sha512-0MhX9/B4iL6Re04jPrttDm+BsP8y6mS7byuv0BvXgdXhbV5PdlsHt55dvNsuBCPZ7xq1oTAOOuotR9NFbQyMSA==",
239 - "bundleDependencies": [
240 - "is-unicode-supported"
241 - ],
242 - "dev": true,
243 - "dependencies": {
244 - "@clack/core": "^0.3.3",
245 - "is-unicode-supported": "*",
246 - "picocolors": "^1.0.0",
247 - "sisteransi": "^1.0.5"
248 - }
249 - },
250 - "node_modules/@antfu/eslint-config/node_modules/@clack/prompts/node_modules/is-unicode-supported": {
251 - "version": "1.3.0",
252 - "dev": true,
253 - "inBundle": true,
254 - "license": "MIT",
255 - "engines": {
256 - "node": ">=12"
257 - },
258 - "funding": {
259 - "url": "https://github.com/sponsors/sindresorhus"
260 - }
261 - },
236 "node_modules/@antfu/eslint-config/node_modules/globals": {
237 "version": "15.12.0",
238 "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz",
@@ -2408,12 +2382,13 @@
2382 ]
2383 },
2384 "node_modules/@rollup/rollup-linux-x64-gnu": {
2411 - "version": "4.27.2",
2412 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.2.tgz",
2413 - "integrity": "sha512-PaW2DY5Tan+IFvNJGHDmUrORadbe/Ceh8tQxi8cmdQVCCYsLoQo2cuaSj+AU+YRX8M4ivS2vJ9UGaxfuNN7gmg==",
2385 + "version": "4.27.3",
2386 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.3.tgz",
2387 + "integrity": "sha512-/6bn6pp1fsCGEY5n3yajmzZQAh+mW4QPItbiWxs69zskBzJuheb3tNynEjL+mKOsUSFK11X4LYF2BwwXnzWleA==",
2388 "cpu": [
2389 "x64"
2390 ],
2391 + "license": "MIT",
2392 "optional": true,
2393 "os": [
2394 "linux"
@@ -2472,50 +2447,55 @@
2447 ]
2448 },
2449 "node_modules/@shikijs/core": {
2475 - "version": "1.23.0",
2476 - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.23.0.tgz",
2477 - "integrity": "sha512-J4Fo22oBlfRHAXec+1AEzcowv+Qdf4ZQkuP/X/UHYH9+KA9LvyFXSXyS+HxuBRFfon+u7bsmKdRBjoZlbDVRkQ==",
2450 + "version": "1.23.1",
2451 + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.23.1.tgz",
2452 + "integrity": "sha512-NuOVgwcHgVC6jBVH5V7iblziw6iQbWWHrj5IlZI3Fqu2yx9awH7OIQkXIcsHsUmY19ckwSgUMgrqExEyP5A0TA==",
2453 + "license": "MIT",
2454 "dependencies": {
2479 - "@shikijs/engine-javascript": "1.23.0",
2480 - "@shikijs/engine-oniguruma": "1.23.0",
2481 - "@shikijs/types": "1.23.0",
2455 + "@shikijs/engine-javascript": "1.23.1",
2456 + "@shikijs/engine-oniguruma": "1.23.1",
2457 + "@shikijs/types": "1.23.1",
2458 "@shikijs/vscode-textmate": "^9.3.0",
2459 "@types/hast": "^3.0.4",
2460 "hast-util-to-html": "^9.0.3"
2461 }
2462 },
2463 "node_modules/@shikijs/engine-javascript": {
2488 - "version": "1.23.0",
2489 - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.23.0.tgz",
2490 - "integrity": "sha512-CcrppseWShG+8Efp1iil9divltuXVdCaU4iu+CKvzTGZO5RmXyAiSx668M7VbX8+s/vt1ZKu75Vn/jWi8O3G/Q==",
2464 + "version": "1.23.1",
2465 + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.23.1.tgz",
2466 + "integrity": "sha512-i/LdEwT5k3FVu07SiApRFwRcSJs5QM9+tod5vYCPig1Ywi8GR30zcujbxGQFJHwYD7A5BUqagi8o5KS+LEVgBg==",
2467 + "license": "MIT",
2468 "dependencies": {
2492 - "@shikijs/types": "1.23.0",
2469 + "@shikijs/types": "1.23.1",
2470 "@shikijs/vscode-textmate": "^9.3.0",
2494 - "oniguruma-to-es": "0.1.2"
2471 + "oniguruma-to-es": "0.4.1"
2472 }
2473 },
2474 "node_modules/@shikijs/engine-oniguruma": {
2498 - "version": "1.23.0",
2499 - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.23.0.tgz",
2500 - "integrity": "sha512-gS8bZLqVvmZXX+E5JUMJICsBp+kx6gj79MH/UEpKHKIqnUzppgbmEn6zLa6mB5D+sHse2gFei3YYJxQe1EzZXQ==",
2475 + "version": "1.23.1",
2476 + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.23.1.tgz",
2477 + "integrity": "sha512-KQ+lgeJJ5m2ISbUZudLR1qHeH3MnSs2mjFg7bnencgs5jDVPeJ2NVDJ3N5ZHbcTsOIh0qIueyAJnwg7lg7kwXQ==",
2478 + "license": "MIT",
2479 "dependencies": {
2502 - "@shikijs/types": "1.23.0",
2480 + "@shikijs/types": "1.23.1",
2481 "@shikijs/vscode-textmate": "^9.3.0"
2482 }
2483 },
2484 "node_modules/@shikijs/markdown-it": {
2507 - "version": "1.23.0",
2508 - "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.23.0.tgz",
2509 - "integrity": "sha512-PEAy+CVQqu5tgoi/hfSheaklSyjmWRiyr5aNMDu6lStJMgf+CN560WqabMAyuYNF5d2zSgpQmdvvPa2TQ0XXFw==",
2485 + "version": "1.23.1",
2486 + "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.23.1.tgz",
2487 + "integrity": "sha512-Odpj0AiQBe4v6D+XwAQkdErxncVnaBt+nZTc2JDrwWrOjvkM5JfRG55n9idTqGZfO0EMAZrhP7fmstNJ0yKmlg==",
2488 + "license": "MIT",
2489 "dependencies": {
2490 "markdown-it": "^14.1.0",
2512 - "shiki": "1.23.0"
2491 + "shiki": "1.23.1"
2492 }
2493 },
2494 "node_modules/@shikijs/types": {
2516 - "version": "1.23.0",
2517 - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.23.0.tgz",
2518 - "integrity": "sha512-HiwzsihRao+IbPk7FER/EQT/D0dEEK3n5LAtHDzL5iRT+JMblA7y9uitUnjEnHeLkKigNM+ZplrP7MuEyyc5kA==",
2495 + "version": "1.23.1",
2496 + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.23.1.tgz",
2497 + "integrity": "sha512-98A5hGyEhzzAgQh2dAeHKrWW4HfCMeoFER2z16p5eJ+vmPeF6lZ/elEne6/UCU551F/WqkopqRsr1l2Yu6+A0g==",
2498 + "license": "MIT",
2499 "dependencies": {
2500 "@shikijs/vscode-textmate": "^9.3.0",
2501 "@types/hast": "^3.0.4"
@@ -2524,7 +2504,8 @@
2504 "node_modules/@shikijs/vscode-textmate": {
2505 "version": "9.3.0",
2506 "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz",
2527 - "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA=="
2507 + "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==",
2508 + "license": "MIT"
2509 },
2510 "node_modules/@sideway/address": {
2511 "version": "4.1.5",
@@ -2640,6 +2621,7 @@
2621 "version": "3.0.4",
2622 "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
2623 "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
2624 + "license": "MIT",
2625 "dependencies": {
2626 "@types/unist": "*"
2627 }
@@ -2782,16 +2764,17 @@
2764 }
2765 },
2766 "node_modules/@typescript-eslint/eslint-plugin": {
2785 - "version": "8.14.0",
2786 - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.14.0.tgz",
2787 - "integrity": "sha512-tqp8H7UWFaZj0yNO6bycd5YjMwxa6wIHOLZvWPkidwbgLCsBMetQoGj7DPuAlWa2yGO3H48xmPwjhsSPPCGU5w==",
2767 + "version": "8.15.0",
2768 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.15.0.tgz",
2769 + "integrity": "sha512-+zkm9AR1Ds9uLWN3fkoeXgFppaQ+uEVtfOV62dDmsy9QCNqlRHWNEck4yarvRNrvRcHQLGfqBNui3cimoz8XAg==",
2770 "dev": true,
2771 + "license": "MIT",
2772 "dependencies": {
2773 "@eslint-community/regexpp": "^4.10.0",
2791 - "@typescript-eslint/scope-manager": "8.14.0",
2792 - "@typescript-eslint/type-utils": "8.14.0",
2793 - "@typescript-eslint/utils": "8.14.0",
2794 - "@typescript-eslint/visitor-keys": "8.14.0",
2774 + "@typescript-eslint/scope-manager": "8.15.0",
2775 + "@typescript-eslint/type-utils": "8.15.0",
2776 + "@typescript-eslint/utils": "8.15.0",
2777 + "@typescript-eslint/visitor-keys": "8.15.0",
2778 "graphemer": "^1.4.0",
2779 "ignore": "^5.3.1",
2780 "natural-compare": "^1.4.0",
@@ -2815,15 +2798,16 @@
2798 }
2799 },
2800 "node_modules/@typescript-eslint/parser": {
2818 - "version": "8.14.0",
2819 - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.14.0.tgz",
2820 - "integrity": "sha512-2p82Yn9juUJq0XynBXtFCyrBDb6/dJombnz6vbo6mgQEtWHfvHbQuEa9kAOVIt1c9YFwi7H6WxtPj1kg+80+RA==",
2801 + "version": "8.15.0",
2802 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.15.0.tgz",
2803 + "integrity": "sha512-7n59qFpghG4uazrF9qtGKBZXn7Oz4sOMm8dwNWDQY96Xlm2oX67eipqcblDj+oY1lLCbf1oltMZFpUso66Kl1A==",
2804 "dev": true,
2805 + "license": "BSD-2-Clause",
2806 "dependencies": {
2823 - "@typescript-eslint/scope-manager": "8.14.0",
2824 - "@typescript-eslint/types": "8.14.0",
2825 - "@typescript-eslint/typescript-estree": "8.14.0",
2826 - "@typescript-eslint/visitor-keys": "8.14.0",
2807 + "@typescript-eslint/scope-manager": "8.15.0",
2808 + "@typescript-eslint/types": "8.15.0",
2809 + "@typescript-eslint/typescript-estree": "8.15.0",
2810 + "@typescript-eslint/visitor-keys": "8.15.0",
2811 "debug": "^4.3.4"
2812 },
2813 "engines": {
@@ -2843,13 +2827,14 @@
2827 }
2828 },
2829 "node_modules/@typescript-eslint/scope-manager": {
2846 - "version": "8.14.0",
2847 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz",
2848 - "integrity": "sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==",
2830 + "version": "8.15.0",
2831 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.15.0.tgz",
2832 + "integrity": "sha512-QRGy8ADi4J7ii95xz4UoiymmmMd/zuy9azCaamnZ3FM8T5fZcex8UfJcjkiEZjJSztKfEBe3dZ5T/5RHAmw2mA==",
2833 "dev": true,
2834 + "license": "MIT",
2835 "dependencies": {
2851 - "@typescript-eslint/types": "8.14.0",
2852 - "@typescript-eslint/visitor-keys": "8.14.0"
2836 + "@typescript-eslint/types": "8.15.0",
2837 + "@typescript-eslint/visitor-keys": "8.15.0"
2838 },
2839 "engines": {
2840 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2860,13 +2845,14 @@
2845 }
2846 },
2847 "node_modules/@typescript-eslint/type-utils": {
2863 - "version": "8.14.0",
2864 - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.14.0.tgz",
2865 - "integrity": "sha512-Xcz9qOtZuGusVOH5Uk07NGs39wrKkf3AxlkK79RBK6aJC1l03CobXjJbwBPSidetAOV+5rEVuiT1VSBUOAsanQ==",
2848 + "version": "8.15.0",
2849 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.15.0.tgz",
2850 + "integrity": "sha512-UU6uwXDoI3JGSXmcdnP5d8Fffa2KayOhUUqr/AiBnG1Gl7+7ut/oyagVeSkh7bxQ0zSXV9ptRh/4N15nkCqnpw==",
2851 "dev": true,
2852 + "license": "MIT",
2853 "dependencies": {
2868 - "@typescript-eslint/typescript-estree": "8.14.0",
2869 - "@typescript-eslint/utils": "8.14.0",
2854 + "@typescript-eslint/typescript-estree": "8.15.0",
2855 + "@typescript-eslint/utils": "8.15.0",
2856 "debug": "^4.3.4",
2857 "ts-api-utils": "^1.3.0"
2858 },
@@ -2877,6 +2863,9 @@
2863 "type": "opencollective",
2864 "url": "https://opencollective.com/typescript-eslint"
2865 },
2866 + "peerDependencies": {
2867 + "eslint": "^8.57.0 || ^9.0.0"
2868 + },
2869 "peerDependenciesMeta": {
2870 "typescript": {
2871 "optional": true
@@ -2884,10 +2873,11 @@
2873 }
2874 },
2875 "node_modules/@typescript-eslint/types": {
2887 - "version": "8.14.0",
2888 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.14.0.tgz",
2889 - "integrity": "sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==",
2876 + "version": "8.15.0",
2877 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.15.0.tgz",
2878 + "integrity": "sha512-n3Gt8Y/KyJNe0S3yDCD2RVKrHBC4gTUcLTebVBXacPy091E6tNspFLKRXlk3hwT4G55nfr1n2AdFqi/XMxzmPQ==",
2879 "dev": true,
2880 + "license": "MIT",
2881 "engines": {
2882 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
2883 },
@@ -2897,13 +2887,14 @@
2887 }
2888 },
2889 "node_modules/@typescript-eslint/typescript-estree": {
2900 - "version": "8.14.0",
2901 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz",
2902 - "integrity": "sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==",
2890 + "version": "8.15.0",
2891 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.15.0.tgz",
2892 + "integrity": "sha512-1eMp2JgNec/niZsR7ioFBlsh/Fk0oJbhaqO0jRyQBMgkz7RrFfkqF9lYYmBoGBaSiLnu8TAPQTwoTUiSTUW9dg==",
2893 "dev": true,
2894 + "license": "BSD-2-Clause",
2895 "dependencies": {
2905 - "@typescript-eslint/types": "8.14.0",
2906 - "@typescript-eslint/visitor-keys": "8.14.0",
2896 + "@typescript-eslint/types": "8.15.0",
2897 + "@typescript-eslint/visitor-keys": "8.15.0",
2898 "debug": "^4.3.4",
2899 "fast-glob": "^3.3.2",
2900 "is-glob": "^4.0.3",
@@ -2925,15 +2916,16 @@
2916 }
2917 },
2918 "node_modules/@typescript-eslint/utils": {
2928 - "version": "8.14.0",
2929 - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.14.0.tgz",
2930 - "integrity": "sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==",
2919 + "version": "8.15.0",
2920 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.15.0.tgz",
2921 + "integrity": "sha512-k82RI9yGhr0QM3Dnq+egEpz9qB6Un+WLYhmoNcvl8ltMEededhh7otBVVIDDsEEttauwdY/hQoSsOv13lxrFzQ==",
2922 "dev": true,
2923 + "license": "MIT",
2924 "dependencies": {
2925 "@eslint-community/eslint-utils": "^4.4.0",
2934 - "@typescript-eslint/scope-manager": "8.14.0",
2935 - "@typescript-eslint/types": "8.14.0",
2936 - "@typescript-eslint/typescript-estree": "8.14.0"
2926 + "@typescript-eslint/scope-manager": "8.15.0",
2927 + "@typescript-eslint/types": "8.15.0",
2928 + "@typescript-eslint/typescript-estree": "8.15.0"
2929 },
2930 "engines": {
2931 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2944,16 +2936,22 @@
2936 },
2937 "peerDependencies": {
2938 "eslint": "^8.57.0 || ^9.0.0"
2939 + },
2940 + "peerDependenciesMeta": {
2941 + "typescript": {
2942 + "optional": true
2943 + }
2944 }
2945 },
2946 "node_modules/@typescript-eslint/visitor-keys": {
2950 - "version": "8.14.0",
2951 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz",
2952 - "integrity": "sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==",
2947 + "version": "8.15.0",
2948 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.15.0.tgz",
2949 + "integrity": "sha512-h8vYOulWec9LhpwfAdZf2bjr8xIp0KNKnpgqSz0qqYYKAW/QZKw3ktRndbiAtUz4acH4QLQavwZBYCc0wulA/Q==",
2950 "dev": true,
2951 + "license": "MIT",
2952 "dependencies": {
2955 - "@typescript-eslint/types": "8.14.0",
2956 - "eslint-visitor-keys": "^3.4.3"
2953 + "@typescript-eslint/types": "8.15.0",
2954 + "eslint-visitor-keys": "^4.2.0"
2955 },
2956 "engines": {
2957 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2963,18 +2961,6 @@
2961 "url": "https://opencollective.com/typescript-eslint"
2962 }
2963 },
2966 - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
2967 - "version": "3.4.3",
2968 - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
2969 - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
2970 - "dev": true,
2971 - "engines": {
2972 - "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
2973 - },
2974 - "funding": {
2975 - "url": "https://opencollective.com/eslint"
2976 - }
2977 - },
2964 "node_modules/@ungap/structured-clone": {
2965 "version": "1.2.0",
2966 "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
@@ -4694,6 +4680,7 @@
4680 "version": "2.1.0",
4681 "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
4682 "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
4683 + "license": "MIT",
4684 "funding": {
4685 "type": "github",
4686 "url": "https://github.com/sponsors/wooorm"
@@ -4703,6 +4690,7 @@
4690 "version": "3.0.0",
4691 "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
4692 "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
4693 + "license": "MIT",
4694 "funding": {
4695 "type": "github",
4696 "url": "https://github.com/sponsors/wooorm"
@@ -4952,6 +4940,7 @@
4940 "version": "2.0.3",
4941 "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
4942 "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
4943 + "license": "MIT",
4944 "funding": {
4945 "type": "github",
4946 "url": "https://github.com/sponsors/wooorm"
@@ -5886,7 +5875,8 @@
5875 "node_modules/emoji-regex-xs": {
5876 "version": "1.0.0",
5877 "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz",
5889 - "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg=="
5878 + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==",
5879 + "license": "MIT"
5880 },
5881 "node_modules/encodeurl": {
5882 "version": "1.0.2",
@@ -5911,6 +5901,7 @@
5901 "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
5902 "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
5903 "dev": true,
5904 + "license": "MIT",
5905 "dependencies": {
5906 "graceful-fs": "^4.2.4",
5907 "tapable": "^2.2.0"
@@ -6179,6 +6170,7 @@
6170 "resolved": "https://registry.npmjs.org/eslint-json-compat-utils/-/eslint-json-compat-utils-0.2.1.tgz",
6171 "integrity": "sha512-YzEodbDyW8DX8bImKhAcCeu/L31Dd/70Bidx2Qex9OFUtgzXLqtfWL4Hr5fM/aCCB8QUZLuJur0S9k6UfgFkfg==",
6172 "dev": true,
6173 + "license": "MIT",
6174 "dependencies": {
6175 "esquery": "^1.6.0"
6176 },
@@ -6246,6 +6238,7 @@
6238 "https://github.com/sponsors/ota-meshi",
6239 "https://opencollective.com/eslint"
6240 ],
6241 + "license": "MIT",
6242 "dependencies": {
6243 "@eslint-community/eslint-utils": "^4.1.2",
6244 "@eslint-community/regexpp": "^4.11.0",
@@ -6350,10 +6343,11 @@
6343 "dev": true
6344 },
6345 "node_modules/eslint-plugin-jsonc": {
6353 - "version": "2.18.1",
6354 - "resolved": "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.18.1.tgz",
6355 - "integrity": "sha512-6qY8zDpxOwPQNcr8eZ+RxwGX6IPHws5/Qef7aBEjER8rB9+UMB6zQWVIVcbP7xzFmEMHAesNFPe/sIlU4c78dg==",
6346 + "version": "2.18.2",
6347 + "resolved": "https://registry.npmjs.org/eslint-plugin-jsonc/-/eslint-plugin-jsonc-2.18.2.tgz",
6348 + "integrity": "sha512-SDhJiSsWt3nItl/UuIv+ti4g3m4gpGkmnUJS9UWR3TrpyNsIcnJoBRD7Kof6cM4Rk3L0wrmY5Tm3z7ZPjR2uGg==",
6349 "dev": true,
6350 + "license": "MIT",
6351 "dependencies": {
6352 "@eslint-community/eslint-utils": "^4.2.0",
6353 "eslint-compat-utils": "^0.6.0",
@@ -6375,10 +6369,11 @@
6369 }
6370 },
6371 "node_modules/eslint-plugin-jsonc/node_modules/eslint-compat-utils": {
6378 - "version": "0.6.0",
6379 - "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.6.0.tgz",
6380 - "integrity": "sha512-1vVBdI/HLS6HTHVQCJGlN+LOF0w1Rs/WB9se23mQr84cRM0iMM8PulMFFhQdQ1BvS0cGwjpis4xziI91Rk0l6g==",
6372 + "version": "0.6.3",
6373 + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.6.3.tgz",
6374 + "integrity": "sha512-9IDdksh5pUYP2ZLi7mOdROxVjLY8gY2qKxprmrJ/5Dyqud7M/IFKxF3o0VLlRhITm1pK6Fk7NiBxE39M/VlUcw==",
6375 "dev": true,
6376 + "license": "MIT",
6377 "dependencies": {
6378 "semver": "^7.5.4"
6379 },
@@ -6394,6 +6389,7 @@
6389 "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
6390 "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
6391 "dev": true,
6392 + "license": "Apache-2.0",
6393 "engines": {
6394 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
6395 },
@@ -6406,6 +6402,7 @@
6402 "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
6403 "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
6404 "dev": true,
6405 + "license": "BSD-2-Clause",
6406 "dependencies": {
6407 "acorn": "^8.9.0",
6408 "acorn-jsx": "^5.3.2",
@@ -6423,6 +6420,7 @@
6420 "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.6.2.tgz",
6421 "integrity": "sha512-Vhf+bUa//YSTYKseDiiEuQmhGCoIF3CVBhunm3r/DQnYiGT4JssmnKQc44BIyOZRK2pKjXXAgbhfmbeoC9CJpA==",
6422 "dev": true,
6423 + "license": "MIT",
6424 "dependencies": {
6425 "tslib": "^2.3.1"
6426 },
@@ -6434,13 +6432,15 @@
6432 "version": "2.8.1",
6433 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
6434 "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
6437 - "dev": true
6435 + "dev": true,
6436 + "license": "0BSD"
6437 },
6438 "node_modules/eslint-plugin-n": {
6440 - "version": "17.13.1",
6441 - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.13.1.tgz",
6442 - "integrity": "sha512-97qzhk1z3DdSJNCqT45EslwCu5+LB9GDadSyBItgKUfGsXAmN/aa7LRQ0ZxHffUxUzvgbTPJL27/pE9ZQWHy7A==",
6439 + "version": "17.13.2",
6440 + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.13.2.tgz",
6441 + "integrity": "sha512-MhBAKkT01h8cOXcTBTlpuR7bxH5OBUNpUXefsvwSVEy46cY4m/Kzr2osUCQvA3zJFD6KuCeNNDv0+HDuWk/OcA==",
6442 "dev": true,
6443 + "license": "MIT",
6444 "dependencies": {
6445 "@eslint-community/eslint-utils": "^4.4.1",
6446 "enhanced-resolve": "^5.17.1",
@@ -6507,13 +6507,14 @@
6507 }
6508 },
6509 "node_modules/eslint-plugin-regexp": {
6510 - "version": "2.6.0",
6511 - "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.6.0.tgz",
6512 - "integrity": "sha512-FCL851+kislsTEQEMioAlpDuK5+E5vs0hi1bF8cFlPlHcEjeRhuAzEsGikXRreE+0j4WhW2uO54MqTjXtYOi3A==",
6510 + "version": "2.7.0",
6511 + "resolved": "https://registry.npmjs.org/eslint-plugin-regexp/-/eslint-plugin-regexp-2.7.0.tgz",
6512 + "integrity": "sha512-U8oZI77SBtH8U3ulZ05iu0qEzIizyEDXd+BWHvyVxTOjGwcDcvy/kEpgFG4DYca2ByRLiVPFZ2GeH7j1pdvZTA==",
6513 "dev": true,
6514 + "license": "MIT",
6515 "dependencies": {
6516 "@eslint-community/eslint-utils": "^4.2.0",
6516 - "@eslint-community/regexpp": "^4.9.1",
6517 + "@eslint-community/regexpp": "^4.11.0",
6518 "comment-parser": "^1.4.0",
6519 "jsdoc-type-pratt-parser": "^4.0.0",
6520 "refa": "^0.12.1",
@@ -7628,6 +7629,7 @@
7629 "version": "9.0.3",
7630 "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz",
7631 "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==",
7632 + "license": "MIT",
7633 "dependencies": {
7634 "@types/hast": "^3.0.0",
7635 "@types/unist": "^3.0.0",
@@ -7650,6 +7652,7 @@
7652 "version": "3.0.0",
7653 "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
7654 "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
7655 + "license": "MIT",
7656 "dependencies": {
7657 "@types/hast": "^3.0.0"
7658 },
@@ -7746,6 +7749,7 @@
7749 "version": "3.0.0",
7750 "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
7751 "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
7752 + "license": "MIT",
7753 "funding": {
7754 "type": "github",
7755 "url": "https://github.com/sponsors/wooorm"
@@ -8499,6 +8503,7 @@
8503 "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.0.tgz",
8504 "integrity": "sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==",
8505 "dev": true,
8506 + "license": "MIT",
8507 "dependencies": {
8508 "acorn": "^8.5.0",
8509 "eslint-visitor-keys": "^3.0.0",
@@ -8517,6 +8522,7 @@
8522 "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
8523 "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
8524 "dev": true,
8525 + "license": "Apache-2.0",
8526 "engines": {
8527 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
8528 },
@@ -8529,6 +8535,7 @@
8535 "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
8536 "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
8537 "dev": true,
8538 + "license": "BSD-2-Clause",
8539 "dependencies": {
8540 "acorn": "^8.9.0",
8541 "acorn-jsx": "^5.3.2",
@@ -9226,6 +9233,7 @@
9233 "version": "13.2.0",
9234 "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
9235 "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
9236 + "license": "MIT",
9237 "dependencies": {
9238 "@types/hast": "^3.0.0",
9239 "@types/mdast": "^4.0.0",
@@ -10572,13 +10580,14 @@
10580 }
10581 },
10582 "node_modules/oniguruma-to-es": {
10575 - "version": "0.1.2",
10576 - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.1.2.tgz",
10577 - "integrity": "sha512-sBYKVJlIMB0WPO+tSu/NNB1ytSFeHyyJZ3Ayxfx3f/QUuXu0lvZk0VB4K7npmdlHSC0ldqanzh/sUSlAbgCTfw==",
10583 + "version": "0.4.1",
10584 + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.4.1.tgz",
10585 + "integrity": "sha512-rNcEohFz095QKGRovP/yqPIKc+nP+Sjs4YTHMv33nMePGKrq/r2eu9Yh4646M5XluGJsUnmwoXuiXE69KDs+fQ==",
10586 + "license": "MIT",
10587 "dependencies": {
10588 "emoji-regex-xs": "^1.0.0",
10580 - "regex": "^4.4.0",
10581 - "regex-recursion": "^4.1.0"
10589 + "regex": "^5.0.0",
10590 + "regex-recursion": "^4.2.1"
10591 }
10592 },
10593 "node_modules/only": {
@@ -11270,10 +11279,11 @@
11279 }
11280 },
11281 "node_modules/prettier-plugin-tailwindcss": {
11273 - "version": "0.6.8",
11274 - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.8.tgz",
11275 - "integrity": "sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA==",
11282 + "version": "0.6.9",
11283 + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.9.tgz",
11284 + "integrity": "sha512-r0i3uhaZAXYP0At5xGfJH876W3HHGHDp+LCRUJrs57PBeQ6mYHMwr25KH8NPX44F2yGTvdnH7OqCshlQx183Eg==",
11285 "dev": true,
11286 + "license": "MIT",
11287 "engines": {
11288 "node": ">=14.21.3"
11289 },
@@ -11372,6 +11382,7 @@
11382 "version": "6.5.0",
11383 "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz",
11384 "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==",
11385 + "license": "MIT",
11386 "funding": {
11387 "type": "github",
11388 "url": "https://github.com/sponsors/wooorm"
@@ -11632,6 +11643,7 @@
11643 "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.1.tgz",
11644 "integrity": "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==",
11645 "dev": true,
11646 + "license": "MIT",
11647 "dependencies": {
11648 "@eslint-community/regexpp": "^4.8.0"
11649 },
@@ -11640,14 +11652,19 @@
11652 }
11653 },
11654 "node_modules/regex": {
11643 - "version": "4.4.0",
11644 - "resolved": "https://registry.npmjs.org/regex/-/regex-4.4.0.tgz",
11645 - "integrity": "sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ=="
11655 + "version": "5.0.2",
11656 + "resolved": "https://registry.npmjs.org/regex/-/regex-5.0.2.tgz",
11657 + "integrity": "sha512-/pczGbKIQgfTMRV0XjABvc5RzLqQmwqxLHdQao2RTXPk+pmTXB2P0IaUHYdYyk412YLwUIkaeMd5T+RzVgTqnQ==",
11658 + "license": "MIT",
11659 + "dependencies": {
11660 + "regex-utilities": "^2.3.0"
11661 + }
11662 },
11663 "node_modules/regex-recursion": {
11664 "version": "4.2.1",
11665 "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-4.2.1.tgz",
11666 "integrity": "sha512-QHNZyZAeKdndD1G3bKAbBEKOSSK4KOHQrAJ01N1LJeb0SoH4DJIeFhp0uUpETgONifS4+P3sOgoA1dhzgrQvhA==",
11667 + "license": "MIT",
11668 "dependencies": {
11669 "regex-utilities": "^2.3.0"
11670 }
@@ -11655,13 +11672,15 @@
11672 "node_modules/regex-utilities": {
11673 "version": "2.3.0",
11674 "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz",
11658 - "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="
11675 + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==",
11676 + "license": "MIT"
11677 },
11678 "node_modules/regexp-ast-analysis": {
11679 "version": "0.7.1",
11680 "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz",
11681 "integrity": "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==",
11682 "dev": true,
11683 + "license": "MIT",
11684 "dependencies": {
11685 "@eslint-community/regexpp": "^4.8.0",
11686 "refa": "^0.12.1"
@@ -12227,6 +12246,7 @@
12246 "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz",
12247 "integrity": "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==",
12248 "dev": true,
12249 + "license": "MIT",
12250 "dependencies": {
12251 "@eslint-community/regexpp": "^4.8.0",
12252 "refa": "^0.12.0",
@@ -12327,14 +12347,15 @@
12347 }
12348 },
12349 "node_modules/shiki": {
12330 - "version": "1.23.0",
12331 - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.23.0.tgz",
12332 - "integrity": "sha512-xfdu9DqPkIpExH29cmiTlgo0/jBki5la1Tkfhsv+Wu5TT3APLNHslR1acxuKJOCWqVdSc+pIbs/2ozjVRGppdg==",
12333 - "dependencies": {
12334 - "@shikijs/core": "1.23.0",
12335 - "@shikijs/engine-javascript": "1.23.0",
12336 - "@shikijs/engine-oniguruma": "1.23.0",
12337 - "@shikijs/types": "1.23.0",
12350 + "version": "1.23.1",
12351 + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.23.1.tgz",
12352 + "integrity": "sha512-8kxV9TH4pXgdKGxNOkrSMydn1Xf6It8lsle0fiqxf7a1149K1WGtdOu3Zb91T5r1JpvRPxqxU3C2XdZZXQnrig==",
12353 + "license": "MIT",
12354 + "dependencies": {
12355 + "@shikijs/core": "1.23.1",
12356 + "@shikijs/engine-javascript": "1.23.1",
12357 + "@shikijs/engine-oniguruma": "1.23.1",
12358 + "@shikijs/types": "1.23.1",
12359 "@shikijs/vscode-textmate": "^9.3.0",
12360 "@types/hast": "^3.0.4"
12361 }
@@ -12446,6 +12467,7 @@
12467 "version": "2.0.2",
12468 "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
12469 "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
12470 + "license": "MIT",
12471 "funding": {
12472 "type": "github",
12473 "url": "https://github.com/sponsors/wooorm"
@@ -12679,6 +12701,7 @@
12701 "version": "4.0.4",
12702 "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
12703 "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
12704 + "license": "MIT",
12705 "dependencies": {
12706 "character-entities-html4": "^2.0.0",
12707 "character-entities-legacy": "^3.0.0"
@@ -13072,6 +13095,7 @@
13095 "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
13096 "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
13097 "dev": true,
13098 + "license": "MIT",
13099 "engines": {
13100 "node": ">=6"
13101 }
@@ -13375,6 +13399,7 @@
13399 "version": "3.0.1",
13400 "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
13401 "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
13402 + "license": "MIT",
13403 "funding": {
13404 "type": "github",
13405 "url": "https://github.com/sponsors/wooorm"
@@ -14027,6 +14052,7 @@
14052 "version": "5.0.0",
14053 "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
14054 "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
14055 + "license": "MIT",
14056 "dependencies": {
14057 "@types/unist": "^3.0.0"
14058 },
@@ -14292,6 +14318,7 @@
14318 "version": "6.0.3",
14319 "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
14320 "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
14321 + "license": "MIT",
14322 "dependencies": {
14323 "@types/unist": "^3.0.0",
14324 "vfile-message": "^4.0.0"
@@ -14305,6 +14332,7 @@
14332 "version": "4.0.2",
14333 "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz",
14334 "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==",
14335 + "license": "MIT",
14336 "dependencies": {
14337 "@types/unist": "^3.0.0",
14338 "unist-util-stringify-position": "^4.0.0"
frontend/package.json
+11 -10
@@ -20,7 +20,9 @@
20 "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
21 "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
22 "type-check": "vue-tsc --build --force",
23 + "format": "prettier --write src/",
24 "lint": "eslint . --fix",
25 + "lint-format": "run-s lint type-check format",
26 "tailwind-config-viewer": "tailwind-config-viewer -o",
27 "design-tokens": "node scripts/tokens-tool.js",
28 "start-server-old": "cd ../backend && uvicorn copilot:app --reload --port=5000",
@@ -31,8 +33,7 @@
33 "libs-check": "taze && depcheck",
34 "libs-reload": "rm -rf node_modules package-lock.json && npm install",
35 "open:swagger": "open http://127.0.0.1:5000/docs#/",
34 - "open:redoc": "open http://127.0.0.1:5000/redoc",
35 - "format": "prettier --write src/"
36 + "open:redoc": "open http://127.0.0.1:5000/redoc"
37 },
38 "dependencies": {
39 "@ajoelp/json-to-formdata": "^1.5.0",
@@ -40,7 +41,7 @@
41 "@fontsource/jetbrains-mono": "^5.1.1",
42 "@fontsource/lexend": "^5.1.1",
43 "@fontsource/public-sans": "^5.1.1",
43 - "@shikijs/markdown-it": "^1.23.0",
44 + "@shikijs/markdown-it": "^1.23.1",
45 "@tailwindcss/container-queries": "^0.1.1",
46 "@vueuse/core": "^11.2.0",
47 "axios": "^1.7.7",
@@ -61,7 +62,7 @@
62 "pinia": "^2.2.6",
63 "pinia-plugin-persistedstate": "^4.1.3",
64 "secure-ls": "^2.0.0",
64 - "shiki": "^1.23.0",
65 + "shiki": "^1.23.1",
66 "validator": "^13.12.0",
67 "vue": "^3.5.13",
68 "vue-advanced-cropper": "^2.8.9",
@@ -74,10 +75,10 @@
75 "vuedraggable": "^4.1.0"
76 },
77 "optionalDependencies": {
77 - "@rollup/rollup-linux-x64-gnu": "^4.27.2"
78 + "@rollup/rollup-linux-x64-gnu": "^4.27.3"
79 },
80 "devDependencies": {
80 - "@antfu/eslint-config": "^3.9.1",
81 + "@antfu/eslint-config": "^3.9.2",
82 "@clack/prompts": "^0.8.1",
83 "@iconify/vue": "^4.1.2",
84 "@tsconfig/node20": "^20.1.4",
@@ -102,7 +103,7 @@
103 "npm-run-all2": "^7.0.1",
104 "postcss": "^8.4.49",
105 "prettier": "^3.3.3",
105 - "prettier-plugin-tailwindcss": "^0.6.8",
106 + "prettier-plugin-tailwindcss": "^0.6.9",
107 "sass": "^1.81.0",
108 "start-server-and-test": "^2.0.8",
109 "tailwind-config-viewer": "^2.0.4",
@@ -120,9 +121,9 @@
121 },
122 "pnpm": {
123 "overrides": {
123 - "@typescript-eslint/eslint-plugin": "^8.14.0",
124 + "@typescript-eslint/eslint-plugin": "^8.15.0",
125 "@typescript-eslint/eslint-plugin>eslint": "$eslint",
125 - "@typescript-eslint/parser": "^8.14.0",
126 + "@typescript-eslint/parser": "^8.15.0",
127 "@typescript-eslint/parser>eslint": "$eslint",
128 "eslint": "$eslint"
129 }
@@ -134,6 +135,6 @@
135 "@typescript-eslint/parser": {
136 "eslint": "^9.15.0"
137 },
137 - "@typescript-eslint/typescript-estree": "^8.14.0"
138 + "@typescript-eslint/typescript-estree": "^8.15.0"
139 }
140 }
frontend/src/app-layouts/common/Toolbar/Breadcrumb.vue
+22 -15
@@ -7,8 +7,8 @@
7 <n-breadcrumb-item
8 v-for="(item, index) of items"
9 :key="item.key"
10 - :clickable="false"
10 :class="`index-${index}`"
11 + @click="goto({ path: item.path })"
12 >
13 {{ item.name }}
14 </n-breadcrumb-item>
@@ -20,8 +20,9 @@
20 import type { RouteLocationNormalizedLoaded } from "vue-router"
21 import Icon from "@/components/common/Icon.vue"
22 import _capitalize from "lodash/capitalize"
23 +import _compact from "lodash/compact"
24 +import _isEqual from "lodash/isEqual"
25 import _split from "lodash/split"
24 -import _upperCase from "lodash/upperCase"
26 import { NBreadcrumb, NBreadcrumbItem } from "naive-ui"
27 import { onBeforeMount, ref } from "vue"
28 import { useRoute, useRouter } from "vue-router"
@@ -40,7 +41,6 @@ const items = ref<Page[]>([])
41 function goto(page: Partial<Page>) {
42 if (page.name && page.name !== route.name) {
43 router.push({ name: page.name })
43 - return
44 }
45 if (page.path && page.path !== route.path) {
46 router.push({ path: page.path })
@@ -49,22 +49,29 @@ function goto(page: Partial<Page>) {
49
50 function checkRoute(route: RouteLocationNormalizedLoaded) {
51 const newItems: Page[] = []
52 - const pathChunks = route?.path?.indexOf("/") !== -1 ? _split(route?.path || "", "/") : [route?.path]
52 + let pathChunks = _compact(_split(route?.path || "", "/"))
53 + if (!pathChunks.length) {
54 + pathChunks = _compact(_split(route?.matched?.[0]?.aliasOf?.path || "", "/"))
55 + }
56 +
57 + let cumulativePath = ""
58
59 for (const chunk of pathChunks) {
55 - if (chunk) {
56 - const name = _capitalize(_upperCase(chunk))
57 - const path = chunk.toLowerCase()
58 -
59 - newItems.push({
60 - name,
61 - path,
62 - key: name + path
63 - })
64 - }
60 + const name = _capitalize(chunk)
61 + cumulativePath += `/${chunk}`
62 +
63 + newItems.push({
64 + name,
65 + path: cumulativePath,
66 + key: name + cumulativePath
67 + })
68 + }
69 +
70 + if (route.meta?.title && newItems.length) {
71 + newItems[newItems.length - 1].name = route.meta.title
72 }
73
67 - if (JSON.stringify(items.value) !== JSON.stringify(newItems)) {
74 + if (!_isEqual(items.value, newItems)) {
75 items.value = newItems
76 }
77 }
frontend/src/components/incidentManagement/sources/SourceConfigurationDetails.vue
+1
@@ -61,6 +61,7 @@ function getSourceConfiguration() {
61 if (res.data.success) {
62 sourceConfiguration.value = {
63 field_names: res.data.field_names || [],
64 + ioc_field_names: res.data.ioc_field_names || [],
65 asset_name: res.data.asset_name || "",
66 timefield_name: res.data.timefield_name || "",
67 alert_title_name: res.data.alert_title_name || "",
frontend/src/components/incidentManagement/sources/SourceConfigurationForm.vue
+18
@@ -67,6 +67,19 @@
67 :loading="loadingAvailableMappings"
68 />
69 </n-form-item>
70 + <n-form-item label="IOC Field names" path="ioc_field_names">
71 + <n-select
72 + v-model:value="form.ioc_field_names"
73 + :options="availableMappingsOptions"
74 + placeholder="Select..."
75 + clearable
76 + filterable
77 + multiple
78 + to="body"
79 + :disabled="!isFieldEnabled"
80 + :loading="loadingAvailableMappings"
81 + />
82 + </n-form-item>
83 <n-form-item label="Asset name" path="asset_name">
84 <n-select
85 v-model:value="form.asset_name"
@@ -292,6 +305,7 @@ function validate(cb?: () => void) {
305 function getSourceConfigurationForm(): SourceConfigurationModel {
306 return {
307 field_names: sourceConfigurationModel.value?.field_names || [],
308 + ioc_field_names: sourceConfigurationModel.value?.ioc_field_names || [],
309 asset_name: sourceConfigurationModel.value?.asset_name || null,
310 timefield_name: sourceConfigurationModel.value?.timefield_name || null,
311 alert_title_name: sourceConfigurationModel.value?.alert_title_name || null,
@@ -315,6 +329,7 @@ function sanitizeFields() {
329 const availableMappings = availableMappingsOptions.value.map(o => o.value)
330
331 form.value.field_names = _intersection(availableMappings, form.value.field_names)
332 + form.value.ioc_field_names = _intersection(availableMappings, form.value.ioc_field_names)
333
334 if (form.value.asset_name && !availableMappings.includes(form.value.asset_name)) {
335 form.value.asset_name = null
@@ -330,6 +345,7 @@ function sanitizeFields() {
345 function submit() {
346 const payload: SourceConfiguration = {
347 field_names: form.value?.field_names || [],
348 + ioc_field_names: form.value?.ioc_field_names || [],
349 asset_name: form.value?.asset_name || "",
350 timefield_name: form.value?.timefield_name || "",
351 alert_title_name: form.value?.alert_title_name || "",
@@ -354,6 +370,7 @@ function resetSource() {
370
371 function setSocfortressRecommendsWazuh() {
372 form.value.field_names = socfortressRecommendsWazuh.value?.field_names || []
373 + form.value.ioc_field_names = socfortressRecommendsWazuh.value?.ioc_field_names || []
374 form.value.asset_name = socfortressRecommendsWazuh.value?.asset_name || null
375 form.value.timefield_name = socfortressRecommendsWazuh.value?.timefield_name || null
376 form.value.alert_title_name = socfortressRecommendsWazuh.value?.alert_title_name || null
@@ -374,6 +391,7 @@ function getSocfortressRecommendsWazuh() {
391 if (res.data.success) {
392 socfortressRecommendsWazuh.value = {
393 field_names: res.data.field_names,
394 + ioc_field_names: res.data.ioc_field_names,
395 asset_name: res.data.asset_name,
396 timefield_name: res.data.timefield_name,
397 alert_title_name: res.data.alert_title_name,
frontend/src/components/incidentManagement/sources/SourceConfigurationViewer.vue
+13
@@ -18,6 +18,19 @@
18 </div>
19 </template>
20 </CardKV>
21 + <CardKV>
22 + <template #key>IOC Field names</template>
23 + <template #value>
24 + <div v-if="sourceConfiguration.ioc_field_names?.length" class="flex flex-wrap gap-2">
25 + <Badge v-for="field of sourceConfiguration.ioc_field_names" :key="field" type="splitted" fluid>
26 + <template #value>
27 + {{ field }}
28 + </template>
29 + </Badge>
30 + </div>
31 + <span v-else>-</span>
32 + </template>
33 + </CardKV>
34 <CardKV>
35 <template #key>Asset name</template>
36 <template #value>
frontend/src/components/incidentManagement/sources/SourceConfigurationWizard.vue
+1
@@ -142,6 +142,7 @@ function setSourceConfiguration(indexName: string | null) {
142 if (indexName) {
143 sourceConfigurationModel.value = {
144 field_names: [],
145 + ioc_field_names: [],
146 asset_name: null,
147 timefield_name: null,
148 alert_title_name: null,
frontend/src/types/incidentManagement/sources.d.ts
+1
@@ -6,6 +6,7 @@ export interface SourceConfiguration {
6 timefield_name: string
7 alert_title_name: string
8 source: string
9 + ioc_field_names: string[]
10 }
11
12 export interface SourceConfigurationModel extends SourceConfiguration {