@cryptotaxi247 / CoPilot / commits / 5850465b

Reporting (#179)

* Update type hints for monitoring alert creation * Update index set and event stream titles and descriptions * Refactor Office365 provision functions * Update title and description formatting in build_index_set_config and build_event_stream_config * Update index and event stream configuration * Update index and event stream titles in SAP SIEM provision service * Add data links to Grafana datasource * Add dataLinks to GrafanaJsonData and create_grafana_datasource * updated dependencies * added reporting api/types * added Report Creation page * Add Grafana datasource URLs for data_vulnerability_cve and _id fields * updated Report Creation page * Add job functionality to scheduler * Fix timestamp_utc fallback in create_alert_details function * Add logging for agent details retrieval * default to `timestamp` if timefield is None * Update job time interval and add job metadata * licensing test and github action docker build * test reading auth secret * updated Report Creation page * license key checks * Refactor code to enable browser testing using Playwright * initial testing of reporting using playwright * Add async support for agent synchronization. Remove the background task and run every 15 minutes * Refactor sync_all_agents function to pass session parameter to sync_agents * Add GenerateReportRequest to create_report function * just use chromium browser * Add GenerateReportResponse class and update create_report function signature * migrage reporting to grafana connector * updated Report Creation page * license try catch * added print media query * updated connector form * updated report page * added playwrite test page * added report template * updated dependencies * updated report page * active response log analysis update * sap siem analysis 10 minute window * Update threshold and add time range parameter for SAP SIEM multiple logins analysis * Update index names in sap_siem_multiple_logins.py * same login failures multiple diff ips * same user failed diff geo location * Successful same user login from different locations * brute force failed logins * Refactor brute force failed logins route to handle multiple IPs * brute_force_failed_logins_same_ip * sap_siem_successful_login_after_failures * change index name * add sap siem to scheduler * sap siem correction for multiple IPs attempting to login with same user followed by a success. Added a correction to update the assets tab with the correct data * pdf generation * Update Jinja2 version to 3.1.2 * sort on the page number * remove reportlab * updated report page (added d&d) * updated report panels drag & drop * Update event stream configuration in graylog.py * Update dashboard enum names * Commented out Office365 related code * Update Office365Dashboard enum value for SUMMARY * Update index skipping logic in IndexConfigModel * Update IndexConfigModel.is_valid_index method to allow non-"wazuh_" index names and exclude "deflector" index. * Update Graylog schema for optional TLS and TCP keepalive * updated report panels drag & drop * updated report panels drag & drop * modify wazuh agent config * Refactor GenerateReportRequest schema in Grafana reporting module * remodel report generation to fit new request schema * use playwright for pdf generation * Update custom_attributes field description in SingleCaseModel * Add default value for custom_attributes in SingleCaseModel constructor * Update custom_attributes field in SingleCaseModel * test frontend auto build * Add dependency installation step to Docker workflow * Update dependencies in docker.yml and frontend/package-lock.json * Fix template value for CUSTOMER_CODE in provision_custom_alert * Fix incorrect client creation error message * Add InfluxDB alerts fetching functionality to verification check. the ping and version will return true even if API token is not valid * Update YouTube Tutorial link in README * Fix cover-box typo and adjust screenshot styling * Update Wazuh group configuration and replace placeholder with cluster name * Fix placeholder replacement in wazuh_manager.py * Add wazuh_worker_hostname field to default settings and schema * Add wazuhWorkerHostname field to ProvisioningDefaultSettingsPayload and CustomerProvisioningDefaultSettings * updated report panels drag & drop * updated report template * haproxy provisioning connector * Add Grafana data link for O365 datasource * Add feature enum and API endpoint to add a feature to a license * Add feature check for reporting * Update error message for disabled feature * office365 fix all office365 api keys now added to same office365 block * delete wazuh_config.xml * Add optional field for event definition configuration * Refactor Config class in graylog/schema/events.py * Add HAPROXY_PROVISIONING_URL to .env.example * Add ExpressionItem class to handle complex expressions in Conditions * Add SeriesItem model to Config class * Add logging for Graylog alert definition provisioned response * Add event_limit field to GraylogAlertProvisionConfig * Add event limit to provision functions * Commented out license check in create_report function * Add panel width and height to RequestPanel model * updated report api * Update generate_panel_urls to include a theme parameter * Add license-related API endpoints and models * Update edr av malware ioc dashboard template * Add ProvisionHaProxyRequest to customer_provisioning schema * Add EDR_NETWORK_CONNECTIONS dashboard * update grafana dashboard templates * Add wazuh_agent_status field to WazuhAgent model and Agents table * Update wazuh_agent_status default value * updated report editor * Add company name, timerange text, and logo to GenerateReportRequest * Add theme field to GrafanaGenerateIframeLinksRequest and RequestPanel models * Update report template and remove unnecessary files * Update headless option in browser launch * Add playwright dependencies installation step * Update Grafana login handling and launch browser in headless mode * Fix login issue in Grafana service * precommit fixes * RSA PUB KEY into build as env * Add RSA_PUBLIC_KEY to build-args in docker.yml * more precommit fixes * Add PRODUCT_ID environment variable * added report panel height settings * Update branch name in Docker workflow * Add HTTPException for feature not enabled * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Mar 25, 2024 at 10:52 UTC 5850465b4d1839fe1fa702d3a8ce315b7eec9fb8
101 files changed +12545 -2101
.env.example
+2
@@ -55,3 +55,5 @@ EVENT_SHIPPER_URL=graylog_host
55 GELF_INPUT_PORT=gelf_port
56
57 ALERT_CREATION_PROVISIONING_URL=http://1.1.1.1
58 +
59 +HAPROXY_PROVISIONING_URL=http://1.1.1.1
.flake8
+1 -1
@@ -1,6 +1,6 @@
1 [flake8]
2 #max-complexity = 18
3 -max-line-length = 170
3 +max-line-length = 180
4 #select = B,C,E,F,W,T4,B9
5 #ignore = E203, E266, E501, W503, F403, F401
6 ignore = E402, W503, E231, W605, E266
.github/workflows/docker.yml new
+58
@@ -0,0 +1,58 @@
1 +name: Docker
2 +
3 +on:
4 + push:
5 + branches: [main]
6 +
7 +jobs:
8 + build-backend:
9 + runs-on: ubuntu-latest
10 +
11 + steps:
12 + - name: Check out code
13 + uses: actions/checkout@v2
14 +
15 + - name: Set up Docker Buildx
16 + uses: docker/setup-buildx-action@v1
17 +
18 + - name: Login to GitHub Container Registry
19 + uses: docker/login-action@v1
20 + with:
21 + registry: ghcr.io
22 + username: ${{ secrets.REPO_OWNER }}
23 + password: ${{ secrets.REPO_TOKEN }}
24 +
25 + - name: Build and push Docker image
26 + uses: docker/build-push-action@v2
27 + with:
28 + context: ./backend
29 + push: true
30 + tags: ghcr.io/socfortress/copilot-backend:latest
31 + build-args: |
32 + CRYPTOLENS_AUTH=${{ secrets.CRYPTOLENS_AUTH }}
33 + RSA_PUBLIC_KEY=${{ secrets.RSA_PUBLIC_KEY }}
34 + PRODUCT_ID=${{ secrets.PRODUCT_ID }}
35 +
36 + build-frontend:
37 + runs-on: ubuntu-latest
38 +
39 + steps:
40 + - name: Check out code
41 + uses: actions/checkout@v2
42 +
43 + - name: Set up Docker Buildx
44 + uses: docker/setup-buildx-action@v1
45 +
46 + - name: Login to GitHub Container Registry
47 + uses: docker/login-action@v1
48 + with:
49 + registry: ghcr.io
50 + username: ${{ secrets.REPO_OWNER }}
51 + password: ${{ secrets.REPO_TOKEN }}
52 +
53 + - name: Build and push Docker image
54 + uses: docker/build-push-action@v2
55 + with:
56 + context: ./frontend
57 + push: true
58 + tags: ghcr.io/socfortress/copilot-frontend:latest
.gitignore
+5
@@ -55,3 +55,8 @@ package-lock.json
55 firewall.spec
56
57 frontend/cypress/screenshots/
58 +
59 +backend/report.pdf
60 +
61 +backend/report.html
62 +backend/app/integrations/office365/services/wazuh_config.xml
.vscode/extensions.json
-2
@@ -7,12 +7,10 @@
7 "ecmel.vscode-html-css",
8 "george-alison.html-preview-vscodes",
9 "esbenp.prettier-vscode",
10 - "octref.vetur",
10 "ms-vscode.remtoe-remote-wsl",
11 "dbaeumer.vscode-eslint",
12 "Vue.volar",
13 "Gruntfuggly.todo-tree",
15 - "Vue.vscode-typescript-vue-plugin",
14 "usernamehw.errorlens",
15 "streetsidesoftware.code-spell-checker"
16 ]
.vscode/settings.json
+2
@@ -11,6 +11,7 @@
11 "forgotpassword",
12 "Healthcheck",
13 "healthchecks",
14 + "Indicies",
15 "majesticons",
16 "mimecast",
17 "mynaui",
@@ -21,6 +22,7 @@
22 "Socfortress",
23 "sparkline",
24 "taze",
25 + "timerange",
26 "uvicorn",
27 "venv",
28 "vuesjv",
README.md
+1 -1
@@ -102,7 +102,7 @@ Copilot shall be available on the host interface, port 443, protocol HTTPS - `ht
102 By default, an `admin` account is created. The password is printed in stdout the very first time Copilot is started. It won't be printed anymore after that.
103 `Admin user password` can be searched in the logs of the `copilot` docker to find the password. You will use the `plain` password to login to the web interface.
104
105 -🚀 **YouTube Tutorial:** [STILL NEED TO UPDATE](https://www.youtube.com/channel/UC4EUQtTxeC8wGrKRafI6pZg)
105 +🚀 **YouTube Tutorial:** [INSTALLING COPILOT](https://youtu.be/7dUHSMWWTuY?si=lRbn4tBHnyKmiTka)
106
107 #### SSL
108
backend/Dockerfile
+16
@@ -41,6 +41,13 @@ WORKDIR /opt/copilot/backend
41 COPY requirements.txt ./
42 RUN /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
43
44 +# Install required browsers for playwright
45 +RUN playwright install-deps
46 +RUN playwright install
47 +
48 +# Install wkhtmltopdf
49 +RUN apt-get install -y wkhtmltopdf
50 +
51 # Copy your application into the Docker image
52 WORKDIR /opt/copilot/backend
53 COPY . .
@@ -102,5 +109,14 @@ ENV GELF_INPUT_PORT=gelf_port
109
110 ENV ALERT_CREATION_PROVISIONING_URL=http://1.1.1.1
111
112 +ARG CRYPTOLENS_AUTH
113 +ENV CRYPTOLENS_AUTH=$CRYPTOLENS_AUTH
114 +
115 +ARG RSA_PUBLIC_KEY
116 +ENV RSA_PUBLIC_KEY=$RSA_PUBLIC_KEY
117 +
118 +ARG PRODUCT_ID
119 +ENV PRODUCT_ID=$PRODUCT_ID
120 +
121 # Run your application
122 CMD ["sh", "-c", "ls -la && /opt/venv/bin/python copilot.py"]
backend/app/agents/routes/agents.py
+7 -3
@@ -1,5 +1,7 @@
1 +import asyncio
2 +
3 +# from fastapi import BackgroundTasks
4 from fastapi import APIRouter
2 -from fastapi import BackgroundTasks
5 from fastapi import Depends
6 from fastapi import HTTPException
7 from fastapi import Security
@@ -218,7 +220,7 @@ async def get_agent_by_hostname(
220 ],
221 )
222 async def sync_all_agents(
221 - backgroud_tasks: BackgroundTasks,
223 + # backgroud_tasks: BackgroundTasks,
224 session: AsyncSession = Depends(get_db),
225 ) -> SyncedAgentsResponse:
226 """
@@ -236,7 +238,9 @@ async def sync_all_agents(
238
239 """
240 logger.info("Syncing agents from Wazuh Manager")
239 - backgroud_tasks.add_task(sync_agents, session)
241 + # backgroud_tasks.add_task(sync_agents, session)
242 + loop = asyncio.get_event_loop()
243 + loop.create_task(sync_agents(session=session))
244 return SyncedAgentsResponse(
245 success=True,
246 message="Agents synced started successfully",
backend/app/agents/wazuh/schema/agents.py
+1
@@ -14,6 +14,7 @@ class WazuhAgent(BaseModel):
14 agent_label: str = Field(..., alias="label")
15 agent_last_seen: str = Field(..., alias="wazuh_last_seen")
16 wazuh_agent_version: str = Field(..., alias="wazuh_agent_version")
17 + wazuh_agent_status: Optional[str] = Field(None, alias="wazuh_agent_status")
18
19 @property
20 def agent_last_seen_as_datetime(self):
backend/app/agents/wazuh/services/agents.py
+1
@@ -60,6 +60,7 @@ async def collect_wazuh_agents() -> WazuhAgentsList:
60 agent_label=agent_group,
61 agent_last_seen=last_keep_alive,
62 wazuh_agent_version=agent.get("version", "n/a"),
63 + wazuh_agent_status=agent.get("status", "n/a"),
64 )
65 wazuh_agents_list.append(wazuh_agent)
66
backend/app/connectors/dfir_iris/schema/cases.py
+13 -1
@@ -58,7 +58,14 @@ class SingleCaseModel(BaseModel):
58 classification: Optional[Union[str, None]]
59 classification_id: Optional[Union[int, None]]
60 close_date: Optional[Union[str, None]]
61 - custom_attributes: Optional[Union[str, None]]
61 + # custom_attributes: Optional[Union[str, None]] = Field(
62 + # None,
63 + # description="The custom attributes of the case.",
64 + # )
65 + custom_attributes: Optional[Dict[str, Union[str, None]]] = Field(
66 + None,
67 + description="The custom attributes of the case.",
68 + )
69 customer_id: int
70 customer_name: str
71 initial_date: str
@@ -77,6 +84,11 @@ class SingleCaseModel(BaseModel):
84 status_name: str
85 customer_code: str
86
87 + def __init__(self, **data):
88 + if "custom_attributes" in data and not data["custom_attributes"]:
89 + data["custom_attributes"] = {"default_key": "no custom attributes found"} # Replace with your default entry
90 + super().__init__(**data)
91 +
92
93 class SingleCaseBody(BaseModel):
94 case_id: int
backend/app/connectors/grafana/dashboards/Wazuh/edr_av_malware_ioc.json
+259 -254
@@ -87,7 +87,6 @@
87 "y": 1
88 },
89 "id": 134,
90 - "links": [],
90 "options": {
91 "colorMode": "value",
92 "graphMode": "area",
@@ -98,10 +97,12 @@
97 "fields": "",
98 "values": false
99 },
100 + "showPercentChange": false,
101 "text": {},
102 - "textMode": "auto"
102 + "textMode": "auto",
103 + "wideLayout": true
104 },
104 - "pluginVersion": "10.0.2",
105 + "pluginVersion": "10.4.0",
106 "targets": [
107 {
108 "bucketAggs": [
@@ -189,7 +190,6 @@
190 "y": 1
191 },
192 "id": 130,
192 - "links": [],
193 "maxDataPoints": 3,
194 "options": {
195 "cellHeight": "sm",
@@ -202,7 +202,7 @@
202 "showHeader": true,
203 "sortBy": []
204 },
205 - "pluginVersion": "10.0.2",
205 + "pluginVersion": "10.4.0",
206 "targets": [
207 {
208 "bucketAggs": [
@@ -364,7 +364,6 @@
364 "y": 1
365 },
366 "id": 131,
367 - "links": [],
367 "maxDataPoints": 3,
368 "options": {
369 "cellHeight": "sm",
@@ -377,7 +376,7 @@
376 "showHeader": true,
377 "sortBy": []
378 },
380 - "pluginVersion": "10.0.2",
379 + "pluginVersion": "10.4.0",
380 "targets": [
381 {
382 "bucketAggs": [
@@ -648,7 +647,7 @@
647 "showHeader": true,
648 "sortBy": []
649 },
651 - "pluginVersion": "10.0.2",
650 + "pluginVersion": "10.4.0",
651 "targets": [
652 {
653 "alias": "",
@@ -1133,7 +1132,6 @@
1132 "y": 2
1133 },
1134 "id": 140,
1136 - "links": [],
1135 "options": {
1136 "colorMode": "value",
1137 "graphMode": "area",
@@ -1144,10 +1142,12 @@
1142 "fields": "",
1143 "values": false
1144 },
1145 + "showPercentChange": false,
1146 "text": {},
1148 - "textMode": "auto"
1147 + "textMode": "auto",
1148 + "wideLayout": true
1149 },
1150 - "pluginVersion": "10.0.2",
1150 + "pluginVersion": "10.4.0",
1151 "targets": [
1152 {
1153 "bucketAggs": [
@@ -1235,7 +1235,6 @@
1235 "y": 2
1236 },
1237 "id": 141,
1238 - "links": [],
1238 "maxDataPoints": 3,
1239 "options": {
1240 "cellHeight": "sm",
@@ -1248,7 +1247,7 @@
1247 "showHeader": true,
1248 "sortBy": []
1249 },
1251 - "pluginVersion": "10.0.2",
1250 + "pluginVersion": "10.4.0",
1251 "targets": [
1252 {
1253 "bucketAggs": [
@@ -1422,7 +1421,6 @@
1421 "y": 2
1422 },
1423 "id": 142,
1425 - "links": [],
1424 "maxDataPoints": 3,
1425 "options": {
1426 "cellHeight": "sm",
@@ -1435,7 +1433,7 @@
1433 "showHeader": true,
1434 "sortBy": []
1435 },
1438 - "pluginVersion": "10.0.2",
1436 + "pluginVersion": "10.4.0",
1437 "targets": [
1438 {
1439 "bucketAggs": [
@@ -1709,7 +1707,7 @@
1707 "showHeader": true,
1708 "sortBy": []
1709 },
1712 - "pluginVersion": "10.0.2",
1710 + "pluginVersion": "10.4.0",
1711 "targets": [
1712 {
1713 "alias": "",
@@ -2387,10 +2385,9 @@
2385 "h": 8,
2386 "w": 4,
2387 "x": 0,
2390 - "y": 23
2388 + "y": 3
2389 },
2390 "id": 113,
2393 - "links": [],
2391 "options": {
2392 "colorMode": "value",
2393 "graphMode": "area",
@@ -2401,10 +2398,12 @@
2398 "fields": "",
2399 "values": false
2400 },
2401 + "showPercentChange": false,
2402 "text": {},
2405 - "textMode": "auto"
2403 + "textMode": "auto",
2404 + "wideLayout": true
2405 },
2407 - "pluginVersion": "10.0.2",
2406 + "pluginVersion": "10.4.0",
2407 "targets": [
2408 {
2409 "bucketAggs": [
@@ -2633,10 +2632,9 @@
2632 "h": 8,
2633 "w": 5,
2634 "x": 4,
2636 - "y": 23
2635 + "y": 3
2636 },
2637 "id": 68,
2639 - "links": [],
2638 "maxDataPoints": 3,
2639 "options": {
2640 "displayLabels": [],
@@ -2832,10 +2830,9 @@
2830 "h": 8,
2831 "w": 15,
2832 "x": 9,
2835 - "y": 23
2833 + "y": 3
2834 },
2835 "id": 115,
2838 - "links": [],
2836 "maxDataPoints": 3,
2837 "options": {
2838 "cellHeight": "sm",
@@ -2848,7 +2845,7 @@
2845 "showHeader": true,
2846 "sortBy": []
2847 },
2851 - "pluginVersion": "10.0.2",
2848 + "pluginVersion": "10.4.0",
2849 "targets": [
2850 {
2851 "bucketAggs": [
@@ -2948,10 +2945,9 @@
2945 "h": 7,
2946 "w": 9,
2947 "x": 0,
2951 - "y": 31
2948 + "y": 11
2949 },
2950 "id": 70,
2954 - "links": [],
2951 "maxDataPoints": 3,
2952 "options": {
2953 "cellHeight": "sm",
@@ -2964,7 +2960,7 @@
2960 "showHeader": true,
2961 "sortBy": []
2962 },
2967 - "pluginVersion": "10.0.2",
2963 + "pluginVersion": "10.4.0",
2964 "targets": [
2965 {
2966 "bucketAggs": [
@@ -3003,52 +2999,86 @@
2999 "type": "table"
3000 },
3001 {
3006 - "aliasColors": {},
3007 - "bars": true,
3008 - "dashLength": 10,
3009 - "dashes": false,
3002 "datasource": {
3003 "type": "elasticsearch",
3004 "uid": "replace_datasource_uid"
3005 },
3014 - "fill": 1,
3015 - "fillGradient": 0,
3006 + "fieldConfig": {
3007 + "defaults": {
3008 + "color": {
3009 + "mode": "palette-classic"
3010 + },
3011 + "custom": {
3012 + "axisBorderShow": false,
3013 + "axisCenteredZero": false,
3014 + "axisColorMode": "text",
3015 + "axisLabel": "",
3016 + "axisPlacement": "auto",
3017 + "barAlignment": 0,
3018 + "drawStyle": "bars",
3019 + "fillOpacity": 100,
3020 + "gradientMode": "none",
3021 + "hideFrom": {
3022 + "legend": false,
3023 + "tooltip": false,
3024 + "viz": false
3025 + },
3026 + "insertNulls": false,
3027 + "lineInterpolation": "linear",
3028 + "lineWidth": 1,
3029 + "pointSize": 5,
3030 + "scaleDistribution": {
3031 + "type": "linear"
3032 + },
3033 + "showPoints": "never",
3034 + "spanNulls": false,
3035 + "stacking": {
3036 + "group": "A",
3037 + "mode": "normal"
3038 + },
3039 + "thresholdsStyle": {
3040 + "mode": "off"
3041 + }
3042 + },
3043 + "mappings": [],
3044 + "thresholds": {
3045 + "mode": "absolute",
3046 + "steps": [
3047 + {
3048 + "color": "green",
3049 + "value": null
3050 + },
3051 + {
3052 + "color": "red",
3053 + "value": 80
3054 + }
3055 + ]
3056 + },
3057 + "unit": "short"
3058 + },
3059 + "overrides": []
3060 + },
3061 "gridPos": {
3062 "h": 7,
3063 "w": 15,
3064 "x": 9,
3020 - "y": 31
3065 + "y": 11
3066 },
3022 - "hiddenSeries": false,
3067 "id": 83,
3024 - "legend": {
3025 - "alignAsTable": true,
3026 - "avg": false,
3027 - "current": false,
3028 - "max": false,
3029 - "min": false,
3030 - "rightSide": true,
3031 - "show": true,
3032 - "total": false,
3033 - "values": false
3034 - },
3035 - "lines": false,
3036 - "linewidth": 1,
3037 - "links": [],
3068 "maxDataPoints": 3,
3039 - "nullPointMode": "null",
3069 "options": {
3041 - "alertThreshold": true
3042 - },
3043 - "percentage": false,
3044 - "pluginVersion": "10.0.2",
3045 - "pointradius": 2,
3046 - "points": false,
3047 - "renderer": "flot",
3048 - "seriesOverrides": [],
3049 - "spaceLength": 10,
3050 - "stack": true,
3051 - "steppedLine": false,
3070 + "legend": {
3071 + "calcs": [],
3072 + "displayMode": "table",
3073 + "placement": "right",
3074 + "showLegend": true
3075 + },
3076 + "tooltip": {
3077 + "mode": "multi",
3078 + "sort": "none"
3079 + }
3080 + },
3081 + "pluginVersion": "10.4.0",
3082 "targets": [
3083 {
3084 "alias": "",
@@ -3092,35 +3122,8 @@
3122 "timeField": "timestamp"
3123 }
3124 ],
3095 - "thresholds": [],
3096 - "timeRegions": [],
3125 "title": "WINDOWS DEFENDER - EVENTS BY AGENT (HISTOGRAM)",
3098 - "tooltip": {
3099 - "shared": true,
3100 - "sort": 0,
3101 - "value_type": "individual"
3102 - },
3103 - "type": "graph",
3104 - "xaxis": {
3105 - "mode": "time",
3106 - "show": true,
3107 - "values": []
3108 - },
3109 - "yaxes": [
3110 - {
3111 - "format": "short",
3112 - "logBase": 1,
3113 - "show": true
3114 - },
3115 - {
3116 - "format": "short",
3117 - "logBase": 1,
3118 - "show": true
3119 - }
3120 - ],
3121 - "yaxis": {
3122 - "align": false
3123 - }
3126 + "type": "timeseries"
3127 },
3128 {
3129 "datasource": {
@@ -3263,7 +3266,7 @@
3266 "h": 10,
3267 "w": 24,
3268 "x": 0,
3266 - "y": 38
3269 + "y": 18
3270 },
3271 "id": 85,
3272 "options": {
@@ -3277,7 +3280,7 @@
3280 "showHeader": true,
3281 "sortBy": []
3282 },
3280 - "pluginVersion": "10.0.2",
3283 + "pluginVersion": "10.4.0",
3284 "targets": [
3285 {
3286 "alias": "",
@@ -3535,10 +3538,9 @@
3538 "h": 7,
3539 "w": 4,
3540 "x": 0,
3538 - "y": 24
3541 + "y": 4
3542 },
3543 "id": 100,
3541 - "links": [],
3544 "options": {
3545 "colorMode": "value",
3546 "graphMode": "area",
@@ -3549,10 +3551,12 @@
3551 "fields": "",
3552 "values": false
3553 },
3554 + "showPercentChange": false,
3555 "text": {},
3553 - "textMode": "auto"
3556 + "textMode": "auto",
3557 + "wideLayout": true
3558 },
3555 - "pluginVersion": "10.0.2",
3559 + "pluginVersion": "10.4.0",
3560 "targets": [
3561 {
3562 "bucketAggs": [
@@ -3736,10 +3740,9 @@
3740 "h": 7,
3741 "w": 5,
3742 "x": 4,
3739 - "y": 24
3743 + "y": 4
3744 },
3745 "id": 101,
3742 - "links": [],
3746 "maxDataPoints": 3,
3747 "options": {
3748 "displayLabels": [],
@@ -3886,10 +3889,9 @@
3889 "h": 7,
3890 "w": 15,
3891 "x": 9,
3889 - "y": 24
3892 + "y": 4
3893 },
3894 "id": 102,
3892 - "links": [],
3895 "maxDataPoints": 3,
3896 "options": {
3897 "cellHeight": "sm",
@@ -3901,7 +3903,7 @@
3903 },
3904 "showHeader": true
3905 },
3904 - "pluginVersion": "10.0.2",
3906 + "pluginVersion": "10.4.0",
3907 "targets": [
3908 {
3909 "bucketAggs": [
@@ -4001,10 +4003,9 @@
4003 "h": 7,
4004 "w": 9,
4005 "x": 0,
4004 - "y": 31
4006 + "y": 11
4007 },
4008 "id": 106,
4007 - "links": [],
4009 "maxDataPoints": 3,
4010 "options": {
4011 "cellHeight": "sm",
@@ -4016,7 +4017,7 @@
4017 },
4018 "showHeader": true
4019 },
4019 - "pluginVersion": "10.0.2",
4020 + "pluginVersion": "10.4.0",
4021 "targets": [
4022 {
4023 "bucketAggs": [
@@ -4055,52 +4056,86 @@
4056 "type": "table"
4057 },
4058 {
4058 - "aliasColors": {},
4059 - "bars": true,
4060 - "dashLength": 10,
4061 - "dashes": false,
4059 "datasource": {
4060 "type": "elasticsearch",
4061 "uid": "replace_datasource_uid"
4062 },
4066 - "fill": 1,
4067 - "fillGradient": 0,
4063 + "fieldConfig": {
4064 + "defaults": {
4065 + "color": {
4066 + "mode": "palette-classic"
4067 + },
4068 + "custom": {
4069 + "axisBorderShow": false,
4070 + "axisCenteredZero": false,
4071 + "axisColorMode": "text",
4072 + "axisLabel": "",
4073 + "axisPlacement": "auto",
4074 + "barAlignment": 0,
4075 + "drawStyle": "bars",
4076 + "fillOpacity": 100,
4077 + "gradientMode": "none",
4078 + "hideFrom": {
4079 + "legend": false,
4080 + "tooltip": false,
4081 + "viz": false
4082 + },
4083 + "insertNulls": false,
4084 + "lineInterpolation": "linear",
4085 + "lineWidth": 1,
4086 + "pointSize": 5,
4087 + "scaleDistribution": {
4088 + "type": "linear"
4089 + },
4090 + "showPoints": "never",
4091 + "spanNulls": false,
4092 + "stacking": {
4093 + "group": "A",
4094 + "mode": "normal"
4095 + },
4096 + "thresholdsStyle": {
4097 + "mode": "off"
4098 + }
4099 + },
4100 + "mappings": [],
4101 + "thresholds": {
4102 + "mode": "absolute",
4103 + "steps": [
4104 + {
4105 + "color": "green",
4106 + "value": null
4107 + },
4108 + {
4109 + "color": "red",
4110 + "value": 80
4111 + }
4112 + ]
4113 + },
4114 + "unit": "short"
4115 + },
4116 + "overrides": []
4117 + },
4118 "gridPos": {
4119 "h": 7,
4120 "w": 15,
4121 "x": 9,
4072 - "y": 31
4122 + "y": 11
4123 },
4074 - "hiddenSeries": false,
4124 "id": 107,
4076 - "legend": {
4077 - "alignAsTable": true,
4078 - "avg": false,
4079 - "current": false,
4080 - "max": false,
4081 - "min": false,
4082 - "rightSide": true,
4083 - "show": true,
4084 - "total": false,
4085 - "values": false
4086 - },
4087 - "lines": false,
4088 - "linewidth": 1,
4089 - "links": [],
4125 "maxDataPoints": 3,
4091 - "nullPointMode": "null",
4126 "options": {
4093 - "alertThreshold": true
4094 - },
4095 - "percentage": false,
4096 - "pluginVersion": "10.0.2",
4097 - "pointradius": 2,
4098 - "points": false,
4099 - "renderer": "flot",
4100 - "seriesOverrides": [],
4101 - "spaceLength": 10,
4102 - "stack": true,
4103 - "steppedLine": false,
4127 + "legend": {
4128 + "calcs": [],
4129 + "displayMode": "table",
4130 + "placement": "right",
4131 + "showLegend": true
4132 + },
4133 + "tooltip": {
4134 + "mode": "multi",
4135 + "sort": "none"
4136 + }
4137 + },
4138 + "pluginVersion": "10.4.0",
4139 "targets": [
4140 {
4141 "alias": "",
@@ -4144,35 +4179,8 @@
4179 "timeField": "timestamp"
4180 }
4181 ],
4147 - "thresholds": [],
4148 - "timeRegions": [],
4182 "title": "WINDOWS SIGCHECK - EVENTS BY AGENT (HISTOGRAM)",
4150 - "tooltip": {
4151 - "shared": true,
4152 - "sort": 0,
4153 - "value_type": "individual"
4154 - },
4155 - "type": "graph",
4156 - "xaxis": {
4157 - "mode": "time",
4158 - "show": true,
4159 - "values": []
4160 - },
4161 - "yaxes": [
4162 - {
4163 - "format": "short",
4164 - "logBase": 1,
4165 - "show": true
4166 - },
4167 - {
4168 - "format": "short",
4169 - "logBase": 1,
4170 - "show": true
4171 - }
4172 - ],
4173 - "yaxis": {
4174 - "align": false
4175 - }
4183 + "type": "timeseries"
4184 },
4185 {
4186 "datasource": {
@@ -4322,10 +4330,9 @@
4330 "h": 9,
4331 "w": 5,
4332 "x": 0,
4325 - "y": 38
4333 + "y": 18
4334 },
4335 "id": 110,
4328 - "links": [],
4336 "maxDataPoints": 3,
4337 "options": {
4338 "displayLabels": [],
@@ -4456,10 +4463,9 @@
4463 "h": 9,
4464 "w": 19,
4465 "x": 5,
4459 - "y": 38
4466 + "y": 18
4467 },
4468 "id": 103,
4462 - "links": [],
4469 "maxDataPoints": 3,
4470 "options": {
4471 "cellHeight": "sm",
@@ -4472,7 +4478,7 @@
4478 "showHeader": true,
4479 "sortBy": []
4480 },
4475 - "pluginVersion": "10.0.2",
4481 + "pluginVersion": "10.4.0",
4482 "targets": [
4483 {
4484 "bucketAggs": [
@@ -4723,7 +4729,7 @@
4729 "h": 10,
4730 "w": 24,
4731 "x": 0,
4726 - "y": 47
4732 + "y": 27
4733 },
4734 "id": 93,
4735 "options": {
@@ -4737,7 +4743,7 @@
4743 "showHeader": true,
4744 "sortBy": []
4745 },
4740 - "pluginVersion": "10.0.2",
4746 + "pluginVersion": "10.4.0",
4747 "targets": [
4748 {
4749 "alias": "",
@@ -5084,7 +5090,6 @@
5090 "y": 5
5091 },
5092 "id": 43,
5087 - "links": [],
5093 "options": {
5094 "colorMode": "value",
5095 "graphMode": "area",
@@ -5095,10 +5100,12 @@
5100 "fields": "",
5101 "values": false
5102 },
5103 + "showPercentChange": false,
5104 "text": {},
5099 - "textMode": "auto"
5105 + "textMode": "auto",
5106 + "wideLayout": true
5107 },
5101 - "pluginVersion": "10.0.2",
5108 + "pluginVersion": "10.4.0",
5109 "targets": [
5110 {
5111 "bucketAggs": [
@@ -5285,7 +5292,6 @@
5292 "y": 5
5293 },
5294 "id": 59,
5288 - "links": [],
5295 "maxDataPoints": 3,
5296 "options": {
5297 "displayLabels": [],
@@ -5435,7 +5441,6 @@
5441 "y": 5
5442 },
5443 "id": 63,
5438 - "links": [],
5444 "maxDataPoints": 3,
5445 "options": {
5446 "cellHeight": "sm",
@@ -5447,7 +5452,7 @@
5452 },
5453 "showHeader": true
5454 },
5450 - "pluginVersion": "10.0.2",
5455 + "pluginVersion": "10.4.0",
5456 "targets": [
5457 {
5458 "bucketAggs": [
@@ -5550,7 +5555,6 @@
5555 "y": 12
5556 },
5557 "id": 60,
5553 - "links": [],
5558 "maxDataPoints": 3,
5559 "options": {
5560 "cellHeight": "sm",
@@ -5562,7 +5566,7 @@
5566 },
5567 "showHeader": true
5568 },
5565 - "pluginVersion": "10.0.2",
5569 + "pluginVersion": "10.4.0",
5570 "targets": [
5571 {
5572 "bucketAggs": [
@@ -5601,52 +5605,86 @@
5605 "type": "table"
5606 },
5607 {
5604 - "aliasColors": {},
5605 - "bars": true,
5606 - "dashLength": 10,
5607 - "dashes": false,
5608 "datasource": {
5609 "type": "elasticsearch",
5610 "uid": "replace_datasource_uid"
5611 },
5612 - "fill": 1,
5613 - "fillGradient": 0,
5612 + "fieldConfig": {
5613 + "defaults": {
5614 + "color": {
5615 + "mode": "palette-classic"
5616 + },
5617 + "custom": {
5618 + "axisBorderShow": false,
5619 + "axisCenteredZero": false,
5620 + "axisColorMode": "text",
5621 + "axisLabel": "",
5622 + "axisPlacement": "auto",
5623 + "barAlignment": 0,
5624 + "drawStyle": "bars",
5625 + "fillOpacity": 100,
5626 + "gradientMode": "none",
5627 + "hideFrom": {
5628 + "legend": false,
5629 + "tooltip": false,
5630 + "viz": false
5631 + },
5632 + "insertNulls": false,
5633 + "lineInterpolation": "linear",
5634 + "lineWidth": 1,
5635 + "pointSize": 5,
5636 + "scaleDistribution": {
5637 + "type": "linear"
5638 + },
5639 + "showPoints": "never",
5640 + "spanNulls": false,
5641 + "stacking": {
5642 + "group": "A",
5643 + "mode": "normal"
5644 + },
5645 + "thresholdsStyle": {
5646 + "mode": "off"
5647 + }
5648 + },
5649 + "mappings": [],
5650 + "thresholds": {
5651 + "mode": "absolute",
5652 + "steps": [
5653 + {
5654 + "color": "green",
5655 + "value": null
5656 + },
5657 + {
5658 + "color": "red",
5659 + "value": 80
5660 + }
5661 + ]
5662 + },
5663 + "unit": "short"
5664 + },
5665 + "overrides": []
5666 + },
5667 "gridPos": {
5668 "h": 7,
5669 "w": 15,
5670 "x": 9,
5671 "y": 12
5672 },
5620 - "hiddenSeries": false,
5673 "id": 92,
5622 - "legend": {
5623 - "alignAsTable": true,
5624 - "avg": false,
5625 - "current": false,
5626 - "max": false,
5627 - "min": false,
5628 - "rightSide": true,
5629 - "show": true,
5630 - "total": false,
5631 - "values": false
5632 - },
5633 - "lines": false,
5634 - "linewidth": 1,
5635 - "links": [],
5674 "maxDataPoints": 3,
5637 - "nullPointMode": "null",
5675 "options": {
5639 - "alertThreshold": true
5640 - },
5641 - "percentage": false,
5642 - "pluginVersion": "10.0.2",
5643 - "pointradius": 2,
5644 - "points": false,
5645 - "renderer": "flot",
5646 - "seriesOverrides": [],
5647 - "spaceLength": 10,
5648 - "stack": true,
5649 - "steppedLine": false,
5676 + "legend": {
5677 + "calcs": [],
5678 + "displayMode": "table",
5679 + "placement": "right",
5680 + "showLegend": true
5681 + },
5682 + "tooltip": {
5683 + "mode": "multi",
5684 + "sort": "none"
5685 + }
5686 + },
5687 + "pluginVersion": "10.4.0",
5688 "targets": [
5689 {
5690 "alias": "",
@@ -5690,35 +5728,8 @@
5728 "timeField": "timestamp"
5729 }
5730 ],
5693 - "thresholds": [],
5694 - "timeRegions": [],
5731 "title": "WINDOWS AUTORUNS - EVENTS BY AGENT (HISTOGRAM)",
5696 - "tooltip": {
5697 - "shared": true,
5698 - "sort": 0,
5699 - "value_type": "individual"
5700 - },
5701 - "type": "graph",
5702 - "xaxis": {
5703 - "mode": "time",
5704 - "show": true,
5705 - "values": []
5706 - },
5707 - "yaxes": [
5708 - {
5709 - "format": "short",
5710 - "logBase": 1,
5711 - "show": true
5712 - },
5713 - {
5714 - "format": "short",
5715 - "logBase": 1,
5716 - "show": true
5717 - }
5718 - ],
5719 - "yaxis": {
5720 - "align": false
5721 - }
5732 + "type": "timeseries"
5733 },
5734 {
5735 "datasource": {
@@ -5871,7 +5882,6 @@
5882 "y": 19
5883 },
5884 "id": 94,
5874 - "links": [],
5885 "maxDataPoints": 3,
5886 "options": {
5887 "displayLabels": [],
@@ -5994,7 +6004,6 @@
6004 "y": 19
6005 },
6006 "id": 95,
5997 - "links": [],
6007 "maxDataPoints": 3,
6008 "options": {
6009 "cellHeight": "sm",
@@ -6006,7 +6015,7 @@
6015 },
6016 "showHeader": true
6017 },
6009 - "pluginVersion": "10.0.2",
6018 + "pluginVersion": "10.4.0",
6019 "targets": [
6020 {
6021 "bucketAggs": [
@@ -6111,7 +6120,6 @@
6120 "y": 19
6121 },
6122 "id": 96,
6114 - "links": [],
6123 "maxDataPoints": 3,
6124 "options": {
6125 "cellHeight": "sm",
@@ -6123,7 +6131,7 @@
6131 },
6132 "showHeader": true
6133 },
6126 - "pluginVersion": "10.0.2",
6134 + "pluginVersion": "10.4.0",
6135 "targets": [
6136 {
6137 "bucketAggs": [
@@ -6238,7 +6246,6 @@
6246 "y": 26
6247 },
6248 "id": 105,
6241 - "links": [],
6249 "maxDataPoints": 3,
6250 "options": {
6251 "cellHeight": "sm",
@@ -6251,7 +6258,7 @@
6258 "showHeader": true,
6259 "sortBy": []
6260 },
6254 - "pluginVersion": "10.0.2",
6261 + "pluginVersion": "10.4.0",
6262 "targets": [
6263 {
6264 "bucketAggs": [
@@ -6352,7 +6359,6 @@
6359 "y": 26
6360 },
6361 "id": 97,
6355 - "links": [],
6362 "maxDataPoints": 3,
6363 "options": {
6364 "cellHeight": "sm",
@@ -6365,7 +6371,7 @@
6371 "showHeader": true,
6372 "sortBy": []
6373 },
6368 - "pluginVersion": "10.0.2",
6374 + "pluginVersion": "10.4.0",
6375 "targets": [
6376 {
6377 "bucketAggs": [
@@ -6648,7 +6654,7 @@
6654 "showHeader": true,
6655 "sortBy": []
6656 },
6651 - "pluginVersion": "10.0.2",
6657 + "pluginVersion": "10.4.0",
6658 "targets": [
6659 {
6660 "alias": "",
@@ -6932,8 +6938,7 @@
6938 }
6939 ],
6940 "refresh": "",
6935 - "schemaVersion": 38,
6936 - "style": "dark",
6941 + "schemaVersion": 39,
6942 "tags": ["EDR"],
6943 "templating": {
6944 "list": [
backend/app/connectors/grafana/dashboards/Wazuh/edr_dns_requests.json
+164 -70
@@ -25,7 +25,6 @@
25 "fiscalYearStartMonth": 0,
26 "graphTooltip": 0,
27 "id": null,
28 - "iteration": 1658194226116,
28 "links": [
29 {
30 "asDropdown": true,
@@ -95,7 +94,6 @@
94 "y": 1
95 },
96 "id": 68,
98 - "links": [],
97 "options": {
98 "colorMode": "value",
99 "graphMode": "area",
@@ -106,10 +104,12 @@
104 "fields": "",
105 "values": false
106 },
107 + "showPercentChange": false,
108 "text": {},
110 - "textMode": "auto"
109 + "textMode": "auto",
110 + "wideLayout": true
111 },
112 - "pluginVersion": "9.0.0",
112 + "pluginVersion": "10.4.0",
113 "targets": [
114 {
115 "bucketAggs": [
@@ -144,12 +144,101 @@
144 "type": "stat"
145 },
146 {
147 - "columns": [],
147 "datasource": {
148 "type": "elasticsearch",
149 "uid": "replace_datasource_uid"
150 },
152 - "fontSize": "100%",
151 + "fieldConfig": {
152 + "defaults": {
153 + "color": {
154 + "mode": "thresholds"
155 + },
156 + "custom": {
157 + "align": "auto",
158 + "cellOptions": {
159 + "type": "auto"
160 + },
161 + "inspect": false
162 + },
163 + "mappings": [],
164 + "thresholds": {
165 + "mode": "absolute",
166 + "steps": [
167 + {
168 + "color": "green",
169 + "value": null
170 + },
171 + {
172 + "color": "red",
173 + "value": 80
174 + }
175 + ]
176 + }
177 + },
178 + "overrides": [
179 + {
180 + "matcher": {
181 + "id": "byName",
182 + "options": "Time"
183 + },
184 + "properties": [
185 + {
186 + "id": "displayName",
187 + "value": "Time"
188 + },
189 + {
190 + "id": "unit",
191 + "value": "time: YYYY-MM-DD HH:mm:ss"
192 + },
193 + {
194 + "id": "custom.align"
195 + }
196 + ]
197 + },
198 + {
199 + "matcher": {
200 + "id": "byName",
201 + "options": "Count"
202 + },
203 + "properties": [
204 + {
205 + "id": "unit",
206 + "value": "short"
207 + },
208 + {
209 + "id": "decimals",
210 + "value": -1
211 + },
212 + {
213 + "id": "custom.align"
214 + }
215 + ]
216 + },
217 + {
218 + "matcher": {
219 + "id": "byName",
220 + "options": "agent_name"
221 + },
222 + "properties": [
223 + {
224 + "id": "displayName",
225 + "value": "AGENT"
226 + },
227 + {
228 + "id": "unit",
229 + "value": "short"
230 + },
231 + {
232 + "id": "decimals",
233 + "value": 2
234 + },
235 + {
236 + "id": "custom.align"
237 + }
238 + ]
239 + }
240 + ]
241 + },
242 "gridPos": {
243 "h": 7,
244 "w": 6,
@@ -157,47 +246,17 @@
246 "y": 1
247 },
248 "id": 31,
160 - "showHeader": true,
161 - "sort": {
162 - "col": 0,
163 - "desc": true
164 - },
165 - "styles": [
166 - {
167 - "$$hashKey": "object:289",
168 - "alias": "Time",
169 - "align": "auto",
170 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
171 - "pattern": "Time",
172 - "type": "date"
173 - },
174 - {
175 - "$$hashKey": "object:290",
176 - "alias": "",
177 - "align": "auto",
178 - "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
179 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
180 - "decimals": -1,
181 - "mappingType": 1,
182 - "pattern": "Count",
183 - "thresholds": [],
184 - "type": "number",
185 - "unit": "short"
249 + "options": {
250 + "cellHeight": "sm",
251 + "footer": {
252 + "countRows": false,
253 + "fields": "",
254 + "reducer": ["sum"],
255 + "show": false
256 },
187 - {
188 - "$$hashKey": "object:291",
189 - "alias": "AGENT",
190 - "align": "auto",
191 - "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
192 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
193 - "decimals": 2,
194 - "mappingType": 1,
195 - "pattern": "agent_name",
196 - "thresholds": [],
197 - "type": "number",
198 - "unit": "short"
199 - }
200 - ],
257 + "showHeader": true
258 + },
259 + "pluginVersion": "10.4.0",
260 "targets": [
261 {
262 "bucketAggs": [
@@ -231,8 +290,15 @@
290 }
291 ],
292 "title": "DNS QUERIES / AGENTS",
234 - "transform": "table",
235 - "type": "table-old"
293 + "transformations": [
294 + {
295 + "id": "merge",
296 + "options": {
297 + "reducers": []
298 + }
299 + }
300 + ],
301 + "type": "table"
302 },
303 {
304 "datasource": {
@@ -245,6 +311,9 @@
311 "mode": "palette-classic"
312 },
313 "custom": {
314 + "axisBorderShow": false,
315 + "axisCenteredZero": false,
316 + "axisColorMode": "text",
317 "axisLabel": "",
318 "axisPlacement": "auto",
319 "barAlignment": 0,
@@ -256,6 +325,7 @@
325 "tooltip": false,
326 "viz": false
327 },
328 + "insertNulls": false,
329 "lineInterpolation": "linear",
330 "lineWidth": 1,
331 "pointSize": 5,
@@ -300,7 +370,8 @@
370 "legend": {
371 "calcs": [],
372 "displayMode": "table",
303 - "placement": "right"
373 + "placement": "right",
374 + "showLegend": true
375 },
376 "tooltip": {
377 "mode": "single",
@@ -380,13 +451,13 @@
451 "y": 8
452 },
453 "id": 65,
383 - "links": [],
454 "options": {
455 "displayLabels": [],
456 "legend": {
457 "calcs": [],
388 - "displayMode": "hidden",
458 + "displayMode": "list",
459 "placement": "right",
460 + "showLegend": false,
461 "values": ["value", "percent"]
462 },
463 "pieType": "pie",
@@ -462,7 +533,9 @@
533 "defaults": {
534 "custom": {
535 "align": "auto",
465 - "displayMode": "auto",
536 + "cellOptions": {
537 + "type": "auto"
538 + },
539 "filterable": false,
540 "inspect": false
541 },
@@ -504,7 +577,9 @@
577 },
578 "id": 71,
579 "options": {
580 + "cellHeight": "sm",
581 "footer": {
582 + "countRows": false,
583 "fields": "",
584 "reducer": ["sum"],
585 "show": false
@@ -512,7 +587,7 @@
587 "showHeader": true,
588 "sortBy": []
589 },
515 - "pluginVersion": "9.0.0",
590 + "pluginVersion": "10.4.0",
591 "targets": [
592 {
593 "bucketAggs": [
@@ -585,8 +660,10 @@
660 "id": 59,
661 "options": {
662 "displayMode": "gradient",
663 + "maxVizHeight": 300,
664 "minVizHeight": 10,
665 "minVizWidth": 0,
666 + "namePlacement": "auto",
667 "orientation": "horizontal",
668 "reduceOptions": {
669 "calcs": ["sum"],
@@ -594,9 +671,11 @@
671 "values": false
672 },
673 "showUnfilled": true,
597 - "text": {}
674 + "sizing": "auto",
675 + "text": {},
676 + "valueMode": "color"
677 },
599 - "pluginVersion": "9.0.0",
678 + "pluginVersion": "10.4.0",
679 "targets": [
680 {
681 "bucketAggs": [
@@ -657,7 +736,9 @@
736 "defaults": {
737 "custom": {
738 "align": "auto",
660 - "displayMode": "auto",
739 + "cellOptions": {
740 + "type": "auto"
741 + },
742 "filterable": false,
743 "inspect": false
744 },
@@ -699,7 +780,9 @@
780 },
781 "id": 70,
782 "options": {
783 + "cellHeight": "sm",
784 "footer": {
785 + "countRows": false,
786 "fields": "",
787 "reducer": ["sum"],
788 "show": false
@@ -707,7 +790,7 @@
790 "showHeader": true,
791 "sortBy": []
792 },
710 - "pluginVersion": "9.0.0",
793 + "pluginVersion": "10.4.0",
794 "targets": [
795 {
796 "bucketAggs": [
@@ -759,7 +842,8 @@
842 "mode": "absolute",
843 "steps": [
844 {
762 - "color": "green"
845 + "color": "green",
846 + "value": null
847 },
848 {
849 "color": "red",
@@ -779,8 +863,10 @@
863 "id": 55,
864 "options": {
865 "displayMode": "gradient",
866 + "maxVizHeight": 300,
867 "minVizHeight": 10,
868 "minVizWidth": 0,
869 + "namePlacement": "auto",
870 "orientation": "horizontal",
871 "reduceOptions": {
872 "calcs": ["sum"],
@@ -788,9 +874,11 @@
874 "values": false
875 },
876 "showUnfilled": true,
791 - "text": {}
877 + "sizing": "auto",
878 + "text": {},
879 + "valueMode": "color"
880 },
793 - "pluginVersion": "9.0.0",
881 + "pluginVersion": "10.4.0",
882 "targets": [
883 {
884 "bucketAggs": [
@@ -854,7 +942,9 @@
942 },
943 "custom": {
944 "align": "auto",
857 - "displayMode": "auto",
945 + "cellOptions": {
946 + "type": "auto"
947 + },
948 "inspect": false
949 },
950 "mappings": [],
@@ -862,7 +952,8 @@
952 "mode": "absolute",
953 "steps": [
954 {
865 - "color": "green"
955 + "color": "green",
956 + "value": null
957 },
958 {
959 "color": "red",
@@ -943,8 +1034,11 @@
1034 "value": -2
1035 },
1036 {
946 - "id": "custom.displayMode",
947 - "value": "color-background"
1037 + "id": "custom.cellOptions",
1038 + "value": {
1039 + "mode": "gradient",
1040 + "type": "color-background"
1041 + }
1042 },
1043 {
1044 "id": "custom.align"
@@ -955,7 +1049,8 @@
1049 "mode": "absolute",
1050 "steps": [
1051 {
958 - "color": "#37872D"
1052 + "color": "#37872D",
1053 + "value": null
1054 },
1055 {
1056 "color": "#37872D",
@@ -1136,14 +1231,16 @@
1231 },
1232 "id": 69,
1233 "options": {
1234 + "cellHeight": "sm",
1235 "footer": {
1236 + "countRows": false,
1237 "fields": "",
1238 "reducer": ["sum"],
1239 "show": false
1240 },
1241 "showHeader": true
1242 },
1146 - "pluginVersion": "9.0.0",
1243 + "pluginVersion": "10.4.0",
1244 "targets": [
1245 {
1246 "bucketAggs": [],
@@ -1212,8 +1309,7 @@
1309 }
1310 ],
1311 "refresh": false,
1215 - "schemaVersion": 36,
1216 - "style": "dark",
1312 + "schemaVersion": 39,
1313 "tags": ["EDR"],
1314 "templating": {
1315 "list": [
@@ -1268,7 +1364,5 @@
1364 },
1365 "timezone": "",
1366 "title": "EDR - DNS REQUESTS",
1271 - "uid": null,
1272 - "version": 4,
1367 "weekStart": ""
1368 }
backend/app/connectors/grafana/dashboards/Wazuh/edr_fim.json
+106 -111
@@ -94,7 +94,6 @@
94 "y": 1
95 },
96 "id": 137,
97 - "links": [],
97 "options": {
98 "colorMode": "value",
99 "graphMode": "area",
@@ -105,10 +104,12 @@
104 "fields": "",
105 "values": false
106 },
107 + "showPercentChange": false,
108 "text": {},
109 - "textMode": "auto"
109 + "textMode": "auto",
110 + "wideLayout": true
111 },
111 - "pluginVersion": "10.0.2",
112 + "pluginVersion": "10.4.0",
113 "targets": [
114 {
115 "bucketAggs": [
@@ -314,7 +315,6 @@
315 "y": 1
316 },
317 "id": 138,
317 - "links": [],
318 "maxDataPoints": 3,
319 "options": {
320 "cellHeight": "sm",
@@ -327,7 +327,7 @@
327 "showHeader": true,
328 "sortBy": []
329 },
330 - "pluginVersion": "10.0.2",
330 + "pluginVersion": "10.4.0",
331 "targets": [
332 {
333 "bucketAggs": [
@@ -442,7 +442,6 @@
442 "y": 9
443 },
444 "id": 113,
445 - "links": [],
445 "options": {
446 "colorMode": "value",
447 "graphMode": "area",
@@ -453,10 +452,12 @@
452 "fields": "",
453 "values": false
454 },
455 + "showPercentChange": false,
456 "text": {},
457 - "textMode": "auto"
457 + "textMode": "auto",
458 + "wideLayout": true
459 },
459 - "pluginVersion": "10.0.2",
460 + "pluginVersion": "10.4.0",
461 "targets": [
462 {
463 "bucketAggs": [
@@ -733,7 +734,6 @@
734 "y": 9
735 },
736 "id": 68,
736 - "links": [],
737 "maxDataPoints": 3,
738 "options": {
739 "displayLabels": [],
@@ -982,7 +982,6 @@
982 "y": 9
983 },
984 "id": 115,
985 - "links": [],
985 "maxDataPoints": 3,
986 "options": {
987 "cellHeight": "sm",
@@ -995,7 +994,7 @@
994 "showHeader": true,
995 "sortBy": []
996 },
998 - "pluginVersion": "10.0.2",
997 + "pluginVersion": "10.4.0",
998 "targets": [
999 {
1000 "bucketAggs": [
@@ -1111,7 +1110,6 @@
1110 "y": 17
1111 },
1112 "id": 70,
1114 - "links": [],
1113 "maxDataPoints": 3,
1114 "options": {
1115 "cellHeight": "sm",
@@ -1124,7 +1122,7 @@
1122 "showHeader": true,
1123 "sortBy": []
1124 },
1127 - "pluginVersion": "10.0.2",
1125 + "pluginVersion": "10.4.0",
1126 "targets": [
1127 {
1128 "bucketAggs": [
@@ -1175,52 +1173,86 @@
1173 "type": "table"
1174 },
1175 {
1178 - "aliasColors": {},
1179 - "bars": true,
1180 - "dashLength": 10,
1181 - "dashes": false,
1176 "datasource": {
1177 "type": "elasticsearch",
1178 "uid": "replace_datasource_uid"
1179 },
1186 - "fill": 1,
1187 - "fillGradient": 0,
1180 + "fieldConfig": {
1181 + "defaults": {
1182 + "color": {
1183 + "mode": "palette-classic"
1184 + },
1185 + "custom": {
1186 + "axisBorderShow": false,
1187 + "axisCenteredZero": false,
1188 + "axisColorMode": "text",
1189 + "axisLabel": "",
1190 + "axisPlacement": "auto",
1191 + "barAlignment": 0,
1192 + "drawStyle": "bars",
1193 + "fillOpacity": 100,
1194 + "gradientMode": "none",
1195 + "hideFrom": {
1196 + "legend": false,
1197 + "tooltip": false,
1198 + "viz": false
1199 + },
1200 + "insertNulls": false,
1201 + "lineInterpolation": "linear",
1202 + "lineWidth": 1,
1203 + "pointSize": 5,
1204 + "scaleDistribution": {
1205 + "type": "linear"
1206 + },
1207 + "showPoints": "never",
1208 + "spanNulls": false,
1209 + "stacking": {
1210 + "group": "A",
1211 + "mode": "normal"
1212 + },
1213 + "thresholdsStyle": {
1214 + "mode": "off"
1215 + }
1216 + },
1217 + "mappings": [],
1218 + "thresholds": {
1219 + "mode": "absolute",
1220 + "steps": [
1221 + {
1222 + "color": "green",
1223 + "value": null
1224 + },
1225 + {
1226 + "color": "red",
1227 + "value": 80
1228 + }
1229 + ]
1230 + },
1231 + "unit": "short"
1232 + },
1233 + "overrides": []
1234 + },
1235 "gridPos": {
1236 "h": 7,
1237 "w": 15,
1238 "x": 9,
1239 "y": 17
1240 },
1194 - "hiddenSeries": false,
1241 "id": 83,
1196 - "legend": {
1197 - "alignAsTable": true,
1198 - "avg": false,
1199 - "current": false,
1200 - "max": false,
1201 - "min": false,
1202 - "rightSide": true,
1203 - "show": true,
1204 - "total": false,
1205 - "values": false
1206 - },
1207 - "lines": false,
1208 - "linewidth": 1,
1209 - "links": [],
1242 "maxDataPoints": 3,
1211 - "nullPointMode": "null",
1243 "options": {
1213 - "alertThreshold": true
1244 + "legend": {
1245 + "calcs": [],
1246 + "displayMode": "table",
1247 + "placement": "right",
1248 + "showLegend": true
1249 + },
1250 + "tooltip": {
1251 + "mode": "multi",
1252 + "sort": "none"
1253 + }
1254 },
1215 - "percentage": false,
1216 - "pluginVersion": "10.0.2",
1217 - "pointradius": 2,
1218 - "points": false,
1219 - "renderer": "flot",
1220 - "seriesOverrides": [],
1221 - "spaceLength": 10,
1222 - "stack": true,
1223 - "steppedLine": false,
1255 + "pluginVersion": "10.4.0",
1256 "targets": [
1257 {
1258 "alias": "",
@@ -1264,35 +1296,8 @@
1296 "timeField": "timestamp"
1297 }
1298 ],
1267 - "thresholds": [],
1268 - "timeRegions": [],
1299 "title": "EVENTS BY AGENT (HISTOGRAM)",
1270 - "tooltip": {
1271 - "shared": true,
1272 - "sort": 0,
1273 - "value_type": "individual"
1274 - },
1275 - "type": "graph",
1276 - "xaxis": {
1277 - "mode": "time",
1278 - "show": true,
1279 - "values": []
1280 - },
1281 - "yaxes": [
1282 - {
1283 - "format": "short",
1284 - "logBase": 1,
1285 - "show": true
1286 - },
1287 - {
1288 - "format": "short",
1289 - "logBase": 1,
1290 - "show": true
1291 - }
1292 - ],
1293 - "yaxis": {
1294 - "align": false
1295 - }
1300 + "type": "timeseries"
1301 },
1302 {
1303 "datasource": {
@@ -1501,7 +1506,7 @@
1506 "showHeader": true,
1507 "sortBy": []
1508 },
1504 - "pluginVersion": "10.0.2",
1509 + "pluginVersion": "10.4.0",
1510 "targets": [
1511 {
1512 "alias": "",
@@ -1825,7 +1830,6 @@
1830 "y": 35
1831 },
1832 "id": 119,
1828 - "links": [],
1833 "options": {
1834 "colorMode": "value",
1835 "graphMode": "area",
@@ -1836,10 +1840,12 @@
1840 "fields": "",
1841 "values": false
1842 },
1843 + "showPercentChange": false,
1844 "text": {},
1840 - "textMode": "auto"
1845 + "textMode": "auto",
1846 + "wideLayout": true
1847 },
1842 - "pluginVersion": "10.0.2",
1848 + "pluginVersion": "10.4.0",
1849 "targets": [
1850 {
1851 "bucketAggs": [
@@ -1927,7 +1933,6 @@
1933 "y": 35
1934 },
1935 "id": 120,
1930 - "links": [],
1936 "maxDataPoints": 3,
1937 "options": {
1938 "cellHeight": "sm",
@@ -1940,7 +1945,7 @@
1945 "showHeader": true,
1946 "sortBy": []
1947 },
1943 - "pluginVersion": "10.0.2",
1948 + "pluginVersion": "10.4.0",
1949 "targets": [
1950 {
1951 "bucketAggs": [
@@ -2247,7 +2252,6 @@
2252 "y": 35
2253 },
2254 "id": 121,
2250 - "links": [],
2255 "maxDataPoints": 3,
2256 "options": {
2257 "cellHeight": "sm",
@@ -2260,7 +2264,7 @@
2264 "showHeader": true,
2265 "sortBy": []
2266 },
2263 - "pluginVersion": "10.0.2",
2267 + "pluginVersion": "10.4.0",
2268 "targets": [
2269 {
2270 "bucketAggs": [
@@ -2434,7 +2438,6 @@
2438 "y": 42
2439 },
2440 "id": 132,
2437 - "links": [],
2441 "maxDataPoints": 3,
2442 "options": {
2443 "cellHeight": "sm",
@@ -2447,7 +2450,7 @@
2450 "showHeader": true,
2451 "sortBy": []
2452 },
2450 - "pluginVersion": "10.0.2",
2453 + "pluginVersion": "10.4.0",
2454 "targets": [
2455 {
2456 "bucketAggs": [
@@ -2634,7 +2637,6 @@
2637 "y": 42
2638 },
2639 "id": 133,
2637 - "links": [],
2640 "maxDataPoints": 3,
2641 "options": {
2642 "cellHeight": "sm",
@@ -2648,7 +2650,7 @@
2650 "showHeader": true,
2651 "sortBy": []
2652 },
2651 - "pluginVersion": "10.0.2",
2653 + "pluginVersion": "10.4.0",
2654 "targets": [
2655 {
2656 "bucketAggs": [
@@ -2923,7 +2925,7 @@
2925 "showHeader": true,
2926 "sortBy": []
2927 },
2926 - "pluginVersion": "10.0.2",
2928 + "pluginVersion": "10.4.0",
2929 "targets": [
2930 {
2931 "alias": "",
@@ -3380,7 +3382,6 @@
3382 "y": 36
3383 },
3384 "id": 127,
3383 - "links": [],
3385 "options": {
3386 "colorMode": "value",
3387 "graphMode": "area",
@@ -3391,10 +3392,12 @@
3392 "fields": "",
3393 "values": false
3394 },
3395 + "showPercentChange": false,
3396 "text": {},
3395 - "textMode": "auto"
3397 + "textMode": "auto",
3398 + "wideLayout": true
3399 },
3397 - "pluginVersion": "10.0.2",
3400 + "pluginVersion": "10.4.0",
3401 "targets": [
3402 {
3403 "bucketAggs": [
@@ -3482,7 +3485,6 @@
3485 "y": 36
3486 },
3487 "id": 128,
3485 - "links": [],
3488 "maxDataPoints": 3,
3489 "options": {
3490 "cellHeight": "sm",
@@ -3495,7 +3497,7 @@
3497 "showHeader": true,
3498 "sortBy": []
3499 },
3498 - "pluginVersion": "10.0.2",
3500 + "pluginVersion": "10.4.0",
3501 "targets": [
3502 {
3503 "bucketAggs": [
@@ -3756,7 +3758,6 @@
3758 "y": 36
3759 },
3760 "id": 129,
3759 - "links": [],
3761 "maxDataPoints": 3,
3762 "options": {
3763 "cellHeight": "sm",
@@ -3769,7 +3770,7 @@
3770 "showHeader": true,
3771 "sortBy": []
3772 },
3772 - "pluginVersion": "10.0.2",
3773 + "pluginVersion": "10.4.0",
3774 "targets": [
3775 {
3776 "bucketAggs": [
@@ -3943,7 +3944,6 @@
3944 "y": 43
3945 },
3946 "id": 134,
3946 - "links": [],
3947 "maxDataPoints": 3,
3948 "options": {
3949 "cellHeight": "sm",
@@ -3956,7 +3956,7 @@
3956 "showHeader": true,
3957 "sortBy": []
3958 },
3959 - "pluginVersion": "10.0.2",
3959 + "pluginVersion": "10.4.0",
3960 "targets": [
3961 {
3962 "bucketAggs": [
@@ -4143,7 +4143,6 @@
4143 "y": 43
4144 },
4145 "id": 135,
4146 - "links": [],
4146 "maxDataPoints": 3,
4147 "options": {
4148 "cellHeight": "sm",
@@ -4156,7 +4155,7 @@
4155 "showHeader": true,
4156 "sortBy": []
4157 },
4159 - "pluginVersion": "10.0.2",
4158 + "pluginVersion": "10.4.0",
4159 "targets": [
4160 {
4161 "bucketAggs": [
@@ -4389,7 +4388,7 @@
4388 "showHeader": true,
4389 "sortBy": []
4390 },
4392 - "pluginVersion": "10.0.2",
4391 + "pluginVersion": "10.4.0",
4392 "targets": [
4393 {
4394 "alias": "",
@@ -4908,7 +4907,6 @@
4907 "y": 37
4908 },
4909 "id": 100,
4911 - "links": [],
4910 "options": {
4911 "colorMode": "value",
4912 "graphMode": "area",
@@ -4919,10 +4917,12 @@
4917 "fields": "",
4918 "values": false
4919 },
4920 + "showPercentChange": false,
4921 "text": {},
4923 - "textMode": "auto"
4922 + "textMode": "auto",
4923 + "wideLayout": true
4924 },
4925 - "pluginVersion": "10.0.2",
4925 + "pluginVersion": "10.4.0",
4926 "targets": [
4927 {
4928 "bucketAggs": [
@@ -5010,7 +5010,6 @@
5010 "y": 37
5011 },
5012 "id": 136,
5013 - "links": [],
5013 "maxDataPoints": 3,
5014 "options": {
5015 "cellHeight": "sm",
@@ -5023,7 +5022,7 @@
5022 "showHeader": true,
5023 "sortBy": []
5024 },
5026 - "pluginVersion": "10.0.2",
5025 + "pluginVersion": "10.4.0",
5026 "targets": [
5027 {
5028 "bucketAggs": [
@@ -5224,7 +5223,6 @@
5223 "y": 37
5224 },
5225 "id": 101,
5227 - "links": [],
5226 "maxDataPoints": 3,
5227 "options": {
5228 "displayLabels": [],
@@ -5347,7 +5345,6 @@
5345 "y": 37
5346 },
5347 "id": 106,
5350 - "links": [],
5348 "maxDataPoints": 3,
5349 "options": {
5350 "cellHeight": "sm",
@@ -5359,7 +5356,7 @@
5356 },
5357 "showHeader": true
5358 },
5362 - "pluginVersion": "10.0.2",
5359 + "pluginVersion": "10.4.0",
5360 "targets": [
5361 {
5362 "bucketAggs": [
@@ -5500,7 +5497,6 @@
5497 "y": 44
5498 },
5499 "id": 102,
5503 - "links": [],
5500 "maxDataPoints": 3,
5501 "options": {
5502 "cellHeight": "sm",
@@ -5513,7 +5509,7 @@
5509 "showHeader": true,
5510 "sortBy": []
5511 },
5516 - "pluginVersion": "10.0.2",
5512 + "pluginVersion": "10.4.0",
5513 "targets": [
5514 {
5515 "bucketAggs": [
@@ -5843,7 +5839,7 @@
5839 "showHeader": true,
5840 "sortBy": []
5841 },
5846 - "pluginVersion": "10.0.2",
5842 + "pluginVersion": "10.4.0",
5843 "targets": [
5844 {
5845 "alias": "",
@@ -6179,8 +6175,7 @@
6175 }
6176 ],
6177 "refresh": "",
6182 - "schemaVersion": 38,
6183 - "style": "dark",
6178 + "schemaVersion": 39,
6179 "tags": ["EDR"],
6180 "templating": {
6181 "list": [
backend/app/connectors/grafana/dashboards/Wazuh/edr_network_connections.json new
+2111
@@ -0,0 +1,2111 @@
1 +{
2 + "annotations": {
3 + "list": [
4 + {
5 + "builtIn": 1,
6 + "datasource": {
7 + "type": "datasource",
8 + "uid": "grafana"
9 + },
10 + "enable": true,
11 + "hide": true,
12 + "iconColor": "rgba(0, 211, 255, 1)",
13 + "name": "Annotations & Alerts",
14 + "target": {
15 + "limit": 100,
16 + "matchAny": false,
17 + "tags": [],
18 + "type": "dashboard"
19 + },
20 + "type": "dashboard"
21 + }
22 + ]
23 + },
24 + "editable": false,
25 + "fiscalYearStartMonth": 0,
26 + "graphTooltip": 0,
27 + "id": null,
28 + "links": [
29 + {
30 + "asDropdown": true,
31 + "icon": "external link",
32 + "includeVars": true,
33 + "keepTime": true,
34 + "tags": ["EDR"],
35 + "targetBlank": true,
36 + "title": "",
37 + "type": "dashboards"
38 + }
39 + ],
40 + "liveNow": false,
41 + "panels": [
42 + {
43 + "datasource": {
44 + "type": "elasticsearch",
45 + "uid": "replace_datasource_uid"
46 + },
47 + "fieldConfig": {
48 + "defaults": {
49 + "mappings": [
50 + {
51 + "options": {
52 + "match": "null",
53 + "result": {
54 + "text": "N/A"
55 + }
56 + },
57 + "type": "special"
58 + }
59 + ],
60 + "thresholds": {
61 + "mode": "absolute",
62 + "steps": [
63 + {
64 + "color": "dark-orange",
65 + "value": null
66 + }
67 + ]
68 + },
69 + "unit": "none"
70 + },
71 + "overrides": []
72 + },
73 + "gridPos": {
74 + "h": 7,
75 + "w": 4,
76 + "x": 0,
77 + "y": 0
78 + },
79 + "id": 56,
80 + "options": {
81 + "colorMode": "value",
82 + "graphMode": "area",
83 + "justifyMode": "auto",
84 + "orientation": "horizontal",
85 + "reduceOptions": {
86 + "calcs": ["sum"],
87 + "fields": "",
88 + "values": false
89 + },
90 + "showPercentChange": false,
91 + "text": {},
92 + "textMode": "auto",
93 + "wideLayout": true
94 + },
95 + "pluginVersion": "10.4.0",
96 + "targets": [
97 + {
98 + "bucketAggs": [
99 + {
100 + "field": "timestamp",
101 + "id": "2",
102 + "settings": {
103 + "interval": "auto",
104 + "min_doc_count": 0,
105 + "trimEdges": 0
106 + },
107 + "type": "date_histogram"
108 + }
109 + ],
110 + "datasource": {
111 + "type": "elasticsearch",
112 + "uid": "replace_datasource_uid"
113 + },
114 + "metrics": [
115 + {
116 + "field": "select field",
117 + "id": "1",
118 + "type": "count"
119 + }
120 + ],
121 + "query": "agent_name:$agent_name AND ((rule_group2:opencti AND data_opencti_entity_type:IPv4-Addr) OR (_exists_:misp_category AND misp_type:ip-dst))",
122 + "refId": "A",
123 + "timeField": "timestamp"
124 + }
125 + ],
126 + "title": "FLAGGED IPs",
127 + "type": "stat"
128 + },
129 + {
130 + "datasource": {
131 + "type": "elasticsearch",
132 + "uid": "replace_datasource_uid"
133 + },
134 + "fieldConfig": {
135 + "defaults": {
136 + "color": {
137 + "mode": "thresholds"
138 + },
139 + "custom": {
140 + "align": "auto",
141 + "cellOptions": {
142 + "type": "auto"
143 + },
144 + "inspect": false
145 + },
146 + "decimals": -1,
147 + "mappings": [],
148 + "thresholds": {
149 + "mode": "absolute",
150 + "steps": [
151 + {
152 + "color": "green",
153 + "value": null
154 + },
155 + {
156 + "color": "red",
157 + "value": 80
158 + }
159 + ]
160 + },
161 + "unit": "short"
162 + },
163 + "overrides": [
164 + {
165 + "matcher": {
166 + "id": "byName",
167 + "options": "Count"
168 + },
169 + "properties": [
170 + {
171 + "id": "unit",
172 + "value": "short"
173 + },
174 + {
175 + "id": "decimals",
176 + "value": -1
177 + },
178 + {
179 + "id": "custom.cellOptions",
180 + "value": {
181 + "mode": "gradient",
182 + "type": "color-background"
183 + }
184 + },
185 + {
186 + "id": "custom.align"
187 + },
188 + {
189 + "id": "thresholds",
190 + "value": {
191 + "mode": "absolute",
192 + "steps": [
193 + {
194 + "color": "rgba(50, 172, 45, 0.97)",
195 + "value": null
196 + },
197 + {
198 + "color": "rgba(237, 129, 40, 0.89)",
199 + "value": 0
200 + },
201 + {
202 + "color": "#FA6400",
203 + "value": 1
204 + }
205 + ]
206 + }
207 + }
208 + ]
209 + },
210 + {
211 + "matcher": {
212 + "id": "byName",
213 + "options": "agent_name"
214 + },
215 + "properties": [
216 + {
217 + "id": "displayName",
218 + "value": "AGENT"
219 + },
220 + {
221 + "id": "unit",
222 + "value": "short"
223 + },
224 + {
225 + "id": "decimals",
226 + "value": 2
227 + },
228 + {
229 + "id": "custom.align"
230 + }
231 + ]
232 + }
233 + ]
234 + },
235 + "gridPos": {
236 + "h": 7,
237 + "w": 8,
238 + "x": 4,
239 + "y": 0
240 + },
241 + "id": 57,
242 + "options": {
243 + "cellHeight": "sm",
244 + "footer": {
245 + "countRows": false,
246 + "fields": "",
247 + "reducer": ["sum"],
248 + "show": false
249 + },
250 + "showHeader": true
251 + },
252 + "pluginVersion": "10.4.0",
253 + "targets": [
254 + {
255 + "bucketAggs": [
256 + {
257 + "$$hashKey": "object:140",
258 + "fake": true,
259 + "field": "agent_name",
260 + "id": "4",
261 + "settings": {
262 + "min_doc_count": 1,
263 + "order": "desc",
264 + "orderBy": "_term",
265 + "size": "0"
266 + },
267 + "type": "terms"
268 + }
269 + ],
270 + "datasource": {
271 + "type": "elasticsearch",
272 + "uid": "replace_datasource_uid"
273 + },
274 + "metrics": [
275 + {
276 + "$$hashKey": "object:138",
277 + "field": "select field",
278 + "id": "1",
279 + "type": "count"
280 + }
281 + ],
282 + "query": "agent_name:$agent_name AND ((rule_group2:opencti AND data_opencti_entity_type:IPv4-Addr) OR (_exists_:misp_category AND misp_type:ip-dst OR misp_type:\"ip-dst|port\"))",
283 + "refId": "A",
284 + "timeField": "timestamp"
285 + }
286 + ],
287 + "title": "FLAGGED DST IPs / AGENT",
288 + "transformations": [
289 + {
290 + "id": "merge",
291 + "options": {
292 + "reducers": []
293 + }
294 + }
295 + ],
296 + "type": "table"
297 + },
298 + {
299 + "datasource": {
300 + "type": "elasticsearch",
301 + "uid": "replace_datasource_uid"
302 + },
303 + "fieldConfig": {
304 + "defaults": {
305 + "color": {
306 + "mode": "thresholds"
307 + },
308 + "custom": {
309 + "align": "auto",
310 + "cellOptions": {
311 + "type": "auto"
312 + },
313 + "inspect": false
314 + },
315 + "mappings": [],
316 + "thresholds": {
317 + "mode": "absolute",
318 + "steps": [
319 + {
320 + "color": "dark-orange",
321 + "value": null
322 + }
323 + ]
324 + }
325 + },
326 + "overrides": [
327 + {
328 + "matcher": {
329 + "id": "byName",
330 + "options": "data_misp_value"
331 + },
332 + "properties": [
333 + {
334 + "id": "displayName",
335 + "value": "DST IP"
336 + },
337 + {
338 + "id": "custom.align"
339 + }
340 + ]
341 + },
342 + {
343 + "matcher": {
344 + "id": "byName",
345 + "options": "Count"
346 + },
347 + "properties": [
348 + {
349 + "id": "unit",
350 + "value": "short"
351 + },
352 + {
353 + "id": "decimals",
354 + "value": -1
355 + },
356 + {
357 + "id": "custom.align"
358 + }
359 + ]
360 + },
361 + {
362 + "matcher": {
363 + "id": "byName",
364 + "options": "data_opencti_value"
365 + },
366 + "properties": [
367 + {
368 + "id": "custom.cellOptions",
369 + "value": {
370 + "mode": "basic",
371 + "type": "color-background"
372 + }
373 + },
374 + {
375 + "id": "displayName",
376 + "value": "DST IP"
377 + }
378 + ]
379 + },
380 + {
381 + "matcher": {
382 + "id": "byName",
383 + "options": "misp_value"
384 + },
385 + "properties": [
386 + {
387 + "id": "links",
388 + "value": [
389 + {
390 + "targetBlank": true,
391 + "title": "TALOS THREAT INTEL",
392 + "url": "https://talosintelligence.com/reputation_center/lookup?search=${__value.text}"
393 + }
394 + ]
395 + }
396 + ]
397 + }
398 + ]
399 + },
400 + "gridPos": {
401 + "h": 7,
402 + "w": 12,
403 + "x": 12,
404 + "y": 0
405 + },
406 + "id": 75,
407 + "options": {
408 + "cellHeight": "sm",
409 + "footer": {
410 + "countRows": false,
411 + "fields": "",
412 + "reducer": ["sum"],
413 + "show": false
414 + },
415 + "showHeader": true
416 + },
417 + "pluginVersion": "10.4.0",
418 + "targets": [
419 + {
420 + "bucketAggs": [
421 + {
422 + "fake": true,
423 + "field": "misp_value",
424 + "id": "4",
425 + "settings": {
426 + "min_doc_count": 1,
427 + "order": "desc",
428 + "orderBy": "_count",
429 + "size": "0"
430 + },
431 + "type": "terms"
432 + }
433 + ],
434 + "datasource": {
435 + "type": "elasticsearch",
436 + "uid": "replace_datasource_uid"
437 + },
438 + "metrics": [
439 + {
440 + "field": "select field",
441 + "id": "1",
442 + "type": "count"
443 + }
444 + ],
445 + "query": "agent_name:$agent_name AND _exists_:misp_category AND (misp_type:ip-dst OR misp_type:\"ip-dst|port\")",
446 + "refId": "A",
447 + "timeField": "timestamp"
448 + },
449 + {
450 + "alias": "",
451 + "bucketAggs": [
452 + {
453 + "field": "data_opencti_value",
454 + "id": "2",
455 + "settings": {
456 + "min_doc_count": "1",
457 + "order": "desc",
458 + "orderBy": "_term",
459 + "size": "10"
460 + },
461 + "type": "terms"
462 + }
463 + ],
464 + "datasource": {
465 + "type": "elasticsearch",
466 + "uid": "replace_datasource_uid"
467 + },
468 + "hide": false,
469 + "metrics": [
470 + {
471 + "id": "1",
472 + "type": "count"
473 + }
474 + ],
475 + "query": "rule_group2:opencti AND agent_name:$agent_name AND data_opencti_entity_type:IPv4-Addr",
476 + "refId": "B",
477 + "timeField": "timestamp"
478 + }
479 + ],
480 + "title": "FLAGGED IPs",
481 + "transformations": [
482 + {
483 + "id": "merge",
484 + "options": {
485 + "reducers": []
486 + }
487 + }
488 + ],
489 + "type": "table"
490 + },
491 + {
492 + "datasource": {
493 + "type": "elasticsearch",
494 + "uid": "replace_datasource_uid"
495 + },
496 + "fieldConfig": {
497 + "defaults": {
498 + "mappings": [
499 + {
500 + "options": {
501 + "match": "null",
502 + "result": {
503 + "text": "N/A"
504 + }
505 + },
506 + "type": "special"
507 + }
508 + ],
509 + "thresholds": {
510 + "mode": "absolute",
511 + "steps": [
512 + {
513 + "color": "blue",
514 + "value": null
515 + }
516 + ]
517 + },
518 + "unit": "short"
519 + },
520 + "overrides": []
521 + },
522 + "gridPos": {
523 + "h": 7,
524 + "w": 4,
525 + "x": 0,
526 + "y": 7
527 + },
528 + "id": 43,
529 + "options": {
530 + "colorMode": "value",
531 + "graphMode": "area",
532 + "justifyMode": "auto",
533 + "orientation": "horizontal",
534 + "reduceOptions": {
535 + "calcs": ["sum"],
536 + "fields": "",
537 + "values": false
538 + },
539 + "showPercentChange": false,
540 + "text": {},
541 + "textMode": "auto",
542 + "wideLayout": true
543 + },
544 + "pluginVersion": "10.4.0",
545 + "targets": [
546 + {
547 + "bucketAggs": [
548 + {
549 + "$$hashKey": "object:473",
550 + "field": "timestamp",
551 + "id": "2",
552 + "settings": {
553 + "interval": "auto",
554 + "min_doc_count": 0,
555 + "trimEdges": 0
556 + },
557 + "type": "date_histogram"
558 + }
559 + ],
560 + "datasource": {
561 + "type": "elasticsearch",
562 + "uid": "replace_datasource_uid"
563 + },
564 + "metrics": [
565 + {
566 + "$$hashKey": "object:471",
567 + "field": "select field",
568 + "id": "1",
569 + "type": "count"
570 + }
571 + ],
572 + "query": "(rule_group3:sysmon_event3 OR rule_group2:packetbeat) AND agent_name:$agent_name",
573 + "refId": "A",
574 + "timeField": "timestamp"
575 + }
576 + ],
577 + "title": "NETWORK EVENTS",
578 + "type": "stat"
579 + },
580 + {
581 + "datasource": {
582 + "type": "elasticsearch",
583 + "uid": "replace_datasource_uid"
584 + },
585 + "fieldConfig": {
586 + "defaults": {
587 + "color": {
588 + "mode": "thresholds"
589 + },
590 + "custom": {
591 + "align": "auto",
592 + "cellOptions": {
593 + "type": "auto"
594 + },
595 + "inspect": false
596 + },
597 + "mappings": [],
598 + "thresholds": {
599 + "mode": "absolute",
600 + "steps": [
601 + {
602 + "color": "green",
603 + "value": null
604 + },
605 + {
606 + "color": "red",
607 + "value": 80
608 + }
609 + ]
610 + }
611 + },
612 + "overrides": [
613 + {
614 + "matcher": {
615 + "id": "byName",
616 + "options": "Time"
617 + },
618 + "properties": [
619 + {
620 + "id": "displayName",
621 + "value": "Time"
622 + },
623 + {
624 + "id": "unit",
625 + "value": "time: YYYY-MM-DD HH:mm:ss"
626 + },
627 + {
628 + "id": "custom.align"
629 + }
630 + ]
631 + },
632 + {
633 + "matcher": {
634 + "id": "byName",
635 + "options": "Count"
636 + },
637 + "properties": [
638 + {
639 + "id": "unit",
640 + "value": "short"
641 + },
642 + {
643 + "id": "decimals",
644 + "value": -1
645 + },
646 + {
647 + "id": "custom.align"
648 + }
649 + ]
650 + },
651 + {
652 + "matcher": {
653 + "id": "byName",
654 + "options": "agent_name"
655 + },
656 + "properties": [
657 + {
658 + "id": "displayName",
659 + "value": "AGENT"
660 + },
661 + {
662 + "id": "unit",
663 + "value": "short"
664 + },
665 + {
666 + "id": "decimals",
667 + "value": 2
668 + },
669 + {
670 + "id": "custom.align"
671 + }
672 + ]
673 + }
674 + ]
675 + },
676 + "gridPos": {
677 + "h": 7,
678 + "w": 8,
679 + "x": 4,
680 + "y": 7
681 + },
682 + "id": 63,
683 + "options": {
684 + "cellHeight": "sm",
685 + "footer": {
686 + "countRows": false,
687 + "fields": "",
688 + "reducer": ["sum"],
689 + "show": false
690 + },
691 + "showHeader": true
692 + },
693 + "pluginVersion": "10.4.0",
694 + "targets": [
695 + {
696 + "bucketAggs": [
697 + {
698 + "fake": true,
699 + "field": "agent_name",
700 + "id": "4",
701 + "settings": {
702 + "min_doc_count": 1,
703 + "order": "desc",
704 + "orderBy": "_count",
705 + "size": "0"
706 + },
707 + "type": "terms"
708 + }
709 + ],
710 + "datasource": {
711 + "type": "elasticsearch",
712 + "uid": "replace_datasource_uid"
713 + },
714 + "metrics": [
715 + {
716 + "field": "select field",
717 + "id": "1",
718 + "type": "count"
719 + }
720 + ],
721 + "query": "(rule_group3:sysmon_event3 OR rule_group2:packetbeat) AND agent_name:$agent_name",
722 + "refId": "A",
723 + "timeField": "timestamp"
724 + }
725 + ],
726 + "title": "AGENTS",
727 + "transformations": [
728 + {
729 + "id": "merge",
730 + "options": {
731 + "reducers": []
732 + }
733 + }
734 + ],
735 + "type": "table"
736 + },
737 + {
738 + "datasource": {
739 + "type": "elasticsearch",
740 + "uid": "replace_datasource_uid"
741 + },
742 + "fieldConfig": {
743 + "defaults": {
744 + "mappings": [],
745 + "thresholds": {
746 + "mode": "absolute",
747 + "steps": [
748 + {
749 + "color": "green",
750 + "value": null
751 + },
752 + {
753 + "color": "red",
754 + "value": 80
755 + }
756 + ]
757 + }
758 + },
759 + "overrides": []
760 + },
761 + "gridPos": {
762 + "h": 7,
763 + "w": 12,
764 + "x": 12,
765 + "y": 7
766 + },
767 + "id": 37,
768 + "options": {
769 + "displayMode": "gradient",
770 + "maxVizHeight": 300,
771 + "minVizHeight": 10,
772 + "minVizWidth": 0,
773 + "namePlacement": "auto",
774 + "orientation": "horizontal",
775 + "reduceOptions": {
776 + "calcs": ["sum"],
777 + "fields": "",
778 + "values": false
779 + },
780 + "showUnfilled": true,
781 + "sizing": "auto",
782 + "text": {},
783 + "valueMode": "color"
784 + },
785 + "pluginVersion": "10.4.0",
786 + "targets": [
787 + {
788 + "bucketAggs": [
789 + {
790 + "fake": true,
791 + "field": "dst_ip",
792 + "id": "6",
793 + "settings": {
794 + "min_doc_count": 1,
795 + "order": "desc",
796 + "orderBy": "_count",
797 + "size": "10"
798 + },
799 + "type": "terms"
800 + },
801 + {
802 + "fake": true,
803 + "field": "timestamp",
804 + "id": "5",
805 + "settings": {
806 + "interval": "auto",
807 + "min_doc_count": 0,
808 + "trimEdges": 0
809 + },
810 + "type": "date_histogram"
811 + }
812 + ],
813 + "datasource": {
814 + "type": "elasticsearch",
815 + "uid": "replace_datasource_uid"
816 + },
817 + "metrics": [
818 + {
819 + "field": "type",
820 + "id": "1",
821 + "meta": {},
822 + "settings": {},
823 + "type": "count"
824 + }
825 + ],
826 + "query": "(rule_group3:sysmon_event3 OR rule_group2:packetbeat) AND agent_name:$agent_name",
827 + "refId": "A",
828 + "timeField": "timestamp"
829 + }
830 + ],
831 + "title": "TOP 10 DST IPs",
832 + "type": "bargauge"
833 + },
834 + {
835 + "datasource": {
836 + "type": "elasticsearch",
837 + "uid": "replace_datasource_uid"
838 + },
839 + "fieldConfig": {
840 + "defaults": {
841 + "mappings": [],
842 + "thresholds": {
843 + "mode": "absolute",
844 + "steps": [
845 + {
846 + "color": "green",
847 + "value": null
848 + },
849 + {
850 + "color": "red",
851 + "value": 80
852 + }
853 + ]
854 + }
855 + },
856 + "overrides": []
857 + },
858 + "gridPos": {
859 + "h": 13,
860 + "w": 24,
861 + "x": 0,
862 + "y": 14
863 + },
864 + "id": 74,
865 + "options": {
866 + "color": "yellow",
867 + "iteration": 20,
868 + "monochrome": false,
869 + "nodeColor": "super-light-purple",
870 + "nodePadding": 20,
871 + "nodeWidth": 30
872 + },
873 + "targets": [
874 + {
875 + "alias": "",
876 + "bucketAggs": [
877 + {
878 + "field": "src_ip",
879 + "id": "2",
880 + "settings": {
881 + "min_doc_count": "1",
882 + "order": "desc",
883 + "orderBy": "_term",
884 + "size": "10"
885 + },
886 + "type": "terms"
887 + },
888 + {
889 + "field": "dst_ip",
890 + "id": "3",
891 + "settings": {
892 + "min_doc_count": "1",
893 + "order": "desc",
894 + "orderBy": "_term",
895 + "size": "10"
896 + },
897 + "type": "terms"
898 + },
899 + {
900 + "field": "dst_port",
901 + "id": "4",
902 + "settings": {
903 + "min_doc_count": "1",
904 + "order": "desc",
905 + "orderBy": "_count",
906 + "size": "1"
907 + },
908 + "type": "terms"
909 + }
910 + ],
911 + "datasource": {
912 + "type": "elasticsearch",
913 + "uid": "replace_datasource_uid"
914 + },
915 + "metrics": [
916 + {
917 + "id": "1",
918 + "type": "count"
919 + }
920 + ],
921 + "query": "(rule_group3:sysmon_event3 OR rule_group2:packetbeat) AND agent_name:$agent_name AND (data_eventdata_sourceIsIpv6:false OR data_win_eventdata_sourceIsIpv6:false)",
922 + "refId": "A",
923 + "timeField": "timestamp"
924 + }
925 + ],
926 + "title": "CONNECTIONS MAP (TOP 10 IPs)",
927 + "transformations": [
928 + {
929 + "id": "organize",
930 + "options": {
931 + "excludeByName": {},
932 + "indexByName": {},
933 + "renameByName": {
934 + "Count": "Count",
935 + "dst_ip": "DST IP",
936 + "dst_port": "DST PORT",
937 + "src_ip": "SRC IP"
938 + }
939 + }
940 + }
941 + ],
942 + "transparent": true,
943 + "type": "netsage-sankey-panel"
944 + },
945 + {
946 + "datasource": {
947 + "type": "elasticsearch",
948 + "uid": "replace_datasource_uid"
949 + },
950 + "fieldConfig": {
951 + "defaults": {
952 + "color": {
953 + "mode": "palette-classic"
954 + },
955 + "custom": {
956 + "axisBorderShow": false,
957 + "axisCenteredZero": false,
958 + "axisColorMode": "text",
959 + "axisLabel": "",
960 + "axisPlacement": "auto",
961 + "barAlignment": 0,
962 + "drawStyle": "bars",
963 + "fillOpacity": 0,
964 + "gradientMode": "none",
965 + "hideFrom": {
966 + "legend": false,
967 + "tooltip": false,
968 + "viz": false
969 + },
970 + "insertNulls": false,
971 + "lineInterpolation": "linear",
972 + "lineWidth": 1,
973 + "pointSize": 5,
974 + "scaleDistribution": {
975 + "type": "linear"
976 + },
977 + "showPoints": "auto",
978 + "spanNulls": false,
979 + "stacking": {
980 + "group": "A",
981 + "mode": "normal"
982 + },
983 + "thresholdsStyle": {
984 + "mode": "off"
985 + }
986 + },
987 + "mappings": [],
988 + "thresholds": {
989 + "mode": "absolute",
990 + "steps": [
991 + {
992 + "color": "green",
993 + "value": null
994 + },
995 + {
996 + "color": "red",
997 + "value": 80
998 + }
999 + ]
1000 + }
1001 + },
1002 + "overrides": []
1003 + },
1004 + "gridPos": {
1005 + "h": 11,
1006 + "w": 14,
1007 + "x": 0,
1008 + "y": 27
1009 + },
1010 + "id": 77,
1011 + "options": {
1012 + "legend": {
1013 + "calcs": [],
1014 + "displayMode": "table",
1015 + "placement": "right",
1016 + "showLegend": true
1017 + },
1018 + "tooltip": {
1019 + "mode": "single",
1020 + "sort": "none"
1021 + }
1022 + },
1023 + "targets": [
1024 + {
1025 + "alias": "",
1026 + "bucketAggs": [
1027 + {
1028 + "field": "agent_name",
1029 + "id": "3",
1030 + "settings": {
1031 + "min_doc_count": "1",
1032 + "order": "desc",
1033 + "orderBy": "_term",
1034 + "size": "10"
1035 + },
1036 + "type": "terms"
1037 + },
1038 + {
1039 + "field": "timestamp",
1040 + "id": "2",
1041 + "settings": {
1042 + "interval": "auto"
1043 + },
1044 + "type": "date_histogram"
1045 + }
1046 + ],
1047 + "datasource": {
1048 + "type": "elasticsearch",
1049 + "uid": "replace_datasource_uid"
1050 + },
1051 + "metrics": [
1052 + {
1053 + "id": "1",
1054 + "type": "count"
1055 + }
1056 + ],
1057 + "query": "(rule_group3:sysmon_event3 OR rule_group2:packetbeat) AND agent_name:$agent_name",
1058 + "refId": "A",
1059 + "timeField": "timestamp"
1060 + }
1061 + ],
1062 + "title": "TOP 10 AGENTS - HISTOGRAM",
1063 + "transparent": true,
1064 + "type": "timeseries"
1065 + },
1066 + {
1067 + "datasource": {
1068 + "type": "elasticsearch",
1069 + "uid": "replace_datasource_uid"
1070 + },
1071 + "fieldConfig": {
1072 + "defaults": {
1073 + "color": {
1074 + "mode": "thresholds"
1075 + },
1076 + "custom": {
1077 + "hideFrom": {
1078 + "legend": false,
1079 + "tooltip": false,
1080 + "viz": false
1081 + }
1082 + },
1083 + "mappings": [],
1084 + "thresholds": {
1085 + "mode": "absolute",
1086 + "steps": [
1087 + {
1088 + "color": "rgba(245, 54, 54, 0.9)",
1089 + "value": null
1090 + },
1091 + {
1092 + "color": "rgba(237, 129, 40, 0.89)",
1093 + "value": 0
1094 + },
1095 + {
1096 + "color": "rgba(50, 172, 45, 0.97)",
1097 + "value": 10
1098 + }
1099 + ]
1100 + }
1101 + },
1102 + "overrides": []
1103 + },
1104 + "gridPos": {
1105 + "h": 11,
1106 + "w": 10,
1107 + "x": 14,
1108 + "y": 27
1109 + },
1110 + "id": 61,
1111 + "maxDataPoints": 1,
1112 + "options": {
1113 + "basemap": {
1114 + "name": "Basemap",
1115 + "type": "default"
1116 + },
1117 + "controls": {
1118 + "mouseWheelZoom": false,
1119 + "showAttribution": true,
1120 + "showDebug": false,
1121 + "showMeasure": false,
1122 + "showScale": false,
1123 + "showZoom": true
1124 + },
1125 + "layers": [
1126 + {
1127 + "config": {
1128 + "showLegend": true,
1129 + "style": {
1130 + "color": {
1131 + "fixed": "dark-green"
1132 + },
1133 + "opacity": 0.4,
1134 + "rotation": {
1135 + "fixed": 0,
1136 + "max": 360,
1137 + "min": -360,
1138 + "mode": "mod"
1139 + },
1140 + "size": {
1141 + "fixed": 5,
1142 + "max": 30,
1143 + "min": 2
1144 + },
1145 + "symbol": {
1146 + "fixed": "img/icons/marker/circle.svg",
1147 + "mode": "fixed"
1148 + },
1149 + "symbolAlign": {
1150 + "horizontal": "center",
1151 + "vertical": "center"
1152 + },
1153 + "textConfig": {
1154 + "fontSize": 12,
1155 + "offsetX": 0,
1156 + "offsetY": 0,
1157 + "textAlign": "center",
1158 + "textBaseline": "middle"
1159 + }
1160 + }
1161 + },
1162 + "location": {
1163 + "gazetteer": "public/gazetteer/countries.json",
1164 + "mode": "lookup"
1165 + },
1166 + "name": "Layer 0",
1167 + "tooltip": true,
1168 + "type": "markers"
1169 + }
1170 + ],
1171 + "tooltip": {
1172 + "mode": "details"
1173 + },
1174 + "view": {
1175 + "allLayers": true,
1176 + "id": "zero",
1177 + "lat": 0,
1178 + "lon": 0,
1179 + "zoom": 1
1180 + }
1181 + },
1182 + "pluginVersion": "10.4.0",
1183 + "targets": [
1184 + {
1185 + "bucketAggs": [
1186 + {
1187 + "fake": true,
1188 + "field": "dst_ip_country_code",
1189 + "id": "3",
1190 + "settings": {
1191 + "min_doc_count": 1,
1192 + "order": "desc",
1193 + "orderBy": "_term",
1194 + "size": "0"
1195 + },
1196 + "type": "terms"
1197 + },
1198 + {
1199 + "field": "timestamp",
1200 + "id": "2",
1201 + "settings": {
1202 + "interval": "auto",
1203 + "min_doc_count": 0,
1204 + "trimEdges": 0
1205 + },
1206 + "type": "date_histogram"
1207 + }
1208 + ],
1209 + "datasource": {
1210 + "type": "elasticsearch",
1211 + "uid": "replace_datasource_uid"
1212 + },
1213 + "metrics": [
1214 + {
1215 + "field": "select field",
1216 + "id": "1",
1217 + "type": "count"
1218 + }
1219 + ],
1220 + "query": "(rule_group3:sysmon_event3 OR rule_group2:packetbeat) AND agent_name:$agent_name",
1221 + "refId": "A",
1222 + "timeField": "timestamp"
1223 + }
1224 + ],
1225 + "title": "DST GEOIP",
1226 + "transformations": [
1227 + {
1228 + "id": "reduce",
1229 + "options": {
1230 + "reducers": ["sum"]
1231 + }
1232 + }
1233 + ],
1234 + "type": "geomap"
1235 + },
1236 + {
1237 + "datasource": {
1238 + "type": "elasticsearch",
1239 + "uid": "replace_datasource_uid"
1240 + },
1241 + "fieldConfig": {
1242 + "defaults": {
1243 + "mappings": [],
1244 + "thresholds": {
1245 + "mode": "absolute",
1246 + "steps": [
1247 + {
1248 + "color": "green",
1249 + "value": null
1250 + },
1251 + {
1252 + "color": "red",
1253 + "value": 80
1254 + }
1255 + ]
1256 + }
1257 + },
1258 + "overrides": []
1259 + },
1260 + "gridPos": {
1261 + "h": 7,
1262 + "w": 14,
1263 + "x": 0,
1264 + "y": 38
1265 + },
1266 + "id": 59,
1267 + "options": {
1268 + "displayMode": "gradient",
1269 + "maxVizHeight": 300,
1270 + "minVizHeight": 10,
1271 + "minVizWidth": 0,
1272 + "namePlacement": "auto",
1273 + "orientation": "horizontal",
1274 + "reduceOptions": {
1275 + "calcs": ["sum"],
1276 + "fields": "",
1277 + "values": false
1278 + },
1279 + "showUnfilled": true,
1280 + "sizing": "auto",
1281 + "text": {},
1282 + "valueMode": "color"
1283 + },
1284 + "pluginVersion": "10.4.0",
1285 + "targets": [
1286 + {
1287 + "bucketAggs": [
1288 + {
1289 + "fake": true,
1290 + "field": "process_image",
1291 + "id": "6",
1292 + "settings": {
1293 + "min_doc_count": 1,
1294 + "order": "desc",
1295 + "orderBy": "_count",
1296 + "size": "10"
1297 + },
1298 + "type": "terms"
1299 + },
1300 + {
1301 + "fake": true,
1302 + "field": "timestamp",
1303 + "id": "5",
1304 + "settings": {
1305 + "interval": "auto",
1306 + "min_doc_count": 0,
1307 + "trimEdges": 0
1308 + },
1309 + "type": "date_histogram"
1310 + }
1311 + ],
1312 + "datasource": {
1313 + "type": "elasticsearch",
1314 + "uid": "replace_datasource_uid"
1315 + },
1316 + "metrics": [
1317 + {
1318 + "field": "type",
1319 + "id": "1",
1320 + "meta": {},
1321 + "settings": {},
1322 + "type": "count"
1323 + }
1324 + ],
1325 + "query": "rule_group3:sysmon_event3 AND agent_name:$agent_name",
1326 + "refId": "A",
1327 + "timeField": "timestamp"
1328 + }
1329 + ],
1330 + "title": "TOP 10 PROCESSES - NETWORK CONNS",
1331 + "type": "bargauge"
1332 + },
1333 + {
1334 + "datasource": {
1335 + "type": "elasticsearch",
1336 + "uid": "replace_datasource_uid"
1337 + },
1338 + "fieldConfig": {
1339 + "defaults": {
1340 + "custom": {
1341 + "align": "auto",
1342 + "cellOptions": {
1343 + "type": "auto"
1344 + },
1345 + "filterable": false,
1346 + "inspect": false
1347 + },
1348 + "mappings": [],
1349 + "thresholds": {
1350 + "mode": "absolute",
1351 + "steps": [
1352 + {
1353 + "color": "green",
1354 + "value": null
1355 + },
1356 + {
1357 + "color": "red",
1358 + "value": 80
1359 + }
1360 + ]
1361 + }
1362 + },
1363 + "overrides": []
1364 + },
1365 + "gridPos": {
1366 + "h": 14,
1367 + "w": 10,
1368 + "x": 14,
1369 + "y": 38
1370 + },
1371 + "id": 70,
1372 + "options": {
1373 + "cellHeight": "sm",
1374 + "footer": {
1375 + "countRows": false,
1376 + "fields": "",
1377 + "reducer": ["sum"],
1378 + "show": false
1379 + },
1380 + "frameIndex": 0,
1381 + "showHeader": true
1382 + },
1383 + "pluginVersion": "10.4.0",
1384 + "targets": [
1385 + {
1386 + "bucketAggs": [
1387 + {
1388 + "$$hashKey": "object:167",
1389 + "fake": true,
1390 + "field": "process_image",
1391 + "id": "6",
1392 + "settings": {
1393 + "min_doc_count": 1,
1394 + "order": "desc",
1395 + "orderBy": "_count",
1396 + "size": "0"
1397 + },
1398 + "type": "terms"
1399 + }
1400 + ],
1401 + "datasource": {
1402 + "type": "elasticsearch",
1403 + "uid": "replace_datasource_uid"
1404 + },
1405 + "metrics": [
1406 + {
1407 + "$$hashKey": "object:165",
1408 + "field": "type",
1409 + "id": "1",
1410 + "meta": {},
1411 + "settings": {},
1412 + "type": "count"
1413 + }
1414 + ],
1415 + "query": "rule_group3:sysmon_event3 AND agent_name:$agent_name",
1416 + "refId": "A",
1417 + "timeField": "timestamp"
1418 + }
1419 + ],
1420 + "title": "PROCESSES - NETWORK CONNS",
1421 + "type": "table"
1422 + },
1423 + {
1424 + "datasource": {
1425 + "type": "elasticsearch",
1426 + "uid": "replace_datasource_uid"
1427 + },
1428 + "fieldConfig": {
1429 + "defaults": {
1430 + "mappings": [],
1431 + "thresholds": {
1432 + "mode": "absolute",
1433 + "steps": [
1434 + {
1435 + "color": "green",
1436 + "value": null
1437 + },
1438 + {
1439 + "color": "red",
1440 + "value": 80
1441 + }
1442 + ]
1443 + }
1444 + },
1445 + "overrides": []
1446 + },
1447 + "gridPos": {
1448 + "h": 7,
1449 + "w": 14,
1450 + "x": 0,
1451 + "y": 45
1452 + },
1453 + "id": 55,
1454 + "options": {
1455 + "displayMode": "gradient",
1456 + "maxVizHeight": 300,
1457 + "minVizHeight": 10,
1458 + "minVizWidth": 0,
1459 + "namePlacement": "auto",
1460 + "orientation": "horizontal",
1461 + "reduceOptions": {
1462 + "calcs": ["sum"],
1463 + "fields": "",
1464 + "values": false
1465 + },
1466 + "showUnfilled": true,
1467 + "sizing": "auto",
1468 + "text": {},
1469 + "valueMode": "color"
1470 + },
1471 + "pluginVersion": "10.4.0",
1472 + "targets": [
1473 + {
1474 + "bucketAggs": [
1475 + {
1476 + "fake": true,
1477 + "field": "process_image",
1478 + "id": "6",
1479 + "settings": {
1480 + "min_doc_count": 1,
1481 + "order": "asc",
1482 + "orderBy": "_count",
1483 + "size": "10"
1484 + },
1485 + "type": "terms"
1486 + },
1487 + {
1488 + "fake": true,
1489 + "field": "timestamp",
1490 + "id": "5",
1491 + "settings": {
1492 + "interval": "auto",
1493 + "min_doc_count": 0,
1494 + "trimEdges": 0
1495 + },
1496 + "type": "date_histogram"
1497 + }
1498 + ],
1499 + "datasource": {
1500 + "type": "elasticsearch",
1501 + "uid": "replace_datasource_uid"
1502 + },
1503 + "metrics": [
1504 + {
1505 + "field": "type",
1506 + "id": "1",
1507 + "meta": {},
1508 + "settings": {},
1509 + "type": "count"
1510 + }
1511 + ],
1512 + "query": "rule_group3:sysmon_event3 AND agent_name:$agent_name",
1513 + "refId": "A",
1514 + "timeField": "timestamp"
1515 + }
1516 + ],
1517 + "title": "LEAST SEEN PROCESSES - NETWORK CONNS",
1518 + "type": "bargauge"
1519 + },
1520 + {
1521 + "datasource": {
1522 + "type": "elasticsearch",
1523 + "uid": "replace_datasource_uid"
1524 + },
1525 + "fieldConfig": {
1526 + "defaults": {
1527 + "color": {
1528 + "mode": "palette-classic"
1529 + },
1530 + "custom": {
1531 + "hideFrom": {
1532 + "legend": false,
1533 + "tooltip": false,
1534 + "viz": false
1535 + }
1536 + },
1537 + "decimals": 0,
1538 + "mappings": [],
1539 + "unit": "short"
1540 + },
1541 + "overrides": []
1542 + },
1543 + "gridPos": {
1544 + "h": 11,
1545 + "w": 7,
1546 + "x": 0,
1547 + "y": 52
1548 + },
1549 + "id": 54,
1550 + "maxDataPoints": 3,
1551 + "options": {
1552 + "displayLabels": [],
1553 + "legend": {
1554 + "calcs": [],
1555 + "displayMode": "list",
1556 + "placement": "right",
1557 + "showLegend": false,
1558 + "values": ["value", "percent"]
1559 + },
1560 + "pieType": "donut",
1561 + "reduceOptions": {
1562 + "calcs": ["sum"],
1563 + "fields": "",
1564 + "values": false
1565 + },
1566 + "text": {},
1567 + "tooltip": {
1568 + "mode": "single",
1569 + "sort": "none"
1570 + }
1571 + },
1572 + "targets": [
1573 + {
1574 + "bucketAggs": [
1575 + {
1576 + "$$hashKey": "object:378",
1577 + "fake": true,
1578 + "field": "user_name",
1579 + "id": "3",
1580 + "settings": {
1581 + "min_doc_count": 1,
1582 + "order": "desc",
1583 + "orderBy": "_count",
1584 + "size": "10"
1585 + },
1586 + "type": "terms"
1587 + },
1588 + {
1589 + "$$hashKey": "object:379",
1590 + "field": "timestamp",
1591 + "id": "2",
1592 + "settings": {
1593 + "interval": "auto",
1594 + "min_doc_count": 0,
1595 + "trimEdges": 0
1596 + },
1597 + "type": "date_histogram"
1598 + }
1599 + ],
1600 + "datasource": {
1601 + "type": "elasticsearch",
1602 + "uid": "replace_datasource_uid"
1603 + },
1604 + "metrics": [
1605 + {
1606 + "$$hashKey": "object:376",
1607 + "field": "select field",
1608 + "id": "1",
1609 + "type": "count"
1610 + }
1611 + ],
1612 + "query": "rule_group3:sysmon_event3 AND agent_name:$agent_name",
1613 + "refId": "A",
1614 + "timeField": "timestamp"
1615 + }
1616 + ],
1617 + "title": "NETWORK CONNS - TOP 10 USER ACCOUNTS",
1618 + "type": "piechart"
1619 + },
1620 + {
1621 + "datasource": {
1622 + "type": "elasticsearch",
1623 + "uid": "replace_datasource_uid"
1624 + },
1625 + "fieldConfig": {
1626 + "defaults": {
1627 + "custom": {
1628 + "align": "auto",
1629 + "cellOptions": {
1630 + "type": "auto"
1631 + },
1632 + "filterable": false,
1633 + "inspect": false
1634 + },
1635 + "mappings": [],
1636 + "thresholds": {
1637 + "mode": "absolute",
1638 + "steps": [
1639 + {
1640 + "color": "green",
1641 + "value": null
1642 + },
1643 + {
1644 + "color": "red",
1645 + "value": 80
1646 + }
1647 + ]
1648 + }
1649 + },
1650 + "overrides": []
1651 + },
1652 + "gridPos": {
1653 + "h": 11,
1654 + "w": 17,
1655 + "x": 7,
1656 + "y": 52
1657 + },
1658 + "id": 72,
1659 + "maxDataPoints": 3,
1660 + "options": {
1661 + "cellHeight": "sm",
1662 + "footer": {
1663 + "countRows": false,
1664 + "fields": "",
1665 + "reducer": ["sum"],
1666 + "show": false
1667 + },
1668 + "showHeader": true
1669 + },
1670 + "pluginVersion": "10.4.0",
1671 + "targets": [
1672 + {
1673 + "bucketAggs": [
1674 + {
1675 + "$$hashKey": "object:327",
1676 + "fake": true,
1677 + "field": "user_name",
1678 + "id": "3",
1679 + "settings": {
1680 + "min_doc_count": 1,
1681 + "order": "desc",
1682 + "orderBy": "_count",
1683 + "size": "0"
1684 + },
1685 + "type": "terms"
1686 + }
1687 + ],
1688 + "datasource": {
1689 + "type": "elasticsearch",
1690 + "uid": "replace_datasource_uid"
1691 + },
1692 + "metrics": [
1693 + {
1694 + "$$hashKey": "object:325",
1695 + "field": "select field",
1696 + "id": "1",
1697 + "type": "count"
1698 + }
1699 + ],
1700 + "query": "rule_group3:sysmon_event3 AND agent_name:$agent_name",
1701 + "refId": "A",
1702 + "timeField": "timestamp"
1703 + }
1704 + ],
1705 + "title": "NETWORK CONNS - USER / ACCOUNT",
1706 + "type": "table"
1707 + },
1708 + {
1709 + "datasource": {
1710 + "type": "elasticsearch",
1711 + "uid": "replace_datasource_uid"
1712 + },
1713 + "fieldConfig": {
1714 + "defaults": {
1715 + "color": {
1716 + "mode": "thresholds"
1717 + },
1718 + "custom": {
1719 + "align": "auto",
1720 + "cellOptions": {
1721 + "type": "auto"
1722 + },
1723 + "inspect": false
1724 + },
1725 + "mappings": [],
1726 + "thresholds": {
1727 + "mode": "absolute",
1728 + "steps": [
1729 + {
1730 + "color": "green",
1731 + "value": null
1732 + },
1733 + {
1734 + "color": "red",
1735 + "value": 80
1736 + }
1737 + ]
1738 + }
1739 + },
1740 + "overrides": [
1741 + {
1742 + "matcher": {
1743 + "id": "byName",
1744 + "options": "timestamp"
1745 + },
1746 + "properties": [
1747 + {
1748 + "id": "displayName",
1749 + "value": "DATE/TIME"
1750 + },
1751 + {
1752 + "id": "unit",
1753 + "value": "time: YYYY-MM-DD HH:mm:ss"
1754 + },
1755 + {
1756 + "id": "custom.align"
1757 + }
1758 + ]
1759 + },
1760 + {
1761 + "matcher": {
1762 + "id": "byName",
1763 + "options": "dst_ip"
1764 + },
1765 + "properties": [
1766 + {
1767 + "id": "displayName",
1768 + "value": "DST IP"
1769 + },
1770 + {
1771 + "id": "unit",
1772 + "value": "short"
1773 + },
1774 + {
1775 + "id": "decimals",
1776 + "value": -1
1777 + },
1778 + {
1779 + "id": "links",
1780 + "value": [
1781 + {
1782 + "targetBlank": true,
1783 + "title": "TALOS THREAT INTEL",
1784 + "url": "https://talosintelligence.com/reputation_center/lookup?search=${__value.text}"
1785 + }
1786 + ]
1787 + },
1788 + {
1789 + "id": "custom.align"
1790 + }
1791 + ]
1792 + },
1793 + {
1794 + "matcher": {
1795 + "id": "byName",
1796 + "options": "src_port"
1797 + },
1798 + "properties": [
1799 + {
1800 + "id": "displayName",
1801 + "value": "SRC PORT"
1802 + },
1803 + {
1804 + "id": "unit",
1805 + "value": "none"
1806 + },
1807 + {
1808 + "id": "decimals",
1809 + "value": -2
1810 + },
1811 + {
1812 + "id": "custom.align"
1813 + }
1814 + ]
1815 + },
1816 + {
1817 + "matcher": {
1818 + "id": "byName",
1819 + "options": "process_image"
1820 + },
1821 + "properties": [
1822 + {
1823 + "id": "displayName",
1824 + "value": "FILE IMAGE"
1825 + },
1826 + {
1827 + "id": "unit",
1828 + "value": "short"
1829 + },
1830 + {
1831 + "id": "decimals",
1832 + "value": 2
1833 + },
1834 + {
1835 + "id": "custom.align"
1836 + }
1837 + ]
1838 + },
1839 + {
1840 + "matcher": {
1841 + "id": "byName",
1842 + "options": "user_name"
1843 + },
1844 + "properties": [
1845 + {
1846 + "id": "displayName",
1847 + "value": "USER/ACCOUNT"
1848 + },
1849 + {
1850 + "id": "unit",
1851 + "value": "short"
1852 + },
1853 + {
1854 + "id": "decimals",
1855 + "value": 2
1856 + },
1857 + {
1858 + "id": "custom.align"
1859 + }
1860 + ]
1861 + },
1862 + {
1863 + "matcher": {
1864 + "id": "byName",
1865 + "options": "agent_name"
1866 + },
1867 + "properties": [
1868 + {
1869 + "id": "displayName",
1870 + "value": "AGENT"
1871 + },
1872 + {
1873 + "id": "unit",
1874 + "value": "short"
1875 + },
1876 + {
1877 + "id": "decimals",
1878 + "value": 2
1879 + },
1880 + {
1881 + "id": "custom.align"
1882 + }
1883 + ]
1884 + },
1885 + {
1886 + "matcher": {
1887 + "id": "byName",
1888 + "options": "src_ip"
1889 + },
1890 + "properties": [
1891 + {
1892 + "id": "displayName",
1893 + "value": "SRC IP"
1894 + },
1895 + {
1896 + "id": "unit",
1897 + "value": "short"
1898 + },
1899 + {
1900 + "id": "decimals",
1901 + "value": 2
1902 + },
1903 + {
1904 + "id": "custom.align"
1905 + }
1906 + ]
1907 + },
1908 + {
1909 + "matcher": {
1910 + "id": "byName",
1911 + "options": "dst_port"
1912 + },
1913 + "properties": [
1914 + {
1915 + "id": "displayName",
1916 + "value": "DST PORT"
1917 + },
1918 + {
1919 + "id": "unit",
1920 + "value": "none"
1921 + },
1922 + {
1923 + "id": "decimals",
1924 + "value": -1
1925 + },
1926 + {
1927 + "id": "custom.align"
1928 + }
1929 + ]
1930 + },
1931 + {
1932 + "matcher": {
1933 + "id": "byName",
1934 + "options": "EVENT ID"
1935 + },
1936 + "properties": [
1937 + {
1938 + "id": "links",
1939 + "value": [
1940 + {
1941 + "targetBlank": true,
1942 + "title": "VIEW EVENT DETAILS",
1943 + "url": "https://grafana.company.local/explore?left=%7B%22datasource%22:%22WAZUH%22,%22queries%22:%5B%7B%22refId%22:%22A%22,%22query%22:%22_id:${__value.text}%22,%22alias%22:%22%22,%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:%7B%22limit%22:%22500%22%7D%7D%5D,%22bucketAggs%22:%5B%5D,%22timeField%22:%22timestamp%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D"
1944 + }
1945 + ]
1946 + }
1947 + ]
1948 + }
1949 + ]
1950 + },
1951 + "gridPos": {
1952 + "h": 12,
1953 + "w": 24,
1954 + "x": 0,
1955 + "y": 63
1956 + },
1957 + "id": 51,
1958 + "options": {
1959 + "cellHeight": "sm",
1960 + "footer": {
1961 + "countRows": false,
1962 + "fields": "",
1963 + "reducer": ["sum"],
1964 + "show": false
1965 + },
1966 + "showHeader": true,
1967 + "sortBy": [
1968 + {
1969 + "desc": true,
1970 + "displayName": "DATE/TIME"
1971 + }
1972 + ]
1973 + },
1974 + "pluginVersion": "10.4.0",
1975 + "targets": [
1976 + {
1977 + "bucketAggs": [],
1978 + "datasource": {
1979 + "type": "elasticsearch",
1980 + "uid": "replace_datasource_uid"
1981 + },
1982 + "metrics": [
1983 + {
1984 + "id": "1",
1985 + "settings": {
1986 + "size": "250"
1987 + },
1988 + "type": "raw_data"
1989 + }
1990 + ],
1991 + "query": "(rule_group3:sysmon_event3 OR rule_group2:packetbeat) AND agent_name:$agent_name",
1992 + "refId": "A",
1993 + "timeField": "timestamp"
1994 + }
1995 + ],
1996 + "title": "NETWORK CONNS",
1997 + "transformations": [
1998 + {
1999 + "id": "filterFieldsByName",
2000 + "options": {
2001 + "include": {
2002 + "names": [
2003 + "timestamp",
2004 + "agent_name",
2005 + "dst_ip",
2006 + "dst_ip_country_code",
2007 + "dst_port",
2008 + "process_image",
2009 + "protocol",
2010 + "src_ip",
2011 + "src_port",
2012 + "user_name",
2013 + "_id"
2014 + ]
2015 + }
2016 + }
2017 + },
2018 + {
2019 + "id": "organize",
2020 + "options": {
2021 + "excludeByName": {},
2022 + "indexByName": {
2023 + "_id": 1,
2024 + "agent_name": 2,
2025 + "dst_ip": 5,
2026 + "dst_ip_country_code": 6,
2027 + "dst_port": 8,
2028 + "process_image": 9,
2029 + "protocol": 7,
2030 + "src_ip": 3,
2031 + "src_port": 4,
2032 + "timestamp": 0,
2033 + "user_name": 10
2034 + },
2035 + "renameByName": {
2036 + "_id": "EVENT ID",
2037 + "agent_name": "AGENT",
2038 + "dst_ip": "DST IP",
2039 + "dst_ip_country_code": "DST GEOIP",
2040 + "dst_port": "DST PORT",
2041 + "process_image": "PROCESS IMAGE",
2042 + "protocol": "PROTOCOL",
2043 + "src_ip": "SRC IP",
2044 + "src_port": "SRC PORT",
2045 + "timestamp": "DATE/TIME",
2046 + "user_name": "USER/ACCOUNT"
2047 + }
2048 + }
2049 + }
2050 + ],
2051 + "type": "table"
2052 + }
2053 + ],
2054 + "refresh": false,
2055 + "schemaVersion": 39,
2056 + "tags": ["EDR"],
2057 + "templating": {
2058 + "list": [
2059 + {
2060 + "datasource": {
2061 + "type": "elasticsearch",
2062 + "uid": "replace_datasource_uid"
2063 + },
2064 + "filters": [],
2065 + "hide": 0,
2066 + "label": "",
2067 + "name": "Filters",
2068 + "skipUrlSync": false,
2069 + "type": "adhoc"
2070 + },
2071 + {
2072 + "current": {
2073 + "selected": false,
2074 + "text": "All",
2075 + "value": "$__all"
2076 + },
2077 + "datasource": {
2078 + "type": "elasticsearch",
2079 + "uid": "replace_datasource_uid"
2080 + },
2081 + "definition": "{ \"find\": \"terms\", \"field\": \"agent_name\", \"query\": \"(rule_group3:sysmon_event3 OR rule_group2:misp_alert OR rule_group2:packetbeat) AND (data_win_eventdata_destinationIsIpv6:false OR data_eventdata_destinationIsIpv6:false OR data_event_category:\\\"network_traffic, network\\\")\"}",
2082 + "hide": 0,
2083 + "includeAll": true,
2084 + "label": "Agent",
2085 + "multi": false,
2086 + "name": "agent_name",
2087 + "options": [],
2088 + "query": "{ \"find\": \"terms\", \"field\": \"agent_name\", \"query\": \"(rule_group3:sysmon_event3 OR rule_group2:misp_alert OR rule_group2:packetbeat) AND (data_win_eventdata_destinationIsIpv6:false OR data_eventdata_destinationIsIpv6:false OR data_event_category:\\\"network_traffic, network\\\")\"}",
2089 + "refresh": 2,
2090 + "regex": "",
2091 + "skipUrlSync": false,
2092 + "sort": 2,
2093 + "tagValuesQuery": "",
2094 + "tagsQuery": "",
2095 + "type": "query",
2096 + "useTags": false
2097 + }
2098 + ]
2099 + },
2100 + "time": {
2101 + "from": "now-6h",
2102 + "to": "now"
2103 + },
2104 + "timepicker": {
2105 + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
2106 + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
2107 + },
2108 + "timezone": "",
2109 + "title": "EDR - NETWORK CONNECTIONS",
2110 + "weekStart": ""
2111 +}
backend/app/connectors/grafana/dashboards/Wazuh/edr_system_security_audit.json
+105 -87
@@ -25,7 +25,6 @@
25 "fiscalYearStartMonth": 0,
26 "graphTooltip": 0,
27 "id": null,
28 - "iteration": 1658194363330,
28 "links": [
29 {
30 "asDropdown": true,
@@ -78,7 +77,6 @@
77 "y": 0
78 },
79 "id": 43,
81 - "links": [],
80 "options": {
81 "colorMode": "value",
82 "graphMode": "area",
@@ -89,10 +87,12 @@
87 "fields": "",
88 "values": false
89 },
90 + "showPercentChange": false,
91 "text": {},
93 - "textMode": "auto"
92 + "textMode": "auto",
93 + "wideLayout": true
94 },
95 - "pluginVersion": "9.0.0",
95 + "pluginVersion": "10.4.0",
96 "targets": [
97 {
98 "bucketAggs": [
@@ -136,7 +136,9 @@
136 },
137 "custom": {
138 "align": "auto",
139 - "displayMode": "auto",
139 + "cellOptions": {
140 + "type": "auto"
141 + },
142 "inspect": false
143 },
144 "mappings": [],
@@ -215,7 +217,9 @@
217 },
218 "id": 31,
219 "options": {
220 + "cellHeight": "sm",
221 "footer": {
222 + "countRows": false,
223 "fields": "",
224 "reducer": ["sum"],
225 "show": false
@@ -223,7 +227,7 @@
227 "showHeader": true,
228 "sortBy": []
229 },
226 - "pluginVersion": "9.0.0",
230 + "pluginVersion": "10.4.0",
231 "targets": [
232 {
233 "bucketAggs": [
@@ -277,7 +281,9 @@
281 },
282 "custom": {
283 "align": "auto",
280 - "displayMode": "auto",
284 + "cellOptions": {
285 + "type": "auto"
286 + },
287 "inspect": false
288 },
289 "mappings": [],
@@ -380,7 +386,9 @@
386 },
387 "id": 44,
388 "options": {
389 + "cellHeight": "sm",
390 "footer": {
391 + "countRows": false,
392 "fields": "",
393 "reducer": ["sum"],
394 "show": false
@@ -388,7 +396,7 @@
396 "showHeader": true,
397 "sortBy": []
398 },
391 - "pluginVersion": "9.0.0",
399 + "pluginVersion": "10.4.0",
400 "targets": [
401 {
402 "bucketAggs": [
@@ -611,13 +619,13 @@
619 "y": 7
620 },
621 "id": 23,
614 - "links": [],
622 "maxDataPoints": 3,
623 "options": {
624 "legend": {
625 "calcs": [],
626 "displayMode": "table",
627 "placement": "right",
628 + "showLegend": true,
629 "values": ["value"]
630 },
631 "pieType": "pie",
@@ -678,57 +686,87 @@
686 "type": "piechart"
687 },
688 {
681 - "aliasColors": {},
682 - "bars": true,
683 - "dashLength": 10,
684 - "dashes": false,
689 "datasource": {
690 "type": "elasticsearch",
691 "uid": "replace_datasource_uid"
692 },
693 "fieldConfig": {
694 "defaults": {
691 - "links": []
695 + "color": {
696 + "mode": "palette-classic"
697 + },
698 + "custom": {
699 + "axisBorderShow": false,
700 + "axisCenteredZero": false,
701 + "axisColorMode": "text",
702 + "axisLabel": "",
703 + "axisPlacement": "auto",
704 + "barAlignment": 0,
705 + "drawStyle": "bars",
706 + "fillOpacity": 100,
707 + "gradientMode": "none",
708 + "hideFrom": {
709 + "legend": false,
710 + "tooltip": false,
711 + "viz": false
712 + },
713 + "insertNulls": false,
714 + "lineInterpolation": "linear",
715 + "lineWidth": 1,
716 + "pointSize": 5,
717 + "scaleDistribution": {
718 + "type": "linear"
719 + },
720 + "showPoints": "never",
721 + "spanNulls": false,
722 + "stacking": {
723 + "group": "A",
724 + "mode": "normal"
725 + },
726 + "thresholdsStyle": {
727 + "mode": "off"
728 + }
729 + },
730 + "decimals": -1,
731 + "links": [],
732 + "mappings": [],
733 + "thresholds": {
734 + "mode": "absolute",
735 + "steps": [
736 + {
737 + "color": "green",
738 + "value": null
739 + },
740 + {
741 + "color": "red",
742 + "value": 80
743 + }
744 + ]
745 + },
746 + "unit": "short"
747 },
748 "overrides": []
749 },
695 - "fill": 1,
696 - "fillGradient": 0,
750 "gridPos": {
751 "h": 12,
752 "w": 18,
753 "x": 6,
754 "y": 7
755 },
703 - "hiddenSeries": false,
756 "id": 10,
705 - "legend": {
706 - "alignAsTable": true,
707 - "avg": false,
708 - "current": false,
709 - "max": false,
710 - "min": false,
711 - "rightSide": true,
712 - "show": true,
713 - "total": false,
714 - "values": false
715 - },
716 - "lines": false,
717 - "linewidth": 1,
718 - "links": [],
719 - "nullPointMode": "null",
757 "options": {
721 - "alertThreshold": true
758 + "legend": {
759 + "calcs": [],
760 + "displayMode": "table",
761 + "placement": "right",
762 + "showLegend": true
763 + },
764 + "tooltip": {
765 + "mode": "multi",
766 + "sort": "none"
767 + }
768 },
723 - "percentage": false,
724 - "pluginVersion": "9.0.0",
725 - "pointradius": 5,
726 - "points": false,
727 - "renderer": "flot",
728 - "seriesOverrides": [],
729 - "spaceLength": 10,
730 - "stack": true,
731 - "steppedLine": false,
769 + "pluginVersion": "10.4.0",
770 "targets": [
771 {
772 "bucketAggs": [
@@ -770,37 +808,8 @@
808 "timeField": "timestamp"
809 }
810 ],
773 - "thresholds": [],
774 - "timeRegions": [],
811 "title": "TOP 10 AGENTS - HISTOGRAM",
776 - "tooltip": {
777 - "shared": true,
778 - "sort": 0,
779 - "value_type": "individual"
780 - },
781 - "type": "graph",
782 - "xaxis": {
783 - "mode": "time",
784 - "show": true,
785 - "values": []
786 - },
787 - "yaxes": [
788 - {
789 - "decimals": -1,
790 - "format": "short",
791 - "logBase": 1,
792 - "show": true
793 - },
794 - {
795 - "decimals": -1,
796 - "format": "short",
797 - "logBase": 1,
798 - "show": true
799 - }
800 - ],
801 - "yaxis": {
802 - "align": false
803 - }
812 + "type": "timeseries"
813 },
814 {
815 "datasource": {
@@ -983,13 +992,13 @@
992 "y": 19
993 },
994 "id": 45,
986 - "links": [],
995 "maxDataPoints": 3,
996 "options": {
997 "legend": {
998 "calcs": [],
991 - "displayMode": "hidden",
999 + "displayMode": "list",
1000 "placement": "right",
1001 + "showLegend": false,
1002 "values": ["value"]
1003 },
1004 "pieType": "donut",
@@ -1065,7 +1074,9 @@
1074 },
1075 "custom": {
1076 "align": "auto",
1068 - "displayMode": "auto",
1077 + "cellOptions": {
1078 + "type": "auto"
1079 + },
1080 "inspect": false
1081 },
1082 "decimals": 0,
@@ -1257,10 +1268,11 @@
1268 "y": 19
1269 },
1270 "id": 46,
1260 - "links": [],
1271 "maxDataPoints": 3,
1272 "options": {
1273 + "cellHeight": "sm",
1274 "footer": {
1275 + "countRows": false,
1276 "fields": "",
1277 "reducer": ["sum"],
1278 "show": false
@@ -1268,7 +1280,7 @@
1280 "showHeader": true,
1281 "sortBy": []
1282 },
1271 - "pluginVersion": "9.0.0",
1283 + "pluginVersion": "10.4.0",
1284 "targets": [
1285 {
1286 "bucketAggs": [
@@ -1531,13 +1543,13 @@
1543 "y": 19
1544 },
1545 "id": 47,
1534 - "links": [],
1546 "maxDataPoints": 3,
1547 "options": {
1548 "legend": {
1549 "calcs": [],
1550 "displayMode": "table",
1551 "placement": "right",
1552 + "showLegend": true,
1553 "values": ["value"]
1554 },
1555 "pieType": "donut",
@@ -1614,14 +1626,18 @@
1626 },
1627 "custom": {
1628 "align": "auto",
1617 - "displayMode": "auto"
1629 + "cellOptions": {
1630 + "type": "auto"
1631 + },
1632 + "inspect": false
1633 },
1634 "mappings": [],
1635 "thresholds": {
1636 "mode": "absolute",
1637 "steps": [
1638 {
1624 - "color": "semi-dark-orange"
1639 + "color": "semi-dark-orange",
1640 + "value": null
1641 }
1642 ]
1643 }
@@ -1703,8 +1719,11 @@
1719 "value": "Not Applicable"
1720 },
1721 {
1706 - "id": "custom.displayMode",
1707 - "value": "color-background-solid"
1722 + "id": "custom.cellOptions",
1723 + "value": {
1724 + "mode": "basic",
1725 + "type": "color-background"
1726 + }
1727 },
1728 {
1729 "id": "mappings",
@@ -1776,7 +1795,9 @@
1795 },
1796 "id": 27,
1797 "options": {
1798 + "cellHeight": "sm",
1799 "footer": {
1800 + "countRows": false,
1801 "fields": "",
1802 "reducer": ["sum"],
1803 "show": false
@@ -1784,7 +1805,7 @@
1805 "showHeader": true,
1806 "sortBy": []
1807 },
1787 - "pluginVersion": "8.4.3",
1808 + "pluginVersion": "10.4.0",
1809 "targets": [
1810 {
1811 "bucketAggs": [],
@@ -1860,8 +1881,7 @@
1881 }
1882 ],
1883 "refresh": false,
1863 - "schemaVersion": 36,
1864 - "style": "dark",
1884 + "schemaVersion": 39,
1885 "tags": ["EDR"],
1886 "templating": {
1887 "list": [
@@ -1916,7 +1936,5 @@
1936 },
1937 "timezone": "",
1938 "title": "EDR - SYSTEM SECURITY AUDIT",
1919 - "uid": null,
1920 - "version": 2,
1939 "weekStart": ""
1940 }
backend/app/connectors/grafana/dashboards/Wazuh/edr_system_vulnerabilities.json
+448 -158
@@ -92,7 +92,6 @@
92 "y": 1
93 },
94 "id": 43,
95 - "links": [],
95 "options": {
96 "colorMode": "value",
97 "graphMode": "area",
@@ -103,10 +102,12 @@
102 "fields": "",
103 "values": false
104 },
105 + "showPercentChange": false,
106 "text": {},
107 - "textMode": "auto"
107 + "textMode": "auto",
108 + "wideLayout": true
109 },
109 - "pluginVersion": "10.0.3",
110 + "pluginVersion": "10.4.0",
111 "targets": [
112 {
113 "bucketAggs": [
@@ -141,12 +142,127 @@
142 "type": "stat"
143 },
144 {
144 - "columns": [],
145 "datasource": {
146 "type": "elasticsearch",
147 "uid": "replace_datasource_uid"
148 },
149 - "fontSize": "100%",
149 + "fieldConfig": {
150 + "defaults": {
151 + "color": {
152 + "mode": "thresholds"
153 + },
154 + "custom": {
155 + "align": "auto",
156 + "cellOptions": {
157 + "type": "auto"
158 + },
159 + "inspect": false
160 + },
161 + "mappings": [],
162 + "thresholds": {
163 + "mode": "absolute",
164 + "steps": [
165 + {
166 + "color": "green",
167 + "value": null
168 + },
169 + {
170 + "color": "red",
171 + "value": 80
172 + }
173 + ]
174 + }
175 + },
176 + "overrides": [
177 + {
178 + "matcher": {
179 + "id": "byName",
180 + "options": "Time"
181 + },
182 + "properties": [
183 + {
184 + "id": "displayName",
185 + "value": "Time"
186 + },
187 + {
188 + "id": "unit",
189 + "value": "time: YYYY-MM-DD HH:mm:ss"
190 + },
191 + {
192 + "id": "custom.align"
193 + }
194 + ]
195 + },
196 + {
197 + "matcher": {
198 + "id": "byName",
199 + "options": "Count"
200 + },
201 + "properties": [
202 + {
203 + "id": "unit",
204 + "value": "short"
205 + },
206 + {
207 + "id": "decimals",
208 + "value": -1
209 + },
210 + {
211 + "id": "custom.cellOptions",
212 + "value": {
213 + "type": "color-background"
214 + }
215 + },
216 + {
217 + "id": "custom.align"
218 + },
219 + {
220 + "id": "thresholds",
221 + "value": {
222 + "mode": "absolute",
223 + "steps": [
224 + {
225 + "color": "rgba(50, 172, 45, 0.97)",
226 + "value": null
227 + },
228 + {
229 + "color": "rgba(237, 129, 40, 0.89)",
230 + "value": 0
231 + },
232 + {
233 + "color": "#FA6400",
234 + "value": 1
235 + }
236 + ]
237 + }
238 + }
239 + ]
240 + },
241 + {
242 + "matcher": {
243 + "id": "byName",
244 + "options": "agent_name"
245 + },
246 + "properties": [
247 + {
248 + "id": "displayName",
249 + "value": "AGENT"
250 + },
251 + {
252 + "id": "unit",
253 + "value": "short"
254 + },
255 + {
256 + "id": "decimals",
257 + "value": 2
258 + },
259 + {
260 + "id": "custom.align"
261 + }
262 + ]
263 + }
264 + ]
265 + },
266 "gridPos": {
267 "h": 7,
268 "w": 8,
@@ -154,45 +270,17 @@
270 "y": 1
271 },
272 "id": 31,
157 - "showHeader": true,
158 - "sort": {
159 - "col": 0,
160 - "desc": true
161 - },
162 - "styles": [
163 - {
164 - "alias": "Time",
165 - "align": "auto",
166 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
167 - "pattern": "Time",
168 - "type": "date"
169 - },
170 - {
171 - "alias": "",
172 - "align": "auto",
173 - "colorMode": "row",
174 - "colors": ["rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "#FA6400"],
175 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
176 - "decimals": -1,
177 - "mappingType": 1,
178 - "pattern": "Count",
179 - "thresholds": ["0", "1"],
180 - "type": "number",
181 - "unit": "short"
273 + "options": {
274 + "cellHeight": "sm",
275 + "footer": {
276 + "countRows": false,
277 + "fields": "",
278 + "reducer": ["sum"],
279 + "show": false
280 },
183 - {
184 - "alias": "AGENT",
185 - "align": "auto",
186 - "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
187 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
188 - "decimals": 2,
189 - "mappingType": 1,
190 - "pattern": "agent_name",
191 - "thresholds": [],
192 - "type": "number",
193 - "unit": "short"
194 - }
195 - ],
281 + "showHeader": true
282 + },
283 + "pluginVersion": "10.4.0",
284 "targets": [
285 {
286 "bucketAggs": [
@@ -222,16 +310,138 @@
310 }
311 ],
312 "title": "AGENTS",
225 - "transform": "table",
226 - "type": "table-old"
313 + "transformations": [
314 + {
315 + "id": "merge",
316 + "options": {
317 + "reducers": []
318 + }
319 + }
320 + ],
321 + "type": "table"
322 },
323 {
229 - "columns": [],
324 "datasource": {
325 "type": "elasticsearch",
326 "uid": "replace_datasource_uid"
327 },
234 - "fontSize": "100%",
328 + "fieldConfig": {
329 + "defaults": {
330 + "color": {
331 + "mode": "thresholds"
332 + },
333 + "custom": {
334 + "align": "auto",
335 + "cellOptions": {
336 + "type": "auto"
337 + },
338 + "inspect": false
339 + },
340 + "mappings": [],
341 + "thresholds": {
342 + "mode": "absolute",
343 + "steps": [
344 + {
345 + "color": "green",
346 + "value": null
347 + },
348 + {
349 + "color": "red",
350 + "value": 80
351 + }
352 + ]
353 + }
354 + },
355 + "overrides": [
356 + {
357 + "matcher": {
358 + "id": "byName",
359 + "options": "Time"
360 + },
361 + "properties": [
362 + {
363 + "id": "displayName",
364 + "value": "Time"
365 + },
366 + {
367 + "id": "unit",
368 + "value": "time: YYYY-MM-DD HH:mm:ss"
369 + },
370 + {
371 + "id": "custom.align"
372 + }
373 + ]
374 + },
375 + {
376 + "matcher": {
377 + "id": "byName",
378 + "options": "Count"
379 + },
380 + "properties": [
381 + {
382 + "id": "unit",
383 + "value": "short"
384 + },
385 + {
386 + "id": "decimals",
387 + "value": -1
388 + },
389 + {
390 + "id": "custom.cellOptions",
391 + "value": {
392 + "type": "color-background"
393 + }
394 + },
395 + {
396 + "id": "custom.align"
397 + },
398 + {
399 + "id": "thresholds",
400 + "value": {
401 + "mode": "absolute",
402 + "steps": [
403 + {
404 + "color": "rgba(50, 172, 45, 0.97)",
405 + "value": null
406 + },
407 + {
408 + "color": "rgba(237, 129, 40, 0.89)",
409 + "value": 0
410 + },
411 + {
412 + "color": "#FA6400",
413 + "value": 1
414 + }
415 + ]
416 + }
417 + }
418 + ]
419 + },
420 + {
421 + "matcher": {
422 + "id": "byName",
423 + "options": "data_vulnerability_cvss_cvss2_base_score"
424 + },
425 + "properties": [
426 + {
427 + "id": "displayName",
428 + "value": "CVSS2"
429 + },
430 + {
431 + "id": "unit",
432 + "value": "short"
433 + },
434 + {
435 + "id": "decimals",
436 + "value": 2
437 + },
438 + {
439 + "id": "custom.align"
440 + }
441 + ]
442 + }
443 + ]
444 + },
445 "gridPos": {
446 "h": 7,
447 "w": 6,
@@ -239,45 +449,17 @@
449 "y": 1
450 },
451 "id": 54,
242 - "showHeader": true,
243 - "sort": {
244 - "col": 0,
245 - "desc": true
246 - },
247 - "styles": [
248 - {
249 - "alias": "Time",
250 - "align": "auto",
251 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
252 - "pattern": "Time",
253 - "type": "date"
254 - },
255 - {
256 - "alias": "",
257 - "align": "auto",
258 - "colorMode": "row",
259 - "colors": ["rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "#FA6400"],
260 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
261 - "decimals": -1,
262 - "mappingType": 1,
263 - "pattern": "Count",
264 - "thresholds": ["0", "1"],
265 - "type": "number",
266 - "unit": "short"
452 + "options": {
453 + "cellHeight": "sm",
454 + "footer": {
455 + "countRows": false,
456 + "fields": "",
457 + "reducer": ["sum"],
458 + "show": false
459 },
268 - {
269 - "alias": "CVSS2",
270 - "align": "auto",
271 - "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
272 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
273 - "decimals": 2,
274 - "mappingType": 1,
275 - "pattern": "data_vulnerability_cvss_cvss2_base_score",
276 - "thresholds": [],
277 - "type": "number",
278 - "unit": "short"
279 - }
280 - ],
460 + "showHeader": true
461 + },
462 + "pluginVersion": "10.4.0",
463 "targets": [
464 {
465 "bucketAggs": [
@@ -307,8 +489,15 @@
489 }
490 ],
491 "title": "CVSS2 BASE SCORE",
310 - "transform": "table",
311 - "type": "table-old"
492 + "transformations": [
493 + {
494 + "id": "merge",
495 + "options": {
496 + "reducers": []
497 + }
498 + }
499 + ],
500 + "type": "table"
501 },
502 {
503 "datasource": {
@@ -430,7 +619,7 @@
619 },
620 "showHeader": true
621 },
433 - "pluginVersion": "10.0.3",
622 + "pluginVersion": "10.4.0",
623 "targets": [
624 {
625 "bucketAggs": [
@@ -529,8 +718,10 @@
718 "id": 37,
719 "options": {
720 "displayMode": "gradient",
721 + "maxVizHeight": 300,
722 "minVizHeight": 10,
723 "minVizWidth": 0,
724 + "namePlacement": "auto",
725 "orientation": "horizontal",
726 "reduceOptions": {
727 "calcs": ["sum"],
@@ -538,10 +729,11 @@
729 "values": false
730 },
731 "showUnfilled": true,
732 + "sizing": "auto",
733 "text": {},
734 "valueMode": "color"
735 },
544 - "pluginVersion": "10.0.3",
736 + "pluginVersion": "10.4.0",
737 "targets": [
738 {
739 "bucketAggs": [
@@ -681,7 +873,6 @@
873 "y": 8
874 },
875 "id": 45,
684 - "links": [],
876 "maxDataPoints": 3,
877 "options": {
878 "legend": {
@@ -909,7 +1100,7 @@
1100 },
1101 "showHeader": true
1102 },
912 - "pluginVersion": "10.0.3",
1103 + "pluginVersion": "10.4.0",
1104 "targets": [
1105 {
1106 "bucketAggs": [
@@ -1162,7 +1353,7 @@
1353 "showHeader": true,
1354 "sortBy": []
1355 },
1165 - "pluginVersion": "10.0.3",
1356 + "pluginVersion": "10.4.0",
1357 "targets": [
1358 {
1359 "bucketAggs": [
@@ -1227,6 +1418,7 @@
1418 "mode": "palette-classic"
1419 },
1420 "custom": {
1421 + "axisBorderShow": false,
1422 "axisCenteredZero": false,
1423 "axisColorMode": "text",
1424 "axisLabel": "",
@@ -1240,6 +1432,7 @@
1432 "tooltip": false,
1433 "viz": false
1434 },
1435 + "insertNulls": false,
1436 "lineInterpolation": "linear",
1437 "lineWidth": 1,
1438 "pointSize": 5,
@@ -1589,7 +1782,7 @@
1782 "showHeader": true,
1783 "sortBy": []
1784 },
1592 - "pluginVersion": "10.0.3",
1785 + "pluginVersion": "10.4.0",
1786 "targets": [
1787 {
1788 "bucketAggs": [],
@@ -1883,7 +2076,7 @@
2076 },
2077 "showHeader": true
2078 },
1886 - "pluginVersion": "10.0.3",
2079 + "pluginVersion": "10.4.0",
2080 "targets": [
2081 {
2082 "bucketAggs": [],
@@ -1977,10 +2170,9 @@
2170 "h": 7,
2171 "w": 4,
2172 "x": 0,
1980 - "y": 27
2173 + "y": 54
2174 },
2175 "id": 63,
1983 - "links": [],
2176 "options": {
2177 "colorMode": "value",
2178 "graphMode": "area",
@@ -1991,10 +2183,12 @@
2183 "fields": "",
2184 "values": false
2185 },
2186 + "showPercentChange": false,
2187 "text": {},
1995 - "textMode": "auto"
2188 + "textMode": "auto",
2189 + "wideLayout": true
2190 },
1997 - "pluginVersion": "10.0.3",
2191 + "pluginVersion": "10.4.0",
2192 "targets": [
2193 {
2194 "bucketAggs": [
@@ -2029,61 +2223,145 @@
2223 "type": "stat"
2224 },
2225 {
2032 - "columns": [],
2226 "datasource": {
2227 "type": "elasticsearch",
2228 "uid": "replace_datasource_uid"
2229 },
2037 - "fontSize": "100%",
2230 + "fieldConfig": {
2231 + "defaults": {
2232 + "color": {
2233 + "mode": "thresholds"
2234 + },
2235 + "custom": {
2236 + "align": "auto",
2237 + "cellOptions": {
2238 + "type": "auto"
2239 + },
2240 + "inspect": false
2241 + },
2242 + "mappings": [],
2243 + "thresholds": {
2244 + "mode": "absolute",
2245 + "steps": [
2246 + {
2247 + "color": "green",
2248 + "value": null
2249 + },
2250 + {
2251 + "color": "red",
2252 + "value": 80
2253 + }
2254 + ]
2255 + }
2256 + },
2257 + "overrides": [
2258 + {
2259 + "matcher": {
2260 + "id": "byName",
2261 + "options": "Time"
2262 + },
2263 + "properties": [
2264 + {
2265 + "id": "displayName",
2266 + "value": "Time"
2267 + },
2268 + {
2269 + "id": "unit",
2270 + "value": "time: YYYY-MM-DD HH:mm:ss"
2271 + },
2272 + {
2273 + "id": "custom.align"
2274 + }
2275 + ]
2276 + },
2277 + {
2278 + "matcher": {
2279 + "id": "byName",
2280 + "options": "Count"
2281 + },
2282 + "properties": [
2283 + {
2284 + "id": "unit",
2285 + "value": "short"
2286 + },
2287 + {
2288 + "id": "decimals",
2289 + "value": -1
2290 + },
2291 + {
2292 + "id": "custom.cellOptions",
2293 + "value": {
2294 + "type": "color-background"
2295 + }
2296 + },
2297 + {
2298 + "id": "custom.align"
2299 + },
2300 + {
2301 + "id": "thresholds",
2302 + "value": {
2303 + "mode": "absolute",
2304 + "steps": [
2305 + {
2306 + "color": "rgba(50, 172, 45, 0.97)",
2307 + "value": null
2308 + },
2309 + {
2310 + "color": "rgba(237, 129, 40, 0.89)",
2311 + "value": 0
2312 + },
2313 + {
2314 + "color": "#FA6400",
2315 + "value": 1
2316 + }
2317 + ]
2318 + }
2319 + }
2320 + ]
2321 + },
2322 + {
2323 + "matcher": {
2324 + "id": "byName",
2325 + "options": "agent_name"
2326 + },
2327 + "properties": [
2328 + {
2329 + "id": "displayName",
2330 + "value": "AGENT"
2331 + },
2332 + {
2333 + "id": "unit",
2334 + "value": "short"
2335 + },
2336 + {
2337 + "id": "decimals",
2338 + "value": 2
2339 + },
2340 + {
2341 + "id": "custom.align"
2342 + }
2343 + ]
2344 + }
2345 + ]
2346 + },
2347 "gridPos": {
2348 "h": 7,
2349 "w": 8,
2350 "x": 4,
2042 - "y": 27
2351 + "y": 54
2352 },
2353 "id": 64,
2045 - "showHeader": true,
2046 - "sort": {
2047 - "col": 0,
2048 - "desc": true
2049 - },
2050 - "styles": [
2051 - {
2052 - "$$hashKey": "object:108",
2053 - "alias": "Time",
2054 - "align": "auto",
2055 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
2056 - "pattern": "Time",
2057 - "type": "date"
2058 - },
2059 - {
2060 - "$$hashKey": "object:109",
2061 - "alias": "",
2062 - "align": "auto",
2063 - "colorMode": "row",
2064 - "colors": ["rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "#FA6400"],
2065 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
2066 - "decimals": -1,
2067 - "mappingType": 1,
2068 - "pattern": "Count",
2069 - "thresholds": ["0", "1"],
2070 - "type": "number",
2071 - "unit": "short"
2354 + "options": {
2355 + "cellHeight": "sm",
2356 + "footer": {
2357 + "countRows": false,
2358 + "fields": "",
2359 + "reducer": ["sum"],
2360 + "show": false
2361 },
2073 - {
2074 - "$$hashKey": "object:110",
2075 - "alias": "AGENT",
2076 - "align": "auto",
2077 - "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
2078 - "dateFormat": "YYYY-MM-DD HH:mm:ss",
2079 - "decimals": 2,
2080 - "mappingType": 1,
2081 - "pattern": "agent_name",
2082 - "thresholds": [],
2083 - "type": "number",
2084 - "unit": "short"
2085 - }
2086 - ],
2362 + "showHeader": true
2363 + },
2364 + "pluginVersion": "10.4.0",
2365 "targets": [
2366 {
2367 "bucketAggs": [
@@ -2117,8 +2395,15 @@
2395 }
2396 ],
2397 "title": "DOCKER HOST",
2120 - "transform": "table",
2121 - "type": "table-old"
2398 + "transformations": [
2399 + {
2400 + "id": "merge",
2401 + "options": {
2402 + "reducers": []
2403 + }
2404 + }
2405 + ],
2406 + "type": "table"
2407 },
2408 {
2409 "datasource": {
@@ -2255,7 +2540,7 @@
2540 "h": 7,
2541 "w": 6,
2542 "x": 12,
2258 - "y": 27
2543 + "y": 54
2544 },
2545 "id": 71,
2546 "options": {
@@ -2268,7 +2553,7 @@
2553 },
2554 "showHeader": true
2555 },
2271 - "pluginVersion": "10.0.3",
2556 + "pluginVersion": "10.4.0",
2557 "targets": [
2558 {
2559 "bucketAggs": [
@@ -2449,7 +2734,7 @@
2734 "h": 7,
2735 "w": 6,
2736 "x": 18,
2452 - "y": 27
2737 + "y": 54
2738 },
2739 "id": 65,
2740 "options": {
@@ -2462,7 +2747,7 @@
2747 },
2748 "showHeader": true
2749 },
2465 - "pluginVersion": "10.0.3",
2750 + "pluginVersion": "10.4.0",
2751 "targets": [
2752 {
2753 "bucketAggs": [
@@ -2536,13 +2821,15 @@
2821 "h": 11,
2822 "w": 17,
2823 "x": 0,
2539 - "y": 34
2824 + "y": 61
2825 },
2826 "id": 67,
2827 "options": {
2828 "displayMode": "gradient",
2829 + "maxVizHeight": 300,
2830 "minVizHeight": 10,
2831 "minVizWidth": 0,
2832 + "namePlacement": "auto",
2833 "orientation": "vertical",
2834 "reduceOptions": {
2835 "calcs": ["sum"],
@@ -2550,10 +2837,11 @@
2837 "values": false
2838 },
2839 "showUnfilled": true,
2840 + "sizing": "auto",
2841 "text": {},
2842 "valueMode": "color"
2843 },
2556 - "pluginVersion": "10.0.3",
2844 + "pluginVersion": "10.4.0",
2845 "targets": [
2846 {
2847 "bucketAggs": [
@@ -2720,10 +3008,9 @@
3008 "h": 11,
3009 "w": 7,
3010 "x": 17,
2723 - "y": 34
3011 + "y": 61
3012 },
3013 "id": 66,
2726 - "links": [],
3014 "maxDataPoints": 3,
3015 "options": {
3016 "legend": {
@@ -2821,14 +3108,16 @@
3108 "align": "auto",
3109 "cellOptions": {
3110 "type": "auto"
2824 - }
3111 + },
3112 + "inspect": false
3113 },
3114 "mappings": [],
3115 "thresholds": {
3116 "mode": "absolute",
3117 "steps": [
3118 {
2831 - "color": "green"
3119 + "color": "green",
3120 + "value": null
3121 },
3122 {
3123 "color": "red",
@@ -2974,18 +3263,20 @@
3263 "h": 12,
3264 "w": 24,
3265 "x": 0,
2977 - "y": 46
3266 + "y": 55
3267 },
3268 "id": 68,
3269 "options": {
3270 + "cellHeight": "sm",
3271 "footer": {
3272 + "countRows": false,
3273 "fields": "",
3274 "reducer": ["sum"],
3275 "show": false
3276 },
3277 "showHeader": true
3278 },
2988 - "pluginVersion": "8.3.3",
3279 + "pluginVersion": "10.4.0",
3280 "targets": [
3281 {
3282 "bucketAggs": [],
@@ -3068,8 +3359,7 @@
3359 }
3360 ],
3361 "refresh": "",
3071 - "schemaVersion": 38,
3072 - "style": "dark",
3362 + "schemaVersion": 39,
3363 "tags": ["EDR"],
3364 "templating": {
3365 "list": [
backend/app/connectors/grafana/dashboards/Wazuh/edr_windows_event_logs.json
+492 -362
@@ -25,7 +25,6 @@
25 "fiscalYearStartMonth": 0,
26 "graphTooltip": 0,
27 "id": null,
28 - "iteration": 1658194464346,
28 "links": [
29 {
30 "asDropdown": true,
@@ -76,7 +75,8 @@
75 "mode": "absolute",
76 "steps": [
77 {
79 - "color": "blue"
78 + "color": "blue",
79 + "value": null
80 }
81 ]
82 },
@@ -91,7 +91,6 @@
91 "y": 1
92 },
93 "id": 67,
94 - "links": [],
94 "options": {
95 "colorMode": "value",
96 "graphMode": "area",
@@ -102,10 +101,12 @@
101 "fields": "",
102 "values": false
103 },
104 + "showPercentChange": false,
105 "text": {},
106 - "textMode": "auto"
106 + "textMode": "auto",
107 + "wideLayout": true
108 },
108 - "pluginVersion": "9.0.0",
109 + "pluginVersion": "10.4.0",
110 "targets": [
111 {
112 "bucketAggs": [
@@ -333,7 +334,6 @@
334 "y": 1
335 },
336 "id": 68,
336 - "links": [],
337 "maxDataPoints": 3,
338 "options": {
339 "displayLabels": [],
@@ -341,6 +341,7 @@
341 "calcs": [],
342 "displayMode": "table",
343 "placement": "bottom",
344 + "showLegend": true,
345 "values": ["value"]
346 },
347 "pieType": "donut",
@@ -408,7 +409,9 @@
409 "defaults": {
410 "custom": {
411 "align": "auto",
411 - "displayMode": "auto",
412 + "cellOptions": {
413 + "type": "auto"
414 + },
415 "filterable": false,
416 "inspect": false
417 },
@@ -417,7 +420,8 @@
420 "mode": "absolute",
421 "steps": [
422 {
420 - "color": "orange"
423 + "color": "orange",
424 + "value": null
425 },
426 {
427 "color": "red",
@@ -434,8 +438,11 @@
438 },
439 "properties": [
440 {
437 - "id": "custom.displayMode",
438 - "value": "basic"
441 + "id": "custom.cellOptions",
442 + "value": {
443 + "mode": "basic",
444 + "type": "gauge"
445 + }
446 }
447 ]
448 },
@@ -521,10 +528,11 @@
528 "y": 1
529 },
530 "id": 69,
524 - "links": [],
531 "maxDataPoints": 3,
532 "options": {
533 + "cellHeight": "sm",
534 "footer": {
535 + "countRows": false,
536 "fields": "",
537 "reducer": ["sum"],
538 "show": false
@@ -532,7 +540,7 @@
540 "showHeader": true,
541 "sortBy": []
542 },
535 - "pluginVersion": "9.0.0",
543 + "pluginVersion": "10.4.0",
544 "targets": [
545 {
546 "bucketAggs": [
@@ -588,7 +596,9 @@
596 "defaults": {
597 "custom": {
598 "align": "auto",
591 - "displayMode": "auto",
599 + "cellOptions": {
600 + "type": "auto"
601 + },
602 "filterable": false,
603 "inspect": false
604 },
@@ -597,7 +607,8 @@
607 "mode": "absolute",
608 "steps": [
609 {
600 - "color": "green"
610 + "color": "green",
611 + "value": null
612 },
613 {
614 "color": "red",
@@ -628,10 +639,11 @@
639 "y": 8
640 },
641 "id": 70,
631 - "links": [],
642 "maxDataPoints": 3,
643 "options": {
644 + "cellHeight": "sm",
645 "footer": {
646 + "countRows": false,
647 "fields": "",
648 "reducer": ["sum"],
649 "show": false
@@ -639,7 +651,7 @@
651 "showHeader": true,
652 "sortBy": []
653 },
642 - "pluginVersion": "9.0.0",
654 + "pluginVersion": "10.4.0",
655 "targets": [
656 {
657 "bucketAggs": [
@@ -674,52 +686,86 @@
686 "type": "table"
687 },
688 {
677 - "aliasColors": {},
678 - "bars": true,
679 - "dashLength": 10,
680 - "dashes": false,
689 "datasource": {
690 "type": "elasticsearch",
691 "uid": "replace_datasource_uid"
692 },
685 - "fill": 1,
686 - "fillGradient": 0,
693 + "fieldConfig": {
694 + "defaults": {
695 + "color": {
696 + "mode": "palette-classic"
697 + },
698 + "custom": {
699 + "axisBorderShow": false,
700 + "axisCenteredZero": false,
701 + "axisColorMode": "text",
702 + "axisLabel": "",
703 + "axisPlacement": "auto",
704 + "barAlignment": 0,
705 + "drawStyle": "bars",
706 + "fillOpacity": 100,
707 + "gradientMode": "none",
708 + "hideFrom": {
709 + "legend": false,
710 + "tooltip": false,
711 + "viz": false
712 + },
713 + "insertNulls": false,
714 + "lineInterpolation": "linear",
715 + "lineWidth": 1,
716 + "pointSize": 5,
717 + "scaleDistribution": {
718 + "type": "linear"
719 + },
720 + "showPoints": "never",
721 + "spanNulls": false,
722 + "stacking": {
723 + "group": "A",
724 + "mode": "normal"
725 + },
726 + "thresholdsStyle": {
727 + "mode": "off"
728 + }
729 + },
730 + "mappings": [],
731 + "thresholds": {
732 + "mode": "absolute",
733 + "steps": [
734 + {
735 + "color": "green",
736 + "value": null
737 + },
738 + {
739 + "color": "red",
740 + "value": 80
741 + }
742 + ]
743 + },
744 + "unit": "short"
745 + },
746 + "overrides": []
747 + },
748 "gridPos": {
749 "h": 7,
750 "w": 15,
751 "x": 9,
752 "y": 8
753 },
693 - "hiddenSeries": false,
754 "id": 83,
695 - "legend": {
696 - "alignAsTable": true,
697 - "avg": false,
698 - "current": false,
699 - "max": false,
700 - "min": false,
701 - "rightSide": true,
702 - "show": true,
703 - "total": false,
704 - "values": false
705 - },
706 - "lines": false,
707 - "linewidth": 1,
708 - "links": [],
755 "maxDataPoints": 3,
710 - "nullPointMode": "null",
756 "options": {
712 - "alertThreshold": true
713 - },
714 - "percentage": false,
715 - "pluginVersion": "9.0.0",
716 - "pointradius": 2,
717 - "points": false,
718 - "renderer": "flot",
719 - "seriesOverrides": [],
720 - "spaceLength": 10,
721 - "stack": true,
722 - "steppedLine": false,
757 + "legend": {
758 + "calcs": [],
759 + "displayMode": "table",
760 + "placement": "right",
761 + "showLegend": true
762 + },
763 + "tooltip": {
764 + "mode": "multi",
765 + "sort": "none"
766 + }
767 + },
768 + "pluginVersion": "10.4.0",
769 "targets": [
770 {
771 "alias": "",
@@ -759,35 +805,8 @@
805 "timeField": "timestamp"
806 }
807 ],
762 - "thresholds": [],
763 - "timeRegions": [],
808 "title": "WINDOWS SYSTEM - EVENTS BY AGENT (HISTOGRAM)",
765 - "tooltip": {
766 - "shared": true,
767 - "sort": 0,
768 - "value_type": "individual"
769 - },
770 - "type": "graph",
771 - "xaxis": {
772 - "mode": "time",
773 - "show": true,
774 - "values": []
775 - },
776 - "yaxes": [
777 - {
778 - "format": "short",
779 - "logBase": 1,
780 - "show": true
781 - },
782 - {
783 - "format": "short",
784 - "logBase": 1,
785 - "show": true
786 - }
787 - ],
788 - "yaxis": {
789 - "align": false
790 - }
809 + "type": "timeseries"
810 },
811 {
812 "datasource": {
@@ -801,7 +820,9 @@
820 },
821 "custom": {
822 "align": "auto",
804 - "displayMode": "auto",
823 + "cellOptions": {
824 + "type": "auto"
825 + },
826 "inspect": false
827 },
828 "mappings": [],
@@ -809,7 +830,8 @@
830 "mode": "absolute",
831 "steps": [
832 {
812 - "color": "green"
833 + "color": "green",
834 + "value": null
835 },
836 {
837 "color": "red",
@@ -943,7 +965,9 @@
965 },
966 "id": 85,
967 "options": {
968 + "cellHeight": "sm",
969 "footer": {
970 + "countRows": false,
971 "fields": "",
972 "reducer": ["sum"],
973 "show": false
@@ -951,7 +975,7 @@
975 "showHeader": true,
976 "sortBy": []
977 },
954 - "pluginVersion": "9.0.0",
978 + "pluginVersion": "10.4.0",
979 "targets": [
980 {
981 "alias": "",
@@ -1193,7 +1217,8 @@
1217 "mode": "absolute",
1218 "steps": [
1219 {
1196 - "color": "blue"
1220 + "color": "blue",
1221 + "value": null
1222 }
1223 ]
1224 },
@@ -1208,7 +1233,6 @@
1233 "y": 2
1234 },
1235 "id": 86,
1211 - "links": [],
1236 "options": {
1237 "colorMode": "value",
1238 "graphMode": "area",
@@ -1219,10 +1243,12 @@
1243 "fields": "",
1244 "values": false
1245 },
1246 + "showPercentChange": false,
1247 "text": {},
1223 - "textMode": "auto"
1248 + "textMode": "auto",
1249 + "wideLayout": true
1250 },
1225 - "pluginVersion": "9.0.0",
1251 + "pluginVersion": "10.4.0",
1252 "targets": [
1253 {
1254 "bucketAggs": [
@@ -1450,7 +1476,6 @@
1476 "y": 2
1477 },
1478 "id": 87,
1453 - "links": [],
1479 "maxDataPoints": 3,
1480 "options": {
1481 "displayLabels": [],
@@ -1458,6 +1483,7 @@
1483 "calcs": [],
1484 "displayMode": "table",
1485 "placement": "bottom",
1486 + "showLegend": true,
1487 "values": ["value"]
1488 },
1489 "pieType": "donut",
@@ -1525,7 +1551,9 @@
1551 "defaults": {
1552 "custom": {
1553 "align": "auto",
1528 - "displayMode": "auto",
1554 + "cellOptions": {
1555 + "type": "auto"
1556 + },
1557 "filterable": false,
1558 "inspect": false
1559 },
@@ -1534,7 +1562,8 @@
1562 "mode": "absolute",
1563 "steps": [
1564 {
1537 - "color": "orange"
1565 + "color": "orange",
1566 + "value": null
1567 },
1568 {
1569 "color": "red",
@@ -1551,8 +1580,11 @@
1580 },
1581 "properties": [
1582 {
1554 - "id": "custom.displayMode",
1555 - "value": "basic"
1583 + "id": "custom.cellOptions",
1584 + "value": {
1585 + "mode": "basic",
1586 + "type": "gauge"
1587 + }
1588 }
1589 ]
1590 },
@@ -1638,10 +1670,11 @@
1670 "y": 2
1671 },
1672 "id": 88,
1641 - "links": [],
1673 "maxDataPoints": 3,
1674 "options": {
1675 + "cellHeight": "sm",
1676 "footer": {
1677 + "countRows": false,
1678 "fields": "",
1679 "reducer": ["sum"],
1680 "show": false
@@ -1649,7 +1682,7 @@
1682 "showHeader": true,
1683 "sortBy": []
1684 },
1652 - "pluginVersion": "9.0.0",
1685 + "pluginVersion": "10.4.0",
1686 "targets": [
1687 {
1688 "bucketAggs": [
@@ -1705,7 +1738,9 @@
1738 "defaults": {
1739 "custom": {
1740 "align": "auto",
1708 - "displayMode": "auto",
1741 + "cellOptions": {
1742 + "type": "auto"
1743 + },
1744 "filterable": false,
1745 "inspect": false
1746 },
@@ -1714,7 +1749,8 @@
1749 "mode": "absolute",
1750 "steps": [
1751 {
1717 - "color": "green"
1752 + "color": "green",
1753 + "value": null
1754 },
1755 {
1756 "color": "red",
@@ -1745,10 +1781,11 @@
1781 "y": 9
1782 },
1783 "id": 90,
1748 - "links": [],
1784 "maxDataPoints": 3,
1785 "options": {
1786 + "cellHeight": "sm",
1787 "footer": {
1788 + "countRows": false,
1789 "fields": "",
1790 "reducer": ["sum"],
1791 "show": false
@@ -1756,7 +1793,7 @@
1793 "showHeader": true,
1794 "sortBy": []
1795 },
1759 - "pluginVersion": "9.0.0",
1796 + "pluginVersion": "10.4.0",
1797 "targets": [
1798 {
1799 "bucketAggs": [
@@ -1791,52 +1828,86 @@
1828 "type": "table"
1829 },
1830 {
1794 - "aliasColors": {},
1795 - "bars": true,
1796 - "dashLength": 10,
1797 - "dashes": false,
1831 "datasource": {
1832 "type": "elasticsearch",
1833 "uid": "replace_datasource_uid"
1834 },
1802 - "fill": 1,
1803 - "fillGradient": 0,
1835 + "fieldConfig": {
1836 + "defaults": {
1837 + "color": {
1838 + "mode": "palette-classic"
1839 + },
1840 + "custom": {
1841 + "axisBorderShow": false,
1842 + "axisCenteredZero": false,
1843 + "axisColorMode": "text",
1844 + "axisLabel": "",
1845 + "axisPlacement": "auto",
1846 + "barAlignment": 0,
1847 + "drawStyle": "bars",
1848 + "fillOpacity": 100,
1849 + "gradientMode": "none",
1850 + "hideFrom": {
1851 + "legend": false,
1852 + "tooltip": false,
1853 + "viz": false
1854 + },
1855 + "insertNulls": false,
1856 + "lineInterpolation": "linear",
1857 + "lineWidth": 1,
1858 + "pointSize": 5,
1859 + "scaleDistribution": {
1860 + "type": "linear"
1861 + },
1862 + "showPoints": "never",
1863 + "spanNulls": false,
1864 + "stacking": {
1865 + "group": "A",
1866 + "mode": "normal"
1867 + },
1868 + "thresholdsStyle": {
1869 + "mode": "off"
1870 + }
1871 + },
1872 + "mappings": [],
1873 + "thresholds": {
1874 + "mode": "absolute",
1875 + "steps": [
1876 + {
1877 + "color": "green",
1878 + "value": null
1879 + },
1880 + {
1881 + "color": "red",
1882 + "value": 80
1883 + }
1884 + ]
1885 + },
1886 + "unit": "short"
1887 + },
1888 + "overrides": []
1889 + },
1890 "gridPos": {
1891 "h": 7,
1892 "w": 15,
1893 "x": 9,
1894 "y": 9
1895 },
1810 - "hiddenSeries": false,
1896 "id": 91,
1812 - "legend": {
1813 - "alignAsTable": true,
1814 - "avg": false,
1815 - "current": false,
1816 - "max": false,
1817 - "min": false,
1818 - "rightSide": true,
1819 - "show": true,
1820 - "total": false,
1821 - "values": false
1822 - },
1823 - "lines": false,
1824 - "linewidth": 1,
1825 - "links": [],
1897 "maxDataPoints": 3,
1827 - "nullPointMode": "null",
1898 "options": {
1829 - "alertThreshold": true
1830 - },
1831 - "percentage": false,
1832 - "pluginVersion": "9.0.0",
1833 - "pointradius": 2,
1834 - "points": false,
1835 - "renderer": "flot",
1836 - "seriesOverrides": [],
1837 - "spaceLength": 10,
1838 - "stack": true,
1839 - "steppedLine": false,
1899 + "legend": {
1900 + "calcs": [],
1901 + "displayMode": "table",
1902 + "placement": "right",
1903 + "showLegend": true
1904 + },
1905 + "tooltip": {
1906 + "mode": "multi",
1907 + "sort": "none"
1908 + }
1909 + },
1910 + "pluginVersion": "10.4.0",
1911 "targets": [
1912 {
1913 "alias": "",
@@ -1876,35 +1947,8 @@
1947 "timeField": "timestamp"
1948 }
1949 ],
1879 - "thresholds": [],
1880 - "timeRegions": [],
1950 "title": "WINDOWS APPLICATIONS - EVENTS BY AGENT (HISTOGRAM)",
1882 - "tooltip": {
1883 - "shared": true,
1884 - "sort": 0,
1885 - "value_type": "individual"
1886 - },
1887 - "type": "graph",
1888 - "xaxis": {
1889 - "mode": "time",
1890 - "show": true,
1891 - "values": []
1892 - },
1893 - "yaxes": [
1894 - {
1895 - "format": "short",
1896 - "logBase": 1,
1897 - "show": true
1898 - },
1899 - {
1900 - "format": "short",
1901 - "logBase": 1,
1902 - "show": true
1903 - }
1904 - ],
1905 - "yaxis": {
1906 - "align": false
1907 - }
1951 + "type": "timeseries"
1952 },
1953 {
1954 "datasource": {
@@ -1918,7 +1962,9 @@
1962 },
1963 "custom": {
1964 "align": "auto",
1921 - "displayMode": "auto",
1965 + "cellOptions": {
1966 + "type": "auto"
1967 + },
1968 "inspect": false
1969 },
1970 "mappings": [],
@@ -1926,7 +1972,8 @@
1972 "mode": "absolute",
1973 "steps": [
1974 {
1929 - "color": "green"
1975 + "color": "green",
1976 + "value": null
1977 },
1978 {
1979 "color": "red",
@@ -2060,7 +2107,9 @@
2107 },
2108 "id": 89,
2109 "options": {
2110 + "cellHeight": "sm",
2111 "footer": {
2112 + "countRows": false,
2113 "fields": "",
2114 "reducer": ["sum"],
2115 "show": false
@@ -2068,7 +2117,7 @@
2117 "showHeader": true,
2118 "sortBy": []
2119 },
2071 - "pluginVersion": "9.0.0",
2120 + "pluginVersion": "10.4.0",
2121 "targets": [
2122 {
2123 "alias": "",
@@ -2311,7 +2360,8 @@
2360 "mode": "absolute",
2361 "steps": [
2362 {
2314 - "color": "blue"
2363 + "color": "blue",
2364 + "value": null
2365 }
2366 ]
2367 },
@@ -2326,7 +2376,6 @@
2376 "y": 3
2377 },
2378 "id": 43,
2329 - "links": [],
2379 "options": {
2380 "colorMode": "value",
2381 "graphMode": "area",
@@ -2337,10 +2386,12 @@
2386 "fields": "",
2387 "values": false
2388 },
2389 + "showPercentChange": false,
2390 "text": {},
2341 - "textMode": "auto"
2391 + "textMode": "auto",
2392 + "wideLayout": true
2393 },
2343 - "pluginVersion": "9.0.0",
2394 + "pluginVersion": "10.4.0",
2395 "targets": [
2396 {
2397 "bucketAggs": [
@@ -2557,7 +2608,6 @@
2608 "y": 3
2609 },
2610 "id": 59,
2560 - "links": [],
2611 "maxDataPoints": 3,
2612 "options": {
2613 "displayLabels": [],
@@ -2565,6 +2615,7 @@
2615 "calcs": [],
2616 "displayMode": "table",
2617 "placement": "right",
2618 + "showLegend": true,
2619 "values": ["value"]
2620 },
2621 "pieType": "donut",
@@ -2636,7 +2687,9 @@
2687 "defaults": {
2688 "custom": {
2689 "align": "auto",
2639 - "displayMode": "auto",
2690 + "cellOptions": {
2691 + "type": "auto"
2692 + },
2693 "filterable": false,
2694 "inspect": false
2695 },
@@ -2645,7 +2698,8 @@
2698 "mode": "absolute",
2699 "steps": [
2700 {
2648 - "color": "blue"
2701 + "color": "blue",
2702 + "value": null
2703 }
2704 ]
2705 }
@@ -2658,8 +2712,11 @@
2712 },
2713 "properties": [
2714 {
2661 - "id": "custom.displayMode",
2662 - "value": "basic"
2715 + "id": "custom.cellOptions",
2716 + "value": {
2717 + "mode": "basic",
2718 + "type": "gauge"
2719 + }
2720 }
2721 ]
2722 }
@@ -2672,17 +2729,18 @@
2729 "y": 3
2730 },
2731 "id": 63,
2675 - "links": [],
2732 "maxDataPoints": 3,
2733 "options": {
2734 + "cellHeight": "sm",
2735 "footer": {
2736 + "countRows": false,
2737 "fields": "",
2738 "reducer": ["sum"],
2739 "show": false
2740 },
2741 "showHeader": true
2742 },
2685 - "pluginVersion": "9.0.0",
2743 + "pluginVersion": "10.4.0",
2744 "targets": [
2745 {
2746 "bucketAggs": [
@@ -2742,7 +2800,9 @@
2800 "defaults": {
2801 "custom": {
2802 "align": "auto",
2745 - "displayMode": "auto",
2803 + "cellOptions": {
2804 + "type": "auto"
2805 + },
2806 "filterable": false,
2807 "inspect": false
2808 },
@@ -2751,7 +2811,8 @@
2811 "mode": "absolute",
2812 "steps": [
2813 {
2754 - "color": "orange"
2814 + "color": "orange",
2815 + "value": null
2816 },
2817 {
2818 "color": "red",
@@ -2769,17 +2830,18 @@
2830 "y": 10
2831 },
2832 "id": 60,
2772 - "links": [],
2833 "maxDataPoints": 3,
2834 "options": {
2835 + "cellHeight": "sm",
2836 "footer": {
2837 + "countRows": false,
2838 "fields": "",
2839 "reducer": ["sum"],
2840 "show": false
2841 },
2842 "showHeader": true
2843 },
2782 - "pluginVersion": "9.0.0",
2844 + "pluginVersion": "10.4.0",
2845 "targets": [
2846 {
2847 "bucketAggs": [
@@ -2818,52 +2880,86 @@
2880 "type": "table"
2881 },
2882 {
2821 - "aliasColors": {},
2822 - "bars": true,
2823 - "dashLength": 10,
2824 - "dashes": false,
2883 "datasource": {
2884 "type": "elasticsearch",
2885 "uid": "replace_datasource_uid"
2886 },
2829 - "fill": 1,
2830 - "fillGradient": 0,
2887 + "fieldConfig": {
2888 + "defaults": {
2889 + "color": {
2890 + "mode": "palette-classic"
2891 + },
2892 + "custom": {
2893 + "axisBorderShow": false,
2894 + "axisCenteredZero": false,
2895 + "axisColorMode": "text",
2896 + "axisLabel": "",
2897 + "axisPlacement": "auto",
2898 + "barAlignment": 0,
2899 + "drawStyle": "bars",
2900 + "fillOpacity": 100,
2901 + "gradientMode": "none",
2902 + "hideFrom": {
2903 + "legend": false,
2904 + "tooltip": false,
2905 + "viz": false
2906 + },
2907 + "insertNulls": false,
2908 + "lineInterpolation": "linear",
2909 + "lineWidth": 1,
2910 + "pointSize": 5,
2911 + "scaleDistribution": {
2912 + "type": "linear"
2913 + },
2914 + "showPoints": "never",
2915 + "spanNulls": false,
2916 + "stacking": {
2917 + "group": "A",
2918 + "mode": "normal"
2919 + },
2920 + "thresholdsStyle": {
2921 + "mode": "off"
2922 + }
2923 + },
2924 + "mappings": [],
2925 + "thresholds": {
2926 + "mode": "absolute",
2927 + "steps": [
2928 + {
2929 + "color": "green",
2930 + "value": null
2931 + },
2932 + {
2933 + "color": "red",
2934 + "value": 80
2935 + }
2936 + ]
2937 + },
2938 + "unit": "short"
2939 + },
2940 + "overrides": []
2941 + },
2942 "gridPos": {
2943 "h": 7,
2944 "w": 15,
2945 "x": 9,
2946 "y": 10
2947 },
2837 - "hiddenSeries": false,
2948 "id": 92,
2839 - "legend": {
2840 - "alignAsTable": true,
2841 - "avg": false,
2842 - "current": false,
2843 - "max": false,
2844 - "min": false,
2845 - "rightSide": true,
2846 - "show": true,
2847 - "total": false,
2848 - "values": false
2849 - },
2850 - "lines": false,
2851 - "linewidth": 1,
2852 - "links": [],
2949 "maxDataPoints": 3,
2854 - "nullPointMode": "null",
2950 "options": {
2856 - "alertThreshold": true
2857 - },
2858 - "percentage": false,
2859 - "pluginVersion": "9.0.0",
2860 - "pointradius": 2,
2861 - "points": false,
2862 - "renderer": "flot",
2863 - "seriesOverrides": [],
2864 - "spaceLength": 10,
2865 - "stack": true,
2866 - "steppedLine": false,
2951 + "legend": {
2952 + "calcs": [],
2953 + "displayMode": "table",
2954 + "placement": "right",
2955 + "showLegend": true
2956 + },
2957 + "tooltip": {
2958 + "mode": "multi",
2959 + "sort": "none"
2960 + }
2961 + },
2962 + "pluginVersion": "10.4.0",
2963 "targets": [
2964 {
2965 "alias": "",
@@ -2907,35 +3003,8 @@
3003 "timeField": "timestamp"
3004 }
3005 ],
2910 - "thresholds": [],
2911 - "timeRegions": [],
3006 "title": "WINDOWS SECURITY - EVENTS BY AGENT (HISTOGRAM)",
2913 - "tooltip": {
2914 - "shared": true,
2915 - "sort": 0,
2916 - "value_type": "individual"
2917 - },
2918 - "type": "graph",
2919 - "xaxis": {
2920 - "mode": "time",
2921 - "show": true,
2922 - "values": []
2923 - },
2924 - "yaxes": [
2925 - {
2926 - "format": "short",
2927 - "logBase": 1,
2928 - "show": true
2929 - },
2930 - {
2931 - "format": "short",
2932 - "logBase": 1,
2933 - "show": true
2934 - }
2935 - ],
2936 - "yaxis": {
2937 - "align": false
2938 - }
3007 + "type": "timeseries"
3008 },
3009 {
3010 "datasource": {
@@ -3088,14 +3157,14 @@
3157 "y": 17
3158 },
3159 "id": 94,
3091 - "links": [],
3160 "maxDataPoints": 3,
3161 "options": {
3162 "displayLabels": [],
3163 "legend": {
3164 "calcs": [],
3097 - "displayMode": "hidden",
3165 + "displayMode": "list",
3166 "placement": "bottom",
3167 + "showLegend": false,
3168 "values": ["value"]
3169 },
3170 "pieType": "donut",
@@ -3167,7 +3236,9 @@
3236 "defaults": {
3237 "custom": {
3238 "align": "auto",
3170 - "displayMode": "auto",
3239 + "cellOptions": {
3240 + "type": "auto"
3241 + },
3242 "filterable": false,
3243 "inspect": false
3244 },
@@ -3176,7 +3247,8 @@
3247 "mode": "absolute",
3248 "steps": [
3249 {
3179 - "color": "orange"
3250 + "color": "orange",
3251 + "value": null
3252 },
3253 {
3254 "color": "red",
@@ -3194,17 +3266,18 @@
3266 "y": 17
3267 },
3268 "id": 95,
3197 - "links": [],
3269 "maxDataPoints": 3,
3270 "options": {
3271 + "cellHeight": "sm",
3272 "footer": {
3273 + "countRows": false,
3274 "fields": "",
3275 "reducer": ["sum"],
3276 "show": false
3277 },
3278 "showHeader": true
3279 },
3207 - "pluginVersion": "9.0.0",
3280 + "pluginVersion": "10.4.0",
3281 "targets": [
3282 {
3283 "bucketAggs": [
@@ -3251,7 +3324,9 @@
3324 "defaults": {
3325 "custom": {
3326 "align": "auto",
3254 - "displayMode": "auto",
3327 + "cellOptions": {
3328 + "type": "auto"
3329 + },
3330 "filterable": false,
3331 "inspect": false
3332 },
@@ -3260,7 +3335,8 @@
3335 "mode": "absolute",
3336 "steps": [
3337 {
3263 - "color": "orange"
3338 + "color": "orange",
3339 + "value": null
3340 },
3341 {
3342 "color": "red",
@@ -3277,8 +3353,11 @@
3353 },
3354 "properties": [
3355 {
3280 - "id": "custom.displayMode",
3281 - "value": "basic"
3356 + "id": "custom.cellOptions",
3357 + "value": {
3358 + "mode": "basic",
3359 + "type": "gauge"
3360 + }
3361 }
3362 ]
3363 }
@@ -3291,17 +3370,18 @@
3370 "y": 17
3371 },
3372 "id": 96,
3294 - "links": [],
3373 "maxDataPoints": 3,
3374 "options": {
3375 + "cellHeight": "sm",
3376 "footer": {
3377 + "countRows": false,
3378 "fields": "",
3379 "reducer": ["sum"],
3380 "show": false
3381 },
3382 "showHeader": true
3383 },
3304 - "pluginVersion": "9.0.0",
3384 + "pluginVersion": "10.4.0",
3385 "targets": [
3386 {
3387 "bucketAggs": [
@@ -3364,7 +3444,9 @@
3444 },
3445 "custom": {
3446 "align": "auto",
3367 - "displayMode": "auto",
3447 + "cellOptions": {
3448 + "type": "auto"
3449 + },
3450 "inspect": false
3451 },
3452 "mappings": [],
@@ -3372,7 +3454,8 @@
3454 "mode": "absolute",
3455 "steps": [
3456 {
3375 - "color": "green"
3457 + "color": "green",
3458 + "value": null
3459 },
3460 {
3461 "color": "red",
@@ -3494,7 +3577,9 @@
3577 },
3578 "id": 93,
3579 "options": {
3580 + "cellHeight": "sm",
3581 "footer": {
3582 + "countRows": false,
3583 "fields": "",
3584 "reducer": ["sum"],
3585 "show": false
@@ -3502,7 +3587,7 @@
3587 "showHeader": true,
3588 "sortBy": []
3589 },
3505 - "pluginVersion": "9.0.0",
3590 + "pluginVersion": "10.4.0",
3591 "targets": [
3592 {
3593 "alias": "",
@@ -3822,7 +3907,8 @@
3907 "mode": "absolute",
3908 "steps": [
3909 {
3825 - "color": "blue"
3910 + "color": "blue",
3911 + "value": null
3912 }
3913 ]
3914 },
@@ -3837,7 +3923,6 @@
3923 "y": 4
3924 },
3925 "id": 99,
3840 - "links": [],
3926 "options": {
3927 "colorMode": "value",
3928 "graphMode": "area",
@@ -3848,10 +3933,12 @@
3933 "fields": "",
3934 "values": false
3935 },
3936 + "showPercentChange": false,
3937 "text": {},
3852 - "textMode": "auto"
3938 + "textMode": "auto",
3939 + "wideLayout": true
3940 },
3854 - "pluginVersion": "9.0.0",
3941 + "pluginVersion": "10.4.0",
3942 "targets": [
3943 {
3944 "bucketAggs": [
@@ -4038,14 +4125,14 @@
4125 "y": 4
4126 },
4127 "id": 102,
4041 - "links": [],
4128 "maxDataPoints": 3,
4129 "options": {
4130 "displayLabels": [],
4131 "legend": {
4132 "calcs": [],
4047 - "displayMode": "hidden",
4133 + "displayMode": "list",
4134 "placement": "bottom",
4135 + "showLegend": false,
4136 "values": ["value"]
4137 },
4138 "pieType": "donut",
@@ -4117,7 +4204,9 @@
4204 "defaults": {
4205 "custom": {
4206 "align": "auto",
4120 - "displayMode": "auto",
4207 + "cellOptions": {
4208 + "type": "auto"
4209 + },
4210 "filterable": false,
4211 "inspect": false
4212 },
@@ -4126,7 +4215,8 @@
4215 "mode": "absolute",
4216 "steps": [
4217 {
4129 - "color": "orange"
4218 + "color": "orange",
4219 + "value": null
4220 },
4221 {
4222 "color": "red",
@@ -4157,10 +4247,11 @@
4247 "y": 4
4248 },
4249 "id": 101,
4160 - "links": [],
4250 "maxDataPoints": 3,
4251 "options": {
4252 + "cellHeight": "sm",
4253 "footer": {
4254 + "countRows": false,
4255 "fields": "",
4256 "reducer": ["sum"],
4257 "show": false
@@ -4168,7 +4259,7 @@
4259 "showHeader": true,
4260 "sortBy": []
4261 },
4171 - "pluginVersion": "9.0.0",
4262 + "pluginVersion": "10.4.0",
4263 "targets": [
4264 {
4265 "bucketAggs": [
@@ -4215,7 +4306,9 @@
4306 "defaults": {
4307 "custom": {
4308 "align": "auto",
4218 - "displayMode": "auto",
4309 + "cellOptions": {
4310 + "type": "auto"
4311 + },
4312 "filterable": false,
4313 "inspect": false
4314 },
@@ -4224,7 +4317,8 @@
4317 "mode": "absolute",
4318 "steps": [
4319 {
4227 - "color": "orange"
4320 + "color": "orange",
4321 + "value": null
4322 },
4323 {
4324 "color": "red",
@@ -4255,10 +4349,11 @@
4349 "y": 4
4350 },
4351 "id": 105,
4258 - "links": [],
4352 "maxDataPoints": 3,
4353 "options": {
4354 + "cellHeight": "sm",
4355 "footer": {
4356 + "countRows": false,
4357 "fields": "",
4358 "reducer": ["sum"],
4359 "show": false
@@ -4266,7 +4361,7 @@
4361 "showHeader": true,
4362 "sortBy": []
4363 },
4269 - "pluginVersion": "9.0.0",
4364 + "pluginVersion": "10.4.0",
4365 "targets": [
4366 {
4367 "bucketAggs": [
@@ -4313,7 +4408,9 @@
4408 "defaults": {
4409 "custom": {
4410 "align": "auto",
4316 - "displayMode": "auto",
4411 + "cellOptions": {
4412 + "type": "auto"
4413 + },
4414 "filterable": false,
4415 "inspect": false
4416 },
@@ -4322,7 +4419,8 @@
4419 "mode": "absolute",
4420 "steps": [
4421 {
4325 - "color": "orange"
4422 + "color": "orange",
4423 + "value": null
4424 },
4425 {
4426 "color": "red",
@@ -4353,10 +4451,11 @@
4451 "y": 11
4452 },
4453 "id": 100,
4356 - "links": [],
4454 "maxDataPoints": 3,
4455 "options": {
4456 + "cellHeight": "sm",
4457 "footer": {
4458 + "countRows": false,
4459 "fields": "",
4460 "reducer": ["sum"],
4461 "show": false
@@ -4364,7 +4463,7 @@
4463 "showHeader": true,
4464 "sortBy": []
4465 },
4367 - "pluginVersion": "9.0.0",
4466 + "pluginVersion": "10.4.0",
4467 "targets": [
4468 {
4469 "bucketAggs": [
@@ -4403,52 +4502,86 @@
4502 "type": "table"
4503 },
4504 {
4406 - "aliasColors": {},
4407 - "bars": true,
4408 - "dashLength": 10,
4409 - "dashes": false,
4505 "datasource": {
4506 "type": "elasticsearch",
4507 "uid": "replace_datasource_uid"
4508 },
4414 - "fill": 1,
4415 - "fillGradient": 0,
4509 + "fieldConfig": {
4510 + "defaults": {
4511 + "color": {
4512 + "mode": "palette-classic"
4513 + },
4514 + "custom": {
4515 + "axisBorderShow": false,
4516 + "axisCenteredZero": false,
4517 + "axisColorMode": "text",
4518 + "axisLabel": "",
4519 + "axisPlacement": "auto",
4520 + "barAlignment": 0,
4521 + "drawStyle": "bars",
4522 + "fillOpacity": 100,
4523 + "gradientMode": "none",
4524 + "hideFrom": {
4525 + "legend": false,
4526 + "tooltip": false,
4527 + "viz": false
4528 + },
4529 + "insertNulls": false,
4530 + "lineInterpolation": "linear",
4531 + "lineWidth": 1,
4532 + "pointSize": 5,
4533 + "scaleDistribution": {
4534 + "type": "linear"
4535 + },
4536 + "showPoints": "never",
4537 + "spanNulls": false,
4538 + "stacking": {
4539 + "group": "A",
4540 + "mode": "normal"
4541 + },
4542 + "thresholdsStyle": {
4543 + "mode": "off"
4544 + }
4545 + },
4546 + "mappings": [],
4547 + "thresholds": {
4548 + "mode": "absolute",
4549 + "steps": [
4550 + {
4551 + "color": "green",
4552 + "value": null
4553 + },
4554 + {
4555 + "color": "red",
4556 + "value": 80
4557 + }
4558 + ]
4559 + },
4560 + "unit": "short"
4561 + },
4562 + "overrides": []
4563 + },
4564 "gridPos": {
4565 "h": 7,
4566 "w": 15,
4567 "x": 9,
4568 "y": 11
4569 },
4422 - "hiddenSeries": false,
4570 "id": 103,
4424 - "legend": {
4425 - "alignAsTable": true,
4426 - "avg": false,
4427 - "current": false,
4428 - "max": false,
4429 - "min": false,
4430 - "rightSide": true,
4431 - "show": true,
4432 - "total": false,
4433 - "values": false
4434 - },
4435 - "lines": false,
4436 - "linewidth": 1,
4437 - "links": [],
4571 "maxDataPoints": 3,
4439 - "nullPointMode": "null",
4572 "options": {
4441 - "alertThreshold": true
4442 - },
4443 - "percentage": false,
4444 - "pluginVersion": "9.0.0",
4445 - "pointradius": 2,
4446 - "points": false,
4447 - "renderer": "flot",
4448 - "seriesOverrides": [],
4449 - "spaceLength": 10,
4450 - "stack": true,
4451 - "steppedLine": false,
4573 + "legend": {
4574 + "calcs": [],
4575 + "displayMode": "table",
4576 + "placement": "right",
4577 + "showLegend": true
4578 + },
4579 + "tooltip": {
4580 + "mode": "multi",
4581 + "sort": "none"
4582 + }
4583 + },
4584 + "pluginVersion": "10.4.0",
4585 "targets": [
4586 {
4587 "alias": "",
@@ -4492,35 +4625,8 @@
4625 "timeField": "timestamp"
4626 }
4627 ],
4495 - "thresholds": [],
4496 - "timeRegions": [],
4628 "title": "WINDOWS LOGON SESSIONS - (HISTOGRAM)",
4498 - "tooltip": {
4499 - "shared": true,
4500 - "sort": 0,
4501 - "value_type": "individual"
4502 - },
4503 - "type": "graph",
4504 - "xaxis": {
4505 - "mode": "time",
4506 - "show": true,
4507 - "values": []
4508 - },
4509 - "yaxes": [
4510 - {
4511 - "format": "short",
4512 - "logBase": 1,
4513 - "show": true
4514 - },
4515 - {
4516 - "format": "short",
4517 - "logBase": 1,
4518 - "show": true
4519 - }
4520 - ],
4521 - "yaxis": {
4522 - "align": false
4523 - }
4629 + "type": "timeseries"
4630 },
4631 {
4632 "datasource": {
@@ -4534,7 +4640,9 @@
4640 },
4641 "custom": {
4642 "align": "auto",
4537 - "displayMode": "auto",
4643 + "cellOptions": {
4644 + "type": "auto"
4645 + },
4646 "inspect": false
4647 },
4648 "mappings": [],
@@ -4542,7 +4650,8 @@
4650 "mode": "absolute",
4651 "steps": [
4652 {
4545 - "color": "green"
4653 + "color": "green",
4654 + "value": null
4655 },
4656 {
4657 "color": "red",
@@ -4664,7 +4773,9 @@
4773 },
4774 "id": 104,
4775 "options": {
4776 + "cellHeight": "sm",
4777 "footer": {
4778 + "countRows": false,
4779 "fields": "",
4780 "reducer": ["sum"],
4781 "show": false
@@ -4672,7 +4783,7 @@
4783 "showHeader": true,
4784 "sortBy": []
4785 },
4675 - "pluginVersion": "9.0.0",
4786 + "pluginVersion": "10.4.0",
4787 "targets": [
4788 {
4789 "alias": "",
@@ -4963,7 +5074,8 @@
5074 "mode": "absolute",
5075 "steps": [
5076 {
4966 - "color": "blue"
5077 + "color": "blue",
5078 + "value": null
5079 }
5080 ]
5081 },
@@ -4978,7 +5090,6 @@
5090 "y": 5
5091 },
5092 "id": 108,
4981 - "links": [],
5093 "options": {
5094 "colorMode": "value",
5095 "graphMode": "area",
@@ -4989,10 +5100,12 @@
5100 "fields": "",
5101 "values": false
5102 },
5103 + "showPercentChange": false,
5104 "text": {},
4993 - "textMode": "auto"
5105 + "textMode": "auto",
5106 + "wideLayout": true
5107 },
4995 - "pluginVersion": "9.0.0",
5108 + "pluginVersion": "10.4.0",
5109 "targets": [
5110 {
5111 "bucketAggs": [
@@ -5194,7 +5307,6 @@
5307 "y": 5
5308 },
5309 "id": 109,
5197 - "links": [],
5310 "maxDataPoints": 3,
5311 "options": {
5312 "displayLabels": [],
@@ -5202,6 +5314,7 @@
5314 "calcs": [],
5315 "displayMode": "table",
5316 "placement": "right",
5317 + "showLegend": true,
5318 "values": ["value"]
5319 },
5320 "pieType": "donut",
@@ -5445,7 +5558,6 @@
5558 "y": 5
5559 },
5560 "id": 111,
5448 - "links": [],
5561 "maxDataPoints": 3,
5562 "options": {
5563 "displayLabels": [],
@@ -5453,6 +5565,7 @@
5565 "calcs": [],
5566 "displayMode": "table",
5567 "placement": "right",
5568 + "showLegend": true,
5569 "values": ["value"]
5570 },
5571 "pieType": "donut",
@@ -5524,7 +5637,9 @@
5637 "defaults": {
5638 "custom": {
5639 "align": "auto",
5527 - "displayMode": "auto",
5640 + "cellOptions": {
5641 + "type": "auto"
5642 + },
5643 "filterable": false,
5644 "inspect": false
5645 },
@@ -5533,7 +5648,8 @@
5648 "mode": "absolute",
5649 "steps": [
5650 {
5536 - "color": "orange"
5651 + "color": "orange",
5652 + "value": null
5653 },
5654 {
5655 "color": "red",
@@ -5576,10 +5692,11 @@
5692 "y": 5
5693 },
5694 "id": 110,
5579 - "links": [],
5695 "maxDataPoints": 3,
5696 "options": {
5697 + "cellHeight": "sm",
5698 "footer": {
5699 + "countRows": false,
5700 "fields": "",
5701 "reducer": ["sum"],
5702 "show": false
@@ -5587,7 +5704,7 @@
5704 "showHeader": true,
5705 "sortBy": []
5706 },
5590 - "pluginVersion": "9.0.0",
5707 + "pluginVersion": "10.4.0",
5708 "targets": [
5709 {
5710 "bucketAggs": [
@@ -5637,7 +5754,9 @@
5754 },
5755 "custom": {
5756 "align": "auto",
5640 - "displayMode": "auto",
5757 + "cellOptions": {
5758 + "type": "auto"
5759 + },
5760 "inspect": false
5761 },
5762 "mappings": [],
@@ -5645,7 +5764,8 @@
5764 "mode": "absolute",
5765 "steps": [
5766 {
5648 - "color": "green"
5767 + "color": "green",
5768 + "value": null
5769 },
5770 {
5771 "color": "red",
@@ -5779,7 +5899,9 @@
5899 },
5900 "id": 112,
5901 "options": {
5902 + "cellHeight": "sm",
5903 "footer": {
5904 + "countRows": false,
5905 "fields": "",
5906 "reducer": ["sum"],
5907 "show": false
@@ -5787,7 +5909,7 @@
5909 "showHeader": true,
5910 "sortBy": []
5911 },
5790 - "pluginVersion": "9.0.0",
5912 + "pluginVersion": "10.4.0",
5913 "targets": [
5914 {
5915 "alias": "",
@@ -6094,7 +6216,8 @@
6216 "mode": "absolute",
6217 "steps": [
6218 {
6097 - "color": "orange"
6219 + "color": "orange",
6220 + "value": null
6221 }
6222 ]
6223 },
@@ -6109,7 +6232,6 @@
6232 "y": 6
6233 },
6234 "id": 115,
6112 - "links": [],
6235 "options": {
6236 "colorMode": "value",
6237 "graphMode": "area",
@@ -6120,10 +6242,12 @@
6242 "fields": "",
6243 "values": false
6244 },
6245 + "showPercentChange": false,
6246 "text": {},
6124 - "textMode": "auto"
6247 + "textMode": "auto",
6248 + "wideLayout": true
6249 },
6126 - "pluginVersion": "9.0.0",
6250 + "pluginVersion": "10.4.0",
6251 "targets": [
6252 {
6253 "bucketAggs": [
@@ -6216,7 +6340,6 @@
6340 "y": 6
6341 },
6342 "id": 116,
6219 - "links": [],
6343 "maxDataPoints": 3,
6344 "options": {
6345 "displayLabels": [],
@@ -6224,6 +6347,7 @@
6347 "calcs": [],
6348 "displayMode": "table",
6349 "placement": "right",
6350 + "showLegend": true,
6351 "values": ["value"]
6352 },
6353 "pieType": "donut",
@@ -6358,7 +6482,6 @@
6482 "y": 6
6483 },
6484 "id": 118,
6361 - "links": [],
6485 "maxDataPoints": 3,
6486 "options": {
6487 "displayLabels": [],
@@ -6366,6 +6489,7 @@
6489 "calcs": [],
6490 "displayMode": "table",
6491 "placement": "right",
6492 + "showLegend": true,
6493 "values": ["value"]
6494 },
6495 "pieType": "donut",
@@ -6433,7 +6557,9 @@
6557 "defaults": {
6558 "custom": {
6559 "align": "auto",
6436 - "displayMode": "auto",
6560 + "cellOptions": {
6561 + "type": "auto"
6562 + },
6563 "filterable": false,
6564 "inspect": false
6565 },
@@ -6442,7 +6568,8 @@
6568 "mode": "absolute",
6569 "steps": [
6570 {
6445 - "color": "orange"
6571 + "color": "orange",
6572 + "value": null
6573 },
6574 {
6575 "color": "red",
@@ -6497,10 +6624,11 @@
6624 "y": 6
6625 },
6626 "id": 117,
6500 - "links": [],
6627 "maxDataPoints": 3,
6628 "options": {
6629 + "cellHeight": "sm",
6630 "footer": {
6631 + "countRows": false,
6632 "fields": "",
6633 "reducer": ["sum"],
6634 "show": false
@@ -6508,7 +6636,7 @@
6636 "showHeader": true,
6637 "sortBy": []
6638 },
6511 - "pluginVersion": "9.0.0",
6639 + "pluginVersion": "10.4.0",
6640 "targets": [
6641 {
6642 "bucketAggs": [
@@ -6554,7 +6682,9 @@
6682 },
6683 "custom": {
6684 "align": "auto",
6557 - "displayMode": "auto",
6685 + "cellOptions": {
6686 + "type": "auto"
6687 + },
6688 "inspect": false
6689 },
6690 "mappings": [],
@@ -6562,7 +6692,8 @@
6692 "mode": "absolute",
6693 "steps": [
6694 {
6565 - "color": "green"
6695 + "color": "green",
6696 + "value": null
6697 },
6698 {
6699 "color": "red",
@@ -6726,7 +6857,9 @@
6857 },
6858 "id": 119,
6859 "options": {
6860 + "cellHeight": "sm",
6861 "footer": {
6862 + "countRows": false,
6863 "fields": "",
6864 "reducer": ["sum"],
6865 "show": false
@@ -6734,7 +6867,7 @@
6867 "showHeader": true,
6868 "sortBy": []
6869 },
6737 - "pluginVersion": "9.0.0",
6870 + "pluginVersion": "10.4.0",
6871 "targets": [
6872 {
6873 "alias": "",
@@ -7089,8 +7222,7 @@
7222 }
7223 ],
7224 "refresh": "",
7092 - "schemaVersion": 36,
7093 - "style": "dark",
7225 + "schemaVersion": 39,
7226 "tags": ["EDR"],
7227 "templating": {
7228 "list": [
@@ -7145,7 +7277,5 @@
7277 },
7278 "timezone": "",
7279 "title": "EDR - WINDOWS EVENT LOGS",
7148 - "uid": null,
7149 - "version": 3,
7280 "weekStart": ""
7281 }
backend/app/connectors/grafana/reporting/report-template.html new
+109
@@ -0,0 +1,109 @@
1 +<!doctype html>
2 +<html lang="en">
3 + <head>
4 + <meta charset="UTF-8" />
5 + <title>Grafana Dashboard Report</title>
6 + <style>
7 + :root {
8 + --border-radius: 6px;
9 + --gap: 20px;
10 + }
11 + html,
12 + body {
13 + padding: 0;
14 + margin: 0;
15 + font-family: Arial, sans-serif;
16 + }
17 + * {
18 + padding: 0;
19 + margin: 0;
20 + box-sizing: border-box;
21 + }
22 + .screenshots-container {
23 + display: flex;
24 + flex-direction: column;
25 + width: 100%;
26 + padding: var(--gap);
27 + }
28 + .screenshot-row {
29 + display: flex;
30 + gap: var(--gap);
31 + width: 100%;
32 + page-break-inside: avoid;
33 + padding: calc(var(--gap) / 2) 0;
34 + }
35 + .screenshot {
36 + flex: 1 1 0px;
37 + border-radius: var(--border-radius);
38 + overflow: hidden;
39 + }
40 + .screenshot img {
41 + width: 100%;
42 + height: 100%;
43 + display: block;
44 + }
45 +
46 + .cover-box {
47 + page-break-after: always;
48 + width: 100%;
49 + height: 1030px;
50 + overflow: hidden;
51 + position: relative;
52 + }
53 +
54 + .cover-box .cover-bg {
55 + width: 100%;
56 + height: 100%;
57 + display: block;
58 + }
59 + .cover-box .cover-meta {
60 + position: absolute;
61 + top: 400px;
62 + left: 100px;
63 + }
64 + .cover-box .cover-meta .cover-logo {
65 + height: 80px;
66 + margin-bottom: 30px;
67 + }
68 + .cover-box .cover-meta .cover-company {
69 + font-size: 40px;
70 + font-weight: bold;
71 + margin-bottom: 30px;
72 + }
73 + .cover-box .cover-meta .cover-subtitle {
74 + font-size: 25px;
75 + margin-bottom: 5px;
76 + }
77 + .cover-box .cover-meta .cover-timerange {
78 + font-size: 20px;
79 + opacity: 0.6;
80 + }
81 + </style>
82 + </head>
83 + <body>
84 + <div class="cover-box">
85 + <img
86 + class="cover-bg"
87 + src="data:image/png;base64,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"
88 + />
89 +
90 + <div class="cover-meta">
91 + <img class="cover-logo" src="{{ logo_base64 }}" />
92 + <div class="cover-company">{{ company_name }}</div>
93 + <div class="cover-subtitle">Report</div>
94 + <div class="cover-timerange">{{ timerange_text }}</div>
95 + </div>
96 + </div>
97 + <div class="screenshots-container">
98 + {% for group in panel_groups %}
99 + <div class="screenshot-row">
100 + {% for panel in group %}
101 + <div class="screenshot">
102 + <img src="data:image/png;base64,{{ panel.panel_base64 }}" />
103 + </div>
104 + {% endfor %}
105 + </div>
106 + {% endfor %}
107 + </div>
108 + </body>
109 +</html>
backend/app/connectors/grafana/routes/reporting.py
+29 -10
@@ -6,11 +6,14 @@ from typing import List
6 from fastapi import APIRouter
7 from fastapi import Depends
8 from fastapi import Security
9 +from fastapi.exceptions import HTTPException
10 from loguru import logger
11 from sqlalchemy.ext.asyncio import AsyncSession
12 from sqlalchemy.future import select
13
14 from app.auth.utils import AuthHandler
15 +from app.connectors.grafana.schema.reporting import GenerateReportRequest
16 +from app.connectors.grafana.schema.reporting import GenerateReportResponse
17 from app.connectors.grafana.schema.reporting import GrafanaDashboardDetailsResponse
18 from app.connectors.grafana.schema.reporting import GrafanaDashboardPanelsResponse
19 from app.connectors.grafana.schema.reporting import GrafanaDashboardResponse
@@ -20,12 +23,15 @@ from app.connectors.grafana.schema.reporting import GrafanaLinksList
23 from app.connectors.grafana.schema.reporting import GrafanaOrganizationsResponse
24 from app.connectors.grafana.schema.reporting import Panel
25 from app.connectors.grafana.schema.reporting import TimeRange
26 +from app.connectors.grafana.services.reporting import generate_report
27 from app.connectors.grafana.services.reporting import get_dashboard_details
28 from app.connectors.grafana.services.reporting import get_dashboards
29 from app.connectors.grafana.services.reporting import get_orgs
30 from app.connectors.models import Connectors
31 from app.db.db_session import get_db
32
33 +# from app.middleware.license import is_feature_enabled
34 +
35 # App specific imports
36
37
@@ -40,11 +46,11 @@ async def get_grafana_url(session: AsyncSession):
46
47 def calculate_unix_timestamps(time_range: TimeRange):
48 now = datetime.now()
43 - if time_range.unit == "minutes":
49 + if time_range.unit == "m":
50 start_time = now - timedelta(minutes=time_range.value)
45 - elif time_range.unit == "hours":
51 + elif time_range.unit == "h":
52 start_time = now - timedelta(hours=time_range.value)
47 - elif time_range.unit == "days":
53 + elif time_range.unit == "d":
54 start_time = now - timedelta(days=time_range.value)
55
56 timestamp_from = int(time.mktime(start_time.timetuple())) * 1000
@@ -55,13 +61,13 @@ def calculate_unix_timestamps(time_range: TimeRange):
61
62 def generate_panel_urls(grafana_url: str, request: GrafanaGenerateIframeLinksRequest, timestamp_from: int, timestamp_to: int):
63 panel_links: List[GrafanaLinksList] = []
58 - for panel_id in request.panel_ids:
59 - panel_url = (
60 - f"{grafana_url}/d-solo/{request.dashboard_uid}/{request.dashboard_title}"
61 - f"?orgId={request.org_id}&from={timestamp_from}&to={timestamp_to}"
62 - f"&panelId={panel_id}"
63 - )
64 - panel_links.append(GrafanaLinksList(panel_id=panel_id, panel_url=panel_url))
64 + # for panel_id in request.panel_ids:
65 + panel_url = (
66 + f"{grafana_url}/d-solo/{request.dashboard_uid}/{request.dashboard_title}"
67 + f"?orgId={request.org_id}&from={timestamp_from}&to={timestamp_to}"
68 + f"&panelId={request.panel_id}"
69 + )
70 + panel_links.append(GrafanaLinksList(panel_id=request.panel_id, panel_url=panel_url))
71 return panel_links
72
73
@@ -206,3 +212,16 @@ async def generate_grafana_iframe_links(
212 links=panel_urls,
213 success=True,
214 )
215 +
216 +
217 +@grafana_reporting_router.post(
218 + "/generate-report",
219 + response_model=GenerateReportResponse,
220 + description="Create a new report.",
221 +)
222 +async def create_report(request: GenerateReportRequest, session: AsyncSession = Depends(get_db)) -> GenerateReportResponse:
223 + logger.info("Generating report")
224 + # ! License Check
225 + # await is_feature_enabled("REPORTING", session)
226 + raise HTTPException(status_code=501, detail="Feature not enabled. Please check back later.")
227 + return await generate_report(request, session)
backend/app/connectors/grafana/schema/dashboards.py
+5 -4
@@ -23,7 +23,7 @@ class GrafanaDashboardResponse(BaseModel):
23
24 # ! DASHBOARD CLASSES NEED TO BE DEFINED HERE !
25 class WazuhDashboard(Enum):
26 - SUMMARY = ("Wazuh", "summary.json")
26 + WAZUH_SUMMARY = ("Wazuh", "summary.json")
27 EDR_WINDOWS_EVENT_LOGS = ("Wazuh", "edr_windows_event_logs.json")
28 EDR_WAZUH_INVENOTRY = ("Wazuh", "edr_wazuh_inventory.json")
29 EDR_USERS_AND_GROUPS = ("Wazuh", "edr_users_and_groups.json")
@@ -33,6 +33,7 @@ class WazuhDashboard(Enum):
33 EDR_PROCESS_INJECTION = ("Wazuh", "edr_process_injection.json")
34 EDR_OPEN_AUDIT = ("Wazuh", "edr_open_audit.json")
35 EDR_NETWORK_SCAN = ("Wazuh", "edr_network_scan.json")
36 + EDR_NETWORK_CONNECTIONS = ("Wazuh", "edr_network_connections.json")
37 EDR_MITRE = ("Wazuh", "edr_mitre.json")
38 EDR_FIM = ("Wazuh", "edr_fim.json")
39 EDR_DOCKER_MONITORING = ("Wazuh", "edr_docker_monitoring.json")
@@ -57,13 +58,13 @@ class Office365Dashboard(Enum):
58 ONEDRIVE = ("Office365", "onedrive.json")
59 POWERBI = ("Office365", "powerbi.json")
60 SHAREPOINT = ("Office365", "sharepoint.json")
60 - SUMMARY = ("Office365", "summary.json")
61 + OFFICE365_SUMMARY = ("Office365", "summary.json")
62 TEAMS = ("Office365", "teams.json")
63 THREAT_INTELLIGENCE = ("Office365", "threat_intelligence.json")
64
65
66 class MimecastDashboard(Enum):
66 - SUMMARY = ("Mimecast", "summary.json")
67 + MIMECAST_SUMMARY = ("Mimecast", "summary.json")
68
69
70 class SapSiemDashboard(Enum):
@@ -71,7 +72,7 @@ class SapSiemDashboard(Enum):
72
73
74 class HuntressDashboard(Enum):
74 - SUMMARY = ("Huntress", "summary.json")
75 + HUNTRESS_SUMMARY = ("Huntress", "summary.json")
76
77
78 class DashboardProvisionRequest(BaseModel):
backend/app/connectors/grafana/schema/reporting.py
+116 -2
@@ -166,7 +166,7 @@ class TimeRange(BaseModel):
166
167 @validator("unit")
168 def validate_unit(cls, unit):
169 - valid_units = ["minutes", "hours", "days"]
169 + valid_units = ["m", "h", "d"]
170 if unit not in valid_units:
171 raise HTTPException(status_code=400, detail=f"Invalid time range unit: {unit}. Must be one of {valid_units}")
172 return unit
@@ -176,8 +176,9 @@ class GrafanaGenerateIframeLinksRequest(BaseModel):
176 org_id: int = Field(..., description="The ID of the organization.")
177 dashboard_title: str = Field(..., description="The title of the dashboard.")
178 dashboard_uid: str = Field(..., description="The UID of the dashboard.")
179 - panel_ids: List[int] = Field(..., description="The IDs of the panels.")
179 + panel_id: int = Field(..., description="The IDs of the panels.")
180 time_range: TimeRange = Field(..., description="Time range in minutes, hours, or days")
181 + theme: Optional[str] = Field(None, description="The theme of the panel.")
182
183
184 class GrafanaLinksList(BaseModel):
@@ -189,3 +190,116 @@ class GrafanaGenerateIframeLinksResponse(BaseModel):
190 message: str = Field(..., description="The message from the response.")
191 links: List[GrafanaLinksList] = Field(..., description="The links collected from Grafana.")
192 success: bool = Field(..., description="The success of the response.")
193 +
194 +
195 +class RequestPanel(BaseModel):
196 + panel_id: int = Field(..., description="Panel ID")
197 + org_id: int = Field(..., description="Organization ID")
198 + dashboard_title: str = Field(..., description="Dashboard title")
199 + dashboard_uid: str = Field(..., description="Dashboard UID")
200 + panel_url: Optional[str] = Field(None, description="Panel URL")
201 + panel_base64: Optional[str] = Field(None, description="Panel Base64")
202 + row_id: Optional[int] = Field(None, description="Row ID")
203 + panel_width: int = Field(..., description="Panel width")
204 + panel_height: int = Field(..., description="Panel height")
205 + theme: Optional[str] = Field(None, description="Panel theme")
206 +
207 +
208 +class RequestRow(BaseModel):
209 + id: int = Field(..., description="Row ID")
210 + panels: List[RequestPanel] = Field(..., description="Panels in the row")
211 +
212 +
213 +class GenerateReportRequest(BaseModel):
214 + company_name: str = Field(..., description="Company name", example="SOC Fortress")
215 + timerange_text: str = Field(..., description="Time range text", example="Last 7 days")
216 + logo_base64: str = Field(
217 + ...,
218 + description="Base64 encoded logo",
219 + example="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABjklEQVRIS+2Vv0oDQRDG",
220 + )
221 + timerange: str = Field(..., description="Time range for the report")
222 + # rows: List[RequestRow] = Field(..., description="Rows in the report")
223 + rows: List[RequestRow] = Field(
224 + ...,
225 + description="Rows in the report",
226 + example=[
227 + {
228 + "id": 1710437961108,
229 + "panels": [
230 + {
231 + "panel_id": 5,
232 + "org_id": 1,
233 + "dashboard_title": "HUNTRESS - _SUMMARY",
234 + "dashboard_uid": "ab9bab2c-5d86-43e7-bac2-c1d68fc91342",
235 + "panel_width": 500,
236 + "panel_height": 300,
237 + },
238 + {
239 + "panel_id": 3,
240 + "org_id": 1,
241 + "dashboard_title": "HUNTRESS - _SUMMARY",
242 + "dashboard_uid": "ab9bab2c-5d86-43e7-bac2-c1d68fc91342",
243 + "panel_width": 500,
244 + "panel_height": 300,
245 + },
246 + ],
247 + },
248 + {
249 + "id": 1710437961109,
250 + "panels": [
251 + {
252 + "panel_id": 5,
253 + "org_id": 1,
254 + "dashboard_title": "HUNTRESS - _SUMMARY",
255 + "dashboard_uid": "ab9bab2c-5d86-43e7-bac2-c1d68fc91342",
256 + "panel_width": 500,
257 + "panel_height": 300,
258 + },
259 + {
260 + "panel_id": 3,
261 + "org_id": 1,
262 + "dashboard_title": "HUNTRESS - _SUMMARY",
263 + "dashboard_uid": "ab9bab2c-5d86-43e7-bac2-c1d68fc91342",
264 + "panel_width": 500,
265 + "panel_height": 300,
266 + },
267 + ],
268 + },
269 + ],
270 + )
271 +
272 +
273 +class GenerateReportCreation(BaseModel):
274 + urls: list[str] = Field(
275 + [
276 + (
277 + "http://ashdevcopilot01.socfortress.local:3000/d-solo/ab9bab2c-5d86-43e7-bac2-c1d68fc91342/"
278 + "huntress-summary?orgId=1&from=1708725633941&to=1709330433941&panelId=5"
279 + ),
280 + (
281 + "http://ashdevcopilot01.socfortress.local:3000/d-solo/ab9bab2c-5d86-43e7-bac2-c1d68fc91342/"
282 + "huntress-summary?orgId=1&from=1708725654862&to=1709330454862&panelId=1"
283 + ),
284 + (
285 + "http://ashdevcopilot01.socfortress.local:3000/d-solo/a1891b09-fba9-498e-807e-1ad774c8557f/"
286 + "sap-users-auth?orgId=44&from=1709303384274&to=1709389784274&panelId=43"
287 + ),
288 + (
289 + "http://ashdevcopilot01.socfortress.local:3000/d-solo/ab9bab2c-5d86-43e7-bac2-c1d68fc91342/"
290 + "huntress-summary?orgId=1&from=1706799780600&to=1709391780600&panelId=10"
291 + ),
292 + ],
293 + description="List of URLs to generate screenshots for",
294 + )
295 +
296 +
297 +class Base64Image(BaseModel):
298 + base64_image: str
299 + url: str
300 +
301 +
302 +class GenerateReportResponse(BaseModel):
303 + base64_result: str
304 + message: str
305 + success: bool
backend/app/connectors/grafana/services/reporting.py
+253
@@ -1,12 +1,71 @@
1 +import base64
2 +import os
3 +import time
4 +import traceback
5 +from datetime import datetime
6 +from datetime import timedelta
7 +from pathlib import Path
8 from typing import List
9
10 from fastapi import HTTPException
11 +from jinja2 import Environment
12 +from jinja2 import FileSystemLoader
13 from loguru import logger
14 +from playwright.async_api import async_playwright
15 +from sqlalchemy.ext.asyncio import AsyncSession
16 +from sqlalchemy.future import select
17
18 +from app.connectors.grafana.schema.reporting import GenerateReportRequest
19 +from app.connectors.grafana.schema.reporting import GenerateReportResponse
20 from app.connectors.grafana.schema.reporting import GrafanaDashboardDetails
21 +from app.connectors.grafana.schema.reporting import GrafanaGenerateIframeLinksRequest
22 +from app.connectors.grafana.schema.reporting import GrafanaLinksList
23 from app.connectors.grafana.schema.reporting import GrafanaOrganizationDashboards
24 from app.connectors.grafana.schema.reporting import GrafanaOrganizations
25 +from app.connectors.grafana.schema.reporting import RequestPanel
26 +from app.connectors.grafana.schema.reporting import TimeRange
27 from app.connectors.grafana.utils.universal import create_grafana_client
28 +from app.connectors.models import Connectors
29 +from app.utils import get_connector_attribute
30 +
31 +
32 +async def get_grafana_url(session: AsyncSession):
33 + connector = await session.execute(select(Connectors).where(Connectors.connector_name == "Grafana"))
34 + connector = connector.scalars().first()
35 + return connector.connector_url
36 +
37 +
38 +def calculate_unix_timestamps(time_range: TimeRange):
39 + now = datetime.now()
40 + if time_range.unit == "m":
41 + start_time = now - timedelta(minutes=time_range.value)
42 + elif time_range.unit == "h":
43 + start_time = now - timedelta(hours=time_range.value)
44 + elif time_range.unit == "d":
45 + start_time = now - timedelta(days=time_range.value)
46 +
47 + timestamp_from = int(time.mktime(start_time.timetuple())) * 1000
48 + timestamp_to = int(time.mktime(now.timetuple())) * 1000
49 +
50 + return timestamp_from, timestamp_to
51 +
52 +
53 +def generate_panel_urls(
54 + grafana_url: str,
55 + request: GrafanaGenerateIframeLinksRequest,
56 + timestamp_from: int,
57 + timestamp_to: int,
58 + theme: str = "dark",
59 +):
60 + panel_links: List[GrafanaLinksList] = []
61 + # for panel_id in request.panel_ids:
62 + panel_url = (
63 + f"{grafana_url}/d-solo/{request.dashboard_uid}/{request.dashboard_title}"
64 + f"?orgId={request.org_id}&from={timestamp_from}&to={timestamp_to}"
65 + f"&panelId={request.panel_id}&theme={theme}"
66 + )
67 + panel_links.append(GrafanaLinksList(panel_id=request.panel_id, panel_url=panel_url))
68 + return panel_links
69
70
71 async def get_orgs() -> List[GrafanaOrganizations]:
@@ -71,3 +130,197 @@ async def get_dashboard_details(dashboard_uid: str) -> GrafanaDashboardDetails:
130 except Exception as e:
131 logger.error(f"Failed to collect dashboard details: {e}")
132 raise HTTPException(status_code=500, detail=f"Failed to collect dashboard details: {e}")
133 +
134 +
135 +async def login_to_page(page, session: AsyncSession):
136 + try:
137 + # Navigate to the login page
138 + await page.goto(f'{await get_connector_attribute(connector_id=12, column_name="connector_url", session=session)}/login')
139 + # Enter the username and password
140 + await page.fill(
141 + 'input[name="user"]',
142 + f'{await get_connector_attribute(connector_id=12, column_name="connector_username", session=session)}',
143 + )
144 + await page.fill(
145 + 'input[name="password"]',
146 + f'{await get_connector_attribute(connector_id=12, column_name="connector_password", session=session)}',
147 + )
148 + # Click the login button
149 + await page.click('button[data-testid="data-testid Login button"]')
150 + # Wait for navigation to complete
151 + await page.wait_for_load_state(state="networkidle")
152 + except Exception as e:
153 + logger.error(f"Failed to login to Grafana: {e}")
154 + traceback.print_exc()
155 + raise HTTPException(status_code=500, detail="Failed to login to Grafana")
156 +
157 +
158 +async def check_login_success(page):
159 + # Check if login was successful by checking for an element that is only visible when logged in
160 + body_class = await page.evaluate("document.body.className")
161 + logger.info(f"Body class: {body_class}")
162 + # if 'app-grafana no-overlay-scrollbar page-dashboard' in body_class:
163 + if "app-grafana" in body_class:
164 + logger.info("Login to Grafana successful")
165 + return True
166 + else:
167 + raise HTTPException(status_code=500, detail="Failed to login to Grafana")
168 +
169 +
170 +async def capture_screenshots(page, panels: List[RequestPanel]) -> List[RequestPanel]:
171 + logger.info("Capturing screenshots")
172 + last_url = ""
173 + for panel in panels:
174 + try:
175 + # Check if the panel's URL is different from the last to optimize navigation
176 + if panel.panel_url != last_url:
177 + await page.goto(panel.panel_url)
178 + await page.wait_for_load_state(state="networkidle")
179 + last_url = panel.panel_url
180 +
181 + # Assuming default dimensions are always used in this example
182 + logger.info(f"Panel width: {panel.panel_width}, height: {panel.panel_height} for panel {panel.panel_id}")
183 + width = panel.panel_width
184 + height = panel.panel_height
185 + await page.set_viewport_size({"width": width, "height": height})
186 +
187 + screenshot = await page.screenshot(type="png")
188 + base64_image = base64.b64encode(screenshot).decode("utf-8")
189 + panel.panel_base64 = base64_image
190 + except Exception as e:
191 + print(f"Failed to capture screenshot for panel {panel.panel_id}: {e}")
192 + # Optionally, set panel_base64 to None or a default value in case of failure
193 + panel.panel_base64 = None
194 + return panels
195 +
196 +
197 +async def generate_grafana_iframe_links(request: GrafanaGenerateIframeLinksRequest, session: AsyncSession):
198 + """
199 + Function to generate Grafana dashboard iframe links.
200 +
201 + Args:
202 + request (GrafanaGenerateIframeLinksRequest): The request body containing the dashboard UID and organization ID.
203 +
204 + Returns:
205 + GrafanaDashboardPanelsResponse: The response containing the result of the dashboard provisioning.
206 + """
207 + # get the Grafana URL from the database
208 + grafana_url = await get_grafana_url(session)
209 + logger.info(f"Grafana URL: {grafana_url}")
210 +
211 + # calculate the Unix timestamps based on the current time and the provided time range
212 + timestamp_from, timestamp_to = calculate_unix_timestamps(request.time_range)
213 +
214 + # build the URL string for each panel_id
215 + panel_urls = generate_panel_urls(grafana_url, request, timestamp_from, timestamp_to, theme=request.theme)
216 +
217 + # return only the panel url
218 + return panel_urls[0].panel_url
219 +
220 +
221 +def generate_html(panels: List[RequestPanel], request: GenerateReportRequest) -> str:
222 + # Load the template
223 + logger.info(f"Rendering HTML with panels: {len(panels)} panels")
224 + templates_dir = Path(__file__).parent / "../reporting"
225 + env = Environment(loader=FileSystemLoader(templates_dir))
226 + logger.info(f"Templates dir: {templates_dir}")
227 + template = env.get_template("report-template.html")
228 +
229 + panel_groups = {}
230 + for panel in panels:
231 + if panel.row_id not in panel_groups:
232 + panel_groups[panel.row_id] = [panel]
233 + else:
234 + panel_groups[panel.row_id].append(panel)
235 +
236 + # Convert the dict to a list of panel groups for the template
237 + panel_groups_list = list(panel_groups.values())
238 +
239 + # Render the template with the grouped panels
240 + html_content = template.render(
241 + panel_groups=panel_groups_list,
242 + company_name=request.company_name,
243 + timerange_text=request.timerange_text,
244 + logo_base64=request.logo_base64,
245 + )
246 + return html_content
247 +
248 +
249 +def parse_timerange(timerange: str) -> dict:
250 + """Parse the timerange string into a dictionary with value and unit keys."""
251 + timerange_value, timerange_unit = int(timerange[:-1]), timerange[-1]
252 + return {"value": timerange_value, "unit": timerange_unit}
253 +
254 +
255 +async def generate_panel_urls_object(panel: RequestPanel, timerange: dict, session: AsyncSession) -> str:
256 + """Generate the iframe links for a panel."""
257 + iframe_links_request = GrafanaGenerateIframeLinksRequest(
258 + time_range=timerange,
259 + dashboard_uid=panel.dashboard_uid,
260 + dashboard_title=panel.dashboard_title,
261 + org_id=panel.org_id,
262 + panel_id=panel.panel_id,
263 + theme=panel.theme,
264 + )
265 + return await generate_grafana_iframe_links(iframe_links_request, session)
266 +
267 +
268 +async def write_html_to_file(html_string: str, file_path: str):
269 + """Write the given HTML string to a file."""
270 + with open(file_path, "w") as f:
271 + f.write(html_string)
272 +
273 +
274 +async def generate_pdf_from_html(html_file_path: str, pdf_file_path: str):
275 + """Generate a PDF from the given HTML file using Playwright."""
276 + async with async_playwright() as p:
277 + browser = await p.chromium.launch()
278 + context = await browser.new_context()
279 + page = await context.new_page()
280 + await page.emulate_media(media="screen")
281 + await page.goto(f"file://{os.getcwd()}/{html_file_path}")
282 + await page.pdf(path=pdf_file_path)
283 + await browser.close()
284 +
285 +
286 +async def generate_report(request: GenerateReportRequest, session: AsyncSession):
287 + logger.info("Generating report")
288 + for row in request.rows:
289 + for panel in row.panels:
290 + panel.row_id = row.id
291 + # Parse the timerange string
292 + timerange = parse_timerange(request.timerange)
293 + # Iterate over each row in the request
294 + for row in request.rows:
295 + # Iterate over each panel in the row
296 + for panel in row.panels:
297 + # Generate the iframe links for each panel
298 + panel_urls = await generate_panel_urls_object(panel, timerange, session)
299 + logger.info(f"Panel URLs: {panel_urls}")
300 + # add the panel url to the request.panel_url
301 + panel.panel_url = panel_urls
302 +
303 + async with async_playwright() as p:
304 + browser = await p.chromium.launch(headless=True)
305 + context = await browser.new_context(ignore_https_errors=True)
306 + page = await context.new_page()
307 + await login_to_page(page, session)
308 + if not await check_login_success(page):
309 + await browser.close()
310 + return
311 + # Flatten the list of panels
312 + all_panels = [panel for row in request.rows for panel in row.panels]
313 + panels = await capture_screenshots(page, all_panels)
314 + await browser.close()
315 + html_string = generate_html(panels, request)
316 + await write_html_to_file(html_string, "report.html")
317 + await generate_pdf_from_html("report.html", "report.pdf")
318 +
319 + # ! convert pdf to base64 and return
320 + with open("report.pdf", "rb") as f:
321 + pdf_base64 = base64.b64encode(f.read()).decode("utf-8")
322 +
323 + # ! Delete the report.html and report.pdf files
324 + os.remove("report.html")
325 + os.remove("report.pdf")
326 + return GenerateReportResponse(base64_result=pdf_base64, message="Report generated successfully", success=True)
backend/app/connectors/graylog/schema/collector.py
+6 -6
@@ -61,7 +61,7 @@ class GraylogIndicesResponse(BaseModel):
61
62 class ConfiguredInputAttributes(BaseModel):
63 recv_buffer_size: int
64 - tcp_keepalive: bool
64 + tcp_keepalive: Optional[bool] = Field(None, description="TCP keepalive")
65 use_null_delimiter: Optional[bool]
66 number_worker_threads: int
67 tls_client_auth_cert_file: Optional[str]
@@ -72,10 +72,10 @@ class ConfiguredInputAttributes(BaseModel):
72 expand_structured_data: Optional[bool]
73 port: int
74 tls_key_file: Optional[str]
75 - tls_enable: bool
75 + tls_enable: Optional[bool] = Field(None, description="TLS is enabled")
76 tls_key_password: Optional[str]
77 max_message_size: Optional[int]
78 - tls_client_auth: str
78 + tls_client_auth: Optional[str] = Field(None, description="TLS client authentication")
79 override_source: Optional[str]
80 charset_name: Optional[str]
81 allow_override_date: Optional[bool]
@@ -97,7 +97,7 @@ class ConfiguredInput(BaseModel):
97
98 class MessageInputAttributes(BaseModel):
99 recv_buffer_size: int
100 - tcp_keepalive: bool
100 + tcp_keepalive: Optional[bool] = Field(None, description="TCP keepalive")
101 use_null_delimiter: Optional[bool]
102 number_worker_threads: int
103 tls_client_auth_cert_file: Optional[str]
@@ -105,10 +105,10 @@ class MessageInputAttributes(BaseModel):
105 tls_cert_file: Optional[str]
106 port: int
107 tls_key_file: Optional[str]
108 - tls_enable: bool
108 + tls_enable: Optional[bool] = Field(None, description="TLS is enabled")
109 tls_key_password: Optional[str]
110 max_message_size: Optional[int]
111 - tls_client_auth: str
111 + tls_client_auth: Optional[str] = Field(None, description="TLS client authentication")
112
113
114 class MessageInput(BaseModel):
backend/app/connectors/graylog/schema/events.py
+29 -11
@@ -4,6 +4,7 @@ from typing import Optional
4 from typing import Union
5
6 from pydantic import BaseModel
7 +from pydantic import Field
8
9
10 class Provider(BaseModel):
@@ -17,20 +18,37 @@ class FieldSpecItem(BaseModel):
18 providers: List[Provider]
19
20
21 +class ExpressionItem(BaseModel):
22 + expr: str
23 + ref: Optional[str] = None
24 + value: Optional[int] = None
25 + left: Optional["ExpressionItem"] = None
26 + right: Optional["ExpressionItem"] = None
27 +
28 +
29 +ExpressionItem.update_forward_refs()
30 +
31 +
32 class Conditions(BaseModel):
21 - expression: Optional[str]
33 + expression: Optional[Union[str, ExpressionItem]] = None
34
35
24 -class Config(BaseModel):
25 - conditions: Conditions
26 - execute_every_ms: int
27 - group_by: List[str]
28 - query: str
29 - query_parameters: List[str]
30 - search_within_ms: int
31 - series: List[str]
32 - streams: List[str]
36 +class SeriesItem(BaseModel):
37 type: str
38 + id: str
39 + field: Optional[str] = None
40 +
41 +
42 +class Config(BaseModel):
43 + conditions: Optional[Conditions] = Field(None, description="The conditions to be met for the config")
44 + execute_every_ms: Optional[int] = Field(None, description="The execution frequency in milliseconds")
45 + group_by: Optional[List[str]] = Field(None, description="The fields to group by")
46 + query: Optional[str] = Field(None, description="The query to be executed")
47 + query_parameters: Optional[List[str]] = Field(None, description="The parameters for the query")
48 + search_within_ms: Optional[int] = Field(None, description="The search window in milliseconds")
49 + series: Optional[Union[str, List[SeriesItem]]] = Field(None, description="The series to be included in the config")
50 + streams: Optional[List[str]] = Field(None, description="The streams to be included in the config")
51 + type: str = Field(..., description="The type of the config")
52
53
54 class NotificationSettings(BaseModel):
@@ -46,7 +64,7 @@ class Storage(BaseModel):
64 class EventDefinition(BaseModel):
65 _scope: str
66 alert: bool
49 - config: Config
67 + config: Optional[Config] = Field(None, description="The configuration for the event definition")
68 description: str
69 field_spec: Dict[str, FieldSpecItem]
70 id: str
backend/app/connectors/influxdb/utils/universal.py
+83 -1
@@ -1,10 +1,13 @@
1 from typing import Any
2 from typing import Dict
3 +from typing import List
4
5 from fastapi import HTTPException
6 from influxdb_client.client.influxdb_client_async import InfluxDBClientAsync
7 from loguru import logger
8
9 +from app.connectors.influxdb.schema.alerts import InfluxDBAlert
10 +from app.connectors.influxdb.schema.alerts import InfluxDBAlertsResponse
11 from app.connectors.utils import get_connector_info_from_db
12 from app.db.db_session import get_db_session
13
@@ -28,6 +31,9 @@ async def verify_influxdb_credentials(attributes: Dict[str, Any]) -> Dict[str, A
31 logger.info(f"Response from InfluxDB: {ping}")
32 if ping:
33 logger.info(f"Connection to {attributes['connector_url']} successful")
34 + # Now try to fetch alerts
35 + await get_alerts()
36 + # logger.info(f"Alerts from InfluxDB: {alerts}")
37 return {
38 "connectionSuccessful": True,
39 "message": "InfluxDB connection successful",
@@ -92,7 +98,7 @@ async def create_influxdb_client(connector_name: str) -> InfluxDBClientAsync:
98 except Exception as e:
99 raise HTTPException(
100 status_code=500,
95 - detail=f"Failed to create Elasticsearch client: {e}",
101 + detail=f"Failed to create InfluxDB client: {e}",
102 )
103
104
@@ -109,3 +115,79 @@ async def get_influxdb_organization() -> str:
115 detail="No InfluxDB connector found in the database",
116 )
117 return attributes["connector_extra_data"].split(",")[0]
118 +
119 +
120 +# ! RUN A TEST QUERY TO FETCH ALERTS AND VERIFY THE CONNECTION
121 +# Constants
122 +BUCKET_NAME = "_monitoring"
123 +
124 +
125 +def construct_query() -> str:
126 + """Constructs the InfluxDB query.
127 +
128 + Returns:
129 + str: The constructed InfluxDB query.
130 + """
131 + return """
132 + from(bucket: "{bucket_name}")
133 + |> range(start: -1h, stop: now())
134 + |> filter(fn: (r) => r._measurement == "statuses" and r._field == "_message")
135 + |> filter(fn: (r) => exists r._check_id and exists r._value and exists r._check_name and exists r._level)
136 + |> keep(columns: ["_time", "_value", "_check_id", "_check_name", "_level"])
137 + |> rename(columns: {{ "_time": "time", "_value": "message", "_check_id": "checkID", "_check_name": "checkName", "_level": "level" }})
138 + |> group()
139 + |> sort(columns: ["time"], desc: true)
140 + |> limit(n: 100, offset: 29)
141 + """.format(
142 + bucket_name=BUCKET_NAME,
143 + )
144 +
145 +
146 +async def process_alert_records(result) -> List[InfluxDBAlert]:
147 + """Processes alert records from InfluxDB query result.
148 +
149 + Args:
150 + result: The query result from InfluxDB.
151 +
152 + Returns:
153 + A list of InfluxDBAlert objects representing the processed alert records.
154 + """
155 + alerts = []
156 + for table in result:
157 + for record in table.records:
158 + alert = InfluxDBAlert(
159 + time=record.values.get("time").isoformat() if record.values.get("time") else None,
160 + message=record.values.get("message"),
161 + checkID=record.values.get("checkID"),
162 + checkName=record.values.get("checkName"),
163 + level=record.values.get("level"),
164 + )
165 + alerts.append(alert)
166 + return alerts
167 +
168 +
169 +async def get_alerts() -> InfluxDBAlertsResponse:
170 + """Fetches alerts from InfluxDB and returns them.
171 +
172 + Returns:
173 + InfluxDBAlertsResponse: The response object containing the fetched alerts.
174 +
175 + Raises:
176 + HTTPException: If there is an error fetching the alerts.
177 + """
178 + client = await create_influxdb_client("InfluxDB")
179 + try:
180 + query = construct_query()
181 + logger.info(f"Fetching alerts from InfluxDB: {query}")
182 + result = await client.query_api().query(org=await get_influxdb_organization(), query=query)
183 + logger.info(f"Alerts from InfluxDB: {result}")
184 + alerts = await process_alert_records(result)
185 + return InfluxDBAlertsResponse(alerts=alerts, success=True, message="Alerts fetched successfully")
186 + except Exception as e:
187 + raise HTTPException(
188 + status_code=500,
189 + detail=f"Failed to fetch alerts from InfluxDB: {e}",
190 + )
191 + finally:
192 + # Make sure to close the client session
193 + await client.close()
backend/app/connectors/services.py
+14
@@ -33,6 +33,7 @@ from app.threat_intel.services.socfortress import (
33 verifiy_socfortress_threat_intel_connector,
34 )
35 from app.utils import verify_alert_creation_provisioning_connection
36 +from app.utils import verify_haproxy_provisioning_connection
37 from app.utils import verify_wazuh_worker_provisioning_connection
38
39 UPLOAD_FOLDER = "file-store"
@@ -153,6 +154,17 @@ class WazuhWorkerProvisioningService(ConnectorServiceInterface):
154 )
155
156
157 +# HAProxy Provisioning Service
158 +class HAProxyProvisioningService(ConnectorServiceInterface):
159 + async def verify_authentication(
160 + self,
161 + connector: ConnectorResponse,
162 + ) -> Optional[ConnectorResponse]:
163 + return await verify_haproxy_provisioning_connection(
164 + connector.connector_name,
165 + )
166 +
167 +
168 # SOCFortress Threat Intel Service
169 class SocfortressThreatIntelService(ConnectorServiceInterface):
170 async def verify_authentication(
@@ -216,6 +228,7 @@ def get_connector_service(connector_name: str) -> Type[ConnectorServiceInterface
228 "InfluxDB": InfluxDBService,
229 "Grafana": GrafanaService,
230 "Wazuh Worker Provisioning": WazuhWorkerProvisioningService,
231 + "HAProxy Provisioning": HAProxyProvisioningService,
232 "SocfortressThreatIntel": SocfortressThreatIntelService,
233 "AskSocfortress": AskSocfortressService,
234 "Event Shipper": EventShipperService,
@@ -314,6 +327,7 @@ class ConnectorServices:
327 connector_response = await service_instance.verify_authentication(
328 connector_response,
329 )
330 +
331 # If the connector is verified, update the connector record in the database
332 if connector_response["connectionSuccessful"]:
333 connector.connector_verified = True
backend/app/connectors/wazuh_indexer/schema/indices.py
+10 -1
@@ -15,6 +15,10 @@ class IndexConfigModel(BaseModel):
15 default={
16 "wazuh-statistics": True,
17 "wazuh-monitoring": True,
18 + ".opendistro": True,
19 + ".opensearch": True,
20 + ".kibana": True,
21 + "praeco": True,
22 },
23 description="A dictionary containing index names to be skipped and their skip status.",
24 )
@@ -34,6 +38,8 @@ class IndexConfigModel(BaseModel):
38 def is_valid_index(self, index_name: str) -> bool:
39 """
40 Checks if the index name starts with "wazuh_" and is not in the SKIP_INDEX_NAMES list.
41 + UPDATE: Modifying this method to return not self.is_index_skipped(index_name) and not index_name.__contains__("deflector")
42 + So that users whom do not use `wazuh-` index naming convention can still receive alerts.
43
44 Args:
45 index_name (str): The name of the index to check.
@@ -41,4 +47,7 @@ class IndexConfigModel(BaseModel):
47 Returns:
48 bool: True if the index is valid, False otherwise.
49 """
44 - return index_name.startswith("wazuh") and not self.is_index_skipped(index_name)
50 + # return index_name.startswith("wazuh") and not self.is_index_skipped(index_name)
51 + # ! Modifying the return statement to return not self.is_index_skipped(index_name) and not index_name.__contains__("deflector")
52 + # ! Using this so that users whom do not use `wazuh-` index naming convention can still receive alerts
53 + return not self.is_index_skipped(index_name) and not index_name.__contains__("deflector")
backend/app/customer_provisioning/models/default_settings.py
+1
@@ -11,3 +11,4 @@ class CustomerProvisioningDefaultSettings(SQLModel, table=True):
11 cluster_key: str = Field(max_length=1000, nullable=False)
12 master_ip: str = Field(max_length=50, nullable=False)
13 grafana_url: str = Field(max_length=1024, nullable=False)
14 + wazuh_worker_hostname: str = Field(max_length=100, nullable=False)
backend/app/customer_provisioning/routes/provision.py
+3 -3
@@ -8,7 +8,6 @@ from sqlalchemy.ext.asyncio import AsyncSession
8 from sqlalchemy.future import select
9
10 from app.auth.utils import AuthHandler
11 -from app.connectors.grafana.schema.dashboards import Office365Dashboard
11 from app.connectors.grafana.schema.dashboards import WazuhDashboard
12 from app.customer_provisioning.schema.provision import CustomerProvisionResponse
13 from app.customer_provisioning.schema.provision import CustomersMetaResponse
@@ -39,8 +38,9 @@ def get_available_dashboards():
38 """
39 try:
40 wazuh_dashboards = [dashboard.name for dashboard in WazuhDashboard]
42 - office365_dashboards = [dashboard.name for dashboard in Office365Dashboard]
43 - return wazuh_dashboards + office365_dashboards
41 + # office365_dashboards = [dashboard.name for dashboard in Office365Dashboard]
42 + # return wazuh_dashboards + office365_dashboards
43 + return wazuh_dashboards
44 except Exception as e:
45 raise HTTPException(
46 status_code=500,
backend/app/customer_provisioning/schema/grafana.py
+9
@@ -29,6 +29,15 @@ class GrafanaJsonData(BaseModel):
29 maxConcurrentShardRequests: int = Field(5, alias="maxConcurrentShardRequests")
30 timeField: str = Field(..., alias="timeField")
31 tlsSkipVerify: bool = Field(True, alias="tlsSkipVerify")
32 + dataLinks: list = Field(
33 + [
34 + {
35 + "field": "^data_vulnerability_cve$",
36 + "url": "https://nvd.nist.gov/vuln/detail",
37 + },
38 + ],
39 + alias="dataLinks",
40 + )
41
42
43 class GrafanaDatasource(BaseModel):
backend/app/customer_provisioning/schema/provision.py
+28 -1
@@ -13,7 +13,7 @@ from app.db.universal_models import CustomersMeta
13
14 class CustomerSubsctipion(Enum):
15 WAZUH = "Wazuh"
16 - OFFICE365 = "Office365"
16 + # OFFICE365 = "Office365"
17
18
19 class ProvisionNewCustomer(BaseModel):
@@ -88,6 +88,10 @@ class ProvisionNewCustomer(BaseModel):
88 None,
89 description="ID of the Graylog stream",
90 )
91 + wazuh_worker_hostname: Optional[str] = Field(
92 + None,
93 + description="Hostname of the Wazuh worker",
94 + )
95
96 @validator("customer_index_name")
97 def validate_customer_index_name(cls, v):
@@ -162,3 +166,26 @@ class CustomersMetaResponse(BaseModel):
166 ...,
167 description="Customer meta data for the newly provisioned customer",
168 )
169 +
170 +
171 +class ProvisionHaProxyRequest(BaseModel):
172 + customer_name: str = Field(
173 + ...,
174 + example="SOCFortress",
175 + description="The name of the customer",
176 + )
177 + wazuh_registration_port: str = Field(
178 + ...,
179 + example="1515",
180 + description="The port for the Wazuh registration service",
181 + )
182 + wazuh_logs_port: str = Field(
183 + ...,
184 + example="1514",
185 + description="The port for the Wazuh logs service",
186 + )
187 + wazuh_worker_hostname: Optional[str] = Field(
188 + None,
189 + example="worker1",
190 + description="The hostname of the Wazuh worker",
191 + )
backend/app/customer_provisioning/schema/wazuh_worker.py
+7
@@ -1,3 +1,5 @@
1 +from typing import Optional
2 +
3 from pydantic import BaseModel
4 from pydantic import Field
5
@@ -43,6 +45,11 @@ class ProvisionWorkerRequest(BaseModel):
45 example="1.1.1.1",
46 description="The IP address of the Wazuh master",
47 )
48 + wazuh_worker_hostname: Optional[str] = Field(
49 + None,
50 + example="worker1",
51 + description="The hostname of the Wazuh worker",
52 + )
53
54
55 class ProvisionWorkerResponse(BaseModel):
backend/app/customer_provisioning/services/grafana.py
+13
@@ -83,6 +83,19 @@ async def create_grafana_datasource(
83 },
84 isDefault=False,
85 jsonData={
86 + "dataLinks": [
87 + {"field": "^data_vulnerability_cve$", "url": "https://nvd.nist.gov/vuln/detail/${__value.raw}"},
88 + {
89 + "field": "^_id$",
90 + "url": (
91 + "{}/explore?left=%7B%22datasource%22:%22WAZUH%22,%22queries%22:%5B%7B"
92 + "%22refId%22:%22A%22,%22query%22:%22_id:${{__value.raw}}%22,%22alias%22:%22%22,"
93 + "%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:"
94 + "%7B%22limit%22:%22500%22%7D%7D%5D,%22bucketAggs%22:%5B%5D,%22timeField%22:"
95 + "%22timestamp%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D"
96 + ).format(request.grafana_url),
97 + },
98 + ],
99 "database": f"{request.customer_index_name}*",
100 "flavor": "opensearch",
101 "includeFrozen": False,
backend/app/customer_provisioning/services/graylog.py
+6 -5
@@ -29,8 +29,8 @@ def build_index_set_config(request: ProvisionNewCustomer) -> TimeBasedIndexSet:
29 TimeBasedIndexSet: The configured time-based index set.
30 """
31 return TimeBasedIndexSet(
32 - title=f"Wazuh - {request.customer_name}",
33 - description=f"Wazuh - {request.customer_name}",
32 + title=f"{request.customer_name} - Wazuh EDR EVENTS",
33 + description=f"{request.customer_name} - Wazuh EDR EVENTS",
34 index_prefix=request.customer_index_name,
35 rotation_strategy_class="org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategy",
36 rotation_strategy={
@@ -126,8 +126,8 @@ def build_event_stream_config(
126 WazuhEventStream: The configured Wazuh event stream.
127 """
128 return WazuhEventStream(
129 - title=f"WAZUH EVENTS CUSTOMERS - {request.customer_name}",
130 - description=f"WAZUH EVENTS CUSTOMERS - {request.customer_name}",
129 + title=f"{request.customer_name} - Wazuh EDR EVENTS",
130 + description=f"{request.customer_name} - Wazuh EDR EVENTS",
131 index_set_id=index_set_id,
132 rules=[
133 {
@@ -136,8 +136,9 @@ def build_event_stream_config(
136 "inverted": False,
137 "value": request.customer_code,
138 },
139 + {"field": "cluster_node", "type": 1, "inverted": False, "value": f"wazuh.worker.{request.customer_name}"},
140 ],
140 - matching_type="AND",
141 + matching_type="OR",
142 remove_matches_from_default_stream=True,
143 content_pack=None,
144 )
backend/app/customer_provisioning/services/provision.py
+58
@@ -9,6 +9,7 @@ from app.connectors.graylog.services.management import start_stream
9 from app.customer_provisioning.schema.graylog import StreamConnectionToPipelineRequest
10 from app.customer_provisioning.schema.provision import CustomerProvisionMeta
11 from app.customer_provisioning.schema.provision import CustomerProvisionResponse
12 +from app.customer_provisioning.schema.provision import ProvisionHaProxyRequest
13 from app.customer_provisioning.schema.provision import ProvisionNewCustomer
14 from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerRequest
15 from app.customer_provisioning.schema.wazuh_worker import ProvisionWorkerResponse
@@ -128,6 +129,24 @@ async def provision_wazuh_customer(
129 wazuh_worker_provisioned=False,
130 )
131
132 + provsion_haproxy = await provision_haproxy(
133 + ProvisionHaProxyRequest(
134 + customer_name=request.customer_name,
135 + wazuh_registration_port=request.wazuh_registration_port,
136 + wazuh_logs_port=request.wazuh_logs_port,
137 + wazuh_worker_hostname=request.wazuh_worker_hostname,
138 + ),
139 + session,
140 + )
141 +
142 + if provsion_haproxy.success is False:
143 + return CustomerProvisionResponse(
144 + message=f"Customer {request.customer_name} provisioned successfully, but the HAProxy failed to provision",
145 + success=True,
146 + customer_meta=customer_meta.dict(),
147 + wazuh_worker_provisioned=True,
148 + )
149 +
150 return CustomerProvisionResponse(
151 message=f"Customer {request.customer_name} provisioned successfully",
152 success=True,
@@ -246,3 +265,42 @@ async def provision_wazuh_worker(
265 success=True,
266 message="Wazuh worker provisioned successfully",
267 )
268 +
269 +
270 +######### ! Provision HAProxy ! ############
271 +async def provision_haproxy(
272 + request: ProvisionWorkerRequest,
273 + session: AsyncSession,
274 +) -> ProvisionWorkerResponse:
275 + """
276 + Provisions a HAProxy.
277 +
278 + Args:
279 + request (ProvisionWorkerRequest): The request object containing the necessary information for provisioning.
280 + session (AsyncSession): The async session object for making HTTP requests.
281 +
282 + Returns:
283 + ProvisionWorkerResponse: The response object indicating the success or failure of the provisioning operation.
284 + """
285 + logger.info(f"Provisioning HAProxy {request}")
286 + api_endpoint = await get_connector_attribute(
287 + connector_id=16,
288 + column_name="connector_url",
289 + session=session,
290 + )
291 + # Send the POST request to the Wazuh worker
292 + response = requests.post(
293 + url=f"{api_endpoint}/provision_worker/haproxy",
294 + json=request.dict(),
295 + )
296 + # Check the response status code
297 + if response.status_code != 200:
298 + return ProvisionWorkerResponse(
299 + success=False,
300 + message=f"Failed to provision HAProxy: {response.text}",
301 + )
302 + # Return the response
303 + return ProvisionWorkerResponse(
304 + success=True,
305 + message="HAProxy provisioned successfully",
306 + )
backend/app/customer_provisioning/services/wazuh_manager.py
+4 -2
@@ -101,7 +101,7 @@ def get_template_path(template_info: WazuhAgentsTemplatePaths) -> Path:
101
102
103 # Function to update Wazuh group configuration
104 -async def configure_wazuh_group(group_code, template_path):
104 +async def configure_wazuh_group(group_code, template_path, request: ProvisionNewCustomer):
105 """
106 Configures a Wazuh group with the provided group code and template file.
107
@@ -120,6 +120,8 @@ async def configure_wazuh_group(group_code, template_path):
120
121 # Replace placeholder with the customer code
122 group_config = config_template.replace("REPLACE", group_code.split("_")[-1])
123 + # Replace placeholder with the cluster name
124 + group_config = group_config.replace("CLUSTER_NAME", request.wazuh_cluster_name)
125
126 # Make the API request to update the group configuration
127 return await send_wazuh_put_request(
@@ -158,7 +160,7 @@ async def apply_group_configurations(request: ProvisionNewCustomer):
160 group_code = f"{group}_{request.customer_code}"
161 template_path = get_template_path(template)
162 try:
161 - await configure_wazuh_group(group_code, template_path)
163 + await configure_wazuh_group(group_code, template_path, request)
164 except Exception as e:
165 logger.error(f"Error configuring group {group_code}: {e}")
166
backend/app/customer_provisioning/templates/windows_agent.conf
+23 -1
@@ -140,6 +140,28 @@
140 <ca_verification>yes</ca_verification>
141 </active-response>
142 <!-- Log analysis -->
143 + <localfile>
144 + <location>Application</location>
145 + <log_format>eventchannel</log_format>
146 + </localfile>
147 +
148 + <localfile>
149 + <location>Security</location>
150 + <log_format>eventchannel</log_format>
151 + <query>Event/System[EventID != 5145 and EventID != 5156 and EventID != 5447 and
152 + EventID != 4656 and EventID != 4658 and EventID != 4663 and EventID != 4660 and
153 + EventID != 4670 and EventID != 4690 and EventID != 4703 and EventID != 4907 and
154 + EventID != 5152 and EventID != 5157]</query>
155 + </localfile>
156 +
157 + <localfile>
158 + <location>System</location>
159 + <log_format>eventchannel</log_format>
160 + </localfile>
161 + <localfile>
162 + <location>active-response\active-responses.log</location>
163 + <log_format>syslog</log_format>
164 + </localfile>
165 <localfile>
166 <location>Microsoft-Windows-Sysmon/Operational</location>
167 <log_format>eventchannel</log_format>
@@ -211,7 +233,7 @@
233 <wodle name="command">
234 <disabled>no</disabled>
235 <tag>dll_for_chainsaw</tag>
214 - <command>Powershell.exe -executionpolicy bypass -File "C:\Program Files (x86)\socfortress\Files\install_dll.ps1"</command>
236 + <command>Powershell.exe -executionpolicy bypass -File "C:\Program Files (x86)\CLUSTER_NAME\Files\install_dll.ps1"</command>
237 <interval>24h</interval>
238 <ignore_output>yes</ignore_output>
239 <run_on_start>yes</run_on_start>
backend/app/db/db_populate.py
+10
@@ -129,6 +129,16 @@ def get_connectors_list():
129 "Connection to Graylog GELF Input to receive events from integrations. Make sure you have created a GELF Input in Graylog.",
130 "GELF_INPUT_PORT",
131 ),
132 + (
133 + "HAProxy Provisioning",
134 + "3",
135 + "host_only",
136 + (
137 + "Connection to HAProxy Provisioning. Make sure you have deployed the "
138 + "HAProxy Provisioning Application provided by "
139 + "SOCFortress: https://github.com/socfortress/Customer-Provisioning-Worker"
140 + ),
141 + ),
142 # ... Add more connectors as needed ...
143 ]
144
backend/app/db/db_setup.py
+5 -1
@@ -48,7 +48,11 @@ async def update_tables(async_engine):
48 logger.info("Updating tables")
49
50 # Define the new columns to be added
51 - new_columns = {"scheduled_job_metadata": ["extra_data TEXT"]}
51 + new_columns = {
52 + "scheduled_job_metadata": ["extra_data TEXT"],
53 + "customer_provisioning_default_settings": ["wazuh_worker_hostname TEXT"],
54 + "agents": ["wazuh_agent_status TEXT"],
55 + }
56
57 async with async_engine.begin() as conn:
58 for table_name, columns in new_columns.items():
backend/app/db/universal_models.py
+12
@@ -93,6 +93,7 @@ class Agents(SQLModel, table=True):
93 velociraptor_id: str = Field(max_length=256)
94 velociraptor_last_seen: datetime
95 wazuh_agent_version: str = Field(max_length=256)
96 + wazuh_agent_status: str = Field("not found", max_length=256)
97 velociraptor_agent_version: str = Field(max_length=256)
98 customer_code: Optional[str] = Field(foreign_key="customers.customer_code")
99 quarantined: bool = Field(default=False)
@@ -115,6 +116,7 @@ class Agents(SQLModel, table=True):
116 label=wazuh_agent.agent_label,
117 wazuh_last_seen=wazuh_last_seen_value,
118 wazuh_agent_version=wazuh_agent.wazuh_agent_version,
119 + wazuh_agent_status=wazuh_agent.wazuh_agent_status if wazuh_agent.wazuh_agent_status else "not found",
120 velociraptor_id=velociraptor_agent.client_id if velociraptor_agent.client_id else "n/a",
121 velociraptor_last_seen=velociraptor_agent.client_last_seen_as_datetime,
122 velociraptor_agent_version=velociraptor_agent.client_version,
@@ -137,6 +139,7 @@ class Agents(SQLModel, table=True):
139 self.label = wazuh_agent.agent_label
140 self.wazuh_last_seen = wazuh_last_seen_value
141 self.wazuh_agent_version = wazuh_agent.wazuh_agent_version
142 + self.wazuh_agent_status = wazuh_agent.wazuh_agent_status if wazuh_agent.wazuh_agent_status else "not found"
143 self.velociraptor_id = velociraptor_agent.client_id if velociraptor_agent.client_id else "n/a"
144 self.velociraptor_last_seen = velociraptor_agent.client_last_seen_as_datetime
145 self.velociraptor_agent_version = velociraptor_agent.client_version
@@ -154,3 +157,12 @@ class LogEntry(SQLModel, table=True):
157 status_code: int
158 message: str
159 additional_info: str = Field(default=None, nullable=True)
160 +
161 +
162 +class License(SQLModel, table=True):
163 + __tablename__ = "license"
164 + id: Optional[int] = Field(primary_key=True)
165 + license_key: str = Field(max_length=1024)
166 + customer_name: str = Field(max_length=1024)
167 + customer_email: str = Field(max_length=1024)
168 + company_name: str = Field(max_length=1024)
backend/app/integrations/huntress/services/provision.py
+4 -4
@@ -45,8 +45,8 @@ async def build_index_set_config(
45 TimeBasedIndexSet: The configured time-based index set.
46 """
47 return TimeBasedIndexSet(
48 - title=f"HUNTRESS - {(await get_customer(customer_code, session)).customer.customer_name}",
49 - description=f"HUNTRESS - {customer_code}",
48 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - HUNTRESS",
49 + description=f"{customer_code} - HUNTRESS",
50 index_prefix=f"huntress_{customer_code}",
51 rotation_strategy_class="org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategy",
52 rotation_strategy={
@@ -130,8 +130,8 @@ async def build_event_stream_config(
130 HuntressEventStream: The configured Huntress event stream.
131 """
132 return HuntressEventStream(
133 - title=f"HUNTRESS EVENTS - {(await get_customer(customer_code, session)).customer.customer_name}",
134 - description=f"HUNTRESS EVENTS - {(await get_customer(customer_code, session)).customer.customer_name}",
133 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - HUNTRESS",
134 + description=f"{(await get_customer(customer_code, session)).customer.customer_name} - HUNTRESS",
135 index_set_id=index_set_id,
136 rules=[
137 {
backend/app/integrations/mimecast/services/provision.py
+4 -4
@@ -45,8 +45,8 @@ async def build_index_set_config(
45 TimeBasedIndexSet: The configured time-based index set.
46 """
47 return TimeBasedIndexSet(
48 - title=f"Mimecast - {(await get_customer(customer_code, session)).customer.customer_name}",
49 - description=f"Mimecast - {customer_code}",
48 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - Mimecast",
49 + description=f"{customer_code} - Mimecast",
50 index_prefix=f"mimecast_{customer_code}",
51 rotation_strategy_class="org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategy",
52 rotation_strategy={
@@ -130,8 +130,8 @@ async def build_event_stream_config(
130 MimecastEventStream: The configured Mimecast event stream.
131 """
132 return MimecastEventStream(
133 - title=f"Mimecast EVENTS - {(await get_customer(customer_code, session)).customer.customer_name}",
134 - description=f"Mimecast EVENTS - {(await get_customer(customer_code, session)).customer.customer_name}",
133 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - Mimecast",
134 + description=f"{(await get_customer(customer_code, session)).customer.customer_name} - Mimecast",
135 index_set_id=index_set_id,
136 rules=[
137 {
backend/app/integrations/monitoring_alert/routes/monitoring_alert.py
+5 -4
@@ -170,11 +170,11 @@ async def create_custom_monitoring_alert(
170 Create a new monitoring alert. This receives the alert from Graylog and stores it in the database.
171
172 Args:
173 - monitoring_alert (MonitoringAlertsRequestModel): The monitoring alert details.
173 + monitoring_alert (GraylogPostRequest): The monitoring alert details.
174 session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
175
176 Returns:
177 - MonitoringAlertsRequestModel: The created monitoring alert.
177 + GraylogPostRequest: The created monitoring alert.
178 """
179 logger.info(f"Creating monitoring alert: {monitoring_alert}")
180 logger.info(f"Found index name {monitoring_alert.event.alert_index}")
@@ -443,7 +443,8 @@ async def run_sap_siem_suspicious_logins_analysis(
443 response_model=AlertAnalysisResponse,
444 )
445 async def run_sap_siem_multiple_logins_same_ip_analysis(
446 - threshold: Optional[int] = 1,
446 + threshold: Optional[int] = 0,
447 + time_range: Optional[int] = 10,
448 session: AsyncSession = Depends(get_db),
449 ) -> AlertAnalysisResponse:
450 """
@@ -464,7 +465,7 @@ async def run_sap_siem_multiple_logins_same_ip_analysis(
465 logger.info("Running analysis for SAP SIEM multiple logins")
466
467 # Call the analyze_wazuh_alerts function to analyze the alerts
467 - await sap_siem_multiple_logins_same_ip(threshold=threshold, session=session)
468 + await sap_siem_multiple_logins_same_ip(threshold=threshold, time_range=time_range, session=session)
469
470 return AlertAnalysisResponse(
471 success=True,
backend/app/integrations/monitoring_alert/schema/provision.py
+1
@@ -162,6 +162,7 @@ class GraylogAlertProvisionConfig(BaseModel):
162 series: List
163 conditions: Dict
164 type: str = Field(..., alias="type")
165 + event_limit: int = Field(1000, description="The event limit for the config")
166
167
168 class GraylogAlertProvisionNotificationSettings(BaseModel):
backend/app/integrations/monitoring_alert/services/provision.py
+7 -1
@@ -248,6 +248,7 @@ async def provision_alert_definition(
248 endpoint="/api/events/definitions",
249 data=alert_definition_model.dict(),
250 )
251 + logger.info(f"Graylog alert definition provisioned response: {response}")
252 if response["success"]:
253 return True
254 raise HTTPException(status_code=500, detail="Failed to provision alert definition")
@@ -318,6 +319,7 @@ async def provision_wazuh_monitoring_alert(
319 execute_every_ms=await convert_seconds_to_milliseconds(
320 request.execute_every,
321 ),
322 + event_limit=1000,
323 ),
324 field_spec={
325 "ALERT_ID": GraylogAlertProvisionFieldSpecItem(
@@ -437,6 +439,7 @@ async def provision_suricata_monitoring_alert(
439 execute_every_ms=await convert_seconds_to_milliseconds(
440 request.execute_every,
441 ),
442 + event_limit=1000,
443 ),
444 field_spec={
445 "ALERT_ID": GraylogAlertProvisionFieldSpecItem(
@@ -556,6 +559,7 @@ async def provision_office365_exchange_online_alert(
559 execute_every_ms=await convert_seconds_to_milliseconds(
560 request.execute_every,
561 ),
562 + event_limit=1000,
563 ),
564 field_spec={
565 "ALERT_ID": GraylogAlertProvisionFieldSpecItem(
@@ -675,6 +679,7 @@ async def provision_office365_threat_intel_alert(
679 execute_every_ms=await convert_seconds_to_milliseconds(
680 request.execute_every,
681 ),
682 + event_limit=1000,
683 ),
684 field_spec={
685 "ALERT_ID": GraylogAlertProvisionFieldSpecItem(
@@ -792,6 +797,7 @@ async def provision_custom_alert(request: CustomMonitoringAlertProvisionModel) -
797 execute_every_ms=await convert_seconds_to_milliseconds(
798 request.execute_every_ms,
799 ),
800 + event_limit=1000,
801 ),
802 field_spec={
803 custom_field.name: GraylogAlertProvisionFieldSpecItem(
@@ -799,7 +805,7 @@ async def provision_custom_alert(request: CustomMonitoringAlertProvisionModel) -
805 providers=[
806 GraylogAlertProvisionProvider(
807 type="template-v1",
802 - template=f"${{source.{custom_field.value}}}",
808 + template=f"${{source.{custom_field.value}}}" if custom_field.name != "CUSTOMER_CODE" else custom_field.value,
809 require_values=True,
810 ),
811 ],
backend/app/integrations/monitoring_alert/services/wazuh.py
+2 -2
@@ -332,7 +332,7 @@ async def build_alert_payload(
332 customer_code=alert_details.agent_labels_customer,
333 session=session,
334 )
335 - ).timefield
335 + ).timefield or "timestamp"
336 # Get the timefield value from the alert_details
337 if hasattr(alert_details, timefield):
338 alert_details.time_field = getattr(alert_details, timefield)
@@ -399,7 +399,7 @@ async def create_alert_details(alert_details: WazuhAlertModel) -> CreateAlertReq
399 agent_id=alert_details._source["agent_id"],
400 agent_labels_customer=alert_details._source["agent_labels_customer"],
401 timestamp=alert_details._source["timestamp"],
402 - timestamp_utc=alert_details._source["timestamp_utc"],
402 + timestamp_utc=alert_details._source.get("timestamp_utc", alert_details._source["timestamp"]),
403 process_id=alert_details._source.get("process_id", "No process ID found"),
404 )
405
backend/app/integrations/office365/services/provision.py
+216 -206
@@ -1,9 +1,15 @@
1 import json
2 import os
3 +import xml.etree.ElementTree as ET
4 from datetime import datetime
5 from typing import List
6 +from xml.dom import minidom
7 +from xml.dom.minidom import parseString
8 +from xml.etree.ElementTree import SubElement
9 +from xml.etree.ElementTree import parse
10 +from xml.etree.ElementTree import tostring
11
6 -import requests
12 +import aiofiles
13 from dotenv import load_dotenv
14 from fastapi import HTTPException
15 from loguru import logger
@@ -45,17 +51,18 @@ from app.integrations.office365.schema.provision import PipelineRuleTitles
51 from app.integrations.office365.schema.provision import PipelineTitles
52 from app.integrations.office365.schema.provision import ProvisionOffice365AuthKeys
53 from app.integrations.office365.schema.provision import ProvisionOffice365Response
48 -from app.integrations.utils.schema import PraecoAlertConfig
49 -from app.integrations.utils.schema import PraecoProvisionAlertResponse
54 from app.utils import get_connector_attribute
55
56 load_dotenv()
57
58
59 ############ ! WAZUH MANAGER ! ############
56 -async def get_wazuh_configuration() -> str:
60 +async def get_wazuh_configuration(file_name: str) -> str:
61 """
58 - Retrieves the Wazuh configuration from the manager.
62 + Retrieves the Wazuh configuration from the manager and writes it to a file.
63 +
64 + Args:
65 + file_name (str): The name of the file where the configuration data will be written.
66
67 Returns:
68 str: The Wazuh configuration data.
@@ -63,7 +70,17 @@ async def get_wazuh_configuration() -> str:
70 endpoint = "manager/configuration"
71 params = {"raw": True}
72 response = await send_get_request(endpoint=endpoint, params=params)
66 - return response["data"]
73 + config_data = response["data"]
74 +
75 + # Get the directory of the current module
76 + dir_path = os.path.dirname(os.path.realpath(__file__))
77 + # Create the full file path
78 + file_path = os.path.join(dir_path, file_name)
79 +
80 + async with aiofiles.open(file_path, "w") as f:
81 + await f.write(config_data)
82 +
83 + return config_data
84
85
86 async def office365_template_with_api_type(
@@ -74,87 +91,74 @@ async def office365_template_with_api_type(
91 Returns a configured Office365 template for Wazuh.
92
93 Args:
77 - wazuh_config (str): The current Wazuh configuration.
78 - tenant_id (str): Office365 Tenant ID.
79 - client_id (str): Office365 Client ID.
80 - client_secret (str): Office365 Client Secret.
81 - api_type (str): The type of Office365 API.
94 + customer_code (str): The customer code.
95 + provision_office365_auth_keys (ProvisionOffice365AuthKeys): The Office365 auth keys.
96
97 Returns:
98 str: The Office365 template configured with the given parameters.
99 """
100
87 - template = f"""
88 - <ossec_config>
89 - <office365>
90 - <enabled>yes</enabled>
91 - <interval>1m</interval>
92 - <curl_max_size>5M</curl_max_size>
93 - <only_future_events>yes</only_future_events>
94 - <!-- Office365 Integration For {customer_code} -->
95 - <api_auth>
96 - <tenant_id>{provision_office365_auth_keys.TENANT_ID}</tenant_id>
97 - <client_id>{provision_office365_auth_keys.CLIENT_ID}</client_id>
98 - <client_secret>{provision_office365_auth_keys.CLIENT_SECRET}</client_secret>
99 - <api_type>{provision_office365_auth_keys.API_TYPE}</api_type>
100 - </api_auth>
101 - <subscriptions>
102 - <subscription>Audit.SharePoint</subscription>
103 - <subscription>Audit.Exchange</subscription>
104 - <subscription>DLP.ALL</subscription>
105 - <subscription>Audit.General</subscription>
106 - <subscription>Audit.AzureActiveDirectory</subscription>
107 - </subscriptions>
108 - </office365>
109 - </ossec_config>
110 - """
111 -
112 - return template
113 -
114 -
115 -async def office365_template(
116 - customer_code: str,
117 - provision_office365_auth_keys: ProvisionOffice365AuthKeys,
118 -) -> str:
119 - """
120 - Returns a configured Office365 template for Wazuh.
101 + # Get the directory of the current module
102 + dir_path = os.path.dirname(os.path.realpath(__file__))
103 + # Create the full file path
104 + wazuh_config = os.path.join(dir_path, "wazuh_config.xml")
105
122 - Args:
123 - wazuh_config (str): The current Wazuh configuration.
124 - tenant_id (str): Office365 Tenant ID.
125 - client_id (str): Office365 Client ID.
126 - client_secret (str): Office365 Client Secret.
127 - api_type (str): The type of Office365 API.
106 + # Parse the existing XML file
107 + tree = parse(wazuh_config)
108 + root = tree.getroot()
109
129 - Returns:
130 - str: The Office365 template configured with the given parameters.
131 - """
110 + # Create the office365 element and add it to ossec_config
111 + office365 = SubElement(root, "office365")
112 +
113 + # Add the child elements to office365
114 + SubElement(office365, "enabled").text = "yes"
115 + SubElement(office365, "interval").text = "1m"
116 + SubElement(office365, "curl_max_size").text = "5M"
117 + SubElement(office365, "only_future_events").text = "yes"
118 +
119 + # Create the api_auth element and add it to office365
120 + api_auth = SubElement(office365, "api_auth")
121 +
122 + # Add the customer code as a comment within the api_auth block
123 + # api_auth.insert(0, Comment(f' Customer Code: {customer_code} '))
124 +
125 + # Add the child elements to api_auth
126 + SubElement(api_auth, "tenant_id").text = provision_office365_auth_keys.TENANT_ID
127 + SubElement(api_auth, "client_id").text = provision_office365_auth_keys.CLIENT_ID
128 + SubElement(api_auth, "client_secret").text = provision_office365_auth_keys.CLIENT_SECRET
129 + SubElement(api_auth, "api_type").text = provision_office365_auth_keys.API_TYPE
130 +
131 + # Create the subscriptions element and add it to office365
132 + subscriptions = SubElement(office365, "subscriptions")
133 +
134 + # Add the child elements to subscriptions
135 + SubElement(subscriptions, "subscription").text = "Audit.SharePoint"
136 + SubElement(subscriptions, "subscription").text = "Audit.Exchange"
137 + SubElement(subscriptions, "subscription").text = "DLP.ALL"
138 + SubElement(subscriptions, "subscription").text = "Audit.General"
139 + SubElement(subscriptions, "subscription").text = "Audit.AzureActiveDirectory"
140 +
141 + # Convert the office365 element to a string
142 + office365_str = tostring(office365).decode("utf-8")
143 +
144 + # Pretty print the office365 element
145 + dom = parseString(office365_str)
146 + pretty_office365_str = dom.toprettyxml(indent=" ")
147 +
148 + # Remove the XML declaration from the pretty printed office365 string
149 + pretty_office365_str = pretty_office365_str.replace('<?xml version="1.0" ?>', "").strip()
150 +
151 + # Convert the entire XML to a string
152 + xml_str = tostring(root).decode("utf-8")
153 +
154 + # Replace the original office365 string with the pretty printed office365 string
155 + xml_str = xml_str.replace(office365_str, pretty_office365_str)
156
133 - template = f"""
134 - <ossec_config>
135 - <office365>
136 - <enabled>yes</enabled>
137 - <interval>1m</interval>
138 - <curl_max_size>5M</curl_max_size>
139 - <only_future_events>yes</only_future_events>
140 - <!-- Office365 Integration For {customer_code} -->
141 - <api_auth>
142 - <tenant_id>{provision_office365_auth_keys.TENANT_ID}</tenant_id>
143 - <client_id>{provision_office365_auth_keys.CLIENT_ID}</client_id>
144 - <client_secret>{provision_office365_auth_keys.CLIENT_SECRET}</client_secret>
145 - </api_auth>
146 - <subscriptions>
147 - <subscription>Audit.SharePoint</subscription>
148 - <subscription>Audit.Exchange</subscription>
149 - <subscription>DLP.ALL</subscription>
150 - <subscription>Audit.General</subscription>
151 - <subscription>Audit.AzureActiveDirectory</subscription>
152 - </subscriptions>
153 - </office365>
154 - </ossec_config>
155 - """
156 -
157 - return template
157 + # Overwrite the existing XML file with the new contents
158 + async with aiofiles.open(wazuh_config, "w") as f:
159 + await f.write(xml_str)
160 +
161 + return xml_str
162
163
164 async def append_office365_template(wazuh_config: str, office365_template: str) -> str:
@@ -172,18 +176,81 @@ async def append_office365_template(wazuh_config: str, office365_template: str)
176 return wazuh_config + office365_template
177
178
179 +async def add_api_auth_to_office365_block(customer_code: str, provision_office365_auth_keys: ProvisionOffice365AuthKeys) -> str:
180 + try:
181 + # Get the directory of the current module
182 + dir_path = os.path.dirname(os.path.realpath(__file__))
183 + # Create the full file path
184 + wazuh_config = os.path.join(dir_path, "wazuh_config.xml")
185 +
186 + # Parse the existing XML file
187 + tree = ET.ElementTree()
188 + tree.parse(wazuh_config)
189 + root = tree.getroot()
190 +
191 + # Find the office365 block
192 + office365_block = root.find("office365")
193 +
194 + # If the office365 block exists
195 + if office365_block is not None:
196 + # Find the index of the subscriptions block
197 + subscriptions_index = list(office365_block).index(office365_block.find("subscriptions"))
198 +
199 + # Create a new api_auth block
200 + api_auth_block = ET.Element("api_auth")
201 +
202 + # Add the tenant_id, client_id, client_secret, and api_type to the api_auth block
203 + ET.SubElement(api_auth_block, "tenant_id").text = provision_office365_auth_keys.TENANT_ID
204 + ET.SubElement(api_auth_block, "client_id").text = provision_office365_auth_keys.CLIENT_ID
205 + ET.SubElement(api_auth_block, "client_secret").text = provision_office365_auth_keys.CLIENT_SECRET
206 + ET.SubElement(api_auth_block, "api_type").text = provision_office365_auth_keys.API_TYPE
207 +
208 + # Pretty print the new api_auth block
209 + pretty_api_auth_block = minidom.parseString(ET.tostring(api_auth_block)).toprettyxml(indent=" ")
210 +
211 + # Parse the pretty printed api_auth block back to an Element
212 + pretty_api_auth_element = ET.fromstring(pretty_api_auth_block)
213 +
214 + # Insert the new pretty printed api_auth block above the subscriptions block
215 + office365_block.insert(subscriptions_index, pretty_api_auth_element)
216 +
217 + # Convert the modified configuration back to string format
218 + modified_config = ET.tostring(root, encoding="utf-8").decode("utf-8")
219 +
220 + # Overwrite the existing XML file with the new contents
221 + with open(wazuh_config, "w") as f:
222 + f.write(modified_config)
223 +
224 + return modified_config
225 +
226 + except Exception as e:
227 + logger.error(f"An error occurred: {e}")
228 + # Print the full traceback
229 + import traceback
230 +
231 + traceback.print_exc()
232 +
233 +
234 async def update_wazuh_configuration(
176 - wazuh_config: str,
235 provision_office365_auth_keys: ProvisionOffice365AuthKeys,
236 ) -> None:
237 """
238 Updates the Wazuh configuration. If it fails, remove the <api_type> tag and retry.
239
240 Args:
183 - wazuh_config (str): The Wazuh configuration in string format.
241 provision_office365_auth_keys (ProvisionOffice365AuthKeys): The Office365 authentication keys.
242 """
243 endpoint = "manager/configuration"
244 +
245 + # Get the directory of the current module
246 + dir_path = os.path.dirname(os.path.realpath(__file__))
247 + # Create the full file path
248 + wazuh_config_path = os.path.join(dir_path, "wazuh_config.xml")
249 +
250 + # Read the Wazuh configuration from the file
251 + async with aiofiles.open(wazuh_config_path, "r") as f:
252 + wazuh_config = await f.read()
253 +
254 data = wazuh_config.encode("utf-8")
255
256 try:
@@ -241,7 +308,26 @@ async def check_if_office365_is_already_provisioned(
308 wazuh_config: str,
309 ) -> bool:
310 """
244 - If the string "Office365 Integration For {customer_code}" is found in the Wazuh configuration, return True.
311 + If the string "Office365 Integration" is found in the Wazuh configuration, return True.
312 +
313 + Args:
314 + customer_code (str): The customer code.
315 + wazuh_config (str): The Wazuh configuration in string format.
316 +
317 + Returns:
318 + bool: True if the Office365 integration is already provisioned, False otherwise.
319 + """
320 + if "office365" in wazuh_config:
321 + return True
322 + return False
323 +
324 +
325 +async def check_if_office365_is_already_provisioned_for_customer(
326 + tenant_id: str,
327 + wazuh_config: str,
328 +) -> bool:
329 + """
330 + If the string "Office365 Integration" is found in the Wazuh configuration, return True.
331
332 Args:
333 customer_code (str): The customer code.
@@ -250,10 +336,10 @@ async def check_if_office365_is_already_provisioned(
336 Returns:
337 bool: True if the Office365 integration is already provisioned, False otherwise.
338 """
253 - if f"Office365 Integration For {customer_code}" in wazuh_config:
339 + if f"{tenant_id}" in wazuh_config:
340 raise HTTPException(
341 status_code=400,
256 - detail=f"Office365 integration already provisioned for customer {customer_code}.",
342 + detail=f"Office365 integration is already provisioned for customer {tenant_id}.",
343 )
344
345
@@ -282,8 +368,8 @@ async def build_index_set_config(
368 TimeBasedIndexSet: The configured time-based index set.
369 """
370 return TimeBasedIndexSet(
285 - title=f"Office365 - {(await get_customer(customer_code, session)).customer.customer_name}",
286 - description=f"Office365 - {customer_code}",
371 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - Office365",
372 + description=f"{customer_code} - Office365",
373 index_prefix=f"office365_{customer_code}",
374 rotation_strategy_class="org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategy",
375 rotation_strategy={
@@ -382,8 +468,8 @@ async def build_event_stream_config(
468 Office365EventStream: The configured Wazuh event stream.
469 """
470 return Office365EventStream(
385 - title=f"Office365 EVENTS - {(await get_customer(customer_code, session)).customer.customer_name}",
386 - description=f"Office365 EVENTS - {(await get_customer(customer_code, session)).customer.customer_name}",
471 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - Office365",
472 + description=f"{(await get_customer(customer_code, session)).customer.customer_name} - Office365",
473 index_set_id=index_set_id,
474 rules=[
475 {
@@ -673,6 +759,11 @@ async def create_grafana_datasource(
759 """
760 logger.info("Creating Grafana datasource")
761 grafana_client = await create_grafana_client("Grafana")
762 + grafana_url = await get_connector_attribute(
763 + connector_id=12,
764 + column_name="connector_url",
765 + session=session,
766 + )
767 # Switch to the newly created organization
768 grafana_client.user.switch_actual_user_organisation(
769 (await get_customer_meta(customer_code, session)).customer_meta.customer_meta_grafana_org_id,
@@ -703,6 +794,18 @@ async def create_grafana_datasource(
794 },
795 isDefault=False,
796 jsonData={
797 + "dataLinks": [
798 + {
799 + "field": "^_id$",
800 + "url": (
801 + "{}/explore?left=%7B%22datasource%22:%22O365%22,%22queries%22:%5B%7B"
802 + "%22refId%22:%22A%22,%22query%22:%22_id:${{__value.raw}}%22,%22alias%22:%22%22,"
803 + "%22metrics%22:%5B%7B%22id%22:%221%22,%22type%22:%22logs%22,%22settings%22:"
804 + "%7B%22limit%22:%22500%22%7D%7D%5D,%22bucketAggs%22:%5B%5D,%22timeField%22:"
805 + "%22timestamp%22%7D%5D,%22range%22:%7B%22from%22:%22now-6h%22,%22to%22:%22now%22%7D%7D"
806 + ).format(grafana_url),
807 + },
808 + ],
809 "database": f"office365_{customer_code}*",
810 "flavor": "opensearch",
811 "includeFrozen": False,
@@ -733,22 +836,38 @@ async def provision_office365(
836 logger.info(f"Provisioning Office365 integration for customer {customer_code}.")
837
838 # Get Wazuh configuration
736 - wazuh_config = await get_wazuh_configuration()
839 + wazuh_config = await get_wazuh_configuration(file_name="wazuh_config.xml")
840
841 # Check if Office365 is already provisioned
739 - await check_if_office365_is_already_provisioned(customer_code, wazuh_config)
842 + office365_provisioned = await check_if_office365_is_already_provisioned(customer_code, wazuh_config)
843
844 # Create Office365 template
742 - office365_templated = await office365_template_with_api_type(
743 - customer_code,
744 - provision_office365_auth_keys,
745 - )
845 + if office365_provisioned:
846 + logger.info("Office365 integration is already provisioned.")
847 + else:
848 + logger.info("Office365 integration is not yet provisioned.")
849 + logger.info("Creating new Office365 block.")
850 + office365_templated = await office365_template_with_api_type(
851 + customer_code,
852 + provision_office365_auth_keys,
853 + )
854
747 - # Append Office365 template to Wazuh configuration
748 - wazuh_config = await append_office365_template(wazuh_config, office365_templated)
855 + # Check if Office365 is already provisioned for customer
856 + await check_if_office365_is_already_provisioned_for_customer(provision_office365_auth_keys.TENANT_ID, wazuh_config)
857 +
858 + # If Office365 is already provisioned but not for the customer, add the api_auth contents to the office365 block
859 + if office365_provisioned and not await check_if_office365_is_already_provisioned_for_customer(
860 + provision_office365_auth_keys.TENANT_ID,
861 + wazuh_config,
862 + ):
863 + wazuh_config = await add_api_auth_to_office365_block(customer_code, provision_office365_auth_keys)
864 + else:
865 + # Append Office365 template to Wazuh configuration
866 + wazuh_config = await append_office365_template(wazuh_config, office365_templated)
867
868 # Update Wazuh configuration
751 - await update_wazuh_configuration(wazuh_config, provision_office365_auth_keys)
869 + # await update_wazuh_configuration(wazuh_config, provision_office365_auth_keys)
870 + await update_wazuh_configuration(provision_office365_auth_keys)
871
872 # Restart Wazuh manager
873 await restart_wazuh_manager()
@@ -798,75 +917,6 @@ async def provision_office365(
917 ),
918 )
919
801 - # Create alert in Praeco
802 - await provision_alert_in_praeco(
803 - PraecoAlertConfig(
804 - alert=["post"],
805 - filter=[
806 - {
807 - "query": {
808 - "query_string": {
809 - "query": "syslog_level:ALERT AND data_office365_Subscription:Audit.Exchange",
810 - },
811 - },
812 - },
813 - ],
814 - generate_kibana_discover_url=False,
815 - http_post_ignore_ssl_errors=False,
816 - http_post_timeout=60,
817 - http_post_url=[
818 - f"http://{os.getenv('SERVER_IP')}:5000/api/v1/alerts/office365/exchange",
819 - ],
820 - import_config="BaseRule.config",
821 - index="office365_*",
822 - is_enabled=True,
823 - kibana_discover_from_timedelta={"minutes": 10},
824 - kibana_discover_to_timedelta={"minutes": 10},
825 - match_enhancements=[],
826 - name="Office365 - Exchange",
827 - realert={"minutes": 0},
828 - timestamp_field="timestamp_utc",
829 - timestamp_type="iso",
830 - type="any",
831 - use_strftime_index=False,
832 - ),
833 - session=session,
834 - )
835 -
836 - await provision_alert_in_praeco(
837 - PraecoAlertConfig(
838 - alert=["post"],
839 - filter=[
840 - {
841 - "query": {
842 - "query_string": {
843 - "query": "syslog_level:ALERT AND data_office365_UserId:ThreatIntel",
844 - },
845 - },
846 - },
847 - ],
848 - generate_kibana_discover_url=False,
849 - http_post_ignore_ssl_errors=False,
850 - http_post_timeout=60,
851 - http_post_url=[
852 - f"http://{os.getenv('SERVER_IP')}:5000/api/v1/alerts/office365/threat_intel",
853 - ],
854 - import_config="BaseRule.config",
855 - index="office365_*",
856 - is_enabled=True,
857 - kibana_discover_from_timedelta={"minutes": 10},
858 - kibana_discover_to_timedelta={"minutes": 10},
859 - match_enhancements=[],
860 - name="Office365 - Threat Intel",
861 - realert={"minutes": 0},
862 - timestamp_field="timestamp_utc",
863 - timestamp_type="iso",
864 - type="any",
865 - use_strftime_index=False,
866 - ),
867 - session=session,
868 - )
869 -
920 await update_customer_integration_table(customer_code, session)
921
922 return ProvisionOffice365Response(
@@ -875,46 +925,6 @@ async def provision_office365(
925 )
926
927
878 -######### ! Provision in Praeco ! ############
879 -async def provision_alert_in_praeco(
880 - request: PraecoAlertConfig,
881 - session: AsyncSession,
882 -) -> PraecoProvisionAlertResponse:
883 - """
884 - Provisions the given alert in Praeco. https://github.com/socfortress/Customer-Provisioning-Alert
885 -
886 - Args:
887 - request (PraecoAlertConfig): The request object containing the necessary information for provisioning.
888 - session (AsyncSession): The async session object for making HTTP requests.
889 -
890 - Returns:
891 - PraecoProvisionAlertResponse: The response object indicating the success or failure of the provisioning operation.
892 - """
893 - logger.info(f"Provisioning to alert creation - Praeco {request}")
894 - api_endpoint = await get_connector_attribute(
895 - connector_id=15,
896 - column_name="connector_url",
897 - session=session,
898 - )
899 - # Send the POST request to Praeco
900 - response = requests.post(
901 - url=f"{api_endpoint}/provision_alert",
902 - json=request.dict(by_alias=True),
903 - )
904 - logger.info(f"Response: {response.json()}")
905 - # Check the response status code
906 - if response.status_code != 200:
907 - return PraecoProvisionAlertResponse(
908 - success=False,
909 - message=f"Failed to provision to Alert Creation App: {response.text}",
910 - )
911 - # Return the response
912 - return PraecoProvisionAlertResponse(
913 - success=True,
914 - message="Successfully provisioned to Alert Creation App.",
915 - )
916 -
917 -
928 ######### ! Update Database ! ############
929 async def update_customer_integration_table(
930 customer_code: str,
backend/app/integrations/sap_siem/routes/sap_siem.py
+181
@@ -1,3 +1,5 @@
1 +from typing import Optional
2 +
3 from fastapi import APIRouter
4 from fastapi import Depends
5 from loguru import logger
@@ -10,6 +12,27 @@ from app.integrations.sap_siem.schema.sap_siem import InvokeSapSiemRequest
12 from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
13 from app.integrations.sap_siem.schema.sap_siem import SapSiemAuthKeys
14 from app.integrations.sap_siem.services.collect import collect_sap_siem
15 +from app.integrations.sap_siem.services.sap_siem_brute_force_same_ip import (
16 + sap_siem_brute_force_failed_same_ip,
17 +)
18 +from app.integrations.sap_siem.services.sap_siem_brute_forced_failed_logins import (
19 + sap_siem_brute_force_failed_multiple_ips,
20 +)
21 +from app.integrations.sap_siem.services.sap_siem_failed_same_user_different_geo_location import (
22 + sap_siem_failed_same_user_diff_geo,
23 +)
24 +from app.integrations.sap_siem.services.sap_siem_failed_same_user_from_different_ip import (
25 + sap_siem_failed_same_user_diff_ip,
26 +)
27 +from app.integrations.sap_siem.services.sap_siem_successful_login_same_ip_after_multiple_failures import (
28 + sap_siem_successful_login_after_failures,
29 +)
30 +from app.integrations.sap_siem.services.sap_siem_successful_same_user_different_geo_location import (
31 + sap_siem_successful_same_user_diff_geo,
32 +)
33 +from app.integrations.sap_siem.services.sap_siem_successful_user_login_after_using_different_ip import (
34 + sap_siem_successful_user_login_with_different_ip,
35 +)
36 from app.integrations.utils.utils import extract_auth_keys
37 from app.integrations.utils.utils import get_customer_integration_response
38
@@ -69,3 +92,161 @@ async def collect_sap_siem_route(sap_siem_request: InvokeSapSiemRequest, session
92 await collect_sap_siem(sap_siem_request=collect_sap_siem_request)
93
94 return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
95 +
96 +
97 +@integration_sap_siem_router.post(
98 + "/successful_user_login_with_different_ip",
99 + response_model=InvokeSAPSiemResponse,
100 + description="Rule: Successful user login after using different IP addresses\n\n"
101 + "Period: within 15 minutes\n\n"
102 + "Prerequisite: \n\n"
103 + "- Login attempts from different IP addresses, regardless of login status (at least 2 failed IP addresses)\n\n"
104 + "- Successful login afterwards (from the third successful IP address)\n\n"
105 + "Result: User compressed, IP addresses belong to an attack network",
106 +)
107 +async def invoke_sap_siem_successful_user_login_with_different_ip_route(
108 + threshold: Optional[int] = 0,
109 + time_range: Optional[int] = 15,
110 + session: AsyncSession = Depends(get_db),
111 +):
112 + logger.info("Invoking SAP SIEM integration for successful user login with different IP.")
113 + await sap_siem_successful_user_login_with_different_ip(threshold=threshold, time_range=time_range, session=session)
114 +
115 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
116 +
117 +
118 +@integration_sap_siem_router.post(
119 + "/same_user_failed_login_from_different_ip",
120 + response_model=InvokeSAPSiemResponse,
121 + description="Rule: Same user from different IP addresses\n\n"
122 + "Period: within 10 minutes\n\n"
123 + "Prerequisite: \n\n"
124 + "- At least 3 failed login attempts with the same user name from 3 different IP addresses\n\n"
125 + "Result: User compressed, IP addresses belong to an attack network",
126 +)
127 +async def invoke_sap_siem_same_user_failed_login_from_different_ip_route(
128 + threshold: Optional[int] = 0,
129 + time_range: Optional[int] = 10,
130 + session: AsyncSession = Depends(get_db),
131 +):
132 + logger.info("Invoking SAP SIEM integration for same user failed login from different IP.")
133 + await sap_siem_failed_same_user_diff_ip(threshold=threshold, time_range=time_range, session=session)
134 +
135 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
136 +
137 +
138 +@integration_sap_siem_router.post(
139 + "/same_user_failed_login_from_different_geo_location",
140 + response_model=InvokeSAPSiemResponse,
141 + description="Rule: Same user from different geo locations\n\n"
142 + "Period: within 20 minutes\n\n"
143 + "Prerequisite: \n\n"
144 + "- At least 3 failed login attempts with the same user name from at least two different GEO IP country locations\n\n"
145 + "Result: User compressed, IP addresses belong to an attack network",
146 +)
147 +async def invoke_sap_siem_same_user_failed_login_from_different_geo_location_route(
148 + threshold: Optional[int] = 0,
149 + time_range: Optional[int] = 20,
150 + session: AsyncSession = Depends(get_db),
151 +):
152 + logger.info("Invoking SAP SIEM integration for same user failed login from different geo location.")
153 + await sap_siem_failed_same_user_diff_geo(threshold=threshold, time_range=time_range, session=session)
154 +
155 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
156 +
157 +
158 +@integration_sap_siem_router.post(
159 + "/same_user_successful_login_from_different_geo_location",
160 + response_model=InvokeSAPSiemResponse,
161 + description="Rule: Same user from different geo locations\n\n"
162 + "Period: within 20 minutes\n\n"
163 + "Prerequisite: \n\n"
164 + "- At least 1 failed login attempt with the same username from two different GEO IP country locations\n\n"
165 + "- from the 2nd successful login thereafter in another GEO IP country location\n\n"
166 + "Result: User compressed, IP addresses belong to an attack network\n\n"
167 + "This function would trigger a suspicious login when the following conditions are met:\n\n"
168 + "1. There is at least one failed login attempt from the same user (identified by `login_id`) from two different GEO IP country locations within the last 20 minutes.\n"
169 + "2. There is at least one successful login attempt from the same user from a different GEO IP country location within the last 20 minutes.\n\n"
170 + "Here are some examples:\n\n"
171 + "Example 1:\n"
172 + "- At 12:00, a failed login attempt is made by user `user1` from IP `1.1.1.1` located in the US.\n"
173 + "- At 12:10, another failed login attempt is made by `user1` from IP `2.2.2.2` located in Canada.\n"
174 + "- At 12:15, a successful login attempt is made by `user1` from IP `3.3.3.3` located in the UK.\n"
175 + "- In this case, the function would trigger a suspicious login for `user1` because there are failed login attempts from two different countries (US and Canada) "
176 + "and a successful login from a different country (UK) within 20 minutes.\n\n"
177 + "Example 2:\n"
178 + "- At 12:00, a failed login attempt is made by user `user2` from IP `4.4.4.4` located in the US.\n"
179 + "- At 12:10, another failed login attempt is made by `user2` from IP `5.5.5.5` also located in the US.\n"
180 + "- At 12:15, a successful login attempt is made by `user2` from IP `6.6.6.6` located in the US.\n"
181 + "- In this case, the function would not trigger a suspicious login for `user2` because all the login attempts are from the same country (US).",
182 +)
183 +async def invoke_sap_siem_same_user_successful_login_from_different_geo_location_route(
184 + threshold: Optional[int] = 0,
185 + time_range: Optional[int] = 20,
186 + session: AsyncSession = Depends(get_db),
187 +):
188 + logger.info("Invoking SAP SIEM integration for same user successful login from different geo location.")
189 + await sap_siem_successful_same_user_diff_geo(threshold=threshold, time_range=time_range, session=session)
190 +
191 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
192 +
193 +
194 +@integration_sap_siem_router.post(
195 + "/brute_force_failed_logins_multiple_ips",
196 + response_model=InvokeSAPSiemResponse,
197 + description="Rule: Logins from different IP addresses\n\n"
198 + "Period: within 3 minutes\n\n"
199 + "Prerequisite: \n\n"
200 + "- At least 25 failed login attempts from different IP addresses\n\n"
201 + "Result: IP addresses belong to an attack network",
202 +)
203 +async def invoke_sap_siem_brute_force_failed_logins_route(
204 + threshold: Optional[int] = 0,
205 + time_range: Optional[int] = 3,
206 + session: AsyncSession = Depends(get_db),
207 +):
208 + logger.info("Invoking SAP SIEM integration for brute force failed logins.")
209 + await sap_siem_brute_force_failed_multiple_ips(threshold=threshold, time_range=time_range, session=session)
210 +
211 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
212 +
213 +
214 +@integration_sap_siem_router.post(
215 + "/brute_force_failed_logins_same_ip",
216 + response_model=InvokeSAPSiemResponse,
217 + description="Rule: Logins from the same IP address\n\n"
218 + "Period: within 5 minutes\n\n"
219 + "Prerequisite: \n\n"
220 + "- At least 10 different user name failed login attempts from the same IP address\n\n"
221 + "Result: IP addresses belong to an attack network",
222 +)
223 +async def invoke_sap_siem_brute_force_failed_logins_same_ip_route(
224 + threshold: Optional[int] = 0,
225 + time_range: Optional[int] = 5,
226 + session: AsyncSession = Depends(get_db),
227 +):
228 + logger.info("Invoking SAP SIEM integration for brute force failed logins from the same IP.")
229 + await sap_siem_brute_force_failed_same_ip(threshold=threshold, time_range=time_range, session=session)
230 +
231 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
232 +
233 +
234 +@integration_sap_siem_router.post(
235 + "/successful_login_after_multiple_failed_logins",
236 + response_model=InvokeSAPSiemResponse,
237 + description="Rule: Successful login after multiple failed logins\n\n"
238 + "Period: within 2 minutes\n\n"
239 + "Prerequisite: \n\n"
240 + "- At least 3 different user names that have failed from the same IP addressn\n"
241 + "- At least one successful login from the same IP address after 3 different user names. \n\n"
242 + "Result: User compromised, IP address belongs to an attack network",
243 +)
244 +async def invoke_sap_siem_successful_login_after_multiple_failed_logins_route(
245 + threshold: Optional[int] = 0,
246 + time_range: Optional[int] = 2,
247 + session: AsyncSession = Depends(get_db),
248 +):
249 + logger.info("Invoking SAP SIEM integration for successful login after multiple failed logins.")
250 + await sap_siem_successful_login_after_failures(threshold=threshold, time_range=time_range, session=session)
251 +
252 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM Events collected successfully.")
backend/app/integrations/sap_siem/schema/sap_siem.py
+28
@@ -204,6 +204,34 @@ class Result(BaseModel):
204 "False",
205 description="Whether the event has been analyzed for multiple logins",
206 )
207 + event_analyzed_success_login_diff_ip: Optional[str] = Field(
208 + "False",
209 + description="Whether the event has been analyzed for successful login from different IP",
210 + )
211 + event_analyzed_same_user_failed_diff_ip: Optional[str] = Field(
212 + "False",
213 + description="Whether the event has been analyzed for same user failed login from different IP",
214 + )
215 + event_analyzed_same_user_failed_diff_geo: Optional[str] = Field(
216 + "False",
217 + description="Whether the event has been analyzed for same user failed login from different geo",
218 + )
219 + event_analyzed_same_user_successful_diff_geo: Optional[str] = Field(
220 + "False",
221 + description="Whether the event has been analyzed for same user successful login from different geo",
222 + )
223 + event_analyzed_brute_force_ip: Optional[str] = Field(
224 + "False",
225 + description="Whether the event has been analyzed for brute force IP",
226 + )
227 + event_analyzed_brute_force_same_ip: Optional[str] = Field(
228 + "False",
229 + description="Whether the event has been analyzed for brute force same IP",
230 + )
231 + event_analyzed_successful_login_after_failures_diff_loginID: Optional[str] = Field(
232 + "False",
233 + description="Whether the event has been analyzed for successful login after failures",
234 + )
235
236
237 class SapSiemResponseBody(BaseModel):
backend/app/integrations/sap_siem/services/provision.py
+4 -4
@@ -45,8 +45,8 @@ async def build_index_set_config(
45 TimeBasedIndexSet: The configured time-based index set.
46 """
47 return TimeBasedIndexSet(
48 - title=f"SAP SIEM - {(await get_customer(customer_code, session)).customer.customer_name}",
49 - description=f"SAP SIEM - {customer_code}",
48 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - SAP SIEM",
49 + description=f"{customer_code} - SAP SIEM",
50 index_prefix=f"sap_siem_{customer_code}",
51 rotation_strategy_class="org.graylog2.indexer.rotation.strategies.TimeBasedRotationStrategy",
52 rotation_strategy={
@@ -130,8 +130,8 @@ async def build_event_stream_config(
130 SapSiemEventStream: The configured SAP SIEM event stream.
131 """
132 return SapSiemEventStream(
133 - title=f"SAP SIEM EVENTS - {(await get_customer(customer_code, session)).customer.customer_name}",
134 - description=f"SAP SIEM EVENTS - {(await get_customer(customer_code, session)).customer.customer_name}",
133 + title=f"{(await get_customer(customer_code, session)).customer.customer_name} - SAP SIEM",
134 + description=f"{(await get_customer(customer_code, session)).customer.customer_name} - SAP SIEM",
135 index_set_id=index_set_id,
136 rules=[
137 {
backend/app/integrations/sap_siem/services/sap_siem_brute_force_same_ip.py new
+575
@@ -0,0 +1,575 @@
1 +from collections import defaultdict
2 +from datetime import datetime
3 +from datetime import timedelta
4 +from typing import List
5 +from typing import Set
6 +
7 +from fastapi import HTTPException
8 +from loguru import logger
9 +from sqlalchemy.ext.asyncio import AsyncSession
10 +from sqlalchemy.future import select
11 +
12 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 +from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 +from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 +from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 +from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 +from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 +from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 +from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 +from app.integrations.utils.alerts import send_to_shuffle
23 +from app.integrations.utils.schema import ShufflePayload
24 +from app.utils import get_customer_alert_settings
25 +
26 +# Global set to keep track of IPs that have already been checked
27 +checked_ips = set()
28 +
29 +
30 +async def handle_common_suspicious_login_tasks(
31 + suspicious_login,
32 + unique_instances,
33 + case_ids,
34 + create_case_fn,
35 + session: AsyncSession,
36 +):
37 + """
38 + Handles common tasks for suspicious logins.
39 +
40 + Args:
41 + suspicious_login: The suspicious login object.
42 + unique_instances: List of unique instances.
43 + case_ids: List of case IDs.
44 + create_case_fn: Function to create a case.
45 + session: The async session.
46 +
47 + Returns:
48 + None
49 + """
50 + case = await create_case_fn(suspicious_login, session)
51 + case_ids.append(case.data.case_id)
52 + user_activity = await collect_user_activity(suspicious_login)
53 + await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 + await mark_as_checked(suspicious_login)
55 + alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 + await send_to_shuffle(
57 + ShufflePayload(
58 + alert_id=case.data.case_id,
59 + customer=suspicious_login.customer_code,
60 + customer_code=suspicious_login.customer_code,
61 + alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 + rule_description=f"{case.data.case_name}",
63 + hostname=suspicious_login.ip,
64 + ),
65 + session=session,
66 + )
67 +
68 +
69 +async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
70 + """
71 + Handles suspicious login events with multiple logins.
72 +
73 + Args:
74 + suspicious_login: The suspicious login event.
75 + unique_instances: List of unique instances of the suspicious login event.
76 + case_ids: List of case IDs associated with the suspicious login event.
77 + session: The database session.
78 +
79 + Returns:
80 + None
81 + """
82 + await handle_common_suspicious_login_tasks(
83 + suspicious_login,
84 + unique_instances,
85 + case_ids,
86 + create_iris_case_multiple,
87 + session,
88 + )
89 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
90 +
91 +
92 +async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
93 + """
94 + Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
95 +
96 + :param suspicious_login: The suspicious login to update
97 +
98 + :return: None
99 + """
100 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
101 + try:
102 + es_client.update(
103 + index=index,
104 + id=id,
105 + body={
106 + "doc": {
107 + "event_analyzed_brute_force_same_ip": "True",
108 + },
109 + },
110 + )
111 + logger.info(f"Updated event_analyzed_brute_force_same_ip flag for suspicious login: {id}")
112 + except Exception as e:
113 + logger.error(
114 + f"Failed to update case created flag {e}",
115 + )
116 + # Attempt to remove read-only block
117 + try:
118 + es_client.indices.put_settings(
119 + index=index,
120 + body={"index.blocks.write": None},
121 + )
122 + logger.info(
123 + f"Removed read-only block from index {index}. Retrying update.",
124 + )
125 +
126 + # Retry the update operation
127 + es_client.update(
128 + index=index,
129 + id=id,
130 + body={"doc": {"event_analyzed_brute_force_same_ip": "True"}},
131 + )
132 + logger.info(
133 + f"Added event_analyzed_brute_force_same_ip flag to index {index} for suspicious login: {id}",
134 + )
135 +
136 + # Reenable the write block
137 + es_client.indices.put_settings(
138 + index=index,
139 + body={"index.blocks.write": True},
140 + )
141 + except Exception as e2:
142 + logger.error(
143 + f"Failed to remove read-only block from index {index}: {e2}",
144 + )
145 + return False
146 +
147 +
148 +async def mark_as_checked(suspicious_login):
149 + """
150 + Marks a suspicious login as checked by adding it to the set of checked IPs.
151 +
152 + Args:
153 + suspicious_login (Login): The suspicious login object to mark as checked.
154 +
155 + Returns:
156 + None
157 + """
158 + checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
159 +
160 +
161 +async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
162 + """
163 + Handles user activity by processing each hit in the user_activity and performing the following steps:
164 + 1. Extracts relevant information from the hit.
165 + 2. Checks if the current activity is already present in the unique_instances set.
166 + 3. If not present, adds the user activity to the IRIS case.
167 + 4. Creates an asset payload using the current activity.
168 + 5. Updates the case with the asset payload.
169 + 6. Updates the event analyzed multiple logins flag for the hit.
170 + 7. Adds the current activity to the unique_instances set.
171 +
172 + Parameters:
173 + - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
174 + - unique_instances (set): A set containing unique instances of user activity.
175 + - case_id (str): The ID of the IRIS case.
176 +
177 + Returns:
178 + None
179 + """
180 + for hit in user_activity.hits.hits:
181 + current_activity = {
182 + "loginID": hit.source.params_loginID,
183 + "ip": hit.source.ip,
184 + "country": hit.source.httpReq_country,
185 + "errMessage": hit.source.errMessage,
186 + "event_timestamp": hit.source.event_timestamp,
187 + "customer_code": hit.source.customer_code,
188 + "errDetails": hit.source.errDetails,
189 + }
190 + current_activity_frozenset = frozenset(current_activity.items())
191 + if current_activity_frozenset not in unique_instances:
192 + logger.info(f"Adding user activity to IRIS case: {current_activity}")
193 + current_asset = SuspiciousLogin(**current_activity)
194 + asset_payload = create_asset_payload(asset=current_asset)
195 + logger.info(f"Asset Payload: {asset_payload}")
196 + await update_case_with_asset(case_id, asset_payload)
197 + await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
198 + unique_instances.add(current_activity_frozenset)
199 +
200 +
201 +def create_asset_payload(asset: SuspiciousLogin):
202 + """
203 + Create a payload for adding an asset based on a SuspiciousLogin object.
204 +
205 + Args:
206 + asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
207 +
208 + Returns:
209 + AddAssetModel: The payload for adding the asset.
210 +
211 + """
212 + if asset.errMessage == "OK":
213 + return AddAssetModel(
214 + name=asset.loginID,
215 + ip=asset.ip,
216 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
217 + asset_type=1,
218 + compromise_status=1,
219 + analysis_status=2,
220 + )
221 + return AddAssetModel(
222 + name=asset.loginID,
223 + ip=asset.ip,
224 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
225 + asset_type=1,
226 + analysis_status=2,
227 + )
228 +
229 +
230 +async def update_case_with_asset(case_id: str, asset_payload):
231 + """
232 + Update the case with the asset information.
233 +
234 + :param case_id: The ID of the case to update
235 + :param asset_payload: The payload to update the case with
236 +
237 + :return: None
238 + """
239 + logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
240 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
241 + return await fetch_and_validate_data(
242 + client,
243 + case_client.add_asset,
244 + cid=case_id,
245 + **asset_payload.to_dict(),
246 + )
247 +
248 +
249 +async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
250 + """
251 + Creates an IRIS case for multiple logins with the same IP address.
252 +
253 + Args:
254 + suspicious_login (SuspiciousLogin): The suspicious login information.
255 + session (AsyncSession): The async session for database operations.
256 +
257 + Returns:
258 + CaseResponse: The response containing the created case information.
259 + """
260 + logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
261 + case_name = (
262 + f"Log Source: {suspicious_login.logSource} SAP SIEM. "
263 + f"10 or more login failures for different users from the same IP within 5 minutes."
264 + )
265 +
266 + case_description = (
267 + f"Log Source: {suspicious_login.logSource}\n\n"
268 + f"IP Address: {suspicious_login.ip}\n\n"
269 + f"Country: {suspicious_login.country}\n\n"
270 + f"Timestamp: {suspicious_login.event_timestamp}"
271 + )
272 +
273 + case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
274 +
275 + payload = IrisCasePayload(
276 + case_name=case_name,
277 + case_description=case_description,
278 + case_customer=case_customer,
279 + case_classification=18,
280 + soc_id="1",
281 + create_customer=False,
282 + )
283 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
284 + result = await fetch_and_validate_data(
285 + client,
286 + case_client.add_case,
287 + **payload.to_dict(),
288 + )
289 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
290 +
291 + return CaseResponse(**result)
292 +
293 +
294 +async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
295 + """
296 + Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
297 + Collects a max of 1000 records.
298 +
299 + :param suspicious_logins: A list of suspicious logins
300 +
301 + :return: List of the user Activity collected from the sap_siem table
302 + """
303 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
304 + results = es_client.search(
305 + index="sap_siem_*",
306 + # index="new-integrations*",
307 + body={
308 + "size": 1000,
309 + "query": {"bool": {"must": [{"term": {"ip": suspicious_logins.ip}}]}},
310 + },
311 + )
312 + return SapSiemWazuhIndexerResponse(**results)
313 +
314 +
315 +async def get_initial_search_results(es_client):
316 + """
317 + Retrieves the initial search results from Elasticsearch.
318 +
319 + Args:
320 + es_client (Elasticsearch): The Elasticsearch client.
321 +
322 + Returns:
323 + dict: The search results.
324 + """
325 + return es_client.search(
326 + index="sap_siem_*",
327 + # index="new-integrations*",
328 + body={
329 + "size": 1000,
330 + "query": {"bool": {"must": [{"term": {"event_analyzed_brute_force_same_ip": "False"}}]}},
331 + "sort": [{"event_timestamp": {"order": "asc"}}],
332 + },
333 + scroll="1m",
334 + )
335 +
336 +
337 +async def get_next_batch_of_results(es_client, scroll_id):
338 + """
339 + Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
340 +
341 + Args:
342 + es_client (Elasticsearch): The Elasticsearch client.
343 + scroll_id (str): The scroll ID to retrieve the next batch of results.
344 +
345 + Returns:
346 + dict: The next batch of results.
347 + """
348 + return es_client.scroll(scroll_id=scroll_id, scroll="1m")
349 +
350 +
351 +async def process_hits(hits, ip_to_login_ids, suspicious_activity, time_range):
352 + """
353 + Process the hits received from SAP SIEM and update the IP to login IDs mapping and suspicious activity.
354 +
355 + Args:
356 + hits (list): List of hits received from SAP SIEM.
357 + ip_to_login_ids (dict): Dictionary mapping IP addresses to login IDs.
358 + suspicious_activity (dict): Dictionary mapping IP addresses to a list of suspicious login objects.
359 +
360 + Returns:
361 + None
362 + """
363 + ip_to_login_ids = defaultdict(lambda: defaultdict(list))
364 +
365 + for hit in hits:
366 + # Convert loginID to lowercase before comparing
367 + login_id = hit.source.params_loginID.lower()
368 + ip = hit.source.ip
369 + errCode = hit.source.errCode
370 +
371 + # Ignore loginID if it does not contain a '@'
372 + if "@" not in login_id:
373 + logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
374 + continue
375 +
376 + # Parse the event timestamp
377 + event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
378 +
379 + # Add the timestamp and errCode to the list for this IP for this loginID
380 + ip_to_login_ids[ip][login_id].append((event_timestamp, errCode))
381 +
382 + logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
383 +
384 + # Check if there are at least 10 different loginIDs for the same IP within the last 5 minutes
385 + login_ids_in_last_5_minutes = set()
386 + for other_login_id, other_info in ip_to_login_ids[ip].items():
387 + for other_timestamp, other_errCode in other_info:
388 + if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
389 + login_ids_in_last_5_minutes.add((other_login_id, other_errCode))
390 +
391 + # If there are at least 10 different failed loginIDs, log the suspicious activity
392 + failed_login_ids = [login_id for login_id, errCode in login_ids_in_last_5_minutes if errCode != "0"]
393 + logger.info(f"Failed loginIDs: {failed_login_ids}")
394 + if len(failed_login_ids) >= 10:
395 + logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
396 + suspicious_login = SuspiciousLogin(
397 + _index=hit.index,
398 + _id=hit.id,
399 + customer_code=hit.source.customer_code,
400 + logSource=hit.source.logSource,
401 + loginID=hit.source.params_loginID,
402 + country=hit.source.httpReq_country,
403 + ip=hit.source.ip,
404 + event_timestamp=hit.source.event_timestamp,
405 + errMessage=hit.source.errMessage,
406 + errDetails=hit.source.errDetails,
407 + )
408 + suspicious_activity[ip].append(suspicious_login)
409 + logger.info(f"Added suspicious login: {suspicious_login}")
410 +
411 +
412 +async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
413 + """
414 + Checks for multiple successful logins by IP address.
415 +
416 + Args:
417 + threshold (int): The minimum number of logins required to be considered suspicious.
418 +
419 + Returns:
420 + List[SuspiciousLogin]: A list of suspicious login objects.
421 + """
422 + ip_to_login_ids = defaultdict(set)
423 + suspicious_activity = defaultdict(list)
424 +
425 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
426 + scroll_id = None
427 +
428 + while True:
429 + if scroll_id is None:
430 + results = await get_initial_search_results(es_client)
431 + else:
432 + results = await get_next_batch_of_results(es_client, scroll_id)
433 +
434 + if not results["hits"]["hits"]:
435 + break
436 +
437 + results = SapSiemWazuhIndexerResponse(**results)
438 + await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
439 +
440 + scroll_id = results.scroll_id
441 +
442 + # Clear the scroll when you're done to free up resources
443 + if scroll_id is not None:
444 + es_client.clear_scroll(scroll_id=scroll_id)
445 +
446 + logger.info(f"Suspicious activity: {suspicious_activity}")
447 + suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
448 +
449 + return [login for sublist in suspicious_activity.values() for login in sublist]
450 +
451 +
452 +async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
453 + """
454 + Retrieves a list of suspicious login attempts based on the specified threshold.
455 +
456 + Args:
457 + threshold (int): The number of successful logins from the same IP address that is considered suspicious.
458 +
459 + Returns:
460 + List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
461 + """
462 + return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
463 +
464 +
465 +async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
466 + """
467 + Retrieves an existing database record for the given IP address.
468 +
469 + Args:
470 + session (AsyncSession): The async session object for database operations.
471 + ip (str): The IP address to search for.
472 +
473 + Returns:
474 + SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
475 + """
476 + result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
477 + return result.scalar_one_or_none() if result is not None else None
478 +
479 +
480 +def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
481 + """
482 + Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
483 +
484 + Args:
485 + existing_case (SapSiemMultipleLogins): The existing database record to be updated.
486 + new_login_ids (Set[str]): The new login IDs to be added to the existing record.
487 +
488 + Returns:
489 + None
490 + """
491 + existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
492 + if not new_login_ids.issubset(existing_loginIDs):
493 + updated_login_ids = existing_loginIDs.union(new_login_ids)
494 + existing_case.associated_loginIDs = ",".join(updated_login_ids)
495 + existing_case.last_case_created_timestamp = datetime.now()
496 +
497 +
498 +def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
499 + """
500 + Creates a new database record for SAP SIEM multiple logins.
501 +
502 + Args:
503 + ip (str): The IP address associated with the multiple logins.
504 + new_login_ids (Set[str]): The set of new login IDs.
505 +
506 + Returns:
507 + SapSiemMultipleLogins: The newly created database record.
508 + """
509 + return SapSiemMultipleLogins(
510 + ip=ip,
511 + last_case_created_timestamp=datetime.now(),
512 + associated_loginIDs=",".join(new_login_ids),
513 + )
514 +
515 +
516 +async def sap_siem_brute_force_failed_same_ip(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
517 + """
518 + Finds same IP with multiple users and handles suspicious logins.
519 +
520 + Args:
521 + threshold (int): The threshold value for determining suspicious logins.
522 + session (AsyncSession): The database session.
523 +
524 + Returns:
525 + InvokeSAPSiemResponse: The response indicating the success of the operation.
526 + """
527 + logger.info("Finding same user from different IP addresses")
528 +
529 + suspicious_ips = await get_suspicious_ips(threshold, time_range)
530 + logger.info(f"Suspicious IPs: {suspicious_ips}")
531 +
532 + unique_instances = set()
533 + case_ids = []
534 + # Dictionary to aggregate suspicious logins by IP
535 + aggregated_logins_by_ip = defaultdict(list)
536 +
537 + for suspicious_login in suspicious_ips:
538 + aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
539 +
540 + for ip, associated_logins in aggregated_logins_by_ip.items():
541 + logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
542 + if session is not None:
543 + existing_case = await get_existing_database_record(session, ip)
544 +
545 + new_login_ids = {login.loginID for login in associated_logins}
546 + if existing_case:
547 + logger.info(f"Updating existing database record: {existing_case}")
548 + update_existing_database_record(existing_case, new_login_ids)
549 + else:
550 + logger.info(f"Creating new case for IP: {ip}")
551 + new_case = create_new_database_record(ip, new_login_ids)
552 + session.add(new_case)
553 +
554 + # Create a single new IRIS case for this IP
555 + # Modify this to include information from all associated_logins
556 + await handle_suspicious_login_multiple(
557 + associated_logins[0],
558 + unique_instances,
559 + case_ids,
560 + session=session,
561 + )
562 + else:
563 + raise HTTPException(
564 + status_code=500,
565 + detail="Failed to create IRIS case",
566 + )
567 + await session.commit()
568 +
569 + # Clear the global set
570 + checked_ips.clear()
571 +
572 + return InvokeSAPSiemResponse(
573 + success=True,
574 + message="SAP SIEM multiple logins invoked.",
575 + )
backend/app/integrations/sap_siem/services/sap_siem_brute_forced_failed_logins.py new
+572
@@ -0,0 +1,572 @@
1 +from collections import defaultdict
2 +from datetime import datetime
3 +from datetime import timedelta
4 +from typing import List
5 +from typing import Set
6 +
7 +from fastapi import HTTPException
8 +from loguru import logger
9 +from sqlalchemy.ext.asyncio import AsyncSession
10 +from sqlalchemy.future import select
11 +
12 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 +from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 +from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 +from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 +from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 +from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 +from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 +from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 +from app.integrations.utils.alerts import send_to_shuffle
23 +from app.integrations.utils.schema import ShufflePayload
24 +from app.utils import get_customer_alert_settings
25 +
26 +# Global set to keep track of IPs that have already been checked
27 +checked_ips = set()
28 +
29 +
30 +async def handle_common_suspicious_login_tasks(
31 + suspicious_login,
32 + unique_instances,
33 + case_ids,
34 + create_case_fn,
35 + session: AsyncSession,
36 +):
37 + """
38 + Handles common tasks for suspicious logins.
39 +
40 + Args:
41 + suspicious_login: The suspicious login object.
42 + unique_instances: List of unique instances.
43 + case_ids: List of case IDs.
44 + create_case_fn: Function to create a case.
45 + session: The async session.
46 +
47 + Returns:
48 + None
49 + """
50 + case = await create_case_fn(suspicious_login, session)
51 + case_ids.append(case.data.case_id)
52 + user_activity = await collect_user_activity(suspicious_login)
53 + await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 + await mark_as_checked(suspicious_login)
55 + alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 + await send_to_shuffle(
57 + ShufflePayload(
58 + alert_id=case.data.case_id,
59 + customer=suspicious_login.customer_code,
60 + customer_code=suspicious_login.customer_code,
61 + alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 + rule_description=f"{case.data.case_name}",
63 + hostname=suspicious_login.ip,
64 + ),
65 + session=session,
66 + )
67 +
68 +
69 +async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
70 + """
71 + Handles suspicious login events with multiple logins.
72 +
73 + Args:
74 + suspicious_login: The suspicious login event.
75 + unique_instances: List of unique instances of the suspicious login event.
76 + case_ids: List of case IDs associated with the suspicious login event.
77 + session: The database session.
78 +
79 + Returns:
80 + None
81 + """
82 + await handle_common_suspicious_login_tasks(
83 + suspicious_login,
84 + unique_instances,
85 + case_ids,
86 + create_iris_case_multiple,
87 + session,
88 + )
89 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
90 +
91 +
92 +async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
93 + """
94 + Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
95 +
96 + :param suspicious_login: The suspicious login to update
97 +
98 + :return: None
99 + """
100 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
101 + try:
102 + es_client.update(
103 + index=index,
104 + id=id,
105 + body={
106 + "doc": {
107 + "event_analyzed_brute_force_ip": "True",
108 + },
109 + },
110 + )
111 + logger.info(f"Updated event_analyzed_brute_force_ip flag for suspicious login: {id}")
112 + except Exception as e:
113 + logger.error(
114 + f"Failed to update case created flag {e}",
115 + )
116 + # Attempt to remove read-only block
117 + try:
118 + es_client.indices.put_settings(
119 + index=index,
120 + body={"index.blocks.write": None},
121 + )
122 + logger.info(
123 + f"Removed read-only block from index {index}. Retrying update.",
124 + )
125 +
126 + # Retry the update operation
127 + es_client.update(
128 + index=index,
129 + id=id,
130 + body={"doc": {"event_analyzed_brute_force_ip": "True"}},
131 + )
132 + logger.info(
133 + f"Added event_analyzed_brute_force_ip flag to index {index} for suspicious login: {id}",
134 + )
135 +
136 + # Reenable the write block
137 + es_client.indices.put_settings(
138 + index=index,
139 + body={"index.blocks.write": True},
140 + )
141 + except Exception as e2:
142 + logger.error(
143 + f"Failed to remove read-only block from index {index}: {e2}",
144 + )
145 + return False
146 +
147 +
148 +async def mark_as_checked(suspicious_login):
149 + """
150 + Marks a suspicious login as checked by adding it to the set of checked IPs.
151 +
152 + Args:
153 + suspicious_login (Login): The suspicious login object to mark as checked.
154 +
155 + Returns:
156 + None
157 + """
158 + checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
159 +
160 +
161 +async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
162 + """
163 + Handles user activity by processing each hit in the user_activity and performing the following steps:
164 + 1. Extracts relevant information from the hit.
165 + 2. Checks if the current activity is already present in the unique_instances set.
166 + 3. If not present, adds the user activity to the IRIS case.
167 + 4. Creates an asset payload using the current activity.
168 + 5. Updates the case with the asset payload.
169 + 6. Updates the event analyzed multiple logins flag for the hit.
170 + 7. Adds the current activity to the unique_instances set.
171 +
172 + Parameters:
173 + - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
174 + - unique_instances (set): A set containing unique instances of user activity.
175 + - case_id (str): The ID of the IRIS case.
176 +
177 + Returns:
178 + None
179 + """
180 + for hit in user_activity.hits.hits:
181 + current_activity = {
182 + "loginID": hit.source.params_loginID,
183 + "ip": hit.source.ip,
184 + "country": hit.source.httpReq_country,
185 + "errMessage": hit.source.errMessage,
186 + "event_timestamp": hit.source.event_timestamp,
187 + "customer_code": hit.source.customer_code,
188 + "errDetails": hit.source.errDetails,
189 + }
190 + current_activity_frozenset = frozenset(current_activity.items())
191 + if current_activity_frozenset not in unique_instances:
192 + logger.info(f"Adding user activity to IRIS case: {current_activity}")
193 + current_asset = SuspiciousLogin(**current_activity)
194 + asset_payload = create_asset_payload(asset=current_asset)
195 + logger.info(f"Asset Payload: {asset_payload}")
196 + await update_case_with_asset(case_id, asset_payload)
197 + await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
198 + unique_instances.add(current_activity_frozenset)
199 +
200 +
201 +def create_asset_payload(asset: SuspiciousLogin):
202 + """
203 + Create a payload for adding an asset based on a SuspiciousLogin object.
204 +
205 + Args:
206 + asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
207 +
208 + Returns:
209 + AddAssetModel: The payload for adding the asset.
210 +
211 + """
212 + if asset.errMessage == "OK":
213 + return AddAssetModel(
214 + name=asset.loginID,
215 + ip=asset.ip,
216 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
217 + asset_type=1,
218 + compromise_status=1,
219 + analysis_status=2,
220 + )
221 + return AddAssetModel(
222 + name=asset.loginID,
223 + ip=asset.ip,
224 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
225 + asset_type=1,
226 + analysis_status=2,
227 + )
228 +
229 +
230 +async def update_case_with_asset(case_id: str, asset_payload):
231 + """
232 + Update the case with the asset information.
233 +
234 + :param case_id: The ID of the case to update
235 + :param asset_payload: The payload to update the case with
236 +
237 + :return: None
238 + """
239 + logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
240 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
241 + return await fetch_and_validate_data(
242 + client,
243 + case_client.add_asset,
244 + cid=case_id,
245 + **asset_payload.to_dict(),
246 + )
247 +
248 +
249 +async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
250 + """
251 + Creates an IRIS case for multiple logins with the same IP address.
252 +
253 + Args:
254 + suspicious_login (SuspiciousLogin): The suspicious login information.
255 + session (AsyncSession): The async session for database operations.
256 +
257 + Returns:
258 + CaseResponse: The response containing the created case information.
259 + """
260 + logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
261 + case_name = f"Log Source: {suspicious_login.logSource} SAP SIEM. " f"25 or more login failures from different IP addresses."
262 +
263 + case_description = (
264 + f"Log Source: {suspicious_login.logSource}\n\n"
265 + f"IP Address: {suspicious_login.ip}\n\n"
266 + f"Country: {suspicious_login.country}\n\n"
267 + f"Timestamp: {suspicious_login.event_timestamp}"
268 + )
269 +
270 + case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
271 +
272 + payload = IrisCasePayload(
273 + case_name=case_name,
274 + case_description=case_description,
275 + case_customer=case_customer,
276 + case_classification=18,
277 + soc_id="1",
278 + create_customer=False,
279 + )
280 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
281 + result = await fetch_and_validate_data(
282 + client,
283 + case_client.add_case,
284 + **payload.to_dict(),
285 + )
286 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
287 +
288 + return CaseResponse(**result)
289 +
290 +
291 +async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
292 + """
293 + Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
294 + Collects a max of 1000 records.
295 +
296 + :param suspicious_logins: A list of suspicious logins
297 +
298 + :return: List of the user Activity collected from the sap_siem table
299 + """
300 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
301 + results = es_client.search(
302 + index="sap_siem_*",
303 + # index="new-integrations*",
304 + body={
305 + "size": 1000,
306 + "query": {"bool": {"must": [{"term": {"params_loginID": suspicious_logins.loginID}}]}},
307 + },
308 + )
309 + return SapSiemWazuhIndexerResponse(**results)
310 +
311 +
312 +async def get_initial_search_results(es_client):
313 + """
314 + Retrieves the initial search results from Elasticsearch.
315 +
316 + Args:
317 + es_client (Elasticsearch): The Elasticsearch client.
318 +
319 + Returns:
320 + dict: The search results.
321 + """
322 + return es_client.search(
323 + index="sap_siem_*",
324 + # index="new-integrations*",
325 + body={
326 + "size": 1000,
327 + "query": {"bool": {"must": [{"term": {"event_analyzed_brute_force_ip": "False"}}]}},
328 + "sort": [{"event_timestamp": {"order": "asc"}}],
329 + },
330 + scroll="1m",
331 + )
332 +
333 +
334 +async def get_next_batch_of_results(es_client, scroll_id):
335 + """
336 + Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
337 +
338 + Args:
339 + es_client (Elasticsearch): The Elasticsearch client.
340 + scroll_id (str): The scroll ID to retrieve the next batch of results.
341 +
342 + Returns:
343 + dict: The next batch of results.
344 + """
345 + return es_client.scroll(scroll_id=scroll_id, scroll="1m")
346 +
347 +
348 +async def process_hits(hits, login_id_to_ips, suspicious_activity, time_range):
349 + """
350 + Process the hits received from SAP SIEM and update the login ID to IPs mapping and suspicious activity.
351 +
352 + Args:
353 + hits (list): List of hits received from SAP SIEM.
354 + login_id_to_ips (dict): Dictionary mapping login IDs to IP addresses.
355 + suspicious_activity (dict): Dictionary mapping login IDs to a list of suspicious login objects.
356 +
357 + Returns:
358 + None
359 + """
360 + login_id_to_ips = defaultdict(lambda: defaultdict(list))
361 +
362 + for hit in hits:
363 + # Convert loginID to lowercase before comparing
364 + login_id = hit.source.params_loginID.lower()
365 + ip = hit.source.ip
366 + errCode = hit.source.errCode
367 +
368 + # Ignore loginID if it does not contain a '@'
369 + if "@" not in login_id:
370 + logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
371 + continue
372 +
373 + # Parse the event timestamp
374 + event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
375 +
376 + # Add the timestamp and errCode to the list for this loginID for this IP
377 + login_id_to_ips[login_id][ip].append((event_timestamp, errCode))
378 +
379 + logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
380 +
381 + # Check if there are at least 25 different IPs for the same loginID within the last 3 minutes
382 + ips_in_last_3_minutes = set()
383 + for other_ip, other_info in login_id_to_ips[login_id].items():
384 + for other_timestamp, other_errCode in other_info:
385 + if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
386 + ips_in_last_3_minutes.add((other_ip, other_errCode))
387 +
388 + # If there are at least 25 different failed IPs, log the suspicious activity
389 + failed_ips = [ip for ip, errCode in ips_in_last_3_minutes if errCode != "0"]
390 + logger.info(f"Failed IPs: {failed_ips}")
391 + if len(failed_ips) >= 25:
392 + logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
393 + suspicious_login = SuspiciousLogin(
394 + _index=hit.index,
395 + _id=hit.id,
396 + customer_code=hit.source.customer_code,
397 + logSource=hit.source.logSource,
398 + loginID=hit.source.params_loginID,
399 + country=hit.source.httpReq_country,
400 + ip=hit.source.ip,
401 + event_timestamp=hit.source.event_timestamp,
402 + errMessage=hit.source.errMessage,
403 + errDetails=hit.source.errDetails,
404 + )
405 + suspicious_activity[login_id].append(suspicious_login)
406 + logger.info(f"Added suspicious login: {suspicious_login}")
407 +
408 +
409 +async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
410 + """
411 + Checks for multiple successful logins by IP address.
412 +
413 + Args:
414 + threshold (int): The minimum number of logins required to be considered suspicious.
415 +
416 + Returns:
417 + List[SuspiciousLogin]: A list of suspicious login objects.
418 + """
419 + ip_to_login_ids = defaultdict(set)
420 + suspicious_activity = defaultdict(list)
421 +
422 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
423 + scroll_id = None
424 +
425 + while True:
426 + if scroll_id is None:
427 + results = await get_initial_search_results(es_client)
428 + else:
429 + results = await get_next_batch_of_results(es_client, scroll_id)
430 +
431 + if not results["hits"]["hits"]:
432 + break
433 +
434 + results = SapSiemWazuhIndexerResponse(**results)
435 + await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
436 +
437 + scroll_id = results.scroll_id
438 +
439 + # Clear the scroll when you're done to free up resources
440 + if scroll_id is not None:
441 + es_client.clear_scroll(scroll_id=scroll_id)
442 +
443 + logger.info(f"Suspicious activity: {suspicious_activity}")
444 + suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
445 +
446 + return [login for sublist in suspicious_activity.values() for login in sublist]
447 +
448 +
449 +async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
450 + """
451 + Retrieves a list of suspicious login attempts based on the specified threshold.
452 +
453 + Args:
454 + threshold (int): The number of successful logins from the same IP address that is considered suspicious.
455 +
456 + Returns:
457 + List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
458 + """
459 + return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
460 +
461 +
462 +async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
463 + """
464 + Retrieves an existing database record for the given IP address.
465 +
466 + Args:
467 + session (AsyncSession): The async session object for database operations.
468 + ip (str): The IP address to search for.
469 +
470 + Returns:
471 + SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
472 + """
473 + result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
474 + return result.scalar_one_or_none() if result is not None else None
475 +
476 +
477 +def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
478 + """
479 + Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
480 +
481 + Args:
482 + existing_case (SapSiemMultipleLogins): The existing database record to be updated.
483 + new_login_ids (Set[str]): The new login IDs to be added to the existing record.
484 +
485 + Returns:
486 + None
487 + """
488 + existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
489 + if not new_login_ids.issubset(existing_loginIDs):
490 + updated_login_ids = existing_loginIDs.union(new_login_ids)
491 + existing_case.associated_loginIDs = ",".join(updated_login_ids)
492 + existing_case.last_case_created_timestamp = datetime.now()
493 +
494 +
495 +def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
496 + """
497 + Creates a new database record for SAP SIEM multiple logins.
498 +
499 + Args:
500 + ip (str): The IP address associated with the multiple logins.
501 + new_login_ids (Set[str]): The set of new login IDs.
502 +
503 + Returns:
504 + SapSiemMultipleLogins: The newly created database record.
505 + """
506 + return SapSiemMultipleLogins(
507 + ip=ip,
508 + last_case_created_timestamp=datetime.now(),
509 + associated_loginIDs=",".join(new_login_ids),
510 + )
511 +
512 +
513 +async def sap_siem_brute_force_failed_multiple_ips(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
514 + """
515 + Finds same IP with multiple users and handles suspicious logins.
516 +
517 + Args:
518 + threshold (int): The threshold value for determining suspicious logins.
519 + session (AsyncSession): The database session.
520 +
521 + Returns:
522 + InvokeSAPSiemResponse: The response indicating the success of the operation.
523 + """
524 + logger.info("Finding same user from different IP addresses")
525 +
526 + suspicious_ips = await get_suspicious_ips(threshold, time_range)
527 + logger.info(f"Suspicious IPs: {suspicious_ips}")
528 +
529 + unique_instances = set()
530 + case_ids = []
531 + # Dictionary to aggregate suspicious logins by IP
532 + aggregated_logins_by_ip = defaultdict(list)
533 +
534 + for suspicious_login in suspicious_ips:
535 + aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
536 +
537 + for ip, associated_logins in aggregated_logins_by_ip.items():
538 + logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
539 + if session is not None:
540 + existing_case = await get_existing_database_record(session, ip)
541 +
542 + new_login_ids = {login.loginID for login in associated_logins}
543 + if existing_case:
544 + logger.info(f"Updating existing database record: {existing_case}")
545 + update_existing_database_record(existing_case, new_login_ids)
546 + else:
547 + logger.info(f"Creating new case for IP: {ip}")
548 + new_case = create_new_database_record(ip, new_login_ids)
549 + session.add(new_case)
550 +
551 + # Create a single new IRIS case for this IP
552 + # Modify this to include information from all associated_logins
553 + await handle_suspicious_login_multiple(
554 + associated_logins[0],
555 + unique_instances,
556 + case_ids,
557 + session=session,
558 + )
559 + else:
560 + raise HTTPException(
561 + status_code=500,
562 + detail="Failed to create IRIS case",
563 + )
564 + await session.commit()
565 +
566 + # Clear the global set
567 + checked_ips.clear()
568 +
569 + return InvokeSAPSiemResponse(
570 + success=True,
571 + message="SAP SIEM multiple logins invoked.",
572 + )
backend/app/integrations/sap_siem/services/sap_siem_failed_same_user_different_geo_location.py new
+577
@@ -0,0 +1,577 @@
1 +from collections import defaultdict
2 +from datetime import datetime
3 +from datetime import timedelta
4 +from typing import List
5 +from typing import Set
6 +
7 +from fastapi import HTTPException
8 +from loguru import logger
9 +from sqlalchemy.ext.asyncio import AsyncSession
10 +from sqlalchemy.future import select
11 +
12 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 +from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 +from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 +from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 +from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 +from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 +from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 +from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 +from app.integrations.utils.alerts import send_to_shuffle
23 +from app.integrations.utils.schema import ShufflePayload
24 +from app.utils import get_customer_alert_settings
25 +
26 +# Global set to keep track of IPs that have already been checked
27 +checked_ips = set()
28 +
29 +
30 +async def handle_common_suspicious_login_tasks(
31 + suspicious_login,
32 + unique_instances,
33 + case_ids,
34 + create_case_fn,
35 + session: AsyncSession,
36 +):
37 + """
38 + Handles common tasks for suspicious logins.
39 +
40 + Args:
41 + suspicious_login: The suspicious login object.
42 + unique_instances: List of unique instances.
43 + case_ids: List of case IDs.
44 + create_case_fn: Function to create a case.
45 + session: The async session.
46 +
47 + Returns:
48 + None
49 + """
50 + case = await create_case_fn(suspicious_login, session)
51 + case_ids.append(case.data.case_id)
52 + user_activity = await collect_user_activity(suspicious_login)
53 + await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 + await mark_as_checked(suspicious_login)
55 + alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 + await send_to_shuffle(
57 + ShufflePayload(
58 + alert_id=case.data.case_id,
59 + customer=suspicious_login.customer_code,
60 + customer_code=suspicious_login.customer_code,
61 + alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 + rule_description=f"{case.data.case_name}",
63 + hostname=suspicious_login.ip,
64 + ),
65 + session=session,
66 + )
67 +
68 +
69 +async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
70 + """
71 + Handles suspicious login events with multiple logins.
72 +
73 + Args:
74 + suspicious_login: The suspicious login event.
75 + unique_instances: List of unique instances of the suspicious login event.
76 + case_ids: List of case IDs associated with the suspicious login event.
77 + session: The database session.
78 +
79 + Returns:
80 + None
81 + """
82 + await handle_common_suspicious_login_tasks(
83 + suspicious_login,
84 + unique_instances,
85 + case_ids,
86 + create_iris_case_multiple,
87 + session,
88 + )
89 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
90 +
91 +
92 +async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
93 + """
94 + Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
95 +
96 + :param suspicious_login: The suspicious login to update
97 +
98 + :return: None
99 + """
100 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
101 + try:
102 + es_client.update(
103 + index=index,
104 + id=id,
105 + body={
106 + "doc": {
107 + "event_analyzed_same_user_failed_diff_geo": "True",
108 + },
109 + },
110 + )
111 + logger.info(f"Updated event_analyzed_same_user_failed_diff_geo flag for suspicious login: {id}")
112 + except Exception as e:
113 + logger.error(
114 + f"Failed to update case created flag {e}",
115 + )
116 + # Attempt to remove read-only block
117 + try:
118 + es_client.indices.put_settings(
119 + index=index,
120 + body={"index.blocks.write": None},
121 + )
122 + logger.info(
123 + f"Removed read-only block from index {index}. Retrying update.",
124 + )
125 +
126 + # Retry the update operation
127 + es_client.update(
128 + index=index,
129 + id=id,
130 + body={"doc": {"event_analyzed_same_user_failed_diff_geo": "True"}},
131 + )
132 + logger.info(
133 + f"Added event_analyzed_same_user_failed_diff_geo flag to index {index} for suspicious login: {id}",
134 + )
135 +
136 + # Reenable the write block
137 + es_client.indices.put_settings(
138 + index=index,
139 + body={"index.blocks.write": True},
140 + )
141 + except Exception as e2:
142 + logger.error(
143 + f"Failed to remove read-only block from index {index}: {e2}",
144 + )
145 + return False
146 +
147 +
148 +async def mark_as_checked(suspicious_login):
149 + """
150 + Marks a suspicious login as checked by adding it to the set of checked IPs.
151 +
152 + Args:
153 + suspicious_login (Login): The suspicious login object to mark as checked.
154 +
155 + Returns:
156 + None
157 + """
158 + checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
159 +
160 +
161 +async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
162 + """
163 + Handles user activity by processing each hit in the user_activity and performing the following steps:
164 + 1. Extracts relevant information from the hit.
165 + 2. Checks if the current activity is already present in the unique_instances set.
166 + 3. If not present, adds the user activity to the IRIS case.
167 + 4. Creates an asset payload using the current activity.
168 + 5. Updates the case with the asset payload.
169 + 6. Updates the event analyzed multiple logins flag for the hit.
170 + 7. Adds the current activity to the unique_instances set.
171 +
172 + Parameters:
173 + - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
174 + - unique_instances (set): A set containing unique instances of user activity.
175 + - case_id (str): The ID of the IRIS case.
176 +
177 + Returns:
178 + None
179 + """
180 + for hit in user_activity.hits.hits:
181 + current_activity = {
182 + "loginID": hit.source.params_loginID,
183 + "ip": hit.source.ip,
184 + "country": hit.source.httpReq_country,
185 + "errMessage": hit.source.errMessage,
186 + "event_timestamp": hit.source.event_timestamp,
187 + "customer_code": hit.source.customer_code,
188 + "errDetails": hit.source.errDetails,
189 + }
190 + current_activity_frozenset = frozenset(current_activity.items())
191 + if current_activity_frozenset not in unique_instances:
192 + logger.info(f"Adding user activity to IRIS case: {current_activity}")
193 + current_asset = SuspiciousLogin(**current_activity)
194 + asset_payload = create_asset_payload(asset=current_asset)
195 + logger.info(f"Asset Payload: {asset_payload}")
196 + await update_case_with_asset(case_id, asset_payload)
197 + await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
198 + unique_instances.add(current_activity_frozenset)
199 +
200 +
201 +def create_asset_payload(asset: SuspiciousLogin):
202 + """
203 + Create a payload for adding an asset based on a SuspiciousLogin object.
204 +
205 + Args:
206 + asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
207 +
208 + Returns:
209 + AddAssetModel: The payload for adding the asset.
210 +
211 + """
212 + if asset.errMessage == "OK":
213 + return AddAssetModel(
214 + name=asset.loginID,
215 + ip=asset.ip,
216 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
217 + asset_type=1,
218 + compromise_status=1,
219 + analysis_status=2,
220 + )
221 + return AddAssetModel(
222 + name=asset.loginID,
223 + ip=asset.ip,
224 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
225 + asset_type=1,
226 + analysis_status=2,
227 + )
228 +
229 +
230 +async def update_case_with_asset(case_id: str, asset_payload):
231 + """
232 + Update the case with the asset information.
233 +
234 + :param case_id: The ID of the case to update
235 + :param asset_payload: The payload to update the case with
236 +
237 + :return: None
238 + """
239 + logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
240 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
241 + return await fetch_and_validate_data(
242 + client,
243 + case_client.add_asset,
244 + cid=case_id,
245 + **asset_payload.to_dict(),
246 + )
247 +
248 +
249 +async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
250 + """
251 + Creates an IRIS case for multiple logins with the same IP address.
252 +
253 + Args:
254 + suspicious_login (SuspiciousLogin): The suspicious login information.
255 + session (AsyncSession): The async session for database operations.
256 +
257 + Returns:
258 + CaseResponse: The response containing the created case information.
259 + """
260 + logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
261 + case_name = (
262 + f"Log Source: {suspicious_login.logSource} SAP SIEM. "
263 + f"User {suspicious_login.loginID} had multiple login failures from different GEO locations."
264 + )
265 +
266 + case_description = (
267 + f"Log Source: {suspicious_login.logSource}\n\n"
268 + f"IP Address: {suspicious_login.ip}\n\n"
269 + f"Country: {suspicious_login.country}\n\n"
270 + f"Timestamp: {suspicious_login.event_timestamp}"
271 + )
272 +
273 + case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
274 +
275 + payload = IrisCasePayload(
276 + case_name=case_name,
277 + case_description=case_description,
278 + case_customer=case_customer,
279 + case_classification=18,
280 + soc_id="1",
281 + create_customer=False,
282 + )
283 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
284 + result = await fetch_and_validate_data(
285 + client,
286 + case_client.add_case,
287 + **payload.to_dict(),
288 + )
289 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
290 +
291 + return CaseResponse(**result)
292 +
293 +
294 +async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
295 + """
296 + Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
297 + Collects a max of 1000 records.
298 +
299 + :param suspicious_logins: A list of suspicious logins
300 +
301 + :return: List of the user Activity collected from the sap_siem table
302 + """
303 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
304 + results = es_client.search(
305 + index="sap_siem_*",
306 + # index="new-integrations*",
307 + body={
308 + "size": 1000,
309 + "query": {"bool": {"must": [{"term": {"params_loginID": suspicious_logins.loginID}}]}},
310 + },
311 + )
312 + return SapSiemWazuhIndexerResponse(**results)
313 +
314 +
315 +async def get_initial_search_results(es_client):
316 + """
317 + Retrieves the initial search results from Elasticsearch.
318 +
319 + Args:
320 + es_client (Elasticsearch): The Elasticsearch client.
321 +
322 + Returns:
323 + dict: The search results.
324 + """
325 + return es_client.search(
326 + index="sap_siem_*",
327 + # index="new-integrations*",
328 + body={
329 + "size": 1000,
330 + "query": {"bool": {"must": [{"term": {"event_analyzed_same_user_failed_diff_geo": "False"}}]}},
331 + "sort": [{"event_timestamp": {"order": "asc"}}],
332 + },
333 + scroll="1m",
334 + )
335 +
336 +
337 +async def get_next_batch_of_results(es_client, scroll_id):
338 + """
339 + Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
340 +
341 + Args:
342 + es_client (Elasticsearch): The Elasticsearch client.
343 + scroll_id (str): The scroll ID to retrieve the next batch of results.
344 +
345 + Returns:
346 + dict: The next batch of results.
347 + """
348 + return es_client.scroll(scroll_id=scroll_id, scroll="1m")
349 +
350 +
351 +async def process_hits(hits, login_id_to_ips, suspicious_activity, time_range):
352 + """
353 + Process the hits received from SAP SIEM and update the login ID to IPs mapping and suspicious activity.
354 +
355 + Args:
356 + hits (list): List of hits received from SAP SIEM.
357 + login_id_to_ips (dict): Dictionary mapping login IDs to IP addresses.
358 + suspicious_activity (dict): Dictionary mapping login IDs to a list of suspicious login objects.
359 +
360 + Returns:
361 + None
362 + """
363 + login_id_to_ips = defaultdict(lambda: defaultdict(list))
364 +
365 + for hit in hits:
366 + # Convert loginID to lowercase before comparing
367 + login_id = hit.source.params_loginID.lower()
368 + ip = hit.source.ip
369 + errCode = hit.source.errCode
370 + country = hit.source.httpReq_country
371 +
372 + # Ignore loginID if it does not contain a '@'
373 + if "@" not in login_id:
374 + logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
375 + continue
376 +
377 + # Parse the event timestamp
378 + event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
379 +
380 + # Add the timestamp, errCode and country to the list for this loginID for this IP
381 + login_id_to_ips[login_id][ip].append((event_timestamp, errCode, country))
382 +
383 + logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
384 +
385 + # Check if there are at least 3 different IPs for the same loginID within the last 20 minutes
386 + ips_in_last_20_minutes = set()
387 + for other_ip, other_info in login_id_to_ips[login_id].items():
388 + for other_timestamp, other_errCode, other_country in other_info:
389 + if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
390 + ips_in_last_20_minutes.add((other_ip, other_errCode, other_country))
391 +
392 + # If there are at least 3 different failed IPs from at least two different GEO IP country locations, log the suspicious activity
393 + failed_ips = [ip for ip, errCode, country in ips_in_last_20_minutes if errCode != "0"]
394 + countries = set([country for ip, errCode, country in ips_in_last_20_minutes if errCode != "0"])
395 + logger.info(f"Failed IPs: {failed_ips}")
396 + if len(failed_ips) >= 3 and len(countries) >= 2:
397 + logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
398 + suspicious_login = SuspiciousLogin(
399 + _index=hit.index,
400 + _id=hit.id,
401 + customer_code=hit.source.customer_code,
402 + logSource=hit.source.logSource,
403 + loginID=hit.source.params_loginID,
404 + country=hit.source.httpReq_country,
405 + ip=hit.source.ip,
406 + event_timestamp=hit.source.event_timestamp,
407 + errMessage=hit.source.errMessage,
408 + errDetails=hit.source.errDetails,
409 + )
410 + suspicious_activity[login_id].append(suspicious_login)
411 + logger.info(f"Added suspicious login: {suspicious_login}")
412 +
413 +
414 +async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
415 + """
416 + Checks for multiple successful logins by IP address.
417 +
418 + Args:
419 + threshold (int): The minimum number of logins required to be considered suspicious.
420 +
421 + Returns:
422 + List[SuspiciousLogin]: A list of suspicious login objects.
423 + """
424 + ip_to_login_ids = defaultdict(set)
425 + suspicious_activity = defaultdict(list)
426 +
427 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
428 + scroll_id = None
429 +
430 + while True:
431 + if scroll_id is None:
432 + results = await get_initial_search_results(es_client)
433 + else:
434 + results = await get_next_batch_of_results(es_client, scroll_id)
435 +
436 + if not results["hits"]["hits"]:
437 + break
438 +
439 + results = SapSiemWazuhIndexerResponse(**results)
440 + await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
441 +
442 + scroll_id = results.scroll_id
443 +
444 + # Clear the scroll when you're done to free up resources
445 + if scroll_id is not None:
446 + es_client.clear_scroll(scroll_id=scroll_id)
447 +
448 + logger.info(f"Suspicious activity: {suspicious_activity}")
449 + suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
450 +
451 + return [login for sublist in suspicious_activity.values() for login in sublist]
452 +
453 +
454 +async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
455 + """
456 + Retrieves a list of suspicious login attempts based on the specified threshold.
457 +
458 + Args:
459 + threshold (int): The number of successful logins from the same IP address that is considered suspicious.
460 +
461 + Returns:
462 + List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
463 + """
464 + return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
465 +
466 +
467 +async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
468 + """
469 + Retrieves an existing database record for the given IP address.
470 +
471 + Args:
472 + session (AsyncSession): The async session object for database operations.
473 + ip (str): The IP address to search for.
474 +
475 + Returns:
476 + SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
477 + """
478 + result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
479 + return result.scalar_one_or_none() if result is not None else None
480 +
481 +
482 +def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
483 + """
484 + Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
485 +
486 + Args:
487 + existing_case (SapSiemMultipleLogins): The existing database record to be updated.
488 + new_login_ids (Set[str]): The new login IDs to be added to the existing record.
489 +
490 + Returns:
491 + None
492 + """
493 + existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
494 + if not new_login_ids.issubset(existing_loginIDs):
495 + updated_login_ids = existing_loginIDs.union(new_login_ids)
496 + existing_case.associated_loginIDs = ",".join(updated_login_ids)
497 + existing_case.last_case_created_timestamp = datetime.now()
498 +
499 +
500 +def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
501 + """
502 + Creates a new database record for SAP SIEM multiple logins.
503 +
504 + Args:
505 + ip (str): The IP address associated with the multiple logins.
506 + new_login_ids (Set[str]): The set of new login IDs.
507 +
508 + Returns:
509 + SapSiemMultipleLogins: The newly created database record.
510 + """
511 + return SapSiemMultipleLogins(
512 + ip=ip,
513 + last_case_created_timestamp=datetime.now(),
514 + associated_loginIDs=",".join(new_login_ids),
515 + )
516 +
517 +
518 +async def sap_siem_failed_same_user_diff_geo(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
519 + """
520 + Finds same IP with multiple users and handles suspicious logins.
521 +
522 + Args:
523 + threshold (int): The threshold value for determining suspicious logins.
524 + session (AsyncSession): The database session.
525 +
526 + Returns:
527 + InvokeSAPSiemResponse: The response indicating the success of the operation.
528 + """
529 + logger.info("Finding same user from different IP addresses")
530 +
531 + suspicious_ips = await get_suspicious_ips(threshold, time_range)
532 + logger.info(f"Suspicious IPs: {suspicious_ips}")
533 +
534 + unique_instances = set()
535 + case_ids = []
536 + # Dictionary to aggregate suspicious logins by IP
537 + aggregated_logins_by_ip = defaultdict(list)
538 +
539 + for suspicious_login in suspicious_ips:
540 + aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
541 +
542 + for ip, associated_logins in aggregated_logins_by_ip.items():
543 + logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
544 + if session is not None:
545 + existing_case = await get_existing_database_record(session, ip)
546 +
547 + new_login_ids = {login.loginID for login in associated_logins}
548 + if existing_case:
549 + logger.info(f"Updating existing database record: {existing_case}")
550 + update_existing_database_record(existing_case, new_login_ids)
551 + else:
552 + logger.info(f"Creating new case for IP: {ip}")
553 + new_case = create_new_database_record(ip, new_login_ids)
554 + session.add(new_case)
555 +
556 + # Create a single new IRIS case for this IP
557 + # Modify this to include information from all associated_logins
558 + await handle_suspicious_login_multiple(
559 + associated_logins[0],
560 + unique_instances,
561 + case_ids,
562 + session=session,
563 + )
564 + else:
565 + raise HTTPException(
566 + status_code=500,
567 + detail="Failed to create IRIS case",
568 + )
569 + await session.commit()
570 +
571 + # Clear the global set
572 + checked_ips.clear()
573 +
574 + return InvokeSAPSiemResponse(
575 + success=True,
576 + message="SAP SIEM multiple logins invoked.",
577 + )
backend/app/integrations/sap_siem/services/sap_siem_failed_same_user_from_different_ip.py new
+575
@@ -0,0 +1,575 @@
1 +from collections import defaultdict
2 +from datetime import datetime
3 +from datetime import timedelta
4 +from typing import List
5 +from typing import Set
6 +
7 +from fastapi import HTTPException
8 +from loguru import logger
9 +from sqlalchemy.ext.asyncio import AsyncSession
10 +from sqlalchemy.future import select
11 +
12 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 +from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 +from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 +from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 +from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 +from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 +from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 +from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 +from app.integrations.utils.alerts import send_to_shuffle
23 +from app.integrations.utils.schema import ShufflePayload
24 +from app.utils import get_customer_alert_settings
25 +
26 +# Global set to keep track of IPs that have already been checked
27 +checked_ips = set()
28 +
29 +
30 +async def handle_common_suspicious_login_tasks(
31 + suspicious_login,
32 + unique_instances,
33 + case_ids,
34 + create_case_fn,
35 + session: AsyncSession,
36 +):
37 + """
38 + Handles common tasks for suspicious logins.
39 +
40 + Args:
41 + suspicious_login: The suspicious login object.
42 + unique_instances: List of unique instances.
43 + case_ids: List of case IDs.
44 + create_case_fn: Function to create a case.
45 + session: The async session.
46 +
47 + Returns:
48 + None
49 + """
50 + case = await create_case_fn(suspicious_login, session)
51 + case_ids.append(case.data.case_id)
52 + user_activity = await collect_user_activity(suspicious_login)
53 + await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 + await mark_as_checked(suspicious_login)
55 + alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 + await send_to_shuffle(
57 + ShufflePayload(
58 + alert_id=case.data.case_id,
59 + customer=suspicious_login.customer_code,
60 + customer_code=suspicious_login.customer_code,
61 + alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 + rule_description=f"{case.data.case_name}",
63 + hostname=suspicious_login.ip,
64 + ),
65 + session=session,
66 + )
67 +
68 +
69 +async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
70 + """
71 + Handles suspicious login events with multiple logins.
72 +
73 + Args:
74 + suspicious_login: The suspicious login event.
75 + unique_instances: List of unique instances of the suspicious login event.
76 + case_ids: List of case IDs associated with the suspicious login event.
77 + session: The database session.
78 +
79 + Returns:
80 + None
81 + """
82 + await handle_common_suspicious_login_tasks(
83 + suspicious_login,
84 + unique_instances,
85 + case_ids,
86 + create_iris_case_multiple,
87 + session,
88 + )
89 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
90 +
91 +
92 +async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
93 + """
94 + Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
95 +
96 + :param suspicious_login: The suspicious login to update
97 +
98 + :return: None
99 + """
100 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
101 + try:
102 + es_client.update(
103 + index=index,
104 + id=id,
105 + body={
106 + "doc": {
107 + "event_analyzed_same_user_failed_diff_ip": "True",
108 + },
109 + },
110 + )
111 + logger.info(f"Updated event_analyzed_same_user_failed_diff_ip flag for suspicious login: {id}")
112 + except Exception as e:
113 + logger.error(
114 + f"Failed to update case created flag {e}",
115 + )
116 + # Attempt to remove read-only block
117 + try:
118 + es_client.indices.put_settings(
119 + index=index,
120 + body={"index.blocks.write": None},
121 + )
122 + logger.info(
123 + f"Removed read-only block from index {index}. Retrying update.",
124 + )
125 +
126 + # Retry the update operation
127 + es_client.update(
128 + index=index,
129 + id=id,
130 + body={"doc": {"event_analyzed_same_user_failed_diff_ip": "True"}},
131 + )
132 + logger.info(
133 + f"Added event_analyzed_same_user_failed_diff_ip flag to index {index} for suspicious login: {id}",
134 + )
135 +
136 + # Reenable the write block
137 + es_client.indices.put_settings(
138 + index=index,
139 + body={"index.blocks.write": True},
140 + )
141 + except Exception as e2:
142 + logger.error(
143 + f"Failed to remove read-only block from index {index}: {e2}",
144 + )
145 + return False
146 +
147 +
148 +async def mark_as_checked(suspicious_login):
149 + """
150 + Marks a suspicious login as checked by adding it to the set of checked IPs.
151 +
152 + Args:
153 + suspicious_login (Login): The suspicious login object to mark as checked.
154 +
155 + Returns:
156 + None
157 + """
158 + checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
159 +
160 +
161 +async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
162 + """
163 + Handles user activity by processing each hit in the user_activity and performing the following steps:
164 + 1. Extracts relevant information from the hit.
165 + 2. Checks if the current activity is already present in the unique_instances set.
166 + 3. If not present, adds the user activity to the IRIS case.
167 + 4. Creates an asset payload using the current activity.
168 + 5. Updates the case with the asset payload.
169 + 6. Updates the event analyzed multiple logins flag for the hit.
170 + 7. Adds the current activity to the unique_instances set.
171 +
172 + Parameters:
173 + - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
174 + - unique_instances (set): A set containing unique instances of user activity.
175 + - case_id (str): The ID of the IRIS case.
176 +
177 + Returns:
178 + None
179 + """
180 + for hit in user_activity.hits.hits:
181 + current_activity = {
182 + "loginID": hit.source.params_loginID,
183 + "ip": hit.source.ip,
184 + "country": hit.source.httpReq_country,
185 + "errMessage": hit.source.errMessage,
186 + "event_timestamp": hit.source.event_timestamp,
187 + "customer_code": hit.source.customer_code,
188 + "errDetails": hit.source.errDetails,
189 + }
190 + current_activity_frozenset = frozenset(current_activity.items())
191 + if current_activity_frozenset not in unique_instances:
192 + logger.info(f"Adding user activity to IRIS case: {current_activity}")
193 + current_asset = SuspiciousLogin(**current_activity)
194 + asset_payload = create_asset_payload(asset=current_asset)
195 + logger.info(f"Asset Payload: {asset_payload}")
196 + await update_case_with_asset(case_id, asset_payload)
197 + await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
198 + unique_instances.add(current_activity_frozenset)
199 +
200 +
201 +def create_asset_payload(asset: SuspiciousLogin):
202 + """
203 + Create a payload for adding an asset based on a SuspiciousLogin object.
204 +
205 + Args:
206 + asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
207 +
208 + Returns:
209 + AddAssetModel: The payload for adding the asset.
210 +
211 + """
212 + if asset.errMessage == "OK":
213 + return AddAssetModel(
214 + name=asset.loginID,
215 + ip=asset.ip,
216 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
217 + asset_type=1,
218 + compromise_status=1,
219 + analysis_status=2,
220 + )
221 + return AddAssetModel(
222 + name=asset.loginID,
223 + ip=asset.ip,
224 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
225 + asset_type=1,
226 + analysis_status=2,
227 + )
228 +
229 +
230 +async def update_case_with_asset(case_id: str, asset_payload):
231 + """
232 + Update the case with the asset information.
233 +
234 + :param case_id: The ID of the case to update
235 + :param asset_payload: The payload to update the case with
236 +
237 + :return: None
238 + """
239 + logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
240 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
241 + return await fetch_and_validate_data(
242 + client,
243 + case_client.add_asset,
244 + cid=case_id,
245 + **asset_payload.to_dict(),
246 + )
247 +
248 +
249 +async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
250 + """
251 + Creates an IRIS case for multiple logins with the same IP address.
252 +
253 + Args:
254 + suspicious_login (SuspiciousLogin): The suspicious login information.
255 + session (AsyncSession): The async session for database operations.
256 +
257 + Returns:
258 + CaseResponse: The response containing the created case information.
259 + """
260 + logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
261 + case_name = (
262 + f"Log Source: {suspicious_login.logSource} SAP SIEM. "
263 + f"User {suspicious_login.loginID} had multiple login failures from different IP addresses."
264 + )
265 +
266 + case_description = (
267 + f"Log Source: {suspicious_login.logSource}\n\n"
268 + f"IP Address: {suspicious_login.ip}\n\n"
269 + f"Country: {suspicious_login.country}\n\n"
270 + f"Timestamp: {suspicious_login.event_timestamp}"
271 + )
272 +
273 + case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
274 +
275 + payload = IrisCasePayload(
276 + case_name=case_name,
277 + case_description=case_description,
278 + case_customer=case_customer,
279 + case_classification=18,
280 + soc_id="1",
281 + create_customer=False,
282 + )
283 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
284 + result = await fetch_and_validate_data(
285 + client,
286 + case_client.add_case,
287 + **payload.to_dict(),
288 + )
289 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
290 +
291 + return CaseResponse(**result)
292 +
293 +
294 +async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
295 + """
296 + Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
297 + Collects a max of 1000 records.
298 +
299 + :param suspicious_logins: A list of suspicious logins
300 +
301 + :return: List of the user Activity collected from the sap_siem table
302 + """
303 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
304 + results = es_client.search(
305 + index="sap_siem_*",
306 + # index="new-integrations*",
307 + body={
308 + "size": 1000,
309 + "query": {"bool": {"must": [{"term": {"params_loginID": suspicious_logins.loginID}}]}},
310 + },
311 + )
312 + return SapSiemWazuhIndexerResponse(**results)
313 +
314 +
315 +async def get_initial_search_results(es_client):
316 + """
317 + Retrieves the initial search results from Elasticsearch.
318 +
319 + Args:
320 + es_client (Elasticsearch): The Elasticsearch client.
321 +
322 + Returns:
323 + dict: The search results.
324 + """
325 + return es_client.search(
326 + index="sap_siem_*",
327 + # index="new-integrations*",
328 + body={
329 + "size": 1000,
330 + "query": {"bool": {"must": [{"term": {"event_analyzed_same_user_failed_diff_ip": "False"}}]}},
331 + "sort": [{"event_timestamp": {"order": "asc"}}],
332 + },
333 + scroll="1m",
334 + )
335 +
336 +
337 +async def get_next_batch_of_results(es_client, scroll_id):
338 + """
339 + Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
340 +
341 + Args:
342 + es_client (Elasticsearch): The Elasticsearch client.
343 + scroll_id (str): The scroll ID to retrieve the next batch of results.
344 +
345 + Returns:
346 + dict: The next batch of results.
347 + """
348 + return es_client.scroll(scroll_id=scroll_id, scroll="1m")
349 +
350 +
351 +async def process_hits(hits, login_id_to_ips, suspicious_activity, time_range):
352 + """
353 + Process the hits received from SAP SIEM and update the login ID to IPs mapping and suspicious activity.
354 +
355 + Args:
356 + hits (list): List of hits received from SAP SIEM.
357 + login_id_to_ips (dict): Dictionary mapping login IDs to IP addresses.
358 + suspicious_activity (dict): Dictionary mapping login IDs to a list of suspicious login objects.
359 +
360 + Returns:
361 + None
362 + """
363 + login_id_to_ips = defaultdict(lambda: defaultdict(list))
364 +
365 + for hit in hits:
366 + # Convert loginID to lowercase before comparing
367 + login_id = hit.source.params_loginID.lower()
368 + ip = hit.source.ip
369 + errCode = hit.source.errCode
370 +
371 + # Ignore loginID if it does not contain a '@'
372 + if "@" not in login_id:
373 + logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
374 + continue
375 +
376 + # Parse the event timestamp
377 + event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
378 +
379 + # Add the timestamp and errCode to the list for this loginID for this IP
380 + login_id_to_ips[login_id][ip].append((event_timestamp, errCode))
381 +
382 + logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
383 +
384 + # Check if there are at least 3 different IPs for the same loginID within the last 10 minutes
385 + ips_in_last_10_minutes = set()
386 + for other_ip, other_info in login_id_to_ips[login_id].items():
387 + for other_timestamp, other_errCode in other_info:
388 + if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
389 + ips_in_last_10_minutes.add((other_ip, other_errCode))
390 +
391 + # If there are at least 3 different failed IPs, log the suspicious activity
392 + failed_ips = [ip for ip, errCode in ips_in_last_10_minutes if errCode != "0"]
393 + logger.info(f"Failed IPs: {failed_ips}")
394 + if len(failed_ips) >= 3:
395 + logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
396 + suspicious_login = SuspiciousLogin(
397 + _index=hit.index,
398 + _id=hit.id,
399 + customer_code=hit.source.customer_code,
400 + logSource=hit.source.logSource,
401 + loginID=hit.source.params_loginID,
402 + country=hit.source.httpReq_country,
403 + ip=hit.source.ip,
404 + event_timestamp=hit.source.event_timestamp,
405 + errMessage=hit.source.errMessage,
406 + errDetails=hit.source.errDetails,
407 + )
408 + suspicious_activity[login_id].append(suspicious_login)
409 + logger.info(f"Added suspicious login: {suspicious_login}")
410 +
411 +
412 +async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
413 + """
414 + Checks for multiple successful logins by IP address.
415 +
416 + Args:
417 + threshold (int): The minimum number of logins required to be considered suspicious.
418 +
419 + Returns:
420 + List[SuspiciousLogin]: A list of suspicious login objects.
421 + """
422 + ip_to_login_ids = defaultdict(set)
423 + suspicious_activity = defaultdict(list)
424 +
425 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
426 + scroll_id = None
427 +
428 + while True:
429 + if scroll_id is None:
430 + results = await get_initial_search_results(es_client)
431 + else:
432 + results = await get_next_batch_of_results(es_client, scroll_id)
433 +
434 + if not results["hits"]["hits"]:
435 + break
436 +
437 + results = SapSiemWazuhIndexerResponse(**results)
438 + await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
439 +
440 + scroll_id = results.scroll_id
441 +
442 + # Clear the scroll when you're done to free up resources
443 + if scroll_id is not None:
444 + es_client.clear_scroll(scroll_id=scroll_id)
445 +
446 + logger.info(f"Suspicious activity: {suspicious_activity}")
447 + suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
448 +
449 + return [login for sublist in suspicious_activity.values() for login in sublist]
450 +
451 +
452 +async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
453 + """
454 + Retrieves a list of suspicious login attempts based on the specified threshold.
455 +
456 + Args:
457 + threshold (int): The number of successful logins from the same IP address that is considered suspicious.
458 +
459 + Returns:
460 + List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
461 + """
462 + return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
463 +
464 +
465 +async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
466 + """
467 + Retrieves an existing database record for the given IP address.
468 +
469 + Args:
470 + session (AsyncSession): The async session object for database operations.
471 + ip (str): The IP address to search for.
472 +
473 + Returns:
474 + SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
475 + """
476 + result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
477 + return result.scalar_one_or_none() if result is not None else None
478 +
479 +
480 +def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
481 + """
482 + Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
483 +
484 + Args:
485 + existing_case (SapSiemMultipleLogins): The existing database record to be updated.
486 + new_login_ids (Set[str]): The new login IDs to be added to the existing record.
487 +
488 + Returns:
489 + None
490 + """
491 + existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
492 + if not new_login_ids.issubset(existing_loginIDs):
493 + updated_login_ids = existing_loginIDs.union(new_login_ids)
494 + existing_case.associated_loginIDs = ",".join(updated_login_ids)
495 + existing_case.last_case_created_timestamp = datetime.now()
496 +
497 +
498 +def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
499 + """
500 + Creates a new database record for SAP SIEM multiple logins.
501 +
502 + Args:
503 + ip (str): The IP address associated with the multiple logins.
504 + new_login_ids (Set[str]): The set of new login IDs.
505 +
506 + Returns:
507 + SapSiemMultipleLogins: The newly created database record.
508 + """
509 + return SapSiemMultipleLogins(
510 + ip=ip,
511 + last_case_created_timestamp=datetime.now(),
512 + associated_loginIDs=",".join(new_login_ids),
513 + )
514 +
515 +
516 +async def sap_siem_failed_same_user_diff_ip(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
517 + """
518 + Finds same IP with multiple users and handles suspicious logins.
519 +
520 + Args:
521 + threshold (int): The threshold value for determining suspicious logins.
522 + session (AsyncSession): The database session.
523 +
524 + Returns:
525 + InvokeSAPSiemResponse: The response indicating the success of the operation.
526 + """
527 + logger.info("Finding same user from different IP addresses")
528 +
529 + suspicious_ips = await get_suspicious_ips(threshold, time_range)
530 + logger.info(f"Suspicious IPs: {suspicious_ips}")
531 +
532 + unique_instances = set()
533 + case_ids = []
534 + # Dictionary to aggregate suspicious logins by IP
535 + aggregated_logins_by_ip = defaultdict(list)
536 +
537 + for suspicious_login in suspicious_ips:
538 + aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
539 +
540 + for ip, associated_logins in aggregated_logins_by_ip.items():
541 + logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
542 + if session is not None:
543 + existing_case = await get_existing_database_record(session, ip)
544 +
545 + new_login_ids = {login.loginID for login in associated_logins}
546 + if existing_case:
547 + logger.info(f"Updating existing database record: {existing_case}")
548 + update_existing_database_record(existing_case, new_login_ids)
549 + else:
550 + logger.info(f"Creating new case for IP: {ip}")
551 + new_case = create_new_database_record(ip, new_login_ids)
552 + session.add(new_case)
553 +
554 + # Create a single new IRIS case for this IP
555 + # Modify this to include information from all associated_logins
556 + await handle_suspicious_login_multiple(
557 + associated_logins[0],
558 + unique_instances,
559 + case_ids,
560 + session=session,
561 + )
562 + else:
563 + raise HTTPException(
564 + status_code=500,
565 + detail="Failed to create IRIS case",
566 + )
567 + await session.commit()
568 +
569 + # Clear the global set
570 + checked_ips.clear()
571 +
572 + return InvokeSAPSiemResponse(
573 + success=True,
574 + message="SAP SIEM multiple logins invoked.",
575 + )
backend/app/integrations/sap_siem/services/sap_siem_multiple_logins.py
+77 -22
@@ -1,5 +1,6 @@
1 from collections import defaultdict
2 from datetime import datetime
3 +from datetime import timedelta
4 from typing import List
5 from typing import Set
6
@@ -301,6 +302,7 @@ async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWa
302 es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
303 results = es_client.search(
304 index="sap_siem_*",
305 + # index="new-integrations*",
306 body={
307 "size": 1000,
308 "query": {"bool": {"must": [{"term": {"ip": suspicious_logins.ip}}]}},
@@ -321,6 +323,7 @@ async def get_initial_search_results(es_client):
323 """
324 return es_client.search(
325 index="sap_siem_*",
326 + # index="new-integrations*",
327 body={
328 "size": 1000,
329 "query": {"bool": {"must": [{"term": {"errMessage": "OK"}}, {"term": {"event_analyzed_multiple_logins": "False"}}]}},
@@ -344,7 +347,7 @@ async def get_next_batch_of_results(es_client, scroll_id):
347 return es_client.scroll(scroll_id=scroll_id, scroll="1m")
348
349
347 -async def process_hits(hits, ip_to_login_ids, suspicious_activity):
350 +async def process_hits(hits, ip_to_login_ids, suspicious_activity, time_range):
351 """
352 Process the hits received from SAP SIEM and update the IP to login IDs mapping and suspicious activity.
353
@@ -356,29 +359,80 @@ async def process_hits(hits, ip_to_login_ids, suspicious_activity):
359 Returns:
360 None
361 """
362 + # for hit in hits:
363 + # if hit.source.errMessage == "OK":
364 + # # Convert loginID to lowercase before comparing
365 + # login_id = hit.source.params_loginID.lower()
366 + # ip_to_login_ids[hit.source.ip].add(login_id)
367 +
368 + # suspicious_login = SuspiciousLogin(
369 + # _index=hit.index,
370 + # _id=hit.id,
371 + # customer_code=hit.source.customer_code,
372 + # logSource=hit.source.logSource,
373 + # loginID=hit.source.params_loginID,
374 + # country=hit.source.httpReq_country,
375 + # ip=hit.source.ip,
376 + # event_timestamp=hit.source.event_timestamp,
377 + # errMessage=hit.source.errMessage,
378 + # errDetails=hit.source.errDetails,
379 + # )
380 +
381 + # suspicious_activity[hit.source.ip].append(suspicious_login)
382 + # Keep track of the timestamps for each loginID for each IP
383 + ip_to_login_timestamps = defaultdict(lambda: defaultdict(list))
384 +
385 for hit in hits:
386 if hit.source.errMessage == "OK":
387 + # logger.info(f"Processing hit: {hit}")
388 # Convert loginID to lowercase before comparing
389 login_id = hit.source.params_loginID.lower()
363 - ip_to_login_ids[hit.source.ip].add(login_id)
364 -
365 - suspicious_login = SuspiciousLogin(
366 - _index=hit.index,
367 - _id=hit.id,
368 - customer_code=hit.source.customer_code,
369 - logSource=hit.source.logSource,
370 - loginID=hit.source.params_loginID,
371 - country=hit.source.httpReq_country,
372 - ip=hit.source.ip,
373 - event_timestamp=hit.source.event_timestamp,
374 - errMessage=hit.source.errMessage,
375 - errDetails=hit.source.errDetails,
376 - )
390 + ip = hit.source.ip
391 +
392 + # Ignore loginID if it does not contain a '@'
393 + if "@" not in login_id:
394 + logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
395 + continue
396 +
397 + # Parse the event timestamp
398 + event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
399 +
400 + # Add the timestamp to the list for this loginID for this IP
401 + ip_to_login_timestamps[ip][login_id].append(event_timestamp)
402 +
403 + logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
404 +
405 + # Check if there's another loginID for the same IP within the last 10 minutes
406 + for other_login_id, timestamps in ip_to_login_timestamps[ip].items():
407 + if other_login_id != login_id:
408 + if any(
409 + event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp
410 + for other_timestamp in timestamps
411 + ):
412 + # Add the loginID to the set for this IP
413 + ip_to_login_ids[ip].add(login_id)
414 +
415 + logger.info(f"Detected multiple logins within 10 minutes for IP {ip}: {login_id} and {other_login_id}")
416 +
417 + suspicious_login = SuspiciousLogin(
418 + _index=hit.index,
419 + _id=hit.id,
420 + customer_code=hit.source.customer_code,
421 + logSource=hit.source.logSource,
422 + loginID=hit.source.params_loginID,
423 + country=hit.source.httpReq_country,
424 + ip=hit.source.ip,
425 + event_timestamp=hit.source.event_timestamp,
426 + errMessage=hit.source.errMessage,
427 + errDetails=hit.source.errDetails,
428 + )
429
378 - suspicious_activity[hit.source.ip].append(suspicious_login)
430 + suspicious_activity[ip].append(suspicious_login)
431 + logger.info(f"Added suspicious login: {suspicious_login}")
432 + break
433
434
381 -async def check_multiple_successful_logins_by_ip(threshold: int) -> List[SuspiciousLogin]:
435 +async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
436 """
437 Checks for multiple successful logins by IP address.
438
@@ -397,6 +451,7 @@ async def check_multiple_successful_logins_by_ip(threshold: int) -> List[Suspici
451 while True:
452 if scroll_id is None:
453 results = await get_initial_search_results(es_client)
454 + logger.info(f"Initial search results: {results}")
455 else:
456 results = await get_next_batch_of_results(es_client, scroll_id)
457
@@ -404,7 +459,7 @@ async def check_multiple_successful_logins_by_ip(threshold: int) -> List[Suspici
459 break
460
461 results = SapSiemWazuhIndexerResponse(**results)
407 - await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity)
462 + await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
463
464 scroll_id = results.scroll_id
465
@@ -417,7 +472,7 @@ async def check_multiple_successful_logins_by_ip(threshold: int) -> List[Suspici
472 return [login for sublist in suspicious_activity.values() for login in sublist]
473
474
420 -async def get_suspicious_ips(threshold: int) -> List[SuspiciousLogin]:
475 +async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
476 """
477 Retrieves a list of suspicious login attempts based on the specified threshold.
478
@@ -427,7 +482,7 @@ async def get_suspicious_ips(threshold: int) -> List[SuspiciousLogin]:
482 Returns:
483 List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
484 """
430 - return await check_multiple_successful_logins_by_ip(threshold=threshold)
485 + return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
486
487
488 async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
@@ -481,7 +536,7 @@ def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMulti
536 )
537
538
484 -async def sap_siem_multiple_logins_same_ip(threshold: int, session: AsyncSession) -> InvokeSAPSiemResponse:
539 +async def sap_siem_multiple_logins_same_ip(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
540 """
541 Finds same IP with multiple users and handles suspicious logins.
542
@@ -494,7 +549,7 @@ async def sap_siem_multiple_logins_same_ip(threshold: int, session: AsyncSession
549 """
550 logger.info("Finding same IP with multiple users")
551
497 - suspicious_ips = await get_suspicious_ips(threshold)
552 + suspicious_ips = await get_suspicious_ips(threshold, time_range)
553 logger.info(f"Suspicious IPs: {suspicious_ips}")
554
555 unique_instances = set()
backend/app/integrations/sap_siem/services/sap_siem_successful_login_same_ip_after_multiple_failures.py new
+593
@@ -0,0 +1,593 @@
1 +from collections import defaultdict
2 +from datetime import datetime
3 +from datetime import timedelta
4 +from typing import List
5 +from typing import Set
6 +
7 +from fastapi import HTTPException
8 +from loguru import logger
9 +from sqlalchemy.ext.asyncio import AsyncSession
10 +from sqlalchemy.future import select
11 +
12 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 +from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 +from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 +from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 +from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 +from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 +from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 +from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 +from app.integrations.utils.alerts import send_to_shuffle
23 +from app.integrations.utils.schema import ShufflePayload
24 +from app.utils import get_customer_alert_settings
25 +
26 +# Global set to keep track of IPs that have already been checked
27 +checked_ips = set()
28 +
29 +
30 +async def handle_common_suspicious_login_tasks(
31 + suspicious_login,
32 + unique_instances,
33 + case_ids,
34 + create_case_fn,
35 + session: AsyncSession,
36 +):
37 + """
38 + Handles common tasks for suspicious logins.
39 +
40 + Args:
41 + suspicious_login: The suspicious login object.
42 + unique_instances: List of unique instances.
43 + case_ids: List of case IDs.
44 + create_case_fn: Function to create a case.
45 + session: The async session.
46 +
47 + Returns:
48 + None
49 + """
50 + case = await create_case_fn(suspicious_login, session)
51 + case_ids.append(case.data.case_id)
52 + user_activity = await collect_user_activity(suspicious_login)
53 + await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 + await mark_as_checked(suspicious_login)
55 + alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 + await send_to_shuffle(
57 + ShufflePayload(
58 + alert_id=case.data.case_id,
59 + customer=suspicious_login.customer_code,
60 + customer_code=suspicious_login.customer_code,
61 + alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 + rule_description=f"{case.data.case_name}",
63 + hostname=suspicious_login.ip,
64 + ),
65 + session=session,
66 + )
67 +
68 +
69 +async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
70 + """
71 + Handles suspicious login events with multiple logins.
72 +
73 + Args:
74 + suspicious_login: The suspicious login event.
75 + unique_instances: List of unique instances of the suspicious login event.
76 + case_ids: List of case IDs associated with the suspicious login event.
77 + session: The database session.
78 +
79 + Returns:
80 + None
81 + """
82 + await handle_common_suspicious_login_tasks(
83 + suspicious_login,
84 + unique_instances,
85 + case_ids,
86 + create_iris_case_multiple,
87 + session,
88 + )
89 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
90 +
91 +
92 +async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
93 + """
94 + Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
95 +
96 + :param suspicious_login: The suspicious login to update
97 +
98 + :return: None
99 + """
100 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
101 + try:
102 + es_client.update(
103 + index=index,
104 + id=id,
105 + body={
106 + "doc": {
107 + "event_analyzed_successful_login_after_failures_diff_loginID": "True",
108 + },
109 + },
110 + )
111 + logger.info(f"Updated event_analyzed_successful_login_after_failures_diff_loginID flag for suspicious login: {id}")
112 + except Exception as e:
113 + logger.error(
114 + f"Failed to update case created flag {e}",
115 + )
116 + # Attempt to remove read-only block
117 + try:
118 + es_client.indices.put_settings(
119 + index=index,
120 + body={"index.blocks.write": None},
121 + )
122 + logger.info(
123 + f"Removed read-only block from index {index}. Retrying update.",
124 + )
125 +
126 + # Retry the update operation
127 + es_client.update(
128 + index=index,
129 + id=id,
130 + body={"doc": {"event_analyzed_successful_login_after_failures_diff_loginID": "True"}},
131 + )
132 + logger.info(
133 + f"Added event_analyzed_successful_login_after_failures_diff_loginID flag to index {index} for suspicious login: {id}",
134 + )
135 +
136 + # Reenable the write block
137 + es_client.indices.put_settings(
138 + index=index,
139 + body={"index.blocks.write": True},
140 + )
141 + except Exception as e2:
142 + logger.error(
143 + f"Failed to remove read-only block from index {index}: {e2}",
144 + )
145 + return False
146 +
147 +
148 +async def mark_as_checked(suspicious_login):
149 + """
150 + Marks a suspicious login as checked by adding it to the set of checked IPs.
151 +
152 + Args:
153 + suspicious_login (Login): The suspicious login object to mark as checked.
154 +
155 + Returns:
156 + None
157 + """
158 + checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
159 +
160 +
161 +async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
162 + """
163 + Handles user activity by processing each hit in the user_activity and performing the following steps:
164 + 1. Extracts relevant information from the hit.
165 + 2. Checks if the current activity is already present in the unique_instances set.
166 + 3. If not present, adds the user activity to the IRIS case.
167 + 4. Creates an asset payload using the current activity.
168 + 5. Updates the case with the asset payload.
169 + 6. Updates the event analyzed multiple logins flag for the hit.
170 + 7. Adds the current activity to the unique_instances set.
171 +
172 + Parameters:
173 + - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
174 + - unique_instances (set): A set containing unique instances of user activity.
175 + - case_id (str): The ID of the IRIS case.
176 +
177 + Returns:
178 + None
179 + """
180 + for hit in user_activity.hits.hits:
181 + current_activity = {
182 + "loginID": hit.source.params_loginID,
183 + "ip": hit.source.ip,
184 + "country": hit.source.httpReq_country,
185 + "errMessage": hit.source.errMessage,
186 + "event_timestamp": hit.source.event_timestamp,
187 + "customer_code": hit.source.customer_code,
188 + "errDetails": hit.source.errDetails,
189 + }
190 + current_activity_frozenset = frozenset(current_activity.items())
191 + if current_activity_frozenset not in unique_instances:
192 + logger.info(f"Adding user activity to IRIS case: {current_activity}")
193 + current_asset = SuspiciousLogin(**current_activity)
194 + asset_payload = create_asset_payload(asset=current_asset)
195 + logger.info(f"Asset Payload: {asset_payload}")
196 + await update_case_with_asset(case_id, asset_payload)
197 + await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
198 + unique_instances.add(current_activity_frozenset)
199 +
200 +
201 +def create_asset_payload(asset: SuspiciousLogin):
202 + """
203 + Create a payload for adding an asset based on a SuspiciousLogin object.
204 +
205 + Args:
206 + asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
207 +
208 + Returns:
209 + AddAssetModel: The payload for adding the asset.
210 +
211 + """
212 + if asset.errMessage == "OK":
213 + return AddAssetModel(
214 + name=asset.loginID,
215 + ip=asset.ip,
216 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
217 + asset_type=1,
218 + compromise_status=1,
219 + analysis_status=2,
220 + )
221 + return AddAssetModel(
222 + name=asset.loginID,
223 + ip=asset.ip,
224 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
225 + asset_type=1,
226 + analysis_status=2,
227 + )
228 +
229 +
230 +async def update_case_with_asset(case_id: str, asset_payload):
231 + """
232 + Update the case with the asset information.
233 +
234 + :param case_id: The ID of the case to update
235 + :param asset_payload: The payload to update the case with
236 +
237 + :return: None
238 + """
239 + logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
240 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
241 + return await fetch_and_validate_data(
242 + client,
243 + case_client.add_asset,
244 + cid=case_id,
245 + **asset_payload.to_dict(),
246 + )
247 +
248 +
249 +async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
250 + """
251 + Creates an IRIS case for multiple logins with the same IP address.
252 +
253 + Args:
254 + suspicious_login (SuspiciousLogin): The suspicious login information.
255 + session (AsyncSession): The async session for database operations.
256 +
257 + Returns:
258 + CaseResponse: The response containing the created case information.
259 + """
260 + logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
261 + case_name = (
262 + f"Log Source: {suspicious_login.logSource} SAP SIEM. "
263 + f"Succesful login after multiple failures from {suspicious_login.ip} using multiple user names."
264 + )
265 +
266 + case_description = (
267 + f"Log Source: {suspicious_login.logSource}\n\n"
268 + f"IP Address: {suspicious_login.ip}\n\n"
269 + f"Country: {suspicious_login.country}\n\n"
270 + f"Timestamp: {suspicious_login.event_timestamp}"
271 + )
272 +
273 + case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
274 +
275 + payload = IrisCasePayload(
276 + case_name=case_name,
277 + case_description=case_description,
278 + case_customer=case_customer,
279 + case_classification=18,
280 + soc_id="1",
281 + create_customer=False,
282 + )
283 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
284 + result = await fetch_and_validate_data(
285 + client,
286 + case_client.add_case,
287 + **payload.to_dict(),
288 + )
289 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
290 +
291 + return CaseResponse(**result)
292 +
293 +
294 +async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
295 + """
296 + Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
297 + Collects a max of 1000 records.
298 +
299 + :param suspicious_logins: A list of suspicious logins
300 +
301 + :return: List of the user Activity collected from the sap_siem table
302 + """
303 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
304 + results = es_client.search(
305 + index="sap_siem_*",
306 + # index="new-integrations*",
307 + body={
308 + "size": 1000,
309 + "query": {"bool": {"must": [{"term": {"ip": suspicious_logins.ip}}]}},
310 + },
311 + )
312 + return SapSiemWazuhIndexerResponse(**results)
313 +
314 +
315 +async def get_initial_search_results(es_client):
316 + """
317 + Retrieves the initial search results from Elasticsearch.
318 +
319 + Args:
320 + es_client (Elasticsearch): The Elasticsearch client.
321 +
322 + Returns:
323 + dict: The search results.
324 + """
325 + return es_client.search(
326 + index="sap_siem_*",
327 + # index="new-integrations*",
328 + body={
329 + "size": 1000,
330 + "query": {"bool": {"must": [{"term": {"event_analyzed_successful_login_after_failures_diff_loginID": "False"}}]}},
331 + "sort": [{"event_timestamp": {"order": "asc"}}],
332 + },
333 + scroll="1m",
334 + )
335 +
336 +
337 +async def get_next_batch_of_results(es_client, scroll_id):
338 + """
339 + Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
340 +
341 + Args:
342 + es_client (Elasticsearch): The Elasticsearch client.
343 + scroll_id (str): The scroll ID to retrieve the next batch of results.
344 +
345 + Returns:
346 + dict: The next batch of results.
347 + """
348 + return es_client.scroll(scroll_id=scroll_id, scroll="1m")
349 +
350 +
351 +async def process_hits(hits, ip_to_login_ids, suspicious_activity, time_range):
352 + """
353 + This function, `process_hits`, is designed to analyze a list of login attempts (or "hits") and identify suspicious activity based on certain criteria.
354 + It takes four arguments: `hits`, `ip_to_login_ids`, `suspicious_activity`, and `time_range`.
355 +
356 + Here's a simplified explanation of what it does:
357 +
358 + 1. It starts by creating a dictionary (`ip_to_login_ids`) that maps IP addresses to login IDs. Each login ID is associated with a list of timestamps and error codes.
359 +
360 + 2. It then loops through each login attempt in `hits`. For each attempt, it extracts the login ID, IP address, and error code. If the login ID doesn't contain a '@',
361 + it's ignored.
362 +
363 + 3. The function then checks if there are at least 3 different failed login attempts (identified by error codes not equal to "0") from the same IP address within the
364 + last 2 minutes. It also checks if there's at least one successful login attempt (identified by error code "0") from the same IP address after the 4th login attempt.
365 +
366 + 4. If these conditions are met, the function considers this as suspicious activity. It creates a `SuspiciousLogin` object with
367 + details about the suspicious login attempt and adds it to the `suspicious_activity` dictionary, which maps IP addresses to a list of suspicious login objects.
368 +
369 + For example, consider the following sequence of login attempts from the same IP address:
370 +
371 + - User1 fails to login at 12:00:00
372 + - User2 fails to login at 12:00:30
373 + - User3 fails to login at 12:01:00
374 + - User4 successfully logs in at 12:01:30
375 + - User5 successfully logs in at 12:02:00
376 +
377 + In this case, the function would identify the IP address as suspicious because there are 3 different failed login attempts within 2 minutes,
378 + followed by at least one successful login attempt.
379 + """
380 + ip_to_login_ids = defaultdict(lambda: defaultdict(list))
381 +
382 + for hit in hits:
383 + # Convert loginID to lowercase before comparing
384 + login_id = hit.source.params_loginID.lower()
385 + ip = hit.source.ip
386 + errCode = hit.source.errCode
387 +
388 + # Ignore loginID if it does not contain a '@'
389 + if "@" not in login_id:
390 + logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
391 + continue
392 +
393 + # Parse the event timestamp
394 + event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
395 +
396 + # Add the timestamp and errCode to the list for this IP for this loginID
397 + ip_to_login_ids[ip][login_id].append((event_timestamp, errCode))
398 +
399 + logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
400 +
401 + # Check if there are at least 3 different loginIDs for the same IP within the last 2 minutes
402 + login_ids_in_last_2_minutes = set()
403 + for other_login_id, other_info in ip_to_login_ids[ip].items():
404 + for other_timestamp, other_errCode in other_info:
405 + if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
406 + login_ids_in_last_2_minutes.add((other_login_id, other_errCode))
407 +
408 + # If there are at least 3 different failed loginIDs and at least one successful login after the 4th login, log the suspicious activity
409 + failed_login_ids = [login_id for login_id, errCode in login_ids_in_last_2_minutes if errCode != "0"]
410 + successful_login_ids = [login_id for login_id, errCode in login_ids_in_last_2_minutes if errCode == "0"]
411 + logger.info(f"Failed loginIDs: {failed_login_ids}, Successful loginIDs: {successful_login_ids}")
412 + if len(failed_login_ids) >= 3 and len(successful_login_ids) >= 1:
413 + logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
414 + suspicious_login = SuspiciousLogin(
415 + _index=hit.index,
416 + _id=hit.id,
417 + customer_code=hit.source.customer_code,
418 + logSource=hit.source.logSource,
419 + loginID=hit.source.params_loginID,
420 + country=hit.source.httpReq_country,
421 + ip=hit.source.ip,
422 + event_timestamp=hit.source.event_timestamp,
423 + errMessage=hit.source.errMessage,
424 + errDetails=hit.source.errDetails,
425 + )
426 + suspicious_activity[ip].append(suspicious_login)
427 + logger.info(f"Added suspicious login: {suspicious_login}")
428 +
429 +
430 +async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
431 + """
432 + Checks for multiple successful logins by IP address.
433 +
434 + Args:
435 + threshold (int): The minimum number of logins required to be considered suspicious.
436 +
437 + Returns:
438 + List[SuspiciousLogin]: A list of suspicious login objects.
439 + """
440 + ip_to_login_ids = defaultdict(set)
441 + suspicious_activity = defaultdict(list)
442 +
443 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
444 + scroll_id = None
445 +
446 + while True:
447 + if scroll_id is None:
448 + results = await get_initial_search_results(es_client)
449 + else:
450 + results = await get_next_batch_of_results(es_client, scroll_id)
451 +
452 + if not results["hits"]["hits"]:
453 + break
454 +
455 + results = SapSiemWazuhIndexerResponse(**results)
456 + await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
457 +
458 + scroll_id = results.scroll_id
459 +
460 + # Clear the scroll when you're done to free up resources
461 + if scroll_id is not None:
462 + es_client.clear_scroll(scroll_id=scroll_id)
463 +
464 + logger.info(f"Suspicious activity: {suspicious_activity}")
465 + suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
466 +
467 + return [login for sublist in suspicious_activity.values() for login in sublist]
468 +
469 +
470 +async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
471 + """
472 + Retrieves a list of suspicious login attempts based on the specified threshold.
473 +
474 + Args:
475 + threshold (int): The number of successful logins from the same IP address that is considered suspicious.
476 +
477 + Returns:
478 + List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
479 + """
480 + return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
481 +
482 +
483 +async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
484 + """
485 + Retrieves an existing database record for the given IP address.
486 +
487 + Args:
488 + session (AsyncSession): The async session object for database operations.
489 + ip (str): The IP address to search for.
490 +
491 + Returns:
492 + SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
493 + """
494 + result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
495 + return result.scalar_one_or_none() if result is not None else None
496 +
497 +
498 +def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
499 + """
500 + Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
501 +
502 + Args:
503 + existing_case (SapSiemMultipleLogins): The existing database record to be updated.
504 + new_login_ids (Set[str]): The new login IDs to be added to the existing record.
505 +
506 + Returns:
507 + None
508 + """
509 + existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
510 + if not new_login_ids.issubset(existing_loginIDs):
511 + updated_login_ids = existing_loginIDs.union(new_login_ids)
512 + existing_case.associated_loginIDs = ",".join(updated_login_ids)
513 + existing_case.last_case_created_timestamp = datetime.now()
514 +
515 +
516 +def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
517 + """
518 + Creates a new database record for SAP SIEM multiple logins.
519 +
520 + Args:
521 + ip (str): The IP address associated with the multiple logins.
522 + new_login_ids (Set[str]): The set of new login IDs.
523 +
524 + Returns:
525 + SapSiemMultipleLogins: The newly created database record.
526 + """
527 + return SapSiemMultipleLogins(
528 + ip=ip,
529 + last_case_created_timestamp=datetime.now(),
530 + associated_loginIDs=",".join(new_login_ids),
531 + )
532 +
533 +
534 +async def sap_siem_successful_login_after_failures(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
535 + """
536 + Finds same IP with multiple users and handles suspicious logins.
537 +
538 + Args:
539 + threshold (int): The threshold value for determining suspicious logins.
540 + session (AsyncSession): The database session.
541 +
542 + Returns:
543 + InvokeSAPSiemResponse: The response indicating the success of the operation.
544 + """
545 + logger.info("Finding same user from different IP addresses")
546 +
547 + suspicious_ips = await get_suspicious_ips(threshold, time_range)
548 + logger.info(f"Suspicious IPs: {suspicious_ips}")
549 +
550 + unique_instances = set()
551 + case_ids = []
552 + # Dictionary to aggregate suspicious logins by IP
553 + aggregated_logins_by_ip = defaultdict(list)
554 +
555 + for suspicious_login in suspicious_ips:
556 + aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
557 +
558 + for ip, associated_logins in aggregated_logins_by_ip.items():
559 + logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
560 + if session is not None:
561 + existing_case = await get_existing_database_record(session, ip)
562 +
563 + new_login_ids = {login.loginID for login in associated_logins}
564 + if existing_case:
565 + logger.info(f"Updating existing database record: {existing_case}")
566 + update_existing_database_record(existing_case, new_login_ids)
567 + else:
568 + logger.info(f"Creating new case for IP: {ip}")
569 + new_case = create_new_database_record(ip, new_login_ids)
570 + session.add(new_case)
571 +
572 + # Create a single new IRIS case for this IP
573 + # Modify this to include information from all associated_logins
574 + await handle_suspicious_login_multiple(
575 + associated_logins[0],
576 + unique_instances,
577 + case_ids,
578 + session=session,
579 + )
580 + else:
581 + raise HTTPException(
582 + status_code=500,
583 + detail="Failed to create IRIS case",
584 + )
585 + await session.commit()
586 +
587 + # Clear the global set
588 + checked_ips.clear()
589 +
590 + return InvokeSAPSiemResponse(
591 + success=True,
592 + message="SAP SIEM multiple logins invoked.",
593 + )
backend/app/integrations/sap_siem/services/sap_siem_successful_same_user_different_geo_location.py new
+591
@@ -0,0 +1,591 @@
1 +from collections import defaultdict
2 +from datetime import datetime
3 +from datetime import timedelta
4 +from typing import List
5 +from typing import Set
6 +
7 +from fastapi import HTTPException
8 +from loguru import logger
9 +from sqlalchemy.ext.asyncio import AsyncSession
10 +from sqlalchemy.future import select
11 +
12 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 +from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 +from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 +from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 +from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 +from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 +from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 +from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 +from app.integrations.utils.alerts import send_to_shuffle
23 +from app.integrations.utils.schema import ShufflePayload
24 +from app.utils import get_customer_alert_settings
25 +
26 +# Global set to keep track of IPs that have already been checked
27 +checked_ips = set()
28 +
29 +
30 +async def handle_common_suspicious_login_tasks(
31 + suspicious_login,
32 + unique_instances,
33 + case_ids,
34 + create_case_fn,
35 + session: AsyncSession,
36 +):
37 + """
38 + Handles common tasks for suspicious logins.
39 +
40 + Args:
41 + suspicious_login: The suspicious login object.
42 + unique_instances: List of unique instances.
43 + case_ids: List of case IDs.
44 + create_case_fn: Function to create a case.
45 + session: The async session.
46 +
47 + Returns:
48 + None
49 + """
50 + case = await create_case_fn(suspicious_login, session)
51 + case_ids.append(case.data.case_id)
52 + user_activity = await collect_user_activity(suspicious_login)
53 + await handle_user_activity(user_activity, unique_instances, case.data.case_id)
54 + await mark_as_checked(suspicious_login)
55 + alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
56 + await send_to_shuffle(
57 + ShufflePayload(
58 + alert_id=case.data.case_id,
59 + customer=suspicious_login.customer_code,
60 + customer_code=suspicious_login.customer_code,
61 + alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
62 + rule_description=f"{case.data.case_name}",
63 + hostname=suspicious_login.ip,
64 + ),
65 + session=session,
66 + )
67 +
68 +
69 +async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
70 + """
71 + Handles suspicious login events with multiple logins.
72 +
73 + Args:
74 + suspicious_login: The suspicious login event.
75 + unique_instances: List of unique instances of the suspicious login event.
76 + case_ids: List of case IDs associated with the suspicious login event.
77 + session: The database session.
78 +
79 + Returns:
80 + None
81 + """
82 + await handle_common_suspicious_login_tasks(
83 + suspicious_login,
84 + unique_instances,
85 + case_ids,
86 + create_iris_case_multiple,
87 + session,
88 + )
89 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
90 +
91 +
92 +async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
93 + """
94 + Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
95 +
96 + :param suspicious_login: The suspicious login to update
97 +
98 + :return: None
99 + """
100 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
101 + try:
102 + es_client.update(
103 + index=index,
104 + id=id,
105 + body={
106 + "doc": {
107 + "event_analyzed_same_user_successful_diff_geo": "True",
108 + },
109 + },
110 + )
111 + logger.info(f"Updated event_analyzed_same_user_successful_diff_geo flag for suspicious login: {id}")
112 + except Exception as e:
113 + logger.error(
114 + f"Failed to update case created flag {e}",
115 + )
116 + # Attempt to remove read-only block
117 + try:
118 + es_client.indices.put_settings(
119 + index=index,
120 + body={"index.blocks.write": None},
121 + )
122 + logger.info(
123 + f"Removed read-only block from index {index}. Retrying update.",
124 + )
125 +
126 + # Retry the update operation
127 + es_client.update(
128 + index=index,
129 + id=id,
130 + body={"doc": {"event_analyzed_same_user_successful_diff_geo": "True"}},
131 + )
132 + logger.info(
133 + f"Added event_analyzed_same_user_successful_diff_geo flag to index {index} for suspicious login: {id}",
134 + )
135 +
136 + # Reenable the write block
137 + es_client.indices.put_settings(
138 + index=index,
139 + body={"index.blocks.write": True},
140 + )
141 + except Exception as e2:
142 + logger.error(
143 + f"Failed to remove read-only block from index {index}: {e2}",
144 + )
145 + return False
146 +
147 +
148 +async def mark_as_checked(suspicious_login):
149 + """
150 + Marks a suspicious login as checked by adding it to the set of checked IPs.
151 +
152 + Args:
153 + suspicious_login (Login): The suspicious login object to mark as checked.
154 +
155 + Returns:
156 + None
157 + """
158 + checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
159 +
160 +
161 +async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
162 + """
163 + Handles user activity by processing each hit in the user_activity and performing the following steps:
164 + 1. Extracts relevant information from the hit.
165 + 2. Checks if the current activity is already present in the unique_instances set.
166 + 3. If not present, adds the user activity to the IRIS case.
167 + 4. Creates an asset payload using the current activity.
168 + 5. Updates the case with the asset payload.
169 + 6. Updates the event analyzed multiple logins flag for the hit.
170 + 7. Adds the current activity to the unique_instances set.
171 +
172 + Parameters:
173 + - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
174 + - unique_instances (set): A set containing unique instances of user activity.
175 + - case_id (str): The ID of the IRIS case.
176 +
177 + Returns:
178 + None
179 + """
180 + for hit in user_activity.hits.hits:
181 + current_activity = {
182 + "loginID": hit.source.params_loginID,
183 + "ip": hit.source.ip,
184 + "country": hit.source.httpReq_country,
185 + "errMessage": hit.source.errMessage,
186 + "event_timestamp": hit.source.event_timestamp,
187 + "customer_code": hit.source.customer_code,
188 + "errDetails": hit.source.errDetails,
189 + }
190 + current_activity_frozenset = frozenset(current_activity.items())
191 + if current_activity_frozenset not in unique_instances:
192 + logger.info(f"Adding user activity to IRIS case: {current_activity}")
193 + current_asset = SuspiciousLogin(**current_activity)
194 + asset_payload = create_asset_payload(asset=current_asset)
195 + logger.info(f"Asset Payload: {asset_payload}")
196 + await update_case_with_asset(case_id, asset_payload)
197 + await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
198 + unique_instances.add(current_activity_frozenset)
199 +
200 +
201 +def create_asset_payload(asset: SuspiciousLogin):
202 + """
203 + Create a payload for adding an asset based on a SuspiciousLogin object.
204 +
205 + Args:
206 + asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
207 +
208 + Returns:
209 + AddAssetModel: The payload for adding the asset.
210 +
211 + """
212 + if asset.errMessage == "OK":
213 + return AddAssetModel(
214 + name=asset.loginID,
215 + ip=asset.ip,
216 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
217 + asset_type=1,
218 + compromise_status=1,
219 + analysis_status=2,
220 + )
221 + return AddAssetModel(
222 + name=asset.loginID,
223 + ip=asset.ip,
224 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
225 + asset_type=1,
226 + analysis_status=2,
227 + )
228 +
229 +
230 +async def update_case_with_asset(case_id: str, asset_payload):
231 + """
232 + Update the case with the asset information.
233 +
234 + :param case_id: The ID of the case to update
235 + :param asset_payload: The payload to update the case with
236 +
237 + :return: None
238 + """
239 + logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
240 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
241 + return await fetch_and_validate_data(
242 + client,
243 + case_client.add_asset,
244 + cid=case_id,
245 + **asset_payload.to_dict(),
246 + )
247 +
248 +
249 +async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
250 + """
251 + Creates an IRIS case for multiple logins with the same IP address.
252 +
253 + Args:
254 + suspicious_login (SuspiciousLogin): The suspicious login information.
255 + session (AsyncSession): The async session for database operations.
256 +
257 + Returns:
258 + CaseResponse: The response containing the created case information.
259 + """
260 + logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
261 + case_name = (
262 + f"Log Source: {suspicious_login.logSource} SAP SIEM. "
263 + f"User {suspicious_login.loginID} had at least one failed login attempt from two different GEO IP country locations followed by a successful login."
264 + )
265 +
266 + case_description = (
267 + f"Log Source: {suspicious_login.logSource}\n\n"
268 + f"IP Address: {suspicious_login.ip}\n\n"
269 + f"Country: {suspicious_login.country}\n\n"
270 + f"Timestamp: {suspicious_login.event_timestamp}"
271 + )
272 +
273 + case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
274 +
275 + payload = IrisCasePayload(
276 + case_name=case_name,
277 + case_description=case_description,
278 + case_customer=case_customer,
279 + case_classification=18,
280 + soc_id="1",
281 + create_customer=False,
282 + )
283 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
284 + result = await fetch_and_validate_data(
285 + client,
286 + case_client.add_case,
287 + **payload.to_dict(),
288 + )
289 + await update_event_analyzed_multiple_logins_flag(suspicious_login.id, suspicious_login.index)
290 +
291 + return CaseResponse(**result)
292 +
293 +
294 +async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
295 + """
296 + Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
297 + Collects a max of 1000 records.
298 +
299 + :param suspicious_logins: A list of suspicious logins
300 +
301 + :return: List of the user Activity collected from the sap_siem table
302 + """
303 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
304 + results = es_client.search(
305 + index="sap_siem_*",
306 + # index="new-integrations*",
307 + body={
308 + "size": 1000,
309 + "query": {"bool": {"must": [{"term": {"params_loginID": suspicious_logins.loginID}}]}},
310 + },
311 + )
312 + return SapSiemWazuhIndexerResponse(**results)
313 +
314 +
315 +async def get_initial_search_results(es_client):
316 + """
317 + Retrieves the initial search results from Elasticsearch.
318 +
319 + Args:
320 + es_client (Elasticsearch): The Elasticsearch client.
321 +
322 + Returns:
323 + dict: The search results.
324 + """
325 + return es_client.search(
326 + index="sap_siem_*",
327 + # index="new-integrations*",
328 + body={
329 + "size": 1000,
330 + "query": {"bool": {"must": [{"term": {"event_analyzed_same_user_successful_diff_geo": "False"}}]}},
331 + "sort": [{"event_timestamp": {"order": "asc"}}],
332 + },
333 + scroll="1m",
334 + )
335 +
336 +
337 +async def get_next_batch_of_results(es_client, scroll_id):
338 + """
339 + Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
340 +
341 + Args:
342 + es_client (Elasticsearch): The Elasticsearch client.
343 + scroll_id (str): The scroll ID to retrieve the next batch of results.
344 +
345 + Returns:
346 + dict: The next batch of results.
347 + """
348 + return es_client.scroll(scroll_id=scroll_id, scroll="1m")
349 +
350 +
351 +async def process_hits(hits, login_id_to_ips, suspicious_activity, time_range=20):
352 + """
353 + This function would trigger a suspicious login when the following conditions are met:
354 +
355 + 1. There is at least one failed login attempt from the same user (identified by `login_id`) from two different GEO IP country locations within the last 20 minutes.
356 + 2. There is at least one successful login attempt from the same user from a different GEO IP country location within the last 20 minutes.
357 +
358 + Here are some examples:
359 +
360 + Example 1:
361 + - At 12:00, a failed login attempt is made by user `user1` from IP `1.1.1.1` located in the US.
362 + - At 12:10, another failed login attempt is made by `user1` from IP `2.2.2.2` located in Canada.
363 + - At 12:15, a successful login attempt is made by `user1` from IP `3.3.3.3` located in the UK.
364 + - In this case, the function would trigger a suspicious login for `user1` because there are failed login attempts from two different countries (US and Canada) and
365 + a successful login from a different country (UK) within 20 minutes.
366 +
367 + Example 2:
368 + - At 12:00, a failed login attempt is made by user `user2` from IP `4.4.4.4` located in the US.
369 + - At 12:10, another failed login attempt is made by `user2` from IP `5.5.5.5` also located in the US.
370 + - At 12:15, a successful login attempt is made by `user2` from IP `6.6.6.6` located in the US.
371 + - In this case, the function would not trigger a suspicious login for `user2` because all the login attempts are from the same country (US).
372 + """
373 + login_id_to_ips = defaultdict(lambda: defaultdict(list))
374 +
375 + for hit in hits:
376 + # Convert loginID to lowercase before comparing
377 + login_id = hit.source.params_loginID.lower()
378 + ip = hit.source.ip
379 + errCode = hit.source.errCode
380 + country = hit.source.httpReq_country
381 +
382 + # Ignore loginID if it does not contain a '@'
383 + if "@" not in login_id:
384 + logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
385 + continue
386 +
387 + # Parse the event timestamp
388 + event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
389 +
390 + # Add the timestamp, errCode and country to the list for this loginID for this IP
391 + login_id_to_ips[login_id][ip].append((event_timestamp, errCode, country))
392 +
393 + logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
394 +
395 + # Check if there are at least 2 different IPs for the same loginID within the last 20 minutes
396 + ips_in_last_20_minutes = set()
397 + for other_ip, other_info in login_id_to_ips[login_id].items():
398 + for other_timestamp, other_errCode, other_country in other_info:
399 + if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
400 + ips_in_last_20_minutes.add((other_ip, other_errCode, other_country))
401 + logger.info(f"IPs in last 20 minutes: {ips_in_last_20_minutes}")
402 +
403 + # If there is at least 1 failed IP from at least two different GEO IP country locations and a successful login from a different location,
404 + # log the suspicious activity
405 + failed_ips = [ip for ip, errCode, country in ips_in_last_20_minutes if errCode != "0"]
406 + successful_ips = [ip for ip, errCode, country in ips_in_last_20_minutes if errCode == "0"]
407 + countries = set([country for ip, errCode, country in ips_in_last_20_minutes if errCode != "0"])
408 + logger.info(f"Failed IPs: {failed_ips}, Successful IPs: {successful_ips}, Countries: {countries}")
409 + if len(failed_ips) >= 1 and len(successful_ips) >= 1 and len(countries) >= 2:
410 + logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
411 + suspicious_login = SuspiciousLogin(
412 + _index=hit.index,
413 + _id=hit.id,
414 + customer_code=hit.source.customer_code,
415 + logSource=hit.source.logSource,
416 + loginID=hit.source.params_loginID,
417 + country=hit.source.httpReq_country,
418 + ip=hit.source.ip,
419 + event_timestamp=hit.source.event_timestamp,
420 + errMessage=hit.source.errMessage,
421 + errDetails=hit.source.errDetails,
422 + )
423 + suspicious_activity[login_id].append(suspicious_login)
424 + logger.info(f"Added suspicious login: {suspicious_login}")
425 +
426 +
427 +async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
428 + """
429 + Checks for multiple successful logins by IP address.
430 +
431 + Args:
432 + threshold (int): The minimum number of logins required to be considered suspicious.
433 +
434 + Returns:
435 + List[SuspiciousLogin]: A list of suspicious login objects.
436 + """
437 + ip_to_login_ids = defaultdict(set)
438 + suspicious_activity = defaultdict(list)
439 +
440 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
441 + scroll_id = None
442 +
443 + while True:
444 + if scroll_id is None:
445 + results = await get_initial_search_results(es_client)
446 + # logger.info(f"Initial search results: {results}")
447 + else:
448 + results = await get_next_batch_of_results(es_client, scroll_id)
449 +
450 + if not results["hits"]["hits"]:
451 + break
452 +
453 + results = SapSiemWazuhIndexerResponse(**results)
454 + await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
455 +
456 + scroll_id = results.scroll_id
457 +
458 + # Clear the scroll when you're done to free up resources
459 + if scroll_id is not None:
460 + es_client.clear_scroll(scroll_id=scroll_id)
461 +
462 + logger.info(f"Suspicious activity: {suspicious_activity}")
463 + suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
464 +
465 + return [login for sublist in suspicious_activity.values() for login in sublist]
466 +
467 +
468 +async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
469 + """
470 + Retrieves a list of suspicious login attempts based on the specified threshold.
471 +
472 + Args:
473 + threshold (int): The number of successful logins from the same IP address that is considered suspicious.
474 +
475 + Returns:
476 + List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
477 + """
478 + return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
479 +
480 +
481 +async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
482 + """
483 + Retrieves an existing database record for the given IP address.
484 +
485 + Args:
486 + session (AsyncSession): The async session object for database operations.
487 + ip (str): The IP address to search for.
488 +
489 + Returns:
490 + SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
491 + """
492 + result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
493 + return result.scalar_one_or_none() if result is not None else None
494 +
495 +
496 +def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
497 + """
498 + Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
499 +
500 + Args:
501 + existing_case (SapSiemMultipleLogins): The existing database record to be updated.
502 + new_login_ids (Set[str]): The new login IDs to be added to the existing record.
503 +
504 + Returns:
505 + None
506 + """
507 + existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
508 + if not new_login_ids.issubset(existing_loginIDs):
509 + updated_login_ids = existing_loginIDs.union(new_login_ids)
510 + existing_case.associated_loginIDs = ",".join(updated_login_ids)
511 + existing_case.last_case_created_timestamp = datetime.now()
512 +
513 +
514 +def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
515 + """
516 + Creates a new database record for SAP SIEM multiple logins.
517 +
518 + Args:
519 + ip (str): The IP address associated with the multiple logins.
520 + new_login_ids (Set[str]): The set of new login IDs.
521 +
522 + Returns:
523 + SapSiemMultipleLogins: The newly created database record.
524 + """
525 + return SapSiemMultipleLogins(
526 + ip=ip,
527 + last_case_created_timestamp=datetime.now(),
528 + associated_loginIDs=",".join(new_login_ids),
529 + )
530 +
531 +
532 +async def sap_siem_successful_same_user_diff_geo(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
533 + """
534 + Finds same IP with multiple users and handles suspicious logins.
535 +
536 + Args:
537 + threshold (int): The threshold value for determining suspicious logins.
538 + session (AsyncSession): The database session.
539 +
540 + Returns:
541 + InvokeSAPSiemResponse: The response indicating the success of the operation.
542 + """
543 + logger.info("Finding same user from different IP addresses")
544 +
545 + suspicious_ips = await get_suspicious_ips(threshold, time_range)
546 + logger.info(f"Suspicious IPs: {suspicious_ips}")
547 +
548 + unique_instances = set()
549 + case_ids = []
550 + # Dictionary to aggregate suspicious logins by IP
551 + aggregated_logins_by_ip = defaultdict(list)
552 +
553 + for suspicious_login in suspicious_ips:
554 + aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
555 +
556 + for ip, associated_logins in aggregated_logins_by_ip.items():
557 + logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
558 + if session is not None:
559 + existing_case = await get_existing_database_record(session, ip)
560 +
561 + new_login_ids = {login.loginID for login in associated_logins}
562 + if existing_case:
563 + logger.info(f"Updating existing database record: {existing_case}")
564 + update_existing_database_record(existing_case, new_login_ids)
565 + else:
566 + logger.info(f"Creating new case for IP: {ip}")
567 + new_case = create_new_database_record(ip, new_login_ids)
568 + session.add(new_case)
569 +
570 + # Create a single new IRIS case for this IP
571 + # Modify this to include information from all associated_logins
572 + await handle_suspicious_login_multiple(
573 + associated_logins[0],
574 + unique_instances,
575 + case_ids,
576 + session=session,
577 + )
578 + else:
579 + raise HTTPException(
580 + status_code=500,
581 + detail="Failed to create IRIS case",
582 + )
583 + await session.commit()
584 +
585 + # Clear the global set
586 + checked_ips.clear()
587 +
588 + return InvokeSAPSiemResponse(
589 + success=True,
590 + message="SAP SIEM multiple logins invoked.",
591 + )
backend/app/integrations/sap_siem/services/sap_siem_successful_user_login_after_using_different_ip.py new
+575
@@ -0,0 +1,575 @@
1 +from collections import defaultdict
2 +from datetime import datetime
3 +from datetime import timedelta
4 +from typing import List
5 +from typing import Set
6 +
7 +from fastapi import HTTPException
8 +from loguru import logger
9 +from sqlalchemy.ext.asyncio import AsyncSession
10 +from sqlalchemy.future import select
11 +
12 +from app.connectors.dfir_iris.utils.universal import fetch_and_validate_data
13 +from app.connectors.dfir_iris.utils.universal import initialize_client_and_case
14 +from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
15 +from app.integrations.sap_siem.models.sap_siem import SapSiemMultipleLogins
16 +from app.integrations.sap_siem.schema.sap_siem import AddAssetModel
17 +from app.integrations.sap_siem.schema.sap_siem import CaseResponse
18 +from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
19 +from app.integrations.sap_siem.schema.sap_siem import IrisCasePayload
20 +from app.integrations.sap_siem.schema.sap_siem import SapSiemWazuhIndexerResponse
21 +from app.integrations.sap_siem.schema.sap_siem import SuspiciousLogin
22 +from app.integrations.utils.alerts import send_to_shuffle
23 +from app.integrations.utils.schema import ShufflePayload
24 +from app.utils import get_customer_alert_settings
25 +
26 +# Global set to keep track of IPs that have already been checked
27 +checked_ips = set()
28 +
29 +
30 +async def handle_common_suspicious_login_tasks(
31 + suspicious_login,
32 + unique_instances,
33 + case_ids,
34 + create_case_fn,
35 + session: AsyncSession,
36 +):
37 + """
38 + Handles common tasks for suspicious logins.
39 +
40 + Args:
41 + suspicious_login: The suspicious login object.
42 + unique_instances: List of unique instances.
43 + case_ids: List of case IDs.
44 + create_case_fn: Function to create a case.
45 + session: The async session.
46 +
47 + Returns:
48 + None
49 + """
50 + case = await create_case_fn(suspicious_login, session)
51 + case_ids.append(case.data.case_id)
52 + user_activity = await collect_user_activity(suspicious_login)
53 + logger.info(f"User activity: {user_activity}")
54 + await handle_user_activity(user_activity, unique_instances, case.data.case_id)
55 + await mark_as_checked(suspicious_login)
56 + alert_source_link = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).shuffle_endpoint
57 + await send_to_shuffle(
58 + ShufflePayload(
59 + alert_id=case.data.case_id,
60 + customer=suspicious_login.customer_code,
61 + customer_code=suspicious_login.customer_code,
62 + alert_source_link=f"{alert_source_link}/case?cid={case.data.case_id}",
63 + rule_description=f"{case.data.case_name}",
64 + hostname=suspicious_login.ip,
65 + ),
66 + session=session,
67 + )
68 +
69 +
70 +async def handle_suspicious_login_multiple(suspicious_login, unique_instances, case_ids, session: AsyncSession):
71 + """
72 + Handles suspicious login events with multiple logins.
73 +
74 + Args:
75 + suspicious_login: The suspicious login event.
76 + unique_instances: List of unique instances of the suspicious login event.
77 + case_ids: List of case IDs associated with the suspicious login event.
78 + session: The database session.
79 +
80 + Returns:
81 + None
82 + """
83 + await handle_common_suspicious_login_tasks(
84 + suspicious_login,
85 + unique_instances,
86 + case_ids,
87 + create_iris_case_multiple,
88 + session,
89 + )
90 +
91 +
92 +async def update_event_analyzed_multiple_logins_flag(id: str, index: str):
93 + """
94 + Update the event_analyzed_multiple_logins flag in the Elasticsearch document to True.
95 +
96 + :param suspicious_login: The suspicious login to update
97 +
98 + :return: None
99 + """
100 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
101 + try:
102 + es_client.update(
103 + index=index,
104 + id=id,
105 + body={
106 + "doc": {
107 + "event_analyzed_success_login_diff_ip": "True",
108 + },
109 + },
110 + )
111 + logger.info(f"Updated event_analyzed_success_login_diff_ip flag for suspicious login: {id}")
112 + except Exception as e:
113 + logger.error(
114 + f"Failed to update case created flag {e}",
115 + )
116 + # Attempt to remove read-only block
117 + try:
118 + es_client.indices.put_settings(
119 + index=index,
120 + body={"index.blocks.write": None},
121 + )
122 + logger.info(
123 + f"Removed read-only block from index {index}. Retrying update.",
124 + )
125 +
126 + # Retry the update operation
127 + es_client.update(
128 + index=index,
129 + id=id,
130 + body={"doc": {"event_analyzed_multiple_logins": "True"}},
131 + )
132 + logger.info(
133 + f"Added event_analyzed_multiple_logins flag to index {index} for suspicious login: {id}",
134 + )
135 +
136 + # Reenable the write block
137 + es_client.indices.put_settings(
138 + index=index,
139 + body={"index.blocks.write": True},
140 + )
141 + except Exception as e2:
142 + logger.error(
143 + f"Failed to remove read-only block from index {index}: {e2}",
144 + )
145 + return False
146 +
147 +
148 +async def mark_as_checked(suspicious_login):
149 + """
150 + Marks a suspicious login as checked by adding it to the set of checked IPs.
151 +
152 + Args:
153 + suspicious_login (Login): The suspicious login object to mark as checked.
154 +
155 + Returns:
156 + None
157 + """
158 + checked_ips.add((suspicious_login.loginID, suspicious_login.ip))
159 +
160 +
161 +async def handle_user_activity(user_activity: SapSiemWazuhIndexerResponse, unique_instances, case_id):
162 + """
163 + Handles user activity by processing each hit in the user_activity and performing the following steps:
164 + 1. Extracts relevant information from the hit.
165 + 2. Checks if the current activity is already present in the unique_instances set.
166 + 3. If not present, adds the user activity to the IRIS case.
167 + 4. Creates an asset payload using the current activity.
168 + 5. Updates the case with the asset payload.
169 + 6. Updates the event analyzed multiple logins flag for the hit.
170 + 7. Adds the current activity to the unique_instances set.
171 +
172 + Parameters:
173 + - user_activity (SapSiemWazuhIndexerResponse): The user activity to be processed.
174 + - unique_instances (set): A set containing unique instances of user activity.
175 + - case_id (str): The ID of the IRIS case.
176 +
177 + Returns:
178 + None
179 + """
180 + for hit in user_activity.hits.hits:
181 + current_activity = {
182 + "loginID": hit.source.params_loginID,
183 + "ip": hit.source.ip,
184 + "country": hit.source.httpReq_country,
185 + "errMessage": hit.source.errMessage,
186 + "event_timestamp": hit.source.event_timestamp,
187 + "customer_code": hit.source.customer_code,
188 + "errDetails": hit.source.errDetails,
189 + }
190 + current_activity_frozenset = frozenset(current_activity.items())
191 + if current_activity_frozenset not in unique_instances:
192 + logger.info(f"Adding user activity to IRIS case: {current_activity}")
193 + current_asset = SuspiciousLogin(**current_activity)
194 + asset_payload = create_asset_payload(asset=current_asset)
195 + logger.info(f"Asset Payload: {asset_payload}")
196 + await update_case_with_asset(case_id, asset_payload)
197 + await update_event_analyzed_multiple_logins_flag(hit.id, hit.index)
198 + unique_instances.add(current_activity_frozenset)
199 +
200 +
201 +def create_asset_payload(asset: SuspiciousLogin):
202 + """
203 + Create a payload for adding an asset based on a SuspiciousLogin object.
204 +
205 + Args:
206 + asset (SuspiciousLogin): The SuspiciousLogin object containing the asset details.
207 +
208 + Returns:
209 + AddAssetModel: The payload for adding the asset.
210 +
211 + """
212 + if asset.errMessage == "OK":
213 + return AddAssetModel(
214 + name=asset.loginID,
215 + ip=asset.ip,
216 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
217 + asset_type=1,
218 + compromise_status=1,
219 + analysis_status=2,
220 + )
221 + return AddAssetModel(
222 + name=asset.loginID,
223 + ip=asset.ip,
224 + description=f"Country: {asset.country}\n\nMessage: {asset.errDetails}\n\nTimestamp: {asset.event_timestamp}",
225 + asset_type=1,
226 + analysis_status=2,
227 + )
228 +
229 +
230 +async def update_case_with_asset(case_id: str, asset_payload):
231 + """
232 + Update the case with the asset information.
233 +
234 + :param case_id: The ID of the case to update
235 + :param asset_payload: The payload to update the case with
236 +
237 + :return: None
238 + """
239 + logger.info(f"Updating IRIS case {case_id} with asset: {asset_payload}")
240 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
241 + return await fetch_and_validate_data(
242 + client,
243 + case_client.add_asset,
244 + cid=case_id,
245 + **asset_payload.to_dict(),
246 + )
247 +
248 +
249 +async def create_iris_case_multiple(suspicious_login: SuspiciousLogin, session: AsyncSession) -> CaseResponse:
250 + """
251 + Creates an IRIS case for multiple logins with the same IP address.
252 +
253 + Args:
254 + suspicious_login (SuspiciousLogin): The suspicious login information.
255 + session (AsyncSession): The async session for database operations.
256 +
257 + Returns:
258 + CaseResponse: The response containing the created case information.
259 + """
260 + logger.info(f"Creating IRIS case same IP with multiple users: {suspicious_login}")
261 + case_name = (
262 + f"Log Source: {suspicious_login.logSource} SAP SIEM. "
263 + f"User {suspicious_login.loginID} had a successful login after failed attempts from different IPs."
264 + )
265 +
266 + case_description = (
267 + f"Log Source: {suspicious_login.logSource}\n\n"
268 + f"IP Address: {suspicious_login.ip}\n\n"
269 + f"Country: {suspicious_login.country}\n\n"
270 + f"Timestamp: {suspicious_login.event_timestamp}"
271 + )
272 +
273 + case_customer = (await get_customer_alert_settings(suspicious_login.customer_code, session=session)).iris_customer_id
274 +
275 + payload = IrisCasePayload(
276 + case_name=case_name,
277 + case_description=case_description,
278 + case_customer=case_customer,
279 + case_classification=18,
280 + soc_id="1",
281 + create_customer=False,
282 + )
283 + client, case_client = await initialize_client_and_case("DFIR-IRIS")
284 + result = await fetch_and_validate_data(
285 + client,
286 + case_client.add_case,
287 + **payload.to_dict(),
288 + )
289 +
290 + return CaseResponse(**result)
291 +
292 +
293 +async def collect_user_activity(suspicious_logins: SuspiciousLogin) -> SapSiemWazuhIndexerResponse:
294 + """
295 + Collect the IP addresses of the suspicious logins and query the database for all activity from those IP addresses.
296 + Collects a max of 1000 records.
297 +
298 + :param suspicious_logins: A list of suspicious logins
299 +
300 + :return: List of the user Activity collected from the sap_siem table
301 + """
302 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
303 + results = es_client.search(
304 + # index="sap_siem_*",
305 + index="new-integrations*",
306 + body={
307 + "size": 1000,
308 + "query": {"bool": {"must": [{"term": {"params_loginID": suspicious_logins.loginID}}]}},
309 + },
310 + )
311 + return SapSiemWazuhIndexerResponse(**results)
312 +
313 +
314 +async def get_initial_search_results(es_client):
315 + """
316 + Retrieves the initial search results from Elasticsearch.
317 +
318 + Args:
319 + es_client (Elasticsearch): The Elasticsearch client.
320 +
321 + Returns:
322 + dict: The search results.
323 + """
324 + return es_client.search(
325 + # index="sap_siem_*",
326 + index="new-integrations*",
327 + body={
328 + "size": 1000,
329 + "query": {"bool": {"must": [{"term": {"event_analyzed_success_login_diff_ip": "False"}}]}},
330 + "sort": [{"event_timestamp": {"order": "asc"}}],
331 + },
332 + scroll="1m",
333 + )
334 +
335 +
336 +async def get_next_batch_of_results(es_client, scroll_id):
337 + """
338 + Retrieves the next batch of results using the provided Elasticsearch client and scroll ID.
339 +
340 + Args:
341 + es_client (Elasticsearch): The Elasticsearch client.
342 + scroll_id (str): The scroll ID to retrieve the next batch of results.
343 +
344 + Returns:
345 + dict: The next batch of results.
346 + """
347 + return es_client.scroll(scroll_id=scroll_id, scroll="1m")
348 +
349 +
350 +async def process_hits(hits, login_id_to_ips, suspicious_activity, time_range):
351 + """
352 + Process the hits received from SAP SIEM and update the login ID to IPs mapping and suspicious activity.
353 +
354 + Args:
355 + hits (list): List of hits received from SAP SIEM.
356 + login_id_to_ips (dict): Dictionary mapping login IDs to IP addresses.
357 + suspicious_activity (dict): Dictionary mapping login IDs to a list of suspicious login objects.
358 +
359 + Returns:
360 + None
361 + """
362 + logger.info(f"Processing hits: {hits} for sap_siem_successful_user_login_after_using_different_ip")
363 + login_id_to_ips = defaultdict(lambda: defaultdict(list))
364 +
365 + for hit in hits:
366 + # Convert loginID to lowercase before comparing
367 + login_id = hit.source.params_loginID.lower()
368 + ip = hit.source.ip
369 + errCode = hit.source.errCode
370 +
371 + # Ignore loginID if it does not contain a '@'
372 + if "@" not in login_id:
373 + logger.info(f"Ignoring loginID {login_id} as it does not contain a '@'")
374 + continue
375 +
376 + # Parse the event timestamp
377 + event_timestamp = datetime.strptime(hit.source.event_timestamp, "%Y-%m-%dT%H:%M:%S.%fZ")
378 +
379 + # Add the timestamp and errCode to the list for this loginID for this IP
380 + login_id_to_ips[login_id][ip].append((event_timestamp, errCode))
381 +
382 + logger.info(f"Added timestamp {event_timestamp} for IP {ip} and loginID {login_id}")
383 +
384 + # Check if there are at least 2 different IPs for the same loginID within the last 15 minutes
385 + ips_in_last_15_minutes = set()
386 + for other_ip, other_info in login_id_to_ips[login_id].items():
387 + for other_timestamp, other_errCode in other_info:
388 + if event_timestamp - timedelta(minutes=time_range) <= other_timestamp <= event_timestamp:
389 + ips_in_last_15_minutes.add((other_ip, other_errCode))
390 +
391 + # If the current hit is a successful login and there are at least 2 different failed IPs, log the suspicious activity
392 + failed_ips = [ip for ip, errCode in ips_in_last_15_minutes if errCode != "0"]
393 + logger.info(f"Failed IPs: {failed_ips}")
394 + if errCode == "0" and len(failed_ips) >= 2:
395 + logger.info(f"Found suspicious login: {login_id} with IP: {ip} and errCode: {errCode}")
396 + suspicious_login = SuspiciousLogin(
397 + _index=hit.index,
398 + _id=hit.id,
399 + customer_code=hit.source.customer_code,
400 + logSource=hit.source.logSource,
401 + loginID=hit.source.params_loginID,
402 + country=hit.source.httpReq_country,
403 + ip=hit.source.ip,
404 + event_timestamp=hit.source.event_timestamp,
405 + errMessage=hit.source.errMessage,
406 + errDetails=hit.source.errDetails,
407 + )
408 + suspicious_activity[login_id].append(suspicious_login)
409 + logger.info(f"Added suspicious login: {suspicious_login}")
410 +
411 +
412 +async def check_multiple_successful_logins_by_ip(threshold: int, time_range: int) -> List[SuspiciousLogin]:
413 + """
414 + Checks for multiple successful logins by IP address.
415 +
416 + Args:
417 + threshold (int): The minimum number of logins required to be considered suspicious.
418 +
419 + Returns:
420 + List[SuspiciousLogin]: A list of suspicious login objects.
421 + """
422 + ip_to_login_ids = defaultdict(set)
423 + suspicious_activity = defaultdict(list)
424 +
425 + es_client = await create_wazuh_indexer_client("Wazuh-Indexer")
426 + scroll_id = None
427 +
428 + while True:
429 + if scroll_id is None:
430 + results = await get_initial_search_results(es_client)
431 + else:
432 + results = await get_next_batch_of_results(es_client, scroll_id)
433 +
434 + if not results["hits"]["hits"]:
435 + break
436 +
437 + results = SapSiemWazuhIndexerResponse(**results)
438 + await process_hits(results.hits.hits, ip_to_login_ids, suspicious_activity, time_range)
439 +
440 + scroll_id = results.scroll_id
441 +
442 + # Clear the scroll when you're done to free up resources
443 + if scroll_id is not None:
444 + es_client.clear_scroll(scroll_id=scroll_id)
445 +
446 + logger.info(f"Suspicious activity: {suspicious_activity}")
447 + suspicious_activity = {ip: results for ip, results in suspicious_activity.items()}
448 +
449 + return [login for sublist in suspicious_activity.values() for login in sublist]
450 +
451 +
452 +async def get_suspicious_ips(threshold: int, time_range: int) -> List[SuspiciousLogin]:
453 + """
454 + Retrieves a list of suspicious login attempts based on the specified threshold.
455 +
456 + Args:
457 + threshold (int): The number of successful logins from the same IP address that is considered suspicious.
458 +
459 + Returns:
460 + List[SuspiciousLogin]: A list of SuspiciousLogin objects representing the suspicious login attempts.
461 + """
462 + return await check_multiple_successful_logins_by_ip(threshold=threshold, time_range=time_range)
463 +
464 +
465 +async def get_existing_database_record(session: AsyncSession, ip: str) -> SapSiemMultipleLogins:
466 + """
467 + Retrieves an existing database record for the given IP address.
468 +
469 + Args:
470 + session (AsyncSession): The async session object for database operations.
471 + ip (str): The IP address to search for.
472 +
473 + Returns:
474 + SapSiemMultipleLogins: The database record matching the IP address, or None if not found.
475 + """
476 + result = await session.execute(select(SapSiemMultipleLogins).where(SapSiemMultipleLogins.ip == ip))
477 + return result.scalar_one_or_none() if result is not None else None
478 +
479 +
480 +def update_existing_database_record(existing_case: SapSiemMultipleLogins, new_login_ids: Set[str]) -> None:
481 + """
482 + Update the existing database record for a SapSiemMultipleLogins case with new login IDs.
483 +
484 + Args:
485 + existing_case (SapSiemMultipleLogins): The existing database record to be updated.
486 + new_login_ids (Set[str]): The new login IDs to be added to the existing record.
487 +
488 + Returns:
489 + None
490 + """
491 + existing_loginIDs = set(existing_case.associated_loginIDs.split(","))
492 + if not new_login_ids.issubset(existing_loginIDs):
493 + updated_login_ids = existing_loginIDs.union(new_login_ids)
494 + existing_case.associated_loginIDs = ",".join(updated_login_ids)
495 + existing_case.last_case_created_timestamp = datetime.now()
496 +
497 +
498 +def create_new_database_record(ip: str, new_login_ids: Set[str]) -> SapSiemMultipleLogins:
499 + """
500 + Creates a new database record for SAP SIEM multiple logins.
501 +
502 + Args:
503 + ip (str): The IP address associated with the multiple logins.
504 + new_login_ids (Set[str]): The set of new login IDs.
505 +
506 + Returns:
507 + SapSiemMultipleLogins: The newly created database record.
508 + """
509 + return SapSiemMultipleLogins(
510 + ip=ip,
511 + last_case_created_timestamp=datetime.now(),
512 + associated_loginIDs=",".join(new_login_ids),
513 + )
514 +
515 +
516 +async def sap_siem_successful_user_login_with_different_ip(threshold: int, time_range: int, session: AsyncSession) -> InvokeSAPSiemResponse:
517 + """
518 + Finds same IP with multiple users and handles suspicious logins.
519 +
520 + Args:
521 + threshold (int): The threshold value for determining suspicious logins.
522 + session (AsyncSession): The database session.
523 +
524 + Returns:
525 + InvokeSAPSiemResponse: The response indicating the success of the operation.
526 + """
527 + logger.info("Finding same IP with multiple users")
528 +
529 + suspicious_ips = await get_suspicious_ips(threshold, time_range)
530 + logger.info(f"Suspicious IPs: {suspicious_ips}")
531 +
532 + unique_instances = set()
533 + case_ids = []
534 + # Dictionary to aggregate suspicious logins by IP
535 + aggregated_logins_by_ip = defaultdict(list)
536 +
537 + for suspicious_login in suspicious_ips:
538 + aggregated_logins_by_ip[suspicious_login.ip].append(suspicious_login)
539 +
540 + for ip, associated_logins in aggregated_logins_by_ip.items():
541 + logger.info(f"IP: {ip}, Associated Logins: {associated_logins}")
542 + if session is not None:
543 + existing_case = await get_existing_database_record(session, ip)
544 +
545 + new_login_ids = {login.loginID for login in associated_logins}
546 + if existing_case:
547 + logger.info(f"Updating existing database record: {existing_case}")
548 + update_existing_database_record(existing_case, new_login_ids)
549 + else:
550 + logger.info(f"Creating new case for IP: {ip}")
551 + new_case = create_new_database_record(ip, new_login_ids)
552 + session.add(new_case)
553 +
554 + # Create a single new IRIS case for this IP
555 + # Modify this to include information from all associated_logins
556 + await handle_suspicious_login_multiple(
557 + associated_logins[0],
558 + unique_instances,
559 + case_ids,
560 + session=session,
561 + )
562 + else:
563 + raise HTTPException(
564 + status_code=500,
565 + detail="Failed to create IRIS case",
566 + )
567 + await session.commit()
568 +
569 + # Clear the global set
570 + checked_ips.clear()
571 +
572 + return InvokeSAPSiemResponse(
573 + success=True,
574 + message="SAP SIEM multiple logins invoked.",
575 + )
backend/app/middleware/license.py new
+442
@@ -0,0 +1,442 @@
1 +import os
2 +from datetime import datetime as dt
3 +from enum import Enum
4 +from typing import Any
5 +from typing import List
6 +from typing import Optional
7 +
8 +from fastapi import APIRouter
9 +from fastapi import Depends
10 +from fastapi import HTTPException
11 +from licensing.methods import Data
12 +from licensing.methods import Helpers
13 +from licensing.methods import Key
14 +
15 +# from licensing.models import *
16 +from loguru import logger
17 +from pydantic import BaseModel
18 +from pydantic import Field
19 +from sqlalchemy import select
20 +from sqlalchemy.ext.asyncio import AsyncSession
21 +
22 +from app.db.db_session import get_db
23 +from app.db.universal_models import License
24 +
25 +
26 +class ReplaceLicenseRequest(BaseModel):
27 + """
28 + A Pydantic model for replacing a license.
29 +
30 + Attributes:
31 + license_key (str): The license key to replace.
32 + """
33 +
34 + license_key: str = Field(..., title="The license key to replace")
35 +
36 +
37 +class CreateLicenseRequest(BaseModel):
38 + """
39 + A Pydantic model for creating a license.
40 +
41 + Attributes:
42 + product_id (int): The product id.
43 + notes (str): The notes.
44 + new_customer (bool): Whether the customer is new.
45 + name (str): The customer name.
46 + email (str): The customer email.
47 + company_name (str): The customer company name.
48 + """
49 +
50 + product_id: int = Field(24355, title="The product id")
51 + notes: str = Field("Test Key", title="The notes")
52 + new_customer: bool = Field(True, title="Whether the customer is new")
53 + name: str = Field("Test Customer", title="The customer name")
54 + email: str = Field(..., title="The customer email")
55 + company_name: str = Field("Test Company", title="The customer company name")
56 +
57 +
58 +class CreateCustomerKeyResult(BaseModel):
59 + customerId: int
60 + key: str
61 + result: int
62 + message: Optional[str]
63 +
64 +
65 +class CreateCustomerKeyResponseModel(BaseModel):
66 + response: List[Optional[CreateCustomerKeyResult]]
67 +
68 +
69 +class Customer(BaseModel):
70 + Id: int
71 + Name: str
72 + Email: str
73 + CompanyName: str
74 + Created: int
75 +
76 +
77 +class RawResponse(BaseModel):
78 + license_key: str
79 + signature: str
80 + result: int
81 + message: str
82 + metadata: Optional[Any]
83 +
84 +
85 +class LicenseResponse(BaseModel):
86 + product_id: int
87 + id: int
88 + key: str
89 + created: dt
90 + expires: dt
91 + period: int
92 + f1: bool
93 + f2: bool
94 + f3: bool
95 + f4: bool
96 + f5: bool
97 + f6: bool
98 + f7: bool
99 + f8: bool
100 + notes: str
101 + block: bool
102 + global_id: int
103 + customer: Customer
104 + activated_machines: List
105 + trial_activation: bool
106 + max_no_of_machines: int
107 + allowed_machines: Optional[Any]
108 + data_objects: List
109 + sign_date: dt
110 + reseller: Optional[Any]
111 +
112 +
113 +class Feature(Enum):
114 + MIMECAST = "MIMECAST"
115 + SAP_SIEM = "SAP SIEM"
116 + HUNTRESS = "HUNTRESS"
117 + REPORTING = "REPORTING"
118 + # Add more features as needed
119 +
120 + @classmethod
121 + def get_feature_name(cls, feature_name):
122 + feature_map = {
123 + cls.MIMECAST.value: "MIMECAST",
124 + cls.SAP_SIEM.value: "SAP SIEM",
125 + cls.HUNTRESS.value: "HUNTRESS",
126 + cls.REPORTING.value: "REPORTING",
127 + # Add more mappings as needed
128 + }
129 + return feature_map.get(feature_name)
130 +
131 +
132 +license_router = APIRouter()
133 +
134 +
135 +async def check_if_license_exists(session: AsyncSession):
136 + # Get the first row and raise HTTPException stating license already exists
137 + result = await session.execute(select(License))
138 + license = result.scalars().first()
139 + logger.info(f"License: {license}")
140 + if license:
141 + raise HTTPException(status_code=400, detail="License already exists")
142 +
143 +
144 +def get_auth_token():
145 + auth = os.getenv("CRYPTOLENS_AUTH")
146 + if not auth:
147 + raise HTTPException(status_code=500, detail="Auth token not found")
148 + return auth
149 +
150 +
151 +def get_rsa_pub_key():
152 + rsa_public_key = os.getenv("RSA_PUBLIC_KEY")
153 + if not rsa_public_key:
154 + raise HTTPException(status_code=500, detail="RSA public key not found")
155 + return rsa_public_key
156 +
157 +
158 +def get_product_id():
159 + product_id = os.getenv("PRODUCT_ID")
160 + if not product_id:
161 + raise HTTPException(status_code=500, detail="Product id not found")
162 + return product_id
163 +
164 +
165 +def create_trial_key(auth, request):
166 + result, _ = Key.create_key(
167 + token=auth,
168 + product_id=request.product_id,
169 + period=7,
170 + notes=request.notes,
171 + new_customer=request.new_customer,
172 + name=request.name,
173 + email=request.email,
174 + company_name=request.company_name,
175 + )
176 + logger.info(result)
177 + result = CreateCustomerKeyResponseModel(response=[result])
178 + return result
179 +
180 +
181 +def create_key(auth, request):
182 + result, _ = Key.create_key(
183 + token=auth,
184 + product_id=request.product_id,
185 + period=365,
186 + notes=request.notes,
187 + new_customer=request.new_customer,
188 + name=request.name,
189 + email=request.email,
190 + company_name=request.company_name,
191 + )
192 + logger.info(result)
193 + result = CreateCustomerKeyResponseModel(response=[result])
194 + return result
195 +
196 +
197 +async def add_license_to_db(session: AsyncSession, result, request):
198 + new_license = License(
199 + license_key=result.response[0].key,
200 + customer_name=request.name,
201 + customer_email=request.email,
202 + company_name=request.company_name,
203 + )
204 + logger.info(f"Adding new license: {new_license} to the database")
205 + session.add(new_license)
206 + await session.commit()
207 + return new_license
208 +
209 +
210 +async def get_license(session: AsyncSession) -> License:
211 + try:
212 + result = await session.execute(select(License))
213 + license = result.scalars().first()
214 + if not license:
215 + raise HTTPException(status_code=404, detail="No license found")
216 + return license
217 + except Exception as e:
218 + logger.error(e)
219 + raise HTTPException(status_code=404, detail="No license found")
220 +
221 +
222 +def check_license(license: License):
223 + result, _ = Key.activate(
224 + token=get_auth_token(),
225 + rsa_pub_key=get_rsa_pub_key(),
226 + product_id=get_product_id(),
227 + key=license.license_key,
228 + machine_code=Helpers.GetMachineCode(v=2),
229 + )
230 + return result
231 +
232 +
233 +def extend_license(license: License, period: int):
234 + result, _ = Key.extend_license(
235 + token=get_auth_token(),
236 + product_id=get_product_id(),
237 + key=license.license_key,
238 + no_of_days=period,
239 + )
240 + logger.info(result)
241 + return result
242 +
243 +
244 +def is_license_expired(license: dict) -> bool:
245 + """
246 + Check if a license is expired.
247 +
248 + Args:
249 + license (dict): The license to check.
250 +
251 + Returns:
252 + bool: True if the license is expired, False otherwise.
253 + """
254 + return dt.now() > license["expires"]
255 +
256 +
257 +async def is_feature_enabled(feature_name: str, session: AsyncSession) -> bool:
258 + """
259 + Check if a feature is enabled in a license.
260 +
261 + Args:
262 + license (License): The license to check.
263 + feature_name (str): The feature name to check.
264 + session (AsyncSession): The database session.
265 +
266 + Returns:
267 + bool: True if the feature is enabled, False otherwise.
268 + """
269 + license = await get_license(session)
270 + license_details = LicenseResponse(**check_license(license).__dict__)
271 + for data_object in license_details.data_objects:
272 + if data_object["Name"] == feature_name and data_object["IntValue"] == 1:
273 + return True
274 +
275 + raise HTTPException(status_code=400, detail="Feature not enabled. You must purchase a license to use this feature.")
276 +
277 +
278 +@license_router.post(
279 + "/create_trial_key",
280 + description="Create a trial license key",
281 +)
282 +async def create_trial_license_key(request: CreateLicenseRequest, session: AsyncSession = Depends(get_db)):
283 + """
284 + Create a trial license key.
285 +
286 + Args:
287 + request (CreateLicenseRequest): The request containing the license key to create.
288 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
289 +
290 + Returns:
291 + LicenseVerificationResponse: A Pydantic model containing the verification status and message.
292 + """
293 + await check_if_license_exists(session)
294 + auth = get_auth_token()
295 + result = create_trial_key(auth, request)
296 + await add_license_to_db(session, result, request)
297 + return result
298 +
299 +
300 +@license_router.post(
301 + "/create_new_key",
302 + description="Create a new license key",
303 +)
304 +async def create_new_license_key(request: CreateLicenseRequest, session: AsyncSession = Depends(get_db)):
305 + """
306 + Create a new license key.
307 +
308 + Args:
309 + license_key (str): The license key to verify.
310 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
311 +
312 + Returns:
313 + LicenseVerificationResponse: A Pydantic model containing the verification status and message.
314 + """
315 + await check_if_license_exists(session)
316 + auth = get_auth_token()
317 + result = create_key(auth, request)
318 + await add_license_to_db(session, result, request)
319 + return result
320 +
321 +
322 +@license_router.post(
323 + "/extend_license",
324 + description="Extend a license",
325 +)
326 +async def extend_license_key(period: int, session: AsyncSession = Depends(get_db)):
327 + """
328 + Extend a license key.
329 +
330 + Args:
331 + period (int): The period to extend the license by.
332 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
333 +
334 + Returns:
335 + LicenseVerificationResponse: A Pydantic model containing the verification status and message.
336 + """
337 + try:
338 + license = await get_license(session)
339 + logger.info(f"License: {license}")
340 + result = extend_license(license, period)
341 + return result
342 + except Exception as e:
343 + logger.error(e)
344 + raise HTTPException(status_code=400, detail="License extension failed")
345 +
346 +
347 +@license_router.get(
348 + "/verify_license",
349 + response_model=LicenseResponse,
350 + description="Verify a license key",
351 +)
352 +async def verify_license_key(session: AsyncSession = Depends(get_db)) -> LicenseResponse:
353 + """ "
354 + Verify a license key.
355 +
356 + Args:
357 + license_key (str): The license key to verify.
358 +
359 + Returns:
360 + LicenseVerificationResponse: A Pydantic model containing the verification status and message.
361 + """
362 + try:
363 + license = await get_license(session)
364 + logger.info(f"License: {license}")
365 + result = check_license(license)
366 + result = result.__dict__
367 + logger.info(result)
368 + if is_license_expired(result):
369 + raise HTTPException(status_code=400, detail="License is expired")
370 + return result
371 + except Exception as e:
372 + logger.error(e)
373 + raise HTTPException(status_code=400, detail="License verification failed")
374 +
375 +
376 +@license_router.post(
377 + "/add_feature/{feature_name}",
378 + description="Add a feature to a license",
379 +)
380 +async def add_feature_to_license(feature_name: str, session: AsyncSession = Depends(get_db)):
381 + """
382 + Add a feature to a license.
383 +
384 + Args:
385 + feature_name (str): The feature name to add.
386 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
387 +
388 + Returns:
389 + LicenseVerificationResponse: A Pydantic model containing the verification status and message.
390 + """
391 + logger.info(f"Adding feature: {feature_name} to license")
392 + # Check if the feature name is valid
393 + feature_name = Feature.get_feature_name(feature_name)
394 + if feature_name is None:
395 + logger.error("Invalid feature name")
396 + raise HTTPException(status_code=400, detail="Invalid feature name")
397 + try:
398 + license = await get_license(session)
399 + logger.info(f"License: {license}")
400 + result, _ = Data.add_data_object_to_key(
401 + token=get_auth_token(),
402 + product_id=get_product_id(),
403 + key=license.license_key,
404 + name=feature_name,
405 + string_value=f"[{feature_name}]",
406 + check_for_duplicates=True,
407 + int_value=1,
408 + )
409 + logger.info(result)
410 + return result
411 + except Exception as e:
412 + logger.error(e)
413 + raise HTTPException(status_code=400, detail="Feature addition failed")
414 +
415 +
416 +@license_router.post(
417 + "/replace_license_in_db",
418 + description="Replace a license",
419 +)
420 +async def replace_license_in_db(request: ReplaceLicenseRequest, session: AsyncSession = Depends(get_db)):
421 + """
422 + Replace a license in the database.
423 +
424 + Args:
425 + request (ReplaceLicenseRequest): The request containing the license key to replace.
426 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
427 +
428 + Returns:
429 + LicenseVerificationResponse: A Pydantic model containing the verification status and message.
430 + """
431 + try:
432 + # Update the license in the database
433 + result = await session.execute(select(License))
434 + license = result.scalars().first()
435 + if not license:
436 + raise HTTPException(status_code=404, detail="No license found")
437 + license.license_key = request.license_key
438 + await session.commit()
439 + return {"message": "License replaced successfully", "success": True}
440 + except Exception as e:
441 + logger.error(e)
442 + raise HTTPException(status_code=400, detail="License replacement failed")
backend/app/routers/license.py new
+13
@@ -0,0 +1,13 @@
1 +from fastapi import APIRouter
2 +
3 +from app.middleware.license import license_router
4 +
5 +# Instantiate the APIRouter
6 +router = APIRouter()
7 +
8 +# Include the License related routes
9 +router.include_router(
10 + license_router,
11 + prefix="/license",
12 + tags=["License"],
13 +)
backend/app/schedulers/routes/scheduler.py
+65
@@ -8,6 +8,7 @@ from sqlalchemy.future import select
8
9 from app.db.db_session import get_db
10 from app.schedulers.models.scheduler import JobMetadata
11 +from app.schedulers.scheduler import get_function_by_name
12 from app.schedulers.scheduler import init_scheduler
13 from app.schedulers.schema.scheduler import JobsResponse
14
@@ -49,6 +50,18 @@ async def manage_job_metadata(session, job_id, action, **kwargs):
50 Returns:
51 JobMetadata: The updated or deleted job metadata.
52 """
53 + if action == "add":
54 + job_metadata = JobMetadata(
55 + job_id=job_id,
56 + last_success=None,
57 + time_interval=kwargs["time_interval"],
58 + enabled=True,
59 + extra_data=kwargs["extra_data"],
60 + )
61 + session.add(job_metadata)
62 + await session.commit()
63 + return job_metadata
64 +
65 job_metadata = await session.execute(select(JobMetadata).filter_by(job_id=job_id))
66 job_metadata = job_metadata.scalars().first()
67
@@ -99,6 +112,49 @@ async def get_all_jobs(session: AsyncSession = Depends(get_db)) -> JobsResponse:
112 )
113
114
115 +@scheduler_router.post("/add", description="Add a job")
116 +async def add_job(
117 + job_id: str,
118 + function_name: str,
119 + time_interval: int,
120 + extra_data: Optional[str] = None,
121 + session: AsyncSession = Depends(get_db),
122 +):
123 + """
124 + Add a job to the scheduler.
125 +
126 + Args:
127 + job_id (str): The ID of the job.
128 + function_name (str): The name of the function to be scheduled.
129 + time_interval (int): The time interval for the job in minutes.
130 + extra_data (str, optional): Additional data to be stored with the job metadata. Defaults to None.
131 + session (AsyncSession, optional): The database session. Defaults to Depends(get_db).
132 +
133 + Returns:
134 + dict: A dictionary containing the success status and a message.
135 + """
136 + scheduler = get_scheduler()
137 + job_function = get_function_by_name(function_name)
138 + scheduler.add_job(
139 + job_function,
140 + "interval",
141 + minutes=time_interval,
142 + id=job_id,
143 + replace_existing=True,
144 + )
145 + await manage_job_metadata(
146 + session,
147 + job_id,
148 + "add",
149 + time_interval=time_interval,
150 + extra_data=extra_data,
151 + )
152 + if not scheduler.running:
153 + scheduler.start()
154 + logger.info(f"Job {job_id} added successfully")
155 + return {"success": True, "message": "Job added successfully"}
156 +
157 +
158 @scheduler_router.post("/start/{job_id}", description="Start a job")
159 async def start_job(job_id: str):
160 """
@@ -181,6 +237,15 @@ async def update_job(
237 extra_data=extra_data,
238 )
239 logger.info(f"Job {job_id} updated successfully")
240 + # Update the job metadata
241 + await manage_job_metadata(
242 + session,
243 + job_id,
244 + "update",
245 + time_interval=time_interval,
246 + extra_data=extra_data,
247 + )
248 +
249 return {"success": True, "message": "Job updated successfully"}
250 logger.error(f"Job {job_id} not found for updating")
251 return {"success": False, "message": "Job not found"}
backend/app/schedulers/scheduler.py
+30 -1
@@ -10,10 +10,31 @@ from app.schedulers.services.agent_sync import agent_sync
10 from app.schedulers.services.invoke_huntress import invoke_huntress_integration_collect
11 from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration
12 from app.schedulers.services.invoke_mimecast import invoke_mimecast_integration_ttp
13 +from app.schedulers.services.invoke_sap_siem import (
14 + invoke_sap_siem_integration_brute_force_failed_logins,
15 +)
16 +from app.schedulers.services.invoke_sap_siem import (
17 + invoke_sap_siem_integration_brute_force_failed_logins_same_ip,
18 +)
19 from app.schedulers.services.invoke_sap_siem import invoke_sap_siem_integration_collect
20 from app.schedulers.services.invoke_sap_siem import (
21 invoke_sap_siem_integration_multiple_logins_same_ip_analysis,
22 )
23 +from app.schedulers.services.invoke_sap_siem import (
24 + invoke_sap_siem_integration_same_user_failed_login_from_different_geo_location,
25 +)
26 +from app.schedulers.services.invoke_sap_siem import (
27 + invoke_sap_siem_integration_same_user_failed_login_from_different_ip,
28 +)
29 +from app.schedulers.services.invoke_sap_siem import (
30 + invoke_sap_siem_integration_same_user_successful_login_from_different_geo_location,
31 +)
32 +from app.schedulers.services.invoke_sap_siem import (
33 + invoke_sap_siem_integration_successful_login_after_multiple_failed_logins,
34 +)
35 +from app.schedulers.services.invoke_sap_siem import (
36 + invoke_sap_siem_integration_successful_user_login_with_different_ip,
37 +)
38 from app.schedulers.services.invoke_sap_siem import (
39 invoke_sap_siem_integration_suspicious_logins_analysis,
40 )
@@ -50,7 +71,7 @@ def initialize_job_metadata():
71 # Implement logic to initialize or update job metadata.
72 # Example: Check and add metadata for each known job
73 known_jobs = [
53 - {"job_id": "agent_sync", "time_interval": 60, "function": agent_sync},
74 + {"job_id": "agent_sync", "time_interval": 15, "function": agent_sync},
75 # {"job_id": "invoke_mimecast_integration", "time_interval": 5, "function": invoke_mimecast_integration}
76 ]
77 for job in known_jobs:
@@ -85,6 +106,7 @@ def schedule_enabled_jobs(scheduler):
106 id=job_metadata.job_id,
107 replace_existing=True,
108 )
109 + logger.info(f"Scheduled job: {job_metadata.job_id}")
110 except ValueError as e:
111 logger.error(f"Error scheduling job: {e}")
112
@@ -104,6 +126,13 @@ def get_function_by_name(function_name: str):
126 "invoke_sap_siem_integration_collection": invoke_sap_siem_integration_collect,
127 "invoke_sap_siem_integration_suspicious_logins_analysis": invoke_sap_siem_integration_suspicious_logins_analysis,
128 "invoke_sap_siem_integration_multiple_logins_same_ip_analysis": invoke_sap_siem_integration_multiple_logins_same_ip_analysis,
129 + "invoke_sap_siem_integration_successful_user_login_with_different_ip": invoke_sap_siem_integration_successful_user_login_with_different_ip,
130 + "invoke_sap_siem_integration_same_user_failed_login_from_different_ip": invoke_sap_siem_integration_same_user_failed_login_from_different_ip,
131 + "invoke_sap_siem_integration_same_user_failed_login_from_different_geo_location": invoke_sap_siem_integration_same_user_failed_login_from_different_geo_location,
132 + "invoke_sap_siem_integration_same_user_successful_login_from_different_geo_location": invoke_sap_siem_integration_same_user_successful_login_from_different_geo_location,
133 + "invoke_sap_siem_integration_brute_force_failed_logins": invoke_sap_siem_integration_brute_force_failed_logins,
134 + "invoke_sap_siem_integration_brute_force_failed_logins_same_ip": invoke_sap_siem_integration_brute_force_failed_logins_same_ip,
135 + "invoke_sap_siem_integration_successful_login_after_multiple_failed_logins": invoke_sap_siem_integration_successful_login_after_multiple_failed_logins,
136 "invoke_huntress_integration_collection": invoke_huntress_integration_collect,
137 # Add other function mappings here
138 }
backend/app/schedulers/services/agent_sync.py
+5 -19
@@ -1,17 +1,16 @@
1 -import os
1 from datetime import datetime
2
4 -import requests
3 from dotenv import load_dotenv
4
5 +from app.agents.routes.agents import sync_all_agents
6 +from app.db.db_session import get_db_session
7 from app.db.db_session import get_sync_db_session
8 from app.schedulers.models.scheduler import JobMetadata
9 -from app.schedulers.utils.universal import scheduler_login
9
10 load_dotenv()
11
12
14 -def agent_sync():
13 +async def agent_sync():
14 """
15 Synchronizes agents by sending a request to the server and updating the job metadata.
16
@@ -21,21 +20,8 @@ def agent_sync():
20 If the token retrieval fails, it prints a failure message. If the job metadata for
21 'agent_sync' does not exist, it prints a message indicating the absence of the metadata.
22 """
24 - # Get the scheduler auth token
25 - headers = scheduler_login()
26 -
27 - # Check if the token was successfully retrieved
28 - if headers:
29 - # Your actual task
30 - response = requests.post(
31 - f"http://{os.getenv('SERVER_IP')}:5000/agents/sync",
32 - headers=headers,
33 - )
34 -
35 - # Process the response here if needed
36 - print(response.json())
37 - else:
38 - print("Failed to retrieve token")
23 + async with get_db_session() as session:
24 + await sync_all_agents(session=session)
25
26 # Use get_sync_db_session to create and manage a synchronous session
27 with get_sync_db_session() as session:
backend/app/schedulers/services/invoke_sap_siem.py
+372 -1
@@ -14,6 +14,27 @@ from app.integrations.monitoring_alert.routes.monitoring_alert import (
14 run_sap_siem_suspicious_logins_analysis,
15 )
16 from app.integrations.sap_siem.routes.sap_siem import collect_sap_siem_route
17 +from app.integrations.sap_siem.routes.sap_siem import (
18 + invoke_sap_siem_brute_force_failed_logins_route,
19 +)
20 +from app.integrations.sap_siem.routes.sap_siem import (
21 + invoke_sap_siem_brute_force_failed_logins_same_ip_route,
22 +)
23 +from app.integrations.sap_siem.routes.sap_siem import (
24 + invoke_sap_siem_same_user_failed_login_from_different_geo_location_route,
25 +)
26 +from app.integrations.sap_siem.routes.sap_siem import (
27 + invoke_sap_siem_same_user_failed_login_from_different_ip_route,
28 +)
29 +from app.integrations.sap_siem.routes.sap_siem import (
30 + invoke_sap_siem_same_user_successful_login_from_different_geo_location_route,
31 +)
32 +from app.integrations.sap_siem.routes.sap_siem import (
33 + invoke_sap_siem_successful_login_after_multiple_failed_logins_route,
34 +)
35 +from app.integrations.sap_siem.routes.sap_siem import (
36 + invoke_sap_siem_successful_user_login_with_different_ip_route,
37 +)
38 from app.integrations.sap_siem.schema.sap_siem import InvokeSapSiemRequest
39 from app.integrations.sap_siem.schema.sap_siem import InvokeSAPSiemResponse
40 from app.schedulers.models.scheduler import JobMetadata
@@ -111,9 +132,17 @@ async def invoke_sap_siem_integration_multiple_logins_same_ip_analysis() -> Invo
132 logger.info(f"customer_codes: {customer_codes}")
133 for customer_code in customer_codes:
134 extra_data = (await get_scheduled_job_metadata("invoke_sap_siem_integration_multiple_logins_same_ip_analysis")).extra_data
114 - threshold = int(extra_data) if extra_data is not None else 1
135 + if extra_data is not None:
136 + data_parts = extra_data.split(",")
137 + for part in data_parts:
138 + key, value = part.split("=")
139 + if key == "threshold":
140 + threshold = int(value)
141 + elif key == "time_range":
142 + time_range = int(value)
143 await run_sap_siem_multiple_logins_same_ip_analysis(
144 threshold=threshold,
145 + time_range=time_range,
146 session=session,
147 )
148 # Close the session
@@ -132,3 +161,345 @@ async def invoke_sap_siem_integration_multiple_logins_same_ip_analysis() -> Invo
161 print("JobMetadata for 'invoke_sap_siem_integration_multiple_logins_same_ip_analysis' not found.")
162
163 return InvokeSAPSiemResponse(success=True, message="SAP SIEM integration invoked for multiple logins from the same IP analysis.")
164 +
165 +
166 +async def invoke_sap_siem_integration_successful_user_login_with_different_ip() -> InvokeSAPSiemResponse:
167 + """
168 + Invokes the SAP SIEM integration for successful user login with different IP.
169 + """
170 + logger.info("Invoking SAP SIEM integration for successful user login with different IP scheduled job.")
171 + customer_codes = []
172 + async with get_db_session() as session:
173 + stmt = select(CustomerIntegrations).where(
174 + CustomerIntegrations.integration_service_name == "SAP SIEM",
175 + )
176 + result = await session.execute(stmt)
177 + customer_codes = [row.customer_code for row in result.scalars()]
178 + logger.info(f"customer_codes: {customer_codes}")
179 + for customer_code in customer_codes:
180 + extra_data = (await get_scheduled_job_metadata("invoke_sap_siem_integration_multiple_logins_same_ip_analysis")).extra_data
181 + if extra_data is not None:
182 + data_parts = extra_data.split(",")
183 + for part in data_parts:
184 + key, value = part.split("=")
185 + if key == "threshold":
186 + threshold = int(value)
187 + elif key == "time_range":
188 + time_range = int(value)
189 + await invoke_sap_siem_successful_user_login_with_different_ip_route(
190 + threshold=threshold,
191 + time_range=time_range,
192 + session=session,
193 + )
194 + # Close the session
195 + await session.close()
196 + with get_sync_db_session() as session:
197 + # Synchronous ORM operations
198 + job_metadata = (
199 + session.query(JobMetadata).filter_by(job_id="invoke_sap_siem_integration_successful_user_login_with_different_ip").one_or_none()
200 + )
201 + if job_metadata:
202 + job_metadata.last_success = datetime.utcnow()
203 + session.add(job_metadata)
204 + session.commit()
205 + else:
206 + # Handle the case where job_metadata does not exist
207 + print("JobMetadata for 'invoke_sap_siem_integration_successful_user_login_with_different_ip' not found.")
208 +
209 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM integration invoked for successful user login with different IP.")
210 +
211 +
212 +async def invoke_sap_siem_integration_same_user_failed_login_from_different_ip() -> InvokeSAPSiemResponse:
213 + """
214 + Invokes the SAP SIEM integration for same user failed login from different IP.
215 + """
216 + logger.info("Invoking SAP SIEM integration for same user failed login from different IP scheduled job.")
217 + customer_codes = []
218 + async with get_db_session() as session:
219 + stmt = select(CustomerIntegrations).where(
220 + CustomerIntegrations.integration_service_name == "SAP SIEM",
221 + )
222 + result = await session.execute(stmt)
223 + customer_codes = [row.customer_code for row in result.scalars()]
224 + logger.info(f"customer_codes: {customer_codes}")
225 + for customer_code in customer_codes:
226 + extra_data = (
227 + await get_scheduled_job_metadata("invoke_sap_siem_integration_same_user_failed_login_from_different_ip")
228 + ).extra_data
229 + if extra_data is not None:
230 + data_parts = extra_data.split(",")
231 + for part in data_parts:
232 + key, value = part.split("=")
233 + if key == "threshold":
234 + threshold = int(value)
235 + elif key == "time_range":
236 + time_range = int(value)
237 + await invoke_sap_siem_same_user_failed_login_from_different_ip_route(
238 + threshold=threshold,
239 + time_range=time_range,
240 + session=session,
241 + )
242 + # Close the session
243 + await session.close()
244 + with get_sync_db_session() as session:
245 + # Synchronous ORM operations
246 + job_metadata = (
247 + session.query(JobMetadata)
248 + .filter_by(job_id="invoke_sap_siem_integration_same_user_failed_login_from_different_ip")
249 + .one_or_none()
250 + )
251 + if job_metadata:
252 + job_metadata.last_success = datetime.utcnow()
253 + session.add(job_metadata)
254 + session.commit()
255 + else:
256 + # Handle the case where job_metadata does not exist
257 + print("JobMetadata for 'invoke_sap_siem_integration_same_user_failed_login_from_different_ip' not found.")
258 +
259 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM integration invoked for same user failed login from different IP.")
260 +
261 +
262 +async def invoke_sap_siem_integration_same_user_failed_login_from_different_geo_location() -> InvokeSAPSiemResponse:
263 + """
264 + Invokes the SAP SIEM integration for same user failed login from different geo location.
265 + """
266 + logger.info("Invoking SAP SIEM integration for same user failed login from different geo location scheduled job.")
267 + customer_codes = []
268 + async with get_db_session() as session:
269 + stmt = select(CustomerIntegrations).where(
270 + CustomerIntegrations.integration_service_name == "SAP SIEM",
271 + )
272 + result = await session.execute(stmt)
273 + customer_codes = [row.customer_code for row in result.scalars()]
274 + logger.info(f"customer_codes: {customer_codes}")
275 + for customer_code in customer_codes:
276 + extra_data = (
277 + await get_scheduled_job_metadata("invoke_sap_siem_integration_same_user_failed_login_from_different_geo_location")
278 + ).extra_data
279 + if extra_data is not None:
280 + data_parts = extra_data.split(",")
281 + for part in data_parts:
282 + key, value = part.split("=")
283 + if key == "threshold":
284 + threshold = int(value)
285 + elif key == "time_range":
286 + time_range = int(value)
287 + await invoke_sap_siem_same_user_failed_login_from_different_geo_location_route(
288 + threshold=threshold,
289 + time_range=time_range,
290 + session=session,
291 + )
292 + # Close the session
293 + await session.close()
294 + with get_sync_db_session() as session:
295 + # Synchronous ORM operations
296 + job_metadata = (
297 + session.query(JobMetadata)
298 + .filter_by(job_id="invoke_sap_siem_integration_same_user_failed_login_from_different_geo_location")
299 + .one_or_none()
300 + )
301 + if job_metadata:
302 + job_metadata.last_success = datetime.utcnow()
303 + session.add(job_metadata)
304 + session.commit()
305 + else:
306 + # Handle the case where job_metadata does not exist
307 + print("JobMetadata for 'invoke_sap_siem_integration_same_user_failed_login_from_different_geo_location' not found.")
308 +
309 + return InvokeSAPSiemResponse(
310 + success=True,
311 + message="SAP SIEM integration invoked for same user failed login from different geo location.",
312 + )
313 +
314 +
315 +async def invoke_sap_siem_integration_same_user_successful_login_from_different_geo_location() -> InvokeSAPSiemResponse:
316 + """
317 + Invokes the SAP SIEM integration for same user successful login from different geo location.
318 + """
319 + logger.info("Invoking SAP SIEM integration for same user successful login from different geo location scheduled job.")
320 + customer_codes = []
321 + async with get_db_session() as session:
322 + stmt = select(CustomerIntegrations).where(
323 + CustomerIntegrations.integration_service_name == "SAP SIEM",
324 + )
325 + result = await session.execute(stmt)
326 + customer_codes = [row.customer_code for row in result.scalars()]
327 + logger.info(f"customer_codes: {customer_codes}")
328 + for customer_code in customer_codes:
329 + extra_data = (
330 + await get_scheduled_job_metadata("invoke_sap_siem_integration_same_user_successful_login_from_different_geo_location")
331 + ).extra_data
332 + if extra_data is not None:
333 + data_parts = extra_data.split(",")
334 + for part in data_parts:
335 + key, value = part.split("=")
336 + if key == "threshold":
337 + threshold = int(value)
338 + elif key == "time_range":
339 + time_range = int(value)
340 + await invoke_sap_siem_same_user_successful_login_from_different_geo_location_route(
341 + threshold=threshold,
342 + time_range=time_range,
343 + session=session,
344 + )
345 + # Close the session
346 + await session.close()
347 + with get_sync_db_session() as session:
348 + # Synchronous ORM operations
349 + job_metadata = (
350 + session.query(JobMetadata)
351 + .filter_by(job_id="invoke_sap_siem_integration_same_user_successful_login_from_different_geo_location")
352 + .one_or_none()
353 + )
354 + if job_metadata:
355 + job_metadata.last_success = datetime.utcnow()
356 + session.add(job_metadata)
357 + session.commit()
358 + else:
359 + # Handle the case where job_metadata does not exist
360 + print("JobMetadata for 'invoke_sap_siem_integration_same_user_successful_login_from_different_geo_location' not found.")
361 +
362 + return InvokeSAPSiemResponse(
363 + success=True,
364 + message="SAP SIEM integration invoked for same user successful login from different geo location.",
365 + )
366 +
367 +
368 +async def invoke_sap_siem_integration_brute_force_failed_logins() -> InvokeSAPSiemResponse:
369 + """
370 + Invokes the SAP SIEM integration for brute force failed logins.
371 + """
372 + logger.info("Invoking SAP SIEM integration for brute force failed logins scheduled job.")
373 + customer_codes = []
374 + async with get_db_session() as session:
375 + stmt = select(CustomerIntegrations).where(
376 + CustomerIntegrations.integration_service_name == "SAP SIEM",
377 + )
378 + result = await session.execute(stmt)
379 + customer_codes = [row.customer_code for row in result.scalars()]
380 + logger.info(f"customer_codes: {customer_codes}")
381 + for customer_code in customer_codes:
382 + extra_data = (await get_scheduled_job_metadata("invoke_sap_siem_integration_brute_force_failed_logins")).extra_data
383 + if extra_data is not None:
384 + data_parts = extra_data.split(",")
385 + for part in data_parts:
386 + key, value = part.split("=")
387 + if key == "threshold":
388 + threshold = int(value)
389 + elif key == "time_range":
390 + time_range = int(value)
391 + await invoke_sap_siem_brute_force_failed_logins_route(
392 + threshold=threshold,
393 + time_range=time_range,
394 + session=session,
395 + )
396 + # Close the session
397 + await session.close()
398 + with get_sync_db_session() as session:
399 + # Synchronous ORM operations
400 + job_metadata = session.query(JobMetadata).filter_by(job_id="invoke_sap_siem_integration_brute_force_failed_logins").one_or_none()
401 + if job_metadata:
402 + job_metadata.last_success = datetime.utcnow()
403 + session.add(job_metadata)
404 + session.commit()
405 + else:
406 + # Handle the case where job_metadata does not exist
407 + print("JobMetadata for 'invoke_sap_siem_integration_brute_force_failed_logins' not found.")
408 +
409 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM integration invoked for brute force failed logins.")
410 +
411 +
412 +async def invoke_sap_siem_integration_brute_force_failed_logins_same_ip() -> InvokeSAPSiemResponse:
413 + """
414 + Invokes the SAP SIEM integration for brute force failed logins from the same IP.
415 + """
416 + logger.info("Invoking SAP SIEM integration for brute force failed logins from the same IP scheduled job.")
417 + customer_codes = []
418 + async with get_db_session() as session:
419 + stmt = select(CustomerIntegrations).where(
420 + CustomerIntegrations.integration_service_name == "SAP SIEM",
421 + )
422 + result = await session.execute(stmt)
423 + customer_codes = [row.customer_code for row in result.scalars()]
424 + logger.info(f"customer_codes: {customer_codes}")
425 + for customer_code in customer_codes:
426 + extra_data = (await get_scheduled_job_metadata("invoke_sap_siem_integration_brute_force_failed_logins_same_ip")).extra_data
427 + if extra_data is not None:
428 + data_parts = extra_data.split(",")
429 + for part in data_parts:
430 + key, value = part.split("=")
431 + if key == "threshold":
432 + threshold = int(value)
433 + elif key == "time_range":
434 + time_range = int(value)
435 + await invoke_sap_siem_brute_force_failed_logins_same_ip_route(
436 + threshold=threshold,
437 + time_range=time_range,
438 + session=session,
439 + )
440 + # Close the session
441 + await session.close()
442 + with get_sync_db_session() as session:
443 + # Synchronous ORM operations
444 + job_metadata = (
445 + session.query(JobMetadata).filter_by(job_id="invoke_sap_siem_integration_brute_force_failed_logins_same_ip").one_or_none()
446 + )
447 + if job_metadata:
448 + job_metadata.last_success = datetime.utcnow()
449 + session.add(job_metadata)
450 + session.commit()
451 + else:
452 + # Handle the case where job_metadata does not exist
453 + print("JobMetadata for 'invoke_sap_siem_integration_brute_force_failed_logins_same_ip' not found.")
454 +
455 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM integration invoked for brute force failed logins from the same IP.")
456 +
457 +
458 +async def invoke_sap_siem_integration_successful_login_after_multiple_failed_logins() -> InvokeSAPSiemResponse:
459 + """
460 + Invokes the SAP SIEM integration for successful login after multiple failed logins.
461 + """
462 + logger.info("Invoking SAP SIEM integration for successful login after multiple failed logins scheduled job.")
463 + customer_codes = []
464 + async with get_db_session() as session:
465 + stmt = select(CustomerIntegrations).where(
466 + CustomerIntegrations.integration_service_name == "SAP SIEM",
467 + )
468 + result = await session.execute(stmt)
469 + customer_codes = [row.customer_code for row in result.scalars()]
470 + logger.info(f"customer_codes: {customer_codes}")
471 + for customer_code in customer_codes:
472 + extra_data = (
473 + await get_scheduled_job_metadata("invoke_sap_siem_integration_successful_login_after_multiple_failed_logins")
474 + ).extra_data
475 + if extra_data is not None:
476 + data_parts = extra_data.split(",")
477 + for part in data_parts:
478 + key, value = part.split("=")
479 + if key == "threshold":
480 + threshold = int(value)
481 + elif key == "time_range":
482 + time_range = int(value)
483 + await invoke_sap_siem_successful_login_after_multiple_failed_logins_route(
484 + threshold=threshold,
485 + time_range=time_range,
486 + session=session,
487 + )
488 + # Close the session
489 + await session.close()
490 + with get_sync_db_session() as session:
491 + # Synchronous ORM operations
492 + job_metadata = (
493 + session.query(JobMetadata)
494 + .filter_by(job_id="invoke_sap_siem_integration_successful_login_after_multiple_failed_logins")
495 + .one_or_none()
496 + )
497 + if job_metadata:
498 + job_metadata.last_success = datetime.utcnow()
499 + session.add(job_metadata)
500 + session.commit()
501 + else:
502 + # Handle the case where job_metadata does not exist
503 + print("JobMetadata for 'invoke_sap_siem_integration_successful_login_after_multiple_failed_logins' not found.")
504 +
505 + return InvokeSAPSiemResponse(success=True, message="SAP SIEM integration invoked for successful login after multiple failed logins.")
backend/app/utils.py
+58
@@ -782,6 +782,64 @@ async def verify_wazuh_worker_provisioning_connection(connector_name: str) -> st
782 return await verify_wazuh_worker_provisioning_healtcheck(attributes)
783
784
785 +################## ! HAPROXY Provisioning App ! ##################
786 +################## ! https://github.com/socfortress/Customer-Provisioning-Worker ! ##################
787 +async def verify_haproxy_provisioning_healtcheck(
788 + attributes: Dict[str, Any],
789 +) -> Dict[str, Any]:
790 + """
791 + Verifies the connection to HAPROXY Provisioning service.
792 +
793 + Returns:
794 + dict: A dictionary containing 'connectionSuccessful' status.
795 + """
796 + logger.info(
797 + f"Verifying the HAPROXY provisioning connection to {attributes['connector_url']}",
798 + )
799 +
800 + try:
801 + wazuh_worker_provisioning_healthcheck = requests.get(
802 + f"{attributes['connector_url']}/provision_worker/healthcheck",
803 + verify=False,
804 + )
805 +
806 + if wazuh_worker_provisioning_healthcheck.status_code == 200:
807 + return {
808 + "connectionSuccessful": True,
809 + "message": "Wazuh Worker Provisioning healthcheck successful",
810 + }
811 + else:
812 + logger.error(
813 + f"Connection to {attributes['connector_url']} failed with error: {wazuh_worker_provisioning_healthcheck.text}",
814 + )
815 +
816 + return {
817 + "connectionSuccessful": False,
818 + "message": f"Connection to {attributes['connector_url']} failed",
819 + }
820 + except Exception as e:
821 + logger.error(
822 + f"Connection to {attributes['connector_url']} failed with error: {e}",
823 + )
824 +
825 + return {
826 + "connectionSuccessful": False,
827 + "message": f"Connection to {attributes['connector_url']} failed with error.",
828 + }
829 +
830 +
831 +async def verify_haproxy_provisioning_connection(connector_name: str) -> str:
832 + """
833 + Returns the status of the connection to HAPROXY Provisioning service.
834 + """
835 + async with get_db_session() as session: # This will correctly enter the context manager
836 + attributes = await get_connector_info_from_db(connector_name, session)
837 + if attributes is None:
838 + logger.error("No HAPROXY Provisioning connector found in the database")
839 + return None
840 + return await verify_haproxy_provisioning_healtcheck(attributes)
841 +
842 +
843 ################## ! Alert Creation Provisioning App ! ##################
844 ################## ! https://github.com/socfortress/Customer-Provisioning-Alert ! ##################
845 async def verify_alert_creation_provisioning_healtcheck(
backend/copilot.py
+2
@@ -40,6 +40,7 @@ from app.routers import healthcheck
40 from app.routers import huntress
41 from app.routers import influxdb
42 from app.routers import integrations
43 +from app.routers import license
44 from app.routers import logs
45 from app.routers import mimecast
46 from app.routers import monitoring_alert
@@ -123,6 +124,7 @@ api_router.include_router(sap_siem.router)
124 api_router.include_router(stack_provisioning.router)
125 api_router.include_router(active_response.router)
126 api_router.include_router(huntress.router)
127 +api_router.include_router(license.router)
128
129 # Include the APIRouter in the FastAPI app
130 app.include_router(api_router)
backend/requirements.txt
+4
@@ -62,11 +62,13 @@ iniconfig==2.0.0
62 isoduration==20.11.0
63 itsdangerous==2.1.2
64 Jinja2==3.1.2
65 +Jinja2==3.1.2
66 jsonpointer==2.4
67 jsonschema==4.17.3
68 kiwisolver==1.4.5
69 kombu==5.3.2
70 libmagic==1.0
71 +licensing==0.43
72 loguru==0.7.2
73 lxml==4.9.3
74 maec==4.1.0.17
@@ -90,6 +92,7 @@ packaging==23.2
92 pandas==2.1.1
93 passlib==1.7.4
94 pcre2==0.3.0
95 +pdfkit==1.0.0
96 pika==1.3.2
97 Pillow==10.0.1
98 pip==22.3.1
@@ -112,6 +115,7 @@ PyJWT==2.8.0
115 pyparsing==3.1.1
116 pyrsistent==0.19.3
117 pytest==7.4.2
118 +pytest-playwright==0.4.4
119 python-dateutil==2.8.2
120 python-dotenv==1.0.0
121 python-jose==3.3.0
frontend/package-lock.json
+890 -670
@@ -10,38 +10,40 @@
10 "dependencies": {
11 "@ajoelp/json-to-formdata": "^1.5.0",
12 "@f3ve/vue-markdown-it": "^0.2.0",
13 - "@fontsource/jetbrains-mono": "^5.0.18",
14 - "@fontsource/lexend": "^5.0.18",
15 - "@fontsource/public-sans": "^5.0.16",
13 + "@fontsource/jetbrains-mono": "^5.0.19",
14 + "@fontsource/lexend": "^5.0.19",
15 + "@fontsource/public-sans": "^5.0.17",
16 "@popperjs/core": "^2.11.8",
17 - "@vueuse/components": "^10.8.0",
18 - "@vueuse/core": "^10.8.0",
19 - "apexcharts": "^3.46.0",
17 + "@vueuse/components": "^10.9.0",
18 + "@vueuse/core": "^10.9.0",
19 + "apexcharts": "^3.47.0",
20 "bytes": "^3.1.2",
21 "colord": "^2.9.3",
22 "crypto-js": "^4.2.0",
23 "dayjs": "^1.11.10",
24 "detect-touch-device": "^1.1.6",
25 "echarts": "^5.5.0",
26 - "jose": "^5.2.2",
26 + "file-saver": "^2.0.5",
27 + "jose": "^5.2.3",
28 "js-md5": "^0.8.3",
29 "lodash": "^4.17.21",
30 "markdown-it-highlightjs": "^4.0.1",
31 "mitt": "^3.0.1",
31 - "naive-ui": "^2.38.0",
32 + "naive-ui": "^2.38.1",
33 "password-validator": "^5.3.0",
34 "pinia": "^2.1.7",
35 "pinia-plugin-persistedstate": "^3.2.1",
36 "secure-ls": "^1.2.6",
37 "validator": "^13.11.0",
37 - "vue": "^3.4.19",
38 + "vue": "^3.4.21",
39 "vue-advanced-cropper": "^2.8.8",
40 "vue-highlight-words": "^3.0.1",
40 - "vue-i18n": "^9.9.1",
41 + "vue-i18n": "^9.10.1",
42 "vue-router": "^4.3.0",
43 "vue-sjv": "^0.0.6",
44 "vue3-apexcharts": "^1.5.2",
44 - "vue3-marquee": "^4.2.0"
45 + "vue3-marquee": "^4.2.0",
46 + "vuedraggable": "^4.1.0"
47 },
48 "devDependencies": {
49 "@clack/prompts": "^0.7.0",
@@ -49,26 +51,28 @@
51 "@rushstack/eslint-patch": "^1.7.2",
52 "@tsconfig/node18": "^18.2.2",
53 "@types/bytes": "^3.1.4",
54 + "@types/file-saver": "^2.0.7",
55 "@types/fs-extra": "^11.0.4",
56 "@types/highlight.js": "^10.1.0",
57 + "@types/html2canvas": "^1.0.0",
58 "@types/inquirer": "^9.0.7",
59 "@types/jsdom": "^21.1.6",
56 - "@types/lodash": "^4.14.202",
60 + "@types/lodash": "^4.17.0",
61 "@types/markdown-it": "^13.0.7",
62 "@types/markdown-it-highlightjs": "^3.3.4",
59 - "@types/node": "^20.11.20",
63 + "@types/node": "^20.11.27",
64 "@types/validator": "^13.11.9",
65 "@vitejs/plugin-vue": "^5.0.4",
66 "@vitejs/plugin-vue-jsx": "^3.1.0",
67 "@vue/eslint-config-prettier": "^9.0.0",
64 - "@vue/eslint-config-typescript": "^12.0.0",
65 - "@vue/test-utils": "^2.4.4",
68 + "@vue/eslint-config-typescript": "^13.0.0",
69 + "@vue/test-utils": "^2.4.5",
70 "@vue/tsconfig": "^0.5.1",
67 - "autoprefixer": "^10.4.17",
68 - "cypress": "^13.6.6",
69 - "eslint": "^8.56.0",
71 + "autoprefixer": "^10.4.18",
72 + "cypress": "^13.7.0",
73 + "eslint": "^8.57.0",
74 "eslint-plugin-cypress": "^2.15.1",
71 - "eslint-plugin-vue": "^9.22.0",
75 + "eslint-plugin-vue": "^9.23.0",
76 "fs-extra": "^11.2.0",
77 "ip": "^2.0.1",
78 "jsdom": "^24.0.0",
@@ -77,20 +81,18 @@
81 "picocolors": "^1.0.0",
82 "postcss": "^8.4.35",
83 "prettier": "^3.2.5",
80 - "sass": "^1.71.1",
84 + "sass": "^1.72.0",
85 "start-server-and-test": "^2.0.3",
86 "tailwind-config-viewer": "^1.7.3",
87 "tailwindcss": "^3.4.1",
88 "taze": "^0.13.3",
85 - "ts-node": "^10.9.2",
86 - "typescript": "~5.3.3",
89 "unplugin-vue-components": "^0.26.0",
88 - "vite": "^5.1.4",
89 - "vite-bundle-analyzer": "^0.8.0",
90 - "vite-bundle-visualizer": "^1.0.1",
90 + "vite": "^5.1.6",
91 + "vite-bundle-analyzer": "^0.8.3",
92 + "vite-bundle-visualizer": "^1.1.0",
93 "vite-svg-loader": "^5.1.0",
94 "vitest": "^1.3.1",
93 - "vue-tsc": "^1.8.27"
95 + "vue-tsc": "^2.0.6"
96 },
97 "engines": {
98 "node": ">=18.0.0"
@@ -125,13 +127,13 @@
127 }
128 },
129 "node_modules/@ampproject/remapping": {
128 - "version": "2.2.1",
129 - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
130 - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==",
130 + "version": "2.3.0",
131 + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
132 + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
133 "dev": true,
134 "dependencies": {
133 - "@jridgewell/gen-mapping": "^0.3.0",
134 - "@jridgewell/trace-mapping": "^0.3.9"
135 + "@jridgewell/gen-mapping": "^0.3.5",
136 + "@jridgewell/trace-mapping": "^0.3.24"
137 },
138 "engines": {
139 "node": ">=6.0.0"
@@ -183,9 +185,9 @@
185 }
186 },
187 "node_modules/@babel/core": {
186 - "version": "7.23.9",
187 - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz",
188 - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==",
188 + "version": "7.24.0",
189 + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.0.tgz",
190 + "integrity": "sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==",
191 "dev": true,
192 "dependencies": {
193 "@ampproject/remapping": "^2.2.0",
@@ -193,11 +195,11 @@
195 "@babel/generator": "^7.23.6",
196 "@babel/helper-compilation-targets": "^7.23.6",
197 "@babel/helper-module-transforms": "^7.23.3",
196 - "@babel/helpers": "^7.23.9",
197 - "@babel/parser": "^7.23.9",
198 - "@babel/template": "^7.23.9",
199 - "@babel/traverse": "^7.23.9",
200 - "@babel/types": "^7.23.9",
198 + "@babel/helpers": "^7.24.0",
199 + "@babel/parser": "^7.24.0",
200 + "@babel/template": "^7.24.0",
201 + "@babel/traverse": "^7.24.0",
202 + "@babel/types": "^7.24.0",
203 "convert-source-map": "^2.0.0",
204 "debug": "^4.1.0",
205 "gensync": "^1.0.0-beta.2",
@@ -274,9 +276,9 @@
276 }
277 },
278 "node_modules/@babel/helper-create-class-features-plugin": {
277 - "version": "7.23.10",
278 - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.10.tgz",
279 - "integrity": "sha512-2XpP2XhkXzgxecPNEEK8Vz8Asj9aRxt08oKOqtiZoqV2UGZ5T+EkyP9sXQ9nwMxBIG34a7jmasVqoMop7VdPUw==",
279 + "version": "7.24.0",
280 + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.0.tgz",
281 + "integrity": "sha512-QAH+vfvts51BCsNZ2PhY6HAggnlS6omLLFTsIpeqZk/MmJ6cW7tgz5yRv0fMJThcr6FmbMrENh1RgrWPTYA76g==",
282 "dev": true,
283 "dependencies": {
284 "@babel/helper-annotate-as-pure": "^7.22.5",
@@ -395,9 +397,9 @@
397 }
398 },
399 "node_modules/@babel/helper-plugin-utils": {
398 - "version": "7.22.5",
399 - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
400 - "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
400 + "version": "7.24.0",
401 + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.0.tgz",
402 + "integrity": "sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==",
403 "dev": true,
404 "engines": {
405 "node": ">=6.9.0"
@@ -484,14 +486,14 @@
486 }
487 },
488 "node_modules/@babel/helpers": {
487 - "version": "7.23.9",
488 - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz",
489 - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==",
489 + "version": "7.24.0",
490 + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.0.tgz",
491 + "integrity": "sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==",
492 "dev": true,
493 "dependencies": {
492 - "@babel/template": "^7.23.9",
493 - "@babel/traverse": "^7.23.9",
494 - "@babel/types": "^7.23.9"
494 + "@babel/template": "^7.24.0",
495 + "@babel/traverse": "^7.24.0",
496 + "@babel/types": "^7.24.0"
497 },
498 "engines": {
499 "node": ">=6.9.0"
@@ -512,9 +514,9 @@
514 }
515 },
516 "node_modules/@babel/parser": {
515 - "version": "7.23.9",
516 - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz",
517 - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==",
517 + "version": "7.24.0",
518 + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz",
519 + "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==",
520 "bin": {
521 "parser": "bin/babel-parser.js"
522 },
@@ -571,9 +573,9 @@
573 }
574 },
575 "node_modules/@babel/runtime": {
574 - "version": "7.23.9",
575 - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz",
576 - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==",
576 + "version": "7.24.0",
577 + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
578 + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
579 "dependencies": {
580 "regenerator-runtime": "^0.14.0"
581 },
@@ -582,23 +584,23 @@
584 }
585 },
586 "node_modules/@babel/template": {
585 - "version": "7.23.9",
586 - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz",
587 - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==",
587 + "version": "7.24.0",
588 + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz",
589 + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==",
590 "dev": true,
591 "dependencies": {
592 "@babel/code-frame": "^7.23.5",
591 - "@babel/parser": "^7.23.9",
592 - "@babel/types": "^7.23.9"
593 + "@babel/parser": "^7.24.0",
594 + "@babel/types": "^7.24.0"
595 },
596 "engines": {
597 "node": ">=6.9.0"
598 }
599 },
600 "node_modules/@babel/traverse": {
599 - "version": "7.23.9",
600 - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz",
601 - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==",
601 + "version": "7.24.0",
602 + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.0.tgz",
603 + "integrity": "sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==",
604 "dev": true,
605 "dependencies": {
606 "@babel/code-frame": "^7.23.5",
@@ -607,8 +609,8 @@
609 "@babel/helper-function-name": "^7.23.0",
610 "@babel/helper-hoist-variables": "^7.22.5",
611 "@babel/helper-split-export-declaration": "^7.22.6",
610 - "@babel/parser": "^7.23.9",
611 - "@babel/types": "^7.23.9",
612 + "@babel/parser": "^7.24.0",
613 + "@babel/types": "^7.24.0",
614 "debug": "^4.3.1",
615 "globals": "^11.1.0"
616 },
@@ -626,9 +628,9 @@
628 }
629 },
630 "node_modules/@babel/types": {
629 - "version": "7.23.9",
630 - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz",
631 - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==",
631 + "version": "7.24.0",
632 + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz",
633 + "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==",
634 "dev": true,
635 "dependencies": {
636 "@babel/helper-string-parser": "^7.23.4",
@@ -640,9 +642,9 @@
642 }
643 },
644 "node_modules/@clack/core": {
643 - "version": "0.3.3",
644 - "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.3.tgz",
645 - "integrity": "sha512-5ZGyb75BUBjlll6eOa1m/IZBxwk91dooBWhPSL67sWcLS0zt9SnswRL0l26TVdBhb0wnWORRxUn//uH6n4z7+A==",
645 + "version": "0.3.4",
646 + "resolved": "https://registry.npmjs.org/@clack/core/-/core-0.3.4.tgz",
647 + "integrity": "sha512-H4hxZDXgHtWTwV3RAVenqcC4VbJZNegbBjlPvzOzCouXtS2y3sDvlO3IsbrPNWuLWPPlYVYPghQdSF64683Ldw==",
648 "dev": true,
649 "dependencies": {
650 "picocolors": "^1.0.0",
@@ -685,28 +687,6 @@
687 "node": ">=0.1.90"
688 }
689 },
688 - "node_modules/@cspotcode/source-map-support": {
689 - "version": "0.8.1",
690 - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
691 - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
692 - "dev": true,
693 - "dependencies": {
694 - "@jridgewell/trace-mapping": "0.3.9"
695 - },
696 - "engines": {
697 - "node": ">=12"
698 - }
699 - },
700 - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
701 - "version": "0.3.9",
702 - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
703 - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
704 - "dev": true,
705 - "dependencies": {
706 - "@jridgewell/resolve-uri": "^3.0.3",
707 - "@jridgewell/sourcemap-codec": "^1.4.10"
708 - }
709 - },
690 "node_modules/@css-render/plugin-bem": {
691 "version": "0.15.12",
692 "resolved": "https://registry.npmjs.org/@css-render/plugin-bem/-/plugin-bem-0.15.12.tgz",
@@ -1209,9 +1189,9 @@
1189 }
1190 },
1191 "node_modules/@eslint/js": {
1212 - "version": "8.56.0",
1213 - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
1214 - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
1192 + "version": "8.57.0",
1193 + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
1194 + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
1195 "engines": {
1196 "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
1197 }
@@ -1259,19 +1239,19 @@
1239 }
1240 },
1241 "node_modules/@fontsource/jetbrains-mono": {
1262 - "version": "5.0.18",
1263 - "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.18.tgz",
1264 - "integrity": "sha512-0+YDAaAnCdXjirHFO3NfHLUW8xtKCT5rlm23Q0qG3TpZp3QBrZh5r9ikUxh3ufHc2+fVnk4Y6GWNEnVIfB7u/g=="
1242 + "version": "5.0.19",
1243 + "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.0.19.tgz",
1244 + "integrity": "sha512-SdwUuvdfuAvGWRRc4LOFRSmDrpkE+vFUpCtOIOUl1PpXdLfeU//93BZiGf7j/oFGSZJbHAurfux2uLT38/NIjw=="
1245 },
1246 "node_modules/@fontsource/lexend": {
1267 - "version": "5.0.18",
1268 - "resolved": "https://registry.npmjs.org/@fontsource/lexend/-/lexend-5.0.18.tgz",
1269 - "integrity": "sha512-RcNekPIeQGX5ZvwRtX7UHDoDrGTg8IV2Yae13qjtxW6FO4kFaUKSlITKnrvaK8r8ly/fQ6x2mXva9jmMZPZ4Ug=="
1247 + "version": "5.0.19",
1248 + "resolved": "https://registry.npmjs.org/@fontsource/lexend/-/lexend-5.0.19.tgz",
1249 + "integrity": "sha512-eXUgYzJ+XHrNvcXZLVAJRaOZm1hP6UFQvVG/w411ltb0LUT7TJ1jEXVfwlByylySx8AMdajyej0cjrVj+W8uIg=="
1250 },
1251 "node_modules/@fontsource/public-sans": {
1272 - "version": "5.0.16",
1273 - "resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.0.16.tgz",
1274 - "integrity": "sha512-bThZip6sLRsnfzi/oBr1/9+aWmvHkb59QlLh/OtoAIA0Mi2+Z1cKOxtR1B2ITauljlLJHPRvjpTZDUZitUN8pA=="
1252 + "version": "5.0.17",
1253 + "resolved": "https://registry.npmjs.org/@fontsource/public-sans/-/public-sans-5.0.17.tgz",
1254 + "integrity": "sha512-s5qJhMelUX7faD9wOkBRaR24e9ZB9A/HolfAwMnJ9G+uMvnF7UF2hypBxtb30skeGgGw9LBpFAgk0bCd2v6TXg=="
1255 },
1256 "node_modules/@hapi/hoek": {
1257 "version": "9.3.0",
@@ -1360,12 +1340,12 @@
1340 }
1341 },
1342 "node_modules/@intlify/core-base": {
1363 - "version": "9.9.1",
1364 - "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.9.1.tgz",
1365 - "integrity": "sha512-qsV15dg7jNX2faBRyKMgZS8UcFJViWEUPLdzZ9UR0kQZpFVeIpc0AG7ZOfeP7pX2T9SQ5jSiorq/tii9nkkafA==",
1343 + "version": "9.10.1",
1344 + "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.10.1.tgz",
1345 + "integrity": "sha512-0+Wtjj04GIyglh5KKiNjRwgjpHrhqqGZhaKY/QVjjogWKZq5WHROrTi84pNVsRN18QynyPmjtsVUWqFKPQ45xQ==",
1346 "dependencies": {
1367 - "@intlify/message-compiler": "9.9.1",
1368 - "@intlify/shared": "9.9.1"
1347 + "@intlify/message-compiler": "9.10.1",
1348 + "@intlify/shared": "9.10.1"
1349 },
1350 "engines": {
1351 "node": ">= 16"
@@ -1375,11 +1355,11 @@
1355 }
1356 },
1357 "node_modules/@intlify/message-compiler": {
1378 - "version": "9.9.1",
1379 - "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.9.1.tgz",
1380 - "integrity": "sha512-zTvP6X6HeumHOXuAE1CMMsV6tTX+opKMOxO1OHTCg5N5Sm/F7d8o2jdT6W6L5oHUsJ/vvkGefHIs7Q3hfowmsA==",
1358 + "version": "9.10.1",
1359 + "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.10.1.tgz",
1360 + "integrity": "sha512-b68UTmRhgZfswJZI7VAgW6BXZK5JOpoi5swMLGr4j6ss2XbFY13kiw+Hu+xYAfulMPSapcHzdWHnq21VGnMCnA==",
1361 "dependencies": {
1382 - "@intlify/shared": "9.9.1",
1362 + "@intlify/shared": "9.10.1",
1363 "source-map-js": "^1.0.2"
1364 },
1365 "engines": {
@@ -1390,9 +1370,9 @@
1370 }
1371 },
1372 "node_modules/@intlify/shared": {
1393 - "version": "9.9.1",
1394 - "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.9.1.tgz",
1395 - "integrity": "sha512-b3Pta1nwkz5rGq434v0psHwEwHGy1pYCttfcM22IE//K9owbpkEvFptx9VcuRAxjQdrO2If249cmDDjBu5wMDA==",
1373 + "version": "9.10.1",
1374 + "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.10.1.tgz",
1375 + "integrity": "sha512-liyH3UMoglHBUn70iCYcy9CQlInx/lp50W2aeSxqqrvmG+LDj/Jj7tBJhBoQL4fECkldGhbmW0g2ommHfL6Wmw==",
1376 "engines": {
1377 "node": ">= 16"
1378 },
@@ -1480,32 +1460,32 @@
1460 }
1461 },
1462 "node_modules/@jridgewell/gen-mapping": {
1483 - "version": "0.3.3",
1484 - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
1485 - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==",
1463 + "version": "0.3.5",
1464 + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
1465 + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
1466 "dev": true,
1467 "dependencies": {
1488 - "@jridgewell/set-array": "^1.0.1",
1468 + "@jridgewell/set-array": "^1.2.1",
1469 "@jridgewell/sourcemap-codec": "^1.4.10",
1490 - "@jridgewell/trace-mapping": "^0.3.9"
1470 + "@jridgewell/trace-mapping": "^0.3.24"
1471 },
1472 "engines": {
1473 "node": ">=6.0.0"
1474 }
1475 },
1476 "node_modules/@jridgewell/resolve-uri": {
1497 - "version": "3.1.1",
1498 - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
1499 - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==",
1477 + "version": "3.1.2",
1478 + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
1479 + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
1480 "dev": true,
1481 "engines": {
1482 "node": ">=6.0.0"
1483 }
1484 },
1485 "node_modules/@jridgewell/set-array": {
1506 - "version": "1.1.2",
1507 - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
1508 - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
1486 + "version": "1.2.1",
1487 + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
1488 + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
1489 "dev": true,
1490 "engines": {
1491 "node": ">=6.0.0"
@@ -1517,9 +1497,9 @@
1497 "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
1498 },
1499 "node_modules/@jridgewell/trace-mapping": {
1520 - "version": "0.3.22",
1521 - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz",
1522 - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==",
1500 + "version": "0.3.25",
1501 + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
1502 + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
1503 "dev": true,
1504 "dependencies": {
1505 "@jridgewell/resolve-uri": "^3.1.0",
@@ -1605,9 +1585,9 @@
1585 }
1586 },
1587 "node_modules/@npmcli/config": {
1608 - "version": "8.1.0",
1609 - "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-8.1.0.tgz",
1610 - "integrity": "sha512-61LNEybTFaa9Z/f8y6X9s2Blc75aijZK67LxqC5xicBcfkw8M/88nYrRXGXxAUKm6GRlxTZ216dp1UK2+TbaYw==",
1588 + "version": "8.2.0",
1589 + "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-8.2.0.tgz",
1590 + "integrity": "sha512-YoEYZFg0hRSRP/Chmq+J4FvULFvji6SORUYWQc10FiJ+ReAnViXcDCENg6kM6dID04bAoKNUygrby798+gYBbQ==",
1591 "dev": true,
1592 "dependencies": {
1593 "@npmcli/map-workspaces": "^3.0.2",
@@ -1955,9 +1935,9 @@
1935 }
1936 },
1937 "node_modules/@rollup/rollup-android-arm-eabi": {
1958 - "version": "4.10.0",
1959 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.10.0.tgz",
1960 - "integrity": "sha512-/MeDQmcD96nVoRumKUljsYOLqfv1YFJps+0pTrb2Z9Nl/w5qNUysMaWQsrd1mvAlNT4yza1iVyIu4Q4AgF6V3A==",
1938 + "version": "4.12.0",
1939 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.12.0.tgz",
1940 + "integrity": "sha512-+ac02NL/2TCKRrJu2wffk1kZ+RyqxVUlbjSagNgPm94frxtr+XDL12E5Ll1enWskLrtrZ2r8L3wED1orIibV/w==",
1941 "cpu": [
1942 "arm"
1943 ],
@@ -1968,9 +1948,9 @@
1948 ]
1949 },
1950 "node_modules/@rollup/rollup-android-arm64": {
1971 - "version": "4.10.0",
1972 - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.10.0.tgz",
1973 - "integrity": "sha512-lvu0jK97mZDJdpZKDnZI93I0Om8lSDaiPx3OiCk0RXn3E8CMPJNS/wxjAvSJJzhhZpfjXsjLWL8LnS6qET4VNQ==",
1951 + "version": "4.12.0",
1952 + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.12.0.tgz",
1953 + "integrity": "sha512-OBqcX2BMe6nvjQ0Nyp7cC90cnumt8PXmO7Dp3gfAju/6YwG0Tj74z1vKrfRz7qAv23nBcYM8BCbhrsWqO7PzQQ==",
1954 "cpu": [
1955 "arm64"
1956 ],
@@ -1981,9 +1961,9 @@
1961 ]
1962 },
1963 "node_modules/@rollup/rollup-darwin-arm64": {
1984 - "version": "4.10.0",
1985 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.10.0.tgz",
1986 - "integrity": "sha512-uFpayx8I8tyOvDkD7X6n0PriDRWxcqEjqgtlxnUA/G9oS93ur9aZ8c8BEpzFmsed1TH5WZNG5IONB8IiW90TQg==",
1964 + "version": "4.12.0",
1965 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.12.0.tgz",
1966 + "integrity": "sha512-X64tZd8dRE/QTrBIEs63kaOBG0b5GVEd3ccoLtyf6IdXtHdh8h+I56C2yC3PtC9Ucnv0CpNFJLqKFVgCYe0lOQ==",
1967 "cpu": [
1968 "arm64"
1969 ],
@@ -1994,9 +1974,9 @@
1974 ]
1975 },
1976 "node_modules/@rollup/rollup-darwin-x64": {
1997 - "version": "4.10.0",
1998 - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.10.0.tgz",
1999 - "integrity": "sha512-nIdCX03qFKoR/MwQegQBK+qZoSpO3LESurVAC6s6jazLA1Mpmgzo3Nj3H1vydXp/JM29bkCiuF7tDuToj4+U9Q==",
1977 + "version": "4.12.0",
1978 + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.12.0.tgz",
1979 + "integrity": "sha512-cc71KUZoVbUJmGP2cOuiZ9HSOP14AzBAThn3OU+9LcA1+IUqswJyR1cAJj3Mg55HbjZP6OLAIscbQsQLrpgTOg==",
1980 "cpu": [
1981 "x64"
1982 ],
@@ -2007,9 +1987,9 @@
1987 ]
1988 },
1989 "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
2010 - "version": "4.10.0",
2011 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.10.0.tgz",
2012 - "integrity": "sha512-Fz7a+y5sYhYZMQFRkOyCs4PLhICAnxRX/GnWYReaAoruUzuRtcf+Qnw+T0CoAWbHCuz2gBUwmWnUgQ67fb3FYw==",
1990 + "version": "4.12.0",
1991 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.12.0.tgz",
1992 + "integrity": "sha512-a6w/Y3hyyO6GlpKL2xJ4IOh/7d+APaqLYdMf86xnczU3nurFTaVN9s9jOXQg97BE4nYm/7Ga51rjec5nfRdrvA==",
1993 "cpu": [
1994 "arm"
1995 ],
@@ -2020,9 +2000,9 @@
2000 ]
2001 },
2002 "node_modules/@rollup/rollup-linux-arm64-gnu": {
2023 - "version": "4.10.0",
2024 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.10.0.tgz",
2025 - "integrity": "sha512-yPtF9jIix88orwfTi0lJiqINnlWo6p93MtZEoaehZnmCzEmLL0eqjA3eGVeyQhMtxdV+Mlsgfwhh0+M/k1/V7Q==",
2003 + "version": "4.12.0",
2004 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.12.0.tgz",
2005 + "integrity": "sha512-0fZBq27b+D7Ar5CQMofVN8sggOVhEtzFUwOwPppQt0k+VR+7UHMZZY4y+64WJ06XOhBTKXtQB/Sv0NwQMXyNAA==",
2006 "cpu": [
2007 "arm64"
2008 ],
@@ -2033,9 +2013,9 @@
2013 ]
2014 },
2015 "node_modules/@rollup/rollup-linux-arm64-musl": {
2036 - "version": "4.10.0",
2037 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.10.0.tgz",
2038 - "integrity": "sha512-9GW9yA30ib+vfFiwjX+N7PnjTnCMiUffhWj4vkG4ukYv1kJ4T9gHNg8zw+ChsOccM27G9yXrEtMScf1LaCuoWQ==",
2016 + "version": "4.12.0",
2017 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.12.0.tgz",
2018 + "integrity": "sha512-eTvzUS3hhhlgeAv6bfigekzWZjaEX9xP9HhxB0Dvrdbkk5w/b+1Sxct2ZuDxNJKzsRStSq1EaEkVSEe7A7ipgQ==",
2019 "cpu": [
2020 "arm64"
2021 ],
@@ -2046,9 +2026,9 @@
2026 ]
2027 },
2028 "node_modules/@rollup/rollup-linux-riscv64-gnu": {
2049 - "version": "4.10.0",
2050 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.10.0.tgz",
2051 - "integrity": "sha512-X1ES+V4bMq2ws5fF4zHornxebNxMXye0ZZjUrzOrf7UMx1d6wMQtfcchZ8SqUnQPPHdOyOLW6fTcUiFgHFadRA==",
2029 + "version": "4.12.0",
2030 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.12.0.tgz",
2031 + "integrity": "sha512-ix+qAB9qmrCRiaO71VFfY8rkiAZJL8zQRXveS27HS+pKdjwUfEhqo2+YF2oI+H/22Xsiski+qqwIBxVewLK7sw==",
2032 "cpu": [
2033 "riscv64"
2034 ],
@@ -2059,9 +2039,9 @@
2039 ]
2040 },
2041 "node_modules/@rollup/rollup-linux-x64-gnu": {
2062 - "version": "4.10.0",
2063 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.10.0.tgz",
2064 - "integrity": "sha512-w/5OpT2EnI/Xvypw4FIhV34jmNqU5PZjZue2l2Y3ty1Ootm3SqhI+AmfhlUYGBTd9JnpneZCDnt3uNOiOBkMyw==",
2042 + "version": "4.12.0",
2043 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.12.0.tgz",
2044 + "integrity": "sha512-TenQhZVOtw/3qKOPa7d+QgkeM6xY0LtwzR8OplmyL5LrgTWIXpTQg2Q2ycBf8jm+SFW2Wt/DTn1gf7nFp3ssVA==",
2045 "cpu": [
2046 "x64"
2047 ],
@@ -2072,9 +2052,9 @@
2052 ]
2053 },
2054 "node_modules/@rollup/rollup-linux-x64-musl": {
2075 - "version": "4.10.0",
2076 - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.10.0.tgz",
2077 - "integrity": "sha512-q/meftEe3QlwQiGYxD9rWwB21DoKQ9Q8wA40of/of6yGHhZuGfZO0c3WYkN9dNlopHlNT3mf5BPsUSxoPuVQaw==",
2055 + "version": "4.12.0",
2056 + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.12.0.tgz",
2057 + "integrity": "sha512-LfFdRhNnW0zdMvdCb5FNuWlls2WbbSridJvxOvYWgSBOYZtgBfW9UGNJG//rwMqTX1xQE9BAodvMH9tAusKDUw==",
2058 "cpu": [
2059 "x64"
2060 ],
@@ -2085,9 +2065,9 @@
2065 ]
2066 },
2067 "node_modules/@rollup/rollup-win32-arm64-msvc": {
2088 - "version": "4.10.0",
2089 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.10.0.tgz",
2090 - "integrity": "sha512-NrR6667wlUfP0BHaEIKgYM/2va+Oj+RjZSASbBMnszM9k+1AmliRjHc3lJIiOehtSSjqYiO7R6KLNrWOX+YNSQ==",
2068 + "version": "4.12.0",
2069 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.12.0.tgz",
2070 + "integrity": "sha512-JPDxovheWNp6d7AHCgsUlkuCKvtu3RB55iNEkaQcf0ttsDU/JZF+iQnYcQJSk/7PtT4mjjVG8N1kpwnI9SLYaw==",
2071 "cpu": [
2072 "arm64"
2073 ],
@@ -2098,9 +2078,9 @@
2078 ]
2079 },
2080 "node_modules/@rollup/rollup-win32-ia32-msvc": {
2101 - "version": "4.10.0",
2102 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.10.0.tgz",
2103 - "integrity": "sha512-FV0Tpt84LPYDduIDcXvEC7HKtyXxdvhdAOvOeWMWbQNulxViH2O07QXkT/FffX4FqEI02jEbCJbr+YcuKdyyMg==",
2081 + "version": "4.12.0",
2082 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.12.0.tgz",
2083 + "integrity": "sha512-fjtuvMWRGJn1oZacG8IPnzIV6GF2/XG+h71FKn76OYFqySXInJtseAqdprVTDTyqPxQOG9Exak5/E9Z3+EJ8ZA==",
2084 "cpu": [
2085 "ia32"
2086 ],
@@ -2111,9 +2091,9 @@
2091 ]
2092 },
2093 "node_modules/@rollup/rollup-win32-x64-msvc": {
2114 - "version": "4.10.0",
2115 - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.10.0.tgz",
2116 - "integrity": "sha512-OZoJd+o5TaTSQeFFQ6WjFCiltiYVjIdsXxwu/XZ8qRpsvMQr4UsVrE5UyT9RIvsnuF47DqkJKhhVZ2Q9YW9IpQ==",
2094 + "version": "4.12.0",
2095 + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.12.0.tgz",
2096 + "integrity": "sha512-ZYmr5mS2wd4Dew/JjT0Fqi2NPB/ZhZ2VvPp7SmvPZb4Y1CG/LRcS6tcRo2cYU7zLK5A7cdbhWnnWmUjoI4qapg==",
2097 "cpu": [
2098 "x64"
2099 ],
@@ -2151,12 +2131,12 @@
2131 "dev": true
2132 },
2133 "node_modules/@sigstore/bundle": {
2154 - "version": "2.1.1",
2155 - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.1.1.tgz",
2156 - "integrity": "sha512-v3/iS+1nufZdKQ5iAlQKcCsoh0jffQyABvYIxKsZQFWc4ubuGjwZklFHpDgV6O6T7vvV78SW5NHI91HFKEcxKg==",
2134 + "version": "2.2.0",
2135 + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-2.2.0.tgz",
2136 + "integrity": "sha512-5VI58qgNs76RDrwXNhpmyN/jKpq9evV/7f1XrcqcAfvxDl5SeVY/I5Rmfe96ULAV7/FK5dge9RBKGBJPhL1WsQ==",
2137 "dev": true,
2138 "dependencies": {
2159 - "@sigstore/protobuf-specs": "^0.2.1"
2139 + "@sigstore/protobuf-specs": "^0.3.0"
2140 },
2141 "engines": {
2142 "node": "^16.14.0 || >=18.0.0"
@@ -2172,23 +2152,23 @@
2152 }
2153 },
2154 "node_modules/@sigstore/protobuf-specs": {
2175 - "version": "0.2.1",
2176 - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.2.1.tgz",
2177 - "integrity": "sha512-XTWVxnWJu+c1oCshMLwnKvz8ZQJJDVOlciMfgpJBQbThVjKTCG8dwyhgLngBD2KN0ap9F/gOV8rFDEx8uh7R2A==",
2155 + "version": "0.3.0",
2156 + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.0.tgz",
2157 + "integrity": "sha512-zxiQ66JFOjVvP9hbhGj/F/qNdsZfkGb/dVXSanNRNuAzMlr4MC95voPUBX8//ZNnmv3uSYzdfR/JSkrgvZTGxA==",
2158 "dev": true,
2159 "engines": {
2160 "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
2161 }
2162 },
2163 "node_modules/@sigstore/sign": {
2184 - "version": "2.2.2",
2185 - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.2.tgz",
2186 - "integrity": "sha512-mAifqvvGOCkb5BJ5d/SRrVP5+kKCGxtcHuti6lgqZalIfNxikxlJMMptOqFp9+xV5LAnJMSaMWtzvcgNZ3PlPA==",
2164 + "version": "2.2.3",
2165 + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-2.2.3.tgz",
2166 + "integrity": "sha512-LqlA+ffyN02yC7RKszCdMTS6bldZnIodiox+IkT8B2f8oRYXCB3LQ9roXeiEL21m64CVH1wyveYAORfD65WoSw==",
2167 "dev": true,
2168 "dependencies": {
2189 - "@sigstore/bundle": "^2.1.1",
2169 + "@sigstore/bundle": "^2.2.0",
2170 "@sigstore/core": "^1.0.0",
2191 - "@sigstore/protobuf-specs": "^0.2.1",
2171 + "@sigstore/protobuf-specs": "^0.3.0",
2172 "make-fetch-happen": "^13.0.0"
2173 },
2174 "engines": {
@@ -2196,12 +2176,12 @@
2176 }
2177 },
2178 "node_modules/@sigstore/tuf": {
2199 - "version": "2.3.0",
2200 - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.0.tgz",
2201 - "integrity": "sha512-S98jo9cpJwO1mtQ+2zY7bOdcYyfVYCUaofCG6wWRzk3pxKHVAkSfshkfecto2+LKsx7Ovtqbgb2LS8zTRhxJ9Q==",
2179 + "version": "2.3.1",
2180 + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-2.3.1.tgz",
2181 + "integrity": "sha512-9Iv40z652td/QbV0o5n/x25H9w6IYRt2pIGbTX55yFDYlApDQn/6YZomjz6+KBx69rXHLzHcbtTS586mDdFD+Q==",
2182 "dev": true,
2183 "dependencies": {
2204 - "@sigstore/protobuf-specs": "^0.2.1",
2184 + "@sigstore/protobuf-specs": "^0.3.0",
2185 "tuf-js": "^2.2.0"
2186 },
2187 "engines": {
@@ -2209,14 +2189,14 @@
2189 }
2190 },
2191 "node_modules/@sigstore/verify": {
2212 - "version": "1.0.0",
2213 - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.0.0.tgz",
2214 - "integrity": "sha512-sRU6nblDBQ4pVTWni019Kij+XQj4RP75WXN5z3qHk81dt/L8A7r3v8RgRInTup4/Jf90WNods9CcbnWj7zJ26w==",
2192 + "version": "1.1.0",
2193 + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-1.1.0.tgz",
2194 + "integrity": "sha512-1fTqnqyTBWvV7cftUUFtDcHPdSox0N3Ub7C0lRyReYx4zZUlNTZjCV+HPy4Lre+r45dV7Qx5JLKvqqsgxuyYfg==",
2195 "dev": true,
2196 "dependencies": {
2217 - "@sigstore/bundle": "^2.1.1",
2197 + "@sigstore/bundle": "^2.2.0",
2198 "@sigstore/core": "^1.0.0",
2219 - "@sigstore/protobuf-specs": "^0.2.1"
2199 + "@sigstore/protobuf-specs": "^0.3.0"
2200 },
2201 "engines": {
2202 "node": "^16.14.0 || >=18.0.0"
@@ -2237,30 +2217,6 @@
2217 "node": ">=10.13.0"
2218 }
2219 },
2240 - "node_modules/@tsconfig/node10": {
2241 - "version": "1.0.9",
2242 - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz",
2243 - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==",
2244 - "dev": true
2245 - },
2246 - "node_modules/@tsconfig/node12": {
2247 - "version": "1.0.11",
2248 - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
2249 - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
2250 - "dev": true
2251 - },
2252 - "node_modules/@tsconfig/node14": {
2253 - "version": "1.0.3",
2254 - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
2255 - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
2256 - "dev": true
2257 - },
2258 - "node_modules/@tsconfig/node16": {
2259 - "version": "1.0.4",
2260 - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
2261 - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
2262 - "dev": true
2263 - },
2220 "node_modules/@tsconfig/node18": {
2221 "version": "18.2.2",
2222 "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.2.tgz",
@@ -2301,6 +2257,12 @@
2257 "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
2258 "dev": true
2259 },
2260 + "node_modules/@types/file-saver": {
2261 + "version": "2.0.7",
2262 + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz",
2263 + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
2264 + "dev": true
2265 + },
2266 "node_modules/@types/fs-extra": {
2267 "version": "11.0.4",
2268 "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
@@ -2321,6 +2283,16 @@
2283 "highlight.js": "*"
2284 }
2285 },
2286 + "node_modules/@types/html2canvas": {
2287 + "version": "1.0.0",
2288 + "resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-1.0.0.tgz",
2289 + "integrity": "sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==",
2290 + "deprecated": "This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed.",
2291 + "dev": true,
2292 + "dependencies": {
2293 + "html2canvas": "*"
2294 + }
2295 + },
2296 "node_modules/@types/inquirer": {
2297 "version": "9.0.7",
2298 "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.7.tgz",
@@ -2368,9 +2340,9 @@
2340 "dev": true
2341 },
2342 "node_modules/@types/lodash": {
2371 - "version": "4.14.202",
2372 - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz",
2373 - "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ=="
2343 + "version": "4.17.0",
2344 + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
2345 + "integrity": "sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA=="
2346 },
2347 "node_modules/@types/lodash-es": {
2348 "version": "4.17.12",
@@ -2416,18 +2388,18 @@
2388 "dev": true
2389 },
2390 "node_modules/@types/node": {
2419 - "version": "20.11.20",
2420 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
2421 - "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
2391 + "version": "20.11.28",
2392 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.28.tgz",
2393 + "integrity": "sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==",
2394 "dev": true,
2395 "dependencies": {
2396 "undici-types": "~5.26.4"
2397 }
2398 },
2399 "node_modules/@types/semver": {
2428 - "version": "7.5.6",
2429 - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
2430 - "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A=="
2400 + "version": "7.5.8",
2401 + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
2402 + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ=="
2403 },
2404 "node_modules/@types/sinonjs__fake-timers": {
2405 "version": "8.1.1",
@@ -2856,30 +2828,30 @@
2828 }
2829 },
2830 "node_modules/@volar/language-core": {
2859 - "version": "1.11.1",
2860 - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz",
2861 - "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==",
2831 + "version": "2.1.2",
2832 + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.1.2.tgz",
2833 + "integrity": "sha512-5qsDp0Gf6fE09UWCeK7bkVn6NxMwC9OqFWQkMMkeej8h8XjyABPdRygC2RCrqDrfVdGijqlMQeXs6yRS+vfZYA==",
2834 "dev": true,
2835 "dependencies": {
2864 - "@volar/source-map": "1.11.1"
2836 + "@volar/source-map": "2.1.2"
2837 }
2838 },
2839 "node_modules/@volar/source-map": {
2868 - "version": "1.11.1",
2869 - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz",
2870 - "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==",
2840 + "version": "2.1.2",
2841 + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.1.2.tgz",
2842 + "integrity": "sha512-yFJqsuLm1OaWrsz9E3yd3bJcYIlHqdZ8MbmIoZLrAzMYQDcoF26/INIhgziEXSdyHc8xd7rd/tJdSnUyh0gH4Q==",
2843 "dev": true,
2844 "dependencies": {
2873 - "muggle-string": "^0.3.1"
2845 + "muggle-string": "^0.4.0"
2846 }
2847 },
2848 "node_modules/@volar/typescript": {
2877 - "version": "1.11.1",
2878 - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz",
2879 - "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==",
2849 + "version": "2.1.2",
2850 + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.1.2.tgz",
2851 + "integrity": "sha512-lhTancZqamvaLvoz0u/uth8dpudENNt2LFZOWCw9JZiX14xRFhdhfzmphiCRb7am9E6qAJSbdS/gMt1utXAoHQ==",
2852 "dev": true,
2853 "dependencies": {
2882 - "@volar/language-core": "1.11.1",
2854 + "@volar/language-core": "2.1.2",
2855 "path-browserify": "^1.0.1"
2856 }
2857 },
@@ -2933,55 +2905,55 @@
2905 }
2906 },
2907 "node_modules/@vue/compiler-core": {
2936 - "version": "3.4.19",
2937 - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.19.tgz",
2938 - "integrity": "sha512-gj81785z0JNzRcU0Mq98E56e4ltO1yf8k5PQ+tV/7YHnbZkrM0fyFyuttnN8ngJZjbpofWE/m4qjKBiLl8Ju4w==",
2908 + "version": "3.4.21",
2909 + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.21.tgz",
2910 + "integrity": "sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==",
2911 "dependencies": {
2912 "@babel/parser": "^7.23.9",
2941 - "@vue/shared": "3.4.19",
2913 + "@vue/shared": "3.4.21",
2914 "entities": "^4.5.0",
2915 "estree-walker": "^2.0.2",
2916 "source-map-js": "^1.0.2"
2917 }
2918 },
2919 "node_modules/@vue/compiler-dom": {
2948 - "version": "3.4.19",
2949 - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.19.tgz",
2950 - "integrity": "sha512-vm6+cogWrshjqEHTzIDCp72DKtea8Ry/QVpQRYoyTIg9k7QZDX6D8+HGURjtmatfgM8xgCFtJJaOlCaRYRK3QA==",
2920 + "version": "3.4.21",
2921 + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz",
2922 + "integrity": "sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==",
2923 "dependencies": {
2952 - "@vue/compiler-core": "3.4.19",
2953 - "@vue/shared": "3.4.19"
2924 + "@vue/compiler-core": "3.4.21",
2925 + "@vue/shared": "3.4.21"
2926 }
2927 },
2928 "node_modules/@vue/compiler-sfc": {
2957 - "version": "3.4.19",
2958 - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.19.tgz",
2959 - "integrity": "sha512-LQ3U4SN0DlvV0xhr1lUsgLCYlwQfUfetyPxkKYu7dkfvx7g3ojrGAkw0AERLOKYXuAGnqFsEuytkdcComei3Yg==",
2929 + "version": "3.4.21",
2930 + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz",
2931 + "integrity": "sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==",
2932 "dependencies": {
2933 "@babel/parser": "^7.23.9",
2962 - "@vue/compiler-core": "3.4.19",
2963 - "@vue/compiler-dom": "3.4.19",
2964 - "@vue/compiler-ssr": "3.4.19",
2965 - "@vue/shared": "3.4.19",
2934 + "@vue/compiler-core": "3.4.21",
2935 + "@vue/compiler-dom": "3.4.21",
2936 + "@vue/compiler-ssr": "3.4.21",
2937 + "@vue/shared": "3.4.21",
2938 "estree-walker": "^2.0.2",
2967 - "magic-string": "^0.30.6",
2968 - "postcss": "^8.4.33",
2939 + "magic-string": "^0.30.7",
2940 + "postcss": "^8.4.35",
2941 "source-map-js": "^1.0.2"
2942 }
2943 },
2944 "node_modules/@vue/compiler-ssr": {
2973 - "version": "3.4.19",
2974 - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.19.tgz",
2975 - "integrity": "sha512-P0PLKC4+u4OMJ8sinba/5Z/iDT84uMRRlrWzadgLA69opCpI1gG4N55qDSC+dedwq2fJtzmGald05LWR5TFfLw==",
2945 + "version": "3.4.21",
2946 + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz",
2947 + "integrity": "sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==",
2948 "dependencies": {
2977 - "@vue/compiler-dom": "3.4.19",
2978 - "@vue/shared": "3.4.19"
2949 + "@vue/compiler-dom": "3.4.21",
2950 + "@vue/shared": "3.4.21"
2951 }
2952 },
2953 "node_modules/@vue/devtools-api": {
2982 - "version": "6.5.1",
2983 - "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.5.1.tgz",
2984 - "integrity": "sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA=="
2954 + "version": "6.6.1",
2955 + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.1.tgz",
2956 + "integrity": "sha512-LgPscpE3Vs0x96PzSSB4IGVSZXZBZHpfxs+ZA1d+VEPwHdOXowy/Y2CsvCAIFrf+ssVU1pD1jidj505EpUnfbA=="
2957 },
2958 "node_modules/@vue/eslint-config-prettier": {
2959 "version": "9.0.0",
@@ -2998,22 +2970,22 @@
2970 }
2971 },
2972 "node_modules/@vue/eslint-config-typescript": {
3001 - "version": "12.0.0",
3002 - "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz",
3003 - "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==",
2973 + "version": "13.0.0",
2974 + "resolved": "https://registry.npmjs.org/@vue/eslint-config-typescript/-/eslint-config-typescript-13.0.0.tgz",
2975 + "integrity": "sha512-MHh9SncG/sfqjVqjcuFLOLD6Ed4dRAis4HNt0dXASeAuLqIAx4YMB1/m2o4pUKK1vCt8fUvYG8KKX2Ot3BVZTg==",
2976 "dev": true,
2977 "dependencies": {
3006 - "@typescript-eslint/eslint-plugin": "^6.7.0",
3007 - "@typescript-eslint/parser": "^6.7.0",
2978 + "@typescript-eslint/eslint-plugin": "^7.1.1",
2979 + "@typescript-eslint/parser": "^7.1.1",
2980 "vue-eslint-parser": "^9.3.1"
2981 },
2982 "engines": {
3011 - "node": "^14.17.0 || >=16.0.0"
2983 + "node": "^18.18.0 || >=20.0.0"
2984 },
2985 "peerDependencies": {
3014 - "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0",
2986 + "eslint": "^8.56.0",
2987 "eslint-plugin-vue": "^9.0.0",
3016 - "typescript": "*"
2988 + "typescript": ">=4.7.4"
2989 },
2990 "peerDependenciesMeta": {
2991 "typescript": {
@@ -3021,19 +2993,207 @@
2993 }
2994 }
2995 },
2996 + "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/eslint-plugin": {
2997 + "version": "7.2.0",
2998 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz",
2999 + "integrity": "sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==",
3000 + "dev": true,
3001 + "dependencies": {
3002 + "@eslint-community/regexpp": "^4.5.1",
3003 + "@typescript-eslint/scope-manager": "7.2.0",
3004 + "@typescript-eslint/type-utils": "7.2.0",
3005 + "@typescript-eslint/utils": "7.2.0",
3006 + "@typescript-eslint/visitor-keys": "7.2.0",
3007 + "debug": "^4.3.4",
3008 + "graphemer": "^1.4.0",
3009 + "ignore": "^5.2.4",
3010 + "natural-compare": "^1.4.0",
3011 + "semver": "^7.5.4",
3012 + "ts-api-utils": "^1.0.1"
3013 + },
3014 + "engines": {
3015 + "node": "^16.0.0 || >=18.0.0"
3016 + },
3017 + "funding": {
3018 + "type": "opencollective",
3019 + "url": "https://opencollective.com/typescript-eslint"
3020 + },
3021 + "peerDependencies": {
3022 + "@typescript-eslint/parser": "^7.0.0",
3023 + "eslint": "^8.56.0"
3024 + },
3025 + "peerDependenciesMeta": {
3026 + "typescript": {
3027 + "optional": true
3028 + }
3029 + }
3030 + },
3031 + "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/parser": {
3032 + "version": "7.2.0",
3033 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.2.0.tgz",
3034 + "integrity": "sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==",
3035 + "dev": true,
3036 + "dependencies": {
3037 + "@typescript-eslint/scope-manager": "7.2.0",
3038 + "@typescript-eslint/types": "7.2.0",
3039 + "@typescript-eslint/typescript-estree": "7.2.0",
3040 + "@typescript-eslint/visitor-keys": "7.2.0",
3041 + "debug": "^4.3.4"
3042 + },
3043 + "engines": {
3044 + "node": "^16.0.0 || >=18.0.0"
3045 + },
3046 + "funding": {
3047 + "type": "opencollective",
3048 + "url": "https://opencollective.com/typescript-eslint"
3049 + },
3050 + "peerDependencies": {
3051 + "eslint": "^8.56.0"
3052 + },
3053 + "peerDependenciesMeta": {
3054 + "typescript": {
3055 + "optional": true
3056 + }
3057 + }
3058 + },
3059 + "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/scope-manager": {
3060 + "version": "7.2.0",
3061 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz",
3062 + "integrity": "sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==",
3063 + "dev": true,
3064 + "dependencies": {
3065 + "@typescript-eslint/types": "7.2.0",
3066 + "@typescript-eslint/visitor-keys": "7.2.0"
3067 + },
3068 + "engines": {
3069 + "node": "^16.0.0 || >=18.0.0"
3070 + },
3071 + "funding": {
3072 + "type": "opencollective",
3073 + "url": "https://opencollective.com/typescript-eslint"
3074 + }
3075 + },
3076 + "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/type-utils": {
3077 + "version": "7.2.0",
3078 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz",
3079 + "integrity": "sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==",
3080 + "dev": true,
3081 + "dependencies": {
3082 + "@typescript-eslint/typescript-estree": "7.2.0",
3083 + "@typescript-eslint/utils": "7.2.0",
3084 + "debug": "^4.3.4",
3085 + "ts-api-utils": "^1.0.1"
3086 + },
3087 + "engines": {
3088 + "node": "^16.0.0 || >=18.0.0"
3089 + },
3090 + "funding": {
3091 + "type": "opencollective",
3092 + "url": "https://opencollective.com/typescript-eslint"
3093 + },
3094 + "peerDependencies": {
3095 + "eslint": "^8.56.0"
3096 + },
3097 + "peerDependenciesMeta": {
3098 + "typescript": {
3099 + "optional": true
3100 + }
3101 + }
3102 + },
3103 + "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/types": {
3104 + "version": "7.2.0",
3105 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz",
3106 + "integrity": "sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==",
3107 + "dev": true,
3108 + "engines": {
3109 + "node": "^16.0.0 || >=18.0.0"
3110 + },
3111 + "funding": {
3112 + "type": "opencollective",
3113 + "url": "https://opencollective.com/typescript-eslint"
3114 + }
3115 + },
3116 + "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/typescript-estree": {
3117 + "version": "7.2.0",
3118 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz",
3119 + "integrity": "sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==",
3120 + "dev": true,
3121 + "dependencies": {
3122 + "@typescript-eslint/types": "7.2.0",
3123 + "@typescript-eslint/visitor-keys": "7.2.0",
3124 + "debug": "^4.3.4",
3125 + "globby": "^11.1.0",
3126 + "is-glob": "^4.0.3",
3127 + "minimatch": "9.0.3",
3128 + "semver": "^7.5.4",
3129 + "ts-api-utils": "^1.0.1"
3130 + },
3131 + "engines": {
3132 + "node": "^16.0.0 || >=18.0.0"
3133 + },
3134 + "funding": {
3135 + "type": "opencollective",
3136 + "url": "https://opencollective.com/typescript-eslint"
3137 + },
3138 + "peerDependenciesMeta": {
3139 + "typescript": {
3140 + "optional": true
3141 + }
3142 + }
3143 + },
3144 + "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/utils": {
3145 + "version": "7.2.0",
3146 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.2.0.tgz",
3147 + "integrity": "sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==",
3148 + "dev": true,
3149 + "dependencies": {
3150 + "@eslint-community/eslint-utils": "^4.4.0",
3151 + "@types/json-schema": "^7.0.12",
3152 + "@types/semver": "^7.5.0",
3153 + "@typescript-eslint/scope-manager": "7.2.0",
3154 + "@typescript-eslint/types": "7.2.0",
3155 + "@typescript-eslint/typescript-estree": "7.2.0",
3156 + "semver": "^7.5.4"
3157 + },
3158 + "engines": {
3159 + "node": "^16.0.0 || >=18.0.0"
3160 + },
3161 + "funding": {
3162 + "type": "opencollective",
3163 + "url": "https://opencollective.com/typescript-eslint"
3164 + },
3165 + "peerDependencies": {
3166 + "eslint": "^8.56.0"
3167 + }
3168 + },
3169 + "node_modules/@vue/eslint-config-typescript/node_modules/@typescript-eslint/visitor-keys": {
3170 + "version": "7.2.0",
3171 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz",
3172 + "integrity": "sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==",
3173 + "dev": true,
3174 + "dependencies": {
3175 + "@typescript-eslint/types": "7.2.0",
3176 + "eslint-visitor-keys": "^3.4.1"
3177 + },
3178 + "engines": {
3179 + "node": "^16.0.0 || >=18.0.0"
3180 + },
3181 + "funding": {
3182 + "type": "opencollective",
3183 + "url": "https://opencollective.com/typescript-eslint"
3184 + }
3185 + },
3186 "node_modules/@vue/language-core": {
3025 - "version": "1.8.27",
3026 - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz",
3027 - "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==",
3187 + "version": "2.0.6",
3188 + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.6.tgz",
3189 + "integrity": "sha512-UzqU12tzf9XLqRO3TiWPwRNpP4fyUzE6MAfOQWQNZ4jy6a30ARRUpmODDKq6O8C4goMc2AlPqTmjOHPjHkilSg==",
3190 "dev": true,
3191 "dependencies": {
3030 - "@volar/language-core": "~1.11.1",
3031 - "@volar/source-map": "~1.11.1",
3032 - "@vue/compiler-dom": "^3.3.0",
3033 - "@vue/shared": "^3.3.0",
3192 + "@volar/language-core": "~2.1.2",
3193 + "@vue/compiler-dom": "^3.4.0",
3194 + "@vue/shared": "^3.4.0",
3195 "computeds": "^0.0.1",
3196 "minimatch": "^9.0.3",
3036 - "muggle-string": "^0.3.1",
3197 "path-browserify": "^1.0.1",
3198 "vue-template-compiler": "^2.7.14"
3199 },
@@ -3047,66 +3207,57 @@
3207 }
3208 },
3209 "node_modules/@vue/reactivity": {
3050 - "version": "3.4.19",
3051 - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.19.tgz",
3052 - "integrity": "sha512-+VcwrQvLZgEclGZRHx4O2XhyEEcKaBi50WbxdVItEezUf4fqRh838Ix6amWTdX0CNb/b6t3Gkz3eOebfcSt+UA==",
3210 + "version": "3.4.21",
3211 + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.21.tgz",
3212 + "integrity": "sha512-UhenImdc0L0/4ahGCyEzc/pZNwVgcglGy9HVzJ1Bq2Mm9qXOpP8RyNTjookw/gOCUlXSEtuZ2fUg5nrHcoqJcw==",
3213 "dependencies": {
3054 - "@vue/shared": "3.4.19"
3214 + "@vue/shared": "3.4.21"
3215 }
3216 },
3217 "node_modules/@vue/runtime-core": {
3058 - "version": "3.4.19",
3059 - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.19.tgz",
3060 - "integrity": "sha512-/Z3tFwOrerJB/oyutmJGoYbuoadphDcJAd5jOuJE86THNZji9pYjZroQ2NFsZkTxOq0GJbb+s2kxTYToDiyZzw==",
3218 + "version": "3.4.21",
3219 + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.21.tgz",
3220 + "integrity": "sha512-pQthsuYzE1XcGZznTKn73G0s14eCJcjaLvp3/DKeYWoFacD9glJoqlNBxt3W2c5S40t6CCcpPf+jG01N3ULyrA==",
3221 "dependencies": {
3062 - "@vue/reactivity": "3.4.19",
3063 - "@vue/shared": "3.4.19"
3222 + "@vue/reactivity": "3.4.21",
3223 + "@vue/shared": "3.4.21"
3224 }
3225 },
3226 "node_modules/@vue/runtime-dom": {
3067 - "version": "3.4.19",
3068 - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.19.tgz",
3069 - "integrity": "sha512-IyZzIDqfNCF0OyZOauL+F4yzjMPN2rPd8nhqPP2N1lBn3kYqJpPHHru+83Rkvo2lHz5mW+rEeIMEF9qY3PB94g==",
3227 + "version": "3.4.21",
3228 + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.21.tgz",
3229 + "integrity": "sha512-gvf+C9cFpevsQxbkRBS1NpU8CqxKw0ebqMvLwcGQrNpx6gqRDodqKqA+A2VZZpQ9RpK2f9yfg8VbW/EpdFUOJw==",
3230 "dependencies": {
3071 - "@vue/runtime-core": "3.4.19",
3072 - "@vue/shared": "3.4.19",
3231 + "@vue/runtime-core": "3.4.21",
3232 + "@vue/shared": "3.4.21",
3233 "csstype": "^3.1.3"
3234 }
3235 },
3236 "node_modules/@vue/server-renderer": {
3077 - "version": "3.4.19",
3078 - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.19.tgz",
3079 - "integrity": "sha512-eAj2p0c429RZyyhtMRnttjcSToch+kTWxFPHlzGMkR28ZbF1PDlTcmGmlDxccBuqNd9iOQ7xPRPAGgPVj+YpQw==",
3237 + "version": "3.4.21",
3238 + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.21.tgz",
3239 + "integrity": "sha512-aV1gXyKSN6Rz+6kZ6kr5+Ll14YzmIbeuWe7ryJl5muJ4uwSwY/aStXTixx76TwkZFJLm1aAlA/HSWEJ4EyiMkg==",
3240 "dependencies": {
3081 - "@vue/compiler-ssr": "3.4.19",
3082 - "@vue/shared": "3.4.19"
3241 + "@vue/compiler-ssr": "3.4.21",
3242 + "@vue/shared": "3.4.21"
3243 },
3244 "peerDependencies": {
3085 - "vue": "3.4.19"
3245 + "vue": "3.4.21"
3246 }
3247 },
3248 "node_modules/@vue/shared": {
3089 - "version": "3.4.19",
3090 - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.19.tgz",
3091 - "integrity": "sha512-/KliRRHMF6LoiThEy+4c1Z4KB/gbPrGjWwJR+crg2otgrf/egKzRaCPvJ51S5oetgsgXLfc4Rm5ZgrKHZrtMSw=="
3249 + "version": "3.4.21",
3250 + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.21.tgz",
3251 + "integrity": "sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g=="
3252 },
3253 "node_modules/@vue/test-utils": {
3094 - "version": "2.4.4",
3095 - "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.4.tgz",
3096 - "integrity": "sha512-8jkRxz8pNhClAf4Co4ZrpAoFISdvT3nuSkUlY6Ys6rmTpw3DMWG/X3mw3gQ7QJzgCZO9f+zuE2kW57fi09MW7Q==",
3254 + "version": "2.4.5",
3255 + "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-2.4.5.tgz",
3256 + "integrity": "sha512-oo2u7vktOyKUked36R93NB7mg2B+N7Plr8lxp2JBGwr18ch6EggFjixSCdIVVLkT6Qr0z359Xvnafc9dcKyDUg==",
3257 "dev": true,
3258 "dependencies": {
3259 "js-beautify": "^1.14.9",
3100 - "vue-component-type-helpers": "^1.8.21"
3101 - },
3102 - "peerDependencies": {
3103 - "@vue/server-renderer": "^3.0.1",
3104 - "vue": "^3.0.1"
3105 - },
3106 - "peerDependenciesMeta": {
3107 - "@vue/server-renderer": {
3108 - "optional": true
3109 - }
3260 + "vue-component-type-helpers": "^2.0.0"
3261 }
3262 },
3263 "node_modules/@vue/tsconfig": {
@@ -3116,12 +3267,12 @@
3267 "dev": true
3268 },
3269 "node_modules/@vueuse/components": {
3119 - "version": "10.8.0",
3120 - "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.8.0.tgz",
3121 - "integrity": "sha512-5k/4Cxgt+aoxeHIOWSet6kkHXY+96QuPkJzGpOHaCj9DD0ASBni6L/wHQUWL118Ac9xq5+QQJuK5VvFs/yBAEw==",
3270 + "version": "10.9.0",
3271 + "resolved": "https://registry.npmjs.org/@vueuse/components/-/components-10.9.0.tgz",
3272 + "integrity": "sha512-BHQpA0yIi3y7zKa1gYD0FUzLLkcRTqVhP8smnvsCK6GFpd94Nziq1XVPD7YpFeho0k5BzbBiNZF7V/DpkJ967A==",
3273 "dependencies": {
3123 - "@vueuse/core": "10.8.0",
3124 - "@vueuse/shared": "10.8.0",
3274 + "@vueuse/core": "10.9.0",
3275 + "@vueuse/shared": "10.9.0",
3276 "vue-demi": ">=0.14.7"
3277 }
3278 },
@@ -3151,13 +3302,13 @@
3302 }
3303 },
3304 "node_modules/@vueuse/core": {
3154 - "version": "10.8.0",
3155 - "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.8.0.tgz",
3156 - "integrity": "sha512-G9Ok9fjx10TkNIPn8V1dJmK1NcdJCtYmDRyYiTMUyJ1p0Tywc1zmOoCQ2xhHYyz8ULBU4KjIJQ9n+Lrty74iVw==",
3305 + "version": "10.9.0",
3306 + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.9.0.tgz",
3307 + "integrity": "sha512-/1vjTol8SXnx6xewDEKfS0Ra//ncg4Hb0DaZiwKf7drgfMsKFExQ+FnnENcN6efPen+1kIzhLQoGSy0eDUVOMg==",
3308 "dependencies": {
3309 "@types/web-bluetooth": "^0.0.20",
3159 - "@vueuse/metadata": "10.8.0",
3160 - "@vueuse/shared": "10.8.0",
3310 + "@vueuse/metadata": "10.9.0",
3311 + "@vueuse/shared": "10.9.0",
3312 "vue-demi": ">=0.14.7"
3313 },
3314 "funding": {
@@ -3190,17 +3341,17 @@
3341 }
3342 },
3343 "node_modules/@vueuse/metadata": {
3193 - "version": "10.8.0",
3194 - "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.8.0.tgz",
3195 - "integrity": "sha512-Nim/Vle5OgXcXhAvGOgkJQXB1Yb+Kq/fMbLuv3YYDYbiQrwr39ljuD4k9fPeq4yUyokYRo2RaNQmbbIMWB/9+w==",
3344 + "version": "10.9.0",
3345 + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.9.0.tgz",
3346 + "integrity": "sha512-iddNbg3yZM0X7qFY2sAotomgdHK7YJ6sKUvQqbvwnf7TmaVPxS4EJydcNsVejNdS8iWCtDk+fYXr7E32nyTnGA==",
3347 "funding": {
3348 "url": "https://github.com/sponsors/antfu"
3349 }
3350 },
3351 "node_modules/@vueuse/shared": {
3201 - "version": "10.8.0",
3202 - "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.8.0.tgz",
3203 - "integrity": "sha512-dUdy6zwHhULGxmr9YUg8e+EnB39gcM4Fe2oKBSrh3cOsV30JcMPtsyuspgFCUo5xxFNaeMf/W2yyKfST7Bg8oQ==",
3352 + "version": "10.9.0",
3353 + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.9.0.tgz",
3354 + "integrity": "sha512-Uud2IWncmAfJvRaFYzv5OHDli+FbOzxiVEQdLCKQKLyhz94PIyFC3CHcH7EDMwIn8NPtD06+PNbC/PiO0LGLtw==",
3355 "dependencies": {
3356 "vue-demi": ">=0.14.7"
3357 },
@@ -3404,9 +3555,9 @@
3555 }
3556 },
3557 "node_modules/apexcharts": {
3407 - "version": "3.46.0",
3408 - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.46.0.tgz",
3409 - "integrity": "sha512-ELAY6vj8JQD7QLktKasTzwm9Wt0qxqfQSo+3QWS7G7I774iK8HCkG1toGsqJH0mkK6PtYBtnSIe66uUcwoCw1w==",
3558 + "version": "3.47.0",
3559 + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.47.0.tgz",
3560 + "integrity": "sha512-s/fgNCA69b8lJdhI3R7Z+/Df47RPplLyHwuvttecR+aaZ3/Pm6wHYPiAGjqDNbVsMGXhuA9mcOpIYU5ZWeSdeg==",
3561 "dependencies": {
3562 "@yr/monotone-cubic-spline": "^1.0.3",
3563 "svg.draggable.js": "^2.2.2",
@@ -3557,9 +3708,9 @@
3708 }
3709 },
3710 "node_modules/autoprefixer": {
3560 - "version": "10.4.17",
3561 - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
3562 - "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
3711 + "version": "10.4.18",
3712 + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz",
3713 + "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==",
3714 "dev": true,
3715 "funding": [
3716 {
@@ -3576,8 +3727,8 @@
3727 }
3728 ],
3729 "dependencies": {
3579 - "browserslist": "^4.22.2",
3580 - "caniuse-lite": "^1.0.30001578",
3730 + "browserslist": "^4.23.0",
3731 + "caniuse-lite": "^1.0.30001591",
3732 "fraction.js": "^4.3.7",
3733 "normalize-range": "^0.1.2",
3734 "picocolors": "^1.0.0",
@@ -3594,10 +3745,13 @@
3745 }
3746 },
3747 "node_modules/available-typed-arrays": {
3597 - "version": "1.0.6",
3598 - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz",
3599 - "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==",
3748 + "version": "1.0.7",
3749 + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
3750 + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
3751 "dev": true,
3752 + "dependencies": {
3753 + "possible-typed-array-names": "^1.0.0"
3754 + },
3755 "engines": {
3756 "node": ">= 0.4"
3757 },
@@ -3656,6 +3810,15 @@
3810 "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
3811 "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
3812 },
3813 + "node_modules/base64-arraybuffer": {
3814 + "version": "1.0.2",
3815 + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
3816 + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
3817 + "dev": true,
3818 + "engines": {
3819 + "node": ">= 0.6.0"
3820 + }
3821 + },
3822 "node_modules/base64-js": {
3823 "version": "1.5.1",
3824 "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3752,9 +3915,9 @@
3915 }
3916 },
3917 "node_modules/browserslist": {
3755 - "version": "4.22.3",
3756 - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
3757 - "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
3918 + "version": "4.23.0",
3919 + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
3920 + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
3921 "dev": true,
3922 "funding": [
3923 {
@@ -3771,8 +3934,8 @@
3934 }
3935 ],
3936 "dependencies": {
3774 - "caniuse-lite": "^1.0.30001580",
3775 - "electron-to-chromium": "^1.4.648",
3937 + "caniuse-lite": "^1.0.30001587",
3938 + "electron-to-chromium": "^1.4.668",
3939 "node-releases": "^2.0.14",
3940 "update-browserslist-db": "^1.0.13"
3941 },
@@ -3912,15 +4075,16 @@
4075 }
4076 },
4077 "node_modules/call-bind": {
3915 - "version": "1.0.6",
3916 - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz",
3917 - "integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==",
4078 + "version": "1.0.7",
4079 + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
4080 + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
4081 "dev": true,
4082 "dependencies": {
4083 + "es-define-property": "^1.0.0",
4084 "es-errors": "^1.3.0",
4085 "function-bind": "^1.1.2",
3922 - "get-intrinsic": "^1.2.3",
3923 - "set-function-length": "^1.2.0"
4086 + "get-intrinsic": "^1.2.4",
4087 + "set-function-length": "^1.2.1"
4088 },
4089 "engines": {
4090 "node": ">= 0.4"
@@ -3959,9 +4123,9 @@
4123 }
4124 },
4125 "node_modules/caniuse-lite": {
3962 - "version": "1.0.30001585",
3963 - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001585.tgz",
3964 - "integrity": "sha512-yr2BWR1yLXQ8fMpdS/4ZZXpseBgE7o4g41x3a6AJOqZuOi+iE/WdJYAuZ6Y95i4Ohd2Y+9MzIWRR+uGABH4s3Q==",
4126 + "version": "1.0.30001593",
4127 + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001593.tgz",
4128 + "integrity": "sha512-UWM1zlo3cZfkpBysd7AS+z+v007q9G1+fLTUU42rQnY6t2axoogPW/xol6T7juU5EUoOhML4WgBIdG+9yYqAjQ==",
4129 "dev": true,
4130 "funding": [
4131 {
@@ -4369,12 +4533,6 @@
4533 "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==",
4534 "dev": true
4535 },
4372 - "node_modules/create-require": {
4373 - "version": "1.1.1",
4374 - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
4375 - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
4376 - "dev": true
4377 - },
4536 "node_modules/cross-spawn": {
4537 "version": "7.0.3",
4538 "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -4393,6 +4551,15 @@
4551 "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
4552 "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
4553 },
4554 + "node_modules/css-line-break": {
4555 + "version": "2.1.0",
4556 + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
4557 + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
4558 + "dev": true,
4559 + "dependencies": {
4560 + "utrie": "^1.0.2"
4561 + }
4562 + },
4563 "node_modules/css-render": {
4564 "version": "0.15.12",
4565 "resolved": "https://registry.npmjs.org/css-render/-/css-render-0.15.12.tgz",
@@ -4510,9 +4677,9 @@
4677 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4678 },
4679 "node_modules/cypress": {
4513 - "version": "13.6.6",
4514 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.6.6.tgz",
4515 - "integrity": "sha512-S+2S9S94611hXimH9a3EAYt81QM913ZVA03pUmGDfLTFa5gyp85NJ8dJGSlEAEmyRsYkioS1TtnWtbv/Fzt11A==",
4680 + "version": "13.7.0",
4681 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.7.0.tgz",
4682 + "integrity": "sha512-UimjRSJJYdTlvkChcdcfywKJ6tUYuwYuk/n1uMMglrvi+ZthNhoRYcxnWgTqUtkl17fXrPAsD5XT2rcQYN1xKA==",
4683 "dev": true,
4684 "hasInstallScript": true,
4685 "dependencies": {
@@ -4887,9 +5054,9 @@
5054 }
5055 },
5056 "node_modules/default-browser/node_modules/npm-run-path": {
4890 - "version": "5.2.0",
4891 - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
4892 - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
5057 + "version": "5.3.0",
5058 + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
5059 + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
5060 "dev": true,
5061 "dependencies": {
5062 "path-key": "^4.0.0"
@@ -4941,18 +5108,20 @@
5108 }
5109 },
5110 "node_modules/define-data-property": {
4944 - "version": "1.1.2",
4945 - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.2.tgz",
4946 - "integrity": "sha512-SRtsSqsDbgpJBbW3pABMCOt6rQyeM8s8RiyeSN8jYG8sYmt/kGJejbydttUsnDs1tadr19tvhT4ShwMyoqAm4g==",
5111 + "version": "1.1.4",
5112 + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
5113 + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
5114 "dev": true,
5115 "dependencies": {
5116 + "es-define-property": "^1.0.0",
5117 "es-errors": "^1.3.0",
4950 - "get-intrinsic": "^1.2.2",
4951 - "gopd": "^1.0.1",
4952 - "has-property-descriptors": "^1.0.1"
5118 + "gopd": "^1.0.1"
5119 },
5120 "engines": {
5121 "node": ">= 0.4"
5122 + },
5123 + "funding": {
5124 + "url": "https://github.com/sponsors/ljharb"
5125 }
5126 },
5127 "node_modules/define-lazy-prop": {
@@ -5040,15 +5209,6 @@
5209 "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
5210 "dev": true
5211 },
5043 - "node_modules/diff": {
5044 - "version": "4.0.2",
5045 - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
5046 - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
5047 - "dev": true,
5048 - "engines": {
5049 - "node": ">=0.3.1"
5050 - }
5051 - },
5212 "node_modules/diff-sequences": {
5213 "version": "29.6.3",
5214 "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
@@ -5226,9 +5386,9 @@
5386 "dev": true
5387 },
5388 "node_modules/electron-to-chromium": {
5229 - "version": "1.4.665",
5230 - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.665.tgz",
5231 - "integrity": "sha512-UpyCWObBoD+nSZgOC2ToaIdZB0r9GhqT2WahPKiSki6ckkSuKhQNso8V2PrFcHBMleI/eqbKgVQgVC4Wni4ilw==",
5389 + "version": "1.4.690",
5390 + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.690.tgz",
5391 + "integrity": "sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA==",
5392 "dev": true
5393 },
5394 "node_modules/emoji-regex": {
@@ -5314,50 +5474,52 @@
5474 }
5475 },
5476 "node_modules/es-abstract": {
5317 - "version": "1.22.3",
5318 - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz",
5319 - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==",
5477 + "version": "1.22.5",
5478 + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz",
5479 + "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==",
5480 "dev": true,
5481 "dependencies": {
5322 - "array-buffer-byte-length": "^1.0.0",
5323 - "arraybuffer.prototype.slice": "^1.0.2",
5324 - "available-typed-arrays": "^1.0.5",
5325 - "call-bind": "^1.0.5",
5326 - "es-set-tostringtag": "^2.0.1",
5482 + "array-buffer-byte-length": "^1.0.1",
5483 + "arraybuffer.prototype.slice": "^1.0.3",
5484 + "available-typed-arrays": "^1.0.7",
5485 + "call-bind": "^1.0.7",
5486 + "es-define-property": "^1.0.0",
5487 + "es-errors": "^1.3.0",
5488 + "es-set-tostringtag": "^2.0.3",
5489 "es-to-primitive": "^1.2.1",
5490 "function.prototype.name": "^1.1.6",
5329 - "get-intrinsic": "^1.2.2",
5330 - "get-symbol-description": "^1.0.0",
5491 + "get-intrinsic": "^1.2.4",
5492 + "get-symbol-description": "^1.0.2",
5493 "globalthis": "^1.0.3",
5494 "gopd": "^1.0.1",
5333 - "has-property-descriptors": "^1.0.0",
5334 - "has-proto": "^1.0.1",
5495 + "has-property-descriptors": "^1.0.2",
5496 + "has-proto": "^1.0.3",
5497 "has-symbols": "^1.0.3",
5336 - "hasown": "^2.0.0",
5337 - "internal-slot": "^1.0.5",
5338 - "is-array-buffer": "^3.0.2",
5498 + "hasown": "^2.0.1",
5499 + "internal-slot": "^1.0.7",
5500 + "is-array-buffer": "^3.0.4",
5501 "is-callable": "^1.2.7",
5340 - "is-negative-zero": "^2.0.2",
5502 + "is-negative-zero": "^2.0.3",
5503 "is-regex": "^1.1.4",
5342 - "is-shared-array-buffer": "^1.0.2",
5504 + "is-shared-array-buffer": "^1.0.3",
5505 "is-string": "^1.0.7",
5344 - "is-typed-array": "^1.1.12",
5506 + "is-typed-array": "^1.1.13",
5507 "is-weakref": "^1.0.2",
5508 "object-inspect": "^1.13.1",
5509 "object-keys": "^1.1.1",
5348 - "object.assign": "^4.1.4",
5349 - "regexp.prototype.flags": "^1.5.1",
5350 - "safe-array-concat": "^1.0.1",
5351 - "safe-regex-test": "^1.0.0",
5510 + "object.assign": "^4.1.5",
5511 + "regexp.prototype.flags": "^1.5.2",
5512 + "safe-array-concat": "^1.1.0",
5513 + "safe-regex-test": "^1.0.3",
5514 "string.prototype.trim": "^1.2.8",
5515 "string.prototype.trimend": "^1.0.7",
5516 "string.prototype.trimstart": "^1.0.7",
5355 - "typed-array-buffer": "^1.0.0",
5356 - "typed-array-byte-length": "^1.0.0",
5357 - "typed-array-byte-offset": "^1.0.0",
5358 - "typed-array-length": "^1.0.4",
5517 + "typed-array-buffer": "^1.0.2",
5518 + "typed-array-byte-length": "^1.0.1",
5519 + "typed-array-byte-offset": "^1.0.2",
5520 + "typed-array-length": "^1.0.5",
5521 "unbox-primitive": "^1.0.2",
5360 - "which-typed-array": "^1.1.13"
5522 + "which-typed-array": "^1.1.14"
5523 },
5524 "engines": {
5525 "node": ">= 0.4"
@@ -5366,6 +5528,18 @@
5528 "url": "https://github.com/sponsors/ljharb"
5529 }
5530 },
5531 + "node_modules/es-define-property": {
5532 + "version": "1.0.0",
5533 + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
5534 + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
5535 + "dev": true,
5536 + "dependencies": {
5537 + "get-intrinsic": "^1.2.4"
5538 + },
5539 + "engines": {
5540 + "node": ">= 0.4"
5541 + }
5542 + },
5543 "node_modules/es-errors": {
5544 "version": "1.3.0",
5545 "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
@@ -5376,14 +5550,14 @@
5550 }
5551 },
5552 "node_modules/es-set-tostringtag": {
5379 - "version": "2.0.2",
5380 - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
5381 - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==",
5553 + "version": "2.0.3",
5554 + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
5555 + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
5556 "dev": true,
5557 "dependencies": {
5384 - "get-intrinsic": "^1.2.2",
5385 - "has-tostringtag": "^1.0.0",
5386 - "hasown": "^2.0.0"
5558 + "get-intrinsic": "^1.2.4",
5559 + "has-tostringtag": "^1.0.2",
5560 + "hasown": "^2.0.1"
5561 },
5562 "engines": {
5563 "node": ">= 0.4"
@@ -5469,15 +5643,15 @@
5643 }
5644 },
5645 "node_modules/eslint": {
5472 - "version": "8.56.0",
5473 - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
5474 - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
5646 + "version": "8.57.0",
5647 + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
5648 + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
5649 "dependencies": {
5650 "@eslint-community/eslint-utils": "^4.2.0",
5651 "@eslint-community/regexpp": "^4.6.1",
5652 "@eslint/eslintrc": "^2.1.4",
5479 - "@eslint/js": "8.56.0",
5480 - "@humanwhocodes/config-array": "^0.11.13",
5653 + "@eslint/js": "8.57.0",
5654 + "@humanwhocodes/config-array": "^0.11.14",
5655 "@humanwhocodes/module-importer": "^1.0.1",
5656 "@nodelib/fs.walk": "^1.2.8",
5657 "@ungap/structured-clone": "^1.2.0",
@@ -5595,9 +5769,9 @@
5769 }
5770 },
5771 "node_modules/eslint-plugin-vue": {
5598 - "version": "9.22.0",
5599 - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.22.0.tgz",
5600 - "integrity": "sha512-7wCXv5zuVnBtZE/74z4yZ0CM8AjH6bk4MQGm7hZjUC2DBppKU5ioeOk5LGSg/s9a1ZJnIsdPLJpXnu1Rc+cVHg==",
5772 + "version": "9.23.0",
5773 + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.23.0.tgz",
5774 + "integrity": "sha512-Bqd/b7hGYGrlV+wP/g77tjyFmp81lh5TMw0be9093X02SyelxRRfCI6/IsGq/J7Um0YwB9s0Ry0wlFyjPdmtUw==",
5775 "dependencies": {
5776 "@eslint-community/eslint-utils": "^4.4.0",
5777 "natural-compare": "^1.4.0",
@@ -5985,6 +6159,11 @@
6159 "node": "^10.12.0 || >=12.0.0"
6160 }
6161 },
6162 + "node_modules/file-saver": {
6163 + "version": "2.0.5",
6164 + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
6165 + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
6166 + },
6167 "node_modules/fill-range": {
6168 "version": "7.0.1",
6169 "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -6025,9 +6204,9 @@
6204 }
6205 },
6206 "node_modules/flatted": {
6028 - "version": "3.2.9",
6029 - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
6030 - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ=="
6207 + "version": "3.3.1",
6208 + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
6209 + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw=="
6210 },
6211 "node_modules/follow-redirects": {
6212 "version": "1.15.5",
@@ -6452,21 +6631,21 @@
6631 }
6632 },
6633 "node_modules/has-property-descriptors": {
6455 - "version": "1.0.1",
6456 - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz",
6457 - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==",
6634 + "version": "1.0.2",
6635 + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
6636 + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
6637 "dev": true,
6638 "dependencies": {
6460 - "get-intrinsic": "^1.2.2"
6639 + "es-define-property": "^1.0.0"
6640 },
6641 "funding": {
6642 "url": "https://github.com/sponsors/ljharb"
6643 }
6644 },
6645 "node_modules/has-proto": {
6467 - "version": "1.0.1",
6468 - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz",
6469 - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==",
6646 + "version": "1.0.3",
6647 + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
6648 + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
6649 "dev": true,
6650 "engines": {
6651 "node": ">= 0.4"
@@ -6566,6 +6745,19 @@
6745 "url": "https://github.com/sponsors/sindresorhus"
6746 }
6747 },
6748 + "node_modules/html2canvas": {
6749 + "version": "1.4.1",
6750 + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
6751 + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
6752 + "dev": true,
6753 + "dependencies": {
6754 + "css-line-break": "^2.1.0",
6755 + "text-segmentation": "^1.0.3"
6756 + },
6757 + "engines": {
6758 + "node": ">=8.0.0"
6759 + }
6760 + },
6761 "node_modules/http-assert": {
6762 "version": "1.5.0",
6763 "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz",
@@ -6636,9 +6828,9 @@
6828 }
6829 },
6830 "node_modules/http-proxy-agent": {
6639 - "version": "7.0.0",
6640 - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz",
6641 - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==",
6831 + "version": "7.0.2",
6832 + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
6833 + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
6834 "dev": true,
6835 "dependencies": {
6836 "agent-base": "^7.1.0",
@@ -6663,9 +6855,9 @@
6855 }
6856 },
6857 "node_modules/https-proxy-agent": {
6666 - "version": "7.0.2",
6667 - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz",
6668 - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==",
6858 + "version": "7.0.4",
6859 + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz",
6860 + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==",
6861 "dev": true,
6862 "dependencies": {
6863 "agent-base": "^7.0.2",
@@ -6840,6 +7032,25 @@
7032 "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==",
7033 "dev": true
7034 },
7035 + "node_modules/ip-address": {
7036 + "version": "9.0.5",
7037 + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
7038 + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
7039 + "dev": true,
7040 + "dependencies": {
7041 + "jsbn": "1.1.0",
7042 + "sprintf-js": "^1.1.3"
7043 + },
7044 + "engines": {
7045 + "node": ">= 12"
7046 + }
7047 + },
7048 + "node_modules/ip-address/node_modules/jsbn": {
7049 + "version": "1.1.0",
7050 + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
7051 + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
7052 + "dev": true
7053 + },
7054 "node_modules/is-array-buffer": {
7055 "version": "3.0.4",
7056 "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
@@ -7067,9 +7278,9 @@
7278 "dev": true
7279 },
7280 "node_modules/is-negative-zero": {
7070 - "version": "2.0.2",
7071 - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
7072 - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==",
7281 + "version": "2.0.3",
7282 + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
7283 + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
7284 "dev": true,
7285 "engines": {
7286 "node": ">= 0.4"
@@ -7132,12 +7343,15 @@
7343 }
7344 },
7345 "node_modules/is-shared-array-buffer": {
7135 - "version": "1.0.2",
7136 - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
7137 - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==",
7346 + "version": "1.0.3",
7347 + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
7348 + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
7349 "dev": true,
7350 "dependencies": {
7140 - "call-bind": "^1.0.2"
7351 + "call-bind": "^1.0.7"
7352 + },
7353 + "engines": {
7354 + "node": ">= 0.4"
7355 },
7356 "funding": {
7357 "url": "https://github.com/sponsors/ljharb"
@@ -7286,9 +7500,9 @@
7500 }
7501 },
7502 "node_modules/joi": {
7289 - "version": "17.12.1",
7290 - "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz",
7291 - "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==",
7503 + "version": "17.12.2",
7504 + "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz",
7505 + "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==",
7506 "dev": true,
7507 "dependencies": {
7508 "@hapi/hoek": "^9.3.0",
@@ -7299,22 +7513,23 @@
7513 }
7514 },
7515 "node_modules/jose": {
7302 - "version": "5.2.2",
7303 - "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.2.tgz",
7304 - "integrity": "sha512-/WByRr4jDcsKlvMd1dRJnPfS1GVO3WuKyaurJ/vvXcOaUQO8rnNObCQMlv/5uCceVQIq5Q4WLF44ohsdiTohdg==",
7516 + "version": "5.2.3",
7517 + "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.3.tgz",
7518 + "integrity": "sha512-KUXdbctm1uHVL8BYhnyHkgp3zDX5KW8ZhAKVFEfUbU2P8Alpzjb+48hHvjOdQIyPshoblhzsuqOwEEAbtHVirA==",
7519 "funding": {
7520 "url": "https://github.com/sponsors/panva"
7521 }
7522 },
7523 "node_modules/js-beautify": {
7310 - "version": "1.14.11",
7311 - "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.14.11.tgz",
7312 - "integrity": "sha512-rPogWqAfoYh1Ryqqh2agUpVfbxAhbjuN1SmU86dskQUKouRiggUTCO4+2ym9UPXllc2WAp0J+T5qxn7Um3lCdw==",
7524 + "version": "1.15.1",
7525 + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.1.tgz",
7526 + "integrity": "sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==",
7527 "dev": true,
7528 "dependencies": {
7529 "config-chain": "^1.1.13",
7316 - "editorconfig": "^1.0.3",
7530 + "editorconfig": "^1.0.4",
7531 "glob": "^10.3.3",
7532 + "js-cookie": "^3.0.5",
7533 "nopt": "^7.2.0"
7534 },
7535 "bin": {
@@ -7326,6 +7541,15 @@
7541 "node": ">=14"
7542 }
7543 },
7544 + "node_modules/js-cookie": {
7545 + "version": "3.0.5",
7546 + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
7547 + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
7548 + "dev": true,
7549 + "engines": {
7550 + "node": ">=14"
7551 + }
7552 + },
7553 "node_modules/js-md5": {
7554 "version": "0.8.3",
7555 "resolved": "https://registry.npmjs.org/js-md5/-/js-md5-0.8.3.tgz",
@@ -8097,9 +8321,9 @@
8321 }
8322 },
8323 "node_modules/magic-string": {
8100 - "version": "0.30.7",
8101 - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
8102 - "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==",
8324 + "version": "0.30.8",
8325 + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz",
8326 + "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==",
8327 "dependencies": {
8328 "@jridgewell/sourcemap-codec": "^1.4.15"
8329 },
@@ -8107,12 +8331,6 @@
8331 "node": ">=12"
8332 }
8333 },
8110 - "node_modules/make-error": {
8111 - "version": "1.3.6",
8112 - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
8113 - "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
8114 - "dev": true
8115 - },
8334 "node_modules/make-fetch-happen": {
8335 "version": "13.0.0",
8336 "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz",
@@ -8497,9 +8715,9 @@
8715 }
8716 },
8717 "node_modules/mlly": {
8500 - "version": "1.5.0",
8501 - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz",
8502 - "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==",
8718 + "version": "1.6.1",
8719 + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz",
8720 + "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==",
8721 "dependencies": {
8722 "acorn": "^8.11.3",
8723 "pathe": "^1.1.2",
@@ -8522,9 +8740,9 @@
8740 "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
8741 },
8742 "node_modules/muggle-string": {
8525 - "version": "0.3.1",
8526 - "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz",
8527 - "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==",
8743 + "version": "0.4.1",
8744 + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
8745 + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
8746 "dev": true
8747 },
8748 "node_modules/mz": {
@@ -8539,9 +8757,9 @@
8757 }
8758 },
8759 "node_modules/naive-ui": {
8542 - "version": "2.38.0",
8543 - "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.38.0.tgz",
8544 - "integrity": "sha512-Sa1hPUSTaOBvFy5hBcdQ6ajGVqt59QHN4p6wcsC5stVo6JjQPnbRTUvWtNG3gAsLhCvHQBm9NXeJG5Ne1OFQ4Q==",
8760 + "version": "2.38.1",
8761 + "resolved": "https://registry.npmjs.org/naive-ui/-/naive-ui-2.38.1.tgz",
8762 + "integrity": "sha512-AnU1FQ7K/CbhguAX++V4kCFjk7h7RvWt4nvZPRjORMpq+fUIlzD+EcQ5Cv1VqDloNF8+eMv4Akc2Ogacc9S+5A==",
8763 "dependencies": {
8764 "@css-render/plugin-bem": "^0.15.12",
8765 "@css-render/vue3-ssr": "^0.15.12",
@@ -9486,6 +9704,15 @@
9704 "ms": "^2.1.1"
9705 }
9706 },
9707 + "node_modules/possible-typed-array-names": {
9708 + "version": "1.0.0",
9709 + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
9710 + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
9711 + "dev": true,
9712 + "engines": {
9713 + "node": ">= 0.4"
9714 + }
9715 + },
9716 "node_modules/postcss": {
9717 "version": "8.4.35",
9718 "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
@@ -9585,12 +9812,15 @@
9812 }
9813 },
9814 "node_modules/postcss-load-config/node_modules/lilconfig": {
9588 - "version": "3.0.0",
9589 - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz",
9590 - "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==",
9815 + "version": "3.1.1",
9816 + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz",
9817 + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==",
9818 "dev": true,
9819 "engines": {
9820 "node": ">=14"
9821 + },
9822 + "funding": {
9823 + "url": "https://github.com/sponsors/antonk52"
9824 }
9825 },
9826 "node_modules/postcss-nested": {
@@ -9974,14 +10204,15 @@
10204 "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
10205 },
10206 "node_modules/regexp.prototype.flags": {
9977 - "version": "1.5.1",
9978 - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz",
9979 - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==",
10207 + "version": "1.5.2",
10208 + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
10209 + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
10210 "dev": true,
10211 "dependencies": {
9982 - "call-bind": "^1.0.2",
9983 - "define-properties": "^1.2.0",
9984 - "set-function-name": "^2.0.0"
10212 + "call-bind": "^1.0.6",
10213 + "define-properties": "^1.2.1",
10214 + "es-errors": "^1.3.0",
10215 + "set-function-name": "^2.0.1"
10216 },
10217 "engines": {
10218 "node": ">= 0.4"
@@ -10317,9 +10548,9 @@
10548 }
10549 },
10550 "node_modules/rollup": {
10320 - "version": "4.10.0",
10321 - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.10.0.tgz",
10322 - "integrity": "sha512-t2v9G2AKxcQ8yrG+WGxctBes1AomT0M4ND7jTFBCVPXQ/WFTvNSefIrNSmLKhIKBrvN8SG+CZslimJcT3W2u2g==",
10551 + "version": "4.12.0",
10552 + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.12.0.tgz",
10553 + "integrity": "sha512-wz66wn4t1OHIJw3+XU7mJJQV/2NAfw5OAk6G6Hoo3zcvz/XOfQ52Vgi+AN4Uxoxi0KBBwk2g8zPrTDA4btSB/Q==",
10554 "dev": true,
10555 "dependencies": {
10556 "@types/estree": "1.0.5"
@@ -10332,19 +10563,19 @@
10563 "npm": ">=8.0.0"
10564 },
10565 "optionalDependencies": {
10335 - "@rollup/rollup-android-arm-eabi": "4.10.0",
10336 - "@rollup/rollup-android-arm64": "4.10.0",
10337 - "@rollup/rollup-darwin-arm64": "4.10.0",
10338 - "@rollup/rollup-darwin-x64": "4.10.0",
10339 - "@rollup/rollup-linux-arm-gnueabihf": "4.10.0",
10340 - "@rollup/rollup-linux-arm64-gnu": "4.10.0",
10341 - "@rollup/rollup-linux-arm64-musl": "4.10.0",
10342 - "@rollup/rollup-linux-riscv64-gnu": "4.10.0",
10343 - "@rollup/rollup-linux-x64-gnu": "4.10.0",
10344 - "@rollup/rollup-linux-x64-musl": "4.10.0",
10345 - "@rollup/rollup-win32-arm64-msvc": "4.10.0",
10346 - "@rollup/rollup-win32-ia32-msvc": "4.10.0",
10347 - "@rollup/rollup-win32-x64-msvc": "4.10.0",
10566 + "@rollup/rollup-android-arm-eabi": "4.12.0",
10567 + "@rollup/rollup-android-arm64": "4.12.0",
10568 + "@rollup/rollup-darwin-arm64": "4.12.0",
10569 + "@rollup/rollup-darwin-x64": "4.12.0",
10570 + "@rollup/rollup-linux-arm-gnueabihf": "4.12.0",
10571 + "@rollup/rollup-linux-arm64-gnu": "4.12.0",
10572 + "@rollup/rollup-linux-arm64-musl": "4.12.0",
10573 + "@rollup/rollup-linux-riscv64-gnu": "4.12.0",
10574 + "@rollup/rollup-linux-x64-gnu": "4.12.0",
10575 + "@rollup/rollup-linux-x64-musl": "4.12.0",
10576 + "@rollup/rollup-win32-arm64-msvc": "4.12.0",
10577 + "@rollup/rollup-win32-ia32-msvc": "4.12.0",
10578 + "@rollup/rollup-win32-x64-msvc": "4.12.0",
10579 "fsevents": "~2.3.2"
10580 }
10581 },
@@ -10549,9 +10780,9 @@
10780 "dev": true
10781 },
10782 "node_modules/sass": {
10552 - "version": "1.71.1",
10553 - "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
10554 - "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
10783 + "version": "1.72.0",
10784 + "resolved": "https://registry.npmjs.org/sass/-/sass-1.72.0.tgz",
10785 + "integrity": "sha512-Gpczt3WA56Ly0Mn8Sl21Vj94s1axi9hDIzDFn9Ph9x3C3p4nNyvsqJoQyVXKou6cBlfFWEgRW4rT8Tb4i3XnVA==",
10786 "dev": true,
10787 "dependencies": {
10788 "chokidar": ">=3.0.0 <4.0.0",
@@ -10639,14 +10870,15 @@
10870 }
10871 },
10872 "node_modules/set-function-name": {
10642 - "version": "2.0.1",
10643 - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz",
10644 - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==",
10873 + "version": "2.0.2",
10874 + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
10875 + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
10876 "dev": true,
10877 "dependencies": {
10647 - "define-data-property": "^1.0.1",
10878 + "define-data-property": "^1.1.4",
10879 + "es-errors": "^1.3.0",
10880 "functions-have-names": "^1.2.3",
10649 - "has-property-descriptors": "^1.0.0"
10881 + "has-property-descriptors": "^1.0.2"
10882 },
10883 "engines": {
10884 "node": ">= 0.4"
@@ -10687,12 +10919,12 @@
10919 }
10920 },
10921 "node_modules/side-channel": {
10690 - "version": "1.0.5",
10691 - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz",
10692 - "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==",
10922 + "version": "1.0.6",
10923 + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
10924 + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
10925 "dev": true,
10926 "dependencies": {
10695 - "call-bind": "^1.0.6",
10927 + "call-bind": "^1.0.7",
10928 "es-errors": "^1.3.0",
10929 "get-intrinsic": "^1.2.4",
10930 "object-inspect": "^1.13.1"
@@ -10717,17 +10949,17 @@
10949 "dev": true
10950 },
10951 "node_modules/sigstore": {
10720 - "version": "2.2.1",
10721 - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.2.1.tgz",
10722 - "integrity": "sha512-OBBSKvmjr4DCyUb+IC2p7wooOCsCNwaqvCilTJVNPo0y8lJl+LsCrfz4LtMwnw3Gn+8frt816wi1+DWZTUCpBQ==",
10952 + "version": "2.2.2",
10953 + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-2.2.2.tgz",
10954 + "integrity": "sha512-2A3WvXkQurhuMgORgT60r6pOWiCOO5LlEqY2ADxGBDGVYLSo5HN0uLtb68YpVpuL/Vi8mLTe7+0Dx2Fq8lLqEg==",
10955 "dev": true,
10956 "dependencies": {
10725 - "@sigstore/bundle": "^2.1.1",
10957 + "@sigstore/bundle": "^2.2.0",
10958 "@sigstore/core": "^1.0.0",
10727 - "@sigstore/protobuf-specs": "^0.2.1",
10728 - "@sigstore/sign": "^2.2.2",
10729 - "@sigstore/tuf": "^2.3.0",
10730 - "@sigstore/verify": "^1.0.0"
10959 + "@sigstore/protobuf-specs": "^0.3.0",
10960 + "@sigstore/sign": "^2.2.3",
10961 + "@sigstore/tuf": "^2.3.1",
10962 + "@sigstore/verify": "^1.1.0"
10963 },
10964 "engines": {
10965 "node": "^16.14.0 || >=18.0.0"
@@ -10819,16 +11051,16 @@
11051 }
11052 },
11053 "node_modules/socks": {
10822 - "version": "2.7.1",
10823 - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
10824 - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
11054 + "version": "2.8.1",
11055 + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.1.tgz",
11056 + "integrity": "sha512-B6w7tkwNid7ToxjZ08rQMT8M9BJAf8DKx8Ft4NivzH0zBUfd6jldGcisJn/RLgxcX3FPNDdNQCUEMMT79b+oCQ==",
11057 "dev": true,
11058 "dependencies": {
10827 - "ip": "^2.0.0",
11059 + "ip-address": "^9.0.5",
11060 "smart-buffer": "^4.2.0"
11061 },
11062 "engines": {
10831 - "node": ">= 10.13.0",
11063 + "node": ">= 10.0.0",
11064 "npm": ">= 3.0.0"
11065 }
11066 },
@@ -10846,6 +11078,11 @@
11078 "node": ">= 14"
11079 }
11080 },
11081 + "node_modules/sortablejs": {
11082 + "version": "1.14.0",
11083 + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.14.0.tgz",
11084 + "integrity": "sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w=="
11085 + },
11086 "node_modules/source-map": {
11087 "version": "0.7.4",
11088 "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
@@ -10874,9 +11111,9 @@
11111 }
11112 },
11113 "node_modules/spdx-exceptions": {
10877 - "version": "2.4.0",
10878 - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz",
10879 - "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==",
11114 + "version": "2.5.0",
11115 + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
11116 + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
11117 "dev": true
11118 },
11119 "node_modules/spdx-expression-parse": {
@@ -10907,6 +11144,12 @@
11144 "node": "*"
11145 }
11146 },
11147 + "node_modules/sprintf-js": {
11148 + "version": "1.1.3",
11149 + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
11150 + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
11151 + "dev": true
11152 + },
11153 "node_modules/sshpk": {
11154 "version": "1.18.0",
11155 "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
@@ -11649,9 +11892,9 @@
11892 }
11893 },
11894 "node_modules/taze/node_modules/npm-run-path": {
11652 - "version": "5.2.0",
11653 - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
11654 - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
11895 + "version": "5.3.0",
11896 + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
11897 + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
11898 "dev": true,
11899 "dependencies": {
11900 "path-key": "^4.0.0"
@@ -11714,6 +11957,15 @@
11957 "url": "https://github.com/sponsors/sindresorhus"
11958 }
11959 },
11960 + "node_modules/text-segmentation": {
11961 + "version": "1.0.3",
11962 + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
11963 + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
11964 + "dev": true,
11965 + "dependencies": {
11966 + "utrie": "^1.0.2"
11967 + }
11968 + },
11969 "node_modules/text-table": {
11970 "version": "0.2.0",
11971 "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
@@ -11792,15 +12044,12 @@
12044 }
12045 },
12046 "node_modules/tmp": {
11795 - "version": "0.2.1",
11796 - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz",
11797 - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==",
12047 + "version": "0.2.3",
12048 + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
12049 + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==",
12050 "dev": true,
11799 - "dependencies": {
11800 - "rimraf": "^3.0.0"
11801 - },
12051 "engines": {
11803 - "node": ">=8.17.0"
12052 + "node": ">=14.14"
12053 }
12054 },
12055 "node_modules/to-fast-properties": {
@@ -11899,55 +12148,6 @@
12148 "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
12149 "dev": true
12150 },
11902 - "node_modules/ts-node": {
11903 - "version": "10.9.2",
11904 - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
11905 - "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
11906 - "dev": true,
11907 - "dependencies": {
11908 - "@cspotcode/source-map-support": "^0.8.0",
11909 - "@tsconfig/node10": "^1.0.7",
11910 - "@tsconfig/node12": "^1.0.7",
11911 - "@tsconfig/node14": "^1.0.0",
11912 - "@tsconfig/node16": "^1.0.2",
11913 - "acorn": "^8.4.1",
11914 - "acorn-walk": "^8.1.1",
11915 - "arg": "^4.1.0",
11916 - "create-require": "^1.1.0",
11917 - "diff": "^4.0.1",
11918 - "make-error": "^1.1.1",
11919 - "v8-compile-cache-lib": "^3.0.1",
11920 - "yn": "3.1.1"
11921 - },
11922 - "bin": {
11923 - "ts-node": "dist/bin.js",
11924 - "ts-node-cwd": "dist/bin-cwd.js",
11925 - "ts-node-esm": "dist/bin-esm.js",
11926 - "ts-node-script": "dist/bin-script.js",
11927 - "ts-node-transpile-only": "dist/bin-transpile.js",
11928 - "ts-script": "dist/bin-script-deprecated.js"
11929 - },
11930 - "peerDependencies": {
11931 - "@swc/core": ">=1.2.50",
11932 - "@swc/wasm": ">=1.2.50",
11933 - "@types/node": "*",
11934 - "typescript": ">=2.7"
11935 - },
11936 - "peerDependenciesMeta": {
11937 - "@swc/core": {
11938 - "optional": true
11939 - },
11940 - "@swc/wasm": {
11941 - "optional": true
11942 - }
11943 - }
11944 - },
11945 - "node_modules/ts-node/node_modules/arg": {
11946 - "version": "4.1.3",
11947 - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
11948 - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
11949 - "dev": true
11950 - },
12151 "node_modules/tslib": {
12152 "version": "2.3.0",
12153 "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
@@ -12039,12 +12239,12 @@
12239 }
12240 },
12241 "node_modules/typed-array-buffer": {
12042 - "version": "1.0.1",
12043 - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz",
12044 - "integrity": "sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ==",
12242 + "version": "1.0.2",
12243 + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
12244 + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
12245 "dev": true,
12246 "dependencies": {
12047 - "call-bind": "^1.0.6",
12247 + "call-bind": "^1.0.7",
12248 "es-errors": "^1.3.0",
12249 "is-typed-array": "^1.1.13"
12250 },
@@ -12053,15 +12253,16 @@
12253 }
12254 },
12255 "node_modules/typed-array-byte-length": {
12056 - "version": "1.0.0",
12057 - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz",
12058 - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==",
12256 + "version": "1.0.1",
12257 + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
12258 + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
12259 "dev": true,
12260 "dependencies": {
12061 - "call-bind": "^1.0.2",
12261 + "call-bind": "^1.0.7",
12262 "for-each": "^0.3.3",
12063 - "has-proto": "^1.0.1",
12064 - "is-typed-array": "^1.1.10"
12263 + "gopd": "^1.0.1",
12264 + "has-proto": "^1.0.3",
12265 + "is-typed-array": "^1.1.13"
12266 },
12267 "engines": {
12268 "node": ">= 0.4"
@@ -12071,16 +12272,17 @@
12272 }
12273 },
12274 "node_modules/typed-array-byte-offset": {
12074 - "version": "1.0.0",
12075 - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz",
12076 - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==",
12275 + "version": "1.0.2",
12276 + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
12277 + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
12278 "dev": true,
12279 "dependencies": {
12079 - "available-typed-arrays": "^1.0.5",
12080 - "call-bind": "^1.0.2",
12280 + "available-typed-arrays": "^1.0.7",
12281 + "call-bind": "^1.0.7",
12282 "for-each": "^0.3.3",
12082 - "has-proto": "^1.0.1",
12083 - "is-typed-array": "^1.1.10"
12283 + "gopd": "^1.0.1",
12284 + "has-proto": "^1.0.3",
12285 + "is-typed-array": "^1.1.13"
12286 },
12287 "engines": {
12288 "node": ">= 0.4"
@@ -12090,14 +12292,20 @@
12292 }
12293 },
12294 "node_modules/typed-array-length": {
12093 - "version": "1.0.4",
12094 - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz",
12095 - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==",
12295 + "version": "1.0.5",
12296 + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz",
12297 + "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==",
12298 "dev": true,
12299 "dependencies": {
12098 - "call-bind": "^1.0.2",
12300 + "call-bind": "^1.0.7",
12301 "for-each": "^0.3.3",
12100 - "is-typed-array": "^1.1.9"
12302 + "gopd": "^1.0.1",
12303 + "has-proto": "^1.0.3",
12304 + "is-typed-array": "^1.1.13",
12305 + "possible-typed-array-names": "^1.0.0"
12306 + },
12307 + "engines": {
12308 + "node": ">= 0.4"
12309 },
12310 "funding": {
12311 "url": "https://github.com/sponsors/ljharb"
@@ -12107,6 +12315,7 @@
12315 "version": "5.3.3",
12316 "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
12317 "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
12318 + "peer": true,
12319 "bin": {
12320 "tsc": "bin/tsc",
12321 "tsserver": "bin/tsserver"
@@ -12194,13 +12403,13 @@
12403 }
12404 },
12405 "node_modules/unplugin": {
12197 - "version": "1.7.1",
12198 - "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.7.1.tgz",
12199 - "integrity": "sha512-JqzORDAPxxs8ErLV4x+LL7bk5pk3YlcWqpSNsIkAZj972KzFZLClc/ekppahKkOczGkwIG6ElFgdOgOlK4tXZw==",
12406 + "version": "1.8.0",
12407 + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.8.0.tgz",
12408 + "integrity": "sha512-yGEQsodWICmgt7asHF7QzqDZYeEP9h14vyd9Lul98UnYf29pLZZLwI09z2QdTjwU/FCkum1SRvsK7cx232X8NA==",
12409 "dev": true,
12410 "dependencies": {
12411 "acorn": "^8.11.3",
12203 - "chokidar": "^3.5.3",
12412 + "chokidar": "^3.6.0",
12413 "webpack-sources": "^3.2.3",
12414 "webpack-virtual-modules": "^0.6.1"
12415 }
@@ -12304,6 +12513,15 @@
12513 "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
12514 "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
12515 },
12516 + "node_modules/utrie": {
12517 + "version": "1.0.2",
12518 + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
12519 + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
12520 + "dev": true,
12521 + "dependencies": {
12522 + "base64-arraybuffer": "^1.0.2"
12523 + }
12524 + },
12525 "node_modules/uuid": {
12526 "version": "8.3.2",
12527 "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
@@ -12313,12 +12531,6 @@
12531 "uuid": "dist/bin/uuid"
12532 }
12533 },
12316 - "node_modules/v8-compile-cache-lib": {
12317 - "version": "3.0.1",
12318 - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
12319 - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
12320 - "dev": true
12321 - },
12534 "node_modules/validate-npm-package-license": {
12535 "version": "3.0.4",
12536 "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
@@ -12384,9 +12596,9 @@
12596 }
12597 },
12598 "node_modules/vite": {
12387 - "version": "5.1.4",
12388 - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz",
12389 - "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==",
12599 + "version": "5.1.6",
12600 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz",
12601 + "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==",
12602 "dev": true,
12603 "dependencies": {
12604 "esbuild": "^0.19.3",
@@ -12439,9 +12651,9 @@
12651 }
12652 },
12653 "node_modules/vite-bundle-analyzer": {
12442 - "version": "0.8.0",
12443 - "resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-0.8.0.tgz",
12444 - "integrity": "sha512-qoUFZW7vo2H3c/j2+phhWS7BH7cEu64PAfb+Ze/LEbv15q+HiOadBvMlFlk0dPmJwc6T4LFlngIp2EWDwuFMvg==",
12654 + "version": "0.8.3",
12655 + "resolved": "https://registry.npmjs.org/vite-bundle-analyzer/-/vite-bundle-analyzer-0.8.3.tgz",
12656 + "integrity": "sha512-R4QRR9lruBooroMm9sxxIGC5UxTzux7MFS5MZYu2LS2BWdd/mj4bnPnB7hSGgOEHYZHXNc+yCOPpmxauKyN5KA==",
12657 "dev": true,
12658 "dependencies": {
12659 "fast-glob": "^3.3.1",
@@ -12482,9 +12694,9 @@
12694 }
12695 },
12696 "node_modules/vite-bundle-visualizer": {
12485 - "version": "1.0.1",
12486 - "resolved": "https://registry.npmjs.org/vite-bundle-visualizer/-/vite-bundle-visualizer-1.0.1.tgz",
12487 - "integrity": "sha512-JdUu5viGyw7K1HMstqaAN7y1rnNz93srGeF7FJgFCzM7NL1nH/QlpywDA296qv/KjPPPsq60mOJhtXddikVKSA==",
12697 + "version": "1.1.0",
12698 + "resolved": "https://registry.npmjs.org/vite-bundle-visualizer/-/vite-bundle-visualizer-1.1.0.tgz",
12699 + "integrity": "sha512-cmi5OuS7Eta5keTJmCTEbBBA7gOsUQ4K44W5dbsP+n/X0GIilIIFbJeXF120MQpTxdiZ/GIx4A9zkPEcKpPAog==",
12700 "dev": true,
12701 "dependencies": {
12702 "cac": "^6.7.14",
@@ -12494,6 +12706,9 @@
12706 },
12707 "bin": {
12708 "vite-bundle-visualizer": "bin.js"
12709 + },
12710 + "engines": {
12711 + "node": "^18.19.0 || >=20.6.0"
12712 }
12713 },
12714 "node_modules/vite-node": {
@@ -12680,9 +12895,9 @@
12895 }
12896 },
12897 "node_modules/vitest/node_modules/npm-run-path": {
12683 - "version": "5.2.0",
12684 - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz",
12685 - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==",
12898 + "version": "5.3.0",
12899 + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
12900 + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
12901 "dev": true,
12902 "dependencies": {
12903 "path-key": "^4.0.0"
@@ -12757,15 +12972,15 @@
12972 }
12973 },
12974 "node_modules/vue": {
12760 - "version": "3.4.19",
12761 - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.19.tgz",
12762 - "integrity": "sha512-W/7Fc9KUkajFU8dBeDluM4sRGc/aa4YJnOYck8dkjgZoXtVsn3OeTGni66FV1l3+nvPA7VBFYtPioaGKUmEADw==",
12975 + "version": "3.4.21",
12976 + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.21.tgz",
12977 + "integrity": "sha512-5hjyV/jLEIKD/jYl4cavMcnzKwjMKohureP8ejn3hhEjwhWIhWeuzL2kJAjzl/WyVsgPY56Sy4Z40C3lVshxXA==",
12978 "dependencies": {
12764 - "@vue/compiler-dom": "3.4.19",
12765 - "@vue/compiler-sfc": "3.4.19",
12766 - "@vue/runtime-dom": "3.4.19",
12767 - "@vue/server-renderer": "3.4.19",
12768 - "@vue/shared": "3.4.19"
12979 + "@vue/compiler-dom": "3.4.21",
12980 + "@vue/compiler-sfc": "3.4.21",
12981 + "@vue/runtime-dom": "3.4.21",
12982 + "@vue/server-renderer": "3.4.21",
12983 + "@vue/shared": "3.4.21"
12984 },
12985 "peerDependencies": {
12986 "typescript": "*"
@@ -12794,9 +13009,9 @@
13009 }
13010 },
13011 "node_modules/vue-component-type-helpers": {
12797 - "version": "1.8.27",
12798 - "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.8.27.tgz",
12799 - "integrity": "sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==",
13012 + "version": "2.0.6",
13013 + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-2.0.6.tgz",
13014 + "integrity": "sha512-qdGXCtoBrwqk1BT6r2+1Wcvl583ZVkuSZ3or7Y1O2w5AvWtlvvxwjGhmz5DdPJS9xqRdDlgTJ/38ehWnEi0tFA==",
13015 "dev": true
13016 },
13017 "node_modules/vue-eslint-parser": {
@@ -12834,12 +13049,12 @@
13049 }
13050 },
13051 "node_modules/vue-i18n": {
12837 - "version": "9.9.1",
12838 - "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.9.1.tgz",
12839 - "integrity": "sha512-xyQ4VspLdNSPTKBFBPWa1tvtj+9HuockZwgFeD2OhxxXuC2CWeNvV4seu2o9+vbQOyQbhAM5Ez56oxUrrnTWdw==",
13052 + "version": "9.10.1",
13053 + "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.10.1.tgz",
13054 + "integrity": "sha512-37HVJQZ/pZaRXGzFmmMomM1u1k7kndv3xCBPYHKEVfv5W3UVK67U/TpBug71ILYLNmjHLHdvTUPRF81pFT5fFg==",
13055 "dependencies": {
12841 - "@intlify/core-base": "9.9.1",
12842 - "@intlify/shared": "9.9.1",
13056 + "@intlify/core-base": "9.10.1",
13057 + "@intlify/shared": "9.10.1",
13058 "@vue/devtools-api": "^6.5.0"
13059 },
13060 "engines": {
@@ -12885,13 +13100,13 @@
13100 }
13101 },
13102 "node_modules/vue-tsc": {
12888 - "version": "1.8.27",
12889 - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-1.8.27.tgz",
12890 - "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==",
13103 + "version": "2.0.6",
13104 + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.6.tgz",
13105 + "integrity": "sha512-kK50W4XqQL34vHRkxlRWLicrT6+F9xfgCgJ4KSmCHcytKzc1u3c94XXgI+CjmhOSxyw0krpExF7Obo7y4+0dVQ==",
13106 "dev": true,
13107 "dependencies": {
12893 - "@volar/typescript": "~1.11.1",
12894 - "@vue/language-core": "1.8.27",
13108 + "@volar/typescript": "~2.1.2",
13109 + "@vue/language-core": "2.0.6",
13110 "semver": "^7.5.4"
13111 },
13112 "bin": {
@@ -12921,6 +13136,17 @@
13136 "vue": "^3.2"
13137 }
13138 },
13139 + "node_modules/vuedraggable": {
13140 + "version": "4.1.0",
13141 + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-4.1.0.tgz",
13142 + "integrity": "sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==",
13143 + "dependencies": {
13144 + "sortablejs": "1.14.0"
13145 + },
13146 + "peerDependencies": {
13147 + "vue": "^3.0.1"
13148 + }
13149 + },
13150 "node_modules/vueuc": {
13151 "version": "0.4.58",
13152 "resolved": "https://registry.npmjs.org/vueuc/-/vueuc-0.4.58.tgz",
@@ -13293,10 +13519,13 @@
13519 "dev": true
13520 },
13521 "node_modules/yaml": {
13296 - "version": "2.3.4",
13297 - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
13298 - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
13522 + "version": "2.4.0",
13523 + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz",
13524 + "integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==",
13525 "dev": true,
13526 + "bin": {
13527 + "yaml": "bin.mjs"
13528 + },
13529 "engines": {
13530 "node": ">= 14"
13531 }
@@ -13347,15 +13576,6 @@
13576 "node": ">= 4.0.0"
13577 }
13578 },
13350 - "node_modules/yn": {
13351 - "version": "3.1.1",
13352 - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
13353 - "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
13354 - "dev": true,
13355 - "engines": {
13356 - "node": ">=6"
13357 - }
13358 - },
13579 "node_modules/yocto-queue": {
13580 "version": "0.1.0",
13581 "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
frontend/package.json
+24 -22
@@ -35,20 +35,21 @@
35 "dependencies": {
36 "@ajoelp/json-to-formdata": "^1.5.0",
37 "@f3ve/vue-markdown-it": "^0.2.0",
38 - "@fontsource/jetbrains-mono": "^5.0.18",
39 - "@fontsource/lexend": "^5.0.18",
40 - "@fontsource/public-sans": "^5.0.16",
38 + "@fontsource/jetbrains-mono": "^5.0.19",
39 + "@fontsource/lexend": "^5.0.19",
40 + "@fontsource/public-sans": "^5.0.17",
41 "@popperjs/core": "^2.11.8",
42 "@vueuse/components": "^10.9.0",
43 "@vueuse/core": "^10.9.0",
44 - "apexcharts": "^3.46.0",
44 + "apexcharts": "^3.47.0",
45 "bytes": "^3.1.2",
46 "colord": "^2.9.3",
47 "crypto-js": "^4.2.0",
48 "dayjs": "^1.11.10",
49 "detect-touch-device": "^1.1.6",
50 "echarts": "^5.5.0",
51 - "jose": "^5.2.2",
51 + "file-saver": "^2.0.5",
52 + "jose": "^5.2.3",
53 "js-md5": "^0.8.3",
54 "lodash": "^4.17.21",
55 "markdown-it-highlightjs": "^4.0.1",
@@ -59,14 +60,15 @@
60 "pinia-plugin-persistedstate": "^3.2.1",
61 "secure-ls": "^1.2.6",
62 "validator": "^13.11.0",
62 - "vue": "^3.4.20",
63 + "vue": "^3.4.21",
64 "vue-advanced-cropper": "^2.8.8",
65 "vue-highlight-words": "^3.0.1",
65 - "vue-i18n": "^9.9.1",
66 + "vue-i18n": "^9.10.1",
67 "vue-router": "^4.3.0",
68 "vue-sjv": "^0.0.6",
69 "vue3-apexcharts": "^1.5.2",
69 - "vue3-marquee": "^4.2.0"
70 + "vue3-marquee": "^4.2.0",
71 + "vuedraggable": "^4.1.0"
72 },
73 "devDependencies": {
74 "@clack/prompts": "^0.7.0",
@@ -74,26 +76,28 @@
76 "@rushstack/eslint-patch": "^1.7.2",
77 "@tsconfig/node18": "^18.2.2",
78 "@types/bytes": "^3.1.4",
79 + "@types/file-saver": "^2.0.7",
80 "@types/fs-extra": "^11.0.4",
81 "@types/highlight.js": "^10.1.0",
82 + "@types/html2canvas": "^1.0.0",
83 "@types/inquirer": "^9.0.7",
84 "@types/jsdom": "^21.1.6",
81 - "@types/lodash": "^4.14.202",
85 + "@types/lodash": "^4.17.0",
86 "@types/markdown-it": "^13.0.7",
87 "@types/markdown-it-highlightjs": "^3.3.4",
84 - "@types/node": "^20.11.20",
88 + "@types/node": "^20.11.27",
89 "@types/validator": "^13.11.9",
90 "@vitejs/plugin-vue": "^5.0.4",
91 "@vitejs/plugin-vue-jsx": "^3.1.0",
92 "@vue/eslint-config-prettier": "^9.0.0",
89 - "@vue/eslint-config-typescript": "^12.0.0",
90 - "@vue/test-utils": "^2.4.4",
93 + "@vue/eslint-config-typescript": "^13.0.0",
94 + "@vue/test-utils": "^2.4.5",
95 "@vue/tsconfig": "^0.5.1",
92 - "autoprefixer": "^10.4.17",
93 - "cypress": "^13.6.6",
96 + "autoprefixer": "^10.4.18",
97 + "cypress": "^13.7.0",
98 "eslint": "^8.57.0",
99 "eslint-plugin-cypress": "^2.15.1",
96 - "eslint-plugin-vue": "^9.22.0",
100 + "eslint-plugin-vue": "^9.23.0",
101 "fs-extra": "^11.2.0",
102 "ip": "^2.0.1",
103 "jsdom": "^24.0.0",
@@ -102,20 +106,18 @@
106 "picocolors": "^1.0.0",
107 "postcss": "^8.4.35",
108 "prettier": "^3.2.5",
105 - "sass": "^1.71.1",
109 + "sass": "^1.72.0",
110 "start-server-and-test": "^2.0.3",
111 "tailwind-config-viewer": "^1.7.3",
112 "tailwindcss": "^3.4.1",
113 "taze": "^0.13.3",
110 - "ts-node": "^10.9.2",
111 - "typescript": "~5.3.3",
114 "unplugin-vue-components": "^0.26.0",
113 - "vite": "^5.1.4",
114 - "vite-bundle-analyzer": "^0.8.1",
115 - "vite-bundle-visualizer": "^1.0.1",
115 + "vite": "^5.1.6",
116 + "vite-bundle-analyzer": "^0.8.3",
117 + "vite-bundle-visualizer": "^1.1.0",
118 "vite-svg-loader": "^5.1.0",
119 "vitest": "^1.3.1",
118 - "vue-tsc": "^1.8.27"
120 + "vue-tsc": "^2.0.6"
121 },
122 "engines": {
123 "node": ">=18.0.0"
frontend/public/images/connectors/haproxy provisioning.svg new
+3
@@ -0,0 +1,3 @@
1 +<?xml version="1.0" standalone="no"?>
2 +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
3 +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg" version="1.1" width="400" height="497.1875" viewBox="0, 0, 400,497.1875"><g id="svgg"><path id="path0" d="M163.187 283.504 C 163.117 320.247,163.007 318.404,165.423 320.832 C 165.964 321.375,166.881 322.392,167.461 323.091 C 168.041 323.791,168.642 324.285,168.797 324.190 C 168.952 324.094,169.045 324.132,169.004 324.274 C 168.877 324.716,169.816 325.938,170.284 325.938 C 170.529 325.938,170.635 326.032,170.520 326.147 C 170.405 326.261,170.544 326.613,170.829 326.928 C 171.324 327.474,172.623 327.500,199.952 327.500 L 228.557 327.500 229.669 326.299 C 237.848 317.465,236.850 323.401,236.796 283.906 C 236.771 265.688,236.702 251.801,236.643 253.047 L 236.535 255.313 200.000 255.313 L 163.465 255.313 163.357 253.047 C 163.298 251.801,163.221 265.506,163.187 283.504 M0.313 265.477 C 0.313 275.210,0.341 275.707,0.973 276.966 C 2.064 279.137,3.126 280.334,4.489 280.930 C 5.200 281.240,7.750 282.439,10.156 283.594 C 12.563 284.749,16.289 286.538,18.438 287.569 C 20.586 288.600,23.328 289.905,24.531 290.469 C 25.734 291.033,29.180 292.679,32.188 294.128 C 35.195 295.576,38.254 297.025,38.984 297.347 L 40.313 297.933 40.382 317.326 C 40.420 327.992,40.494 334.645,40.546 332.109 L 40.642 327.500 86.406 327.500 L 132.171 327.500 132.278 332.109 C 132.337 334.645,132.364 323.500,132.338 307.344 C 132.296 281.827,132.223 277.579,131.783 275.000 C 130.884 269.738,130.032 267.327,127.695 263.438 C 126.365 261.225,123.870 258.438,123.220 258.438 C 122.996 258.438,122.813 258.313,122.813 258.161 C 122.812 258.008,122.355 257.653,121.797 257.371 C 120.339 256.634,119.564 256.139,119.308 255.781 C 119.161 255.575,98.839 255.441,59.699 255.387 L 0.313 255.305 0.313 265.477 M280.703 255.737 C 280.316 255.929,280.000 256.193,280.000 256.324 C 280.000 256.455,279.812 256.563,279.582 256.563 C 278.920 256.563,275.625 259.049,275.625 259.548 C 275.625 259.797,275.438 260.000,275.209 260.000 C 274.444 260.000,272.173 263.141,270.916 265.938 C 270.568 266.711,270.166 267.562,270.021 267.829 C 269.668 268.483,268.526 273.110,268.097 275.625 C 267.864 276.992,267.728 287.319,267.682 307.188 C 267.644 323.430,267.661 334.645,267.721 332.109 L 267.829 327.500 313.594 327.500 L 359.358 327.500 359.454 332.109 C 359.506 334.645,359.582 328.000,359.622 317.344 L 359.695 297.969 360.863 297.423 C 363.848 296.030,370.955 292.650,375.739 290.348 C 378.638 288.954,381.044 287.813,381.087 287.813 C 381.131 287.813,384.103 286.390,387.692 284.652 C 391.282 282.914,394.922 281.178,395.781 280.794 C 399.749 279.022,399.869 278.559,399.757 265.445 L 399.671 255.313 340.539 255.351 C 297.771 255.379,281.212 255.486,280.703 255.737 M172.721 329.024 C 173.441 329.716,173.926 329.912,173.595 329.377 C 173.489 329.206,173.094 328.907,172.717 328.713 L 172.031 328.360 172.721 329.024 " stroke="none" fill="#fbab44" fill-rule="evenodd"/><path id="path1" d="M0.207 176.484 C 0.094 180.223,0.034 204.094,0.074 229.531 C 0.115 254.969,0.186 271.175,0.234 265.545 L 0.321 255.309 59.223 255.389 C 91.619 255.433,118.125 255.435,118.125 255.395 C 118.125 255.251,115.844 254.370,115.142 254.243 C 114.750 254.172,114.142 253.960,113.790 253.771 C 113.438 253.583,113.039 253.448,112.903 253.470 C 112.767 253.493,112.586 253.484,112.500 253.451 C 111.792 253.175,104.237 251.869,103.433 251.884 C 103.006 251.892,101.953 251.817,101.094 251.717 C 99.332 251.512,96.400 251.225,94.531 251.076 C 93.844 251.021,92.508 250.892,91.563 250.789 C 90.617 250.686,89.281 250.542,88.594 250.469 C 68.487 248.333,56.970 240.804,48.921 224.531 C 42.866 212.290,38.598 191.192,38.317 172.109 L 38.281 169.688 19.347 169.688 L 0.412 169.688 0.207 176.484 M39.844 170.000 C 39.950 170.172,40.240 170.313,40.487 170.313 C 40.735 170.313,40.938 170.418,40.939 170.547 C 40.942 170.889,42.824 171.905,43.117 171.724 C 43.254 171.639,43.540 171.779,43.753 172.035 C 43.965 172.291,44.262 172.500,44.413 172.500 C 44.564 172.500,44.969 172.781,45.313 173.125 C 45.656 173.469,46.212 173.750,46.547 173.750 C 46.882 173.750,47.269 173.940,47.406 174.173 C 47.544 174.405,48.310 174.957,49.110 175.399 C 49.909 175.841,51.136 176.565,51.838 177.008 C 52.539 177.450,53.189 177.813,53.282 177.813 C 53.375 177.813,53.975 178.147,54.616 178.556 C 55.257 178.964,58.172 180.669,61.094 182.345 C 64.016 184.020,68.023 186.338,70.000 187.495 C 74.085 189.886,97.436 203.374,124.375 218.902 C 147.524 232.246,148.698 232.924,151.719 234.704 C 153.094 235.514,154.790 236.464,155.489 236.815 C 160.132 239.149,162.895 244.302,163.298 251.380 L 163.522 255.313 199.996 255.313 L 236.471 255.313 236.688 251.641 C 237.236 242.355,238.811 240.161,249.253 234.146 C 264.241 225.511,277.317 217.958,288.906 211.239 C 290.994 210.029,296.178 207.048,303.281 202.973 C 307.406 200.607,312.398 197.720,314.375 196.559 C 316.352 195.398,318.461 194.168,319.063 193.826 C 319.664 193.484,322.898 191.618,326.250 189.679 C 329.602 187.741,332.766 185.916,333.281 185.625 C 334.295 185.052,341.339 180.967,343.125 179.916 C 343.727 179.563,345.977 178.235,348.125 176.967 C 350.273 175.698,352.313 174.495,352.656 174.292 C 353.000 174.090,353.809 173.602,354.453 173.208 C 355.098 172.814,355.625 172.590,355.625 172.710 C 355.625 172.831,356.030 172.542,356.525 172.068 C 357.311 171.314,357.854 171.055,358.847 170.961 C 358.986 170.948,359.013 170.798,358.908 170.629 C 358.799 170.451,358.924 170.399,359.203 170.506 C 359.469 170.608,359.688 170.547,359.688 170.369 C 359.688 170.191,359.934 169.982,360.234 169.903 C 360.535 169.824,288.527 169.744,200.216 169.724 C 93.243 169.700,39.715 169.792,39.844 170.000 M361.724 171.954 C 361.726 173.200,361.637 175.063,361.525 176.094 C 361.413 177.125,361.234 179.336,361.127 181.006 C 358.825 217.064,347.461 239.582,327.963 246.718 C 325.065 247.779,324.284 248.053,323.910 248.143 C 323.830 248.162,323.023 248.383,322.116 248.633 C 321.210 248.884,320.117 249.142,319.688 249.208 C 319.258 249.273,317.641 249.538,316.094 249.797 C 313.333 250.258,311.392 250.513,308.281 250.822 C 306.745 250.975,306.251 251.017,302.188 251.334 C 300.248 251.485,299.357 251.578,295.577 252.021 C 293.281 252.291,288.384 253.131,287.813 253.354 C 287.555 253.454,287.168 253.508,286.953 253.473 C 286.738 253.438,286.563 253.572,286.563 253.770 C 286.563 253.968,286.365 254.054,286.124 253.961 C 285.883 253.869,285.598 253.881,285.490 253.989 C 285.383 254.097,285.123 254.216,284.913 254.254 C 279.486 255.244,280.800 255.268,340.855 255.290 L 399.679 255.313 399.766 265.391 C 399.814 270.934,399.886 254.727,399.926 229.375 C 399.966 204.023,399.906 180.223,399.793 176.484 L 399.588 169.688 380.653 169.688 L 361.719 169.689 361.724 171.954 " stroke="none" fill="#fb9b3c" fill-rule="evenodd"/><path id="path2" d="M43.051 418.013 C 42.581 418.263,41.808 418.977,41.333 419.600 L 40.469 420.732 40.387 456.698 L 40.305 492.664 41.012 494.048 C 41.401 494.809,42.116 495.722,42.601 496.075 L 43.483 496.719 199.724 496.798 C 338.186 496.868,356.068 496.823,356.873 496.408 C 357.372 496.149,358.174 495.422,358.656 494.790 L 359.531 493.642 359.531 457.212 L 359.531 420.781 358.631 419.605 C 356.922 417.371,358.257 417.478,333.366 417.572 C 308.066 417.667,309.971 417.467,308.594 420.176 C 307.992 421.360,307.965 422.117,307.881 440.391 L 307.794 459.375 289.060 459.375 L 270.326 459.375 270.241 439.914 L 270.156 420.453 269.428 419.523 C 267.785 417.426,269.037 417.528,244.756 417.513 C 220.973 417.499,221.546 417.460,220.063 419.198 C 218.773 420.710,218.750 421.077,218.750 440.489 L 218.750 459.375 200.000 459.375 L 181.250 459.375 181.250 440.507 C 181.250 418.749,181.296 419.072,178.027 417.833 C 176.227 417.152,133.830 417.407,132.284 418.108 C 129.592 419.331,129.688 418.513,129.688 440.300 L 129.688 459.375 110.938 459.375 L 92.188 459.375 92.188 440.973 C 92.188 420.244,92.175 420.112,90.046 418.489 L 88.955 417.656 66.431 417.608 C 47.275 417.567,43.778 417.628,43.051 418.013 " stroke="none" fill="#fccf5b" fill-rule="evenodd"/><path id="path3" d="M2.859 0.840 C 2.283 1.125,1.439 1.922,0.984 2.610 L 0.156 3.861 0.146 91.383 C 0.140 139.521,0.179 176.834,0.232 174.302 L 0.329 169.697 19.461 169.614 C 29.984 169.569,38.760 169.602,38.962 169.688 C 39.165 169.773,111.798 169.773,200.368 169.688 C 288.939 169.602,370.016 169.569,380.539 169.614 L 399.671 169.697 399.768 174.302 C 399.821 176.834,399.860 139.521,399.854 91.383 L 399.844 3.861 399.012 2.604 C 397.377 0.133,399.990 0.294,363.069 0.386 L 330.156 0.469 329.104 1.219 C 326.750 2.898,326.890 0.648,326.882 37.109 L 326.875 69.688 308.906 69.688 L 290.938 69.688 290.938 44.441 L 290.937 19.195 290.234 17.802 C 289.772 16.886,289.103 16.169,288.281 15.707 L 287.031 15.006 254.163 15.081 C 217.137 15.166,220.575 14.931,218.828 17.497 L 218.125 18.530 218.125 44.109 L 218.125 69.688 200.000 69.688 L 181.875 69.688 181.875 44.109 L 181.875 18.530 181.172 17.497 C 179.425 14.931,182.863 15.166,145.837 15.081 L 112.969 15.006 111.719 15.707 C 110.897 16.169,110.228 16.886,109.766 17.802 L 109.062 19.195 109.063 44.441 L 109.063 69.688 91.094 69.688 L 73.125 69.688 73.118 37.109 C 73.110 0.648,73.250 2.898,70.896 1.219 L 69.844 0.469 36.875 0.395 C 6.708 0.328,3.817 0.366,2.859 0.840 M361.648 171.250 C 361.648 172.023,361.709 172.340,361.783 171.953 C 361.858 171.566,361.858 170.934,361.783 170.547 C 361.709 170.160,361.648 170.477,361.648 171.250 M357.144 171.568 L 356.406 172.198 357.266 171.830 C 357.738 171.628,358.125 171.344,358.125 171.200 C 358.125 170.810,357.958 170.873,357.144 171.568 M43.125 171.759 C 43.125 171.921,43.547 172.302,44.063 172.607 C 44.580 172.912,45.000 173.012,45.000 172.830 C 45.000 172.649,44.835 172.500,44.634 172.500 C 44.433 172.500,44.011 172.267,43.696 171.983 C 43.382 171.698,43.125 171.598,43.125 171.759 " stroke="none" fill="#fc862e" fill-rule="evenodd"/><path id="path4" d="M40.519 334.297 C 40.406 338.035,40.317 349.602,40.320 360.000 C 40.327 381.137,40.273 380.601,42.542 382.218 L 43.594 382.969 86.025 383.050 C 123.482 383.122,128.563 383.076,129.374 382.657 C 130.775 381.932,131.789 380.671,132.162 379.189 C 132.542 377.680,132.636 345.641,132.293 334.297 L 132.088 327.500 86.406 327.500 L 40.725 327.500 40.519 334.297 M172.522 328.457 C 173.111 328.983,173.594 329.525,173.594 329.661 C 173.594 329.965,174.624 331.148,183.281 340.783 C 185.000 342.696,187.250 345.224,188.281 346.401 C 189.313 347.577,190.302 348.692,190.481 348.879 C 190.660 349.066,191.920 350.484,193.281 352.031 C 196.329 355.495,196.773 355.907,198.037 356.446 C 200.772 357.613,202.678 356.646,206.742 352.031 C 208.407 350.141,210.278 348.028,210.900 347.335 C 211.522 346.643,212.664 345.373,213.438 344.512 C 214.211 343.651,215.941 341.731,217.282 340.245 C 218.623 338.758,220.029 337.148,220.407 336.665 C 221.032 335.867,223.081 333.572,224.453 332.135 C 224.754 331.820,224.941 331.563,224.868 331.563 C 224.751 331.563,225.925 330.264,227.905 328.203 L 228.581 327.500 200.015 327.500 L 171.450 327.500 172.522 328.457 M267.707 334.297 C 267.364 345.641,267.458 377.680,267.838 379.189 C 268.211 380.671,269.225 381.932,270.626 382.657 C 271.437 383.076,276.518 383.122,313.975 383.050 L 356.406 382.969 357.458 382.218 C 359.727 380.601,359.673 381.137,359.680 360.000 C 359.683 349.602,359.594 338.035,359.481 334.297 L 359.275 327.500 313.594 327.500 L 267.912 327.500 267.707 334.297 " stroke="none" fill="#fcbc4c" fill-rule="evenodd"/></g></svg>
frontend/src/api/customers.ts
+5 -2
@@ -21,6 +21,7 @@ export interface ProvisioningDefaultSettingsPayload {
21 clusterKey: string
22 masterIp: string
23 grafanaUrl: string
24 + wazuhWorkerHostname: string
25 }
26
27 export default {
@@ -143,7 +144,8 @@ export default {
144 cluster_name: payload.clusterName,
145 cluster_key: payload.clusterKey,
146 master_ip: payload.masterIp,
146 - grafana_url: payload.grafanaUrl
147 + grafana_url: payload.grafanaUrl,
148 + wazuh_worker_hostname: payload.wazuhWorkerHostname
149 })
150 },
151 updateProvisioningDefaultSettings(payload: ProvisioningDefaultSettingsPayload) {
@@ -154,7 +156,8 @@ export default {
156 cluster_name: payload.clusterName,
157 cluster_key: payload.clusterKey,
158 master_ip: payload.masterIp,
157 - grafana_url: payload.grafanaUrl
159 + grafana_url: payload.grafanaUrl,
160 + wazuh_worker_hostname: payload.wazuhWorkerHostname
161 })
162 }
163 }
frontend/src/api/index.ts
+3 -1
@@ -16,6 +16,7 @@ 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
21 export default {
22 agents,
@@ -35,5 +36,6 @@ export default {
36 integrations,
37 monitoringAlerts,
38 activeResponse,
38 - stackProvisioning
39 + stackProvisioning,
40 + reporting
41 }
frontend/src/api/reporting.ts new
+65
@@ -0,0 +1,65 @@
1 +import type { Dashboard, Org, Panel, PanelLink } from "@/types/reporting"
2 +import { HttpClient } from "./httpClient"
3 +import type { FlaskBaseResponse } from "@/types/flask.d"
4 +
5 +export type PanelsLinksTimeUnit = "minutes" | "hours" | "days"
6 +
7 +export interface PanelsLinksPayload {
8 + org_id: string | number
9 + dashboard_title: string
10 + dashboard_uid: string
11 + panel_ids: number[]
12 + time_range: {
13 + value: number
14 + unit: PanelsLinksTimeUnit
15 + }
16 +}
17 +
18 +export type RowPanelTimeUnit = "m" | "h" | "d"
19 +
20 +export interface RowPanelPayload {
21 + org_id: number
22 + dashboard_title: string
23 + dashboard_uid: string
24 + panel_id: number
25 + panel_width: number
26 + panel_height: number
27 + theme: "light" | "dark"
28 +}
29 +
30 +export interface RowPayload {
31 + id: string | number
32 + panels: RowPanelPayload[]
33 +}
34 +
35 +export interface GenerateReportPayload {
36 + timerange: ReportTimeRange
37 + timerange_text: string
38 + logo_base64: string
39 + company_name: string
40 + rows: RowPayload[]
41 +}
42 +
43 +export type ReportTimeRange = `${number}${RowPanelTimeUnit}`
44 +
45 +export default {
46 + getOrgs() {
47 + return HttpClient.get<FlaskBaseResponse & { orgs: Org[] }>(`/reporting/orgs`)
48 + },
49 + getDashboards(orgId: string) {
50 + return HttpClient.get<FlaskBaseResponse & { dashboards: Dashboard[] }>(`/reporting/dashboards/${orgId}`)
51 + },
52 + getPanels(dashboardUID: string) {
53 + return HttpClient.get<FlaskBaseResponse & { panels: Panel[] }>(`/reporting/dashboard_panels/${dashboardUID}`)
54 + },
55 + generatePanelsLinks(payload: PanelsLinksPayload) {
56 + return HttpClient.post<FlaskBaseResponse & { links: PanelLink[] }>(`/reporting/generate_iframe_links`, payload)
57 + },
58 + generateReport({ timerange, timerange_text, logo_base64, company_name, rows }: GenerateReportPayload) {
59 + return HttpClient.post<FlaskBaseResponse & { base64_result: string }>(
60 + `/reporting/generate-report`,
61 + { timerange, timerange_text, logo_base64, company_name, rows },
62 + { timeout: 0 }
63 + )
64 + }
65 +}
frontend/src/assets/scss/index.scss
+48
@@ -19,6 +19,7 @@ body {
19 text-rendering: optimizeLegibility;
20 line-height: 1.35;
21 font-size: 15px;
22 + text-wrap: pretty;
23
24 margin: 0;
25 padding: 0;
@@ -150,3 +151,50 @@ mark {
151 background-color: var(--primary-030-color);
152 color: var(--fg-color);
153 }
154 +
155 +@media print {
156 + html,
157 + body {
158 + overflow: initial;
159 + }
160 + body {
161 + #app {
162 + height: initial;
163 + background-color: white;
164 +
165 + & > .n-config-provider {
166 + & > .layout {
167 + display: block;
168 + height: initial;
169 + background-color: white;
170 +
171 + & > .header-bar {
172 + display: none;
173 + }
174 + & > .main {
175 + background-color: white;
176 + overflow: initial;
177 + height: initial;
178 +
179 + header.toolbar {
180 + display: none;
181 + }
182 + footer.footer {
183 + display: none;
184 + }
185 +
186 + & > .n-scrollbar {
187 + overflow: initial;
188 + height: initial;
189 +
190 + & > .n-scrollbar-container {
191 + overflow: initial;
192 + height: initial;
193 + }
194 + }
195 + }
196 + }
197 + }
198 + }
199 + }
200 +}
frontend/src/components/AuthForm/SignUp.vue
+1
@@ -69,6 +69,7 @@
69 <ImageCropper
70 v-slot="{ openCropper }"
71 @crop="setCroppedImage"
72 + shape="circle"
73 :placeholder="'Select your profile picture'"
74 >
75 <n-button type="primary" @click="openCropper()" size="small">
frontend/src/components/agents/AgentCard.vue
+3 -3
@@ -70,9 +70,9 @@
70
71 <script setup lang="ts">
72 import { computed, ref, toRefs } from "vue"
73 -import { type Agent } from "@/types/agents.d"
73 +import { AgentStatus, type Agent } from "@/types/agents.d"
74 import dayjs from "@/utils/dayjs"
75 -import { handleDeleteAgent, isAgentOnline, toggleAgentCritical } from "./utils"
75 +import { handleDeleteAgent, toggleAgentCritical } from "./utils"
76 import { NTooltip, NButton, NSpin, NCard, useMessage, useDialog } from "naive-ui"
77 import Icon from "@/components/common/Icon.vue"
78 import { useSettingsStore } from "@/stores/settings"
@@ -97,7 +97,7 @@ const loading = ref(false)
97 const message = useMessage()
98 const dialog = useDialog()
99 const isOnline = computed(() => {
100 - return isAgentOnline(agent.value.wazuh_last_seen)
100 + return agent.value.wazuh_agent_status === AgentStatus.Active
101 })
102 const formatLastSeen = computed(() => {
103 const lastSeenDate = dayjs(agent.value.wazuh_last_seen)
frontend/src/components/agents/utils.ts
+3 -2
@@ -5,9 +5,10 @@ import type { MessageApiInjection } from "naive-ui/es/message/src/MessageProvide
5 import type { DialogApiInjection } from "naive-ui/es/dialog/src/DialogProvider"
6 import { h } from "vue"
7
8 -export function isAgentOnline(last_seen: string) {
9 - const lastSeenDate = dayjs(last_seen)
8 +export function isAgentOnline(lastSeen: string) {
9 + const lastSeenDate = dayjs(lastSeen)
10 if (!lastSeenDate.isValid()) return false
11 + if (!dayjs().isAfter(lastSeenDate)) return false
12
13 return lastSeenDate.isAfter(dayjs().subtract(1, "h"))
14 }
frontend/src/components/common/ImageCropper.vue
+8 -6
@@ -27,7 +27,7 @@
27 :stencil-props="stencilProps"
28 :resize-image="resizeImage"
29 image-restriction="stencil"
30 - :stencil-component="CircleStencil"
30 + :stencil-component="stencil"
31 ></Cropper>
32 </div>
33 </div>
@@ -42,9 +42,9 @@
42
43 <script lang="ts" setup>
44 import { NButton, NCard, NUpload, NUploadDragger, NModal } from "naive-ui"
45 -import { type FileInfo } from "naive-ui/lib/upload/src/interface"
46 -import { ref, toRefs } from "vue"
47 -import { Cropper, CircleStencil, type CropperResult } from "vue-advanced-cropper"
45 +import { type FileInfo } from "naive-ui/es/upload/src/interface"
46 +import { computed, ref, toRefs } from "vue"
47 +import { Cropper, CircleStencil, RectangleStencil, type CropperResult } from "vue-advanced-cropper"
48 import "vue-advanced-cropper/dist/style.css"
49
50 export type ImageCropperResult = CropperResult
@@ -56,11 +56,13 @@ const emit = defineEmits<{
56 const props = withDefaults(
57 defineProps<{
58 placeholder?: string
59 + shape?: "square" | "circle"
60 }>(),
60 - { placeholder: "Select a picture" }
61 + { placeholder: "Select an image", shape: "square" }
62 )
62 -const { placeholder } = toRefs(props)
63 +const { placeholder, shape } = toRefs(props)
64
65 +const stencil = computed(() => (shape.value === "circle" ? CircleStencil : RectangleStencil))
66 const img = ref("")
67 const showCropper = ref(false)
68 const cropper = ref<typeof Cropper | null>(null)
frontend/src/components/common/LayoutSettings.vue
+4 -4
@@ -47,8 +47,8 @@
47 :type="theme === ThemeEnum.Light ? 'primary' : 'default'"
48 >
49 <template #icon>
50 - <Icon :name="LigthIcon" v-if="theme === ThemeEnum.Light"></Icon>
51 - <Icon :name="LigthOutlineIcon" v-else></Icon>
50 + <Icon :name="LightIcon" v-if="theme === ThemeEnum.Light"></Icon>
51 + <Icon :name="LightOutlineIcon" v-else></Icon>
52 </template>
53 Light
54 </n-button>
@@ -160,9 +160,9 @@ import { useWindowSize } from "@vueuse/core"
160
161 const SettingsIcon = "carbon:settings-adjust"
162 const CloseIcon = "carbon:close"
163 -const LigthIcon = "ion:sunny"
163 +const LightIcon = "ion:sunny"
164 const DarkIcon = "ion:moon"
165 -const LigthOutlineIcon = "ion:sunny-outline"
165 +const LightOutlineIcon = "ion:sunny-outline"
166 const DarkOutlineIcon = "ion:moon-outline"
167 const ColorIcon = "carbon:circle-solid"
168
frontend/src/components/connectors/ConfigForm/FormTypes/CredentialsType.vue
+7 -1
@@ -7,7 +7,13 @@
7 <n-input v-model:value="form.connector_username" required type="text" />
8 </n-form-item>
9 <n-form-item label="Password" path="connector_password">
10 - <n-input v-model:value="form.connector_password" required type="password" autocomplete="off" />
10 + <n-input
11 + v-model:value="form.connector_password"
12 + required
13 + type="password"
14 + show-password-on="click"
15 + autocomplete="off"
16 + />
17 </n-form-item>
18 </n-form>
19 </template>
frontend/src/components/customers/CustomerAgents.vue
+1 -5
@@ -24,7 +24,6 @@ import { useMessage, NSpin, NEmpty } from "naive-ui"
24 import type { Customer } from "@/types/customers.d"
25 import { useRouter } from "vue-router"
26 import type { Agent } from "@/types/agents.d"
27 -import { isAgentOnline } from "@/components/agents/utils"
27
28 const props = defineProps<{
29 customer: Customer
@@ -47,10 +46,7 @@ function getAgents() {
46 .getCustomerAgents(customer.value.customer_code)
47 .then(res => {
48 if (res.data.success) {
50 - list.value = (res.data.agents || []).map(o => {
51 - o.online = isAgentOnline(o.wazuh_last_seen)
52 - return o
53 - })
49 + list.value = res.data.agents || []
50 } else {
51 message.error(res.data?.message || "An error occurred. Please try again later.")
52 }
frontend/src/components/customers/provision/CustomerDefaultSettingsForm.vue
+12 -2
@@ -89,6 +89,10 @@ const rules: FormRules = {
89 grafana_url: {
90 validator: validateUrl,
91 trigger: ["blur"]
92 + },
93 + wazuh_worker_hostname: {
94 + message: "Please input the Wazuh Worker Hostname",
95 + trigger: ["input", "blur"]
96 }
97 }
98
@@ -108,6 +112,10 @@ const fieldsMeta = {
112 grafana_url: {
113 label: "Grafana URL",
114 placeholder: "Insert the Grafana URL"
115 + },
116 + wazuh_worker_hostname: {
117 + label: "Wazuh Worker Hostname",
118 + placeholder: "Insert the Wazuh Worker Hostname"
119 }
120 }
121
@@ -143,7 +151,8 @@ function getClearForm(settings?: Omit<CustomerProvisioningDefaultSettings, "id">
151 cluster_name: settings?.cluster_name || "",
152 cluster_key: settings?.cluster_key || "",
153 master_ip: settings?.master_ip || "",
146 - grafana_url: settings?.grafana_url || ""
154 + grafana_url: settings?.grafana_url || "",
155 + wazuh_worker_hostname: settings?.wazuh_worker_hostname || ""
156 }
157 }
158
@@ -167,7 +176,8 @@ function submit() {
176 clusterName: form.value.cluster_name,
177 clusterKey: form.value.cluster_key,
178 masterIp: form.value.master_ip,
170 - grafanaUrl: form.value.grafana_url
179 + grafanaUrl: form.value.grafana_url,
180 + wazuhWorkerHostname: form.value.wazuh_worker_hostname
181 }
182
183 Api.customers[method](payload)
frontend/src/components/overview/AgentsCard.vue
+3 -7
@@ -20,8 +20,7 @@
20
21 <script setup lang="ts">
22 import { computed, onBeforeMount, ref } from "vue"
23 -import { type Agent } from "@/types/agents.d"
24 -import { isAgentOnline } from "@/components/agents/utils"
23 +import { AgentStatus, type Agent } from "@/types/agents.d"
24 import CardStatsDouble from "@/components/common/CardStatsDouble.vue"
25 import CardStatsIcon from "@/components/common/CardStatsIcon.vue"
26 import Api from "@/api"
@@ -39,7 +38,7 @@ const total = computed<number>(() => {
38 })
39
40 const onlineTotal = computed(() => {
42 - return agents.value.filter(({ online }) => online).length || 0
41 + return agents.value.filter(({ wazuh_agent_status }) => wazuh_agent_status === AgentStatus.Active).length || 0
42 })
43
44 function getData() {
@@ -49,10 +48,7 @@ function getData() {
48 .getAgents()
49 .then(res => {
50 if (res.data.success) {
52 - agents.value = (res.data.agents || []).map(o => {
53 - o.online = isAgentOnline(o.wazuh_last_seen)
54 - return o
55 - })
51 + agents.value = res.data.agents || []
52 } else {
53 message.error(res.data?.message || "An error occurred. Please try again later.")
54 }
frontend/src/components/reportCreation/Panels.vue new
+613
@@ -0,0 +1,613 @@
1 +<template>
2 + <n-spin v-model:show="loading" class="overflow-hidden h-full w-full" content-class="overflow-hidden h-full w-full">
3 + <div class="report-panels h-full w-full flex gap-2" v-if="org">
4 + <div class="panels-container grow h-full flex flex-col gap-4">
5 + <div class="rows-container">
6 + <n-scrollbar style="max-height: 100%" trigger="none">
7 + <div class="drag-wrapper">
8 + <draggable
9 + v-model="rows"
10 + item-key="id"
11 + :animation="200"
12 + ghost-class="ghost-row"
13 + handle=".pan-area"
14 + :group="{ name: 'rows', pull: false, put: false }"
15 + class="flex flex-col gap-2"
16 + >
17 + <template #item="{ element: row }">
18 + <div class="row p-3" :class="{ 'height-large': row.height === 2 }">
19 + <div class="empty-message" v-if="!row.panels.length">Drop panels here</div>
20 + <draggable
21 + v-model="row.panels"
22 + item-key="id"
23 + :animation="200"
24 + ghost-class="ghost-panel"
25 + :group="{
26 + name: 'panels',
27 + put(to: any) {
28 + return to.el.children.length < 4
29 + },
30 + pull: ['panels']
31 + }"
32 + class="drop-panels-area flex gap-3 w-full h-full"
33 + >
34 + <template #header>
35 + <div class="left-box flex justify-end">
36 + <div class="pan-area">
37 + <Icon :name="PanIcon" :size="20"></Icon>
38 + </div>
39 + <div class="delete-box">
40 + <n-tooltip trigger="hover">
41 + <template #trigger>
42 + <n-button text @click="removeRow(row)" type="error">
43 + <template #icon>
44 + <Icon :name="CloseIcon"></Icon>
45 + </template>
46 + </n-button>
47 + </template>
48 + Remove Row
49 + </n-tooltip>
50 + </div>
51 + <div class="settings-box">
52 + <n-popover trigger="click">
53 + <template #trigger>
54 + <n-button text>
55 + <template #icon>
56 + <Icon :name="RowSettingsIcon" :size="13"></Icon>
57 + </template>
58 + </n-button>
59 + </template>
60 +
61 + <div class="py-1 flex gap-3 items-center">
62 + <div class="text-secondary-color text-sm">
63 + Row height:
64 + </div>
65 + <n-switch
66 + size="small"
67 + v-model:value="row.height"
68 + :unchecked-value="1"
69 + :checked-value="2"
70 + >
71 + <template #checked>
72 + <small>Large</small>
73 + </template>
74 + <template #unchecked>
75 + <small>Regular</small>
76 + </template>
77 + </n-switch>
78 + </div>
79 + </n-popover>
80 + </div>
81 + </div>
82 + </template>
83 + <template #item="{ element: panel }">
84 + <div class="panel">
85 + <div class="delete-box">
86 + <n-tooltip trigger="hover">
87 + <template #trigger>
88 + <n-button
89 + text
90 + @click="removePanel(row, panel)"
91 + type="error"
92 + >
93 + <template #icon>
94 + <Icon :name="CloseIcon"></Icon>
95 + </template>
96 + </n-button>
97 + </template>
98 + Remove Panel
99 + </n-tooltip>
100 + </div>
101 + <div class="dashboard-title">
102 + {{ panel.dashboardTitle }}
103 + </div>
104 + <div class="content">
105 + {{ panel.panelTitle }}
106 + </div>
107 + </div>
108 + </template>
109 + </draggable>
110 + </div>
111 + </template>
112 + </draggable>
113 + </div>
114 + </n-scrollbar>
115 + </div>
116 + <div class="toolbar pr-4 flex items-center justify-between gap-2">
117 + <n-button
118 + class="add-task-btn flex items-center justify-center !mt-0"
119 + @click="addRow()"
120 + v-if="dashboard || panelsReady"
121 + >
122 + <template #icon>
123 + <Icon :name="AddIcon"></Icon>
124 + </template>
125 + <span>Add row</span>
126 + </n-button>
127 +
128 + <div class="flex items-center gap-2" v-if="panelsReady">
129 + <n-button type="success" @click="print()" :loading="loading">
130 + <template #icon>
131 + <Icon :name="PrintIcon"></Icon>
132 + </template>
133 + Print Report
134 + </n-button>
135 + <n-button type="success" @click="openSettings()" :loading="loading">
136 + <template #icon>
137 + <Icon :name="SettingsIcon"></Icon>
138 + </template>
139 + </n-button>
140 + </div>
141 + </div>
142 + </div>
143 +
144 + <div class="panels-sidebar h-full">
145 + <n-scrollbar style="max-height: 100%" trigger="none" v-if="dashboard && panelsList.length">
146 + <div class="p-3">
147 + <draggable
148 + class="flex flex-col gap-3"
149 + :list="panelsList"
150 + :group="{ name: 'panels', pull: 'clone', put: false }"
151 + :sort="false"
152 + item-key="id"
153 + >
154 + <template #item="{ element: panel }">
155 + <div class="panel">
156 + <div class="content">
157 + {{ panel.panelTitle }}
158 + </div>
159 + </div>
160 + </template>
161 + </draggable>
162 + </div>
163 + </n-scrollbar>
164 + <div v-else class="p-3 h-full">
165 + <div class="empty-message p-3">
166 + <div v-if="!dashboard">
167 + Select a
168 + <code>Dashboard</code>
169 + to get the panels
170 + </div>
171 + <div v-else>No Panels found</div>
172 + </div>
173 + </div>
174 + </div>
175 + </div>
176 +
177 + <div v-else class="empty-message">
178 + Select an
179 + <code>Organization</code>
180 + /
181 + <code>Dashboard</code>
182 + to create the Report
183 + </div>
184 +
185 + <n-drawer
186 + v-model:show="settingDrawerOpen"
187 + :width="500"
188 + style="max-width: 90vw"
189 + :trap-focus="false"
190 + :class="{ 'opacity-0': preloadingPrintSettings }"
191 + display-directive="show"
192 + >
193 + <n-drawer-content title="Report Settings" closable :native-scrollbar="false">
194 + <PrintSettings @update="printSettings = $event" />
195 + </n-drawer-content>
196 + </n-drawer>
197 + </n-spin>
198 +</template>
199 +
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"
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"
209 +import { saveAs } from "file-saver"
210 +import { useStorage } from "@vueuse/core"
211 +import _kebabCase from "lodash/kebabCase"
212 +import * as defaultSettings from "./defaultSettings"
213 +
214 +const ROW_GAP = 20
215 +const ROW_WIDTH = 800
216 +const ROW_HEIGHT = 320
217 +
218 +interface OrgData {
219 + id: number
220 + rows: Row[]
221 +}
222 +
223 +interface Row {
224 + id: number
225 + height: 1 | 2
226 + panels: PanelData[]
227 +}
228 +
229 +interface PanelData {
230 + panelId: number
231 + panelTitle: string
232 + orgId: number
233 + orgName: string
234 + dashboardUID: string
235 + dashboardTitle: string
236 +}
237 +
238 +const props = defineProps<{
239 + timerange: ReportTimeRange | null
240 + org: Org | null
241 + dashboard: Dashboard | null
242 + panels: Panel[]
243 +}>()
244 +const { timerange, org, dashboard, panels } = toRefs(props)
245 +
246 +const PanIcon = "carbon:draggable"
247 +const CloseIcon = "carbon:close"
248 +const SettingsIcon = "carbon:settings"
249 +const AddIcon = "carbon:add-alt"
250 +const PrintIcon = "carbon:printer"
251 +const RowSettingsIcon = "carbon:fit-to-height"
252 +const message = useMessage()
253 +const loadingPrint = ref(false)
254 +const loading = computed(() => loadingPrint.value)
255 +
256 +const settingDrawerOpen = ref(false)
257 +const printSettings = ref<Partial<PrintSettingsData>>({})
258 +const preloadingPrintSettings = ref(true)
259 +
260 +const orgs = useStorage<OrgData[]>("report-panel-orgs-data", [], localStorage)
261 +const rows = computed<Row[]>({
262 + get() {
263 + return orgs.value.find(o => o.id === org.value?.id)?.rows || []
264 + },
265 + set(val: Row[]) {
266 + const orgData = orgs.value.find(o => o.id === org.value?.id)
267 + if (orgData?.rows) {
268 + orgData.rows = val
269 + }
270 + }
271 +})
272 +const panelsList = ref<PanelData[]>([])
273 +
274 +const panelsReady = computed<number>(() => {
275 + return rows.value.reduce((acc, row) => {
276 + return acc + row.panels.length
277 + }, 0)
278 +})
279 +
280 +watch(org, val => {
281 + if (val) {
282 + setOrg(val)
283 + }
284 + panelsList.value = []
285 +})
286 +
287 +watch(panels, val => {
288 + if (val?.length) {
289 + setPanelsList(val)
290 +
291 + if (!rows.value.length) {
292 + addRow()
293 + }
294 + }
295 +})
296 +
297 +function openSettings() {
298 + settingDrawerOpen.value = true
299 +}
300 +
301 +function addRow() {
302 + rows.value.push({
303 + id: new Date().getTime(),
304 + height: 1,
305 + panels: []
306 + })
307 +}
308 +
309 +function removeRow(row: Row) {
310 + rows.value.splice(
311 + rows.value.findIndex(o => o.id === row.id),
312 + 1
313 + )
314 +}
315 +function removePanel(row: Row, panel: PanelData) {
316 + row.panels.splice(
317 + row.panels.findIndex(o => o.panelId === panel.panelId),
318 + 1
319 + )
320 +}
321 +
322 +function setPanelsList(panels: Panel[]) {
323 + if (org.value && dashboard.value && panels.length) {
324 + panelsList.value = []
325 +
326 + for (const panel of panels) {
327 + panelsList.value.push({
328 + panelId: panel.id,
329 + panelTitle: panel.title,
330 + orgId: org.value.id,
331 + orgName: org.value.name,
332 + dashboardUID: dashboard.value.uid,
333 + dashboardTitle: dashboard.value.title
334 + })
335 + }
336 + }
337 +}
338 +
339 +function setOrg(org: Org) {
340 + const exist = orgs.value.find(o => o.id === org.id)
341 + if (!exist) {
342 + orgs.value.push({
343 + id: org.id,
344 + rows: []
345 + })
346 + }
347 +}
348 +
349 +function print() {
350 + if (!timerange.value) {
351 + return
352 + }
353 +
354 + loadingPrint.value = true
355 +
356 + const timeValue = parseInt(timerange.value.match(/\d+/)?.[0] || "1")
357 + const timeUnit = (timerange.value.match(/[a-z]/i)?.[0] || "h").toLocaleLowerCase()
358 + const timerangeText = `Last ${timeValue} ${timeUnit === "d" ? "Day" : timeUnit === "h" ? "Hour" : "minute"}${
359 + timeValue > 1 ? "s" : ""
360 + }`
361 +
362 + const payload: GenerateReportPayload = {
363 + timerange: timerange.value,
364 + timerange_text: timerangeText,
365 + logo_base64: printSettings.value.logo || defaultSettings.logo,
366 + company_name: printSettings.value.company || defaultSettings.company,
367 + rows: []
368 + }
369 +
370 + const density = printSettings.value.retina ? 2 : 1
371 +
372 + for (const row of rows.value) {
373 + if (row.panels.length) {
374 + const panel_width = ((ROW_WIDTH - ROW_GAP * (row.panels.length - 1)) / row.panels.length) * density
375 + const panel_height = ROW_HEIGHT * density * (row.height || 1)
376 +
377 + payload.rows.push({
378 + id: row.id,
379 + panels: row.panels.map(o => ({
380 + org_id: o.orgId,
381 + dashboard_title: o.dashboardTitle,
382 + dashboard_uid: o.dashboardUID,
383 + panel_id: o.panelId,
384 + panel_width,
385 + panel_height,
386 + theme: printSettings.value.theme || defaultSettings.theme
387 + }))
388 + })
389 + }
390 + }
391 +
392 + const reportFileName = `report${org.value?.name ? "-" + _kebabCase(org.value.name) : ""}.pdf`
393 +
394 + Api.reporting
395 + .generateReport(payload)
396 + .then(res => {
397 + if (res.data.success) {
398 + const dataUri = "data:application/pdf;base64," + res.data.base64_result
399 + saveAs(dataUri, reportFileName)
400 + } else {
401 + message.warning(res.data?.message || "An error occurred. Please try again later.")
402 + }
403 + })
404 + .catch(err => {
405 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
406 + })
407 + .finally(() => {
408 + loadingPrint.value = false
409 + })
410 +}
411 +
412 +onMounted(() => {
413 + // need to preload print settings from drawer
414 + settingDrawerOpen.value = true
415 + setTimeout(() => {
416 + settingDrawerOpen.value = false
417 + setTimeout(() => {
418 + preloadingPrintSettings.value = false
419 + }, 300)
420 + }, 100)
421 +})
422 +</script>
423 +
424 +<style lang="scss" scoped>
425 +.empty-message {
426 + border: 2px dashed var(--border-color) !important;
427 + border-radius: var(--border-radius);
428 + height: 100%;
429 + width: 100%;
430 + display: flex;
431 + align-items: center;
432 + justify-content: center;
433 + text-align: center;
434 +}
435 +.report-panels {
436 + overflow: hidden;
437 +
438 + .panel {
439 + aspect-ratio: 1.72;
440 + cursor: move;
441 +
442 + .content {
443 + border-radius: var(--border-radius);
444 + background-color: var(--bg-color);
445 + border: var(--border-small-050);
446 + overflow: hidden;
447 + display: flex;
448 + align-items: center;
449 + justify-content: center;
450 + font-size: 12px;
451 + font-weight: bold;
452 + padding: 16px;
453 + text-align: center;
454 + transition: border-color 0.2s;
455 + width: 100%;
456 + height: 100%;
457 + }
458 +
459 + &:hover {
460 + .content {
461 + border-color: var(--primary-040-color);
462 + }
463 + }
464 +
465 + &.ghost-panel {
466 + .content {
467 + border: 2px dashed var(--primary-040-color) !important;
468 + }
469 + }
470 + }
471 +
472 + .panels-container {
473 + overflow: hidden;
474 +
475 + .rows-container {
476 + overflow: hidden;
477 + border-radius: var(--border-radius);
478 +
479 + .drag-wrapper {
480 + padding-right: 16px;
481 + padding-left: 20px;
482 + }
483 +
484 + .row {
485 + border-radius: var(--border-radius);
486 + background-color: var(--bg-secondary-color);
487 + border: var(--border-small-050);
488 + height: 155px;
489 + position: relative;
490 + transition: border-color 0.2s;
491 +
492 + &.height-large {
493 + height: 300px;
494 + }
495 + .empty-message {
496 + position: absolute;
497 + z-index: 0;
498 + width: unset;
499 + height: unset;
500 + @apply top-3 bottom-3 left-3 right-3;
501 + }
502 +
503 + .drop-panels-area {
504 + position: absolute;
505 + width: unset;
506 + height: unset;
507 + @apply top-3 bottom-3 left-3 right-3;
508 + }
509 +
510 + .left-box {
511 + position: absolute;
512 + top: 0;
513 + left: -32px;
514 + bottom: 0;
515 + width: 25px;
516 + border: var(--border-small-050);
517 + background-color: var(--bg-secondary-color);
518 + border-radius: var(--border-radius);
519 +
520 + .pan-area {
521 + position: absolute;
522 + top: 50%;
523 + left: 2px;
524 + transform: translateY(-50%);
525 + cursor: move;
526 + }
527 +
528 + .delete-box {
529 + position: absolute;
530 + top: 0px;
531 + left: 0px;
532 + right: 0;
533 + background-color: rgba(var(--secondary4-color-rgb), 0.1);
534 + border-top-left-radius: var(--border-radius);
535 + border-top-right-radius: var(--border-radius);
536 + text-align: center;
537 + padding-top: 4px;
538 + }
539 +
540 + .settings-box {
541 + position: absolute;
542 + bottom: 0px;
543 + left: 0px;
544 + right: 0;
545 + background-color: var(--hover-005-color);
546 + border-bottom-left-radius: var(--border-radius);
547 + border-bottom-right-radius: var(--border-radius);
548 + text-align: center;
549 + padding-top: 4px;
550 + }
551 +
552 + &:hover {
553 + border-color: var(--primary-020-color);
554 + }
555 + }
556 +
557 + .panel {
558 + height: 100%;
559 + aspect-ratio: unset;
560 + flex: 1 1 0px;
561 + position: relative;
562 +
563 + .delete-box {
564 + position: absolute;
565 + top: 4px;
566 + right: 4px;
567 + }
568 +
569 + .dashboard-title {
570 + font-size: 10px;
571 + position: absolute;
572 + top: 0px;
573 + left: 0px;
574 + right: 28px;
575 + padding: 5px 10px;
576 + color: var(--fg-secondary-color);
577 + white-space: nowrap;
578 + overflow: hidden;
579 + text-overflow: ellipsis;
580 + }
581 + }
582 +
583 + &:hover {
584 + border-color: var(--primary-020-color);
585 + }
586 +
587 + &.ghost-row {
588 + border: 2px dashed var(--primary-040-color) !important;
589 + }
590 + }
591 + }
592 + }
593 +
594 + .panels-sidebar {
595 + border-radius: var(--border-radius);
596 + background-color: var(--bg-secondary-color);
597 + border: var(--border-small-050);
598 + width: 200px;
599 +
600 + :deep() {
601 + .n-scrollbar {
602 + .n-scrollbar-rail {
603 + right: 3px;
604 + }
605 + }
606 + }
607 +
608 + .panel {
609 + width: 100%;
610 + }
611 + }
612 +}
613 +</style>
frontend/src/components/reportCreation/PrintSettings.vue new
+86
@@ -0,0 +1,86 @@
1 +<template>
2 + <div class="flex flex-col gap-8">
3 + <div class="flex gap-6 mb-2">
4 + <n-avatar :size="110" :src="logo" />
5 +
6 + <div class="flex flex-col justify-between grow">
7 + <ImageCropper v-slot="{ openCropper }" @crop="setCroppedImage" :placeholder="'Select a Logo'">
8 + <n-button @click="openCropper()">
9 + <template #icon>
10 + <Icon :name="EditIcon"></Icon>
11 + </template>
12 + Edit Logo Image
13 + </n-button>
14 + </ImageCropper>
15 +
16 + <n-form-item label="Company" :show-feedback="false">
17 + <n-input v-model:value="company" placeholder="Insert you company name" />
18 + </n-form-item>
19 + </div>
20 + </div>
21 +
22 + <div class="theme">
23 + <n-form-item label="Theme" feedback="⊙ choose panels palette">
24 + <n-radio-group v-model:value="theme">
25 + <n-radio-button value="light">Light</n-radio-button>
26 + <n-radio-button value="dark">Dark</n-radio-button>
27 + </n-radio-group>
28 + </n-form-item>
29 + </div>
30 +
31 + <div class="retina">
32 + <n-form-item label="Retina" feedback="⊙ improve panels resolution; it will increase the report size">
33 + <n-switch v-model:value="retina" />
34 + </n-form-item>
35 + </div>
36 + </div>
37 +</template>
38 +
39 +<script setup lang="ts">
40 +import { watch, onMounted } from "vue"
41 +import { NAvatar, NButton, NInput, NFormItem, NRadioGroup, NRadioButton, NSwitch } from "naive-ui"
42 +import ImageCropper, { type ImageCropperResult } from "@/components/common/ImageCropper.vue"
43 +import Icon from "@/components/common/Icon.vue"
44 +import { useStorage } from "@vueuse/core"
45 +import * as defaultSettings from "./defaultSettings"
46 +
47 +export interface PrintSettingsData {
48 + logo: string
49 + company: string
50 + theme: "light" | "dark"
51 + retina: boolean
52 +}
53 +
54 +const emit = defineEmits<{
55 + (e: "update", value: PrintSettingsData): void
56 +}>()
57 +
58 +const EditIcon = "uil:image-edit"
59 +
60 +const logo = useStorage<string>("report-settings-logo", defaultSettings.logo, localStorage)
61 +const company = useStorage<string>("report-settings-company", defaultSettings.company, localStorage)
62 +const theme = useStorage<"light" | "dark">("report-settings-theme", defaultSettings.theme, localStorage)
63 +const retina = useStorage<boolean>("report-settings-retina", defaultSettings.retina, localStorage)
64 +
65 +watch([logo, company, theme, retina], () => {
66 + emitData()
67 +})
68 +
69 +function setCroppedImage(result: ImageCropperResult) {
70 + const canvas = result.canvas as HTMLCanvasElement
71 + logo.value = canvas.toDataURL()
72 +}
73 +
74 +function emitData() {
75 + emit("update", {
76 + logo: logo.value,
77 + company: company.value,
78 + theme: theme.value,
79 + retina: retina.value
80 + })
81 +}
82 +
83 +onMounted(() => {
84 + emitData()
85 +})
86 +</script>
frontend/src/components/reportCreation/Wizard.vue new
+231
@@ -0,0 +1,231 @@
1 +<template>
2 + <div class="report-wizard">
3 + <n-spin v-model:show="loading">
4 + <n-form :label-width="80" class="flex flex-col gap-5" :show-feedback="false">
5 + <div class="flex gap-5 flex-col sm:flex-row">
6 + <n-form-item label="Time Range" class="sm:max-w-56">
7 + <n-input-group>
8 + <n-select
9 + v-model:value="timeUnit"
10 + :options="timeUnitOptions"
11 + placeholder="Time unit"
12 + class="!min-w-28 basis-1"
13 + />
14 + <n-input-number v-model:value="timeValue" :min="1" placeholder="Time" class="grow" />
15 + </n-input-group>
16 + </n-form-item>
17 +
18 + <n-form-item label="Organization" class="flex flex-grow">
19 + <n-select
20 + v-model:value="selectedOrgId"
21 + :options="orgsOptions"
22 + :loading="loadingOrgs"
23 + clearable
24 + />
25 + </n-form-item>
26 +
27 + <n-form-item label="Dashboard" v-if="canSelectDashboard" class="flex flex-grow">
28 + <n-select
29 + v-model:value="selectedDashboardUID"
30 + :options="dashboardsOptions"
31 + :loading="loadingDashboards"
32 + clearable
33 + />
34 + </n-form-item>
35 + </div>
36 + <div class="flex" v-if="!hidePanelsSelect">
37 + <n-form-item label="Panels" v-if="canSelectPanels" class="grow">
38 + <n-select
39 + v-model:value="selectedPanelsIds"
40 + :options="panelsOptions"
41 + :loading="loadingPanels"
42 + multiple
43 + clearable
44 + />
45 + </n-form-item>
46 + </div>
47 + </n-form>
48 + </n-spin>
49 + </div>
50 +</template>
51 +
52 +<script setup lang="ts">
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"
58 +import { useStorage } from "@vueuse/core"
59 +
60 +const emit = defineEmits<{
61 + (e: "selected", value: Panel[]): void
62 + (e: "panels", value: Panel[]): void
63 + (e: "dashboard", value: Dashboard | null): void
64 + (e: "organization", value: Org | null): void
65 + (e: "timerange", value: ReportTimeRange): void
66 +}>()
67 +
68 +const props = defineProps<{
69 + hidePanelsSelect?: boolean
70 +}>()
71 +const { hidePanelsSelect } = toRefs(props)
72 +
73 +const message = useMessage()
74 +const orgsList = ref<Org[]>([])
75 +const dashboardsList = ref<Dashboard[]>([])
76 +const panelsList = ref<Panel[]>([])
77 +
78 +const loadingOrgs = ref(false)
79 +const loadingDashboards = ref(false)
80 +const loadingPanels = ref(false)
81 +
82 +const selectedOrgId = ref<number | string | null>(null)
83 +const selectedOrg = computed(() =>
84 + selectedOrgId.value ? orgsList.value.find(o => o.id === selectedOrgId.value) || null : null
85 +)
86 +const selectedDashboardUID = ref<string | null>(null)
87 +const selectedDashboard = computed(() =>
88 + selectedDashboardUID.value ? dashboardsList.value.find(o => o.uid === selectedDashboardUID.value) || null : null
89 +)
90 +const selectedPanelsIds = ref<number[]>([])
91 +const selectedPanels = computed(() =>
92 + selectedPanelsIds.value ? panelsList.value.filter(o => selectedPanelsIds.value.includes(o.id)) : []
93 +)
94 +const timeUnit = useStorage<RowPanelTimeUnit>("report-wizard-time-unit", "h", localStorage)
95 +const timeValue = useStorage<number>("report-wizard-time-value", 1, localStorage)
96 +
97 +const orgsOptions = computed(() => orgsList.value.map(o => ({ value: o.id, label: o.name })))
98 +const dashboardsOptions = computed(() => dashboardsList.value.map(o => ({ value: o.uid, label: o.title })))
99 +const panelsOptions = computed(() => panelsList.value.map(o => ({ value: o.id, label: o.title })))
100 +const timeUnitOptions: { label: string; value: RowPanelTimeUnit }[] = [
101 + { label: "Minutes", value: "m" },
102 + { label: "Hours", value: "h" },
103 + { label: "Days", value: "d" }
104 +]
105 +
106 +const canSelectDashboard = computed(() => !!selectedOrgId.value)
107 +const canSelectPanels = computed(() => canSelectDashboard.value && !!selectedDashboardUID.value)
108 +
109 +const loading = computed(() => loadingOrgs.value || loadingDashboards.value || loadingPanels.value)
110 +
111 +watch(
112 + selectedOrgId,
113 + val => {
114 + dashboardsList.value = []
115 + panelsList.value = []
116 + selectedDashboardUID.value = null
117 + selectedPanelsIds.value = []
118 +
119 + emit("organization", selectedOrg.value)
120 +
121 + if (val) {
122 + getDashboards()
123 + }
124 + },
125 + { immediate: true }
126 +)
127 +
128 +watch(
129 + selectedDashboardUID,
130 + val => {
131 + panelsList.value = []
132 + selectedPanelsIds.value = []
133 +
134 + emit("dashboard", selectedDashboard.value)
135 +
136 + if (val) {
137 + getPanels()
138 + }
139 + },
140 + { immediate: true }
141 +)
142 +
143 +watch(
144 + panelsList,
145 + val => {
146 + emit("panels", val)
147 + },
148 + { immediate: true }
149 +)
150 +
151 +watch(selectedPanelsIds, () => {
152 + emit("selected", selectedPanels.value)
153 +})
154 +
155 +watch(
156 + [timeValue, timeUnit],
157 + ([value, unit]) => {
158 + console.log("timerange")
159 + emit("timerange", `${value}${unit}`)
160 + },
161 + { immediate: true }
162 +)
163 +
164 +function getOrgs() {
165 + loadingOrgs.value = true
166 +
167 + Api.reporting
168 + .getOrgs()
169 + .then(res => {
170 + if (res.data.success) {
171 + orgsList.value = res.data?.orgs || []
172 + } else {
173 + message.warning(res.data?.message || "An error occurred. Please try again later.")
174 + }
175 + })
176 + .catch(err => {
177 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
178 + })
179 + .finally(() => {
180 + loadingOrgs.value = false
181 + })
182 +}
183 +
184 +function getDashboards() {
185 + if (selectedOrgId.value) {
186 + loadingDashboards.value = true
187 +
188 + Api.reporting
189 + .getDashboards(selectedOrgId.value.toString())
190 + .then(res => {
191 + if (res.data.success) {
192 + dashboardsList.value = res.data?.dashboards || []
193 + } else {
194 + message.warning(res.data?.message || "An error occurred. Please try again later.")
195 + }
196 + })
197 + .catch(err => {
198 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
199 + })
200 + .finally(() => {
201 + loadingDashboards.value = false
202 + })
203 + }
204 +}
205 +
206 +function getPanels() {
207 + if (selectedDashboardUID.value) {
208 + loadingPanels.value = true
209 +
210 + Api.reporting
211 + .getPanels(selectedDashboardUID.value.toString())
212 + .then(res => {
213 + if (res.data.success) {
214 + panelsList.value = res.data?.panels || []
215 + } else {
216 + message.warning(res.data?.message || "An error occurred. Please try again later.")
217 + }
218 + })
219 + .catch(err => {
220 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
221 + })
222 + .finally(() => {
223 + loadingPanels.value = false
224 + })
225 + }
226 +}
227 +
228 +onBeforeMount(() => {
229 + getOrgs()
230 +})
231 +</script>
frontend/src/components/reportCreation/defaultSettings.ts new
+6
@@ -0,0 +1,6 @@
1 +export const logo =
2 + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAHxCAMAAAB0yRoBAAACylBMVEUAAAD8z1z7uEv8hi78ijH7zlz7qEL8vEz90Fv8iDD8hy38hi38hi79z1v8hS/7hy/8hy78hi77pkL70Vz8hi77pUL7ijL8vEz8z1z8q0X7hS//pUD7mzz8zlv8mzv7izL7mzz7z1v7mzz8hS3/o0D7mzz7mzz7mzz8qkP8hi78ukv/hi37vEz7mzz7pUH7hzD/nDv8pkH8vEz7mzz7q0T8vEz/hS7/nj/7mzz7q0T8hi/8rET/p0L7q0T7mzz7mzz9vEz8q0T80Fv8mzz6kzf/sUD7q0T7q0P7mzz6mzz7q0T8mzz7q0T8rET7vEz6mzv9vEz8q0P60Fv7q0T7mzv8hi78qkP7mzz8vEz9q0T8uUv9rEX60Fr6mzz5oD3/zlv4nD36mTz8hi78iDD8q0P7mzz7q0T7mzz7mjv8q0P7rET70Fv7q0P7mzz7zlr/uEz7q0T8mjz80Fv9mjz7mzz9vEz8mzz60Fz/lzr7q0T6mzz7mzz8vEz7z1r9vUv8vEz7ukv7nD3/lDj7mzz8mzz7qkP7mzz7mzz9vEz9vEz7mzv6oD37q0T7pkH7qkP80Fv9vEz7nDz70Fv6sUf8hi37mzz7q0T7p0L8mjv8vEz8mzz7z1v8vEv9z1v9hi77ukv8s0j/zFf/zlb8z1v8vEz8vEz7q0T7q0T7rET7mzv8q0T6pED7hy/70Fv7q0P8z1v90Fr8z1v8mzz7z1r6vEz7rEb/lDj/zFn7q0T8vEz70Fv9hi77mzz6nDz8q0P8pUD7hy79z1v6qkT8z1v8vEz/qUL8vEz8rET7vEz7mzz8vEz7hS79vUz7q0T8vUz7hi3/zFf8z1v8z1v8z1v8vEv9iTD7mzz7q0T7rUX8mjz8hS78z1r7vUz7hi77q0T8z1v8sET9vE37izH80Fr8hi78hi78mjv8nDz8vEz8hi77mzz7q0T8z1v8vExzWkFsAAAA6XRSTlMAmcLEu4jW3cy0VebheZkrxu7JQsPNwu6qXYgH2EnOuNY79sEN2df7td27KIPywaAXtJrKiYAlEOXEqlMK/enSzKCTTTIFzL+0o5BeyJiIb2xZLdK+urCvn51hTDEtKCckIL2tk4d/eWlHQD06OjgVu56cl451RzUZ+OHEtINwU0U/E+3d0ceAfWZkNc+7qaB4dUYw87u3rZqVko6NfXJAKyMQ+vr09OjhwquopouFgGdVVD80Hx0a7uHXzKqnpZyOb25eWxvn29q4kYV6d0xEH/C8samYi3JkWOvb2NfWqGjSwK2VY1taxxWktUgAABHwSURBVHja7NghjsJAFIDhl1RBCVALctcQBKu6SbOSK/QA3ALdM1T3GGtq6ivqSXCLeLfYrgIMzIYJPJj/l5NJZjJfxjwRmXdV1c5+xv+pmZ2XSvCls9Muvmd22qpf6Pe3VTVcy1/bWm+vPEjgHUq9vVFPMq/VS50EXade2s+lVT+NJehW6qdWIvXTmwTdVP0UeQNZSNCNzIEkEnSlOZBSgg4QY6k5EJWgA8RWc4MgXxJw7wZBPiXgCoMghQTch0GQrQTct0GQjbiU5npecqF4cWxf11GeD7Ksqdbi2LpqsmyQ51Fd7xfH4uRCel6eiksbgyBON+9KPxM4p2bqobITh9InBSl26qW10/9QL8XFC4M06qfpPQewzQuDxOqn0T0HsLFcbwkIIIAAAggggAACCCCAPAJkCQgggAACCCCAAPL0IBNAAAEEEEAAeSGQCSCAAAIIIIAAAggggAACCCCAAHI7yBQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAAAQQQQAABBBBAAAEEEEAeB1INr5eon3ZDh3bqp8ThrEo9Ne1ByFARILYCxFiAGAsQYwFirF927ZgGABiGgWClUiiD8AcWFqXxwx2FlzcLEiNIjCAxgsQIEiNIjCAxgsQIEiNIjCAxgsQIEiNIjCAxgsR4ncRYSIwgMYLECBIjSIwgMYLECBIjSIwgMYLE3HOXEEFiBIkRJEaQGEFiBIkRJEaQGEFiBIkRJEaQmCdIiyAxgsQIEiNIjCAxgsQIEiNIjCAxgsS885YQQWIEiREkRpAYQWIEiREkRpAYQWIEiREkRpCYEaRFkBhBYgSJESRGkBhBYgSJESRGkJg5s4R8du7ltakgiuP4byGCGxeCgkZiCalxE41UTNSaUlI1MUoQa4tIQqvWVjFoSAm1XdVqC60KRarSlSvfuCgoLnwhiLhw6Wsj4kLxzD9hfdZqHufezGQmyf0sXYlfz713DtM6QQzjBDGME8QwThDDOEEM4wQxzEEniFmcIIY5iINfHQZxghjGCWIYJ4hh9jlBzOIEMYwTxDD7sO+rwyBOEMPsw+tl83xa9dOC+X98Z/G6dXe+23zoL4tYFlSVVYskOLTuX69XvF6V16dl87xGljg+Iz1IDttCEYwTRxZXiCMOBHvd5LAl9NQF9BHHFWSIYxyzNt7xkMMyT+w5ZsWJI4MYcSTwQzLmJLGqsxuzuBMSwyPi6MUvbWfIYUFjDr9MEscjdBLHGH5r7nGGhM3f68JvY7x5wkvi6Mecrig5WGIDmDNNHC+ZQbL4W66FHCW5h/G3R8wgU7zWmG88TI6iRvow3xRxTKGd+Wj7R7DfR46CfNnT+IebONqZQYbwn9QUOQq42Yb/hKQGaUQeEecTOK9QHHn4mEFuEocf+bh6nU/g/3jGmpFHkDlbGCIWF/IauEKOeaLdyKuLWIYwSiz3UcDEEDnmferml5YcZAIFxUPk+CE8joIixDKKJmKJoDCvs035ztcfRGHjkoP0oZiuTqp70RSK6SWWJriJpR/FDdf5NqUxguKykoN0opS+EapbI4ltKCFKLG5uEDdKCvbU6zYlFkBJDdx/5xZi8YMhVZeL+fYkGPzcIA3EEwBHroHqzGAcHEHiaWEHSYPFNemnOuLvdYElSTwNWE0842AKZKhuxAbANMMOspJ4+sHWdpPqgjsNtjHpE9IOC97XwTXH8DgseCR9QsKworm3xl8lvukgrGiRHoQGYMlAjGrYVArWeKQ/sigHi4Zr9i5wQw4WdZH8IJMorS7upsztSfhmiGk1/5H1ENY1j9XeYj4WgHVvFExICGw1fM2xPQk72hVMCHUjvzpazA/GYY+f5H9l0Rf8UL+vEs+YC/akSEWQadh1OlsLi/nMAOyKk4pHlhv2pV5SlWtKw76YkiA+L8owU9XXHAffoxwhJUEognK4ElW7TfH0eFGOFKkJkgVXbV1zjHajPH2KggyiXBOjVHWa0ijXQ0VBKAWm2rnmGIqjbC6/lSCNxJfAP2p9m+Kb9qJ8EVI1IVOQobtqrjl2dkGGK8S30tKE+ALIqzYX86tzkCOsbEKoD5L0Gb9NGemDJMOkbELoJniq/pqj70oAsmSI5LzU+RvfmlvMtychjddvbUJWkhWTkCjSSEY6E4FE46QyiBs8VXzN0T/pgkxDSoPQMKQKGLdNyQQgVZLIxkudrxMs1XrNsSkNyWKKg1AXZIsYc80xFIds932qg/SDoxqvOc4t2SXqJ9VB/EHId/8RafewG/Kd9pM1jZaDUA/yqPrFvHsYKvST8gkhzwCUiIdJm5EElOj2VCAIZaGG942PtPC9CUKNh0TqH1nka8N8Vb5NiXZBkTRVJAiNwg5Df2h0dQ6quBrsBFlJ1vVBmW2JEWLQeJOdr4fm2HmH8Hm6oM7paR9VSiwAddp8dgYWq8mGIaiUbKfCDF2y/8/bQGUEMeMwUvq3OZq9J5nnIdnRgAayJQKlXPm3KSbcZGeapMoG8bdBrcAVH6nT2Q21cr4KB6FQNxRLTpEioQgUS3uo0kGo8T5UmwmTCrEgFJvwk00taCG7znRDtfstJJ0nDtUmwqQjCIXaoNrpMyRZOA3VIh7SE4Q8M1At5SGpPBNQLeEj2UH4+l3IS/vnYyEzUCz4iMrhLjMItSSh2BmSKArFhgepzCDusn99sBdK9ZFEKSgVyBKpCsIXTrigkNdD0riRh1GXNRhBGEIJL9SJkjS9UCfQGyIyJAiRP9uGOcY+s3JQJZ3xEBkUZNbqsSSUaCNpAlAi2TNIkjQxgvCFMp+7IJ3LR5KMQL7AzHQjydOEJpIr3NnzPukCTPzwXS03RTreEz1Ds4wO8lNoKNaTiKTvSykzRJLchATN3RORxHS0wUM/VEmQP0YapmJvEl9ybUHY1kmSdMK2YDIXH8tGRwf/7VBtQeaEhzKTw17YkCFJMrAhEOnNDIWpgCoO8lPjw8QALMqSJNOwaOB9dJAqapQRRDZfe19Q0o1ltT9KEe8MU6WN4vbC/5xdyrc2n/V5XL79w5Pvdt7dchV8+xdKsh98x9+9PbJh1v7W7y50dKyZ7/xvSyxburCwJ7gstDj1ygWuDiFJB7gCR7YKLS4wgihy4yiY1lQ8yJ4HQpPLuCB0OXVkm6FBNggLaieIEB28IpeEJGvA8kxYUUtBRKuJQR4LjS7jsdDpLRhOCkkugeGu0KlDc5BTF00LcnyrsKimJkRsNyyI64PQqgOtQq/lKGmHtCBmv9DNCHKjuXJBTqKU5utCrw7sFpp9NCnIM6HZLf1BPpgU5IbQ7BYOC92OmRPkuLCsBoPsNCfIBqHbLewUur2oWJAdKOGs0O2WAf8pxD0Ut6lSQYJbhW4XTAiyxZQgR4V2RgTZYEqQI0K7VhP+EidNCbJGaHcYy4V2N0wJcl1odwTP9b/ITlUqyCYUd01otwf6j+pCBFHUrkoF0f9/c+s94IT+QX1uRpBtQrsOzDqmfVK7zAjiFbpdCuK7i5uEXlfNCHJC6LXrwNxSbY3Q6SKK2l6hIPeETpsO4G8TtzS+0Y6bESQltNnacRz/Su0/JTQ5VqEgu1DURaHJ1sdX84/sYW6S2gwyIbQ4tbsLhZw48kAw1GiQ40KDazufoxjvK0aSCgc5V7tBru0MoBTvW8ZJ0ZkQGa6/8oJj2/IXogRnQsp341kz2A5sFxV01Iwgx0QFvXjmgiVbipyPqzXIdmOCbN8CNs7x3Qki81DOd/FCgeO7E6QMJ4/DvqutlUhyoEKXc7brv+Owdc1FlOfezjzH9yoNck53kFOtKZQv8N/x3Qki4VDOwD6+O0Hs5NhwGvJ4n6k8vm+pUJCz+oJcf+uFXK2CQU2QvRUKckCo0wrZ9os51RpkrxPECeIEsR9ki1BnvxPECWLfHifIN3bun6WtMIrj+A/fRAhdM4lDkWYQDbioIDWkSwkKdRHBUohLhsIlNxQKZikBoSAEkpaklA5OkdalU2qD2NDBwanoVric99AWCjdJTXofc05ycu/zeQEqfnOfv0nMbXg+GyTaQVY9Jj9sEJZvc3g4pifkkSdnI0xBFjwmqzYIS5A1j8mCDcISZN9jshamIDuenPiYPvq3b4OwBHnsMXk9uSA7YQryymPywQZhCfKZ7b8SpiDrnpz4mK5idm0QkyDyv/oKQ8U9OethCvLNY7Jn8ITYIPJnTKkX9glhCXI/Jf1ea/kgDxCYgi+oj2Mss/qVDcIUZGnNY/ByxQZhCoIEw5XIpyewQbiCIPfVG9G7Z5hkkGNwO/YMsAfByYhFnu/BBuEMArwfqccWwhZk1zMgEeTpCEVSi5hwkN3wBcFi6s494ghfkOsZOVsIpHZ56TjO7C1mhrtCIFszcq7BbZPkVBFMnu4kiWCKJGcTCGEQVOgOKrBBDBURlEPGsrBB5IIkbsjQ8j0bRDAIPp6SkbMThDRIieR0EFwtRgZihwiuQ3JK4JYhOS4MVOsUWL0KAy7JyYBbgeS4MNGpC/3gFnWJdJCW0MNagJogBXDLk5wWzKQpkIqidct0BdmEoSwF4Ghat+TBrUxySjC04gTYgCzZIOOb8RLL9B/tnKqFZBncLsg3+SDItWmos++61i0X4HZAcgow9+Zs+IZQ2TR5AG5J6jXxAbbZoIHqRW2j8nQFKcPHskF01Y3KSXCbo24qXj4lGiCjb1Sei0KQQX9TWuEgwB8kTd2UPM9ZukVW46icjkaQ2zaIsys2yMSCYOkn9bl5q3JU5g9SIZ+eIP9sEE+/6JwmK1EJgu0YdYnVlK5b+IOck09TkJ4NYqypdSEZoSBdd7qNqtqV/Tm4HZFPVxC49b89inq3WkdTFWQOo3Eb9Fv7UPHelz/IPPm0BUEu7ziZe5oPI+ajFUT/6ZANomzvyx8kSz4bxFjWBrFBNN3eqJtDbBBlQaZrDjmACkny6Q8yT31Uv0dG3RXudAUpQwUbRFmQC/JFO0geKpTJF+0gBaiQp17RPVy0QZQFyUCFAnWxQSZvuoKcky+kQTLUS/cVbpoG0/epenUfxU/jj2k5V2hBhRb1UX06VCY5LlRwqY/qrVaG5BShQof6qJ4mSySnChWq1Ev3NOmSnCZUaFIv3aNyjcQ0ElAh0SAx22DXph6ql4S/2K1j1MbBIAzD48JkG1Uu1mBUCLbKGVyEgC+gKoVABzDI4EZNfIBUAbchhaU62MIBke2MIbXtpFtDirSTO6yICSwibGZ2/cv6k+85wMeIVwjV7td+S/t3e/9ixnmHaqJz/mLG/S0ZMHg8KWyPPnL3vezu6H3F3PHZBdXIxdlxcZTiwT6yPSk8DggAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFSi5cy5bkqFJJb4bracLXbjp41Cu/Cw2Wye2394ajROm85yohh2Z870dbL9jsvGTrE6cpyZm5NYLxi/XjxqvrlplOyGm1NnkYVkQq/NKuuMZJIn1hmS0CI1NOwohx+6tH+XrJTGJHLFWksScVlrRCIxaw1p7zI2dcVP1nomkQ2r5SQxZLWM9s1htW8k0UtZ6xeJrFltYugVYqcOQdYkMWC1lCS6bCrI2tIgTBIh6yUkkBsLktYhyDXrdUggYL0VCaxYLyQJtjVIQgKRqRc5NBWkZ22QnAT6rOeTgG8qSMfaICEJtFgvIIHA1MVda4P4poLEJOCZuviHtUECU0Gig34Lk1oEmZoKMkYQBPm/IAMEKfNIYI4gZfHnDJIjSGVBAnuCjOoUpH/QICtrg0Qk4JoK0kIQ+4PEJDBBkLL5QYOE1gbxTAVpIcgXCRIhiIVB/FoEaSIIglQfxEMQBEGQv+gjiIVBAgSpLMgcQRDkH918iSCi4RhBEARBEMSeIBGCVBZkbE+QKwR54yFIZUFcBPndrh2bIAxFURh+KQI2TuA2AadJZ+8MziDWQmzTBUdImcIF7hAWgdQvj/vg5PJ/A5zmb88Bg/QEIQhBQgYZTxmWWsOj7bdkDR82CCoG6QwEiYMgYggihiBiCCKGIGIIIoYgYggixj9IYyBIHAQRQxAxBBFDEDEEEeMf5GYgSBwEEUMQMQQRQxAxBBHjH+RhIEgcBBFDEDEEEUMQMQQRw+tEjH+Q58tQ7pvc3Q3FfsnfdTYUmi+phql10TVxvdvNeTUNw6dP+f4roNz63ESkpQAAAABJRU5ErkJggg=="
3 +
4 +export const company = "SOCFortress"
5 +export const theme: "light" | "dark" = "light"
6 +export const retina = false
frontend/src/layouts/common/Navbar/items.tsx
+15
@@ -16,6 +16,7 @@ const CustomersIcon = "carbon:user-multiple"
16 const LogsIcon = "carbon:cloud-logging"
17 const UsersIcon = "carbon:group-security"
18 const IntegrationsIcon = "carbon:ibm-cloud-direct-link-2-dedicated"
19 +const ReportCreationIcon = "carbon:report-data"
20
21 /*eslint @typescript-eslint/no-unused-vars: "off"*/
22 export default function getItems(mode: "vertical" | "horizontal", collapsed: boolean): MenuMixedOption[] {
@@ -265,6 +266,20 @@ export default function getItems(mode: "vertical" | "horizontal", collapsed: boo
266 ),
267 key: "Integrations",
268 icon: renderIcon(IntegrationsIcon)
269 + },
270 + {
271 + label: () =>
272 + h(
273 + RouterLink,
274 + {
275 + to: {
276 + name: "ReportCreation"
277 + }
278 + },
279 + { default: () => "Report Creation" }
280 + ),
281 + key: "ReportCreation",
282 + icon: renderIcon(ReportCreationIcon)
283 }
284 ]
285 }
frontend/src/router/index.ts
+6
@@ -141,6 +141,12 @@ const router = createRouter({
141 component: () => import("@/views/Integrations.vue"),
142 meta: { title: "Integrations", auth: true, roles: UserRole.All }
143 },
144 + {
145 + path: "/report-creation",
146 + name: "ReportCreation",
147 + component: () => import("@/views/ReportCreation.vue"),
148 + meta: { title: "Report Creation", auth: true, roles: UserRole.All }
149 + },
150
151 {
152 path: "/profile",
frontend/src/stores/theme.ts
+2
@@ -313,6 +313,7 @@ export const useThemeStore = defineStore("theme", {
313
314 const bgSidebar = this.sidebarBackground
315 const bgBody = this.bodyBackground
316 + const bgBodyRGB = hex2rgb(bgBody).join(", ")
317
318 const boxedWidth = state.boxed.width
319 const routerTransitionDuration = state.routerTransitionDuration
@@ -362,6 +363,7 @@ export const useThemeStore = defineStore("theme", {
363 return {
364 "--bg-sidebar": `${bgSidebar}`,
365 "--bg-body": `${bgBody}`,
366 + "--bg-body-rgb": `${bgBodyRGB}`,
367
368 "--fg-color": `${fgColor}`,
369 "--fg-secondary-color": `${fgSecondaryColor}`,
frontend/src/types/agents.d.ts
+7
@@ -10,6 +10,7 @@ export interface Agent {
10 velociraptor_id: string
11 velociraptor_last_seen: string
12 wazuh_agent_version: string
13 + wazuh_agent_status: AgentStatus
14 velociraptor_agent_version: string
15 customer_code: null | string
16 vulnerabilities?: AgentVulnerabilities[]
@@ -17,6 +18,12 @@ export interface Agent {
18 quarantined?: boolean
19 }
20
21 +export enum AgentStatus {
22 + Active = "active",
23 + Disconnected = "disconnected",
24 + NotFound = "not found"
25 +}
26 +
27 export interface AgentVulnerabilities {
28 id?: string
29 architecture: string
frontend/src/types/customers.d.ts
+1
@@ -96,4 +96,5 @@ export interface CustomerProvisioningDefaultSettings {
96 cluster_key: string
97 master_ip: string
98 grafana_url: string
99 + wazuh_worker_hostname: string
100 }
frontend/src/types/reporting.d.ts new
+55
@@ -0,0 +1,55 @@
1 +export interface Org {
2 + id: number
3 + name: string
4 +}
5 +
6 +export interface Dashboard {
7 + id: number
8 + uid: string
9 + title: string
10 + uri: string
11 + url: string
12 + slug: string
13 + type: string
14 + tags: string[]
15 + isStarred: boolean
16 + sortMeta: number
17 + folderId: number | null
18 + folderUid: null | string
19 + folderTitle: null | string
20 + folderUrl: null | string
21 +}
22 +
23 +export interface Panel {
24 + fieldConfig: null | {
25 + defaults: {
26 + color: string
27 + custom: string
28 + mappings: any[]
29 + thresholds: string
30 + }
31 + overrides: any[]
32 + }
33 + gridPos: null | {
34 + h: number
35 + w: number
36 + x: number
37 + y: number
38 + }
39 + id: number
40 + options: null | { legend: any; tooltip: any }
41 + title: string
42 + type: null | string
43 + collapsed: null | boolean
44 + panels: Panel[]
45 +}
46 +
47 +export interface PanelLink {
48 + panel_id: number
49 + panel_url: string
50 +}
51 +
52 +export interface PanelImage {
53 + base64_image: string
54 + url: string
55 +}
frontend/src/views/AgentOverview.vue
+3 -3
@@ -110,8 +110,8 @@
110 import { ref, onBeforeMount, computed, nextTick } from "vue"
111 import { useRoute } from "vue-router"
112 import Api from "@/api"
113 -import { type Agent } from "@/types/agents.d"
114 -import { handleDeleteAgent, isAgentOnline, toggleAgentCritical } from "@/components/agents/utils"
113 +import { AgentStatus, type Agent } from "@/types/agents.d"
114 +import { handleDeleteAgent, toggleAgentCritical } from "@/components/agents/utils"
115 import { useRouter } from "vue-router"
116 import VulnerabilitiesSection from "@/components/agents/VulnerabilitiesSection.vue"
117 import AlertsList from "@/components/alerts/AlertsList.vue"
@@ -141,7 +141,7 @@ const agentId = ref<string | null>(null)
141 const artifacts = ref<Artifact[]>([])
142
143 const isOnline = computed(() => {
144 - return isAgentOnline(agent.value?.wazuh_last_seen ?? "")
144 + return agent.value?.wazuh_agent_status === AgentStatus.Active
145 })
146
147 const isQuarantined = computed(() => {
frontend/src/views/Agents.vue
+3 -7
@@ -54,10 +54,9 @@
54
55 <script setup lang="ts">
56 import { computed, onBeforeMount, ref, watch } from "vue"
57 -import { type Agent } from "@/types/agents.d"
57 +import { AgentStatus, type Agent } from "@/types/agents.d"
58 import AgentCard from "@/components/agents/AgentCard.vue"
59 import AgentToolbar from "@/components/agents/AgentToolbar.vue"
60 -import { isAgentOnline } from "@/components/agents/utils"
60 import Api from "@/api"
61 import { useRouter } from "vue-router"
62 import { useMessage, NSpin, NScrollbar, NEmpty, NPagination } from "naive-ui"
@@ -104,7 +103,7 @@ const agentsCritical = computed(() => {
103 })
104
105 const agentsOnline = computed(() => {
107 - return agents.value.filter(({ online }) => online)
106 + return agents.value.filter(({ wazuh_agent_status }) => wazuh_agent_status === AgentStatus.Active)
107 })
108
109 function gotoAgentPage(agent: Agent) {
@@ -118,10 +117,7 @@ function getAgents() {
117 .getAgents()
118 .then(res => {
119 if (res.data.success) {
121 - agents.value = (res.data.agents || []).map(o => {
122 - o.online = isAgentOnline(o.wazuh_last_seen)
123 - return o
124 - })
120 + agents.value = res.data.agents || []
121 } else {
122 message.error(res.data?.message || "An error occurred. Please try again later.")
123 }
frontend/src/views/Profile.vue
+2
@@ -8,6 +8,7 @@
8 v-if="propicEnabled"
9 v-slot="{ openCropper }"
10 @crop="setCroppedImage"
11 + shape="circle"
12 :placeholder="'Select your profile picture'"
13 >
14 <Icon :name="EditIcon" :size="16" class="edit" @click="openCropper()"></Icon>
@@ -44,6 +45,7 @@
45 v-if="propicEnabled"
46 v-slot="{ openCropper }"
47 @crop="setCroppedImage"
48 + shape="circle"
49 :placeholder="'Select your profile picture'"
50 >
51 <n-button size="large" type="primary" @click="openCropper()">Edit profile image</n-button>
frontend/src/views/ReportCreation.vue new
+63
@@ -0,0 +1,63 @@
1 +<template>
2 + <div class="page page-wrapped page-without-footer flex flex-col gap-5">
3 + <ReportWizard
4 + @timerange="timerange = $event"
5 + @organization="org = $event"
6 + @dashboard="dashboard = $event"
7 + @panels="panels = $event"
8 + hide-panels-select
9 + />
10 + <ReportPanels :timerange="timerange" :org="org" :dashboard="dashboard" :panels="panels" />
11 + <div class="mobile-overlay">
12 + <div>
13 + <Icon :name="AlertIcon" :size="18" class="relative top-0.5 mr-1"></Icon>
14 + This function is available only for desktop devices
15 + </div>
16 + </div>
17 + </div>
18 +</template>
19 +
20 +<script setup lang="ts">
21 +import { ref } from "vue"
22 +import ReportWizard from "@/components/reportCreation/Wizard.vue"
23 +import ReportPanels from "@/components/reportCreation/Panels.vue"
24 +import type { Dashboard, Org, Panel } from "@/types/reporting"
25 +import type { ReportTimeRange } from "@/api/reporting"
26 +import Icon from "@/components/common/Icon.vue"
27 +
28 +const AlertIcon = "mdi:alert-outline"
29 +
30 +const timerange = ref<ReportTimeRange | null>(null)
31 +const org = ref<Org | null>(null)
32 +const dashboard = ref<Dashboard | null>(null)
33 +const panels = ref<Panel[]>([])
34 +</script>
35 +
36 +<style lang="scss" scoped>
37 +@import "../layouts/HorizontalNav/_variables.scss";
38 +
39 +.page {
40 + overflow: hidden;
41 + position: relative;
42 +
43 + .mobile-overlay {
44 + background-color: rgba(var(--bg-body-rgb), 0.8);
45 + position: absolute;
46 + top: 0;
47 + left: 0;
48 + right: 0;
49 + bottom: 0;
50 + display: flex;
51 + align-items: center;
52 + justify-content: center;
53 + padding: 40px;
54 + text-align: center;
55 + font-size: 20px;
56 + display: none;
57 +
58 + @media (max-width: $sidebar-bp) {
59 + display: flex;
60 + }
61 + }
62 +}
63 +</style>
report-template-test.html new
+113
@@ -0,0 +1,113 @@
1 +<html lang="en">
2 + <head>
3 + <meta charset="UTF-8" />
4 + <title>test</title>
5 + <style>
6 + html,
7 + body {
8 + padding: 0;
9 + margin: 0;
10 + }
11 +
12 + * {
13 + box-sizing: border-box;
14 + }
15 + </style>
16 +
17 + <script src="https://cdnjs.cloudflare.com/ajax/libs/nunjucks/2.4.2/nunjucks.min.js"></script>
18 + <script src="https://code.jquery.com/jquery-latest.js"></script>
19 + </head>
20 +
21 + <body>
22 + <span id="output"></span>
23 +
24 + <script>
25 + var content = `
26 +{% set panels = panels | we_parse %}
27 +
28 +<html>
29 + <head>
30 + <style>
31 + :root {
32 + --border-radius: 6px;
33 + --bg-secondary-color: red;
34 + --border-small-050: 1px solid green;
35 + }
36 +
37 + html,
38 + body {
39 + padding: 0;
40 + margin: 0;
41 + }
42 +
43 + * {
44 + box-sizing: border-box;
45 + }
46 +
47 + .panels-container {
48 + background-color: var(--bg-secondary-color);
49 + display: flex;
50 + flex-wrap: wrap;
51 + box-sizing: border-box;
52 + padding: 10px;
53 + }
54 +
55 + .panel {
56 + background-color: var(--bg-secondary-color);
57 + overflow: hidden;
58 + flex-grow: 1;
59 + min-width: 100px;
60 + box-sizing: border-box;
61 + padding: 10px;
62 + }
63 +
64 + .panel img {
65 + width: 100%;
66 + border-radius: var(--border-radius);
67 + border: var(--border-small-050);
68 + }
69 + </style>
70 + </head>
71 + <body>
72 + <div class="panels-container">
73 + {% for panel in panels %}
74 + <div class="panel" style="{{'flex-basis:'+panel.width+'%' if panel.width else ''}}">
75 + <img src="{{panel.image}}" />
76 + </div>
77 + {% endfor %}
78 + </div>
79 + </body>
80 +</html>
81 +`
82 +
83 + function _parse(string) {
84 + if (typeof string === "string") {
85 + try {
86 + return JSON.parse(string)
87 + } catch (e) {
88 + return {}
89 + }
90 + }
91 + return {}
92 + }
93 +
94 + var env = new nunjucks.Environment()
95 + env.addGlobal("we_parse", _parse)
96 + env.addFilter("we_parse", _parse)
97 + var t = nunjucks.compile(content, env)
98 +
99 + var ctx = {
100 + panels: JSON.stringify([
101 + { width: 50, image: "https://placehold.co/600x400" },
102 + { width: 50, image: "https://placehold.co/600x400" },
103 + { width: 0, image: "https://placehold.co/600x400" },
104 + { width: 20, image: "https://placehold.co/600x400" },
105 + { width: 80, image: "https://placehold.co/600x400" }
106 + ])
107 + }
108 +
109 + console.log(t.render(ctx))
110 + $("#output").html(t.render(ctx))
111 + </script>
112 + </body>
113 +</html>
report-template.html new
+56
@@ -0,0 +1,56 @@
1 +{% set panels = panels | we_parse %}
2 +
3 +<html>
4 + <head>
5 + <style>
6 + :root {
7 + --border-radius: 6px;
8 + --bg-secondary-color: red;
9 + --border-small-050: 1px solid green;
10 + }
11 +
12 + html,
13 + body {
14 + padding: 0;
15 + margin: 0;
16 + }
17 +
18 + * {
19 + box-sizing: border-box;
20 + }
21 +
22 + .panels-container {
23 + background-color: var(--bg-secondary-color);
24 + display: flex;
25 + flex-wrap: wrap;
26 + box-sizing: border-box;
27 + padding: 10px;
28 + }
29 +
30 + .panel {
31 + background-color: var(--bg-secondary-color);
32 + overflow: hidden;
33 + flex-grow: 1;
34 + min-width: 100px;
35 + box-sizing: border-box;
36 + padding: 10px;
37 + page-break-inside: avoid;
38 + }
39 +
40 + .panel img {
41 + width: 100%;
42 + border-radius: var(--border-radius);
43 + border: var(--border-small-050);
44 + }
45 + </style>
46 + </head>
47 + <body>
48 + <div class="panels-container">
49 + {% for panel in panels %}
50 + <div class="panel" style="{{'flex-basis:'+panel.width+'%' if panel.width else ''}}">
51 + <img src="{{panel.image}}" />
52 + </div>
53 + {% endfor %}
54 + </div>
55 + </body>
56 +</html>