@cryptotaxi247 / CoPilot / commits / 4fa16de6

Incident management (#275)

* improve: grid style * improve: types * add: configured sources components * fix: source configured form * update: dependencies * refactor: badge component * refactor: kvcard component * add: source configuration form/viewer component * prettify * refactor: add update_alert_status endpoint * refactor: update alert assigned_to endpoint * refactor: add list_alerts_by_assigned_to_endpoint * some pre commit fixes * responses basemodels * precommit fixes some * remove commented out * refactor: add does_assit_exist and add_asset_to_copilot_alert functions * refactor: add get_available_users endpoint * fetch alerts from gl-events index * refactor: Add Source field to AlertPayloadItem schema * refactor: Remove commented out code and pre-commit fixes * refactor: Add create_alert_auto_route to incident_alert.py * refactor: Update create_alert_auto_route in incident_alert.py to use get_original_alert_id and get_original_alert_index_name * update: graylog api * update: graylog types * add: Incident management Sources wizard * update * update: configured Sources List page * available sources route * get indices via source * update: dependencies * update: incident Management apis/types * update: SourceConfiguration wizard/form * update: SourceConfiguration wizard/form * chore: Update field and asset names for source * update: SourceConfiguration wizard/form * some logging * update: SourceConfiguration wizard/form * update: dependencies * refactor: apis/types * update: incident Management apis/types * Update source field type in FieldAndAssetNames schema * chore: Update alert creation route to handle no alerts found * chore: Add COPILOT_ALERT_ID field to Graylog alert provision services * refactor: layout lists * add: incident alerts component * chore: Update create_alert_auto_route return type to AutoCreateAlertResponse * some cleanup * cleanup * some precommit fixes * add process_name to alert_context * chore: Add logging statements for debugging incident alert creation * chore: Create case from alert endpoint * case operations * some precommit fixes * update: incident api/types * update: badge component * add: alert card * update: alert card * improve: alerts list * update: dependencies * add: alert details * add: alert overview * improve: kvcard component * add: alert assets list * update: alert details * update: dependencies * update: auth store * update: incident api/type * add: alert comments components * refactor * update: alert overview * chore: Delete alert only if it is not linked to a case * added sigma queries table * creating sigma things * sigma rules downloads * more sigma things * delete sigma rule * add deletion endpoint * get active sigma queries * update sigma time interval * set time for last_updated * execute sigma query against the indexer * chore: Update execute_query function to include rule name and index parameter * feat: Add endpoint to run active Sigma queries * chore: Update execute_query function to include last execution time * resize wazuh index fields function and route * chore: Refactor scheduler to include Wazuh index fields resizing job * chore: Update default time interval for SigmaQuery to 5 minutes * chore: Activate all Sigma queries * refactor: artifact / threat intel components * update: dependencies * update: css helpers * add: ThreatIntelProcessEvaluationProvider * update: alert item * update: alert asset * dashboard template fix * added graylog receiver * added comment * update: alert asset * chore: Delete all Sigma queries * mappings adjust * feat: Add endpoint to deactivate all Sigma queries The code changes include the addition of a new endpoint `/deactivate-all-queries` in the `sigma.py` file. This endpoint deactivates all Sigma queries by setting their `active` status to `False` in the database. Recent user commits: - mappings adjust - chore: Delete all Sigma queries - update: alert asset - added comment - added graylog receiver - dashboard template fix - update: alert asset - update: alert item - add: ThreatIntelProcessEvaluationProvider * precommit fixes * chore: Comment out Incident Management menu item temporarily * get builds out * remove packaging constraint * chore: Update packaging constraint to allow version 24.1 or higher The packaging constraint in the backend requirements.txt file was updated to allow version 24.1 or higher. This change was made to ensure compatibility with other dependencies. * chore: Update packaging constraint to version 23.2 The packaging constraint in the backend requirements.txt file was reverted back to version 23.2. This change was made to test why the container is crashing at start time * i think not issue related to python library * chore: Catch all exceptions when applying migrations The code changes in `db_setup.py` modify the `apply_migrations` function to catch any type of exception, instead of just `OperationalError`. This change was made to ensure that any errors encountered during the migration process are properly logged and raised. Recent user commits: - i think not issue related to python library - chore: Update packaging constraint to version 23.2 - chore: Update packaging constraint to allow version 24.1 or higher - remove packaging constraint - get builds out - chore: Comment out Incident Management menu item temporarily - precommit fixes - feat: Add endpoint to deactivate all Sigma queries - mappings adjust - chore: Delete all Sigma queries * chore: Catch all exceptions when applying migrations * chore: Update pySigma dependency to version 0.11.9 The code changes in the backend requirements.txt file update the pySigma dependency to version 0.11.9. This change was made to ensure compatibility with other dependencies and to remove a comment that was no longer needed. Recent user commits: - i think not issue related to python library - chore: Update packaging constraint to version 23.2 - chore: Update packaging constraint to allow version 24.1 or higher - remove packaging constraint - get builds out - chore: Comment out Incident Management menu item temporarily - precommit fixes - feat: Add endpoint to deactivate all Sigma queries - mappings adjust - chore: Delete all Sigma queries * chore: Add endpoint to execute Shuffle workflow This commit adds a new endpoint `/invoke-workflow` to the Shuffle integrations routes. The endpoint allows users with the "admin" or "analyst" scope to execute a workflow by providing the workflow ID and execution arguments. The execution ID is returned in the response. * chore: Update Shuffle connection details The code changes in `db_populate.py` update the connection details for the Shuffle integration. The API key for the Shuffle connection has been modified to include the `WORKFLOW_ID` parameter. This change was made to ensure proper authentication and connection to Shuffle. * chore: Update Shuffle connection details * added incidnet management notification table * chore: Add endpoint to manage customer notifications This commit adds two new endpoints to the incidents routes: `/notification/{customer_code}` and `/notification`. The `/notification/{customer_code}` endpoint retrieves the notifications for a specific customer, while the `/notification` endpoint allows for updating the notifications. These endpoints are used to manage customer-specific notifications in the incident management system. * chore: Add customer notification workflow execution to incident management created alerts * chore: Add customer notification workflow execution to incident management created alerts * chore: Add .ql index to skip list This commit adds the ".ql" index to the skip list in the IndexConfigModel class. This change was made to ensure that the ".ql" index is skipped during indexing operations in the Wazuh indexer schema. * add invoke alert creation collect as a scheduled job to start at runtime * refactor: Remove unnecessary code for scheduling Wazuh monitoring alert * chore: Remove unused notifications in GraylogAlertProvisionModel * chore: Update Docker workflow to use main branch for push events * add ability for user to set sigma update to db rule level comment out invoke_alert_creation_collect schedule until ready * chore: Update Docker workflow to use main branch for push events * chore: Add Sigma query management endpoints * feat: Add Sigma query upload endpoint * make a build * chore: Update Docker workflow to use main branch for push events * chore: remove shuffle extra data on connector config * chore: Update Docker workflow to use main branch for push events * update: dependencies * uncomment Incident Management menu item * improve: alert list responsiveness * add customer code table for incident management * add customer code configuration * chore: Remove unused customer code functionality * feat: Add index_name parameter to run_active_sigma_queries_endpoint * chore: Update Docker workflow to use incident-management branch for push events * chore: Refactor get_indices_full function to handle different formats of indices_data * feat: Add API endpoint to retrieve details of a single alert * add: cases api/types * improve: alert item component * refactor: incident management components * add: cases components * add: cases page * fix: types * refactor: Update list_alerts function to include linked cases in AlertOut response * refactor: Include linked cases in AlertOut response for get_alert_by_id function * try iso for sigma time format * refactor: Update get_single_alert_details_route to use CreateAlertRequestRoute in incident_alert.py * add: alert details api/type * update: alert overview * refactor: Update last execution time in run_active_sigma_queries_endpoint * refactor: Update run_single_sigma_query_endpoint to execute Sigma queries based on time interval * refactor: Include linked cases in get_case_by_id and list_cases functions * refactor: Include case creation time in CaseOut and CaseOutResponse models * update: alert and case type * update: cases page * improve: goto composable * add: AlertDetails on alert asset * add: case creation * update: case item component * add: case details modal * fix: case creation * update: dependencies * feat: Add case status to list_cases function * add creation tag feature * Refactor code to delete alert tags in create_alert_tag_endpoint and delete_alert_tag functions * feat: Add id field to AlertTagBase model * feat: Add AlertTagDelete model and update delete_alert_tag_endpoint * feat: Add pagination to list_alerts_endpoint * feat: Add pagination to list_alerts_by_status_endpoint * feat: Add pagination to list_alerts_by_asset_name_endpoint * feat: Add pagination to list_alerts_by_assigned_to_endpoint * feat: Add list_alerts_by_title_endpoint for retrieving alerts by title with pagination * feat: Add Socfortress Recommends Wazuh endpoint for retrieving field names and asset names * refactor: Add Socfortress Recommends Wazuh field names and asset names to enum * Refactor SocfortressRecommendsWazuhFieldNames enum to include extra use for Within CoPilot * update dependencies * updated alert tag component * refactor alert assign component * refactor case assign component * update tw config * added alert-link api * add alert-link feature * improve case item component * refactor alert overview component * update alerts list component * start alert timeline collection * feat: Add alert timeline retrieval to get_alert_timeline_route * chore: Add start and end of day calculation for alert timeline retrieval * chore: Add size parameter to fetch_alert_timeline for limiting the number of results * chore: Update list_alerts_endpoint to include total count of alerts * chore: Update list_alerts_endpoint to include total count of alerts * chore: Update list_alerts_endpoint to include total count of alerts * chore: Update alert response model to include counts for open, in progress, and closed alerts * try velo file collection * add "get Socfortress Recommends Wazuh" feature * update alert list filters and pagination * add alert timeline components * refactor alert assets components * update dependencies * asset and totals update * chore: Update list_alerts_by_assigned_to_endpoint to include total count of alerts by assigned_to * feat: Add pagination and filters to list_alerts_by_tag_endpoint * updated menu items * update dependencies * retrieve alerts from graylog index now * chore: Uncomment invoke_alert_creation_collect job in scheduler * chore: Refactor create_alert_auto_route to handle case when no alerts are found * chore: Rename create_alert_route to create_alert_manual_route in incident_alert.py and escalate_alert.py * chore: Remove unused dfir-iris fields from ProvisionNewCustomer schema since we are no longer using it * chore: Remove unused dfir-iris connection verification * chore: Remove unused dfir-iris connection verification and fields from ProvisionNewCustomer schema * chore: Update ProvisionNewCustomer schema to make iris_customer_id optional * update alert page * improve cases page * update alert actions * add alert item highlight feature * update dependencies * precommit fixes * chore: Remove unused dfir-iris connector from the database since we are now using built in incident management * update dependencies * add incidents notification api/types * refactor customer details modal * added incident notifications components * Improved lazy loading of components * prettier * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Aug 27, 2024 at 11:31 UTC 4fa16de6abedcd4f122df624acfb0f0c2ce9509c
274 files changed +15781 -2706
.env.example
+1
@@ -27,6 +27,7 @@ GRAYLOG_PASSWORD=dummy
27
28 SHUFFLE_URL=https://127.1.1.1
29 SHUFFLER_API_KEY=dummy
30 +SHUFFLE_WORKFLOW_ID=dummy
31
32 DFIR_IRIS_URL=https://127.1.1.1
33 DFIR_IRIS_API_KEY=dummy
.github/workflows/docker.yml
+15 -15
@@ -2,7 +2,7 @@ name: Docker
2
3 on:
4 push:
5 - branches: [main]
5 + branches: [incident-management]
6
7 jobs:
8 build-backend:
@@ -27,16 +27,16 @@ jobs:
27 with:
28 context: ./backend
29 push: true
30 - tags: ghcr.io/socfortress/copilot-backend:latest
30 + tags: ghcr.io/socfortress/copilot-backend:lab
31 build-args: |
32 COPILOT_API_KEY=${{ secrets.COPILOT_API_KEY }}
33
34 - - name: Notify Discord
35 - uses: appleboy/discord-action@v1.0.0
36 - with:
37 - webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
38 - webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
39 - message: "Docker image for backend has been updated."
34 + # - name: Notify Discord
35 + # uses: appleboy/discord-action@v1.0.0
36 + # with:
37 + # webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
38 + # webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
39 + # message: "Docker image for backend has been updated."
40
41 build-frontend:
42 runs-on: ubuntu-latest
@@ -60,11 +60,11 @@ jobs:
60 with:
61 context: ./frontend
62 push: true
63 - tags: ghcr.io/socfortress/copilot-frontend:latest
63 + tags: ghcr.io/socfortress/copilot-frontend:lab
64
65 - - name: Notify Discord
66 - uses: appleboy/discord-action@v1.0.0
67 - with:
68 - webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
69 - webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
70 - message: "Docker image for frontend has been updated."
65 + # - name: Notify Discord
66 + # uses: appleboy/discord-action@v1.0.0
67 + # with:
68 + # webhook_id: ${{ secrets.DISCORD_WEBHOOK_ID }}
69 + # webhook_token: ${{ secrets.DISCORD_WEBHOOK_TOKEN }}
70 + # message: "Docker image for frontend has been updated."
.gitignore
+1
@@ -57,6 +57,7 @@ backend/report.pdf
57 backend/report.html
58 backend/scoutsuite-report
59 backend/app/integrations/office365/services/wazuh_config.xml
60 +backend/app/connectors/wazuh_indexer/sigma_artifacts
61
62 frontend/src/unplugin.components.d.ts
63 frontend/cypress/screenshots/
.vscode/settings.json
+1
@@ -53,6 +53,7 @@
53 "Sysmon",
54 "tabler",
55 "taze",
56 + "timefield",
57 "timerange",
58 "timesec",
59 "tsbuildinfo",
backend/alembic/env.py
+14
@@ -9,6 +9,7 @@ from alembic import context
9 # from app.db.all_models import *
10 from app.auth.models.users import User
11 from app.connectors.models import Connectors
12 +from app.connectors.wazuh_indexer.models.sigma import SigmaQuery
13
14 # from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
15 from app.customer_provisioning.models.default_settings import (
@@ -21,6 +22,19 @@ from app.db.universal_models import Agents
22 from app.db.universal_models import Customers
23 from app.db.universal_models import CustomersMeta
24 from app.db.universal_models import LogEntry
25 +from app.incidents.models import Alert
26 +from app.incidents.models import AlertContext
27 +from app.incidents.models import AlertTag
28 +from app.incidents.models import AlertTitleFieldName
29 +from app.incidents.models import AlertToTag
30 +from app.incidents.models import Asset
31 +from app.incidents.models import AssetFieldName
32 +from app.incidents.models import Case
33 +from app.incidents.models import CaseAlertLink
34 +from app.incidents.models import Comment
35 +from app.incidents.models import CustomerCodeFieldName
36 +from app.incidents.models import FieldName
37 +from app.incidents.models import Notification
38 from app.integrations.alert_creation_settings.models.alert_creation_settings import (
39 AlertCreationSettings,
40 )
backend/alembic/versions/1501739c6898_add_incident_tables.py new
+126
@@ -0,0 +1,126 @@
1 +"""Add incident tables
2 +
3 +Revision ID: 1501739c6898
4 +Revises: fed7739bd07c
5 +Create Date: 2024-07-15 09:07:17.324615
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +from sqlalchemy.dialects import mysql
13 +
14 +from alembic import op
15 +
16 +# revision identifiers, used by Alembic.
17 +revision: str = "1501739c6898"
18 +down_revision: Union[str, None] = "fed7739bd07c"
19 +branch_labels: Union[str, Sequence[str], None] = None
20 +depends_on: Union[str, Sequence[str], None] = None
21 +
22 +
23 +def upgrade() -> None:
24 + # ### commands auto generated by Alembic - please adjust! ###
25 + op.create_table(
26 + "incident_management_alert",
27 + sa.Column("id", sa.Integer(), nullable=False),
28 + sa.Column("alert_name", mysql.TEXT(length=1000000), nullable=False),
29 + sa.Column("alert_description", mysql.TEXT(length=1000000), nullable=False),
30 + sa.Column("status", sa.String(length=50), nullable=False),
31 + sa.Column("alert_creation_time", sa.DateTime(), nullable=False),
32 + sa.Column("customer_code", sa.String(length=50), nullable=False),
33 + sa.Column("time_closed", sa.DateTime(), nullable=True),
34 + sa.Column("source", sa.String(length=50), nullable=False),
35 + sa.PrimaryKeyConstraint("id"),
36 + )
37 + op.create_table(
38 + "incident_management_alertcontext",
39 + sa.Column("context", sa.JSON(), nullable=True),
40 + sa.Column("id", sa.Integer(), nullable=False),
41 + sa.Column("source", sa.String(length=50), nullable=False),
42 + sa.PrimaryKeyConstraint("id"),
43 + )
44 + op.create_table(
45 + "incident_management_assetfieldname",
46 + sa.Column("id", sa.Integer(), nullable=False),
47 + sa.Column("source", sa.String(length=50), nullable=False),
48 + sa.Column("field_name", sa.String(length=100), nullable=False),
49 + sa.PrimaryKeyConstraint("id"),
50 + )
51 + op.create_table(
52 + "incident_management_case",
53 + sa.Column("id", sa.Integer(), nullable=False),
54 + sa.Column("case_name", sa.String(length=10000), nullable=False),
55 + sa.Column("case_description", mysql.TEXT(length=1000000), nullable=False),
56 + sa.PrimaryKeyConstraint("id"),
57 + )
58 + op.create_table(
59 + "incident_management_fieldname",
60 + sa.Column("id", sa.Integer(), nullable=False),
61 + sa.Column("source", sa.String(length=50), nullable=False),
62 + sa.Column("field_name", sa.String(length=100), nullable=False),
63 + sa.PrimaryKeyConstraint("id"),
64 + )
65 + op.create_table(
66 + "incident_management_asset",
67 + sa.Column("id", sa.Integer(), nullable=False),
68 + sa.Column("alert_linked", sa.Integer(), nullable=True),
69 + sa.Column("asset_name", sa.String(length=255), nullable=False),
70 + sa.Column("alert_context_id", sa.Integer(), nullable=False),
71 + sa.Column("agent_id", sa.String(length=50), nullable=True),
72 + sa.Column("velociraptor_id", sa.String(length=150), nullable=True),
73 + sa.Column("customer_code", sa.String(length=50), nullable=False),
74 + sa.Column("index_name", sa.String(length=255), nullable=False),
75 + sa.Column("index_id", sa.String(length=255), nullable=False),
76 + sa.ForeignKeyConstraint(
77 + ["alert_context_id"],
78 + ["incident_management_alertcontext.id"],
79 + ),
80 + sa.ForeignKeyConstraint(
81 + ["alert_linked"],
82 + ["incident_management_alert.id"],
83 + ),
84 + sa.PrimaryKeyConstraint("id"),
85 + )
86 + op.create_table(
87 + "incident_management_casealertlink",
88 + sa.Column("case_id", sa.Integer(), nullable=True),
89 + sa.Column("alert_id", sa.Integer(), nullable=True),
90 + sa.ForeignKeyConstraint(
91 + ["alert_id"],
92 + ["incident_management_alert.id"],
93 + ),
94 + sa.ForeignKeyConstraint(
95 + ["case_id"],
96 + ["incident_management_case.id"],
97 + ),
98 + sa.PrimaryKeyConstraint("case_id", "alert_id"),
99 + )
100 + op.create_table(
101 + "incident_management_comment",
102 + sa.Column("id", sa.Integer(), nullable=False),
103 + sa.Column("alert_id", sa.Integer(), nullable=True),
104 + sa.Column("comment", mysql.TEXT(length=1000000), nullable=False),
105 + sa.Column("user_name", sa.String(length=50), nullable=False),
106 + sa.Column("created_at", sa.DateTime(), nullable=False),
107 + sa.ForeignKeyConstraint(
108 + ["alert_id"],
109 + ["incident_management_alert.id"],
110 + ),
111 + sa.PrimaryKeyConstraint("id"),
112 + )
113 + # ### end Alembic commands ###
114 +
115 +
116 +def downgrade() -> None:
117 + # ### commands auto generated by Alembic - please adjust! ###
118 + op.drop_table("incident_management_comment")
119 + op.drop_table("incident_management_casealertlink")
120 + op.drop_table("incident_management_asset")
121 + op.drop_table("incident_management_fieldname")
122 + op.drop_table("incident_management_case")
123 + op.drop_table("incident_management_assetfieldname")
124 + op.drop_table("incident_management_alertcontext")
125 + op.drop_table("incident_management_alert")
126 + # ### end Alembic commands ###
backend/alembic/versions/7187bab836ab_add_alert_title_field_name_table.py new
+37
@@ -0,0 +1,37 @@
1 +"""Add alert title field name table
2 +
3 +Revision ID: 7187bab836ab
4 +Revises: 8af27c21d10c
5 +Create Date: 2024-07-17 14:28:04.828930
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 = "7187bab836ab"
17 +down_revision: Union[str, None] = "8af27c21d10c"
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_alerttitlefieldname",
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_alerttitlefieldname")
37 + # ### end Alembic commands ###
backend/alembic/versions/7f846a07c2b8_add_sigma_queries_table.py new
+42
@@ -0,0 +1,42 @@
1 +"""Add sigma queries table
2 +
3 +Revision ID: 7f846a07c2b8
4 +Revises: 7187bab836ab
5 +Create Date: 2024-07-31 11:20:41.741602
6 +
7 +"""
8 +from typing import Sequence
9 +from typing import Union
10 +
11 +import sqlalchemy as sa
12 +from sqlalchemy.dialects import mysql
13 +
14 +from alembic import op
15 +
16 +# revision identifiers, used by Alembic.
17 +revision: str = "7f846a07c2b8"
18 +down_revision: Union[str, None] = "7187bab836ab"
19 +branch_labels: Union[str, Sequence[str], None] = None
20 +depends_on: Union[str, Sequence[str], None] = None
21 +
22 +
23 +def upgrade() -> None:
24 + # ### commands auto generated by Alembic - please adjust! ###
25 + op.create_table(
26 + "sigma_queries",
27 + sa.Column("id", sa.Integer(), nullable=False),
28 + sa.Column("rule_name", mysql.TEXT(length=1000000), nullable=False),
29 + sa.Column("rule_query", mysql.TEXT(length=1000000), nullable=False),
30 + sa.Column("active", sa.Boolean(), nullable=False),
31 + sa.Column("time_interval", sa.String(length=50), nullable=False),
32 + sa.Column("last_updated", sa.DateTime(), nullable=False),
33 + sa.Column("last_execution_time", sa.DateTime(), nullable=True),
34 + sa.PrimaryKeyConstraint("id"),
35 + )
36 + # ### end Alembic commands ###
37 +
38 +
39 +def downgrade() -> None:
40 + # ### commands auto generated by Alembic - please adjust! ###
41 + op.drop_table("sigma_queries")
42 + # ### end Alembic commands ###
backend/alembic/versions/8af27c21d10c_add_timefield_field_name_table.py new
+37
@@ -0,0 +1,37 @@
1 +"""Add timefield field name table
2 +
3 +Revision ID: 8af27c21d10c
4 +Revises: e90f3760fa40
5 +Create Date: 2024-07-15 18:44:01.436938
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 = "8af27c21d10c"
17 +down_revision: Union[str, None] = "e90f3760fa40"
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_timestampfieldname",
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_timestampfieldname")
37 + # ### end Alembic commands ###
backend/alembic/versions/aae5e3008a35_add_incident_management_notifications_.py new
+38
@@ -0,0 +1,38 @@
1 +"""Add incident management notifications table
2 +
3 +Revision ID: aae5e3008a35
4 +Revises: 7f846a07c2b8
5 +Create Date: 2024-08-06 11:09:40.763589
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 = "aae5e3008a35"
17 +down_revision: Union[str, None] = "7f846a07c2b8"
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_notification",
26 + sa.Column("id", sa.Integer(), nullable=False),
27 + sa.Column("customer_code", sa.String(length=50), nullable=False),
28 + sa.Column("shuffle_workflow_id", sa.String(length=1000), nullable=False),
29 + sa.Column("enabled", sa.Boolean(), nullable=False),
30 + sa.PrimaryKeyConstraint("id"),
31 + )
32 + # ### end Alembic commands ###
33 +
34 +
35 +def downgrade() -> None:
36 + # ### commands auto generated by Alembic - please adjust! ###
37 + op.drop_table("incident_management_notification")
38 + # ### end Alembic commands ###
backend/alembic/versions/ba98bd0e3967_add_alert_tags.py new
+51
@@ -0,0 +1,51 @@
1 +"""Add alert tags
2 +
3 +Revision ID: ba98bd0e3967
4 +Revises: 1501739c6898
5 +Create Date: 2024-07-15 11:31:37.990149
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 = "ba98bd0e3967"
17 +down_revision: Union[str, None] = "1501739c6898"
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_alerttag",
26 + sa.Column("id", sa.Integer(), nullable=False),
27 + sa.Column("tag", sa.String(length=50), nullable=False),
28 + sa.PrimaryKeyConstraint("id"),
29 + )
30 + op.create_table(
31 + "incident_management_alert_to_tag",
32 + sa.Column("alert_id", sa.Integer(), nullable=False),
33 + sa.Column("tag_id", sa.Integer(), nullable=False),
34 + sa.ForeignKeyConstraint(
35 + ["alert_id"],
36 + ["incident_management_alert.id"],
37 + ),
38 + sa.ForeignKeyConstraint(
39 + ["tag_id"],
40 + ["incident_management_alerttag.id"],
41 + ),
42 + sa.PrimaryKeyConstraint("alert_id", "tag_id"),
43 + )
44 + # ### end Alembic commands ###
45 +
46 +
47 +def downgrade() -> None:
48 + # ### commands auto generated by Alembic - please adjust! ###
49 + op.drop_table("incident_management_alert_to_tag")
50 + op.drop_table("incident_management_alerttag")
51 + # ### end Alembic commands ###
backend/alembic/versions/e18dc3169b33_add_incident_management_customer_code_.py new
+37
@@ -0,0 +1,37 @@
1 +"""Add incident management customer code table
2 +
3 +Revision ID: e18dc3169b33
4 +Revises: aae5e3008a35
5 +Create Date: 2024-08-15 08:04:02.006222
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 = "e18dc3169b33"
17 +down_revision: Union[str, None] = "aae5e3008a35"
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_customercodefieldname",
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_customercodefieldname")
37 + # ### end Alembic commands ###
backend/alembic/versions/e90f3760fa40_add_assigned_to_and_case_meta.py new
+37
@@ -0,0 +1,37 @@
1 +"""Add assigned to and case meta
2 +
3 +Revision ID: e90f3760fa40
4 +Revises: ba98bd0e3967
5 +Create Date: 2024-07-15 18:07:45.012570
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 = "e90f3760fa40"
17 +down_revision: Union[str, None] = "ba98bd0e3967"
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.add_column("incident_management_alert", sa.Column("assigned_to", sa.String(length=50), nullable=True))
25 + op.add_column("incident_management_case", sa.Column("case_creation_time", sa.DateTime(), nullable=False))
26 + op.add_column("incident_management_case", sa.Column("case_status", sa.String(length=50), nullable=False))
27 + op.add_column("incident_management_case", sa.Column("assigned_to", sa.String(length=50), nullable=True))
28 + # ### end Alembic commands ###
29 +
30 +
31 +def downgrade() -> None:
32 + # ### commands auto generated by Alembic - please adjust! ###
33 + op.drop_column("incident_management_case", "assigned_to")
34 + op.drop_column("incident_management_case", "case_status")
35 + op.drop_column("incident_management_case", "case_creation_time")
36 + op.drop_column("incident_management_alert", "assigned_to")
37 + # ### end Alembic commands ###
backend/app/active_response/scripts/windows/windows_firewall_multiple_failures.py new
+141
@@ -0,0 +1,141 @@
1 +#!/usr/bin/python3
2 +# Copyright (C) 2024, SOCFortress LLP.
3 +# All rights reserved.
4 +
5 +# This program is free software; you can redistribute it
6 +# and/or modify it under the terms of the GNU General Public
7 +# License (version 2) as published by the FSF - Free Software
8 +# Foundation.
9 +
10 +import datetime
11 +import ipaddress
12 +import json
13 +import os
14 +import subprocess
15 +import sys
16 +
17 +LOG_FILE = (
18 + "C:\\Program Files (x86)\\ossec-agent\\active-response\\active-responses.log"
19 + if os.name == "nt"
20 + else "/var/ossec/logs/active-responses.log"
21 +)
22 +
23 +COMMANDS = {"add": 0, "delete": 1, "continue": 2, "abort": 3}
24 +
25 +OS_SUCCESS = 0
26 +OS_INVALID = -1
27 +
28 +
29 +class Message:
30 + def __init__(self, alert="", command=0):
31 + self.alert = alert
32 + self.command = command
33 +
34 +
35 +def write_debug_file(ar_name, msg):
36 + """Writes a debug message to the log file."""
37 + with open(LOG_FILE, mode="a") as log_file:
38 + log_msg = {
39 + "timestamp": datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"),
40 + "active_response": "windows_firewall",
41 + "message": json.loads(msg) if isinstance(msg, str) and msg.strip().startswith("{") else msg,
42 + }
43 + log_file.write(json.dumps(log_msg) + "\n")
44 +
45 +
46 +def setup_and_check_message(argv):
47 + """Reads and validates the input message."""
48 + input_str = next(sys.stdin, "")
49 + write_debug_file(argv[0], input_str)
50 + try:
51 + data = json.loads(input_str)
52 + except ValueError:
53 + write_debug_file(argv[0], "Decoding JSON has failed, invalid input format")
54 + return Message(command=OS_INVALID)
55 + command = COMMANDS.get(data.get("command"), OS_INVALID)
56 + if command == OS_INVALID:
57 + write_debug_file(argv[0], "Not valid command: " + data.get("command"))
58 + return Message(alert=data, command=command)
59 +
60 +
61 +def is_valid_ipv4(ip):
62 + """Checks if an IP address is valid and not private."""
63 + try:
64 + ip_obj = ipaddress.IPv4Address(ip)
65 + return ip_obj.is_global
66 + except ipaddress.AddressValueError:
67 + return False
68 +
69 +
70 +def block_ip(ip):
71 + """Blocks an IP address on the Windows Firewall."""
72 + try:
73 + subprocess.run(
74 + [
75 + r"C:\Windows\System32\netsh",
76 + "advfirewall",
77 + "firewall",
78 + "add",
79 + "rule",
80 + f"name=SOCFortress Block Inbound {ip}",
81 + "dir=in",
82 + "action=block",
83 + f"remoteip={ip}",
84 + ],
85 + check=True,
86 + )
87 + return f"Blocked IP {ip} on Windows Firewall"
88 + except subprocess.CalledProcessError as e:
89 + return f"Failed to block IP {ip} on Windows Firewall: {e}"
90 +
91 +
92 +def remove_ip(ip):
93 + """Removes a blocked IP address from the Windows Firewall."""
94 + try:
95 + subprocess.run(
96 + [r"C:\Windows\System32\netsh", "advfirewall", "firewall", "delete", "rule", f"name=SOCFortress Block Inbound {ip}"],
97 + check=True,
98 + )
99 + return f"Removed blocked IP {ip} from Windows Firewall"
100 + except subprocess.CalledProcessError as e:
101 + return f"Failed to remove blocked IP {ip} from Windows Firewall: {e}"
102 +
103 +
104 +def extract_alert_info(msg, argv):
105 + """Extracts the action and IP from the alert message."""
106 + try:
107 + alert = msg.alert["parameters"]["alert"]
108 + action = "block"
109 + """Extract the IP"""
110 + ip = alert["data"]["win"]["eventdata"]["ipAddress"]
111 + except KeyError as e:
112 + write_debug_file(argv[0], f"Missing key in alert message: {str(e)}")
113 + sys.exit(OS_INVALID)
114 + return action, ip
115 +
116 +
117 +def main(argv):
118 + write_debug_file(argv[0], {"status": "Started"})
119 + msg = setup_and_check_message(argv)
120 + if msg.command < 0:
121 + sys.exit(OS_INVALID)
122 + if msg.command == COMMANDS["add"]:
123 + action, ip = extract_alert_info(msg, argv)
124 + if not is_valid_ipv4(ip):
125 + write_debug_file(argv[0], {"status": "failed", "message": f"Invalid IP address {ip}"})
126 + sys.exit(OS_INVALID)
127 + if action == "block":
128 + write_debug_file(argv[0], block_ip(ip))
129 + if action == "unblock":
130 + write_debug_file(argv[0], remove_ip(ip))
131 + elif msg.command == COMMANDS["delete"]:
132 + # Optionally, include logic here to remove the firewall rule if necessary
133 + pass
134 + else:
135 + write_debug_file(argv[0], "Invalid command")
136 + write_debug_file(argv[0], "Ended")
137 + sys.exit(OS_SUCCESS)
138 +
139 +
140 +if __name__ == "__main__":
141 + main(sys.argv)
backend/app/connectors/grafana/dashboards/Crowdstrike/summary.json
+3 -3
@@ -1061,7 +1061,7 @@
1061 {
1062 "targetBlank": true,
1063 "title": "VIEW EVENT DETAILS",
1064 - "url": "https://paschoalotto.soc.gemina.io/explore?left=%5B%22now-6h%22,%22now%22,%22CROWDSTRIKE%22,%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"
1064 + "url": "https://grafana.company.local/explore?left=%5B%22now-6h%22,%22now%22,%22CROWDSTRIKE%22,%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"
1065 }
1066 ]
1067 }
@@ -2063,7 +2063,7 @@
2063 {
2064 "targetBlank": true,
2065 "title": "VIEW EVENT DETAILS",
2066 - "url": "https://paschoalotto.soc.gemina.io/explore?left=%5B%22now-6h%22,%22now%22,%22CROWDSTRIKE%22,%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"
2066 + "url": "https://grafana.company.local/explore?left=%5B%22now-6h%22,%22now%22,%22CROWDSTRIKE%22,%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"
2067 }
2068 ]
2069 }
@@ -3019,7 +3019,7 @@
3019 {
3020 "targetBlank": true,
3021 "title": "VIEW EVENT DETAILS",
3022 - "url": "https://paschoalotto.soc.gemina.io/explore?left=%5B%22now-6h%22,%22now%22,%22CROWDSTRIKE%22,%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"
3022 + "url": "https://grafana.company.local/explore?left=%5B%22now-6h%22,%22now%22,%22CROWDSTRIKE%22,%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"
3023 }
3024 ]
3025 },
backend/app/connectors/graylog/routes/receiver.py new
+36
@@ -0,0 +1,36 @@
1 +from fastapi import APIRouter
2 +from loguru import logger
3 +
4 +from app.integrations.utils.event_shipper import event_shipper
5 +from app.integrations.utils.schema import EventShipperPayload
6 +
7 +# App specific imports
8 +
9 +
10 +graylog_receiver_router = APIRouter()
11 +
12 +
13 +################## ! GRAYLOG FORWARDER ! ##################
14 +# ! This function is meant to receive a JSON payload from Shuffle (or another tool)
15 +# ! and forward it to a Graylog server.
16 +# ! Payload must include `customer_code` and `integration` fields.
17 +@graylog_receiver_router.post(
18 + "/receiver",
19 + description="Forward a message to Graylog",
20 +)
21 +async def forward_to_graylog(payload: dict) -> dict:
22 + """
23 + Forward a message to Graylog.
24 +
25 + This endpoint forwards a message to Graylog.
26 +
27 + Returns:
28 + ConfiguredInputsResponse: The response containing the configured inputs.
29 + """
30 + logger.info("Forwarding message to Graylog")
31 + logger.info(f"Payload: {payload}")
32 + message = EventShipperPayload(
33 + **payload,
34 + )
35 + await event_shipper(message)
36 + return {"message": "Message forwarded to Graylog", "success": True}
backend/app/connectors/graylog/services/collector.py
+39 -2
@@ -14,6 +14,38 @@ from app.connectors.graylog.schema.collector import RunningInputsResponse
14 from app.connectors.graylog.schema.management import UrlWhitelistEntryResponse
15 from app.connectors.graylog.utils.universal import send_get_request
16
17 +# async def get_indices_full() -> GraylogIndicesResponse:
18 +# """Get indices from Graylog.
19 +
20 +# Returns:
21 +# GraylogIndicesResponse: The response object containing the collected indices.
22 +
23 +# Raises:
24 +# HTTPException: If there is an error collecting the indices.
25 +# """
26 +# logger.info("Getting indices from Graylog")
27 +# indices_collected = await send_get_request(endpoint="/api/system/indexer/indices")
28 +# if indices_collected["success"]:
29 +# try:
30 +# indices_data = indices_collected["data"]["all"]["indices"]
31 +# except KeyError:
32 +# raise HTTPException(status_code=500, detail="Failed to collect indices key")
33 +
34 +# # Convert the dictionary to a list of GraylogIndexItem
35 +# indices_list = [GraylogIndexItem(index_name=name, index_info=info) for name, info in indices_data.items()]
36 +
37 +# return GraylogIndicesResponse(
38 +# indices=indices_list,
39 +# success=True,
40 +# message="Indices collected successfully",
41 +# )
42 +# else:
43 +# return GraylogIndicesResponse(
44 +# indices=[],
45 +# success=False,
46 +# message="Failed to collect indices",
47 +# )
48 +
49
50 async def get_indices_full() -> GraylogIndicesResponse:
51 """Get indices from Graylog.
@@ -32,8 +64,13 @@ async def get_indices_full() -> GraylogIndicesResponse:
64 except KeyError:
65 raise HTTPException(status_code=500, detail="Failed to collect indices key")
66
35 - # Convert the dictionary to a list of GraylogIndexItem
36 - indices_list = [GraylogIndexItem(index_name=name, index_info=info) for name, info in indices_data.items()]
67 + # Check if indices_data is a dictionary or a list and process accordingly
68 + if isinstance(indices_data, dict):
69 + indices_list = [GraylogIndexItem(index_name=name, index_info=info) for name, info in indices_data.items()]
70 + elif isinstance(indices_data, list):
71 + indices_list = [GraylogIndexItem(index_name=index["index_name"], index_info=index) for index in indices_data]
72 + else:
73 + raise HTTPException(status_code=500, detail="Unexpected format for indices data")
74
75 return GraylogIndicesResponse(
76 indices=indices_list,
backend/app/connectors/shuffle/routes/integrations.py
+21
@@ -3,8 +3,10 @@ from fastapi import Security
3 from loguru import logger
4
5 from app.auth.utils import AuthHandler
6 +from app.connectors.shuffle.schema.integrations import ExecuteWorkflowRequest
7 from app.connectors.shuffle.schema.integrations import IntegrationRequest
8 from app.connectors.shuffle.services.integrations import execute_integration
9 +from app.connectors.shuffle.services.integrations import execute_workflow
10
11 shuffle_integrations_router = APIRouter()
12
@@ -26,3 +28,22 @@ async def execute_integration_route(request: IntegrationRequest):
28 """
29 logger.info("Executing workflow")
30 return await execute_integration(request)
31 +
32 +
33 +@shuffle_integrations_router.post(
34 + "/invoke-workflow",
35 + description="Invoke a Shuffle Workflow.",
36 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
37 +)
38 +async def invoke_workflow_route(request: ExecuteWorkflowRequest):
39 + """
40 + Execute a workflow.
41 +
42 + Args:
43 + request (IntegrationRequest): The request object containing the workflow ID.
44 +
45 + Returns:
46 + dict: The response containing the execution ID.
47 + """
48 + logger.info("Executing workflow")
49 + return await execute_workflow(request)
backend/app/connectors/shuffle/schema/integrations.py
+19
@@ -3,8 +3,10 @@ from typing import Dict
3 from typing import List
4 from typing import Optional
5
6 +from fastapi import HTTPException
7 from pydantic import BaseModel
8 from pydantic import Field
9 +from pydantic import root_validator
10
11
12 class IntegrationRequest(BaseModel):
@@ -25,3 +27,20 @@ class IntegrationRequest(BaseModel):
27 description="Skip the workflow",
28 example=True,
29 )
30 +
31 +
32 +class ExecuteWorkflowRequest(BaseModel):
33 + workflow_id: str = Field(..., description="The ID of the workflow", example="workflow_id")
34 + execution_arguments: Optional[Dict[str, Any]] = Field(
35 + {},
36 + description="The execution arguments",
37 + example={"key": "value"},
38 + )
39 + start: str = Field("", description="The start of the workflow", example="start")
40 +
41 + @root_validator
42 + def check_customer_code(cls, values):
43 + execution_arguments = values.get("execution_arguments", {})
44 + if "customer_code" not in execution_arguments or not execution_arguments["customer_code"]:
45 + raise HTTPException(status_code=400, detail="customer_code is required")
46 + return values
backend/app/connectors/shuffle/services/integrations.py
+17
@@ -1,5 +1,6 @@
1 from loguru import logger
2
3 +from app.connectors.shuffle.schema.integrations import ExecuteWorkflowRequest
4 from app.connectors.shuffle.schema.integrations import IntegrationRequest
5 from app.connectors.shuffle.utils.universal import send_post_request
6
@@ -18,3 +19,19 @@ async def execute_integration(request: IntegrationRequest) -> dict:
19 response = await send_post_request("/api/v1/apps/categories/run", request.dict())
20 logger.info(f"Response: {response}")
21 return response
22 +
23 +
24 +async def execute_workflow(request: ExecuteWorkflowRequest) -> dict:
25 + """
26 + Execute a workflow.
27 +
28 + Args:
29 + request (IntegrationRequest): The request object containing the workflow ID.
30 +
31 + Returns:
32 + dict: The response containing the execution ID.
33 + """
34 + logger.info(f"Executing workflow: {request}")
35 + response = await send_post_request(f"/api/v1/workflows/{request.workflow_id}/execute", request.dict())
36 + logger.info(f"Response: {response}")
37 + return response
backend/app/connectors/velociraptor/routes/artifacts.py
+40
@@ -14,6 +14,7 @@ from app.connectors.velociraptor.schema.artifacts import ArtifactReccomendationR
14 from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
15 from app.connectors.velociraptor.schema.artifacts import CollectArtifactBody
16 from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
17 +from app.connectors.velociraptor.schema.artifacts import CollectFileBody
18 from app.connectors.velociraptor.schema.artifacts import OSPrefixEnum
19 from app.connectors.velociraptor.schema.artifacts import OSPrefixModel
20 from app.connectors.velociraptor.schema.artifacts import QuarantineBody
@@ -24,6 +25,7 @@ from app.connectors.velociraptor.services.artifacts import get_artifacts
25 from app.connectors.velociraptor.services.artifacts import post_to_copilot_ai_module
26 from app.connectors.velociraptor.services.artifacts import quarantine_host
27 from app.connectors.velociraptor.services.artifacts import run_artifact_collection
28 +from app.connectors.velociraptor.services.artifacts import run_file_collection
29 from app.connectors.velociraptor.services.artifacts import run_remote_command
30 from app.db.db_session import get_db
31 from app.db.universal_models import Agents
@@ -476,3 +478,41 @@ async def get_artifact_recommendation(request: ArtifactReccomendationAIRequest):
478 prompt=request.prompt,
479 ),
480 )
481 +
482 +
483 +# ! WIP ! #
484 +@velociraptor_artifacts_router.post(
485 + "/collect/file",
486 + response_model=CollectArtifactResponse,
487 + description="Run an the artifact to collect a file",
488 +)
489 +async def collect_file(collect_artifact_body: CollectFileBody, session: AsyncSession = Depends(get_db)):
490 + """
491 + Collects a file based on the artifact.
492 +
493 + Returns:
494 + CollectArtifactResponse: The response containing the collected file.
495 + """
496 + logger.info(f"Received request to collect artifact {collect_artifact_body}")
497 + # result = await get_all_artifacts_for_hostname(
498 + # collect_artifact_body.hostname,
499 + # session,
500 + # )
501 + # artifact_names = [artifact.name for artifact in result.artifacts]
502 +
503 + # if collect_artifact_body.artifact_name not in artifact_names:
504 + # raise HTTPException(
505 + # status_code=400,
506 + # detail=f"Artifact name {collect_artifact_body.artifact_name} does not apply for hostname {collect_artifact_body.hostname} or does not exist",
507 + # )
508 +
509 + # collect_artifact_body.velociraptor_id = await get_velociraptor_id(
510 + # session,
511 + # collect_artifact_body.hostname,
512 + # )
513 +
514 + # collect_artifact_body.velociraptor_org = await get_velociraptor_org(
515 + # session,
516 + # collect_artifact_body.hostname,
517 + # )
518 + return await run_file_collection(collect_artifact_body)
backend/app/connectors/velociraptor/schema/artifacts.py
+17
@@ -4,8 +4,10 @@ from typing import Dict
4 from typing import List
5 from typing import Optional
6
7 +from fastapi import HTTPException
8 from pydantic import BaseModel
9 from pydantic import Field
10 +from pydantic import validator
11
12
13 class Artifacts(BaseModel):
@@ -83,6 +85,21 @@ class CollectArtifactBody(BaseBody):
85 )
86
87
88 +class CollectFileBody(BaseBody):
89 + artifact_name: str = Field(
90 + "Generic.Collectors.File",
91 + description="Name of the artifact for collection or command running",
92 + )
93 + file: str = Field("Glob\nUsers\\Administrator\\Documents\\*\n", description="File to collect")
94 + root_disk: Optional[str] = Field("C:", description="Root disk to collect from")
95 +
96 + @validator("artifact_name")
97 + def validate_artifact_name(cls, value):
98 + if value != "Generic.Collectors.File":
99 + raise HTTPException(status_code=400, detail="Invalid artifact name. Name should be 'Generic.Collectors.File'")
100 + return value
101 +
102 +
103 class RunCommandBody(BaseBody):
104 command: Optional[str] = Field(None, description="Command to run")
105 artifact_name: CommandArtifactsEnum = Field(
backend/app/connectors/velociraptor/services/artifacts.py
+75
@@ -8,6 +8,7 @@ from app.connectors.velociraptor.schema.artifacts import Artifacts
8 from app.connectors.velociraptor.schema.artifacts import ArtifactsResponse
9 from app.connectors.velociraptor.schema.artifacts import CollectArtifactBody
10 from app.connectors.velociraptor.schema.artifacts import CollectArtifactResponse
11 +from app.connectors.velociraptor.schema.artifacts import CollectFileBody
12 from app.connectors.velociraptor.schema.artifacts import QuarantineBody
13 from app.connectors.velociraptor.schema.artifacts import QuarantineResponse
14 from app.connectors.velociraptor.schema.artifacts import RunCommandBody
@@ -165,6 +166,80 @@ async def run_artifact_collection(
166 )
167
168
169 +async def run_file_collection(
170 + collect_artifact_body: CollectFileBody,
171 +) -> CollectArtifactResponse:
172 + """
173 + Run an artifact collection on a client.
174 +
175 + Args:
176 + run_analyzer_body (RunAnalyzerBody): The body of the request.
177 +
178 + Returns:
179 + RunAnalyzerResponse: A dictionary containing the success status and a message.
180 + """
181 + velociraptor_service = await UniversalService.create("Velociraptor")
182 + try:
183 + # ! Can specify org_id with org_id='OL680' ! #
184 + query = create_query(
185 + (
186 + f"SELECT collect_client("
187 + f"org_id='{collect_artifact_body.velociraptor_org}', "
188 + f"client_id='{collect_artifact_body.velociraptor_id}', "
189 + f"artifacts=['{collect_artifact_body.artifact_name}'], "
190 + f"specs=[{{"
191 + f" 'parameters': {{"
192 + f" 'env': ["
193 + f" {{'key': 'collectionSpec', 'value': 'Glob\\n{collect_artifact_body.file}\\n'}},"
194 + f" {{'key': 'Root', 'value': '{collect_artifact_body.root_disk}'}}"
195 + f" ]"
196 + f" }}"
197 + f"}}]"
198 + f") "
199 + f"FROM scope()"
200 + ),
201 + )
202 + logger.info(f"Query: {query}")
203 + flow = velociraptor_service.execute_query(query, org_id=collect_artifact_body.velociraptor_org)
204 + logger.info(f"Successfully ran artifact collection on {flow}")
205 +
206 + artifact_key = get_artifact_key(analyzer_body=collect_artifact_body)
207 +
208 + flow_id = flow["results"][0][artifact_key]["flow_id"]
209 + logger.info(f"Extracted flow_id: {flow_id}")
210 +
211 + completed = velociraptor_service.watch_flow_completion(flow_id, org_id=collect_artifact_body.velociraptor_org)
212 + logger.info(f"Successfully watched flow completion on {completed}")
213 +
214 + results = velociraptor_service.read_collection_results(
215 + client_id=collect_artifact_body.velociraptor_id,
216 + flow_id=flow_id,
217 + org_id=collect_artifact_body.velociraptor_org,
218 + artifact=collect_artifact_body.artifact_name,
219 + )
220 +
221 + logger.info(f"Successfully read collection results on {results}")
222 +
223 + return CollectArtifactResponse(
224 + success=results["success"],
225 + message=results["message"],
226 + results=results["results"],
227 + )
228 + except HTTPException as he: # Catch HTTPException separately to propagate the original message
229 + logger.error(
230 + f"HTTPException while running artifact collection on {collect_artifact_body}: {he.detail}",
231 + )
232 + raise he
233 + except Exception as err:
234 + logger.error(
235 + f"Failed to run artifact collection on {collect_artifact_body}: {err}",
236 + )
237 + raise HTTPException(
238 + status_code=500,
239 + detail=f"Failed to run artifact collection on {collect_artifact_body}: {err}",
240 + )
241 +
242 +
243 async def run_remote_command(run_command_body: RunCommandBody) -> RunCommandResponse:
244 """
245 Run a remote command on a client.
backend/app/connectors/wazuh_indexer/models/sigma.py new
+17
@@ -0,0 +1,17 @@
1 +from datetime import datetime
2 +from typing import Optional
3 +
4 +from sqlmodel import Field
5 +from sqlmodel import SQLModel
6 +from sqlmodel import Text
7 +
8 +
9 +class SigmaQuery(SQLModel, table=True):
10 + __tablename__ = "sigma_queries"
11 + id: Optional[int] = Field(default=None, primary_key=True)
12 + rule_name: str = Field(sa_column=Text, nullable=False)
13 + rule_query: str = Field(sa_column=Text, nullable=False)
14 + active: bool = Field(default=False)
15 + time_interval: str = Field(default="5m", nullable=False)
16 + last_updated: datetime = Field(default_factory=datetime.utcnow)
17 + last_execution_time: Optional[datetime] = Field(default_factory=datetime.utcnow)
backend/app/connectors/wazuh_indexer/routes/alerts.py
+15
@@ -12,6 +12,7 @@ from app.connectors.wazuh_indexer.schema.alerts import AlertsByRulePerHostRespon
12 from app.connectors.wazuh_indexer.schema.alerts import AlertsByRuleResponse
13 from app.connectors.wazuh_indexer.schema.alerts import AlertsSearchBody
14 from app.connectors.wazuh_indexer.schema.alerts import AlertsSearchResponse
15 +from app.connectors.wazuh_indexer.schema.alerts import GraylogAlertsSearchBody
16 from app.connectors.wazuh_indexer.schema.alerts import HostAlertsSearchBody
17 from app.connectors.wazuh_indexer.schema.alerts import HostAlertsSearchResponse
18 from app.connectors.wazuh_indexer.schema.alerts import IndexAlertsSearchBody
@@ -20,6 +21,7 @@ from app.connectors.wazuh_indexer.services.alerts import get_alerts
21 from app.connectors.wazuh_indexer.services.alerts import get_alerts_by_host
22 from app.connectors.wazuh_indexer.services.alerts import get_alerts_by_rule
23 from app.connectors.wazuh_indexer.services.alerts import get_alerts_by_rule_per_host
24 +from app.connectors.wazuh_indexer.services.alerts import get_graylog_alerts
25 from app.connectors.wazuh_indexer.services.alerts import get_host_alerts
26 from app.connectors.wazuh_indexer.services.alerts import get_index_alerts
27 from app.connectors.wazuh_indexer.utils.universal import collect_indices
@@ -196,3 +198,16 @@ async def get_all_alerts_by_rule_per_host(
198 """
199 logger.info("Fetching number of all alerts for all rules per host")
200 return await get_alerts_by_rule_per_host(alerts_search_body)
201 +
202 +
203 +@wazuh_indexer_alerts_router.post(
204 + "/alerts/graylog",
205 + response_model=AlertsSearchResponse,
206 + description="Get alerts that are configured Via Graylog",
207 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
208 +)
209 +async def get_alerts_not_created_in_copilot(request: GraylogAlertsSearchBody) -> AlertsSearchResponse:
210 + """
211 + Get the Graylog event indices. Then get all the results from the list of indices, where `copilot_alert_id` does not exist.
212 + """
213 + return AlertsSearchResponse(success=True, message="Alerts retrieved", alerts_summary=await get_graylog_alerts(request))
backend/app/connectors/wazuh_indexer/routes/monitoring.py
+20
@@ -18,6 +18,7 @@ from app.connectors.wazuh_indexer.services.monitoring import (
18 output_shard_number_to_be_set_based_on_nodes,
19 )
20 from app.connectors.wazuh_indexer.services.monitoring import shards
21 +from app.connectors.wazuh_indexer.utils.universal import resize_wazuh_index_fields
22
23 wazuh_indexer_router = APIRouter()
24
@@ -147,3 +148,22 @@ async def get_output_shard_number_to_be_set_based_on_nodes_route() -> int:
148 return output_shard_number_to_be_set_based_on_nodes_response
149 else:
150 raise HTTPException(status_code=500, detail="Failed to retrieve output_shard_number_to_be_set_based_on_nodes.")
151 +
152 +
153 +@wazuh_indexer_router.get(
154 + "/resize_wazuh_index_fields",
155 + description="Resize Wazuh Index fields",
156 +)
157 +async def resize_wazuh_index_fields_route():
158 + """
159 + Resize Wazuh Index fields.
160 +
161 + This endpoint resizes the Wazuh Index fields.
162 +
163 + Returns:
164 + str: A string representing the resize Wazuh Index fields response.
165 +
166 + Raises:
167 + HTTPException: An exception with a 500 status code is raised if the Wazuh Index fields cannot be resized.
168 + """
169 + return await resize_wazuh_index_fields()
backend/app/connectors/wazuh_indexer/routes/sigma.py new
+488
@@ -0,0 +1,488 @@
1 +import os
2 +from datetime import datetime
3 +
4 +from fastapi import APIRouter
5 +from fastapi import Depends
6 +from fastapi import File
7 +from fastapi import HTTPException
8 +from fastapi import Query
9 +from fastapi import UploadFile
10 +from loguru import logger
11 +from sqlalchemy.ext.asyncio import AsyncSession
12 +
13 +from app.connectors.wazuh_indexer.schema.sigma import ActivateSigmaQueryResponse
14 +from app.connectors.wazuh_indexer.schema.sigma import BulkUploadToDBResponse
15 +from app.connectors.wazuh_indexer.schema.sigma import CreateSigmaQuery
16 +from app.connectors.wazuh_indexer.schema.sigma import DeactivateSigmaQueryResponse
17 +from app.connectors.wazuh_indexer.schema.sigma import DeleteSigmaQueryResponse
18 +from app.connectors.wazuh_indexer.schema.sigma import DownloadSigmaRulesRequest
19 +from app.connectors.wazuh_indexer.schema.sigma import RunActiveSigmaQueries
20 +from app.connectors.wazuh_indexer.schema.sigma import SigmaQueryOutResponse
21 +from app.connectors.wazuh_indexer.schema.sigma import SigmaRuleUploadRequest
22 +from app.connectors.wazuh_indexer.schema.sigma import UpdateSigmaActive
23 +from app.connectors.wazuh_indexer.schema.sigma import UpdateSigmaTimeInterval
24 +from app.connectors.wazuh_indexer.services.sigma.execute_query import execute_query
25 +from app.connectors.wazuh_indexer.services.sigma.sigma_db_operations import (
26 + add_sigma_queries_to_db,
27 +)
28 +from app.connectors.wazuh_indexer.services.sigma.sigma_db_operations import (
29 + create_sigma_query,
30 +)
31 +from app.connectors.wazuh_indexer.services.sigma.sigma_db_operations import (
32 + delete_sigma_rule,
33 +)
34 +from app.connectors.wazuh_indexer.services.sigma.sigma_db_operations import (
35 + get_sigma_query_by_id,
36 +)
37 +from app.connectors.wazuh_indexer.services.sigma.sigma_db_operations import (
38 + list_active_sigma_queries,
39 +)
40 +from app.connectors.wazuh_indexer.services.sigma.sigma_db_operations import (
41 + list_inactive_sigma_queries,
42 +)
43 +from app.connectors.wazuh_indexer.services.sigma.sigma_db_operations import (
44 + list_sigma_queries,
45 +)
46 +from app.connectors.wazuh_indexer.services.sigma.sigma_db_operations import (
47 + parse_time_interval,
48 +)
49 +from app.connectors.wazuh_indexer.services.sigma.sigma_db_operations import (
50 + process_sigma_file,
51 +)
52 +from app.connectors.wazuh_indexer.services.sigma.sigma_db_operations import (
53 + set_sigma_query_active,
54 +)
55 +from app.connectors.wazuh_indexer.services.sigma.sigma_db_operations import (
56 + update_sigma_time_interval,
57 +)
58 +from app.connectors.wazuh_indexer.services.sigma.sigma_download import (
59 + download_and_extract_zip,
60 +)
61 +from app.connectors.wazuh_indexer.services.sigma.sigma_download import (
62 + keep_only_folder_directory,
63 +)
64 +from app.db.db_session import get_db
65 +
66 +wazuh_indexer_sigma_router = APIRouter()
67 +
68 +
69 +def save_file(file: UploadFile, file_path: str):
70 + """
71 + Save the uploaded file to the specified path.
72 +
73 + Args:
74 + file (UploadFile): The uploaded file.
75 + file_path (str): The path to save the file.
76 + """
77 + try:
78 + with open(file_path, "wb") as f:
79 + f.write(file.file.read())
80 + logger.info(f"File saved to {file_path}")
81 + except Exception as e:
82 + logger.error(f"Error saving file: {e}")
83 + raise HTTPException(status_code=500, detail="An error occurred while saving the file.")
84 +
85 +
86 +def cleanup_file(file_path: str):
87 + """
88 + Remove the file from the specified path.
89 +
90 + Args:
91 + file_path (str): The path of the file to remove.
92 + """
93 + try:
94 + if os.path.exists(file_path):
95 + os.remove(file_path)
96 + logger.info(f"File {file_path} removed")
97 + except Exception as e:
98 + logger.error(f"Error removing file: {e}")
99 +
100 +
101 +@wazuh_indexer_sigma_router.get("/queries/available", response_model=SigmaQueryOutResponse)
102 +async def get_sigma_queries_endpoint(
103 + db: AsyncSession = Depends(get_db),
104 +):
105 + """
106 + Retrieves a list of Sigma queries.
107 +
108 + Args:
109 + db (AsyncSession): The database session.
110 +
111 + Returns:
112 + List[SigmaQuery]: A list of Sigma queries.
113 + """
114 + return SigmaQueryOutResponse(
115 + sigma_queries=await list_sigma_queries(db),
116 + success=True,
117 + message="Successfully retrieved the Sigma queries.",
118 + )
119 +
120 +
121 +@wazuh_indexer_sigma_router.get("/queries/active", response_model=SigmaQueryOutResponse)
122 +async def get_active_sigma_queries_endpoint(
123 + db: AsyncSession = Depends(get_db),
124 +):
125 + """
126 + Retrieves a list of active Sigma queries.
127 +
128 + Args:
129 + db (AsyncSession): The database session.
130 +
131 + Returns:
132 + List[SigmaQuery]: A list of Sigma queries.
133 + """
134 + return SigmaQueryOutResponse(
135 + sigma_queries=await list_active_sigma_queries(db),
136 + success=True,
137 + message="Successfully retrieved the active Sigma queries.",
138 + )
139 +
140 +
141 +@wazuh_indexer_sigma_router.get("/queries/inactive", response_model=SigmaQueryOutResponse)
142 +async def get_inactive_sigma_queries_endpoint(
143 + db: AsyncSession = Depends(get_db),
144 +):
145 + """
146 + Retrieves a list of inactive Sigma queries.
147 +
148 + Args:
149 + db (AsyncSession): The database session.
150 +
151 + Returns:
152 + List[SigmaQuery]: A list of Sigma queries.
153 + """
154 + return SigmaQueryOutResponse(
155 + sigma_queries=await list_inactive_sigma_queries(db),
156 + success=True,
157 + message="Successfully retrieved the inactive Sigma queries.",
158 + )
159 +
160 +
161 +@wazuh_indexer_sigma_router.post("/queries/create", response_model=SigmaQueryOutResponse)
162 +async def create_sigma_query_endpoint(
163 + sigma_query: CreateSigmaQuery,
164 + db: AsyncSession = Depends(get_db),
165 +):
166 + """
167 + Creates a Sigma query.
168 +
169 + Args:
170 + db (AsyncSession): The database session.
171 + sigma_query (CreateSigmaQuery): The Sigma query creation request.
172 +
173 + Returns:
174 + SigmaQuery: The created Sigma query.
175 + """
176 + logger.info(f"Creating Sigma query: {sigma_query.dict()}")
177 + return SigmaQueryOutResponse(
178 + sigma_queries=[await create_sigma_query(sigma_query, db)],
179 + success=True,
180 + message="Successfully created the Sigma query.",
181 + )
182 +
183 +
184 +@wazuh_indexer_sigma_router.post(
185 + "/download",
186 +)
187 +async def download_sigma_queries_endpoint(
188 + request: DownloadSigmaRulesRequest,
189 + db: AsyncSession = Depends(get_db),
190 +):
191 + """
192 + Downloads the Sigma queries from the Wazuh repository.
193 +
194 + Args:
195 + db (AsyncSession): The database session.
196 +
197 + Returns:
198 + SigmaQueryOutResponse: The Sigma queries response.
199 + """
200 + # Define the URL to download the Sigma queries from
201 + await download_and_extract_zip(request.url)
202 + await keep_only_folder_directory(folder=request.folder)
203 + return {"message": "Successfully downloaded the Sigma queries.", "success": True}
204 +
205 +
206 +@wazuh_indexer_sigma_router.post("/bulk-upload-to-db", response_model=BulkUploadToDBResponse)
207 +async def upload_sigma_queries_to_db_endpoint(
208 + request: SigmaRuleUploadRequest,
209 + db: AsyncSession = Depends(get_db),
210 +):
211 + """
212 + Uploads the Sigma queries to the database.
213 +
214 + Args:
215 + db (AsyncSession): The database session.
216 +
217 + Returns:
218 + SigmaQueryOutResponse: The Sigma queries response.
219 + """
220 + await add_sigma_queries_to_db(request, db)
221 + return BulkUploadToDBResponse(success=True, message="Successfully uploaded the Sigma queries to the database.")
222 +
223 +
224 +@wazuh_indexer_sigma_router.post("/activate-all-queries", response_model=ActivateSigmaQueryResponse)
225 +async def activate_all_sigma_queries_endpoint(
226 + db: AsyncSession = Depends(get_db),
227 +):
228 + """
229 + Activates all Sigma queries.
230 +
231 +
232 + Args:
233 + db (AsyncSession): The database session.
234 +
235 + Returns:
236 + ActivateSigmaQueryResponse: The Sigma queries response.
237 + """
238 + sigma_queries = await list_sigma_queries(db)
239 + enabled_queries = []
240 + for query in sigma_queries:
241 + await set_sigma_query_active(query.rule_name, True, db)
242 + enabled_queries.append(query.rule_name)
243 + return ActivateSigmaQueryResponse(
244 + success=True,
245 + message="Successfully activated all Sigma queries.",
246 + enabled_queries=enabled_queries,
247 + )
248 +
249 +
250 +@wazuh_indexer_sigma_router.post("/deactivate-all-queries", response_model=DeactivateSigmaQueryResponse)
251 +async def deactivate_all_sigma_queries_endpoint(
252 + db: AsyncSession = Depends(get_db),
253 +):
254 + """
255 + Deactivates all Sigma queries.
256 +
257 + Args:
258 + db (AsyncSession): The database session.
259 +
260 + Returns:
261 + DeactivateSigmaQueryResponse: The Sigma queries response.
262 + """
263 + sigma_queries = await list_sigma_queries(db)
264 + disabled_queries = []
265 + for query in sigma_queries:
266 + await set_sigma_query_active(query.rule_name, False, db)
267 + disabled_queries.append(query.rule_name)
268 + return DeactivateSigmaQueryResponse(
269 + success=True,
270 + message="Successfully deactivated all Sigma queries.",
271 + disabled_queries=disabled_queries,
272 + )
273 +
274 +
275 +@wazuh_indexer_sigma_router.post("/run-active-queries", response_model=SigmaQueryOutResponse)
276 +async def run_active_sigma_queries_endpoint(
277 + index_name: str = Query(default="wazuh*"),
278 + db: AsyncSession = Depends(get_db),
279 +):
280 + """
281 + Runs the active Sigma queries.
282 +
283 + Args:
284 + db (AsyncSession): The database session.
285 +
286 + Returns:
287 + SigmaQueryOutResponse: The Sigma queries response.
288 + """
289 + active_sigma_queries = await list_active_sigma_queries(db)
290 + for query in active_sigma_queries:
291 + time_interval_delta = parse_time_interval(query.time_interval)
292 + logger.info(f"Time interval delta: {time_interval_delta}")
293 + current_time = datetime.now()
294 + logger.info(f"Current time: {current_time}")
295 + logger.info(f"Last execution time: {query.last_execution_time}")
296 +
297 + # Check if the current time is less than the last execution time
298 + if current_time < query.last_execution_time or current_time - query.last_execution_time >= time_interval_delta:
299 + logger.info(f"Running Sigma query: {query.rule_name}")
300 + await execute_query(
301 + RunActiveSigmaQueries(
302 + query=query.rule_query,
303 + time_interval=query.time_interval,
304 + last_execution_time=query.last_execution_time,
305 + rule_name=query.rule_name,
306 + index=index_name,
307 + ),
308 + session=db,
309 + )
310 + # Update the last execution time to the current time and commit the changes
311 + # ! Remove commented out code after testing ! #
312 + query.last_execution_time = current_time
313 + await db.commit()
314 + else:
315 + time_comparison = current_time - query.last_execution_time
316 + logger.info(f"Time comparison: {time_comparison}")
317 + logger.info(f"Skipping Sigma query because the time interval has not passed: {query.rule_name}")
318 + return SigmaQueryOutResponse(
319 + success=True,
320 + message="Successfully ran the active Sigma queries.",
321 + )
322 +
323 +
324 +@wazuh_indexer_sigma_router.post("/run-single-query", response_model=SigmaQueryOutResponse)
325 +async def run_single_sigma_query_endpoint(
326 + index_name: str = Query(default="wazuh*"),
327 + rule_id: int = Query(...),
328 + db: AsyncSession = Depends(get_db),
329 +):
330 + """
331 + Runs a single Sigma query.
332 +
333 + Args:
334 + db (AsyncSession): The database session.
335 + rule_id (int): The rule ID to run.
336 +
337 + Returns:
338 + SigmaQueryOutResponse: The Sigma queries response.
339 + """
340 + query = await get_sigma_query_by_id(db=db, sigma_query_id=rule_id)
341 + time_interval_delta = parse_time_interval(query.time_interval)
342 + current_time = datetime.now()
343 + if current_time < query.last_execution_time or current_time - query.last_execution_time >= time_interval_delta:
344 + await execute_query(
345 + RunActiveSigmaQueries(
346 + query=query.rule_query,
347 + time_interval=query.time_interval,
348 + last_execution_time=query.last_execution_time,
349 + rule_name=query.rule_name,
350 + index=index_name,
351 + ),
352 + session=db,
353 + )
354 + query.last_execution_time = current_time
355 + await db.commit()
356 + else:
357 + time_comparison = current_time - query.last_execution_time
358 + logger.info(f"Time comparison: {time_comparison}")
359 + logger.info(f"Skipping Sigma query because the time interval has not passed: {query.rule_name}")
360 + return SigmaQueryOutResponse(
361 + success=True,
362 + message="Successfully ran the active Sigma queries.",
363 + )
364 +
365 +
366 +@wazuh_indexer_sigma_router.post("/upload")
367 +async def upload_sigma_queries_endpoint(
368 + file: UploadFile = File(...),
369 + db: AsyncSession = Depends(get_db),
370 +):
371 + """
372 + Uploads the Sigma queries to the database.
373 +
374 + Args:
375 + db (AsyncSession): The database session.
376 +
377 + Returns:
378 + SigmaQueryOutResponse: The Sigma queries response.
379 + """
380 + if not file.filename.endswith(".yml"):
381 + raise HTTPException(status_code=400, detail="File must be a YAML file.")
382 +
383 + file_path = f"app/connectors/wazuh_indexer/sigma_artifacts/{file.filename}"
384 +
385 + try:
386 + save_file(file, file_path)
387 + await process_sigma_file(file=file_path, db=db)
388 + except Exception as e:
389 + logger.error(f"Error processing Sigma file: {e}")
390 + raise HTTPException(status_code=500, detail="An error occurred while processing the file.")
391 + finally:
392 + cleanup_file(file_path)
393 +
394 + return {"message": "Successfully uploaded the Sigma queries to the database.", "success": True}
395 +
396 +
397 +@wazuh_indexer_sigma_router.put("/queries/set-active", response_model=SigmaQueryOutResponse)
398 +async def set_sigma_query_active_endpoint(
399 + request: UpdateSigmaActive,
400 + db: AsyncSession = Depends(get_db),
401 +):
402 + """
403 + Sets the active status of a Sigma query.
404 +
405 + Args:
406 + db (AsyncSession): The database session.
407 + rule_name (str): The rule name to set active.
408 + active (bool): The active status to set.
409 +
410 + Returns:
411 + SigmaQueryOutResponse: The Sigma queries response.
412 + """
413 + return SigmaQueryOutResponse(
414 + sigma_queries=[await set_sigma_query_active(request.rule_name, request.active, db)],
415 + success=True,
416 + message=f"Successfully set the active status of the Sigma query: {request.rule_name} to {request.active}",
417 + )
418 +
419 +
420 +@wazuh_indexer_sigma_router.put("/queries/set-time-interval", response_model=SigmaQueryOutResponse)
421 +async def set_sigma_query_time_interval_endpoint(
422 + request: UpdateSigmaTimeInterval,
423 + db: AsyncSession = Depends(get_db),
424 +):
425 + """
426 + Sets the time interval of a Sigma query.
427 +
428 + Args:
429 + db (AsyncSession): The database session.
430 + rule_name (str): The rule name to set active.
431 + time_interval (str): The time interval to set.
432 +
433 + Returns:
434 + SigmaQueryOutResponse: The Sigma queries response.
435 + """
436 + return SigmaQueryOutResponse(
437 + sigma_queries=[await update_sigma_time_interval(request.rule_name, request.time_interval, db)],
438 + success=True,
439 + message=f"Successfully set the time interval of the Sigma query: {request.rule_name} to {request.time_interval}",
440 + )
441 +
442 +
443 +@wazuh_indexer_sigma_router.delete("/queries/delete", response_model=DeleteSigmaQueryResponse)
444 +async def delete_sigma_rule_endpoint(
445 + rule_name: str = Query(...),
446 + db: AsyncSession = Depends(get_db),
447 +):
448 + """
449 + Deletes a Sigma query.
450 +
451 + Args:
452 + db (AsyncSession): The database session.
453 + rule_name (str): The rule name to delete.
454 +
455 + Returns:
456 + SigmaQueryOutResponse: The Sigma queries response.
457 + """
458 + await delete_sigma_rule(rule_name, db)
459 + return DeleteSigmaQueryResponse(
460 + deleted_queries=[rule_name],
461 + success=True,
462 + message=f"Successfully deleted the Sigma query: {rule_name}",
463 + )
464 +
465 +
466 +@wazuh_indexer_sigma_router.delete("/queries/delete-all", response_model=DeleteSigmaQueryResponse)
467 +async def delete_all_sigma_rules_endpoint(
468 + db: AsyncSession = Depends(get_db),
469 +):
470 + """
471 + Deletes all Sigma queries.
472 +
473 + Args:
474 + db (AsyncSession): The database session.
475 +
476 + Returns:
477 + DeleteSigmaQueryResponse: The Sigma queries response.
478 + """
479 + sigma_queries = await list_sigma_queries(db)
480 + deleted_queries = []
481 + for query in sigma_queries:
482 + await delete_sigma_rule(query.rule_name, db)
483 + deleted_queries.append(query.rule_name)
484 + return DeleteSigmaQueryResponse(
485 + deleted_queries=deleted_queries,
486 + success=True,
487 + message="Successfully deleted all Sigma queries.",
488 + )
backend/app/connectors/wazuh_indexer/schema/alerts.py
+33
@@ -119,8 +119,41 @@ class AlertsByRulePerHostResponse(BaseModel):
119 message: str
120
121
122 +class GraylogAlertsSearchBody(BaseModel):
123 + size: int = Field(10, description="The number of alerts to return.")
124 + timerange: str = Field("24h", description="The time range to search alerts in.")
125 + index_prefix: str = Field(
126 + "gl-events*",
127 + description="The index prefix to search alerts in.",
128 + )
129 +
130 + @validator("timerange")
131 + def validate_timerange(cls, value):
132 + if value[-1] not in ("h", "d", "w"):
133 + raise ValueError(
134 + "Invalid timerange format. The string should end with either 'h', 'd', 'w'.",
135 + )
136 +
137 + # Optionally, you can check that the prefix is a number
138 + if not value[:-1].isdigit():
139 + raise ValueError(
140 + "Invalid timerange format. The string should start with a number.",
141 + )
142 +
143 + return value
144 +
145 +
146 ############# ! PASSABLE MESSAGES FROM ES CLIENT ! #############
147 class SkippableWazuhIndexerClientErrors(Enum):
148 NO_MAPPING_FOR_TIMESTAMP = "No mapping found for [timestamp_utc] in order to sort on"
149 # Add other error messages here, for example:
150 # ANOTHER_ERROR = "Another specific error message"
151 +
152 +
153 +class AlertNotFound(BaseModel):
154 + index: str = Field(alias="_index")
155 + id: str = Field(alias="_id")
156 + source: Dict[str, str] = Field(alias="_source")
157 +
158 + def to_dict(self) -> Dict[str, str]:
159 + return self.dict(by_alias=True)
backend/app/connectors/wazuh_indexer/schema/indices.py
+1
@@ -24,6 +24,7 @@ class IndexConfigModel(BaseModel):
24 ".task": True,
25 "wazuh-states-vulnerabilities": True,
26 ".plugins": True,
27 + ".ql": True,
28 },
29 description="A dictionary containing index names to be skipped and their skip status.",
30 )
backend/app/connectors/wazuh_indexer/schema/sigma.py new
+223
@@ -0,0 +1,223 @@
1 +import re
2 +from datetime import datetime
3 +from enum import Enum
4 +from typing import List
5 +from typing import Optional
6 +
7 +from pydantic import BaseModel
8 +from pydantic import Field
9 +from pydantic import validator
10 +
11 +
12 +class SigmaRulesLevel(str, Enum):
13 + """
14 + Represents the Sigma rules level.
15 + """
16 +
17 + high = "high"
18 + critical = "critical"
19 +
20 +
21 +class SigmaRuleUploadRequest(BaseModel):
22 + rule_levels: List[SigmaRulesLevel] = Field(
23 + ...,
24 + description="The Sigma rule levels to upload.",
25 + )
26 +
27 +
28 +class DownloadSigmaRulesRequest(BaseModel):
29 + url: str = Field(
30 + "https://github.com/SigmaHQ/sigma/releases/download/r2024-07-17/sigma_all_rules.zip",
31 + title="URL",
32 + description="The URL to download the Sigma rules from.",
33 + )
34 + folder: str = Field(
35 + "windows",
36 + title="Folder",
37 + description="The folder of Sigma rules to download.",
38 + )
39 +
40 +
41 +class BulkUploadToDBResponse(BaseModel):
42 + success: bool
43 + message: str
44 +
45 +
46 +class SigmaQueriesOut(BaseModel):
47 + """
48 + Represents the Sigma queries output.
49 + """
50 +
51 + id: int
52 + rule_name: str
53 + rule_query: str
54 + active: bool
55 + time_interval: str
56 + last_updated: Optional[datetime] = None
57 + last_execution_time: Optional[datetime] = None
58 +
59 +
60 +class SigmaQueryOutResponse(BaseModel):
61 + """
62 + Represents the Sigma query output response.
63 + """
64 +
65 + sigma_queries: Optional[List[SigmaQueriesOut]] = []
66 + success: bool
67 + message: str
68 +
69 +
70 +class ActivateSigmaQueryResponse(BaseModel):
71 + """
72 + Represents the Sigma query activation response.
73 + """
74 +
75 + success: bool
76 + message: str
77 + enabled_queries: List[str] = []
78 +
79 +
80 +class DeactivateSigmaQueryResponse(BaseModel):
81 + """
82 + Represents the Sigma query deactivation response.
83 + """
84 +
85 + success: bool
86 + message: str
87 + disabled_queries: List[str] = []
88 +
89 +
90 +class DeleteSigmaQueryResponse(BaseModel):
91 + """
92 + Represents the Sigma query deletion response.
93 + """
94 +
95 + success: bool
96 + message: str
97 + deleted_queries: List[str] = []
98 +
99 +
100 +class RunActiveSigmaQueries(BaseModel):
101 + query: str = Field(
102 + ...,
103 + description="The query to run.",
104 + )
105 + time_interval: str = Field(
106 + ...,
107 + description="The time interval to run the query for.",
108 + )
109 + last_execution_time: datetime = None
110 + rule_name: str = Field(
111 + ...,
112 + description="The name of the rule.",
113 + )
114 + index: str = Field(
115 + "wazuh*",
116 + description="The index to run the query on.",
117 + )
118 +
119 +
120 +class CreateSigmaQuery(BaseModel):
121 + """
122 + Represents the Sigma query creation request.
123 + """
124 +
125 + rule_name: str
126 + rule_query: str
127 + active: bool
128 + time_interval: str
129 +
130 + @validator("rule_name")
131 + def validate_rule_name(cls, rule_name: str) -> str:
132 + """
133 + Validates the rule name.
134 +
135 + Args:
136 + rule_name (str): The rule name.
137 +
138 + Returns:
139 + str: The validated rule name.
140 + """
141 + if len(rule_name) < 1:
142 + raise ValueError("The rule name must not be empty.")
143 +
144 + return rule_name
145 +
146 + @validator("rule_query")
147 + def validate_rule_query(cls, rule_query: str) -> str:
148 + """
149 + Validates the rule query.
150 +
151 + Args:
152 + rule_query (str): The rule query.
153 +
154 + Returns:
155 + str: The validated rule query.
156 + """
157 + if len(rule_query) < 1:
158 + raise ValueError("The rule query must not be empty.")
159 +
160 + return rule_query
161 +
162 + @validator("time_interval")
163 + def validate_time_interval(cls, time_interval: str) -> str:
164 + """
165 + Validates the time interval.
166 +
167 + Args:
168 + time_interval (str): The time interval.
169 +
170 + Returns:
171 + str: The validated time interval.
172 + """
173 + if not re.match(r"^\d+[mhd]$", time_interval):
174 + raise ValueError("The time interval must be in the format of minutes (e.g., '1m'), hours (e.g., '1h'), or days (e.g., '1d').")
175 +
176 + return time_interval
177 +
178 +
179 +class QueryString(BaseModel):
180 + query: str
181 + analyze_wildcard: bool
182 +
183 +
184 +class MustItem(BaseModel):
185 + query_string: QueryString
186 +
187 +
188 +class BoolQuery(BaseModel):
189 + must: List[MustItem]
190 +
191 +
192 +class Query(BaseModel):
193 + bool: BoolQuery
194 +
195 +
196 +class SigmaQueryGenerationResponse(BaseModel):
197 + query: Query
198 +
199 +
200 +class UpdateSigmaActive(BaseModel):
201 + rule_name: str
202 + active: bool
203 +
204 +
205 +class UpdateSigmaTimeInterval(BaseModel):
206 + rule_name: str
207 + time_interval: str
208 +
209 + @validator("time_interval")
210 + def validate_time_interval(cls, time_interval: str) -> str:
211 + """
212 + Validates the time interval.
213 +
214 + Args:
215 + time_interval (str): The time interval.
216 +
217 + Returns:
218 + str: The validated time interval.
219 + """
220 + if not re.match(r"^\d+[mhd]$", time_interval):
221 + raise ValueError("The time interval must be in the format of minutes (e.g., '1m'), hours (e.g., '1h'), or days (e.g., '1d').")
222 +
223 + return time_interval
backend/app/connectors/wazuh_indexer/services/alerts.py
+153
@@ -1,12 +1,19 @@
1 +from collections import defaultdict
2 +from datetime import datetime
3 +from datetime import timedelta
4 from typing import Dict
5 from typing import List
6 from typing import Optional
7 +from typing import Tuple
8 from typing import Type
9
10 +from elasticsearch7.exceptions import NotFoundError
11 from elasticsearch7.exceptions import RequestError
12 from fastapi import HTTPException
13 from loguru import logger
14
15 +from app.connectors.wazuh_indexer.schema.alerts import Alert
16 +from app.connectors.wazuh_indexer.schema.alerts import AlertNotFound
17 from app.connectors.wazuh_indexer.schema.alerts import AlertsByHost
18 from app.connectors.wazuh_indexer.schema.alerts import AlertsByHostResponse
19 from app.connectors.wazuh_indexer.schema.alerts import AlertsByRule
@@ -16,6 +23,7 @@ from app.connectors.wazuh_indexer.schema.alerts import AlertsByRuleResponse
23 from app.connectors.wazuh_indexer.schema.alerts import AlertsSearchBody
24 from app.connectors.wazuh_indexer.schema.alerts import AlertsSearchResponse
25 from app.connectors.wazuh_indexer.schema.alerts import CollectAlertsResponse
26 +from app.connectors.wazuh_indexer.schema.alerts import GraylogAlertsSearchBody
27 from app.connectors.wazuh_indexer.schema.alerts import HostAlertsSearchBody
28 from app.connectors.wazuh_indexer.schema.alerts import HostAlertsSearchResponse
29 from app.connectors.wazuh_indexer.schema.alerts import IndexAlertsSearchBody
@@ -331,3 +339,148 @@ async def get_alerts_by_rule_per_host(
339 success=bool(alerts_by_rule_per_host_list),
340 message="Successfully collected alerts by rule per host",
341 )
342 +
343 +
344 +def parse_timerange(timerange: str) -> str:
345 + """
346 + Parses the timerange string and returns the corresponding datetime string for Elasticsearch.
347 + """
348 + unit = timerange[-1]
349 + amount = int(timerange[:-1])
350 +
351 + if unit == "h":
352 + delta = timedelta(hours=amount)
353 + elif unit == "d":
354 + delta = timedelta(days=amount)
355 + elif unit == "w":
356 + delta = timedelta(weeks=amount)
357 + else:
358 + raise HTTPException(
359 + status_code=400,
360 + detail="Invalid timerange unit. Must be one of 'h', 'd', or 'w'.",
361 + )
362 +
363 + start_time = datetime.utcnow() - delta
364 + return start_time.isoformat() + "Z"
365 +
366 +
367 +async def get_original_alert_id(origin_context: str) -> Tuple[str, str]:
368 + """
369 + Extracts the index name and id from the origin_context field of an alert.
370 +
371 + Args:
372 + origin_context (str): The origin_context field of an alert.
373 +
374 + Returns:
375 + Tuple[str, str]: A tuple containing the index name and id of the alert.
376 + """
377 + index_name, index_id = origin_context.split(":")[-2:]
378 + return index_name, index_id
379 +
380 +
381 +async def get_single_alert_details(
382 + index_name: str,
383 + index_id: str,
384 +) -> Dict:
385 + """
386 + Retrieves the details of a single alert based on the index name and id.
387 +
388 + Args:
389 + es_client: The Elasticsearch client.
390 + index_name (str): The name of the index to search for the alert.
391 + index_id (str): The id of the alert to retrieve.
392 +
393 + Returns:
394 + dict: The details of the alert.
395 + """
396 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
397 + try:
398 + alert = es_client.get(index=index_name, id=index_id)
399 + return alert
400 + except NotFoundError:
401 + logger.warning(f"Alert not found for index {index_name} and id {index_id}")
402 + return AlertNotFound(_index=index_name, _id=index_id, _source={"message": "alert not found"}).to_dict()
403 +
404 +
405 +async def fetch_alerts_from_graylog(index_prefix: str, size: int, timerange: str) -> List[Dict]:
406 + """
407 + Fetches alerts from the Graylog Alert Index.
408 +
409 + Args:
410 + es_client: The Elasticsearch client.
411 + index_prefix (str): The prefix of the index to search for alerts.
412 + size (int): The number of alerts to retrieve.
413 +
414 + Returns:
415 + List[Dict]: A list of alerts.
416 + """
417 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
418 + es_query = {
419 + "size": size,
420 + "query": {
421 + "bool": {
422 + "must": [
423 + {
424 + "range": {
425 + "timestamp": {
426 + "gte": parse_timerange(timerange),
427 + "format": "strict_date_optional_time",
428 + },
429 + },
430 + },
431 + ],
432 + },
433 + },
434 + }
435 + response = es_client.search(index=index_prefix, body=es_query)
436 + logger.info(f"Graylog alerts response: {response}")
437 + return response["hits"]["hits"]
438 +
439 +
440 +async def process_alert_hits(hits: List[Dict]) -> List[Dict]:
441 + """
442 + Processes the hits from the Graylog alert search response.
443 +
444 + Args:
445 + es_client: The Elasticsearch client.
446 + hits (List[Dict]): The hits from the search response.
447 +
448 + Returns:
449 + List[Dict]: A list of detailed alerts.
450 + """
451 + alerts_dict = defaultdict(lambda: {"total_alerts": 0, "alerts": []})
452 +
453 + for hit in hits:
454 + origin_context = hit["_source"]["origin_context"]
455 + index_name, index_id = await get_original_alert_id(origin_context)
456 + logger.info(f"Fetching alert details for index {index_name} and id {index_id}")
457 + alert_details = await get_single_alert_details(index_name, index_id)
458 + logger.info(f"Alert details: {alert_details}")
459 +
460 + alerts_dict[index_name]["total_alerts"] += 1
461 + alerts_dict[index_name]["alerts"].append(alert_details)
462 +
463 + alerts = [
464 + Alert(index_name=index_name, total_alerts=data["total_alerts"], alerts=data["alerts"]) for index_name, data in alerts_dict.items()
465 + ]
466 +
467 + return alerts
468 +
469 +
470 +async def get_graylog_alerts(
471 + request: GraylogAlertsSearchBody,
472 +) -> AlertsSearchResponse:
473 + """
474 + Retrieves alerts from the Graylog Alert Index.
475 + Looks up the actual alert details via the origin_context field.
476 + Strips out the index name and id from the origin_context field.
477 + Example: urn:graylog:message:es:huntress_00002_0:b4d2c721-f690-11ee-ac73-8600007a2218
478 + Looks up each alert by the index name and id.
479 + Adds each alert to the response.
480 + """
481 + logger.info(f"Fetching Graylog alerts for request: {request}")
482 +
483 + hits = await fetch_alerts_from_graylog(request.index_prefix, request.size, request.timerange)
484 + alerts = await process_alert_hits(hits)
485 +
486 + return alerts
backend/app/connectors/wazuh_indexer/services/sigma/elasticsearch.py new
+458
@@ -0,0 +1,458 @@
1 +import json
2 +import re
3 +from typing import Any
4 +from typing import ClassVar
5 +from typing import Dict
6 +from typing import Iterable
7 +from typing import List
8 +from typing import Optional
9 +from typing import Pattern
10 +from typing import Tuple
11 +from typing import Union
12 +
13 +import sigma
14 +from sigma.conditions import ConditionAND
15 +from sigma.conditions import ConditionFieldEqualsValueExpression
16 +from sigma.conditions import ConditionItem
17 +from sigma.conditions import ConditionNOT
18 +from sigma.conditions import ConditionOR
19 +from sigma.conversion.base import TextQueryBackend
20 +from sigma.conversion.deferred import DeferredQueryExpression
21 +from sigma.conversion.state import ConversionState
22 +from sigma.data.mitre_attack import mitre_attack_tactics
23 +from sigma.data.mitre_attack import mitre_attack_techniques
24 +from sigma.rule import SigmaRule
25 +from sigma.rule import SigmaRuleTag
26 +from sigma.types import SigmaCompareExpression
27 +from sigma.types import SigmaNull
28 +
29 +
30 +class LuceneBackend(TextQueryBackend):
31 + """
32 + Elasticsearch query string backend. Generates query strings described here in the
33 + Elasticsearch documentation:
34 +
35 + https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax
36 + """
37 +
38 + # A descriptive name of the backend
39 + name: ClassVar[str] = "Elasticsearch Lucene"
40 + # Output formats provided by the backend as name -> description mapping.
41 + # The name should match to finalize_output_<name>.
42 + formats: ClassVar[Dict[str, str]] = {
43 + "default": "Plain Elasticsearch Lucene queries",
44 + "kibana_ndjson": "Kibana NDJSON import file with Lucene queries",
45 + "dsl_lucene": "Elasticsearch query DSL with embedded Lucene queries",
46 + "siem_rule": "Elasticsearch query DSL as SIEM Rules in JSON Format",
47 + "siem_rule_ndjson": "Elasticsearch query DSL as SIEM Rules in NDJSON Format",
48 + }
49 + # Does the backend requires that a processing pipeline is provided?
50 + requires_pipeline: ClassVar[bool] = True
51 +
52 + # Operator precedence: tuple of Condition{AND,OR,NOT} in order of precedence.
53 + # The backend generates grouping if required
54 + precedence: ClassVar[Tuple[ConditionItem, ConditionItem, ConditionItem]] = (
55 + ConditionNOT,
56 + ConditionOR,
57 + ConditionAND,
58 + )
59 + # Expression for precedence override grouping as format string with {expr} placeholder
60 + group_expression: ClassVar[str] = "({expr})"
61 + parenthesize: bool = True
62 +
63 + # Generated query tokens
64 + token_separator: str = " " # separator inserted between all boolean operators
65 + or_token: ClassVar[str] = "OR"
66 + and_token: ClassVar[str] = "AND"
67 + not_token: ClassVar[str] = "NOT"
68 + # Token inserted between field and value (without separator)
69 + eq_token: ClassVar[str] = ":"
70 +
71 + # String output
72 + # Fields
73 + # No quoting of field names
74 + # Escaping
75 + # Character to escape particular parts defined in field_escape_pattern.
76 + field_escape: ClassVar[str] = "\\"
77 + # All matches of this pattern are prepended with the string contained in field_escape.
78 + field_escape_pattern: ClassVar[Pattern] = re.compile("[\\s*]")
79 +
80 + # Values
81 + # string quoting character (added as escaping character)
82 + str_quote: ClassVar[str] = '"'
83 + str_quote_pattern: ClassVar[Pattern] = re.compile(r"^$")
84 + str_quote_pattern_negation: ClassVar[bool] = False
85 + # Escaping character for special characrers inside string
86 + escape_char: ClassVar[str] = "\\"
87 + # Character used as multi-character wildcard
88 + wildcard_multi: ClassVar[str] = "*"
89 + # Character used as single-character wildcard
90 + wildcard_single: ClassVar[str] = "?"
91 + # Characters quoted in addition to wildcards and string quote
92 + add_escaped: ClassVar[str] = '+-=&|!(){}[]<>^"~*?:\\/ '
93 + bool_values: ClassVar[Dict[bool, str]] = { # Values to which boolean values are mapped.
94 + True: "true",
95 + False: "false",
96 + }
97 +
98 + # Regular expressions
99 + # Regular expression query as format string with placeholders {field} and {regex}
100 + re_expression: ClassVar[str] = "{field}:/{regex}/"
101 + # Character used for escaping in regular expressions
102 + re_escape_char: ClassVar[str] = "\\"
103 + re_escape: ClassVar[Tuple[str]] = ("/",)
104 + # Don't escape the escape char
105 + re_escape_escape_char: ClassVar[bool] = False
106 +
107 + # cidr expressions
108 + # CIDR expression query as format string with placeholders {field} = {value}
109 + cidr_expression: ClassVar[str] = "{field}:{network}\\/{prefixlen}"
110 +
111 + # Numeric comparison operators
112 + # Compare operation query as format string with placeholders {field}, {operator} and {value}
113 + compare_op_expression: ClassVar[str] = "{field}:{operator}{value}"
114 + # Mapping between CompareOperators elements and strings used as replacement
115 + # for {operator} in compare_op_expression
116 + compare_operators: ClassVar[Dict[SigmaCompareExpression.CompareOperators, str]] = {
117 + SigmaCompareExpression.CompareOperators.LT: "<",
118 + SigmaCompareExpression.CompareOperators.LTE: "<=",
119 + SigmaCompareExpression.CompareOperators.GT: ">",
120 + SigmaCompareExpression.CompareOperators.GTE: ">=",
121 + }
122 +
123 + # Null/None expressions
124 + # Expression for field has null value as format string with {field} placeholder for field name
125 + field_null_expression: ClassVar[str] = "NOT _exists_:{field}"
126 +
127 + # Field value in list, e.g. "field in (value list)" or "field containsall (value list)"
128 + # Convert OR as in-expression
129 + convert_or_as_in: ClassVar[bool] = True
130 + # Convert AND as in-expression
131 + convert_and_as_in: ClassVar[bool] = False
132 + # Values in list can contain wildcards. If set to False (default)
133 + # only plain values are converted into in-expressions.
134 + in_expressions_allow_wildcards: ClassVar[bool] = True
135 + # Expression for field in list of values as format string with
136 + # placeholders {field}, {op} and {list}
137 + field_in_list_expression: ClassVar[str] = "{field}{op}({list})"
138 + # Operator used to convert OR into in-expressions. Must be set if convert_or_as_in is set
139 + or_in_operator: ClassVar[str] = ":"
140 + # List element separator
141 + list_separator: ClassVar[str] = " OR "
142 +
143 + # Value not bound to a field
144 + # Expression for string value not bound to a field as format string with placeholder {value}
145 + unbound_value_str_expression: ClassVar[str] = "*{value}*"
146 + # Expression for number value not bound to a field as format string with placeholder {value}
147 + unbound_value_num_expression: ClassVar[str] = "{value}"
148 +
149 + def __init__(
150 + self,
151 + processing_pipeline: Optional["sigma.processing.pipeline.ProcessingPipeline"] = None,
152 + collect_errors: bool = False,
153 + index_names: List = [
154 + "apm-*-transaction*",
155 + "auditbeat-*",
156 + "endgame-*",
157 + "filebeat-*",
158 + "logs-*",
159 + "packetbeat-*",
160 + "traces-apm*",
161 + "winlogbeat-*",
162 + "-*elastic-cloud-logs-*",
163 + ],
164 + schedule_interval: int = 5,
165 + schedule_interval_unit: str = "m",
166 + **kwargs,
167 + ):
168 + super().__init__(processing_pipeline, collect_errors, **kwargs)
169 + self.index_names = index_names or [
170 + "apm-*-transaction*",
171 + "auditbeat-*",
172 + "endgame-*",
173 + "filebeat-*",
174 + "logs-*",
175 + "packetbeat-*",
176 + "traces-apm*",
177 + "winlogbeat-*",
178 + "-*elastic-cloud-logs-*",
179 + ]
180 + self.schedule_interval = schedule_interval or 5
181 + self.schedule_interval_unit = schedule_interval_unit or "m"
182 + self.severity_risk_mapping = {
183 + "INFORMATIONAL": 1,
184 + "LOW": 21,
185 + "MEDIUM": 47,
186 + "HIGH": 73,
187 + "CRITICAL": 99,
188 + }
189 +
190 + @staticmethod
191 + def _is_field_null_condition(cond: ConditionItem) -> bool:
192 + return isinstance(cond, ConditionFieldEqualsValueExpression) and isinstance(cond.value, SigmaNull)
193 +
194 + def convert_condition_not(self, cond: ConditionNOT, state: ConversionState) -> Union[str, DeferredQueryExpression]:
195 + """When checking if a field is not null, convert "NOT NOT _exists_:field" to "_exists_:field"."""
196 + if LuceneBackend._is_field_null_condition(cond.args[0]):
197 + return f"_exists_:{cond.args[0].field}"
198 +
199 + return super().convert_condition_not(cond, state)
200 +
201 + def convert_condition_field_eq_val_cidr(
202 + self,
203 + cond: ConditionFieldEqualsValueExpression,
204 + state: ConversionState,
205 + ) -> Union[str, DeferredQueryExpression]:
206 + if ":" in cond.value.cidr:
207 + return super().convert_condition_field_eq_val_cidr(cond, state).replace(":", r"\:").replace(r"\:", ":", 1)
208 + else:
209 + return super().convert_condition_field_eq_val_cidr(cond, state)
210 +
211 + def convert_condition_field_eq_expansion(self, cond: ConditionFieldEqualsValueExpression, state: ConversionState) -> Any:
212 + """
213 + Convert each value of the expansion with the field from the containing condition and OR-link
214 + all converted subconditions.
215 + """
216 + or_cond = ConditionOR(
217 + [ConditionFieldEqualsValueExpression(cond.field, value) for value in cond.value.values],
218 + cond.source,
219 + )
220 + if self.decide_convert_condition_as_in_expression(or_cond, state):
221 + return self.convert_condition_as_in_expression(or_cond, state)
222 + else:
223 + return self.convert_condition_or(cond, state)
224 +
225 + def compare_precedence(self, outer: ConditionItem, inner: ConditionItem) -> bool:
226 + """Override precedence check for null field conditions."""
227 + if isinstance(inner, ConditionNOT) and LuceneBackend._is_field_null_condition(inner.args[0]):
228 + # inner will turn into "_exists_:field", no parentheses needed
229 + return True
230 +
231 + if LuceneBackend._is_field_null_condition(inner):
232 + # inner will turn into "NOT _exists_:field", force parentheses
233 + return False
234 +
235 + return super().compare_precedence(outer, inner)
236 +
237 + def finalize_output_threat_model(self, tags: List[SigmaRuleTag]) -> Iterable[Dict]:
238 + attack_tags = [t for t in tags if t.namespace == "attack"]
239 + if not len(attack_tags) >= 2:
240 + return []
241 +
242 + techniques = [tag.name.upper() for tag in attack_tags if re.match(r"[tT]\d{4}", tag.name)]
243 + tactics = [tag.name.lower() for tag in attack_tags if not re.match(r"[tT]\d{4}", tag.name)]
244 +
245 + for tactic, technique in zip(tactics, techniques):
246 + if not tactic or not technique: # Only add threat if tactic and technique is known
247 + continue
248 +
249 + try:
250 + if "." in technique: # Contains reference to Mitre Att&ck subtechnique
251 + sub_technique = technique
252 + technique = technique[0:5]
253 + sub_technique_name = mitre_attack_techniques[sub_technique]
254 +
255 + sub_techniques = [
256 + {
257 + "id": sub_technique,
258 + "reference": f"https://attack.mitre.org/techniques/{sub_technique.replace('.', '/')}",
259 + "name": sub_technique_name,
260 + },
261 + ]
262 + else:
263 + sub_techniques = []
264 +
265 + tactic_id = [id for (id, name) in mitre_attack_tactics.items() if name == tactic.replace("_", "-")][0]
266 + technique_name = mitre_attack_techniques[technique]
267 + except (IndexError, KeyError):
268 + # Occurs when Sigma Mitre Att&ck list is out of date
269 + continue
270 +
271 + yield {
272 + "tactic": {
273 + "id": tactic_id,
274 + "reference": f"https://attack.mitre.org/tactics/{tactic_id}",
275 + "name": tactic.title().replace("_", " "),
276 + },
277 + "framework": "MITRE ATT&CK",
278 + "technique": [
279 + {
280 + "id": technique,
281 + "reference": f"https://attack.mitre.org/techniques/{technique}",
282 + "name": technique_name,
283 + "subtechnique": sub_techniques,
284 + },
285 + ],
286 + }
287 +
288 + for tag in attack_tags:
289 + tags.remove(tag)
290 +
291 + def finalize_query_dsl_lucene(self, rule: SigmaRule, query: str, index: int, state: ConversionState) -> Dict:
292 + return {"query": {"bool": {"must": [{"query_string": {"query": query, "analyze_wildcard": True}}]}}}
293 +
294 + def finalize_output_dsl_lucene(self, queries: List[Dict]) -> Dict:
295 + return list(queries)
296 +
297 + def finalize_query_kibana_ndjson(self, rule: SigmaRule, query: str, index: int, state: ConversionState) -> Dict:
298 + # TODO: implement the per-query output for the output format kibana here. Usually, the
299 + # generated query is embedded into a template, e.g. a JSON format with additional
300 + # information from the Sigma rule.
301 + columns = []
302 + index = "beats-*"
303 + ndjson = {
304 + "id": str(rule.id),
305 + "type": "search",
306 + "attributes": {
307 + "title": f"SIGMA - {rule.title}",
308 + "description": rule.description,
309 + "hits": 0,
310 + "columns": columns,
311 + "sort": ["@timestamp", "desc"],
312 + "version": 1,
313 + "kibanaSavedObjectMeta": {
314 + "searchSourceJSON": str(
315 + json.dumps(
316 + {
317 + "index": index,
318 + "filter": [],
319 + "highlight": {
320 + "pre_tags": ["@kibana-highlighted-field@"],
321 + "post_tags": ["@/kibana-highlighted-field@"],
322 + "fields": {"*": {}},
323 + "require_field_match": False,
324 + "fragment_size": 2147483647,
325 + },
326 + "query": {
327 + "query_string": {
328 + "query": query,
329 + "analyze_wildcard": True,
330 + },
331 + },
332 + },
333 + ),
334 + ),
335 + },
336 + },
337 + "references": [
338 + {
339 + "id": index,
340 + "name": "kibanaSavedObjectMeta.searchSourceJSON.index",
341 + "type": "index-pattern",
342 + },
343 + ],
344 + }
345 + return ndjson
346 +
347 + def finalize_output_kibana_ndjson(self, queries: List[str]) -> List[Dict]:
348 + # TODO: implement the output finalization for all generated queries for the format kibana
349 + # here. Usually, the single generated queries are embedded into a structure, e.g. some
350 + # JSON or XML that can be imported into the SIEM.
351 + return list(queries)
352 +
353 + def finalize_query_siem_rule(self, rule: SigmaRule, query: str, index: int, state: ConversionState) -> Dict:
354 + """
355 + Create SIEM Rules in JSON Format. These rules could be imported into Kibana using the
356 + Create Rule API https://www.elastic.co/guide/en/kibana/8.6/create-rule-api.html
357 + This API (and generated data) is NOT the same like importing Detection Rules via:
358 + Kibana -> Security -> Alerts -> Manage Rules -> Import
359 + If you want to have a nice importable NDJSON File for the Security Rule importer
360 + use pySigma Format 'siem_rule_ndjson' instead.
361 + """
362 +
363 + siem_rule = {
364 + "name": f"SIGMA - {rule.title}",
365 + "consumer": "siem",
366 + "enabled": True,
367 + "throttle": None,
368 + "schedule": {"interval": f"{self.schedule_interval}{self.schedule_interval_unit}"},
369 + "params": {
370 + "author": [rule.author] if rule.author is not None else [],
371 + "description": (rule.description if rule.description is not None else "No description"),
372 + "ruleId": str(rule.id),
373 + "falsePositives": rule.falsepositives,
374 + "from": f"now-{self.schedule_interval}{self.schedule_interval_unit}",
375 + "immutable": False,
376 + "license": "DRL",
377 + "outputIndex": "",
378 + "meta": {
379 + "from": "1m",
380 + },
381 + "maxSignals": 100,
382 + "riskScore": (self.severity_risk_mapping[rule.level.name] if rule.level is not None else 21),
383 + "riskScoreMapping": [],
384 + "severity": (str(rule.level.name).lower() if rule.level is not None else "low"),
385 + "severityMapping": [],
386 + "threat": list(self.finalize_output_threat_model(rule.tags)),
387 + "to": "now",
388 + "references": rule.references,
389 + "version": 1,
390 + "exceptionsList": [],
391 + "relatedIntegrations": [],
392 + "requiredFields": [],
393 + "setup": "",
394 + "type": "query",
395 + "language": "lucene",
396 + "index": self.index_names,
397 + "query": query,
398 + "filters": [],
399 + },
400 + "rule_type_id": "siem.queryRule",
401 + "tags": [f"{n.namespace}-{n.name}" for n in rule.tags],
402 + "notify_when": "onActiveAlert",
403 + "actions": [],
404 + }
405 + return siem_rule
406 +
407 + def finalize_output_siem_rule(self, queries: List[Dict]) -> Dict:
408 + return list(queries)
409 +
410 + def finalize_query_siem_rule_ndjson(self, rule: SigmaRule, query: str, index: int, state: ConversionState) -> Dict:
411 + """
412 + Generating SIEM/Detection Rules in NDJSON Format. Compatible with
413 +
414 + https://www.elastic.co/guide/en/security/8.6/rules-ui-management.html#import-export-rules-ui
415 + """
416 +
417 + siem_rule = {
418 + "id": str(rule.id),
419 + "name": f"SIGMA - {rule.title}",
420 + "enabled": True,
421 + "throttle": "no_actions",
422 + "interval": f"{self.schedule_interval}{self.schedule_interval_unit}",
423 + "author": [rule.author] if rule.author is not None else [],
424 + "description": (rule.description if rule.description is not None else "No description"),
425 + "rule_id": str(rule.id),
426 + "false_positives": rule.falsepositives,
427 + "from": f"now-{self.schedule_interval}{self.schedule_interval_unit}",
428 + "immutable": False,
429 + "license": "DRL",
430 + "output_index": "",
431 + "meta": {
432 + "from": "1m",
433 + },
434 + "max_signals": 100,
435 + "risk_score": (self.severity_risk_mapping[rule.level.name] if rule.level is not None else 21),
436 + "risk_score_mapping": [],
437 + "severity": (str(rule.level.name).lower() if rule.level is not None else "low"),
438 + "severity_mapping": [],
439 + "threat": list(self.finalize_output_threat_model(rule.tags)),
440 + "tags": [f"{n.namespace}-{n.name}" for n in rule.tags],
441 + "to": "now",
442 + "references": rule.references,
443 + "version": 1,
444 + "exceptions_list": [],
445 + "related_integrations": [],
446 + "required_fields": [],
447 + "setup": "",
448 + "type": "query",
449 + "language": "lucene",
450 + "index": self.index_names,
451 + "query": query,
452 + "filters": [],
453 + "actions": [],
454 + }
455 + return siem_rule
456 +
457 + def finalize_output_siem_rule_ndjson(self, queries: List[Dict]) -> Dict:
458 + return list(queries)
backend/app/connectors/wazuh_indexer/services/sigma/execute_query.py new
+156
@@ -0,0 +1,156 @@
1 +from datetime import datetime
2 +from typing import List
3 +
4 +from fastapi import HTTPException
5 +from loguru import logger
6 +from sqlalchemy.ext.asyncio import AsyncSession
7 +
8 +from app.connectors.wazuh_indexer.schema.sigma import RunActiveSigmaQueries
9 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
10 +from app.incidents.schema.incident_alert import CreatedAlertPayload
11 +from app.incidents.services.incident_alert import add_asset_to_copilot_alert
12 +from app.incidents.services.incident_alert import build_alert_context_payload
13 +from app.incidents.services.incident_alert import create_alert_full
14 +from app.incidents.services.incident_alert import get_all_field_names
15 +from app.incidents.services.incident_alert import get_customer_code
16 +from app.incidents.services.incident_alert import is_customer_code_valid
17 +from app.incidents.services.incident_alert import open_alert_exists
18 +
19 +
20 +async def build_alert_payload(
21 + sigma_rule_name: str,
22 + syslog_type: str,
23 + index_name: str,
24 + index_id: str,
25 + alert_payload: dict,
26 + session: AsyncSession,
27 +) -> CreatedAlertPayload:
28 + field_names = await get_all_field_names(syslog_type, session)
29 + validate_field_names(field_names, alert_payload)
30 + return await create_alert_payload(sigma_rule_name, syslog_type, index_name, index_id, alert_payload, field_names)
31 +
32 +
33 +def validate_field_names(field_names, alert_payload):
34 + for field_name in [field_names.asset_name, field_names.timefield_name, field_names.alert_title_name]:
35 + if field_name not in alert_payload:
36 + raise HTTPException(
37 + status_code=400,
38 + detail=f"Field name {field_name} not found in alert payload",
39 + )
40 +
41 +
42 +async def create_alert_payload(sigma_rule_name, syslog_type, index_name, index_id, alert_payload, field_names):
43 + return CreatedAlertPayload(
44 + alert_context_payload=await build_alert_context_payload(alert_payload, field_names),
45 + asset_payload=alert_payload.get(field_names.asset_name),
46 + timefield_payload=alert_payload.get(field_names.timefield_name),
47 + alert_title_payload="SIGMA Alert: " + sigma_rule_name,
48 + source=syslog_type,
49 + index_name=index_name,
50 + index_id=index_id,
51 + )
52 +
53 +
54 +async def format_opensearch_query(query: str, time_interval: str, last_execution_time: datetime) -> dict:
55 + logger.info(f"Last execution time: {last_execution_time}")
56 + formatted_last_execution_time = last_execution_time.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
57 + return {
58 + "query": {
59 + "bool": {
60 + "must": [
61 + {
62 + "query_string": {
63 + "query": query,
64 + "fields": [],
65 + "type": "best_fields",
66 + "default_operator": "or",
67 + "max_determinized_states": 10000,
68 + "enable_position_increments": True,
69 + "fuzziness": "AUTO",
70 + "fuzzy_prefix_length": 0,
71 + "fuzzy_max_expansions": 50,
72 + "phrase_slop": 0,
73 + "analyze_wildcard": True,
74 + "escape": False,
75 + "auto_generate_synonyms_phrase_query": True,
76 + "fuzzy_transpositions": True,
77 + "boost": 1,
78 + },
79 + },
80 + {
81 + "range": {
82 + "timestamp": {
83 + # "from": f"now-{time_interval}",
84 + "from": formatted_last_execution_time,
85 + "to": "now",
86 + "include_lower": True,
87 + "include_upper": False,
88 + "boost": 1,
89 + },
90 + },
91 + },
92 + ],
93 + "adjust_pure_negative": True,
94 + "boost": 1,
95 + },
96 + },
97 + }
98 +
99 +
100 +async def send_query_to_opensearch(
101 + es_client,
102 + query: dict,
103 + rule_name: str,
104 + index: str = "wazuh*",
105 + session: AsyncSession = None,
106 +) -> List[dict]:
107 + try:
108 + response = es_client.search(index=index, body=query)
109 + logger.info(f"Response: {response}")
110 + hits = response["hits"]["hits"]
111 + return await process_hits(hits, rule_name, session)
112 + except Exception as e:
113 + logger.error(f"Error executing query: {e}")
114 + return []
115 +
116 +
117 +async def process_hits(hits, rule_name, session: AsyncSession):
118 + logger.info(f"Processing number of hits: {len(hits)}")
119 + results = []
120 + for hit in hits:
121 + doc_id = hit["_id"]
122 + index = hit["_index"]
123 + customer_code = await get_customer_code(alert_details=hit["_source"])
124 + await is_customer_code_valid(customer_code=customer_code, session=session)
125 + alert_payload = await build_alert_payload(
126 + sigma_rule_name=rule_name,
127 + syslog_type="wazuh",
128 + index_name=index,
129 + index_id=doc_id,
130 + alert_payload=hit["_source"],
131 + session=session,
132 + )
133 + logger.info(f"Alert payload: {alert_payload}")
134 + existing_alert = await open_alert_exists(alert_payload, customer_code, session)
135 + if existing_alert:
136 + logger.info(f"Alert already exists: {existing_alert}")
137 + await add_asset_to_copilot_alert(
138 + alert_payload=alert_payload,
139 + alert_id=existing_alert,
140 + customer_code=customer_code,
141 + session=session,
142 + )
143 + results.append(existing_alert)
144 + else:
145 + new_alert = await create_alert_full(alert_payload=alert_payload, customer_code=customer_code, session=session)
146 + results.append(new_alert)
147 + return results
148 +
149 +
150 +async def execute_query(payload: RunActiveSigmaQueries, session: AsyncSession = None):
151 + client = await create_wazuh_indexer_client()
152 + formatted_query = await format_opensearch_query(payload.query, payload.time_interval, payload.last_execution_time)
153 + logger.info(f"Executing query: {formatted_query}")
154 + results = await send_query_to_opensearch(client, formatted_query, payload.rule_name, index=payload.index, session=session)
155 + logger.info(f"Results: {results}")
156 + return results
backend/app/connectors/wazuh_indexer/services/sigma/generate_query.py new
+41
@@ -0,0 +1,41 @@
1 +from sigma.collection import SigmaCollection
2 +from sigma.processing.resolver import ProcessingPipelineResolver
3 +
4 +from app.connectors.wazuh_indexer.schema.sigma import SigmaQueryGenerationResponse
5 +from app.connectors.wazuh_indexer.services.sigma.opensearch import (
6 + OpensearchLuceneBackend,
7 +)
8 +from app.connectors.wazuh_indexer.services.sigma.sysmon import sysmon_pipeline
9 +from app.connectors.wazuh_indexer.services.sigma.windows import ecs_windows
10 +
11 +
12 +async def create_sigma_query_from_rule(rule: str) -> SigmaQueryGenerationResponse:
13 + # Create our pipeline resolver
14 + piperesolver = ProcessingPipelineResolver()
15 +
16 + # Add wanted pipelines
17 + piperesolver.add_pipeline_class(ecs_windows())
18 + piperesolver.add_pipeline_class(sysmon_pipeline())
19 +
20 + # Create a single sorted and prioritzed pipeline
21 + resolved_pipeline = piperesolver.resolve(piperesolver.pipelines)
22 +
23 + # Instantiate backend, using our resolved pipeline
24 + # and some backend parameter
25 + backend = OpensearchLuceneBackend(
26 + resolved_pipeline,
27 + index_names=["logs-*-*", "beats-*"],
28 + monitor_interval=10,
29 + monitor_interval_unit="MINUTES",
30 + )
31 +
32 + rules = SigmaCollection.from_yaml(rule)
33 +
34 + # Convert the rule to DSL format
35 + dsl_result = backend.convert(rules, output_format="dsl_lucene")[0]
36 +
37 + # Create the Pydantic model from the DSL result
38 + sigma_query_out_response = SigmaQueryGenerationResponse.parse_obj(dsl_result)
39 +
40 + # return the DSL query as a string
41 + return sigma_query_out_response
backend/app/connectors/wazuh_indexer/services/sigma/opensearch.py new
+82
@@ -0,0 +1,82 @@
1 +from typing import ClassVar
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +
6 +import sigma
7 +from sigma.conversion.state import ConversionState
8 +from sigma.rule import SigmaRule
9 +
10 +from app.connectors.wazuh_indexer.services.sigma.elasticsearch import LuceneBackend
11 +
12 +
13 +class OpensearchLuceneBackend(LuceneBackend):
14 + """OpensearchLuceneBackend backend."""
15 +
16 + name: ClassVar[str] = "OpenSearch Lucene" # A descriptive name of the backend
17 + formats: ClassVar[
18 + Dict[str, str]
19 + ] = { # Output formats provided by the backend as name -> description mapping. The name should match to finalize_output_<name>.
20 + "default": "Plain OpenSearch Lucene queries",
21 + "dashboards_ndjson": "OpenSearch Dashboards NDJSON import file with Lucene queries",
22 + "monitor_rule": "OpenSearch monitor rule with embedded Lucene query",
23 + "dsl_lucene": "OpenSearch query DSL with embedded Lucene queries",
24 + }
25 + # Does the backend requires that a processing pipeline is provided?
26 + requires_pipeline: ClassVar[bool] = True
27 +
28 + def __init__(
29 + self,
30 + processing_pipeline: Optional["sigma.processing.pipeline.ProcessingPipeline"] = None,
31 + collect_errors: bool = False,
32 + index_names: List = ["beats-*"],
33 + monitor_interval: int = 5,
34 + monitor_interval_unit: str = "MINUTES",
35 + **kwargs,
36 + ):
37 + super().__init__(processing_pipeline, collect_errors, **kwargs)
38 + self.index_names = index_names or ["beats-*"]
39 + self.monitor_interval = monitor_interval or 5
40 + self.monitor_interval_unit = monitor_interval_unit or "MINUTES"
41 +
42 + def finalize_query_monitor_rule(self, rule: SigmaRule, query: str, index: int, state: ConversionState) -> dict:
43 + severity_mapping = {5: 1, 4: 2, 3: 3, 2: 4, 1: 5}
44 + monitor_rule = {
45 + "type": "monitor",
46 + "name": f"SIGMA - {rule.title}",
47 + "description": rule.description,
48 + "enabled": True,
49 + "schedule": {"period": {"interval": self.monitor_interval, "unit": self.monitor_interval_unit}},
50 + "inputs": [
51 + {
52 + "search": {
53 + "indices": self.index_names,
54 + "query": {"size": 1, "query": {"bool": {"must": [{"query_string": {"query": query, "analyze_wildcard": True}}]}}},
55 + },
56 + },
57 + ],
58 + "tags": [f"{n.namespace}-{n.name}" for n in rule.tags],
59 + "triggers": [
60 + {
61 + "name": "generated-trigger",
62 + "severity": severity_mapping[rule.level.value] if rule.level is not None else 1,
63 + "condition": {"script": {"source": "ctx.results[0].hits.total.value > 0", "lang": "painless"}},
64 + "actions": [],
65 + },
66 + ],
67 + "sigma_meta_data": {"rule_id": str(rule.id), "threat": []},
68 + "references": rule.references,
69 + }
70 +
71 + return monitor_rule
72 +
73 + def finalize_output_monitor_rule(self, queries: List[str]) -> str:
74 + return list(queries)
75 +
76 + def finalize_query_dashboards_ndjson(self, rule: SigmaRule, query: str, index: int, state: ConversionState) -> str:
77 + """Alias to Kibana NDJSON query finalization."""
78 + return self.finalize_query_kibana_ndjson(rule, query, index, state)
79 +
80 + def finalize_output_dashboards_ndjson(self, queries: List[str]) -> str:
81 + """Alias to Kibana NDJSON output finalization."""
82 + return self.finalize_output_kibana_ndjson(queries)
backend/app/connectors/wazuh_indexer/services/sigma/sigma_db_operations.py new
+316
@@ -0,0 +1,316 @@
1 +import os
2 +import re
3 +from datetime import datetime
4 +from datetime import timedelta
5 +from typing import List
6 +
7 +import yaml
8 +from fastapi import HTTPException
9 +from loguru import logger
10 +from sqlalchemy.exc import IntegrityError
11 +from sqlalchemy.ext.asyncio import AsyncSession
12 +from sqlalchemy.future import select
13 +
14 +from app.connectors.wazuh_indexer.models.sigma import SigmaQuery
15 +from app.connectors.wazuh_indexer.schema.sigma import CreateSigmaQuery
16 +from app.connectors.wazuh_indexer.schema.sigma import SigmaRuleUploadRequest
17 +from app.connectors.wazuh_indexer.services.sigma.generate_query import (
18 + create_sigma_query_from_rule,
19 +)
20 +from app.connectors.wazuh_indexer.services.sigma.sigma_download import find_yaml_files
21 +
22 +
23 +def parse_time_interval(interval: str) -> timedelta:
24 + match = re.match(r"(\d+)([smhd])", interval)
25 + if not match:
26 + raise ValueError(f"Invalid time interval format: {interval}")
27 + value, unit = match.groups()
28 + value = int(value)
29 + if unit == "s":
30 + return timedelta(seconds=value)
31 + elif unit == "m":
32 + return timedelta(minutes=value)
33 + elif unit == "h":
34 + return timedelta(hours=value)
35 + elif unit == "d":
36 + return timedelta(days=value)
37 + else:
38 + raise ValueError(f"Invalid time unit: {unit}")
39 +
40 +
41 +def check_level(rule_levels: list, file_path):
42 + delete_file = False
43 + with open(file_path, "r", encoding="utf-8") as file:
44 + for line in file:
45 + if line.startswith("level:"):
46 + level = line.split(":")[1].strip()
47 + if level not in rule_levels:
48 + delete_file = True
49 + break
50 + if delete_file:
51 + logger.info(f"Deleting file: {file_path}")
52 + os.remove(file_path) # delete the file if its level is not high or critical
53 + return False
54 + return True
55 +
56 +
57 +def validate_title(title):
58 + # Check if title is a string
59 + if not isinstance(title, str):
60 + return False
61 +
62 + # Check if title length is within the limit
63 + if len(title) > 65535:
64 + return False
65 +
66 + # Check if title contains only allowed characters
67 + pattern = re.compile(r"^[a-zA-Z0-9,.\-_/ ]*$")
68 + if not pattern.match(title):
69 + return False
70 +
71 + return True
72 +
73 +
74 +async def extract_title(file_path):
75 + # Load the YAML file
76 + with open(file_path, "r", encoding="utf-8") as file:
77 + data = yaml.safe_load(file)
78 +
79 + # Extract the title field
80 + title = data.get("title", "")
81 +
82 + # Validate the title
83 + if not validate_title(title):
84 + # Remove disallowed characters
85 + cleaned_title = re.sub(r"[^a-zA-Z0-9,.\-_/ ]", "", title)
86 +
87 + # Update the title field in the YAML data
88 + data["title"] = cleaned_title
89 +
90 + # Write the updated YAML data back to the file
91 + with open(file_path, "w", encoding="utf-8") as file:
92 + yaml.safe_dump(data, file)
93 +
94 + return cleaned_title
95 +
96 + return title
97 +
98 +
99 +async def get_sigma_query_by_id(
100 + db: AsyncSession,
101 + sigma_query_id: int,
102 +) -> SigmaQuery:
103 + """
104 + Retrieves a Sigma query by ID.
105 +
106 + Args:
107 + db (AsyncSession): The database session.
108 + sigma_query_id (int): The ID of the Sigma query.
109 +
110 + Returns:
111 + SigmaQuery: The Sigma query.
112 + """
113 + # Retrieve the Sigma query
114 + sigma_query = await db.execute(select(SigmaQuery).filter_by(id=sigma_query_id))
115 + sigma_query = sigma_query.scalars().first()
116 +
117 + if not sigma_query:
118 + raise HTTPException(
119 + status_code=404,
120 + detail="The Sigma query does not exist.",
121 + )
122 +
123 + return sigma_query
124 +
125 +
126 +async def list_sigma_queries(
127 + db: AsyncSession,
128 +) -> List[SigmaQuery]:
129 + """
130 + Retrieves a list of Sigma queries.
131 +
132 + Args:
133 + db (AsyncSession): The database session.
134 +
135 + Returns:
136 + List[SigmaQuery]: A list of Sigma queries.
137 + """
138 + # Retrieve the Sigma queries
139 + sigma_queries = await db.execute(select(SigmaQuery))
140 + sigma_queries = sigma_queries.scalars().all()
141 +
142 + return sigma_queries
143 +
144 +
145 +async def list_active_sigma_queries(
146 + db: AsyncSession,
147 +) -> List[SigmaQuery]:
148 + """
149 + Retrieves a list of active Sigma queries.
150 +
151 + Args:
152 + db (AsyncSession): The database session.
153 +
154 + Returns:
155 + List[SigmaQuery]: A list of active Sigma queries.
156 + """
157 + # Retrieve the active Sigma queries
158 + sigma_queries = await db.execute(select(SigmaQuery).filter_by(active=True))
159 + sigma_queries = sigma_queries.scalars().all()
160 +
161 + return sigma_queries
162 +
163 +
164 +async def list_inactive_sigma_queries(
165 + db: AsyncSession,
166 +) -> List[SigmaQuery]:
167 + """
168 + Retrieves a list of inactive Sigma queries.
169 +
170 + Args:
171 + db (AsyncSession): The database session.
172 +
173 + Returns:
174 + List[SigmaQuery]: A list of inactive Sigma queries.
175 + """
176 + # Retrieve the inactive Sigma queries
177 + sigma_queries = await db.execute(select(SigmaQuery).filter_by(active=False))
178 + sigma_queries = sigma_queries.scalars().all()
179 +
180 + return sigma_queries
181 +
182 +
183 +async def create_sigma_query(
184 + sigma_query: CreateSigmaQuery,
185 + db: AsyncSession,
186 +) -> SigmaQuery:
187 + """
188 + Creates a Sigma query.
189 +
190 + Args:
191 + sigma_query (CreateSigmaQuery): The Sigma query to create.
192 + db (AsyncSession): The database session.
193 +
194 + Returns:
195 + SigmaQuery: The created Sigma query.
196 + """
197 + # Create the Sigma query
198 + new_sigma_query = SigmaQuery(
199 + rule_name=sigma_query.rule_name,
200 + rule_query=sigma_query.rule_query,
201 + active=sigma_query.active,
202 + time_interval=sigma_query.time_interval,
203 + )
204 +
205 + # Add the Sigma query to the database
206 + db.add(new_sigma_query)
207 +
208 + try:
209 + await db.commit()
210 + except IntegrityError as e:
211 + logger.error(f"Failed to create the Sigma query: {e}")
212 + raise HTTPException(
213 + status_code=400,
214 + detail="Failed to create the Sigma query.",
215 + )
216 +
217 + return new_sigma_query
218 +
219 +
220 +async def read_sigma_rule(file: str) -> str:
221 + with open(file, "r", encoding="utf-8") as f:
222 + return f.read()
223 +
224 +
225 +async def get_existing_query(rule_name: str, db: AsyncSession):
226 + result = await db.execute(select(SigmaQuery).filter_by(rule_name=rule_name))
227 + return result.scalars().first()
228 +
229 +
230 +async def update_sigma_query(existing_query: SigmaQuery, new_query: CreateSigmaQuery, db: AsyncSession):
231 + existing_query.rule_query = new_query.rule_query
232 + existing_query.active = new_query.active
233 + existing_query.time_interval = new_query.time_interval
234 + await db.commit()
235 +
236 +
237 +async def process_sigma_file(file: str, db: AsyncSession):
238 + rule = await read_sigma_rule(file)
239 + title = await extract_title(file)
240 + query = await create_sigma_query_from_rule(rule)
241 +
242 + new_query = CreateSigmaQuery(
243 + rule_name=title,
244 + rule_query=query.query.bool.must[0].query_string.query,
245 + active=False,
246 + time_interval="5m",
247 + )
248 +
249 + existing_query = await get_existing_query(title, db)
250 + if existing_query:
251 + logger.info(f"Updating existing Sigma query: {title}")
252 + await update_sigma_query(existing_query, new_query, db)
253 + else:
254 + logger.info(f"Creating new Sigma query: {title}")
255 + await create_sigma_query(new_query, db)
256 +
257 +
258 +async def add_sigma_queries_to_db(request: SigmaRuleUploadRequest, db: AsyncSession):
259 + yaml_files = list(await find_yaml_files())
260 + # Only add the CRITICAL Severity SIGMA files
261 + sigma_files = [file for file in yaml_files if check_level(request.rule_levels, file)]
262 + for file in sigma_files:
263 + await process_sigma_file(file, db)
264 + return None
265 +
266 +
267 +async def delete_query_from_db(query, db: AsyncSession):
268 + try:
269 + await db.delete(query)
270 + await db.commit()
271 + except Exception as e:
272 + logger.error(f"Failed to delete the Sigma query: {e}")
273 + raise HTTPException(
274 + status_code=400,
275 + detail="Failed to delete the Sigma query.",
276 + )
277 +
278 +
279 +async def delete_sigma_rule(rule_name: str, db: AsyncSession):
280 + query = await get_existing_query(rule_name, db)
281 + if query:
282 + await delete_query_from_db(query, db)
283 + else:
284 + raise HTTPException(
285 + status_code=404,
286 + detail="The Sigma rule does not exist.",
287 + )
288 + return None
289 +
290 +
291 +async def set_sigma_query_active(rule_name: str, active: bool, db: AsyncSession):
292 + query = await get_existing_query(rule_name, db)
293 + if query:
294 + query.active = active
295 + query.last_updated = datetime.now()
296 + await db.commit()
297 + else:
298 + raise HTTPException(
299 + status_code=404,
300 + detail="The Sigma rule does not exist.",
301 + )
302 + return query
303 +
304 +
305 +async def update_sigma_time_interval(rule_name: str, time_interval: str, db: AsyncSession):
306 + query = await get_existing_query(rule_name, db)
307 + if query:
308 + query.time_interval = time_interval
309 + query.last_updated = datetime.now()
310 + await db.commit()
311 + else:
312 + raise HTTPException(
313 + status_code=404,
314 + detail="The Sigma rule does not exist.",
315 + )
316 + return query
backend/app/connectors/wazuh_indexer/services/sigma/sigma_download.py new
+79
@@ -0,0 +1,79 @@
1 +import os
2 +import shutil
3 +import zipfile
4 +from typing import List
5 +
6 +import requests
7 +from loguru import logger
8 +
9 +
10 +async def download_and_extract_zip(url: str) -> None:
11 + """
12 + Downloads a zipped folder from the given URL and extracts its contents into the specified directory.
13 +
14 + Args:
15 + url (str): The URL of the zipped folder.
16 + extract_to (str): The directory to extract the contents to. Defaults to the current directory.
17 + """
18 + # local_zip_path = os.path.join(extract_to, "sigma_all_rules.zip")
19 + directory = "app/connectors/wazuh_indexer/sigma_artifacts"
20 + full_path = os.path.abspath(directory)
21 +
22 + logger.info(f"Checking directory: {full_path}")
23 + local_zip_path = os.path.join(full_path, "sigma_all_rules.zip")
24 +
25 + # Ensure the directory exists
26 + os.makedirs(full_path, exist_ok=True)
27 +
28 + # Download the zipped folder
29 + response = requests.get(url)
30 + response.raise_for_status() # Check if the request was successful
31 +
32 + # Save the zipped folder to a local file
33 + with open(local_zip_path, "wb") as file:
34 + file.write(response.content)
35 +
36 + # Extract the contents of the zipped folder
37 + with zipfile.ZipFile(local_zip_path, "r") as zip_ref:
38 + zip_ref.extractall(full_path)
39 +
40 + # Remove the downloaded zip file
41 + os.remove(local_zip_path)
42 +
43 +
44 +async def keep_only_folder_directory(folder: str, directory: str = "app/connectors/wazuh_indexer/sigma_artifacts/rules"):
45 + """
46 + Removes all directories except the Windows directory from the specified directory.
47 +
48 + Args:
49 + directory (str): The directory to clean up.
50 + """
51 + for item in os.listdir(directory):
52 + if item != folder:
53 + item_path = os.path.join(directory, item)
54 + if os.path.isdir(item_path):
55 + logger.info(f"Removing directory and its contents: {item_path}")
56 + shutil.rmtree(item_path)
57 +
58 +
59 +async def find_yaml_files(directory: str = "app/connectors/wazuh_indexer/sigma_artifacts/rules/windows") -> List[str]:
60 + """
61 + Finds all YAML files in the specified directory.
62 +
63 + Args:
64 + directory (str): The directory to search for YAML files.
65 +
66 + Returns:
67 + List[str]: A list of YAML file paths.
68 + """
69 + yaml_files = []
70 +
71 + for root, _, files in os.walk(directory):
72 + for file in files:
73 + if file.endswith(".yml"):
74 + # yaml_files.append(os.path.join(root, file))
75 + # logger.info(f"Found YAML file: {file}")
76 + full_path = os.path.join(root, file)
77 + yaml_files.append(full_path)
78 +
79 + return yaml_files
backend/app/connectors/wazuh_indexer/services/sigma/sysmon.py new
+65
@@ -0,0 +1,65 @@
1 +from sigma.pipelines.base import Pipeline
2 +from sigma.processing.conditions import LogsourceCondition
3 +from sigma.processing.pipeline import ProcessingItem
4 +from sigma.processing.pipeline import ProcessingPipeline
5 +from sigma.processing.transformations import AddConditionTransformation
6 +from sigma.processing.transformations import ChangeLogsourceTransformation
7 +
8 +sysmon_generic_logsource_eventid_mapping = { # map generic Sigma log sources to Sysmon event ids
9 + "process_creation": 1,
10 + "file_change": 2,
11 + "network_connection": 3,
12 + "process_termination": 5,
13 + "sysmon_status": [4, 16],
14 + "driver_load": 6,
15 + "image_load": 7,
16 + "create_remote_thread": 8,
17 + "raw_access_thread": 9,
18 + "process_access": 10,
19 + "file_event": 11,
20 + "registry_add": 12,
21 + "registry_delete": 12,
22 + "registry_set": 13,
23 + "registry_rename": 14,
24 + "registry_event": [12, 13, 14],
25 + "create_stream_hash": 15,
26 + "pipe_created": [17, 18],
27 + "wmi_event": [19, 20, 21],
28 + "dns_query": 22,
29 + "file_delete": [23, 26],
30 + "clipboard_capture": 24,
31 + "process_tampering": 25,
32 + "sysmon_error": 255,
33 +}
34 +
35 +
36 +@Pipeline
37 +def sysmon_pipeline() -> ProcessingPipeline:
38 + return ProcessingPipeline(
39 + name="Generic Log Sources to Sysmon Transformation",
40 + priority=10,
41 + items=[
42 + processing_item
43 + for log_source, event_id in sysmon_generic_logsource_eventid_mapping.items()
44 + for processing_item in (
45 + ProcessingItem(
46 + identifier=f"sysmon_{log_source}_eventid",
47 + transformation=AddConditionTransformation(
48 + {
49 + "EventID": event_id,
50 + },
51 + ),
52 + rule_conditions=[LogsourceCondition(category=log_source, product="windows")],
53 + ),
54 + ProcessingItem(
55 + identifier=f"sysmon_{log_source}_logsource",
56 + transformation=ChangeLogsourceTransformation(
57 + product="windows",
58 + service="sysmon",
59 + category=log_source,
60 + ),
61 + rule_conditions=[LogsourceCondition(category=log_source, product="windows")],
62 + ),
63 + )
64 + ],
65 + )
backend/app/connectors/wazuh_indexer/services/sigma/windows.py new
+265
@@ -0,0 +1,265 @@
1 +from sigma.pipelines.common import generate_windows_logsource_items
2 +from sigma.processing.conditions import FieldNameProcessingItemAppliedCondition
3 +from sigma.processing.conditions import IncludeFieldCondition
4 +from sigma.processing.conditions import LogsourceCondition
5 +from sigma.processing.pipeline import ProcessingItem
6 +from sigma.processing.pipeline import ProcessingPipeline
7 +from sigma.processing.transformations import AddFieldnamePrefixTransformation
8 +from sigma.processing.transformations import FieldMappingTransformation
9 +
10 +ecs_windows_variable_mappings = {
11 + "FileVersion": (
12 + ("category", "process_creation", "data_win_eventdata_fileVersion"),
13 + ("category", "image_load", "data_win_eventdata_fileVersion"),
14 + ),
15 + "Description": (
16 + ("category", "process_creation", "data_win_eventdata_description"),
17 + ("category", "image_load", "data_win_eventdata_description"),
18 + ("category", "sysmon_error", "winlog.event_data.Description"),
19 + ),
20 + "Product": (
21 + ("category", "process_creation", "data_win_eventdata_product"),
22 + ("category", "image_load", "data_win_eventdata_product"),
23 + ),
24 + "Company": (
25 + ("category", "process_creation", "data_win_eventdata_company"),
26 + ("category", "image_load", "data_win_eventdata_company"),
27 + ),
28 + "OriginalFileName": (
29 + ("category", "process_creation", "data_win_eventdata_image"),
30 + ("category", "process_creation", "data_win_eventdata_image"),
31 + ("category", "image_load", "data_win_eventdata_originalFileName"),
32 + ),
33 + "CommandLine": (
34 + ("category", "process_creation", "data_win_eventdata_commandLine"),
35 + ("service", "security", "data_win_eventdata_commandLine"),
36 + ("service", "powershell-classic", "powershell.command.value"),
37 + ),
38 + "Protocol": (("category", "network_connection", "data_win_eventdata_protocol"),),
39 + "Initiated": (("category", "network_connection", "data_win_eventdata_initiated"),),
40 + "Signature": (
41 + ("category", "driver_loaded", "data_win_eventdata_signatureSubjectName"),
42 + ("category", "image_loaded", "data_win_eventdata_signatureSubjectName"),
43 + ),
44 + "EngineVersion": (("service", "powershell-classic", "powershell.engine.version"),),
45 + "HostVersion": (("service", "powershell-classic", "powershell.process.executable_version"),),
46 + "SubjectLogonId": (("service", "security", "data_win_eventdata_subjectLogonId"),),
47 + "ServiceName": (("service", "security", "data_win_eventdata_serviceName"),),
48 + "SubjectDomainName": (("service", "security", "data_win_eventdata_subjectDomainName"),),
49 + "SubjectUserName": (("service", "security", "data_win_eventdata_subjectUserName"),),
50 + "SubjectUserSid": (("service", "security", "data_win_eventdata_subjectUserSid"),),
51 + "TargetLogonId": (("service", "security", "data_win_eventdata_targetLogonId"),),
52 +}
53 +
54 +
55 +def ecs_windows() -> ProcessingPipeline:
56 + return ProcessingPipeline(
57 + name="Elastic Common Schema (ECS) Windows log mappings from Winlogbeat from version 7",
58 + priority=20,
59 + allowed_backends=("elasticsearch", "eql", "lucene", "opensearch"),
60 + items=generate_windows_logsource_items("data_win_system_channel", "{source}")
61 + + [ # Variable field mapping depending on category/service
62 + ProcessingItem(
63 + identifier=f"elasticsearch_windows-{field}-{logsrc_field}-{logsrc}",
64 + transformation=FieldMappingTransformation({field: mapped}),
65 + rule_conditions=[
66 + LogsourceCondition(
67 + **{
68 + "product": "windows",
69 + logsrc_field: logsrc,
70 + },
71 + ),
72 + ],
73 + )
74 + for field, mappings in ecs_windows_variable_mappings.items()
75 + for (logsrc_field, logsrc, mapped) in mappings
76 + ]
77 + + [
78 + ProcessingItem( # Field mappings
79 + identifier="ecs_windows_field_mapping",
80 + transformation=FieldMappingTransformation(
81 + {
82 + "Accesses": "data_win_eventdata_accesses",
83 + "AccessList": "data_win_eventdata_accessList",
84 + "AccessMask": "data_win_eventdata_accessMask",
85 + "AccountName": "data_win_eventdata_targetUserName",
86 + "Action": "data_win_eventdata_action",
87 + "AllowedToDelegateTo": "data_win_eventdata_allowedToDelegateTo",
88 + "Application": "data_win_eventdata_application",
89 + "ApplicationPath": "data_win_eventdata_applicationPath",
90 + "AttributeLDAPDisplayName": "data_win_eventdata_attributeLDAPDisplayName",
91 + "AttributeValue": "data_win_eventdata_attributeValue",
92 + "AuditPolicyChanges": "data_win_evendata_auditPolicyChanges",
93 + "AuditSourceName": "data_win_eventdata_auditSourceName",
94 + "AuthenticationPackage": "data_win_eventdata_authenticationPackageName",
95 + "AuthenticationPackageName": "data_win_eventdata_authenticationPackageName",
96 + "CallTrace": "data_win_eventdata_callTrace",
97 + "Caption": "data_win_eventdata_caption",
98 + "Channel": "data_win_eventdata_channel",
99 + "ChildImage": "data_win_eventdata_image",
100 + "CommandLine": "data_win_eventdata_commandLine",
101 + "Company": "data_win_eventdata_company",
102 + "ComputerName": "data_win_system_computer",
103 + "ContextInfo": "data_win_system_contextInfo",
104 + "CurrentDirectory": "data_win_eventdata_currentDirectory",
105 + "Description": "data_win_eventdata_description",
106 + "DestAddress": "data_win_eventdata_destAddress",
107 + "Destination": "data_win_eventdata_destination",
108 + "DestinationHostname": "data_win_eventdata_destinationHostname",
109 + "DestinationIp": "data_win_eventdata_destinationIp",
110 + "DestinationIsIpv6": "data_win_eventdata_destinationIsIpv6",
111 + "DestinationPort": "data_win_eventdata_destinationPort",
112 + "Details": "data_win_eventdata_details",
113 + "DeviceClassName": "data_win_eventdata_deviceClassName",
114 + "DeviceDescription": "data_win_eventdata_deviceDescription",
115 + "DeviceName": "data_win_eventdata_deviceName",
116 + "DestPort": "data_win_eventdata_destinationPort",
117 + "EngineVersion": "data_win_eventdata_engineVersion",
118 + "EventID": "data_win_system_eventID",
119 + "EventType": "data_win_eventdata_eventType",
120 + "FailureCode": "data_win_eventdata_failureCode",
121 + "FileVersion": "data_win_eventdata_fileVersion",
122 + "FilterName": "data_win_evendata_filterName",
123 + "FilterOrigin": "data_win_eventdata_filterOrigin",
124 + "FolderPath": "data_win_eventdata_image",
125 + "GrantedAccess": "data_win_eventdata_grantedAccess",
126 + "Hash": "data_win_eventdata_hashes",
127 + "Hashes": "data_win_eventdata_hashes",
128 + "HostApplication": "data_win_eventdata_hostApplication",
129 + "HostName": "data_win_eventdata_hostName",
130 + "HostVersion": "data_win_eventdata_hostVersion",
131 + "Image": "data_win_eventdata_image",
132 + "ImageName": "data_win_evendata_imageName",
133 + "ImagePath": "data_win_eventdata_imagePath",
134 + "ImageLoaded": "data_win_eventdata_imageLoaded",
135 + "ImpHash": "data_win_eventdata_impHash",
136 + "Imphash": "data_win_eventdata_imphash",
137 + "ImpersonationLevel": "data_win_eventdata_impersonationLevel",
138 + "Initiated": "data_win_eventdata_initiated",
139 + "IntegrityLevel": "data_win_eventdata_integrityLevel",
140 + "IpAddress": "data_win_eventdata_ipAddress",
141 + "KeyLength": "data_win_eventdata_keyLength",
142 + "Keywords": "data_win_eventdata_keywords",
143 + "LayerRTID": "data_win_eventdata_layerRTID",
144 + "Level": "data_win_system_level",
145 + "LogonGuid": "data_win_eventdata_logonGuid",
146 + "LogonId": "data_win_eventdata_logonId",
147 + "LogonProcessName": "data_win_eventdata_logonProcessName",
148 + "LogonType": "data_win_eventdata_logonType",
149 + "md5": "data_win_eventdata_hashes",
150 + "Message": "data_win_system_message",
151 + "ModifyingApplication": "data_win_system_modifyingApplication",
152 + "NewName": "data_win_eventdata_newName",
153 + "NewTargetUserName": "data_win_evendata_newTargetUserName",
154 + "NewUacValue": "data_win_eventdata_newUacValue",
155 + "NewValue": "data_win_eventdata_newValue",
156 + "ObjectClass": "data_win_eventdata_objectClass",
157 + "ObjectName": "data_win_eventdata_objectName",
158 + "ObjectServer": "data_win_eventdata_objectServer",
159 + "ObjectType": "data_win_eventdata_objectType",
160 + "ObjectValueName": "data_win_eventdata_objectValueName",
161 + "OldUacValue": "data_win_eventdata_oldUacValue",
162 + "Origin": "data_win_eventdata_origin",
163 + "OriginalFileName": "data_win_eventdata_originalFileName",
164 + "PackageName": "data_win_eventdata_packageName",
165 + "Param1": "data_win_evendata_param1",
166 + "Param2": "data_win_evendata_param2",
167 + "Param3": "data_win_evendata_param3",
168 + "Param4": "data_win_evendata_Param4",
169 + "Param5": "data_win_evendata_param5",
170 + "Param6": "data_win_evendata_param6",
171 + "Param7": "data_win_evendata_param7",
172 + "Param8": "data_win_evendata_Param8",
173 + "Param9": "data_win_evendata_Param9",
174 + "Param10": "data_win_evendata_Param10",
175 + "ParentCommandLine": "data_win_eventdata_parentCommandLine",
176 + "ParentImage": "data_win_eventdata_parentImage",
177 + "ParentIntegrityLevel": "data_win_eventdata_parentIntegrityLevel",
178 + "ParentProcessGuid": "data_win_eventdata_parentProcessGuid",
179 + "ParentUser": "data_win_eventdata_parentUser",
180 + "Payload": "data_win_eventdata_payload",
181 + "PipeName": "data_win_eventdata_pipeName",
182 + "PrivilegeList": "data_win_eventdata_privilegeList",
183 + "ProcessCommandLine": "data_win_eventdata_commandLine",
184 + "ProcessID": "data_win_eventdata_processId",
185 + "ProcessName": "data_win_eventdata_processName",
186 + "ProcessPath": "data_win_eventdata_processPath",
187 + "Product": "data_win_eventdata_product",
188 + "Properties": "data_win_eventdata_properties",
189 + "ProviderContextName": "data_win_evendata_providerContextName",
190 + "ProviderName": "data_win_eventdata_providerName",
191 + "Provider_Name": "data_win_eventdata_providerName",
192 + "QueryName": "data_win_eventdata_queryName",
193 + "RelativeTargetName": "data_win_eventdata_relativeTargetName",
194 + "RemoteAddress": "data_win_eventdata_remoteAddress",
195 + "SamAccountName": "data_win_eventdata_samAccountName",
196 + "ScriptBlockText": "data_win_eventdata_scriptBlockText",
197 + "Service": "data_win_eventdata_service",
198 + "ServerName": "data_win_eventdata_serverName",
199 + "ServiceFileName": "data_win_eventdata_serviceFileName",
200 + "ServiceName": "data_win_eventdata_serviceName",
201 + "ServiceStartType": "data_win_evendata_serviceStartType",
202 + "ServiceType": "data_win_evendata_serviceType",
203 + "sha1": "data_win_eventdata_hashes",
204 + "sha256": "data_win_eventdata_hashes",
205 + "ShareName": "data_win_eventdata_shareName",
206 + "SidHistory": "data_win_eventdata_sidHistory",
207 + "Signed": "data_win_eventdata_signed",
208 + "Source": "data_win_eventdata_source",
209 + "Source_Name": "data_win_eventdata_sourceName",
210 + "SourceAddress": "data_win_eventdata_sourceAddress",
211 + "SourceImage": "data_win_eventdata_sourceImage",
212 + "SourceNetworkAddress": "data_win_eventdata_ipAddress",
213 + "TargetImage": "data_win_eventdata_targetImage",
214 + "TargetObject": "data_win_eventdata_targetObject",
215 + "TargetUser": "data_win_eventdata_targetUser",
216 + },
217 + ),
218 + rule_conditions=[LogsourceCondition(product="windows")],
219 + ),
220 + ProcessingItem( # Prepend each field that was not processed by previous field mapping transformation with "winlog.event_data."
221 + identifier="ecs_windows_winlog_eventdata_prefix",
222 + # transformation=AddFieldnamePrefixTransformation("winlog.event_data."),
223 + transformation=AddFieldnamePrefixTransformation(""),
224 + field_name_conditions=[
225 + FieldNameProcessingItemAppliedCondition("ecs_windows_field_mapping"),
226 + IncludeFieldCondition(fields=["\\w+\\."], type="re"),
227 + ],
228 + field_name_condition_negation=True,
229 + field_name_condition_linking=any,
230 + rule_conditions=[LogsourceCondition(product="windows")],
231 + ),
232 + ],
233 + )
234 +
235 +
236 +def ecs_windows_old() -> ProcessingPipeline:
237 + return ProcessingPipeline(
238 + name="Elastic Common Schema (ECS) Windows log mappings from Winlogbeat up to version 6",
239 + priority=20,
240 + allowed_backends=("elasticsearch", "eql", "lucene", "opensearch"),
241 + items=generate_windows_logsource_items("winlog.channel", "{source}")
242 + + [
243 + ProcessingItem( # Field mappings
244 + identifier="ecs_windows_field_mapping",
245 + transformation=FieldMappingTransformation(
246 + {
247 + "EventID": "event_id",
248 + "Channel": "winlog.channel",
249 + },
250 + ),
251 + rule_conditions=[LogsourceCondition(product="windows")],
252 + ),
253 + ProcessingItem( # Prepend each field that was not processed by previous field mapping transformation with "winlog.event_data."
254 + identifier="ecs_windows_eventdata_prefix",
255 + transformation=AddFieldnamePrefixTransformation("event_data."),
256 + field_name_conditions=[
257 + FieldNameProcessingItemAppliedCondition("ecs_windows_field_mapping"),
258 + IncludeFieldCondition(fields=["\\w+\\."], type="re"),
259 + ],
260 + field_name_condition_negation=True,
261 + field_name_condition_linking=any,
262 + rule_conditions=[LogsourceCondition(product="windows")],
263 + ),
264 + ],
265 + )
backend/app/connectors/wazuh_indexer/utils/universal.py
+89 -1
@@ -1,3 +1,4 @@
1 +import re
2 from datetime import datetime
3 from datetime import timedelta
4 from typing import Any
@@ -73,7 +74,7 @@ async def verify_wazuh_indexer_connection(connector_name: str) -> str:
74 return await verify_wazuh_indexer_credentials(attributes)
75
76
76 -async def create_wazuh_indexer_client(connector_name: str) -> Elasticsearch:
77 +async def create_wazuh_indexer_client(connector_name: str = "Wazuh-Indexer") -> Elasticsearch:
78 """
79 Returns an Elasticsearch client for the Wazuh Indexer service.
80
@@ -476,3 +477,90 @@ class LogsQueryBuilder:
477
478 def build(self):
479 return self.query
480 +
481 +
482 +async def get_index_mappings_key_names(index_name: str):
483 + """
484 + Get the mappings of an index.
485 +
486 + Args:
487 + index_name (str): The Name of the index.
488 +
489 + Returns:
490 + list: The field names of the index.
491 + """
492 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
493 + mappings = es_client.indices.get_mapping(index=index_name)
494 + # return only the field names
495 + return list(mappings[index_name]["mappings"]["properties"].keys())
496 +
497 +
498 +async def return_graylog_events_index_names():
499 + """
500 + Return the index names of the Graylog events.
501 +
502 + Returns:
503 + list: The index names of the Graylog events.
504 + """
505 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
506 + indices = es_client.indices.get_alias("gl-events*")
507 + return list(indices.keys())
508 +
509 +
510 +async def get_index_source(index_name: str):
511 + """
512 + Get the 10 latest results from the index and search for where the source contains a field name of `syslog_type` or `integration`
513 + """
514 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
515 + query = {"size": 10, "query": {"bool": {"must": [{"exists": {"field": "syslog_type"}}]}}}
516 + response = es_client.search(index=index_name, body=query)
517 + for hit in response["hits"]["hits"]: # Loop through each hit in the response
518 + if "syslog_type" in hit["_source"]: # Check if 'syslog_type' exists in the source of the hit
519 + if hit["_source"]["syslog_type"] == "integration" and "integration" in hit["_source"]:
520 + return hit["_source"]["integration"] # Return the value of 'integration' if 'syslog_type' equals 'integration'
521 + return hit["_source"]["syslog_type"] # Return the value of 'syslog_type' for other cases
522 + raise HTTPException(status_code=404, detail=f"Source not found in index {index_name}")
523 +
524 +
525 +async def get_available_indices_via_source(source: str):
526 + """
527 + Get the available indices based on the source using regex matching.
528 +
529 + Args:
530 + source (str): The regex pattern for the source of the index.
531 +
532 + Returns:
533 + list: The available indices based on the source regex match.
534 + """
535 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
536 + try:
537 + indices = es_client.indices.get_alias("*")
538 + logger.info(f"Indices: {indices.keys()}")
539 + available_indices = []
540 + source_pattern = re.compile(source) # Compile the regex pattern for the source
541 + exclude_patterns = [re.compile("wazuh-monitoring"), re.compile("wazuh-statistics")]
542 + for index in indices.keys():
543 + if source_pattern.search(index) and not any(exclude.search(index) for exclude in exclude_patterns):
544 + available_indices.append(index)
545 + except Exception as e:
546 + logger.error(f"An error occurred: {e}")
547 + available_indices = []
548 +
549 + logger.info(f"Available indices: {available_indices}")
550 + return available_indices
551 +
552 +
553 +async def resize_wazuh_index_fields():
554 + """
555 + Resize the Wazuh index fields to the correct size.
556 + """
557 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
558 +
559 + settings = {"index.mapping.total_fields.limit": 2000}
560 +
561 + try:
562 + es_client.indices.put_settings(index="wazuh*", body=settings)
563 + logger.info("Successfully resized the Wazuh index fields")
564 + except Exception as e:
565 + logger.error(f"An error occurred: {e}")
566 + return None
backend/app/customer_provisioning/schema/provision.py
+1 -9
@@ -82,14 +82,6 @@ class ProvisionNewCustomer(BaseModel):
82 False,
83 description="Whether to only insert the customer into the database without provisioning any services",
84 )
85 - dfir_iris_id: Optional[int] = Field(
86 - None,
87 - description="ID of the DFIR Iris customer",
88 - )
89 - dfir_iris_username: Optional[str] = Field(
90 - "administrator",
91 - description="Username of the DFIR Iris customer",
92 - )
85 graylog_index_id: Optional[str] = Field(
86 None,
87 description="ID of the Graylog index set",
@@ -128,7 +120,7 @@ class CustomerProvisionMeta(BaseModel):
120 grafana_organization_id: int
121 wazuh_datasource_uid: str
122 grafana_edr_folder_id: int
131 - iris_customer_id: int
123 + iris_customer_id: Optional[int] = None
124
125
126 class CustomerProvisionResponse(BaseModel):
backend/app/customer_provisioning/services/provision.py
-14
@@ -6,7 +6,6 @@ from loguru import logger
6 from sqlalchemy.ext.asyncio import AsyncSession
7
8 from app.agents.routes.agents import check_wazuh_manager_version
9 -from app.connectors.dfir_iris.utils.universal import verify_dfir_iris_connection
9 from app.connectors.grafana.schema.dashboards import DashboardProvisionRequest
10 from app.connectors.grafana.services.dashboards import provision_dashboards
11 from app.connectors.grafana.utils.universal import verify_grafana_connection
@@ -20,8 +19,6 @@ from app.customer_provisioning.schema.provision import ProvisionHaProxyRequest
19 from app.customer_provisioning.schema.provision import ProvisionNewCustomer
20 from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerRequest
21 from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerResponse
23 -from app.customer_provisioning.services.dfir_iris import add_user_to_all_customers
24 -from app.customer_provisioning.services.dfir_iris import create_customer
22 from app.customer_provisioning.services.grafana import create_grafana_datasource
23 from app.customer_provisioning.services.grafana import create_grafana_folder
24 from app.customer_provisioning.services.grafana import create_grafana_organization
@@ -56,7 +53,6 @@ async def verify_required_tools() -> None:
53 await verify_connection("Graylog", verify_graylog_connection)
54 await verify_connection("Wazuh-Manager", verify_wazuh_manager_connection)
55 await verify_connection("Grafana", verify_grafana_connection)
59 - await verify_connection("DFIR-IRIS", verify_dfir_iris_connection)
56
57
58 # ! MAIN FUNCTION ! #
@@ -130,16 +126,6 @@ async def provision_wazuh_customer(
126 ),
127 )
128
133 - try:
134 - provision_meta_data["iris_customer_id"] = (await create_customer(request.customer_name)).data.customer_id
135 - except Exception:
136 - provision_meta_data["iris_customer_id"] = 2
137 -
138 - if request.dfir_iris_username is not None:
139 - await add_user_to_all_customers(
140 - request.dfir_iris_username,
141 - )
142 -
129 customer_provision_meta = CustomerProvisionMeta(**provision_meta_data)
130 customer_meta = await update_customer_meta_table(
131 request,
backend/app/db/db_populate.py
+45 -1
@@ -82,7 +82,7 @@ def get_connectors_list():
82 ),
83 ("Graylog", "5.0.7", "username_password", "Connection to Graylog."),
84 ("Shuffle", "1.1.0", "api_key", "Connection to Shuffle."),
85 - ("DFIR-IRIS", "2.0", "api_key", "Connection to DFIR-IRIS."),
85 + # ("DFIR-IRIS", "2.0", "api_key", "Connection to DFIR-IRIS."),
86 (
87 "Velociraptor",
88 "0.6.8",
@@ -149,6 +149,20 @@ def get_connectors_list():
149 return [load_connector_data(*connector) for connector in connectors]
150
151
152 +def delete_connectors_list():
153 + """
154 + Get a list of connectors with their respective versions and authentication methods.
155 +
156 + Returns:
157 + list: A list of connector data, where each item contains the connector name, version, and authentication method.
158 + """
159 + connectors = [
160 + "DFIR-IRIS",
161 + ]
162 +
163 + return connectors
164 +
165 +
166 async def add_connectors_if_not_exist(session: AsyncSession):
167 """
168 Adds connectors to the database if they do not already exist.
@@ -160,8 +174,10 @@ async def add_connectors_if_not_exist(session: AsyncSession):
174 None
175 """
176 connector_list = get_connectors_list()
177 + logger.info("Checking for existence of connectors.")
178
179 for connector_data in connector_list:
180 + logger.info(f"Checking for existence of connector {connector_data['connector_name']}")
181 query = select(Connectors).where(
182 Connectors.connector_name == connector_data["connector_name"],
183 )
@@ -176,6 +192,34 @@ async def add_connectors_if_not_exist(session: AsyncSession):
192 await session.commit()
193
194
195 +async def delete_connectors_if_exist(session: AsyncSession):
196 + """
197 + Deletes connectors from the database if they already exist.
198 +
199 + Args:
200 + session (AsyncSession): The database session.
201 +
202 + Returns:
203 + None
204 + """
205 + connector_list = delete_connectors_list()
206 + logger.info("Checking for existence of connectors. This connector will be deleted.")
207 +
208 + for connector_data in connector_list:
209 + logger.info(f"Checking for existence of connector {connector_data}")
210 + query = select(Connectors).where(
211 + Connectors.connector_name == connector_data,
212 + )
213 + result = await session.execute(query)
214 + existing_connector = result.scalars().first()
215 +
216 + if existing_connector is not None:
217 + await session.delete(existing_connector)
218 + logger.info(f"Deleted connector: {connector_data}")
219 +
220 + await session.commit()
221 +
222 +
223 async def add_roles_if_not_exist(session: AsyncSession) -> None:
224 """
225 Adds roles to the database if they do not already exist.
backend/app/db/db_setup.py
+22 -1
@@ -21,6 +21,7 @@ from app.db.db_populate import add_available_network_connectors_auth_keys_if_not
21 from app.db.db_populate import add_available_network_connectors_if_not_exist
22 from app.db.db_populate import add_connectors_if_not_exist
23 from app.db.db_populate import add_roles_if_not_exist
24 +from app.db.db_populate import delete_connectors_if_exist
25 from app.db.db_session import SQLALCHEMY_DATABASE_URI
26 from app.db.db_session import db_password
27
@@ -101,7 +102,7 @@ def apply_migrations():
102 # Apply migrations to the latest revision
103 try:
104 command.upgrade(alembic_cfg, "head")
104 - except OperationalError as e:
105 + except Exception as e: # Catch any exception
106 logger.error(f"Error applying migrations: {e}")
107 raise e
108
@@ -122,6 +123,26 @@ async def add_connectors(async_engine):
123 ) as session: # Create an AsyncSession, not just a connection
124 async with session.begin(): # Start a transaction
125 await add_connectors_if_not_exist(session)
126 + logger.info("Connectors added successfully")
127 +
128 +
129 +async def delete_connectors(async_engine):
130 + """
131 + Deletes connectors from the database.
132 +
133 + Args:
134 + async_engine (AsyncEngine): The async engine used to connect to the database.
135 +
136 + Returns:
137 + None
138 + """
139 + logger.info("Deleting connectors")
140 + async with AsyncSession(
141 + async_engine,
142 + ) as session: # Create an AsyncSession, not just a connection
143 + async with session.begin(): # Start a transaction
144 + await delete_connectors_if_exist(session)
145 + logger.info("Connectors deleted successfully")
146
147
148 async def create_tables(async_engine):
backend/app/incidents/models.py new
+149
@@ -0,0 +1,149 @@
1 +from datetime import datetime
2 +from typing import Dict
3 +from typing import List
4 +from typing import Optional
5 +
6 +from sqlalchemy import PrimaryKeyConstraint
7 +from sqlmodel import JSON
8 +from sqlmodel import Column
9 +from sqlmodel import Field
10 +from sqlmodel import Relationship
11 +from sqlmodel import SQLModel
12 +from sqlmodel import Text
13 +
14 +
15 +class Alert(SQLModel, table=True):
16 + __tablename__ = "incident_management_alert"
17 + id: Optional[int] = Field(default=None, primary_key=True)
18 + alert_name: str = Field(sa_column=Text, nullable=False)
19 + alert_description: str = Field(sa_column=Text, nullable=False)
20 + status: str = Field(max_length=50, nullable=False)
21 + alert_creation_time: datetime = Field(default_factory=datetime.utcnow)
22 + customer_code: str = Field(max_length=50, nullable=False)
23 + time_closed: Optional[datetime] = Field(default=None)
24 + source: str = Field(max_length=50, nullable=False)
25 + assigned_to: str = Field(max_length=50, nullable=True)
26 +
27 + comments: List["Comment"] = Relationship(back_populates="alert")
28 + assets: List["Asset"] = Relationship(back_populates="alert")
29 + cases: List["CaseAlertLink"] = Relationship(back_populates="alert")
30 + tags: List["AlertToTag"] = Relationship(back_populates="alert")
31 +
32 +
33 +class AlertTag(SQLModel, table=True):
34 + __tablename__ = "incident_management_alerttag"
35 + id: Optional[int] = Field(default=None, primary_key=True)
36 + tag: str = Field(max_length=50, nullable=False)
37 +
38 + alerts: List["AlertToTag"] = Relationship(back_populates="tag")
39 +
40 +
41 +class AlertToTag(SQLModel, table=True):
42 + __tablename__ = "incident_management_alert_to_tag"
43 + alert_id: int = Field(foreign_key="incident_management_alert.id", primary_key=True)
44 + tag_id: int = Field(foreign_key="incident_management_alerttag.id", primary_key=True)
45 +
46 + alert: Alert = Relationship(back_populates="tags")
47 + tag: AlertTag = Relationship(back_populates="alerts")
48 +
49 +
50 +class Comment(SQLModel, table=True):
51 + __tablename__ = "incident_management_comment"
52 + id: Optional[int] = Field(default=None, primary_key=True)
53 + alert_id: int = Field(default=None, foreign_key="incident_management_alert.id")
54 + comment: str = Field(sa_column=Text)
55 + user_name: str = Field(max_length=50, nullable=False)
56 + created_at: datetime = Field(default_factory=datetime.utcnow)
57 +
58 + alert: Alert = Relationship(back_populates="comments")
59 +
60 +
61 +class AlertContext(SQLModel, table=True):
62 + __tablename__ = "incident_management_alertcontext"
63 + id: Optional[int] = Field(default=None, primary_key=True)
64 + source: str = Field(max_length=50, nullable=False)
65 + context: Optional[Dict] = Field(sa_column=Column(JSON), nullable=True)
66 +
67 + assets: List["Asset"] = Relationship(back_populates="alert_context")
68 +
69 +
70 +class Asset(SQLModel, table=True):
71 + __tablename__ = "incident_management_asset"
72 + id: Optional[int] = Field(default=None, primary_key=True)
73 + alert_linked: int = Field(default=None, foreign_key="incident_management_alert.id")
74 + asset_name: str = Field(max_length=255, nullable=False)
75 + alert_context_id: int = Field(foreign_key="incident_management_alertcontext.id")
76 + agent_id: Optional[str] = Field(default=None, max_length=50)
77 + velociraptor_id: Optional[str] = Field(default=None, max_length=150)
78 + customer_code: str = Field(max_length=50, nullable=False)
79 + index_name: str = Field(max_length=255, nullable=False)
80 + index_id: str = Field(max_length=255, nullable=False)
81 +
82 + alert: Alert = Relationship(back_populates="assets")
83 + alert_context: AlertContext = Relationship(back_populates="assets")
84 +
85 +
86 +class FieldName(SQLModel, table=True):
87 + __tablename__ = "incident_management_fieldname"
88 + id: Optional[int] = Field(default=None, primary_key=True)
89 + source: str = Field(max_length=50, nullable=False)
90 + field_name: str = Field(max_length=100, nullable=False)
91 +
92 +
93 +class AssetFieldName(SQLModel, table=True):
94 + __tablename__ = "incident_management_assetfieldname"
95 + id: Optional[int] = Field(default=None, primary_key=True)
96 + source: str = Field(max_length=50, nullable=False)
97 + field_name: str = Field(max_length=100, nullable=False)
98 +
99 +
100 +class TimestampFieldName(SQLModel, table=True):
101 + __tablename__ = "incident_management_timestampfieldname"
102 + id: Optional[int] = Field(default=None, primary_key=True)
103 + source: str = Field(max_length=50, nullable=False)
104 + field_name: str = Field(max_length=100, nullable=False)
105 +
106 +
107 +class AlertTitleFieldName(SQLModel, table=True):
108 + __tablename__ = "incident_management_alerttitlefieldname"
109 + id: Optional[int] = Field(default=None, primary_key=True)
110 + source: str = Field(max_length=50, nullable=False)
111 + field_name: str = Field(max_length=100, nullable=False)
112 +
113 +
114 +class CustomerCodeFieldName(SQLModel, table=True):
115 + __tablename__ = "incident_management_customercodefieldname"
116 + id: Optional[int] = Field(default=None, primary_key=True)
117 + source: str = Field(max_length=50, nullable=False)
118 + field_name: str = Field(max_length=100, nullable=False)
119 +
120 +
121 +class Case(SQLModel, table=True):
122 + __tablename__ = "incident_management_case"
123 + id: Optional[int] = Field(default=None, primary_key=True)
124 + case_name: str = Field(max_length=10000, nullable=False)
125 + case_description: str = Field(sa_column=Text)
126 + case_creation_time: datetime = Field(default_factory=datetime.utcnow)
127 + case_status: str = Field(max_length=50, nullable=False)
128 + assigned_to: str = Field(max_length=50, nullable=True)
129 +
130 + alerts: List["CaseAlertLink"] = Relationship(back_populates="case")
131 +
132 +
133 +class CaseAlertLink(SQLModel, table=True):
134 + __tablename__ = "incident_management_casealertlink"
135 + case_id: Optional[int] = Field(default=None, foreign_key="incident_management_case.id")
136 + alert_id: Optional[int] = Field(default=None, foreign_key="incident_management_alert.id")
137 +
138 + case: Case = Relationship(back_populates="alerts")
139 + alert: Alert = Relationship(back_populates="cases")
140 +
141 + __table_args__ = (PrimaryKeyConstraint("case_id", "alert_id"),)
142 +
143 +
144 +class Notification(SQLModel, table=True):
145 + __tablename__ = "incident_management_notification"
146 + id: Optional[int] = Field(default=None, primary_key=True)
147 + customer_code: str = Field(max_length=50, nullable=False)
148 + shuffle_workflow_id: str = Field(max_length=1000, nullable=False)
149 + enabled: bool = Field(default=True)
backend/app/incidents/routes/db_operations.py new
+565
@@ -0,0 +1,565 @@
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from fastapi import HTTPException
4 +from fastapi import Query
5 +from loguru import logger
6 +from sqlalchemy import select
7 +from sqlalchemy.ext.asyncio import AsyncSession
8 +
9 +from app.auth.services.universal import select_all_users
10 +from app.connectors.wazuh_indexer.utils.universal import (
11 + get_available_indices_via_source,
12 +)
13 +from app.connectors.wazuh_indexer.utils.universal import get_index_mappings_key_names
14 +from app.connectors.wazuh_indexer.utils.universal import get_index_source
15 +from app.customer_provisioning.routes.provision import check_customer_exists
16 +from app.db.db_session import get_db
17 +from app.db.universal_models import Customers
18 +from app.incidents.models import Alert
19 +from app.incidents.models import FieldName
20 +from app.incidents.schema.db_operations import AlertContextCreate
21 +from app.incidents.schema.db_operations import AlertContextResponse
22 +from app.incidents.schema.db_operations import AlertCreate
23 +from app.incidents.schema.db_operations import AlertOutResponse
24 +from app.incidents.schema.db_operations import AlertResponse
25 +from app.incidents.schema.db_operations import AlertStatus
26 +from app.incidents.schema.db_operations import AlertTagCreate
27 +from app.incidents.schema.db_operations import AlertTagDelete
28 +from app.incidents.schema.db_operations import AlertTagResponse
29 +from app.incidents.schema.db_operations import AssetCreate
30 +from app.incidents.schema.db_operations import AssetResponse
31 +from app.incidents.schema.db_operations import AssignedToAlert
32 +from app.incidents.schema.db_operations import AssignedToCase
33 +from app.incidents.schema.db_operations import AvailableIndicesResponse
34 +from app.incidents.schema.db_operations import AvailableSourcesResponse
35 +from app.incidents.schema.db_operations import AvailableUsersResponse
36 +from app.incidents.schema.db_operations import CaseAlertLinkCreate
37 +from app.incidents.schema.db_operations import CaseAlertLinkResponse
38 +from app.incidents.schema.db_operations import CaseCreate
39 +from app.incidents.schema.db_operations import CaseCreateFromAlert
40 +from app.incidents.schema.db_operations import CaseOutResponse
41 +from app.incidents.schema.db_operations import CaseResponse
42 +from app.incidents.schema.db_operations import CommentCreate
43 +from app.incidents.schema.db_operations import CommentResponse
44 +from app.incidents.schema.db_operations import ConfiguredSourcesResponse
45 +from app.incidents.schema.db_operations import FieldAndAssetNames
46 +from app.incidents.schema.db_operations import FieldAndAssetNamesResponse
47 +from app.incidents.schema.db_operations import MappingsResponse
48 +from app.incidents.schema.db_operations import NotificationResponse
49 +from app.incidents.schema.db_operations import PutNotification
50 +from app.incidents.schema.db_operations import SocfortressRecommendsWazuhAlertTitleName
51 +from app.incidents.schema.db_operations import SocfortressRecommendsWazuhAssetName
52 +from app.incidents.schema.db_operations import SocfortressRecommendsWazuhFieldNames
53 +from app.incidents.schema.db_operations import SocfortressRecommendsWazuhResponse
54 +from app.incidents.schema.db_operations import SocfortressRecommendsWazuhTimeFieldName
55 +from app.incidents.schema.db_operations import UpdateAlertStatus
56 +from app.incidents.schema.db_operations import UpdateCaseStatus
57 +from app.incidents.services.db_operations import add_alert_title_name
58 +from app.incidents.services.db_operations import add_asset_name
59 +from app.incidents.services.db_operations import add_field_name
60 +from app.incidents.services.db_operations import add_timefield_name
61 +from app.incidents.services.db_operations import alert_total
62 +from app.incidents.services.db_operations import alert_total_by_alert_title
63 +from app.incidents.services.db_operations import alert_total_by_assest_name
64 +from app.incidents.services.db_operations import alerts_closed
65 +from app.incidents.services.db_operations import alerts_closed_by_alert_title
66 +from app.incidents.services.db_operations import alerts_closed_by_asset_name
67 +from app.incidents.services.db_operations import alerts_closed_by_assigned_to
68 +from app.incidents.services.db_operations import alerts_closed_by_tag
69 +from app.incidents.services.db_operations import alerts_in_progress
70 +from app.incidents.services.db_operations import alerts_in_progress_by_alert_title
71 +from app.incidents.services.db_operations import alerts_in_progress_by_assest_name
72 +from app.incidents.services.db_operations import alerts_in_progress_by_assigned_to
73 +from app.incidents.services.db_operations import alerts_in_progress_by_tag
74 +from app.incidents.services.db_operations import alerts_open
75 +from app.incidents.services.db_operations import alerts_open_by_alert_title
76 +from app.incidents.services.db_operations import alerts_open_by_assest_name
77 +from app.incidents.services.db_operations import alerts_open_by_assigned_to
78 +from app.incidents.services.db_operations import alerts_open_by_tag
79 +from app.incidents.services.db_operations import alerts_total_by_assigned_to
80 +from app.incidents.services.db_operations import alerts_total_by_tag
81 +from app.incidents.services.db_operations import create_alert
82 +from app.incidents.services.db_operations import create_alert_context
83 +from app.incidents.services.db_operations import create_alert_tag
84 +from app.incidents.services.db_operations import create_asset
85 +from app.incidents.services.db_operations import create_case
86 +from app.incidents.services.db_operations import create_case_alert_link
87 +from app.incidents.services.db_operations import create_case_from_alert
88 +from app.incidents.services.db_operations import create_comment
89 +from app.incidents.services.db_operations import delete_alert
90 +from app.incidents.services.db_operations import delete_alert_tag
91 +from app.incidents.services.db_operations import delete_alert_title_name
92 +from app.incidents.services.db_operations import delete_asset_name
93 +from app.incidents.services.db_operations import delete_case
94 +from app.incidents.services.db_operations import delete_field_name
95 +from app.incidents.services.db_operations import delete_timefield_name
96 +from app.incidents.services.db_operations import get_alert_by_id
97 +from app.incidents.services.db_operations import get_alert_context_by_id
98 +from app.incidents.services.db_operations import get_alert_title_names
99 +from app.incidents.services.db_operations import get_asset_names
100 +from app.incidents.services.db_operations import get_case_by_id
101 +from app.incidents.services.db_operations import get_customer_notification
102 +from app.incidents.services.db_operations import get_field_names
103 +from app.incidents.services.db_operations import get_timefield_names
104 +from app.incidents.services.db_operations import is_alert_linked_to_case
105 +from app.incidents.services.db_operations import list_alert_by_assigned_to
106 +from app.incidents.services.db_operations import list_alert_by_status
107 +from app.incidents.services.db_operations import list_alerts
108 +from app.incidents.services.db_operations import list_alerts_by_asset_name
109 +from app.incidents.services.db_operations import list_alerts_by_tag
110 +from app.incidents.services.db_operations import list_alerts_by_title
111 +from app.incidents.services.db_operations import list_cases
112 +from app.incidents.services.db_operations import list_cases_by_assigned_to
113 +from app.incidents.services.db_operations import list_cases_by_status
114 +from app.incidents.services.db_operations import put_customer_notification
115 +from app.incidents.services.db_operations import replace_alert_title_name
116 +from app.incidents.services.db_operations import replace_asset_name
117 +from app.incidents.services.db_operations import replace_field_name
118 +from app.incidents.services.db_operations import replace_timefield_name
119 +from app.incidents.services.db_operations import update_alert_assigned_to
120 +from app.incidents.services.db_operations import update_alert_status
121 +from app.incidents.services.db_operations import update_case_assigned_to
122 +from app.incidents.services.db_operations import update_case_status
123 +from app.incidents.services.db_operations import validate_source_exists
124 +
125 +incidents_db_operations_router = APIRouter()
126 +
127 +
128 +@incidents_db_operations_router.get("/notification/{customer_code}", response_model=NotificationResponse)
129 +async def get_customer_notification_endpoint(
130 + customer_code: str,
131 + _customer: Customers = Depends(check_customer_exists),
132 + db: AsyncSession = Depends(get_db),
133 +):
134 + return NotificationResponse(
135 + notifications=await get_customer_notification(customer_code, db),
136 + success=True,
137 + message="Notification retrieved successfully",
138 + )
139 +
140 +
141 +@incidents_db_operations_router.put("/notification", response_model=NotificationResponse)
142 +async def put_customer_notification_endpoint(
143 + notification: PutNotification,
144 + _customer: Customers = Depends(check_customer_exists),
145 + db: AsyncSession = Depends(get_db),
146 +):
147 + await put_customer_notification(notification, db)
148 + return NotificationResponse(
149 + notifications=await get_customer_notification(notification.customer_code, db),
150 + success=True,
151 + message="Notification updated successfully",
152 + )
153 +
154 +
155 +@incidents_db_operations_router.get("/available-source/{index_name}", response_model=AvailableSourcesResponse)
156 +async def get_available_source_values(index_name: str, session: AsyncSession = Depends(get_db)):
157 + return AvailableSourcesResponse(source=await get_index_source(index_name), success=True, message="Source retrieved successfully")
158 +
159 +
160 +@incidents_db_operations_router.get("/available-indices/{source}", response_model=AvailableIndicesResponse)
161 +async def get_available_indices(source: str, session: AsyncSession = Depends(get_db)):
162 + return AvailableIndicesResponse(
163 + indices=await get_available_indices_via_source(source),
164 + success=True,
165 + message="Indices retrieved successfully",
166 + )
167 +
168 +
169 +@incidents_db_operations_router.get("/socfortress/recommends/wazuh", response_model=SocfortressRecommendsWazuhResponse)
170 +async def get_socfortress_recommends_wazuh(session: AsyncSession = Depends(get_db)):
171 + return SocfortressRecommendsWazuhResponse(
172 + field_names=[field.value for field in SocfortressRecommendsWazuhFieldNames],
173 + asset_name=SocfortressRecommendsWazuhAssetName.agent_name.value,
174 + timefield_name=SocfortressRecommendsWazuhTimeFieldName.timestamp_utc.value,
175 + alert_title_name=SocfortressRecommendsWazuhAlertTitleName.rule_description.value,
176 + source="wazuh",
177 + success=True,
178 + message="Field names and asset names retrieved successfully",
179 + )
180 +
181 +
182 +@incidents_db_operations_router.get("/configured/sources", response_model=ConfiguredSourcesResponse)
183 +async def get_configured_sources(session: AsyncSession = Depends(get_db)):
184 + query = select(FieldName.source).distinct()
185 + result = await session.execute(query)
186 + return ConfiguredSourcesResponse(sources=[row[0] for row in result], success=True, message="Configured sources retrieved successfully")
187 +
188 +
189 +@incidents_db_operations_router.delete("/configured/sources/{source}")
190 +async def delete_configured_source(source: str, session: AsyncSession = Depends(get_db)):
191 + # Fully deletes the configured sources `field_names`, `asset_name`, timefield_name`, alert_title_name`
192 + field_names = await get_field_names(source, session)
193 + asset_name = await get_asset_names(source, session)
194 + timefield_name = await get_timefield_names(source, session)
195 + alert_title_name = await get_alert_title_names(source, session)
196 +
197 + logger.info(
198 + f"Field names found: {field_names}, Asset name found: {asset_name}, Timefield name found: {timefield_name}, Alert title name found: {alert_title_name}",
199 + )
200 +
201 + for field_name in field_names:
202 + await delete_field_name(source, field_name, session)
203 +
204 + await delete_asset_name(source, asset_name, session)
205 +
206 + await delete_timefield_name(source, timefield_name, session)
207 +
208 + await delete_alert_title_name(source, alert_title_name, session)
209 +
210 + logger.info(f"Field names and asset names deleted successfully for source {source}. Committing changes to the database")
211 +
212 + await session.commit()
213 +
214 + return {"message": f"Configured source {source} deleted successfully", "success": True}
215 +
216 +
217 +@incidents_db_operations_router.get("/mappings/fields-assets-title-and-timefield", response_model=MappingsResponse)
218 +async def get_wazuh_fields_and_assets(index_name: str, session: AsyncSession = Depends(get_db)):
219 + index_mapping = await get_index_mappings_key_names(index_name)
220 + return MappingsResponse(available_mappings=index_mapping, success=True, message="Field names and asset names retrieved successfully")
221 +
222 +
223 +@incidents_db_operations_router.get("/fields-assets-title-and-timefield", response_model=FieldAndAssetNamesResponse)
224 +async def get_source_fields_and_assets(source: str, session: AsyncSession = Depends(get_db)):
225 + await validate_source_exists(source, session)
226 + return FieldAndAssetNamesResponse(
227 + field_names=await get_field_names(source, session),
228 + asset_name=await get_asset_names(source, session),
229 + timefield_name=await get_timefield_names(source, session),
230 + alert_title_name=await get_alert_title_names(source, session),
231 + source=source,
232 + success=True,
233 + message="Field names and asset names retrieved successfully",
234 + )
235 +
236 +
237 +@incidents_db_operations_router.post("/fields-assets-title-and-timefield")
238 +async def create_wazuh_fields_and_assets(names: FieldAndAssetNames, session: AsyncSession = Depends(get_db)):
239 + for field_name in names.field_names:
240 + await add_field_name(names.source, field_name, session)
241 +
242 + await add_asset_name(names.source, names.asset_name, session)
243 +
244 + await add_timefield_name(names.source, names.timefield_name, session)
245 +
246 + await add_alert_title_name(names.source, names.alert_title_name, session)
247 +
248 + logger.info(f"Field names and asset names created successfully for source {names.source}")
249 +
250 + await session.commit()
251 +
252 + return {"message": "Field names and asset names created successfully", "success": True}
253 +
254 +
255 +@incidents_db_operations_router.put("/fields-assets-title-and-timefield")
256 +async def update_fields_and_assets(names: FieldAndAssetNames, session: AsyncSession = Depends(get_db)):
257 + await replace_field_name(names.source, names.field_names, session)
258 +
259 + await replace_asset_name(names.source, names.asset_name, session)
260 +
261 + await replace_timefield_name(names.source, names.timefield_name, session)
262 +
263 + await replace_alert_title_name(names.source, names.alert_title_name, session)
264 +
265 + return {"message": "Field names and asset names created successfully", "success": True}
266 +
267 +
268 +@incidents_db_operations_router.delete("/delete-fields-assets-title-and-timefield")
269 +async def delete_wazuh_fields_and_assets(names: FieldAndAssetNames, session: AsyncSession = Depends(get_db)):
270 + for field_name in names.field_names:
271 + await delete_field_name(names.source, field_name, session)
272 +
273 + await delete_asset_name(names.source, names.asset_name, session)
274 +
275 + await delete_timefield_name(names.source, names.timefield_name, session)
276 +
277 + await delete_alert_title_name(names.source, names.alert_title_name, session)
278 +
279 + await session.commit()
280 +
281 + return {"message": "Field names and asset names deleted successfully", "success": True}
282 +
283 +
284 +@incidents_db_operations_router.delete("/field_name/{field_name}/{source}", deprecated=True)
285 +async def delete_field_name_endpoint(field_name: str, source: str, db: AsyncSession = Depends(get_db)):
286 + return await delete_field_name(source, field_name, db)
287 +
288 +
289 +@incidents_db_operations_router.delete("/asset_name/{asset_name}/{source}", deprecated=True)
290 +async def delete_asset_name_endpoint(asset_name: str, source: str, db: AsyncSession = Depends(get_db)):
291 + return await delete_asset_name(source, asset_name, db)
292 +
293 +
294 +@incidents_db_operations_router.delete("/timefield_name/{timefield_name}/{source}", deprecated=True)
295 +async def delete_timefield_name_endpoint(timefield_name: str, source: str, db: AsyncSession = Depends(get_db)):
296 + return await delete_timefield_name(source, timefield_name, db)
297 +
298 +
299 +@incidents_db_operations_router.delete("/alert_title_name/{alert_title_name}/{source}", deprecated=True)
300 +async def delete_alert_title_name_endpoint(alert_title_name: str, source: str, db: AsyncSession = Depends(get_db)):
301 + return await delete_alert_title_name(source, alert_title_name, db)
302 +
303 +
304 +@incidents_db_operations_router.post("/alert", response_model=Alert)
305 +async def create_alert_endpoint(alert: AlertCreate, db: AsyncSession = Depends(get_db)):
306 + return await create_alert(alert, db)
307 +
308 +
309 +@incidents_db_operations_router.put("/alert/status", response_model=AlertResponse)
310 +async def update_alert_status_endpoint(alert_status: UpdateAlertStatus, db: AsyncSession = Depends(get_db)):
311 + return AlertResponse(alert=await update_alert_status(alert_status, db), success=True, message="Alert status updated successfully")
312 +
313 +
314 +@incidents_db_operations_router.post("/alert/comment", response_model=CommentResponse)
315 +async def create_comment_endpoint(comment: CommentCreate, db: AsyncSession = Depends(get_db)):
316 + return CommentResponse(comment=await create_comment(comment, db), success=True, message="Comment created successfully")
317 +
318 +
319 +@incidents_db_operations_router.get("/alert/available-users", response_model=AvailableUsersResponse)
320 +async def get_available_users(db: AsyncSession = Depends(get_db)):
321 + all_users = await select_all_users()
322 + return AvailableUsersResponse(
323 + available_users=[user.username for user in all_users],
324 + success=True,
325 + message="Available users retrieved successfully",
326 + )
327 +
328 +
329 +@incidents_db_operations_router.put("/alert/assigned-to", response_model=AlertResponse)
330 +async def update_assigned_to_endpoint(assigned_to: AssignedToAlert, db: AsyncSession = Depends(get_db)):
331 + all_users = await select_all_users()
332 + user_names = [user.username for user in all_users]
333 + if assigned_to.assigned_to not in user_names:
334 + raise HTTPException(status_code=400, detail="User does not exist")
335 + return AlertResponse(
336 + alert=await update_alert_assigned_to(assigned_to.alert_id, assigned_to.assigned_to, db),
337 + success=True,
338 + message="Alert assigned to user successfully",
339 + )
340 +
341 +
342 +@incidents_db_operations_router.post("/alert/context", response_model=AlertContextResponse)
343 +async def create_alert_context_endpoint(alert_context: AlertContextCreate, db: AsyncSession = Depends(get_db)):
344 + return AlertContextResponse(
345 + alert_context=await create_alert_context(alert_context, db),
346 + success=True,
347 + message="Alert context created successfully",
348 + )
349 +
350 +
351 +@incidents_db_operations_router.get("/alert/context/{alert_context_id}", response_model=AlertContextResponse)
352 +async def get_alert_context_by_id_endpoint(alert_context_id: int, db: AsyncSession = Depends(get_db)):
353 + return AlertContextResponse(
354 + alert_context=await get_alert_context_by_id(alert_context_id, db),
355 + success=True,
356 + message="Alert context retrieved successfully",
357 + )
358 +
359 +
360 +@incidents_db_operations_router.post("/alert/asset", response_model=AssetResponse)
361 +async def create_asset_endpoint(asset: AssetCreate, db: AsyncSession = Depends(get_db)):
362 + return AssetResponse(asset=await create_asset(asset, db), success=True, message="Asset created successfully")
363 +
364 +
365 +@incidents_db_operations_router.post("/alert/tag", response_model=AlertTagResponse)
366 +async def create_alert_tag_endpoint(alert_tag: AlertTagCreate, db: AsyncSession = Depends(get_db)):
367 + return AlertTagResponse(alert_tag=await create_alert_tag(alert_tag, db), success=True, message="Alert tag created successfully")
368 +
369 +
370 +@incidents_db_operations_router.get("/alert/tag/{tag}", response_model=AlertOutResponse)
371 +async def list_alerts_by_tag_endpoint(
372 + tag: str,
373 + db: AsyncSession = Depends(get_db),
374 + page: int = Query(1, ge=1),
375 + page_size: int = Query(25, ge=1),
376 +):
377 + return AlertOutResponse(
378 + alerts=await list_alerts_by_tag(tag, db, page, page_size),
379 + total=await alerts_total_by_tag(db, tag),
380 + open=await alerts_open_by_tag(db, tag),
381 + in_progress=await alerts_in_progress_by_tag(db, tag),
382 + closed=await alerts_closed_by_tag(db, tag),
383 + success=True,
384 + message="Alert's tags retrieved successfully",
385 + )
386 +
387 +
388 +@incidents_db_operations_router.delete("/alert/tag", response_model=AlertTagResponse)
389 +async def delete_alert_tag_endpoint(alert_tag: AlertTagDelete, db: AsyncSession = Depends(get_db)):
390 + return AlertTagResponse(
391 + alert_tag=await delete_alert_tag(alert_tag.alert_id, alert_tag.tag_id, db),
392 + success=True,
393 + message="Alert tag deleted successfully",
394 + )
395 +
396 +
397 +@incidents_db_operations_router.post("/case/create", response_model=CaseResponse)
398 +async def create_case_endpoint(case: CaseCreate, db: AsyncSession = Depends(get_db)):
399 + return CaseResponse(case=await create_case(case, db), success=True, message="Case created successfully")
400 +
401 +
402 +@incidents_db_operations_router.post("/case/alert-link", response_model=CaseAlertLinkResponse)
403 +async def create_case_alert_link_endpoint(case_alert_link: CaseAlertLinkCreate, db: AsyncSession = Depends(get_db)):
404 + return CaseAlertLinkResponse(
405 + case_alert_link=await create_case_alert_link(case_alert_link, db),
406 + success=True,
407 + message="Case alert link created successfully",
408 + )
409 +
410 +
411 +@incidents_db_operations_router.post("/case/from-alert", response_model=CaseAlertLinkResponse)
412 +async def create_case_from_alert_endpoint(alert_id: CaseCreateFromAlert, db: AsyncSession = Depends(get_db)):
413 + case = await create_case_from_alert(alert_id.alert_id, db)
414 + if case is None:
415 + return CaseResponse(case=None, success=False, message="Case not created")
416 + return CaseAlertLinkResponse(
417 + case_alert_link=await create_case_alert_link(CaseAlertLinkCreate(case_id=case.id, alert_id=alert_id.alert_id), db),
418 + success=True,
419 + message="Case created from alert successfully",
420 + )
421 +
422 +
423 +@incidents_db_operations_router.get("/alerts", response_model=AlertOutResponse)
424 +async def list_alerts_endpoint(page: int = Query(1, ge=1), page_size: int = Query(25, ge=1), db: AsyncSession = Depends(get_db)):
425 + return AlertOutResponse(
426 + alerts=await list_alerts(db, page=page, page_size=page_size),
427 + total=await alert_total(db),
428 + open=await alerts_open(db),
429 + in_progress=await alerts_in_progress(db),
430 + closed=await alerts_closed(db),
431 + success=True,
432 + message="Alerts retrieved successfully",
433 + )
434 +
435 +
436 +@incidents_db_operations_router.get("/alert/{alert_id}", response_model=AlertOutResponse)
437 +async def get_alert_by_id_endpoint(alert_id: int, db: AsyncSession = Depends(get_db)):
438 + return AlertOutResponse(alerts=[await get_alert_by_id(alert_id, db)], success=True, message="Alert retrieved successfully")
439 +
440 +
441 +@incidents_db_operations_router.delete("/alert/{alert_id}")
442 +async def delete_alert_endpoint(alert_id: int, db: AsyncSession = Depends(get_db)):
443 + await is_alert_linked_to_case(alert_id, db)
444 + await delete_alert(alert_id, db)
445 + return {"message": "Alert deleted successfully", "success": True}
446 +
447 +
448 +@incidents_db_operations_router.get("/alerts/status/{status}", response_model=AlertOutResponse)
449 +async def list_alerts_by_status_endpoint(
450 + status: AlertStatus,
451 + page: int = Query(1, ge=1),
452 + page_size: int = Query(25, ge=1),
453 + db: AsyncSession = Depends(get_db),
454 +):
455 + if status not in AlertStatus:
456 + raise HTTPException(status_code=400, detail="Invalid status")
457 + return AlertOutResponse(
458 + alerts=await list_alert_by_status(status.value, db, page=page, page_size=page_size),
459 + total=await alert_total(db),
460 + open=await alerts_open(db),
461 + in_progress=await alerts_in_progress(db),
462 + closed=await alerts_closed(db),
463 + success=True,
464 + message="Alerts retrieved successfully",
465 + )
466 +
467 +
468 +@incidents_db_operations_router.get("/alerts/assigned-to/{assigned_to}", response_model=AlertOutResponse)
469 +async def list_alerts_by_assigned_to_endpoint(
470 + assigned_to: str,
471 + page: int = Query(1, ge=1),
472 + page_size: int = Query(25, ge=1),
473 + db: AsyncSession = Depends(get_db),
474 +):
475 + return AlertOutResponse(
476 + alerts=await list_alert_by_assigned_to(assigned_to, db, page=page, page_size=page_size),
477 + total=await alerts_total_by_assigned_to(db, assigned_to),
478 + open=await alerts_open_by_assigned_to(db, assigned_to),
479 + in_progress=await alerts_in_progress_by_assigned_to(db, assigned_to),
480 + closed=await alerts_closed_by_assigned_to(db, assigned_to),
481 + success=True,
482 + message="Alerts retrieved successfully",
483 + )
484 +
485 +
486 +@incidents_db_operations_router.get("/alerts/asset/{asset_name}", response_model=AlertOutResponse)
487 +async def list_alerts_by_asset_name_endpoint(
488 + asset_name: str,
489 + page: int = Query(1, ge=1),
490 + page_size: int = Query(25, ge=1),
491 + db: AsyncSession = Depends(get_db),
492 +):
493 + return AlertOutResponse(
494 + alerts=await list_alerts_by_asset_name(asset_name, db, page=page, page_size=page_size),
495 + total=await alert_total_by_assest_name(db, asset_name),
496 + open=await alerts_open_by_assest_name(db, asset_name),
497 + in_progress=await alerts_in_progress_by_assest_name(db, asset_name),
498 + closed=await alerts_closed_by_asset_name(db, asset_name),
499 + success=True,
500 + message="Alerts retrieved successfully",
501 + )
502 +
503 +
504 +@incidents_db_operations_router.get("/alerts/title/{title}", response_model=AlertOutResponse)
505 +async def list_alerts_by_title_endpoint(
506 + title: str,
507 + page: int = Query(1, ge=1),
508 + page_size: int = Query(25, ge=1),
509 + db: AsyncSession = Depends(get_db),
510 +):
511 + return AlertOutResponse(
512 + alerts=await list_alerts_by_title(title, db, page=page, page_size=page_size),
513 + total=await alert_total_by_alert_title(db, title),
514 + open=await alerts_open_by_alert_title(db, title),
515 + in_progress=await alerts_in_progress_by_alert_title(db, title),
516 + closed=await alerts_closed_by_alert_title(db, title),
517 + success=True,
518 + message="Alerts retrieved successfully",
519 + )
520 +
521 +
522 +@incidents_db_operations_router.get("/cases", response_model=CaseOutResponse)
523 +async def list_cases_endpoint(db: AsyncSession = Depends(get_db)):
524 + return CaseOutResponse(cases=await list_cases(db), success=True, message="Cases retrieved successfully")
525 +
526 +
527 +@incidents_db_operations_router.get("/case/{case_id}", response_model=CaseOutResponse)
528 +async def get_case_by_id_endpoint(case_id: int, db: AsyncSession = Depends(get_db)):
529 + return CaseOutResponse(cases=[await get_case_by_id(case_id, db)], success=True, message="Case retrieved successfully")
530 +
531 +
532 +@incidents_db_operations_router.put("/case/status", response_model=CaseOutResponse)
533 +async def update_case_status_endpoint(case_status: UpdateCaseStatus, db: AsyncSession = Depends(get_db)):
534 + return CaseOutResponse(cases=[await update_case_status(case_status, db)], success=True, message="Case status updated successfully")
535 +
536 +
537 +@incidents_db_operations_router.put("/case/assigned-to", response_model=CaseOutResponse)
538 +async def update_case_assigned_to_endpoint(assigned_to: AssignedToCase, db: AsyncSession = Depends(get_db)):
539 + all_users = await select_all_users()
540 + user_names = [user.username for user in all_users]
541 + if assigned_to.assigned_to not in user_names:
542 + raise HTTPException(status_code=400, detail="User does not exist")
543 + return CaseOutResponse(
544 + cases=[await update_case_assigned_to(assigned_to.case_id, assigned_to.assigned_to, db)],
545 + success=True,
546 + message="Case assigned to user successfully",
547 + )
548 +
549 +
550 +@incidents_db_operations_router.delete("/case/{case_id}")
551 +async def delete_case_endpoint(case_id: int, db: AsyncSession = Depends(get_db)):
552 + await delete_case(case_id, db)
553 + return {"message": "Case deleted successfully", "success": True}
554 +
555 +
556 +@incidents_db_operations_router.get("/case/status/{status}", response_model=CaseOutResponse)
557 +async def list_cases_by_status_endpoint(status: AlertStatus, db: AsyncSession = Depends(get_db)):
558 + if status not in AlertStatus:
559 + raise HTTPException(status_code=400, detail="Invalid status")
560 + return CaseOutResponse(cases=await list_cases_by_status(status.value, db), success=True, message="Cases retrieved successfully")
561 +
562 +
563 +@incidents_db_operations_router.get("/case/assigned-to/{assigned_to}", response_model=CaseOutResponse)
564 +async def list_cases_by_assigned_to_endpoint(assigned_to: str, db: AsyncSession = Depends(get_db)):
565 + return CaseOutResponse(cases=await list_cases_by_assigned_to(assigned_to, db), success=True, message="Cases retrieved successfully")
backend/app/incidents/routes/incident_alert.py new
+170
@@ -0,0 +1,170 @@
1 +from fastapi import APIRouter
2 +from fastapi import Depends
3 +from fastapi import Security
4 +from loguru import logger
5 +from sqlalchemy.ext.asyncio import AsyncSession
6 +
7 +from app.auth.utils import AuthHandler
8 +from app.db.db_session import get_db
9 +from app.incidents.schema.alert_collection import AlertsPayload
10 +from app.incidents.schema.incident_alert import AlertDetailsResponse
11 +from app.incidents.schema.incident_alert import AlertTimelineResponse
12 +from app.incidents.schema.incident_alert import AutoCreateAlertResponse
13 +from app.incidents.schema.incident_alert import CreateAlertRequest
14 +from app.incidents.schema.incident_alert import CreateAlertRequestRoute
15 +from app.incidents.schema.incident_alert import CreateAlertResponse
16 +from app.incidents.schema.incident_alert import IndexNamesResponse
17 +from app.incidents.services.alert_collection import add_copilot_alert_id
18 +from app.incidents.services.alert_collection import get_alerts_not_created_in_copilot
19 +from app.incidents.services.alert_collection import get_graylog_event_indices
20 +from app.incidents.services.alert_collection import get_original_alert_id
21 +from app.incidents.services.alert_collection import get_original_alert_index_name
22 +from app.incidents.services.incident_alert import create_alert
23 +from app.incidents.services.incident_alert import get_single_alert_details
24 +from app.incidents.services.incident_alert import retrieve_alert_timeline
25 +
26 +incidents_alerts_router = APIRouter()
27 +
28 +
29 +@incidents_alerts_router.get(
30 + "/index/names",
31 + response_model=IndexNamesResponse,
32 + description="Get the Graylog event indices",
33 +)
34 +async def get_index_names_route() -> IndexNamesResponse:
35 + """
36 + Get the Graylog event indices. Get the Graylog event indices for the Graylog events.
37 +
38 + Returns:
39 + List[str]: The list of Graylog event indices.
40 + """
41 + return await get_graylog_event_indices()
42 +
43 +
44 +@incidents_alerts_router.get(
45 + "/alerts/not-created",
46 + description="Get alerts not created in CoPilot",
47 +)
48 +async def get_alerts_not_created_route() -> AlertsPayload:
49 + """
50 + Get alerts not created in CoPilot. Get all the results from the list of indices, where `copilot_alert_id` does not exist.
51 +
52 + Returns:
53 + List[AlertPayloadItem]: The list of alerts that have not been created in CoPilot.
54 + """
55 + return await get_alerts_not_created_in_copilot()
56 +
57 +
58 +@incidents_alerts_router.post(
59 + "/alert/details",
60 + description="Get the details of a single alert",
61 +)
62 +async def get_single_alert_details_route(
63 + create_alert_request: CreateAlertRequestRoute,
64 +) -> AlertDetailsResponse:
65 + """
66 + Get the details of a single alert. Get the details of a single alert based on the alert id.
67 + Takes the alert id and the index name as input.
68 +
69 + Args:
70 + create_alert_request (CreateAlertRequestRoute): The request object containing the details of the alert to be created.
71 +
72 + Returns:
73 + class AlertDetailsResponse(BaseModel): The response object containing the details of the alert.
74 + """
75 + return AlertDetailsResponse(
76 + success=True,
77 + message="Alert details retrieved",
78 + alert_details=await get_single_alert_details(
79 + CreateAlertRequest(index_name=create_alert_request.index_name, alert_id=create_alert_request.index_id),
80 + ),
81 + )
82 +
83 +
84 +@incidents_alerts_router.post(
85 + "/alert/timeline",
86 + description="Get the timeline of an alert",
87 +)
88 +async def get_alert_timeline_route(
89 + alert: CreateAlertRequestRoute,
90 + session: AsyncSession = Depends(get_db),
91 +) -> AlertTimelineResponse:
92 + """
93 + Get the timeline of an alert. This route obtains the process_id from the alert details if it exists
94 + and queries the Indexer for all events with the same process_id and hostname within a 24 hour period.
95 +
96 + Args:
97 + create_alert_request (CreateAlertRequestRoute): The request object containing the details of the alert to be created.
98 +
99 +
100 + Returns:
101 + class AlertTimelineResponse(BaseModel): The response object containing the details of the alert.
102 + """
103 + # await retrieve_alert_timeline(alert, session)
104 + return AlertTimelineResponse(
105 + success=True,
106 + message="Alert timeline retrieved",
107 + alert_timeline=await retrieve_alert_timeline(alert, session),
108 + )
109 +
110 +
111 +@incidents_alerts_router.post(
112 + "/create/manual",
113 + response_model=CreateAlertResponse,
114 + description="Manually create an incident alert in CoPilot",
115 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
116 +)
117 +async def create_alert_manual_route(
118 + create_alert_request: CreateAlertRequest,
119 + session: AsyncSession = Depends(get_db),
120 +) -> CreateAlertResponse:
121 + """
122 + Create an incident alert in CoPilot. Manually create an incident alert within CoPilot.
123 + Used via the Alerts, for manual incident alert creation.
124 +
125 + Args:
126 + create_alert_request (CreateAlertRequest): The request object containing the details of the alert to be created.
127 + session (AsyncSession, optional): The database session. Defaults to Depends(get_session).
128 +
129 + Returns:
130 + CreateAlertResponse: The response object containing the result of the alert creation.
131 + """
132 + logger.info(f"Creating alert {create_alert_request.alert_id} in CoPilot")
133 + return CreateAlertResponse(success=True, message="Alert created in CoPilot", alert_id=await create_alert(create_alert_request, session))
134 +
135 +
136 +@incidents_alerts_router.post(
137 + "/create/auto",
138 + response_model=CreateAlertResponse,
139 + description="Is invoked by the scheduler to create an incident alert in CoPilot",
140 +)
141 +async def create_alert_auto_route(
142 + session: AsyncSession = Depends(get_db),
143 +) -> AutoCreateAlertResponse:
144 + """
145 + Create an incident alert in CoPilot. Automatically create an incident alert within CoPilot.
146 + This queries the `gl-events-*` indices for alerts that have not been created in CoPilot.
147 + It is important to note that Graylog must be configured for the alerts.
148 +
149 + Args:
150 + create_alert_request (CreateAlertRequest): The request object containing the details of the alert to be created.
151 + session (AsyncSession, optional): The database session. Defaults to Depends(get_session).
152 +
153 + Returns:
154 + CreateAlertResponse: The response object containing the result of the alert creation.
155 + """
156 + alerts = await get_alerts_not_created_in_copilot()
157 + logger.info(f"Alerts to create in CoPilot: {alerts}")
158 + if len(alerts.alerts) == 0:
159 + return AutoCreateAlertResponse(success=False, message="No alerts to create in CoPilot")
160 + for alert in alerts.alerts:
161 + logger.info(f"Creating alert {alert} in CoPilot")
162 + create_alert_request = CreateAlertRequest(
163 + index_name=await get_original_alert_index_name(origin_context=alert.source.origin_context),
164 + alert_id=await get_original_alert_id(alert.source.origin_context),
165 + )
166 + logger.info(f"Creating alert {create_alert_request.alert_id} in CoPilot")
167 + alert_id = await create_alert(create_alert_request, session)
168 + # ! ADD THE COPILOT ALERT ID TO GRAYLOG EVENT INDEX # !
169 + await add_copilot_alert_id(index_data=CreateAlertRequest(index_name=alert.index, alert_id=alert.id), alert_id=alert_id)
170 + return AutoCreateAlertResponse(success=True, message=f"{len(alerts.alerts)} alerts created in CoPilot")
backend/app/incidents/schema/alert_collection.py new
+67
@@ -0,0 +1,67 @@
1 +from typing import Dict
2 +from typing import List
3 +from typing import Optional
4 +
5 +from pydantic import BaseModel
6 +from pydantic import Field
7 +from pydantic import validator
8 +
9 +
10 +class Fields(BaseModel):
11 + ALERT_ID: Optional[str] = None
12 + ALERT_SOURCE: Optional[str] = None
13 + CUSTOMER_CODE: Optional[str] = None
14 + COPILOT_ALERT_ID: Optional[str] = None
15 +
16 +
17 +class Source(BaseModel):
18 + id: str
19 + event_definition_type: str
20 + event_definition_id: str
21 + origin_context: str
22 + timestamp: str
23 + timestamp_processing: str
24 + timerange_start: Optional[str] = None
25 + timerange_end: Optional[str] = None
26 + streams: List[str]
27 + source_streams: List[str]
28 + message: str
29 + source: str
30 + key_tuple: List[str]
31 + key: str
32 + priority: int
33 + alert: bool
34 + fields: Fields
35 + group_by_fields: Dict = Field(default_factory=dict)
36 + original_alert_id: Optional[str] = Field(None, alias="original_alert_id")
37 + original_alert_index_name: Optional[str] = Field(None, alias="original_alert_index_name")
38 +
39 + @validator("original_alert_id", "original_alert_index_name", allow_reuse=True, pre=True)
40 + def extract_origin_context(cls, v, values, **kwargs):
41 + origin_context = values.get("origin_context", "")
42 + try:
43 + # Assuming the format is always as given in the example
44 + parts = origin_context.split(":")
45 + if len(parts) == 6:
46 + _, _, _, _, index_name, alert_id = parts
47 + if kwargs["field"].name == "original_alert_id":
48 + return alert_id
49 + elif kwargs["field"].name == "original_alert_index_name":
50 + return index_name
51 + except Exception as e:
52 + # Consider logging the exception to understand what's going wrong
53 + print(f"Error parsing origin_context: {e}")
54 + return v
55 +
56 +
57 +class AlertPayloadItem(BaseModel):
58 + index: str = Field(..., alias="_index")
59 + id: str = Field(..., alias="_id")
60 + score: float = Field(..., alias="_score")
61 + source: Source = Field(..., alias="_source")
62 +
63 +
64 +class AlertsPayload(BaseModel):
65 + alerts: List[AlertPayloadItem]
66 + success: Optional[bool] = True
67 + message: Optional[str] = "Success"
backend/app/incidents/schema/db_operations.py new
+335
@@ -0,0 +1,335 @@
1 +from datetime import datetime
2 +from enum import Enum
3 +from typing import Dict
4 +from typing import List
5 +from typing import Optional
6 +
7 +from pydantic import BaseModel
8 +
9 +from app.incidents.models import Alert
10 +from app.incidents.models import AlertContext
11 +from app.incidents.models import AlertTag
12 +from app.incidents.models import Asset
13 +from app.incidents.models import Case
14 +from app.incidents.models import CaseAlertLink
15 +from app.incidents.models import Comment
16 +
17 +
18 +class SocfortressRecommendsWazuhFieldNames(Enum):
19 + # ! Windows Events
20 + data_win_eventdata_commandLine = "data_win_eventdata_commandLine"
21 + data_win_eventdata_parentCommandLine = "data_win_eventdata_parentCommandLine"
22 + data_win_eventdata_parentImage = "data_win_eventdata_parentImage"
23 + data_win_eventdata_parentUser = "data_win_eventdata_parentUser"
24 + data_win_eventdata_image = "data_win_eventdata_image"
25 + data_win_eventdata_user = "data_win_eventdata_user"
26 + rule_mitre_id = "rule_mitre_id"
27 + rule_mitre_tactic = "rule_mitre_tactic"
28 + rule_mitre_technique = "rule_mitre_technique"
29 + data_win_eventdata_company = "data_win_eventdata_company"
30 + data_win_eventdata_hashes = "data_win_eventdata_hashes"
31 + data_win_eventdata_currentDirectory = "data_win_eventdata_currentDirectory"
32 + data_win_eventdata_originalFileName = "data_win_eventdata_originalFileName"
33 + # ! Windows SIGCHECK HITS
34 + data_Path = "data_Path"
35 + # ! Extra Use for Within CoPilot
36 + process_id = "process_id"
37 + sha256 = "sha256"
38 +
39 +
40 +class SocfortressRecommendsWazuhAssetName(Enum):
41 + agent_name = "agent_name"
42 +
43 +
44 +class SocfortressRecommendsWazuhTimeFieldName(Enum):
45 + timestamp_utc = "timestamp_utc"
46 +
47 +
48 +class SocfortressRecommendsWazuhAlertTitleName(Enum):
49 + rule_description = "rule_description"
50 +
51 +
52 +class SocfortressRecommendsWazuhResponse(BaseModel):
53 + field_names: List[str]
54 + asset_name: str
55 + timefield_name: str
56 + alert_title_name: str
57 + source: str
58 + success: bool
59 + message: str
60 +
61 +
62 +class AvailableSourcesResponse(BaseModel):
63 + source: str
64 + success: bool
65 + message: str
66 +
67 +
68 +class AvailableIndicesResponse(BaseModel):
69 + indices: List[str]
70 + success: bool
71 + message: str
72 +
73 +
74 +class ConfiguredSourcesResponse(BaseModel):
75 + sources: List[str]
76 + success: bool
77 + message: str
78 +
79 +
80 +class MappingsResponse(BaseModel):
81 + available_mappings: List[str]
82 + success: bool
83 + message: str
84 +
85 +
86 +class ValidSources(str, Enum):
87 + WAZUH = "wazuh"
88 +
89 +
90 +class AlertStatus(str, Enum):
91 + OPEN = "OPEN"
92 + CLOSED = "CLOSED"
93 + IN_PROGRESS = "IN_PROGRESS"
94 +
95 +
96 +class UpdateAlertStatus(BaseModel):
97 + alert_id: int
98 + status: AlertStatus
99 +
100 +
101 +class UpdateCaseStatus(BaseModel):
102 + case_id: int
103 + status: AlertStatus
104 +
105 +
106 +class AlertResponse(BaseModel):
107 + alert: Alert
108 + success: bool
109 + message: str
110 +
111 +
112 +class CommentResponse(BaseModel):
113 + comment: Comment
114 + success: bool
115 + message: str
116 +
117 +
118 +class AlertContextResponse(BaseModel):
119 + alert_context: AlertContext
120 + success: bool
121 + message: str
122 +
123 +
124 +class AssetResponse(BaseModel):
125 + asset: Asset
126 + success: bool
127 + message: str
128 +
129 +
130 +class AlertTagResponse(BaseModel):
131 + alert_tag: AlertTag
132 + success: bool
133 + message: str
134 +
135 +
136 +class CaseResponse(BaseModel):
137 + case: Case
138 + success: bool
139 + message: str
140 +
141 +
142 +class CaseAlertLinkResponse(BaseModel):
143 + case_alert_link: CaseAlertLink
144 + success: bool
145 + message: str
146 +
147 +
148 +class AvailableUsersResponse(BaseModel):
149 + available_users: List[str]
150 + success: bool
151 + message: str
152 +
153 +
154 +class FieldAndAssetNames(BaseModel):
155 + field_names: List[str]
156 + asset_name: str
157 + timefield_name: str
158 + alert_title_name: str
159 + source: str
160 +
161 +
162 +class FieldAndAssetNamesResponse(BaseModel):
163 + field_names: List[str]
164 + asset_name: str
165 + timefield_name: str
166 + alert_title_name: str
167 + source: str
168 + success: bool
169 + message: str
170 +
171 +
172 +class AssignedToAlert(BaseModel):
173 + alert_id: int
174 + assigned_to: str
175 +
176 +
177 +class AssignedToCase(BaseModel):
178 + case_id: int
179 + assigned_to: str
180 +
181 +
182 +class AlertCreate(BaseModel):
183 + alert_name: str
184 + alert_description: str
185 + status: str
186 + alert_creation_time: datetime
187 + customer_code: str
188 + time_closed: Optional[datetime]
189 + source: str
190 + assigned_to: str
191 +
192 +
193 +class CommentCreate(BaseModel):
194 + alert_id: int
195 + comment: str
196 + user_name: str
197 + created_at: datetime
198 +
199 +
200 +class AlertContextCreate(BaseModel):
201 + source: str
202 + context: Dict
203 +
204 +
205 +class CaseCreate(BaseModel):
206 + case_name: str
207 + case_description: str
208 + case_creation_time: datetime
209 + case_status: str
210 + assigned_to: str
211 +
212 +
213 +class LinkedCaseCreate(BaseModel):
214 + case_name: str
215 + case_description: str
216 + case_creation_time: datetime
217 + case_status: str
218 + assigned_to: Optional[str] = None
219 + id: int
220 +
221 +
222 +class CaseCreateFromAlert(BaseModel):
223 + alert_id: int
224 +
225 +
226 +class CaseAlertLinkCreate(BaseModel):
227 + case_id: int
228 + alert_id: int
229 +
230 +
231 +class AssetCreate(BaseModel):
232 + alert_linked: int
233 + asset_name: str
234 + alert_context_id: int
235 + agent_id: Optional[str]
236 + velociraptor_id: Optional[str]
237 + customer_code: str
238 + index_name: str
239 + index_id: str
240 +
241 +
242 +class AlertTagBase(BaseModel):
243 + tag: str
244 + id: int
245 +
246 +
247 +class AlertTagCreate(BaseModel):
248 + alert_id: int
249 + tag: str
250 +
251 +
252 +class AlertTagDelete(BaseModel):
253 + alert_id: int
254 + tag_id: int
255 +
256 +
257 +class CommentBase(BaseModel):
258 + user_name: str
259 + alert_id: int
260 + id: int
261 + comment: str
262 + created_at: datetime
263 +
264 +
265 +class AssetBase(BaseModel):
266 + asset_name: str
267 + agent_id: Optional[str] = None
268 + customer_code: str
269 + index_id: str
270 + alert_linked: int
271 + id: int
272 + alert_context_id: int
273 + velociraptor_id: Optional[str] = None
274 + index_name: str
275 +
276 +
277 +class AlertOut(BaseModel):
278 + id: int
279 + alert_creation_time: datetime
280 + time_closed: Optional[datetime] = None
281 + alert_name: str
282 + alert_description: str
283 + status: str
284 + customer_code: str
285 + source: str
286 + assigned_to: Optional[str] = None
287 + comments: List[CommentBase] = []
288 + assets: List[AssetBase] = []
289 + tags: List[AlertTagBase] = []
290 + linked_cases: List[LinkedCaseCreate] = []
291 +
292 +
293 +class AlertOutResponse(BaseModel):
294 + alerts: List[AlertOut]
295 + total: Optional[int] = None
296 + open: Optional[int] = None
297 + in_progress: Optional[int] = None
298 + closed: Optional[int] = None
299 + success: bool
300 + message: str
301 +
302 +
303 +class CaseOut(BaseModel):
304 + id: int
305 + case_name: str
306 + case_description: str
307 + assigned_to: Optional[str] = None
308 + alerts: Optional[List[AlertOut]] = []
309 + case_status: Optional[str] = None
310 + case_creation_time: Optional[datetime] = None
311 +
312 +
313 +class CaseOutResponse(BaseModel):
314 + cases: List[CaseOut]
315 + success: bool
316 + message: str
317 +
318 +
319 +class Notification(BaseModel):
320 + id: int
321 + customer_code: str
322 + shuffle_workflow_id: str
323 + enabled: bool
324 +
325 +
326 +class NotificationResponse(BaseModel):
327 + notifications: Optional[List[Notification]] = []
328 + success: bool
329 + message: str
330 +
331 +
332 +class PutNotification(BaseModel):
333 + customer_code: str
334 + shuffle_workflow_id: str
335 + enabled: bool
backend/app/incidents/schema/incident_alert.py new
+139
@@ -0,0 +1,139 @@
1 +from enum import Enum
2 +from typing import Any
3 +from typing import Dict
4 +from typing import List
5 +from typing import Optional
6 +
7 +from pydantic import BaseModel
8 +from pydantic import Extra
9 +from pydantic import Field
10 +
11 +
12 +class ValidSyslogType(str, Enum):
13 + WAZUH = "wazuh"
14 +
15 +
16 +class CreateAlertRequest(BaseModel):
17 + index_name: str = Field(
18 + ...,
19 + description="The name of the index to search alerts for.",
20 + )
21 + alert_id: str = Field(..., description="The alert id.")
22 +
23 +
24 +class CreateAlertRequestRoute(BaseModel):
25 + index_name: str = Field(
26 + ...,
27 + description="The name of the index to search alerts for.",
28 + )
29 + index_id: str = Field(..., description="The index id.")
30 +
31 +
32 +class CreateAlertResponse(BaseModel):
33 + success: bool
34 + message: str
35 + alert_id: int = Field(..., description="The alert id as created in CoPilot.")
36 +
37 +
38 +class AutoCreateAlertResponse(BaseModel):
39 + success: bool
40 + message: str
41 +
42 +
43 +class IndexNamesResponse(BaseModel):
44 + index_names: List[str]
45 + success: bool
46 + message: str
47 +
48 +
49 +class FieldNames(BaseModel):
50 + field_names: List[str]
51 + asset_name: str
52 + timefield_name: str
53 + alert_title_name: str
54 +
55 +
56 +class GenericSourceModel(BaseModel):
57 + timestamp: str = Field(..., description="The timestamp of the alert.")
58 + timestamp_utc: Optional[str] = Field(
59 + None,
60 + description="The UTC timestamp of the alert.",
61 + )
62 + rule_description: Optional[str] = Field(
63 + "No autogenerated rule_description found",
64 + description="The timefield of the alert to be used when creating the IRIS alert.",
65 + )
66 + syslog_level: Optional[str] = Field(
67 + "No autogenerated syslog_level found",
68 + description="The timefield of the alert to be used when creating the IRIS alert.",
69 + )
70 + syslog_type: Optional[str] = Field(
71 + None,
72 + description="The timefield of the alert to be used when creating the IRIS alert.",
73 + )
74 + process_id: Optional[str] = Field(
75 + None,
76 + description="The process id of the alert.",
77 + )
78 + agent_name: Optional[str] = Field(
79 + None,
80 + description="The agent name of the alert.",
81 + )
82 +
83 + class Config:
84 + extra = Extra.allow
85 +
86 + def to_dict(self):
87 + return self.dict(exclude_none=True)
88 +
89 +
90 +class GenericAlertModel(BaseModel):
91 + _index: str
92 + _id: str
93 + _version: int
94 + _source: GenericSourceModel # Nested model
95 + asset_type_id: Optional[int] = Field(
96 + None,
97 + description="The asset type id of the alert which is needed for when we add the asset to IRIS.",
98 + )
99 + ioc_value: Optional[str] = Field(
100 + None,
101 + description="The IoC value of the alert which is needed for when we add the IoC to IRIS.",
102 + )
103 + ioc_type: Optional[str] = Field(
104 + None,
105 + description="The IoC type of the alert which is needed for when we add the IoC to IRIS.",
106 + )
107 + time_field: Optional[str] = Field(
108 + "timestamp",
109 + description="The timefield of the alert to be used when creating the IRIS alert.",
110 + )
111 + syslog_type: Optional[str] = Field(
112 + None,
113 + description="The type of the alert to be used when creating the CoPilot alert.",
114 + )
115 +
116 + class Config:
117 + extra = Extra.allow
118 +
119 +
120 +class AlertDetailsResponse(BaseModel):
121 + alert_details: GenericAlertModel
122 + success: bool
123 + message: str
124 +
125 +
126 +class CreatedAlertPayload(BaseModel):
127 + alert_context_payload: dict
128 + asset_payload: str
129 + timefield_payload: str
130 + alert_title_payload: str
131 + source: str
132 + index_name: Optional[str] = None
133 + index_id: Optional[str] = None
134 +
135 +
136 +class AlertTimelineResponse(BaseModel):
137 + alert_timeline: List[Dict[str, Any]]
138 + success: bool
139 + message: str
backend/app/incidents/services/alert_collection.py new
+106
@@ -0,0 +1,106 @@
1 +from loguru import logger
2 +
3 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
4 +from app.connectors.wazuh_indexer.utils.universal import (
5 + return_graylog_events_index_names,
6 +)
7 +from app.incidents.schema.alert_collection import AlertPayloadItem
8 +from app.incidents.schema.alert_collection import AlertsPayload
9 +from app.incidents.schema.incident_alert import CreateAlertRequest
10 +from app.incidents.schema.incident_alert import IndexNamesResponse
11 +
12 +
13 +async def get_graylog_event_indices() -> IndexNamesResponse:
14 + """
15 + Get the Graylog event indices. Get the Graylog event indices for the Graylog events.
16 +
17 + Returns:
18 + List[str]: The list of Graylog event indices.
19 + """
20 + # return await return_graylog_events_index_names()
21 + return IndexNamesResponse(index_names=await return_graylog_events_index_names(), success=True, message="Success")
22 +
23 +
24 +async def construct_query():
25 + """
26 + Constructs the query to find alerts where `fields.COPILOT_ALERT_ID` is NONE.
27 + """
28 + return {"query": {"bool": {"must": [{"term": {"fields.COPILOT_ALERT_ID": "NONE"}}]}}}
29 +
30 +
31 +async def fetch_alerts_for_index(es_client, index, query):
32 + """
33 + Fetches alerts for a given index that match the query using the Elasticsearch scroll API.
34 + """
35 + # Start the initial search request
36 + response = es_client.search(
37 + index=index,
38 + body=query,
39 + scroll="2m",
40 + size=1000, # Keep the search context open for 2 minutes # Number of results per "page"
41 + )
42 + scroll_id = response["_scroll_id"]
43 + hits = response["hits"]["hits"]
44 +
45 + # Keep fetching results while there are still results to fetch
46 + while len(response["hits"]["hits"]):
47 + response = es_client.scroll(scroll_id=scroll_id, scroll="2m") # Extend the scroll context for another 2 minutes
48 + # Update the scroll ID in case it changes
49 + scroll_id = response["_scroll_id"]
50 + hits.extend(response["hits"]["hits"])
51 +
52 + # Close the scroll context
53 + es_client.clear_scroll(scroll_id=scroll_id)
54 +
55 + return [AlertPayloadItem(**hit) for hit in hits]
56 +
57 +
58 +async def get_alerts_not_created_in_copilot() -> AlertsPayload:
59 + """
60 + Get the Graylog event indices. Then get all the results from the list of indices, where `copilot_alert_id` does not exist.
61 + """
62 + indices = await return_graylog_events_index_names()
63 + logger.info(f"Indices: {indices}")
64 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
65 + query = await construct_query()
66 +
67 + alerts_not_created = []
68 + for index in indices:
69 + alerts = await fetch_alerts_for_index(es_client, index, query)
70 + alerts_not_created.extend(alerts)
71 +
72 + logger.info(f"Alerts not created: {len(alerts_not_created)} alerts found")
73 + return AlertsPayload(alerts=alerts_not_created)
74 +
75 +
76 +async def get_original_alert_id(origin_context: str):
77 + """
78 + Get the original alert id from the origin context.
79 + """
80 + # Assuming the ID is the last part after the last colon and before the last underscore
81 + try:
82 + return origin_context.split(":")[-1].split("_")[-1]
83 + except IndexError: # In case the origin_context does not follow the expected pattern
84 + return None
85 +
86 +
87 +async def get_original_alert_index_name(origin_context: str):
88 + """
89 + Get the original alert index name from the origin context.
90 + """
91 + # Assuming the index name is the part after 'es:' and before the next colon
92 + try:
93 + return origin_context.split("es:")[-1].split(":")[0]
94 + except IndexError: # In case the origin_context does not follow the expected pattern
95 + return None
96 +
97 +
98 +async def add_copilot_alert_id(index_data: CreateAlertRequest, alert_id: int):
99 + """
100 + Add the CoPilot alert ID to the Graylog event.
101 + """
102 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
103 + body = {"doc": {"fields": {"COPILOT_ALERT_ID": f"{alert_id}"}}}
104 + es_client.update(index=index_data.index_name, id=index_data.alert_id, body=body)
105 + logger.info(f"Added CoPilot alert ID {alert_id} to Graylog event {index_data.alert_id} in index {index_data.index_name}")
106 + return None
backend/app/incidents/services/db_operations.py new
+1202
@@ -0,0 +1,1202 @@
1 +from typing import List
2 +
3 +from fastapi import HTTPException
4 +from loguru import logger
5 +from sqlalchemy import delete
6 +from sqlalchemy.exc import IntegrityError
7 +from sqlalchemy.ext.asyncio import AsyncSession
8 +from sqlalchemy.future import select
9 +from sqlalchemy.orm import selectinload
10 +
11 +from app.incidents.models import Alert
12 +from app.incidents.models import AlertContext
13 +from app.incidents.models import AlertTag
14 +from app.incidents.models import AlertTitleFieldName
15 +from app.incidents.models import AlertToTag
16 +from app.incidents.models import Asset
17 +from app.incidents.models import AssetFieldName
18 +from app.incidents.models import Case
19 +from app.incidents.models import CaseAlertLink
20 +from app.incidents.models import Comment
21 +from app.incidents.models import CustomerCodeFieldName
22 +from app.incidents.models import FieldName
23 +from app.incidents.models import Notification
24 +from app.incidents.models import TimestampFieldName
25 +from app.incidents.schema.db_operations import AlertContextCreate
26 +from app.incidents.schema.db_operations import AlertCreate
27 +from app.incidents.schema.db_operations import AlertOut
28 +from app.incidents.schema.db_operations import AlertTagBase
29 +from app.incidents.schema.db_operations import AlertTagCreate
30 +from app.incidents.schema.db_operations import AssetBase
31 +from app.incidents.schema.db_operations import AssetCreate
32 +from app.incidents.schema.db_operations import CaseAlertLinkCreate
33 +from app.incidents.schema.db_operations import CaseCreate
34 +from app.incidents.schema.db_operations import CaseOut
35 +from app.incidents.schema.db_operations import CommentBase
36 +from app.incidents.schema.db_operations import CommentCreate
37 +from app.incidents.schema.db_operations import LinkedCaseCreate
38 +from app.incidents.schema.db_operations import PutNotification
39 +from app.incidents.schema.db_operations import UpdateAlertStatus
40 +from app.incidents.schema.db_operations import UpdateCaseStatus
41 +
42 +
43 +async def alert_total(db: AsyncSession) -> int:
44 + result = await db.execute(select(Alert))
45 + return len(result.scalars().all())
46 +
47 +
48 +async def alerts_closed(db: AsyncSession) -> int:
49 + result = await db.execute(select(Alert).where(Alert.status == "CLOSED"))
50 + return len(result.scalars().all())
51 +
52 +
53 +async def alerts_in_progress(db: AsyncSession) -> int:
54 + result = await db.execute(select(Alert).where(Alert.status == "IN_PROGRESS"))
55 + return len(result.scalars().all())
56 +
57 +
58 +async def alerts_open(db: AsyncSession) -> int:
59 + result = await db.execute(select(Alert).where(Alert.status == "OPEN"))
60 + return len(result.scalars().all())
61 +
62 +
63 +async def alert_total_by_assest_name(db: AsyncSession, asset_name: str) -> int:
64 + result = await db.execute(select(Alert).join(Asset, Alert.id == Asset.alert_linked).where(Asset.asset_name == asset_name))
65 + return len(result.scalars().all())
66 +
67 +
68 +async def alerts_closed_by_asset_name(db: AsyncSession, asset_name: str) -> int:
69 + result = await db.execute(
70 + select(Alert).join(Asset, Alert.id == Asset.alert_linked).where((Alert.status == "CLOSED") & (Asset.asset_name == asset_name)),
71 + )
72 + return len(result.scalars().all())
73 +
74 +
75 +async def alerts_in_progress_by_assest_name(db: AsyncSession, asset_name: str) -> int:
76 + result = await db.execute(
77 + select(Alert).join(Asset, Alert.id == Asset.alert_linked).where((Alert.status == "IN_PROGRESS") & (Asset.asset_name == asset_name)),
78 + )
79 + return len(result.scalars().all())
80 +
81 +
82 +async def alerts_open_by_assest_name(db: AsyncSession, asset_name: str) -> int:
83 + result = await db.execute(
84 + select(Alert).join(Asset, Alert.id == Asset.alert_linked).where((Alert.status == "OPEN") & (Asset.asset_name == asset_name)),
85 + )
86 + return len(result.scalars().all())
87 +
88 +
89 +async def alert_total_by_alert_title(db: AsyncSession, alert_title: str) -> int:
90 + result = await db.execute(select(Alert).where(Alert.alert_name.like(f"%{alert_title}%")))
91 + return len(result.scalars().all())
92 +
93 +
94 +async def alerts_closed_by_alert_title(db: AsyncSession, alert_title: str) -> int:
95 + result = await db.execute(select(Alert).where((Alert.status == "CLOSED") & (Alert.alert_name.like(f"%{alert_title}%"))))
96 + return len(result.scalars().all())
97 +
98 +
99 +async def alerts_in_progress_by_alert_title(db: AsyncSession, alert_title: str) -> int:
100 + result = await db.execute(select(Alert).where((Alert.status == "IN_PROGRESS") & (Alert.alert_name.like(f"%{alert_title}%"))))
101 + return len(result.scalars().all())
102 +
103 +
104 +async def alerts_open_by_alert_title(db: AsyncSession, alert_title: str) -> int:
105 + result = await db.execute(select(Alert).where((Alert.status == "OPEN") & (Alert.alert_name.like(f"%{alert_title}%"))))
106 + return len(result.scalars().all())
107 +
108 +
109 +async def alerts_total_by_assigned_to(db: AsyncSession, assigned_to: str) -> int:
110 + result = await db.execute(select(Alert).where(Alert.assigned_to == assigned_to))
111 + return len(result.scalars().all())
112 +
113 +
114 +async def alerts_closed_by_assigned_to(db: AsyncSession, assigned_to: str) -> int:
115 + result = await db.execute(select(Alert).where((Alert.status == "CLOSED") & (Alert.assigned_to == assigned_to)))
116 + return len(result.scalars().all())
117 +
118 +
119 +async def alerts_in_progress_by_assigned_to(db: AsyncSession, assigned_to: str) -> int:
120 + result = await db.execute(select(Alert).where((Alert.status == "IN_PROGRESS") & (Alert.assigned_to == assigned_to)))
121 + return len(result.scalars().all())
122 +
123 +
124 +async def alerts_open_by_assigned_to(db: AsyncSession, assigned_to: str) -> int:
125 + result = await db.execute(select(Alert).where((Alert.status == "OPEN") & (Alert.assigned_to == assigned_to)))
126 + return len(result.scalars().all())
127 +
128 +
129 +async def alerts_total_by_tag(db: AsyncSession, tag: str) -> int:
130 + result = await db.execute(
131 + select(Alert)
132 + .join(AlertToTag, Alert.id == AlertToTag.alert_id)
133 + .join(AlertTag, AlertToTag.tag_id == AlertTag.id)
134 + .where(AlertTag.tag == tag),
135 + )
136 + return len(result.scalars().all())
137 +
138 +
139 +async def alerts_closed_by_tag(db: AsyncSession, tag: str) -> int:
140 + result = await db.execute(
141 + select(Alert)
142 + .join(AlertToTag, Alert.id == AlertToTag.alert_id)
143 + .join(AlertTag, AlertToTag.tag_id == AlertTag.id)
144 + .where((Alert.status == "CLOSED") & (AlertTag.tag == tag)),
145 + )
146 + return len(result.scalars().all())
147 +
148 +
149 +async def alerts_in_progress_by_tag(db: AsyncSession, tag: str) -> int:
150 + result = await db.execute(
151 + select(Alert)
152 + .join(AlertToTag, Alert.id == AlertToTag.alert_id)
153 + .join(AlertTag, AlertToTag.tag_id == AlertTag.id)
154 + .where((Alert.status == "IN_PROGRESS") & (AlertTag.tag == tag)),
155 + )
156 + return len(result.scalars().all())
157 +
158 +
159 +async def alerts_open_by_tag(db: AsyncSession, tag: str) -> int:
160 + result = await db.execute(
161 + select(Alert)
162 + .join(AlertToTag, Alert.id == AlertToTag.alert_id)
163 + .join(AlertTag, AlertToTag.tag_id == AlertTag.id)
164 + .where((Alert.status == "OPEN") & (AlertTag.tag == tag)),
165 + )
166 + return len(result.scalars().all())
167 +
168 +
169 +async def validate_source_exists(source: str, session: AsyncSession):
170 + # Check each of the FieldName tables and ensure each contains at least one entry for the source
171 + field_names = await get_field_names(source, session)
172 + asset_names = await get_asset_names(source, session)
173 + timefield_names = await get_timefield_names(source, session)
174 + alert_title_names = await get_alert_title_names(source, session)
175 +
176 + if not field_names or not asset_names or not timefield_names or not alert_title_names:
177 + raise HTTPException(status_code=400, detail="Source does not exist")
178 +
179 +
180 +async def get_field_names(source: str, session: AsyncSession):
181 + result = await session.execute(select(FieldName.field_name).where(FieldName.source == source).distinct())
182 + return result.scalars().all()
183 +
184 +
185 +async def get_asset_names(source: str, session: AsyncSession):
186 + result = await session.execute(select(AssetFieldName.field_name).where(AssetFieldName.source == source).distinct())
187 + return result.scalars().first()
188 +
189 +
190 +async def get_timefield_names(source: str, session: AsyncSession):
191 + result = await session.execute(select(TimestampFieldName.field_name).where(TimestampFieldName.source == source).distinct())
192 + return result.scalars().first()
193 +
194 +
195 +async def get_alert_title_names(source: str, session: AsyncSession):
196 + result = await session.execute(select(AlertTitleFieldName.field_name).where(AlertTitleFieldName.source == source).distinct())
197 + return result.scalars().first()
198 +
199 +
200 +# ! NOT USING FOR NOW. GETTING THE CUSTOMER CODE FROM THE ALERTS SOURCE FIELD INSTEAD ! #
201 +async def get_customer_code_names(source: str, session: AsyncSession):
202 + result = await session.execute(select(CustomerCodeFieldName.field_name).where(CustomerCodeFieldName.source == source).distinct())
203 + return result.scalars().first()
204 +
205 +
206 +async def get_customer_notification(customer_code: str, session: AsyncSession):
207 + result = await session.execute(select(Notification).where(Notification.customer_code == customer_code))
208 + notification = result.scalars().first()
209 + logger.info(f"Notification: {notification}")
210 + return [notification] if notification is not None else []
211 +
212 +
213 +async def put_customer_notification(notification: PutNotification, session: AsyncSession):
214 + result = await session.execute(select(Notification).where(Notification.customer_code == notification.customer_code))
215 + existing_notification = result.scalars().first()
216 + if existing_notification is None:
217 + new_notification = Notification(**notification.dict())
218 + session.add(new_notification)
219 + else:
220 + existing_notification.customer_code = notification.customer_code
221 + existing_notification.shuffle_workflow_id = notification.shuffle_workflow_id
222 + existing_notification.enabled = notification.enabled
223 + await session.commit()
224 +
225 +
226 +async def add_field_name(source: str, field_name: str, session: AsyncSession):
227 + result = await session.execute(select(FieldName).where((FieldName.source == source) & (FieldName.field_name == field_name)))
228 + existing_field = result.scalars().first()
229 + if existing_field is None:
230 + field = FieldName(source=source, field_name=field_name)
231 + session.add(field)
232 +
233 +
234 +async def add_asset_name(source: str, asset_name: str, session: AsyncSession):
235 + result = await session.execute(
236 + select(AssetFieldName).where((AssetFieldName.source == source) & (AssetFieldName.field_name == asset_name)),
237 + )
238 + existing_asset = result.scalars().first()
239 + if existing_asset is None:
240 + asset = AssetFieldName(source=source, field_name=asset_name)
241 + session.add(asset)
242 +
243 +
244 +async def add_timefield_name(source: str, timefield_name: str, session: AsyncSession):
245 + result = await session.execute(
246 + select(TimestampFieldName).where((TimestampFieldName.source == source) & (TimestampFieldName.field_name == timefield_name)),
247 + )
248 + existing_timefield = result.scalars().first()
249 + if existing_timefield is None:
250 + timefield = TimestampFieldName(source=source, field_name=timefield_name)
251 + session.add(timefield)
252 +
253 +
254 +async def add_alert_title_name(source: str, alert_title_name: str, session: AsyncSession):
255 + result = await session.execute(
256 + select(AlertTitleFieldName).where((AlertTitleFieldName.source == source) & (AlertTitleFieldName.field_name == alert_title_name)),
257 + )
258 + existing_alert_title = result.scalars().first()
259 + if existing_alert_title is None:
260 + alert_title = AlertTitleFieldName(source=source, field_name=alert_title_name)
261 + session.add(alert_title)
262 +
263 +
264 +# ! NOT USING FOR NOW. GETTING THE CUSTOMER CODE FROM THE ALERTS SOURCE FIELD INSTEAD ! #
265 +async def add_customer_code_name(source: str, customer_code_name: str, session: AsyncSession):
266 + result = await session.execute(
267 + select(CustomerCodeFieldName).where(
268 + (CustomerCodeFieldName.source == source) & (CustomerCodeFieldName.field_name == customer_code_name),
269 + ),
270 + )
271 + existing_customer_code = result.scalars().first()
272 + if existing_customer_code is None:
273 + customer_code = CustomerCodeFieldName(source=source, field_name=customer_code_name)
274 + session.add(customer_code)
275 +
276 +
277 +async def replace_field_name(source: str, field_names: List[str], session: AsyncSession):
278 + # First delete all the field names for this source, then add the new field names
279 + result = await session.execute(select(FieldName).where(FieldName.source == source))
280 + fields = result.scalars().all()
281 +
282 + # Delete all the field names for this source
283 + for field in fields:
284 + await session.delete(field)
285 +
286 + # Add the new field names
287 + for field_name in field_names:
288 + await add_field_name(source, field_name, session)
289 +
290 + # Commit the changes
291 + await session.commit()
292 +
293 +
294 +async def replace_asset_name(source: str, asset_name: str, session: AsyncSession):
295 + # Load the current asset for this source from the DB, then delete it and replace it with `asset_name`
296 + result = await session.execute(select(AssetFieldName).where(AssetFieldName.source == source))
297 + assets = result.scalars().all()
298 +
299 + # Assuming you want to update the field_name for all assets matching the source
300 + for asset in assets:
301 + asset.field_name = asset_name
302 +
303 + # Commit the changes
304 + await session.commit()
305 +
306 +
307 +async def replace_timefield_name(source: str, timefield_name: str, session: AsyncSession):
308 + # Load the current timefield for this source from the DB, then delete it and replace it with `timefield_name`
309 + result = await session.execute(select(TimestampFieldName).where(TimestampFieldName.source == source))
310 + timefields = result.scalars().all()
311 +
312 + # Assuming you want to update the field_name for all timefields matching the source
313 + for timefield in timefields:
314 + timefield.field_name = timefield_name
315 +
316 + # Commit the changes
317 + await session.commit()
318 +
319 +
320 +async def replace_alert_title_name(source: str, alert_title_name: str, session: AsyncSession):
321 + # Load the current alert_title for this source from the DB, then delete it and replace it with `alert_title_name`
322 + result = await session.execute(select(AlertTitleFieldName).where(AlertTitleFieldName.source == source))
323 + alert_titles = result.scalars().all()
324 +
325 + # Assuming you want to update the field_name for all alert_titles matching the source
326 + for alert_title in alert_titles:
327 + alert_title.field_name = alert_title_name
328 +
329 + # Commit the changes
330 + await session.commit()
331 +
332 +
333 +# ! NOT USING FOR NOW. GETTING THE CUSTOMER CODE FROM THE ALERTS SOURCE FIELD INSTEAD ! #
334 +async def replace_customer_code_name(source: str, customer_code_name: str, session: AsyncSession):
335 + # Load the current customer_code for this source from the DB, then delete it and replace it with `customer_code_name`
336 + result = await session.execute(select(CustomerCodeFieldName).where(CustomerCodeFieldName.source == source))
337 + customer_codes = result.scalars().all()
338 +
339 + # Assuming you want to update the field_name for all customer_codes matching the source
340 + for customer_code in customer_codes:
341 + customer_code.field_name = customer_code_name
342 +
343 + # Commit the changes
344 + await session.commit()
345 +
346 +
347 +async def delete_field_name(source: str, field_name: str, session: AsyncSession):
348 + logger.info(f"Deleting field name {field_name} for source {source}")
349 + field = await session.execute(select(FieldName).where((FieldName.source == source) & (FieldName.field_name == field_name)))
350 + field = field.scalar_one_or_none()
351 + if field:
352 + await session.delete(field)
353 +
354 +
355 +async def delete_asset_name(source: str, asset_name: str, session: AsyncSession):
356 + logger.info(f"Deleting asset name {asset_name} for source {source}")
357 + asset = await session.execute(
358 + select(AssetFieldName).where((AssetFieldName.source == source) & (AssetFieldName.field_name == asset_name)),
359 + )
360 + asset = asset.scalar_one_or_none()
361 + if asset:
362 + await session.delete(asset)
363 +
364 +
365 +async def delete_timefield_name(source: str, timefield_name: str, session: AsyncSession):
366 + logger.info(f"Deleting timefield name {timefield_name} for source {source}")
367 + timefield = await session.execute(
368 + select(TimestampFieldName).where((TimestampFieldName.source == source) & (TimestampFieldName.field_name == timefield_name)),
369 + )
370 + timefield = timefield.scalar_one_or_none()
371 + if timefield:
372 + await session.delete(timefield)
373 +
374 +
375 +async def delete_alert_title_name(source: str, alert_title_name: str, session: AsyncSession):
376 + logger.info(f"Deleting alert title name {alert_title_name} for source {source}")
377 + alert_title = await session.execute(
378 + select(AlertTitleFieldName).where((AlertTitleFieldName.source == source) & (AlertTitleFieldName.field_name == alert_title_name)),
379 + )
380 + alert_title = alert_title.scalar_one_or_none()
381 + if alert_title:
382 + await session.delete(alert_title)
383 +
384 +
385 +# ! NOT USING FOR NOW. GETTING THE CUSTOMER CODE FROM THE ALERTS SOURCE FIELD INSTEAD ! #
386 +async def delete_customer_code_name(source: str, customer_code_name: str, session: AsyncSession):
387 + logger.info(f"Deleting customer code name {customer_code_name} for source {source}")
388 + customer_code = await session.execute(
389 + select(CustomerCodeFieldName).where(
390 + (CustomerCodeFieldName.source == source) & (CustomerCodeFieldName.field_name == customer_code_name),
391 + ),
392 + )
393 + customer_code = customer_code.scalar_one_or_none()
394 + if customer_code:
395 + await session.delete(customer_code)
396 +
397 +
398 +async def create_alert(alert: AlertCreate, db: AsyncSession) -> Alert:
399 + db_alert = Alert(**alert.dict())
400 + db.add(db_alert)
401 + try:
402 + await db.flush()
403 + await db.refresh(db_alert)
404 + await db.commit()
405 + except IntegrityError:
406 + await db.rollback()
407 + raise HTTPException(status_code=400, detail="Alert already exists")
408 + return db_alert
409 +
410 +
411 +async def update_alert_status(update_alert_status: UpdateAlertStatus, db: AsyncSession) -> Alert:
412 + result = await db.execute(select(Alert).where(Alert.id == update_alert_status.alert_id))
413 + alert = result.scalars().first()
414 + if not alert:
415 + raise HTTPException(status_code=404, detail="Alert not found")
416 + alert.status = update_alert_status.status
417 + await db.commit()
418 + return alert
419 +
420 +
421 +async def update_case_status(update_case_status: UpdateCaseStatus, db: AsyncSession) -> Case:
422 + result = await db.execute(select(Case).where(Case.id == update_case_status.case_id))
423 + case = result.scalars().first()
424 + if not case:
425 + raise HTTPException(status_code=404, detail="Case not found")
426 + case.case_status = update_case_status.status
427 + await db.commit()
428 + return case
429 +
430 +
431 +async def update_case_assigned_to(case_id: int, assigned_to: str, db: AsyncSession) -> Case:
432 + result = await db.execute(select(Case).where(Case.id == case_id))
433 + case = result.scalars().first()
434 + if not case:
435 + raise HTTPException(status_code=404, detail="Case not found")
436 + case.assigned_to = assigned_to
437 + await db.commit()
438 + return case
439 +
440 +
441 +async def update_alert_assigned_to(alert_id: int, assigned_to: str, db: AsyncSession) -> Alert:
442 + result = await db.execute(select(Alert).where(Alert.id == alert_id))
443 + alert = result.scalars().first()
444 + if not alert:
445 + raise HTTPException(status_code=404, detail="Alert not found")
446 + alert.assigned_to = assigned_to
447 + await db.commit()
448 + return alert
449 +
450 +
451 +async def create_comment(comment: CommentCreate, db: AsyncSession) -> Comment:
452 + # Check if the alert exists
453 + result = await db.execute(select(Alert).options(selectinload(Alert.comments)).where(Alert.id == comment.alert_id))
454 + alert = result.scalars().first()
455 + if not alert:
456 + raise HTTPException(status_code=404, detail="Alert not found")
457 +
458 + db_comment = Comment(**comment.dict())
459 + db.add(db_comment)
460 + try:
461 + await db.commit()
462 + except IntegrityError:
463 + raise HTTPException(status_code=400, detail="Comment already exists")
464 + return db_comment
465 +
466 +
467 +async def create_asset(asset: AssetCreate, db: AsyncSession) -> Asset:
468 + # Check if the alert exists
469 + result = await db.execute(select(Alert).options(selectinload(Alert.assets)).where(Alert.id == asset.alert_linked))
470 + alert = result.scalars().first()
471 + if not alert:
472 + raise HTTPException(status_code=404, detail="Alert not found")
473 +
474 + # Check that the alert_context exists
475 + result = await db.execute(select(AlertContext).where(AlertContext.id == asset.alert_context_id))
476 + alert_context = result.scalars().first()
477 + if not alert_context:
478 + raise HTTPException(status_code=404, detail="Alert context not found")
479 +
480 + db_asset = Asset(**asset.dict())
481 + db.add(db_asset)
482 + try:
483 + await db.commit()
484 + except IntegrityError:
485 + raise HTTPException(status_code=400, detail="Asset already exists")
486 + return db_asset
487 +
488 +
489 +async def create_alert_tag(alert_tag: AlertTagCreate, db: AsyncSession) -> AlertTag:
490 + # Create the AlertTag instance
491 + db_alert_tag = AlertTag(**alert_tag.dict())
492 + db.add(db_alert_tag)
493 + await db.flush()
494 +
495 + # Create the AlertToTag instance
496 + db_alert_to_tag = AlertToTag(alert_id=alert_tag.alert_id, tag_id=db_alert_tag.id)
497 + db.add(db_alert_to_tag)
498 +
499 + try:
500 + await db.commit()
501 + except IntegrityError:
502 + await db.rollback()
503 + raise HTTPException(status_code=400, detail="Alert tag already exists")
504 + return db_alert_tag
505 +
506 +
507 +async def delete_alert_tag(alert_id: int, tag_id: int, db: AsyncSession):
508 + result = await db.execute(select(AlertTag).where(AlertTag.id == tag_id))
509 + alert_tag = result.scalars().first()
510 + if not alert_tag:
511 + raise HTTPException(status_code=404, detail="Alert tag not found")
512 +
513 + result = await db.execute(select(AlertToTag).where((AlertToTag.alert_id == alert_id) & (AlertToTag.tag_id == tag_id)))
514 + alert_to_tag = result.scalars().first()
515 + if not alert_to_tag:
516 + raise HTTPException(status_code=404, detail="Alert to tag link not found")
517 +
518 + await db.execute(delete(AlertToTag).where((AlertToTag.alert_id == alert_id) & (AlertToTag.tag_id == tag_id)))
519 +
520 + # Delete the tag from the AlertTag table
521 + await db.execute(delete(AlertTag).where(AlertTag.id == tag_id))
522 +
523 + try:
524 + await db.commit()
525 + except IntegrityError:
526 + await db.rollback()
527 + raise HTTPException(status_code=400, detail="Error deleting alert tag")
528 +
529 + return alert_tag
530 +
531 +
532 +async def create_alert_context(alert_context: AlertContextCreate, db: AsyncSession) -> AlertContext:
533 + db_alert_context = AlertContext(**alert_context.dict())
534 + db.add(db_alert_context)
535 + try:
536 + await db.flush()
537 + await db.refresh(db_alert_context)
538 + await db.commit()
539 + except IntegrityError:
540 + await db.rollback()
541 + raise HTTPException(status_code=400, detail="Alert context already exists")
542 + return db_alert_context
543 +
544 +
545 +async def get_alert_by_id(alert_id: int, db: AsyncSession) -> AlertOut:
546 + result = await db.execute(
547 + select(Alert)
548 + .where(Alert.id == alert_id)
549 + .options(
550 + selectinload(Alert.comments),
551 + selectinload(Alert.assets),
552 + selectinload(Alert.cases).selectinload(CaseAlertLink.case),
553 + selectinload(Alert.tags).selectinload(AlertToTag.tag),
554 + ),
555 + )
556 + alert = result.scalars().first()
557 + if not alert:
558 + raise HTTPException(status_code=404, detail="Alert not found")
559 +
560 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
561 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
562 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
563 + linked_cases = [LinkedCaseCreate(**case_alert_link.case.__dict__) for case_alert_link in alert.cases]
564 +
565 + alert_out = AlertOut(
566 + id=alert.id,
567 + alert_creation_time=alert.alert_creation_time,
568 + time_closed=alert.time_closed,
569 + alert_name=alert.alert_name,
570 + alert_description=alert.alert_description,
571 + status=alert.status,
572 + customer_code=alert.customer_code,
573 + source=alert.source,
574 + assigned_to=alert.assigned_to,
575 + comments=comments,
576 + assets=assets,
577 + tags=tags,
578 + linked_cases=linked_cases,
579 + )
580 +
581 + return alert_out
582 +
583 +
584 +# async def list_alerts(db: AsyncSession) -> List[AlertOut]:
585 +# result = await db.execute(
586 +# select(Alert).options(
587 +# selectinload(Alert.comments),
588 +# selectinload(Alert.assets),
589 +# selectinload(Alert.cases).selectinload(CaseAlertLink.case),
590 +# selectinload(Alert.tags).selectinload(AlertToTag.tag),
591 +# ),
592 +# )
593 +# alerts = result.scalars().all()
594 +# alerts_out = []
595 +# for alert in alerts:
596 +# comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
597 +# assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
598 +# tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
599 +# linked_cases = [LinkedCaseCreate(**case_alert_link.case.__dict__) for case_alert_link in alert.cases]
600 +# alert_out = AlertOut(
601 +# id=alert.id,
602 +# alert_creation_time=alert.alert_creation_time,
603 +# time_closed=alert.time_closed,
604 +# alert_name=alert.alert_name,
605 +# alert_description=alert.alert_description,
606 +# status=alert.status,
607 +# customer_code=alert.customer_code,
608 +# source=alert.source,
609 +# assigned_to=alert.assigned_to,
610 +# comments=comments,
611 +# assets=assets,
612 +# tags=tags,
613 +# linked_cases=linked_cases,
614 +# )
615 +# alerts_out.append(alert_out)
616 +# return alerts_out
617 +
618 +
619 +async def list_alerts(db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
620 + offset = (page - 1) * page_size
621 + result = await db.execute(
622 + select(Alert)
623 + .options(
624 + selectinload(Alert.comments),
625 + selectinload(Alert.assets),
626 + selectinload(Alert.cases).selectinload(CaseAlertLink.case),
627 + selectinload(Alert.tags).selectinload(AlertToTag.tag),
628 + )
629 + .offset(offset)
630 + .limit(page_size),
631 + )
632 + alerts = result.scalars().all()
633 + alerts_out = []
634 + for alert in alerts:
635 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
636 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
637 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
638 + linked_cases = [LinkedCaseCreate(**case_alert_link.case.__dict__) for case_alert_link in alert.cases]
639 + alert_out = AlertOut(
640 + id=alert.id,
641 + alert_creation_time=alert.alert_creation_time,
642 + time_closed=alert.time_closed,
643 + alert_name=alert.alert_name,
644 + alert_description=alert.alert_description,
645 + status=alert.status,
646 + customer_code=alert.customer_code,
647 + source=alert.source,
648 + assigned_to=alert.assigned_to,
649 + comments=comments,
650 + assets=assets,
651 + tags=tags,
652 + linked_cases=linked_cases,
653 + )
654 + alerts_out.append(alert_out)
655 + return alerts_out
656 +
657 +
658 +async def create_case(case: CaseCreate, db: AsyncSession) -> Case:
659 + db_case = Case(**case.dict())
660 + db.add(db_case)
661 + try:
662 + await db.flush()
663 + await db.refresh(db_case)
664 + await db.commit()
665 + except IntegrityError:
666 + await db.rollback()
667 + raise HTTPException(status_code=400, detail="Case already exists")
668 + return db_case
669 +
670 +
671 +async def create_case_from_alert(alert_id: int, db: AsyncSession) -> Case:
672 + logger.info(f"Creating case from alert {alert_id}")
673 + result = await db.execute(select(Alert).where(Alert.id == alert_id))
674 + alert = result.scalars().first()
675 + if not alert:
676 + raise HTTPException(status_code=404, detail="Alert not found")
677 + case = Case(
678 + case_name=alert.alert_name,
679 + case_description=alert.alert_description,
680 + case_status=alert.status,
681 + assigned_to=alert.assigned_to,
682 + )
683 + db.add(case)
684 + try:
685 + await db.flush()
686 + await db.refresh(case)
687 + await db.commit()
688 + except IntegrityError:
689 + await db.rollback()
690 + raise HTTPException(status_code=400, detail="Case already exists")
691 + return case
692 +
693 +
694 +async def create_case_alert_link(case_alert_link: CaseAlertLinkCreate, db: AsyncSession) -> CaseAlertLink:
695 + # Check if the case exists
696 + result = await db.execute(select(Case).where(Case.id == case_alert_link.case_id))
697 + case = result.scalars().first()
698 + if not case:
699 + raise HTTPException(status_code=404, detail="Case not found")
700 +
701 + # Check if the alert exists
702 + result = await db.execute(select(Alert).where(Alert.id == case_alert_link.alert_id))
703 + alert = result.scalars().first()
704 + if not alert:
705 + raise HTTPException(status_code=404, detail="Alert not found")
706 +
707 + db_case_alert_link = CaseAlertLink(**case_alert_link.dict())
708 + db.add(db_case_alert_link)
709 + try:
710 + await db.commit()
711 + except IntegrityError:
712 + raise HTTPException(status_code=400, detail="Case alert link already exists")
713 + return db_case_alert_link
714 +
715 +
716 +async def get_case_by_id(case_id: int, db: AsyncSession) -> CaseOut:
717 + result = await db.execute(
718 + select(Case)
719 + .where(Case.id == case_id)
720 + .options(
721 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.comments),
722 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.assets),
723 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.tags).selectinload(AlertToTag.tag),
724 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.cases).selectinload(CaseAlertLink.case),
725 + ),
726 + )
727 + case = result.scalars().first()
728 + if not case:
729 + raise HTTPException(status_code=404, detail="Case not found")
730 + alerts_out = []
731 + for case_alert_link in case.alerts:
732 + alert = case_alert_link.alert
733 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
734 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
735 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
736 + linked_cases = [LinkedCaseCreate(**case_alert_link.case.__dict__) for case_alert_link in alert.cases]
737 + alert_out = AlertOut(
738 + id=alert.id,
739 + alert_creation_time=alert.alert_creation_time,
740 + time_closed=alert.time_closed,
741 + alert_name=alert.alert_name,
742 + alert_description=alert.alert_description,
743 + status=alert.status,
744 + customer_code=alert.customer_code,
745 + source=alert.source,
746 + assigned_to=alert.assigned_to,
747 + comments=comments,
748 + assets=assets,
749 + tags=tags,
750 + linked_cases=linked_cases,
751 + )
752 + alerts_out.append(alert_out)
753 + case_out = CaseOut(
754 + id=case.id,
755 + case_name=case.case_name,
756 + case_description=case.case_description,
757 + assigned_to=case.assigned_to,
758 + alerts=alerts_out,
759 + case_creation_time=case.case_creation_time,
760 + )
761 + return case_out
762 +
763 +
764 +async def list_cases(db: AsyncSession) -> List[CaseOut]:
765 + result = await db.execute(
766 + select(Case).options(
767 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.comments),
768 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.assets),
769 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.tags).selectinload(AlertToTag.tag),
770 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.cases).selectinload(CaseAlertLink.case),
771 + ),
772 + )
773 + cases = result.scalars().all()
774 + cases_out = []
775 + for case in cases:
776 + alerts_out = []
777 + for case_alert_link in case.alerts:
778 + alert = case_alert_link.alert
779 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
780 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
781 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
782 + linked_cases = [LinkedCaseCreate(**case_alert_link.case.__dict__) for case_alert_link in alert.cases]
783 + alert_out = AlertOut(
784 + id=alert.id,
785 + alert_creation_time=alert.alert_creation_time,
786 + time_closed=alert.time_closed,
787 + alert_name=alert.alert_name,
788 + alert_description=alert.alert_description,
789 + status=alert.status,
790 + customer_code=alert.customer_code,
791 + source=alert.source,
792 + assigned_to=alert.assigned_to,
793 + comments=comments,
794 + assets=assets,
795 + tags=tags,
796 + linked_cases=linked_cases,
797 + )
798 + alerts_out.append(alert_out)
799 + case_out = CaseOut(
800 + id=case.id,
801 + case_name=case.case_name,
802 + case_description=case.case_description,
803 + assigned_to=case.assigned_to,
804 + alerts=alerts_out,
805 + case_creation_time=case.case_creation_time,
806 + case_status=case.case_status,
807 + )
808 + cases_out.append(case_out)
809 + return cases_out
810 +
811 +
812 +async def list_cases_by_status(status: str, db: AsyncSession) -> List[CaseOut]:
813 + result = await db.execute(
814 + select(Case)
815 + .where(Case.case_status == status)
816 + .options(
817 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.comments),
818 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.assets),
819 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.tags).selectinload(AlertToTag.tag),
820 + ),
821 + )
822 + cases = result.scalars().all()
823 + cases_out = []
824 + for case in cases:
825 + alerts_out = []
826 + for case_alert_link in case.alerts:
827 + alert = case_alert_link.alert
828 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
829 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
830 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
831 + alert_out = AlertOut(
832 + id=alert.id,
833 + alert_creation_time=alert.alert_creation_time,
834 + time_closed=alert.time_closed,
835 + alert_name=alert.alert_name,
836 + alert_description=alert.alert_description,
837 + status=alert.status,
838 + customer_code=alert.customer_code,
839 + source=alert.source,
840 + assigned_to=alert.assigned_to,
841 + comments=comments,
842 + assets=assets,
843 + tags=tags,
844 + )
845 + alerts_out.append(alert_out)
846 + case_out = CaseOut(
847 + id=case.id,
848 + case_name=case.case_name,
849 + case_description=case.case_description,
850 + assigned_to=case.assigned_to,
851 + alerts=alerts_out,
852 + )
853 + cases_out.append(case_out)
854 + return cases_out
855 +
856 +
857 +async def list_cases_by_assigned_to(assigned_to: str, db: AsyncSession) -> List[CaseOut]:
858 + result = await db.execute(
859 + select(Case)
860 + .where(Case.assigned_to == assigned_to)
861 + .options(
862 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.comments),
863 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.assets),
864 + selectinload(Case.alerts).selectinload(CaseAlertLink.alert).selectinload(Alert.tags).selectinload(AlertToTag.tag),
865 + ),
866 + )
867 + cases = result.scalars().all()
868 + cases_out = []
869 + for case in cases:
870 + alerts_out = []
871 + for case_alert_link in case.alerts:
872 + alert = case_alert_link.alert
873 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
874 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
875 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
876 + alert_out = AlertOut(
877 + id=alert.id,
878 + alert_creation_time=alert.alert_creation_time,
879 + time_closed=alert.time_closed,
880 + alert_name=alert.alert_name,
881 + alert_description=alert.alert_description,
882 + status=alert.status,
883 + customer_code=alert.customer_code,
884 + source=alert.source,
885 + assigned_to=alert.assigned_to,
886 + comments=comments,
887 + assets=assets,
888 + tags=tags,
889 + )
890 + alerts_out.append(alert_out)
891 + case_out = CaseOut(
892 + id=case.id,
893 + case_name=case.case_name,
894 + case_description=case.case_description,
895 + assigned_to=case.assigned_to,
896 + alerts=alerts_out,
897 + )
898 + cases_out.append(case_out)
899 + return cases_out
900 +
901 +
902 +async def get_alert_context_by_id(alert_context_id: int, db: AsyncSession) -> AlertContext:
903 + result = await db.execute(select(AlertContext).where(AlertContext.id == alert_context_id))
904 + alert_context = result.scalars().first()
905 + if not alert_context:
906 + raise HTTPException(status_code=404, detail="Alert context not found")
907 + return alert_context
908 +
909 +
910 +async def list_alerts_by_tag(tag: str, db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
911 + offset = (page - 1) * page_size
912 + result = await db.execute(
913 + select(Alert)
914 + .join(AlertToTag)
915 + .join(AlertTag)
916 + .where(AlertTag.tag == tag)
917 + .options(
918 + selectinload(Alert.comments),
919 + selectinload(Alert.assets),
920 + selectinload(Alert.cases),
921 + selectinload(Alert.tags).selectinload(AlertToTag.tag),
922 + )
923 + .offset(offset)
924 + .limit(page_size),
925 + )
926 + alerts = result.scalars().all()
927 + alerts_out = []
928 + for alert in alerts:
929 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
930 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
931 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
932 + alert_out = AlertOut(
933 + id=alert.id,
934 + alert_creation_time=alert.alert_creation_time,
935 + time_closed=alert.time_closed,
936 + alert_name=alert.alert_name,
937 + alert_description=alert.alert_description,
938 + status=alert.status,
939 + customer_code=alert.customer_code,
940 + source=alert.source,
941 + assigned_to=alert.assigned_to,
942 + comments=comments,
943 + assets=assets,
944 + tags=tags,
945 + )
946 + alerts_out.append(alert_out)
947 + return alerts_out
948 +
949 +
950 +async def list_alert_by_status(status: str, db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
951 + offset = (page - 1) * page_size
952 + result = await db.execute(
953 + select(Alert)
954 + .where(Alert.status == status)
955 + .options(
956 + selectinload(Alert.comments),
957 + selectinload(Alert.assets),
958 + selectinload(Alert.cases),
959 + selectinload(Alert.tags).selectinload(AlertToTag.tag),
960 + )
961 + .offset(offset)
962 + .limit(page_size),
963 + )
964 + alerts = result.scalars().all()
965 + alerts_out = []
966 + for alert in alerts:
967 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
968 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
969 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
970 + alert_out = AlertOut(
971 + id=alert.id,
972 + alert_creation_time=alert.alert_creation_time,
973 + time_closed=alert.time_closed,
974 + alert_name=alert.alert_name,
975 + alert_description=alert.alert_description,
976 + status=alert.status,
977 + customer_code=alert.customer_code,
978 + source=alert.source,
979 + assigned_to=alert.assigned_to,
980 + comments=comments,
981 + assets=assets,
982 + tags=tags,
983 + )
984 + alerts_out.append(alert_out)
985 + return alerts_out
986 +
987 +
988 +async def list_alerts_by_asset_name(asset_name: str, db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
989 + offset = (page - 1) * page_size
990 + result = await db.execute(
991 + select(Alert)
992 + .join(Asset)
993 + .where(Asset.asset_name == asset_name)
994 + .options(
995 + selectinload(Alert.comments),
996 + selectinload(Alert.assets),
997 + selectinload(Alert.cases),
998 + selectinload(Alert.tags).selectinload(AlertToTag.tag),
999 + )
1000 + .offset(offset)
1001 + .limit(page_size),
1002 + )
1003 + alerts = result.scalars().all()
1004 + alerts_out = []
1005 + for alert in alerts:
1006 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
1007 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
1008 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
1009 + alert_out = AlertOut(
1010 + id=alert.id,
1011 + alert_creation_time=alert.alert_creation_time,
1012 + time_closed=alert.time_closed,
1013 + alert_name=alert.alert_name,
1014 + alert_description=alert.alert_description,
1015 + status=alert.status,
1016 + customer_code=alert.customer_code,
1017 + source=alert.source,
1018 + assigned_to=alert.assigned_to,
1019 + comments=comments,
1020 + assets=assets,
1021 + tags=tags,
1022 + )
1023 + alerts_out.append(alert_out)
1024 + return alerts_out
1025 +
1026 +
1027 +async def list_alert_by_assigned_to(assigned_to: str, db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
1028 + offset = (page - 1) * page_size
1029 + result = await db.execute(
1030 + select(Alert)
1031 + .where(Alert.assigned_to == assigned_to)
1032 + .options(
1033 + selectinload(Alert.comments),
1034 + selectinload(Alert.assets),
1035 + selectinload(Alert.cases),
1036 + selectinload(Alert.tags).selectinload(AlertToTag.tag),
1037 + )
1038 + .offset(offset)
1039 + .limit(page_size),
1040 + )
1041 + alerts = result.scalars().all()
1042 + alerts_out = []
1043 + for alert in alerts:
1044 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
1045 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
1046 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
1047 + alert_out = AlertOut(
1048 + id=alert.id,
1049 + alert_creation_time=alert.alert_creation_time,
1050 + time_closed=alert.time_closed,
1051 + alert_name=alert.alert_name,
1052 + alert_description=alert.alert_description,
1053 + status=alert.status,
1054 + customer_code=alert.customer_code,
1055 + source=alert.source,
1056 + assigned_to=alert.assigned_to,
1057 + comments=comments,
1058 + assets=assets,
1059 + tags=tags,
1060 + )
1061 + alerts_out.append(alert_out)
1062 + return alerts_out
1063 +
1064 +
1065 +async def list_alerts_by_title(alert_title: str, db: AsyncSession, page: int = 1, page_size: int = 25) -> List[AlertOut]:
1066 + offset = (page - 1) * page_size
1067 + result = await db.execute(
1068 + select(Alert)
1069 + .where(Alert.alert_name.like(f"%{alert_title}%"))
1070 + .options(
1071 + selectinload(Alert.comments),
1072 + selectinload(Alert.assets),
1073 + selectinload(Alert.cases),
1074 + selectinload(Alert.tags).selectinload(AlertToTag.tag),
1075 + )
1076 + .offset(offset)
1077 + .limit(page_size),
1078 + )
1079 + alerts = result.scalars().all()
1080 + alerts_out = []
1081 + for alert in alerts:
1082 + comments = [CommentBase(**comment.__dict__) for comment in alert.comments]
1083 + assets = [AssetBase(**asset.__dict__) for asset in alert.assets]
1084 + tags = [AlertTagBase(**alert_to_tag.tag.__dict__) for alert_to_tag in alert.tags]
1085 + alert_out = AlertOut(
1086 + id=alert.id,
1087 + alert_creation_time=alert.alert_creation_time,
1088 + time_closed=alert.time_closed,
1089 + alert_name=alert.alert_name,
1090 + alert_description=alert.alert_description,
1091 + status=alert.status,
1092 + customer_code=alert.customer_code,
1093 + source=alert.source,
1094 + assigned_to=alert.assigned_to,
1095 + comments=comments,
1096 + assets=assets,
1097 + tags=tags,
1098 + )
1099 + alerts_out.append(alert_out)
1100 + return alerts_out
1101 +
1102 +
1103 +async def delete_comments(alert_id: int, db: AsyncSession):
1104 + result = await db.execute(select(Comment).where(Comment.alert_id == alert_id))
1105 + comments = result.scalars().all()
1106 + for comment in comments:
1107 + await db.execute(delete(Comment).where(Comment.id == comment.id))
1108 +
1109 +
1110 +async def delete_assets(alert_id: int, db: AsyncSession):
1111 + result = await db.execute(select(Asset).where(Asset.alert_linked == alert_id))
1112 + assets = result.scalars().all()
1113 + for asset in assets:
1114 + await db.execute(delete(Asset).where(Asset.id == asset.id))
1115 + context_result = await db.execute(select(Asset).where(Asset.alert_context_id == asset.alert_context_id))
1116 + if not context_result.scalars().all():
1117 + await db.execute(delete(AlertContext).where(AlertContext.id == asset.alert_context_id))
1118 +
1119 +
1120 +async def delete_tags(alert_id: int, db: AsyncSession):
1121 + result = await db.execute(select(AlertToTag).where(AlertToTag.alert_id == alert_id))
1122 + alert_to_tags = result.scalars().all()
1123 + for alert_to_tag in alert_to_tags:
1124 + await db.execute(
1125 + delete(AlertToTag).where((AlertToTag.alert_id == alert_to_tag.alert_id) & (AlertToTag.tag_id == alert_to_tag.tag_id)),
1126 + )
1127 +
1128 +
1129 +async def is_alert_linked_to_case(alert_id: int, db: AsyncSession) -> bool:
1130 + result = await db.execute(select(CaseAlertLink).where(CaseAlertLink.alert_id == alert_id))
1131 + linked_cases = result.scalars().all()
1132 +
1133 + if linked_cases:
1134 + raise HTTPException(status_code=400, detail="Alert is linked to a case, and cannot be deleted")
1135 + return False
1136 +
1137 +
1138 +async def delete_alert(alert_id: int, db: AsyncSession):
1139 + """
1140 + Delete an alert from the database.
1141 +
1142 + Args:
1143 + alert_id (int): The ID of the alert to be deleted.
1144 + db (AsyncSession): The database session.
1145 +
1146 + Raises:
1147 + HTTPException: If the alert is not found or there is an error deleting the alert.
1148 +
1149 + """
1150 + logger.info(f"Deleting alert {alert_id}")
1151 + result = await db.execute(
1152 + select(Alert)
1153 + .options(selectinload(Alert.comments), selectinload(Alert.assets).selectinload(Asset.alert_context), selectinload(Alert.tags))
1154 + .where(Alert.id == alert_id),
1155 + )
1156 + alert = result.scalars().first()
1157 + if not alert:
1158 + raise HTTPException(status_code=404, detail="Alert not found")
1159 +
1160 + await delete_comments(alert_id, db)
1161 + await delete_assets(alert_id, db)
1162 + await delete_tags(alert_id, db)
1163 +
1164 + await db.execute(delete(Alert).where(Alert.id == alert.id))
1165 +
1166 + try:
1167 + await db.commit()
1168 + except IntegrityError:
1169 + await db.rollback()
1170 + raise HTTPException(status_code=400, detail="Error deleting alert")
1171 +
1172 +
1173 +async def delete_case(case_id: int, db: AsyncSession):
1174 + """
1175 + Delete a case from the database.
1176 +
1177 + Args:
1178 + case_id (int): The ID of the case to be deleted.
1179 + db (AsyncSession): The database session.
1180 +
1181 + Raises:
1182 + HTTPException: If the case is not found or there is an error deleting the case.
1183 +
1184 + """
1185 + result = await db.execute(
1186 + select(Case).options(selectinload(Case.alerts)).where(Case.id == case_id),
1187 + )
1188 + case = result.scalars().first()
1189 + if not case:
1190 + raise HTTPException(status_code=404, detail="Case not found")
1191 +
1192 + # Delete entries from CaseAlertLink table
1193 + await db.execute(delete(CaseAlertLink).where(CaseAlertLink.case_id == case_id))
1194 +
1195 + # Delete the case
1196 + await db.execute(delete(Case).where(Case.id == case.id))
1197 +
1198 + try:
1199 + await db.commit()
1200 + except IntegrityError:
1201 + await db.rollback()
1202 + raise HTTPException(status_code=400, detail="Error deleting case")
backend/app/incidents/services/incident_alert.py new
+754
@@ -0,0 +1,754 @@
1 +import os
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 +
9 +from fastapi import HTTPException
10 +from loguru import logger
11 +from sqlalchemy.ext.asyncio import AsyncSession
12 +from sqlalchemy.future import select
13 +
14 +from app.connectors.shuffle.schema.integrations import ExecuteWorkflowRequest
15 +from app.connectors.shuffle.services.integrations import execute_workflow
16 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
17 +from app.db.universal_models import Agents
18 +from app.incidents.models import Alert
19 +from app.incidents.models import AlertContext
20 +from app.incidents.models import Asset
21 +from app.incidents.routes.db_operations import get_configured_sources
22 +from app.incidents.schema.incident_alert import CreateAlertRequest
23 +from app.incidents.schema.incident_alert import CreateAlertRequestRoute
24 +from app.incidents.schema.incident_alert import CreateAlertResponse
25 +from app.incidents.schema.incident_alert import CreatedAlertPayload
26 +from app.incidents.schema.incident_alert import FieldNames
27 +from app.incidents.schema.incident_alert import GenericAlertModel
28 +from app.incidents.schema.incident_alert import GenericSourceModel
29 +from app.incidents.services.db_operations import get_alert_title_names
30 +from app.incidents.services.db_operations import get_asset_names
31 +from app.incidents.services.db_operations import get_customer_notification
32 +from app.incidents.services.db_operations import get_field_names
33 +from app.incidents.services.db_operations import get_timefield_names
34 +from app.integrations.alert_creation_settings.models.alert_creation_settings import (
35 + AlertCreationSettings,
36 +)
37 +from app.integrations.alert_escalation.schema.escalate_alert import CustomerCodeKeys
38 +
39 +
40 +async def fetch_settings(field: str, value: str, session: AsyncSession):
41 + """
42 + Fetch settings based on the field and value.
43 +
44 + Args:
45 + field (str): The field to check.
46 + value (str): The value to check.
47 + session (AsyncSession): The database session.
48 +
49 + Returns:
50 + AlertCreationSettings: The settings if found, None otherwise.
51 + """
52 + logger.info(f"Checking if {field}: {value} is valid.")
53 + result = await session.execute(
54 + select(AlertCreationSettings).where(
55 + getattr(AlertCreationSettings, field) == value,
56 + ),
57 + )
58 + settings = result.scalars().first()
59 + logger.info(f"Settings: {settings}")
60 + return settings
61 +
62 +
63 +async def is_customer_code_valid(customer_code: str, session: AsyncSession) -> AlertCreationSettings:
64 + """
65 + Check if the customer code is valid.
66 +
67 + Args:
68 + customer_code (str): The customer code to check.
69 + session (AsyncSession): The database session.
70 +
71 + Returns:
72 + bool: True if the customer code is valid, False otherwise.
73 + """
74 + settings = await fetch_settings("customer_code", customer_code, session)
75 +
76 + if settings:
77 + return settings
78 +
79 + # If no settings found with customer_code, try with office365_organization_id
80 + settings = await fetch_settings("office365_organization_id", customer_code, session)
81 +
82 + if settings:
83 + return settings
84 +
85 + raise HTTPException(
86 + status_code=400,
87 + detail=f"Customer code {customer_code} is not valid. Has the customer been provisioned?",
88 + )
89 +
90 +
91 +async def get_single_alert_details(
92 + alert_details: CreateAlertRequest,
93 +) -> GenericAlertModel:
94 + """
95 + Fetches the details of a single alert.
96 +
97 + Args:
98 + alert_details (CreateAlertRequest): The details of the alert to fetch.
99 +
100 + Returns:
101 + GenericAlertModel: The model representing the fetched alert.
102 +
103 + Raises:
104 + HTTPException: If there is an error while fetching the alert details.
105 + """
106 + logger.info(
107 + f"Fetching alert details for alert {alert_details.alert_id} in index {alert_details.index_name}",
108 + )
109 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
110 + try:
111 + alert = es_client.get(index=alert_details.index_name, id=alert_details.alert_id)
112 + source_model = GenericSourceModel(**alert["_source"])
113 + syslog_type = getattr(source_model, "syslog_type", None)
114 + if syslog_type is None:
115 + syslog_type = getattr(source_model, "integration", None)
116 + if syslog_type is None:
117 + raise HTTPException(status_code=400, detail="Neither syslog_type nor integration field found in source_model")
118 + return GenericAlertModel(
119 + _source=source_model,
120 + _id=alert["_id"],
121 + _index=alert["_index"],
122 + _version=alert["_version"],
123 + syslog_type=syslog_type,
124 + )
125 + except Exception as e:
126 + logger.debug(f"Failed to collect alert details: {e}")
127 + raise HTTPException(
128 + status_code=400,
129 + detail=f"Failed to collect alert details: {e}",
130 + )
131 +
132 +
133 +def remove_process_name_if_osquery(source_dict: dict) -> None:
134 + """
135 + Remove the process_name field from the source dictionary if rule_group1 is 'osquery'.
136 +
137 + Args:
138 + source_dict (dict): The source dictionary.
139 + """
140 + rule_group1 = source_dict.get("rule_group1")
141 + if rule_group1 == "osquery":
142 + logger.info("Removing process_name field")
143 + source_dict.pop("process_name", None)
144 +
145 +
146 +def get_process_image(source_dict: dict) -> str:
147 + """
148 + Get the process_image field from the source dictionary.
149 +
150 + Args:
151 + source_dict (dict): The source dictionary.
152 +
153 + Returns:
154 + str: The process image.
155 + """
156 + process_image = source_dict.get("process_image")
157 + if not process_image:
158 + process_image = source_dict.get("data_win_eventdata_image")
159 + if not process_image:
160 + process_image = source_dict.get("data_event_Image")
161 + logger.info(f"Process image: {process_image}")
162 + return process_image
163 +
164 +
165 +def get_process_name_from_image(process_image: str) -> str:
166 + """
167 + Get the process name from the process image.
168 +
169 + Args:
170 + process_image (str): The process image.
171 +
172 + Returns:
173 + str: The process name.
174 + """
175 + process_name = os.path.basename(process_image) if process_image else None
176 + logger.info(f"Process name: {process_name}")
177 + return process_name
178 +
179 +
180 +async def construct_soc_alert_url(root_url: str, soc_alert_id: int) -> str:
181 + """Constructs the full URL for the SOC alert.
182 +
183 + Args:
184 + root_url (str): The root URL of the SOC alert system.
185 + soc_alert_id (int): The ID of the SOC alert.
186 +
187 + Returns:
188 + str: The full URL for the SOC alert.
189 +
190 + """
191 + url_path = f"/alerts?cid=1&page=1&per_page=10&sort=desc&alert_ids={soc_alert_id}"
192 + return f"{root_url}{url_path}"
193 +
194 +
195 +async def get_customer_code(alert_details: dict):
196 + logger.info(f"Fetching customer code for alert {alert_details}")
197 +
198 + # Iterate over the possible keys and return the value if the key is present
199 + for key in CustomerCodeKeys:
200 + logger.info(f"Checking for key {key.value}")
201 + if key.value in alert_details:
202 + return alert_details[key.value]
203 +
204 + # If none of the keys are present, raise an exception
205 + logger.info(f"Failed to fetch customer code. Valid customer code field names are {', '.join([key.value for key in CustomerCodeKeys])}")
206 + raise HTTPException(
207 + status_code=400,
208 + detail=f"Failed to fetch customer code. Valid customer code field names are {', '.join([key.value for key in CustomerCodeKeys])}",
209 + )
210 +
211 +
212 +async def add_alert_to_document(
213 + alert: CreateAlertRequest,
214 + soc_alert_id: int,
215 +) -> Optional[str]:
216 + """
217 + Update the alert document in Elasticsearch with the provided SOC alert ID URL.
218 +
219 + Parameters:
220 + - es_client: The Elasticsearch client instance to use for the update.
221 + - alert: The alert request object containing alert_id and index_name.
222 + - soc_alert_id: The alert ID as it exists within IRIS.
223 + - session: The database session for retrieving connector information.
224 +
225 + Returns:
226 + - True if the update is successful, False otherwise.
227 + """
228 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
229 + try:
230 + es_client.update(
231 + index=alert.index_name,
232 + id=alert.alert_id,
233 + body={"doc": {"alert_id": soc_alert_id}},
234 + )
235 + logger.info(
236 + f"Added alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name}",
237 + )
238 + return soc_alert_id
239 + except Exception as e:
240 + logger.error(
241 + f"Failed to add alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name}: {e}",
242 + )
243 + # Attempt to remove read-only block
244 + try:
245 + es_client.indices.put_settings(
246 + index=alert.index_name,
247 + body={"index.blocks.write": None},
248 + )
249 + logger.info(
250 + f"Removed read-only block from index {alert.index_name}. Retrying update.",
251 + )
252 +
253 + # Retry the update operation
254 + es_client.update(
255 + index=alert.index_name,
256 + id=alert.alert_id,
257 + body={"doc": {"alert_id": soc_alert_id}},
258 + )
259 + logger.info(
260 + f"Added alert ID {soc_alert_id} to alert {alert.alert_id} in index {alert.index_name} after removing read-only block",
261 + )
262 +
263 + # Reenable the write block
264 + es_client.indices.put_settings(
265 + index=alert.index_name,
266 + body={"index.blocks.write": True},
267 + )
268 + return soc_alert_id
269 + except Exception as e2:
270 + logger.error(
271 + f"Failed to remove read-only block from index {alert.index_name}: {e2}",
272 + )
273 + return False
274 +
275 +
276 +async def retrieve_agent_details_from_db(agent_name: str, session: AsyncSession):
277 + """
278 + Retrieve agent details from the database.
279 +
280 + Args:
281 + agent_name (str): The name of the agent.
282 + session (AsyncSession): The database session.
283 +
284 + Returns:
285 + Agents: The agent details.
286 + """
287 + logger.info(f"Retrieving agent details for {agent_name}")
288 + result = await session.execute(
289 + select(Agents).where(Agents.hostname == agent_name),
290 + )
291 + agent = result.scalars().first()
292 + if agent:
293 + return agent
294 + return None
295 +
296 +
297 +async def validate_syslog_type_source(source: str, session: AsyncSession) -> bool:
298 + """
299 + Invoke the `get_configured_sources` to ensure the `source` has been configured
300 + """
301 + sources = await get_configured_sources(session)
302 + if source not in sources.sources:
303 + raise HTTPException(
304 + status_code=400,
305 + detail=f"Syslog type {source} is not configured in the system",
306 + )
307 +
308 +
309 +async def get_all_field_names(syslog_type: str, session: AsyncSession) -> FieldNames:
310 + """
311 + Get the field names for the given syslog type.
312 +
313 + Args:
314 + syslog_type (str): The syslog type.
315 + session (AsyncSession): The database session.
316 +
317 + Returns:
318 + FieldNames: The field names.
319 + """
320 + return FieldNames(
321 + field_names=await get_field_names(syslog_type, session),
322 + asset_name=await get_asset_names(syslog_type, session),
323 + timefield_name=await get_timefield_names(syslog_type, session),
324 + alert_title_name=await get_alert_title_names(syslog_type, session),
325 + )
326 +
327 +
328 +async def get_process_name(source_dict: dict) -> List[str]:
329 + """
330 + Get the process name from the source dictionary.
331 +
332 + Args:
333 + source_dict (dict): The source dictionary.
334 +
335 + Returns:
336 + List[str]: The process name as a list.
337 + """
338 + remove_process_name_if_osquery(source_dict)
339 + process_image = get_process_image(source_dict)
340 + process_name = get_process_name_from_image(process_image)
341 + return [process_name] if process_name else []
342 +
343 +
344 +async def build_alert_context_payload(alert_payload: dict, field_names: Any) -> Dict[str, Any]:
345 + """
346 + Build the alert context payload.
347 +
348 + Args:
349 + alert_payload (dict): The alert payload.
350 + field_names (Any): The field names.
351 +
352 + Returns:
353 + dict: The alert context payload.
354 + """
355 + process_name = await get_process_name(alert_payload)
356 + alert_context_payload = {field: alert_payload[field] for field in field_names.field_names if field in alert_payload}
357 + alert_context_payload["process_name"] = process_name
358 + return alert_context_payload
359 +
360 +
361 +async def build_alert_payload(
362 + syslog_type: str,
363 + index_name: str,
364 + index_id: str,
365 + alert_payload: dict,
366 + session: AsyncSession,
367 +) -> CreatedAlertPayload:
368 + """
369 + Build the alert payload based on the syslog type and the alert payload.
370 +
371 + Args:
372 + syslog_type (str): The syslog type.
373 + alert_payload (dict): The alert payload.
374 + session (AsyncSession): The database session.
375 +
376 + Returns:
377 + dict: The built alert payload.
378 + """
379 + field_names = await get_all_field_names(syslog_type, session)
380 + # Validate that the field_names exist in the alert_payload
381 + for field_name in [field_names.asset_name, field_names.timefield_name, field_names.alert_title_name]:
382 + if field_name not in alert_payload:
383 + raise HTTPException(
384 + status_code=400,
385 + detail=f"Field name {field_name} not found in alert payload",
386 + )
387 +
388 + return CreatedAlertPayload(
389 + alert_context_payload=await build_alert_context_payload(alert_payload, field_names),
390 + asset_payload=alert_payload[field_names.asset_name] if field_names.asset_name in alert_payload else None,
391 + timefield_payload=alert_payload[field_names.timefield_name] if field_names.timefield_name in alert_payload else None,
392 + alert_title_payload=alert_payload[field_names.alert_title_name] if field_names.alert_title_name in alert_payload else None,
393 + source=syslog_type,
394 + index_name=index_name,
395 + index_id=index_id,
396 + )
397 +
398 +
399 +async def handle_customer_notifications(customer_code: str, alert_payload: CreatedAlertPayload, session: AsyncSession):
400 + customer_notifications = await get_customer_notification(customer_code, session)
401 + if customer_notifications and customer_notifications[0].enabled:
402 + logger.info(f"Executing workflow for customer code {customer_code}")
403 + await execute_workflow(
404 + ExecuteWorkflowRequest(
405 + workflow_id=customer_notifications[0].shuffle_workflow_id,
406 + execution_arguments={
407 + "customer_code": customer_code,
408 + "alert_context_payload": alert_payload.alert_context_payload,
409 + },
410 + start="",
411 + ),
412 + )
413 +
414 +
415 +async def create_alert_full(alert_payload: CreatedAlertPayload, customer_code: str, session: AsyncSession) -> Alert:
416 + """
417 + Create an alert in CoPilot.
418 +
419 + Args:
420 + alert_payload (dict): The alert payload.
421 + customer_code (str): The customer code.
422 + session (AsyncSession): The database session.
423 +
424 + Returns:
425 + CreateAlertResponse: The response object containing the created alert details.
426 +
427 + Raises:
428 + HTTPException: If there is an error creating the alert.
429 + """
430 + alert_id = (await create_alert_in_copilot(alert_payload=alert_payload, customer_code=customer_code, session=session)).id
431 + alert_context_id = (
432 + await create_alert_context_payload(source=alert_payload.source, alert_payload=alert_payload.alert_context_payload, session=session)
433 + ).id
434 + asset_id = (
435 + await create_asset_context_payload(
436 + customer_code=customer_code,
437 + asset_payload=alert_payload,
438 + alert_context_id=alert_context_id,
439 + alert_id=alert_id,
440 + session=session,
441 + )
442 + ).id
443 + logger.info(f"Creating alert for customer code {customer_code} with alert context ID {alert_context_id} and asset ID {asset_id}")
444 + await handle_customer_notifications(customer_code, alert_payload, session)
445 +
446 + await add_alert_to_document(CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id), alert_id)
447 +
448 + return alert_id
449 +
450 +
451 +async def does_assit_exist(alert_payload: CreatedAlertPayload, alert_id: int, session: AsyncSession) -> bool:
452 + """
453 + Check if the asset exists for the given alert payload.
454 +
455 + Args:
456 + alert_payload (dict): The alert payload.
457 + alert_id (int): The alert ID.
458 + session (AsyncSession): The database session.
459 +
460 + Returns:
461 + bool: True if the asset exists, None otherwise.
462 + """
463 + logger.info(f"Checking if an asset exists for alert ID {alert_id} with asset name {alert_payload.asset_payload}")
464 + result = await session.execute(
465 + select(Asset).where(
466 + Asset.alert_linked == alert_id,
467 + Asset.asset_name == alert_payload.asset_payload,
468 + ),
469 + )
470 + asset = result.scalars().first()
471 + if asset:
472 + logger.info(f"Asset exists for alert ID {alert_id} with asset name {alert_payload.asset_payload}")
473 + return True
474 + logger.info(f"No asset exists for alert ID {alert_id} with asset name {alert_payload.asset_payload}")
475 + return False
476 +
477 +
478 +async def add_asset_to_copilot_alert(alert_payload: CreatedAlertPayload, alert_id: int, customer_code: str, session: AsyncSession) -> None:
479 + """
480 + Add the asset to the alert in CoPilot.
481 +
482 + Args:
483 + alert_payload (dict): The alert payload.
484 + alert_id (int): The alert ID.
485 + session (AsyncSession): The database session.
486 + """
487 + if await does_assit_exist(alert_payload, alert_id, session):
488 + return None
489 + agent_details = await retrieve_agent_details_from_db(alert_payload.asset_payload, session)
490 + alert_context_id = (
491 + await create_alert_context_payload(source=alert_payload.source, alert_payload=alert_payload.alert_context_payload, session=session)
492 + ).id
493 + agent_id = agent_details.agent_id if agent_details else None
494 + velociraptor_id = agent_details.velociraptor_id if agent_details else None
495 + asset_context = Asset(
496 + alert_linked=alert_id,
497 + asset_name=alert_payload.asset_payload,
498 + alert_context_id=alert_context_id,
499 + agent_id=agent_id,
500 + velociraptor_id=velociraptor_id,
501 + customer_code=customer_code,
502 + index_name=alert_payload.index_name,
503 + index_id=alert_payload.index_id,
504 + )
505 + # Commit it to the database
506 + session.add(asset_context)
507 + await session.commit()
508 + return asset_context
509 +
510 +
511 +async def create_alert_in_copilot(alert_payload: CreatedAlertPayload, customer_code: str, session: AsyncSession) -> Alert:
512 + """
513 + Create an alert in CoPilot.
514 +
515 + Args:
516 + alert_payload (dict): The alert payload.
517 + customer_code (str): The customer code.
518 +
519 + Returns:
520 + CreateAlertResponse: The response object containing the created alert details.
521 +
522 + Raises:
523 + HTTPException: If there is an error creating the alert.
524 + """
525 + logger.info(f"Creating alert for customer code {customer_code} with payload {alert_payload}")
526 + alert = Alert(
527 + alert_name=alert_payload.alert_title_payload,
528 + alert_description=alert_payload.alert_title_payload,
529 + status="OPEN",
530 + alert_creation_time=datetime.utcnow(),
531 + customer_code=customer_code,
532 + source=alert_payload.source,
533 + assigned_to=None,
534 + )
535 + # Commit it to the database
536 + session.add(alert)
537 + await session.commit()
538 + return alert
539 +
540 +
541 +async def create_alert_context_payload(source: str, alert_payload: dict, session: AsyncSession) -> AlertContext:
542 + """
543 + Build the alert context payload based on the valid field names and the alert payload. Then
544 + create the alert context in the database.
545 + """
546 + logger.info(f"Creating alert context for source {source} with payload {alert_payload}")
547 + alert_context = AlertContext(
548 + source=source,
549 + context=alert_payload,
550 + )
551 + # Commit it to the database
552 + session.add(alert_context)
553 + await session.commit()
554 + return alert_context
555 +
556 +
557 +async def create_asset_context_payload(
558 + customer_code: str,
559 + asset_payload: CreatedAlertPayload,
560 + alert_context_id: int,
561 + alert_id: int,
562 + session: AsyncSession,
563 +) -> Asset:
564 + """
565 + Build the asset context payload based on the valid field names and the asset payload. Then
566 + create the asset context in the database.
567 + """
568 + agent_details = await retrieve_agent_details_from_db(asset_payload.asset_payload, session)
569 + agent_id = agent_details.agent_id if agent_details else None
570 + velociraptor_id = agent_details.velociraptor_id if agent_details else None
571 +
572 + asset_context = Asset(
573 + alert_linked=alert_id,
574 + asset_name=asset_payload.asset_payload,
575 + alert_context_id=alert_context_id,
576 + agent_id=agent_id,
577 + velociraptor_id=velociraptor_id,
578 + customer_code=customer_code,
579 + index_name=asset_payload.index_name,
580 + index_id=asset_payload.index_id,
581 + )
582 + # Commit it to the database
583 + session.add(asset_context)
584 + await session.commit()
585 + return asset_context
586 +
587 +
588 +async def open_alert_exists(alert_payload: CreatedAlertPayload, customer_code: str, session: AsyncSession) -> bool:
589 + """
590 + Check if an open alert exists for the given alert payload.
591 +
592 + Args:
593 + alert_payload (dict): The alert payload.
594 + customer_code (str): The customer code.
595 +
596 + Returns:
597 + bool: True if an open alert exists, None otherwise.
598 + """
599 + logger.info(f"Checking if an open alert exists for customer code {customer_code} with alert title {alert_payload.alert_title_payload}")
600 + result = await session.execute(
601 + select(Alert).where(
602 + Alert.customer_code == customer_code,
603 + Alert.alert_name == alert_payload.alert_title_payload,
604 + Alert.status == "OPEN",
605 + ),
606 + )
607 + alert = result.scalars().first()
608 + if alert:
609 + logger.info(f"Open alert exists for customer code {customer_code} with alert title {alert_payload.alert_title_payload}")
610 + return alert.id
611 + logger.info(f"No open alert exists for customer code {customer_code} with alert title {alert_payload.alert_title_payload}")
612 + return None
613 +
614 +
615 +async def create_alert(
616 + alert: CreateAlertRequest,
617 + session: AsyncSession,
618 + simga_alert: str = None,
619 +) -> CreateAlertResponse:
620 + """
621 + Creates an alert in CoPilot.
622 +
623 + Args:
624 + alert (CreateAlertRequest): The request object containing the alert details.
625 + session (AsyncSession): The database session.
626 +
627 + Returns:
628 + CreateAlertResponse: The response object containing the created alert details.
629 +
630 + Raises:
631 + HTTPException: If there is an error creating the alert.
632 + """
633 + logger.info(f"Creating alert {alert.alert_id} in CoPilot")
634 + alert_details = await get_single_alert_details(alert_details=alert)
635 + await validate_syslog_type_source(alert_details.syslog_type, session)
636 + customer_code = await get_customer_code(dict(alert_details._source))
637 + logger.info(f"Customer code: {customer_code}")
638 + customer_alert_creation_settings = await is_customer_code_valid(customer_code=customer_code, session=session)
639 + logger.info(f"Customer creation settings: {customer_alert_creation_settings}")
640 + alert_payload = await build_alert_payload(
641 + alert_details.syslog_type,
642 + alert_details._index,
643 + alert_details._id,
644 + alert_details._source.to_dict(),
645 + session,
646 + )
647 + if simga_alert is not None:
648 + return await create_alert_full(alert_payload, customer_code, session)
649 +
650 + existing_alert = await open_alert_exists(alert_payload, customer_code, session)
651 + if existing_alert:
652 + logger.info(
653 + f"Open alert exists for customer code {customer_code} with alert title {alert_payload.alert_title_payload} and alert ID {existing_alert}",
654 + )
655 + await add_alert_to_document(
656 + CreateAlertRequest(index_name=alert_payload.index_name, alert_id=alert_payload.index_id),
657 + existing_alert,
658 + )
659 + await add_asset_to_copilot_alert(alert_payload, existing_alert, customer_code, session)
660 + return existing_alert
661 + return await create_alert_full(alert_payload, customer_code, session)
662 +
663 +
664 +async def retrieve_alert_timeline(alert: CreateAlertRequestRoute, session: AsyncSession) -> List[Dict[str, Any]]:
665 + """
666 + Retrieve the alert timeline for the given alert.
667 +
668 + Args:
669 + alert (CreateAlertRequestRoute): The alert details.
670 + session (AsyncSession): The database session.
671 +
672 + Returns:
673 + List[Dict[str, Any]]: The alert timeline.
674 + """
675 + alert_details = await get_alert_details(alert)
676 + if alert_details._source.process_id is not None:
677 + alert_timestamp = alert_details._source.timestamp
678 + start_of_day, end_of_day = calculate_day_range(alert_timestamp)
679 + return await fetch_alert_timeline(
680 + alert.index_name,
681 + alert_details._source.process_id,
682 + alert_details._source.agent_name,
683 + start_of_day,
684 + end_of_day,
685 + )
686 + return []
687 +
688 +
689 +async def get_alert_details(alert: CreateAlertRequestRoute) -> Any:
690 + """
691 + Get the details of a single alert.
692 +
693 + Args:
694 + alert (CreateAlertRequestRoute): The alert details.
695 +
696 + Returns:
697 + Any: The alert details.
698 + """
699 + return await get_single_alert_details(CreateAlertRequest(index_name=alert.index_name, alert_id=alert.index_id))
700 +
701 +
702 +def calculate_day_range(timestamp: str) -> (str, str):
703 + """
704 + Calculate the start and end of the day for the given timestamp.
705 +
706 + Args:
707 + timestamp (str): The timestamp.
708 +
709 + Returns:
710 + (str, str): The start and end of the day in the required format.
711 + """
712 + dt = datetime.fromisoformat(timestamp)
713 + start_of_day = dt.replace(hour=0, minute=0, second=0, microsecond=0)
714 + end_of_day = start_of_day + timedelta(days=1)
715 + return start_of_day.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], end_of_day.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
716 +
717 +
718 +async def fetch_alert_timeline(
719 + index_name: str,
720 + process_id: str,
721 + agent_name: str,
722 + start_of_day: str,
723 + end_of_day: str,
724 +) -> List[Dict[str, Any]]:
725 + """
726 + Fetch the alert timeline from the indexer.
727 +
728 + Args:
729 + index_name (str): The name of the index.
730 + process_id (str): The process ID.
731 + agent_name (str): The agent name.
732 + start_of_day (str): The start of the day in the required format.
733 + end_of_day (str): The end of the day in the required format.
734 +
735 + Returns:
736 + List[Dict[str, Any]]: The alert timeline.
737 + """
738 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
739 + alert_timeline = es_client.search(
740 + index=index_name,
741 + body={
742 + "query": {
743 + "bool": {
744 + "must": [
745 + {"match": {"process_id": process_id}},
746 + {"match": {"agent_name": agent_name}},
747 + {"range": {"timestamp": {"gte": start_of_day, "lt": end_of_day}}},
748 + ],
749 + },
750 + },
751 + },
752 + size=50,
753 + )
754 + return alert_timeline["hits"]["hits"]
backend/app/integrations/alert_escalation/routes/escalate_alert.py
+6 -5
@@ -6,9 +6,9 @@ from sqlalchemy.ext.asyncio import AsyncSession
6
7 from app.auth.utils import AuthHandler
8 from app.db.db_session import get_db
9 +from app.incidents.routes.incident_alert import create_alert_manual_route
10 from app.integrations.alert_escalation.schema.escalate_alert import CreateAlertRequest
11 from app.integrations.alert_escalation.schema.escalate_alert import CreateAlertResponse
11 -from app.integrations.alert_escalation.services.escalate_alert import create_alert
12
13 integration_escalate_alerts_router = APIRouter()
14
@@ -16,7 +16,7 @@ integration_escalate_alerts_router = APIRouter()
16 @integration_escalate_alerts_router.post(
17 "/create",
18 response_model=CreateAlertResponse,
19 - description="Manually create an alert in IRIS from Copilot WebUI",
19 + description="Manually create an alert in CoPilot from Copilot WebUI",
20 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
21 )
22 async def create_alert_route(
@@ -24,7 +24,7 @@ async def create_alert_route(
24 session: AsyncSession = Depends(get_db),
25 ) -> CreateAlertResponse:
26 """
27 - Create an alert in IRIS. Manually create an alert in IRIS from Copilot WebUI.
27 + Create an alert in CoPilot. Manually create an alert in CoPilot from Copilot WebUI.
28
29 Args:
30 create_alert_request (CreateAlertRequest): The request object containing the details of the alert to be created.
@@ -33,5 +33,6 @@ async def create_alert_route(
33 Returns:
34 CreateAlertResponse: The response object containing the result of the alert creation.
35 """
36 - logger.info(f"Creating alert {create_alert_request.alert_id} in IRIS")
37 - return await create_alert(create_alert_request, session)
36 + logger.info(f"Creating alert {create_alert_request.alert_id} in CoPilot")
37 + # return await create_alert(create_alert_request, session)
38 + return await create_alert_manual_route(create_alert_request, session)
backend/app/integrations/alert_escalation/routes/general_alert.py
+5 -2
@@ -6,13 +6,14 @@ from sqlalchemy.ext.asyncio import AsyncSession
6
7 from app.auth.utils import AuthHandler
8 from app.db.db_session import get_db
9 +from app.incidents.routes.incident_alert import create_alert_manual_route
10 from app.integrations.alert_escalation.schema.general_alert import CreateAlertRequest
11 from app.integrations.alert_escalation.schema.general_alert import CreateAlertResponse
11 -from app.integrations.alert_escalation.services.general_alert import create_alert
12
13 integration_general_alerts_router = APIRouter()
14
15
16 +# ! TODO: Remove this route and file after the new route is implemented ! #
17 @integration_general_alerts_router.post(
18 "/create",
19 response_model=CreateAlertResponse,
@@ -35,4 +36,6 @@ async def create_alert_route(
36 CreateAlertResponse: The response object containing the result of the alert creation.
37 """
38 logger.info(f"Creating alert {create_alert_request.alert_id} in IRIS")
38 - return await create_alert(create_alert_request, session)
39 + return None
40 + # return await create_alert(create_alert_request, session)
41 + return await create_alert_manual_route(create_alert_request, session)
backend/app/integrations/alert_escalation/schema/escalate_alert.py
+2 -1
@@ -13,6 +13,7 @@ class CustomerCodeKeys(Enum):
13 AGENT_LABELS_CUSTOMER = "agent_labels_customer"
14 DATA_OFFICE365_ORGANIZATION_ID = "data_office365_OrganizationId"
15 SYSLOG_CUSTOMER = "syslog_customer"
16 + CUSTOMER_CODE = "customer_code"
17
18
19 class SyslogLevelMapping(Enum):
@@ -45,7 +46,7 @@ class CreateAlertResponse(BaseModel):
46 success: bool
47 message: str
48 alert_id: int = Field(..., description="The alert id as created in IRIS.")
48 - alert_url: str = Field(..., description="The alert url as created in IRIS.")
49 + alert_url: Optional[str] = Field(None, description="The alert url as created in IRIS.")
50
51
52 class GenericSourceModel(BaseModel):
backend/app/integrations/alert_escalation/schema/general_alert.py
+1 -1
@@ -27,7 +27,7 @@ class CreateAlertResponse(BaseModel):
27 success: bool
28 message: str
29 alert_id: int = Field(..., description="The alert id as created in IRIS.")
30 - alert_url: str = Field(..., description="The alert url as created in IRIS.")
30 + alert_url: Optional[str] = Field(None, description="The alert url as created in IRIS.")
31
32
33 class GenericSourceModel(BaseModel):
backend/app/integrations/alert_escalation/services/escalate_alert.py
+8
@@ -25,6 +25,9 @@ from app.integrations.alert_escalation.schema.escalate_alert import IrisAlertCon
25 from app.integrations.alert_escalation.schema.escalate_alert import IrisAlertPayload
26 from app.integrations.alert_escalation.schema.escalate_alert import SourceFieldsToRemove
27 from app.integrations.alert_escalation.schema.escalate_alert import SyslogLevelMapping
28 +from app.integrations.monitoring_alert.services.wazuh import (
29 + handle_customer_notifications,
30 +)
31 from app.integrations.utils.alerts import get_asset_type_id
32
33
@@ -491,6 +494,11 @@ async def create_alert(
494 iris_alert_payload.to_dict(),
495 )
496 alert_id = result["data"]["alert_id"]
497 + await handle_customer_notifications(
498 + customer_code=customer_code,
499 + alert_payload=iris_alert_payload,
500 + session=session,
501 + )
502
503 await add_asset_if_wazuh(alert_details, alert_id, iris_alert_payload, session)
504
backend/app/integrations/monitoring_alert/routes/provision.py
+8 -7
@@ -68,13 +68,14 @@ async def invoke_provision_wazuh_monitoring_alert(
68 ):
69 # Provision the Wazuh monitoring alert
70 await provision_wazuh_monitoring_alert(request)
71 - await add_scheduler_jobs(
72 - CreateSchedulerRequest(
73 - function_name="invoke_wazuh_monitoring_alert",
74 - time_interval=5,
75 - job_id="invoke_wazuh_monitoring_alert",
76 - ),
77 - )
71 + # ! No longer needed since we have the invoke_alert_creation_collect scheduled job ! #
72 + # await add_scheduler_jobs(
73 + # CreateSchedulerRequest(
74 + # function_name="invoke_wazuh_monitoring_alert",
75 + # time_interval=5,
76 + # job_id="invoke_wazuh_monitoring_alert",
77 + # ),
78 + # )
79
80
81 async def invoke_provision_suricata_monitoring_alert(
backend/app/integrations/monitoring_alert/schema/provision.py
+1 -1
@@ -182,7 +182,7 @@ class GraylogAlertProvisionModel(BaseModel):
182 field_spec: Dict[str, GraylogAlertProvisionFieldSpecItem]
183 key_spec: List
184 notification_settings: GraylogAlertProvisionNotificationSettings
185 - notifications: List[GraylogAlertProvisionNotification]
185 + notifications: Optional[List[GraylogAlertProvisionNotification]] = []
186 alert: bool
187
188
backend/app/integrations/monitoring_alert/services/provision.py
+115 -63
@@ -23,9 +23,6 @@ from app.integrations.monitoring_alert.schema.provision import (
23 from app.integrations.monitoring_alert.schema.provision import (
24 GraylogAlertProvisionModel,
25 )
26 -from app.integrations.monitoring_alert.schema.provision import (
27 - GraylogAlertProvisionNotification,
28 -)
26 from app.integrations.monitoring_alert.schema.provision import (
27 GraylogAlertProvisionNotificationSettings,
28 )
@@ -276,37 +273,38 @@ async def provision_wazuh_monitoring_alert(
273 logger.info(
274 f"Invoking provision_wazuh_monitoring_alert with request: {request.dict()}",
275 )
279 - notification_exists = await check_if_event_notification_exists("SEND TO COPILOT")
280 - if not notification_exists:
281 - # ! Unfortunately Graylog does not support disabling SSL verification when sending webhooks
282 - # ! Therefore, we need to send to API port of Copilot over HTTP
283 - url_whitelisted = await check_if_url_whitelist_entry_exists(
284 - f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
285 - )
286 - if not url_whitelisted:
287 - logger.info("Provisioning URL Whitelist")
288 - whitelisted_urls = await build_url_whitelisted_entries(
289 - whitelist_url_model=GraylogUrlWhitelistEntryConfig(
290 - id=await generate_random_id(),
291 - value=f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
292 - title="SEND TO COPILOT",
293 - type="literal",
294 - ),
295 - )
296 - await provision_webhook_url_whitelist(whitelisted_urls)
297 -
298 - logger.info("Provisioning SEND TO COPILOT Webhook")
299 - notification_id = await provision_webhook(
300 - GraylogAlertWebhookNotificationModel(
301 - title="SEND TO COPILOT",
302 - description="Send alert to Copilot",
303 - config={
304 - "url": f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
305 - "type": "http-notification-v1",
306 - },
307 - ),
308 - )
309 - notification_id = await get_notification_id("SEND TO COPILOT")
276 + # ! TODO Commenting out for now since the plan is to pull from gl-events ! #
277 + # notification_exists = await check_if_event_notification_exists("SEND TO COPILOT")
278 + # if not notification_exists:
279 + # # ! Unfortunately Graylog does not support disabling SSL verification when sending webhooks
280 + # # ! Therefore, we need to send to API port of Copilot over HTTP
281 + # url_whitelisted = await check_if_url_whitelist_entry_exists(
282 + # f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
283 + # )
284 + # if not url_whitelisted:
285 + # logger.info("Provisioning URL Whitelist")
286 + # whitelisted_urls = await build_url_whitelisted_entries(
287 + # whitelist_url_model=GraylogUrlWhitelistEntryConfig(
288 + # id=await generate_random_id(),
289 + # value=f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
290 + # title="SEND TO COPILOT",
291 + # type="literal",
292 + # ),
293 + # )
294 + # await provision_webhook_url_whitelist(whitelisted_urls)
295 +
296 + # logger.info("Provisioning SEND TO COPILOT Webhook")
297 + # notification_id = await provision_webhook(
298 + # GraylogAlertWebhookNotificationModel(
299 + # title="SEND TO COPILOT",
300 + # description="Send alert to Copilot",
301 + # config={
302 + # "url": f"http://{os.getenv('ALERT_FORWARDING_IP')}:5000/api/monitoring_alert/create",
303 + # "type": "http-notification-v1",
304 + # },
305 + # ),
306 + # )
307 + # notification_id = await get_notification_id("SEND TO COPILOT")
308 await provision_alert_definition(
309 GraylogAlertProvisionModel(
310 title="WAZUH SYSLOG LEVEL ALERT",
@@ -361,17 +359,27 @@ async def provision_wazuh_monitoring_alert(
359 ),
360 ],
361 ),
362 + "COPILOT_ALERT_ID": GraylogAlertProvisionFieldSpecItem(
363 + data_type="string",
364 + providers=[
365 + GraylogAlertProvisionProvider(
366 + type="template-v1",
367 + template="NONE",
368 + require_values=True,
369 + ),
370 + ],
371 + ),
372 },
373 key_spec=[],
374 notification_settings=GraylogAlertProvisionNotificationSettings(
375 grace_period_ms=0,
376 backlog_size=None,
377 ),
370 - notifications=[
371 - GraylogAlertProvisionNotification(
372 - notification_id=notification_id,
373 - ),
374 - ],
378 + # notifications=[
379 + # GraylogAlertProvisionNotification(
380 + # notification_id=notification_id,
381 + # ),
382 + # ],
383 alert=True,
384 ),
385 )
@@ -481,17 +489,27 @@ async def provision_suricata_monitoring_alert(
489 ),
490 ],
491 ),
492 + "COPILOT_ALERT_ID": GraylogAlertProvisionFieldSpecItem(
493 + data_type="string",
494 + providers=[
495 + GraylogAlertProvisionProvider(
496 + type="template-v1",
497 + template="NONE",
498 + require_values=True,
499 + ),
500 + ],
501 + ),
502 },
503 key_spec=[],
504 notification_settings=GraylogAlertProvisionNotificationSettings(
505 grace_period_ms=0,
506 backlog_size=None,
507 ),
490 - notifications=[
491 - GraylogAlertProvisionNotification(
492 - notification_id=notification_id,
493 - ),
494 - ],
508 + # notifications=[
509 + # GraylogAlertProvisionNotification(
510 + # notification_id=notification_id,
511 + # ),
512 + # ],
513 alert=True,
514 ),
515 )
@@ -601,17 +619,27 @@ async def provision_office365_exchange_online_alert(
619 ),
620 ],
621 ),
622 + "COPILOT_ALERT_ID": GraylogAlertProvisionFieldSpecItem(
623 + data_type="string",
624 + providers=[
625 + GraylogAlertProvisionProvider(
626 + type="template-v1",
627 + template="NONE",
628 + require_values=True,
629 + ),
630 + ],
631 + ),
632 },
633 key_spec=[],
634 notification_settings=GraylogAlertProvisionNotificationSettings(
635 grace_period_ms=0,
636 backlog_size=None,
637 ),
610 - notifications=[
611 - GraylogAlertProvisionNotification(
612 - notification_id=notification_id,
613 - ),
614 - ],
638 + # notifications=[
639 + # GraylogAlertProvisionNotification(
640 + # notification_id=notification_id,
641 + # ),
642 + # ],
643 alert=True,
644 ),
645 )
@@ -721,17 +749,27 @@ async def provision_office365_threat_intel_alert(
749 ),
750 ],
751 ),
752 + "COPILOT_ALERT_ID": GraylogAlertProvisionFieldSpecItem(
753 + data_type="string",
754 + providers=[
755 + GraylogAlertProvisionProvider(
756 + type="template-v1",
757 + template="NONE",
758 + require_values=True,
759 + ),
760 + ],
761 + ),
762 },
763 key_spec=[],
764 notification_settings=GraylogAlertProvisionNotificationSettings(
765 grace_period_ms=0,
766 backlog_size=None,
767 ),
730 - notifications=[
731 - GraylogAlertProvisionNotification(
732 - notification_id=notification_id,
733 - ),
734 - ],
768 + # notifications=[
769 + # GraylogAlertProvisionNotification(
770 + # notification_id=notification_id,
771 + # ),
772 + # ],
773 alert=True,
774 ),
775 )
@@ -809,28 +847,42 @@ async def provision_custom_alert(request: CustomMonitoringAlertProvisionModel) -
847 event_limit=1000,
848 ),
849 field_spec={
812 - custom_field.name: GraylogAlertProvisionFieldSpecItem(
850 + **{
851 + custom_field.name: GraylogAlertProvisionFieldSpecItem(
852 + data_type="string",
853 + providers=[
854 + GraylogAlertProvisionProvider(
855 + type="template-v1",
856 + template=f"${{source.{custom_field.value}}}"
857 + if custom_field.name != "CUSTOMER_CODE"
858 + else custom_field.value,
859 + require_values=True,
860 + ),
861 + ],
862 + )
863 + for custom_field in request.custom_fields
864 + },
865 + "COPILOT_ALERT_ID": GraylogAlertProvisionFieldSpecItem(
866 data_type="string",
867 providers=[
868 GraylogAlertProvisionProvider(
869 type="template-v1",
817 - template=f"${{source.{custom_field.value}}}" if custom_field.name != "CUSTOMER_CODE" else custom_field.value,
870 + template="NONE",
871 require_values=True,
872 ),
873 ],
821 - )
822 - for custom_field in request.custom_fields
874 + ),
875 },
876 key_spec=[],
877 notification_settings=GraylogAlertProvisionNotificationSettings(
878 grace_period_ms=0,
879 backlog_size=None,
880 ),
829 - notifications=[
830 - GraylogAlertProvisionNotification(
831 - notification_id=notification_id,
832 - ),
833 - ],
881 + # notifications=[
882 + # GraylogAlertProvisionNotification(
883 + # notification_id=notification_id,
884 + # ),
885 + # ],
886 alert=True,
887 ),
888 )
backend/app/integrations/monitoring_alert/services/wazuh.py
+24
@@ -12,8 +12,11 @@ from app.agents.routes.agents import get_agent_by_hostname
12 from app.agents.schema.agents import AgentsResponse
13 from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
14 from app.connectors.dfir_iris.utils.universal import initialize_client_and_alert
15 +from app.connectors.shuffle.schema.integrations import ExecuteWorkflowRequest
16 +from app.connectors.shuffle.services.integrations import execute_workflow
17 from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
18 from app.db.universal_models import CustomersMeta
19 +from app.incidents.services.db_operations import get_customer_notification
20 from app.integrations.alert_creation.general.schema.alert import CreateAlertRequest
21 from app.integrations.alert_creation.general.schema.alert import IrisAsset
22 from app.integrations.alert_creation.general.schema.alert import IrisIoc
@@ -425,6 +428,22 @@ async def build_alert_payload(
428 )
429
430
431 +async def handle_customer_notifications(customer_code: str, alert_payload: dict, session: AsyncSession):
432 + customer_notifications = await get_customer_notification(customer_code, session)
433 + if customer_notifications and customer_notifications[0].enabled:
434 + logger.info(f"Executing workflow for customer code {customer_code}")
435 + await execute_workflow(
436 + ExecuteWorkflowRequest(
437 + workflow_id=customer_notifications[0].shuffle_workflow_id,
438 + execution_arguments={
439 + "customer_code": customer_code,
440 + "alert_context_payload": alert_payload,
441 + },
442 + start="",
443 + ),
444 + )
445 +
446 +
447 async def create_alert_details(alert_details: WazuhAlertModel) -> CreateAlertRequest:
448 """
449 Create an alert details object from the Wazuh alert details.
@@ -507,6 +526,11 @@ async def create_and_update_alert_in_iris(
526 alert_id,
527 {"iocs": [dict(IrisIoc(**iris_alert_payload.alert_iocs[0].to_dict()))]},
528 )
529 + await handle_customer_notifications(
530 + customer_code=alert_details._source.agent_labels_customer,
531 + alert_payload=iris_alert_payload,
532 + session=session,
533 + )
534 return alert_id
535
536
backend/app/routers/graylog.py
+2
@@ -5,6 +5,7 @@ from app.connectors.graylog.routes.events import graylog_events_router
5 from app.connectors.graylog.routes.management import graylog_management_router
6 from app.connectors.graylog.routes.monitoring import graylog_monitoring_router
7 from app.connectors.graylog.routes.pipelines import graylog_pipelines_router
8 +from app.connectors.graylog.routes.receiver import graylog_receiver_router
9 from app.connectors.graylog.routes.streams import graylog_streams_router
10
11 router = APIRouter()
@@ -15,3 +16,4 @@ router.include_router(graylog_management_router, prefix="/graylog", tags=["grayl
16 router.include_router(graylog_monitoring_router, prefix="/graylog", tags=["graylog"])
17 router.include_router(graylog_streams_router, prefix="/graylog", tags=["graylog"])
18 router.include_router(graylog_pipelines_router, prefix="/graylog", tags=["graylog"])
19 +router.include_router(graylog_receiver_router, prefix="/graylog", tags=["graylog"])
backend/app/routers/incidents.py new
+10
@@ -0,0 +1,10 @@
1 +from fastapi import APIRouter
2 +
3 +from app.incidents.routes.db_operations import incidents_db_operations_router
4 +from app.incidents.routes.incident_alert import incidents_alerts_router
5 +
6 +# Instantiate the APIRouter
7 +router = APIRouter()
8 +
9 +router.include_router(incidents_db_operations_router, prefix="/incidents/db_operations", tags=["incidents"])
10 +router.include_router(incidents_alerts_router, prefix="/incidents/alerts", tags=["incidents-alerts"])
backend/app/routers/wazuh_indexer.py
+6
@@ -2,6 +2,7 @@ from fastapi import APIRouter
2
3 from app.connectors.wazuh_indexer.routes.alerts import wazuh_indexer_alerts_router
4 from app.connectors.wazuh_indexer.routes.monitoring import wazuh_indexer_router
5 +from app.connectors.wazuh_indexer.routes.sigma import wazuh_indexer_sigma_router
6
7 # Instantiate the APIRouter
8 router = APIRouter()
@@ -17,3 +18,8 @@ router.include_router(
18 prefix="/wazuh_indexer",
19 tags=["wazuh-indexer-monitoring"],
20 )
21 +router.include_router(
22 + wazuh_indexer_sigma_router,
23 + prefix="/sigma",
24 + tags=["wazuh-indexer-sigma"],
25 +)
backend/app/schedulers/scheduler.py
+17
@@ -14,6 +14,7 @@ from app.db.db_session import sync_engine
14 from app.schedulers.models.scheduler import CreateSchedulerRequest
15 from app.schedulers.models.scheduler import JobMetadata
16 from app.schedulers.services.agent_sync import agent_sync
17 +from app.schedulers.services.invoke_alert_creation import invoke_alert_creation_collect
18 from app.schedulers.services.invoke_carbonblack import (
19 invoke_carbonblack_integration_collect,
20 )
@@ -60,6 +61,7 @@ from app.schedulers.services.monitoring_alert import (
61 from app.schedulers.services.monitoring_alert import invoke_office365_threat_intel_alert
62 from app.schedulers.services.monitoring_alert import invoke_suricata_monitoring_alert
63 from app.schedulers.services.monitoring_alert import invoke_wazuh_monitoring_alert
64 +from app.schedulers.services.wazuh_index_resize import resize_wazuh_index_fields
65
66
67 def scheduler_listener(event):
@@ -129,6 +131,19 @@ async def initialize_job_metadata():
131 "function": agent_sync,
132 "description": "Synchronizes agents with the Wazuh Manager and Velociraptor server.",
133 },
134 + {
135 + "job_id": "wazuh_index_fields_resize",
136 + "time_interval": 1,
137 + "function": resize_wazuh_index_fields,
138 + "description": "Resizes the Wazuh index fields.",
139 + },
140 + # TODO: ! COMMENTING OUT UNTIL INCIDENT MANAGEMENT IS IMPLEMENTED ! #
141 + {
142 + "job_id": "invoke_alert_creation_collect",
143 + "time_interval": 5,
144 + "function": invoke_alert_creation_collect,
145 + "description": "Invokes alert creation collection.",
146 + },
147 # {"job_id": "invoke_mimecast_integration", "time_interval": 5, "function": invoke_mimecast_integration}
148 ]
149 for job in known_jobs:
@@ -196,6 +211,8 @@ def get_function_by_name(function_name: str):
211 """
212 function_map = {
213 "agent_sync": agent_sync,
214 + "wazuh_index_fields_resize": resize_wazuh_index_fields,
215 + "invoke_alert_creation_collect": invoke_alert_creation_collect,
216 "invoke_mimecast_integration": invoke_mimecast_integration,
217 "invoke_mimecast_integration_ttp": invoke_mimecast_integration_ttp,
218 "invoke_wazuh_monitoring_alert": invoke_wazuh_monitoring_alert,
backend/app/schedulers/services/invoke_alert_creation.py new
+35
@@ -0,0 +1,35 @@
1 +from datetime import datetime
2 +
3 +from loguru import logger
4 +from sqlalchemy.future import select
5 +
6 +from app.db.db_session import get_db_session
7 +from app.incidents.routes.incident_alert import create_alert_auto_route
8 +from app.schedulers.models.scheduler import JobMetadata
9 +
10 +
11 +async def invoke_alert_creation_collect():
12 + """
13 + Synchronizes agents by sending a request to the server and updating the job metadata.
14 +
15 + This function retrieves the scheduler auth token, sends a POST request to the server
16 + to synchronize agents, and updates the job metadata with the last success timestamp.
17 +
18 + If the token retrieval fails, it prints a failure message. If the job metadata for
19 + 'invoke_alert_creation_collect' does not exist, it prints a message indicating the absence of the metadata.
20 + """
21 + logger.info("Invoking alert creation collection via scheduler...")
22 + async with get_db_session() as session:
23 + await create_alert_auto_route(session=session)
24 +
25 + stmt = select(JobMetadata).where(JobMetadata.job_id == "invoke_alert_creation_collect")
26 + result = await session.execute(stmt)
27 + job_metadata = result.scalars().first()
28 +
29 + if job_metadata:
30 + job_metadata.last_success = datetime.utcnow()
31 + session.add(job_metadata)
32 + await session.commit() # Asynchronously commit the transaction
33 + logger.info("Updated job metadata with the last success timestamp.")
34 + else:
35 + logger.warning("JobMetadata for 'invoke_alert_creation_collect' not found.")
backend/app/schedulers/services/wazuh_index_resize.py new
+37
@@ -0,0 +1,37 @@
1 +from datetime import datetime
2 +
3 +from loguru import logger
4 +from sqlalchemy.future import select
5 +
6 +from app.connectors.wazuh_indexer.routes.monitoring import (
7 + resize_wazuh_index_fields_route,
8 +)
9 +from app.db.db_session import get_db_session
10 +from app.schedulers.models.scheduler import JobMetadata
11 +
12 +
13 +async def resize_wazuh_index_fields():
14 + """
15 + Synchronizes agents by sending a request to the server and updating the job metadata.
16 +
17 + This function retrieves the scheduler auth token, sends a POST request to the server
18 + to synchronize agents, and updates the job metadata with the last success timestamp.
19 +
20 + If the token retrieval fails, it prints a failure message. If the job metadata for
21 + 'wazuh_index_fields_resize' does not exist, it prints a message indicating the absence of the metadata.
22 + """
23 + logger.info("Resizing Wazuh index fields via scheduler...")
24 + async with get_db_session() as session:
25 + await resize_wazuh_index_fields_route()
26 +
27 + stmt = select(JobMetadata).where(JobMetadata.job_id == "wazuh_index_fields_resize")
28 + result = await session.execute(stmt)
29 + job_metadata = result.scalars().first()
30 +
31 + if job_metadata:
32 + job_metadata.last_success = datetime.utcnow()
33 + session.add(job_metadata)
34 + await session.commit() # Asynchronously commit the transaction
35 + logger.info("Updated job metadata with the last success timestamp.")
36 + else:
37 + logger.warning("JobMetadata for 'wazuh_index_fields_resize' not found.")
backend/copilot.py
+4
@@ -20,6 +20,7 @@ from app.db.db_setup import create_available_network_connectors
20 from app.db.db_setup import create_copilot_user_if_not_exists
21 from app.db.db_setup import create_database_if_not_exists
22 from app.db.db_setup import create_roles
23 +from app.db.db_setup import delete_connectors
24 from app.db.db_setup import ensure_admin_user
25 from app.db.db_setup import ensure_scheduler_user
26 from app.db.db_setup import ensure_scheduler_user_removed
@@ -47,6 +48,7 @@ from app.routers import grafana
48 from app.routers import graylog
49 from app.routers import healthcheck
50 from app.routers import huntress
51 +from app.routers import incidents
52 from app.routers import influxdb
53 from app.routers import integrations
54 from app.routers import license
@@ -149,6 +151,7 @@ api_router.include_router(crowdstrike.router)
151 api_router.include_router(scoutsuite.router)
152 api_router.include_router(nuclei.router)
153 api_router.include_router(duo.router)
154 +api_router.include_router(incidents.router)
155 api_router.include_router(darktrace.router)
156
157 # Include the APIRouter in the FastAPI app
@@ -163,6 +166,7 @@ async def init_db():
166 await create_copilot_user_if_not_exists(db_url=SQLALCHEMY_DATABASE_URI_NO_DB, db_user_name="copilot")
167 apply_migrations()
168 await add_connectors(async_engine)
169 + await delete_connectors(async_engine)
170 await create_roles(async_engine)
171 await create_available_integrations(async_engine)
172 await create_available_network_connectors(async_engine)
backend/requirements.txt
+3 -1
@@ -90,7 +90,8 @@ openai==0.28.1
90 openpyxl==3.1.2
91 ordered-set==4.1.0
92 orjson==3.9.9
93 -packaging==23.2
93 +#packaging==23.2
94 +packaging>=24.1,<25.0
95 #pandas==2.1.1 Had to drop to 1.3.5 which is compatible with python-dateutils==2.8.0 which is required by scoutsuite
96 pandas==1.3.5
97 passlib==1.7.4
@@ -118,6 +119,7 @@ PyJWT==2.8.0
119 PyMySQL==1.1.0
120 pyparsing==3.1.1
121 pyrsistent==0.19.3
122 +pySigma==0.11.9
123 pytest==7.4.2
124 pytest-playwright==0.4.4
125 python-dateutil==2.8.0
frontend/package-lock.json
+1402 -1266
@@ -10,41 +10,42 @@
10 "dependencies": {
11 "@ajoelp/json-to-formdata": "^1.5.0",
12 "@f3ve/vue-markdown-it": "^0.2.2",
13 - "@fontsource/jetbrains-mono": "^5.0.20",
14 - "@fontsource/lexend": "^5.0.20",
13 + "@fontsource/jetbrains-mono": "^5.0.21",
14 + "@fontsource/lexend": "^5.0.21",
15 "@fontsource/public-sans": "^5.0.18",
16 "@popperjs/core": "^2.11.8",
17 - "@shikijs/markdown-it": "^1.10.3",
18 - "@vueuse/components": "^10.11.0",
19 - "@vueuse/core": "^10.11.0",
20 - "apexcharts": "^3.50.0",
17 + "@shikijs/markdown-it": "^1.14.1",
18 + "@vueuse/components": "^11.0.3",
19 + "@vueuse/core": "^11.0.3",
20 + "apexcharts": "^3.52.0",
21 + "axios": "^1.7.5",
22 "bytes": "^3.1.2",
23 "colord": "^2.9.3",
24 "crypto-js": "^4.2.0",
24 - "dayjs": "^1.11.11",
25 + "dayjs": "^1.11.13",
26 "detect-touch-device": "^1.1.6",
27 "echarts": "^5.5.1",
28 "file-saver": "^2.0.5",
29 "html-entities": "^2.5.2",
29 - "jose": "^5.6.3",
30 + "jose": "^5.8.0",
31 "js-md5": "^0.8.3",
32 "lodash": "^4.17.21",
33 "mitt": "^3.0.1",
33 - "naive-ui": "^2.38.2",
34 + "naive-ui": "^2.39.0",
35 "password-validator": "^5.3.0",
35 - "pinia": "^2.1.7",
36 + "pinia": "^2.2.2",
37 "pinia-plugin-persistedstate": "^3.2.1",
38 "secure-ls": "^2.0.0",
38 - "shiki": "^1.10.3",
39 + "shiki": "^1.14.1",
40 "validator": "^13.12.0",
40 - "vue": "^3.4.31",
41 + "vue": "^3.4.38",
42 "vue-advanced-cropper": "^2.8.9",
43 "vue-highlight-words": "^3.0.1",
43 - "vue-i18n": "^9.13.1",
44 - "vue-router": "^4.4.0",
44 + "vue-i18n": "^9.14.0",
45 + "vue-router": "^4.4.3",
46 "vue-sjv": "^0.0.6",
47 "vue3-apexcharts": "^1.5.3",
47 - "vue3-marquee": "^4.2.0",
48 + "vue3-marquee": "^4.2.1",
49 "vuedraggable": "^4.1.0"
50 },
51 "devDependencies": {
@@ -56,49 +57,49 @@
57 "@types/fs-extra": "^11.0.4",
58 "@types/inquirer": "^9.0.7",
59 "@types/jsdom": "^21.1.7",
59 - "@types/lodash": "^4.17.6",
60 - "@types/node": "^20.14.10",
61 - "@types/validator": "^13.12.0",
62 - "@vitejs/plugin-vue": "^5.0.5",
63 - "@vitejs/plugin-vue-jsx": "^4.0.0",
60 + "@types/lodash": "^4.17.7",
61 + "@types/node": "^22.5.0",
62 + "@types/validator": "^13.12.1",
63 + "@vitejs/plugin-vue": "^5.1.2",
64 + "@vitejs/plugin-vue-jsx": "^4.0.1",
65 "@vue/eslint-config-prettier": "^9.0.0",
66 "@vue/eslint-config-typescript": "^13.0.0",
67 "@vue/test-utils": "^2.4.6",
68 "@vue/tsconfig": "^0.5.1",
68 - "autoprefixer": "^10.4.19",
69 - "cypress": "^13.13.0",
70 - "eslint": "^9.6.0",
71 - "eslint-plugin-cypress": "^3.3.0",
69 + "autoprefixer": "^10.4.20",
70 + "cypress": "^13.13.3",
71 + "eslint": "^9.9.1",
72 + "eslint-plugin-cypress": "^3.5.0",
73 "eslint-plugin-vue": "^9.27.0",
74 "flourite": "^1.3.0",
75 "fs-extra": "^11.2.0",
75 - "globals": "^15.8.0",
76 - "jsdom": "^24.1.0",
76 + "globals": "^15.9.0",
77 + "jsdom": "^25.0.0",
78 "json5": "^2.2.3",
79 "npm-run-all2": "^6.2.2",
80 "picocolors": "^1.0.1",
80 - "postcss": "^8.4.39",
81 - "prettier": "^3.3.2",
82 - "sass": "^1.77.7",
83 - "start-server-and-test": "^2.0.4",
81 + "postcss": "^8.4.41",
82 + "prettier": "^3.3.3",
83 + "sass": "^1.77.8",
84 + "start-server-and-test": "^2.0.5",
85 "tailwind-config-viewer": "^2.0.4",
85 - "tailwindcss": "^3.4.4",
86 - "taze": "^0.14.2",
87 - "type-fest": "^4.21.0",
88 - "unplugin-vue-components": "^0.27.2",
89 - "vite": "^5.3.3",
90 - "vite-bundle-analyzer": "^0.10.3",
86 + "tailwindcss": "^3.4.10",
87 + "taze": "^0.16.7",
88 + "type-fest": "^4.25.0",
89 + "unplugin-vue-components": "^0.27.4",
90 + "vite": "^5.4.2",
91 + "vite-bundle-analyzer": "^0.10.6",
92 "vite-bundle-visualizer": "^1.2.1",
92 - "vite-plugin-vue-devtools": "^7.3.5",
93 + "vite-plugin-vue-devtools": "^7.3.9",
94 "vite-svg-loader": "^5.1.0",
94 - "vitest": "^2.0.2",
95 - "vue-tsc": "^2.0.26"
95 + "vitest": "^2.0.5",
96 + "vue-tsc": "^2.0.29"
97 },
98 "engines": {
99 "node": ">=18.0.0"
100 },
101 "optionalDependencies": {
101 - "@rollup/rollup-linux-x64-gnu": "^4.18.1"
102 + "@rollup/rollup-linux-x64-gnu": "^4.21.1"
103 }
104 },
105 "node_modules/@ajoelp/json-to-formdata": {
@@ -135,9 +136,9 @@
136 }
137 },
138 "node_modules/@antfu/ni": {
138 - "version": "0.21.12",
139 - "resolved": "https://registry.npmjs.org/@antfu/ni/-/ni-0.21.12.tgz",
140 - "integrity": "sha512-2aDL3WUv8hMJb2L3r/PIQWsTLyq7RQr3v9xD16fiz6O8ys1xEyLhhTOv8gxtZvJiTzjTF5pHoArvRdesGL1DMQ==",
139 + "version": "0.23.0",
140 + "resolved": "https://registry.npmjs.org/@antfu/ni/-/ni-0.23.0.tgz",
141 + "integrity": "sha512-R5/GkA3PfGewAXLzz6lN5XagunF6PKeDtWt8dbZQXvHfebLS0qEczV+Azg/d+tKgSh6kRBpxvu8oSjARdPtw0A==",
142 "dev": true,
143 "bin": {
144 "na": "bin/na.mjs",
@@ -171,30 +172,30 @@
172 }
173 },
174 "node_modules/@babel/compat-data": {
174 - "version": "7.24.7",
175 - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.7.tgz",
176 - "integrity": "sha512-qJzAIcv03PyaWqxRgO4mSU3lihncDT296vnyuE2O8uA4w3UHWI4S3hgeZd1L8W1Bft40w9JxJ2b412iDUFFRhw==",
175 + "version": "7.25.2",
176 + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz",
177 + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==",
178 "dev": true,
179 "engines": {
180 "node": ">=6.9.0"
181 }
182 },
183 "node_modules/@babel/core": {
183 - "version": "7.24.7",
184 - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.7.tgz",
185 - "integrity": "sha512-nykK+LEK86ahTkX/3TgauT0ikKoNCfKHEaZYTUVupJdTLzGNvrblu4u6fa7DhZONAltdf8e662t/abY8idrd/g==",
184 + "version": "7.25.2",
185 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz",
186 + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==",
187 "dev": true,
188 "dependencies": {
189 "@ampproject/remapping": "^2.2.0",
190 "@babel/code-frame": "^7.24.7",
190 - "@babel/generator": "^7.24.7",
191 - "@babel/helper-compilation-targets": "^7.24.7",
192 - "@babel/helper-module-transforms": "^7.24.7",
193 - "@babel/helpers": "^7.24.7",
194 - "@babel/parser": "^7.24.7",
195 - "@babel/template": "^7.24.7",
196 - "@babel/traverse": "^7.24.7",
197 - "@babel/types": "^7.24.7",
191 + "@babel/generator": "^7.25.0",
192 + "@babel/helper-compilation-targets": "^7.25.2",
193 + "@babel/helper-module-transforms": "^7.25.2",
194 + "@babel/helpers": "^7.25.0",
195 + "@babel/parser": "^7.25.0",
196 + "@babel/template": "^7.25.0",
197 + "@babel/traverse": "^7.25.2",
198 + "@babel/types": "^7.25.2",
199 "convert-source-map": "^2.0.0",
200 "debug": "^4.1.0",
201 "gensync": "^1.0.0-beta.2",
@@ -219,12 +220,12 @@
220 }
221 },
222 "node_modules/@babel/generator": {
222 - "version": "7.24.7",
223 - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz",
224 - "integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==",
223 + "version": "7.25.0",
224 + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz",
225 + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==",
226 "dev": true,
227 "dependencies": {
227 - "@babel/types": "^7.24.7",
228 + "@babel/types": "^7.25.0",
229 "@jridgewell/gen-mapping": "^0.3.5",
230 "@jridgewell/trace-mapping": "^0.3.25",
231 "jsesc": "^2.5.1"
@@ -246,14 +247,14 @@
247 }
248 },
249 "node_modules/@babel/helper-compilation-targets": {
249 - "version": "7.24.7",
250 - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.7.tgz",
251 - "integrity": "sha512-ctSdRHBi20qWOfy27RUb4Fhp07KSJ3sXcuSvTrXrc4aG8NSYDo1ici3Vhg9bg69y5bj0Mr1lh0aeEgTvc12rMg==",
250 + "version": "7.25.2",
251 + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz",
252 + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==",
253 "dev": true,
254 "dependencies": {
254 - "@babel/compat-data": "^7.24.7",
255 - "@babel/helper-validator-option": "^7.24.7",
256 - "browserslist": "^4.22.2",
255 + "@babel/compat-data": "^7.25.2",
256 + "@babel/helper-validator-option": "^7.24.8",
257 + "browserslist": "^4.23.1",
258 "lru-cache": "^5.1.1",
259 "semver": "^6.3.1"
260 },
@@ -271,19 +272,17 @@
272 }
273 },
274 "node_modules/@babel/helper-create-class-features-plugin": {
274 - "version": "7.24.7",
275 - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.7.tgz",
276 - "integrity": "sha512-kTkaDl7c9vO80zeX1rJxnuRpEsD5tA81yh11X1gQo+PhSti3JS+7qeZo9U4RHobKRiFPKaGK3svUAeb8D0Q7eg==",
275 + "version": "7.25.0",
276 + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz",
277 + "integrity": "sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==",
278 "dev": true,
279 "dependencies": {
280 "@babel/helper-annotate-as-pure": "^7.24.7",
280 - "@babel/helper-environment-visitor": "^7.24.7",
281 - "@babel/helper-function-name": "^7.24.7",
282 - "@babel/helper-member-expression-to-functions": "^7.24.7",
281 + "@babel/helper-member-expression-to-functions": "^7.24.8",
282 "@babel/helper-optimise-call-expression": "^7.24.7",
284 - "@babel/helper-replace-supers": "^7.24.7",
283 + "@babel/helper-replace-supers": "^7.25.0",
284 "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7",
286 - "@babel/helper-split-export-declaration": "^7.24.7",
285 + "@babel/traverse": "^7.25.0",
286 "semver": "^6.3.1"
287 },
288 "engines": {
@@ -302,51 +301,14 @@
301 "semver": "bin/semver.js"
302 }
303 },
305 - "node_modules/@babel/helper-environment-visitor": {
306 - "version": "7.24.7",
307 - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz",
308 - "integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==",
309 - "dev": true,
310 - "dependencies": {
311 - "@babel/types": "^7.24.7"
312 - },
313 - "engines": {
314 - "node": ">=6.9.0"
315 - }
316 - },
317 - "node_modules/@babel/helper-function-name": {
318 - "version": "7.24.7",
319 - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz",
320 - "integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==",
321 - "dev": true,
322 - "dependencies": {
323 - "@babel/template": "^7.24.7",
324 - "@babel/types": "^7.24.7"
325 - },
326 - "engines": {
327 - "node": ">=6.9.0"
328 - }
329 - },
330 - "node_modules/@babel/helper-hoist-variables": {
331 - "version": "7.24.7",
332 - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz",
333 - "integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==",
334 - "dev": true,
335 - "dependencies": {
336 - "@babel/types": "^7.24.7"
337 - },
338 - "engines": {
339 - "node": ">=6.9.0"
340 - }
341 - },
304 "node_modules/@babel/helper-member-expression-to-functions": {
343 - "version": "7.24.7",
344 - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.7.tgz",
345 - "integrity": "sha512-LGeMaf5JN4hAT471eJdBs/GK1DoYIJ5GCtZN/EsL6KUiiDZOvO/eKE11AMZJa2zP4zk4qe9V2O/hxAmkRc8p6w==",
305 + "version": "7.24.8",
306 + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz",
307 + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==",
308 "dev": true,
309 "dependencies": {
348 - "@babel/traverse": "^7.24.7",
349 - "@babel/types": "^7.24.7"
310 + "@babel/traverse": "^7.24.8",
311 + "@babel/types": "^7.24.8"
312 },
313 "engines": {
314 "node": ">=6.9.0"
@@ -366,16 +328,15 @@
328 }
329 },
330 "node_modules/@babel/helper-module-transforms": {
369 - "version": "7.24.7",
370 - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.7.tgz",
371 - "integrity": "sha512-1fuJEwIrp+97rM4RWdO+qrRsZlAeL1lQJoPqtCYWv0NL115XM93hIH4CSRln2w52SqvmY5hqdtauB6QFCDiZNQ==",
331 + "version": "7.25.2",
332 + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz",
333 + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==",
334 "dev": true,
335 "dependencies": {
374 - "@babel/helper-environment-visitor": "^7.24.7",
336 "@babel/helper-module-imports": "^7.24.7",
337 "@babel/helper-simple-access": "^7.24.7",
377 - "@babel/helper-split-export-declaration": "^7.24.7",
378 - "@babel/helper-validator-identifier": "^7.24.7"
338 + "@babel/helper-validator-identifier": "^7.24.7",
339 + "@babel/traverse": "^7.25.2"
340 },
341 "engines": {
342 "node": ">=6.9.0"
@@ -397,23 +358,23 @@
358 }
359 },
360 "node_modules/@babel/helper-plugin-utils": {
400 - "version": "7.24.7",
401 - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.7.tgz",
402 - "integrity": "sha512-Rq76wjt7yz9AAc1KnlRKNAi/dMSVWgDRx43FHoJEbcYU6xOWaE2dVPwcdTukJrjxS65GITyfbvEYHvkirZ6uEg==",
361 + "version": "7.24.8",
362 + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz",
363 + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==",
364 "dev": true,
365 "engines": {
366 "node": ">=6.9.0"
367 }
368 },
369 "node_modules/@babel/helper-replace-supers": {
409 - "version": "7.24.7",
410 - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.7.tgz",
411 - "integrity": "sha512-qTAxxBM81VEyoAY0TtLrx1oAEJc09ZK67Q9ljQToqCnA+55eNwCORaxlKyu+rNfX86o8OXRUSNUnrtsAZXM9sg==",
370 + "version": "7.25.0",
371 + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz",
372 + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==",
373 "dev": true,
374 "dependencies": {
414 - "@babel/helper-environment-visitor": "^7.24.7",
415 - "@babel/helper-member-expression-to-functions": "^7.24.7",
416 - "@babel/helper-optimise-call-expression": "^7.24.7"
375 + "@babel/helper-member-expression-to-functions": "^7.24.8",
376 + "@babel/helper-optimise-call-expression": "^7.24.7",
377 + "@babel/traverse": "^7.25.0"
378 },
379 "engines": {
380 "node": ">=6.9.0"
@@ -448,23 +409,10 @@
409 "node": ">=6.9.0"
410 }
411 },
451 - "node_modules/@babel/helper-split-export-declaration": {
452 - "version": "7.24.7",
453 - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz",
454 - "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==",
455 - "dev": true,
456 - "dependencies": {
457 - "@babel/types": "^7.24.7"
458 - },
459 - "engines": {
460 - "node": ">=6.9.0"
461 - }
462 - },
412 "node_modules/@babel/helper-string-parser": {
464 - "version": "7.24.7",
465 - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz",
466 - "integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==",
467 - "dev": true,
413 + "version": "7.24.8",
414 + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz",
415 + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==",
416 "engines": {
417 "node": ">=6.9.0"
418 }
@@ -473,28 +421,27 @@
421 "version": "7.24.7",
422 "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
423 "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
476 - "dev": true,
424 "engines": {
425 "node": ">=6.9.0"
426 }
427 },
428 "node_modules/@babel/helper-validator-option": {
482 - "version": "7.24.7",
483 - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.7.tgz",
484 - "integrity": "sha512-yy1/KvjhV/ZCL+SM7hBrvnZJ3ZuT9OuZgIJAGpPEToANvc3iM6iDvBnRjtElWibHU6n8/LPR/EjX9EtIEYO3pw==",
429 + "version": "7.24.8",
430 + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz",
431 + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==",
432 "dev": true,
433 "engines": {
434 "node": ">=6.9.0"
435 }
436 },
437 "node_modules/@babel/helpers": {
491 - "version": "7.24.7",
492 - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.7.tgz",
493 - "integrity": "sha512-NlmJJtvcw72yRJRcnCmGvSi+3jDEg8qFu3z0AFoymmzLx5ERVWyzd9kVXr7Th9/8yIJi2Zc6av4Tqz3wFs8QWg==",
438 + "version": "7.25.0",
439 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz",
440 + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==",
441 "dev": true,
442 "dependencies": {
496 - "@babel/template": "^7.24.7",
497 - "@babel/types": "^7.24.7"
443 + "@babel/template": "^7.25.0",
444 + "@babel/types": "^7.25.0"
445 },
446 "engines": {
447 "node": ">=6.9.0"
@@ -516,9 +463,12 @@
463 }
464 },
465 "node_modules/@babel/parser": {
519 - "version": "7.24.7",
520 - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz",
521 - "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==",
466 + "version": "7.25.3",
467 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz",
468 + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==",
469 + "dependencies": {
470 + "@babel/types": "^7.25.2"
471 + },
472 "bin": {
473 "parser": "bin/babel-parser.js"
474 },
@@ -616,14 +566,15 @@
566 }
567 },
568 "node_modules/@babel/plugin-transform-typescript": {
619 - "version": "7.24.7",
620 - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.7.tgz",
621 - "integrity": "sha512-iLD3UNkgx2n/HrjBesVbYX6j0yqn/sJktvbtKKgcaLIQ4bTTQ8obAypc1VpyHPD2y4Phh9zHOaAt8e/L14wCpw==",
569 + "version": "7.25.2",
570 + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz",
571 + "integrity": "sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A==",
572 "dev": true,
573 "dependencies": {
574 "@babel/helper-annotate-as-pure": "^7.24.7",
625 - "@babel/helper-create-class-features-plugin": "^7.24.7",
626 - "@babel/helper-plugin-utils": "^7.24.7",
575 + "@babel/helper-create-class-features-plugin": "^7.25.0",
576 + "@babel/helper-plugin-utils": "^7.24.8",
577 + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7",
578 "@babel/plugin-syntax-typescript": "^7.24.7"
579 },
580 "engines": {
@@ -634,9 +585,9 @@
585 }
586 },
587 "node_modules/@babel/runtime": {
637 - "version": "7.24.7",
638 - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz",
639 - "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==",
588 + "version": "7.25.0",
589 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz",
590 + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==",
591 "dependencies": {
592 "regenerator-runtime": "^0.14.0"
593 },
@@ -645,33 +596,30 @@
596 }
597 },
598 "node_modules/@babel/template": {
648 - "version": "7.24.7",
649 - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
650 - "integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
599 + "version": "7.25.0",
600 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz",
601 + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==",
602 "dev": true,
603 "dependencies": {
604 "@babel/code-frame": "^7.24.7",
654 - "@babel/parser": "^7.24.7",
655 - "@babel/types": "^7.24.7"
605 + "@babel/parser": "^7.25.0",
606 + "@babel/types": "^7.25.0"
607 },
608 "engines": {
609 "node": ">=6.9.0"
610 }
611 },
612 "node_modules/@babel/traverse": {
662 - "version": "7.24.7",
663 - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz",
664 - "integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==",
613 + "version": "7.25.3",
614 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz",
615 + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==",
616 "dev": true,
617 "dependencies": {
618 "@babel/code-frame": "^7.24.7",
668 - "@babel/generator": "^7.24.7",
669 - "@babel/helper-environment-visitor": "^7.24.7",
670 - "@babel/helper-function-name": "^7.24.7",
671 - "@babel/helper-hoist-variables": "^7.24.7",
672 - "@babel/helper-split-export-declaration": "^7.24.7",
673 - "@babel/parser": "^7.24.7",
674 - "@babel/types": "^7.24.7",
619 + "@babel/generator": "^7.25.0",
620 + "@babel/parser": "^7.25.3",
621 + "@babel/template": "^7.25.0",
622 + "@babel/types": "^7.25.2",
623 "debug": "^4.3.1",
624 "globals": "^11.1.0"
625 },
@@ -689,12 +637,11 @@
637 }
638 },
639 "node_modules/@babel/types": {
692 - "version": "7.24.7",
693 - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz",
694 - "integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==",
695 - "dev": true,
640 + "version": "7.25.2",
641 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz",
642 + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==",
643 "dependencies": {
697 - "@babel/helper-string-parser": "^7.24.7",
644 + "@babel/helper-string-parser": "^7.24.8",
645 "@babel/helper-validator-identifier": "^7.24.7",
646 "to-fast-properties": "^2.0.0"
647 },
@@ -729,7 +676,6 @@
676 },
677 "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
678 "version": "1.3.0",
732 - "dev": true,
679 "inBundle": true,
680 "license": "MIT",
681 "engines": {
@@ -1209,9 +1155,9 @@
1155 }
1156 },
1157 "node_modules/@eslint/config-array": {
1212 - "version": "0.17.0",
1213 - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.0.tgz",
1214 - "integrity": "sha512-A68TBu6/1mHHuc5YJL0U0VVeGNiklLAL6rRmhTCP2B5XjWLMnrX+HkO+IAXyHvks5cyyY1jjK5ITPQ1HGS2EVA==",
1158 + "version": "0.18.0",
1159 + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
1160 + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
1161 "dependencies": {
1162 "@eslint/object-schema": "^2.1.4",
1163 "debug": "^4.3.1",
@@ -1465,14 +1411,14 @@
1411 }
1412 },
1413 "node_modules/@fontsource/jetbrains-mono": {
1468 - "version": "5.0.20",
1469 - "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.20.tgz",
1470 - "integrity": "sha512-QkrihYWqftzs+04TinulIhnFqNwO6990HR07iCUae/6daOZAMy7urUPzytrRT9M8KLTTHBeHOY0CKqOs1+o2OQ=="
1414 + "version": "5.0.21",
1415 + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.21.tgz",
1416 + "integrity": "sha512-BjI7a4U4HC4R3rEq96dN4CKtH8qxHFA+zU+6Ftusn/9k+VbF+VZ1l5bmHCUtg2ekjXbGF955/1FODlCKXleaGQ=="
1417 },
1418 "node_modules/@fontsource/lexend": {
1473 - "version": "5.0.20",
1474 - "resolved": "https://registry.npmjs.org/@fontsource/lexend/-/lexend-5.0.20.tgz",
1475 - "integrity": "sha512-up8jVBRNP7AMRb7MuRBU/Nh1qHqI1ZWIVfqO1fa4gzOwNEnXyPMfvNy4LV2zYqrS19MfTe5ic7kUKpohsVVsZg=="
1419 + "version": "5.0.21",
1420 + "resolved": "https://registry.npmjs.org/@fontsource/lexend/-/lexend-5.0.21.tgz",
1421 + "integrity": "sha512-y/rRR0B77mSEOqBKZKixT0b9h9Rnqpnyk/ZIqZv9yvxyAJWLEjUwc1GhkagdpsUOB0Sn6zVk1V4PNespcjSxZA=="
1422 },
1423 "node_modules/@fontsource/public-sans": {
1424 "version": "5.0.18",
@@ -1540,12 +1486,12 @@
1486 }
1487 },
1488 "node_modules/@intlify/core-base": {
1543 - "version": "9.13.1",
1544 - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.13.1.tgz",
1545 - "integrity": "sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w==",
1489 + "version": "9.14.0",
1490 + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.0.tgz",
1491 + "integrity": "sha512-zJn0imh9HIsZZUtt9v8T16PeVstPv6bP2YzlrYJwoF8F30gs4brZBwW2KK6EI5WYKFi3NeqX6+UU4gniz5TkGg==",
1492 "dependencies": {
1547 - "@intlify/message-compiler": "9.13.1",
1548 - "@intlify/shared": "9.13.1"
1493 + "@intlify/message-compiler": "9.14.0",
1494 + "@intlify/shared": "9.14.0"
1495 },
1496 "engines": {
1497 "node": ">= 16"
@@ -1555,11 +1501,11 @@
1501 }
1502 },
1503 "node_modules/@intlify/message-compiler": {
1558 - "version": "9.13.1",
1559 - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.13.1.tgz",
1560 - "integrity": "sha512-SKsVa4ajYGBVm7sHMXd5qX70O2XXjm55zdZB3VeMFCvQyvLew/dLvq3MqnaIsTMF1VkkOb9Ttr6tHcMlyPDL9w==",
1504 + "version": "9.14.0",
1505 + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.0.tgz",
1506 + "integrity": "sha512-sXNsoMI0YsipSXW8SR75drmVK56tnJHoYbPXUv2Cf9lz6FzvwsosFm6JtC1oQZI/kU+n7qx0qRrEWkeYFTgETA==",
1507 "dependencies": {
1562 - "@intlify/shared": "9.13.1",
1508 + "@intlify/shared": "9.14.0",
1509 "source-map-js": "^1.0.2"
1510 },
1511 "engines": {
@@ -1570,9 +1516,9 @@
1516 }
1517 },
1518 "node_modules/@intlify/shared": {
1573 - "version": "9.13.1",
1574 - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.13.1.tgz",
1575 - "integrity": "sha512-u3b6BKGhE6j/JeRU6C/RL2FgyJfy6LakbtfeVF8fJXURpZZTzfh3e05J0bu0XPw447Q6/WUp3C4ajv4TMS4YsQ==",
1519 + "version": "9.14.0",
1520 + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.0.tgz",
1521 + "integrity": "sha512-r+N8KRQL7LgN1TMTs1A2svfuAU0J94Wu9wWdJVJqYsoMMLIeJxrPjazihfHpmJqfgZq0ah3Y9Q4pgWV2O90Fyg==",
1522 "engines": {
1523 "node": ">= 16"
1524 },
@@ -1694,21 +1640,6 @@
1640 "@jridgewell/sourcemap-codec": "^1.4.14"
1641 }
1642 },
1697 - "node_modules/@jsdevtools/ez-spawn": {
1698 - "version": "3.0.4",
1699 - "resolved": "https://registry.npmjs.org/@jsdevtools/ez-spawn/-/ez-spawn-3.0.4.tgz",
1700 - "integrity": "sha512-f5DRIOZf7wxogefH03RjMPMdBF7ADTWUMoOs9kaJo06EfwF+aFhMZMDZxHg/Xe12hptN9xoZjGso2fdjapBRIA==",
1701 - "dev": true,
1702 - "dependencies": {
1703 - "call-me-maybe": "^1.0.1",
1704 - "cross-spawn": "^7.0.3",
1705 - "string-argv": "^0.3.1",
1706 - "type-detect": "^4.0.8"
1707 - },
1708 - "engines": {
1709 - "node": ">=10"
1710 - }
1711 - },
1643 "node_modules/@juggle/resize-observer": {
1644 "version": "3.4.0",
1645 "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
@@ -1762,6 +1693,49 @@
1693 "node": ">= 8"
1694 }
1695 },
1696 + "node_modules/@npmcli/agent": {
1697 + "version": "2.2.2",
1698 + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz",
1699 + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==",
1700 + "dev": true,
1701 + "dependencies": {
1702 + "agent-base": "^7.1.0",
1703 + "http-proxy-agent": "^7.0.0",
1704 + "https-proxy-agent": "^7.0.1",
1705 + "lru-cache": "^10.0.1",
1706 + "socks-proxy-agent": "^8.0.3"
1707 + },
1708 + "engines": {
1709 + "node": "^16.14.0 || >=18.0.0"
1710 + }
1711 + },
1712 + "node_modules/@npmcli/agent/node_modules/lru-cache": {
1713 + "version": "10.4.3",
1714 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
1715 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
1716 + "dev": true
1717 + },
1718 + "node_modules/@npmcli/fs": {
1719 + "version": "3.1.1",
1720 + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz",
1721 + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==",
1722 + "dev": true,
1723 + "dependencies": {
1724 + "semver": "^7.3.5"
1725 + },
1726 + "engines": {
1727 + "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
1728 + }
1729 + },
1730 + "node_modules/@npmcli/redact": {
1731 + "version": "2.0.1",
1732 + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-2.0.1.tgz",
1733 + "integrity": "sha512-YgsR5jCQZhVmTJvjduTOIHph0L73pK8xwMVaDY0PatySqVM9AZj93jpoXYSJqfHFxFkN9dmqTw6OiqExsS3LPw==",
1734 + "dev": true,
1735 + "engines": {
1736 + "node": "^16.14.0 || >=18.0.0"
1737 + }
1738 + },
1739 "node_modules/@one-ini/wasm": {
1740 "version": "0.1.1",
1741 "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
@@ -1827,9 +1801,9 @@
1801 }
1802 },
1803 "node_modules/@rollup/rollup-android-arm-eabi": {
1830 - "version": "4.18.1",
1831 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.1.tgz",
1832 - "integrity": "sha512-lncuC4aHicncmbORnx+dUaAgzee9cm/PbIqgWz1PpXuwc+sa1Ct83tnqUDy/GFKleLiN7ZIeytM6KJ4cAn1SxA==",
1804 + "version": "4.21.0",
1805 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.0.tgz",
1806 + "integrity": "sha512-WTWD8PfoSAJ+qL87lE7votj3syLavxunWhzCnx3XFxFiI/BA/r3X7MUM8dVrH8rb2r4AiO8jJsr3ZjdaftmnfA==",
1807 "cpu": [
1808 "arm"
1809 ],
@@ -1840,9 +1814,9 @@
1814 ]
1815 },
1816 "node_modules/@rollup/rollup-android-arm64": {
1843 - "version": "4.18.1",
1844 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.1.tgz",
1845 - "integrity": "sha512-F/tkdw0WSs4ojqz5Ovrw5r9odqzFjb5LIgHdHZG65dFI1lWTWRVy32KDJLKRISHgJvqUeUhdIvy43fX41znyDg==",
1817 + "version": "4.21.0",
1818 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.0.tgz",
1819 + "integrity": "sha512-a1sR2zSK1B4eYkiZu17ZUZhmUQcKjk2/j9Me2IDjk1GHW7LB5Z35LEzj9iJch6gtUfsnvZs1ZNyDW2oZSThrkA==",
1820 "cpu": [
1821 "arm64"
1822 ],
@@ -1853,9 +1827,9 @@
1827 ]
1828 },
1829 "node_modules/@rollup/rollup-darwin-arm64": {
1856 - "version": "4.18.1",
1857 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz",
1858 - "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==",
1830 + "version": "4.21.0",
1831 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.0.tgz",
1832 + "integrity": "sha512-zOnKWLgDld/svhKO5PD9ozmL6roy5OQ5T4ThvdYZLpiOhEGY+dp2NwUmxK0Ld91LrbjrvtNAE0ERBwjqhZTRAA==",
1833 "cpu": [
1834 "arm64"
1835 ],
@@ -1866,9 +1840,9 @@
1840 ]
1841 },
1842 "node_modules/@rollup/rollup-darwin-x64": {
1869 - "version": "4.18.1",
1870 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.1.tgz",
1871 - "integrity": "sha512-IgpzXKauRe1Tafcej9STjSSuG0Ghu/xGYH+qG6JwsAUxXrnkvNHcq/NL6nz1+jzvWAnQkuAJ4uIwGB48K9OCGA==",
1843 + "version": "4.21.0",
1844 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.0.tgz",
1845 + "integrity": "sha512-7doS8br0xAkg48SKE2QNtMSFPFUlRdw9+votl27MvT46vo44ATBmdZdGysOevNELmZlfd+NEa0UYOA8f01WSrg==",
1846 "cpu": [
1847 "x64"
1848 ],
@@ -1879,9 +1853,9 @@
1853 ]
1854 },
1855 "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
1882 - "version": "4.18.1",
1883 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.1.tgz",
1884 - "integrity": "sha512-P9bSiAUnSSM7EmyRK+e5wgpqai86QOSv8BwvkGjLwYuOpaeomiZWifEos517CwbG+aZl1T4clSE1YqqH2JRs+g==",
1856 + "version": "4.21.0",
1857 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.0.tgz",
1858 + "integrity": "sha512-pWJsfQjNWNGsoCq53KjMtwdJDmh/6NubwQcz52aEwLEuvx08bzcy6tOUuawAOncPnxz/3siRtd8hiQ32G1y8VA==",
1859 "cpu": [
1860 "arm"
1861 ],
@@ -1892,9 +1866,9 @@
1866 ]
1867 },
1868 "node_modules/@rollup/rollup-linux-arm-musleabihf": {
1895 - "version": "4.18.1",
1896 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.1.tgz",
1897 - "integrity": "sha512-5RnjpACoxtS+aWOI1dURKno11d7krfpGDEn19jI8BuWmSBbUC4ytIADfROM1FZrFhQPSoP+KEa3NlEScznBTyQ==",
1869 + "version": "4.21.0",
1870 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.0.tgz",
1871 + "integrity": "sha512-efRIANsz3UHZrnZXuEvxS9LoCOWMGD1rweciD6uJQIx2myN3a8Im1FafZBzh7zk1RJ6oKcR16dU3UPldaKd83w==",
1872 "cpu": [
1873 "arm"
1874 ],
@@ -1905,9 +1879,9 @@
1879 ]
1880 },
1881 "node_modules/@rollup/rollup-linux-arm64-gnu": {
1908 - "version": "4.18.1",
1909 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.1.tgz",
1910 - "integrity": "sha512-8mwmGD668m8WaGbthrEYZ9CBmPug2QPGWxhJxh/vCgBjro5o96gL04WLlg5BA233OCWLqERy4YUzX3bJGXaJgQ==",
1882 + "version": "4.21.0",
1883 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.0.tgz",
1884 + "integrity": "sha512-ZrPhydkTVhyeGTW94WJ8pnl1uroqVHM3j3hjdquwAcWnmivjAwOYjTEAuEDeJvGX7xv3Z9GAvrBkEzCgHq9U1w==",
1885 "cpu": [
1886 "arm64"
1887 ],
@@ -1918,9 +1892,9 @@
1892 ]
1893 },
1894 "node_modules/@rollup/rollup-linux-arm64-musl": {
1921 - "version": "4.18.1",
1922 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.1.tgz",
1923 - "integrity": "sha512-dJX9u4r4bqInMGOAQoGYdwDP8lQiisWb9et+T84l2WXk41yEej8v2iGKodmdKimT8cTAYt0jFb+UEBxnPkbXEQ==",
1895 + "version": "4.21.0",
1896 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.0.tgz",
1897 + "integrity": "sha512-cfaupqd+UEFeURmqNP2eEvXqgbSox/LHOyN9/d2pSdV8xTrjdg3NgOFJCtc1vQ/jEke1qD0IejbBfxleBPHnPw==",
1898 "cpu": [
1899 "arm64"
1900 ],
@@ -1931,9 +1905,9 @@
1905 ]
1906 },
1907 "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
1934 - "version": "4.18.1",
1935 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.1.tgz",
1936 - "integrity": "sha512-V72cXdTl4EI0x6FNmho4D502sy7ed+LuVW6Ym8aI6DRQ9hQZdp5sj0a2usYOlqvFBNKQnLQGwmYnujo2HvjCxQ==",
1908 + "version": "4.21.0",
1909 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.0.tgz",
1910 + "integrity": "sha512-ZKPan1/RvAhrUylwBXC9t7B2hXdpb/ufeu22pG2psV7RN8roOfGurEghw1ySmX/CmDDHNTDDjY3lo9hRlgtaHg==",
1911 "cpu": [
1912 "ppc64"
1913 ],
@@ -1944,9 +1918,9 @@
1918 ]
1919 },
1920 "node_modules/@rollup/rollup-linux-riscv64-gnu": {
1947 - "version": "4.18.1",
1948 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.1.tgz",
1949 - "integrity": "sha512-f+pJih7sxoKmbjghrM2RkWo2WHUW8UbfxIQiWo5yeCaCM0TveMEuAzKJte4QskBp1TIinpnRcxkquY+4WuY/tg==",
1921 + "version": "4.21.0",
1922 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.0.tgz",
1923 + "integrity": "sha512-H1eRaCwd5E8eS8leiS+o/NqMdljkcb1d6r2h4fKSsCXQilLKArq6WS7XBLDu80Yz+nMqHVFDquwcVrQmGr28rg==",
1924 "cpu": [
1925 "riscv64"
1926 ],
@@ -1957,9 +1931,9 @@
1931 ]
1932 },
1933 "node_modules/@rollup/rollup-linux-s390x-gnu": {
1960 - "version": "4.18.1",
1961 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.1.tgz",
1962 - "integrity": "sha512-qb1hMMT3Fr/Qz1OKovCuUM11MUNLUuHeBC2DPPAWUYYUAOFWaxInaTwTQmc7Fl5La7DShTEpmYwgdt2hG+4TEg==",
1934 + "version": "4.21.0",
1935 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.0.tgz",
1936 + "integrity": "sha512-zJ4hA+3b5tu8u7L58CCSI0A9N1vkfwPhWd/puGXwtZlsB5bTkwDNW/+JCU84+3QYmKpLi+XvHdmrlwUwDA6kqw==",
1937 "cpu": [
1938 "s390x"
1939 ],
@@ -1970,9 +1944,9 @@
1944 ]
1945 },
1946 "node_modules/@rollup/rollup-linux-x64-gnu": {
1973 - "version": "4.18.1",
1974 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.1.tgz",
1975 - "integrity": "sha512-7O5u/p6oKUFYjRbZkL2FLbwsyoJAjyeXHCU3O4ndvzg2OFO2GinFPSJFGbiwFDaCFc+k7gs9CF243PwdPQFh5g==",
1947 + "version": "4.21.1",
1948 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz",
1949 + "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==",
1950 "cpu": [
1951 "x64"
1952 ],
@@ -1982,9 +1956,9 @@
1956 ]
1957 },
1958 "node_modules/@rollup/rollup-linux-x64-musl": {
1985 - "version": "4.18.1",
1986 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.1.tgz",
1987 - "integrity": "sha512-pDLkYITdYrH/9Cv/Vlj8HppDuLMDUBmgsM0+N+xLtFd18aXgM9Nyqupb/Uw+HeidhfYg2lD6CXvz6CjoVOaKjQ==",
1959 + "version": "4.21.0",
1960 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.0.tgz",
1961 + "integrity": "sha512-1vvmgDdUSebVGXWX2lIcgRebqfQSff0hMEkLJyakQ9JQUbLDkEaMsPTLOmyccyC6IJ/l3FZuJbmrBw/u0A0uCQ==",
1962 "cpu": [
1963 "x64"
1964 ],
@@ -1995,9 +1969,9 @@
1969 ]
1970 },
1971 "node_modules/@rollup/rollup-win32-arm64-msvc": {
1998 - "version": "4.18.1",
1999 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.1.tgz",
2000 - "integrity": "sha512-W2ZNI323O/8pJdBGil1oCauuCzmVd9lDmWBBqxYZcOqWD6aWqJtVBQ1dFrF4dYpZPks6F+xCZHfzG5hYlSHZ6g==",
1972 + "version": "4.21.0",
1973 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.0.tgz",
1974 + "integrity": "sha512-s5oFkZ/hFcrlAyBTONFY1TWndfyre1wOMwU+6KCpm/iatybvrRgmZVM+vCFwxmC5ZhdlgfE0N4XorsDpi7/4XQ==",
1975 "cpu": [
1976 "arm64"
1977 ],
@@ -2008,9 +1982,9 @@
1982 ]
1983 },
1984 "node_modules/@rollup/rollup-win32-ia32-msvc": {
2011 - "version": "4.18.1",
2012 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.1.tgz",
2013 - "integrity": "sha512-ELfEX1/+eGZYMaCIbK4jqLxO1gyTSOIlZr6pbC4SRYFaSIDVKOnZNMdoZ+ON0mrFDp4+H5MhwNC1H/AhE3zQLg==",
1985 + "version": "4.21.0",
1986 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.0.tgz",
1987 + "integrity": "sha512-G9+TEqRnAA6nbpqyUqgTiopmnfgnMkR3kMukFBDsiyy23LZvUCpiUwjTRx6ezYCjJODXrh52rBR9oXvm+Fp5wg==",
1988 "cpu": [
1989 "ia32"
1990 ],
@@ -2021,9 +1995,9 @@
1995 ]
1996 },
1997 "node_modules/@rollup/rollup-win32-x64-msvc": {
2024 - "version": "4.18.1",
2025 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.1.tgz",
2026 - "integrity": "sha512-yjk2MAkQmoaPYCSu35RLJ62+dz358nE83VfTePJRp8CG7aMg25mEJYpXFiD+NcevhX8LxD5OP5tktPXnXN7GDw==",
1998 + "version": "4.21.0",
1999 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.0.tgz",
2000 + "integrity": "sha512-2jsCDZwtQvRhejHLfZ1JY6w6kEuEtfF9nzYsZxzSlNVKDX+DpsDJ+Rbjkm74nvg2rdx0gwBS+IMdvwJuq3S9pQ==",
2001 "cpu": [
2002 "x64"
2003 ],
@@ -2034,29 +2008,29 @@
2008 ]
2009 },
2010 "node_modules/@shikijs/core": {
2037 - "version": "1.10.3",
2038 - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.10.3.tgz",
2039 - "integrity": "sha512-D45PMaBaeDHxww+EkcDQtDAtzv00Gcsp72ukBtaLSmqRvh0WgGMq3Al0rl1QQBZfuneO75NXMIzEZGFitThWbg==",
2011 + "version": "1.14.1",
2012 + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.14.1.tgz",
2013 + "integrity": "sha512-KyHIIpKNaT20FtFPFjCQB5WVSTpLR/n+jQXhWHWVUMm9MaOaG9BGOG0MSyt7yA4+Lm+4c9rTc03tt3nYzeYSfw==",
2014 "dependencies": {
2015 "@types/hast": "^3.0.4"
2016 }
2017 },
2018 "node_modules/@shikijs/markdown-it": {
2045 - "version": "1.10.3",
2046 - "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.10.3.tgz",
2047 - "integrity": "sha512-QH0SbMx+pnUNr6/CWrq3SbfT3myZ0uJGFTsp5+7mlzPbX+73FjRO9K03A7gmv+LutqSiDQCOfkeixSuP7a4sug==",
2019 + "version": "1.14.1",
2020 + "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.14.1.tgz",
2021 + "integrity": "sha512-bb7IHIKLqKMf8ufjdRmsw2wfvX6k9e1DJCfqWEj6RMP59aFohX3emhXIoY8M8uxVfwMTi5ik9QAXGBg6DDnG+A==",
2022 "dependencies": {
2049 - "@shikijs/transformers": "1.10.3",
2023 + "@shikijs/transformers": "1.14.1",
2024 "markdown-it": "^14.1.0",
2051 - "shiki": "1.10.3"
2025 + "shiki": "1.14.1"
2026 }
2027 },
2028 "node_modules/@shikijs/transformers": {
2055 - "version": "1.10.3",
2056 - "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.10.3.tgz",
2057 - "integrity": "sha512-MNjsyye2WHVdxfZUSr5frS97sLGe6G1T+1P41QjyBFJehZphMcr4aBlRLmq6OSPBslYe9byQPVvt/LJCOfxw8Q==",
2029 + "version": "1.14.1",
2030 + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.14.1.tgz",
2031 + "integrity": "sha512-JJqL8QBVCJh3L61jqqEXgFq1cTycwjcGj7aSmqOEsbxnETM9hRlaB74QuXvY/fVJNjbNt8nvWo0VwAXKvMSLRg==",
2032 "dependencies": {
2059 - "shiki": "1.10.3"
2033 + "shiki": "1.14.1"
2034 }
2035 },
2036 "node_modules/@sideway/address": {
@@ -2172,9 +2146,9 @@
2146 "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ=="
2147 },
2148 "node_modules/@types/lodash": {
2175 - "version": "4.17.6",
2176 - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz",
2177 - "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA=="
2149 + "version": "4.17.7",
2150 + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
2151 + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA=="
2152 },
2153 "node_modules/@types/lodash-es": {
2154 "version": "4.17.12",
@@ -2185,12 +2159,12 @@
2159 }
2160 },
2161 "node_modules/@types/node": {
2188 - "version": "20.14.10",
2189 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz",
2190 - "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
2162 + "version": "22.5.0",
2163 + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.0.tgz",
2164 + "integrity": "sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg==",
2165 "dev": true,
2166 "dependencies": {
2193 - "undici-types": "~5.26.4"
2167 + "undici-types": "~6.19.2"
2168 }
2169 },
2170 "node_modules/@types/semver": {
@@ -2226,14 +2200,14 @@
2200 "dev": true
2201 },
2202 "node_modules/@types/unist": {
2229 - "version": "3.0.2",
2230 - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz",
2231 - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ=="
2203 + "version": "3.0.3",
2204 + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
2205 + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
2206 },
2207 "node_modules/@types/validator": {
2234 - "version": "13.12.0",
2235 - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.0.tgz",
2236 - "integrity": "sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==",
2208 + "version": "13.12.1",
2209 + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.12.1.tgz",
2210 + "integrity": "sha512-w0URwf7BQb0rD/EuiG12KP0bailHKHP5YVviJG9zw3ykAokL0TuxU2TUqMB7EwZ59bDHYdeTIvjI5m0S7qHfOA==",
2211 "dev": true
2212 },
2213 "node_modules/@types/web-bluetooth": {
@@ -2252,16 +2226,16 @@
2226 }
2227 },
2228 "node_modules/@typescript-eslint/eslint-plugin": {
2255 - "version": "7.16.0",
2256 - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
2257 - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
2229 + "version": "7.18.0",
2230 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
2231 + "integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
2232 "dev": true,
2233 "dependencies": {
2234 "@eslint-community/regexpp": "^4.10.0",
2261 - "@typescript-eslint/scope-manager": "7.16.0",
2262 - "@typescript-eslint/type-utils": "7.16.0",
2263 - "@typescript-eslint/utils": "7.16.0",
2264 - "@typescript-eslint/visitor-keys": "7.16.0",
2235 + "@typescript-eslint/scope-manager": "7.18.0",
2236 + "@typescript-eslint/type-utils": "7.18.0",
2237 + "@typescript-eslint/utils": "7.18.0",
2238 + "@typescript-eslint/visitor-keys": "7.18.0",
2239 "graphemer": "^1.4.0",
2240 "ignore": "^5.3.1",
2241 "natural-compare": "^1.4.0",
@@ -2285,13 +2259,13 @@
2259 }
2260 },
2261 "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
2288 - "version": "7.16.0",
2289 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
2290 - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
2262 + "version": "7.18.0",
2263 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
2264 + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
2265 "dev": true,
2266 "dependencies": {
2293 - "@typescript-eslint/types": "7.16.0",
2294 - "@typescript-eslint/visitor-keys": "7.16.0"
2267 + "@typescript-eslint/types": "7.18.0",
2268 + "@typescript-eslint/visitor-keys": "7.18.0"
2269 },
2270 "engines": {
2271 "node": "^18.18.0 || >=20.0.0"
@@ -2302,9 +2276,9 @@
2276 }
2277 },
2278 "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": {
2305 - "version": "7.16.0",
2306 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
2307 - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
2279 + "version": "7.18.0",
2280 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
2281 + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
2282 "dev": true,
2283 "engines": {
2284 "node": "^18.18.0 || >=20.0.0"
@@ -2315,13 +2289,13 @@
2289 }
2290 },
2291 "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": {
2318 - "version": "7.16.0",
2319 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
2320 - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
2292 + "version": "7.18.0",
2293 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
2294 + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
2295 "dev": true,
2296 "dependencies": {
2323 - "@typescript-eslint/types": "7.16.0",
2324 - "@typescript-eslint/visitor-keys": "7.16.0",
2297 + "@typescript-eslint/types": "7.18.0",
2298 + "@typescript-eslint/visitor-keys": "7.18.0",
2299 "debug": "^4.3.4",
2300 "globby": "^11.1.0",
2301 "is-glob": "^4.0.3",
@@ -2343,15 +2317,15 @@
2317 }
2318 },
2319 "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": {
2346 - "version": "7.16.0",
2347 - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
2348 - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
2320 + "version": "7.18.0",
2321 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
2322 + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
2323 "dev": true,
2324 "dependencies": {
2325 "@eslint-community/eslint-utils": "^4.4.0",
2352 - "@typescript-eslint/scope-manager": "7.16.0",
2353 - "@typescript-eslint/types": "7.16.0",
2354 - "@typescript-eslint/typescript-estree": "7.16.0"
2326 + "@typescript-eslint/scope-manager": "7.18.0",
2327 + "@typescript-eslint/types": "7.18.0",
2328 + "@typescript-eslint/typescript-estree": "7.18.0"
2329 },
2330 "engines": {
2331 "node": "^18.18.0 || >=20.0.0"
@@ -2365,12 +2339,12 @@
2339 }
2340 },
2341 "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
2368 - "version": "7.16.0",
2369 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
2370 - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
2342 + "version": "7.18.0",
2343 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
2344 + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
2345 "dev": true,
2346 "dependencies": {
2373 - "@typescript-eslint/types": "7.16.0",
2347 + "@typescript-eslint/types": "7.18.0",
2348 "eslint-visitor-keys": "^3.4.3"
2349 },
2350 "engines": {
@@ -2397,15 +2371,15 @@
2371 }
2372 },
2373 "node_modules/@typescript-eslint/parser": {
2400 - "version": "7.16.0",
2401 - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
2402 - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
2374 + "version": "7.18.0",
2375 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.18.0.tgz",
2376 + "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
2377 "dev": true,
2378 "dependencies": {
2405 - "@typescript-eslint/scope-manager": "7.16.0",
2406 - "@typescript-eslint/types": "7.16.0",
2407 - "@typescript-eslint/typescript-estree": "7.16.0",
2408 - "@typescript-eslint/visitor-keys": "7.16.0",
2379 + "@typescript-eslint/scope-manager": "7.18.0",
2380 + "@typescript-eslint/types": "7.18.0",
2381 + "@typescript-eslint/typescript-estree": "7.18.0",
2382 + "@typescript-eslint/visitor-keys": "7.18.0",
2383 "debug": "^4.3.4"
2384 },
2385 "engines": {
@@ -2425,13 +2399,13 @@
2399 }
2400 },
2401 "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": {
2428 - "version": "7.16.0",
2429 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
2430 - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
2402 + "version": "7.18.0",
2403 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
2404 + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
2405 "dev": true,
2406 "dependencies": {
2433 - "@typescript-eslint/types": "7.16.0",
2434 - "@typescript-eslint/visitor-keys": "7.16.0"
2407 + "@typescript-eslint/types": "7.18.0",
2408 + "@typescript-eslint/visitor-keys": "7.18.0"
2409 },
2410 "engines": {
2411 "node": "^18.18.0 || >=20.0.0"
@@ -2442,9 +2416,9 @@
2416 }
2417 },
2418 "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": {
2445 - "version": "7.16.0",
2446 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
2447 - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
2419 + "version": "7.18.0",
2420 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
2421 + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
2422 "dev": true,
2423 "engines": {
2424 "node": "^18.18.0 || >=20.0.0"
@@ -2455,13 +2429,13 @@
2429 }
2430 },
2431 "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": {
2458 - "version": "7.16.0",
2459 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
2460 - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
2432 + "version": "7.18.0",
2433 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
2434 + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
2435 "dev": true,
2436 "dependencies": {
2463 - "@typescript-eslint/types": "7.16.0",
2464 - "@typescript-eslint/visitor-keys": "7.16.0",
2437 + "@typescript-eslint/types": "7.18.0",
2438 + "@typescript-eslint/visitor-keys": "7.18.0",
2439 "debug": "^4.3.4",
2440 "globby": "^11.1.0",
2441 "is-glob": "^4.0.3",
@@ -2483,12 +2457,12 @@
2457 }
2458 },
2459 "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": {
2486 - "version": "7.16.0",
2487 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
2488 - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
2460 + "version": "7.18.0",
2461 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
2462 + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
2463 "dev": true,
2464 "dependencies": {
2491 - "@typescript-eslint/types": "7.16.0",
2465 + "@typescript-eslint/types": "7.18.0",
2466 "eslint-visitor-keys": "^3.4.3"
2467 },
2468 "engines": {
@@ -2531,13 +2505,13 @@
2505 }
2506 },
2507 "node_modules/@typescript-eslint/type-utils": {
2534 - "version": "7.16.0",
2535 - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
2536 - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
2508 + "version": "7.18.0",
2509 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz",
2510 + "integrity": "sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==",
2511 "dev": true,
2512 "dependencies": {
2539 - "@typescript-eslint/typescript-estree": "7.16.0",
2540 - "@typescript-eslint/utils": "7.16.0",
2513 + "@typescript-eslint/typescript-estree": "7.18.0",
2514 + "@typescript-eslint/utils": "7.18.0",
2515 "debug": "^4.3.4",
2516 "ts-api-utils": "^1.3.0"
2517 },
@@ -2558,13 +2532,13 @@
2532 }
2533 },
2534 "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": {
2561 - "version": "7.16.0",
2562 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
2563 - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
2535 + "version": "7.18.0",
2536 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz",
2537 + "integrity": "sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==",
2538 "dev": true,
2539 "dependencies": {
2566 - "@typescript-eslint/types": "7.16.0",
2567 - "@typescript-eslint/visitor-keys": "7.16.0"
2540 + "@typescript-eslint/types": "7.18.0",
2541 + "@typescript-eslint/visitor-keys": "7.18.0"
2542 },
2543 "engines": {
2544 "node": "^18.18.0 || >=20.0.0"
@@ -2575,9 +2549,9 @@
2549 }
2550 },
2551 "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": {
2578 - "version": "7.16.0",
2579 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
2580 - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
2552 + "version": "7.18.0",
2553 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.18.0.tgz",
2554 + "integrity": "sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==",
2555 "dev": true,
2556 "engines": {
2557 "node": "^18.18.0 || >=20.0.0"
@@ -2588,13 +2562,13 @@
2562 }
2563 },
2564 "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": {
2591 - "version": "7.16.0",
2592 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
2593 - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
2565 + "version": "7.18.0",
2566 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz",
2567 + "integrity": "sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==",
2568 "dev": true,
2569 "dependencies": {
2596 - "@typescript-eslint/types": "7.16.0",
2597 - "@typescript-eslint/visitor-keys": "7.16.0",
2570 + "@typescript-eslint/types": "7.18.0",
2571 + "@typescript-eslint/visitor-keys": "7.18.0",
2572 "debug": "^4.3.4",
2573 "globby": "^11.1.0",
2574 "is-glob": "^4.0.3",
@@ -2616,15 +2590,15 @@
2590 }
2591 },
2592 "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": {
2619 - "version": "7.16.0",
2620 - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
2621 - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
2593 + "version": "7.18.0",
2594 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.18.0.tgz",
2595 + "integrity": "sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==",
2596 "dev": true,
2597 "dependencies": {
2598 "@eslint-community/eslint-utils": "^4.4.0",
2625 - "@typescript-eslint/scope-manager": "7.16.0",
2626 - "@typescript-eslint/types": "7.16.0",
2627 - "@typescript-eslint/typescript-estree": "7.16.0"
2599 + "@typescript-eslint/scope-manager": "7.18.0",
2600 + "@typescript-eslint/types": "7.18.0",
2601 + "@typescript-eslint/typescript-estree": "7.18.0"
2602 },
2603 "engines": {
2604 "node": "^18.18.0 || >=20.0.0"
@@ -2638,12 +2612,12 @@
2612 }
2613 },
2614 "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": {
2641 - "version": "7.16.0",
2642 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
2643 - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
2615 + "version": "7.18.0",
2616 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz",
2617 + "integrity": "sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==",
2618 "dev": true,
2619 "dependencies": {
2646 - "@typescript-eslint/types": "7.16.0",
2620 + "@typescript-eslint/types": "7.18.0",
2621 "eslint-visitor-keys": "^3.4.3"
2622 },
2623 "engines": {
@@ -2804,9 +2778,9 @@
2778 }
2779 },
2780 "node_modules/@vitejs/plugin-vue": {
2807 - "version": "5.0.5",
2808 - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.0.5.tgz",
2809 - "integrity": "sha512-LOjm7XeIimLBZyzinBQ6OSm3UBCNVCpLkxGC0oWmm2YPzVZoxMsdvNVimLTBzpAnR9hl/yn1SHGuRfe6/Td9rQ==",
2781 + "version": "5.1.2",
2782 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.2.tgz",
2783 + "integrity": "sha512-nY9IwH12qeiJqumTCLJLE7IiNx7HZ39cbHaysEUd+Myvbz9KAqd2yq+U01Kab1R/H1BmiyM2ShTYlNH32Fzo3A==",
2784 "dev": true,
2785 "engines": {
2786 "node": "^18.0.0 || >=20.0.0"
@@ -2817,13 +2791,13 @@
2791 }
2792 },
2793 "node_modules/@vitejs/plugin-vue-jsx": {
2820 - "version": "4.0.0",
2821 - "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.0.0.tgz",
2822 - "integrity": "sha512-A+6wL2AdQhDsLsDnY+2v4rRDI1HLJGIMc97a8FURO9tqKsH5QvjWrzsa5DH3NlZsM742W2wODl2fF+bfcTWtXw==",
2794 + "version": "4.0.1",
2795 + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.0.1.tgz",
2796 + "integrity": "sha512-7mg9HFGnFHMEwCdB6AY83cVK4A6sCqnrjFYF4WIlebYAQVVJ/sC/CiTruVdrRlhrFoeZ8rlMxY9wYpPTIRhhAg==",
2797 "dev": true,
2798 "dependencies": {
2825 - "@babel/core": "^7.24.6",
2826 - "@babel/plugin-transform-typescript": "^7.24.6",
2799 + "@babel/core": "^7.24.7",
2800 + "@babel/plugin-transform-typescript": "^7.24.7",
2801 "@vue/babel-plugin-jsx": "^1.2.2"
2802 },
2803 "engines": {
@@ -2835,13 +2809,13 @@
2809 }
2810 },
2811 "node_modules/@vitest/expect": {
2838 - "version": "2.0.2",
2839 - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.2.tgz",
2840 - "integrity": "sha512-nKAvxBYqcDugYZ4nJvnm5OR8eDJdgWjk4XM9owQKUjzW70q0icGV2HVnQOyYsp906xJaBDUXw0+9EHw2T8e0mQ==",
2812 + "version": "2.0.5",
2813 + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
2814 + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
2815 "dev": true,
2816 "dependencies": {
2843 - "@vitest/spy": "2.0.2",
2844 - "@vitest/utils": "2.0.2",
2817 + "@vitest/spy": "2.0.5",
2818 + "@vitest/utils": "2.0.5",
2819 "chai": "^5.1.1",
2820 "tinyrainbow": "^1.2.0"
2821 },
@@ -2850,9 +2824,9 @@
2824 }
2825 },
2826 "node_modules/@vitest/pretty-format": {
2853 - "version": "2.0.2",
2854 - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.2.tgz",
2855 - "integrity": "sha512-SBCyOXfGVvddRd9r2PwoVR0fonQjh9BMIcBMlSzbcNwFfGr6ZhOhvBzurjvi2F4ryut2HcqiFhNeDVGwru8tLg==",
2827 + "version": "2.0.5",
2828 + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
2829 + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
2830 "dev": true,
2831 "dependencies": {
2832 "tinyrainbow": "^1.2.0"
@@ -2862,12 +2836,12 @@
2836 }
2837 },
2838 "node_modules/@vitest/runner": {
2865 - "version": "2.0.2",
2866 - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.2.tgz",
2867 - "integrity": "sha512-OCh437Vi8Wdbif1e0OvQcbfM3sW4s2lpmOjAE7qfLrpzJX2M7J1IQlNvEcb/fu6kaIB9n9n35wS0G2Q3en5kHg==",
2839 + "version": "2.0.5",
2840 + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz",
2841 + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==",
2842 "dev": true,
2843 "dependencies": {
2870 - "@vitest/utils": "2.0.2",
2844 + "@vitest/utils": "2.0.5",
2845 "pathe": "^1.1.2"
2846 },
2847 "funding": {
@@ -2875,12 +2849,12 @@
2849 }
2850 },
2851 "node_modules/@vitest/snapshot": {
2878 - "version": "2.0.2",
2879 - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.2.tgz",
2880 - "integrity": "sha512-Yc2ewhhZhx+0f9cSUdfzPRcsM6PhIb+S43wxE7OG0kTxqgqzo8tHkXFuFlndXeDMp09G3sY/X5OAo/RfYydf1g==",
2852 + "version": "2.0.5",
2853 + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz",
2854 + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==",
2855 "dev": true,
2856 "dependencies": {
2883 - "@vitest/pretty-format": "2.0.2",
2857 + "@vitest/pretty-format": "2.0.5",
2858 "magic-string": "^0.30.10",
2859 "pathe": "^1.1.2"
2860 },
@@ -2889,9 +2863,9 @@
2863 }
2864 },
2865 "node_modules/@vitest/spy": {
2892 - "version": "2.0.2",
2893 - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.2.tgz",
2894 - "integrity": "sha512-MgwJ4AZtCgqyp2d7WcQVE8aNG5vQ9zu9qMPYQHjsld/QVsrvg78beNrXdO4HYkP0lDahCO3P4F27aagIag+SGQ==",
2866 + "version": "2.0.5",
2867 + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
2868 + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
2869 "dev": true,
2870 "dependencies": {
2871 "tinyspy": "^3.0.0"
@@ -2901,12 +2875,12 @@
2875 }
2876 },
2877 "node_modules/@vitest/utils": {
2904 - "version": "2.0.2",
2905 - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.2.tgz",
2906 - "integrity": "sha512-pxCY1v7kmOCWYWjzc0zfjGTA3Wmn8PKnlPvSrsA643P1NHl1fOyXj2Q9SaNlrlFE+ivCsxM80Ov3AR82RmHCWQ==",
2878 + "version": "2.0.5",
2879 + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
2880 + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
2881 "dev": true,
2882 "dependencies": {
2909 - "@vitest/pretty-format": "2.0.2",
2883 + "@vitest/pretty-format": "2.0.5",
2884 "estree-walker": "^3.0.3",
2885 "loupe": "^3.1.1",
2886 "tinyrainbow": "^1.2.0"
@@ -2925,27 +2899,27 @@
2899 }
2900 },
2901 "node_modules/@volar/language-core": {
2928 - "version": "2.4.0-alpha.15",
2929 - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.0-alpha.15.tgz",
2930 - "integrity": "sha512-mt8z4Fm2WxfQYoQHPcKVjLQV6PgPqyKLbkCVY2cr5RSaamqCHjhKEpsFX66aL4D/7oYguuaUw9Bx03Vt0TpIIA==",
2902 + "version": "2.4.0-alpha.18",
2903 + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.0-alpha.18.tgz",
2904 + "integrity": "sha512-JAYeJvYQQROmVRtSBIczaPjP3DX4QW1fOqW1Ebs0d3Y3EwSNRglz03dSv0Dm61dzd0Yx3WgTW3hndDnTQqgmyg==",
2905 "dev": true,
2906 "dependencies": {
2933 - "@volar/source-map": "2.4.0-alpha.15"
2907 + "@volar/source-map": "2.4.0-alpha.18"
2908 }
2909 },
2910 "node_modules/@volar/source-map": {
2937 - "version": "2.4.0-alpha.15",
2938 - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.0-alpha.15.tgz",
2939 - "integrity": "sha512-8Htngw5TmBY4L3ClDqBGyfLhsB8EmoEXUH1xydyEtEoK0O6NX5ur4Jw8jgvscTlwzizyl/wsN1vn0cQXVbbXYg==",
2911 + "version": "2.4.0-alpha.18",
2912 + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.0-alpha.18.tgz",
2913 + "integrity": "sha512-MTeCV9MUwwsH0sNFiZwKtFrrVZUK6p8ioZs3xFzHc2cvDXHWlYN3bChdQtwKX+FY2HG6H3CfAu1pKijolzIQ8g==",
2914 "dev": true
2915 },
2916 "node_modules/@volar/typescript": {
2943 - "version": "2.4.0-alpha.15",
2944 - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.0-alpha.15.tgz",
2945 - "integrity": "sha512-U3StRBbDuxV6Woa4hvGS4kz3XcOzrWUKgFdEFN+ba1x3eaYg7+ytau8ul05xgA+UNGLXXsKur7fTUhDFyISk0w==",
2917 + "version": "2.4.0-alpha.18",
2918 + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.0-alpha.18.tgz",
2919 + "integrity": "sha512-sXh5Y8sqGUkgxpMWUGvRXggxYHAVxg0Pa1C42lQZuPDrW6vHJPR0VCK8Sr7WJsAW530HuNQT/ZIskmXtxjybMQ==",
2920 "dev": true,
2921 "dependencies": {
2948 - "@volar/language-core": "2.4.0-alpha.15",
2922 + "@volar/language-core": "2.4.0-alpha.18",
2923 "path-browserify": "^1.0.1",
2924 "vscode-uri": "^3.0.8"
2925 }
@@ -3024,49 +2998,59 @@
2998 }
2999 },
3000 "node_modules/@vue/compiler-core": {
3027 - "version": "3.4.31",
3028 - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.31.tgz",
3029 - "integrity": "sha512-skOiodXWTV3DxfDhB4rOf3OGalpITLlgCeOwb+Y9GJpfQ8ErigdBUHomBzvG78JoVE8MJoQsb+qhZiHfKeNeEg==",
3001 + "version": "3.4.38",
3002 + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.38.tgz",
3003 + "integrity": "sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==",
3004 "dependencies": {
3005 "@babel/parser": "^7.24.7",
3032 - "@vue/shared": "3.4.31",
3006 + "@vue/shared": "3.4.38",
3007 "entities": "^4.5.0",
3008 "estree-walker": "^2.0.2",
3009 "source-map-js": "^1.2.0"
3010 }
3011 },
3012 "node_modules/@vue/compiler-dom": {
3039 - "version": "3.4.31",
3040 - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.31.tgz",
3041 - "integrity": "sha512-wK424WMXsG1IGMyDGyLqB+TbmEBFM78hIsOJ9QwUVLGrcSk0ak6zYty7Pj8ftm7nEtdU/DGQxAXp0/lM/2cEpQ==",
3013 + "version": "3.4.38",
3014 + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.38.tgz",
3015 + "integrity": "sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==",
3016 "dependencies": {
3043 - "@vue/compiler-core": "3.4.31",
3044 - "@vue/shared": "3.4.31"
3017 + "@vue/compiler-core": "3.4.38",
3018 + "@vue/shared": "3.4.38"
3019 }
3020 },
3021 "node_modules/@vue/compiler-sfc": {
3048 - "version": "3.4.31",
3049 - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.31.tgz",
3050 - "integrity": "sha512-einJxqEw8IIJxzmnxmJBuK2usI+lJonl53foq+9etB2HAzlPjAS/wa7r0uUpXw5ByX3/0uswVSrjNb17vJm1kQ==",
3022 + "version": "3.4.38",
3023 + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.38.tgz",
3024 + "integrity": "sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==",
3025 "dependencies": {
3026 "@babel/parser": "^7.24.7",
3053 - "@vue/compiler-core": "3.4.31",
3054 - "@vue/compiler-dom": "3.4.31",
3055 - "@vue/compiler-ssr": "3.4.31",
3056 - "@vue/shared": "3.4.31",
3027 + "@vue/compiler-core": "3.4.38",
3028 + "@vue/compiler-dom": "3.4.38",
3029 + "@vue/compiler-ssr": "3.4.38",
3030 + "@vue/shared": "3.4.38",
3031 "estree-walker": "^2.0.2",
3032 "magic-string": "^0.30.10",
3059 - "postcss": "^8.4.38",
3033 + "postcss": "^8.4.40",
3034 "source-map-js": "^1.2.0"
3035 }
3036 },
3037 "node_modules/@vue/compiler-ssr": {
3064 - "version": "3.4.31",
3065 - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.31.tgz",
3066 - "integrity": "sha512-RtefmITAje3fJ8FSg1gwgDhdKhZVntIVbwupdyZDSifZTRMiWxWehAOTCc8/KZDnBOcYQ4/9VWxsTbd3wT0hAA==",
3038 + "version": "3.4.38",
3039 + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.38.tgz",
3040 + "integrity": "sha512-YXznKFQ8dxYpAz9zLuVvfcXhc31FSPFDcqr0kyujbOwNhlmaNvL2QfIy+RZeJgSn5Fk54CWoEUeW+NVBAogGaw==",
3041 + "dependencies": {
3042 + "@vue/compiler-dom": "3.4.38",
3043 + "@vue/shared": "3.4.38"
3044 + }
3045 + },
3046 + "node_modules/@vue/compiler-vue2": {
3047 + "version": "2.7.16",
3048 + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
3049 + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
3050 + "dev": true,
3051 "dependencies": {
3068 - "@vue/compiler-dom": "3.4.31",
3069 - "@vue/shared": "3.4.31"
3052 + "de-indent": "^1.0.2",
3053 + "he": "^1.2.0"
3054 }
3055 },
3056 "node_modules/@vue/devtools-api": {
@@ -3075,13 +3059,13 @@
3059 "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw=="
3060 },
3061 "node_modules/@vue/devtools-core": {
3078 - "version": "7.3.5",
3079 - "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.3.5.tgz",
3080 - "integrity": "sha512-uSC3IkIp6MtyJYSh5xzY99sgqlAXLq+peE2KKXTi6JeRHOtMngFWFWENXi70IJ1EVGYztiFQoHhI9WZcgKBz8g==",
3062 + "version": "7.3.9",
3063 + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.3.9.tgz",
3064 + "integrity": "sha512-B5zAl9ulNjI6nknSnGNRzmP/ldR9ADUwwT8HkI8Hejo1W00uK9ABUahbfrXzME296rBfmwhQuCFwJ6t9KFdbXQ==",
3065 "dev": true,
3066 "dependencies": {
3083 - "@vue/devtools-kit": "^7.3.5",
3084 - "@vue/devtools-shared": "^7.3.5",
3067 + "@vue/devtools-kit": "^7.3.9",
3068 + "@vue/devtools-shared": "^7.3.9",
3069 "mitt": "^3.0.1",
3070 "nanoid": "^3.3.4",
3071 "pathe": "^1.1.2",
@@ -3092,12 +3076,12 @@
3076 }
3077 },
3078 "node_modules/@vue/devtools-kit": {
3095 - "version": "7.3.5",
3096 - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.3.5.tgz",
3097 - "integrity": "sha512-wwfi10gJ1HMtjzcd8aIOnzBHlIRqsYDgcDyrKvkeyc0Gbcoe7UrkXRVHZUOtcxxoplHA0PwpT6wFg0uUCmi8Ww==",
3079 + "version": "7.3.9",
3080 + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.3.9.tgz",
3081 + "integrity": "sha512-Gr17nA+DaQzqyhNx1DUJr1CJRzTRfbIuuC80ZgU8MD/qNO302tv9la+ROi+Uaw+ULVwU9T71GnwLy4n8m9Lspg==",
3082 "dev": true,
3083 "dependencies": {
3100 - "@vue/devtools-shared": "^7.3.5",
3084 + "@vue/devtools-shared": "^7.3.9",
3085 "birpc": "^0.2.17",
3086 "hookable": "^5.5.3",
3087 "mitt": "^3.0.1",
@@ -3107,9 +3091,9 @@
3091 }
3092 },
3093 "node_modules/@vue/devtools-shared": {
3110 - "version": "7.3.5",
3111 - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.3.5.tgz",
3112 - "integrity": "sha512-Rqii3VazmWTi67a86rYopi61n5Ved05EybJCwyrfoO9Ok3MaS/4yRFl706ouoISMlyrASJFEzM0/AiDA6w4f9A==",
3094 + "version": "7.3.9",
3095 + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.3.9.tgz",
3096 + "integrity": "sha512-CdfMRZKXyI8vw+hqOcQIiLihB6Hbbi7WNZGp7LsuH1Qe4aYAFmTaKjSciRZ301oTnwmU/knC/s5OGuV6UNiNoA==",
3097 "dev": true,
3098 "dependencies": {
3099 "rfdc": "^1.4.1"
@@ -3154,19 +3138,19 @@
3138 }
3139 },
3140 "node_modules/@vue/language-core": {
3157 - "version": "2.0.26",
3158 - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.26.tgz",
3159 - "integrity": "sha512-/lt6SfQ3O1yDAhPsnLv9iSUgXd1dMHqUm/t3RctfqjuwQf1LnftZ414X3UBn6aXT4MiwXWtbNJ4Z0NZWwDWgJQ==",
3141 + "version": "2.0.29",
3142 + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.29.tgz",
3143 + "integrity": "sha512-o2qz9JPjhdoVj8D2+9bDXbaI4q2uZTHQA/dbyZT4Bj1FR9viZxDJnLcKVHfxdn6wsOzRgpqIzJEEmSSvgMvDTQ==",
3144 "dev": true,
3145 "dependencies": {
3162 - "@volar/language-core": "~2.4.0-alpha.15",
3146 + "@volar/language-core": "~2.4.0-alpha.18",
3147 "@vue/compiler-dom": "^3.4.0",
3148 + "@vue/compiler-vue2": "^2.7.16",
3149 "@vue/shared": "^3.4.0",
3150 "computeds": "^0.0.1",
3151 "minimatch": "^9.0.3",
3152 "muggle-string": "^0.4.1",
3168 - "path-browserify": "^1.0.1",
3169 - "vue-template-compiler": "^2.7.14"
3153 + "path-browserify": "^1.0.1"
3154 },
3155 "peerDependencies": {
3156 "typescript": "*"
@@ -3178,49 +3162,49 @@
3162 }
3163 },
3164 "node_modules/@vue/reactivity": {
3181 - "version": "3.4.31",
3182 - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.31.tgz",
3183 - "integrity": "sha512-VGkTani8SOoVkZNds1PfJ/T1SlAIOf8E58PGAhIOUDYPC4GAmFA2u/E14TDAFcf3vVDKunc4QqCe/SHr8xC65Q==",
3165 + "version": "3.4.38",
3166 + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.38.tgz",
3167 + "integrity": "sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==",
3168 "dependencies": {
3185 - "@vue/shared": "3.4.31"
3169 + "@vue/shared": "3.4.38"
3170 }
3171 },
3172 "node_modules/@vue/runtime-core": {
3189 - "version": "3.4.31",
3190 - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.31.tgz",
3191 - "integrity": "sha512-LDkztxeUPazxG/p8c5JDDKPfkCDBkkiNLVNf7XZIUnJ+66GVGkP+TIh34+8LtPisZ+HMWl2zqhIw0xN5MwU1cw==",
3173 + "version": "3.4.38",
3174 + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.38.tgz",
3175 + "integrity": "sha512-21z3wA99EABtuf+O3IhdxP0iHgkBs1vuoCAsCKLVJPEjpVqvblwBnTj42vzHRlWDCyxu9ptDm7sI2ZMcWrQqlA==",
3176 "dependencies": {
3193 - "@vue/reactivity": "3.4.31",
3194 - "@vue/shared": "3.4.31"
3177 + "@vue/reactivity": "3.4.38",
3178 + "@vue/shared": "3.4.38"
3179 }
3180 },
3181 "node_modules/@vue/runtime-dom": {
3198 - "version": "3.4.31",
3199 - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.31.tgz",
3200 - "integrity": "sha512-2Auws3mB7+lHhTFCg8E9ZWopA6Q6L455EcU7bzcQ4x6Dn4cCPuqj6S2oBZgN2a8vJRS/LSYYxwFFq2Hlx3Fsaw==",
3182 + "version": "3.4.38",
3183 + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.38.tgz",
3184 + "integrity": "sha512-afZzmUreU7vKwKsV17H1NDThEEmdYI+GCAK/KY1U957Ig2NATPVjCROv61R19fjZNzMmiU03n79OMnXyJVN0UA==",
3185 "dependencies": {
3202 - "@vue/reactivity": "3.4.31",
3203 - "@vue/runtime-core": "3.4.31",
3204 - "@vue/shared": "3.4.31",
3186 + "@vue/reactivity": "3.4.38",
3187 + "@vue/runtime-core": "3.4.38",
3188 + "@vue/shared": "3.4.38",
3189 "csstype": "^3.1.3"
3190 }
3191 },
3192 "node_modules/@vue/server-renderer": {
3209 - "version": "3.4.31",
3210 - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.31.tgz",
3211 - "integrity": "sha512-D5BLbdvrlR9PE3by9GaUp1gQXlCNadIZytMIb8H2h3FMWJd4oUfkUTEH2wAr3qxoRz25uxbTcbqd3WKlm9EHQA==",
3193 + "version": "3.4.38",
3194 + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.38.tgz",
3195 + "integrity": "sha512-NggOTr82FbPEkkUvBm4fTGcwUY8UuTsnWC/L2YZBmvaQ4C4Jl/Ao4HHTB+l7WnFCt5M/dN3l0XLuyjzswGYVCA==",
3196 "dependencies": {
3213 - "@vue/compiler-ssr": "3.4.31",
3214 - "@vue/shared": "3.4.31"
3197 + "@vue/compiler-ssr": "3.4.38",
3198 + "@vue/shared": "3.4.38"
3199 },
3200 "peerDependencies": {
3217 - "vue": "3.4.31"
3201 + "vue": "3.4.38"
3202 }
3203 },
3204 "node_modules/@vue/shared": {
3221 - "version": "3.4.31",
3222 - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.31.tgz",
3223 - "integrity": "sha512-Yp3wtJk//8cO4NItOPpi3QkLExAr/aLBGZMmTtW9WpdwBCJpRM6zj9WgWktXAl8IDIozwNMByT45JP3tO3ACWA=="
3205 + "version": "3.4.38",
3206 + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.38.tgz",
3207 + "integrity": "sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw=="
3208 },
3209 "node_modules/@vue/test-utils": {
3210 "version": "2.4.6",
@@ -3239,19 +3223,19 @@
3223 "dev": true
3224 },
3225 "node_modules/@vueuse/components": {
3242 - "version": "10.11.0",
3243 - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.11.0.tgz",
3244 - "integrity": "sha512-ZvLZI23d5ZAtva5fGyYh/jQtZO8l+zJ5tAXyYNqHJZkq1o5yWyqZhENvSv5mfDmN5IuAOp4tq02mRmX/ipFGcg==",
3226 + "version": "11.0.3",
3227 + "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-11.0.3.tgz",
3228 + "integrity": "sha512-/mbv4R1ody3rYz42U14SQ6GkAKHCAHhFIBdR6nKJbEJeFMoiB93dzX0sT2S05CpYCJJgv3JLAwEHeehYqqmbyQ==",
3229 "dependencies": {
3246 - "@vueuse/core": "10.11.0",
3247 - "@vueuse/shared": "10.11.0",
3248 - "vue-demi": ">=0.14.8"
3230 + "@vueuse/core": "11.0.3",
3231 + "@vueuse/shared": "11.0.3",
3232 + "vue-demi": ">=0.14.10"
3233 }
3234 },
3235 "node_modules/@vueuse/components/node_modules/vue-demi": {
3252 - "version": "0.14.8",
3253 - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz",
3254 - "integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==",
3236 + "version": "0.14.10",
3237 + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
3238 + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
3239 "hasInstallScript": true,
3240 "bin": {
3241 "vue-demi-fix": "bin/vue-demi-fix.js",
@@ -3274,23 +3258,23 @@
3258 }
3259 },
3260 "node_modules/@vueuse/core": {
3277 - "version": "10.11.0",
3278 - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.0.tgz",
3279 - "integrity": "sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==",
3261 + "version": "11.0.3",
3262 + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-11.0.3.tgz",
3263 + "integrity": "sha512-RENlh64+SYA9XMExmmH1a3TPqeIuJBNNB/63GT35MZI+zpru3oMRUA6cEFr9HmGqEgUisurwGwnIieF6qu3aXw==",
3264 "dependencies": {
3265 "@types/web-bluetooth": "^0.0.20",
3282 - "@vueuse/metadata": "10.11.0",
3283 - "@vueuse/shared": "10.11.0",
3284 - "vue-demi": ">=0.14.8"
3266 + "@vueuse/metadata": "11.0.3",
3267 + "@vueuse/shared": "11.0.3",
3268 + "vue-demi": ">=0.14.10"
3269 },
3270 "funding": {
3271 "url": "https://github.com/sponsors/antfu"
3272 }
3273 },
3274 "node_modules/@vueuse/core/node_modules/vue-demi": {
3291 - "version": "0.14.8",
3292 - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz",
3293 - "integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==",
3275 + "version": "0.14.10",
3276 + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
3277 + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
3278 "hasInstallScript": true,
3279 "bin": {
3280 "vue-demi-fix": "bin/vue-demi-fix.js",
@@ -3313,28 +3297,28 @@
3297 }
3298 },
3299 "node_modules/@vueuse/metadata": {
3316 - "version": "10.11.0",
3317 - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.0.tgz",
3318 - "integrity": "sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ==",
3300 + "version": "11.0.3",
3301 + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-11.0.3.tgz",
3302 + "integrity": "sha512-+FtbO4SD5WpsOcQTcC0hAhNlOid6QNLzqedtquTtQ+CRNBoAt9GuV07c6KNHK1wCmlq8DFPwgiLF2rXwgSHX5Q==",
3303 "funding": {
3304 "url": "https://github.com/sponsors/antfu"
3305 }
3306 },
3307 "node_modules/@vueuse/shared": {
3324 - "version": "10.11.0",
3325 - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.0.tgz",
3326 - "integrity": "sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A==",
3308 + "version": "11.0.3",
3309 + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-11.0.3.tgz",
3310 + "integrity": "sha512-0rY2m6HS5t27n/Vp5cTDsKTlNnimCqsbh/fmT2LgE+aaU42EMfXo8+bNX91W9I7DDmxfuACXMmrd7d79JxkqWA==",
3311 "dependencies": {
3328 - "vue-demi": ">=0.14.8"
3312 + "vue-demi": ">=0.14.10"
3313 },
3314 "funding": {
3315 "url": "https://github.com/sponsors/antfu"
3316 }
3317 },
3318 "node_modules/@vueuse/shared/node_modules/vue-demi": {
3335 - "version": "0.14.8",
3336 - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz",
3337 - "integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==",
3319 + "version": "0.14.10",
3320 + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
3321 + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
3322 "hasInstallScript": true,
3323 "bin": {
3324 "vue-demi-fix": "bin/vue-demi-fix.js",
@@ -3518,9 +3502,9 @@
3502 }
3503 },
3504 "node_modules/apexcharts": {
3521 - "version": "3.50.0",
3522 - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.50.0.tgz",
3523 - "integrity": "sha512-LJT1PNAm+NoIU3aogL2P+ViC0y/Cjik54FdzzGV54UNnGQLBoLe5ok3fxsJDTgyez45BGYT8gqNpYKqhdfy5sg==",
3505 + "version": "3.52.0",
3506 + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.52.0.tgz",
3507 + "integrity": "sha512-7dg0ADKs8AA89iYMZMe2sFDG0XK5PfqllKV9N+i3hKHm3vEtdhwz8AlXGm+/b0nJ6jKiaXsqci5LfVxNhtB+dA==",
3508 "dependencies": {
3509 "@yr/monotone-cubic-spline": "^1.0.3",
3510 "svg.draggable.js": "^2.2.2",
@@ -3620,8 +3604,7 @@
3604 "node_modules/asynckit": {
3605 "version": "0.4.0",
3606 "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
3623 - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
3624 - "dev": true
3607 + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
3608 },
3609 "node_modules/at-least-node": {
3610 "version": "1.0.0",
@@ -3633,9 +3616,9 @@
3616 }
3617 },
3618 "node_modules/autoprefixer": {
3636 - "version": "10.4.19",
3637 - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
3638 - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
3619 + "version": "10.4.20",
3620 + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
3621 + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
3622 "dev": true,
3623 "funding": [
3624 {
@@ -3652,11 +3635,11 @@
3635 }
3636 ],
3637 "dependencies": {
3655 - "browserslist": "^4.23.0",
3656 - "caniuse-lite": "^1.0.30001599",
3638 + "browserslist": "^4.23.3",
3639 + "caniuse-lite": "^1.0.30001646",
3640 "fraction.js": "^4.3.7",
3641 "normalize-range": "^0.1.2",
3659 - "picocolors": "^1.0.0",
3642 + "picocolors": "^1.0.1",
3643 "postcss-value-parser": "^4.2.0"
3644 },
3645 "bin": {
@@ -3685,10 +3668,9 @@
3668 "dev": true
3669 },
3670 "node_modules/axios": {
3688 - "version": "1.7.2",
3689 - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
3690 - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
3691 - "dev": true,
3671 + "version": "1.7.5",
3672 + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz",
3673 + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==",
3674 "dependencies": {
3675 "follow-redirects": "^1.15.6",
3676 "form-data": "^4.0.0",
@@ -3699,7 +3681,6 @@
3681 "version": "4.0.0",
3682 "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
3683 "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
3702 - "dev": true,
3684 "dependencies": {
3685 "asynckit": "^0.4.0",
3686 "combined-stream": "^1.0.8",
@@ -3712,8 +3693,7 @@
3693 "node_modules/axios/node_modules/proxy-from-env": {
3694 "version": "1.1.0",
3695 "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
3715 - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
3716 - "dev": true
3696 + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
3697 },
3698 "node_modules/balanced-match": {
3699 "version": "1.0.2",
@@ -3807,9 +3787,9 @@
3787 }
3788 },
3789 "node_modules/browserslist": {
3810 - "version": "4.23.2",
3811 - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz",
3812 - "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==",
3790 + "version": "4.23.3",
3791 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
3792 + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
3793 "dev": true,
3794 "funding": [
3795 {
@@ -3826,9 +3806,9 @@
3806 }
3807 ],
3808 "dependencies": {
3829 - "caniuse-lite": "^1.0.30001640",
3830 - "electron-to-chromium": "^1.4.820",
3831 - "node-releases": "^2.0.14",
3809 + "caniuse-lite": "^1.0.30001646",
3810 + "electron-to-chromium": "^1.5.4",
3811 + "node-releases": "^2.0.18",
3812 "update-browserslist-db": "^1.1.0"
3813 },
3814 "bin": {
@@ -3918,6 +3898,35 @@
3898 "node": ">=8"
3899 }
3900 },
3901 + "node_modules/cacache": {
3902 + "version": "18.0.4",
3903 + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz",
3904 + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==",
3905 + "dev": true,
3906 + "dependencies": {
3907 + "@npmcli/fs": "^3.1.0",
3908 + "fs-minipass": "^3.0.0",
3909 + "glob": "^10.2.2",
3910 + "lru-cache": "^10.0.1",
3911 + "minipass": "^7.0.3",
3912 + "minipass-collect": "^2.0.1",
3913 + "minipass-flush": "^1.0.5",
3914 + "minipass-pipeline": "^1.2.4",
3915 + "p-map": "^4.0.0",
3916 + "ssri": "^10.0.0",
3917 + "tar": "^6.1.11",
3918 + "unique-filename": "^3.0.0"
3919 + },
3920 + "engines": {
3921 + "node": "^16.14.0 || >=18.0.0"
3922 + }
3923 + },
3924 + "node_modules/cacache/node_modules/lru-cache": {
3925 + "version": "10.4.3",
3926 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
3927 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
3928 + "dev": true
3929 + },
3930 "node_modules/cache-content-type": {
3931 "version": "1.0.1",
3932 "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz",
@@ -3959,12 +3968,6 @@
3968 "url": "https://github.com/sponsors/ljharb"
3969 }
3970 },
3962 - "node_modules/call-me-maybe": {
3963 - "version": "1.0.2",
3964 - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
3965 - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
3966 - "dev": true
3967 - },
3971 "node_modules/callsites": {
3972 "version": "3.1.0",
3973 "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -3995,9 +3998,9 @@
3998 }
3999 },
4000 "node_modules/caniuse-lite": {
3998 - "version": "1.0.30001641",
3999 - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001641.tgz",
4000 - "integrity": "sha512-Phv5thgl67bHYo1TtMY/MurjkHhV4EDaCosezRXgZ8jzA/Ub+wjxAvbGvjoFENStinwi5kCyOYV3mi5tOGykwA==",
4001 + "version": "1.0.30001646",
4002 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001646.tgz",
4003 + "integrity": "sha512-dRg00gudiBDDTmUhClSdv3hqRfpbOnU28IpI1T6PBTLWa+kOj0681C8uML3PifYfREuBrVjDGhL3adYpBT6spw==",
4004 "dev": true,
4005 "funding": [
4006 {
@@ -4104,6 +4107,15 @@
4107 "node": ">= 6"
4108 }
4109 },
4110 + "node_modules/chownr": {
4111 + "version": "2.0.0",
4112 + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
4113 + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
4114 + "dev": true,
4115 + "engines": {
4116 + "node": ">=10"
4117 + }
4118 + },
4119 "node_modules/ci-info": {
4120 "version": "3.9.0",
4121 "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
@@ -4280,7 +4292,6 @@
4292 "version": "1.0.8",
4293 "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
4294 "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
4283 - "dev": true,
4295 "dependencies": {
4296 "delayed-stream": "~1.0.0"
4297 },
@@ -4541,13 +4552,13 @@
4552 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4553 },
4554 "node_modules/cypress": {
4544 - "version": "13.13.0",
4545 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.0.tgz",
4546 - "integrity": "sha512-ou/MQUDq4tcDJI2FsPaod2FZpex4kpIK43JJlcBgWrX8WX7R/05ZxGTuxedOuZBfxjZxja+fbijZGyxiLP6CFA==",
4555 + "version": "13.13.3",
4556 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.13.3.tgz",
4557 + "integrity": "sha512-hUxPrdbJXhUOTzuML+y9Av7CKoYznbD83pt8g3klgpioEha0emfx4WNIuVRx0C76r0xV2MIwAW9WYiXfVJYFQw==",
4558 "dev": true,
4559 "hasInstallScript": true,
4560 "dependencies": {
4550 - "@cypress/request": "^3.0.0",
4561 + "@cypress/request": "^3.0.1",
4562 "@cypress/xvfb": "^1.2.4",
4563 "@types/sinonjs__fake-timers": "8.1.1",
4564 "@types/sizzle": "^2.3.2",
@@ -4746,9 +4757,9 @@
4757 }
4758 },
4759 "node_modules/dayjs": {
4749 - "version": "1.11.11",
4750 - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.11.tgz",
4751 - "integrity": "sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg=="
4760 + "version": "1.11.13",
4761 + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
4762 + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
4763 },
4764 "node_modules/de-indent": {
4765 "version": "1.0.2",
@@ -4762,9 +4773,9 @@
4773 "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug=="
4774 },
4775 "node_modules/debug": {
4765 - "version": "4.3.5",
4766 - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
4767 - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
4776 + "version": "4.3.6",
4777 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
4778 + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
4779 "dependencies": {
4780 "ms": "2.1.2"
4781 },
@@ -4866,7 +4877,6 @@
4877 "version": "1.0.0",
4878 "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
4879 "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
4869 - "dev": true,
4880 "engines": {
4881 "node": ">=0.4.0"
4882 }
@@ -5070,9 +5080,9 @@
5080 "dev": true
5081 },
5082 "node_modules/electron-to-chromium": {
5073 - "version": "1.4.823",
5074 - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.823.tgz",
5075 - "integrity": "sha512-4h+oPeAiGQOHFyUJOqpoEcPj/xxlicxBzOErVeYVMMmAiXUXsGpsFd0QXBMaUUbnD8hhSfLf9uw+MlsoIA7j5w==",
5083 + "version": "1.5.4",
5084 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.4.tgz",
5085 + "integrity": "sha512-orzA81VqLyIGUEA77YkVA1D+N+nNfl2isJVjjmOyrlxuooZ19ynb+dOlaDTqd/idKRS9lDCSBmtzM+kyCsMnkA==",
5086 "dev": true
5087 },
5088 "node_modules/emoji-regex": {
@@ -5090,6 +5100,16 @@
5100 "node": ">= 0.8"
5101 }
5102 },
5103 + "node_modules/encoding": {
5104 + "version": "0.1.13",
5105 + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
5106 + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
5107 + "dev": true,
5108 + "optional": true,
5109 + "dependencies": {
5110 + "iconv-lite": "^0.6.2"
5111 + }
5112 + },
5113 "node_modules/end-of-stream": {
5114 "version": "1.4.4",
5115 "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -5123,10 +5143,16 @@
5143 "url": "https://github.com/fb55/entities?sponsor=1"
5144 }
5145 },
5146 + "node_modules/err-code": {
5147 + "version": "2.0.3",
5148 + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
5149 + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
5150 + "dev": true
5151 + },
5152 "node_modules/error-stack-parser-es": {
5127 - "version": "0.1.4",
5128 - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.4.tgz",
5129 - "integrity": "sha512-l0uy0kAoo6toCgVOYaAayqtPa2a1L15efxUMEnQebKwLQX2X0OpS6wMMQdc4juJXmxd9i40DuaUHq+mjIya9TQ==",
5153 + "version": "0.1.5",
5154 + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz",
5155 + "integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==",
5156 "dev": true,
5157 "funding": {
5158 "url": "https://github.com/sponsors/antfu"
@@ -5216,15 +5242,15 @@
5242 }
5243 },
5244 "node_modules/eslint": {
5219 - "version": "9.6.0",
5220 - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.6.0.tgz",
5221 - "integrity": "sha512-ElQkdLMEEqQNM9Njff+2Y4q2afHk7JpkPvrd7Xh7xefwgQynqPxwf55J7di9+MEibWUGdNjFF9ITG9Pck5M84w==",
5245 + "version": "9.9.1",
5246 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
5247 + "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
5248 "dependencies": {
5249 "@eslint-community/eslint-utils": "^4.2.0",
5224 - "@eslint-community/regexpp": "^4.6.1",
5225 - "@eslint/config-array": "^0.17.0",
5250 + "@eslint-community/regexpp": "^4.11.0",
5251 + "@eslint/config-array": "^0.18.0",
5252 "@eslint/eslintrc": "^3.1.0",
5227 - "@eslint/js": "9.6.0",
5253 + "@eslint/js": "9.9.1",
5254 "@humanwhocodes/module-importer": "^1.0.1",
5255 "@humanwhocodes/retry": "^0.3.0",
5256 "@nodelib/fs.walk": "^1.2.8",
@@ -5233,7 +5259,7 @@
5259 "cross-spawn": "^7.0.2",
5260 "debug": "^4.3.2",
5261 "escape-string-regexp": "^4.0.0",
5236 - "eslint-scope": "^8.0.1",
5262 + "eslint-scope": "^8.0.2",
5263 "eslint-visitor-keys": "^4.0.0",
5264 "espree": "^10.1.0",
5265 "esquery": "^1.5.0",
@@ -5263,6 +5289,14 @@
5289 },
5290 "funding": {
5291 "url": "https://eslint.org/donate"
5292 + },
5293 + "peerDependencies": {
5294 + "jiti": "*"
5295 + },
5296 + "peerDependenciesMeta": {
5297 + "jiti": {
5298 + "optional": true
5299 + }
5300 }
5301 },
5302 "node_modules/eslint-config-prettier": {
@@ -5297,9 +5331,9 @@
5331 }
5332 },
5333 "node_modules/eslint-plugin-cypress": {
5300 - "version": "3.3.0",
5301 - "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-3.3.0.tgz",
5302 - "integrity": "sha512-HPHMPzYBIshzJM8wqgKSKHG2p/8R0Gbg4Pb3tcdC9WrmkuqxiKxSKbjunUrajhV5l7gCIFrh1P7C7GuBqH6YuQ==",
5334 + "version": "3.5.0",
5335 + "resolved": "https://registry.npmjs.org/eslint-plugin-cypress/-/eslint-plugin-cypress-3.5.0.tgz",
5336 + "integrity": "sha512-JZQ6XnBTNI8h1B9M7wJSFzc48SYbh7VMMKaNTQOFa3BQlnmXPrVc4PKen8R+fpv6VleiPeej6VxloGb42zdRvw==",
5337 "dev": true,
5338 "dependencies": {
5339 "globals": "^13.20.0"
@@ -5336,12 +5370,12 @@
5370 }
5371 },
5372 "node_modules/eslint-plugin-prettier": {
5339 - "version": "5.1.3",
5340 - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz",
5341 - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==",
5373 + "version": "5.2.1",
5374 + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz",
5375 + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==",
5376 "dependencies": {
5377 "prettier-linter-helpers": "^1.0.0",
5344 - "synckit": "^0.8.6"
5378 + "synckit": "^0.9.1"
5379 },
5380 "engines": {
5381 "node": "^14.18.0 || >=16.0.0"
@@ -5364,6 +5398,26 @@
5398 }
5399 }
5400 },
5401 + "node_modules/eslint-plugin-prettier/node_modules/synckit": {
5402 + "version": "0.9.1",
5403 + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz",
5404 + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==",
5405 + "dependencies": {
5406 + "@pkgr/core": "^0.1.0",
5407 + "tslib": "^2.6.2"
5408 + },
5409 + "engines": {
5410 + "node": "^14.18.0 || >=16.0.0"
5411 + },
5412 + "funding": {
5413 + "url": "https://opencollective.com/unts"
5414 + }
5415 + },
5416 + "node_modules/eslint-plugin-prettier/node_modules/tslib": {
5417 + "version": "2.6.3",
5418 + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
5419 + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
5420 + },
5421 "node_modules/eslint-plugin-vue": {
5422 "version": "9.27.0",
5423 "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz",
@@ -5411,9 +5465,9 @@
5465 }
5466 },
5467 "node_modules/eslint-scope": {
5414 - "version": "8.0.1",
5415 - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz",
5416 - "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==",
5468 + "version": "8.0.2",
5469 + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz",
5470 + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==",
5471 "dependencies": {
5472 "esrecurse": "^4.3.0",
5473 "estraverse": "^5.2.0"
@@ -5437,9 +5491,9 @@
5491 }
5492 },
5493 "node_modules/eslint/node_modules/@eslint/js": {
5440 - "version": "9.6.0",
5441 - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.6.0.tgz",
5442 - "integrity": "sha512-D9B0/3vNg44ZeWbYMpBoXqNP4j6eQD5vNwIlGAuFRRzK/WtT/jvDQW3Bi9kkf3PMDMlM7Yi+73VLUsn5bJcl8A==",
5494 + "version": "9.9.1",
5495 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
5496 + "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
5497 "engines": {
5498 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
5499 }
@@ -5876,7 +5930,6 @@
5930 "version": "1.15.6",
5931 "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
5932 "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
5879 - "dev": true,
5933 "funding": [
5934 {
5935 "type": "individual",
@@ -5985,6 +6038,18 @@
6038 "node": ">=14.14"
6039 }
6040 },
6041 + "node_modules/fs-minipass": {
6042 + "version": "3.0.3",
6043 + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
6044 + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
6045 + "dev": true,
6046 + "dependencies": {
6047 + "minipass": "^7.0.3"
6048 + },
6049 + "engines": {
6050 + "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
6051 + }
6052 + },
6053 "node_modules/fs.realpath": {
6054 "version": "1.0.0",
6055 "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@@ -6076,9 +6141,9 @@
6141 }
6142 },
6143 "node_modules/get-tsconfig": {
6079 - "version": "4.7.5",
6080 - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.5.tgz",
6081 - "integrity": "sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==",
6144 + "version": "4.7.6",
6145 + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.6.tgz",
6146 + "integrity": "sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==",
6147 "dev": true,
6148 "dependencies": {
6149 "resolve-pkg-maps": "^1.0.0"
@@ -6167,9 +6232,9 @@
6232 }
6233 },
6234 "node_modules/globals": {
6170 - "version": "15.8.0",
6171 - "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz",
6172 - "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==",
6235 + "version": "15.9.0",
6236 + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz",
6237 + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==",
6238 "dev": true,
6239 "engines": {
6240 "node": ">=18"
@@ -6320,6 +6385,24 @@
6385 "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
6386 "dev": true
6387 },
6388 + "node_modules/hosted-git-info": {
6389 + "version": "7.0.2",
6390 + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
6391 + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
6392 + "dev": true,
6393 + "dependencies": {
6394 + "lru-cache": "^10.0.1"
6395 + },
6396 + "engines": {
6397 + "node": "^16.14.0 || >=18.0.0"
6398 + }
6399 + },
6400 + "node_modules/hosted-git-info/node_modules/lru-cache": {
6401 + "version": "10.4.3",
6402 + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
6403 + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
6404 + "dev": true
6405 + },
6406 "node_modules/html-encoding-sniffer": {
6407 "version": "4.0.0",
6408 "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
@@ -6406,6 +6489,12 @@
6489 "node": ">= 0.6"
6490 }
6491 },
6492 + "node_modules/http-cache-semantics": {
6493 + "version": "4.1.1",
6494 + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
6495 + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
6496 + "dev": true
6497 + },
6498 "node_modules/http-errors": {
6499 "version": "2.0.0",
6500 "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -6512,9 +6601,9 @@
6601 }
6602 },
6603 "node_modules/immutable": {
6515 - "version": "4.3.6",
6516 - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz",
6517 - "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==",
6604 + "version": "4.3.7",
6605 + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
6606 + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
6607 "dev": true
6608 },
6609 "node_modules/import-fresh": {
@@ -6556,15 +6645,16 @@
6645 }
6646 },
6647 "node_modules/importx": {
6559 - "version": "0.3.11",
6560 - "resolved": "https://registry.npmjs.org/importx/-/importx-0.3.11.tgz",
6561 - "integrity": "sha512-KsFrXKNGeNdeaBsTWU2sEhL3xo+uxquONy5FWnTBititJKebUBg8EgHW8Wl5bpNP+8iN9yOpGqIhVyjfjJigtA==",
6648 + "version": "0.4.3",
6649 + "resolved": "https://registry.npmjs.org/importx/-/importx-0.4.3.tgz",
6650 + "integrity": "sha512-x6E6OxmWq/SUaj7wDeDeSjyHP+rMUbEaqJ5fw0uEtC/FTX9ocxNMFJ+ONnpJIsRpFz3ya6qJAK4orwSKqw0BSQ==",
6651 "dev": true,
6652 "dependencies": {
6653 "bundle-require": "^5.0.0",
6565 - "debug": "^4.3.5",
6566 - "esbuild": "^0.20.2",
6567 - "jiti": "^1.21.6",
6654 + "debug": "^4.3.6",
6655 + "esbuild": "^0.20.2 || ^0.21.0 || ^0.22.0 || ^0.23.0",
6656 + "jiti": "2.0.0-beta.2",
6657 + "jiti-v1": "npm:jiti@^1",
6658 "pathe": "^1.1.2",
6659 "pkg-types": "^1.1.3",
6660 "tsx": "^4.16.2"
@@ -6573,410 +6663,13 @@
6663 "url": "https://github.com/sponsors/antfu"
6664 }
6665 },
6576 - "node_modules/importx/node_modules/@esbuild/aix-ppc64": {
6577 - "version": "0.20.2",
6578 - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz",
6579 - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==",
6580 - "cpu": [
6581 - "ppc64"
6582 - ],
6666 + "node_modules/importx/node_modules/jiti": {
6667 + "version": "2.0.0-beta.2",
6668 + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.0.0-beta.2.tgz",
6669 + "integrity": "sha512-c+PHQZakiQuMKbnhvrjZUvrK6E/AfmTOf4P+E3Y4FNVHcNMX9e/XrnbEvO+m4wS6ZjsvhHh/POQTlfy8uXFc0A==",
6670 "dev": true,
6584 - "optional": true,
6585 - "os": [
6586 - "aix"
6587 - ],
6588 - "engines": {
6589 - "node": ">=12"
6590 - }
6591 - },
6592 - "node_modules/importx/node_modules/@esbuild/android-arm": {
6593 - "version": "0.20.2",
6594 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz",
6595 - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==",
6596 - "cpu": [
6597 - "arm"
6598 - ],
6599 - "dev": true,
6600 - "optional": true,
6601 - "os": [
6602 - "android"
6603 - ],
6604 - "engines": {
6605 - "node": ">=12"
6606 - }
6607 - },
6608 - "node_modules/importx/node_modules/@esbuild/android-arm64": {
6609 - "version": "0.20.2",
6610 - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz",
6611 - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==",
6612 - "cpu": [
6613 - "arm64"
6614 - ],
6615 - "dev": true,
6616 - "optional": true,
6617 - "os": [
6618 - "android"
6619 - ],
6620 - "engines": {
6621 - "node": ">=12"
6622 - }
6623 - },
6624 - "node_modules/importx/node_modules/@esbuild/android-x64": {
6625 - "version": "0.20.2",
6626 - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz",
6627 - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==",
6628 - "cpu": [
6629 - "x64"
6630 - ],
6631 - "dev": true,
6632 - "optional": true,
6633 - "os": [
6634 - "android"
6635 - ],
6636 - "engines": {
6637 - "node": ">=12"
6638 - }
6639 - },
6640 - "node_modules/importx/node_modules/@esbuild/darwin-arm64": {
6641 - "version": "0.20.2",
6642 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz",
6643 - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==",
6644 - "cpu": [
6645 - "arm64"
6646 - ],
6647 - "dev": true,
6648 - "optional": true,
6649 - "os": [
6650 - "darwin"
6651 - ],
6652 - "engines": {
6653 - "node": ">=12"
6654 - }
6655 - },
6656 - "node_modules/importx/node_modules/@esbuild/darwin-x64": {
6657 - "version": "0.20.2",
6658 - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz",
6659 - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==",
6660 - "cpu": [
6661 - "x64"
6662 - ],
6663 - "dev": true,
6664 - "optional": true,
6665 - "os": [
6666 - "darwin"
6667 - ],
6668 - "engines": {
6669 - "node": ">=12"
6670 - }
6671 - },
6672 - "node_modules/importx/node_modules/@esbuild/freebsd-arm64": {
6673 - "version": "0.20.2",
6674 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz",
6675 - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==",
6676 - "cpu": [
6677 - "arm64"
6678 - ],
6679 - "dev": true,
6680 - "optional": true,
6681 - "os": [
6682 - "freebsd"
6683 - ],
6684 - "engines": {
6685 - "node": ">=12"
6686 - }
6687 - },
6688 - "node_modules/importx/node_modules/@esbuild/freebsd-x64": {
6689 - "version": "0.20.2",
6690 - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz",
6691 - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==",
6692 - "cpu": [
6693 - "x64"
6694 - ],
6695 - "dev": true,
6696 - "optional": true,
6697 - "os": [
6698 - "freebsd"
6699 - ],
6700 - "engines": {
6701 - "node": ">=12"
6702 - }
6703 - },
6704 - "node_modules/importx/node_modules/@esbuild/linux-arm": {
6705 - "version": "0.20.2",
6706 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz",
6707 - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==",
6708 - "cpu": [
6709 - "arm"
6710 - ],
6711 - "dev": true,
6712 - "optional": true,
6713 - "os": [
6714 - "linux"
6715 - ],
6716 - "engines": {
6717 - "node": ">=12"
6718 - }
6719 - },
6720 - "node_modules/importx/node_modules/@esbuild/linux-arm64": {
6721 - "version": "0.20.2",
6722 - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz",
6723 - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==",
6724 - "cpu": [
6725 - "arm64"
6726 - ],
6727 - "dev": true,
6728 - "optional": true,
6729 - "os": [
6730 - "linux"
6731 - ],
6732 - "engines": {
6733 - "node": ">=12"
6734 - }
6735 - },
6736 - "node_modules/importx/node_modules/@esbuild/linux-ia32": {
6737 - "version": "0.20.2",
6738 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz",
6739 - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==",
6740 - "cpu": [
6741 - "ia32"
6742 - ],
6743 - "dev": true,
6744 - "optional": true,
6745 - "os": [
6746 - "linux"
6747 - ],
6748 - "engines": {
6749 - "node": ">=12"
6750 - }
6751 - },
6752 - "node_modules/importx/node_modules/@esbuild/linux-loong64": {
6753 - "version": "0.20.2",
6754 - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz",
6755 - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==",
6756 - "cpu": [
6757 - "loong64"
6758 - ],
6759 - "dev": true,
6760 - "optional": true,
6761 - "os": [
6762 - "linux"
6763 - ],
6764 - "engines": {
6765 - "node": ">=12"
6766 - }
6767 - },
6768 - "node_modules/importx/node_modules/@esbuild/linux-mips64el": {
6769 - "version": "0.20.2",
6770 - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz",
6771 - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==",
6772 - "cpu": [
6773 - "mips64el"
6774 - ],
6775 - "dev": true,
6776 - "optional": true,
6777 - "os": [
6778 - "linux"
6779 - ],
6780 - "engines": {
6781 - "node": ">=12"
6782 - }
6783 - },
6784 - "node_modules/importx/node_modules/@esbuild/linux-ppc64": {
6785 - "version": "0.20.2",
6786 - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz",
6787 - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==",
6788 - "cpu": [
6789 - "ppc64"
6790 - ],
6791 - "dev": true,
6792 - "optional": true,
6793 - "os": [
6794 - "linux"
6795 - ],
6796 - "engines": {
6797 - "node": ">=12"
6798 - }
6799 - },
6800 - "node_modules/importx/node_modules/@esbuild/linux-riscv64": {
6801 - "version": "0.20.2",
6802 - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz",
6803 - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==",
6804 - "cpu": [
6805 - "riscv64"
6806 - ],
6807 - "dev": true,
6808 - "optional": true,
6809 - "os": [
6810 - "linux"
6811 - ],
6812 - "engines": {
6813 - "node": ">=12"
6814 - }
6815 - },
6816 - "node_modules/importx/node_modules/@esbuild/linux-s390x": {
6817 - "version": "0.20.2",
6818 - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz",
6819 - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==",
6820 - "cpu": [
6821 - "s390x"
6822 - ],
6823 - "dev": true,
6824 - "optional": true,
6825 - "os": [
6826 - "linux"
6827 - ],
6828 - "engines": {
6829 - "node": ">=12"
6830 - }
6831 - },
6832 - "node_modules/importx/node_modules/@esbuild/linux-x64": {
6833 - "version": "0.20.2",
6834 - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz",
6835 - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==",
6836 - "cpu": [
6837 - "x64"
6838 - ],
6839 - "dev": true,
6840 - "optional": true,
6841 - "os": [
6842 - "linux"
6843 - ],
6844 - "engines": {
6845 - "node": ">=12"
6846 - }
6847 - },
6848 - "node_modules/importx/node_modules/@esbuild/netbsd-x64": {
6849 - "version": "0.20.2",
6850 - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz",
6851 - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==",
6852 - "cpu": [
6853 - "x64"
6854 - ],
6855 - "dev": true,
6856 - "optional": true,
6857 - "os": [
6858 - "netbsd"
6859 - ],
6860 - "engines": {
6861 - "node": ">=12"
6862 - }
6863 - },
6864 - "node_modules/importx/node_modules/@esbuild/openbsd-x64": {
6865 - "version": "0.20.2",
6866 - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz",
6867 - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==",
6868 - "cpu": [
6869 - "x64"
6870 - ],
6871 - "dev": true,
6872 - "optional": true,
6873 - "os": [
6874 - "openbsd"
6875 - ],
6876 - "engines": {
6877 - "node": ">=12"
6878 - }
6879 - },
6880 - "node_modules/importx/node_modules/@esbuild/sunos-x64": {
6881 - "version": "0.20.2",
6882 - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz",
6883 - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==",
6884 - "cpu": [
6885 - "x64"
6886 - ],
6887 - "dev": true,
6888 - "optional": true,
6889 - "os": [
6890 - "sunos"
6891 - ],
6892 - "engines": {
6893 - "node": ">=12"
6894 - }
6895 - },
6896 - "node_modules/importx/node_modules/@esbuild/win32-arm64": {
6897 - "version": "0.20.2",
6898 - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz",
6899 - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==",
6900 - "cpu": [
6901 - "arm64"
6902 - ],
6903 - "dev": true,
6904 - "optional": true,
6905 - "os": [
6906 - "win32"
6907 - ],
6908 - "engines": {
6909 - "node": ">=12"
6910 - }
6911 - },
6912 - "node_modules/importx/node_modules/@esbuild/win32-ia32": {
6913 - "version": "0.20.2",
6914 - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz",
6915 - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==",
6916 - "cpu": [
6917 - "ia32"
6918 - ],
6919 - "dev": true,
6920 - "optional": true,
6921 - "os": [
6922 - "win32"
6923 - ],
6924 - "engines": {
6925 - "node": ">=12"
6926 - }
6927 - },
6928 - "node_modules/importx/node_modules/@esbuild/win32-x64": {
6929 - "version": "0.20.2",
6930 - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz",
6931 - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==",
6932 - "cpu": [
6933 - "x64"
6934 - ],
6935 - "dev": true,
6936 - "optional": true,
6937 - "os": [
6938 - "win32"
6939 - ],
6940 - "engines": {
6941 - "node": ">=12"
6942 - }
6943 - },
6944 - "node_modules/importx/node_modules/esbuild": {
6945 - "version": "0.20.2",
6946 - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz",
6947 - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==",
6948 - "dev": true,
6949 - "hasInstallScript": true,
6671 "bin": {
6951 - "esbuild": "bin/esbuild"
6952 - },
6953 - "engines": {
6954 - "node": ">=12"
6955 - },
6956 - "optionalDependencies": {
6957 - "@esbuild/aix-ppc64": "0.20.2",
6958 - "@esbuild/android-arm": "0.20.2",
6959 - "@esbuild/android-arm64": "0.20.2",
6960 - "@esbuild/android-x64": "0.20.2",
6961 - "@esbuild/darwin-arm64": "0.20.2",
6962 - "@esbuild/darwin-x64": "0.20.2",
6963 - "@esbuild/freebsd-arm64": "0.20.2",
6964 - "@esbuild/freebsd-x64": "0.20.2",
6965 - "@esbuild/linux-arm": "0.20.2",
6966 - "@esbuild/linux-arm64": "0.20.2",
6967 - "@esbuild/linux-ia32": "0.20.2",
6968 - "@esbuild/linux-loong64": "0.20.2",
6969 - "@esbuild/linux-mips64el": "0.20.2",
6970 - "@esbuild/linux-ppc64": "0.20.2",
6971 - "@esbuild/linux-riscv64": "0.20.2",
6972 - "@esbuild/linux-s390x": "0.20.2",
6973 - "@esbuild/linux-x64": "0.20.2",
6974 - "@esbuild/netbsd-x64": "0.20.2",
6975 - "@esbuild/openbsd-x64": "0.20.2",
6976 - "@esbuild/sunos-x64": "0.20.2",
6977 - "@esbuild/win32-arm64": "0.20.2",
6978 - "@esbuild/win32-ia32": "0.20.2",
6979 - "@esbuild/win32-x64": "0.20.2"
6672 + "jiti": "lib/jiti-cli.mjs"
6673 }
6674 },
6675 "node_modules/imurmurhash": {
@@ -7022,6 +6715,25 @@
6715 "node": ">=10"
6716 }
6717 },
6718 + "node_modules/ip-address": {
6719 + "version": "9.0.5",
6720 + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
6721 + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
6722 + "dev": true,
6723 + "dependencies": {
6724 + "jsbn": "1.1.0",
6725 + "sprintf-js": "^1.1.3"
6726 + },
6727 + "engines": {
6728 + "node": ">= 12"
6729 + }
6730 + },
6731 + "node_modules/ip-address/node_modules/jsbn": {
6732 + "version": "1.1.0",
6733 + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
6734 + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
6735 + "dev": true
6736 + },
6737 "node_modules/is-binary-path": {
6738 "version": "2.1.0",
6739 "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -7047,9 +6759,9 @@
6759 }
6760 },
6761 "node_modules/is-core-module": {
7050 - "version": "2.14.0",
7051 - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.14.0.tgz",
7052 - "integrity": "sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==",
6762 + "version": "2.15.0",
6763 + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
6764 + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
6765 "dev": true,
6766 "dependencies": {
6767 "hasown": "^2.0.2"
@@ -7168,6 +6880,12 @@
6880 "url": "https://github.com/sponsors/sindresorhus"
6881 }
6882 },
6883 + "node_modules/is-lambda": {
6884 + "version": "1.0.1",
6885 + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
6886 + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==",
6887 + "dev": true
6888 + },
6889 "node_modules/is-number": {
6890 "version": "7.0.0",
6891 "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -7278,6 +6996,16 @@
6996 "jiti": "bin/jiti.js"
6997 }
6998 },
6999 + "node_modules/jiti-v1": {
7000 + "name": "jiti",
7001 + "version": "1.21.6",
7002 + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
7003 + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
7004 + "dev": true,
7005 + "bin": {
7006 + "jiti": "bin/jiti.js"
7007 + }
7008 + },
7009 "node_modules/joi": {
7010 "version": "17.13.3",
7011 "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
@@ -7292,9 +7020,9 @@
7020 }
7021 },
7022 "node_modules/jose": {
7295 - "version": "5.6.3",
7296 - "resolved": "https://registry.npmjs.org/jose/-/jose-5.6.3.tgz",
7297 - "integrity": "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==",
7023 + "version": "5.8.0",
7024 + "resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz",
7025 + "integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==",
7026 "funding": {
7027 "url": "https://github.com/sponsors/panva"
7028 }
@@ -7358,9 +7086,9 @@
7086 "dev": true
7087 },
7088 "node_modules/jsdom": {
7361 - "version": "24.1.0",
7362 - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.0.tgz",
7363 - "integrity": "sha512-6gpM7pRXCwIOKxX47cgOyvyQDN/Eh0f1MeKySBV2xGdKtqJBLj8P25eY3EVCWo2mglDDzozR2r2MW4T+JiNUZA==",
7089 + "version": "25.0.0",
7090 + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.0.tgz",
7091 + "integrity": "sha512-OhoFVT59T7aEq75TVw9xxEfkXgacpqAhQaYgP9y/fDqWQCMB/b1H66RfmPm/MaeaAIU9nDwMOVTlPN51+ao6CQ==",
7092 "dev": true,
7093 "dependencies": {
7094 "cssstyle": "^4.0.1",
@@ -7369,11 +7097,11 @@
7097 "form-data": "^4.0.0",
7098 "html-encoding-sniffer": "^4.0.0",
7099 "http-proxy-agent": "^7.0.2",
7372 - "https-proxy-agent": "^7.0.4",
7100 + "https-proxy-agent": "^7.0.5",
7101 "is-potential-custom-element-name": "^1.0.1",
7374 - "nwsapi": "^2.2.10",
7102 + "nwsapi": "^2.2.12",
7103 "parse5": "^7.1.2",
7376 - "rrweb-cssom": "^0.7.0",
7104 + "rrweb-cssom": "^0.7.1",
7105 "saxes": "^6.0.0",
7106 "symbol-tree": "^3.2.4",
7107 "tough-cookie": "^4.1.4",
@@ -7382,7 +7110,7 @@
7110 "whatwg-encoding": "^3.1.1",
7111 "whatwg-mimetype": "^4.0.0",
7112 "whatwg-url": "^14.0.0",
7385 - "ws": "^8.17.0",
7113 + "ws": "^8.18.0",
7114 "xml-name-validator": "^5.0.0"
7115 },
7116 "engines": {
@@ -7492,6 +7220,15 @@
7220 "graceful-fs": "^4.1.6"
7221 }
7222 },
7223 + "node_modules/jsonparse": {
7224 + "version": "1.3.1",
7225 + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
7226 + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
7227 + "dev": true,
7228 + "engines": [
7229 + "node >= 0.2.0"
7230 + ]
7231 + },
7232 "node_modules/jsprim": {
7233 "version": "2.0.2",
7234 "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz",
@@ -8066,11 +7803,34 @@
7803 }
7804 },
7805 "node_modules/magic-string": {
8069 - "version": "0.30.10",
8070 - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
8071 - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
7806 + "version": "0.30.11",
7807 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
7808 + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
7809 + "dependencies": {
7810 + "@jridgewell/sourcemap-codec": "^1.5.0"
7811 + }
7812 + },
7813 + "node_modules/make-fetch-happen": {
7814 + "version": "13.0.1",
7815 + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz",
7816 + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==",
7817 + "dev": true,
7818 "dependencies": {
8073 - "@jridgewell/sourcemap-codec": "^1.4.15"
7819 + "@npmcli/agent": "^2.0.0",
7820 + "cacache": "^18.0.0",
7821 + "http-cache-semantics": "^4.1.1",
7822 + "is-lambda": "^1.0.1",
7823 + "minipass": "^7.0.2",
7824 + "minipass-fetch": "^3.0.0",
7825 + "minipass-flush": "^1.0.5",
7826 + "minipass-pipeline": "^1.2.4",
7827 + "negotiator": "^0.6.3",
7828 + "proc-log": "^4.2.0",
7829 + "promise-retry": "^2.0.1",
7830 + "ssri": "^10.0.0"
7831 + },
7832 + "engines": {
7833 + "node": "^16.14.0 || >=18.0.0"
7834 }
7835 },
7836 "node_modules/map-stream": {
@@ -8148,9 +7908,9 @@
7908 }
7909 },
7910 "node_modules/micromatch": {
8151 - "version": "4.0.7",
8152 - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
8153 - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
7911 + "version": "4.0.8",
7912 + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
7913 + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
7914 "dependencies": {
7915 "braces": "^3.0.3",
7916 "picomatch": "^2.3.1"
@@ -8163,7 +7923,6 @@
7923 "version": "1.52.0",
7924 "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
7925 "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
8166 - "dev": true,
7926 "engines": {
7927 "node": ">= 0.6"
7928 }
@@ -8172,55 +7931,204 @@
7931 "version": "2.1.35",
7932 "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
7933 "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
7934 + "dependencies": {
7935 + "mime-db": "1.52.0"
7936 + },
7937 + "engines": {
7938 + "node": ">= 0.6"
7939 + }
7940 + },
7941 + "node_modules/mimic-fn": {
7942 + "version": "2.1.0",
7943 + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
7944 + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
7945 + "dev": true,
7946 + "engines": {
7947 + "node": ">=6"
7948 + }
7949 + },
7950 + "node_modules/minimatch": {
7951 + "version": "9.0.3",
7952 + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
7953 + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
7954 + "dependencies": {
7955 + "brace-expansion": "^2.0.1"
7956 + },
7957 + "engines": {
7958 + "node": ">=16 || 14 >=14.17"
7959 + },
7960 + "funding": {
7961 + "url": "https://github.com/sponsors/isaacs"
7962 + }
7963 + },
7964 + "node_modules/minimist": {
7965 + "version": "1.2.8",
7966 + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
7967 + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
7968 + "dev": true,
7969 + "funding": {
7970 + "url": "https://github.com/sponsors/ljharb"
7971 + }
7972 + },
7973 + "node_modules/minipass": {
7974 + "version": "7.1.2",
7975 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
7976 + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
7977 + "dev": true,
7978 + "engines": {
7979 + "node": ">=16 || 14 >=14.17"
7980 + }
7981 + },
7982 + "node_modules/minipass-collect": {
7983 + "version": "2.0.1",
7984 + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz",
7985 + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==",
7986 + "dev": true,
7987 + "dependencies": {
7988 + "minipass": "^7.0.3"
7989 + },
7990 + "engines": {
7991 + "node": ">=16 || 14 >=14.17"
7992 + }
7993 + },
7994 + "node_modules/minipass-fetch": {
7995 + "version": "3.0.5",
7996 + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz",
7997 + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==",
7998 + "dev": true,
7999 + "dependencies": {
8000 + "minipass": "^7.0.3",
8001 + "minipass-sized": "^1.0.3",
8002 + "minizlib": "^2.1.2"
8003 + },
8004 + "engines": {
8005 + "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
8006 + },
8007 + "optionalDependencies": {
8008 + "encoding": "^0.1.13"
8009 + }
8010 + },
8011 + "node_modules/minipass-flush": {
8012 + "version": "1.0.5",
8013 + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
8014 + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
8015 + "dev": true,
8016 + "dependencies": {
8017 + "minipass": "^3.0.0"
8018 + },
8019 + "engines": {
8020 + "node": ">= 8"
8021 + }
8022 + },
8023 + "node_modules/minipass-flush/node_modules/minipass": {
8024 + "version": "3.3.6",
8025 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
8026 + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
8027 + "dev": true,
8028 + "dependencies": {
8029 + "yallist": "^4.0.0"
8030 + },
8031 + "engines": {
8032 + "node": ">=8"
8033 + }
8034 + },
8035 + "node_modules/minipass-flush/node_modules/yallist": {
8036 + "version": "4.0.0",
8037 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
8038 + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
8039 + "dev": true
8040 + },
8041 + "node_modules/minipass-pipeline": {
8042 + "version": "1.2.4",
8043 + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
8044 + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
8045 + "dev": true,
8046 + "dependencies": {
8047 + "minipass": "^3.0.0"
8048 + },
8049 + "engines": {
8050 + "node": ">=8"
8051 + }
8052 + },
8053 + "node_modules/minipass-pipeline/node_modules/minipass": {
8054 + "version": "3.3.6",
8055 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
8056 + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
8057 + "dev": true,
8058 + "dependencies": {
8059 + "yallist": "^4.0.0"
8060 + },
8061 + "engines": {
8062 + "node": ">=8"
8063 + }
8064 + },
8065 + "node_modules/minipass-pipeline/node_modules/yallist": {
8066 + "version": "4.0.0",
8067 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
8068 + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
8069 + "dev": true
8070 + },
8071 + "node_modules/minipass-sized": {
8072 + "version": "1.0.3",
8073 + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
8074 + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
8075 "dev": true,
8076 "dependencies": {
8177 - "mime-db": "1.52.0"
8077 + "minipass": "^3.0.0"
8078 },
8079 "engines": {
8180 - "node": ">= 0.6"
8080 + "node": ">=8"
8081 }
8082 },
8183 - "node_modules/mimic-fn": {
8184 - "version": "2.1.0",
8185 - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
8186 - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
8083 + "node_modules/minipass-sized/node_modules/minipass": {
8084 + "version": "3.3.6",
8085 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
8086 + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
8087 "dev": true,
8188 - "engines": {
8189 - "node": ">=6"
8190 - }
8191 - },
8192 - "node_modules/minimatch": {
8193 - "version": "9.0.3",
8194 - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
8195 - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
8088 "dependencies": {
8197 - "brace-expansion": "^2.0.1"
8089 + "yallist": "^4.0.0"
8090 },
8091 "engines": {
8200 - "node": ">=16 || 14 >=14.17"
8201 - },
8202 - "funding": {
8203 - "url": "https://github.com/sponsors/isaacs"
8092 + "node": ">=8"
8093 }
8094 },
8206 - "node_modules/minimist": {
8207 - "version": "1.2.8",
8208 - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
8209 - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
8095 + "node_modules/minipass-sized/node_modules/yallist": {
8096 + "version": "4.0.0",
8097 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
8098 + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
8099 + "dev": true
8100 + },
8101 + "node_modules/minizlib": {
8102 + "version": "2.1.2",
8103 + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
8104 + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
8105 "dev": true,
8211 - "funding": {
8212 - "url": "https://github.com/sponsors/ljharb"
8106 + "dependencies": {
8107 + "minipass": "^3.0.0",
8108 + "yallist": "^4.0.0"
8109 + },
8110 + "engines": {
8111 + "node": ">= 8"
8112 }
8113 },
8215 - "node_modules/minipass": {
8216 - "version": "7.1.2",
8217 - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
8218 - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
8114 + "node_modules/minizlib/node_modules/minipass": {
8115 + "version": "3.3.6",
8116 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
8117 + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
8118 "dev": true,
8119 + "dependencies": {
8120 + "yallist": "^4.0.0"
8121 + },
8122 "engines": {
8221 - "node": ">=16 || 14 >=14.17"
8123 + "node": ">=8"
8124 }
8125 },
8126 + "node_modules/minizlib/node_modules/yallist": {
8127 + "version": "4.0.0",
8128 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
8129 + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
8130 + "dev": true
8131 + },
8132 "node_modules/mitt": {
8133 "version": "3.0.1",
8134 "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
@@ -8282,9 +8190,9 @@
8190 }
8191 },
8192 "node_modules/naive-ui": {
8285 - "version": "2.38.2",
8286 - "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.38.2.tgz",
8287 - "integrity": "sha512-WhZ+6DW61aYSmFyfH7evcSGFmd2xR68Yq1mNRrVdJwBhZsnNdAUsMN9IeNCVEPMCND/jzYZghkStoNoR5Xa09g==",
8193 + "version": "2.39.0",
8194 + "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.39.0.tgz",
8195 + "integrity": "sha512-5oUJzRG+rtLSH8eRU+fJvVYiQids2BxF9jp+fwGoAqHOptEINrBlgBu9uy+95RHE5FLJ7Q/z41o+qkoGnUrKxQ==",
8196 "dependencies": {
8197 "@css-render/plugin-bem": "^0.15.12",
8198 "@css-render/vue3-ssr": "^0.15.12",
@@ -8348,9 +8256,9 @@
8256 "dev": true
8257 },
8258 "node_modules/node-releases": {
8351 - "version": "2.0.14",
8352 - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
8353 - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
8259 + "version": "2.0.18",
8260 + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
8261 + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
8262 "dev": true
8263 },
8264 "node_modules/nopt": {
@@ -8395,6 +8303,40 @@
8303 "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
8304 }
8305 },
8306 + "node_modules/npm-package-arg": {
8307 + "version": "11.0.3",
8308 + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz",
8309 + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==",
8310 + "dev": true,
8311 + "dependencies": {
8312 + "hosted-git-info": "^7.0.0",
8313 + "proc-log": "^4.0.0",
8314 + "semver": "^7.3.5",
8315 + "validate-npm-package-name": "^5.0.0"
8316 + },
8317 + "engines": {
8318 + "node": "^16.14.0 || >=18.0.0"
8319 + }
8320 + },
8321 + "node_modules/npm-registry-fetch": {
8322 + "version": "17.1.0",
8323 + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-17.1.0.tgz",
8324 + "integrity": "sha512-5+bKQRH0J1xG1uZ1zMNvxW0VEyoNWgJpY9UDuluPFLKDfJ9u2JmmjmTJV1srBGQOROfdBMiVvnH2Zvpbm+xkVA==",
8325 + "dev": true,
8326 + "dependencies": {
8327 + "@npmcli/redact": "^2.0.0",
8328 + "jsonparse": "^1.3.1",
8329 + "make-fetch-happen": "^13.0.0",
8330 + "minipass": "^7.0.2",
8331 + "minipass-fetch": "^3.0.0",
8332 + "minizlib": "^2.1.2",
8333 + "npm-package-arg": "^11.0.0",
8334 + "proc-log": "^4.0.0"
8335 + },
8336 + "engines": {
8337 + "node": "^16.14.0 || >=18.0.0"
8338 + }
8339 + },
8340 "node_modules/npm-run-all2": {
8341 "version": "6.2.2",
8342 "resolved": "https://registry.npmjs.org/npm-run-all2/-/npm-run-all2-6.2.2.tgz",
@@ -8456,9 +8398,9 @@
8398 }
8399 },
8400 "node_modules/nwsapi": {
8459 - "version": "2.2.10",
8460 - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.10.tgz",
8461 - "integrity": "sha512-QK0sRs7MKv0tKe1+5uZIQk/C8XGza4DAnztJG8iD+TpJIORARrCxczA738awHrZoHeTjSSoHqao2teO0dC/gFQ==",
8401 + "version": "2.2.12",
8402 + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.12.tgz",
8403 + "integrity": "sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==",
8404 "dev": true
8405 },
8406 "node_modules/object-assign": {
@@ -8631,6 +8573,12 @@
8573 "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==",
8574 "dev": true
8575 },
8576 + "node_modules/package-manager-detector": {
8577 + "version": "0.2.0",
8578 + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-0.2.0.tgz",
8579 + "integrity": "sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog==",
8580 + "dev": true
8581 + },
8582 "node_modules/parent-module": {
8583 "version": "1.0.1",
8584 "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -8824,12 +8772,12 @@
8772 }
8773 },
8774 "node_modules/pinia": {
8827 - "version": "2.1.7",
8828 - "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.1.7.tgz",
8829 - "integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==",
8775 + "version": "2.2.2",
8776 + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.2.tgz",
8777 + "integrity": "sha512-ja2XqFWZC36mupU4z1ZzxeTApV7DOw44cV4dhQ9sGwun+N89v/XP7+j7q6TanS1u1tdbK4r+1BUx7heMaIdagA==",
8778 "dependencies": {
8831 - "@vue/devtools-api": "^6.5.0",
8832 - "vue-demi": ">=0.14.5"
8779 + "@vue/devtools-api": "^6.6.3",
8780 + "vue-demi": "^0.14.10"
8781 },
8782 "funding": {
8783 "url": "https://github.com/sponsors/posva"
@@ -8857,9 +8805,9 @@
8805 }
8806 },
8807 "node_modules/pinia/node_modules/vue-demi": {
8860 - "version": "0.14.8",
8861 - "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.8.tgz",
8862 - "integrity": "sha512-Uuqnk9YE9SsWeReYqK2alDI5YzciATE0r2SkA6iMAtuXvNTMNACJLJEXNXaEy94ECuBe4Sk6RzRU80kjdbIo1Q==",
8808 + "version": "0.14.10",
8809 + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
8810 + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
8811 "hasInstallScript": true,
8812 "bin": {
8813 "vue-demi-fix": "bin/vue-demi-fix.js",
@@ -8934,9 +8882,9 @@
8882 }
8883 },
8884 "node_modules/postcss": {
8937 - "version": "8.4.39",
8938 - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
8939 - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
8885 + "version": "8.4.41",
8886 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
8887 + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
8888 "funding": [
8889 {
8890 "type": "opencollective",
@@ -9044,28 +8992,34 @@
8992 }
8993 },
8994 "node_modules/postcss-nested": {
9047 - "version": "6.0.1",
9048 - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz",
9049 - "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==",
8995 + "version": "6.2.0",
8996 + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
8997 + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
8998 "dev": true,
8999 + "funding": [
9000 + {
9001 + "type": "opencollective",
9002 + "url": "https://opencollective.com/postcss/"
9003 + },
9004 + {
9005 + "type": "github",
9006 + "url": "https://github.com/sponsors/ai"
9007 + }
9008 + ],
9009 "dependencies": {
9052 - "postcss-selector-parser": "^6.0.11"
9010 + "postcss-selector-parser": "^6.1.1"
9011 },
9012 "engines": {
9013 "node": ">=12.0"
9014 },
9057 - "funding": {
9058 - "type": "opencollective",
9059 - "url": "https://opencollective.com/postcss/"
9060 - },
9015 "peerDependencies": {
9016 "postcss": "^8.2.14"
9017 }
9018 },
9019 "node_modules/postcss-selector-parser": {
9066 - "version": "6.1.0",
9067 - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.0.tgz",
9068 - "integrity": "sha512-UMz42UD0UY0EApS0ZL9o1XnLhSTtvvvLe5Dc2H2O56fvRZi+KulDyf5ctDhhtYJBGKStV2FL1fy6253cmLgqVQ==",
9020 + "version": "6.1.1",
9021 + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
9022 + "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
9023 "dependencies": {
9024 "cssesc": "^3.0.0",
9025 "util-deprecate": "^1.0.2"
@@ -9089,9 +9043,9 @@
9043 }
9044 },
9045 "node_modules/prettier": {
9092 - "version": "3.3.2",
9093 - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
9094 - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
9046 + "version": "3.3.3",
9047 + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
9048 + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
9049 "bin": {
9050 "prettier": "bin/prettier.cjs"
9051 },
@@ -9125,6 +9079,15 @@
9079 "url": "https://github.com/sponsors/sindresorhus"
9080 }
9081 },
9082 + "node_modules/proc-log": {
9083 + "version": "4.2.0",
9084 + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz",
9085 + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==",
9086 + "dev": true,
9087 + "engines": {
9088 + "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
9089 + }
9090 + },
9091 "node_modules/process": {
9092 "version": "0.11.10",
9093 "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -9134,6 +9097,19 @@
9097 "node": ">= 0.6.0"
9098 }
9099 },
9100 + "node_modules/promise-retry": {
9101 + "version": "2.0.1",
9102 + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
9103 + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
9104 + "dev": true,
9105 + "dependencies": {
9106 + "err-code": "^2.0.2",
9107 + "retry": "^0.12.0"
9108 + },
9109 + "engines": {
9110 + "node": ">=10"
9111 + }
9112 + },
9113 "node_modules/proto-list": {
9114 "version": "1.2.4",
9115 "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@@ -9531,6 +9507,15 @@
9507 "node": ">=8"
9508 }
9509 },
9510 + "node_modules/retry": {
9511 + "version": "0.12.0",
9512 + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
9513 + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
9514 + "dev": true,
9515 + "engines": {
9516 + "node": ">= 4"
9517 + }
9518 + },
9519 "node_modules/reusify": {
9520 "version": "1.0.4",
9521 "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
@@ -9547,9 +9532,9 @@
9532 "dev": true
9533 },
9534 "node_modules/rollup": {
9550 - "version": "4.18.1",
9551 - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz",
9552 - "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==",
9535 + "version": "4.21.0",
9536 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.0.tgz",
9537 + "integrity": "sha512-vo+S/lfA2lMS7rZ2Qoubi6I5hwZwzXeUIctILZLbHI+laNtvhhOIon2S1JksA5UEDQ7l3vberd0fxK44lTYjbQ==",
9538 "dev": true,
9539 "dependencies": {
9540 "@types/estree": "1.0.5"
@@ -9562,22 +9547,22 @@
9547 "npm": ">=8.0.0"
9548 },
9549 "optionalDependencies": {
9565 - "@rollup/rollup-android-arm-eabi": "4.18.1",
9566 - "@rollup/rollup-android-arm64": "4.18.1",
9567 - "@rollup/rollup-darwin-arm64": "4.18.1",
9568 - "@rollup/rollup-darwin-x64": "4.18.1",
9569 - "@rollup/rollup-linux-arm-gnueabihf": "4.18.1",
9570 - "@rollup/rollup-linux-arm-musleabihf": "4.18.1",
9571 - "@rollup/rollup-linux-arm64-gnu": "4.18.1",
9572 - "@rollup/rollup-linux-arm64-musl": "4.18.1",
9573 - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1",
9574 - "@rollup/rollup-linux-riscv64-gnu": "4.18.1",
9575 - "@rollup/rollup-linux-s390x-gnu": "4.18.1",
9576 - "@rollup/rollup-linux-x64-gnu": "4.18.1",
9577 - "@rollup/rollup-linux-x64-musl": "4.18.1",
9578 - "@rollup/rollup-win32-arm64-msvc": "4.18.1",
9579 - "@rollup/rollup-win32-ia32-msvc": "4.18.1",
9580 - "@rollup/rollup-win32-x64-msvc": "4.18.1",
9550 + "@rollup/rollup-android-arm-eabi": "4.21.0",
9551 + "@rollup/rollup-android-arm64": "4.21.0",
9552 + "@rollup/rollup-darwin-arm64": "4.21.0",
9553 + "@rollup/rollup-darwin-x64": "4.21.0",
9554 + "@rollup/rollup-linux-arm-gnueabihf": "4.21.0",
9555 + "@rollup/rollup-linux-arm-musleabihf": "4.21.0",
9556 + "@rollup/rollup-linux-arm64-gnu": "4.21.0",
9557 + "@rollup/rollup-linux-arm64-musl": "4.21.0",
9558 + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.0",
9559 + "@rollup/rollup-linux-riscv64-gnu": "4.21.0",
9560 + "@rollup/rollup-linux-s390x-gnu": "4.21.0",
9561 + "@rollup/rollup-linux-x64-gnu": "4.21.0",
9562 + "@rollup/rollup-linux-x64-musl": "4.21.0",
9563 + "@rollup/rollup-win32-arm64-msvc": "4.21.0",
9564 + "@rollup/rollup-win32-ia32-msvc": "4.21.0",
9565 + "@rollup/rollup-win32-x64-msvc": "4.21.0",
9566 "fsevents": "~2.3.2"
9567 }
9568 },
@@ -9624,6 +9609,19 @@
9609 "url": "https://github.com/sponsors/sindresorhus"
9610 }
9611 },
9612 + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
9613 + "version": "4.21.0",
9614 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.0.tgz",
9615 + "integrity": "sha512-e2hrvElFIh6kW/UNBQK/kzqMNY5mO+67YtEh9OA65RM5IJXYTWiXjX6fjIiPaqOkBthYF1EqgiZ6OXKcQsM0hg==",
9616 + "cpu": [
9617 + "x64"
9618 + ],
9619 + "dev": true,
9620 + "optional": true,
9621 + "os": [
9622 + "linux"
9623 + ]
9624 + },
9625 "node_modules/rrweb-cssom": {
9626 "version": "0.7.1",
9627 "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
@@ -9700,9 +9698,9 @@
9698 "dev": true
9699 },
9700 "node_modules/sass": {
9703 - "version": "1.77.7",
9704 - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.7.tgz",
9705 - "integrity": "sha512-9ywH75cO+rLjbrZ6en3Gp8qAMwPGBapFtlsMJoDTkcMU/bSe5a6cjKVUn5Jr4Gzg5GbP3HE8cm+02pLCgcoMow==",
9701 + "version": "1.77.8",
9702 + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
9703 + "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
9704 "dev": true,
9705 "dependencies": {
9706 "chokidar": ">=3.0.0 <4.0.0",
@@ -9746,9 +9744,9 @@
9744 "integrity": "sha512-MW8Qs6vbzo0pHmDpFSYPna+lwpZ6Zk1ancbajw/7E8TKtHdV+1DfZZD+kKJEhG/cAoB/i+LiT+5msZOqj0DwRA=="
9745 },
9746 "node_modules/semver": {
9749 - "version": "7.6.2",
9750 - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
9751 - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
9747 + "version": "7.6.3",
9748 + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
9749 + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
9750 "bin": {
9751 "semver": "bin/semver.js"
9752 },
@@ -9808,11 +9806,11 @@
9806 }
9807 },
9808 "node_modules/shiki": {
9811 - "version": "1.10.3",
9812 - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.10.3.tgz",
9813 - "integrity": "sha512-eneCLncGuvPdTutJuLyUGS8QNPAVFO5Trvld2wgEq1e002mwctAhJKeMGWtWVXOIEzmlcLRqcgPSorR6AVzOmQ==",
9809 + "version": "1.14.1",
9810 + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.14.1.tgz",
9811 + "integrity": "sha512-FujAN40NEejeXdzPt+3sZ3F2dx1U24BY2XTY01+MG8mbxCiA2XukXdcbyMyLAHJ/1AUUnQd1tZlvIjefWWEJeA==",
9812 "dependencies": {
9815 - "@shikijs/core": "1.10.3",
9813 + "@shikijs/core": "1.14.1",
9814 "@types/hast": "^3.0.4"
9815 }
9816 },
@@ -9921,6 +9919,44 @@
9919 "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
9920 "dev": true
9921 },
9922 + "node_modules/smart-buffer": {
9923 + "version": "4.2.0",
9924 + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
9925 + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
9926 + "dev": true,
9927 + "engines": {
9928 + "node": ">= 6.0.0",
9929 + "npm": ">= 3.0.0"
9930 + }
9931 + },
9932 + "node_modules/socks": {
9933 + "version": "2.8.3",
9934 + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
9935 + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
9936 + "dev": true,
9937 + "dependencies": {
9938 + "ip-address": "^9.0.5",
9939 + "smart-buffer": "^4.2.0"
9940 + },
9941 + "engines": {
9942 + "node": ">= 10.0.0",
9943 + "npm": ">= 3.0.0"
9944 + }
9945 + },
9946 + "node_modules/socks-proxy-agent": {
9947 + "version": "8.0.4",
9948 + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
9949 + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
9950 + "dev": true,
9951 + "dependencies": {
9952 + "agent-base": "^7.1.1",
9953 + "debug": "^4.3.4",
9954 + "socks": "^2.8.3"
9955 + },
9956 + "engines": {
9957 + "node": ">= 14"
9958 + }
9959 + },
9960 "node_modules/sortablejs": {
9961 "version": "1.14.0",
9962 "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
@@ -9964,6 +10000,12 @@
10000 "node": "*"
10001 }
10002 },
10003 + "node_modules/sprintf-js": {
10004 + "version": "1.1.3",
10005 + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
10006 + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
10007 + "dev": true
10008 + },
10009 "node_modules/sshpk": {
10010 "version": "1.18.0",
10011 "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
@@ -9989,6 +10031,18 @@
10031 "node": ">=0.10.0"
10032 }
10033 },
10034 + "node_modules/ssri": {
10035 + "version": "10.0.6",
10036 + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz",
10037 + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==",
10038 + "dev": true,
10039 + "dependencies": {
10040 + "minipass": "^7.0.3"
10041 + },
10042 + "engines": {
10043 + "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
10044 + }
10045 + },
10046 "node_modules/stackback": {
10047 "version": "0.0.2",
10048 "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -9996,15 +10050,15 @@
10050 "dev": true
10051 },
10052 "node_modules/start-server-and-test": {
9999 - "version": "2.0.4",
10000 - "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.4.tgz",
10001 - "integrity": "sha512-CKNeBTcP0hVqIlNismHMudb9q3lLdAjcVPO13/7gfI66fcJpeIb/o4NzQd1JK/CD+lfWVqr10ZH9Y14+OwlJuw==",
10053 + "version": "2.0.5",
10054 + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.5.tgz",
10055 + "integrity": "sha512-2CV4pz69NJVJKQmJeSr+O+SPtOreu0yxvhPmSXclzmAKkPREuMabyMh+Txpzemjx0RDzXOcG2XkhiUuxjztSQw==",
10056 "dev": true,
10057 "dependencies": {
10058 "arg": "^5.0.2",
10059 "bluebird": "3.7.2",
10060 "check-more-types": "2.24.0",
10007 - "debug": "4.3.5",
10061 + "debug": "4.3.6",
10062 "execa": "5.1.1",
10063 "lazy-ass": "1.6.0",
10064 "ps-tree": "1.2.0",
@@ -10087,15 +10141,6 @@
10141 "duplexer": "~0.1.1"
10142 }
10143 },
10090 - "node_modules/string-argv": {
10091 - "version": "0.3.2",
10092 - "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
10093 - "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==",
10094 - "dev": true,
10095 - "engines": {
10096 - "node": ">=0.6.19"
10097 - }
10098 - },
10144 "node_modules/string-width": {
10145 "version": "4.2.3",
10146 "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -10427,9 +10472,9 @@
10472 }
10473 },
10474 "node_modules/tailwindcss": {
10430 - "version": "3.4.4",
10431 - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",
10432 - "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==",
10475 + "version": "3.4.10",
10476 + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
10477 + "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
10478 "dev": true,
10479 "dependencies": {
10480 "@alloc/quick-lru": "^5.2.0",
@@ -10463,16 +10508,87 @@
10508 "node": ">=14.0.0"
10509 }
10510 },
10511 + "node_modules/tar": {
10512 + "version": "6.2.1",
10513 + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
10514 + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
10515 + "dev": true,
10516 + "dependencies": {
10517 + "chownr": "^2.0.0",
10518 + "fs-minipass": "^2.0.0",
10519 + "minipass": "^5.0.0",
10520 + "minizlib": "^2.1.1",
10521 + "mkdirp": "^1.0.3",
10522 + "yallist": "^4.0.0"
10523 + },
10524 + "engines": {
10525 + "node": ">=10"
10526 + }
10527 + },
10528 + "node_modules/tar/node_modules/fs-minipass": {
10529 + "version": "2.1.0",
10530 + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
10531 + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
10532 + "dev": true,
10533 + "dependencies": {
10534 + "minipass": "^3.0.0"
10535 + },
10536 + "engines": {
10537 + "node": ">= 8"
10538 + }
10539 + },
10540 + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": {
10541 + "version": "3.3.6",
10542 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
10543 + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
10544 + "dev": true,
10545 + "dependencies": {
10546 + "yallist": "^4.0.0"
10547 + },
10548 + "engines": {
10549 + "node": ">=8"
10550 + }
10551 + },
10552 + "node_modules/tar/node_modules/minipass": {
10553 + "version": "5.0.0",
10554 + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
10555 + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
10556 + "dev": true,
10557 + "engines": {
10558 + "node": ">=8"
10559 + }
10560 + },
10561 + "node_modules/tar/node_modules/mkdirp": {
10562 + "version": "1.0.4",
10563 + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
10564 + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
10565 + "dev": true,
10566 + "bin": {
10567 + "mkdirp": "bin/cmd.js"
10568 + },
10569 + "engines": {
10570 + "node": ">=10"
10571 + }
10572 + },
10573 + "node_modules/tar/node_modules/yallist": {
10574 + "version": "4.0.0",
10575 + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
10576 + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
10577 + "dev": true
10578 + },
10579 "node_modules/taze": {
10467 - "version": "0.14.2",
10468 - "resolved": "https://registry.npmjs.org/taze/-/taze-0.14.2.tgz",
10469 - "integrity": "sha512-VjOanFrc3lYIOi6o+FX4RI5m0NOhOb3d+vBK+fRoR4izS6IIGh3y1liMGsnSpC4V7SEWPmh1Z8z4Vr9AWJKFOg==",
10580 + "version": "0.16.7",
10581 + "resolved": "https://registry.npmjs.org/taze/-/taze-0.16.7.tgz",
10582 + "integrity": "sha512-bVKeFJc/rewVI5MFcG8EK5+6jWx37c3IiDy9qnk9Pv6FV8OLu6GhTk1ru+KLmvGwQc2twqtKA8HW3HmjHA2bEQ==",
10583 "dev": true,
10584 "dependencies": {
10472 - "@antfu/ni": "^0.21.12",
10473 - "@jsdevtools/ez-spawn": "^3.0.4",
10585 + "@antfu/ni": "^0.23.0",
10586 + "js-yaml": "^4.1.0",
10587 + "npm-registry-fetch": "^17.1.0",
10588 "ofetch": "^1.3.4",
10475 - "unconfig": "^0.4.5",
10589 + "package-manager-detector": "^0.2.0",
10590 + "tinyexec": "^0.3.0",
10591 + "unconfig": "^0.5.5",
10592 "yargs": "^17.7.2"
10593 },
10594 "bin": {
@@ -10483,14 +10599,14 @@
10599 }
10600 },
10601 "node_modules/taze/node_modules/unconfig": {
10486 - "version": "0.4.5",
10487 - "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-0.4.5.tgz",
10488 - "integrity": "sha512-+b4pZmGFeT3FD9EHWGkNwuOwrNzfgUq4ZoTh63+EKDyxLvYKajbowjRcKLumGExmPKNi5jxIpzhheKsPtV8dig==",
10602 + "version": "0.5.5",
10603 + "resolved": "https://registry.npmjs.org/unconfig/-/unconfig-0.5.5.tgz",
10604 + "integrity": "sha512-VQZ5PT9HDX+qag0XdgQi8tJepPhXiR/yVOkn707gJDKo31lGjRilPREiQJ9Z6zd/Ugpv6ZvO5VxVIcatldYcNQ==",
10605 "dev": true,
10606 "dependencies": {
10607 "@antfu/utils": "^0.7.10",
10608 "defu": "^6.1.4",
10493 - "importx": "^0.3.10"
10609 + "importx": "^0.4.3"
10610 },
10611 "funding": {
10612 "url": "https://github.com/sponsors/antfu"
@@ -10543,6 +10659,12 @@
10659 "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
10660 "dev": true
10661 },
10662 + "node_modules/tinyexec": {
10663 + "version": "0.3.0",
10664 + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz",
10665 + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==",
10666 + "dev": true
10667 + },
10668 "node_modules/tinypool": {
10669 "version": "1.0.0",
10670 "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz",
@@ -10583,7 +10705,6 @@
10705 "version": "2.0.0",
10706 "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
10707 "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==",
10586 - "dev": true,
10708 "engines": {
10709 "node": ">=4"
10710 }
@@ -10690,9 +10811,9 @@
10811 }
10812 },
10813 "node_modules/tsx": {
10693 - "version": "4.16.2",
10694 - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.2.tgz",
10695 - "integrity": "sha512-C1uWweJDgdtX2x600HjaFaucXTilT7tgUZHbOE4+ypskZ1OP8CRCSDkCxG6Vya9EwaFIVagWwpaVAn5wzypaqQ==",
10814 + "version": "4.16.5",
10815 + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.16.5.tgz",
10816 + "integrity": "sha512-ArsiAQHEW2iGaqZ8fTA1nX0a+lN5mNTyuGRRO6OW3H/Yno1y9/t1f9YOI1Cfoqz63VAthn++ZYcbDP7jPflc+A==",
10817 "dev": true,
10818 "dependencies": {
10819 "esbuild": "~0.21.5",
@@ -10737,19 +10858,10 @@
10858 "node": ">= 0.8.0"
10859 }
10860 },
10740 - "node_modules/type-detect": {
10741 - "version": "4.0.8",
10742 - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
10743 - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
10744 - "dev": true,
10745 - "engines": {
10746 - "node": ">=4"
10747 - }
10748 - },
10861 "node_modules/type-fest": {
10750 - "version": "4.21.0",
10751 - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.21.0.tgz",
10752 - "integrity": "sha512-ADn2w7hVPcK6w1I0uWnM//y1rLXZhzB9mr0a3OirzclKF1Wp6VzevUmzz/NRAWunOT6E8HrnpGY7xOfc6K57fA==",
10862 + "version": "4.25.0",
10863 + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.25.0.tgz",
10864 + "integrity": "sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw==",
10865 "dev": true,
10866 "engines": {
10867 "node": ">=16"
@@ -10772,9 +10884,9 @@
10884 }
10885 },
10886 "node_modules/typescript": {
10775 - "version": "5.5.3",
10776 - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
10777 - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
10887 + "version": "5.5.4",
10888 + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
10889 + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
10890 "peer": true,
10891 "bin": {
10892 "tsc": "bin/tsc",
@@ -10790,9 +10902,9 @@
10902 "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="
10903 },
10904 "node_modules/ufo": {
10793 - "version": "1.5.3",
10794 - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz",
10795 - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==",
10905 + "version": "1.5.4",
10906 + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz",
10907 + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
10908 "dev": true
10909 },
10910 "node_modules/unconfig": {
@@ -10809,11 +10921,35 @@
10921 }
10922 },
10923 "node_modules/undici-types": {
10812 - "version": "5.26.5",
10813 - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
10814 - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
10924 + "version": "6.19.6",
10925 + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz",
10926 + "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==",
10927 "dev": true
10928 },
10929 + "node_modules/unique-filename": {
10930 + "version": "3.0.0",
10931 + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz",
10932 + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==",
10933 + "dev": true,
10934 + "dependencies": {
10935 + "unique-slug": "^4.0.0"
10936 + },
10937 + "engines": {
10938 + "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
10939 + }
10940 + },
10941 + "node_modules/unique-slug": {
10942 + "version": "4.0.0",
10943 + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz",
10944 + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==",
10945 + "dev": true,
10946 + "dependencies": {
10947 + "imurmurhash": "^0.1.4"
10948 + },
10949 + "engines": {
10950 + "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
10951 + }
10952 + },
10953 "node_modules/universalify": {
10954 "version": "2.0.1",
10955 "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
@@ -10824,36 +10960,36 @@
10960 }
10961 },
10962 "node_modules/unplugin": {
10827 - "version": "1.11.0",
10828 - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.11.0.tgz",
10829 - "integrity": "sha512-3r7VWZ/webh0SGgJScpWl2/MRCZK5d3ZYFcNaeci/GQ7Teop7zf0Nl2pUuz7G21BwPd9pcUPOC5KmJ2L3WgC5g==",
10963 + "version": "1.12.1",
10964 + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.12.1.tgz",
10965 + "integrity": "sha512-aXEH9c5qi3uYZHo0niUtxDlT9ylG/luMW/dZslSCkbtC31wCyFkmM0kyoBBh+Grhn7CL+/kvKLfN61/EdxPxMQ==",
10966 "dev": true,
10967 "dependencies": {
10832 - "acorn": "^8.11.3",
10968 + "acorn": "^8.12.1",
10969 "chokidar": "^3.6.0",
10970 "webpack-sources": "^3.2.3",
10835 - "webpack-virtual-modules": "^0.6.1"
10971 + "webpack-virtual-modules": "^0.6.2"
10972 },
10973 "engines": {
10974 "node": ">=14.0.0"
10975 }
10976 },
10977 "node_modules/unplugin-vue-components": {
10842 - "version": "0.27.2",
10843 - "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.27.2.tgz",
10844 - "integrity": "sha512-YifnsmslMRNt+JRQiCG4ZX1+xUQuubUZm76K7Qtg8dmchZJkHIDxZSyfZb5/jqrLWMTm/TUjGJ3ZDlzO6SFnSQ==",
10978 + "version": "0.27.4",
10979 + "resolved": "https://registry.npmjs.org/unplugin-vue-components/-/unplugin-vue-components-0.27.4.tgz",
10980 + "integrity": "sha512-1XVl5iXG7P1UrOMnaj2ogYa5YTq8aoh5jwDPQhemwO/OrXW+lPQKDXd1hMz15qxQPxgb/XXlbgo3HQ2rLEbmXQ==",
10981 "dev": true,
10982 "dependencies": {
10983 "@antfu/utils": "^0.7.10",
10984 "@rollup/pluginutils": "^5.1.0",
10985 "chokidar": "^3.6.0",
10850 - "debug": "^4.3.5",
10986 + "debug": "^4.3.6",
10987 "fast-glob": "^3.3.2",
10988 "local-pkg": "^0.5.0",
10853 - "magic-string": "^0.30.10",
10989 + "magic-string": "^0.30.11",
10990 "minimatch": "^9.0.5",
10991 "mlly": "^1.7.1",
10856 - "unplugin": "^1.10.1"
10992 + "unplugin": "^1.12.1"
10993 },
10994 "engines": {
10995 "node": ">=14"
@@ -10961,6 +11097,15 @@
11097 "uuid": "dist/bin/uuid"
11098 }
11099 },
11100 + "node_modules/validate-npm-package-name": {
11101 + "version": "5.0.1",
11102 + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz",
11103 + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==",
11104 + "dev": true,
11105 + "engines": {
11106 + "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
11107 + }
11108 + },
11109 "node_modules/validator": {
11110 "version": "13.12.0",
11111 "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
@@ -11004,14 +11149,14 @@
11149 }
11150 },
11151 "node_modules/vite": {
11007 - "version": "5.3.3",
11008 - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
11009 - "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
11152 + "version": "5.4.2",
11153 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
11154 + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
11155 "dev": true,
11156 "dependencies": {
11157 "esbuild": "^0.21.3",
11013 - "postcss": "^8.4.39",
11014 - "rollup": "^4.13.0"
11158 + "postcss": "^8.4.41",
11159 + "rollup": "^4.20.0"
11160 },
11161 "bin": {
11162 "vite": "bin/vite.js"
@@ -11030,6 +11175,7 @@
11175 "less": "*",
11176 "lightningcss": "^1.21.0",
11177 "sass": "*",
11178 + "sass-embedded": "*",
11179 "stylus": "*",
11180 "sugarss": "*",
11181 "terser": "^5.4.0"
@@ -11047,6 +11193,9 @@
11193 "sass": {
11194 "optional": true
11195 },
11196 + "sass-embedded": {
11197 + "optional": true
11198 + },
11199 "stylus": {
11200 "optional": true
11201 },
@@ -11059,13 +11208,10 @@
11208 }
11209 },
11210 "node_modules/vite-bundle-analyzer": {
11062 - "version": "0.10.3",
11063 - "resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-0.10.3.tgz",
11064 - "integrity": "sha512-Q5ko1EOu808q0mVv//H8OYd7yrijfEPUBlsg2NVx0rvbZ0ncUbnDh7UYqRmZWxaRDj0is4anE9DXatTiwZ1ZLg==",
11065 - "dev": true,
11066 - "dependencies": {
11067 - "picocolors": "^1.0.0"
11068 - }
11211 + "version": "0.10.6",
11212 + "resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-0.10.6.tgz",
11213 + "integrity": "sha512-w/5wvRZeZo2lKdJzRGCTn4YW/mT++fKeii2PQPK3odblfFqsODYHF72V+QTg1Xznbxy543N0WfT5vPiSosVWxA==",
11214 + "dev": true
11215 },
11216 "node_modules/vite-bundle-visualizer": {
11217 "version": "1.2.1",
@@ -11098,9 +11244,9 @@
11244 }
11245 },
11246 "node_modules/vite-node": {
11101 - "version": "2.0.2",
11102 - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.2.tgz",
11103 - "integrity": "sha512-w4vkSz1Wo+NIQg8pjlEn0jQbcM/0D+xVaYjhw3cvarTanLLBh54oNiRbsT8PNK5GfuST0IlVXjsNRoNlqvY/fw==",
11247 + "version": "2.0.5",
11248 + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz",
11249 + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==",
11250 "dev": true,
11251 "dependencies": {
11252 "cac": "^6.7.14",
@@ -11120,19 +11266,19 @@
11266 }
11267 },
11268 "node_modules/vite-plugin-inspect": {
11123 - "version": "0.8.4",
11124 - "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.4.tgz",
11125 - "integrity": "sha512-G0N3rjfw+AiiwnGw50KlObIHYWfulVwaCBUBLh2xTW9G1eM9ocE5olXkEYUbwyTmX+azM8duubi+9w5awdCz+g==",
11269 + "version": "0.8.5",
11270 + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.5.tgz",
11271 + "integrity": "sha512-JvTUqsP1JNDw0lMZ5Z/r5cSj81VK2B7884LO1DC3GMBhdcjcsAnJjdWq7bzQL01Xbh+v60d3lju3g+z7eAtNew==",
11272 "dev": true,
11273 "dependencies": {
11128 - "@antfu/utils": "^0.7.7",
11274 + "@antfu/utils": "^0.7.10",
11275 "@rollup/pluginutils": "^5.1.0",
11130 - "debug": "^4.3.4",
11131 - "error-stack-parser-es": "^0.1.1",
11276 + "debug": "^4.3.5",
11277 + "error-stack-parser-es": "^0.1.4",
11278 "fs-extra": "^11.2.0",
11279 "open": "^10.1.0",
11280 "perfect-debounce": "^1.0.0",
11135 - "picocolors": "^1.0.0",
11281 + "picocolors": "^1.0.1",
11282 "sirv": "^2.0.4"
11283 },
11284 "engines": {
@@ -11196,18 +11342,18 @@
11342 }
11343 },
11344 "node_modules/vite-plugin-vue-devtools": {
11199 - "version": "7.3.5",
11200 - "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.3.5.tgz",
11201 - "integrity": "sha512-6omLXTfYu0bmSmncPSbj4mdMPB3t5dAZkUyriJikahGEnvv5gynHlydDsJShHT6l/5dCkvmSesSji/2a6FfutQ==",
11345 + "version": "7.3.9",
11346 + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.3.9.tgz",
11347 + "integrity": "sha512-ybDV2kepW0NpusvtfbRKHs0pvyrReNcFtL572gyZ6Alox6u5uebYefd2eAG/7mJSU3NPI5UxUH1e/Mof5exdlw==",
11348 "dev": true,
11349 "dependencies": {
11204 - "@vue/devtools-core": "^7.3.5",
11205 - "@vue/devtools-kit": "^7.3.5",
11206 - "@vue/devtools-shared": "^7.3.5",
11350 + "@vue/devtools-core": "^7.3.9",
11351 + "@vue/devtools-kit": "^7.3.9",
11352 + "@vue/devtools-shared": "^7.3.9",
11353 "execa": "^8.0.1",
11354 "sirv": "^2.0.4",
11209 - "vite-plugin-inspect": "^0.8.4",
11210 - "vite-plugin-vue-inspector": "^5.1.2"
11355 + "vite-plugin-inspect": "^0.8.5",
11356 + "vite-plugin-vue-inspector": "^5.1.3"
11357 },
11358 "engines": {
11359 "node": ">=v14.21.3"
@@ -11351,9 +11497,9 @@
11497 }
11498 },
11499 "node_modules/vite-plugin-vue-inspector": {
11354 - "version": "5.1.2",
11355 - "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.1.2.tgz",
11356 - "integrity": "sha512-M+yH2LlQtVNzJAljQM+61CqDXBvHim8dU5ImGaQuwlo13tMDHue5D7IC20YwDJuWDODiYc/cZBUYspVlyPf2vQ==",
11500 + "version": "5.1.3",
11501 + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.1.3.tgz",
11502 + "integrity": "sha512-pMrseXIDP1Gb38mOevY+BvtNGNqiqmqa2pKB99lnLsADQww9w9xMbAfT4GB6RUoaOkSPrtlXqpq2Fq+Dj2AgFg==",
11503 "dev": true,
11504 "dependencies": {
11505 "@babel/core": "^7.23.0",
@@ -11383,18 +11529,18 @@
11529 }
11530 },
11531 "node_modules/vitest": {
11386 - "version": "2.0.2",
11387 - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.2.tgz",
11388 - "integrity": "sha512-WlpZ9neRIjNBIOQwBYfBSr0+of5ZCbxT2TVGKW4Lv0c8+srCFIiRdsP7U009t8mMn821HQ4XKgkx5dVWpyoyLw==",
11532 + "version": "2.0.5",
11533 + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz",
11534 + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==",
11535 "dev": true,
11536 "dependencies": {
11537 "@ampproject/remapping": "^2.3.0",
11392 - "@vitest/expect": "2.0.2",
11393 - "@vitest/pretty-format": "^2.0.2",
11394 - "@vitest/runner": "2.0.2",
11395 - "@vitest/snapshot": "2.0.2",
11396 - "@vitest/spy": "2.0.2",
11397 - "@vitest/utils": "2.0.2",
11538 + "@vitest/expect": "2.0.5",
11539 + "@vitest/pretty-format": "^2.0.5",
11540 + "@vitest/runner": "2.0.5",
11541 + "@vitest/snapshot": "2.0.5",
11542 + "@vitest/spy": "2.0.5",
11543 + "@vitest/utils": "2.0.5",
11544 "chai": "^5.1.1",
11545 "debug": "^4.3.5",
11546 "execa": "^8.0.1",
@@ -11405,8 +11551,8 @@
11551 "tinypool": "^1.0.0",
11552 "tinyrainbow": "^1.2.0",
11553 "vite": "^5.0.0",
11408 - "vite-node": "2.0.2",
11409 - "why-is-node-running": "^2.2.2"
11554 + "vite-node": "2.0.5",
11555 + "why-is-node-running": "^2.3.0"
11556 },
11557 "bin": {
11558 "vitest": "vitest.mjs"
@@ -11420,8 +11566,8 @@
11566 "peerDependencies": {
11567 "@edge-runtime/vm": "*",
11568 "@types/node": "^18.0.0 || >=20.0.0",
11423 - "@vitest/browser": "2.0.2",
11424 - "@vitest/ui": "2.0.2",
11569 + "@vitest/browser": "2.0.5",
11570 + "@vitest/ui": "2.0.5",
11571 "happy-dom": "*",
11572 "jsdom": "*"
11573 },
@@ -11598,15 +11744,15 @@
11744 "dev": true
11745 },
11746 "node_modules/vue": {
11601 - "version": "3.4.31",
11602 - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.31.tgz",
11603 - "integrity": "sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==",
11747 + "version": "3.4.38",
11748 + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.38.tgz",
11749 + "integrity": "sha512-f0ZgN+mZ5KFgVv9wz0f4OgVKukoXtS3nwET4c2vLBGQR50aI8G0cqbFtLlX9Yiyg3LFGBitruPHt2PxwTduJEw==",
11750 "dependencies": {
11605 - "@vue/compiler-dom": "3.4.31",
11606 - "@vue/compiler-sfc": "3.4.31",
11607 - "@vue/runtime-dom": "3.4.31",
11608 - "@vue/server-renderer": "3.4.31",
11609 - "@vue/shared": "3.4.31"
11751 + "@vue/compiler-dom": "3.4.38",
11752 + "@vue/compiler-sfc": "3.4.38",
11753 + "@vue/runtime-dom": "3.4.38",
11754 + "@vue/server-renderer": "3.4.38",
11755 + "@vue/shared": "3.4.38"
11756 },
11757 "peerDependencies": {
11758 "typescript": "*"
@@ -11635,9 +11781,9 @@
11781 }
11782 },
11783 "node_modules/vue-component-type-helpers": {
11638 - "version": "2.0.26",
11639 - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.26.tgz",
11640 - "integrity": "sha512-sO9qQ8oC520SW6kqlls0iqDak53gsTVSrYylajgjmkt1c0vcgjsGSy1KzlDrbEx8pm02IEYhlUkU5hCYf8rwtg==",
11784 + "version": "2.0.29",
11785 + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.29.tgz",
11786 + "integrity": "sha512-58i+ZhUAUpwQ+9h5Hck0D+jr1qbYl4voRt5KffBx8qzELViQ4XdT/Tuo+mzq8u63teAG8K0lLaOiL5ofqW38rg==",
11787 "dev": true
11788 },
11789 "node_modules/vue-eslint-parser": {
@@ -11706,12 +11852,12 @@
11852 }
11853 },
11854 "node_modules/vue-i18n": {
11709 - "version": "9.13.1",
11710 - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.13.1.tgz",
11711 - "integrity": "sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==",
11855 + "version": "9.14.0",
11856 + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.0.tgz",
11857 + "integrity": "sha512-LxmpRuCt2rI8gqU+kxeflRZMQn4D5+4M3oP3PWZdowW/ePJraHqhF7p4CuaME52mUxdw3Mmy2yAUKgfZYgCRjA==",
11858 "dependencies": {
11713 - "@intlify/core-base": "9.13.1",
11714 - "@intlify/shared": "9.13.1",
11859 + "@intlify/core-base": "9.14.0",
11860 + "@intlify/shared": "9.14.0",
11861 "@vue/devtools-api": "^6.5.0"
11862 },
11863 "engines": {
@@ -11725,11 +11871,11 @@
11871 }
11872 },
11873 "node_modules/vue-router": {
11728 - "version": "4.4.0",
11729 - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.0.tgz",
11730 - "integrity": "sha512-HB+t2p611aIZraV2aPSRNXf0Z/oLZFrlygJm+sZbdJaW6lcFqEDQwnzUBXn+DApw+/QzDU/I9TeWx9izEjTmsA==",
11874 + "version": "4.4.3",
11875 + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.3.tgz",
11876 + "integrity": "sha512-sv6wmNKx2j3aqJQDMxLFzs/u/mjA9Z5LCgy6BE0f7yFWMjrPLnS/sPNn8ARY/FXw6byV18EFutn5lTO6+UsV5A==",
11877 "dependencies": {
11732 - "@vue/devtools-api": "^6.5.1"
11878 + "@vue/devtools-api": "^6.6.3"
11879 },
11880 "funding": {
11881 "url": "https://github.com/sponsors/posva"
@@ -11746,24 +11892,14 @@
11892 "vue": "^3.3.4"
11893 }
11894 },
11749 - "node_modules/vue-template-compiler": {
11750 - "version": "2.7.16",
11751 - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
11752 - "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
11753 - "dev": true,
11754 - "dependencies": {
11755 - "de-indent": "^1.0.2",
11756 - "he": "^1.2.0"
11757 - }
11758 - },
11895 "node_modules/vue-tsc": {
11760 - "version": "2.0.26",
11761 - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.26.tgz",
11762 - "integrity": "sha512-tOhuwy2bIXbMhz82ef37qeiaQHMXKQkD6mOF6CCPl3/uYtST3l6fdNyfMxipudrQTxTfXVPlgJdMENBFfC1CfQ==",
11896 + "version": "2.0.29",
11897 + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.29.tgz",
11898 + "integrity": "sha512-MHhsfyxO3mYShZCGYNziSbc63x7cQ5g9kvijV7dRe1TTXBRLxXyL0FnXWpUF1xII2mJ86mwYpYsUmMwkmerq7Q==",
11899 "dev": true,
11900 "dependencies": {
11765 - "@volar/typescript": "~2.4.0-alpha.15",
11766 - "@vue/language-core": "2.0.26",
11901 + "@volar/typescript": "~2.4.0-alpha.18",
11902 + "@vue/language-core": "2.0.29",
11903 "semver": "^7.5.4"
11904 },
11905 "bin": {
@@ -11783,9 +11919,9 @@
11919 }
11920 },
11921 "node_modules/vue3-marquee": {
11786 - "version": "4.2.0",
11787 - "resolved": "https://registry.npmjs.org/vue3-marquee/-/vue3-marquee-4.2.0.tgz",
11788 - "integrity": "sha512-Ij4iyvHZjTHMrsPqblpJjq6TYbEFq3utihUllAOfuy0E6W2QKX1vX4E7e7qV/ShmHbCbG0pljcEDq3y0FtzBGw==",
11922 + "version": "4.2.1",
11923 + "resolved": "https://registry.npmjs.org/vue3-marquee/-/vue3-marquee-4.2.1.tgz",
11924 + "integrity": "sha512-3nJs2ZNnEre9qZapeRcJ8WUrhE7Z8dm9pH7jR5TYBLmGre46w6vLCht6kcNruOO/c50wuYCx73wQCaXJ0Y/Kvg==",
11925 "engines": {
11926 "node": ">=12"
11927 },
@@ -12144,9 +12280,9 @@
12280 "dev": true
12281 },
12282 "node_modules/yaml": {
12147 - "version": "2.4.5",
12148 - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz",
12149 - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==",
12283 + "version": "2.5.0",
12284 + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
12285 + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
12286 "dev": true,
12287 "bin": {
12288 "yaml": "bin.mjs"
frontend/package.json
+45 -43
@@ -32,41 +32,42 @@
32 "dependencies": {
33 "@ajoelp/json-to-formdata": "^1.5.0",
34 "@f3ve/vue-markdown-it": "^0.2.2",
35 - "@fontsource/jetbrains-mono": "^5.0.20",
36 - "@fontsource/lexend": "^5.0.20",
35 + "@fontsource/jetbrains-mono": "^5.0.21",
36 + "@fontsource/lexend": "^5.0.21",
37 "@fontsource/public-sans": "^5.0.18",
38 "@popperjs/core": "^2.11.8",
39 - "@shikijs/markdown-it": "^1.10.3",
40 - "@vueuse/components": "^10.11.0",
41 - "@vueuse/core": "^10.11.0",
42 - "apexcharts": "^3.50.0",
39 + "@shikijs/markdown-it": "^1.14.1",
40 + "@vueuse/components": "^11.0.3",
41 + "@vueuse/core": "^11.0.3",
42 + "apexcharts": "^3.52.0",
43 + "axios": "^1.7.5",
44 "bytes": "^3.1.2",
45 "colord": "^2.9.3",
46 "crypto-js": "^4.2.0",
46 - "dayjs": "^1.11.11",
47 + "dayjs": "^1.11.13",
48 "detect-touch-device": "^1.1.6",
49 "echarts": "^5.5.1",
50 "file-saver": "^2.0.5",
51 "html-entities": "^2.5.2",
51 - "jose": "^5.6.3",
52 + "jose": "^5.8.0",
53 "js-md5": "^0.8.3",
54 "lodash": "^4.17.21",
55 "mitt": "^3.0.1",
55 - "naive-ui": "^2.38.2",
56 + "naive-ui": "^2.39.0",
57 "password-validator": "^5.3.0",
57 - "pinia": "^2.1.7",
58 + "pinia": "^2.2.2",
59 "pinia-plugin-persistedstate": "^3.2.1",
60 "secure-ls": "^2.0.0",
60 - "shiki": "^1.10.3",
61 + "shiki": "^1.14.1",
62 "validator": "^13.12.0",
62 - "vue": "^3.4.31",
63 + "vue": "^3.4.38",
64 "vue-advanced-cropper": "^2.8.9",
65 "vue-highlight-words": "^3.0.1",
65 - "vue-i18n": "^9.13.1",
66 - "vue-router": "^4.4.0",
66 + "vue-i18n": "^9.14.0",
67 + "vue-router": "^4.4.3",
68 "vue-sjv": "^0.0.6",
69 "vue3-apexcharts": "^1.5.3",
69 - "vue3-marquee": "^4.2.0",
70 + "vue3-marquee": "^4.2.1",
71 "vuedraggable": "^4.1.0"
72 },
73 "devDependencies": {
@@ -78,60 +79,61 @@
79 "@types/fs-extra": "^11.0.4",
80 "@types/inquirer": "^9.0.7",
81 "@types/jsdom": "^21.1.7",
81 - "@types/lodash": "^4.17.6",
82 - "@types/node": "^20.14.10",
83 - "@types/validator": "^13.12.0",
84 - "@vitejs/plugin-vue": "^5.0.5",
85 - "@vitejs/plugin-vue-jsx": "^4.0.0",
82 + "@types/lodash": "^4.17.7",
83 + "@types/node": "^22.5.0",
84 + "@types/validator": "^13.12.1",
85 + "@vitejs/plugin-vue": "^5.1.2",
86 + "@vitejs/plugin-vue-jsx": "^4.0.1",
87 "@vue/eslint-config-prettier": "^9.0.0",
88 "@vue/eslint-config-typescript": "^13.0.0",
89 "@vue/test-utils": "^2.4.6",
90 "@vue/tsconfig": "^0.5.1",
90 - "autoprefixer": "^10.4.19",
91 - "cypress": "^13.13.0",
92 - "eslint": "^9.6.0",
93 - "eslint-plugin-cypress": "^3.3.0",
91 + "autoprefixer": "^10.4.20",
92 + "cypress": "^13.13.3",
93 + "eslint": "^9.9.1",
94 + "eslint-plugin-cypress": "^3.5.0",
95 "eslint-plugin-vue": "^9.27.0",
96 "flourite": "^1.3.0",
97 "fs-extra": "^11.2.0",
97 - "globals": "^15.8.0",
98 - "jsdom": "^24.1.0",
98 + "globals": "^15.9.0",
99 + "jsdom": "^25.0.0",
100 "json5": "^2.2.3",
101 "npm-run-all2": "^6.2.2",
102 "picocolors": "^1.0.1",
102 - "postcss": "^8.4.39",
103 - "prettier": "^3.3.2",
104 - "sass": "^1.77.7",
105 - "start-server-and-test": "^2.0.4",
103 + "postcss": "^8.4.41",
104 + "prettier": "^3.3.3",
105 + "sass": "^1.77.8",
106 + "start-server-and-test": "^2.0.5",
107 "tailwind-config-viewer": "^2.0.4",
107 - "tailwindcss": "^3.4.4",
108 - "taze": "^0.14.2",
109 - "type-fest": "^4.21.0",
110 - "unplugin-vue-components": "^0.27.2",
111 - "vite": "^5.3.3",
112 - "vite-bundle-analyzer": "^0.10.3",
108 + "tailwindcss": "^3.4.10",
109 + "taze": "^0.16.7",
110 + "type-fest": "^4.25.0",
111 + "unplugin-vue-components": "^0.27.4",
112 + "vite": "^5.4.2",
113 + "vite-bundle-analyzer": "^0.10.6",
114 "vite-bundle-visualizer": "^1.2.1",
114 - "vite-plugin-vue-devtools": "^7.3.5",
115 + "vite-plugin-vue-devtools": "^7.3.9",
116 "vite-svg-loader": "^5.1.0",
116 - "vitest": "^2.0.2",
117 - "vue-tsc": "^2.0.26"
117 + "vitest": "^2.0.5",
118 + "vue-tsc": "^2.0.29"
119 },
120 "optionalDependencies": {
120 - "@rollup/rollup-linux-x64-gnu": "^4.18.1"
121 + "@rollup/rollup-linux-x64-gnu": "^4.21.1"
122 },
123 "overrides": {
124 "@vue/eslint-config-typescript": {
124 - "eslint": "^9.6.0"
125 + "eslint": "^9.9.1"
126 },
127 "@typescript-eslint/type-utils": {
127 - "eslint": "^9.6.0"
128 + "eslint": "^9.9.1"
129 },
130 "@typescript-eslint/utils": {
130 - "eslint": "^9.6.0"
131 + "eslint": "^9.9.1"
132 },
133 "secure-ls": {
134 "crypto-js": "^4.2.0"
135 },
136 + "micromatch": "^4.0.8",
137 "ws": "^8.18.0"
138 },
139 "engines": {
frontend/src/api/endpoints/activeResponse.ts renamed
+1 -1
@@ -1,5 +1,5 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type { ActiveResponseDetails, SupportedActiveResponse } from "@/types/activeResponse.d"
4
5 export type InvokeRequestAction = "block" | "unblock"
frontend/src/api/endpoints/agents.ts renamed
+3 -1
@@ -1,5 +1,5 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type {
4 Agent,
5 AgentVulnerabilities,
@@ -70,12 +70,14 @@ export default {
70 },
71
72 // IGNORE AT THE MOMENT !
73 + /** @deprecated */
74 agentsWazuhOutdated() {
75 return HttpClient.get<FlaskBaseResponse & { outdated_wazuh_agents: OutdatedWazuhAgents }>(
76 `/agents/wazuh/outdated`
77 ) // Include the outdated Wazuh agents
78 },
79 // IGNORE AT THE MOMENT !
80 + /** @deprecated */
81 agentsVelociraptorOutdated() {
82 return HttpClient.get<FlaskBaseResponse & { outdated_velociraptor_agents: OutdatedVelociraptorAgents }>(
83 `/agents/velociraptor/outdated`
frontend/src/api/endpoints/alerts.ts renamed
+26 -1
@@ -1,4 +1,4 @@
1 -import { HttpClient } from "./httpClient"
1 +import { HttpClient } from "../httpClient"
2 import type { FlaskBaseResponse } from "@/types/flask.d"
3 import type {
4 AlertSourceContent,
@@ -11,6 +11,12 @@ import type {
11
12 export type AlertsQueryTimeRange = `${number}${"h" | "d" | "w"}`
13
14 +export interface GraylogAlertsQuery {
15 + size: number
16 + timerange: AlertsQueryTimeRange
17 + index_prefix: string
18 +}
19 +
20 interface AlertsQuery {
21 size: number
22 timerange: AlertsQueryTimeRange
@@ -48,7 +54,26 @@ function getQueryByFilter(filter?: AlertsSummaryQuery): AlertsQuery {
54 return query
55 }
56
57 +function getGraylogQueryByFilter(filter?: Partial<GraylogAlertsQuery>): GraylogAlertsQuery {
58 + const query: GraylogAlertsQuery = {
59 + size: filter?.size || 10,
60 + timerange: filter?.timerange || "24h",
61 + index_prefix: "gl-events*"
62 + }
63 +
64 + return query
65 +}
66 +
67 export default {
68 + getGraylogAlertsList(filter?: Partial<GraylogAlertsQuery>, signal?: AbortSignal) {
69 + const query = getGraylogQueryByFilter(filter)
70 +
71 + return HttpClient.post<FlaskBaseResponse & { alerts_summary: AlertsSummary[] }>(
72 + `/alerts/alerts/graylog`,
73 + query,
74 + signal ? { signal } : {}
75 + )
76 + },
77 getAll(filter?: AlertsSummaryQuery, signal?: AbortSignal) {
78 const query = getQueryByFilter(filter)
79
frontend/src/api/endpoints/artifacts.ts renamed
+3 -4
@@ -1,8 +1,7 @@
1 -import { HttpClient } from "./httpClient"
1 +import { HttpClient } from "../httpClient"
2 import type { FlaskBaseResponse } from "@/types/flask.d"
3 import type { Artifact, CollectResult, CommandResult, QuarantineResult, Recommendation } from "@/types/artifacts.d"
4 -import type { AlertContext } from "@/types/soc/alert"
5 -import type { OsTypesFull, OsTypesLower } from "@/types/common"
4 +import type { OsTypesFull, OsTypesLower } from "@/types/common.d"
5
6 export interface ArtifactsQuery {
7 os?: OsTypesLower
@@ -31,7 +30,7 @@ export interface QuarantineRequest {
30
31 export interface ArtifactRecommendationRequest {
32 os: OsTypesFull
34 - prompt: AlertContext
33 + prompt: string | object
34 }
35
36 export default {
frontend/src/api/endpoints/askSocfortress.ts renamed
+1 -1
@@ -1,4 +1,4 @@
1 -import { HttpClient } from "./httpClient"
1 +import { HttpClient } from "../httpClient"
2 import type { FlaskBaseResponse } from "@/types/flask.d"
3
4 export default {
frontend/src/api/endpoints/auth.ts renamed
+1 -1
@@ -1,5 +1,5 @@
1 import type { FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type { AuthUser, LoginPayload, RegisterPayload } from "@/types/auth.d"
4 import jsonToFormData from "@ajoelp/json-to-formdata"
5
frontend/src/api/endpoints/cloudSecurityAssessment.ts renamed
+1 -1
@@ -1,5 +1,5 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type {
4 ScoutSuiteAwsReportPayload,
5 ScoutSuiteAzureReportPayload,
frontend/src/api/endpoints/connectors.ts renamed
+1 -1
@@ -1,6 +1,6 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 import type { Connector, ConnectorRequestPayload } from "@/types/connectors.d"
3 -import { HttpClient } from "./httpClient"
3 +import { HttpClient } from "../httpClient"
4
5 export default {
6 getAll() {
frontend/src/api/endpoints/customers.ts renamed
+6 -1
@@ -1,5 +1,5 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type {
4 Customer,
5 CustomerAgentHealth,
@@ -26,6 +26,7 @@ export interface ProvisioningDefaultSettingsPayload {
26 }
27
28 export default {
29 + // #region Customer
30 getCustomers(code?: string) {
31 return HttpClient.get<FlaskBaseResponse & { customers?: Customer[]; customer?: Customer }>(
32 `/customers${code ? "/" + code : ""}`
@@ -95,6 +96,9 @@ export default {
96 }
97 })
98 },
99 + // #endregion
100 +
101 + // #region Customer Provisioning
102 newCustomerProvision(provision: CustomerProvision, code: string) {
103 return HttpClient.post<FlaskBaseResponse & { customer_meta: CustomerMeta; wazuh_worker_provisioned: boolean }>(
104 `/customer_provisioning/provision`,
@@ -161,4 +165,5 @@ export default {
165 wazuh_worker_hostname: payload.wazuhWorkerHostname
166 })
167 }
168 + // #endregion
169 }
frontend/src/api/endpoints/flow.ts renamed
+1 -1
@@ -1,4 +1,4 @@
1 -import { HttpClient } from "./httpClient"
1 +import { HttpClient } from "../httpClient"
2 import type { FlaskBaseResponse } from "@/types/flask.d"
3 import type { CollectResult, FlowResult } from "@/types/flow.d"
4
frontend/src/api/endpoints/graylog.ts renamed
+27 -2
@@ -1,13 +1,16 @@
1 -import { HttpClient } from "./httpClient"
1 +import { HttpClient } from "../httpClient"
2 import type { FlaskBaseResponse } from "@/types/flask.d"
3 -import type { Message, ThroughputMetric } from "@/types/graylog/index.d"
3 +import type { Message } from "@/types/graylog/messages.d"
4 import type { Alerts, AlertsQuery } from "@/types/graylog/alerts.d"
5 import type { EventDefinition } from "@/types/graylog/event-definition.d"
6 import type { Stream } from "@/types/graylog/stream.d"
7 import type { ConfiguredInput, RunningInput } from "@/types/graylog/inputs.d"
8 import type { PipelineRule, Pipeline, PipelineFull } from "@/types/graylog/pipelines.d"
9 +import type { GraylogIndex } from "@/types/graylog/indices"
10 +import type { ThroughputMetric } from "@/types/graylog/metrics.d"
11
12 export default {
13 + // #region Messages
14 getMessages(page?: number) {
15 return HttpClient.get<FlaskBaseResponse & { graylog_messages: Message[]; total_messages: number }>(
16 `/graylog/messages`,
@@ -18,6 +21,9 @@ export default {
21 }
22 )
23 },
24 + // #endregion
25 +
26 + // #region Event
27 getAlerts(query: AlertsQuery) {
28 return HttpClient.post<FlaskBaseResponse & { alerts: Alerts }>(`/graylog/event/alerts`, query)
29 },
@@ -26,6 +32,9 @@ export default {
32 `/graylog/event/definitions`
33 )
34 },
35 + // #endregion
36 +
37 + // #region Stream
38 getStreams() {
39 return HttpClient.get<FlaskBaseResponse & { streams: Stream[]; total: number }>(`/graylog/streams`)
40 },
@@ -39,6 +48,9 @@ export default {
48 stream_id: streamId
49 })
50 },
51 + // #endregion
52 +
53 + // #region Inputs
54 getInputs() {
55 return HttpClient.get<
56 FlaskBaseResponse & { configured_inputs: ConfiguredInput[]; running_inputs: RunningInput[] }
@@ -60,11 +72,17 @@ export default {
72 input_id: inputId
73 })
74 },
75 + // #endregion
76 +
77 + // #region Metrics
78 getMetrics() {
79 return HttpClient.get<
80 FlaskBaseResponse & { throughput_metrics: ThroughputMetric[]; uncommitted_journal_entries: number }
81 >(`/graylog/metrics`)
82 },
83 + // #endregion
84 +
85 + // #region Pipelines
86 getPipelines() {
87 return HttpClient.get<FlaskBaseResponse & { pipelines: Pipeline[] }>(`/graylog/pipelines`)
88 },
@@ -74,9 +92,16 @@ export default {
92 getPipelinesRules() {
93 return HttpClient.get<FlaskBaseResponse & { pipeline_rules: PipelineRule[] }>(`/graylog/pipeline/rules`)
94 },
95 + // #endregion
96 +
97 + // #region Indices
98 + getIndices() {
99 + return HttpClient.get<FlaskBaseResponse & { indices: GraylogIndex[] }>(`/graylog/indices`)
100 + },
101 deleteIndex(indexName: string) {
102 return HttpClient.delete<FlaskBaseResponse>(`/graylog/index`, {
103 data: { index_name: indexName }
104 })
105 }
106 + // #endregion
107 }
frontend/src/api/endpoints/healthchecks.ts renamed
+1 -1
@@ -1,5 +1,5 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type { InfluxDBAlert } from "@/types/healthchecks.d"
4
5 export default {
frontend/src/api/endpoints/incidentManagement.ts new
+241
@@ -0,0 +1,241 @@
1 +import { type FlaskBaseResponse } from "@/types/flask.d"
2 +import { HttpClient } from "../httpClient"
3 +import type { SourceConfiguration, SourceName } from "@/types/incidentManagement/sources.d"
4 +import type {
5 + Alert,
6 + AlertComment,
7 + AlertContext,
8 + AlertDetails,
9 + AlertStatus,
10 + AlertTag,
11 + AlertTimeline
12 +} from "@/types/incidentManagement/alerts.d"
13 +import type { Case } from "@/types/incidentManagement/cases.d"
14 +import type { IncidentNotification, IncidentNotificationPayload } from "@/types/incidentManagement/notifications.d"
15 +
16 +export type AlertsFilter =
17 + | { status: AlertStatus }
18 + | { assetName: string }
19 + | { assignedTo: string }
20 + | { tag: string }
21 + | { title: string }
22 +
23 +export interface AlertsQuery {
24 + page: number
25 + pageSize: number
26 + filters: AlertsFilter
27 +}
28 +
29 +export type CasesFilter = { status: AlertStatus } | { assignedTo: string }
30 +
31 +export type AlertCommentPayload = Omit<AlertComment, "id">
32 +
33 +export default {
34 + // #region Sources
35 + getConfiguredSources() {
36 + return HttpClient.get<FlaskBaseResponse & { sources: SourceName[] }>(
37 + `/incidents/db_operations/configured/sources`
38 + )
39 + },
40 + getAvailableMappings(indexName: string) {
41 + return HttpClient.get<FlaskBaseResponse & { available_mappings: string[] }>(
42 + `/incidents/db_operations/mappings/fields-assets-title-and-timefield`,
43 + {
44 + params: { index_name: indexName }
45 + }
46 + )
47 + },
48 + getSourceByIndex(indexName: string) {
49 + return HttpClient.get<FlaskBaseResponse & { source: SourceName }>(
50 + `/incidents/db_operations/available-source/${indexName}`
51 + )
52 + },
53 + getAvailableIndices(source: SourceName) {
54 + return HttpClient.get<FlaskBaseResponse & { indices: string[] }>(
55 + `/incidents/db_operations/available-indices/${source}`
56 + )
57 + },
58 + createSourceConfiguration(payload: SourceConfiguration) {
59 + return HttpClient.post<FlaskBaseResponse>(`/incidents/db_operations/fields-assets-title-and-timefield`, payload)
60 + },
61 + updateSourceConfiguration(payload: SourceConfiguration) {
62 + return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/fields-assets-title-and-timefield`, payload)
63 + },
64 + getSourceConfiguration(source: SourceName) {
65 + return HttpClient.get<FlaskBaseResponse & SourceConfiguration>(
66 + `/incidents/db_operations/fields-assets-title-and-timefield`,
67 + {
68 + params: { source }
69 + }
70 + )
71 + },
72 + getSocfortressRecommendsWazuh() {
73 + return HttpClient.get<FlaskBaseResponse & SourceConfiguration>(
74 + `/incidents/db_operations/socfortress/recommends/wazuh`
75 + )
76 + },
77 + deleteSourceConfiguration(source: SourceName) {
78 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/configured/sources/${source}`)
79 + },
80 + // #endregion
81 +
82 + // #region Alerts
83 + getAlertsList(args?: Partial<AlertsQuery>) {
84 + let url = `/incidents/db_operations/alerts`
85 +
86 + if (args?.filters && "status" in args.filters) {
87 + url = `/incidents/db_operations/alerts/status/${args.filters.status}`
88 + }
89 + if (args?.filters && "assetName" in args.filters) {
90 + url = `/incidents/db_operations/alerts/asset/${args.filters.assetName}`
91 + }
92 + if (args?.filters && "assignedTo" in args.filters) {
93 + url = `/incidents/db_operations/alerts/assigned-to/${args.filters.assignedTo}`
94 + }
95 + if (args?.filters && "tag" in args.filters) {
96 + url = `/incidents/db_operations/alert/tag/${args.filters.tag}`
97 + }
98 + if (args?.filters && "title" in args.filters) {
99 + url = `/incidents/db_operations/alerts/title/${args.filters.title}`
100 + }
101 +
102 + return HttpClient.get<
103 + FlaskBaseResponse & { alerts: Alert[]; closed: number; in_progress: number; open: number; total: number }
104 + >(url, {
105 + params: {
106 + page: args?.page || 1,
107 + page_size: args?.pageSize || 25
108 + }
109 + })
110 + },
111 + getAlert(alertId: number) {
112 + return HttpClient.get<FlaskBaseResponse & { alerts: Alert[] }>(`/incidents/db_operations/alert/${alertId}`)
113 + },
114 + getAlertDetails(indexId: string, indexName: string) {
115 + return HttpClient.post<FlaskBaseResponse & { alert_details: AlertDetails }>(`/incidents/alerts/alert/details`, {
116 + index_id: indexId,
117 + index_name: indexName
118 + })
119 + },
120 + getAlertTimeline(indexId: string, indexName: string) {
121 + return HttpClient.post<FlaskBaseResponse & { alert_timeline: AlertTimeline[] }>(
122 + `/incidents/alerts/alert/timeline`,
123 + {
124 + index_id: indexId,
125 + index_name: indexName
126 + }
127 + )
128 + },
129 + getAvailableUsers() {
130 + return HttpClient.get<FlaskBaseResponse & { available_users: string[] }>(
131 + `/incidents/db_operations/alert/available-users`
132 + )
133 + },
134 + updateAlertStatus(alertId: number, status: AlertStatus) {
135 + return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/alert/status`, {
136 + alert_id: alertId,
137 + status
138 + })
139 + },
140 + updateAlertAssignedUser(alertId: number, user: string) {
141 + return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/alert/assigned-to`, {
142 + alert_id: alertId,
143 + assigned_to: user
144 + })
145 + },
146 + deleteAlertTag(alertId: number, tagId: number) {
147 + return HttpClient.delete<FlaskBaseResponse & { alert_tag: AlertTag }>(`/incidents/db_operations/alert/tag`, {
148 + data: { alert_id: alertId, tag_id: tagId }
149 + })
150 + },
151 + deleteAlert(alertId: number) {
152 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/alert/${alertId}`)
153 + },
154 + getAlertContext(alertContextId: number) {
155 + return HttpClient.get<FlaskBaseResponse & { alert_context: AlertContext }>(
156 + `/incidents/db_operations/alert/context/${alertContextId}`
157 + )
158 + },
159 + newAlertComment(payload: AlertCommentPayload) {
160 + return HttpClient.post<FlaskBaseResponse & { comment: AlertComment }>(
161 + `/incidents/db_operations/alert/comment`,
162 + payload
163 + )
164 + },
165 + newAlertTag(alertId: number, tag: string) {
166 + return HttpClient.post<FlaskBaseResponse & { alert_tag: AlertTag }>(`/incidents/db_operations/alert/tag`, {
167 + alert_id: alertId,
168 + tag
169 + })
170 + },
171 + // #endregion
172 +
173 + // #region Cases
174 + getCasesList(filters?: CasesFilter) {
175 + let url = `/incidents/db_operations/cases`
176 +
177 + if (filters && "status" in filters) {
178 + url = `/incidents/db_operations/case/status/${filters.status}`
179 + }
180 + if (filters && "assignedTo" in filters) {
181 + url = `/incidents/db_operations/case/assigned-to/${filters.assignedTo}`
182 + }
183 +
184 + return HttpClient.get<FlaskBaseResponse & { cases: Case[] }>(url)
185 + },
186 + getCase(caseId: number) {
187 + return HttpClient.get<FlaskBaseResponse & { cases: Case[] }>(`/incidents/db_operations/case/${caseId}`)
188 + },
189 + createCase(alertId: number) {
190 + return HttpClient.post<FlaskBaseResponse & { case_alert_link: { case_id: number; alert_id: number } }>(
191 + `/incidents/db_operations/case/from-alert`,
192 + {
193 + alert_id: alertId
194 + }
195 + )
196 + },
197 + linkCase(alertId: number, caseId: number) {
198 + return HttpClient.post<FlaskBaseResponse & { case_alert_link: { case_id: number; alert_id: number } }>(
199 + `/incidents/db_operations/case/alert-link`,
200 + {
201 + alert_id: alertId,
202 + case_id: caseId
203 + }
204 + )
205 + },
206 + updateCaseStatus(caseId: number, status: AlertStatus) {
207 + return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/case/status`, {
208 + case_id: caseId,
209 + status
210 + })
211 + },
212 + updateCaseAssignedUser(caseId: number, user: string) {
213 + return HttpClient.put<FlaskBaseResponse>(`/incidents/db_operations/case/assigned-to`, {
214 + case_id: caseId,
215 + assigned_to: user
216 + })
217 + },
218 + deleteCase(caseId: number) {
219 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/case/${caseId}`)
220 + },
221 + // #endregion
222 +
223 + // #region Notification
224 + getNotifications(customerCode: string) {
225 + return HttpClient.get<FlaskBaseResponse & { notifications: IncidentNotification[] }>(
226 + `/incidents/db_operations/notification/${customerCode}`
227 + )
228 + },
229 + setNotification(notification: IncidentNotificationPayload) {
230 + return HttpClient.put<FlaskBaseResponse & { notifications: IncidentNotification[] }>(
231 + `/incidents/db_operations/notification`,
232 + notification,
233 + {
234 + params: {
235 + customer_code: notification.customer_code
236 + }
237 + }
238 + )
239 + }
240 + // #endregion
241 +}
frontend/src/api/endpoints/indices.ts renamed
+1 -1
@@ -1,5 +1,5 @@
1 import type { FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type { ClusterHealth, IndexStats, IndexAllocation, IndexShard } from "@/types/indices.d"
4
5 export default {
frontend/src/api/endpoints/integrations.ts renamed
+5 -1
@@ -1,5 +1,5 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type { AvailableIntegration, CustomerIntegration } from "@/types/integrations.d"
4
5 export interface NewIntegration {
@@ -21,6 +21,7 @@ export interface NewIntegrationPayload extends NewIntegration {
21 }
22
23 export default {
24 + // #region Integrations
25 getAvailableIntegrations() {
26 return HttpClient.get<FlaskBaseResponse & { available_integrations: AvailableIntegration[] }>(
27 `/integrations/available_integrations`
@@ -47,7 +48,9 @@ export default {
48 data: { customer_code: customerCode, integration_name: integrationName }
49 })
50 },
51 + // #endregion
52
53 + // #region Provision
54 office365Provision(customerCode: string, integrationName: string) {
55 return HttpClient.post<FlaskBaseResponse>(`/office365/provision`, {
56 customer_code: customerCode,
@@ -80,4 +83,5 @@ export default {
83 integration_name: integrationName
84 })
85 }
86 + // #endregion
87 }
frontend/src/api/endpoints/license.ts renamed
+3 -1
@@ -1,5 +1,5 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type {
4 CheckoutPayload,
5 License,
@@ -60,6 +60,7 @@ export default {
60 return HttpClient.post<FlaskBaseResponse & { docker_compose: string }>(`/license/retrieve-docker-compose`)
61 },
62 // TODO: remove, deprecated
63 + /** @deprecated */
64 extendLicense(period: number) {
65 return HttpClient.post<FlaskBaseResponse>(
66 `/license/extend_license`,
@@ -70,6 +71,7 @@ export default {
71 )
72 },
73 // TODO: remove, deprecated
74 + /** @deprecated */
75 createLicense({ name, email, companyName }: NewLicensePayload) {
76 return HttpClient.post<FlaskBaseResponse>(`/license/create_new_key`, {
77 product_id: 24355,
frontend/src/api/endpoints/logs.ts renamed
+2 -14
@@ -1,18 +1,6 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
3 -import type { Log, LogEventType } from "@/types/logs.d"
4 -import type { SafeAny } from "@/types/common.d"
5 -
6 -export type LogsQueryTimeRange = `${number}${"h" | "d" | "w"}`
7 -export type LogsQueryEventType = `${LogEventType}`
8 -export type LogsQuery = { userId: string } | { timeRange: LogsQueryTimeRange } | { eventType: LogsQueryEventType }
9 -
10 -// Extraction of keys from the union type LogsQuery
11 -type KeysOfLogsQuery<T> = T extends { [K in keyof T]: SafeAny } ? keyof T : never
12 -
13 -// Union of values extracted from keys
14 -export type LogsQueryTypes = KeysOfLogsQuery<LogsQuery>
15 -export type LogsQueryValues = string | LogsQueryTimeRange | LogsQueryEventType
2 +import { HttpClient } from "../httpClient"
3 +import type { Log, LogsQuery, LogsQueryTimeRange } from "@/types/logs.d"
4
5 export default {
6 getLogs(query?: LogsQuery) {
frontend/src/api/endpoints/monitoringAlerts.ts renamed
+2 -8
@@ -1,18 +1,12 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
3 -import type { AvailableMonitoringAlert, MonitoringAlert } from "@/types/monitoringAlerts.d"
2 +import { HttpClient } from "../httpClient"
3 +import type { AvailableMonitoringAlert, CustomProvisionPriority, MonitoringAlert } from "@/types/monitoringAlerts.d"
4
5 export interface ProvisionsMonitoringAlertParams {
6 searchWithinLast: number
7 executeEvery: number
8 }
9
10 -export enum CustomProvisionPriority {
11 - "LOW" = 1,
12 - "MEDIUM" = 2,
13 - "HIGH" = 3
14 -}
15 -
10 export interface CustomProvisionPayload {
11 alert_name: string
12 alert_description: string
frontend/src/api/endpoints/networkConnectors.ts renamed
+6 -2
@@ -1,6 +1,6 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
3 -import type { CustomerNetworkConnector, NetworkConnector } from "@/types/networkConnectors"
2 +import { HttpClient } from "../httpClient"
3 +import type { CustomerNetworkConnector, NetworkConnector } from "@/types/networkConnectors.d"
4
5 export interface NewNetworkConnector {
6 customer_code: string
@@ -33,6 +33,7 @@ export interface FortinetProvisionPayload extends FortinetProvision {
33 }
34
35 export default {
36 + // #region Network Connector
37 getAvailableNetworkConnectors() {
38 return HttpClient.get<FlaskBaseResponse & { network_connector_keys: NetworkConnector[] }>(
39 `/network_connectors/available_network_connectors`
@@ -65,7 +66,9 @@ export default {
66 network_connector: networkConnectorName
67 })
68 },
69 + // #endregion
70
71 + // #region Provision
72 fortinetProvision(customerCode: string, networkConnectorName: string, props: FortinetProvision) {
73 const payload: FortinetProvisionPayload = {
74 ...props,
@@ -74,4 +77,5 @@ export default {
77 }
78 return HttpClient.post<FlaskBaseResponse>(`/stack_provisioning/graylog/provision/fortinet`, payload)
79 }
80 + // #endregion
81 }
frontend/src/api/endpoints/reporting.ts renamed
+1 -1
@@ -1,5 +1,5 @@
1 import type { Dashboard, Org, Panel, PanelLink } from "@/types/reporting.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type { FlaskBaseResponse } from "@/types/flask.d"
4
5 export type PanelsLinksTimeUnit = "minutes" | "hours" | "days"
frontend/src/api/endpoints/scheduler.ts renamed
+2 -2
@@ -1,6 +1,6 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
3 -import type { Job } from "@/types/scheduler"
2 +import { HttpClient } from "../httpClient"
3 +import type { Job } from "@/types/scheduler.d"
4
5 export interface UpdateJobPayload {
6 /** minutes */
frontend/src/api/endpoints/soc.ts renamed
+43 -29
@@ -1,5 +1,5 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type { SocAlert, SocAlertCaseResponse } from "@/types/soc/alert.d"
4 import type { SocCase, SocCaseExt } from "@/types/soc/case.d"
5 import type { SocAlertAsset, SocCaseAsset, SocCaseAssetsState } from "@/types/soc/asset.d"
@@ -21,6 +21,7 @@ export interface AlertsFilter {
21 type TimeUnit = "hours" | "days" | "weeks"
22
23 export default {
24 + // #region Alerts
25 getAlerts(filters?: Partial<AlertsFilter>, signal?: AbortSignal) {
26 return HttpClient.post<FlaskBaseResponse & { alerts: SocAlert[] }>(
27 `/soc/alerts`,
@@ -72,6 +73,46 @@ export default {
73 createCase(alertId: string) {
74 return HttpClient.post<FlaskBaseResponse & { case: SocAlertCaseResponse }>(`/soc/alerts/create_case/${alertId}`)
75 },
76 + // #endregion
77 +
78 + // #region Assets
79 + getAssetsByCase(caseId: string) {
80 + return HttpClient.get<FlaskBaseResponse & { assets: SocCaseAsset[]; state: SocCaseAssetsState }>(
81 + `/soc/assets/${caseId}`
82 + )
83 + },
84 + // #endregion
85 +
86 + // #region Notes
87 + getNotesByCase(caseId: string | number, payload: { searchTerm?: string }, signal?: AbortSignal) {
88 + return HttpClient.get<FlaskBaseResponse & { notes: SocNote[] }>(`/soc/notes/${caseId}`, {
89 + params: {
90 + search_term: payload.searchTerm || "%"
91 + },
92 + signal
93 + })
94 + },
95 + createCaseNote(caseId: string | number, payload?: { title?: string; content?: string }) {
96 + return HttpClient.post<FlaskBaseResponse & { note: SocNewNote }>(`/soc/notes/${caseId}`, {
97 + note_title: payload?.title || "",
98 + note_content: payload?.content || ""
99 + })
100 + },
101 + // #endregion
102 +
103 + // #region Users
104 + getUsers() {
105 + return HttpClient.get<FlaskBaseResponse & { users: SocUser[] }>(`/soc/users`)
106 + },
107 + assignUserToAlert(alertId: string, userId: string) {
108 + return HttpClient.post<FlaskBaseResponse & { alert: SocAlert }>(`/soc/users/assign/${alertId}/${userId}`)
109 + },
110 + removeUserAlertAssign(alertId: string, userId: string) {
111 + return HttpClient.delete<FlaskBaseResponse & { alert: SocAlert }>(`/soc/users/assign/${alertId}/${userId}`)
112 + },
113 + // #endregion
114 +
115 + // #region Cases
116 getCases(payload?: string | CasesFilter) {
117 let apiMethod: "get" | "post" = "get"
118 let url = `/soc/cases`
@@ -113,34 +154,6 @@ export default {
154 }
155 )
156 },
116 - getAssetsByCase(caseId: string) {
117 - return HttpClient.get<FlaskBaseResponse & { assets: SocCaseAsset[]; state: SocCaseAssetsState }>(
118 - `/soc/assets/${caseId}`
119 - )
120 - },
121 - getNotesByCase(caseId: string | number, payload: { searchTerm?: string }, signal?: AbortSignal) {
122 - return HttpClient.get<FlaskBaseResponse & { notes: SocNote[] }>(`/soc/notes/${caseId}`, {
123 - params: {
124 - search_term: payload.searchTerm || "%"
125 - },
126 - signal
127 - })
128 - },
129 - createCaseNote(caseId: string | number, payload?: { title?: string; content?: string }) {
130 - return HttpClient.post<FlaskBaseResponse & { note: SocNewNote }>(`/soc/notes/${caseId}`, {
131 - note_title: payload?.title || "",
132 - note_content: payload?.content || ""
133 - })
134 - },
135 - getUsers() {
136 - return HttpClient.get<FlaskBaseResponse & { users: SocUser[] }>(`/soc/users`)
137 - },
138 - assignUserToAlert(alertId: string, userId: string) {
139 - return HttpClient.post<FlaskBaseResponse & { alert: SocAlert }>(`/soc/users/assign/${alertId}/${userId}`)
140 - },
141 - removeUserAlertAssign(alertId: string, userId: string) {
142 - return HttpClient.delete<FlaskBaseResponse & { alert: SocAlert }>(`/soc/users/assign/${alertId}/${userId}`)
143 - },
157 closeCase(caseId: string) {
158 return HttpClient.put<FlaskBaseResponse & { case: SocAlertCaseResponse }>(`/soc/cases/close/${caseId}`)
159 },
@@ -153,4 +166,5 @@ export default {
166 purgeAllCases() {
167 return HttpClient.delete<FlaskBaseResponse>(`/soc/cases/purge`)
168 }
169 + // #endregion
170 }
frontend/src/api/endpoints/stackProvisioning.ts renamed
+1 -1
@@ -1,5 +1,5 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type { AvailableContentPack } from "@/types/stackProvisioning.d"
4
5 export default {
frontend/src/api/endpoints/threatIntel.ts renamed
+1 -1
@@ -1,4 +1,4 @@
1 -import { HttpClient } from "./httpClient"
1 +import { HttpClient } from "../httpClient"
2 import type { FlaskBaseResponse } from "@/types/flask.d"
3 import type { EpssScore, EvaluationData, ThreatIntelResponse } from "@/types/threatIntel.d"
4
frontend/src/api/endpoints/webVulnerabilityAssessment.ts renamed
+1 -1
@@ -1,5 +1,5 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 -import { HttpClient } from "./httpClient"
2 +import { HttpClient } from "../httpClient"
3 import type { NucleiReport, ScanHostPayload } from "@/types/webVulnerabilityAssessment.d"
4
5 export default {
frontend/src/api/index.ts
+27 -25
@@ -1,27 +1,28 @@
1 -import agents from "./agents"
2 -import alerts from "./alerts"
3 -import artifacts from "./artifacts"
4 -import auth from "./auth"
5 -import connectors from "./connectors"
6 -import graylog from "./graylog"
7 -import indices from "./indices"
8 -import soc from "./soc"
9 -import healthchecks from "./healthchecks"
10 -import threatIntel from "./threatIntel"
11 -import askSocfortress from "./askSocfortress"
12 -import customers from "./customers"
13 -import logs from "./logs"
14 -import flow from "./flow"
15 -import integrations from "./integrations"
16 -import monitoringAlerts from "./monitoringAlerts"
17 -import activeResponse from "./activeResponse"
18 -import stackProvisioning from "./stackProvisioning"
19 -import reporting from "./reporting"
20 -import license from "./license"
21 -import scheduler from "./scheduler"
22 -import networkConnectors from "./networkConnectors"
23 -import cloudSecurityAssessment from "./cloudSecurityAssessment"
24 -import webVulnerabilityAssessment from "./webVulnerabilityAssessment"
1 +import agents from "./endpoints/agents"
2 +import alerts from "./endpoints/alerts"
3 +import artifacts from "./endpoints/artifacts"
4 +import auth from "./endpoints/auth"
5 +import connectors from "./endpoints/connectors"
6 +import graylog from "./endpoints/graylog"
7 +import indices from "./endpoints/indices"
8 +import soc from "./endpoints/soc"
9 +import healthchecks from "./endpoints/healthchecks"
10 +import threatIntel from "./endpoints/threatIntel"
11 +import askSocfortress from "./endpoints/askSocfortress"
12 +import customers from "./endpoints/customers"
13 +import logs from "./endpoints/logs"
14 +import flow from "./endpoints/flow"
15 +import integrations from "./endpoints/integrations"
16 +import monitoringAlerts from "./endpoints/monitoringAlerts"
17 +import activeResponse from "./endpoints/activeResponse"
18 +import stackProvisioning from "./endpoints/stackProvisioning"
19 +import reporting from "./endpoints/reporting"
20 +import license from "./endpoints/license"
21 +import scheduler from "./endpoints/scheduler"
22 +import networkConnectors from "./endpoints/networkConnectors"
23 +import cloudSecurityAssessment from "./endpoints/cloudSecurityAssessment"
24 +import webVulnerabilityAssessment from "./endpoints/webVulnerabilityAssessment"
25 +import incidentManagement from "./endpoints/incidentManagement"
26
27 export default {
28 agents,
@@ -47,5 +48,6 @@ export default {
48 scheduler,
49 networkConnectors,
50 cloudSecurityAssessment,
50 - webVulnerabilityAssessment
51 + webVulnerabilityAssessment,
52 + incidentManagement
53 }
frontend/src/assets/icons/alert-settings-icon.svg new
+6
@@ -0,0 +1,6 @@
1 +<svg width="24" height="24" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
2 + <path id="Percorso" fill="none" stroke="currentColor" stroke-width="7" stroke-linejoin="round" d="M 68.567665 43.250656 L 50.283127 12.225441 C 49.26506 10.499969 46.749729 10.517128 45.754795 12.256355 L 2.492177 87.871727 C 1.497366 89.61084 2.769681 91.765846 4.78288 91.752052 L 48.279461 91.752052"/>
3 + <path id="path1" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round" d="M 48.453129 74.678711 L 48.406059 74.679123"/>
4 + <path id="path2" fill="none" stroke="currentColor" stroke-width="7" stroke-linecap="round" stroke-linejoin="round" d="M 48.279461 63.363575 L 48.279865 40.032139"/>
5 + <path id="path3" fill="currentColor" fill-rule="evenodd" stroke="none" d="M 74.265823 51.852451 C 73.775658 52.338959 73.775658 53.121677 73.775658 54.687527 L 73.775658 56.174652 C 73.775658 57.224365 73.775658 57.749012 73.549637 58.135708 C 73.324036 58.521591 72.775032 58.833626 71.67704 59.457287 C 71.360954 59.636627 71.05217 59.828156 70.750282 60.03022 C 69.596283 60.804417 69.019691 61.191113 68.527908 61.202473 C 68.03653 61.213436 67.537842 60.910736 66.539658 60.305332 L 65.790215 59.850468 C 64.381805 58.995926 63.678211 58.568665 62.984756 58.752872 C 62.291718 58.936691 61.896912 59.655701 61.106888 61.094131 L 59.376701 64.243683 C 58.655254 65.557953 58.294128 66.214478 58.466984 66.8564 C 58.639843 67.497498 59.282974 67.887848 60.569241 68.668533 L 62.184597 69.648056 C 63.054142 70.175949 63.488724 70.439697 63.702965 70.823547 C 63.916798 71.208214 63.910309 71.795761 63.896515 72.971664 L 63.89529 73.164406 C 63.89529 73.500778 63.905849 73.834724 63.92532 74.166237 C 64.001198 75.413147 64.038933 76.0364 63.823883 76.445007 C 63.60923 76.852798 63.148285 77.13237 62.227604 77.691513 L 60.568439 78.698212 C 59.282566 79.478905 58.639431 79.868843 58.466167 80.510361 C 58.29372 81.151459 58.654041 81.808403 59.375889 83.122261 L 61.106075 86.271805 C 61.896099 87.710243 62.291306 88.429245 62.984356 88.613464 C 63.677395 88.797279 64.381401 88.370415 65.789406 87.515877 L 67.365387 86.559898 C 68.305542 85.988991 68.775826 85.703735 69.243668 85.702515 C 69.711113 85.700485 70.273506 86.036865 71.399094 86.708809 C 71.490791 86.764 71.583305 86.817963 71.67704 86.871117 C 72.775032 87.495178 73.324036 87.806404 73.549637 88.1931 C 73.775658 88.579796 73.775658 89.104446 73.775658 90.153748 L 73.775658 92.678413 C 73.775658 94.244263 73.775658 95.026978 74.265823 95.513489 C 74.755569 96 75.54438 96 77.121986 96 L 80.305611 96 C 81.883232 96 82.67244 96 83.161797 95.513489 C 83.651955 95.026978 83.651955 94.244263 83.651955 92.678413 L 83.651955 90.149696 C 83.651955 89.100792 83.651955 88.576141 83.87796 88.18985 C 84.103569 87.803558 84.652168 87.491936 85.749344 86.867867 C 85.840645 86.815529 85.931534 86.763184 86.021622 86.708809 C 87.146797 86.036865 87.70919 85.70089 88.177444 85.70211 C 88.644882 85.703735 89.115166 85.988991 90.055725 86.559494 L 91.632523 87.516281 C 93.04052 88.370827 93.744522 88.797684 94.437576 88.613464 C 95.130211 88.429657 95.525017 87.710648 96.315041 86.272614 L 98.046028 83.122673 C 98.767479 81.809212 99.128212 81.151871 98.955353 80.510757 C 98.782494 79.869247 98.139366 79.478905 96.853088 78.698616 L 95.192703 77.691109 C 94.272026 77.13237 93.811073 76.852386 93.59642 76.444595 C 93.381775 76.035995 93.41951 75.412743 93.49498 74.165825 C 93.514862 73.834724 93.525002 73.500778 93.525002 73.164406 L 93.523788 72.973694 C 93.510406 71.798195 93.503502 71.209831 93.718147 70.825989 C 93.931992 70.442131 94.36657 70.178383 95.236115 69.650894 L 96.854713 68.668533 C 98.140976 67.887848 98.784119 67.497498 98.957382 66.8564 C 99.129822 66.214882 98.769508 65.557953 98.047653 64.24408 L 96.317078 61.094543 C 95.527054 59.656109 95.132248 58.937092 94.439194 58.752872 C 93.746147 58.569065 93.042152 58.995926 91.634148 59.850468 L 90.882675 60.306545 C 89.884094 60.91235 89.384995 61.21505 88.893616 61.20369 C 88.402237 61.192738 87.825241 60.805641 86.671654 60.031033 C 86.370979 59.829773 86.063416 59.639469 85.749344 59.460938 C 84.652168 58.836868 84.103569 58.524837 83.87796 58.13855 C 83.651955 57.752262 83.651955 57.227604 83.651955 56.178707 L 83.651955 54.687527 C 83.651955 53.121677 83.651955 52.338959 83.161797 51.852451 C 82.67244 51.36594 81.883232 51.36594 80.305611 51.36594 L 77.121986 51.36594 C 75.54438 51.36594 74.755569 51.36594 74.265823 51.852451 Z M 78.629311 83.265717 C 85.086212 83.265717 88.314659 77.592979 88.314659 73.580376 C 88.314659 69.567368 85.086212 63.895035 78.629311 63.895035 C 72.172417 63.895035 68.94397 69.567368 68.94397 73.580376 C 68.94397 77.592979 72.172417 83.265717 78.629311 83.265717 Z"/>
6 +</svg>
frontend/src/assets/scss/helpers.scss
+19 -2
@@ -96,14 +96,22 @@
96 }
97 }
98
99 -.grid-auto-flow-200 {
99 +.grid-auto-fit-200 {
100 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
101 grid-auto-flow: row dense;
102 }
103 -.grid-auto-flow-250 {
103 +.grid-auto-fit-250 {
104 grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
105 grid-auto-flow: row dense;
106 }
107 +.grid-auto-fill-200 {
108 + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
109 + grid-auto-flow: row dense;
110 +}
111 +.grid-auto-fill-250 {
112 + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
113 + grid-auto-flow: row dense;
114 +}
115
116 .bg-color {
117 background-color: var(--bg-color);
@@ -145,3 +153,12 @@
153 .text-success-color {
154 color: var(--success-color);
155 }
156 +
157 +.border-primary-color {
158 + border-color: var(--primary-color) !important;
159 +}
160 +.hover\:border-primary-color {
161 + &:hover {
162 + border-color: var(--primary-color) !important;
163 + }
164 +}
frontend/src/components/activeResponse/ActiveResponseInvokeForm.vue
+7 -8
@@ -3,7 +3,7 @@
3 <div class="form-box">
4 <n-spin v-model:show="loading">
5 <n-form :label-width="80" :model="form" :rules="rules" ref="formRef">
6 - <div class="grid gap-6 grid-auto-flow-200">
6 + <div class="grid gap-6 grid-auto-fit-200">
7 <n-form-item label="Action" path="action">
8 <n-select v-model:value="form.action" :options="invokeActionOptions" />
9 </n-form-item>
@@ -28,6 +28,7 @@
28 </template>
29
30 <script setup lang="ts">
31 +import { computed, onMounted, ref, watch } from "vue"
32 import {
33 NButton,
34 NForm,
@@ -35,18 +36,16 @@ import {
36 NInput,
37 NSelect,
38 NSpin,
39 + useMessage,
40 type FormItemRule,
41 type FormRules,
42 type FormInst,
41 - type FormValidationError,
42 - useMessage
43 + type FormValidationError
44 } from "naive-ui"
44 -import { computed, onMounted, ref } from "vue"
45 -import { watch } from "vue"
46 -import type { SupportedActiveResponse } from "@/types/activeResponse.d"
47 -import isIP from "validator/es/lib/isIP"
48 -import type { InvokeRequest, InvokeRequestAction } from "@/api/activeResponse"
45 import Api from "@/api"
46 +import isIP from "validator/es/lib/isIP"
47 +import type { InvokeRequest, InvokeRequestAction } from "@/api/endpoints/activeResponse"
48 +import type { SupportedActiveResponse } from "@/types/activeResponse.d"
49
50 interface InvokeForm {
51 action: null | InvokeRequestAction
frontend/src/components/activeResponse/ActiveResponseWizard.vue
+1 -1
@@ -103,7 +103,7 @@ import type { SupportedActiveResponse } from "@/types/activeResponse.d"
103 import ActiveResponseItem from "./ActiveResponseItem.vue"
104 import ActiveResponseInvokeForm from "./ActiveResponseInvokeForm.vue"
105 import { iconFromOs } from "@/utils"
106 -import type { OsTypesLower } from "@/types/common"
106 +import type { OsTypesLower } from "@/types/common.d"
107
108 const emit = defineEmits<{
109 (e: "update:loading", value: boolean): void
frontend/src/components/activeResponse/ActiveResponseWizardButton.vue
+1 -2
@@ -11,8 +11,7 @@
11 :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
12 title="Active Response Wizard"
13 :bordered="false"
14 - content-style="padding:0"
15 - content-class="flex flex-col"
14 + content-class="flex flex-col !p-0"
15 segmented
16 >
17 <ActiveResponseWizard @mounted="activeResponseWizardCTX = $event" v-model:loading="loading" />
frontend/src/components/agents/AgentCard.vue
+3 -4
@@ -77,10 +77,6 @@ import { NTooltip, NButton, NSpin, NCard, useMessage, useDialog } from "naive-ui
77 import Icon from "@/components/common/Icon.vue"
78 import { useSettingsStore } from "@/stores/settings"
79
80 -const QuarantinedIcon = "ph:seal-warning-light"
81 -const StarIcon = "carbon:star"
82 -const DeleteIcon = "ph:trash"
83 -
80 const emit = defineEmits<{
81 (e: "delete"): void
82 }>()
@@ -92,6 +88,9 @@ const props = defineProps<{
88 }>()
89 const { agent, showActions, bgSecondary } = toRefs(props)
90
91 +const QuarantinedIcon = "ph:seal-warning-light"
92 +const StarIcon = "carbon:star"
93 +const DeleteIcon = "ph:trash"
94 const dFormats = useSettingsStore().dateFormat
95 const loading = ref(false)
96 const message = useMessage()
frontend/src/components/agents/AgentCases.vue
+2 -2
@@ -8,14 +8,14 @@
8 </div>
9 </div>
10 </div>
11 - <div class="list my-3">
11 + <div class="list flex flex-col gap-2 my-3">
12 <template v-if="casesList.length">
13 <SocCaseItem
14 v-for="item of casesList"
15 :key="item"
16 :caseId="item"
17 @deleted="getData()"
18 - class="mb-2 item-appear item-appear-bottom item-appear-005"
18 + class="item-appear item-appear-bottom item-appear-005"
19 />
20 </template>
21 <template v-else>
frontend/src/components/agents/AgentVelociraptorIdForm.vue
+1 -1
@@ -33,7 +33,7 @@ import { onBeforeMount, ref, toRefs } from "vue"
33 import Api from "@/api"
34 import { useMessage, NInput, NButton, NInputGroup } from "naive-ui"
35 import Icon from "@/components/common/Icon.vue"
36 -import type { Agent } from "@/types/agents"
36 +import type { Agent } from "@/types/agents.d"
37
38 const velociraptorId = defineModel<string>("velociraptorId", { default: "" })
39
frontend/src/components/agents/OverviewSection.vue
+1 -21
@@ -1,6 +1,6 @@
1 <template>
2 <div class="overview-section">
3 - <div class="property-group">
3 + <div class="property-group gap-2 grid grid-auto-fit-250">
4 <KVCard v-for="item of propsSanitized" :key="item.key">
5 <template #key>{{ item.key }}</template>
6 <template #value>
@@ -58,23 +58,3 @@ const propsSanitized = computed(() => {
58 return obj
59 })
60 </script>
61 -
62 -<style lang="scss" scoped>
63 -.overview-section {
64 - container-type: inline-size;
65 -
66 - .property-group {
67 - width: 100%;
68 - display: grid;
69 - @apply gap-2;
70 - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
71 - grid-auto-flow: row dense;
72 - }
73 -
74 - @container (max-width: 500px) {
75 - .property-group {
76 - grid-template-columns: repeat(auto-fit, 100%);
77 - }
78 - }
79 -}
80 -</style>
frontend/src/components/agents/agentFlow/AgentFlowCollectList.vue
+2 -2
@@ -8,14 +8,14 @@
8 </div>
9 </div>
10 </div>
11 - <div class="list my-3">
11 + <div class="list flex flex-col gap-4 my-3">
12 <template v-if="collectList.length">
13 <CollectItem
14 v-for="item of collectList"
15 :key="item.___id"
16 :collect="item"
17 embedded
18 - class="mb-4 item-appear item-appear-bottom item-appear-005"
18 + class="item-appear item-appear-bottom item-appear-005"
19 />
20 </template>
21 <template v-else>
frontend/src/components/agents/agentFlow/AgentFlowItem.vue
+11 -9
@@ -8,7 +8,7 @@
8 </div>
9 </div>
10 <div class="time">
11 - <n-popover overlap placement="top-end" style="max-height: 240px" scrollable to="body">
11 + <n-popover overlap placement="top-end" class="max-h-64" scrollable to="body">
12 <template #trigger>
13 <div class="flex items-center gap-2 cursor-help">
14 <span>
@@ -30,15 +30,15 @@
30 </span>
31 </div>
32 <div class="badges-box flex flex-wrap items-center gap-3 mt-4">
33 - <Badge type="splitted">
33 + <Badge type="splitted" color="primary">
34 <template #label>State</template>
35 <template #value>{{ flow.state || "-" }}</template>
36 </Badge>
37 - <Badge type="splitted">
37 + <Badge type="splitted" color="primary">
38 <template #label>Status</template>
39 <template #value>{{ flow.status || "-" }}</template>
40 </Badge>
41 - <Badge type="splitted">
41 + <Badge type="splitted" color="primary">
42 <template #label>Exec. time</template>
43 <template #value>{{ executionDuration }}</template>
44 </Badge>
@@ -71,7 +71,7 @@
71 >
72 <n-tabs type="line" animated :tabs-padding="24">
73 <n-tab-pane name="Info" tab="Info" display-directive="show">
74 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="properties">
74 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4" v-if="properties">
75 <KVCard v-for="(value, key) of properties" :key="key">
76 <template #key>{{ key }}</template>
77 <template #value>{{ value === "" ? "-" : value ?? "-" }}</template>
@@ -138,6 +138,7 @@
138 </div>
139 </n-tab-pane>
140 <n-tab-pane name="Collect" tab="Collect" display-directive="show:lazy">
141 + <!-- TODO: replace max-height or similar with TailWind classes -->
142 <n-scrollbar style="max-height: 430px" trigger="none">
143 <div class="px-7">
144 <AgentFlowCollectList :flow="flow" />
@@ -150,22 +151,23 @@
151 </template>
152
153 <script setup lang="ts">
154 +import { computed, defineAsyncComponent, ref } from "vue"
155 import { NPopover, NModal, NTabs, NTabPane, NEmpty, NScrollbar, NInput } from "naive-ui"
156 import { useSettingsStore } from "@/stores/settings"
157 import dayjs from "@/utils/dayjs"
158 import { formatDate } from "@/utils"
159 import type { FlowResult } from "@/types/flow.d"
160 import Icon from "@/components/common/Icon.vue"
159 -import AgentFlowTimeline from "./AgentFlowTimeline.vue"
160 -import AgentFlowQueryStat from "./AgentFlowQueryStat.vue"
161 -import AgentFlowCollectList from "./AgentFlowCollectList.vue"
161 import KVCard from "@/components/common/KVCard.vue"
163 -import { computed, ref } from "vue"
162 import _pick from "lodash/pick"
163 import Badge from "@/components/common/Badge.vue"
164 import { SimpleJsonViewer } from "vue-sjv"
165 import "@/assets/scss/vuesjv-override.scss"
166
167 +const AgentFlowTimeline = defineAsyncComponent(() => import("./AgentFlowTimeline.vue"))
168 +const AgentFlowQueryStat = defineAsyncComponent(() => import("./AgentFlowQueryStat.vue"))
169 +const AgentFlowCollectList = defineAsyncComponent(() => import("./AgentFlowCollectList.vue"))
170 +
171 const { flow, embedded } = defineProps<{ flow: FlowResult; embedded?: boolean }>()
172
173 const TimeIcon = "carbon:time"
frontend/src/components/agents/agentFlow/AgentFlowList.vue
+2 -2
@@ -8,14 +8,14 @@
8 </div>
9 </div>
10 </div>
11 - <div class="list my-3">
11 + <div class="list flex flex-col gap-2 my-3">
12 <template v-if="flowList.length">
13 <AgentFlowItem
14 v-for="item of flowList"
15 :key="item.id"
16 :flow="item"
17 embedded
18 - class="mb-2 item-appear item-appear-bottom item-appear-005"
18 + class="item-appear item-appear-bottom item-appear-005"
19 />
20 </template>
21 <template v-else>
frontend/src/components/agents/agentFlow/AgentFlowQueryStat.vue
+3 -3
@@ -29,11 +29,11 @@
29 {{ stat.error_message }}
30 </div>
31 <div class="badges-box flex flex-wrap items-center gap-3 mt-1">
32 - <Badge type="splitted">
32 + <Badge type="splitted" color="primary">
33 <template #label>Status</template>
34 <template #value>{{ stat.status || "-" }}</template>
35 </Badge>
36 - <Badge type="splitted">
36 + <Badge type="splitted" color="primary">
37 <template #label>Duration</template>
38 <template #value>{{ duration }}</template>
39 </Badge>
@@ -61,7 +61,7 @@
61 >
62 <n-tabs type="line" animated :tabs-padding="24">
63 <n-tab-pane name="Info" tab="Info" display-directive="show">
64 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="properties">
64 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4" v-if="properties">
65 <KVCard v-for="(value, key) of properties" :key="key">
66 <template #key>{{ key }}</template>
67 <template #value>{{ value === "" ? "-" : value ?? "-" }}</template>
frontend/src/components/agents/sca/ScaItem.vue
+4 -3
@@ -28,7 +28,7 @@
28 </div>
29 </n-card>
30 </div>
31 - <div class="grid gap-2 grid-auto-flow-200 px-7" v-if="properties">
31 + <div class="grid gap-2 grid-auto-fit-200 px-7" v-if="properties">
32 <KVCard v-for="(value, key) of properties" :key="key">
33 <template #key>{{ key }}</template>
34 <template #value>
@@ -77,16 +77,17 @@
77 </template>
78
79 <script setup lang="ts">
80 +import { computed, defineAsyncComponent } from "vue"
81 import { NTabs, NTabPane, NInput, NStatistic, NCard } from "naive-ui"
82 import { useSettingsStore } from "@/stores/settings"
83 import { formatDate } from "@/utils"
84 import { type Agent, type AgentSca } from "@/types/agents.d"
85 import KVCard from "@/components/common/KVCard.vue"
86 import Icon from "@/components/common/Icon.vue"
86 -import { computed } from "vue"
87 -import ScaResults from "./ScaResults.vue"
87 import _pick from "lodash/pick"
88
89 +const ScaResults = defineAsyncComponent(() => import("./ScaResults.vue"))
90 +
91 const { sca, agent } = defineProps<{ sca: AgentSca; agent: Agent }>()
92
93 const dFormats = useSettingsStore().dateFormat
frontend/src/components/agents/sca/ScaResultItem.vue
+4 -4
@@ -31,17 +31,17 @@
31 <template #label>{{ data.result }}</template>
32 </Badge>
33
34 - <Badge type="splitted">
34 + <Badge type="splitted" color="primary">
35 <template #label>Compliance</template>
36 <template #value>{{ data.compliance?.length || "-" }}</template>
37 </Badge>
38
39 - <Badge type="splitted">
39 + <Badge type="splitted" color="primary">
40 <template #label>Condition</template>
41 <template #value>{{ data.condition || "-" }}</template>
42 </Badge>
43
44 - <Badge type="splitted">
44 + <Badge type="splitted" color="primary">
45 <template #label>Rules</template>
46 <template #value>{{ data.rules?.length || "-" }}</template>
47 </Badge>
@@ -67,7 +67,7 @@ import Icon from "@/components/common/Icon.vue"
67 import Badge from "@/components/common/Badge.vue"
68 import { ref } from "vue"
69 import { NModal, NButton } from "naive-ui"
70 -import type { ScaPolicyResult } from "@/types/agents"
70 +import type { ScaPolicyResult } from "@/types/agents.d"
71 import ScaResultItemDetails from "./ScaResultItemDetails.vue"
72
73 const { data, embedded } = defineProps<{
frontend/src/components/agents/sca/ScaResultItemDetails.vue
+2 -2
@@ -38,7 +38,7 @@
38 </n-card>
39 </div>
40
41 - <div class="grid gap-2 grid-auto-flow-200 px-7" v-if="properties">
41 + <div class="grid gap-2 grid-auto-fit-200 px-7" v-if="properties">
42 <KVCard v-for="(value, key) of properties" :key="key">
43 <template #key>{{ key }}</template>
44 <template #value>{{ value ?? "-" }}</template>
@@ -142,7 +142,7 @@ import _pick from "lodash/pick"
142 import KVCard from "@/components/common/KVCard.vue"
143 import { computed } from "vue"
144 import { NTabs, NTabPane, NStatistic, NInput, NCard } from "naive-ui"
145 -import type { ScaPolicyResult } from "@/types/agents"
145 +import type { ScaPolicyResult } from "@/types/agents.d"
146
147 const { data } = defineProps<{
148 data: ScaPolicyResult
frontend/src/components/agents/sca/ScaResults.vue
+2 -2
@@ -66,9 +66,9 @@
66 </div>
67 </n-popover>
68 </div>
69 - <div class="list my-3">
69 + <div class="list flex flex-col gap-2 my-3">
70 <template v-if="itemsPaginated.length">
71 - <ScaResultItem v-for="item of itemsPaginated" :key="item.id" :data="item" embedded class="mb-2" />
71 + <ScaResultItem v-for="item of itemsPaginated" :key="item.id" :data="item" embedded />
72 </template>
73 <template v-else>
74 <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
frontend/src/components/agents/sca/ScaTable.vue
+1 -22
@@ -1,5 +1,5 @@
1 <template>
2 - <n-spin class="sca-section" :show="loading">
2 + <n-spin class="sca-table" :show="loading">
3 <n-scrollbar x-scrollable style="width: 100%">
4 <n-table :bordered="true" class="min-w-max">
5 <thead>
@@ -144,24 +144,3 @@ onBeforeMount(() => {
144 if (agent?.value?.agent_id) getSCA(agent.value.agent_id)
145 })
146 </script>
147 -
148 -<style lang="scss" scoped>
149 -.sca-section {
150 - container-type: inline-size;
151 - min-height: 100px;
152 -
153 - .group {
154 - @apply gap-4;
155 - width: 100%;
156 - display: grid;
157 - grid-template-columns: repeat(auto-fit, minmax(175px, 1fr));
158 - grid-auto-flow: row dense;
159 - }
160 -
161 - @container (max-width: 500px) {
162 - .group {
163 - grid-template-columns: repeat(auto-fit, 100%);
164 - }
165 - }
166 -}
167 -</style>
frontend/src/components/agents/vulnerabilities/VulnerabilitiesGrid.vue
+2 -23
@@ -5,7 +5,7 @@
5 <n-select v-model:value="severity" :options="severityOptions" class="max-w-48" />
6 </n-form-item>
7 </div>
8 - <div class="group">
8 + <div class="group gap-4 grid grid-auto-fill-200">
9 <VulnerabilityCard :vulnerability="item" v-for="item of vulnerabilities" :key="item.id" hide-tooltip />
10 </div>
11 <n-empty
@@ -23,7 +23,7 @@ import { type Agent, type AgentVulnerabilities } from "@/types/agents.d"
23 import VulnerabilityCard from "./VulnerabilityCard.vue"
24 import { nanoid } from "nanoid"
25 import { useMessage, NSpin, NEmpty, NFormItem, NSelect } from "naive-ui"
26 -import type { VulnerabilitySeverityType } from "@/api/agents"
26 +import type { VulnerabilitySeverityType } from "@/api/endpoints/agents"
27
28 const props = defineProps<{
29 agent: Agent
@@ -78,24 +78,3 @@ onBeforeMount(() => {
78 if (agent?.value?.agent_id) getVulnerabilities(agent.value.agent_id)
79 })
80 </script>
81 -
82 -<style lang="scss" scoped>
83 -.vulnerabilities-section {
84 - container-type: inline-size;
85 - min-height: 100px;
86 -
87 - .group {
88 - @apply gap-4;
89 - width: 100%;
90 - display: grid;
91 - grid-template-columns: repeat(auto-fit, minmax(175px, 1fr));
92 - grid-auto-flow: row dense;
93 - }
94 -
95 - @container (max-width: 500px) {
96 - .group {
97 - grid-template-columns: repeat(auto-fit, 100%);
98 - }
99 - }
100 -}
101 -</style>
frontend/src/components/agents/vulnerabilities/VulnerabilityCard.vue
+5 -4
@@ -70,7 +70,7 @@
70 <template #value>{{ vulnerability.title ?? "-" }}</template>
71 </KVCard>
72 </div>
73 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-2" v-if="vulnerabilitySanitized">
73 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-2" v-if="vulnerabilitySanitized">
74 <KVCard v-for="item of vulnerabilitySanitized" :key="item.label">
75 <template #key>{{ item.label }}</template>
76 <template #value>{{ item.value ?? "-" }}</template>
@@ -88,7 +88,7 @@
88 </n-tab-pane>
89 <n-tab-pane name="EPSS Score" tab="EPSS Score" display-directive="show:lazy">
90 <div class="p-7 pt-2">
91 - <VulnerabilityEpss :cve="vulnerability.cve" />
91 + <ThreatIntelEpssScore :cve="vulnerability.cve" />
92 </div>
93 </n-tab-pane>
94 </n-tabs>
@@ -97,7 +97,7 @@
97 </template>
98
99 <script setup lang="ts">
100 -import { computed, ref, toRefs } from "vue"
100 +import { computed, defineAsyncComponent, ref, toRefs } from "vue"
101 import { type AgentVulnerabilities } from "@/types/agents.d"
102 import dayjs from "@/utils/dayjs"
103 import { cloneDeep } from "lodash"
@@ -106,7 +106,8 @@ import Icon from "@/components/common/Icon.vue"
106 import KVCard from "@/components/common/KVCard.vue"
107 import { useSettingsStore } from "@/stores/settings"
108 import _truncate from "lodash/truncate"
109 -import VulnerabilityEpss from "./VulnerabilityEpss.vue"
109 +
110 +const ThreatIntelEpssScore = defineAsyncComponent(() => import("@/components/threatIntel/ThreatIntelEpssScore.vue"))
111
112 const props = defineProps<{
113 vulnerability: AgentVulnerabilities
frontend/src/components/alerts/Alert.vue
+35 -28
@@ -15,7 +15,7 @@
15 <div class="rule-groups">{{ alert._source.rule_groups }}</div>
16
17 <div class="badges-box flex flex-wrap items-center gap-3" v-if="alert._id">
18 - <Badge type="splitted">
18 + <Badge type="splitted" color="primary">
19 <template #iconLeft>
20 <Icon :name="TargetIcon" :size="13" class="!opacity-80"></Icon>
21 </template>
@@ -32,7 +32,7 @@
32
33 <n-popover overlap placement="bottom-start">
34 <template #trigger>
35 - <Badge type="splitted" hint-cursor>
35 + <Badge type="splitted" color="primary" hint-cursor>
36 <template #iconLeft>
37 <Icon :name="AgentIcon" :size="13" class="!opacity-80"></Icon>
38 </template>
@@ -73,19 +73,19 @@
73 </div>
74 </div>
75 </n-popover>
76 - <Badge type="splitted">
76 + <Badge type="splitted" color="primary">
77 <template #label>syslog</template>
78 <template #value>{{ alert._source.syslog_type }} / {{ alert._source.syslog_level }}</template>
79 </Badge>
80 - <Badge type="splitted" class="hide-on-small">
80 + <Badge type="splitted" color="primary" class="hide-on-small">
81 <template #label>manager</template>
82 <template #value>{{ alert._source.manager_name }}</template>
83 </Badge>
84 - <Badge type="splitted" class="hide-on-small">
84 + <Badge type="splitted" color="primary" class="hide-on-small">
85 <template #label>decoder</template>
86 <template #value>{{ alert._source.decoder_name }}</template>
87 </Badge>
88 - <Badge type="splitted" class="hide-on-small">
88 + <Badge type="splitted" color="primary" class="hide-on-small">
89 <template #label>source</template>
90 <template #value>{{ alert._source.source }}</template>
91 </Badge>
@@ -94,10 +94,12 @@
94 <AlertActions
95 v-if="!hideActions"
96 class="actions-box"
97 - :alert="alert"
97 + :alert
98 + :socAlertField="socAlertCreationField"
99 @start-loading="loading = true"
100 @stop-loading="loading = false"
101 @updated-url="alert._source.alert_url = $event"
102 + @updated-id="alert._source.alert_id = $event"
103 @updated-ask-message="alert._source.ask_socfortress_message = $event"
104 />
105 </div>
@@ -105,11 +107,13 @@
107 <AlertActions
108 v-if="!hideActions"
109 class="actions-box"
108 - :alert="alert"
110 + :alert
111 + :socAlertField="socAlertCreationField"
112 :size="'small'"
113 @start-loading="loading = true"
114 @stop-loading="loading = false"
115 @updated-url="alert._source.alert_url = $event"
116 + @updated-id="alert._source.alert_id = $event"
117 @updated-ask-message="alert._source.ask_socfortress_message = $event"
118 />
119 <div class="time">{{ formatDate(alert._source.timestamp_utc, dFormats.datetimesec) }}</div>
@@ -126,7 +130,7 @@
130 >
131 <n-tabs type="line" animated :tabs-padding="24">
132 <n-tab-pane name="Agent" tab="Agent" display-directive="show" v-if="alert._id">
129 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="agentProperties">
133 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4" v-if="agentProperties">
134 <KVCard v-for="(value, key) of agentProperties" :key="key">
135 <template #key>{{ key }}</template>
136 <template #value>
@@ -173,16 +177,14 @@
177 </n-tab-pane>
178 <n-tab-pane name="Message" tab="Message" v-if="alert._source.message" display-directive="show">
179 <div class="p-7 pt-4">
176 - <n-input
177 - :value="alert._source.message"
178 - type="textarea"
179 - readonly
180 - placeholder="Empty"
181 - size="large"
182 - :autosize="{
183 - minRows: 3
184 - }"
185 - />
180 + <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden">
181 + <div
182 + class="scrollbar-styled overflow-hidden code-bg-transparent"
183 + v-shiki="{ decode: false }"
184 + >
185 + <pre>{{ alert._source.message }}</pre>
186 + </div>
187 + </n-card>
188 </div>
189 </n-tab-pane>
190 <n-tab-pane
@@ -206,11 +208,14 @@
208 </n-tab-pane>
209 <n-tab-pane name="Details" tab="Details" display-directive="show:lazy">
210 <div class="p-7 pt-4">
209 - <SimpleJsonViewer
210 - class="vuesjv-override"
211 - :model-value="alert._source"
212 - :initialExpandedDepth="2"
213 - />
211 + <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden">
212 + <div
213 + class="scrollbar-styled overflow-hidden code-bg-transparent"
214 + v-shiki="{ lang: 'json', decode: false }"
215 + >
216 + <pre>{{ alert._source }}</pre>
217 + </div>
218 + </n-card>
219 </div>
220 </n-tab-pane>
221 </n-tabs>
@@ -219,19 +224,19 @@
224 </template>
225
226 <script setup lang="ts">
222 -import { computed, defineAsyncComponent, ref, toRefs } from "vue"
223 -import { NPopover, NModal, NTabs, NTabPane, NInput } from "naive-ui"
227 +import { computed, defineAsyncComponent, inject, ref, toRefs } from "vue"
228 +import { NPopover, NModal, NTabs, NTabPane, NInput, NCard } from "naive-ui"
229 import { useSettingsStore } from "@/stores/settings"
230 import { formatDate } from "@/utils"
231 import Icon from "@/components/common/Icon.vue"
232 import Badge from "@/components/common/Badge.vue"
233 const AlertActions = defineAsyncComponent(() => import("./AlertActions.vue"))
234 import type { Alert } from "@/types/alerts.d"
230 -import { SimpleJsonViewer } from "vue-sjv"
231 -import "@/assets/scss/vuesjv-override.scss"
235 +import vShiki from "@/directives/v-shiki"
236 import _pick from "lodash/pick"
237 import KVCard from "@/components/common/KVCard.vue"
238 import { useGoto } from "@/composables/useGoto"
239 +import type { SocAlertField } from "./type.d"
240
241 const props = defineProps<{ alert: Alert; hideActions?: boolean }>()
242 const { alert, hideActions } = toRefs(props)
@@ -260,6 +265,8 @@ const agentProperties = computed(() => {
265 "agent_name"
266 ])
267 })
268 +
269 +const socAlertCreationField = ref(inject<SocAlertField>("soc-alert-creation-field", "alert_url"))
270 </script>
271
272 <style lang="scss" scoped>
frontend/src/components/alerts/AlertActions.vue
+30 -3
@@ -1,6 +1,6 @@
1 <template>
2 <div class="alert-actions flex flex-col gap-2 justify-end">
3 - <n-button type="success" secondary :size="size" v-if="alertUrl" tag="a" :href="alertUrl" target="_blank">
3 + <n-button type="success" secondary :size="size" v-if="socAlertFieldValue" @click="gotoSocAlertUrl()">
4 <template #icon><Icon :name="ViewIcon"></Icon></template>
5 View SOC Alert
6 </n-button>
@@ -10,7 +10,7 @@
10 secondary
11 :size="size"
12 @click="createAlert()"
13 - v-if="!alertUrl"
13 + v-if="!socAlertFieldValue"
14 >
15 <template #icon><Icon :name="DangerIcon"></Icon></template>
16 Create SOC Alert
@@ -82,21 +82,29 @@ import AlertWazuhRules from "./AlertWazuhRules.vue"
82 import Api from "@/api"
83 import Icon from "@/components/common/Icon.vue"
84 import type { Alert, WazuhRuleExclude } from "@/types/alerts.d"
85 +import type { SocAlertField } from "./type.d"
86 +import { useRouter } from "vue-router"
87
88 const emit = defineEmits<{
89 (e: "startLoading"): void
90 (e: "stopLoading"): void
91 (e: "updatedUrl", value: string): void
92 + (e: "updatedId", value: number): void
93 (e: "updatedAskMessage", value: string): void
94 }>()
95
93 -const { alert, size } = defineProps<{ alert: Alert; size?: "tiny" | "small" | "medium" | "large" }>()
96 +const { alert, size, socAlertField } = defineProps<{
97 + alert: Alert
98 + size?: "tiny" | "small" | "medium" | "large"
99 + socAlertField: SocAlertField
100 +}>()
101
102 const DangerIcon = "majesticons:exclamation-line"
103 const AskIcon = "majesticons:question-mark-circle-line"
104 const ViewIcon = "iconoir:eye-alt"
105 const RulesIcon = "carbon:rule-cancelled"
106
107 +const router = useRouter()
108 const message = useMessage()
109 const showSocResponse = ref(false)
110 const showWazuhRuleExclude = ref(false)
@@ -106,9 +114,12 @@ const loadingWazuhRuleExclude = ref(false)
114 const loading = computed(() => loadingSocAlert.value || loadingAskSoc.value || loadingWazuhRuleExclude.value)
115
116 const alertUrl = ref("")
117 +const alertId = ref(0)
118 const alertAskMessage = ref("")
119 const wazuhRuleData = ref<WazuhRuleExclude | null>(null)
120
121 +const socAlertFieldValue = computed(() => (socAlertField === "alert_id" ? alertId.value : alertUrl.value))
122 +
123 const isAskVisible = computed(() => alert._source?.rule_group3 === "sigma" && !alertAskMessage.value)
124 const isWazuhRulesVisible = computed(() => alert._source)
125
@@ -126,12 +137,26 @@ watch(alertUrl, val => {
137 }
138 })
139
140 +watch(alertId, val => {
141 + if (val) {
142 + emit("updatedId", val)
143 + }
144 +})
145 +
146 watch(alertAskMessage, val => {
147 if (val) {
148 emit("updatedAskMessage", val)
149 }
150 })
151
152 +function gotoSocAlertUrl() {
153 + if (socAlertField === "alert_url") {
154 + window.open(alertUrl.value, "_blank")
155 + } else if (socAlertField === "alert_id") {
156 + router.push({ name: "IncidentManagement-Alerts", query: alertId.value ? { alert_id: alertId.value } : {} })
157 + }
158 +}
159 +
160 function askSOCFortress() {
161 loadingAskSoc.value = true
162
@@ -190,6 +215,7 @@ function createAlert() {
215 .then(res => {
216 if (res.data.success) {
217 res.data.alert_url && (alertUrl.value = res.data.alert_url)
218 + res.data.alert_id && (alertId.value = res.data.alert_id)
219 message.success(res.data?.message || "SOC Alert created.")
220 } else {
221 message.warning(res.data?.message || "An error occurred. Please try again later.")
@@ -205,6 +231,7 @@ function createAlert() {
231
232 onBeforeMount(() => {
233 alertUrl.value = alert._source.alert_url || ""
234 + alertId.value = alert._source.alert_id || 0
235 alertAskMessage.value = alert._source.ask_socfortress_message || ""
236 })
237 </script>
frontend/src/components/alerts/AlertsFilters.vue
+1 -1
@@ -61,7 +61,7 @@ import { NSelect, NButton, NInput, NInputGroup, NEmpty, NFormItem, type SelectOp
61 import Icon from "@/components/common/Icon.vue"
62 import { useStorage } from "@vueuse/core"
63 import _uniqBy from "lodash/uniqBy"
64 -import type { AlertsQueryTimeRange, AlertsSummaryQuery } from "@/api/alerts"
64 +import type { AlertsQueryTimeRange, AlertsSummaryQuery } from "@/api/endpoints/alerts"
65
66 const props = defineProps<{ filters: AlertsSummaryQuery }>()
67 const { filters } = toRefs(props)
frontend/src/components/alerts/AlertsGraylogFilters.vue new
+80
@@ -0,0 +1,80 @@
1 +<template>
2 + <div class="alerts-filters flex flex-col gap-2">
3 + <div class="flex gap-2">
4 + <n-form-item label="Alerts for group" class="basis-1/2">
5 + <n-select v-model:value="filters.size" :options="sizeOptions" />
6 + </n-form-item>
7 + <n-form-item label="Time range" class="basis-1/2">
8 + <n-select v-model:value="filters.timerange" :options="timerangeOptions" />
9 + </n-form-item>
10 + </div>
11 +
12 + <div class="flex justify-end">
13 + <n-button strong secondary type="primary" @click="search()">
14 + <template #icon>
15 + <Icon :name="SearchIcon"></Icon>
16 + </template>
17 + Search
18 + </n-button>
19 + </div>
20 + </div>
21 +</template>
22 +
23 +<script setup lang="ts">
24 +import { onBeforeMount, toRefs } from "vue"
25 +import { NSelect, NButton, NFormItem } from "naive-ui"
26 +import Icon from "@/components/common/Icon.vue"
27 +import { useStorage } from "@vueuse/core"
28 +import type { AlertsQueryTimeRange, GraylogAlertsQuery } from "@/api/endpoints/alerts"
29 +
30 +const props = defineProps<{ filters: Partial<GraylogAlertsQuery> }>()
31 +const { filters } = toRefs(props)
32 +
33 +const emit = defineEmits<{
34 + (e: "search"): void
35 +}>()
36 +
37 +const SearchIcon = "carbon:search"
38 +
39 +const timerangeOptions: { label: string; value: AlertsQueryTimeRange }[] = [
40 + { label: "1 Hour", value: "1h" },
41 + { label: "6 Hours", value: "6h" },
42 + { label: "12 Hours", value: "12h" },
43 + { label: "1 Day", value: "1d" },
44 + { label: "2 Day", value: "2d" },
45 + { label: "5 Day", value: "5d" },
46 + { label: "1 Week", value: "1w" },
47 + { label: "2 Week", value: "2w" },
48 + { label: "3 Week", value: "3w" },
49 + { label: "4 Week", value: "4w" }
50 +]
51 +
52 +const sizeOptions = [
53 + { label: "1 Alert", value: 1 },
54 + { label: "5 Alert", value: 5 },
55 + { label: "10 Alert", value: 10 },
56 + { label: "20 Alert", value: 20 }
57 +]
58 +
59 +const sizeDefault = useStorage<number>("alert-size-default", sizeOptions[2].value, localStorage)
60 +const timerangeDefault = useStorage<AlertsQueryTimeRange>(
61 + "alert-timerange-default",
62 + timerangeOptions[3].value,
63 + localStorage
64 +)
65 +
66 +function search() {
67 + emit("search")
68 + timerangeDefault.value = filters.value.timerange
69 + sizeDefault.value = filters.value.size
70 +}
71 +
72 +onBeforeMount(() => {
73 + if (!filters.value.timerange) {
74 + filters.value.timerange = timerangeDefault.value
75 + }
76 + if (!filters.value.size) {
77 + filters.value.size = sizeDefault.value
78 + }
79 +})
80 +</script>
frontend/src/components/alerts/AlertsGraylogList.vue new
+194
@@ -0,0 +1,194 @@
1 +<template>
2 + <div class="alerts-list">
3 + <div class="header flex items-center justify-end gap-2">
4 + <div class="info grow flex gap-5">
5 + <n-popover overlap placement="bottom-start">
6 + <template #trigger>
7 + <div class="bg-color border-radius">
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 Summaries:
18 + <code>{{ totalAlertsSummary }}</code>
19 + </div>
20 + <div class="box">
21 + Total Alerts:
22 + <code>{{ totalAlerts }}</code>
23 + </div>
24 + </div>
25 + </n-popover>
26 + </div>
27 + <div class="actions flex gap-2 items-center">
28 + <n-button size="small" @click="showFiltersDrawer = true">
29 + <template #icon>
30 + <Icon :name="FilterIcon" :size="15"></Icon>
31 + </template>
32 + Filters
33 + </n-button>
34 + <ThreatIntelButton size="small" type="primary" />
35 + </div>
36 + </div>
37 + <n-spin :show="loading">
38 + <template #description>Alerts are being fetched, this may take up to 1 minute.</template>
39 +
40 + <div class="list flex flex-col gap-2 my-3">
41 + <template v-if="alertsSummaryList.length">
42 + <AlertsSummaryItem
43 + v-for="alertsSummary of alertsSummaryList"
44 + :key="alertsSummary.index_name"
45 + :alertsSummary="alertsSummary"
46 + class="item-appear item-appear-bottom item-appear-005"
47 + />
48 + </template>
49 + <template v-else>
50 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
51 + </template>
52 + </div>
53 + </n-spin>
54 +
55 + <n-drawer
56 + v-model:show="showFiltersDrawer"
57 + display-directive="show"
58 + :trap-focus="false"
59 + style="max-width: 90vw; width: 500px"
60 + :show-mask="loadingFilters ? 'transparent' : undefined"
61 + :class="{ 'opacity-0': loadingFilters }"
62 + >
63 + <n-drawer-content title="Alerts filters" closable :native-scrollbar="false">
64 + <AlertsGraylogFilters :filters @search="startSearch(true)" />
65 + </n-drawer-content>
66 + </n-drawer>
67 + </div>
68 +</template>
69 +
70 +<script setup lang="ts">
71 +// MOCK
72 +// import { alerts_summary } from "./mock"
73 +// import type { AlertsSummary } from "@/types/alerts.d"
74 +
75 +import { ref, onBeforeMount, computed, nextTick, onMounted, onBeforeUnmount, defineAsyncComponent, provide } from "vue"
76 +import { useMessage, NSpin, NPopover, NButton, NEmpty, NDrawer, NDrawerContent } from "naive-ui"
77 +import Api from "@/api"
78 +const ThreatIntelButton = defineAsyncComponent(() => import("@/components/threatIntel/ThreatIntelButton.vue"))
79 +import AlertsGraylogFilters from "./AlertsGraylogFilters.vue"
80 +import AlertsSummaryItem, { type AlertsSummaryExt } from "./AlertsSummary.vue"
81 +import Icon from "@/components/common/Icon.vue"
82 +import type { GraylogAlertsQuery } from "@/api/endpoints/alerts"
83 +import type { IndexStats } from "@/types/indices.d"
84 +import axios from "axios"
85 +import type { SocAlertField } from "./type.d"
86 +
87 +const message = useMessage()
88 +const loading = ref(false)
89 +const indices = ref<IndexStats[]>([])
90 +const alertsSummaryList = ref<AlertsSummaryExt[]>([])
91 +const loadingFilters = ref(true)
92 +const showFiltersDrawer = ref(true)
93 +let abortController: AbortController | null = null
94 +
95 +const InfoIcon = "carbon:information"
96 +const FilterIcon = "carbon:filter-edit"
97 +
98 +const totalAlertsSummary = computed<number>(() => {
99 + return alertsSummaryList.value.length || 0
100 +})
101 +const totalAlerts = computed<number>(() => {
102 + return alertsSummaryList.value.reduce((acc: number, val: AlertsSummaryExt) => {
103 + return acc + val.alerts.length
104 + }, 0)
105 +})
106 +
107 +const filters = ref<Partial<GraylogAlertsQuery>>({})
108 +
109 +function addIndexInfo() {
110 + if (indices.value?.length && alertsSummaryList.value.length) {
111 + for (const alert of alertsSummaryList.value) {
112 + const index = indices.value.find(o => o.index === alert.index_name)
113 + alert.indexStats = index
114 + }
115 + }
116 +}
117 +
118 +function getData() {
119 + loading.value = true
120 +
121 + abortController = new AbortController()
122 +
123 + Api.alerts
124 + .getGraylogAlertsList(filters.value, abortController.signal)
125 + .then(res => {
126 + alertsSummaryList.value = res.data?.alerts_summary || []
127 +
128 + if (res.data.success) {
129 + nextTick(() => {
130 + addIndexInfo()
131 + })
132 + } else {
133 + message.warning(res.data?.message || "An error occurred. Please try again later.")
134 + }
135 + })
136 + .catch(err => {
137 + if (!axios.isCancel(err)) {
138 + alertsSummaryList.value = []
139 +
140 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
141 + }
142 + })
143 + .finally(() => {
144 + loading.value = false
145 + })
146 +}
147 +
148 +function startSearch(closeDrawer?: boolean) {
149 + cancelSearch()
150 +
151 + setTimeout(() => {
152 + getData()
153 + }, 200)
154 +
155 + if (closeDrawer) {
156 + showFiltersDrawer.value = false
157 + }
158 +}
159 +
160 +function cancelSearch() {
161 + abortController?.abort()
162 +}
163 +
164 +provide<SocAlertField>("soc-alert-creation-field", "alert_id")
165 +
166 +onBeforeMount(() => {
167 + nextTick(() => {
168 + // MOCK
169 + // alertsSummaryList.value = alerts_summary as AlertsSummary[]
170 + startSearch()
171 + })
172 +})
173 +
174 +onMounted(() => {
175 + showFiltersDrawer.value = false
176 +
177 + setTimeout(() => {
178 + loadingFilters.value = false
179 + }, 1000)
180 +})
181 +
182 +onBeforeUnmount(() => {
183 + cancelSearch()
184 +})
185 +</script>
186 +
187 +<style lang="scss" scoped>
188 +.alerts-list {
189 + .list {
190 + container-type: inline-size;
191 + min-height: 200px;
192 + }
193 +}
194 +</style>
frontend/src/components/alerts/AlertsList.vue
+6 -6
@@ -43,13 +43,13 @@
43 <n-spin :show="loading">
44 <template #description>Alerts are being fetched, this may take up to 1 minute.</template>
45
46 - <div class="list my-3">
46 + <div class="list flex flex-col gap-2 my-3">
47 <template v-if="alertsSummaryList.length">
48 <AlertsSummaryItem
49 v-for="alertsSummary of alertsSummaryList"
50 :key="alertsSummary.index_name"
51 :alertsSummary="alertsSummary"
52 - class="item-appear item-appear-bottom item-appear-005 mb-2"
52 + class="item-appear item-appear-bottom item-appear-005"
53 />
54 </template>
55 <template v-else>
@@ -132,17 +132,17 @@
132 <script setup lang="ts">
133 // MOCK
134 // import { alerts_summary } from "./mock"
135 -// import type { AlertsSummary } from "@/types/alerts"
135 +// import type { AlertsSummary } from "@/types/alerts.d"
136
137 -import { ref, onBeforeMount, toRefs, computed, nextTick, onMounted, onBeforeUnmount } from "vue"
137 +import { ref, onBeforeMount, toRefs, computed, nextTick, onMounted, onBeforeUnmount, defineAsyncComponent } from "vue"
138 import { useMessage, NSpin, NPopover, NButton, NEmpty, NDrawer, NDrawerContent, NSelect } from "naive-ui"
139 import Api from "@/api"
140 -import ThreatIntelButton from "./ThreatIntelButton.vue"
140 +const ThreatIntelButton = defineAsyncComponent(() => import("@/components/threatIntel/ThreatIntelButton.vue"))
141 import AlertsStats, { type AlertsStatsCTX } from "./AlertsStats.vue"
142 import AlertsFilters from "./AlertsFilters.vue"
143 import AlertsSummaryItem, { type AlertsSummaryExt } from "./AlertsSummary.vue"
144 import Icon from "@/components/common/Icon.vue"
145 -import type { AlertsSummaryQuery } from "@/api/alerts"
145 +import type { AlertsSummaryQuery } from "@/api/endpoints/alerts"
146 import type { IndexStats } from "@/types/indices.d"
147 import axios from "axios"
148 import type { Agent } from "@/types/agents.d"
frontend/src/components/alerts/AlertsStats.vue
+1 -1
@@ -80,7 +80,7 @@ import { useMessage, NSpin, NEmpty, NTabs, NTabPane } from "naive-ui"
80 import Api from "@/api"
81 import AlertsStatsItem from "./AlertsStatsItem.vue"
82 import type { AlertsByHost, AlertsByRule, AlertsByRulePerHost } from "@/types/alerts.d"
83 -import type { AlertsSummaryQuery } from "@/api/alerts"
83 +import type { AlertsSummaryQuery } from "@/api/endpoints/alerts"
84 import axios from "axios"
85 import { onMounted } from "vue"
86 // import { alerts_by_host, alerts_by_rule, alerts_by_rule_per_host } from "./mock"
frontend/src/components/alerts/type.d.ts new
+1
@@ -0,0 +1 @@
1 +export type SocAlertField = "alert_url" | "alert_id"
frontend/src/components/artifacts/ArtifactRecommendation.vue renamed
+10 -10
@@ -24,6 +24,7 @@
24 :options="osOptions"
25 class="!w-32"
26 placeholder="Select OS"
27 + :disabled="loading"
28 />
29 <n-button
30 size="small"
@@ -65,23 +66,22 @@
66 </template>
67
68 <script setup lang="ts">
68 -import Icon from "@/components/common/Icon.vue"
69 -import { useMessage, NModal, NSpin, NEmpty, NButton, NDivider, NSelect, NCard } from "naive-ui"
70 -import type { Size } from "naive-ui/es/button/src/interface"
69 import { computed, ref } from "vue"
70 +import { useMessage, NModal, NSpin, NEmpty, NButton, NDivider, NSelect, NCard } from "naive-ui"
71 +import Icon from "@/components/common/Icon.vue"
72 import Api from "@/api"
73 import _uniqBy from "lodash/uniqBy"
74 -import type { SocAlert } from "@/types/soc/alert"
75 -import type { OsTypesFull } from "@/types/common"
76 -import type { Recommendation } from "@/types/artifacts"
74 +import type { OsTypesFull } from "@/types/common.d"
75 +import type { Recommendation } from "@/types/artifacts.d"
76 +import type { Size } from "naive-ui/es/button/src/interface"
77
78 interface RecommendationStore {
79 os: OsTypesFull
80 recommendation: Recommendation[]
81 }
82
83 -const { alert, size } = defineProps<{
84 - alert: SocAlert
83 +const { context, size } = defineProps<{
84 + context: string | object
85 size?: Size
86 }>()
87
@@ -107,13 +107,13 @@ function openRecommendations() {
107 }
108
109 function getRecommendations() {
110 - if (selectedOs.value !== null && alert) {
110 + if (selectedOs.value !== null && context) {
111 loading.value = true
112
113 const requestedOs = selectedOs.value
114
115 Api.artifacts
116 - .getArtifactRecommendation({ os: requestedOs, prompt: alert.alert_context })
116 + .getArtifactRecommendation({ os: requestedOs, prompt: context })
117 .then(res => {
118 if (res.data.success) {
119 recommendationsStore.value.push({
frontend/src/components/artifacts/ArtifactsCollect.vue
+1 -1
@@ -93,7 +93,7 @@ import Api from "@/api"
93 import Icon from "@/components/common/Icon.vue"
94 import CollectItem from "./CollectItem.vue"
95 import type { Agent } from "@/types/agents.d"
96 -import type { ArtifactsQuery, CollectRequest } from "@/api/artifacts"
96 +import type { ArtifactsQuery, CollectRequest } from "@/api/endpoints/artifacts"
97 import type { Artifact, CollectResult } from "@/types/artifacts.d"
98 import { nanoid } from "nanoid"
99 // import { collectResult } from "./mock"
frontend/src/components/artifacts/ArtifactsCommand.vue
+3 -3
@@ -54,7 +54,7 @@
54 <div class="badges-box flex gap-2 flex-wrap grow">
55 <n-tooltip trigger="hover" v-if="commandTime">
56 <template #trigger>
57 - <Badge type="splitted" hint-cursor>
57 + <Badge type="splitted" color="primary" hint-cursor>
58 <template #iconLeft>
59 <Icon :name="TimeIcon"></Icon>
60 </template>
@@ -72,7 +72,7 @@
72 Last request time / last response time
73 </n-tooltip>
74
75 - <Badge type="splitted" v-if="diffTime">
75 + <Badge type="splitted" color="primary" v-if="diffTime">
76 <template #iconLeft>
77 <Icon :name="StopWatchIcon" :size="15"></Icon>
78 </template>
@@ -119,7 +119,7 @@ import Api from "@/api"
119 import CommandItem from "./CommandItem.vue"
120 import Badge from "@/components/common/Badge.vue"
121 import type { Agent } from "@/types/agents.d"
122 -import type { CommandRequest } from "@/api/artifacts"
122 +import type { CommandRequest } from "@/api/endpoints/artifacts"
123 import type { Artifact, CommandResult } from "@/types/artifacts.d"
124 import dayjs from "@/utils/dayjs"
125 import Icon from "@/components/common/Icon.vue"
frontend/src/components/artifacts/ArtifactsList.vue
+3 -3
@@ -98,13 +98,13 @@
98 </n-popover>
99 </div>
100 <n-spin :show="loading">
101 - <div class="list my-3">
101 + <div class="list flex flex-col gap-2 my-3">
102 <template v-if="artifactsList.length">
103 <ArtifactItem
104 v-for="artifact of itemsPaginated"
105 :key="artifact.name"
106 :artifact="artifact"
107 - class="item-appear item-appear-bottom item-appear-005 mb-2"
107 + class="item-appear item-appear-bottom item-appear-005"
108 />
109 </template>
110 <template v-else>
@@ -132,7 +132,7 @@ import _cloneDeep from "lodash/cloneDeep"
132 import Icon from "@/components/common/Icon.vue"
133 import ArtifactItem from "./ArtifactItem.vue"
134 import type { Agent } from "@/types/agents.d"
135 -import type { ArtifactsQuery } from "@/api/artifacts"
135 +import type { ArtifactsQuery } from "@/api/endpoints/artifacts"
136 import type { Artifact } from "@/types/artifacts.d"
137 import { useResizeObserver } from "@vueuse/core"
138
frontend/src/components/artifacts/ArtifactsQuarantine.vue
+1 -1
@@ -84,7 +84,7 @@ import Api from "@/api"
84 import Icon from "@/components/common/Icon.vue"
85 import QuarantineItem from "./QuarantineItem.vue"
86 import type { Agent } from "@/types/agents.d"
87 -import type { QuarantineRequest } from "@/api/artifacts"
87 +import type { QuarantineRequest } from "@/api/endpoints/artifacts"
88 import type { Artifact, QuarantineResult } from "@/types/artifacts.d"
89 // import { quarantineResult } from "./mock"
90
frontend/src/components/artifacts/CollectItem.vue
-4
@@ -123,10 +123,6 @@ onBeforeMount(() => {
123 @container (max-width: 500px) {
124 flex-direction: column;
125
126 - .kv-card {
127 - flex-basis: initial;
128 - flex-grow: initial;
129 - }
126 .hide-mobile {
127 display: none;
128 }
frontend/src/components/cloudSecurityAssessment/AvailableReportsList.vue
+1 -1
@@ -30,7 +30,7 @@
30 </div>
31 </div>
32 <n-spin :show="loading" class="min-h-32">
33 - <div class="list grid gap-4 grid-auto-flow-250">
33 + <div class="list grid gap-4 grid-auto-fill-250">
34 <template v-if="reportsList.length">
35 <AvailableReportsItem
36 v-for="report of reportsList"
frontend/src/components/cloudSecurityAssessment/CreationReportForm.vue
+1 -1
@@ -75,7 +75,7 @@ import {
75 } from "@/types/cloudSecurityAssessment.d"
76 import AwsTypeForm from "./FormTypes/AwsTypeForm.vue"
77 import AzureTypeForm from "./FormTypes/AzureTypeForm.vue"
78 -import type { ApiError, ApiCommonResponse } from "@/types/common"
78 +import type { ApiError, ApiCommonResponse } from "@/types/common.d"
79
80 type BaseFormPayload = Omit<ScoutSuiteReportPayload, "report_type"> & { report_type: ScoutSuiteReportType | null }
81 type TypeFormPayload = ScoutSuiteAwsReportPayload | ScoutSuiteAzureReportPayload
frontend/src/components/common/Badge.vue
+51 -5
@@ -3,7 +3,7 @@
3 :is="!!href ? 'a' : 'div'"
4 class="badge"
5 :href="href"
6 - :class="[type, color, { 'cursor-help': hintCursor, 'cursor-pointer': pointCursor, fluid }]"
6 + :class="[type, color, { 'cursor-help': hintCursor, 'cursor-pointer': pointCursor, fluid, bright }]"
7 >
8 <span v-if="$slots.label || $slots.iconLeft || $slots.iconRight" class="flex items-center gap-2">
9 <slot name="iconLeft"></slot>
@@ -23,7 +23,8 @@ const { type, hintCursor, pointCursor, color, href, fluid } = defineProps<{
23 hintCursor?: boolean
24 pointCursor?: boolean
25 fluid?: boolean
26 - color?: "danger" | "warning" | "success"
26 + bright?: boolean
27 + color?: "danger" | "warning" | "success" | "primary"
28 href?: string
29 }>()
30 </script>
@@ -65,6 +66,7 @@ const { type, hintCursor, pointCursor, color, href, fluid } = defineProps<{
66
67 span:not(:last-child) {
68 border-right: var(--border-small-100);
69 + padding-right: 8px;
70 }
71
72 &.splitted {
@@ -78,8 +80,7 @@ const { type, hintCursor, pointCursor, color, href, fluid } = defineProps<{
80 line-height: 24px;
81
82 &:first-child {
81 - background-color: var(--primary-005-color);
82 - line-height: 1.1;
83 + background-color: var(--divider-005-color);
84 white-space: nowrap;
85 }
86 &:last-child {
@@ -102,7 +103,6 @@ const { type, hintCursor, pointCursor, color, href, fluid } = defineProps<{
103 }
104 }
105 }
105 -
106 &.success {
107 & > span {
108 &:first-child {
@@ -110,6 +110,52 @@ const { type, hintCursor, pointCursor, color, href, fluid } = defineProps<{
110 }
111 }
112 }
113 + &.primary {
114 + & > span {
115 + &:first-child {
116 + background-color: var(--primary-005-color);
117 + }
118 + }
119 + }
120 +
121 + &.bright {
122 + &.danger {
123 + border-color: var(--secondary4-opacity-030-color);
124 +
125 + & > span {
126 + &:first-child {
127 + background-color: var(--secondary4-opacity-020-color);
128 + }
129 + }
130 + }
131 + &.warning {
132 + border-color: var(--secondary3-opacity-030-color);
133 +
134 + & > span {
135 + &:first-child {
136 + background-color: var(--secondary3-opacity-020-color);
137 + }
138 + }
139 + }
140 + &.success {
141 + border-color: rgba(var(--success-color-rgb), 0.3);
142 +
143 + & > span {
144 + &:first-child {
145 + background-color: rgba(var(--success-color-rgb), 0.2);
146 + }
147 + }
148 + }
149 + &.primary {
150 + border-color: var(--primary-030-color);
151 +
152 + & > span {
153 + &:first-child {
154 + background-color: var(--primary-020-color);
155 + }
156 + }
157 + }
158 + }
159 }
160
161 &.fluid {
frontend/src/components/common/KVCard.vue
+59 -3
@@ -1,10 +1,17 @@
1 <template>
2 - <div class="kv-card">
2 + <div class="kv-card" :class="[`kv-color-${color}`, `kv-size-${size}`]">
3 <div class="key" v-if="$slots.key"><slot name="key"></slot></div>
4 <div class="value" v-if="$slots.value"><slot name="value"></slot></div>
5 </div>
6 </template>
7
8 +<script setup lang="ts">
9 +const { color } = defineProps<{
10 + color?: "danger" | "warning" | "success" | "primary"
11 + size?: "lg"
12 +}>()
13 +</script>
14 +
15 <style lang="scss" scoped>
16 .kv-card {
17 border: var(--border-small-100);
@@ -13,8 +20,6 @@
20 overflow: hidden;
21 display: flex;
22 flex-direction: column;
16 - flex-basis: 140px;
17 - flex-grow: 1;
23
24 .key {
25 border-bottom: var(--border-small-050);
@@ -30,5 +35,56 @@
35 flex-grow: 1;
36 word-break: break-word;
37 }
38 +
39 + &.kv-size- {
40 + &lg {
41 + .key {
42 + padding: 10px 12px;
43 + font-size: 14px;
44 + }
45 + .value {
46 + font-size: 16px;
47 + padding: 12px 12px;
48 + }
49 + }
50 + }
51 +
52 + &.kv-color- {
53 + &danger {
54 + border-color: var(--secondary4-opacity-030-color);
55 + background-color: var(--secondary4-opacity-020-color);
56 +
57 + .key {
58 + border-color: var(--secondary4-opacity-030-color);
59 + }
60 + }
61 +
62 + &warning {
63 + border-color: var(--secondary3-opacity-030-color);
64 + background-color: var(--secondary3-opacity-020-color);
65 +
66 + .key {
67 + border-color: var(--secondary3-opacity-030-color);
68 + }
69 + }
70 +
71 + &success {
72 + border-color: rgba(var(--success-color-rgb), 0.3);
73 + background-color: rgba(var(--success-color-rgb), 0.2);
74 +
75 + .key {
76 + border-color: rgba(var(--success-color-rgb), 0.3);
77 + }
78 + }
79 +
80 + &primary {
81 + border-color: var(--primary-030-color);
82 + background-color: var(--primary-020-color);
83 +
84 + .key {
85 + border-color: var(--primary-030-color);
86 + }
87 + }
88 + }
89 }
90 </style>
frontend/src/components/common/LayoutSettings.vue
+4 -4
@@ -72,7 +72,7 @@
72 <div class="ls-section ls-nav-selection">
73 <div class="ls-label">
74 Navbar
75 - <span v-if="isMobileView" class="text-secondary-color">(desktop only)</span>
75 + <span v-if="isMobileView" class="opacity-50 px-1">desktop only</span>
76 </div>
77 <div class="flex items-center gap-2">
78 <div class="basis-1/2">
@@ -104,14 +104,14 @@
104 <div class="flex justify-between items-center">
105 <div class="switch-label">
106 View boxed
107 - <span v-if="isMobileView" class="text-secondary-color px-1">desktop only</span>
107 + <span v-if="isMobileView" class="opacity-50 px-1">desktop only</span>
108 </div>
109 <n-switch v-model:value="boxed" :disabled="isMobileView" size="small" />
110 </div>
111 <div class="flex justify-between items-center">
112 <div class="switch-label">
113 Toolbar boxed
114 - <span v-if="isMobileView" class="text-secondary-color px-1">desktop only</span>
114 + <span v-if="isMobileView" class="opacity-50 px-1">desktop only</span>
115 </div>
116 <n-switch
117 v-model:value="toolbarBoxed"
@@ -126,7 +126,7 @@
126 <div class="flex justify-between items-center">
127 <div class="switch-label">
128 RTL
129 - <span class="text-secondary-color px-1">beta</span>
129 + <span class="opacity-50 px-1">beta</span>
130 </div>
131 <n-switch v-model:value="rtl" size="small" />
132 </div>
frontend/src/components/customers/CustomerInfo.vue
+3 -3
@@ -23,7 +23,7 @@
23 </n-button>
24 </div>
25
26 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
26 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4">
27 <KVCard v-for="(value, key) of customer" :key="key">
28 <template #key>{{ key }}</template>
29 <template #value>{{ value || "-" }}</template>
@@ -34,12 +34,12 @@
34 </template>
35
36 <script setup lang="ts">
37 -import Icon from "@/components/common/Icon.vue"
37 import { h, ref, toRefs, watch } from "vue"
38 +import { useMessage, NButton, useDialog } from "naive-ui"
39 +import Icon from "@/components/common/Icon.vue"
40 import KVCard from "@/components/common/KVCard.vue"
41 import CustomerForm from "./CustomerForm.vue"
42 import Api from "@/api"
42 -import { useMessage, NButton, useDialog } from "naive-ui"
43 import type { Customer } from "@/types/customers.d"
44
45 const emit = defineEmits<{
frontend/src/components/customers/CustomerItem.vue
+26 -13
@@ -29,7 +29,7 @@
29 </div>
30
31 <div class="badges-box flex flex-wrap items-center gap-3 mt-2">
32 - <Badge type="splitted">
32 + <Badge type="splitted" color="primary">
33 <template #iconLeft>
34 <Icon :name="UserTypeIcon" :size="14"></Icon>
35 </template>
@@ -39,7 +39,7 @@
39
40 <n-popover trigger="hover" :disabled="addressLabel === '-'">
41 <template #trigger>
42 - <Badge type="splitted" :class="{ 'cursor-help': addressLabel !== '-' }">
42 + <Badge type="splitted" color="primary" :class="{ 'cursor-help': addressLabel !== '-' }">
43 <template #iconLeft>
44 <Icon :name="LocationIcon" :size="13"></Icon>
45 </template>
@@ -77,14 +77,14 @@
77 </div>
78 </n-popover>
79
80 - <Badge type="splitted">
80 + <Badge type="splitted" color="primary">
81 <template #iconLeft>
82 <Icon :name="PhoneIcon" :size="13"></Icon>
83 </template>
84 <template #value>{{ customerInfo?.phone || "-" }}</template>
85 </Badge>
86
87 - <Badge type="splitted" v-if="customerInfo?.parent_customer_code">
87 + <Badge type="splitted" color="primary" v-if="customerInfo?.parent_customer_code">
88 <template #iconLeft>
89 <Icon :name="ParentIcon" :size="13"></Icon>
90 </template>
@@ -139,6 +139,13 @@
139 :customerName="customer.customer_name"
140 />
141 </n-tab-pane>
142 + <n-tab-pane
143 + name="Notification Workflows"
144 + tab="Notification Workflows"
145 + display-directive="show:lazy"
146 + >
147 + <CustomerNotificationsWorkflows :customerCode="customer.customer_code" />
148 + </n-tab-pane>
149 <template #suffix>
150 <div class="pr-8 hover:text-primary-color cursor-pointer" @click="selectedTabsGroup = 'agents'">
151 Agents
@@ -186,20 +193,26 @@
193 </template>
194
195 <script setup lang="ts">
196 +import { computed, defineAsyncComponent, onBeforeMount, ref, toRefs, watch } from "vue"
197 +import { NAvatar, useMessage, NPopover, NModal, NTabs, NTabPane, NSpin, NScrollbar, NButton } from "naive-ui"
198 import Icon from "@/components/common/Icon.vue"
199 import Badge from "@/components/common/Badge.vue"
191 -import { computed, onBeforeMount, ref, toRefs, watch } from "vue"
192 -import CustomerInfo from "./CustomerInfo.vue"
193 -import CustomerAgents from "./CustomerAgents.vue"
194 -import CustomerProvision from "./provision/CustomerProvision.vue"
195 -import CustomerHealthcheckList from "./healthcheck/CustomerHealthcheckList.vue"
196 -import CustomerIntegrations from "./integrations/CustomerIntegrations.vue"
197 -import CustomerNetworkConnectors from "./networkConnectors/CustomerNetworkConnectors.vue"
200 import Api from "@/api"
199 -import { NAvatar, useMessage, NPopover, NModal, NTabs, NTabPane, NSpin, NScrollbar, NButton } from "naive-ui"
200 -import type { Customer, CustomerMeta } from "@/types/customers.d"
201 import { hashMD5 } from "@/utils"
202 import _toSafeInteger from "lodash/toSafeInteger"
203 +import type { Customer, CustomerMeta } from "@/types/customers.d"
204 +
205 +const CustomerInfo = defineAsyncComponent(() => import("./CustomerInfo.vue"))
206 +const CustomerAgents = defineAsyncComponent(() => import("./CustomerAgents.vue"))
207 +const CustomerProvision = defineAsyncComponent(() => import("./provision/CustomerProvision.vue"))
208 +const CustomerHealthcheckList = defineAsyncComponent(() => import("./healthcheck/CustomerHealthcheckList.vue"))
209 +const CustomerIntegrations = defineAsyncComponent(() => import("./integrations/CustomerIntegrations.vue"))
210 +const CustomerNetworkConnectors = defineAsyncComponent(
211 + () => import("./networkConnectors/CustomerNetworkConnectors.vue")
212 +)
213 +const CustomerNotificationsWorkflows = defineAsyncComponent(
214 + () => import("./notifications/CustomerNotificationsWorkflows.vue")
215 +)
216
217 const emit = defineEmits<{
218 (e: "delete"): void
frontend/src/components/customers/healthcheck/CustomerHealthcheckItem.vue
+4 -4
@@ -24,19 +24,19 @@
24 </div>
25
26 <div class="badges-box flex flex-wrap items-center gap-3 mt-2">
27 - <Badge type="splitted" v-if="agentVersion">
27 + <Badge type="splitted" color="primary" v-if="agentVersion">
28 <template #label>Agent version</template>
29 <template #value>{{ agentVersion }}</template>
30 </Badge>
31
32 - <Badge type="splitted" v-if="source === 'velociraptor'">
32 + <Badge type="splitted" color="primary" v-if="source === 'velociraptor'">
33 <template #label>Velociraptor Id</template>
34 <template #value>{{ healthData.velociraptor_id }}</template>
35 </Badge>
36
37 <n-popover overlap placement="bottom-start">
38 <template #trigger>
39 - <Badge type="splitted" hint-cursor>
39 + <Badge type="splitted" color="primary" hint-cursor>
40 <template #iconLeft>
41 <Icon :name="AgentIcon" :size="13" class="!opacity-80"></Icon>
42 </template>
@@ -72,7 +72,7 @@
72 :bordered="false"
73 segmented
74 >
75 - <div class="grid gap-2 grid-auto-flow-200 px-7 py-6">
75 + <div class="grid gap-2 grid-auto-fit-200 px-7 py-6">
76 <KVCard v-for="(value, key) of healthData" :key="key">
77 <template #key>{{ key }}</template>
78 <template #value>{{ value || "-" }}</template>
frontend/src/components/customers/healthcheck/CustomerHealthcheckList.vue
+1 -1
@@ -61,7 +61,7 @@ import CustomerHealthcheckItem from "./CustomerHealthcheckItem.vue"
61 import { useMessage, NSpin, NEmpty, NSelect, NInputGroup, NInputNumber } from "naive-ui"
62 import type { CustomerAgentHealth, CustomerHealthcheckSource } from "@/types/customers.d"
63 import { watchDebounced } from "@vueuse/core"
64 -import type { CustomerAgentsHealthcheckQuery } from "@/api/customers"
64 +import type { CustomerAgentsHealthcheckQuery } from "@/api/endpoints/customers"
65
66 const { source, customerCode } = defineProps<{
67 source: CustomerHealthcheckSource
frontend/src/components/customers/integrations/CustomerIntegrationActions.vue
+1 -1
@@ -28,7 +28,7 @@ import Icon from "@/components/common/Icon.vue"
28 import Api from "@/api"
29 import type { CustomerIntegration } from "@/types/integrations.d"
30 import type { Size } from "naive-ui/es/button/src/interface"
31 -import type { ApiCommonResponse } from "@/types/common"
31 +import type { ApiCommonResponse } from "@/types/common.d"
32
33 const emit = defineEmits<{
34 (e: "startLoading"): void
frontend/src/components/customers/integrations/CustomerIntegrationForm.vue
+1 -1
@@ -85,7 +85,7 @@ import { useMessage, NFormItem, NInput, NSelect, NButton, NScrollbar, NSteps, NS
85 import Icon from "@/components/common/Icon.vue"
86 import Api from "@/api"
87 import IntegrationsList from "@/components/integrations/IntegrationsList.vue"
88 -import type { NewIntegration } from "@/api/integrations"
88 +import type { NewIntegration } from "@/api/endpoints/integrations"
89 import type { ServiceItemData } from "@/components/services/types"
90
91 interface AuthKeysInput {
frontend/src/components/customers/integrations/CustomerIntegrationItem.vue
+1 -1
@@ -49,7 +49,7 @@
49 :bordered="false"
50 segmented
51 >
52 - <div class="grid gap-2 grid-auto-flow-200">
52 + <div class="grid gap-2 grid-auto-fit-200">
53 <KVCard v-for="ak of authKeys" :key="ak.key">
54 <template #key>{{ ak.key }}</template>
55 <template #value>{{ ak.value || "-" }}</template>
frontend/src/components/customers/networkConnectors/CustomerNetworkConnectorActions.vue
+3 -4
@@ -70,14 +70,13 @@
70 </template>
71
72 <script setup lang="ts">
73 +import { computed, h, watch, ref } from "vue"
74 import { NButton, NModal, NSpin, useDialog, useMessage } from "naive-ui"
75 import Icon from "@/components/common/Icon.vue"
76 import Api from "@/api"
76 -import { computed, h, ref } from "vue"
77 -import { watch } from "vue"
77 import type { Size } from "naive-ui/es/button/src/interface"
79 -import type { CustomerNetworkConnector } from "@/types/networkConnectors"
80 -import type { FortinetProvision } from "@/api/networkConnectors"
78 +import type { CustomerNetworkConnector } from "@/types/networkConnectors.d"
79 +import type { FortinetProvision } from "@/api/endpoints/networkConnectors"
80 import FortinetForm, { type FortinetModel } from "./provisions/FortinetForm.vue"
81
82 const emit = defineEmits<{
frontend/src/components/customers/networkConnectors/CustomerNetworkConnectorForm.vue
+1 -1
@@ -86,7 +86,7 @@ import Icon from "@/components/common/Icon.vue"
86 import Api from "@/api"
87 import NetworkConnectorsList from "@/components/networkConnectors/NetworkConnectorsList.vue"
88 import type { ServiceItemData } from "@/components/services/types"
89 -import type { NewNetworkConnector } from "@/api/networkConnectors"
89 +import type { NewNetworkConnector } from "@/api/endpoints/networkConnectors"
90
91 interface AuthKeysInput {
92 key: string
frontend/src/components/customers/networkConnectors/CustomerNetworkConnectorItem.vue
+4 -4
@@ -51,7 +51,7 @@
51 :bordered="false"
52 segmented
53 >
54 - <div class="grid gap-2 grid-auto-flow-200">
54 + <div class="grid gap-2 grid-auto-fit-200">
55 <KVCard v-for="ak of authKeys" :key="ak.key">
56 <template #key>{{ ak.key }}</template>
57 <template #value>{{ ak.value || "-" }}</template>
@@ -62,14 +62,14 @@
62 </template>
63
64 <script setup lang="ts">
65 -import Icon from "@/components/common/Icon.vue"
66 -import Badge from "@/components/common/Badge.vue"
65 import { computed, ref, toRefs } from "vue"
66 import { NModal, NButton } from "naive-ui"
67 import CustomerNetworkConnectorActions from "./CustomerNetworkConnectorActions.vue"
68 +import Icon from "@/components/common/Icon.vue"
69 +import Badge from "@/components/common/Badge.vue"
70 import KVCard from "@/components/common/KVCard.vue"
71 import _uniqBy from "lodash/uniqBy"
72 -import type { CustomerNetworkConnector } from "@/types/networkConnectors"
72 +import type { CustomerNetworkConnector } from "@/types/networkConnectors.d"
73
74 const props = defineProps<{
75 networkConnector: CustomerNetworkConnector
frontend/src/components/customers/networkConnectors/CustomerNetworkConnectors.vue
+1 -1
@@ -55,7 +55,7 @@ import Icon from "@/components/common/Icon.vue"
55 import Api from "@/api"
56 import CustomerNetworkConnectorForm from "./CustomerNetworkConnectorForm.vue"
57 import CustomerNetworkConnectorItem from "./CustomerNetworkConnectorItem.vue"
58 -import type { CustomerNetworkConnector } from "@/types/networkConnectors"
58 +import type { CustomerNetworkConnector } from "@/types/networkConnectors.d"
59
60 const { customerCode, customerName } = defineProps<{
61 customerCode: string
frontend/src/components/customers/notifications/CustomerNotificationsWorkflows.vue new
+135
@@ -0,0 +1,135 @@
1 +<template>
2 + <div class="customer-notifications-workflows">
3 + <transition name="form-fade" mode="out-in">
4 + <div v-if="showForm" class="p-7 pt-4">
5 + <CustomerNotificationsWorkflowsForm
6 + :customerCode
7 + @mounted="formCTX = $event"
8 + @submitted="refreshList()"
9 + >
10 + <template #additionalActions="{ loading }">
11 + <n-button @click="closeForm()" :disabled="loading">Close</n-button>
12 + </template>
13 + </CustomerNotificationsWorkflowsForm>
14 + </div>
15 + <div v-else>
16 + <n-spin :show="loading" class="min-h-48">
17 + <template v-if="list.length">
18 + <div class="list p-7 pt-4">
19 + <CustomerNotificationsWorkflowsItem
20 + v-for="item of list"
21 + :key="item.id"
22 + :incidentNotification="item"
23 + embedded
24 + @updated="getCustomerNetworkConnectors()"
25 + class="item-appear item-appear-bottom item-appear-005 mb-2"
26 + />
27 + </div>
28 + </template>
29 + <template v-else>
30 + <n-empty class="justify-center h-48" v-if="!loading">
31 + <div class="flex flex-col items-center gap-4">
32 + <p>No Notification found</p>
33 +
34 + <n-button size="small" @click="openForm()" type="primary">
35 + <template #icon>
36 + <Icon :name="AddIcon" :size="14"></Icon>
37 + </template>
38 + Create a Notification
39 + </n-button>
40 + </div>
41 + </n-empty>
42 + </template>
43 + </n-spin>
44 + </div>
45 + </transition>
46 + </div>
47 +</template>
48 +
49 +<script setup lang="ts">
50 +import { ref, onBeforeMount, defineAsyncComponent } from "vue"
51 +import { useMessage, NSpin, NEmpty, NButton } from "naive-ui"
52 +import Icon from "@/components/common/Icon.vue"
53 +import Api from "@/api"
54 +import type { IncidentNotification } from "@/types/incidentManagement/notifications.d"
55 +
56 +const CustomerNotificationsWorkflowsItem = defineAsyncComponent(
57 + () => import("./CustomerNotificationsWorkflowsItem.vue")
58 +)
59 +const CustomerNotificationsWorkflowsForm = defineAsyncComponent(
60 + () => import("./CustomerNotificationsWorkflowsForm.vue")
61 +)
62 +
63 +const { customerCode } = defineProps<{
64 + customerCode: string
65 +}>()
66 +
67 +const AddIcon = "carbon:add-alt"
68 +
69 +const message = useMessage()
70 +const showForm = ref(false)
71 +const loading = ref(false)
72 +const list = ref<IncidentNotification[]>([])
73 +const formCTX = ref<{ reset: (incidentNotification?: IncidentNotification) => void } | null>(null)
74 +
75 +function getCustomerNetworkConnectors() {
76 + loading.value = true
77 +
78 + Api.incidentManagement
79 + .getNotifications(customerCode)
80 + .then(res => {
81 + if (res.data.success) {
82 + list.value = res.data?.notifications || []
83 + } else {
84 + message.warning(res.data?.message || "An error occurred. Please try again later.")
85 + }
86 + })
87 + .catch(err => {
88 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
89 + })
90 + .finally(() => {
91 + loading.value = false
92 + })
93 +}
94 +
95 +function openForm() {
96 + showForm.value = true
97 +}
98 +
99 +function closeForm() {
100 + showForm.value = false
101 +}
102 +
103 +function refreshList() {
104 + closeForm()
105 + getCustomerNetworkConnectors()
106 +}
107 +
108 +onBeforeMount(() => {
109 + getCustomerNetworkConnectors()
110 +})
111 +</script>
112 +
113 +<style lang="scss" scoped>
114 +.customer-notifications-workflows {
115 + .list {
116 + container-type: inline-size;
117 + min-height: 200px;
118 + }
119 +
120 + .form-fade-enter-active,
121 + .form-fade-leave-active {
122 + transition:
123 + opacity 0.2s ease-in-out,
124 + transform 0.3s ease-in-out;
125 + }
126 + .form-fade-enter-from {
127 + opacity: 0;
128 + transform: translateY(10px);
129 + }
130 + .form-fade-leave-to {
131 + opacity: 0;
132 + transform: translateY(-10px);
133 + }
134 +}
135 +</style>
frontend/src/components/customers/notifications/CustomerNotificationsWorkflowsForm.vue new
+158
@@ -0,0 +1,158 @@
1 +<template>
2 + <div class="customer-notifications-workflows-form flex flex-col justify-between grow">
3 + <div class="form-box">
4 + <n-spin v-model:show="loading">
5 + <n-form :label-width="80" :model="form" :rules="rules" ref="formRef">
6 + <n-form-item label="Enabled" path="enabled">
7 + <n-switch v-model:value="form.enabled" />
8 + </n-form-item>
9 + <n-form-item label="Shuffle Workflow Id" path="shuffle_workflow_id">
10 + <n-input
11 + v-model:value.trim="form.shuffle_workflow_id"
12 + placeholder="Input the Shuffle Workflow Id..."
13 + clearable
14 + />
15 + </n-form-item>
16 + </n-form>
17 + </n-spin>
18 + </div>
19 + <div class="buttons-box flex justify-between gap-3">
20 + <div class="flex gap-3">
21 + <slot name="additionalActions" :loading></slot>
22 + </div>
23 + <div class="flex gap-3">
24 + <n-button @click="reset()" secondary :disabled="loading">Reset</n-button>
25 + <n-button type="primary" :disabled="!isValid" @click="validate()" :loading="loading">Submit</n-button>
26 + </div>
27 + </div>
28 + </div>
29 +</template>
30 +
31 +<script setup lang="ts">
32 +import { computed, onMounted, ref, watch } from "vue"
33 +import {
34 + NButton,
35 + NForm,
36 + NFormItem,
37 + NInput,
38 + NSwitch,
39 + NSpin,
40 + useMessage,
41 + type FormRules,
42 + type FormInst,
43 + type FormValidationError
44 +} from "naive-ui"
45 +import Api from "@/api"
46 +import type { IncidentNotification, IncidentNotificationPayload } from "@/types/incidentManagement/notifications.d"
47 +
48 +interface IncidentNotificationForm {
49 + shuffle_workflow_id: string
50 + enabled: boolean
51 +}
52 +
53 +const emit = defineEmits<{
54 + (e: "submitted", value: IncidentNotification): void
55 + (e: "startLoading"): void
56 + (e: "stopLoading"): void
57 + (
58 + e: "mounted",
59 + value: {
60 + reset: () => void
61 + }
62 + ): void
63 +}>()
64 +
65 +const { incidentNotification, customerCode } = defineProps<{
66 + incidentNotification?: IncidentNotification
67 + customerCode: string
68 +}>()
69 +
70 +const message = useMessage()
71 +const form = ref<IncidentNotificationForm>(getClearForm(incidentNotification))
72 +const formRef = ref<FormInst | null>(null)
73 +const loading = ref(false)
74 +
75 +const isValid = computed(() => {
76 + return !!form.value.shuffle_workflow_id
77 +})
78 +
79 +const rules: FormRules = {
80 + shuffle_workflow_id: {
81 + required: true,
82 + message: "Please insert Shuffle Workflow Id",
83 + trigger: ["input", "blur"]
84 + }
85 +}
86 +
87 +watch(loading, val => {
88 + if (val) {
89 + emit("startLoading")
90 + } else {
91 + emit("stopLoading")
92 + }
93 +})
94 +
95 +function getClearForm(incidentNotification?: IncidentNotification): IncidentNotificationForm {
96 + return {
97 + shuffle_workflow_id: incidentNotification?.shuffle_workflow_id ?? "",
98 + enabled: incidentNotification?.enabled ?? false
99 + }
100 +}
101 +
102 +function reset(incidentNotification?: IncidentNotification) {
103 + if (!loading.value) {
104 + resetForm(incidentNotification)
105 + formRef.value?.restoreValidation()
106 + }
107 +}
108 +
109 +function resetForm(incidentNotification?: IncidentNotification) {
110 + form.value = getClearForm(incidentNotification)
111 +}
112 +
113 +function validate() {
114 + if (!formRef.value) return
115 +
116 + formRef.value.validate((errors?: Array<FormValidationError>) => {
117 + if (!errors) {
118 + submit()
119 + } else {
120 + message.warning("You must fill in the required fields correctly.")
121 + return false
122 + }
123 + })
124 +}
125 +
126 +function submit() {
127 + loading.value = true
128 +
129 + const payload: IncidentNotificationPayload = {
130 + customer_code: customerCode,
131 + shuffle_workflow_id: form.value.shuffle_workflow_id,
132 + enabled: form.value.enabled
133 + }
134 +
135 + Api.incidentManagement
136 + .setNotification(payload)
137 + .then(res => {
138 + if (res.data.success) {
139 + message.success(res.data?.message || "Notification saved successfully")
140 + emit("submitted", { ...payload, id: incidentNotification?.id || 0 })
141 + } else {
142 + message.warning(res.data?.message || "An error occurred. Please try again later.")
143 + }
144 + })
145 + .catch(err => {
146 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
147 + })
148 + .finally(() => {
149 + loading.value = false
150 + })
151 +}
152 +
153 +onMounted(() => {
154 + emit("mounted", {
155 + reset
156 + })
157 +})
158 +</script>
frontend/src/components/customers/notifications/CustomerNotificationsWorkflowsItem.vue new
+123
@@ -0,0 +1,123 @@
1 +<template>
2 + <div class="customer-notifications-workflows-item" :class="{ embedded }" @click="openForm()">
3 + <div class="px-4 py-3 flex flex-col gap-1">
4 + <div class="header-box flex justify-between items-center">
5 + <div class="label">shuffle_workflow_id</div>
6 + <div class="status flex gap-2 items-center">
7 + <span>{{ incidentNotification.enabled ? "Enabled" : "Disabled" }}</span>
8 + <Icon
9 + :name="EnabledIcon"
10 + :size="14"
11 + class="text-success-color"
12 + v-if="incidentNotification.enabled"
13 + ></Icon>
14 + <Icon :name="DisabledIcon" :size="14" class="text-secondary-color" v-else></Icon>
15 + </div>
16 + </div>
17 + <div class="main-box flex items-center gap-3">
18 + <div class="content flex flex-col grow">
19 + <div class="title">{{ incidentNotification.shuffle_workflow_id }}</div>
20 + </div>
21 + </div>
22 + </div>
23 +
24 + <n-modal
25 + v-model:show="showForm"
26 + preset="card"
27 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(400px, 90vh)', overflow: 'hidden' }"
28 + title="Notification"
29 + :bordered="false"
30 + display-directive="show"
31 + segmented
32 + >
33 + <CustomerNotificationsWorkflowsForm
34 + :incidentNotification
35 + :customerCode="incidentNotification.customer_code"
36 + @mounted="formCTX = $event"
37 + @submitted="emitUpdate"
38 + />
39 + </n-modal>
40 + </div>
41 +</template>
42 +
43 +<script setup lang="ts">
44 +import { defineAsyncComponent, ref, toRefs, watch } from "vue"
45 +import { NModal } from "naive-ui"
46 +import Icon from "@/components/common/Icon.vue"
47 +import type { IncidentNotification } from "@/types/incidentManagement/notifications.d"
48 +
49 +const CustomerNotificationsWorkflowsForm = defineAsyncComponent(
50 + () => import("./CustomerNotificationsWorkflowsForm.vue")
51 +)
52 +
53 +const props = defineProps<{
54 + incidentNotification: IncidentNotification
55 + embedded?: boolean
56 +}>()
57 +const { incidentNotification, embedded } = toRefs(props)
58 +
59 +const emit = defineEmits<{
60 + (e: "updated", value: IncidentNotification): void
61 +}>()
62 +
63 +const EnabledIcon = "carbon:circle-solid"
64 +const DisabledIcon = "carbon:subtract-alt"
65 +
66 +const formCTX = ref<{ reset: (incidentNotification?: IncidentNotification) => void } | null>(null)
67 +const showForm = ref(false)
68 +
69 +watch(showForm, val => {
70 + if (val) {
71 + formCTX.value?.reset(incidentNotification.value)
72 + }
73 +})
74 +
75 +function openForm() {
76 + showForm.value = true
77 +}
78 +
79 +function emitUpdate(incidentNotification: IncidentNotification) {
80 + emit("updated", incidentNotification)
81 +}
82 +</script>
83 +
84 +<style lang="scss" scoped>
85 +.customer-notifications-workflows-item {
86 + border-radius: var(--border-radius);
87 + background-color: var(--bg-color);
88 + border: var(--border-small-050);
89 + transition: all 0.2s var(--bezier-ease);
90 + cursor: pointer;
91 +
92 + .header-box {
93 + font-size: 13px;
94 +
95 + .label {
96 + font-size: 11px;
97 + font-family: var(--font-family-mono);
98 + word-break: break-word;
99 + color: var(--fg-secondary-color);
100 + line-height: 1.2;
101 + }
102 + }
103 +
104 + .main-box {
105 + .content {
106 + word-break: break-word;
107 +
108 + .label {
109 + color: var(--fg-secondary-color);
110 + font-size: 11px;
111 + }
112 + }
113 + }
114 +
115 + &.embedded {
116 + background-color: var(--bg-secondary-color);
117 + }
118 +
119 + &:hover {
120 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
121 + }
122 +}
123 +</style>
frontend/src/components/customers/provision/CustomerProvision.vue
+1 -1
@@ -30,7 +30,7 @@
30 </n-button>
31 </div>
32
33 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
33 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4">
34 <KVCard v-for="(value, key) of customerMeta" :key="key">
35 <template #key>{{ key }}</template>
36 <template #value>{{ value || "-" }}</template>
frontend/src/components/customers/provision/CustomerProvisionWizard.vue
+8 -2
@@ -54,7 +54,12 @@
54 clearable
55 />
56 </n-form-item>
57 - <n-form-item label="DFIR-IRIS Username" path="dfir_iris_username" class="grow">
57 + <n-form-item
58 + label="DFIR-IRIS Username"
59 + path="dfir_iris_username"
60 + class="grow"
61 + v-if="showDfirIrisUsername"
62 + >
63 <n-input
64 v-model:value.trim="form.dfir_iris_username"
65 placeholder="The Username of the API Key CoPilot uses to connect to DFIR-IRIS..."
@@ -280,8 +285,9 @@ const props = defineProps<{
285 customerCode: string
286 customerName: string
287 mode?: "new" | "update"
288 + showDfirIrisUsername?: boolean
289 }>()
284 -const { customerCode, customerName, mode } = toRefs(props)
290 +const { customerCode, customerName, mode, showDfirIrisUsername } = toRefs(props)
291
292 const SkipIcon = "carbon:subtract"
293 const ArrowRightIcon = "carbon:arrow-right"
frontend/src/components/graylog/Alerts/List.vue
+1 -2
@@ -50,14 +50,13 @@
50 </div>
51 </n-popover>
52 </div>
53 - <div class="list my-3">
53 + <div class="list flex flex-col gap-2 my-3">
54 <template v-if="alertsEvents.length">
55 <AlertsEventItem
56 v-for="alertsEvent of alertsEvents"
57 :key="alertsEvent.event.id"
58 :alertsEvent="alertsEvent"
59 @click-event="gotoEventsPage($event)"
60 - class="mb-2"
60 />
61 </template>
62 <template v-else>
frontend/src/components/graylog/Events/List.vue
+1 -2
@@ -29,14 +29,13 @@
29 style="width: 110px"
30 />
31 </div>
32 - <div class="list my-3">
32 + <div class="list flex flex-col gap-2 my-3">
33 <template v-if="itemsPaginated.length">
34 <EventItem
35 v-for="event of itemsPaginated"
36 :key="event.id"
37 :event="event"
38 :highlight="event.id === highlight"
39 - class="mb-2"
39 />
40 </template>
41 <template v-else>
frontend/src/components/graylog/Inputs/List.vue
+1 -2
@@ -34,14 +34,13 @@
34 />
35 </div>
36 <n-scrollbar class="my-3">
37 - <div class="list">
37 + <div class="list flex flex-col gap-2">
38 <template v-if="itemsFiltered.length">
39 <InputItem
40 v-for="input of itemsFiltered"
41 :key="input.id"
42 :input="input"
43 @updated="getData('running')"
44 - class="mb-2"
44 />
45 </template>
46 <template v-else>
frontend/src/components/graylog/Messages/Item.vue
+1 -1
@@ -14,7 +14,7 @@
14 </template>
15
16 <script setup lang="ts">
17 -import { type Message } from "@/types/graylog/index.d"
17 +import { type Message } from "@/types/graylog/messages.d"
18 import { useSettingsStore } from "@/stores/settings"
19 import { formatDate } from "@/utils"
20
frontend/src/components/graylog/Messages/List.vue
+3 -3
@@ -22,9 +22,9 @@
22 </div>
23 <n-pagination v-model:page="currentPage" :page-size="pageSize" :item-count="total" :page-slot="5" />
24 </div>
25 - <div class="list my-3">
25 + <div class="list flex flex-col gap-2 my-3">
26 <template v-if="messages.length">
27 - <MessageItem v-for="msg of messages" :key="msg.id" :message="msg" class="mb-2" />
27 + <MessageItem v-for="msg of messages" :key="msg.id" :message="msg" />
28 </template>
29 <template v-else>
30 <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
@@ -49,7 +49,7 @@ import Api from "@/api"
49 import MessageItem from "./Item.vue"
50 import Icon from "@/components/common/Icon.vue"
51 import { nanoid } from "nanoid"
52 -import type { MessageExtended } from "@/types/graylog/index.d"
52 +import type { MessageExtended } from "@/types/graylog/messages.d"
53
54 const InfoIcon = "carbon:information"
55
frontend/src/components/graylog/Metrics/List.vue
+1 -1
@@ -34,7 +34,7 @@
34 <script setup lang="ts">
35 import { toRefs, computed } from "vue"
36 import { NCard, NProgress } from "naive-ui"
37 -import type { ThroughputMetric } from "@/types/graylog/index.d"
37 +import type { ThroughputMetric } from "@/types/graylog/metrics.d"
38 import _groupBy from "lodash/groupBy"
39 import _map from "lodash/map"
40 import _trim from "lodash/trim"
frontend/src/components/graylog/MonitoringAlerts/CustomAlertForm.vue
+2 -1
@@ -164,8 +164,9 @@ import {
164 import _trim from "lodash/trim"
165 import _get from "lodash/get"
166 import _toSafeInteger from "lodash/toSafeInteger"
167 -import { type CustomProvisionPayload, CustomProvisionPriority } from "@/api/monitoringAlerts"
167 +import { type CustomProvisionPayload } from "@/api/endpoints/monitoringAlerts"
168 import Icon from "@/components/common/Icon.vue"
169 +import { CustomProvisionPriority } from "@/types/monitoringAlerts.d"
170
171 interface CustomProvisionForm {
172 alert_name: string
frontend/src/components/graylog/MonitoringAlerts/Item.vue
+1 -1
@@ -106,7 +106,7 @@ import {
106 type FormValidationError
107 } from "naive-ui"
108 import Api from "@/api"
109 -import type { ProvisionsMonitoringAlertParams } from "@/api/monitoringAlerts"
109 +import type { ProvisionsMonitoringAlertParams } from "@/api/endpoints/monitoringAlerts"
110
111 const emit = defineEmits<{
112 (e: "provisioned"): void
frontend/src/components/graylog/MonitoringAlerts/List.vue
+1 -2
@@ -36,14 +36,13 @@
36 />
37 </div>
38 <n-spin :show="loading">
39 - <div class="list my-3">
39 + <div class="list flex flex-col gap-2 my-3">
40 <template v-if="alerts.length">
41 <MonitoringAlert
42 v-for="alert of itemsPaginated"
43 :key="alert.name"
44 :alert="alert"
45 :is-enabled="isEnabled(alert)"
46 - class="mb-2"
46 @provisioned="getData()"
47 />
48 </template>
frontend/src/components/graylog/Streams/List.vue
+2 -2
@@ -70,9 +70,9 @@
70 </div>
71 </n-popover>
72 </div>
73 - <div class="list my-3">
73 + <div class="list flex flex-col gap-2 my-3">
74 <template v-if="itemsPaginated.length">
75 - <StreamItem v-for="stream of itemsPaginated" :key="stream.id" :stream="stream" class="mb-2" />
75 + <StreamItem v-for="stream of itemsPaginated" :key="stream.id" :stream="stream" />
76 </template>
77 <template v-else>
78 <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
frontend/src/components/healthcheck/HealthcheckList.vue
+2 -2
@@ -35,13 +35,13 @@
35 />
36 </div>
37 <n-spin :show="loading">
38 - <div class="list my-3">
38 + <div class="list flex flex-col gap-2 my-3">
39 <template v-if="healthcheckList.length">
40 <HealthcheckItem
41 v-for="alert of itemsPaginated"
42 :key="alert.checkID + alert.time"
43 :alert="alert"
44 - class="item-appear item-appear-bottom item-appear-005 mb-2"
44 + class="item-appear item-appear-bottom item-appear-005"
45 />
46 </template>
47 <template v-else>
frontend/src/components/incidentManagement/alerts/AlertAsset.vue new
+219
@@ -0,0 +1,219 @@
1 +<template>
2 + <div class="alert-asset-item" :class="{ embedded }">
3 + <div class="flex flex-col cursor-pointer" @click="showDetails = true">
4 + <div class="main-box flex flex-col gap-3 px-5 py-3">
5 + <div class="content flex flex-col gap-1 grow">
6 + <div class="title">
7 + {{ asset.asset_name }}
8 + </div>
9 + </div>
10 +
11 + <div class="badges-box flex flex-wrap items-center gap-3">
12 + <Badge type="splitted">
13 + <template #label>Index</template>
14 + <template #value>
15 + <div class="flex items-center h-full">
16 + <code
17 + class="cursor-pointer text-primary-color leading-none"
18 + @click.stop="gotoIndex(asset.index_name)"
19 + >
20 + {{ asset.index_name }}
21 + <Icon :name="LinkIcon" :size="14" class="top-0.5 relative" />
22 + </code>
23 + </div>
24 + </template>
25 + </Badge>
26 +
27 + <Badge type="splitted">
28 + <template #label>Agent</template>
29 + <template #value>
30 + <div class="flex items-center h-full">
31 + <code
32 + class="cursor-pointer text-primary-color leading-none"
33 + @click.stop="gotoAgent(asset.agent_id)"
34 + >
35 + {{ asset.agent_id }}
36 + <Icon :name="LinkIcon" :size="14" class="top-0.5 relative" />
37 + </code>
38 + </div>
39 + </template>
40 + </Badge>
41 + </div>
42 + </div>
43 + </div>
44 +
45 + <n-modal
46 + v-model:show="showDetails"
47 + preset="card"
48 + content-class="!p-0"
49 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
50 + :bordered="false"
51 + segmented
52 + >
53 + <template #header>
54 + <div class="min-h-8">
55 + {{ assetNameTruncated }}
56 + </div>
57 + </template>
58 + <template #header-extra>
59 + <div class="min-h-8">
60 + <ArtifactRecommendation v-if="alertContext" :context="alertContext.context" />
61 + </div>
62 + </template>
63 + <n-tabs type="line" animated :tabs-padding="24">
64 + <n-tab-pane name="Info" tab="Info" display-directive="show">
65 + <AlertAssetInfo :asset />
66 + </n-tab-pane>
67 + <n-tab-pane name="Context" tab="Context" display-directive="show">
68 + <n-spin :show="loading" class="min-h-40">
69 + <div class="p-7 pt-4" v-if="alertContext">
70 + <div class="flex flex-wrap gap-3 mb-4">
71 + <Badge type="splitted">
72 + <template #label>id</template>
73 + <template #value>#{{ alertContext.id }}</template>
74 + </Badge>
75 + <Badge type="splitted">
76 + <template #label>source</template>
77 + <template #value>{{ alertContext.source }}</template>
78 + </Badge>
79 + </div>
80 +
81 + <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden">
82 + <div
83 + class="scrollbar-styled overflow-hidden code-bg-transparent"
84 + v-shiki="{ lang: 'json', decode: false }"
85 + >
86 + <pre>{{ alertContext.context }}</pre>
87 + </div>
88 + </n-card>
89 + </div>
90 + </n-spin>
91 + </n-tab-pane>
92 + <n-tab-pane
93 + name="Investigate"
94 + tab="Investigate"
95 + display-directive="show:lazy"
96 + v-if="isInvestigationAvailable"
97 + >
98 + <div class="p-7 pt-4">
99 + <div class="flex flex-wrap gap-2">
100 + <ThreatIntelProcessEvaluationProvider
101 + v-for="pn of processNameList"
102 + :key="pn"
103 + :process-name="pn"
104 + v-slot="{ openEvaluation }"
105 + >
106 + <n-card
107 + @click="openEvaluation()"
108 + size="small"
109 + content-class="bg-secondary-color"
110 + class="overflow-hidden hover:border-primary-color cursor-pointer"
111 + >
112 + {{ pn }}
113 + </n-card>
114 + </ThreatIntelProcessEvaluationProvider>
115 + </div>
116 + </div>
117 + </n-tab-pane>
118 + <n-tab-pane name="Artifact Collection" tab="Artifact Collection" display-directive="show:lazy">
119 + <div class="p-7 pt-2">
120 + <ArtifactsCollect
121 + :hostname="asset.asset_name"
122 + :artifacts-filter="{ hostname: asset.asset_name }"
123 + hide-hostname-field
124 + velociraptor-id="string"
125 + hide-velociraptor-id-field
126 + />
127 + </div>
128 + </n-tab-pane>
129 + <n-tab-pane name="Alert Timeline" tab="Alert Timeline" display-directive="show:lazy">
130 + <div class="p-7 pt-2">
131 + <AlertDetailTimeline :asset />
132 + </div>
133 + </n-tab-pane>
134 + </n-tabs>
135 + </n-modal>
136 + </div>
137 +</template>
138 +
139 +<script setup lang="ts">
140 +import { computed, defineAsyncComponent, ref, toRefs, watch } from "vue"
141 +import { NModal, NSpin, NCard, NTabs, NTabPane, useMessage } from "naive-ui"
142 +import Api from "@/api"
143 +import Icon from "@/components/common/Icon.vue"
144 +import Badge from "@/components/common/Badge.vue"
145 +import vShiki from "@/directives/v-shiki"
146 +import { useGoto } from "@/composables/useGoto"
147 +import _truncate from "lodash/truncate"
148 +import type { AlertAsset, AlertContext } from "@/types/incidentManagement/alerts.d"
149 +
150 +const AlertAssetInfo = defineAsyncComponent(() => import("./AlertAssetInfo.vue"))
151 +const AlertDetailTimeline = defineAsyncComponent(() => import("./AlertDetailTimeline.vue"))
152 +const ArtifactRecommendation = defineAsyncComponent(() => import("@/components/artifacts/ArtifactRecommendation.vue"))
153 +const ThreatIntelProcessEvaluationProvider = defineAsyncComponent(
154 + () => import("@/components/threatIntel/ThreatIntelProcessEvaluationProvider.vue")
155 +)
156 +const ArtifactsCollect = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCollect.vue"))
157 +
158 +const props = defineProps<{ asset: AlertAsset; embedded?: boolean }>()
159 +const { asset, embedded } = toRefs(props)
160 +
161 +const LinkIcon = "carbon:launch"
162 +const { gotoAgent, gotoIndex } = useGoto()
163 +const message = useMessage()
164 +const loading = ref(false)
165 +const showDetails = ref(false)
166 +const assetNameTruncated = computed(() => _truncate(asset.value.asset_name, { length: 50 }))
167 +const alertContext = ref<AlertContext | null>(null)
168 +const processNameList = computed<string[]>(() => alertContext.value?.context?.["process_name"] || [])
169 +const isInvestigationAvailable = computed(() => processNameList.value.length)
170 +
171 +watch(showDetails, val => {
172 + if (val && !alertContext.value) {
173 + getAlertContext(asset.value.alert_context_id)
174 + }
175 +})
176 +
177 +function getAlertContext(alertContextId: number) {
178 + loading.value = true
179 +
180 + Api.incidentManagement
181 + .getAlertContext(alertContextId)
182 + .then(res => {
183 + if (res.data.success) {
184 + alertContext.value = res.data?.alert_context || null
185 + } else {
186 + message.warning(res.data?.message || "An error occurred. Please try again later.")
187 + }
188 + })
189 + .catch(err => {
190 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
191 + })
192 + .finally(() => {
193 + loading.value = false
194 + })
195 +}
196 +</script>
197 +
198 +<style lang="scss" scoped>
199 +.alert-asset-item {
200 + border-radius: var(--border-radius);
201 + background-color: var(--bg-color);
202 + transition: all 0.2s var(--bezier-ease);
203 + border: var(--border-small-050);
204 + overflow: hidden;
205 +
206 + .main-box {
207 + .content {
208 + word-break: break-word;
209 + }
210 + }
211 +
212 + &.embedded {
213 + background-color: var(--bg-secondary-color);
214 + }
215 + &:hover {
216 + box-shadow: 0px 0px 0px 1px var(--primary-color);
217 + }
218 +}
219 +</style>
frontend/src/components/incidentManagement/alerts/AlertAssetInfo.vue new
+145
@@ -0,0 +1,145 @@
1 +<template>
2 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4">
3 + <KVCard v-for="(value, key) of asset" :key="key">
4 + <template #key>{{ key }}</template>
5 + <template #value>
6 + <div v-if="key === 'id'">#{{ value }}</div>
7 + <div v-else-if="key === 'customer_code'">
8 + <code
9 + class="cursor-pointer text-primary-color"
10 + @click.stop="gotoCustomer({ code: asset.customer_code })"
11 + >
12 + #{{ asset.customer_code }}
13 + <Icon :name="LinkIcon" :size="14" class="top-0.5 relative" />
14 + </code>
15 + </div>
16 + <div v-else-if="key === 'agent_id'">
17 + <code class="cursor-pointer text-primary-color" @click.stop="gotoAgent(asset.agent_id)">
18 + {{ asset.agent_id }}
19 + <Icon :name="LinkIcon" :size="14" class="top-0.5 relative" />
20 + </code>
21 + </div>
22 + <div v-else-if="key === 'index_name'">
23 + <code class="cursor-pointer text-primary-color" @click.stop="gotoIndex(asset.index_name)">
24 + {{ asset.index_name }}
25 + <Icon :name="LinkIcon" :size="14" class="top-0.5 relative" />
26 + </code>
27 + </div>
28 + <div v-else-if="key === 'index_id'">
29 + <code class="cursor-pointer text-primary-color" @click.stop="openAlertDetails()">
30 + {{ asset.index_id }}
31 + <Icon :name="ViewIcon" :size="14" class="top-0.5 relative" />
32 + </code>
33 + </div>
34 + <div v-else>{{ value === "" ? "-" : value ?? "-" }}</div>
35 + </template>
36 + </KVCard>
37 + </div>
38 +
39 + <n-modal
40 + v-model:show="showAlertDetails"
41 + preset="card"
42 + content-class="!p-0"
43 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
44 + :bordered="false"
45 + title="Alert Details"
46 + segmented
47 + >
48 + <n-spin :show="loading" class="min-h-40">
49 + <n-tabs type="line" animated :tabs-padding="24">
50 + <n-tab-pane name="Info" tab="Info" display-directive="show">
51 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4">
52 + <KVCard v-for="(value, key) of alertDetailsInfo" :key="key">
53 + <template #key>{{ key }}</template>
54 + <template #value>
55 + <div v-if="key === '_index'">
56 + <code
57 + class="cursor-pointer text-primary-color"
58 + @click.stop="gotoIndex(alertDetailsInfo._index)"
59 + >
60 + {{ alertDetailsInfo._index }}
61 + <Icon :name="LinkIcon" :size="14" class="top-0.5 relative" />
62 + </code>
63 + </div>
64 + <div v-else>{{ value === "" ? "-" : value ?? "-" }}</div>
65 + </template>
66 + </KVCard>
67 + </div>
68 + </n-tab-pane>
69 + <n-tab-pane name="Source" tab="Source" display-directive="show">
70 + <div class="p-7 pt-4" v-if="alertDetailsSource">
71 + <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden">
72 + <div
73 + class="scrollbar-styled overflow-hidden code-bg-transparent"
74 + v-shiki="{ lang: 'json', decode: false }"
75 + >
76 + <pre>{{ alertDetailsSource }}</pre>
77 + </div>
78 + </n-card>
79 + </div>
80 + </n-tab-pane>
81 + </n-tabs>
82 + </n-spin>
83 + </n-modal>
84 +</template>
85 +
86 +<script setup lang="ts">
87 +import { computed, ref, toRefs, watch } from "vue"
88 +import { NModal, NSpin, NCard, NTabs, NTabPane, useMessage } from "naive-ui"
89 +import Api from "@/api"
90 +import vShiki from "@/directives/v-shiki"
91 +import KVCard from "@/components/common/KVCard.vue"
92 +import Icon from "@/components/common/Icon.vue"
93 +import { useGoto } from "@/composables/useGoto"
94 +import _omit from "lodash/omit"
95 +import type { AlertAsset, AlertDetails } from "@/types/incidentManagement/alerts.d"
96 +
97 +const props = defineProps<{ asset: AlertAsset }>()
98 +const { asset } = toRefs(props)
99 +
100 +const LinkIcon = "carbon:launch"
101 +const ViewIcon = "iconoir:eye-alt"
102 +const { gotoAgent, gotoIndex, gotoCustomer } = useGoto()
103 +const message = useMessage()
104 +const loading = ref(false)
105 +const showAlertDetails = ref(false)
106 +const alertDetails = ref<AlertDetails | null>(null)
107 +const alertDetailsInfo = computed(() => _omit(alertDetails.value, ["_source"]))
108 +const alertDetailsSource = computed(() => alertDetails.value?._source)
109 +
110 +watch(showAlertDetails, val => {
111 + if (val && !alertDetails.value) {
112 + getAlertDetails(asset.value.index_id, asset.value.index_name)
113 + }
114 +})
115 +
116 +function getAlertDetails(indexId: string, indexName: string) {
117 + loading.value = true
118 +
119 + Api.incidentManagement
120 + .getAlertDetails(indexId, indexName)
121 + .then(res => {
122 + if (res.data.success) {
123 + alertDetails.value = res.data?.alert_details || null
124 + } else {
125 + closeAlertDetails()
126 + message.warning(res.data?.message || "An error occurred. Please try again later.")
127 + }
128 + })
129 + .catch(err => {
130 + closeAlertDetails()
131 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
132 + })
133 + .finally(() => {
134 + loading.value = false
135 + })
136 +}
137 +
138 +function openAlertDetails() {
139 + showAlertDetails.value = true
140 +}
141 +
142 +function closeAlertDetails() {
143 + showAlertDetails.value = false
144 +}
145 +</script>
frontend/src/components/incidentManagement/alerts/AlertAssetsList.vue new
+20
@@ -0,0 +1,20 @@
1 +<template>
2 + <div class="flex flex-col gap-2">
3 + <template v-if="assets.length">
4 + <AlertAssetItem :asset embedded v-for="asset of assets" :key="asset.id" />
5 + </template>
6 + <template v-else>
7 + <n-empty description="No assets found" class="justify-center h-48" />
8 + </template>
9 + </div>
10 +</template>
11 +
12 +<script setup lang="ts">
13 +import { toRefs } from "vue"
14 +import { NEmpty } from "naive-ui"
15 +import AlertAssetItem from "./AlertAsset.vue"
16 +import type { AlertAsset } from "@/types/incidentManagement/alerts.d"
17 +
18 +const props = defineProps<{ assets: AlertAsset[] }>()
19 +const { assets } = toRefs(props)
20 +</script>
frontend/src/components/incidentManagement/alerts/AlertAssignUser.vue new
+115
@@ -0,0 +1,115 @@
1 +<template>
2 + <n-popselect
3 + v-model:value="userSelected"
4 + v-model:show="usersListVisible"
5 + :options="usersOptions"
6 + :disabled="loadingUsers"
7 + size="medium"
8 + scrollable
9 + to="body"
10 + >
11 + <slot :loading="loadingUsers" />
12 + </n-popselect>
13 +</template>
14 +
15 +<script setup lang="ts">
16 +import { computed, inject, onBeforeMount, ref, toRefs, watch, type Ref } from "vue"
17 +import { useMessage, NPopselect } from "naive-ui"
18 +import Api from "@/api"
19 +import type { Alert } from "@/types/incidentManagement/alerts.d"
20 +
21 +const props = defineProps<{
22 + alert: Alert
23 +}>()
24 +const { alert } = toRefs(props)
25 +
26 +const emit = defineEmits<{
27 + (e: "updated", value: Alert): void
28 +}>()
29 +
30 +const loadingUsers = ref(false)
31 +const users = inject<Ref<string[]>>("assignable-users", ref([]))
32 +const message = useMessage()
33 +const usersListVisible = ref(false)
34 +const assignedTo = computed(() => alert.value.assigned_to)
35 +const usersOptions = ref<
36 + {
37 + label: string
38 + value: string
39 + }[]
40 +>([])
41 +const userSelected = ref<string | null>(null)
42 +
43 +function getUsers() {
44 + loadingUsers.value = true
45 +
46 + Api.incidentManagement
47 + .getAvailableUsers()
48 + .then(res => {
49 + if (res.data.success) {
50 + const usersList = res.data?.available_users || []
51 + parseUsers(usersList)
52 + } else {
53 + message.warning(res.data?.message || "An error occurred. Please try again later.")
54 + }
55 + })
56 + .catch(err => {
57 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
58 + })
59 + .finally(() => {
60 + loadingUsers.value = false
61 + })
62 +}
63 +
64 +function assignUser() {
65 + if (userSelected.value && userSelected.value !== assignedTo.value) {
66 + loadingUsers.value = true
67 +
68 + Api.incidentManagement
69 + .updateAlertAssignedUser(alert.value.id, userSelected.value)
70 + .then(res => {
71 + if (res.data.success) {
72 + emit("updated", { ...alert.value, assigned_to: userSelected.value })
73 + } else {
74 + message.warning(res.data?.message || "An error occurred. Please try again later.")
75 + }
76 + })
77 + .catch(err => {
78 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
79 + })
80 + .finally(() => {
81 + loadingUsers.value = false
82 + })
83 + }
84 +}
85 +
86 +function parseUsers(users: string[]) {
87 + usersOptions.value = users.map(o => ({ label: o, value: o }))
88 +}
89 +
90 +watch(users, val => {
91 + if (val !== undefined && val.length) {
92 + parseUsers(val)
93 + }
94 +})
95 +
96 +watch(userSelected, () => {
97 + assignUser()
98 +})
99 +
100 +watch(usersListVisible, val => {
101 + if (val && !usersOptions.value.length) {
102 + getUsers()
103 + }
104 +})
105 +
106 +onBeforeMount(() => {
107 + if (assignedTo.value) {
108 + userSelected.value = assignedTo.value
109 + }
110 +
111 + if (users.value.length) {
112 + parseUsers(users.value)
113 + }
114 +})
115 +</script>
frontend/src/components/incidentManagement/alerts/AlertComment.vue new
+76
@@ -0,0 +1,76 @@
1 +<template>
2 + <div class="alert-comment-item flex gap-3" :class="{ embedded }">
3 + <div class="user-pic" v-if="userPic">
4 + <n-avatar round :size="32" :src="userPic" />
5 + </div>
6 + <div class="comment grow flex flex-col gap-1">
7 + <div class="user flex gap-3 items-center">
8 + <div class="user-name">{{ comment.user_name }}</div>
9 + <div class="comment-time">{{ formatDate(comment.created_at, dFormats.datetime) }}</div>
10 + </div>
11 + <div class="comment-message" v-html="message"></div>
12 + </div>
13 + </div>
14 +</template>
15 +
16 +<script setup lang="ts">
17 +import { onBeforeMount, ref, toRefs } from "vue"
18 +import { NAvatar } from "naive-ui"
19 +import { getAvatar, getNameInitials } from "@/utils"
20 +import { useSettingsStore } from "@/stores/settings"
21 +import { formatDate } from "@/utils"
22 +import type { AlertComment } from "@/types/incidentManagement/alerts.d"
23 +
24 +const props = defineProps<{ comment: AlertComment; embedded?: boolean }>()
25 +const { comment, embedded } = toRefs(props)
26 +
27 +const dFormats = useSettingsStore().dateFormat
28 +const userPic = ref("")
29 +const message = ref(comment.value.comment.replace(/\n/gim, "<br/>"))
30 +
31 +onBeforeMount(() => {
32 + const initials = getNameInitials(comment.value.user_name)
33 + userPic.value = getAvatar({ seed: initials, text: initials, size: 64 })
34 +})
35 +</script>
36 +
37 +<style lang="scss" scoped>
38 +.alert-comment-item {
39 + width: 100%;
40 +
41 + .user-pic {
42 + padding-top: 4px;
43 + }
44 +
45 + .comment {
46 + .user {
47 + margin-left: 2px;
48 +
49 + .user-name {
50 + font-weight: 600;
51 + }
52 +
53 + .comment-time {
54 + font-size: 11px;
55 + color: var(--fg-secondary-color);
56 + font-family: var(--font-family-mono);
57 + }
58 + }
59 +
60 + .comment-message {
61 + border-radius: var(--border-radius);
62 + background-color: var(--bg-color);
63 + border: var(--border-small-050);
64 + padding: 6px 10px;
65 + }
66 + }
67 +
68 + &.embedded {
69 + .comment {
70 + .comment-message {
71 + background-color: var(--bg-secondary-color);
72 + }
73 + }
74 + }
75 +}
76 +</style>
frontend/src/components/incidentManagement/alerts/AlertCommentsList.vue new
+96
@@ -0,0 +1,96 @@
1 +<template>
2 + <div class="flex flex-col gap-6">
3 + <template v-if="commentsList.length">
4 + <AlertCommentItem :comment embedded v-for="comment of commentsList" :key="comment.id" />
5 + </template>
6 + <template v-else>
7 + <n-empty description="No comments found" class="justify-center h-48" />
8 + </template>
9 + <n-spin :show="submitting">
10 + <div class="comment-form mt-6 flex flex-col gap-3">
11 + <div class="editor-box">
12 + <n-input
13 + placeholder="Write a new comment..."
14 + type="textarea"
15 + v-model:value="commentMessage"
16 + :autosize="{
17 + minRows: 3,
18 + maxRows: 10
19 + }"
20 + />
21 + </div>
22 + <div class="tool-box flex gap-2 justify-end">
23 + <n-button secondary @click="reset()" :disabled="submitting">Reset</n-button>
24 + <n-button
25 + type="primary"
26 + @click="submit()"
27 + :disabled="!trimmedValue || submitting"
28 + :loading="submitting"
29 + >
30 + <template #icon><Icon :name="CommentsIcon" /></template>
31 + Send comment
32 + </n-button>
33 + </div>
34 + </div>
35 + </n-spin>
36 + </div>
37 +</template>
38 +
39 +<script setup lang="ts">
40 +import { computed, ref, toRefs } from "vue"
41 +import { NEmpty, NInput, NButton, NSpin, useMessage } from "naive-ui"
42 +import AlertCommentItem from "./AlertComment.vue"
43 +import Icon from "@/components/common/Icon.vue"
44 +import type { AlertComment } from "@/types/incidentManagement/alerts.d"
45 +import Api from "@/api"
46 +import { useAuthStore } from "@/stores/auth"
47 +import _trim from "lodash/trim"
48 +
49 +const props = defineProps<{ comments: AlertComment[]; alertId: number }>()
50 +const { comments, alertId } = toRefs(props)
51 +
52 +const emit = defineEmits<{
53 + (e: "updated", value: AlertComment[]): void
54 +}>()
55 +
56 +const CommentsIcon = "carbon:chat"
57 +const commentsList = ref<AlertComment[]>(comments.value)
58 +const commentMessage = ref<string | null>(null)
59 +const submitting = ref(false)
60 +const message = useMessage()
61 +const authStore = useAuthStore()
62 +const trimmedValue = computed(() => _trim(commentMessage.value || ""))
63 +
64 +function reset() {
65 + commentMessage.value = ""
66 +}
67 +
68 +function submit() {
69 + if (trimmedValue.value) {
70 + submitting.value = true
71 +
72 + Api.incidentManagement
73 + .newAlertComment({
74 + alert_id: alertId.value,
75 + comment: trimmedValue.value,
76 + created_at: new Date(),
77 + user_name: authStore.userName
78 + })
79 + .then(res => {
80 + if (res.data.success) {
81 + reset()
82 + commentsList.value.push(res.data.comment)
83 + emit("updated", commentsList.value)
84 + } else {
85 + message.warning(res.data?.message || "An error occurred. Please try again later.")
86 + }
87 + })
88 + .catch(err => {
89 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
90 + })
91 + .finally(() => {
92 + submitting.value = false
93 + })
94 + }
95 +}
96 +</script>
frontend/src/components/incidentManagement/alerts/AlertCreateCaseButton.vue new
+63
@@ -0,0 +1,63 @@
1 +<template>
2 + <n-button secondary type="primary" @click="createCase()" :loading="creating">
3 + <template #icon><Icon :name="DangerIcon" /></template>
4 + Create case
5 + </n-button>
6 +</template>
7 +
8 +<script setup lang="ts">
9 +import { ref, toRefs } from "vue"
10 +import { NButton, useMessage } from "naive-ui"
11 +import Icon from "@/components/common/Icon.vue"
12 +import Api from "@/api"
13 +import type { Alert } from "@/types/incidentManagement/alerts.d"
14 +
15 +const props = defineProps<{ alert: Alert }>()
16 +const { alert } = toRefs(props)
17 +
18 +const emit = defineEmits<{
19 + (e: "updated", value: Alert): void
20 +}>()
21 +
22 +const DangerIcon = "majesticons:exclamation-line"
23 +
24 +const message = useMessage()
25 +const creating = ref(false)
26 +
27 +function updateAlert(updatedAlert: Alert) {
28 + emit("updated", updatedAlert)
29 +}
30 +
31 +function createCase() {
32 + creating.value = true
33 +
34 + Api.incidentManagement
35 + .createCase(alert.value.id)
36 + .then(res => {
37 + if (res.data.success) {
38 + updateAlert({
39 + ...alert.value,
40 + linked_cases: [
41 + {
42 + id: res.data.case_alert_link.case_id,
43 + case_name: "",
44 + case_description: "",
45 + case_creation_time: new Date(),
46 + assigned_to: null,
47 + case_status: null
48 + }
49 + ]
50 + })
51 + message.success(res.data?.message || "Case created successfully")
52 + } else {
53 + message.warning(res.data?.message || "An error occurred. Please try again later.")
54 + }
55 + })
56 + .catch(err => {
57 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
58 + })
59 + .finally(() => {
60 + creating.value = false
61 + })
62 +}
63 +</script>
frontend/src/components/incidentManagement/alerts/AlertDetailTimeline.vue new
+61
@@ -0,0 +1,61 @@
1 +<template>
2 + <n-spin :show="loading" class="min-h-48">
3 + <n-timeline class="mt-4">
4 + <n-timeline-item
5 + v-for="(item, $index) of timeline"
6 + :key="item._id"
7 + :type="$index === 0 ? 'success' : undefined"
8 + :line-type="$index === timeline.length - 2 ? 'dashed' : undefined"
9 + :time="formatDateTime(item._source.timestamp)"
10 + class="pb-4"
11 + >
12 + <AlertDetailTimelineItem :timelineData="item" embedded class="-mt-3" />
13 + </n-timeline-item>
14 + </n-timeline>
15 + </n-spin>
16 +</template>
17 +
18 +<script setup lang="ts">
19 +import { onBeforeMount, ref } from "vue"
20 +import { NTimeline, NTimelineItem, NSpin, useMessage } from "naive-ui"
21 +import AlertDetailTimelineItem from "./AlertDetailTimelineItem.vue"
22 +import { useSettingsStore } from "@/stores/settings"
23 +import { formatDate } from "@/utils"
24 +import Api from "@/api"
25 +import type { AlertAsset, AlertTimeline } from "@/types/incidentManagement/alerts.d"
26 +
27 +const { asset } = defineProps<{ asset: AlertAsset }>()
28 +
29 +const dFormats = useSettingsStore().dateFormat
30 +const loading = ref(false)
31 +const message = useMessage()
32 +const timeline = ref<AlertTimeline[]>([])
33 +
34 +function formatDateTime(timestamp: Date | string): string {
35 + return formatDate(timestamp, dFormats.datetimesec).toString()
36 +}
37 +
38 +function getAlertTimeline() {
39 + loading.value = true
40 +
41 + Api.incidentManagement
42 + .getAlertTimeline(asset.index_id, asset.index_name)
43 + .then(res => {
44 + if (res.data.success) {
45 + timeline.value = res.data?.alert_timeline || []
46 + } else {
47 + message.warning(res.data?.message || "An error occurred. Please try again later.")
48 + }
49 + })
50 + .catch(err => {
51 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
52 + })
53 + .finally(() => {
54 + loading.value = false
55 + })
56 +}
57 +
58 +onBeforeMount(() => {
59 + getAlertTimeline()
60 +})
61 +</script>
frontend/src/components/incidentManagement/alerts/AlertDetailTimelineItem.vue new
+100
@@ -0,0 +1,100 @@
1 +<template>
2 + <div class="alert-timeline-item" :class="{ embedded }">
3 + <div class="flex flex-col cursor-pointer" @click="showDetails = true">
4 + <div class="main-box flex flex-col gap-3 px-5 py-3">
5 + <div class="content flex flex-col gap-1 grow">
6 + <div class="title">
7 + {{ timelineData._source.rule_description }}
8 + </div>
9 + </div>
10 + </div>
11 + </div>
12 +
13 + <n-modal
14 + v-model:show="showDetails"
15 + preset="card"
16 + content-class="!p-0"
17 + :style="{ maxWidth: 'min(700px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
18 + :bordered="false"
19 + title="Timeline Details"
20 + segmented
21 + >
22 + <n-tabs type="line" animated :tabs-padding="24">
23 + <n-tab-pane name="Info" tab="Info" display-directive="show">
24 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4">
25 + <KVCard v-for="(value, key) of timelineDetailsInfo" :key="key">
26 + <template #key>{{ key }}</template>
27 + <template #value>
28 + <div v-if="key === '_index'">
29 + <code
30 + class="cursor-pointer text-primary-color"
31 + @click.stop="gotoIndex(timelineDetailsInfo._index)"
32 + >
33 + {{ timelineDetailsInfo._index }}
34 + <Icon :name="LinkIcon" :size="14" class="top-0.5 relative" />
35 + </code>
36 + </div>
37 + <div v-else>{{ value === "" ? "-" : value ?? "-" }}</div>
38 + </template>
39 + </KVCard>
40 + </div>
41 + </n-tab-pane>
42 + <n-tab-pane name="Source" tab="Source" display-directive="show">
43 + <div class="p-7 pt-4" v-if="timelineDetailsSource">
44 + <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden">
45 + <div
46 + class="scrollbar-styled overflow-hidden code-bg-transparent"
47 + v-shiki="{ lang: 'json', decode: false }"
48 + >
49 + <pre>{{ timelineDetailsSource }}</pre>
50 + </div>
51 + </n-card>
52 + </div>
53 + </n-tab-pane>
54 + </n-tabs>
55 + </n-modal>
56 + </div>
57 +</template>
58 +
59 +<script setup lang="ts">
60 +import { computed, ref, toRefs } from "vue"
61 +import { NModal, NCard, NTabs, NTabPane } from "naive-ui"
62 +import vShiki from "@/directives/v-shiki"
63 +import KVCard from "@/components/common/KVCard.vue"
64 +import Icon from "@/components/common/Icon.vue"
65 +import { useGoto } from "@/composables/useGoto"
66 +import _omit from "lodash/omit"
67 +import type { AlertTimeline } from "@/types/incidentManagement/alerts.d"
68 +
69 +const props = defineProps<{ timelineData: AlertTimeline; embedded?: boolean }>()
70 +const { timelineData, embedded } = toRefs(props)
71 +
72 +const LinkIcon = "carbon:launch"
73 +const { gotoIndex } = useGoto()
74 +const showDetails = ref(false)
75 +const timelineDetailsInfo = computed(() => _omit(timelineData.value, ["_source"]))
76 +const timelineDetailsSource = computed(() => timelineData.value?._source)
77 +</script>
78 +
79 +<style lang="scss" scoped>
80 +.alert-timeline-item {
81 + border-radius: var(--border-radius);
82 + background-color: var(--bg-color);
83 + transition: all 0.2s var(--bezier-ease);
84 + border: var(--border-small-050);
85 + overflow: hidden;
86 +
87 + .main-box {
88 + .content {
89 + word-break: break-word;
90 + }
91 + }
92 +
93 + &.embedded {
94 + background-color: var(--bg-secondary-color);
95 + }
96 + &:hover {
97 + box-shadow: 0px 0px 0px 1px var(--primary-color);
98 + }
99 +}
100 +</style>
frontend/src/components/incidentManagement/alerts/AlertDetails.vue new
+105
@@ -0,0 +1,105 @@
1 +<template>
2 + <n-spin :show="loading" class="flex flex-col grow" content-class="flex flex-col grow">
3 + <n-tabs
4 + type="line"
5 + animated
6 + :tabs-padding="24"
7 + v-if="alert"
8 + class="grow"
9 + pane-wrapper-class="flex flex-col grow"
10 + >
11 + <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy" class="flex flex-col grow">
12 + <div class="pt-1">
13 + <AlertOverview :alert @updated="updateAlert($event)" @deleted="emit('deleted')" />
14 + </div>
15 + </n-tab-pane>
16 + <n-tab-pane name="Timeline" tab="Timeline" display-directive="show:lazy">
17 + <div class="p-7 pt-4">
18 + <AlertTimeline :alert />
19 + </div>
20 + </n-tab-pane>
21 + <n-tab-pane name="Assets" tab="Assets" display-directive="show:lazy">
22 + <div class="p-7 pt-4">
23 + <AlertAssetsList :assets="alert.assets" />
24 + </div>
25 + </n-tab-pane>
26 + <n-tab-pane name="Comments" tab="Comments" display-directive="show:lazy">
27 + <div class="p-7 pt-4">
28 + <AlertCommentsList
29 + :comments="alert.comments"
30 + :alertId="alert.id"
31 + @updated="updateComments($event)"
32 + />
33 + </div>
34 + </n-tab-pane>
35 + </n-tabs>
36 + </n-spin>
37 +</template>
38 +
39 +<script setup lang="ts">
40 +import { defineAsyncComponent, onBeforeMount, ref, toRefs } from "vue"
41 +import { NTabs, NTabPane, NSpin, useMessage } from "naive-ui"
42 +import _clone from "lodash/cloneDeep"
43 +import Api from "@/api"
44 +import type { Alert, AlertComment } from "@/types/incidentManagement/alerts.d"
45 +
46 +const AlertTimeline = defineAsyncComponent(() => import("./AlertTimeline.vue"))
47 +const AlertAssetsList = defineAsyncComponent(() => import("./AlertAssetsList.vue"))
48 +const AlertCommentsList = defineAsyncComponent(() => import("./AlertCommentsList.vue"))
49 +const AlertOverview = defineAsyncComponent(() => import("./AlertOverview.vue"))
50 +
51 +const props = defineProps<{
52 + alertData?: Alert
53 + alertId?: number
54 +}>()
55 +const { alertData, alertId } = toRefs(props)
56 +
57 +const emit = defineEmits<{
58 + (e: "deleted"): void
59 + (e: "updated", value: Alert): void
60 +}>()
61 +
62 +const message = useMessage()
63 +const loading = ref(false)
64 +const alert = ref<Alert | null>(null)
65 +
66 +function updateAlert(updatedAlert: Alert) {
67 + alert.value = updatedAlert
68 + emit("updated", updatedAlert)
69 +}
70 +
71 +function updateComments(comments: AlertComment[]) {
72 + if (alert.value) {
73 + alert.value.comments = comments
74 + emit("updated", alert.value)
75 + }
76 +}
77 +
78 +function getAlert(alertId: number) {
79 + loading.value = true
80 +
81 + Api.incidentManagement
82 + .getAlert(alertId)
83 + .then(res => {
84 + if (res.data.success) {
85 + alert.value = res.data?.alerts?.[0] || null
86 + } else {
87 + message.warning(res.data?.message || "An error occurred. Please try again later.")
88 + }
89 + })
90 + .catch(err => {
91 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
92 + })
93 + .finally(() => {
94 + loading.value = false
95 + })
96 +}
97 +
98 +onBeforeMount(() => {
99 + if (alertId.value) {
100 + getAlert(alertId.value)
101 + } else if (alertData.value) {
102 + alert.value = _clone(alertData.value)
103 + }
104 +})
105 +</script>
frontend/src/components/incidentManagement/alerts/AlertItem.vue new
+427
@@ -0,0 +1,427 @@
1 +<template>
2 + <div
3 + class="alert-item"
4 + :class="['status-' + alert?.status, { compact, embedded, 'cursor-pointer': compact, highlight }]"
5 + @click="compact ? openDetails() : undefined"
6 + >
7 + <n-spin :show="loading">
8 + <div class="flex flex-col" v-if="alert">
9 + <div class="header-box px-5 py-3 pb-0 flex justify-between items-center">
10 + <div class="id flex items-center gap-2 cursor-pointer" @click="openDetails()">
11 + <span>#{{ alert.id }} - {{ alert.source }}</span>
12 + <Icon :name="InfoIcon" :size="16" v-if="!compact"></Icon>
13 + </div>
14 + <div class="time">
15 + <n-popover
16 + overlap
17 + placement="top-end"
18 + style="max-height: 240px"
19 + scrollable
20 + to="body"
21 + v-if="!compact"
22 + >
23 + <template #trigger>
24 + <div class="flex items-center gap-2 cursor-help">
25 + <span v-if="alert.alert_creation_time">
26 + {{ formatDate(alert.alert_creation_time, dFormats.datetime) }}
27 + </span>
28 + <Icon :name="TimeIcon" :size="16"></Icon>
29 + </div>
30 + </template>
31 + <div class="flex flex-col py-2 px-1">
32 + <AlertTimeline :alert v-if="alert" />
33 + </div>
34 + </n-popover>
35 + <span v-if="compact && alert.alert_creation_time">
36 + {{ formatDate(alert.alert_creation_time, dFormats.datetime) }}
37 + </span>
38 + </div>
39 + </div>
40 +
41 + <div class="main-box flex flex-col gap-3 px-5 py-3">
42 + <div class="content flex flex-col gap-1 grow">
43 + <div class="title">
44 + {{ alert.alert_name }}
45 + </div>
46 + </div>
47 +
48 + <div class="badges-box flex flex-wrap items-center gap-3" v-if="compact">
49 + <Badge
50 + type="splitted"
51 + class="cursor-pointer"
52 + bright
53 + :color="
54 + alert.status === 'OPEN'
55 + ? 'danger'
56 + : alert.status === 'IN_PROGRESS'
57 + ? 'warning'
58 + : 'success'
59 + "
60 + >
61 + <template #iconLeft>
62 + <StatusIcon :status="alert.status" />
63 + </template>
64 + <template #label>Status</template>
65 + <template #value>
66 + <div class="flex gap-2 items-center">
67 + {{ alert.status || "n/d" }}
68 + </div>
69 + </template>
70 + </Badge>
71 +
72 + <Badge
73 + type="splitted"
74 + class="cursor-pointer"
75 + bright
76 + :color="alert.assigned_to ? 'success' : undefined"
77 + >
78 + <template #iconLeft>
79 + <AssigneeIcon :assignee="alert.assigned_to" />
80 + </template>
81 + <template #label>Assignee</template>
82 + <template #value>
83 + <div class="flex gap-2 items-center">
84 + {{ alert.assigned_to || "n/d" }}
85 + </div>
86 + </template>
87 + </Badge>
88 + </div>
89 + <div class="badges-box flex flex-wrap items-center gap-3" v-else>
90 + <AlertStatusSwitch :alert v-slot="{ loading: loadingStatus }" @updated="updateAlert($event)">
91 + <Badge
92 + type="splitted"
93 + class="cursor-pointer"
94 + bright
95 + :color="
96 + alert.status === 'OPEN'
97 + ? 'danger'
98 + : alert.status === 'IN_PROGRESS'
99 + ? 'warning'
100 + : 'success'
101 + "
102 + >
103 + <template #iconLeft>
104 + <n-spin
105 + :size="12"
106 + :show="loadingStatus"
107 + content-class="flex flex-col justify-center"
108 + >
109 + <StatusIcon :status="alert.status" />
110 + </n-spin>
111 + </template>
112 + <template #label>Status</template>
113 + <template #value>
114 + <div class="flex gap-2 items-center">
115 + {{ alert.status || "n/d" }}
116 + <Icon :name="EditIcon" :size="13" />
117 + </div>
118 + </template>
119 + </Badge>
120 + </AlertStatusSwitch>
121 +
122 + <AlertAssignUser :alert v-slot="{ loading: loadingAssignee }" @updated="updateAlert($event)">
123 + <Badge
124 + type="splitted"
125 + class="cursor-pointer"
126 + bright
127 + :color="alert.assigned_to ? 'success' : undefined"
128 + >
129 + <template #iconLeft>
130 + <n-spin
131 + :size="12"
132 + :show="loadingAssignee"
133 + content-class="flex flex-col justify-center"
134 + >
135 + <AssigneeIcon :assignee="alert.assigned_to" />
136 + </n-spin>
137 + </template>
138 + <template #label>Assignee</template>
139 + <template #value>
140 + <div class="flex gap-2 items-center">
141 + {{ alert.assigned_to || "n/d" }}
142 + <Icon :name="EditIcon" :size="13" />
143 + </div>
144 + </template>
145 + </Badge>
146 + </AlertAssignUser>
147 +
148 + <Badge type="splitted" class="!hidden sm:!flex">
149 + <template #label>Customer</template>
150 + <template #value>
151 + <div class="flex items-center h-full">
152 + <code
153 + class="cursor-pointer text-primary-color leading-none"
154 + @click.stop="gotoCustomer({ code: alert.customer_code })"
155 + >
156 + #{{ alert.customer_code }}
157 + <Icon :name="LinkIcon" :size="14" class="top-0.5 relative" />
158 + </code>
159 + </div>
160 + </template>
161 + </Badge>
162 + </div>
163 + </div>
164 +
165 + <div class="footer-box px-5 py-3 flex justify-between items-center" v-if="!compact">
166 + <div class="details flex gap-3 items-center">
167 + <Badge type="splitted" v-if="alert.alert_creation_time" class="time">
168 + <template #iconLeft>
169 + <Icon :name="TimeIcon" :size="16" />
170 + </template>
171 + <template #value>{{ formatDate(alert.alert_creation_time, dFormats.datetime) }}</template>
172 + </Badge>
173 +
174 + <n-tooltip trigger="hover">
175 + <template #trigger>
176 + <Badge type="splitted" class="!hidden xs:!flex">
177 + <template #iconLeft>
178 + <Icon :name="AssetsIcon" :size="16" />
179 + </template>
180 + <template #value>
181 + {{ alert.assets?.length || 0 }}
182 + </template>
183 + </Badge>
184 + </template>
185 + Assets
186 + </n-tooltip>
187 +
188 + <n-tooltip trigger="hover">
189 + <template #trigger>
190 + <Badge type="splitted" class="!hidden xs:!flex">
191 + <template #iconLeft>
192 + <Icon :name="CommentsIcon" :size="16" />
193 + </template>
194 + <template #value>{{ alert.comments?.length || 0 }}</template>
195 + </Badge>
196 + </template>
197 + Comments
198 + </n-tooltip>
199 +
200 + <span v-for="tag of alert.tags" :key="tag.tag" class="text-secondary-color">
201 + #{{ tag.tag }}
202 + </span>
203 + </div>
204 + <div class="actions-box">
205 + <n-button quaternary size="tiny" @click="handleDelete()">Delete</n-button>
206 + </div>
207 + </div>
208 + </div>
209 + </n-spin>
210 +
211 + <n-modal
212 + v-model:show="showDetails"
213 + :style="{ maxWidth: 'min(850px, 90vw)', minHeight: 'min(540px, 90vh)', overflow: 'hidden' }"
214 + display-directive="show"
215 + >
216 + <n-card
217 + content-class="flex flex-col !p-0"
218 + :title="alertNameTruncated"
219 + closable
220 + @close="closeDetails()"
221 + :bordered="false"
222 + segmented
223 + role="modal"
224 + >
225 + <AlertDetails v-if="alert" :alertData="alert" @deleted="emitDelete()" @updated="updateAlert($event)" />
226 + </n-card>
227 + </n-modal>
228 + </div>
229 +</template>
230 +
231 +<script setup lang="ts">
232 +import { computed, onBeforeMount, onMounted, ref, toRefs } from "vue"
233 +import { NModal, NPopover, NButton, NSpin, NTooltip, NCard, useMessage, useDialog } from "naive-ui"
234 +import { useSettingsStore } from "@/stores/settings"
235 +import { useGoto } from "@/composables/useGoto"
236 +import { formatDate } from "@/utils"
237 +import _clone from "lodash/cloneDeep"
238 +import Api from "@/api"
239 +import Icon from "@/components/common/Icon.vue"
240 +import Badge from "@/components/common/Badge.vue"
241 +import AlertTimeline from "./AlertTimeline.vue"
242 +import AlertAssignUser from "./AlertAssignUser.vue"
243 +import AlertStatusSwitch from "./AlertStatusSwitch.vue"
244 +import StatusIcon from "../common/StatusIcon.vue"
245 +import AssigneeIcon from "../common/AssigneeIcon.vue"
246 +import AlertDetails from "./AlertDetails.vue"
247 +import { handleDeleteAlert } from "./utils"
248 +import _truncate from "lodash/truncate"
249 +import type { Alert } from "@/types/incidentManagement/alerts.d"
250 +
251 +const props = defineProps<{
252 + alertData?: Alert
253 + alertId?: number
254 + compact?: boolean
255 + embedded?: boolean
256 + detailsOnMounted?: boolean
257 + highlight?: boolean
258 +}>()
259 +const { alertData, alertId, compact, embedded, detailsOnMounted, highlight } = toRefs(props)
260 +
261 +const emit = defineEmits<{
262 + (e: "opened"): void
263 + (e: "deleted"): void
264 + (e: "updated", value: Alert): void
265 +}>()
266 +
267 +const InfoIcon = "carbon:information"
268 +const LinkIcon = "carbon:launch"
269 +const TimeIcon = "carbon:time"
270 +const EditIcon = "uil:edit-alt"
271 +const CommentsIcon = "carbon:chat"
272 +const AssetsIcon = "carbon:document-security"
273 +
274 +const { gotoCustomer } = useGoto()
275 +const dialog = useDialog()
276 +const message = useMessage()
277 +const loading = ref(false)
278 +const showDetails = ref(false)
279 +const dFormats = useSettingsStore().dateFormat
280 +const alert = ref<Alert | null>(null)
281 +const alertNameTruncated = computed(() => _truncate(alert.value?.alert_name, { length: 50 }))
282 +
283 +function updateAlert(updatedAlert: Alert) {
284 + alert.value = updatedAlert
285 + emit("updated", updatedAlert)
286 +}
287 +
288 +function getAlert(alertId: number) {
289 + loading.value = true
290 +
291 + Api.incidentManagement
292 + .getAlert(alertId)
293 + .then(res => {
294 + if (res.data.success) {
295 + alert.value = res.data?.alerts?.[0] || null
296 + } else {
297 + message.warning(res.data?.message || "An error occurred. Please try again later.")
298 + }
299 + })
300 + .catch(err => {
301 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
302 + })
303 + .finally(() => {
304 + loading.value = false
305 + })
306 +}
307 +
308 +function handleDelete() {
309 + if (alert.value) {
310 + handleDeleteAlert({
311 + alert: alert.value,
312 + cbBefore: () => {
313 + loading.value = true
314 + },
315 + cbSuccess: () => {
316 + emitDelete()
317 + },
318 + cbAfter: () => {
319 + loading.value = false
320 + },
321 + message,
322 + dialog
323 + })
324 + }
325 +}
326 +
327 +function emitDelete() {
328 + showDetails.value = false
329 + emit("deleted")
330 +}
331 +
332 +function openDetails() {
333 + emit("opened")
334 + showDetails.value = true
335 +}
336 +
337 +function closeDetails() {
338 + showDetails.value = false
339 +}
340 +
341 +onBeforeMount(() => {
342 + if (alertId.value) {
343 + getAlert(alertId.value)
344 + } else if (alertData.value?.id && alertData.value?.linked_cases === undefined) {
345 + getAlert(alertData.value.id)
346 + } else if (alertData.value) {
347 + alert.value = _clone(alertData.value)
348 + }
349 +})
350 +
351 +onMounted(() => {
352 + if (detailsOnMounted.value) {
353 + openDetails()
354 + }
355 +})
356 +</script>
357 +
358 +<style lang="scss" scoped>
359 +.alert-item {
360 + border-radius: var(--border-radius);
361 + background-color: var(--bg-color);
362 + transition: all 0.2s var(--bezier-ease);
363 + border: var(--border-small-050);
364 + overflow: hidden;
365 +
366 + .header-box {
367 + font-family: var(--font-family-mono);
368 + font-size: 13px;
369 + color: var(--fg-secondary-color);
370 +
371 + .id {
372 + word-break: break-word;
373 + line-height: 1.2;
374 + }
375 + }
376 + .main-box {
377 + .content {
378 + word-break: break-word;
379 + }
380 + }
381 +
382 + .footer-box {
383 + border-top: var(--border-small-100);
384 + font-size: 13px;
385 + background-color: var(--bg-secondary-color);
386 +
387 + .time {
388 + display: none;
389 + }
390 + }
391 +
392 + &.embedded {
393 + background-color: var(--bg-secondary-color);
394 + }
395 +
396 + &:hover {
397 + box-shadow: 0px 0px 0px 1px var(--primary-color);
398 + }
399 +
400 + &.highlight {
401 + box-shadow: 0px 0px 0px 1px var(--primary-color);
402 + }
403 +
404 + &:not(.compact) {
405 + .header-box {
406 + .id {
407 + &:hover {
408 + color: var(--primary-color);
409 + }
410 + }
411 + }
412 +
413 + @container (max-width: 600px) {
414 + .header-box {
415 + .time {
416 + display: none;
417 + }
418 + }
419 + .footer-box {
420 + .time {
421 + display: flex;
422 + }
423 + }
424 + }
425 + }
426 +}
427 +</style>
frontend/src/components/incidentManagement/alerts/AlertLinkedCases.vue new
+26
@@ -0,0 +1,26 @@
1 +<template>
2 + <code
3 + class="cursor-pointer text-primary-color"
4 + v-for="linkedCase of linkedCases"
5 + :key="linkedCase.id"
6 + @click="gotoIncidentManagementCases(linkedCase.id)"
7 + >
8 + #{{ linkedCase.id }}
9 + <Icon :name="LinkIcon" :size="14" class="top-0.5 relative" />
10 + </code>
11 +</template>
12 +
13 +<script setup lang="ts">
14 +import { computed, toRefs } from "vue"
15 +import Icon from "@/components/common/Icon.vue"
16 +import { useGoto } from "@/composables/useGoto"
17 +import type { Alert } from "@/types/incidentManagement/alerts.d"
18 +
19 +const props = defineProps<{ alert: Alert }>()
20 +const { alert } = toRefs(props)
21 +
22 +const LinkIcon = "carbon:launch"
23 +
24 +const { gotoIncidentManagementCases } = useGoto()
25 +const linkedCases = computed(() => alert.value.linked_cases)
26 +</script>
frontend/src/components/incidentManagement/alerts/AlertMergeCaseButton.vue new
+165
@@ -0,0 +1,165 @@
1 +<template>
2 + <n-button secondary @click="openDialog()" :loading="merging">
3 + <template #icon><Icon :name="MergeIcon" /></template>
4 + Merge into Case
5 + </n-button>
6 +
7 + <n-modal
8 + v-model:show="showMergeBox"
9 + display-directive="show"
10 + preset="card"
11 + title="Select the case you want to merge it with"
12 + :style="{ maxWidth: 'min(850px, 90vw)', minHeight: 'min(540px, 90vh)', maxHeight: '80vh' }"
13 + content-class="overflow-hidden flex flex-col !px-2"
14 + segmented
15 + >
16 + <n-spin
17 + :show="loadingCases"
18 + class="grow overflow-hidden flex flex-col"
19 + content-class="grow overflow-hidden flex flex-col"
20 + >
21 + <n-scrollbar class="grow flex flex-col" content-class="grow" trigger="none">
22 + <div class="flex flex-col gap-2 px-5 py-2">
23 + <template v-if="linkableCases.length">
24 + <CaseItem
25 + v-for="item of linkableCases"
26 + :key="item.id"
27 + :caseData="item"
28 + compact
29 + embedded
30 + @click="toggleSelectedCase(item)"
31 + :class="{ active: selectedCase?.id === item.id }"
32 + />
33 + </template>
34 + <template v-else>
35 + <n-empty description="No items found" class="justify-center h-48" v-if="!loadingCases" />
36 + </template>
37 + </div>
38 + </n-scrollbar>
39 + </n-spin>
40 +
41 + <template #footer>
42 + <div class="flex justify-end">
43 + <n-button type="success" :disabled="!selectedCase" :loading="merging" @click="linkCase()">
44 + <template #icon><Icon :name="MergeIcon" /></template>
45 + Confirm Merge {{ selectedCase ? `with Case #${selectedCase.id}` : "" }}
46 + </n-button>
47 + </div>
48 + </template>
49 + </n-modal>
50 +</template>
51 +
52 +<script setup lang="ts">
53 +import { inject, ref, toRefs, watch, type Ref } from "vue"
54 +import { NButton, NModal, NSpin, NEmpty, NScrollbar, useMessage } from "naive-ui"
55 +import Icon from "@/components/common/Icon.vue"
56 +import CaseItem from "../cases/CaseItem.vue"
57 +import _orderBy from "lodash/orderBy"
58 +import Api from "@/api"
59 +import type { Alert } from "@/types/incidentManagement/alerts.d"
60 +import type { Case } from "@/types/incidentManagement/cases.d"
61 +
62 +const props = defineProps<{ alert: Alert }>()
63 +const { alert } = toRefs(props)
64 +
65 +const emit = defineEmits<{
66 + (e: "updated", value: Alert): void
67 +}>()
68 +
69 +const MergeIcon = "carbon:ibm-cloud-direct-link-1-connect"
70 +
71 +const message = useMessage()
72 +const merging = ref(false)
73 +const showMergeBox = ref(false)
74 +const loadingCases = ref(false)
75 +const linkableCases = inject<Ref<Case[]>>("linkable-cases", ref([]))
76 +const selectedCase = ref<Case | null>(null)
77 +
78 +watch(showMergeBox, val => {
79 + if (val && !linkableCases.value.length) {
80 + getCasesList()
81 + }
82 +})
83 +
84 +function updateAlert(updatedAlert: Alert) {
85 + emit("updated", updatedAlert)
86 +}
87 +
88 +function openDialog() {
89 + showMergeBox.value = true
90 +}
91 +
92 +function closeDialog() {
93 + showMergeBox.value = false
94 +}
95 +
96 +function toggleSelectedCase(caseEntity: Case) {
97 + if (selectedCase.value?.id === caseEntity.id) {
98 + selectedCase.value = null
99 + } else {
100 + selectedCase.value = caseEntity
101 + }
102 +}
103 +
104 +function getCasesList() {
105 + loadingCases.value = true
106 +
107 + Api.incidentManagement
108 + .getCasesList()
109 + .then(res => {
110 + if (res.data.success) {
111 + linkableCases.value = _orderBy(res.data?.cases || [], ["id"], ["desc"])
112 + } else {
113 + message.warning(res.data?.message || "An error occurred. Please try again later.")
114 + }
115 + })
116 + .catch(err => {
117 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
118 + })
119 + .finally(() => {
120 + loadingCases.value = false
121 + })
122 +}
123 +
124 +function linkCase() {
125 + if (selectedCase.value?.id) {
126 + merging.value = true
127 +
128 + Api.incidentManagement
129 + .linkCase(alert.value.id, selectedCase.value.id)
130 + .then(res => {
131 + if (res.data.success) {
132 + closeDialog()
133 + updateAlert({
134 + ...alert.value,
135 + linked_cases: [
136 + {
137 + id: res.data.case_alert_link.case_id,
138 + case_name: "",
139 + case_description: "",
140 + case_creation_time: new Date(),
141 + assigned_to: null,
142 + case_status: null
143 + }
144 + ]
145 + })
146 + message.success(res.data?.message || "Case linked successfully")
147 + } else {
148 + message.warning(res.data?.message || "An error occurred. Please try again later.")
149 + }
150 + })
151 + .catch(err => {
152 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
153 + })
154 + .finally(() => {
155 + merging.value = false
156 + })
157 + }
158 +}
159 +</script>
160 +
161 +<style lang="scss" scoped>
162 +.active {
163 + box-shadow: 0px 0px 0px 2px var(--success-color) !important;
164 +}
165 +</style>
frontend/src/components/incidentManagement/alerts/AlertOverview.vue new
+218
@@ -0,0 +1,218 @@
1 +<template>
2 + <n-spin :show="loading" class="flex flex-col grow" content-class="flex flex-col grow">
3 + <div class="flex flex-col gap-4 grow justify-between">
4 + <div class="content-box flex flex-col gap-4 py-3">
5 + <div class="px-7 flex sm:!flex-row flex-col gap-4">
6 + <KVCard
7 + :color="
8 + alert.status === 'OPEN' ? 'danger' : alert.status === 'IN_PROGRESS' ? 'warning' : 'success'
9 + "
10 + size="lg"
11 + class="grow w-full"
12 + >
13 + <template #key>
14 + <div class="flex gap-2 items-center">
15 + <StatusIcon :status="alert.status" />
16 + <span>Status</span>
17 + </div>
18 + </template>
19 + <template #value>
20 + <div class="flex">
21 + <AlertStatusSwitch
22 + :alert
23 + v-slot="{ loading: loadingStatus }"
24 + @updated="updateAlert($event)"
25 + >
26 + <div
27 + class="flex gap-3 items-center"
28 + :class="{
29 + 'cursor-not-allowed': loadingStatus,
30 + 'cursor-pointer': !loadingStatus
31 + }"
32 + >
33 + <span>{{ alert.status || "n/d" }}</span>
34 + <n-spin
35 + :size="14"
36 + :show="loadingStatus"
37 + content-class="flex flex-col justify-center"
38 + >
39 + <Icon :name="EditIcon" />
40 + </n-spin>
41 + </div>
42 + </AlertStatusSwitch>
43 + </div>
44 + </template>
45 + </KVCard>
46 +
47 + <KVCard :color="alert.assigned_to ? 'success' : undefined" size="lg" class="grow w-full">
48 + <template #key>
49 + <div class="flex gap-2 items-center">
50 + <AssigneeIcon :assignee="alert.assigned_to" />
51 + <span>Assigned to</span>
52 + </div>
53 + </template>
54 + <template #value>
55 + <div class="flex">
56 + <AlertAssignUser
57 + :alert
58 + v-slot="{ loading: loadingAssignee }"
59 + @updated="updateAlert($event)"
60 + >
61 + <div
62 + class="flex gap-3 items-center"
63 + :class="{
64 + 'cursor-not-allowed': loadingAssignee,
65 + 'cursor-pointer': !loadingAssignee
66 + }"
67 + >
68 + <span>{{ alert.assigned_to || "n/d" }}</span>
69 + <n-spin
70 + :size="14"
71 + :show="loadingAssignee"
72 + content-class="flex flex-col justify-center"
73 + >
74 + <Icon :name="EditIcon" />
75 + </n-spin>
76 + </div>
77 + </AlertAssignUser>
78 + </div>
79 + </template>
80 + </KVCard>
81 + </div>
82 +
83 + <div class="px-7">
84 + <KVCard>
85 + <template #key>description</template>
86 + <template #value>{{ alert.alert_description ?? "-" }}</template>
87 + </KVCard>
88 + </div>
89 +
90 + <div class="px-7 grid gap-2 grid-auto-fit-250">
91 + <KVCard>
92 + <template #key>id</template>
93 + <template #value>#{{ alert.id }}</template>
94 + </KVCard>
95 +
96 + <KVCard>
97 + <template #key>source</template>
98 + <template #value>{{ alert.source ?? "-" }}</template>
99 + </KVCard>
100 +
101 + <KVCard>
102 + <template #key>customer code</template>
103 + <template #value>
104 + <code
105 + class="cursor-pointer text-primary-color"
106 + @click="gotoCustomer({ code: alert.customer_code })"
107 + >
108 + {{ alert.customer_code }}
109 + <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
110 + </code>
111 + </template>
112 + </KVCard>
113 +
114 + <KVCard>
115 + <template #key>assets</template>
116 + <template #value>{{ alert.assets.length }}</template>
117 + </KVCard>
118 +
119 + <KVCard>
120 + <template #key>comments</template>
121 + <template #value>{{ alert.comments.length }}</template>
122 + </KVCard>
123 +
124 + <KVCard>
125 + <template #key>tags</template>
126 + <template #value>
127 + <AlertTags :alert @updated="updateAlert" />
128 + </template>
129 + </KVCard>
130 + </div>
131 + </div>
132 +
133 + <div class="footer-box px-7 py-4 flex items-center gap-2">
134 + <AlertCreateCaseButton :alert @updated="updateAlert" v-if="!linkedCases.length" />
135 +
136 + <AlertMergeCaseButton :alert @updated="updateAlert" v-if="!linkedCases.length" />
137 +
138 + <div v-if="linkedCases.length" class="flex flex-wrap gap-3 items-center">
139 + <span>Linked Cases:</span>
140 + <AlertLinkedCases :alert />
141 + </div>
142 +
143 + <div class="grow"></div>
144 +
145 + <n-button type="error" secondary @click="handleDelete()">
146 + <template #icon><Icon :name="TrashIcon" /></template>
147 + Delete
148 + </n-button>
149 + </div>
150 + </div>
151 + </n-spin>
152 +</template>
153 +
154 +<script setup lang="ts">
155 +import { computed, defineAsyncComponent, ref, toRefs } from "vue"
156 +import { NButton, NSpin, useMessage, useDialog } from "naive-ui"
157 +import AlertAssignUser from "./AlertAssignUser.vue"
158 +import AlertStatusSwitch from "./AlertStatusSwitch.vue"
159 +import AlertTags from "./AlertTags.vue"
160 +import StatusIcon from "../common/StatusIcon.vue"
161 +import AssigneeIcon from "../common/AssigneeIcon.vue"
162 +import KVCard from "@/components/common/KVCard.vue"
163 +import Icon from "@/components/common/Icon.vue"
164 +import { useGoto } from "@/composables/useGoto"
165 +import { handleDeleteAlert } from "./utils"
166 +import type { Alert } from "@/types/incidentManagement/alerts.d"
167 +const AlertCreateCaseButton = defineAsyncComponent(() => import("./AlertCreateCaseButton.vue"))
168 +const AlertMergeCaseButton = defineAsyncComponent(() => import("./AlertMergeCaseButton.vue"))
169 +const AlertLinkedCases = defineAsyncComponent(() => import("./AlertLinkedCases.vue"))
170 +
171 +const props = defineProps<{ alert: Alert }>()
172 +const { alert } = toRefs(props)
173 +
174 +const emit = defineEmits<{
175 + (e: "deleted"): void
176 + (e: "updated", value: Alert): void
177 +}>()
178 +
179 +const TrashIcon = "carbon:trash-can"
180 +const LinkIcon = "carbon:launch"
181 +const EditIcon = "uil:edit-alt"
182 +
183 +const { gotoCustomer } = useGoto()
184 +const dialog = useDialog()
185 +const message = useMessage()
186 +const loading = ref(false)
187 +const linkedCases = computed(() => alert.value.linked_cases)
188 +
189 +function updateAlert(updatedAlert: Alert) {
190 + emit("updated", updatedAlert)
191 +}
192 +
193 +function handleDelete() {
194 + if (alert.value) {
195 + handleDeleteAlert({
196 + alert: alert.value,
197 + cbBefore: () => {
198 + loading.value = true
199 + },
200 + cbSuccess: () => {
201 + emit("deleted")
202 + },
203 + cbAfter: () => {
204 + loading.value = false
205 + },
206 + message,
207 + dialog
208 + })
209 + }
210 +}
211 +</script>
212 +
213 +<style lang="scss" scoped>
214 +.footer-box {
215 + border-top: var(--border-small-100);
216 + background-color: var(--bg-secondary-color);
217 +}
218 +</style>
frontend/src/components/incidentManagement/alerts/AlertStatusSwitch.vue new
+77
@@ -0,0 +1,77 @@
1 +<template>
2 + <n-popselect
3 + v-model:value="statusSelected"
4 + v-model:show="listVisible"
5 + :options="statusOptions"
6 + :disabled="loading"
7 + size="medium"
8 + scrollable
9 + to="body"
10 + >
11 + <slot :loading="loading" />
12 + </n-popselect>
13 +</template>
14 +
15 +<script setup lang="ts">
16 +import { computed, onBeforeMount, ref, toRefs, watch } from "vue"
17 +import { useMessage, NPopselect } from "naive-ui"
18 +import Api from "@/api"
19 +import type { Alert, AlertStatus } from "@/types/incidentManagement/alerts.d"
20 +
21 +const props = defineProps<{
22 + alert: Alert
23 +}>()
24 +const { alert } = toRefs(props)
25 +
26 +const emit = defineEmits<{
27 + (e: "updated", value: Alert): void
28 +}>()
29 +
30 +const loading = ref(false)
31 +const message = useMessage()
32 +const listVisible = ref(false)
33 +const status = computed(() => alert.value.status)
34 +const statusOptions = ref<
35 + {
36 + label: string
37 + value: AlertStatus
38 + }[]
39 +>([
40 + { label: "Open", value: "OPEN" },
41 + { label: "In progress", value: "IN_PROGRESS" },
42 + { label: "Closed", value: "CLOSED" }
43 +])
44 +const statusSelected = ref<AlertStatus | null>(null)
45 +
46 +function updateStatus() {
47 + if (statusSelected.value && statusSelected.value !== status.value) {
48 + loading.value = true
49 +
50 + Api.incidentManagement
51 + .updateAlertStatus(alert.value.id, statusSelected.value)
52 + .then(res => {
53 + if (res.data.success && statusSelected.value) {
54 + emit("updated", { ...alert.value, status: statusSelected.value })
55 + } else {
56 + message.warning(res.data?.message || "An error occurred. Please try again later.")
57 + }
58 + })
59 + .catch(err => {
60 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
61 + })
62 + .finally(() => {
63 + loading.value = false
64 + })
65 + }
66 +}
67 +
68 +watch(statusSelected, () => {
69 + updateStatus()
70 +})
71 +
72 +onBeforeMount(() => {
73 + if (status.value) {
74 + statusSelected.value = status.value
75 + }
76 +})
77 +</script>
frontend/src/components/incidentManagement/alerts/AlertTags.vue new
+103
@@ -0,0 +1,103 @@
1 +<template>
2 + <n-spin :show="deletingTag" size="small" content-class="flex flex-wrap gap-2">
3 + <n-tag closable @close="deleteTag(tag.id)" v-for="tag of alert.tags" :key="tag.id" size="small">
4 + {{ tag.tag }}
5 + </n-tag>
6 +
7 + <n-dynamic-tags @create="newAlertTag" size="small" :value="[]">
8 + <template #trigger="{ activate, disabled }">
9 + <n-button
10 + size="tiny"
11 + type="primary"
12 + dashed
13 + :loading="creatingTag"
14 + :disabled="disabled"
15 + @click="activate()"
16 + >
17 + <template #icon>
18 + <Icon :name="AddIcon" />
19 + </template>
20 + New Tag
21 + </n-button>
22 + </template>
23 + </n-dynamic-tags>
24 + </n-spin>
25 +</template>
26 +
27 +<script setup lang="ts">
28 +import { ref, toRefs } from "vue"
29 +import { NButton, NSpin, NTag, NDynamicTags, useMessage } from "naive-ui"
30 +import Icon from "@/components/common/Icon.vue"
31 +import _trim from "lodash/trim"
32 +import Api from "@/api"
33 +import type { Alert } from "@/types/incidentManagement/alerts.d"
34 +
35 +const props = defineProps<{ alert: Alert }>()
36 +const { alert } = toRefs(props)
37 +
38 +const emit = defineEmits<{
39 + (e: "updated", value: Alert): void
40 +}>()
41 +
42 +const AddIcon = "carbon:add"
43 +
44 +const message = useMessage()
45 +const creatingTag = ref(false)
46 +const deletingTag = ref(false)
47 +
48 +function updateAlert(updatedAlert: Alert) {
49 + emit("updated", updatedAlert)
50 +}
51 +
52 +function deleteTag(tagId: number) {
53 + deletingTag.value = true
54 +
55 + Api.incidentManagement
56 + .deleteAlertTag(alert.value.id, tagId)
57 + .then(res => {
58 + if (res.data.success) {
59 + alert.value.tags = alert.value.tags.filter(o => o.id !== tagId)
60 + updateAlert(alert.value)
61 + } else {
62 + message.error(res.data?.message || "An error occurred. Please try again later.")
63 + }
64 + })
65 + .catch(err => {
66 + if (err.response?.status === 401) {
67 + message.error(err.response?.data?.message || "Alert tag Delete returned Unauthorized.")
68 + } else {
69 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
70 + }
71 + })
72 + .finally(() => {
73 + deletingTag.value = false
74 + })
75 +}
76 +
77 +function newAlertTag(text: string): string | { label: string; value: string } {
78 + const tag = _trim(text)
79 +
80 + if (tag && alert.value.tags.filter(o => o.tag.toLowerCase() === tag.toLowerCase()).length === 0) {
81 + creatingTag.value = true
82 +
83 + Api.incidentManagement
84 + .newAlertTag(alert.value.id, tag)
85 + .then(res => {
86 + if (res.data.success) {
87 + alert.value.tags.push(res.data.alert_tag)
88 + updateAlert(alert.value)
89 + } else {
90 + message.warning(res.data?.message || "An error occurred. Please try again later.")
91 + }
92 + })
93 + .catch(err => {
94 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
95 + })
96 + .finally(() => {
97 + creatingTag.value = false
98 + })
99 + }
100 +
101 + return ""
102 +}
103 +</script>
frontend/src/components/incidentManagement/alerts/AlertTimeline.vue new
+21
@@ -0,0 +1,21 @@
1 +<template>
2 + <n-timeline>
3 + <n-timeline-item type="success" title="Creation time" :time="formatDateTime(alert.alert_creation_time)" />
4 + <n-timeline-item v-if="alert.time_closed" title="Time closed" :time="formatDateTime(alert.time_closed)" />
5 + </n-timeline>
6 +</template>
7 +
8 +<script setup lang="ts">
9 +import { useSettingsStore } from "@/stores/settings"
10 +import { formatDate } from "@/utils"
11 +import { NTimeline, NTimelineItem } from "naive-ui"
12 +import type { Alert } from "@/types/incidentManagement/alerts.d"
13 +
14 +const { alert } = defineProps<{ alert: Alert }>()
15 +
16 +const dFormats = useSettingsStore().dateFormat
17 +
18 +function formatDateTime(timestamp: Date): string {
19 + return formatDate(timestamp, dFormats.datetimesec).toString()
20 +}
21 +</script>
frontend/src/components/incidentManagement/alerts/AlertsList.vue new
+390
@@ -0,0 +1,390 @@
1 +<template>
2 + <div class="alerts-list">
3 + <div class="header flex items-center justify-end gap-2" ref="header">
4 + <div class="info grow flex gap-2 lg:!hidden">
5 + <n-popover overlap placement="left">
6 + <template #trigger>
7 + <div class="bg-color border-radius">
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 class="box text-error-color">
21 + Open :
22 + <code>{{ statusOpenTotal }}</code>
23 + </div>
24 + <div class="box text-warning-color">
25 + In Progress :
26 + <code>{{ statusInProgressTotal }}</code>
27 + </div>
28 + <div class="box text-success-color">
29 + Close :
30 + <code>{{ statusCloseTotal }}</code>
31 + </div>
32 + </div>
33 + </n-popover>
34 + </div>
35 + <div class="info grow lg:flex gap-2 hidden text-sm">
36 + Total :
37 + <code>{{ total }}</code>
38 + <span>/</span>
39 + <span class="text-secondary-color">Open :</span>
40 + <code class="text-error-color">{{ statusOpenTotal }}</code>
41 + <span>/</span>
42 + <span class="text-secondary-color">In Progress :</span>
43 + <code class="text-warning-color">{{ statusInProgressTotal }}</code>
44 + <span>/</span>
45 + <span class="text-secondary-color">Close :</span>
46 + <code class="text-success-color">{{ statusCloseTotal }}</code>
47 + </div>
48 + <n-pagination
49 + v-model:page="currentPage"
50 + v-model:page-size="pageSize"
51 + :page-slot="pageSlot"
52 + :show-size-picker="showSizePicker"
53 + :page-sizes="pageSizes"
54 + :item-count="total"
55 + :simple="simpleMode"
56 + />
57 + <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="!px-0">
58 + <template #trigger>
59 + <div class="bg-color border-radius">
60 + <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
61 + <n-button size="small" @click="showFilters = true">
62 + <template #icon>
63 + <Icon :name="FilterIcon"></Icon>
64 + </template>
65 + </n-button>
66 + </n-badge>
67 + </div>
68 + </template>
69 + <div class="py-1 flex flex-col gap-2">
70 + <div class="px-3">
71 + <n-input-group>
72 + <n-select
73 + v-model:value="filters.type"
74 + :options="typeOptions"
75 + placeholder="Filter by..."
76 + clearable
77 + class="!w-36"
78 + />
79 +
80 + <n-select
81 + v-if="filters.type === 'status'"
82 + v-model:value="filters.value"
83 + :options="statusOptions"
84 + placeholder="Value..."
85 + :disabled="!filters.type"
86 + clearable
87 + class="!w-56"
88 + />
89 + <n-select
90 + v-else-if="filters.type === 'assignedTo'"
91 + v-model:value="filters.value"
92 + :options="usersOptions"
93 + placeholder="Value..."
94 + :disabled="!filters.type"
95 + clearable
96 + class="!w-56"
97 + />
98 + <n-input
99 + v-else
100 + v-model:value="filters.value"
101 + placeholder="Value..."
102 + :disabled="!filters.type"
103 + clearable
104 + class="!w-56"
105 + />
106 + </n-input-group>
107 + </div>
108 + <div class="px-3 flex justify-between gap-2">
109 + <div class="flex justify-start gap-2">
110 + <n-button size="small" @click="showFilters = false" quaternary>Close</n-button>
111 + </div>
112 + <div class="flex justify-end gap-2">
113 + <n-button size="small" @click="resetFilters()" secondary>Reset</n-button>
114 + <n-button size="small" @click="getData()" type="primary" secondary :loading>
115 + Submit
116 + </n-button>
117 + </div>
118 + </div>
119 + </div>
120 + </n-popover>
121 + </div>
122 + <n-spin :show="loading">
123 + <div class="list flex flex-col gap-2 my-3">
124 + <template v-if="alertsList.length">
125 + <AlertItem
126 + v-for="alert of alertsList"
127 + :key="alert.id"
128 + :alertData="alert"
129 + :highlight="highlight === alert.id.toString()"
130 + :detailsOnMounted="highlight === alert.id.toString() && !highlightedItemOpened"
131 + class="item-appear item-appear-bottom item-appear-005"
132 + @opened="highlightedItemOpened = true"
133 + @deleted="getData()"
134 + @updated="updateAlert($event)"
135 + />
136 + </template>
137 + <template v-else>
138 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
139 + </template>
140 + </div>
141 + </n-spin>
142 + <div class="footer flex justify-end">
143 + <n-pagination
144 + v-model:page="currentPage"
145 + :page-size="pageSize"
146 + :item-count="total"
147 + :page-slot="6"
148 + v-if="alertsList.length > 3"
149 + />
150 + </div>
151 + </div>
152 +</template>
153 +
154 +<script setup lang="ts">
155 +import { ref, onBeforeMount, computed, watch, provide, toRefs, nextTick } from "vue"
156 +import {
157 + useMessage,
158 + NSpin,
159 + NPopover,
160 + NButton,
161 + NEmpty,
162 + NSelect,
163 + NPagination,
164 + NInputGroup,
165 + NBadge,
166 + NInput
167 +} from "naive-ui"
168 +import Api from "@/api"
169 +import _cloneDeep from "lodash/cloneDeep"
170 +import _orderBy from "lodash/orderBy"
171 +import Icon from "@/components/common/Icon.vue"
172 +import { useResizeObserver } from "@vueuse/core"
173 +import type { Alert, AlertStatus } from "@/types/incidentManagement/alerts.d"
174 +import type { AlertsQuery } from "@/api/endpoints/incidentManagement"
175 +import AlertItem from "./AlertItem.vue"
176 +import type { Case } from "@/types/incidentManagement/cases.d"
177 +
178 +export interface AlertsListFilter {
179 + type: "status" | "assetName" | "assignedTo"
180 + value: string | AlertStatus
181 +}
182 +
183 +const props = defineProps<{ highlight: string | null | undefined }>()
184 +const { highlight } = toRefs(props)
185 +
186 +const FilterIcon = "carbon:filter-edit"
187 +const InfoIcon = "carbon:information"
188 +
189 +const message = useMessage()
190 +const loading = ref(false)
191 +const showFilters = ref(false)
192 +const alertsList = ref<Alert[]>([])
193 +const availableUsers = ref<string[]>([])
194 +const linkableCases = ref<Case[]>([])
195 +
196 +const pageSize = ref(25)
197 +const currentPage = ref(1)
198 +const simpleMode = ref(false)
199 +const showSizePicker = ref(true)
200 +const pageSizes = [10, 25, 50, 100]
201 +const header = ref()
202 +const pageSlot = ref(8)
203 +
204 +const total = ref(0)
205 +const statusOpenTotal = ref(0)
206 +const statusInProgressTotal = ref(0)
207 +const statusCloseTotal = ref(0)
208 +
209 +const filters = ref<Partial<AlertsListFilter>>({})
210 +const lastFilters = ref<Partial<AlertsListFilter>>({})
211 +
212 +const filtered = computed<boolean>(() => {
213 + return !!filters.value.type && !!filters.value.value
214 +})
215 +
216 +const typeOptions = [
217 + { label: "Status", value: "status" },
218 + { label: "Asset Name", value: "assetName" },
219 + { label: "Assigned To", value: "assignedTo" },
220 + { label: "Tag", value: "tag" },
221 + { label: "Title", value: "title" }
222 +]
223 +
224 +const statusOptions: { label: string; value: AlertStatus }[] = [
225 + { label: "Open", value: "OPEN" },
226 + { label: "Closed", value: "CLOSED" },
227 + { label: "In progress", value: "IN_PROGRESS" }
228 +]
229 +
230 +const usersOptions = computed(() => availableUsers.value.map(o => ({ label: o, value: o })))
231 +
232 +const highlightedItemFound = ref(!highlight.value)
233 +const highlightedItemOpened = ref(!highlight.value)
234 +
235 +watch(showFilters, val => {
236 + if (!val) {
237 + filters.value = _cloneDeep(lastFilters.value)
238 + }
239 +})
240 +
241 +watch(
242 + () => filters.value.type,
243 + () => {
244 + filters.value.value = undefined
245 + }
246 +)
247 +
248 +watch(currentPage, () => {
249 + getData()
250 +})
251 +
252 +watch(pageSize, () => {
253 + if (currentPage.value === 1) {
254 + getData()
255 + } else {
256 + currentPage.value = 1
257 + }
258 +})
259 +
260 +watch(
261 + alertsList,
262 + () => {
263 + if (
264 + alertsList.value.length &&
265 + !alertsList.value.find(o => o.id.toString() === highlight.value) &&
266 + currentPage.value < total.value &&
267 + !highlightedItemFound.value
268 + ) {
269 + nextTick(() => {
270 + currentPage.value++
271 + })
272 + }
273 +
274 + if (alertsList.value.find(o => o.id.toString() === highlight.value)) {
275 + highlightedItemFound.value = true
276 + }
277 + },
278 + { immediate: true }
279 +)
280 +
281 +provide("assignable-users", availableUsers)
282 +
283 +provide("linkable-cases", linkableCases)
284 +
285 +function resetFilters() {
286 + filters.value.type = undefined
287 + filters.value.value = undefined
288 + showFilters.value = false
289 + getData()
290 +}
291 +
292 +function updateAlert(updatedAlert: Alert) {
293 + const alertIndex = alertsList.value.findIndex(o => o.id === updatedAlert.id)
294 + if (alertIndex !== -1) {
295 + alertsList.value[alertIndex] = updatedAlert
296 + }
297 +}
298 +
299 +function getData() {
300 + showFilters.value = false
301 + loading.value = true
302 +
303 + lastFilters.value = _cloneDeep(filters.value)
304 +
305 + const query: Partial<AlertsQuery> = {
306 + page: currentPage.value,
307 + pageSize: pageSize.value
308 + }
309 +
310 + if (filtered.value) {
311 + // @ts-expect-error filters properties infer are ignored
312 + query.filters = { [filters.value.type]: filters.value.value }
313 + }
314 +
315 + Api.incidentManagement
316 + .getAlertsList(query)
317 + .then(res => {
318 + if (res.data.success) {
319 + alertsList.value = res.data?.alerts || []
320 + total.value = res.data.total || 0
321 + statusCloseTotal.value = res.data.closed || 0
322 + statusInProgressTotal.value = res.data.in_progress || 0
323 + statusOpenTotal.value = res.data.open || 0
324 + } else {
325 + message.warning(res.data?.message || "An error occurred. Please try again later.")
326 + }
327 + })
328 + .catch(err => {
329 + alertsList.value = []
330 +
331 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
332 + })
333 + .finally(() => {
334 + loading.value = false
335 + })
336 +}
337 +
338 +function getAvailableUsers() {
339 + Api.incidentManagement
340 + .getAvailableUsers()
341 + .then(res => {
342 + if (res.data.success) {
343 + availableUsers.value = res.data?.available_users || []
344 + } else {
345 + message.warning(res.data?.message || "An error occurred. Please try again later.")
346 + }
347 + })
348 + .catch(err => {
349 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
350 + })
351 +}
352 +
353 +function getCases() {
354 + Api.incidentManagement
355 + .getCasesList()
356 + .then(res => {
357 + if (res.data.success) {
358 + linkableCases.value = _orderBy(res.data?.cases || [], ["id"], ["desc"])
359 + } else {
360 + message.warning(res.data?.message || "An error occurred. Please try again later.")
361 + }
362 + })
363 + .catch(err => {
364 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
365 + })
366 +}
367 +
368 +useResizeObserver(header, entries => {
369 + const entry = entries[0]
370 + const { width } = entry.contentRect
371 +
372 + pageSlot.value = width < 700 ? 5 : 8
373 + simpleMode.value = width < 550
374 +})
375 +
376 +onBeforeMount(() => {
377 + getData()
378 + getAvailableUsers()
379 + getCases()
380 +})
381 +</script>
382 +
383 +<style lang="scss" scoped>
384 +.alerts-list {
385 + .list {
386 + container-type: inline-size;
387 + min-height: 200px;
388 + }
389 +}
390 +</style>
frontend/src/components/incidentManagement/alerts/utils.ts new
+81
@@ -0,0 +1,81 @@
1 +import { h } from "vue"
2 +import Api from "@/api"
3 +import type { MessageApiInjection } from "naive-ui/es/message/src/MessageProvider"
4 +import type { DialogApiInjection } from "naive-ui/es/dialog/src/DialogProvider"
5 +import type { Alert } from "@/types/incidentManagement/alerts.d"
6 +
7 +export interface DeleteAlertParams {
8 + alert: Alert
9 + cbBefore?: () => void
10 + cbSuccess?: () => void
11 + cbAfter?: () => void
12 + cbError?: () => void
13 + message: MessageApiInjection
14 + dialog: DialogApiInjection
15 +}
16 +
17 +export function handleDeleteAlert({
18 + alert,
19 + cbBefore,
20 + cbSuccess,
21 + cbAfter,
22 + cbError,
23 + dialog,
24 + message
25 +}: DeleteAlertParams) {
26 + dialog.warning({
27 + title: "Confirm",
28 + content: () =>
29 + h("div", {
30 + innerHTML: `Are you sure you want to delete the Alert:<br/><strong>${alert.id} - ${alert.alert_name}</strong> ?`
31 + }),
32 + positiveText: "Yes I'm sure",
33 + negativeText: "Cancel",
34 + onPositiveClick: () => {
35 + deleteAlert({ alert, cbBefore, cbSuccess, cbAfter, cbError, dialog, message })
36 + },
37 + onNegativeClick: () => {
38 + message.info("Delete canceled")
39 + }
40 + })
41 +}
42 +
43 +export function deleteAlert({ alert, cbBefore, cbSuccess, cbAfter, cbError, message }: DeleteAlertParams) {
44 + if (cbBefore && typeof cbBefore === "function") {
45 + cbBefore()
46 + }
47 +
48 + Api.incidentManagement
49 + .deleteAlert(alert.id)
50 + .then(res => {
51 + if (res.data.success) {
52 + message.success("Alert was successfully deleted.")
53 +
54 + if (cbSuccess && typeof cbSuccess === "function") {
55 + cbSuccess()
56 + }
57 + } else {
58 + message.error(res.data?.message || "An error occurred. Please try again later.")
59 +
60 + if (cbError && typeof cbError === "function") {
61 + cbError()
62 + }
63 + }
64 + })
65 + .catch(err => {
66 + if (err.response?.status === 401) {
67 + message.error(err.response?.data?.message || "Alert Delete returned Unauthorized.")
68 + } else {
69 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
70 + }
71 +
72 + if (cbError && typeof cbError === "function") {
73 + cbError()
74 + }
75 + })
76 + .finally(() => {
77 + if (cbAfter && typeof cbAfter === "function") {
78 + cbAfter()
79 + }
80 + })
81 +}
frontend/src/components/incidentManagement/cases/CaseAssignUser.vue new
+115
@@ -0,0 +1,115 @@
1 +<template>
2 + <n-popselect
3 + v-model:value="userSelected"
4 + v-model:show="usersListVisible"
5 + :options="usersOptions"
6 + :disabled="loadingUsers"
7 + size="medium"
8 + scrollable
9 + to="body"
10 + >
11 + <slot :loading="loadingUsers" />
12 + </n-popselect>
13 +</template>
14 +
15 +<script setup lang="ts">
16 +import { computed, inject, onBeforeMount, ref, toRefs, watch, type Ref } from "vue"
17 +import { useMessage, NPopselect } from "naive-ui"
18 +import Api from "@/api"
19 +import type { Case } from "@/types/incidentManagement/cases.d"
20 +
21 +const props = defineProps<{
22 + caseData: Case
23 +}>()
24 +const { caseData } = toRefs(props)
25 +
26 +const emit = defineEmits<{
27 + (e: "updated", value: Case): void
28 +}>()
29 +
30 +const loadingUsers = ref(false)
31 +const users = inject<Ref<string[]>>("assignable-users", ref([]))
32 +const message = useMessage()
33 +const usersListVisible = ref(false)
34 +const assignedTo = computed(() => caseData.value.assigned_to)
35 +const usersOptions = ref<
36 + {
37 + label: string
38 + value: string
39 + }[]
40 +>([])
41 +const userSelected = ref<string | null>(null)
42 +
43 +function getUsers() {
44 + loadingUsers.value = true
45 +
46 + Api.incidentManagement
47 + .getAvailableUsers()
48 + .then(res => {
49 + if (res.data.success) {
50 + const usersList = res.data?.available_users || []
51 + parseUsers(usersList)
52 + } else {
53 + message.warning(res.data?.message || "An error occurred. Please try again later.")
54 + }
55 + })
56 + .catch(err => {
57 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
58 + })
59 + .finally(() => {
60 + loadingUsers.value = false
61 + })
62 +}
63 +
64 +function assignUser() {
65 + if (userSelected.value && userSelected.value !== assignedTo.value) {
66 + loadingUsers.value = true
67 +
68 + Api.incidentManagement
69 + .updateCaseAssignedUser(caseData.value.id, userSelected.value)
70 + .then(res => {
71 + if (res.data.success) {
72 + emit("updated", { ...caseData.value, assigned_to: userSelected.value })
73 + } else {
74 + message.warning(res.data?.message || "An error occurred. Please try again later.")
75 + }
76 + })
77 + .catch(err => {
78 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
79 + })
80 + .finally(() => {
81 + loadingUsers.value = false
82 + })
83 + }
84 +}
85 +
86 +function parseUsers(users: string[]) {
87 + usersOptions.value = users.map(o => ({ label: o, value: o }))
88 +}
89 +
90 +watch(users, val => {
91 + if (val !== undefined && val.length) {
92 + parseUsers(val)
93 + }
94 +})
95 +
96 +watch(userSelected, () => {
97 + assignUser()
98 +})
99 +
100 +watch(usersListVisible, val => {
101 + if (val && !usersOptions.value.length) {
102 + getUsers()
103 + }
104 +})
105 +
106 +onBeforeMount(() => {
107 + if (assignedTo.value) {
108 + userSelected.value = assignedTo.value
109 + }
110 +
111 + if (users.value.length) {
112 + parseUsers(users.value)
113 + }
114 +})
115 +</script>
frontend/src/components/incidentManagement/cases/CaseDetails.vue new
+89
@@ -0,0 +1,89 @@
1 +<template>
2 + <n-spin :show="loading" class="flex flex-col grow" content-class="flex flex-col grow">
3 + <n-tabs
4 + type="line"
5 + animated
6 + :tabs-padding="24"
7 + v-if="caseEntity"
8 + class="grow"
9 + pane-wrapper-class="flex flex-col grow"
10 + >
11 + <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy" class="flex flex-col grow">
12 + <div class="pt-1">
13 + <CaseOverview :caseData="caseEntity" @updated="updateCase($event)" @deleted="emit('deleted')" />
14 + </div>
15 + </n-tab-pane>
16 + <n-tab-pane name="Alerts" tab="Alerts" display-directive="show:lazy">
17 + <div class="p-7 pt-4 flex flex-col gap-2">
18 + <AlertItem
19 + v-for="alert of caseEntity.alerts"
20 + :key="alert.id"
21 + :alertData="alert"
22 + embedded
23 + @deleted="getCase(caseEntity.id)"
24 + @updated="getCase(caseEntity.id)"
25 + />
26 + </div>
27 + </n-tab-pane>
28 + </n-tabs>
29 + </n-spin>
30 +</template>
31 +
32 +<script setup lang="ts">
33 +import { defineAsyncComponent, onBeforeMount, ref, toRefs } from "vue"
34 +import { NTabs, NTabPane, NSpin, useMessage } from "naive-ui"
35 +import _clone from "lodash/cloneDeep"
36 +import Api from "@/api"
37 +import type { Case } from "@/types/incidentManagement/cases.d"
38 +
39 +const CaseOverview = defineAsyncComponent(() => import("./CaseOverview.vue"))
40 +const AlertItem = defineAsyncComponent(() => import("../alerts/AlertItem.vue"))
41 +
42 +const props = defineProps<{
43 + caseData?: Case
44 + caseId?: number
45 +}>()
46 +const { caseData, caseId } = toRefs(props)
47 +
48 +const emit = defineEmits<{
49 + (e: "deleted"): void
50 + (e: "updated", value: Case): void
51 +}>()
52 +
53 +const message = useMessage()
54 +const loading = ref(false)
55 +const caseEntity = ref<Case | null>(null)
56 +
57 +function updateCase(updatedCase: Case) {
58 + caseEntity.value = updatedCase
59 + emit("updated", updatedCase)
60 +}
61 +
62 +function getCase(caseId: number) {
63 + loading.value = true
64 +
65 + Api.incidentManagement
66 + .getCase(caseId)
67 + .then(res => {
68 + if (res.data.success) {
69 + caseEntity.value = res.data?.cases?.[0] || null
70 + } else {
71 + message.warning(res.data?.message || "An error occurred. Please try again later.")
72 + }
73 + })
74 + .catch(err => {
75 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
76 + })
77 + .finally(() => {
78 + loading.value = false
79 + })
80 +}
81 +
82 +onBeforeMount(() => {
83 + if (caseId.value) {
84 + getCase(caseId.value)
85 + } else if (caseData.value) {
86 + caseEntity.value = _clone(caseData.value)
87 + }
88 +})
89 +</script>
frontend/src/components/incidentManagement/cases/CaseItem.vue new
+374
@@ -0,0 +1,374 @@
1 +<template>
2 + <div
3 + class="case-item"
4 + :class="['status-' + caseEntity?.case_status, { compact, embedded, 'cursor-pointer': compact, highlight }]"
5 + >
6 + <n-spin :show="loading">
7 + <div class="flex flex-col" v-if="caseEntity">
8 + <div class="header-box px-5 py-3 pb-0 flex justify-between items-center">
9 + <div class="id flex items-center gap-2 cursor-pointer" @click="compact ? undefined : openDetails()">
10 + <span>#{{ caseEntity.id }}</span>
11 + <Icon :name="InfoIcon" :size="16" v-if="!compact"></Icon>
12 + </div>
13 + <div class="time" v-if="caseEntity.case_creation_time">
14 + {{ formatDate(caseEntity.case_creation_time, dFormats.datetime) }}
15 + </div>
16 + </div>
17 +
18 + <div class="main-box flex flex-col gap-3 px-5 py-3">
19 + <div class="content flex flex-col gap-1 grow">
20 + <div class="title">
21 + {{ caseEntity.case_name }}
22 + </div>
23 + </div>
24 +
25 + <div class="badges-box flex flex-wrap items-center gap-3" v-if="compact">
26 + <Badge
27 + type="splitted"
28 + class="cursor-pointer"
29 + bright
30 + :color="
31 + caseEntity.case_status === 'OPEN'
32 + ? 'danger'
33 + : caseEntity.case_status === 'IN_PROGRESS'
34 + ? 'warning'
35 + : caseEntity.case_status === 'CLOSED'
36 + ? 'success'
37 + : undefined
38 + "
39 + >
40 + <template #iconLeft>
41 + <StatusIcon :status="caseEntity.case_status" />
42 + </template>
43 + <template #label>Status</template>
44 + <template #value>
45 + <div class="flex gap-2 items-center">
46 + {{ caseEntity.case_status || "n/d" }}
47 + </div>
48 + </template>
49 + </Badge>
50 +
51 + <Badge
52 + type="splitted"
53 + class="cursor-pointer"
54 + bright
55 + :color="caseEntity.assigned_to ? 'success' : undefined"
56 + >
57 + <template #iconLeft>
58 + <AssigneeIcon :assignee="caseEntity.assigned_to" />
59 + </template>
60 + <template #label>Assignee</template>
61 + <template #value>
62 + <div class="flex gap-2 items-center">
63 + {{ caseEntity.assigned_to || "n/d" }}
64 + </div>
65 + </template>
66 + </Badge>
67 + </div>
68 +
69 + <div class="badges-box flex flex-wrap items-center gap-3" v-else>
70 + <CaseStatusSwitch
71 + :caseData="caseEntity"
72 + v-slot="{ loading: loadingStatus }"
73 + @updated="updateCase($event)"
74 + >
75 + <Badge
76 + type="splitted"
77 + class="cursor-pointer"
78 + bright
79 + :color="
80 + caseEntity.case_status === 'OPEN'
81 + ? 'danger'
82 + : caseEntity.case_status === 'IN_PROGRESS'
83 + ? 'warning'
84 + : caseEntity.case_status === 'CLOSED'
85 + ? 'success'
86 + : undefined
87 + "
88 + >
89 + <template #iconLeft>
90 + <n-spin
91 + :size="12"
92 + :show="loadingStatus"
93 + content-class="flex flex-col justify-center"
94 + >
95 + <StatusIcon :status="caseEntity.case_status" />
96 + </n-spin>
97 + </template>
98 + <template #label>Status</template>
99 + <template #value>
100 + <div class="flex gap-2 items-center">
101 + {{ caseEntity.case_status || "n/d" }}
102 + <Icon :name="EditIcon" :size="13" />
103 + </div>
104 + </template>
105 + </Badge>
106 + </CaseStatusSwitch>
107 +
108 + <CaseAssignUser
109 + :caseData="caseEntity"
110 + v-slot="{ loading: loadingAssignee }"
111 + @updated="updateCase($event)"
112 + >
113 + <Badge
114 + type="splitted"
115 + class="cursor-pointer"
116 + bright
117 + :color="caseEntity.assigned_to ? 'success' : undefined"
118 + >
119 + <template #iconLeft>
120 + <n-spin
121 + :size="12"
122 + :show="loadingAssignee"
123 + content-class="flex flex-col justify-center"
124 + >
125 + <AssigneeIcon :assignee="caseEntity.assigned_to" />
126 + </n-spin>
127 + </template>
128 + <template #label>Assignee</template>
129 + <template #value>
130 + <div class="flex gap-2 items-center">
131 + {{ caseEntity.assigned_to || "n/d" }}
132 + <Icon :name="EditIcon" :size="13" />
133 + </div>
134 + </template>
135 + </Badge>
136 + </CaseAssignUser>
137 + </div>
138 + </div>
139 +
140 + <div class="footer-box px-5 py-3 flex justify-between items-center" v-if="!compact">
141 + <n-collapse :trigger-areas="['main', 'arrow']">
142 + <n-collapse-item name="alerts-list">
143 + <template #header>
144 + Alerts
145 + <code class="ml-2">{{ caseEntity.alerts.length }}</code>
146 + </template>
147 + <template #header-extra>
148 + <div class="actions-box">
149 + <n-button quaternary size="tiny" @click="handleDelete()">Delete Case</n-button>
150 + </div>
151 + </template>
152 + <div>
153 + <template v-if="caseEntity.alerts.length">
154 + <AlertItem
155 + v-for="alert of caseEntity.alerts"
156 + :key="alert.id"
157 + :alertData="alert"
158 + compact
159 + @deleted="getCase(caseEntity.id)"
160 + @updated="getCase(caseEntity.id)"
161 + />
162 + </template>
163 + <template v-else>
164 + <n-empty
165 + description="No alerts attached"
166 + class="justify-center h-24"
167 + v-if="!loading"
168 + />
169 + </template>
170 + </div>
171 + </n-collapse-item>
172 + </n-collapse>
173 + </div>
174 + </div>
175 + </n-spin>
176 +
177 + <n-modal
178 + v-model:show="showDetails"
179 + :style="{ maxWidth: 'min(850px, 90vw)', minHeight: 'min(482px, 90vh)', overflow: 'hidden' }"
180 + >
181 + <n-card
182 + content-class="flex flex-col !p-0"
183 + :title="caseNameTruncated"
184 + closable
185 + @close="closeDetails()"
186 + :bordered="false"
187 + segmented
188 + role="modal"
189 + >
190 + <CaseDetails
191 + v-if="caseEntity"
192 + :caseData="caseEntity"
193 + @deleted="emitDelete()"
194 + @updated="updateCase($event)"
195 + />
196 + </n-card>
197 + </n-modal>
198 + </div>
199 +</template>
200 +
201 +<script setup lang="ts">
202 +import { computed, onBeforeMount, onMounted, ref, toRefs } from "vue"
203 +import { NModal, NButton, NCollapse, NCollapseItem, NSpin, NCard, NEmpty, useMessage, useDialog } from "naive-ui"
204 +import _clone from "lodash/cloneDeep"
205 +import Api from "@/api"
206 +import Icon from "@/components/common/Icon.vue"
207 +import Badge from "@/components/common/Badge.vue"
208 +import CaseAssignUser from "./CaseAssignUser.vue"
209 +import CaseStatusSwitch from "./CaseStatusSwitch.vue"
210 +import CaseDetails from "./CaseDetails.vue"
211 +import StatusIcon from "../common/StatusIcon.vue"
212 +import AssigneeIcon from "../common/AssigneeIcon.vue"
213 +import AlertItem from "../alerts/AlertItem.vue"
214 +import { formatDate } from "@/utils"
215 +import { handleDeleteCase } from "./utils"
216 +import _truncate from "lodash/truncate"
217 +import type { Case } from "@/types/incidentManagement/cases.d"
218 +import { useSettingsStore } from "@/stores/settings"
219 +
220 +const props = defineProps<{
221 + caseData?: Case
222 + caseId?: number
223 + compact?: boolean
224 + embedded?: boolean
225 + detailsOnMounted?: boolean
226 + highlight?: boolean
227 +}>()
228 +const { caseData, caseId, compact, embedded, detailsOnMounted, highlight } = toRefs(props)
229 +
230 +const emit = defineEmits<{
231 + (e: "opened"): void
232 + (e: "deleted"): void
233 + (e: "updated", value: Case): void
234 +}>()
235 +
236 +const InfoIcon = "carbon:information"
237 +const EditIcon = "uil:edit-alt"
238 +
239 +const dialog = useDialog()
240 +const message = useMessage()
241 +const dFormats = useSettingsStore().dateFormat
242 +const loading = ref(false)
243 +const showDetails = ref(false)
244 +const caseEntity = ref<Case | null>(null)
245 +const caseNameTruncated = computed(() => _truncate(caseEntity.value?.case_name, { length: 50 }))
246 +
247 +function updateCase(updatedCase: Case) {
248 + caseEntity.value = updatedCase
249 + emit("updated", updatedCase)
250 +}
251 +
252 +function getCase(caseId: number) {
253 + loading.value = true
254 +
255 + Api.incidentManagement
256 + .getCase(caseId)
257 + .then(res => {
258 + if (res.data.success) {
259 + caseEntity.value = res.data?.cases?.[0] || null
260 + } else {
261 + message.warning(res.data?.message || "An error occurred. Please try again later.")
262 + }
263 + })
264 + .catch(err => {
265 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
266 + })
267 + .finally(() => {
268 + loading.value = false
269 + })
270 +}
271 +
272 +function handleDelete() {
273 + if (caseEntity.value) {
274 + handleDeleteCase({
275 + caseData: caseEntity.value,
276 + cbBefore: () => {
277 + loading.value = true
278 + },
279 + cbSuccess: () => {
280 + emitDelete()
281 + },
282 + cbAfter: () => {
283 + loading.value = false
284 + },
285 + message,
286 + dialog
287 + })
288 + }
289 +}
290 +
291 +function emitDelete() {
292 + showDetails.value = false
293 + emit("deleted")
294 +}
295 +
296 +function openDetails() {
297 + emit("opened")
298 + showDetails.value = true
299 +}
300 +
301 +function closeDetails() {
302 + showDetails.value = false
303 +}
304 +
305 +onBeforeMount(() => {
306 + if (caseId.value) {
307 + getCase(caseId.value)
308 + } else if (caseData.value) {
309 + caseEntity.value = _clone(caseData.value)
310 + }
311 +})
312 +
313 +onMounted(() => {
314 + if (detailsOnMounted.value) {
315 + openDetails()
316 + }
317 +})
318 +</script>
319 +
320 +<style lang="scss" scoped>
321 +.case-item {
322 + border-radius: var(--border-radius);
323 + background-color: var(--bg-color);
324 + transition: all 0.2s var(--bezier-ease);
325 + border: var(--border-small-050);
326 + overflow: hidden;
327 +
328 + .header-box {
329 + font-family: var(--font-family-mono);
330 + font-size: 13px;
331 + color: var(--fg-secondary-color);
332 +
333 + .id {
334 + word-break: break-word;
335 + line-height: 1.2;
336 +
337 + &:hover {
338 + color: var(--primary-color);
339 + }
340 + }
341 + }
342 + .main-box {
343 + .content {
344 + word-break: break-word;
345 + }
346 + }
347 +
348 + .footer-box {
349 + border-top: var(--border-small-100);
350 + font-size: 13px;
351 + background-color: var(--bg-secondary-color);
352 +
353 + .time {
354 + display: none;
355 + }
356 + }
357 +
358 + &.embedded {
359 + background-color: var(--bg-secondary-color);
360 + }
361 +
362 + &:hover {
363 + box-shadow: 0px 0px 0px 1px var(--primary-040-color);
364 + }
365 +
366 + &:hover {
367 + box-shadow: 0px 0px 0px 1px var(--primary-040-color);
368 + }
369 +
370 + &.highlight {
371 + box-shadow: 0px 0px 0px 1px var(--primary-color);
372 + }
373 +}
374 +</style>
frontend/src/components/incidentManagement/cases/CaseOverview.vue new
+178
@@ -0,0 +1,178 @@
1 +<template>
2 + <n-spin :show="loading" class="flex flex-col grow" content-class="flex flex-col grow">
3 + <div class="flex flex-col gap-4 grow justify-between">
4 + <div class="content-box flex flex-col gap-4 py-3">
5 + <div class="px-7 flex sm:!flex-row flex-col gap-4">
6 + <KVCard
7 + :color="
8 + caseData.case_status === 'OPEN'
9 + ? 'danger'
10 + : caseData.case_status === 'IN_PROGRESS'
11 + ? 'warning'
12 + : caseData.case_status === 'CLOSED'
13 + ? 'success'
14 + : undefined
15 + "
16 + size="lg"
17 + class="grow w-full"
18 + >
19 + <template #key>
20 + <div class="flex gap-2 items-center">
21 + <StatusIcon :status="caseData.case_status" />
22 + <span>Status</span>
23 + </div>
24 + </template>
25 + <template #value>
26 + <div class="flex">
27 + <CaseStatusSwitch
28 + :caseData
29 + v-slot="{ loading: loadingStatus }"
30 + @updated="updateCase($event)"
31 + >
32 + <div
33 + class="flex gap-3 items-center"
34 + :class="{
35 + 'cursor-not-allowed': loadingStatus,
36 + 'cursor-pointer': !loadingStatus
37 + }"
38 + >
39 + <span>{{ caseData.case_status || "n/d" }}</span>
40 + <n-spin
41 + :size="14"
42 + :show="loadingStatus"
43 + content-class="flex flex-col justify-center"
44 + >
45 + <Icon :name="EditIcon" />
46 + </n-spin>
47 + </div>
48 + </CaseStatusSwitch>
49 + </div>
50 + </template>
51 + </KVCard>
52 +
53 + <KVCard :color="caseData.assigned_to ? 'success' : undefined" size="lg" class="grow w-full">
54 + <template #key>
55 + <div class="flex gap-2 items-center">
56 + <AssigneeIcon :assignee="caseData.assigned_to" />
57 + <span>Assigned to</span>
58 + </div>
59 + </template>
60 + <template #value>
61 + <div class="flex">
62 + <CaseAssignUser
63 + :caseData
64 + v-slot="{ loading: loadingAssignee }"
65 + @updated="updateCase($event)"
66 + >
67 + <div
68 + class="flex gap-3 items-center"
69 + :class="{
70 + 'cursor-not-allowed': loadingAssignee,
71 + 'cursor-pointer': !loadingAssignee
72 + }"
73 + >
74 + <span>{{ caseData.assigned_to || "n/d" }}</span>
75 + <n-spin
76 + :size="14"
77 + :show="loadingAssignee"
78 + content-class="flex flex-col justify-center"
79 + >
80 + <Icon :name="EditIcon" />
81 + </n-spin>
82 + </div>
83 + </CaseAssignUser>
84 + </div>
85 + </template>
86 + </KVCard>
87 + </div>
88 +
89 + <div class="px-7">
90 + <KVCard>
91 + <template #key>description</template>
92 + <template #value>{{ caseData.case_description ?? "-" }}</template>
93 + </KVCard>
94 + </div>
95 +
96 + <div class="px-7 grid gap-2 grid-auto-fit-250">
97 + <KVCard>
98 + <template #key>id</template>
99 + <template #value>#{{ caseData.id }}</template>
100 + </KVCard>
101 +
102 + <KVCard>
103 + <template #key>creation time</template>
104 + <template #value>{{ formatDate(caseData.case_creation_time, dFormats.datetime) }}</template>
105 + </KVCard>
106 + </div>
107 + </div>
108 +
109 + <div class="footer-box px-7 py-4 flex justify-end">
110 + <n-button type="error" secondary @click="handleDelete()">
111 + <template #icon><Icon :name="TrashIcon" /></template>
112 + Delete
113 + </n-button>
114 + </div>
115 + </div>
116 + </n-spin>
117 +</template>
118 +
119 +<script setup lang="ts">
120 +import { ref, toRefs } from "vue"
121 +import { NButton, NSpin, useMessage, useDialog } from "naive-ui"
122 +import CaseAssignUser from "./CaseAssignUser.vue"
123 +import CaseStatusSwitch from "./CaseStatusSwitch.vue"
124 +import StatusIcon from "../common/StatusIcon.vue"
125 +import AssigneeIcon from "../common/AssigneeIcon.vue"
126 +import KVCard from "@/components/common/KVCard.vue"
127 +import Icon from "@/components/common/Icon.vue"
128 +import { handleDeleteCase } from "./utils"
129 +import { formatDate } from "@/utils"
130 +import { useSettingsStore } from "@/stores/settings"
131 +import type { Case } from "@/types/incidentManagement/cases.d"
132 +
133 +const props = defineProps<{ caseData: Case }>()
134 +const { caseData } = toRefs(props)
135 +
136 +const emit = defineEmits<{
137 + (e: "deleted"): void
138 + (e: "updated", value: Case): void
139 +}>()
140 +
141 +const TrashIcon = "carbon:trash-can"
142 +const EditIcon = "uil:edit-alt"
143 +
144 +const dialog = useDialog()
145 +const message = useMessage()
146 +const dFormats = useSettingsStore().dateFormat
147 +const loading = ref(false)
148 +
149 +function updateCase(updatedAlert: Case) {
150 + emit("updated", updatedAlert)
151 +}
152 +
153 +function handleDelete() {
154 + if (caseData.value) {
155 + handleDeleteCase({
156 + caseData: caseData.value,
157 + cbBefore: () => {
158 + loading.value = true
159 + },
160 + cbSuccess: () => {
161 + emit("deleted")
162 + },
163 + cbAfter: () => {
164 + loading.value = false
165 + },
166 + message,
167 + dialog
168 + })
169 + }
170 +}
171 +</script>
172 +
173 +<style lang="scss" scoped>
174 +.footer-box {
175 + border-top: var(--border-small-100);
176 + background-color: var(--bg-secondary-color);
177 +}
178 +</style>
frontend/src/components/incidentManagement/cases/CaseStatusSwitch.vue new
+78
@@ -0,0 +1,78 @@
1 +<template>
2 + <n-popselect
3 + v-model:value="statusSelected"
4 + v-model:show="listVisible"
5 + :options="statusOptions"
6 + :disabled="loading"
7 + size="medium"
8 + scrollable
9 + to="body"
10 + >
11 + <slot :loading="loading" />
12 + </n-popselect>
13 +</template>
14 +
15 +<script setup lang="ts">
16 +import { computed, onBeforeMount, ref, toRefs, watch } from "vue"
17 +import { useMessage, NPopselect } from "naive-ui"
18 +import Api from "@/api"
19 +import type { AlertStatus } from "@/types/incidentManagement/alerts.d"
20 +import type { Case } from "@/types/incidentManagement/cases.d"
21 +
22 +const props = defineProps<{
23 + caseData: Case
24 +}>()
25 +const { caseData } = toRefs(props)
26 +
27 +const emit = defineEmits<{
28 + (e: "updated", value: Case): void
29 +}>()
30 +
31 +const loading = ref(false)
32 +const message = useMessage()
33 +const listVisible = ref(false)
34 +const status = computed(() => caseData.value.case_status)
35 +const statusOptions = ref<
36 + {
37 + label: string
38 + value: AlertStatus
39 + }[]
40 +>([
41 + { label: "Open", value: "OPEN" },
42 + { label: "In progress", value: "IN_PROGRESS" },
43 + { label: "Closed", value: "CLOSED" }
44 +])
45 +const statusSelected = ref<AlertStatus | null>(null)
46 +
47 +function updateStatus() {
48 + if (statusSelected.value && statusSelected.value !== status.value) {
49 + loading.value = true
50 +
51 + Api.incidentManagement
52 + .updateCaseStatus(caseData.value.id, statusSelected.value)
53 + .then(res => {
54 + if (res.data.success && statusSelected.value) {
55 + emit("updated", { ...caseData.value, case_status: statusSelected.value })
56 + } else {
57 + message.warning(res.data?.message || "An error occurred. Please try again later.")
58 + }
59 + })
60 + .catch(err => {
61 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
62 + })
63 + .finally(() => {
64 + loading.value = false
65 + })
66 + }
67 +}
68 +
69 +watch(statusSelected, () => {
70 + updateStatus()
71 +})
72 +
73 +onBeforeMount(() => {
74 + if (status.value) {
75 + statusSelected.value = status.value
76 + }
77 +})
78 +</script>
frontend/src/components/incidentManagement/cases/CasesList.vue new
+382
@@ -0,0 +1,382 @@
1 +<template>
2 + <div class="cases-list">
3 + <div class="header flex items-center justify-end gap-2" ref="header">
4 + <div class="info grow flex gap-2 lg:!hidden">
5 + <n-popover overlap placement="left">
6 + <template #trigger>
7 + <div class="bg-color border-radius">
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 class="box text-error-color">
21 + Open :
22 + <code>{{ statusOpenTotal }}</code>
23 + </div>
24 + <div class="box text-warning-color">
25 + In Progress :
26 + <code>{{ statusInProgressTotal }}</code>
27 + </div>
28 + <div class="box text-success-color">
29 + Close :
30 + <code>{{ statusCloseTotal }}</code>
31 + </div>
32 + <div class="box text-success-color">
33 + N/D :
34 + <code>{{ statusUndefinedTotal }}</code>
35 + </div>
36 + </div>
37 + </n-popover>
38 + </div>
39 + <div class="info grow lg:flex gap-2 hidden text-sm">
40 + Total :
41 + <code>{{ total }}</code>
42 + <span>/</span>
43 + <span class="text-secondary-color">Open :</span>
44 + <code class="text-error-color">{{ statusOpenTotal }}</code>
45 + <span>/</span>
46 + <span class="text-secondary-color">In Progress :</span>
47 + <code class="text-warning-color">{{ statusInProgressTotal }}</code>
48 + <span>/</span>
49 + <span class="text-secondary-color">Close :</span>
50 + <code class="text-success-color">{{ statusCloseTotal }}</code>
51 + <span>/</span>
52 + <span class="text-secondary-color">N/D :</span>
53 + <code>{{ statusUndefinedTotal }}</code>
54 + </div>
55 + <n-pagination
56 + v-model:page="currentPage"
57 + v-model:page-size="pageSize"
58 + :page-slot="pageSlot"
59 + :show-size-picker="showSizePicker"
60 + :page-sizes="pageSizes"
61 + :item-count="total"
62 + :simple="simpleMode"
63 + />
64 + <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="!px-0">
65 + <template #trigger>
66 + <div class="bg-color border-radius">
67 + <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
68 + <n-button size="small" @click="showFilters = true">
69 + <template #icon>
70 + <Icon :name="FilterIcon"></Icon>
71 + </template>
72 + </n-button>
73 + </n-badge>
74 + </div>
75 + </template>
76 + <div class="py-1 flex flex-col gap-2">
77 + <div class="px-3">
78 + <n-input-group>
79 + <n-select
80 + v-model:value="filters.type"
81 + :options="typeOptions"
82 + placeholder="Filter by..."
83 + clearable
84 + class="!w-36"
85 + />
86 +
87 + <n-select
88 + v-if="filters.type === 'status'"
89 + v-model:value="filters.value"
90 + :options="statusOptions"
91 + placeholder="Value..."
92 + :disabled="!filters.type"
93 + clearable
94 + class="!w-56"
95 + />
96 + <n-select
97 + v-else-if="filters.type === 'assignedTo'"
98 + v-model:value="filters.value"
99 + :options="usersOptions"
100 + placeholder="Value..."
101 + :disabled="!filters.type"
102 + clearable
103 + class="!w-56"
104 + />
105 + <n-input
106 + v-else
107 + v-model:value="filters.value"
108 + placeholder="Value..."
109 + :disabled="!filters.type"
110 + clearable
111 + class="!w-56"
112 + />
113 + </n-input-group>
114 + </div>
115 + <div class="px-3 flex justify-between gap-2">
116 + <div class="flex justify-start gap-2">
117 + <n-button size="small" @click="showFilters = false" quaternary>Close</n-button>
118 + </div>
119 + <div class="flex justify-end gap-2">
120 + <n-button size="small" @click="resetFilters()" secondary>Reset</n-button>
121 + <n-button size="small" @click="getData()" type="primary" secondary :loading>
122 + Submit
123 + </n-button>
124 + </div>
125 + </div>
126 + </div>
127 + </n-popover>
128 + </div>
129 + <n-spin :show="loading">
130 + <div class="list flex flex-col gap-2 my-3">
131 + <template v-if="casesList.length">
132 + <CaseItem
133 + v-for="item of itemsPaginated"
134 + :key="item.id"
135 + :caseData="item"
136 + :highlight="highlight === item.id.toString()"
137 + :detailsOnMounted="highlight === item.id.toString() && !highlightedItemOpened"
138 + class="item-appear item-appear-bottom item-appear-005"
139 + @opened="highlightedItemOpened = true"
140 + @deleted="getData()"
141 + @updated="updateCase($event)"
142 + />
143 + </template>
144 + <template v-else>
145 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
146 + </template>
147 + </div>
148 + </n-spin>
149 + <div class="footer flex justify-end">
150 + <n-pagination
151 + v-model:page="currentPage"
152 + :page-size="pageSize"
153 + :item-count="total"
154 + :page-slot="6"
155 + v-if="itemsPaginated.length > 3"
156 + />
157 + </div>
158 + </div>
159 +</template>
160 +
161 +<script setup lang="ts">
162 +import { ref, onBeforeMount, computed, watch, toRefs, provide, nextTick } from "vue"
163 +import {
164 + useMessage,
165 + NSpin,
166 + NPopover,
167 + NButton,
168 + NEmpty,
169 + NSelect,
170 + NPagination,
171 + NInputGroup,
172 + NBadge,
173 + NInput
174 +} from "naive-ui"
175 +import Api from "@/api"
176 +import _cloneDeep from "lodash/cloneDeep"
177 +import _orderBy from "lodash/orderBy"
178 +import Icon from "@/components/common/Icon.vue"
179 +import { useResizeObserver } from "@vueuse/core"
180 +import type { AlertStatus } from "@/types/incidentManagement/alerts.d"
181 +import type { Case } from "@/types/incidentManagement/cases.d"
182 +import type { CasesFilter } from "@/api/endpoints/incidentManagement"
183 +import CaseItem from "./CaseItem.vue"
184 +
185 +export interface CasesListFilter {
186 + type: "status" | "assignedTo"
187 + value: string | AlertStatus
188 +}
189 +
190 +const props = defineProps<{ highlight: string | null | undefined }>()
191 +const { highlight } = toRefs(props)
192 +
193 +const message = useMessage()
194 +const loading = ref(false)
195 +const showFilters = ref(false)
196 +const casesList = ref<Case[]>([])
197 +const availableUsers = ref<string[]>([])
198 +
199 +const pageSize = ref(25)
200 +const currentPage = ref(1)
201 +const simpleMode = ref(false)
202 +const showSizePicker = ref(true)
203 +const pageSizes = [10, 25, 50, 100]
204 +const header = ref()
205 +const pageSlot = ref(8)
206 +
207 +const itemsPaginated = computed(() => {
208 + const from = (currentPage.value - 1) * pageSize.value
209 + const to = currentPage.value * pageSize.value
210 +
211 + const list = _orderBy(casesList.value, ["id"], ["desc"])
212 +
213 + return list.slice(from, to)
214 +})
215 +
216 +const FilterIcon = "carbon:filter-edit"
217 +const InfoIcon = "carbon:information"
218 +
219 +const total = computed<number>(() => {
220 + return casesList.value.length || 0
221 +})
222 +const statusOpenTotal = computed<number>(() => {
223 + return casesList.value.filter(o => o.case_status === "OPEN").length || 0
224 +})
225 +const statusInProgressTotal = computed<number>(() => {
226 + return casesList.value.filter(o => o.case_status === "IN_PROGRESS").length || 0
227 +})
228 +const statusCloseTotal = computed<number>(() => {
229 + return casesList.value.filter(o => o.case_status === "CLOSED").length || 0
230 +})
231 +const statusUndefinedTotal = computed<number>(() => {
232 + return casesList.value.filter(o => o.case_status === null).length || 0
233 +})
234 +
235 +const filters = ref<Partial<CasesListFilter>>({})
236 +const lastFilters = ref<Partial<CasesListFilter>>({})
237 +
238 +const filtered = computed<boolean>(() => {
239 + return !!filters.value.type && !!filters.value.value
240 +})
241 +
242 +const typeOptions = [
243 + { label: "Status", value: "status" },
244 + { label: "Assigned To", value: "assignedTo" }
245 +]
246 +
247 +const statusOptions: { label: string; value: AlertStatus }[] = [
248 + { label: "Open", value: "OPEN" },
249 + { label: "Closed", value: "CLOSED" },
250 + { label: "In progress", value: "IN_PROGRESS" }
251 +]
252 +
253 +const usersOptions = computed(() => availableUsers.value.map(o => ({ label: o, value: o })))
254 +
255 +const highlightedItemFound = ref(!highlight.value)
256 +const highlightedItemOpened = ref(!highlight.value)
257 +
258 +watch(pageSize, () => {
259 + if (currentPage.value !== 1) {
260 + currentPage.value = 1
261 + }
262 +})
263 +
264 +watch(showFilters, val => {
265 + if (!val) {
266 + filters.value = _cloneDeep(lastFilters.value)
267 + }
268 +})
269 +
270 +watch(
271 + () => filters.value.type,
272 + () => {
273 + filters.value.value = undefined
274 + }
275 +)
276 +
277 +watch(
278 + itemsPaginated,
279 + () => {
280 + const totalPages = Math.ceil(casesList.value.length / pageSize.value)
281 +
282 + if (
283 + itemsPaginated.value.length &&
284 + !itemsPaginated.value.find(o => o.id.toString() === highlight.value) &&
285 + currentPage.value < totalPages &&
286 + !highlightedItemFound.value
287 + ) {
288 + nextTick(() => {
289 + currentPage.value++
290 + })
291 + }
292 +
293 + if (itemsPaginated.value.find(o => o.id.toString() === highlight.value)) {
294 + highlightedItemFound.value = true
295 + }
296 + },
297 + { immediate: true }
298 +)
299 +
300 +provide("assignable-users", availableUsers)
301 +
302 +function resetFilters() {
303 + filters.value.type = undefined
304 + showFilters.value = false
305 + getData()
306 +}
307 +
308 +function updateCase(updatedCase: Case) {
309 + const caseIndex = casesList.value.findIndex(o => o.id === updatedCase.id)
310 + if (caseIndex !== -1) {
311 + casesList.value[caseIndex] = updatedCase
312 + }
313 +}
314 +
315 +function getData() {
316 + showFilters.value = false
317 + loading.value = true
318 +
319 + lastFilters.value = _cloneDeep(filters.value)
320 +
321 + let query: CasesFilter | undefined = undefined
322 + if (filtered.value) {
323 + // @ts-expect-error filters properties infer are ignored
324 + query = { [filters.value.type]: filters.value.value }
325 + }
326 +
327 + Api.incidentManagement
328 + .getCasesList(query)
329 + .then(res => {
330 + if (res.data.success) {
331 + casesList.value = res.data?.cases || []
332 + } else {
333 + message.warning(res.data?.message || "An error occurred. Please try again later.")
334 + }
335 + })
336 + .catch(err => {
337 + casesList.value = []
338 +
339 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
340 + })
341 + .finally(() => {
342 + loading.value = false
343 + })
344 +}
345 +
346 +function getAvailableUsers() {
347 + Api.incidentManagement
348 + .getAvailableUsers()
349 + .then(res => {
350 + if (res.data.success) {
351 + availableUsers.value = res.data?.available_users || []
352 + } else {
353 + message.warning(res.data?.message || "An error occurred. Please try again later.")
354 + }
355 + })
356 + .catch(err => {
357 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
358 + })
359 +}
360 +
361 +useResizeObserver(header, entries => {
362 + const entry = entries[0]
363 + const { width } = entry.contentRect
364 +
365 + pageSlot.value = width < 700 ? 5 : 8
366 + simpleMode.value = width < 550
367 +})
368 +
369 +onBeforeMount(() => {
370 + getData()
371 + getAvailableUsers()
372 +})
373 +</script>
374 +
375 +<style lang="scss" scoped>
376 +.cases-list {
377 + .list {
378 + container-type: inline-size;
379 + min-height: 200px;
380 + }
381 +}
382 +</style>
frontend/src/components/incidentManagement/cases/utils.ts new
+81
@@ -0,0 +1,81 @@
1 +import { h } from "vue"
2 +import Api from "@/api"
3 +import type { MessageApiInjection } from "naive-ui/es/message/src/MessageProvider"
4 +import type { DialogApiInjection } from "naive-ui/es/dialog/src/DialogProvider"
5 +import type { Case } from "@/types/incidentManagement/cases.d"
6 +
7 +export interface DeleteCaseParams {
8 + caseData: Case
9 + cbBefore?: () => void
10 + cbSuccess?: () => void
11 + cbAfter?: () => void
12 + cbError?: () => void
13 + message: MessageApiInjection
14 + dialog: DialogApiInjection
15 +}
16 +
17 +export function handleDeleteCase({
18 + caseData,
19 + cbBefore,
20 + cbSuccess,
21 + cbAfter,
22 + cbError,
23 + dialog,
24 + message
25 +}: DeleteCaseParams) {
26 + dialog.warning({
27 + title: "Confirm",
28 + content: () =>
29 + h("div", {
30 + innerHTML: `Are you sure you want to delete the Case:<br/><strong>${caseData.id} - ${caseData.case_name}</strong> ?`
31 + }),
32 + positiveText: "Yes I'm sure",
33 + negativeText: "Cancel",
34 + onPositiveClick: () => {
35 + deleteAlert({ caseData, cbBefore, cbSuccess, cbAfter, cbError, dialog, message })
36 + },
37 + onNegativeClick: () => {
38 + message.info("Delete canceled")
39 + }
40 + })
41 +}
42 +
43 +export function deleteAlert({ caseData, cbBefore, cbSuccess, cbAfter, cbError, message }: DeleteCaseParams) {
44 + if (cbBefore && typeof cbBefore === "function") {
45 + cbBefore()
46 + }
47 +
48 + Api.incidentManagement
49 + .deleteCase(caseData.id)
50 + .then(res => {
51 + if (res.data.success) {
52 + message.success("Case was successfully deleted.")
53 +
54 + if (cbSuccess && typeof cbSuccess === "function") {
55 + cbSuccess()
56 + }
57 + } else {
58 + message.error(res.data?.message || "An error occurred. Please try again later.")
59 +
60 + if (cbError && typeof cbError === "function") {
61 + cbError()
62 + }
63 + }
64 + })
65 + .catch(err => {
66 + if (err.response?.status === 401) {
67 + message.error(err.response?.data?.message || "Case Delete returned Unauthorized.")
68 + } else {
69 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
70 + }
71 +
72 + if (cbError && typeof cbError === "function") {
73 + cbError()
74 + }
75 + })
76 + .finally(() => {
77 + if (cbAfter && typeof cbAfter === "function") {
78 + cbAfter()
79 + }
80 + })
81 +}
frontend/src/components/incidentManagement/common/AssigneeIcon.vue new
+14
@@ -0,0 +1,14 @@
1 +<template>
2 + <Icon :name="assignee ? AssigneeIcon : WarningIcon" :size="size || 16" />
3 +</template>
4 +
5 +<script setup lang="ts">
6 +import { toRefs } from "vue"
7 +import Icon from "@/components/common/Icon.vue"
8 +
9 +const props = defineProps<{ assignee: string | null; size?: number }>()
10 +const { assignee, size } = toRefs(props)
11 +
12 +const AssigneeIcon = "carbon:user-military"
13 +const WarningIcon = "carbon:warning-hex"
14 +</script>
frontend/src/components/incidentManagement/common/StatusIcon.vue new
+28
@@ -0,0 +1,28 @@
1 +<template>
2 + <Icon
3 + :name="
4 + status === 'OPEN'
5 + ? WarningIcon
6 + : status === 'IN_PROGRESS'
7 + ? ProgressIcon
8 + : status === 'CLOSED'
9 + ? CheckIcon
10 + : UnknownIcon
11 + "
12 + :size="size || 16"
13 + />
14 +</template>
15 +
16 +<script setup lang="ts">
17 +import { toRefs } from "vue"
18 +import Icon from "@/components/common/Icon.vue"
19 +import type { AlertStatus } from "@/types/incidentManagement/alerts.d"
20 +
21 +const props = defineProps<{ status: AlertStatus | null; size?: number }>()
22 +const { status, size } = toRefs(props)
23 +
24 +const ProgressIcon = "carbon:hourglass"
25 +const CheckIcon = "carbon:checkmark-outline"
26 +const WarningIcon = "carbon:warning-hex"
27 +const UnknownIcon = "carbon:unknown"
28 +</script>
frontend/src/components/incidentManagement/sources/ConfiguredSourceItem.vue new
+108
@@ -0,0 +1,108 @@
1 +<template>
2 + <n-spin :show="canceling">
3 + <div class="item flex flex-col gap-2 px-5 py-3" @click="openConfiguredSource()">
4 + <div class="header-box flex justify-between">
5 + {{ source }}
6 + </div>
7 + <div class="footer-box flex justify-end">
8 + <n-popconfirm
9 + @positive-click="deleteSourceConfiguration()"
10 + v-model:show="showConfirm"
11 + trigger="manual"
12 + @clickoutside="showConfirm = false"
13 + >
14 + <template #trigger>
15 + <div @click.stop="showConfirm = true" class="delete-btn">delete</div>
16 + </template>
17 + Are you sure you want to delete the source configuration?
18 + </n-popconfirm>
19 + </div>
20 + </div>
21 +
22 + <n-modal
23 + v-model:show="showDetails"
24 + display-directive="show"
25 + preset="card"
26 + :style="{ maxWidth: 'min(600px, 90vw)', minHeight: 'min(300px, 90vh)', overflow: 'hidden' }"
27 + :title="source"
28 + :bordered="false"
29 + segmented
30 + >
31 + <SourceConfigurationDetails :source />
32 + </n-modal>
33 + </n-spin>
34 +</template>
35 +
36 +<script setup lang="ts">
37 +import Api from "@/api"
38 +import { ref } from "vue"
39 +import SourceConfigurationDetails from "./SourceConfigurationDetails.vue"
40 +import { useMessage, NPopconfirm, NSpin, NModal } from "naive-ui"
41 +import type { SourceName } from "@/types/incidentManagement/sources.d"
42 +
43 +const emit = defineEmits<{
44 + (e: "deleted"): void
45 +}>()
46 +
47 +const { source } = defineProps<{ source: SourceName }>()
48 +
49 +const message = useMessage()
50 +const canceling = ref(false)
51 +const showDetails = ref(false)
52 +const showConfirm = ref(false)
53 +
54 +function deleteSourceConfiguration() {
55 + canceling.value = true
56 +
57 + Api.incidentManagement
58 + .deleteSourceConfiguration(source)
59 + .then(res => {
60 + if (res.data.success) {
61 + message.success(res.data?.message || "Source Configuration deleted successfully")
62 + emit("deleted")
63 + } else {
64 + message.warning(res.data?.message || "An error occurred. Please try again later.")
65 + }
66 + })
67 + .catch(err => {
68 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
69 + })
70 + .finally(() => {
71 + canceling.value = false
72 + })
73 +}
74 +
75 +function openConfiguredSource() {
76 + showDetails.value = true
77 +}
78 +</script>
79 +
80 +<style lang="scss" scoped>
81 +.item {
82 + border-radius: var(--border-radius);
83 + background-color: var(--bg-color);
84 + transition: all 0.2s var(--bezier-ease);
85 + border: var(--border-small-050);
86 + cursor: pointer;
87 +
88 + .header-box {
89 + font-family: var(--font-family-mono);
90 + }
91 + .footer-box {
92 + font-family: var(--font-family-mono);
93 + text-align: right;
94 + font-size: 12px;
95 + color: var(--fg-secondary-color);
96 +
97 + .delete-btn {
98 + &:hover {
99 + color: var(--error-color);
100 + }
101 + }
102 + }
103 +
104 + &:hover {
105 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
106 + }
107 +}
108 +</style>
frontend/src/components/incidentManagement/sources/ConfiguredSourcesList.vue new
+108
@@ -0,0 +1,108 @@
1 +<template>
2 + <div class="configured-sources-list">
3 + <div class="header flex items-center justify-end gap-2 mb-3">
4 + <div class="info grow flex gap-5">
5 + <n-popover overlap placement="bottom-start">
6 + <template #trigger>
7 + <div class="bg-color border-radius">
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>{{ totalConfiguredSources }}</code>
19 + </div>
20 + </div>
21 + </n-popover>
22 + </div>
23 + <div class="actions flex gap-2 items-center">
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>
30 + </div>
31 + </div>
32 + <n-spin :show="loading" class="min-h-32">
33 + <div class="list gap-4 grid grid-auto-fill-250" v-if="configuredSourcesList.length">
34 + <ConfiguredSourceItem
35 + v-for="source of configuredSourcesList"
36 + :key="source"
37 + :source
38 + class="item-appear item-appear-bottom item-appear-005"
39 + @deleted="getConfiguredSources()"
40 + />
41 + </div>
42 + <template v-else>
43 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
44 + </template>
45 + </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 @submitted="getConfiguredSources()" :disabledSources="configuredSourcesList" />
58 + </n-modal>
59 + </div>
60 +</template>
61 +<script setup lang="ts">
62 +import { computed, onBeforeMount, ref, watch } from "vue"
63 +import { NSpin, NEmpty, NPopover, NButton, NModal, useMessage } from "naive-ui"
64 +import Icon from "@/components/common/Icon.vue"
65 +import ConfiguredSourceItem from "./ConfiguredSourceItem.vue"
66 +import SourceConfigurationWizard from "./SourceConfigurationWizard.vue"
67 +import type { SourceName } from "@/types/incidentManagement/sources.d"
68 +import Api from "@/api"
69 +
70 +const InfoIcon = "carbon:information"
71 +const NewSourceConfigurationIcon = "carbon:fetch-upload-cloud"
72 +const message = useMessage()
73 +const showWizard = ref(false)
74 +const loading = ref(false)
75 +const configuredSourcesList = ref<SourceName[]>([])
76 +const totalConfiguredSources = computed(() => configuredSourcesList.value.length)
77 +const formCTX = ref<{ reset: () => void } | null>(null)
78 +
79 +function getConfiguredSources() {
80 + loading.value = true
81 +
82 + Api.incidentManagement
83 + .getConfiguredSources()
84 + .then(res => {
85 + if (res.data.success) {
86 + configuredSourcesList.value = res.data?.sources || []
87 + } else {
88 + message.warning(res.data?.message || "An error occurred. Please try again later.")
89 + }
90 + })
91 + .catch(err => {
92 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
93 + })
94 + .finally(() => {
95 + loading.value = false
96 + })
97 +}
98 +
99 +watch(showWizard, val => {
100 + if (val) {
101 + formCTX.value?.reset()
102 + }
103 +})
104 +
105 +onBeforeMount(() => {
106 + getConfiguredSources()
107 +})
108 +</script>
frontend/src/components/incidentManagement/sources/SourceConfigurationDetails.vue new
+114
@@ -0,0 +1,114 @@
1 +<template>
2 + <n-spin :show="loading || submitting" class="min-h-20">
3 + <SourceConfigurationViewer v-if="sourceConfiguration" v-show="!editing" :sourceConfiguration />
4 +
5 + <div class="flex gap-2 justify-end items-center mt-4" v-if="!editing && !loading">
6 + <n-button @click="setEditMode()">
7 + <template #icon>
8 + <Icon :name="EditIcon" :size="16"></Icon>
9 + </template>
10 + Edit
11 + </n-button>
12 + </div>
13 +
14 + <SourceConfigurationForm
15 + v-if="sourceConfiguration"
16 + v-show="editing"
17 + :sourceConfigurationModel="sourceConfiguration"
18 + show-index-name-field
19 + @mounted="formCTX = $event"
20 + @submitted="updateSourceConfiguration($event)"
21 + >
22 + <template #additionalActions>
23 + <n-button @click="setViewMode()">
24 + <template #icon>
25 + <Icon :name="ArrowIcon" :size="16"></Icon>
26 + </template>
27 + Cancel
28 + </n-button>
29 + </template>
30 + </SourceConfigurationForm>
31 + </n-spin>
32 +</template>
33 +
34 +<script setup lang="ts">
35 +import Api from "@/api"
36 +import { onBeforeMount, ref } from "vue"
37 +import { useMessage, NSpin, NButton } from "naive-ui"
38 +import type { SourceConfiguration, SourceName } from "@/types/incidentManagement/sources.d"
39 +import SourceConfigurationViewer from "./SourceConfigurationViewer.vue"
40 +import SourceConfigurationForm from "./SourceConfigurationForm.vue"
41 +import Icon from "@/components/common/Icon.vue"
42 +import type { ApiError } from "@/types/common.d"
43 +
44 +const { source } = defineProps<{ source: SourceName }>()
45 +
46 +const message = useMessage()
47 +const loading = ref(false)
48 +const submitting = ref(false)
49 +const ArrowIcon = "carbon:arrow-left"
50 +const EditIcon = "uil:edit-alt"
51 +const sourceConfiguration = ref<SourceConfiguration | null>(null)
52 +const editing = ref(false)
53 +const formCTX = ref<{ reset: () => void; toggleSubmittingFlag: () => boolean } | null>(null)
54 +
55 +function getSourceConfiguration() {
56 + loading.value = true
57 +
58 + Api.incidentManagement
59 + .getSourceConfiguration(source)
60 + .then(res => {
61 + if (res.data.success) {
62 + sourceConfiguration.value = {
63 + field_names: res.data.field_names || [],
64 + asset_name: res.data.asset_name || "",
65 + timefield_name: res.data.timefield_name || "",
66 + alert_title_name: res.data.alert_title_name || "",
67 + source: res.data.source || source
68 + }
69 + } else {
70 + message.warning(res.data?.message || "An error occurred. Please try again later.")
71 + }
72 + })
73 + .catch(err => {
74 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
75 + })
76 + .finally(() => {
77 + loading.value = false
78 + })
79 +}
80 +
81 +function setEditMode() {
82 + formCTX.value?.reset()
83 + editing.value = true
84 +}
85 +function setViewMode() {
86 + editing.value = false
87 +}
88 +
89 +function updateSourceConfiguration(payload: SourceConfiguration) {
90 + submitting.value = formCTX.value?.toggleSubmittingFlag() || true
91 +
92 + Api.incidentManagement
93 + .updateSourceConfiguration(payload)
94 + .then(res => {
95 + if (res.data.success) {
96 + message.success(res.data?.message || `Source Configuration sent successfully`)
97 + getSourceConfiguration()
98 + setViewMode()
99 + } else {
100 + message.warning(res.data?.message || "An error occurred. Please try again later.")
101 + }
102 + })
103 + .catch((err: ApiError) => {
104 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
105 + })
106 + .finally(() => {
107 + submitting.value = formCTX.value?.toggleSubmittingFlag() || false
108 + })
109 +}
110 +
111 +onBeforeMount(() => {
112 + getSourceConfiguration()
113 +})
114 +</script>
frontend/src/components/incidentManagement/sources/SourceConfigurationForm.vue new
+499
@@ -0,0 +1,499 @@
1 +<template>
2 + <n-spin :show="loading" class="creation-report-form">
3 + <n-form :model="form" :rules="rules" ref="formRef">
4 + <div class="flex flex-col gap-8">
5 + <div class="flex flex-col gap-2">
6 + <n-form-item label="Index name" path="index_name" v-if="showIndexNameField">
7 + <n-select
8 + v-model:value="form.index_name"
9 + :options="indexNamesOptions"
10 + placeholder="Select..."
11 + clearable
12 + filterable
13 + to="body"
14 + :disabled="disableIndexNameField"
15 + :loading="loadingIndexNames"
16 + />
17 + </n-form-item>
18 + <n-form-item path="source" :show-require-mark="false" v-if="showSourceField" class="source-field">
19 + <template #label>
20 + <div class="flex justify-between items-end gap-2">
21 + <span>
22 + Source
23 + <span class="n-form-item-label__asterisk">*</span>
24 + </span>
25 +
26 + <span v-if="isSocfortressRecommendsAvailable">
27 + <n-button
28 + @click="getSocfortressRecommendsWazuh()"
29 + :loading="loadingSocfortressRecommendsWazuh"
30 + size="tiny"
31 + ghost
32 + type="primary"
33 + >
34 + SOCFortress Recommends
35 + </n-button>
36 + </span>
37 + </div>
38 + </template>
39 + <n-input
40 + v-model:value.trim="form.source"
41 + placeholder="Please insert Source"
42 + clearable
43 + @update:value="resetIndexAvailable()"
44 + :disabled="disableSourceField"
45 + :loading="loadingSource"
46 + />
47 + </n-form-item>
48 +
49 + <n-alert v-if="isSourceNotAllowed" title="Source already exists" type="warning" class="mb-5">
50 + A configuration for
51 + <strong>"{{ form.source }}"</strong>
52 + already exists. Please select a different
53 + <strong>Index name</strong>
54 + to proceed.
55 + </n-alert>
56 +
57 + <n-form-item label="Field names" path="field_names">
58 + <n-select
59 + v-model:value="form.field_names"
60 + :options="availableMappingsOptions"
61 + placeholder="Select..."
62 + clearable
63 + filterable
64 + multiple
65 + to="body"
66 + :disabled="!isFieldEnabled"
67 + :loading="loadingAvailableMappings"
68 + />
69 + </n-form-item>
70 + <n-form-item label="Asset name" path="asset_name">
71 + <n-select
72 + v-model:value="form.asset_name"
73 + :options="availableMappingsOptions"
74 + placeholder="Select..."
75 + clearable
76 + filterable
77 + to="body"
78 + :disabled="!isFieldEnabled"
79 + :loading="loadingAvailableMappings"
80 + />
81 + </n-form-item>
82 + <n-form-item label="Timefield name" path="timefield_name">
83 + <n-select
84 + v-model:value="form.timefield_name"
85 + :options="availableMappingsOptions"
86 + placeholder="Select..."
87 + clearable
88 + filterable
89 + to="body"
90 + :disabled="!isFieldEnabled"
91 + :loading="loadingAvailableMappings"
92 + />
93 + </n-form-item>
94 + <n-form-item label="Alert title name" path="alert_title_name">
95 + <n-select
96 + v-model:value="form.alert_title_name"
97 + :options="availableMappingsOptions"
98 + placeholder="Select..."
99 + clearable
100 + filterable
101 + to="body"
102 + :disabled="!isFieldEnabled"
103 + :loading="loadingAvailableMappings"
104 + />
105 + </n-form-item>
106 + </div>
107 + <div class="flex justify-between gap-3">
108 + <div>
109 + <slot name="additionalActions"></slot>
110 + </div>
111 + <div class="flex gap-3 items-center">
112 + <n-button @click="reset()" :disabled="loading">Reset</n-button>
113 + <n-button
114 + type="primary"
115 + :disabled="!isValid"
116 + @click="validate(() => submit())"
117 + :loading="submitting"
118 + >
119 + Submit
120 + </n-button>
121 + </div>
122 + </div>
123 + </div>
124 + </n-form>
125 + </n-spin>
126 +</template>
127 +
128 +<script setup lang="ts">
129 +import { computed, onBeforeMount, onMounted, ref, toRefs, watch } from "vue"
130 +import Api from "@/api"
131 +import {
132 + NForm,
133 + NFormItem,
134 + NInput,
135 + NButton,
136 + NSpin,
137 + NSelect,
138 + NAlert,
139 + useMessage,
140 + type FormValidationError,
141 + type FormInst,
142 + type FormRules,
143 + type MessageReactive,
144 + type FormItemRule
145 +} from "naive-ui"
146 +import type { SourceConfiguration, SourceName, SourceConfigurationModel } from "@/types/incidentManagement/sources.d"
147 +import _intersection from "lodash/intersection"
148 +
149 +const emit = defineEmits<{
150 + (e: "submitted", value: SourceConfiguration): void
151 + (
152 + e: "mounted",
153 + value: {
154 + reset: () => void
155 + toggleSubmittingFlag: () => boolean
156 + }
157 + ): void
158 +}>()
159 +
160 +const props = defineProps<{
161 + sourceConfigurationModel?: SourceConfigurationModel
162 + showSourceField?: boolean
163 + disableSourceField?: boolean
164 + showIndexNameField?: boolean
165 + disableIndexNameField?: boolean
166 + disabledSources?: SourceName[]
167 +}>()
168 +const {
169 + sourceConfigurationModel,
170 + showSourceField,
171 + disableSourceField,
172 + showIndexNameField,
173 + disableIndexNameField,
174 + disabledSources
175 +} = toRefs(props)
176 +
177 +const submitting = ref(false)
178 +const loadingSource = ref(false)
179 +const loadingIndexNames = ref(false)
180 +const loadingAvailableMappings = ref(false)
181 +const loadingSocfortressRecommendsWazuh = ref(false)
182 +const socfortressRecommendsWazuh = ref<SourceConfiguration | null>(null)
183 +const loading = computed(() => loadingAvailableMappings.value || loadingIndexNames.value || loadingSource.value)
184 +const message = useMessage()
185 +const form = ref<SourceConfigurationModel>(getSourceConfigurationForm())
186 +const formRef = ref<FormInst | null>(null)
187 +const availableMappingsOptions = ref<{ label: string; value: string }[]>([])
188 +const indexNamesOptions = ref<{ label: string; value: string }[]>([])
189 +
190 +const rules: FormRules = {
191 + source: {
192 + required: true,
193 + message: "Please input the Source",
194 + trigger: ["input", "blur"]
195 + },
196 + field_names: {
197 + required: true,
198 + validator: validateAtLeastOne,
199 + trigger: ["blur"]
200 + },
201 + asset_name: {
202 + required: true,
203 + message: "Please input the asset name",
204 + trigger: ["input", "blur"]
205 + },
206 + timefield_name: {
207 + required: true,
208 + message: "Please input the timefield name",
209 + trigger: ["input", "blur"]
210 + },
211 + alert_title_name: {
212 + required: true,
213 + message: "Please input the alert title name",
214 + trigger: ["input", "blur"]
215 + }
216 +}
217 +
218 +let validationMessage: MessageReactive | null = null
219 +
220 +const isSourceNotAllowed = computed(
221 + () => form.value.source && disabledSources.value?.length && disabledSources.value.includes(form.value.source)
222 +)
223 +const isFieldEnabled = computed(() => form.value.index_name && !isSourceNotAllowed.value)
224 +
225 +const isValid = computed(() => {
226 + if (
227 + !form.value.field_names.length ||
228 + !form.value.asset_name ||
229 + !form.value.timefield_name ||
230 + !form.value.alert_title_name ||
231 + !form.value.source ||
232 + isSourceNotAllowed.value
233 + ) {
234 + return false
235 + }
236 +
237 + return true
238 +})
239 +
240 +const isSocfortressRecommendsAvailable = computed(() => form.value.source?.toLowerCase() === "wazuh")
241 +
242 +watch(sourceConfigurationModel, () => {
243 + reset()
244 + init()
245 +})
246 +
247 +watch(
248 + () => form.value.index_name,
249 + val => {
250 + if (val) {
251 + getAvailableMappings(val)
252 + getSourceByIndex(val)
253 + } else {
254 + availableMappingsOptions.value = []
255 + }
256 + }
257 +)
258 +
259 +function resetIndexAvailable() {
260 + form.value.index_name = null
261 + if (form.value.source) {
262 + getAvailableIndices(form.value.source)
263 + }
264 +}
265 +
266 +function validateAtLeastOne(rule: FormItemRule, value: string[]) {
267 + if (!value || !value.length) {
268 + return new Error("Please select at least one option")
269 + }
270 +
271 + return true
272 +}
273 +
274 +function validate(cb?: () => void) {
275 + if (!formRef.value) return
276 +
277 + formRef.value.validate((errors?: Array<FormValidationError>) => {
278 + if (!errors) {
279 + validationMessage?.destroy()
280 + validationMessage = null
281 + if (cb) cb()
282 + } else {
283 + if (!validationMessage) {
284 + validationMessage = message.warning("You must fill in the required fields correctly.")
285 + }
286 + return false
287 + }
288 + })
289 +}
290 +
291 +function getSourceConfigurationForm(): SourceConfigurationModel {
292 + return {
293 + field_names: sourceConfigurationModel.value?.field_names || [],
294 + asset_name: sourceConfigurationModel.value?.asset_name || null,
295 + timefield_name: sourceConfigurationModel.value?.timefield_name || null,
296 + alert_title_name: sourceConfigurationModel.value?.alert_title_name || null,
297 + source: sourceConfigurationModel.value?.source || "",
298 + index_name: sourceConfigurationModel.value?.index_name || null
299 + }
300 +}
301 +
302 +function reset() {
303 + if (!loading.value) {
304 + resetForm()
305 + formRef.value?.restoreValidation()
306 + }
307 +}
308 +
309 +function resetForm() {
310 + form.value = getSourceConfigurationForm()
311 +}
312 +
313 +function sanitizeFields() {
314 + const availableMappings = availableMappingsOptions.value.map(o => o.value)
315 +
316 + form.value.field_names = _intersection(availableMappings, form.value.field_names)
317 +
318 + if (form.value.asset_name && !availableMappings.includes(form.value.asset_name)) {
319 + form.value.asset_name = null
320 + }
321 + if (form.value.timefield_name && !availableMappings.includes(form.value.timefield_name)) {
322 + form.value.timefield_name = null
323 + }
324 + if (form.value.alert_title_name && !availableMappings.includes(form.value.alert_title_name)) {
325 + form.value.alert_title_name = null
326 + }
327 +}
328 +
329 +function submit() {
330 + const payload: SourceConfiguration = {
331 + field_names: form.value?.field_names || [],
332 + asset_name: form.value?.asset_name || "",
333 + timefield_name: form.value?.timefield_name || "",
334 + alert_title_name: form.value?.alert_title_name || "",
335 + source: form.value?.source || ""
336 + }
337 + emit("submitted", payload)
338 +}
339 +
340 +function toggleSubmittingFlag(status?: boolean) {
341 + if (status !== undefined) {
342 + submitting.value = status
343 + } else {
344 + submitting.value = !submitting.value
345 + }
346 +
347 + return submitting.value
348 +}
349 +
350 +function resetSource() {
351 + form.value.source = ""
352 +}
353 +
354 +function setSocfortressRecommendsWazuh() {
355 + form.value.field_names = socfortressRecommendsWazuh.value?.field_names || []
356 + form.value.asset_name = socfortressRecommendsWazuh.value?.asset_name || null
357 + form.value.timefield_name = socfortressRecommendsWazuh.value?.timefield_name || null
358 + form.value.alert_title_name = socfortressRecommendsWazuh.value?.alert_title_name || null
359 + form.value.source = socfortressRecommendsWazuh.value?.source || ""
360 +}
361 +
362 +function getSocfortressRecommendsWazuh() {
363 + if (socfortressRecommendsWazuh.value) {
364 + setSocfortressRecommendsWazuh()
365 + return
366 + }
367 +
368 + loadingSocfortressRecommendsWazuh.value = true
369 +
370 + Api.incidentManagement
371 + .getSocfortressRecommendsWazuh()
372 + .then(res => {
373 + if (res.data.success) {
374 + socfortressRecommendsWazuh.value = {
375 + field_names: res.data.field_names,
376 + asset_name: res.data.asset_name,
377 + timefield_name: res.data.timefield_name,
378 + alert_title_name: res.data.alert_title_name,
379 + source: res.data.source
380 + }
381 +
382 + setSocfortressRecommendsWazuh()
383 + } else {
384 + message.warning(res.data?.message || "An error occurred. Please try again later.")
385 + }
386 + })
387 + .catch(err => {
388 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
389 + })
390 + .finally(() => {
391 + loadingSocfortressRecommendsWazuh.value = false
392 + })
393 +}
394 +
395 +function getAvailableMappings(indexName: string) {
396 + loadingAvailableMappings.value = true
397 +
398 + Api.incidentManagement
399 + .getAvailableMappings(indexName)
400 + .then(res => {
401 + if (res.data.success) {
402 + availableMappingsOptions.value = (res.data?.available_mappings || []).map(o => ({
403 + label: o,
404 + value: o
405 + }))
406 + sanitizeFields()
407 + } else {
408 + message.warning(res.data?.message || "An error occurred. Please try again later.")
409 + }
410 + })
411 + .catch(err => {
412 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
413 + })
414 + .finally(() => {
415 + loadingAvailableMappings.value = false
416 + })
417 +}
418 +
419 +function getAvailableIndices(source: SourceName) {
420 + loadingIndexNames.value = true
421 +
422 + Api.incidentManagement
423 + .getAvailableIndices(source)
424 + .then(res => {
425 + if (res.data.success) {
426 + indexNamesOptions.value = (res.data?.indices || []).map(o => ({
427 + label: o,
428 + value: o
429 + }))
430 + } else {
431 + resetSource()
432 + message.warning(res.data?.message || "An error occurred. Please try again later.")
433 + }
434 + })
435 + .catch(err => {
436 + resetSource()
437 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
438 + })
439 + .finally(() => {
440 + loadingIndexNames.value = false
441 + })
442 +}
443 +
444 +function getSourceByIndex(indexName: string) {
445 + loadingSource.value = true
446 +
447 + Api.incidentManagement
448 + .getSourceByIndex(indexName)
449 + .then(res => {
450 + if (res.data.success) {
451 + form.value.source = res.data.source
452 + } else {
453 + resetSource()
454 + message.warning(res.data?.message || "An error occurred. Please try again later.")
455 + }
456 + })
457 + .catch(err => {
458 + resetSource()
459 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
460 + })
461 + .finally(() => {
462 + loadingSource.value = false
463 + })
464 +}
465 +
466 +function init() {
467 + if (sourceConfigurationModel.value?.index_name) {
468 + getAvailableMappings(sourceConfigurationModel.value.index_name)
469 +
470 + if (!sourceConfigurationModel.value?.source) {
471 + getSourceByIndex(sourceConfigurationModel.value.index_name)
472 + }
473 + }
474 + if (sourceConfigurationModel.value?.source) {
475 + getAvailableIndices(sourceConfigurationModel.value.source)
476 + }
477 +}
478 +
479 +onBeforeMount(() => {
480 + init()
481 +})
482 +
483 +onMounted(() => {
484 + emit("mounted", {
485 + reset,
486 + toggleSubmittingFlag
487 + })
488 +})
489 +</script>
490 +
491 +<style lang="scss" scoped>
492 +.source-field {
493 + :deep() {
494 + .n-form-item-label__text {
495 + width: 100%;
496 + }
497 + }
498 +}
499 +</style>
frontend/src/components/incidentManagement/sources/SourceConfigurationViewer.vue new
+51
@@ -0,0 +1,51 @@
1 +<template>
2 + <div v-if="sourceConfiguration" class="flex flex-col gap-3">
3 + <KVCard v-if="showSource">
4 + <template #key>Source</template>
5 + <template #value>
6 + {{ sourceConfiguration.source }}
7 + </template>
8 + </KVCard>
9 + <KVCard>
10 + <template #key>Field names</template>
11 + <template #value>
12 + <div class="flex flex-wrap gap-2">
13 + <Badge type="splitted" v-for="field of sourceConfiguration.field_names" :key="field" fluid>
14 + <template #value>
15 + {{ field }}
16 + </template>
17 + </Badge>
18 + </div>
19 + </template>
20 + </KVCard>
21 + <KVCard>
22 + <template #key>Asset name</template>
23 + <template #value>
24 + {{ sourceConfiguration.asset_name }}
25 + </template>
26 + </KVCard>
27 + <KVCard>
28 + <template #key>Timefield name</template>
29 + <template #value>
30 + {{ sourceConfiguration.timefield_name }}
31 + </template>
32 + </KVCard>
33 + <KVCard>
34 + <template #key>Alert title name</template>
35 + <template #value>
36 + {{ sourceConfiguration.alert_title_name }}
37 + </template>
38 + </KVCard>
39 + </div>
40 +</template>
41 +
42 +<script setup lang="ts">
43 +import Badge from "@/components/common/Badge.vue"
44 +import KVCard from "@/components/common/KVCard.vue"
45 +import type { SourceConfiguration } from "@/types/incidentManagement/sources.d"
46 +
47 +const { sourceConfiguration, showSource } = defineProps<{
48 + sourceConfiguration: SourceConfiguration
49 + showSource?: boolean
50 +}>()
51 +</script>
frontend/src/components/incidentManagement/sources/SourceConfigurationWizard.vue new
+242
@@ -0,0 +1,242 @@
1 +<template>
2 + <n-spin :show="loading" class="source-configuration-wizard">
3 + <div class="wrapper flex flex-col">
4 + <div class="grow flex flex-col">
5 + <n-scrollbar x-scrollable trigger="none">
6 + <div class="p-7 pt-4">
7 + <n-steps :current="current" size="small" :status="currentStatus">
8 + <n-step title="Source" />
9 + <n-step title="Configuration" />
10 + </n-steps>
11 + </div>
12 + </n-scrollbar>
13 +
14 + <div class="mt-4 grow flex flex-col">
15 + <Transition :name="`slide-form-${slideFormDirection}`">
16 + <div v-if="current === 1" class="px-7 flex flex-col gap-4">
17 + <n-select
18 + v-model:value="selectedIndex"
19 + placeholder="Indices list"
20 + clearable
21 + filterable
22 + to="body"
23 + :loading="loadingIndices"
24 + :options="indexNamesOptions"
25 + @update:value="setSourceConfiguration(selectedIndex)"
26 + ></n-select>
27 +
28 + <div class="flex gap-4 justify-end">
29 + <n-button
30 + @click="next()"
31 + v-if="isNextStepEnabled"
32 + icon-placement="right"
33 + :disabled="!selectedIndex"
34 + >
35 + <template #icon>
36 + <Icon :name="ArrowRightIcon"></Icon>
37 + </template>
38 + Next
39 + </n-button>
40 + </div>
41 + </div>
42 +
43 + <div v-else-if="current === 2" class="px-7 grow flex flex-col pb-7" style="min-height: 401px">
44 + <SourceConfigurationForm
45 + v-if="sourceConfigurationModel"
46 + :sourceConfigurationModel
47 + show-source-field
48 + disable-source-field
49 + show-index-name-field
50 + disable-index-name-field
51 + :disabledSources
52 + @mounted="formCTX = $event"
53 + @submitted="createSourceConfiguration($event)"
54 + >
55 + <template #additionalActions>
56 + <n-button @click="prev()" :disabled="submitting" v-if="isPrevStepEnabled">
57 + <template #icon>
58 + <Icon :name="ArrowLeftIcon"></Icon>
59 + </template>
60 + Prev
61 + </n-button>
62 + </template>
63 + </SourceConfigurationForm>
64 + </div>
65 + </Transition>
66 + </div>
67 + </div>
68 + </div>
69 + </n-spin>
70 +</template>
71 +
72 +<script setup lang="ts">
73 +import { computed, onMounted, ref, toRefs, watch } from "vue"
74 +import { NSteps, NStep, useMessage, NScrollbar, NButton, NSpin, NSelect, type StepsProps } from "naive-ui"
75 +import Icon from "@/components/common/Icon.vue"
76 +import Api from "@/api"
77 +import { onBeforeMount } from "vue"
78 +import SourceConfigurationForm from "./SourceConfigurationForm.vue"
79 +import type { ApiError } from "@/types/common.d"
80 +import type { SourceConfiguration, SourceConfigurationModel, SourceName } from "@/types/incidentManagement/sources.d"
81 +
82 +const emit = defineEmits<{
83 + (e: "update:loading", value: boolean): void
84 + (e: "submitted"): void
85 + (
86 + e: "mounted",
87 + value: {
88 + reset: () => void
89 + }
90 + ): void
91 +}>()
92 +
93 +const props = defineProps<{ disabledSources?: SourceName[] }>()
94 +const { disabledSources } = toRefs(props)
95 +
96 +const ArrowLeftIcon = "carbon:arrow-left"
97 +const ArrowRightIcon = "carbon:arrow-right"
98 +const loadingIndices = ref(false)
99 +const submitting = ref(false)
100 +const loading = computed(() => submitting.value)
101 +const slideFormDirection = ref<"right" | "left">("right")
102 +const message = useMessage()
103 +const current = ref<number>(1)
104 +const currentStatus = ref<StepsProps["status"]>("process")
105 +const formCTX = ref<{ reset: () => void; toggleSubmittingFlag: () => boolean } | null>(null)
106 +const selectedIndex = ref<string | null>(null)
107 +const sourceConfigurationModel = ref<SourceConfigurationModel | null>(null)
108 +const indexNamesOptions = ref<{ label: string; value: string }[]>([])
109 +
110 +watch(loading, val => {
111 + emit("update:loading", val)
112 +})
113 +
114 +const isPrevStepEnabled = computed(() => current.value > 1)
115 +const isNextStepEnabled = computed(() => current.value < 2)
116 +
117 +function next() {
118 + currentStatus.value = "process"
119 + slideFormDirection.value = "right"
120 + current.value++
121 +}
122 +
123 +function prev() {
124 + currentStatus.value = "process"
125 + slideFormDirection.value = "left"
126 + current.value--
127 + formCTX.value?.reset()
128 +}
129 +
130 +function reset(force?: boolean) {
131 + if (!submitting.value || force) {
132 + currentStatus.value = "process"
133 + slideFormDirection.value = "right"
134 + current.value = 1
135 + selectedIndex.value = null
136 + sourceConfigurationModel.value = null
137 + formCTX.value?.reset()
138 + }
139 +}
140 +
141 +function setSourceConfiguration(indexName: string | null) {
142 + if (indexName) {
143 + sourceConfigurationModel.value = {
144 + field_names: [],
145 + asset_name: null,
146 + timefield_name: null,
147 + alert_title_name: null,
148 + source: "",
149 + index_name: indexName
150 + }
151 + next()
152 + }
153 +}
154 +
155 +function getIndices() {
156 + loadingIndices.value = true
157 +
158 + Api.graylog
159 + .getIndices()
160 + .then(res => {
161 + if (res.data.success) {
162 + indexNamesOptions.value = (res.data?.indices || []).map(o => ({
163 + label: o.index_name,
164 + value: o.index_name
165 + }))
166 + } else {
167 + message.warning(res.data?.message || "An error occurred. Please try again later.")
168 + }
169 + })
170 + .catch(err => {
171 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
172 + })
173 + .finally(() => {
174 + loadingIndices.value = false
175 + })
176 +}
177 +
178 +function createSourceConfiguration(payload: SourceConfiguration) {
179 + submitting.value = formCTX.value?.toggleSubmittingFlag() || true
180 +
181 + Api.incidentManagement
182 + .createSourceConfiguration(payload)
183 + .then(res => {
184 + if (res.data.success) {
185 + message.success(res.data?.message || `Source Configuration sent successfully`)
186 + reset(true)
187 + emit("submitted")
188 + } else {
189 + message.warning(res.data?.message || "An error occurred. Please try again later.")
190 + }
191 + })
192 + .catch((err: ApiError) => {
193 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
194 + })
195 + .finally(() => {
196 + submitting.value = formCTX.value?.toggleSubmittingFlag() || false
197 + })
198 +}
199 +
200 +onBeforeMount(() => {
201 + getIndices()
202 +})
203 +
204 +onMounted(() => {
205 + emit("mounted", {
206 + reset
207 + })
208 +})
209 +</script>
210 +
211 +<style lang="scss" scoped>
212 +.source-configuration-wizard {
213 + .wrapper {
214 + min-height: 180px;
215 + }
216 +
217 + .slide-form-right-enter-active,
218 + .slide-form-right-leave-active,
219 + .slide-form-left-enter-active,
220 + .slide-form-left-leave-active {
221 + transition: all 0.2s ease-out;
222 + position: absolute;
223 + width: 100%;
224 + }
225 +
226 + .slide-form-left-enter-from {
227 + transform: translateX(-100%);
228 + }
229 +
230 + .slide-form-left-leave-to {
231 + transform: translateX(100%);
232 + }
233 +
234 + .slide-form-right-enter-from {
235 + transform: translateX(100%);
236 + }
237 +
238 + .slide-form-right-leave-to {
239 + transform: translateX(-100%);
240 + }
241 +}
242 +</style>
frontend/src/components/license/LicenseCheckoutResponse.vue
+1 -1
@@ -34,7 +34,7 @@ import { onBeforeMount, ref, toRefs } from "vue"
34 import Api from "@/api"
35 import { NButton, NCard, NSpin, useMessage } from "naive-ui"
36 import Icon from "@/components/common/Icon.vue"
37 -import type { LicenseKey } from "@/types/license"
37 +import type { LicenseKey } from "@/types/license.d"
38 import { useGoto } from "@/composables/useGoto"
39
40 const props = defineProps<{ type: "success" | "error"; data?: { email?: string } }>()
frontend/src/components/license/LicenseDetails.vue
+13 -7
@@ -1,7 +1,7 @@
1 <template>
2 <n-spin :show="loading" content-class=" grow flex flex-col" class="flex flex-col overflow-hidden">
3 <div v-if="license" class="flex flex-col gap-4">
4 - <KVCard v-if="!hideKey" class="!basis-auto">
4 + <KVCard v-if="!hideKey">
5 <template #key>
6 <span class="flex gap-3 items-center">
7 <Icon :name="KeyIcon" :size="14"></Icon>
@@ -10,7 +10,7 @@
10 </template>
11 <template #value>{{ license.key }}</template>
12 </KVCard>
13 - <KVCard v-if="!hideFeatures" class="!basis-auto">
13 + <KVCard v-if="!hideFeatures">
14 <template #key>
15 <span class="flex gap-3 items-center">
16 <Icon :name="FeaturesIcon" :size="14"></Icon>
@@ -18,7 +18,7 @@
18 </span>
19 </template>
20 <template #value>
21 - <div v-if="features.length" class="grid gap-2 grid-auto-flow-200">
21 + <div v-if="features.length" class="grid gap-2 grid-auto-fit-200">
22 <KVCard v-for="feature of features" :key="feature">
23 <template #value>
24 <span class="flex gap-3 items-center">
@@ -31,7 +31,7 @@
31 <template v-else>No feature enabled</template>
32 </template>
33 </KVCard>
34 - <KVCard class="!basis-auto">
34 + <KVCard>
35 <template #key>
36 <span class="flex gap-3 items-center">
37 <Icon :name="ExpiresIcon" :size="14"></Icon>
@@ -43,7 +43,7 @@
43 <span class="text-secondary-color">({{ periodText }})</span>
44 </template>
45 </KVCard>
46 - <KVCard class="!basis-auto">
46 + <KVCard>
47 <template #key>
48 <span class="flex gap-3 items-center">
49 <Icon :name="CustomerIcon" :size="14"></Icon>
@@ -52,7 +52,13 @@
52 </template>
53 <template #value>
54 <div class="flex flex-wrap gap-2">
55 - <Badge type="splitted" v-for="(value, key) of license.customer" :key="key" fluid>
55 + <Badge
56 + type="splitted"
57 + color="primary"
58 + v-for="(value, key) of license.customer"
59 + :key="key"
60 + fluid
61 + >
62 <template #label>{{ sanitizeKey(key) }}</template>
63 <template #value>
64 <template v-if="key === 'created'">
@@ -64,7 +70,7 @@
70 </div>
71 </template>
72 </KVCard>
67 - <KVCard class="!basis-auto" v-if="dockerCompose">
73 + <KVCard v-if="dockerCompose">
74 <template #key>
75 <span class="flex gap-3 items-center">
76 <Icon :name="ConfigIcon" :size="14"></Icon>
frontend/src/components/license/LicenseFeatureOverlay.vue
+1 -1
@@ -28,7 +28,7 @@ import { NAlert, NButton } from "naive-ui"
28 import Icon from "@/components/common/Icon.vue"
29 import Api from "@/api"
30 import { onBeforeMount } from "vue"
31 -import type { LicenseFeatures } from "@/types/license"
31 +import type { LicenseFeatures } from "@/types/license.d"
32 import { useGoto } from "@/composables/useGoto"
33
34 const { feature } = defineProps<{ feature: LicenseFeatures }>()
frontend/src/components/license/LicenseViewer.vue
+1 -1
@@ -22,7 +22,7 @@ import { ref } from "vue"
22 import { NScrollbar } from "naive-ui"
23 import LicenseFeatures from "./LicenseFeatures.vue"
24 import LicenseDetails from "./LicenseDetails.vue"
25 -import type { License, LicenseKey } from "@/types/license"
25 +import type { License, LicenseKey } from "@/types/license.d"
26
27 const emit = defineEmits<{
28 (e: "licenseKeyLoaded", value: LicenseKey): void
frontend/src/components/license/SubscriptionCard.vue
+2 -2
@@ -79,9 +79,9 @@ import Icon from "@/components/common/Icon.vue"
79 import { ref, toRefs } from "vue"
80 import { NSpin, NModal, NButton, NPopconfirm, useMessage } from "naive-ui"
81 import Api from "@/api"
82 -import type { License, SubscriptionFeature } from "@/types/license"
82 +import type { License, SubscriptionFeature } from "@/types/license.d"
83 import { price } from "@/utils"
84 -import type { CancelSubscriptionPayload } from "@/api/license"
84 +import type { CancelSubscriptionPayload } from "@/api/endpoints/license"
85
86 const emit = defineEmits<{
87 (e: "deleted"): void
frontend/src/components/license/bkp/LicenseEditor.vue
+2 -2
@@ -77,7 +77,7 @@
77
78 <div class="create-box flex flex-col gap-2" v-if="creationEnabled">
79 <n-form :label-width="80" :model="creationForm" :rules="rules" ref="formRef">
80 - <div class="grid gap-2 grid-auto-flow-200">
80 + <div class="grid gap-2 grid-auto-fit-200">
81 <n-form-item label="Name" path="name">
82 <n-input v-model:value.trim="creationForm.name" placeholder="Input name..." clearable />
83 </n-form-item>
@@ -131,7 +131,7 @@ import { onBeforeMount, ref } from "vue"
131 import isEmail from "validator/es/lib/isEmail"
132 import { computed } from "vue"
133 import type { LicenseKey } from "@/types/license.d"
134 -import type { NewLicensePayload } from "@/api/license"
134 +import type { NewLicensePayload } from "@/api/endpoints/license"
135
136 const emit = defineEmits<{
137 (e: "updated"): void
frontend/src/components/logs/LogItem.vue
+2 -2
@@ -20,7 +20,7 @@
20 <div class="description px-1" v-if="log.additional_info">{{ log.additional_info }}</div>
21
22 <div class="badges-box flex flex-wrap items-center gap-3 mt-2">
23 - <Badge type="splitted" :color="log.event_type === LogEventType.ERROR ? 'danger' : undefined">
23 + <Badge type="splitted" :color="log.event_type === LogEventType.ERROR ? 'danger' : 'primary'">
24 <template #iconLeft>
25 <Icon
26 :name="log.event_type === LogEventType.ERROR ? ErrorIcon : InfoIcon"
@@ -30,7 +30,7 @@
30 <template #label>Type</template>
31 <template #value>{{ log.event_type }}</template>
32 </Badge>
33 - <Badge type="splitted" v-if="log.user_id">
33 + <Badge type="splitted" color="primary" v-if="log.user_id">
34 <template #iconLeft>
35 <Icon :name="UserIcon" :size="14"></Icon>
36 </template>
frontend/src/components/logs/LogsFilters.vue
+7 -2
@@ -60,9 +60,14 @@ import { ref, onBeforeMount, computed, watch, toRefs } from "vue"
60 import { NButton, NSelect, NInputGroup, NInputNumber, NInput } from "naive-ui"
61 import _cloneDeep from "lodash/cloneDeep"
62 import _toSafeInteger from "lodash/toSafeInteger"
63 -import type { LogsQueryEventType, LogsQueryTimeRange, LogsQueryTypes, LogsQueryValues } from "@/api/logs"
63 import Api from "@/api"
65 -import { LogEventType } from "@/types/logs.d"
64 +import {
65 + type LogsQueryEventType,
66 + type LogsQueryTimeRange,
67 + type LogsQueryTypes,
68 + type LogsQueryValues,
69 + LogEventType
70 +} from "@/types/logs.d"
71 import type { AuthUser } from "@/types/auth.d"
72
73 const emit = defineEmits<{
frontend/src/components/logs/LogsList.vue
+10 -4
@@ -68,14 +68,14 @@
68 </n-popover>
69 </div>
70 <n-spin :show="loading">
71 - <div class="list my-3">
71 + <div class="list flex flex-col gap-2 my-3">
72 <template v-if="logsList.length">
73 <LogItem
74 v-for="log of itemsPaginated"
75 :key="log.id"
76 :log="log"
77 :users="usersList"
78 - class="item-appear item-appear-bottom item-appear-005 mb-2"
78 + class="item-appear item-appear-bottom item-appear-005"
79 />
80 </template>
81 <template v-else>
@@ -125,8 +125,14 @@ import { nanoid } from "nanoid"
125 import { useResizeObserver } from "@vueuse/core"
126 import LogsFilters from "./LogsFilters.vue"
127 import LogItem from "./LogItem.vue"
128 -import { LogEventType, type Log } from "@/types/logs.d"
129 -import type { LogsQuery, LogsQueryTimeRange, LogsQueryTypes, LogsQueryValues } from "@/api/logs"
128 +import {
129 + type Log,
130 + type LogsQuery,
131 + type LogsQueryTimeRange,
132 + type LogsQueryTypes,
133 + type LogsQueryValues,
134 + LogEventType
135 +} from "@/types/logs.d"
136 import type { AuthUser } from "@/types/auth.d"
137
138 interface LogExt extends Log {
frontend/src/components/monitoringAlerts/Item.vue
+2 -2
@@ -27,7 +27,7 @@
27 <template #label>Customer #{{ alert.customer_code }}</template>
28 </Badge>
29
30 - <Badge type="splitted">
30 + <Badge type="splitted" color="primary">
31 <template #label>Source</template>
32 <template #value>{{ alert.alert_source }}</template>
33 </Badge>
@@ -70,7 +70,7 @@ import { computed, ref } from "vue"
70 import AlertActions from "./ItemActions.vue"
71 import { NSpin } from "naive-ui"
72 import { useGoto } from "@/composables/useGoto"
73 -import type { MonitoringAlert } from "@/types/monitoringAlerts"
73 +import type { MonitoringAlert } from "@/types/monitoringAlerts.d"
74
75 const { alert, embedded } = defineProps<{
76 alert: MonitoringAlert
frontend/src/components/monitoringAlerts/ItemActions.vue
+1 -1
@@ -17,7 +17,7 @@ import Icon from "@/components/common/Icon.vue"
17 import Api from "@/api"
18 import { computed, watch, ref } from "vue"
19 import type { Size } from "naive-ui/es/button/src/interface"
20 -import type { MonitoringAlert } from "@/types/monitoringAlerts"
20 +import type { MonitoringAlert } from "@/types/monitoringAlerts.d"
21
22 const emit = defineEmits<{
23 (e: "startInvoking"): void
frontend/src/components/monitoringAlerts/List.vue
+2 -2
@@ -45,7 +45,7 @@
45 />
46 </div>
47 <n-spin :show="loading">
48 - <div class="list my-3">
48 + <div class="list flex flex-col gap-2 my-3">
49 <template v-if="monitoringAlerts.length">
50 <Alert
51 v-for="alert of itemsPaginated"
@@ -53,7 +53,7 @@
53 :alert="alert"
54 @deleted="getData()"
55 @invoked="getData()"
56 - class="item-appear item-appear-bottom item-appear-005 mb-2"
56 + class="item-appear item-appear-bottom item-appear-005"
57 />
58 </template>
59 <template v-else>
frontend/src/components/reportCreation/Panels.vue
+2 -2
@@ -200,12 +200,12 @@
200 <script setup lang="ts">
201 import { ref, computed, toRefs, watch, onMounted } from "vue"
202 import { NButton, NSpin, NScrollbar, NTooltip, NDrawer, NDrawerContent, NPopover, NSwitch, useMessage } from "naive-ui"
203 -import type { Dashboard, Org, Panel } from "@/types/reporting"
203 +import type { Dashboard, Org, Panel } from "@/types/reporting.d"
204 import Icon from "@/components/common/Icon.vue"
205 import PrintSettings, { type PrintSettingsData } from "./PrintSettings.vue"
206 import draggable from "vuedraggable"
207 import Api from "@/api"
208 -import type { GenerateReportPayload, ReportTimeRange } from "@/api/reporting"
208 +import type { GenerateReportPayload, ReportTimeRange } from "@/api/endpoints/reporting"
209 import { saveAs } from "file-saver"
210 import { useStorage } from "@vueuse/core"
211 import _kebabCase from "lodash/kebabCase"
frontend/src/components/reportCreation/Wizard.vue
+2 -2
@@ -53,8 +53,8 @@
53 import { computed, onBeforeMount, ref, toRefs, watch } from "vue"
54 import { NSpin, NForm, NFormItem, NInputGroup, NInputNumber, NSelect, useMessage } from "naive-ui"
55 import Api from "@/api"
56 -import type { Dashboard, Org, Panel } from "@/types/reporting"
57 -import type { ReportTimeRange, RowPanelTimeUnit } from "@/api/reporting"
56 +import type { Dashboard, Org, Panel } from "@/types/reporting.d"
57 +import type { ReportTimeRange, RowPanelTimeUnit } from "@/api/endpoints/reporting"
58 import { useStorage } from "@vueuse/core"
59
60 const emit = defineEmits<{
frontend/src/components/scheduler/Item.vue
+2 -2
@@ -18,7 +18,7 @@
18 <div class="title">{{ job.name }}</div>
19 <div class="description mt-1">{{ job.description }}</div>
20 <div class="badges-box flex flex-wrap items-center gap-3 mt-4">
21 - <Badge type="splitted">
21 + <Badge type="splitted" color="primary">
22 <template #label>Interval</template>
23 <template #value>
24 {{ job.time_interval }} {{ job.time_interval === 1 ? "minute" : "minutes" }}
@@ -42,7 +42,7 @@
42 import Icon from "@/components/common/Icon.vue"
43 import Badge from "@/components/common/Badge.vue"
44 import { NTooltip } from "naive-ui"
45 -import type { Job } from "@/types/scheduler"
45 +import type { Job } from "@/types/scheduler.d"
46 import { formatDate } from "@/utils"
47 import { useSettingsStore } from "@/stores/settings"
48 import JobActions from "./JobActions.vue"
frontend/src/components/scheduler/JobActions.vue
+2 -2
@@ -53,10 +53,10 @@ import { NButton, NModal, useMessage } from "naive-ui"
53 import Icon from "@/components/common/Icon.vue"
54 import JobForm from "./JobForm.vue"
55 import NextTooltip from "./NextTooltip.vue"
56 -import type { Job } from "@/types/scheduler"
56 +import type { Job } from "@/types/scheduler.d"
57 import type { Size } from "naive-ui/es/button/src/interface"
58 import Api from "@/api"
59 -import type { UpdateJobPayload } from "@/api/scheduler"
59 +import type { UpdateJobPayload } from "@/api/endpoints/scheduler"
60
61 const props = defineProps<{ job: Job; size?: Size; inline?: boolean }>()
62 const { job, size, inline } = toRefs(props)
frontend/src/components/scheduler/JobForm.vue
+2 -2
@@ -46,8 +46,8 @@ import {
46 } from "naive-ui"
47 import _trim from "lodash/trim"
48 import _get from "lodash/get"
49 -import type { UpdateJobPayload } from "@/api/scheduler"
50 -import type { Job } from "@/types/scheduler"
49 +import type { UpdateJobPayload } from "@/api/endpoints/scheduler"
50 +import type { Job } from "@/types/scheduler.d"
51
52 const props = defineProps<{ job: Job }>()
53 const { job } = toRefs(props)
frontend/src/components/scheduler/List.vue
+1 -1
@@ -18,7 +18,7 @@ import { ref, onBeforeMount, computed } from "vue"
18 import { useMessage, NSpin, NEmpty } from "naive-ui"
19 import Api from "@/api"
20 import JobCard from "./Item.vue"
21 -import type { Job } from "@/types/scheduler"
21 +import type { Job } from "@/types/scheduler.d"
22
23 const message = useMessage()
24 const loadingJobs = ref(false)
frontend/src/components/soc/SocAlerts/SocAlertAssets/SocAlertAssetsItem.vue
+5 -4
@@ -33,14 +33,14 @@
33 </div>
34
35 <div class="badges-box flex flex-wrap items-center gap-3 mt-4">
36 - <Badge type="splitted" v-if="asset.date_added">
36 + <Badge type="splitted" color="primary" v-if="asset.date_added">
37 <template #iconLeft>
38 <Icon :name="ClockIcon" :size="14"></Icon>
39 </template>
40 <template #label>Added</template>
41 <template #value>{{ formatDate(asset.date_added) }}</template>
42 </Badge>
43 - <Badge type="splitted" v-if="asset.date_update">
43 + <Badge type="splitted" color="primary" v-if="asset.date_update">
44 <template #iconLeft>
45 <Icon :name="ClockIcon" :size="14"></Icon>
46 </template>
@@ -69,7 +69,7 @@
69 >
70 <n-tabs type="line" animated :tabs-padding="24">
71 <n-tab-pane name="Info" tab="Info" display-directive="show">
72 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="properties">
72 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4" v-if="properties">
73 <KVCard v-for="(value, key) of properties" :key="key">
74 <template #key>{{ key }}</template>
75 <template #value>
@@ -92,7 +92,7 @@
92 </div>
93 </n-tab-pane>
94 <n-tab-pane name="Type" tab="Type" display-directive="show">
95 - <div class="grid gap-2 grid-auto-flow-250 p-7 pt-4" v-if="assetType">
95 + <div class="grid gap-2 grid-auto-fit-250 p-7 pt-4" v-if="assetType">
96 <KVCard v-for="(value, key) of assetType" :key="key">
97 <template #key>{{ key }}</template>
98 <template #value>{{ value || "-" }}</template>
@@ -145,6 +145,7 @@ import { isUrlLike } from "@/utils"
145 import type { SocAlertAsset } from "@/types/soc/asset.d"
146 import { useSettingsStore } from "@/stores/settings"
147 import { useGoto } from "@/composables/useGoto"
148 +
149 const ArtifactsCollect = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCollect.vue"))
150
151 const { asset } = defineProps<{ asset: SocAlertAsset }>()
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItem.vue
+2 -2
@@ -108,7 +108,7 @@
108 <div class="whitespace-nowrap">SOC Alert: {{ alert?.alert_id }}</div>
109 </template>
110 <template #header-extra>
111 - <SocAlertItemRecommendation v-if="alert" :alert />
111 + <ArtifactRecommendation v-if="alert" :context="alert.alert_context" />
112 </template>
113 <SocAlertItemDetails v-if="alert" :alert :users @updated="updateAlert" />
114 </n-modal>
@@ -128,7 +128,7 @@ import { NCollapse, useMessage, NCollapseItem, NModal, NSpin, NCheckbox, NCollap
128 import type { SocUser } from "@/types/soc/user.d"
129 const SocAlertItemDetails = defineAsyncComponent(() => import("./SocAlertItemDetails.vue"))
130 const SocAlertItemBadges = defineAsyncComponent(() => import("./SocAlertItemBadges.vue"))
131 -const SocAlertItemRecommendation = defineAsyncComponent(() => import("./SocAlertItemRecommendation.vue"))
131 +const ArtifactRecommendation = defineAsyncComponent(() => import("@/components/artifacts/ArtifactRecommendation.vue"))
132 const AlertItem = defineAsyncComponent(() => import("@/components/alerts/Alert.vue"))
133
134 const checked = defineModel<boolean>("checked", { default: false })
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemBadges.vue
+4 -4
@@ -2,7 +2,7 @@
2 <div class="flex flex-wrap items-center gap-3 mt-3">
3 <n-tooltip placement="top-start" trigger="hover">
4 <template #trigger>
5 - <Badge type="splitted" hint-cursor>
5 + <Badge type="splitted" color="primary" hint-cursor>
6 <template #iconLeft>
7 <Icon :name="StatusIcon" :size="14"></Icon>
8 </template>
@@ -12,21 +12,21 @@
12 </template>
13 {{ alert.status.status_description }}
14 </n-tooltip>
15 - <Badge type="splitted" :color="alert.severity?.severity_id === 5 ? 'danger' : undefined">
15 + <Badge type="splitted" :color="alert.severity?.severity_id === 5 ? 'danger' : 'primary'">
16 <template #iconLeft>
17 <Icon :name="SeverityIcon" :size="13"></Icon>
18 </template>
19 <template #label>Severity</template>
20 <template #value>{{ alert.severity?.severity_name || "-" }}</template>
21 </Badge>
22 - <Badge type="splitted" class="hide-on-small">
22 + <Badge type="splitted" color="primary" class="hide-on-small">
23 <template #iconLeft>
24 <Icon :name="SourceIcon" :size="13"></Icon>
25 </template>
26 <template #label>Source</template>
27 <template #value>{{ alert.alert_source || "-" }}</template>
28 </Badge>
29 - <Badge type="splitted" class="hide-on-small">
29 + <Badge type="splitted" color="primary" class="hide-on-small">
30 <template #iconLeft>
31 <Icon :name="CustomerIcon" :size="13"></Icon>
32 </template>
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemContext.vue
+9 -3
@@ -6,14 +6,18 @@
6 </template>
7 </n-input>
8
9 - <div class="grid gap-2 grid-auto-flow-200">
9 + <div class="grid gap-2 grid-auto-fit-200">
10 <KVCard v-for="{ value, key } of contextFiltered" :key="key">
11 <template #key>{{ key }}</template>
12 <template #value>
13 <template v-if="key === 'process_name'">
14 <template v-if="value && value !== '-' && value.toString() && processNameList.length">
15 <div class="flex flex-wrap gap-2">
16 - <SocAlertItemEvaluation v-for="pn of processNameList" :key="pn" :process-name="pn" />
16 + <ThreatIntelProcessEvaluationBadge
17 + v-for="pn of processNameList"
18 + :key="pn"
19 + :process-name="pn"
20 + />
21 </div>
22 </template>
23 <template v-else>-</template>
@@ -36,7 +40,9 @@ import { computed, defineAsyncComponent, ref } from "vue"
40 import _split from "lodash/split"
41 import _compact from "lodash/compact"
42 import _uniq from "lodash/uniq"
39 -const SocAlertItemEvaluation = defineAsyncComponent(() => import("./SocAlertItemEvaluation.vue"))
43 +const ThreatIntelProcessEvaluationBadge = defineAsyncComponent(
44 + () => import("@/components/threatIntel/ThreatIntelProcessEvaluationBadge.vue")
45 +)
46 const ExpandableText = defineAsyncComponent(() => import("@/components/common/ExpandableText.vue"))
47
48 const { alert } = defineProps<{
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAlertItemDetails.vue
+12 -11
@@ -9,7 +9,7 @@
9 </div>
10 </n-tab-pane>
11 <n-tab-pane name="Customer" tab="Customer" display-directive="show:lazy">
12 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
12 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4">
13 <KVCard v-for="(value, key) of alert.customer" :key="key">
14 <template #key>{{ key }}</template>
15 <template #value>
@@ -38,7 +38,7 @@
38 <template #label>Go to users page</template>
39 </Badge>
40 </div>
41 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4">
41 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4">
42 <KVCard>
43 <template #key>user_login</template>
44 <template #value>
@@ -89,20 +89,21 @@
89 </template>
90
91 <script setup lang="ts">
92 -import type { SocAlert } from "@/types/soc/alert.d"
92 +import { computed, defineAsyncComponent } from "vue"
93 +import { NTabs, NTabPane, NSpin } from "naive-ui"
94 import Icon from "@/components/common/Icon.vue"
95 import Badge from "@/components/common/Badge.vue"
95 -import { computed } from "vue"
96 -import { SimpleJsonViewer } from "vue-sjv"
96 import KVCard from "@/components/common/KVCard.vue"
98 -import SocAlertItemTimeline from "./SocAlertItemTimeline.vue"
99 -import SocAssignUser from "./SocAssignUser.vue"
100 -import SocAlertItemContext from "./SocAlertItemContext.vue"
101 -import SocAlertAssetsList from "../SocAlertAssets/SocAlertAssetsList.vue"
97 +import { SimpleJsonViewer } from "vue-sjv"
98 import "@/assets/scss/vuesjv-override.scss"
103 -import { NTabs, NTabPane, NSpin } from "naive-ui"
99 import { useGoto } from "@/composables/useGoto"
105 -import type { SocUser } from "@/types/soc/user"
100 +import type { SocAlert } from "@/types/soc/alert.d"
101 +import type { SocUser } from "@/types/soc/user.d"
102 +
103 +const SocAlertItemTimeline = defineAsyncComponent(() => import("./SocAlertItemTimeline.vue"))
104 +const SocAssignUser = defineAsyncComponent(() => import("./SocAssignUser.vue"))
105 +const SocAlertItemContext = defineAsyncComponent(() => import("./SocAlertItemContext.vue"))
106 +const SocAlertAssetsList = defineAsyncComponent(() => import("../SocAlertAssets/SocAlertAssetsList.vue"))
107
108 const emit = defineEmits<{
109 (e: "updated", value: SocAlert): void
frontend/src/components/soc/SocAlerts/SocAlertItem/SocAssignUser.vue
+3 -4
@@ -13,12 +13,11 @@
13 </template>
14
15 <script setup lang="ts">
16 -import type { SocAlert } from "@/types/soc/alert.d"
17 -import { computed, onBeforeMount, ref, toRefs } from "vue"
18 -import Api from "@/api"
16 +import { computed, onBeforeMount, ref, toRefs, watch } from "vue"
17 import { useMessage, NPopselect } from "naive-ui"
18 +import Api from "@/api"
19 +import type { SocAlert } from "@/types/soc/alert.d"
20 import type { SocUser } from "@/types/soc/user.d"
21 -import { watch } from "vue"
21
22 const props = defineProps<{
23 alert: SocAlert
frontend/src/components/soc/SocAlerts/SocAlertsList.vue
+1 -1
@@ -95,7 +95,7 @@ import Api from "@/api"
95 import SocAlertItem from "./SocAlertItem/SocAlertItem.vue"
96 import type { SocAlert } from "@/types/soc/alert.d"
97 import type { SocUser } from "@/types/soc/user.d"
98 -import type { AlertsFilter } from "@/api/soc"
98 +import type { AlertsFilter } from "@/api/endpoints/soc"
99 import { useResizeObserver, watchDebounced } from "@vueuse/core"
100 import PaginationIndeterminate from "@/components/common/PaginationIndeterminate.vue"
101 import Icon from "@/components/common/Icon.vue"
frontend/src/components/soc/SocCases/SocCaseAssetLink.vue
+3 -3
@@ -13,15 +13,15 @@
13 <div class="description mt-2" v-if="link.asset_description">{{ link.asset_description }}</div>
14
15 <div class="badges-box flex flex-wrap items-center gap-3 mt-4">
16 - <Badge type="splitted">
16 + <Badge type="splitted" color="primary">
17 <template #label>Case open date</template>
18 <template #value>{{ formatDate(link.case_open_date) }}</template>
19 </Badge>
20 - <Badge type="splitted">
20 + <Badge type="splitted" color="primary">
21 <template #label>Asset id</template>
22 <template #value>{{ link.asset_id }}</template>
23 </Badge>
24 - <Badge type="splitted">
24 + <Badge type="splitted" color="primary">
25 <template #label>Compromise status</template>
26 <template #value>{{ link.asset_compromise_status_id || "-" }}</template>
27 </Badge>
frontend/src/components/soc/SocCases/SocCaseAssetsItem.vue
+8 -7
@@ -33,15 +33,15 @@
33 </div>
34
35 <div class="badges-box flex flex-wrap items-center gap-3 mt-4">
36 - <Badge type="splitted">
36 + <Badge type="splitted" color="primary">
37 <template #label>Status</template>
38 <template #value>{{ asset.analysis_status }}</template>
39 </Badge>
40 - <Badge type="splitted">
40 + <Badge type="splitted" color="primary">
41 <template #label>Type</template>
42 <template #value>{{ asset.asset_type }}</template>
43 </Badge>
44 - <Badge type="splitted" v-for="tag of tags" :key="tag.key">
44 + <Badge type="splitted" color="primary" v-for="tag of tags" :key="tag.key">
45 <template #label>{{ tag.key }}</template>
46 <template #value v-if="tag.value !== undefined">{{ tag.value || "-" }}</template>
47 </Badge>
@@ -61,7 +61,7 @@
61 >
62 <n-tabs type="line" animated :tabs-padding="24">
63 <n-tab-pane name="Info" tab="Info" display-directive="show">
64 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="properties">
64 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4" v-if="properties">
65 <KVCard v-for="(value, key) of properties" :key="key">
66 <template #key>{{ key }}</template>
67 <template #value>{{ value || "-" }}</template>
@@ -104,18 +104,19 @@
104 </template>
105
106 <script setup lang="ts">
107 +import { computed, defineAsyncComponent, ref } from "vue"
108 +import { NModal, NTabs, NTabPane, NEmpty } from "naive-ui"
109 import Icon from "@/components/common/Icon.vue"
110 import KVCard from "@/components/common/KVCard.vue"
111 import Badge from "@/components/common/Badge.vue"
110 -import SocCaseAssetLink from "./SocCaseAssetLink.vue"
111 -import { computed, ref } from "vue"
112 -import { NModal, NTabs, NTabPane, NEmpty } from "naive-ui"
112 import { isUrlLike } from "@/utils"
113 import _omit from "lodash/omit"
114 import _split from "lodash/split"
115 import _upperFirst from "lodash/upperFirst"
116 import type { SocCaseAsset } from "@/types/soc/asset.d"
117
118 +const SocCaseAssetLink = defineAsyncComponent(() => import("./SocCaseAssetLink.vue"))
119 +
120 const { asset } = defineProps<{ asset: SocCaseAsset }>()
121
122 const InfoIcon = "carbon:information"
frontend/src/components/soc/SocCases/SocCaseItem.vue
+17 -16
@@ -47,7 +47,7 @@
47 <div class="badges-box flex flex-wrap items-center gap-3 mt-4">
48 <Badge
49 type="splitted"
50 - :color="baseInfo.state_name === StateName.Open ? 'warning' : undefined"
50 + :color="baseInfo.state_name === StateName.Open ? 'warning' : 'primary'"
51 >
52 <template #iconLeft>
53 <Icon :name="StatusIcon" :size="14"></Icon>
@@ -55,14 +55,14 @@
55 <template #label>State</template>
56 <template #value>{{ baseInfo.state_name }}</template>
57 </Badge>
58 - <Badge type="splitted">
58 + <Badge type="splitted" color="primary">
59 <template #iconLeft>
60 <Icon :name="OwnerIcon" :size="16"></Icon>
61 </template>
62 <template #label>Owner</template>
63 <template #value>{{ baseInfo.owner }}</template>
64 </Badge>
65 - <Badge type="splitted">
65 + <Badge type="splitted" color="primary">
66 <template #iconLeft>
67 <Icon :name="CustomerIcon" :size="13"></Icon>
68 </template>
@@ -165,7 +165,7 @@
165 </code>
166 </div>
167 </div>
168 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="properties">
168 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4" v-if="properties">
169 <KVCard v-for="(value, key) of properties" :key="key">
170 <template #key>{{ key }}</template>
171 <template #value>
@@ -245,17 +245,7 @@
245 </template>
246
247 <script setup lang="ts">
248 -import Icon from "@/components/common/Icon.vue"
249 -import KVCard from "@/components/common/KVCard.vue"
250 -import Badge from "@/components/common/Badge.vue"
251 -import { computed, onBeforeMount, ref, watch } from "vue"
252 -import SocCaseTimeline from "./SocCaseTimeline.vue"
253 -import SocCaseAssetsList from "./SocCaseAssetsList.vue"
254 -import SocCaseNoteForm from "./SocCaseNoteForm.vue"
255 -import SocCaseNotesList from "./SocCaseNotesList.vue"
256 -import SocCaseItemActions from "./SocCaseItemActions.vue"
257 -import SocAlertItem from "../SocAlerts/SocAlertItem/SocAlertItem.vue"
258 -import Api from "@/api"
248 +import { computed, defineAsyncComponent, onBeforeMount, ref, watch } from "vue"
249 import {
250 useMessage,
251 NPopover,
@@ -270,12 +260,23 @@ import {
260 NCollapse,
261 NCollapseItem
262 } from "naive-ui"
263 +import Icon from "@/components/common/Icon.vue"
264 +import KVCard from "@/components/common/KVCard.vue"
265 +import Badge from "@/components/common/Badge.vue"
266 +import Api from "@/api"
267 import { useSettingsStore } from "@/stores/settings"
268 import dayjs from "@/utils/dayjs"
275 -import { type SocCase, StateName, type SocCaseExt } from "@/types/soc/case.d"
269 import _omit from "lodash/omit"
270 import _split from "lodash/split"
271 import { useGoto } from "@/composables/useGoto"
272 +import { type SocCase, StateName, type SocCaseExt } from "@/types/soc/case.d"
273 +
274 +const SocCaseTimeline = defineAsyncComponent(() => import("./SocCaseTimeline.vue"))
275 +const SocCaseAssetsList = defineAsyncComponent(() => import("./SocCaseAssetsList.vue"))
276 +const SocCaseNoteForm = defineAsyncComponent(() => import("./SocCaseNoteForm.vue"))
277 +const SocCaseNotesList = defineAsyncComponent(() => import("./SocCaseNotesList.vue"))
278 +const SocCaseItemActions = defineAsyncComponent(() => import("./SocCaseItemActions.vue"))
279 +const SocAlertItem = defineAsyncComponent(() => import("../SocAlerts/SocAlertItem/SocAlertItem.vue"))
280
281 const { caseData, caseId, embedded, hideSocCaseAction, hideSocAlertLink } = defineProps<{
282 caseData?: SocCase
frontend/src/components/soc/SocCases/SocCaseNote.vue
+9 -8
@@ -31,15 +31,15 @@
31
32 <!--
33 <div class="badges-box flex flex-wrap items-center gap-3 mt-4">
34 - <Badge type="splitted">
34 + <Badge type="splitted" color="primary">
35 <template #label>Status</template>
36 <template #value>{{ asset.analysis_status }}</template>
37 </Badge>
38 - <Badge type="splitted">
38 + <Badge type="splitted" color="primary">
39 <template #label>Type</template>
40 <template #value>{{ asset.asset_type }}</template>
41 </Badge>
42 - <Badge type="splitted" v-for="tag of tags" :key="tag.key">
42 + <Badge type="splitted" color="primary" v-for="tag of tags" :key="tag.key">
43 <template #label>{{ tag.key }}</template>
44 <template #value v-if="tag.value !== undefined">{{ tag.value || "-" }}</template>
45 </Badge>
@@ -63,7 +63,7 @@
63 >
64 <n-tabs type="line" animated :tabs-padding="24">
65 <n-tab-pane name="Info" tab="Info" display-directive="show">
66 - <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="properties">
66 + <div class="grid gap-2 grid-auto-fit-200 p-7 pt-4" v-if="properties">
67 <KVCard v-for="(value, key) of properties" :key="key">
68 <template #key>{{ key }}</template>
69 <template #value>{{ value || "-" }}</template>
@@ -96,15 +96,16 @@
96 </template>
97
98 <script setup lang="ts">
99 +import { computed, defineAsyncComponent, ref } from "vue"
100 +import { NModal, NTabs, NTabPane, NInput, NPopover } from "naive-ui"
101 import Icon from "@/components/common/Icon.vue"
102 import KVCard from "@/components/common/KVCard.vue"
101 -import SocCaseNoteTimeline from "./SocCaseNoteTimeline.vue"
102 -import { computed, ref } from "vue"
103 -import { NModal, NTabs, NTabPane, NInput, NPopover } from "naive-ui"
103 import _omit from "lodash/omit"
105 -import type { SocNote } from "@/types/soc/note.d"
104 import { useSettingsStore } from "@/stores/settings"
105 import dayjs from "@/utils/dayjs"
106 +import type { SocNote } from "@/types/soc/note.d"
107 +
108 +const SocCaseNoteTimeline = defineAsyncComponent(() => import("./SocCaseNoteTimeline.vue"))
109
110 const { note } = defineProps<{ note: SocNote }>()
111
frontend/src/components/soc/SocCases/SocCasesList.vue
+3 -3
@@ -84,14 +84,14 @@
84 </n-popover>
85 </div>
86 <n-spin :show="loading">
87 - <div class="list my-3">
87 + <div class="list flex flex-col gap-2 my-3">
88 <template v-if="casesList.length">
89 <SocCaseItem
90 v-for="caseData of itemsPaginated"
91 :key="caseData.case_id"
92 :caseData="caseData"
93 @deleted="getData()"
94 - class="item-appear item-appear-bottom item-appear-005 mb-2"
94 + class="item-appear item-appear-bottom item-appear-005"
95 />
96 </template>
97 <template v-else>
@@ -131,7 +131,7 @@ import _cloneDeep from "lodash/cloneDeep"
131 import _orderBy from "lodash/orderBy"
132 import Icon from "@/components/common/Icon.vue"
133 import { useResizeObserver } from "@vueuse/core"
134 -import type { CasesFilter } from "@/api/soc"
134 +import type { CasesFilter } from "@/api/endpoints/soc"
135 import type { DateFormatted, SocCase } from "@/types/soc/case.d"
136 import SocCaseItem from "./SocCaseItem.vue"
137 import dayjs from "@/utils/dayjs"
frontend/src/components/stackProvisioning/StackProvisioningList.vue
+2 -2
@@ -1,9 +1,9 @@
1 <template>
2 <div class="stack-provisioning-list">
3 <n-spin :show="loading">
4 - <div class="list my-3">
4 + <div class="list flex flex-col gap-2 my-3">
5 <template v-if="list.length">
6 - <StackProvisioningItem v-for="item of list" :key="item.name" :content-pack="item" class="mb-2" />
6 + <StackProvisioningItem v-for="item of list" :key="item.name" :content-pack="item" />
7 </template>
8 <template v-else>
9 <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
frontend/src/components/threatIntel/ThreatIntelButton.vue renamed
frontend/src/components/threatIntel/ThreatIntelEpssScore.vue renamed
+3 -3
@@ -46,11 +46,11 @@
46 import { ref, onBeforeMount } from "vue"
47 import { useMessage, NSpin, NEmpty, NCard, NStatistic } from "naive-ui"
48 import Api from "@/api"
49 -import type { EpssScore } from "@/types/threatIntel.d"
50 -import { formatDate } from "@/utils"
49 import KVCard from "@/components/common/KVCard.vue"
52 -import { nanoid } from "nanoid"
50 import { useSettingsStore } from "@/stores/settings"
51 +import { formatDate } from "@/utils"
52 +import { nanoid } from "nanoid"
53 +import type { EpssScore } from "@/types/threatIntel.d"
54
55 interface EpssScoreExt extends EpssScore {
56 ___id?: string
frontend/src/components/threatIntel/ThreatIntelForm.vue renamed
+2 -4
@@ -67,10 +67,10 @@
67 import { ref, computed, onMounted } from "vue"
68 import { useMessage, NSpin, NButton, NInput } from "naive-ui"
69 import Api from "@/api"
70 -import _trim from "lodash/trim"
71 -import type { ThreatIntelResponse } from "@/types/threatIntel.d"
70 import { useSettingsStore } from "@/stores/settings"
71 import { formatDate } from "@/utils"
72 +import _trim from "lodash/trim"
73 +import type { ThreatIntelResponse } from "@/types/threatIntel.d"
74
75 const emit = defineEmits<{
76 (
@@ -83,12 +83,10 @@ const emit = defineEmits<{
83
84 const message = useMessage()
85 const dFormats = useSettingsStore().dateFormat
86 -
86 const loading = ref(false)
87 const iocValue = ref<string>("")
88 const response = ref<ThreatIntelResponse | null>(null)
89 const error = ref<string>("")
91 -
90 const isValid = computed(() => {
91 return !!_trim(iocValue.value)
92 })
frontend/src/components/threatIntel/ThreatIntelProcessEvaluationBadge.vue new
+19
@@ -0,0 +1,19 @@
1 +<template>
2 + <ThreatIntelProcessEvaluationProvider :processName v-slot="{ openEvaluation }">
3 + <code class="cursor-pointer text-primary-color" @click="openEvaluation()">
4 + {{ processName }}
5 + <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
6 + </code>
7 + </ThreatIntelProcessEvaluationProvider>
8 +</template>
9 +
10 +<script setup lang="ts">
11 +import ThreatIntelProcessEvaluationProvider from "./ThreatIntelProcessEvaluationProvider.vue"
12 +import Icon from "@/components/common/Icon.vue"
13 +
14 +const { processName } = defineProps<{
15 + processName: string
16 +}>()
17 +
18 +const LinkIcon = "carbon:launch"
19 +</script>
frontend/src/components/threatIntel/ThreatIntelProcessEvaluationProvider.vue renamed
+4 -9
@@ -1,8 +1,5 @@
1 <template>
2 - <code class="cursor-pointer text-primary-color" @click="openEvaluation()">
3 - {{ processName }}
4 - <Icon :name="LinkIcon" :size="13" class="relative top-0.5" />
5 - </code>
2 + <slot :openEvaluation />
3
4 <n-modal
5 v-model:show="showDetails"
@@ -84,19 +81,17 @@
81 </template>
82
83 <script setup lang="ts">
87 -import Icon from "@/components/common/Icon.vue"
88 -import { useMessage, NModal, NSpin, NTabs, NTabPane, NStatistic, NInput, NCard, NEmpty } from "naive-ui"
89 -import type { EvaluationData } from "@/types/threatIntel"
84 import { computed, defineAsyncComponent, ref } from "vue"
85 +import { useMessage, NModal, NSpin, NTabs, NTabPane, NStatistic, NInput, NCard, NEmpty } from "naive-ui"
86 import Api from "@/api"
92 -import _toSafeInteger from "lodash/toSafeInteger"
87 const ListPercentage = defineAsyncComponent(() => import("@/components/common/ListPercentage.vue"))
88 +import _toSafeInteger from "lodash/toSafeInteger"
89 +import type { EvaluationData } from "@/types/threatIntel.d"
90
91 const { processName } = defineProps<{
92 processName: string
93 }>()
94
99 -const LinkIcon = "carbon:launch"
95 const evaluation = ref<EvaluationData | null>(null)
96 const showDetails = ref<boolean>(false)
97 const loading = ref<boolean>(false)
frontend/src/components/webVulnerabilityAssessment/ReportsList.vue
+1 -1
@@ -30,7 +30,7 @@
30 </div>
31 </div>
32 <n-spin :show="loading" class="min-h-32">
33 - <div class="list grid gap-4 grid-auto-flow-250">
33 + <div class="list grid gap-4 grid-auto-fill-250">
34 <template v-if="reportsList.length">
35 <ReportsItem
36 v-for="report of reportsList"
frontend/src/composables/useGoto.ts
+6 -1
@@ -61,6 +61,10 @@ export function useGoto() {
61 router.push({ name: "Soc-Users", query: userId ? { user_id: userId } : {} })
62 }
63
64 + function gotoIncidentManagementCases(caseId?: number) {
65 + router.push({ name: "IncidentManagement-Cases", query: caseId ? { case_id: caseId } : {} })
66 + }
67 +
68 return {
69 gotoCustomer,
70 gotoAgent,
@@ -73,6 +77,7 @@ export function useGoto() {
77 gotoGraylogPipelines,
78 gotoSocUsers,
79 gotoAlerts,
76 - gotoConnectors
80 + gotoConnectors,
81 + gotoIncidentManagementCases
82 }
83 }
frontend/src/lang/config.ts
+13 -2
@@ -1,12 +1,23 @@
1 import * as locales from "."
2
3 -export type MessageSchema = typeof locales.en
3 export type Locales = keyof typeof locales
4 +export type MessageSchema = (typeof locales)[Locales]
5
6 export function getI18NConf() {
7 + // @ts-expect-error "locales" don't match with [key: string]: { default: MessageSchema } }
8 + const localesEntries = Object.entries<{ default: MessageSchema }>(locales)
9 +
10 + const messages = localesEntries.reduce(
11 + (acc: { [key: string]: MessageSchema }, cur: [string, { default: MessageSchema }]) => {
12 + acc[cur[0]] = cur[1].default
13 + return acc
14 + },
15 + {}
16 + ) as { [key in Locales]: MessageSchema }
17 +
18 return {
19 legacy: false,
20 locale: "en",
10 - messages: locales
21 + messages
22 }
23 }
frontend/src/layouts/common/Navbar/items.tsx
+53 -18
@@ -3,6 +3,8 @@ import { h } from "vue"
3 import { RouterLink } from "vue-router"
4 import { type MenuMixedOption } from "naive-ui/es/menu/src/interface"
5
6 +import IncidentManagementIcon from "@/assets/icons/alert-settings-icon.svg"
7 +
8 const OverviewIcon = "carbon:dashboard"
9 const IndiciesIcon = "ph:list-magnifying-glass"
10 const AgentsIcon = "carbon:network-3"
@@ -10,10 +12,9 @@ const ConnectorsIcon = "carbon:hybrid-networking"
12 const GraylogIcon = "majesticons:pulse-line"
13 const AlertsIcon = "carbon:warning-hex"
14 const ArtifactsIcon = "carbon:document-multiple-01"
13 -const SOCIcon = "carbon:security"
15 +// const SocIcon = "carbon:security"
16 const HealthcheckIcon = "ph:heartbeat"
17 const CustomersIcon = "carbon:user-multiple"
16 -const UsersIcon = "carbon:group-security"
18 const ExternalServicesIcon = "carbon:ibm-cloud-direct-link-2-dedicated"
19 const ReportCreationIcon = "carbon:report-data"
20 const SchedulerIcon = "material-symbols:autoplay"
@@ -150,10 +151,11 @@ export default function getItems(): MenuMixedOption[] {
151 key: "Artifacts",
152 icon: renderIcon(ArtifactsIcon)
153 },
154 + /*
155 {
156 label: "SOC",
155 - key: "SOC",
156 - icon: renderIcon(SOCIcon),
157 + key: "Soc",
158 + icon: renderIcon(SocIcon),
159 children: [
160 {
161 label: () =>
@@ -209,6 +211,53 @@ export default function getItems(): MenuMixedOption[] {
211 }
212 ]
213 },
214 + */
215 + {
216 + label: "Incident Management",
217 + key: "IncidentManagement",
218 + icon: renderIcon(IncidentManagementIcon),
219 + children: [
220 + {
221 + label: () =>
222 + h(
223 + RouterLink,
224 + {
225 + to: {
226 + name: "IncidentManagement-Sources"
227 + }
228 + },
229 + { default: () => "Sources" }
230 + ),
231 + key: "IncidentManagement-Sources"
232 + },
233 + {
234 + label: () =>
235 + h(
236 + RouterLink,
237 + {
238 + to: {
239 + name: "IncidentManagement-Alerts"
240 + }
241 + },
242 + { default: () => "Alerts" }
243 + ),
244 + key: "IncidentManagement-Alerts"
245 + },
246 + {
247 + label: () =>
248 + h(
249 + RouterLink,
250 + {
251 + to: {
252 + name: "IncidentManagement-Cases"
253 + }
254 + },
255 + { default: () => "Cases" }
256 + ),
257 + key: "IncidentManagement-Cases"
258 + }
259 + ]
260 + },
261 {
262 label: () =>
263 h(
@@ -237,20 +286,6 @@ export default function getItems(): MenuMixedOption[] {
286 key: "Customers",
287 icon: renderIcon(CustomersIcon)
288 },
240 - {
241 - label: () =>
242 - h(
243 - RouterLink,
244 - {
245 - to: {
246 - name: "Users"
247 - }
248 - },
249 - { default: () => "Users" }
250 - ),
251 - key: "Users",
252 - icon: renderIcon(UsersIcon)
253 - },
289 {
290 label: "External Services",
291 key: "ExternalServices",
frontend/src/layouts/common/Provider.vue
+29 -19
@@ -1,6 +1,12 @@
1 <template>
2 - <n-config-provider :rtl="rtlOptions" :theme="theme" :theme-overrides="themeOverrides" preflight-style-disabled>
3 - <n-loading-bar-provider container-class="h-0.75">
2 + <n-config-provider
3 + :rtl="rtlOptions"
4 + :theme="theme"
5 + :theme-overrides="themeOverrides"
6 + preflight-style-disabled
7 + inline-theme-disabled
8 + >
9 + <n-loading-bar-provider container-class="!h-0.75">
10 <n-message-provider>
11 <n-notification-provider>
12 <n-dialog-provider>
@@ -68,27 +74,31 @@ watch(
74
75 // This function allows you to utilize the values in the style object as variables within your CSS/SCSS code like: var(-–bg-color)
76 function setGlobalVars() {
71 - const html = document.children[0] as HTMLElement
72 - const body = document.getElementsByTagName("body")?.[0]
73 - if (isRTL.value && body) {
74 - body.classList.add("direction-rtl")
75 - body.classList.remove("direction-ltr")
76 - } else {
77 - body.classList.remove("direction-rtl")
78 - body.classList.add("direction-ltr")
79 - }
80 - //html.dir = isRTL.value ? "rtl" : "ltr"
81 - const { style: htmlStyle } = html
82 - for (const key in style.value) {
83 - htmlStyle.setProperty("--" + key, style.value[key])
77 + if (document) {
78 + const html = document.children[0] as HTMLElement
79 + const body = document.getElementsByTagName("body")?.[0]
80 + if (isRTL.value && body) {
81 + body.classList.add("direction-rtl")
82 + body.classList.remove("direction-ltr")
83 + } else {
84 + body.classList.remove("direction-rtl")
85 + body.classList.add("direction-ltr")
86 + }
87 + //html.dir = isRTL.value ? "rtl" : "ltr"
88 + const { style: htmlStyle } = html
89 + for (const key in style.value) {
90 + htmlStyle.setProperty("--" + key, style.value[key])
91 + }
92 }
93 }
94
95 function setThemeName(val: ThemeName, old?: ThemeName) {
88 - const html = document.children[0] as HTMLElement
89 - if (old) {
90 - html.classList.remove(`theme-${old}`)
96 + if (document) {
97 + const html = document.children[0] as HTMLElement
98 + if (old) {
99 + html.classList.remove(`theme-${old}`)
100 + }
101 + html.classList.add(`theme-${val}`)
102 }
92 - html.classList.add(`theme-${val}`)
103 }
104 </script>
frontend/src/layouts/common/Toolbar/Avatar.vue
+6
@@ -16,6 +16,7 @@ const LicenseIcon = "carbon:license"
16 const LogoutIcon = "ion:log-out-outline"
17 const LogsIcon = "carbon:cloud-logging"
18 const ContactIcon = "ic:outline-alternate-email"
19 +const UsersIcon = "carbon:group-security"
20
21 const router = useRouter()
22 const authStore = useAuthStore()
@@ -33,6 +34,11 @@ const options = ref([
34 key: "route-License",
35 icon: renderIcon(LicenseIcon)
36 },
37 + {
38 + label: "Users",
39 + key: "route-Users",
40 + icon: renderIcon(UsersIcon)
41 + },
42 {
43 label: "Logs",
44 key: "route-Logs",
frontend/src/layouts/common/Toolbar/index.vue
+66 -59
@@ -58,8 +58,6 @@ onMounted(() => {
58 </script>
59
60 <style lang="scss" scoped>
61 -@import "@/assets/scss/functions.scss";
62 -
61 .toolbar {
62 position: sticky;
63 top: 0;
@@ -70,9 +68,8 @@ onMounted(() => {
68 padding: 0 var(--view-padding);
69 z-index: 3;
70 overflow: hidden;
73 - background: var(--bg-body);
71
75 - &::before {
72 + &::after {
73 content: "";
74 width: 100%;
75 height: 100%;
@@ -80,27 +77,14 @@ onMounted(() => {
77 display: block;
78 top: 0;
79 left: 0;
83 - z-index: -1;
80 + z-index: -2;
81 backdrop-filter: blur(20px);
85 - // mask-image: scrimGradient(#000);
82 mask-image: linear-gradient(
83 to bottom,
88 - hsl(0, 0%, 0%) 0%,
89 - hsla(0, 0%, 0%, 0.979) 0.6%,
90 - hsla(0, 0%, 0%, 0.962) 2.2%,
91 - hsla(0, 0%, 0%, 0.946) 4.9%,
92 - hsla(0, 0%, 0%, 0.929) 8.6%,
93 - hsla(0, 0%, 0%, 0.91) 13.3%,
94 - hsla(0, 0%, 0%, 0.884) 18.8%,
95 - hsla(0, 0%, 0%, 0.851) 25.1%,
96 - hsla(0, 0%, 0%, 0.806) 32.3%,
97 - hsla(0, 0%, 0%, 0.749) 40.2%,
98 - hsla(0, 0%, 0%, 0.677) 48.7%,
99 - hsla(0, 0%, 0%, 0.587) 57.9%,
100 - hsla(0, 0%, 0%, 0.476) 67.7%,
101 - hsla(0, 0%, 0%, 0.343) 78%,
102 - hsla(0, 0%, 0%, 0.185) 88.8%,
103 - hsla(0, 0%, 0%, 0) 100%
84 + rgba(0, 0, 0, 1) 0%,
85 + rgba(0, 0, 0, 0.96) 70%,
86 + rgba(0, 0, 0, 0.8) 80%,
87 + rgba(0, 0, 0, 0) 100%
88 );
89 }
90
@@ -109,9 +93,12 @@ onMounted(() => {
93 overflow: hidden;
94 width: 100%;
95 max-width: 100%;
96 + position: relative;
97 + z-index: 0;
98
99 .bubble {
100 background-color: var(--bg-sidebar);
101 + border: var(--border-small-050);
102 color: var(--fg-color);
103 border-radius: 50px;
104 padding: 6px;
@@ -143,45 +130,65 @@ onMounted(() => {
130 }
131
132 &.gradient-bg-sidebar {
146 - background-color: var(--bg-sidebar);
147 - background: linear-gradient(
148 - to bottom,
149 - rgba(var(--bg-sidebar-rgb), 1) 0%,
150 - rgba(var(--bg-sidebar-rgb), 0.945) 8.6%,
151 - rgba(var(--bg-sidebar-rgb), 0.888) 16.2%,
152 - rgba(var(--bg-sidebar-rgb), 0.83) 22.9%,
153 - rgba(var(--bg-sidebar-rgb), 0.769) 28.9%,
154 - rgba(var(--bg-sidebar-rgb), 0.707) 34.4%,
155 - rgba(var(--bg-sidebar-rgb), 0.644) 39.5%,
156 - rgba(var(--bg-sidebar-rgb), 0.578) 44.5%,
157 - rgba(var(--bg-sidebar-rgb), 0.511) 49.5%,
158 - rgba(var(--bg-sidebar-rgb), 0.443) 54.7%,
159 - rgba(var(--bg-sidebar-rgb), 0.373) 60.3%,
160 - rgba(var(--bg-sidebar-rgb), 0.301) 66.4%,
161 - rgba(var(--bg-sidebar-rgb), 0.228) 73.3%,
162 - rgba(var(--bg-sidebar-rgb), 0.153) 81%,
163 - rgba(var(--bg-sidebar-rgb), 0.077) 89.9%,
164 - rgba(var(--bg-sidebar-rgb), 0) 100%
165 - );
133 + &::before {
134 + content: "";
135 + width: 100%;
136 + height: 100%;
137 + position: absolute;
138 + display: block;
139 + top: 0;
140 + left: 0;
141 + z-index: -1;
142 + background-color: var(--bg-sidebar);
143 + background: linear-gradient(
144 + to bottom,
145 + rgba(var(--bg-sidebar-rgb), 1) 0%,
146 + rgba(var(--bg-sidebar-rgb), 0.945) 8.6%,
147 + rgba(var(--bg-sidebar-rgb), 0.888) 16.2%,
148 + rgba(var(--bg-sidebar-rgb), 0.83) 22.9%,
149 + rgba(var(--bg-sidebar-rgb), 0.769) 28.9%,
150 + rgba(var(--bg-sidebar-rgb), 0.707) 34.4%,
151 + rgba(var(--bg-sidebar-rgb), 0.644) 39.5%,
152 + rgba(var(--bg-sidebar-rgb), 0.578) 44.5%,
153 + rgba(var(--bg-sidebar-rgb), 0.511) 49.5%,
154 + rgba(var(--bg-sidebar-rgb), 0.443) 54.7%,
155 + rgba(var(--bg-sidebar-rgb), 0.373) 60.3%,
156 + rgba(var(--bg-sidebar-rgb), 0.301) 66.4%,
157 + rgba(var(--bg-sidebar-rgb), 0.228) 73.3%,
158 + rgba(var(--bg-sidebar-rgb), 0.153) 81%,
159 + rgba(var(--bg-sidebar-rgb), 0.077) 89.9%,
160 + rgba(var(--bg-sidebar-rgb), 0) 100%
161 + );
162 + }
163 }
164 &.gradient-bg-body {
168 - background-color: var(--bg-body);
169 - background: linear-gradient(
170 - to bottom,
171 - rgba(var(--bg-body-rgb), 1) 0%,
172 - rgba(var(--bg-body-rgb), 0.738) 19%,
173 - rgba(var(--bg-body-rgb), 0.541) 34%,
174 - rgba(var(--bg-body-rgb), 0.382) 47%,
175 - rgba(var(--bg-body-rgb), 0.278) 56.5%,
176 - rgba(var(--bg-body-rgb), 0.194) 65%,
177 - rgba(var(--bg-body-rgb), 0.126) 73%,
178 - rgba(var(--bg-body-rgb), 0.075) 80.2%,
179 - rgba(var(--bg-body-rgb), 0.042) 86.1%,
180 - rgba(var(--bg-body-rgb), 0.021) 91%,
181 - rgba(var(--bg-body-rgb), 0.008) 95.2%,
182 - rgba(var(--bg-body-rgb), 0.002) 98.2%,
183 - rgba(var(--bg-body-rgb), 0) 100%
184 - );
165 + &::before {
166 + content: "";
167 + width: 100%;
168 + height: 100%;
169 + position: absolute;
170 + display: block;
171 + top: 0;
172 + left: 0;
173 + z-index: -1;
174 + background-color: var(--bg-body);
175 + background: linear-gradient(
176 + to bottom,
177 + rgba(var(--bg-body-rgb), 1) 0%,
178 + rgba(var(--bg-body-rgb), 0.738) 19%,
179 + rgba(var(--bg-body-rgb), 0.541) 34%,
180 + rgba(var(--bg-body-rgb), 0.382) 47%,
181 + rgba(var(--bg-body-rgb), 0.278) 56.5%,
182 + rgba(var(--bg-body-rgb), 0.194) 65%,
183 + rgba(var(--bg-body-rgb), 0.126) 73%,
184 + rgba(var(--bg-body-rgb), 0.075) 80.2%,
185 + rgba(var(--bg-body-rgb), 0.042) 86.1%,
186 + rgba(var(--bg-body-rgb), 0.021) 91%,
187 + rgba(var(--bg-body-rgb), 0.008) 95.2%,
188 + rgba(var(--bg-body-rgb), 0.002) 98.2%,
189 + rgba(var(--bg-body-rgb), 0) 100%
190 + );
191 + }
192 }
193 }
194
frontend/src/router/index.ts
+31 -1
@@ -83,7 +83,7 @@ const router = createRouter({
83 {
84 path: "/alerts",
85 name: "Alerts",
86 - component: () => import("@/views/Alerts.vue"),
86 + component: () => import("@/views/AlertsGraylog.vue"),
87 meta: { title: "Alerts", auth: true, roles: UserRole.All }
88 },
89 {
@@ -92,6 +92,7 @@ const router = createRouter({
92 component: () => import("@/views/Artifacts.vue"),
93 meta: { title: "Artifacts", auth: true, roles: UserRole.All }
94 },
95 + /*
96 {
97 path: "/soc",
98 redirect: "/soc/alerts",
@@ -126,6 +127,35 @@ const router = createRouter({
127 }
128 ]
129 },
130 + */
131 + {
132 + path: "/incident-management",
133 + redirect: "/incident-management/alerts",
134 + meta: {
135 + auth: true,
136 + roles: UserRole.All
137 + },
138 + children: [
139 + {
140 + path: "sources",
141 + name: "IncidentManagement-Sources",
142 + component: () => import("@/views/incidentManagement/Sources.vue"),
143 + meta: { title: "Incident Sources" }
144 + },
145 + {
146 + path: "alerts",
147 + name: "IncidentManagement-Alerts",
148 + component: () => import("@/views/incidentManagement/Alerts.vue"),
149 + meta: { title: "Incident Alerts" }
150 + },
151 + {
152 + path: "cases",
153 + name: "IncidentManagement-Cases",
154 + component: () => import("@/views/incidentManagement/Cases.vue"),
155 + meta: { title: "Incident Cases" }
156 + }
157 + ]
158 + },
159 {
160 path: "/healthcheck",
161 name: "Healthcheck",
frontend/src/stores/auth.ts
+4 -14
@@ -3,11 +3,10 @@ import { UserRole, type User, type LoginPayload } from "@/types/auth.d"
3 import _castArray from "lodash/castArray"
4 import Api from "@/api"
5 import * as jose from "jose"
6 -import _toNumber from "lodash/toNumber"
6 import { scopeToRole } from "@/utils/auth"
8 -import { hashMD5 } from "@/utils"
9 -import _toSafeInteger from "lodash/toSafeInteger"
7 +import { getAvatar, getNameInitials } from "@/utils"
8 import _toLower from "lodash/toLower"
9 +import _toNumber from "lodash/toNumber"
10 import SecureLS from "secure-ls"
11 const ls = new SecureLS({ encodingType: "aes", isCompression: false })
12
@@ -108,18 +107,9 @@ export const useAuthStore = defineStore("auth", {
107 return UserRole[(state.user?.role || 0) as number]
108 },
109 userPic(): string {
111 - let text = this.userName.slice(0, 2).toUpperCase()
112 -
113 - if (this.userName.indexOf(" ") !== -1) {
114 - const chunks = this.userName.split(" ")
115 - text = (chunks[0][0] + chunks[1][0]).toUpperCase()
116 - }
117 -
118 - const hash = hashMD5(this.userName)
119 - const uniq = hash.split("").find(o => _toSafeInteger(o).toString() === o)
120 - const seed = hash.slice(0, _toSafeInteger(uniq))
110 + const initial = getNameInitials(this.userName)
111
122 - return `https://avatar.vercel.sh/${seed}.svg?text=${text}`
112 + return getAvatar({ seed: initial, text: initial, size: 64 })
113 },
114 isAdmin(): boolean {
115 return _toLower(this.userRoleName) === "admin"
frontend/src/stores/theme.ts
+10 -33
@@ -1,16 +1,7 @@
1 import { defineStore, acceptHMRUpdate } from "pinia"
2 -import {
3 - type ColorAction,
4 - type ColorKey,
5 - type ColorType,
6 - type ThemeColor,
7 - type ThemeName,
8 - Layout,
9 - RouterTransition,
10 - ThemeEnum
11 -} from "@/types/theme.d"
2 +import { type ColorType, type ThemeName, Layout, RouterTransition, ThemeEnum } from "@/types/theme.d"
3 import { type GlobalThemeOverrides, type ThemeCommonVars, darkTheme, lightTheme, useOsTheme } from "naive-ui"
13 -import { exportPrimaryShades, exposure, getTypeValue, hex2rgb, type PrimaryShade } from "@/utils/theme"
4 +import { exportPrimaryShades, getThemeColors, getTypeValue, hex2rgb, type PrimaryShade } from "@/utils/theme"
5 import _get from "lodash/get"
6 import _set from "lodash/set"
7 import _pick from "lodash/pick"
@@ -302,6 +293,10 @@ export const useThemeStore = defineStore("theme", {
293 const errorColor = naive.errorColor
294 const warningColor = naive.warningColor
295 const infoColor = naive.infoColor
296 + const successColorRGB = hex2rgb(successColor).join(", ")
297 + const errorColorRGB = hex2rgb(errorColor).join(", ")
298 + const warningColorRGB = hex2rgb(warningColor).join(", ")
299 + const infoColorRGB = hex2rgb(infoColor).join(", ")
300
301 const { success005, warning005, error005, info005 } = state.colors[state.themeName]
302
@@ -432,6 +427,10 @@ export const useThemeStore = defineStore("theme", {
427 "error-005-color": `${error005}`,
428 "warning-005-color": `${warning005}`,
429 "info-005-color": `${info005}`,
430 + "success-color-rgb": `${successColorRGB}`,
431 + "error-color-rgb": `${errorColorRGB}`,
432 + "warning-color-rgb": `${warningColorRGB}`,
433 + "info-color-rgb": `${infoColorRGB}`,
434
435 "secondary1-color": `${secondary1}`,
436 "secondary1-color-rgb": `${secondary1RGB}`,
@@ -487,25 +486,3 @@ export const useThemeStore = defineStore("theme", {
486 if (import.meta.hot) {
487 import.meta.hot.accept(acceptHMRUpdate(useThemeStore, import.meta.hot))
488 }
490 -
491 -function getThemeColors(colors: Record<ColorType, string>) {
492 - const colorActions: ColorAction[] = [
493 - { scene: "", handler: color => color },
494 - { scene: "Suppl", handler: color => exposure(color, 0.1) },
495 - { scene: "Hover", handler: color => exposure(color, 0.05) },
496 - { scene: "Pressed", handler: color => exposure(color, -0.2) }
497 - ]
498 -
499 - const themeColor: ThemeColor = {}
500 -
501 - for (const colorType in colors) {
502 - const colorValue = colors[colorType as ColorType]
503 -
504 - colorActions.forEach(action => {
505 - const colorKey: ColorKey = `${colorType as ColorType}Color${action.scene}`
506 - themeColor[colorKey] = action.handler(colorValue)
507 - })
508 - }
509 -
510 - return themeColor
511 -}
frontend/src/types/alerts.d.ts
+1
@@ -44,6 +44,7 @@ export interface AlertSourceContent {
44 agent_labels_customer: string
45 agent_name: string
46 alert_url?: string
47 + alert_id?: number
48 ask_socfortress_message?: string
49 data_action?: string
50 data_authors?: string
frontend/src/types/flask.d.ts
+2
@@ -1,4 +1,6 @@
1 export interface FlaskBaseResponse {
2 + /** common response property */
3 message: string
4 + /** common response property */
5 success: boolean
6 }
frontend/src/types/graylog/indices.d.ts new
+50
@@ -0,0 +1,50 @@
1 +export interface GraylogIndex {
2 + index_name: string
3 + index_info: GraylogIndexInfo
4 +}
5 +
6 +export interface GraylogIndexInfo {
7 + all_shards: GraylogIndexShards
8 + primary_shards: GraylogIndexShards
9 + reopened: boolean
10 + routing: GraylogIndexRouting[]
11 +}
12 +
13 +export interface GraylogIndexShards {
14 + documents: GraylogIndexEntitiesCount
15 + flush: GraylogIndexTiming
16 + get: GraylogIndexTiming
17 + index: GraylogIndexTiming
18 + merge: GraylogIndexTiming
19 + open_search_contexts: number
20 + refresh: GraylogIndexTiming
21 + search_fetch: GraylogIndexTiming
22 + search_query: GraylogIndexTiming
23 + segments: number
24 + store_size_bytes: number
25 +}
26 +
27 +export interface GraylogIndexEntitiesCount {
28 + count: number
29 + deleted: number
30 +}
31 +
32 +export interface GraylogIndexTiming {
33 + time_seconds: number
34 + total: number
35 +}
36 +
37 +export interface GraylogIndexRouting {
38 + active: boolean
39 + id: number
40 + node_hostname: string
41 + node_id: string
42 + node_name: string
43 + primary: boolean
44 + relocating_to?: string
45 + state: GraylogIndexRoutingState
46 +}
47 +
48 +export enum GraylogIndexRoutingState {
49 + Started = "started"
50 +}
frontend/src/types/graylog/messages.d.ts renamed
-5
@@ -8,8 +8,3 @@ export interface Message {
8 export interface MessageExtended extends Message {
9 id?: string
10 }
11 -
12 -export interface ThroughputMetric {
13 - metric: string
14 - value: number
15 -}
frontend/src/types/graylog/metrics.d.ts new
+4
@@ -0,0 +1,4 @@
1 +export interface ThroughputMetric {
2 + metric: string
3 + value: number
4 +}
frontend/src/types/incidentManagement/alerts.d.ts new
+80
@@ -0,0 +1,80 @@
1 +import type { Case } from "./cases.d"
2 +
3 +export interface Alert {
4 + id: number
5 + alert_creation_time: Date
6 + alert_description: string
7 + alert_name: string
8 + assigned_to: null | string
9 + customer_code: string
10 + source: string
11 + status: AlertStatus
12 + time_closed: Date | null
13 + comments: AlertComment[]
14 + assets: AlertAsset[]
15 + tags: AlertTag[]
16 + linked_cases: Omit<Case, "alerts">[]
17 +}
18 +
19 +export type AlertStatus = "OPEN" | "CLOSED" | "IN_PROGRESS"
20 +
21 +export interface AlertAsset {
22 + id: number
23 + agent_id: string
24 + alert_context_id: number
25 + alert_linked: number
26 + asset_name: string
27 + customer_code: string
28 + index_id: string
29 + index_name: string
30 + velociraptor_id: string
31 +}
32 +
33 +export interface AlertComment {
34 + id: number
35 + alert_id: number
36 + comment: string
37 + created_at: Date
38 + user_name: string
39 +}
40 +
41 +export interface AlertTag {
42 + id: number
43 + tag: string
44 +}
45 +
46 +export interface AlertContext {
47 + id: number
48 + source: string
49 + context: AlertContextDetails
50 +}
51 +
52 +export interface AlertContextDetails {
53 + [key: string]: string | string[] | number | object
54 + process_name: string[]
55 +}
56 +
57 +export interface AlertDetails {
58 + asset_type_id: null | number
59 + ioc_value: null | string
60 + ioc_type: null | string
61 + time_field: string
62 + syslog_type: string
63 + _index: string
64 + _version: number
65 + _id: string
66 + _source: {
67 + [key: string]: string | string[] | number | object
68 + }
69 +}
70 +
71 +export interface AlertTimeline {
72 + _index: string
73 + _id: string
74 + _score: number
75 + _source: {
76 + rule_description: string
77 + timestamp: string
78 + [key: string]: string | string[] | number | object
79 + }
80 +}
frontend/src/types/incidentManagement/cases.d.ts new
+11
@@ -0,0 +1,11 @@
1 +import type { Alert, AlertStatus } from "./alerts.d"
2 +
3 +export interface Case {
4 + id: number
5 + case_name: string
6 + case_creation_time: string | Date
7 + case_description: string
8 + assigned_to: null | string
9 + alerts: Alert[]
10 + case_status: null | AlertStatus
11 +}
frontend/src/types/incidentManagement/notifications.d.ts new
+12
@@ -0,0 +1,12 @@
1 +export interface IncidentNotificationPayload {
2 + customer_code: string
3 + shuffle_workflow_id: string
4 + enabled: boolean
5 +}
6 +
7 +export interface IncidentNotification {
8 + customer_code: string
9 + enabled: boolean
10 + id: number
11 + shuffle_workflow_id: string
12 +}
frontend/src/types/incidentManagement/sources.d.ts new
+17
@@ -0,0 +1,17 @@
1 +export type SourceName = string
2 +
3 +export interface SourceConfiguration {
4 + field_names: string[]
5 + asset_name: string
6 + timefield_name: string
7 + alert_title_name: string
8 + source: string
9 +}
10 +
11 +export interface SourceConfigurationModel extends SourceConfiguration {
12 + index_name?: string | null
13 + asset_name: string | null
14 + timefield_name: string | null
15 + alert_title_name: string | null
16 + source: string | null
17 +}
frontend/src/types/logs.d.ts
+13
@@ -1,3 +1,5 @@
1 +import type { SafeAny } from "@/types/common.d"
2 +
3 export interface Log {
4 event_type: LogEventType
5 user_id: number | null
@@ -19,3 +21,14 @@ export enum LogMethod {
21 Options = "OPTIONS",
22 Post = "POST"
23 }
24 +
25 +export type LogsQueryTimeRange = `${number}${"h" | "d" | "w"}`
26 +export type LogsQueryEventType = `${LogEventType}`
27 +export type LogsQuery = { userId: string } | { timeRange: LogsQueryTimeRange } | { eventType: LogsQueryEventType }
28 +
29 +// Extraction of keys from the union type LogsQuery
30 +type KeysOfLogsQuery<T> = T extends { [K in keyof T]: SafeAny } ? keyof T : never
31 +
32 +// Union of values extracted from keys
33 +export type LogsQueryTypes = KeysOfLogsQuery<LogsQuery>
34 +export type LogsQueryValues = string | LogsQueryTimeRange | LogsQueryEventType
frontend/src/types/monitoringAlerts.d.ts
+6
@@ -10,3 +10,9 @@ export interface MonitoringAlert {
10 customer_code: string
11 alert_source: string
12 }
13 +
14 +export enum CustomProvisionPriority {
15 + "LOW" = 1,
16 + "MEDIUM" = 2,
17 + "HIGH" = 3
18 +}
frontend/src/types/soc/alert.d.ts
+1 -1
@@ -1,4 +1,4 @@
1 -import type { AlertSourceContent } from "../alerts"
1 +import type { AlertSourceContent } from "../alerts.d"
2
3 export interface SocAlert {
4 alert_classification_id: string | null
frontend/src/utils/index.ts
+20 -1
@@ -3,7 +3,7 @@ import { type Component, h } from "vue"
3 import { isMobile as detectMobile } from "detect-touch-device"
4 import { md5 } from "js-md5"
5 import dayjs from "@/utils/dayjs"
6 -import type { OsTypesFull } from "@/types/common"
6 +import type { OsTypesFull } from "@/types/common.d"
7 import _trim from "lodash/trim"
8
9 // Transform File Instance in base64 string
@@ -114,3 +114,22 @@ export function price(
114 export function getBaseUrl() {
115 return _trim(import.meta.env.VITE_API_URL, "/")
116 }
117 +
118 +export function getNameInitials(name: string, cap?: number) {
119 + let initials = name.slice(0, 2)
120 +
121 + if (name.indexOf(" ") !== -1) {
122 + initials = name
123 + .split(" ")
124 + .map(chunk => chunk[0])
125 + .join()
126 + }
127 +
128 + return (cap ? initials.slice(0, cap) : initials).toUpperCase()
129 +}
130 +
131 +export function getAvatar(params: { seed: string; text?: string; size?: number; format?: "png" | "svg" }) {
132 + const format: "png" | "svg" = params.text ? "svg" : params.format || "svg"
133 +
134 + return `https://avatar.vercel.sh/${params.seed}.${format}?text=${params.text || ""}&size=${params.size || 32}`
135 +}
frontend/src/utils/theme.ts
+23
@@ -1,5 +1,6 @@
1 import { colord } from "colord"
2 import _get from "lodash/get"
3 +import { type ColorAction, type ColorKey, type ColorType, type ThemeColor } from "@/types/theme.d"
4
5 export type PrimaryShade = "005" | "010" | "015" | "020" | "030" | "040" | "050" | "060"
6
@@ -60,3 +61,25 @@ export function getTypeValue(origin: object, val: string) {
61
62 return val
63 }
64 +
65 +export function getThemeColors(colors: Record<ColorType, string>) {
66 + const colorActions: ColorAction[] = [
67 + { scene: "", handler: color => color },
68 + { scene: "Suppl", handler: color => exposure(color, 0.1) },
69 + { scene: "Hover", handler: color => exposure(color, 0.05) },
70 + { scene: "Pressed", handler: color => exposure(color, -0.2) }
71 + ]
72 +
73 + const themeColor: ThemeColor = {}
74 +
75 + for (const colorType in colors) {
76 + const colorValue = colors[colorType as ColorType]
77 +
78 + colorActions.forEach(action => {
79 + const colorKey: ColorKey = `${colorType as ColorType}Color${action.scene}`
80 + themeColor[colorKey] = action.handler(colorValue)
81 + })
82 + }
83 +
84 + return themeColor
85 +}
frontend/src/views/AlertsGraylog.vue new
+11
@@ -0,0 +1,11 @@
1 +<template>
2 + <div class="page">
3 + <AlertsGraylogList />
4 + </div>
5 +</template>
6 +
7 +<script setup>
8 +import AlertsGraylogList from "@/components/alerts/AlertsGraylogList.vue"
9 +</script>
10 +
11 +<style lang="scss" scoped></style>
frontend/src/views/AlertsOld.vue renamed
frontend/src/views/Artifacts.vue
+6 -5
@@ -33,15 +33,16 @@
33 </template>
34
35 <script setup lang="ts">
36 -import { ref } from "vue"
36 +import { defineAsyncComponent, ref } from "vue"
37 import { NTabs, NTabPane } from "naive-ui"
38 -import ArtifactsList from "@/components/artifacts/ArtifactsList.vue"
39 -import ArtifactsCollect from "@/components/artifacts/ArtifactsCollect.vue"
40 -import ArtifactsCommand from "@/components/artifacts/ArtifactsCommand.vue"
41 -import ArtifactsQuarantine from "@/components/artifacts/ArtifactsQuarantine.vue"
38 import type { Artifact } from "@/types/artifacts.d"
39 import type { Agent } from "@/types/agents.d"
40
41 +const ArtifactsList = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsList.vue"))
42 +const ArtifactsCollect = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCollect.vue"))
43 +const ArtifactsCommand = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCommand.vue"))
44 +const ArtifactsQuarantine = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsQuarantine.vue"))
45 +
46 const artifacts = ref<Artifact[]>([])
47 const agents = ref<Agent[]>([])
48
frontend/src/views/Overview.vue
+2 -2
@@ -75,12 +75,12 @@
75 </template>
76
77 <script setup lang="ts">
78 -import { ref } from "vue"
78 +import { defineAsyncComponent, ref } from "vue"
79 import { NButton, NDrawer, NDrawerContent } from "naive-ui"
80 import ClusterHealth from "@/components/indices/ClusterHealth.vue"
81 import NodeAllocation from "@/components/indices/NodeAllocation.vue"
82 import IndicesMarquee from "@/components/indices/Marquee.vue"
83 -import ThreatIntelButton from "@/components/alerts/ThreatIntelButton.vue"
83 +const ThreatIntelButton = defineAsyncComponent(() => import("@/components/threatIntel/ThreatIntelButton.vue"))
84 import ActiveResponseWizardButton from "@/components/activeResponse/ActiveResponseWizardButton.vue"
85 import StackProvisioningButton from "@/components/stackProvisioning/StackProvisioningButton.vue"
86 import CloudSecurityAssessmentButton from "@/components/cloudSecurityAssessment/CloudSecurityAssessmentButton.vue"
frontend/src/views/ReportCreation.vue
+1 -1
@@ -26,7 +26,7 @@ import { NAlert } from "naive-ui"
26 import ReportWizard from "@/components/reportCreation/Wizard.vue"
27 import ReportPanels from "@/components/reportCreation/Panels.vue"
28 import type { Dashboard, Org, Panel } from "@/types/reporting.d"
29 -import type { ReportTimeRange } from "@/api/reporting"
29 +import type { ReportTimeRange } from "@/api/endpoints/reporting"
30 import Icon from "@/components/common/Icon.vue"
31 import LicenseFeatureOverlay from "@/components/license/LicenseFeatureOverlay.vue"
32
frontend/src/views/agents/Overview.vue
+16 -13
@@ -123,25 +123,28 @@
123 </template>
124
125 <script setup lang="ts">
126 -import { ref, onBeforeMount, computed, nextTick } from "vue"
126 +import { ref, onBeforeMount, computed, nextTick, defineAsyncComponent } from "vue"
127 import { useMessage, NSpin, NTooltip, NButton, NTabs, NTabPane, NCard, useDialog } from "naive-ui"
128 import { useRoute, useRouter } from "vue-router"
129 import Api from "@/api"
130 -import { AgentStatus, type Agent } from "@/types/agents.d"
130 import { handleDeleteAgent, toggleAgentCritical } from "@/components/agents/utils"
132 -import VulnerabilitiesGrid from "@/components/agents/vulnerabilities/VulnerabilitiesGrid.vue"
133 -import ScaTable from "@/components/agents/sca/ScaTable.vue"
134 -import AlertsList from "@/components/alerts/AlertsList.vue"
135 -import OverviewSection from "@/components/agents/OverviewSection.vue"
136 -import AgentCases from "@/components/agents/AgentCases.vue"
137 -import AgentFlowList from "@/components/agents/agentFlow/AgentFlowList.vue"
131 import Icon from "@/components/common/Icon.vue"
139 -import type { Artifact } from "@/types/artifacts.d"
140 -import ArtifactsCollect from "@/components/artifacts/ArtifactsCollect.vue"
141 -import ArtifactsCommand from "@/components/artifacts/ArtifactsCommand.vue"
142 -import ArtifactsQuarantine from "@/components/artifacts/ArtifactsQuarantine.vue"
143 -import ActiveResponseAgent from "@/components/activeResponse/ActiveResponseAgent.vue"
132 import { useGoto } from "@/composables/useGoto"
133 +import type { Artifact } from "@/types/artifacts.d"
134 +import { AgentStatus, type Agent } from "@/types/agents.d"
135 +
136 +const VulnerabilitiesGrid = defineAsyncComponent(
137 + () => import("@/components/agents/vulnerabilities/VulnerabilitiesGrid.vue")
138 +)
139 +const ScaTable = defineAsyncComponent(() => import("@/components/agents/sca/ScaTable.vue"))
140 +const AlertsList = defineAsyncComponent(() => import("@/components/alerts/AlertsList.vue"))
141 +const OverviewSection = defineAsyncComponent(() => import("@/components/agents/OverviewSection.vue"))
142 +const AgentCases = defineAsyncComponent(() => import("@/components/agents/AgentCases.vue"))
143 +const AgentFlowList = defineAsyncComponent(() => import("@/components/agents/agentFlow/AgentFlowList.vue"))
144 +const ArtifactsCollect = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCollect.vue"))
145 +const ArtifactsCommand = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsCommand.vue"))
146 +const ArtifactsQuarantine = defineAsyncComponent(() => import("@/components/artifacts/ArtifactsQuarantine.vue"))
147 +const ActiveResponseAgent = defineAsyncComponent(() => import("@/components/activeResponse/ActiveResponseAgent.vue"))
148
149 const StarIcon = "carbon:star"
150 const QuarantinedIcon = "ph:seal-warning-light"
frontend/src/views/graylog/Management.vue
+9 -9
@@ -36,17 +36,17 @@
36 </template>
37
38 <script setup lang="ts">
39 -import { onBeforeMount, ref } from "vue"
39 +import { defineAsyncComponent, onBeforeMount, ref, watch } from "vue"
40 import { NTabs, NTabPane, NButton, NDrawer, NDrawerContent } from "naive-ui"
41 -import Messages from "@/components/graylog/Messages/List.vue"
42 -import Alerts from "@/components/graylog/Alerts/List.vue"
43 -import Events from "@/components/graylog/Events/List.vue"
44 -import Streams from "@/components/graylog/Streams/List.vue"
45 -import MonitoringAlerts from "@/components/graylog/MonitoringAlerts/List.vue"
46 -import Inputs from "@/components/graylog/Inputs/List.vue"
47 -import type { EventDefinition } from "@/types/graylog/event-definition.d"
41 import { useRoute, useRouter } from "vue-router"
49 -import { watch } from "vue"
42 +import type { EventDefinition } from "@/types/graylog/event-definition.d"
43 +
44 +const Messages = defineAsyncComponent(() => import("@/components/graylog/Messages/List.vue"))
45 +const Alerts = defineAsyncComponent(() => import("@/components/graylog/Alerts/List.vue"))
46 +const Events = defineAsyncComponent(() => import("@/components/graylog/Events/List.vue"))
47 +const Streams = defineAsyncComponent(() => import("@/components/graylog/Streams/List.vue"))
48 +const MonitoringAlerts = defineAsyncComponent(() => import("@/components/graylog/MonitoringAlerts/List.vue"))
49 +const Inputs = defineAsyncComponent(() => import("@/components/graylog/Inputs/List.vue"))
50
51 const tabsList = ["messages", "alerts", "events", "streams", "provisioning"]
52 const drawersList = ["inputs"]
frontend/src/views/graylog/Metrics.vue
+1 -1
@@ -36,7 +36,7 @@
36 import { ref, onBeforeMount, computed, watch, nextTick, onBeforeUnmount } from "vue"
37 import { useMessage, NButton, NSelect } from "naive-ui"
38 import Api from "@/api"
39 -import type { ThroughputMetric } from "@/types/graylog/index.d"
39 +import type { ThroughputMetric } from "@/types/graylog/metrics.d"
40 import Icon from "@/components/common/Icon.vue"
41 import UncommittedEntries from "@/components/graylog/Metrics/UncommittedEntries.vue"
42 import MetricsList from "@/components/graylog/Metrics/List.vue"
frontend/src/views/incidentManagement/Alerts.vue new
+21
@@ -0,0 +1,21 @@
1 +<template>
2 + <div class="page">
3 + <AlertsList :highlight />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import AlertsList from "@/components/incidentManagement/alerts/AlertsList.vue"
9 +import { onBeforeMount, ref } from "vue"
10 +import { useRoute } from "vue-router"
11 +
12 +const route = useRoute()
13 +
14 +const highlight = ref<string | undefined>(undefined)
15 +
16 +onBeforeMount(() => {
17 + if (route.query?.alert_id) {
18 + highlight.value = route.query.alert_id.toString() || undefined
19 + }
20 +})
21 +</script>
frontend/src/views/incidentManagement/Cases.vue new
+21
@@ -0,0 +1,21 @@
1 +<template>
2 + <div class="page">
3 + <CasesList :highlight />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import CasesList from "@/components/incidentManagement/cases/CasesList.vue"
9 +import { onBeforeMount, ref } from "vue"
10 +import { useRoute } from "vue-router"
11 +
12 +const route = useRoute()
13 +
14 +const highlight = ref<string | undefined>(undefined)
15 +
16 +onBeforeMount(() => {
17 + if (route.query?.case_id) {
18 + highlight.value = route.query.case_id.toString() || undefined
19 + }
20 +})
21 +</script>
frontend/src/views/incidentManagement/Sources.vue new
+9
@@ -0,0 +1,9 @@
1 +<template>
2 + <div class="page">
3 + <ConfiguredSourcesList />
4 + </div>
5 +</template>
6 +
7 +<script setup lang="ts">
8 +import ConfiguredSourcesList from "@/components/incidentManagement/sources/ConfiguredSourcesList.vue"
9 +</script>
frontend/src/views/license/License.vue
+1 -1
@@ -10,7 +10,7 @@
10
11 <script setup lang="ts">
12 import LicenseViewer from "@/components/license/LicenseViewer.vue"
13 -import type { LicenseKey } from "@/types/license"
13 +import type { LicenseKey } from "@/types/license.d"
14 import { ref } from "vue"
15
16 const licenseKey = ref<LicenseKey | undefined>(undefined)
frontend/src/views/soc/Users.vue
+1 -1
@@ -1,6 +1,6 @@
1 <template>
2 <div class="page">
3 - <SocUsersList :highlight="highlight" />
3 + <SocUsersList :highlight />
4 </div>
5 </template>
6
frontend/tailwind.config.js
+4 -2
@@ -35,7 +35,8 @@ export default {
35 "50vw": "50vw"
36 },
37 maxHeight: {
38 - "50vh": "50vh"
38 + "50vh": "50vh",
39 + 150: "37.5rem"
40 },
41 maxWidth: {
42 "90vw": "90vw"
@@ -55,7 +56,8 @@ export default {
56 "gap-5",
57 "w-72",
58 "max-h-50vh",
58 - "max-w-90vw"
59 + "max-w-90vw",
60 + "!h-0.75"
61 ],
62 plugins: [
63 plugin(function ({ addBase, theme }) {
frontend/ui.excalidraw deleted
-605
@@ -1,605 +0,0 @@
1 -{
2 - "type": "excalidraw",
3 - "version": 2,
4 - "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
5 - "elements": [
6 - {
7 - "type": "rectangle",
8 - "version": 159,
9 - "versionNonce": 524733706,
10 - "isDeleted": false,
11 - "id": "WCXubkJqwY1EBjWdoZ4UF",
12 - "fillStyle": "solid",
13 - "strokeWidth": 2,
14 - "strokeStyle": "solid",
15 - "roughness": 1,
16 - "opacity": 100,
17 - "angle": 0,
18 - "x": 14.140625,
19 - "y": -1095.7784090909095,
20 - "strokeColor": "#1e1e1e",
21 - "backgroundColor": "transparent",
22 - "width": 1035.8203124999998,
23 - "height": 1347.2109374999995,
24 - "seed": 820134410,
25 - "groupIds": [],
26 - "frameId": null,
27 - "roundness": {
28 - "type": 3
29 - },
30 - "boundElements": [],
31 - "updated": 1712331056177,
32 - "link": null,
33 - "locked": false
34 - },
35 - {
36 - "type": "text",
37 - "version": 164,
38 - "versionNonce": 1431519933,
39 - "isDeleted": false,
40 - "id": "f5HHS4R2eos3pcfewyCRm",
41 - "fillStyle": "solid",
42 - "strokeWidth": 2,
43 - "strokeStyle": "solid",
44 - "roughness": 1,
45 - "opacity": 100,
46 - "angle": 0,
47 - "x": 198.7578125,
48 - "y": -1002.1518465909094,
49 - "strokeColor": "#1e1e1e",
50 - "backgroundColor": "transparent",
51 - "width": 569.53125,
52 - "height": 43.199999999999996,
53 - "seed": 203518154,
54 - "groupIds": [],
55 - "frameId": null,
56 - "roundness": null,
57 - "boundElements": [],
58 - "updated": 1712333788867,
59 - "link": null,
60 - "locked": false,
61 - "fontSize": 36,
62 - "fontFamily": 3,
63 - "text": "Add featurs to unlock power",
64 - "textAlign": "left",
65 - "verticalAlign": "top",
66 - "containerId": null,
67 - "originalText": "Add featurs to unlock power",
68 - "lineHeight": 1.2,
69 - "baseline": 35
70 - },
71 - {
72 - "type": "rectangle",
73 - "version": 135,
74 - "versionNonce": 1941740490,
75 - "isDeleted": false,
76 - "id": "kf0f-2Om75MkypwXS174o",
77 - "fillStyle": "hachure",
78 - "strokeWidth": 2,
79 - "strokeStyle": "solid",
80 - "roughness": 1,
81 - "opacity": 100,
82 - "angle": 0,
83 - "x": 117.203125,
84 - "y": -823.0596590909093,
85 - "strokeColor": "#1e1e1e",
86 - "backgroundColor": "#a5d8ff",
87 - "width": 826.4531249999998,
88 - "height": 984.9062499999997,
89 - "seed": 2097989514,
90 - "groupIds": [],
91 - "frameId": null,
92 - "roundness": {
93 - "type": 3
94 - },
95 - "boundElements": [],
96 - "updated": 1712331056177,
97 - "link": null,
98 - "locked": false
99 - },
100 - {
101 - "type": "rectangle",
102 - "version": 295,
103 - "versionNonce": 1032137482,
104 - "isDeleted": false,
105 - "id": "7SJrl4Dw-SUauJXSiJF14",
106 - "fillStyle": "hachure",
107 - "strokeWidth": 2,
108 - "strokeStyle": "solid",
109 - "roughness": 1,
110 - "opacity": 100,
111 - "angle": 0,
112 - "x": -1108.9086326979468,
113 - "y": -1294.9356671554262,
114 - "strokeColor": "#e03131",
115 - "backgroundColor": "transparent",
116 - "width": 3217.2379032258073,
117 - "height": 1870.816532258065,
118 - "seed": 1272923722,
119 - "groupIds": [],
120 - "frameId": null,
121 - "roundness": null,
122 - "boundElements": [],
123 - "updated": 1712331158762,
124 - "link": null,
125 - "locked": false
126 - },
127 - {
128 - "type": "rectangle",
129 - "version": 338,
130 - "versionNonce": 1807733974,
131 - "isDeleted": false,
132 - "id": "Q_YlaJ-8C95ZGoHhRWjC1",
133 - "fillStyle": "solid",
134 - "strokeWidth": 2,
135 - "strokeStyle": "solid",
136 - "roughness": 1,
137 - "opacity": 100,
138 - "angle": 0,
139 - "x": -888.4846793831173,
140 - "y": 1179.2799310064945,
141 - "strokeColor": "#1e1e1e",
142 - "backgroundColor": "transparent",
143 - "width": 932.2488839285713,
144 - "height": 1347.2109374999995,
145 - "seed": 1480733898,
146 - "groupIds": [],
147 - "frameId": null,
148 - "roundness": {
149 - "type": 3
150 - },
151 - "boundElements": [],
152 - "updated": 1712331118544,
153 - "link": null,
154 - "locked": false
155 - },
156 - {
157 - "type": "text",
158 - "version": 284,
159 - "versionNonce": 1317344147,
160 - "isDeleted": false,
161 - "id": "t2JR-U6P-TEZ1kFlzMKlt",
162 - "fillStyle": "solid",
163 - "strokeWidth": 2,
164 - "strokeStyle": "solid",
165 - "roughness": 1,
166 - "opacity": 100,
167 - "angle": 0,
168 - "x": -809.1827820616888,
169 - "y": 1255.244663149352,
170 - "strokeColor": "#1e1e1e",
171 - "backgroundColor": "transparent",
172 - "width": 274.21875,
173 - "height": 43.199999999999996,
174 - "seed": 1170647690,
175 - "groupIds": [],
176 - "frameId": null,
177 - "roundness": null,
178 - "boundElements": [],
179 - "updated": 1712333788867,
180 - "link": null,
181 - "locked": false,
182 - "fontSize": 36,
183 - "fontFamily": 3,
184 - "text": "Your features",
185 - "textAlign": "left",
186 - "verticalAlign": "top",
187 - "containerId": null,
188 - "originalText": "Your features",
189 - "lineHeight": 1.2,
190 - "baseline": 35
191 - },
192 - {
193 - "type": "rectangle",
194 - "version": 412,
195 - "versionNonce": 1144292694,
196 - "isDeleted": false,
197 - "id": "GVKje1B7wQDilUq2QjfT8",
198 - "fillStyle": "hachure",
199 - "strokeWidth": 2,
200 - "strokeStyle": "solid",
201 - "roughness": 1,
202 - "opacity": 100,
203 - "angle": 0,
204 - "x": -795.2715097402604,
205 - "y": 1366.0611810064945,
206 - "strokeColor": "#1e1e1e",
207 - "backgroundColor": "#a5d8ff",
208 - "width": 760.6746651785709,
209 - "height": 220.98325892857127,
210 - "seed": 1521204298,
211 - "groupIds": [],
212 - "frameId": null,
213 - "roundness": {
214 - "type": 3
215 - },
216 - "boundElements": [],
217 - "updated": 1712331057596,
218 - "link": null,
219 - "locked": false
220 - },
221 - {
222 - "type": "rectangle",
223 - "version": 346,
224 - "versionNonce": 1602330582,
225 - "isDeleted": false,
226 - "id": "GjXsh91Zb0JnjA2513ETO",
227 - "fillStyle": "hachure",
228 - "strokeWidth": 2,
229 - "strokeStyle": "solid",
230 - "roughness": 1,
231 - "opacity": 100,
232 - "angle": 0,
233 - "x": -1043.0350531524928,
234 - "y": 913.4095032991206,
235 - "strokeColor": "#e03131",
236 - "backgroundColor": "transparent",
237 - "width": 3217.2379032258073,
238 - "height": 1870.816532258065,
239 - "seed": 1575818058,
240 - "groupIds": [],
241 - "frameId": null,
242 - "roundness": null,
243 - "boundElements": [],
244 - "updated": 1712331160262,
245 - "link": null,
246 - "locked": false
247 - },
248 - {
249 - "type": "rectangle",
250 - "version": 458,
251 - "versionNonce": 1032729622,
252 - "isDeleted": false,
253 - "id": "tUHThzmjGIwgBIaCq8GPQ",
254 - "fillStyle": "hachure",
255 - "strokeWidth": 2,
256 - "strokeStyle": "solid",
257 - "roughness": 1,
258 - "opacity": 100,
259 - "angle": 0,
260 - "x": -791.1141436688317,
261 - "y": 1635.4668729707803,
262 - "strokeColor": "#1e1e1e",
263 - "backgroundColor": "#a5d8ff",
264 - "width": 760.6746651785709,
265 - "height": 220.98325892857127,
266 - "seed": 273495882,
267 - "groupIds": [],
268 - "frameId": null,
269 - "roundness": {
270 - "type": 3
271 - },
272 - "boundElements": [],
273 - "updated": 1712331058945,
274 - "link": null,
275 - "locked": false
276 - },
277 - {
278 - "type": "line",
279 - "version": 49,
280 - "versionNonce": 1737468682,
281 - "isDeleted": false,
282 - "id": "TqLcU40EPWKV32-qNn9Mv",
283 - "fillStyle": "hachure",
284 - "strokeWidth": 2,
285 - "strokeStyle": "solid",
286 - "roughness": 1,
287 - "opacity": 100,
288 - "angle": 0,
289 - "x": -884.2287259111847,
290 - "y": 2304.458512253878,
291 - "strokeColor": "#1e1e1e",
292 - "backgroundColor": "transparent",
293 - "width": 935.5887276785713,
294 - "height": 0,
295 - "seed": 169125526,
296 - "groupIds": [],
297 - "frameId": null,
298 - "roundness": null,
299 - "boundElements": [],
300 - "updated": 1712331071945,
301 - "link": null,
302 - "locked": false,
303 - "startBinding": null,
304 - "endBinding": null,
305 - "lastCommittedPoint": null,
306 - "startArrowhead": null,
307 - "endArrowhead": null,
308 - "points": [
309 - [
310 - 0,
311 - 0
312 - ],
313 - [
314 - 935.5887276785713,
315 - 0
316 - ]
317 - ]
318 - },
319 - {
320 - "type": "rectangle",
321 - "version": 50,
322 - "versionNonce": 617744586,
323 - "isDeleted": false,
324 - "id": "oiA0_cm-MxJTTxEGVA39b",
325 - "fillStyle": "hachure",
326 - "strokeWidth": 2,
327 - "strokeStyle": "solid",
328 - "roughness": 1,
329 - "opacity": 100,
330 - "angle": 0,
331 - "x": -824.8537259111847,
332 - "y": 2354.012083682449,
333 - "strokeColor": "#1e1e1e",
334 - "backgroundColor": "#ffec99",
335 - "width": 805.9709821428571,
336 - "height": 132.17075892857156,
337 - "seed": 1839772950,
338 - "groupIds": [],
339 - "frameId": null,
340 - "roundness": {
341 - "type": 3
342 - },
343 - "boundElements": [
344 - {
345 - "type": "text",
346 - "id": "Q0FSEEZIspJb-fxFIRNvZ"
347 - }
348 - ],
349 - "updated": 1712331080935,
350 - "link": null,
351 - "locked": false
352 - },
353 - {
354 - "type": "text",
355 - "version": 13,
356 - "versionNonce": 845381597,
357 - "isDeleted": false,
358 - "id": "Q0FSEEZIspJb-fxFIRNvZ",
359 - "fillStyle": "hachure",
360 - "strokeWidth": 2,
361 - "strokeStyle": "solid",
362 - "roughness": 1,
363 - "opacity": 100,
364 - "angle": 0,
365 - "x": -537.8838598397562,
366 - "y": 2398.497463146735,
367 - "strokeColor": "#1e1e1e",
368 - "backgroundColor": "#ffec99",
369 - "width": 232.03125,
370 - "height": 43.199999999999996,
371 - "seed": 1369711510,
372 - "groupIds": [],
373 - "frameId": null,
374 - "roundness": null,
375 - "boundElements": [],
376 - "updated": 1712333793473,
377 - "link": null,
378 - "locked": false,
379 - "fontSize": 36,
380 - "fontFamily": 3,
381 - "text": "add feature",
382 - "textAlign": "center",
383 - "verticalAlign": "middle",
384 - "containerId": "oiA0_cm-MxJTTxEGVA39b",
385 - "originalText": "add feature",
386 - "lineHeight": 1.2,
387 - "baseline": 35
388 - },
389 - {
390 - "type": "rectangle",
391 - "version": 449,
392 - "versionNonce": 2045072918,
393 - "isDeleted": false,
394 - "id": "dpsEDP9fMx3RQ_Zw20GLK",
395 - "fillStyle": "solid",
396 - "strokeWidth": 2,
397 - "strokeStyle": "solid",
398 - "roughness": 1,
399 - "opacity": 100,
400 - "angle": 0,
401 - "x": 95.3183340097396,
402 - "y": 1174.3831676136374,
403 - "strokeColor": "#1e1e1e",
404 - "backgroundColor": "transparent",
405 - "width": 1866.0100446428573,
406 - "height": 1347.2109374999995,
407 - "seed": 487683734,
408 - "groupIds": [],
409 - "frameId": null,
410 - "roundness": {
411 - "type": 3
412 - },
413 - "boundElements": [],
414 - "updated": 1712331123430,
415 - "link": null,
416 - "locked": false
417 - },
418 - {
419 - "type": "text",
420 - "version": 350,
421 - "versionNonce": 381018909,
422 - "isDeleted": false,
423 - "id": "WZpG35_7Xp53DBnXfgerm",
424 - "fillStyle": "solid",
425 - "strokeWidth": 2,
426 - "strokeStyle": "solid",
427 - "roughness": 1,
428 - "opacity": 100,
429 - "angle": 0,
430 - "x": 176.50360186688226,
431 - "y": 1263.2245738636377,
432 - "strokeColor": "#1e1e1e",
433 - "backgroundColor": "transparent",
434 - "width": 147.65625,
435 - "height": 43.199999999999996,
436 - "seed": 1338996886,
437 - "groupIds": [],
438 - "frameId": null,
439 - "roundness": null,
440 - "boundElements": [],
441 - "updated": 1712333788867,
442 - "link": null,
443 - "locked": false,
444 - "fontSize": 36,
445 - "fontFamily": 3,
446 - "text": "Details",
447 - "textAlign": "left",
448 - "verticalAlign": "top",
449 - "containerId": null,
450 - "originalText": "Details",
451 - "lineHeight": 1.2,
452 - "baseline": 35
453 - },
454 - {
455 - "type": "rectangle",
456 - "version": 64,
457 - "versionNonce": 328294538,
458 - "isDeleted": false,
459 - "id": "-ElCTpzQ3xaCiYT-X4dXl",
460 - "fillStyle": "hachure",
461 - "strokeWidth": 2,
462 - "strokeStyle": "solid",
463 - "roughness": 1,
464 - "opacity": 100,
465 - "angle": 0,
466 - "x": 191.05029194595772,
467 - "y": 1365.228601539592,
468 - "strokeColor": "#1e1e1e",
469 - "backgroundColor": "#b2f2bb",
470 - "width": 1557.5474330357142,
471 - "height": 143.47098214285734,
472 - "seed": 806531030,
473 - "groupIds": [],
474 - "frameId": null,
475 - "roundness": {
476 - "type": 3
477 - },
478 - "boundElements": [],
479 - "updated": 1712331146599,
480 - "link": null,
481 - "locked": false
482 - },
483 - {
484 - "type": "rectangle",
485 - "version": 105,
486 - "versionNonce": 1761052374,
487 - "isDeleted": false,
488 - "id": "LWqStHnyaoG8w0S2URNGv",
489 - "fillStyle": "hachure",
490 - "strokeWidth": 2,
491 - "strokeStyle": "solid",
492 - "roughness": 1,
493 - "opacity": 100,
494 - "angle": 0,
495 - "x": 189.25062676738617,
496 - "y": 1576.6404318967348,
497 - "strokeColor": "#1e1e1e",
498 - "backgroundColor": "#b2f2bb",
499 - "width": 1557.5474330357142,
500 - "height": 143.47098214285734,
501 - "seed": 1586258634,
502 - "groupIds": [],
503 - "frameId": null,
504 - "roundness": {
505 - "type": 3
506 - },
507 - "boundElements": [],
508 - "updated": 1712331147514,
509 - "link": null,
510 - "locked": false
511 - },
512 - {
513 - "type": "rectangle",
514 - "version": 153,
515 - "versionNonce": 590816010,
516 - "isDeleted": false,
517 - "id": "WycKd7HWykIHf6xFdSAM_",
518 - "fillStyle": "hachure",
519 - "strokeWidth": 2,
520 - "strokeStyle": "solid",
521 - "roughness": 1,
522 - "opacity": 100,
523 - "angle": 0,
524 - "x": 195.41692141024305,
525 - "y": 1777.8820613610205,
526 - "strokeColor": "#1e1e1e",
527 - "backgroundColor": "#b2f2bb",
528 - "width": 1557.5474330357142,
529 - "height": 143.47098214285734,
530 - "seed": 44500310,
531 - "groupIds": [],
532 - "frameId": null,
533 - "roundness": {
534 - "type": 3
535 - },
536 - "boundElements": [],
537 - "updated": 1712331148401,
538 - "link": null,
539 - "locked": false
540 - },
541 - {
542 - "type": "rectangle",
543 - "version": 198,
544 - "versionNonce": 613269846,
545 - "isDeleted": false,
546 - "id": "yuhS5wfbhCXtiHqd5BDgx",
547 - "fillStyle": "hachure",
548 - "strokeWidth": 2,
549 - "strokeStyle": "solid",
550 - "roughness": 1,
551 - "opacity": 100,
552 - "angle": 0,
553 - "x": 197.07707766024305,
554 - "y": 1980.4490256467348,
555 - "strokeColor": "#1e1e1e",
556 - "backgroundColor": "#b2f2bb",
557 - "width": 1557.5474330357142,
558 - "height": 143.47098214285734,
559 - "seed": 1213513802,
560 - "groupIds": [],
561 - "frameId": null,
562 - "roundness": {
563 - "type": 3
564 - },
565 - "boundElements": [],
566 - "updated": 1712331150000,
567 - "link": null,
568 - "locked": false
569 - },
570 - {
571 - "type": "rectangle",
572 - "version": 243,
573 - "versionNonce": 711506646,
574 - "isDeleted": false,
575 - "id": "64UJCaHuygytFtzxrKkQR",
576 - "fillStyle": "hachure",
577 - "strokeWidth": 2,
578 - "strokeStyle": "solid",
579 - "roughness": 1,
580 - "opacity": 100,
581 - "angle": 0,
582 - "x": 203.3689303388146,
583 - "y": 2174.617552432449,
584 - "strokeColor": "#1e1e1e",
585 - "backgroundColor": "#b2f2bb",
586 - "width": 1557.5474330357142,
587 - "height": 143.47098214285734,
588 - "seed": 2071900298,
589 - "groupIds": [],
590 - "frameId": null,
591 - "roundness": {
592 - "type": 3
593 - },
594 - "boundElements": [],
595 - "updated": 1712331151011,
596 - "link": null,
597 - "locked": false
598 - }
599 - ],
600 - "appState": {
601 - "gridSize": null,
602 - "viewBackgroundColor": "#ffffff"
603 - },
604 - "files": {}
605 -}
frontend/vite.config.ts
+1 -2
@@ -18,8 +18,7 @@ export default defineConfig(({ mode }) => {
18 plugins: [
19 vue({
20 script: {
21 - defineModel: true,
22 - propsDestructure: false
21 + defineModel: true
22 }
23 }),
24 vueJsx(),