Virustotal ioc (#343)
* feat: Add VirusTotal threat intelligence endpoint and related functionality * feat: Implement VirusTotal threat intelligence endpoint and response models * feat: Update VirusTotal schema to allow optional fields for result and last_analysis_results * feat: Add verification check for VirusTotal connector before API invocation * chore: update dependencies in frontend * feat: add VirusTotalEnrichmentButton * refactor: improved some components * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>
taylor_socfortress committed
Nov 30, 2024 at 09:44 UTC
d8bb5ed868e618cbc10f20fa7599729675717901
17 files changed
+1303
-243
.vscode/settings.json
+3
@@ -12,6 +12,7 @@
12
"colord",
13
"commonmark",
14
"creationdate",
15
+ "crowdsourced",
16
"Crowdstrike",
17
"darktrace",
18
"datejs",
@@ -32,6 +33,7 @@
33
"iconoir",
34
"Indicies",
35
"iocs",
36
+ "jarm",
37
"lastupdate",
38
"linebreak",
39
"Logsource",
@@ -67,6 +69,7 @@
69
"virustotal",
70
"vuesjv",
71
"Wazuh",
72
+ "whois",
73
"xaxis",
74
"zondicons"
75
]
backend/app/threat_intel/routes/socfortress.py
+58
@@ -20,6 +20,9 @@ from app.threat_intel.schema.socfortress import SocfortressAiWazuhExclusionRuleR
20
from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisRequest
21
from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisResponse
22
from app.threat_intel.schema.socfortress import SocfortressThreatIntelRequest
23
+from app.threat_intel.schema.socfortress import VirusTotalThreatIntelRequest
24
+from app.threat_intel.schema.virustotal import VirusTotalRouteResponse
25
+from app.threat_intel.services.socfortress import invoke_virustotal_api
26
from app.threat_intel.services.socfortress import socfortress_ai_alert_lookup
27
from app.threat_intel.services.socfortress import socfortress_process_analysis_lookup
28
from app.threat_intel.services.socfortress import socfortress_threat_intel_lookup
@@ -96,6 +99,61 @@ async def threat_intel_socfortress(
99
return socfortress_lookup
100
101
102
+@threat_intel_socfortress_router.post(
103
+ "/virustotal",
104
+ response_model=VirusTotalRouteResponse,
105
+ description="VirusTotal Enrichment Threat Intel",
106
+ dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
107
+)
108
+async def threat_intel_virustotal(
109
+ request: VirusTotalThreatIntelRequest,
110
+ session: AsyncSession = Depends(get_db),
111
+):
112
+ """
113
+ Endpoint for VirusTotal Threat Intel.
114
+
115
+ This endpoint allows authorized users with 'admin' or 'analyst' scope to perform VirusTotal threat intelligence lookup.
116
+
117
+ Parameters:
118
+ - request: VirusTotalThreatIntelRequest - The request payload containing the necessary information for the lookup.
119
+ - session: AsyncSession (optional) - The database session to use for the lookup.
120
+ - _key_exists: bool (optional) - A dependency to ensure the API key exists.
121
+
122
+ Returns:
123
+ - IoCResponse: The response model containing the results of the VirusTotal threat intelligence lookup.
124
+ """
125
+ logger.info("Running VirusTotal Threat Intel.")
126
+
127
+ # Check if the connector is verified
128
+ if not await get_connector_attribute(
129
+ connector_name="VirusTotal",
130
+ column_name="connector_verified",
131
+ session=session,
132
+ ):
133
+ raise HTTPException(
134
+ status_code=500,
135
+ detail="VirusTotal connector is not verified.",
136
+ )
137
+
138
+ return VirusTotalRouteResponse(
139
+ data=await invoke_virustotal_api(
140
+ url=await get_connector_attribute(
141
+ connector_name="VirusTotal",
142
+ column_name="connector_url",
143
+ session=session,
144
+ ),
145
+ api_key=await get_connector_attribute(
146
+ connector_name="VirusTotal",
147
+ column_name="connector_api_key",
148
+ session=session,
149
+ ),
150
+ request=request,
151
+ ),
152
+ success=True,
153
+ message="VirusTotal threat intelligence lookup was successful.",
154
+ )
155
+
156
+
157
@threat_intel_socfortress_router.post(
158
"/process_name",
159
response_model=SocfortressProcessNameAnalysisResponse,
backend/app/threat_intel/schema/socfortress.py
+4
@@ -17,6 +17,10 @@ class SocfortressThreatIntelRequest(BaseModel):
17
)
18
19
20
+class VirusTotalThreatIntelRequest(BaseModel):
21
+ ioc_value: str
22
+
23
+
24
class IoCMapping(BaseModel):
25
comment: Optional[str] = Field(None, description="Comment about the IOCs")
26
ioc_source: str = Field(
backend/app/threat_intel/schema/virustotal.py
new
+79
@@ -0,0 +1,79 @@
1
+from typing import Dict
2
+from typing import List
3
+from typing import Optional
4
+
5
+from pydantic import BaseModel
6
+from pydantic import Extra
7
+from pydantic import Field
8
+
9
+
10
+class AnalysisResult(BaseModel):
11
+ method: str
12
+ engine_name: str
13
+ category: str
14
+ result: Optional[str] = Field(default=None)
15
+
16
+ class Config:
17
+ extra = Extra.allow
18
+
19
+
20
+class TotalVotes(BaseModel):
21
+ harmless: int
22
+ malicious: int
23
+
24
+ class Config:
25
+ extra = Extra.allow
26
+
27
+
28
+class Attributes(BaseModel):
29
+ total_votes: TotalVotes
30
+ last_analysis_results: Optional[Dict[str, AnalysisResult]] = Field(default=None)
31
+ regional_internet_registry: Optional[str] = Field(default=None)
32
+ continent: Optional[str] = Field(default=None)
33
+ last_modification_date: Optional[int] = Field(default=None)
34
+ crowdsourced_context: Optional[List[Dict[str, str]]] = Field(default=None)
35
+ tags: Optional[List[str]] = Field(default=None)
36
+ asn: Optional[int] = Field(default=None)
37
+ whois: Optional[str] = Field(default=None)
38
+ whois_date: Optional[int] = Field(default=None)
39
+ reputation: Optional[int] = Field(default=None)
40
+ last_analysis_date: Optional[int] = Field(default=None)
41
+ jarm: Optional[str] = Field(default=None)
42
+ country: Optional[str] = Field(default=None)
43
+ as_owner: Optional[str] = Field(default=None)
44
+ last_analysis_stats: Optional[Dict[str, int]] = Field(default=None)
45
+ last_https_certificate_date: Optional[int] = Field(default=None)
46
+ network: Optional[str] = Field(default=None)
47
+
48
+ class Config:
49
+ extra = Extra.allow
50
+
51
+
52
+class Links(BaseModel):
53
+ self: str
54
+
55
+ class Config:
56
+ extra = Extra.allow
57
+
58
+
59
+class Data(BaseModel):
60
+ id: str
61
+ type: str
62
+ links: Links
63
+ attributes: Attributes
64
+
65
+ class Config:
66
+ extra = Extra.allow
67
+
68
+
69
+class VirusTotalResponse(BaseModel):
70
+ data: Data
71
+
72
+ class Config:
73
+ extra = Extra.allow
74
+
75
+
76
+class VirusTotalRouteResponse(BaseModel):
77
+ data: VirusTotalResponse
78
+ success: bool
79
+ message: str
backend/app/threat_intel/services/socfortress.py
+78
@@ -1,3 +1,4 @@
1
+import re
2
from typing import Any
3
from typing import Dict
4
@@ -19,6 +20,7 @@ from app.threat_intel.schema.socfortress import (
20
from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisRequest
21
from app.threat_intel.schema.socfortress import SocfortressProcessNameAnalysisResponse
22
from app.threat_intel.schema.socfortress import SocfortressThreatIntelRequest
23
+from app.threat_intel.schema.virustotal import VirusTotalResponse
24
from app.utils import get_connector_attribute
25
26
@@ -150,6 +152,82 @@ async def invoke_socfortress_threat_intel_api(
152
return response.json()
153
154
155
+def determine_ioc_type(ioc_value: str) -> str:
156
+ """
157
+ Determine the type of the IOC value and return the appropriate endpoint.
158
+
159
+ Args:
160
+ ioc_value (str): The IOC value.
161
+
162
+ Returns:
163
+ str: The endpoint for the IOC value.
164
+
165
+ Raises:
166
+ ValueError: If the IOC value is invalid.
167
+ """
168
+ ip_pattern = re.compile(r"^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$")
169
+ domain_pattern = re.compile(r"^(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$")
170
+ hash_pattern = re.compile(r"^[a-fA-F0-9]{32}$|^[a-fA-F0-9]{40}$|^[a-fA-F0-9]{64}$")
171
+
172
+ if ip_pattern.match(ioc_value):
173
+ return f"/ip_addresses/{ioc_value}"
174
+ elif domain_pattern.match(ioc_value):
175
+ return f"/domains/{ioc_value}"
176
+ elif hash_pattern.match(ioc_value):
177
+ return f"/files/{ioc_value}"
178
+ else:
179
+ raise HTTPException(
180
+ status_code=400,
181
+ detail="Invalid IOC value provided. Only IP addresses, domains, and hashes are supported.",
182
+ )
183
+
184
+
185
+async def fetch_virustotal_data(api_key: str, full_url: str) -> dict:
186
+ """
187
+ Fetch data from the VirusTotal API.
188
+
189
+ Args:
190
+ api_key (str): The API key for authentication.
191
+ full_url (str): The full URL of the VirusTotal API endpoint.
192
+
193
+ Returns:
194
+ dict: The JSON response from the VirusTotal API.
195
+
196
+ Raises:
197
+ httpx.HTTPStatusError: If the API request fails with a non-successful status code.
198
+ """
199
+ headers = {"x-apikey": api_key}
200
+ async with httpx.AsyncClient() as client:
201
+ response = await client.get(full_url, headers=headers)
202
+ response.raise_for_status()
203
+ return VirusTotalResponse.parse_obj(response.json())
204
+
205
+
206
+async def invoke_virustotal_api(
207
+ api_key: str,
208
+ url: str,
209
+ request: SocfortressThreatIntelRequest,
210
+) -> dict:
211
+ """
212
+ Invokes the VirusTotal API with the provided API key, URL, and request parameters.
213
+
214
+ Args:
215
+ api_key (str): The API key for authentication.
216
+ url (str): The base URL of the VirusTotal API.
217
+ request (SocfortressThreatIntelRequest): The request object containing the IOC value and customer code.
218
+
219
+ Returns:
220
+ dict: The JSON response from the VirusTotal API.
221
+
222
+ Raises:
223
+ httpx.HTTPStatusError: If the API request fails with a non-successful status code.
224
+ """
225
+ ioc_value = request.ioc_value
226
+ endpoint = determine_ioc_type(ioc_value)
227
+ full_url = f"{url}{endpoint}"
228
+ return await fetch_virustotal_data(api_key, full_url)
229
+
230
+
231
async def invoke_socfortress_process_name_api(
232
api_key: str,
233
url: str,
frontend/figma-tokens.json
+4
-4
@@ -1,11 +1,11 @@
1
{
2
"global": {
3
"border-radius-base": {
4
- "value": "6px",
4
+ "value": "8px",
5
"type": "borderRadius"
6
},
7
"border-radius-small": {
8
- "value": "3px",
8
+ "value": "4px",
9
"type": "borderRadius"
10
},
11
"line-heights-base": {
@@ -13,7 +13,7 @@
13
"type": "lineHeights"
14
},
15
"font-sizes-base": {
16
- "value": "15px",
16
+ "value": "16px",
17
"type": "fontSizes"
18
},
19
"font-sizes-card-title": {
@@ -802,7 +802,7 @@
802
"value": {
803
"fontFamily": "{font-families-base}",
804
"fontSize": "{font-sizes-base}",
805
- "lineHeight": "20"
805
+ "lineHeight": "22"
806
},
807
"type": "typography"
808
}
frontend/package-lock.json
+259
-182
@@ -13,7 +13,7 @@
13
"@fontsource/jetbrains-mono": "^5.1.1",
14
"@fontsource/lexend": "^5.1.1",
15
"@fontsource/public-sans": "^5.1.1",
16
- "@shikijs/markdown-it": "^1.23.1",
16
+ "@shikijs/markdown-it": "^1.24.0",
17
"@tailwindcss/container-queries": "^0.1.1",
18
"@vueuse/core": "^12.0.0",
19
"axios": "^1.7.8",
@@ -31,15 +31,15 @@
31
"naive-ui": "^2.40.2",
32
"nanoid": "^5.0.9",
33
"password-validator": "^5.3.0",
34
- "pinia": "^2.2.7",
34
+ "pinia": "^2.2.8",
35
"pinia-plugin-persistedstate": "^4.1.3",
36
"secure-ls": "^2.0.0",
37
- "shiki": "^1.23.1",
37
+ "shiki": "^1.24.0",
38
"validator": "^13.12.0",
39
"vue": "^3.5.13",
40
"vue-advanced-cropper": "^2.8.9",
41
"vue-highlight-words": "^3.0.1",
42
- "vue-i18n": "^10.0.4",
42
+ "vue-i18n": "^10.0.5",
43
"vue-router": "^4.5.0",
44
"vue-sjv": "^0.0.6",
45
"vue3-apexcharts": "^1.8.0",
@@ -47,7 +47,7 @@
47
"vuedraggable": "^4.1.0"
48
},
49
"devDependencies": {
50
- "@antfu/eslint-config": "^3.11.0",
50
+ "@antfu/eslint-config": "^3.11.2",
51
"@clack/prompts": "^0.8.2",
52
"@iconify/vue": "^4.1.2",
53
"@tsconfig/node20": "^20.1.4",
@@ -56,16 +56,16 @@
56
"@types/fs-extra": "^11.0.4",
57
"@types/jsdom": "^21.1.7",
58
"@types/lodash": "^4.17.13",
59
- "@types/node": "^22.10.0",
59
+ "@types/node": "^22.10.1",
60
"@types/validator": "^13.12.2",
61
"@vitejs/plugin-vue": "^5.2.1",
62
"@vitejs/plugin-vue-jsx": "^4.1.1",
63
"@vue/test-utils": "^2.4.6",
64
- "@vue/tsconfig": "^0.6.0",
64
+ "@vue/tsconfig": "^0.7.0",
65
"autoprefixer": "^10.4.20",
66
"cypress": "^13.16.0",
67
"depcheck": "^1.4.7",
68
- "eslint": "^9.15.0",
68
+ "eslint": "^9.16.0",
69
"flourite": "^1.3.0",
70
"fs-extra": "^11.2.0",
71
"jsdom": "^25.0.1",
@@ -83,7 +83,7 @@
83
"unplugin-vue-components": "^0.27.5",
84
"vite": "^5.4.11",
85
"vite-bundle-visualizer": "^1.2.1",
86
- "vite-plugin-vue-devtools": "^7.6.5",
86
+ "vite-plugin-vue-devtools": "^7.6.7",
87
"vite-svg-loader": "^5.1.0",
88
"vitest": "^2.1.6",
89
"vue-tsc": "^2.1.10"
@@ -130,9 +130,9 @@
130
}
131
},
132
"node_modules/@antfu/eslint-config": {
133
- "version": "3.11.0",
134
- "resolved": "https://registry.npmjs.org/@antfu/eslint-config/-/eslint-config-3.11.0.tgz",
135
- "integrity": "sha512-yJ8xkY7qtSoZpTOEOo3gMRsiYYvhNXHaWWh1APN0brDZsmRSjSPJBxMgq+JG+6hjeToJh9koZQxzDiwCbmirEg==",
133
+ "version": "3.11.2",
134
+ "resolved": "https://registry.npmjs.org/@antfu/eslint-config/-/eslint-config-3.11.2.tgz",
135
+ "integrity": "sha512-hoi2MnOdiKL8mIhpMtinwMrqVPq6QVbHPA+BuQD4pqE6yVLyYvjdLFiKApMsezAM+YofCsbhak2oY+JCiIyeNA==",
136
"dev": true,
137
"license": "MIT",
138
"dependencies": {
@@ -143,7 +143,7 @@
143
"@stylistic/eslint-plugin": "^2.11.0",
144
"@typescript-eslint/eslint-plugin": "^8.16.0",
145
"@typescript-eslint/parser": "^8.16.0",
146
- "@vitest/eslint-plugin": "^1.1.11",
146
+ "@vitest/eslint-plugin": "^1.1.12",
147
"eslint-config-flat-gitignore": "^0.3.0",
148
"eslint-flat-config-utils": "^0.4.0",
149
"eslint-merge-processors": "^0.1.0",
@@ -1499,9 +1499,9 @@
1499
}
1500
},
1501
"node_modules/@eslint/js": {
1502
- "version": "9.15.0",
1503
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz",
1504
- "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==",
1502
+ "version": "9.16.0",
1503
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz",
1504
+ "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==",
1505
"dev": true,
1506
"license": "MIT",
1507
"engines": {
@@ -1684,13 +1684,13 @@
1684
}
1685
},
1686
"node_modules/@intlify/core-base": {
1687
- "version": "10.0.4",
1688
- "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.4.tgz",
1689
- "integrity": "sha512-GG428DkrrWCMhxRMRQZjuS7zmSUzarYcaHJqG9VB8dXAxw4iQDoKVQ7ChJRB6ZtsCsX3Jse1PEUlHrJiyQrOTg==",
1687
+ "version": "10.0.5",
1688
+ "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.5.tgz",
1689
+ "integrity": "sha512-F3snDTQs0MdvnnyzTDTVkOYVAZOE/MHwRvF7mn7Jw1yuih4NrFYLNYIymGlLmq4HU2iIdzYsZ7f47bOcwY73XQ==",
1690
"license": "MIT",
1691
"dependencies": {
1692
- "@intlify/message-compiler": "10.0.4",
1693
- "@intlify/shared": "10.0.4"
1692
+ "@intlify/message-compiler": "10.0.5",
1693
+ "@intlify/shared": "10.0.5"
1694
},
1695
"engines": {
1696
"node": ">= 16"
@@ -1700,12 +1700,12 @@
1700
}
1701
},
1702
"node_modules/@intlify/message-compiler": {
1703
- "version": "10.0.4",
1704
- "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.4.tgz",
1705
- "integrity": "sha512-AFbhEo10DP095/45EauinQJ5hJ3rJUmuuqltGguvc3WsvezZN+g8qNHLGWKu60FHQVizMrQY7VJ+zVlBXlQQkQ==",
1703
+ "version": "10.0.5",
1704
+ "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.5.tgz",
1705
+ "integrity": "sha512-6GT1BJ852gZ0gItNZN2krX5QAmea+cmdjMvsWohArAZ3GmHdnNANEcF9JjPXAMRtQ6Ux5E269ymamg/+WU6tQA==",
1706
"license": "MIT",
1707
"dependencies": {
1708
- "@intlify/shared": "10.0.4",
1708
+ "@intlify/shared": "10.0.5",
1709
"source-map-js": "^1.0.2"
1710
},
1711
"engines": {
@@ -1716,9 +1716,9 @@
1716
}
1717
},
1718
"node_modules/@intlify/shared": {
1719
- "version": "10.0.4",
1720
- "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.4.tgz",
1721
- "integrity": "sha512-ukFn0I01HsSgr3VYhYcvkTCLS7rGa0gw4A4AMpcy/A9xx/zRJy7PS2BElMXLwUazVFMAr5zuiTk3MQeoeGXaJg==",
1719
+ "version": "10.0.5",
1720
+ "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.5.tgz",
1721
+ "integrity": "sha512-bmsP4L2HqBF6i6uaMqJMcFBONVjKt+siGluRq4Ca4C0q7W2eMaVZr8iCgF9dKbcVXutftkC7D6z2SaSMmLiDyA==",
1722
"license": "MIT",
1723
"engines": {
1724
"node": ">= 16"
@@ -2584,55 +2584,62 @@
2584
"win32"
2585
]
2586
},
2587
+ "node_modules/@sec-ant/readable-stream": {
2588
+ "version": "0.4.1",
2589
+ "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz",
2590
+ "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==",
2591
+ "dev": true,
2592
+ "license": "MIT"
2593
+ },
2594
"node_modules/@shikijs/core": {
2588
- "version": "1.23.1",
2589
- "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.23.1.tgz",
2590
- "integrity": "sha512-NuOVgwcHgVC6jBVH5V7iblziw6iQbWWHrj5IlZI3Fqu2yx9awH7OIQkXIcsHsUmY19ckwSgUMgrqExEyP5A0TA==",
2595
+ "version": "1.24.0",
2596
+ "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.24.0.tgz",
2597
+ "integrity": "sha512-6pvdH0KoahMzr6689yh0QJ3rCgF4j1XsXRHNEeEN6M4xJTfQ6QPWrmHzIddotg+xPJUPEPzYzYCKzpYyhTI6Gw==",
2598
"license": "MIT",
2599
"dependencies": {
2593
- "@shikijs/engine-javascript": "1.23.1",
2594
- "@shikijs/engine-oniguruma": "1.23.1",
2595
- "@shikijs/types": "1.23.1",
2600
+ "@shikijs/engine-javascript": "1.24.0",
2601
+ "@shikijs/engine-oniguruma": "1.24.0",
2602
+ "@shikijs/types": "1.24.0",
2603
"@shikijs/vscode-textmate": "^9.3.0",
2604
"@types/hast": "^3.0.4",
2605
"hast-util-to-html": "^9.0.3"
2606
}
2607
},
2608
"node_modules/@shikijs/engine-javascript": {
2602
- "version": "1.23.1",
2603
- "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.23.1.tgz",
2604
- "integrity": "sha512-i/LdEwT5k3FVu07SiApRFwRcSJs5QM9+tod5vYCPig1Ywi8GR30zcujbxGQFJHwYD7A5BUqagi8o5KS+LEVgBg==",
2609
+ "version": "1.24.0",
2610
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.24.0.tgz",
2611
+ "integrity": "sha512-ZA6sCeSsF3Mnlxxr+4wGEJ9Tto4RHmfIS7ox8KIAbH0MTVUkw3roHPHZN+LlJMOHJJOVupe6tvuAzRpN8qK1vA==",
2612
"license": "MIT",
2613
"dependencies": {
2607
- "@shikijs/types": "1.23.1",
2614
+ "@shikijs/types": "1.24.0",
2615
"@shikijs/vscode-textmate": "^9.3.0",
2609
- "oniguruma-to-es": "0.4.1"
2616
+ "oniguruma-to-es": "0.7.0"
2617
}
2618
},
2619
"node_modules/@shikijs/engine-oniguruma": {
2613
- "version": "1.23.1",
2614
- "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.23.1.tgz",
2615
- "integrity": "sha512-KQ+lgeJJ5m2ISbUZudLR1qHeH3MnSs2mjFg7bnencgs5jDVPeJ2NVDJ3N5ZHbcTsOIh0qIueyAJnwg7lg7kwXQ==",
2620
+ "version": "1.24.0",
2621
+ "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.0.tgz",
2622
+ "integrity": "sha512-Eua0qNOL73Y82lGA4GF5P+G2+VXX9XnuUxkiUuwcxQPH4wom+tE39kZpBFXfUuwNYxHSkrSxpB1p4kyRW0moSg==",
2623
"license": "MIT",
2624
"dependencies": {
2618
- "@shikijs/types": "1.23.1",
2625
+ "@shikijs/types": "1.24.0",
2626
"@shikijs/vscode-textmate": "^9.3.0"
2627
}
2628
},
2629
"node_modules/@shikijs/markdown-it": {
2623
- "version": "1.23.1",
2624
- "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.23.1.tgz",
2625
- "integrity": "sha512-Odpj0AiQBe4v6D+XwAQkdErxncVnaBt+nZTc2JDrwWrOjvkM5JfRG55n9idTqGZfO0EMAZrhP7fmstNJ0yKmlg==",
2630
+ "version": "1.24.0",
2631
+ "resolved": "https://registry.npmjs.org/@shikijs/markdown-it/-/markdown-it-1.24.0.tgz",
2632
+ "integrity": "sha512-YjYg8jJoTO0cUXUNlFHTZWWFt4wSDOcRd2nM2aB1rnX5RqRlcqwfS2x1vQjlPqmUisv+/GHClvz7uKHeK7ZDBw==",
2633
"license": "MIT",
2634
"dependencies": {
2635
"markdown-it": "^14.1.0",
2629
- "shiki": "1.23.1"
2636
+ "shiki": "1.24.0"
2637
}
2638
},
2639
"node_modules/@shikijs/types": {
2633
- "version": "1.23.1",
2634
- "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.23.1.tgz",
2635
- "integrity": "sha512-98A5hGyEhzzAgQh2dAeHKrWW4HfCMeoFER2z16p5eJ+vmPeF6lZ/elEne6/UCU551F/WqkopqRsr1l2Yu6+A0g==",
2640
+ "version": "1.24.0",
2641
+ "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.24.0.tgz",
2642
+ "integrity": "sha512-aptbEuq1Pk88DMlCe+FzXNnBZ17LCiLIGWAeCWhoFDzia5Q5Krx3DgnULLiouSdd6+LUM39XwXGppqYE0Ghtug==",
2643
"license": "MIT",
2644
"dependencies": {
2645
"@shikijs/vscode-textmate": "^9.3.0",
@@ -2912,9 +2919,9 @@
2919
"license": "MIT"
2920
},
2921
"node_modules/@types/node": {
2915
- "version": "22.10.0",
2916
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz",
2917
- "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==",
2922
+ "version": "22.10.1",
2923
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz",
2924
+ "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==",
2925
"dev": true,
2926
"license": "MIT",
2927
"dependencies": {
@@ -3515,50 +3522,31 @@
3522
"license": "MIT"
3523
},
3524
"node_modules/@vue/devtools-core": {
3518
- "version": "7.6.5",
3519
- "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.6.5.tgz",
3520
- "integrity": "sha512-PKTEZVzY4Ef6G8LnbACKkPDOcdr2snFn3Xk8YqyFgugmogDrA3cyYVQ58CS0XTO9AYUXU9E5FFt5JJf22kXF2w==",
3525
+ "version": "7.6.7",
3526
+ "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.6.7.tgz",
3527
+ "integrity": "sha512-6fW8Q0H1NHDXdEcuV6dylT5U2Yxg3SdMnVCey99Y6S4R2PNgFL2vC+VU9U9rHIiaoEUkeza42S7FfHxV4VI3Jg==",
3528
"dev": true,
3529
"license": "MIT",
3530
"dependencies": {
3524
- "@vue/devtools-kit": "^7.6.5",
3525
- "@vue/devtools-shared": "^7.6.5",
3531
+ "@vue/devtools-kit": "^7.6.7",
3532
+ "@vue/devtools-shared": "^7.6.7",
3533
"mitt": "^3.0.1",
3527
- "nanoid": "^3.3.4",
3534
+ "nanoid": "^5.0.9",
3535
"pathe": "^1.1.2",
3529
- "vite-hot-client": "^0.2.3"
3536
+ "vite-hot-client": "^0.2.4"
3537
},
3538
"peerDependencies": {
3539
"vue": "^3.0.0"
3540
}
3541
},
3535
- "node_modules/@vue/devtools-core/node_modules/nanoid": {
3536
- "version": "3.3.8",
3537
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
3538
- "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
3539
- "dev": true,
3540
- "funding": [
3541
- {
3542
- "type": "github",
3543
- "url": "https://github.com/sponsors/ai"
3544
- }
3545
- ],
3546
- "license": "MIT",
3547
- "bin": {
3548
- "nanoid": "bin/nanoid.cjs"
3549
- },
3550
- "engines": {
3551
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
3552
- }
3553
- },
3542
"node_modules/@vue/devtools-kit": {
3555
- "version": "7.6.5",
3556
- "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.5.tgz",
3557
- "integrity": "sha512-fLQhUwmUbtEDHW1SEiHUF5k2Ptw816As5ZUVb/SzrqkrJzXI8xjEIo8suNBe/N+ewdz/9m5ayeFH8fmcVIbr4Q==",
3543
+ "version": "7.6.7",
3544
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.7.tgz",
3545
+ "integrity": "sha512-V8/jrXY/swHgnblABG9U4QCbE60c6RuPasmv2d9FvVqc5d94t1vDiESuvRmdNJBdWz4/D3q6ffgyAfRVjwHYEw==",
3546
"dev": true,
3547
"license": "MIT",
3548
"dependencies": {
3561
- "@vue/devtools-shared": "^7.6.5",
3549
+ "@vue/devtools-shared": "^7.6.7",
3550
"birpc": "^0.2.19",
3551
"hookable": "^5.5.3",
3552
"mitt": "^3.0.1",
@@ -3568,9 +3556,9 @@
3556
}
3557
},
3558
"node_modules/@vue/devtools-shared": {
3571
- "version": "7.6.5",
3572
- "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.5.tgz",
3573
- "integrity": "sha512-szsXQ0jlpjuFfmxb6F40qkSF4gtLC1W+dKRh/UiTulC+RekZsjqcN/qnVFkzqOO1YnzzShinZwfmv+MbfPJnpw==",
3559
+ "version": "7.6.7",
3560
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.7.tgz",
3561
+ "integrity": "sha512-QggO6SviAsolrePAXZ/sA1dSicSPt4TueZibCvydfhNDieL1lAuyMTgQDGst7TEvMGb4vgYv2I+1sDkO4jWNnw==",
3562
"dev": true,
3563
"license": "MIT",
3564
"dependencies": {
@@ -3664,14 +3652,14 @@
3652
}
3653
},
3654
"node_modules/@vue/tsconfig": {
3667
- "version": "0.6.0",
3668
- "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.6.0.tgz",
3669
- "integrity": "sha512-MHXNd6lzugsEHvuA6l1GqrF5jROqUon8sP/HInLPnthJiYvB0VvpHMywg7em1dBZfFZNBSkR68qH37zOdRHmCw==",
3655
+ "version": "0.7.0",
3656
+ "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.7.0.tgz",
3657
+ "integrity": "sha512-ku2uNz5MaZ9IerPPUyOHzyjhXoX2kVJaVf7hL315DC17vS6IiZRmmCPfggNbU16QTvM80+uYYy3eYJB59WCtvg==",
3658
"dev": true,
3659
"license": "MIT",
3660
"peerDependencies": {
3661
"typescript": "5.x",
3674
- "vue": "^3.3.0"
3662
+ "vue": "^3.4.0"
3663
},
3664
"peerDependenciesMeta": {
3665
"typescript": {
@@ -6067,9 +6055,9 @@
6055
}
6056
},
6057
"node_modules/eslint": {
6070
- "version": "9.15.0",
6071
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz",
6072
- "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==",
6058
+ "version": "9.16.0",
6059
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz",
6060
+ "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==",
6061
"dev": true,
6062
"license": "MIT",
6063
"dependencies": {
@@ -6078,7 +6066,7 @@
6066
"@eslint/config-array": "^0.19.0",
6067
"@eslint/core": "^0.9.0",
6068
"@eslint/eslintrc": "^3.2.0",
6081
- "@eslint/js": "9.15.0",
6069
+ "@eslint/js": "9.16.0",
6070
"@eslint/plugin-kit": "^0.2.3",
6071
"@humanfs/node": "^0.16.6",
6072
"@humanwhocodes/module-importer": "^1.0.1",
@@ -8347,6 +8335,19 @@
8335
"node": ">=8"
8336
}
8337
},
8338
+ "node_modules/is-plain-obj": {
8339
+ "version": "4.1.0",
8340
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
8341
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
8342
+ "dev": true,
8343
+ "license": "MIT",
8344
+ "engines": {
8345
+ "node": ">=12"
8346
+ },
8347
+ "funding": {
8348
+ "url": "https://github.com/sponsors/sindresorhus"
8349
+ }
8350
+ },
8351
"node_modules/is-potential-custom-element-name": {
8352
"version": "1.0.1",
8353
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
@@ -10915,14 +10916,14 @@
10916
}
10917
},
10918
"node_modules/oniguruma-to-es": {
10918
- "version": "0.4.1",
10919
- "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.4.1.tgz",
10920
- "integrity": "sha512-rNcEohFz095QKGRovP/yqPIKc+nP+Sjs4YTHMv33nMePGKrq/r2eu9Yh4646M5XluGJsUnmwoXuiXE69KDs+fQ==",
10919
+ "version": "0.7.0",
10920
+ "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.7.0.tgz",
10921
+ "integrity": "sha512-HRaRh09cE0gRS3+wi2zxekB+I5L8C/gN60S+vb11eADHUaB/q4u8wGGOX3GvwvitG8ixaeycZfeoyruKQzUgNg==",
10922
"license": "MIT",
10923
"dependencies": {
10924
"emoji-regex-xs": "^1.0.0",
10924
- "regex": "^5.0.0",
10925
- "regex-recursion": "^4.2.1"
10925
+ "regex": "^5.0.2",
10926
+ "regex-recursion": "^4.3.0"
10927
}
10928
},
10929
"node_modules/only": {
@@ -11100,6 +11101,19 @@
11101
"url": "https://github.com/sponsors/sindresorhus"
11102
}
11103
},
11104
+ "node_modules/parse-ms": {
11105
+ "version": "4.0.0",
11106
+ "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
11107
+ "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
11108
+ "dev": true,
11109
+ "license": "MIT",
11110
+ "engines": {
11111
+ "node": ">=18"
11112
+ },
11113
+ "funding": {
11114
+ "url": "https://github.com/sponsors/sindresorhus"
11115
+ }
11116
+ },
11117
"node_modules/parse-passwd": {
11118
"version": "1.0.0",
11119
"resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
@@ -11313,9 +11327,9 @@
11327
}
11328
},
11329
"node_modules/pinia": {
11316
- "version": "2.2.7",
11317
- "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.7.tgz",
11318
- "integrity": "sha512-M+X9Eh9V5De+8wyj0rD1cgB0zy1mPN/aBEpCI9y+DgVmzXV2dIwjYBluJ5cMQd/jAoHs0VW+EyUSHMZv/Wtcnw==",
11330
+ "version": "2.2.8",
11331
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.8.tgz",
11332
+ "integrity": "sha512-NRTYy2g+kju5tBRe0oNlriZIbMNvma8ZJrpHsp3qudyiMEA8jMmPPKQ2QMHg0Oc4BkUyQYWagACabrwriCK9HQ==",
11333
"license": "MIT",
11334
"dependencies": {
11335
"@vue/devtools-api": "^6.6.3",
@@ -11766,6 +11780,22 @@
11780
"url": "https://github.com/sponsors/sindresorhus"
11781
}
11782
},
11783
+ "node_modules/pretty-ms": {
11784
+ "version": "9.2.0",
11785
+ "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.2.0.tgz",
11786
+ "integrity": "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg==",
11787
+ "dev": true,
11788
+ "license": "MIT",
11789
+ "dependencies": {
11790
+ "parse-ms": "^4.0.0"
11791
+ },
11792
+ "engines": {
11793
+ "node": ">=18"
11794
+ },
11795
+ "funding": {
11796
+ "url": "https://github.com/sponsors/sindresorhus"
11797
+ }
11798
+ },
11799
"node_modules/process": {
11800
"version": "0.11.10",
11801
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@@ -12081,9 +12111,9 @@
12111
}
12112
},
12113
"node_modules/regex-recursion": {
12084
- "version": "4.2.1",
12085
- "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-4.2.1.tgz",
12086
- "integrity": "sha512-QHNZyZAeKdndD1G3bKAbBEKOSSK4KOHQrAJ01N1LJeb0SoH4DJIeFhp0uUpETgONifS4+P3sOgoA1dhzgrQvhA==",
12114
+ "version": "4.3.0",
12115
+ "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-4.3.0.tgz",
12116
+ "integrity": "sha512-5LcLnizwjcQ2ALfOj95MjcatxyqF5RPySx9yT+PaXu3Gox2vyAtLDjHB8NTJLtMGkvyau6nI3CfpwFCjPUIs/A==",
12117
"license": "MIT",
12118
"dependencies": {
12119
"regex-utilities": "^2.3.0"
@@ -12705,15 +12735,15 @@
12735
}
12736
},
12737
"node_modules/shiki": {
12708
- "version": "1.23.1",
12709
- "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.23.1.tgz",
12710
- "integrity": "sha512-8kxV9TH4pXgdKGxNOkrSMydn1Xf6It8lsle0fiqxf7a1149K1WGtdOu3Zb91T5r1JpvRPxqxU3C2XdZZXQnrig==",
12738
+ "version": "1.24.0",
12739
+ "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.24.0.tgz",
12740
+ "integrity": "sha512-qIneep7QRwxRd5oiHb8jaRzH15V/S8F3saCXOdjwRLgozZJr5x2yeBhQtqkO3FSzQDwYEFAYuifg4oHjpDghrg==",
12741
"license": "MIT",
12742
"dependencies": {
12713
- "@shikijs/core": "1.23.1",
12714
- "@shikijs/engine-javascript": "1.23.1",
12715
- "@shikijs/engine-oniguruma": "1.23.1",
12716
- "@shikijs/types": "1.23.1",
12743
+ "@shikijs/core": "1.24.0",
12744
+ "@shikijs/engine-javascript": "1.24.0",
12745
+ "@shikijs/engine-oniguruma": "1.24.0",
12746
+ "@shikijs/types": "1.24.0",
12747
"@shikijs/vscode-textmate": "^9.3.0",
12748
"@types/hast": "^3.0.4"
12749
}
@@ -14557,19 +14587,19 @@
14587
}
14588
},
14589
"node_modules/vite-plugin-vue-devtools": {
14560
- "version": "7.6.5",
14561
- "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.6.5.tgz",
14562
- "integrity": "sha512-5ISMSoLMrOl/77suAC3DigbuI4oSsWW7fgwdAoKbKvtY6+L3Jv51mjCnirzRog2uP0K59iIXwHHtORUg1aBQ2A==",
14590
+ "version": "7.6.7",
14591
+ "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.6.7.tgz",
14592
+ "integrity": "sha512-H1ZyjtpWjP5mHA5R15sQeYgAARuh2Myg3TDFXWZK6QOQRy8s3XjTIt319DogVjU/x3rC3L/jJQjIasRU04mWXA==",
14593
"dev": true,
14594
"license": "MIT",
14595
"dependencies": {
14566
- "@vue/devtools-core": "^7.6.5",
14567
- "@vue/devtools-kit": "^7.6.5",
14568
- "@vue/devtools-shared": "^7.6.5",
14569
- "execa": "^8.0.1",
14596
+ "@vue/devtools-core": "^7.6.7",
14597
+ "@vue/devtools-kit": "^7.6.7",
14598
+ "@vue/devtools-shared": "^7.6.7",
14599
+ "execa": "^9.5.1",
14600
"sirv": "^3.0.0",
14571
- "vite-plugin-inspect": "^0.8.8",
14572
- "vite-plugin-vue-inspector": "^5.3.0"
14601
+ "vite-plugin-inspect": "0.8.8",
14602
+ "vite-plugin-vue-inspector": "^5.3.1"
14603
},
14604
"engines": {
14605
"node": ">=v14.21.3"
@@ -14578,106 +14608,127 @@
14608
"vite": "^3.1.0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0"
14609
}
14610
},
14611
+ "node_modules/vite-plugin-vue-devtools/node_modules/@sindresorhus/merge-streams": {
14612
+ "version": "4.0.0",
14613
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz",
14614
+ "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==",
14615
+ "dev": true,
14616
+ "license": "MIT",
14617
+ "engines": {
14618
+ "node": ">=18"
14619
+ },
14620
+ "funding": {
14621
+ "url": "https://github.com/sponsors/sindresorhus"
14622
+ }
14623
+ },
14624
"node_modules/vite-plugin-vue-devtools/node_modules/execa": {
14582
- "version": "8.0.1",
14583
- "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
14584
- "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
14625
+ "version": "9.5.1",
14626
+ "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.1.tgz",
14627
+ "integrity": "sha512-QY5PPtSonnGwhhHDNI7+3RvY285c7iuJFFB+lU+oEzMY/gEGJ808owqJsrr8Otd1E/x07po1LkUBmdAc5duPAg==",
14628
"dev": true,
14629
"license": "MIT",
14630
"dependencies": {
14631
+ "@sindresorhus/merge-streams": "^4.0.0",
14632
"cross-spawn": "^7.0.3",
14589
- "get-stream": "^8.0.1",
14590
- "human-signals": "^5.0.0",
14591
- "is-stream": "^3.0.0",
14592
- "merge-stream": "^2.0.0",
14593
- "npm-run-path": "^5.1.0",
14594
- "onetime": "^6.0.0",
14633
+ "figures": "^6.1.0",
14634
+ "get-stream": "^9.0.0",
14635
+ "human-signals": "^8.0.0",
14636
+ "is-plain-obj": "^4.1.0",
14637
+ "is-stream": "^4.0.1",
14638
+ "npm-run-path": "^6.0.0",
14639
+ "pretty-ms": "^9.0.0",
14640
"signal-exit": "^4.1.0",
14596
- "strip-final-newline": "^3.0.0"
14641
+ "strip-final-newline": "^4.0.0",
14642
+ "yoctocolors": "^2.0.0"
14643
},
14644
"engines": {
14599
- "node": ">=16.17"
14645
+ "node": "^18.19.0 || >=20.5.0"
14646
},
14647
"funding": {
14648
"url": "https://github.com/sindresorhus/execa?sponsor=1"
14649
}
14650
},
14651
+ "node_modules/vite-plugin-vue-devtools/node_modules/figures": {
14652
+ "version": "6.1.0",
14653
+ "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
14654
+ "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
14655
+ "dev": true,
14656
+ "license": "MIT",
14657
+ "dependencies": {
14658
+ "is-unicode-supported": "^2.0.0"
14659
+ },
14660
+ "engines": {
14661
+ "node": ">=18"
14662
+ },
14663
+ "funding": {
14664
+ "url": "https://github.com/sponsors/sindresorhus"
14665
+ }
14666
+ },
14667
"node_modules/vite-plugin-vue-devtools/node_modules/get-stream": {
14606
- "version": "8.0.1",
14607
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
14608
- "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
14668
+ "version": "9.0.1",
14669
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
14670
+ "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==",
14671
"dev": true,
14672
"license": "MIT",
14673
+ "dependencies": {
14674
+ "@sec-ant/readable-stream": "^0.4.1",
14675
+ "is-stream": "^4.0.1"
14676
+ },
14677
"engines": {
14612
- "node": ">=16"
14678
+ "node": ">=18"
14679
},
14680
"funding": {
14681
"url": "https://github.com/sponsors/sindresorhus"
14682
}
14683
},
14684
"node_modules/vite-plugin-vue-devtools/node_modules/human-signals": {
14619
- "version": "5.0.0",
14620
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
14621
- "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
14685
+ "version": "8.0.0",
14686
+ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz",
14687
+ "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==",
14688
"dev": true,
14689
"license": "Apache-2.0",
14690
"engines": {
14625
- "node": ">=16.17.0"
14691
+ "node": ">=18.18.0"
14692
}
14693
},
14694
"node_modules/vite-plugin-vue-devtools/node_modules/is-stream": {
14629
- "version": "3.0.0",
14630
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
14631
- "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
14695
+ "version": "4.0.1",
14696
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz",
14697
+ "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==",
14698
"dev": true,
14699
"license": "MIT",
14700
"engines": {
14635
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
14701
+ "node": ">=18"
14702
},
14703
"funding": {
14704
"url": "https://github.com/sponsors/sindresorhus"
14705
}
14706
},
14641
- "node_modules/vite-plugin-vue-devtools/node_modules/mimic-fn": {
14642
- "version": "4.0.0",
14643
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
14644
- "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
14707
+ "node_modules/vite-plugin-vue-devtools/node_modules/is-unicode-supported": {
14708
+ "version": "2.1.0",
14709
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
14710
+ "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
14711
"dev": true,
14712
"license": "MIT",
14713
"engines": {
14648
- "node": ">=12"
14714
+ "node": ">=18"
14715
},
14716
"funding": {
14717
"url": "https://github.com/sponsors/sindresorhus"
14718
}
14719
},
14720
"node_modules/vite-plugin-vue-devtools/node_modules/npm-run-path": {
14655
- "version": "5.3.0",
14656
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
14657
- "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
14658
- "dev": true,
14659
- "license": "MIT",
14660
- "dependencies": {
14661
- "path-key": "^4.0.0"
14662
- },
14663
- "engines": {
14664
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
14665
- },
14666
- "funding": {
14667
- "url": "https://github.com/sponsors/sindresorhus"
14668
- }
14669
- },
14670
- "node_modules/vite-plugin-vue-devtools/node_modules/onetime": {
14721
"version": "6.0.0",
14672
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
14673
- "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
14722
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz",
14723
+ "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==",
14724
"dev": true,
14725
"license": "MIT",
14726
"dependencies": {
14677
- "mimic-fn": "^4.0.0"
14727
+ "path-key": "^4.0.0",
14728
+ "unicorn-magic": "^0.3.0"
14729
},
14730
"engines": {
14680
- "node": ">=12"
14731
+ "node": ">=18"
14732
},
14733
"funding": {
14734
"url": "https://github.com/sponsors/sindresorhus"
@@ -14710,22 +14761,35 @@
14761
}
14762
},
14763
"node_modules/vite-plugin-vue-devtools/node_modules/strip-final-newline": {
14713
- "version": "3.0.0",
14714
- "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
14715
- "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
14764
+ "version": "4.0.0",
14765
+ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
14766
+ "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==",
14767
"dev": true,
14768
"license": "MIT",
14769
"engines": {
14719
- "node": ">=12"
14770
+ "node": ">=18"
14771
+ },
14772
+ "funding": {
14773
+ "url": "https://github.com/sponsors/sindresorhus"
14774
+ }
14775
+ },
14776
+ "node_modules/vite-plugin-vue-devtools/node_modules/unicorn-magic": {
14777
+ "version": "0.3.0",
14778
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
14779
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
14780
+ "dev": true,
14781
+ "license": "MIT",
14782
+ "engines": {
14783
+ "node": ">=18"
14784
},
14785
"funding": {
14786
"url": "https://github.com/sponsors/sindresorhus"
14787
}
14788
},
14789
"node_modules/vite-plugin-vue-inspector": {
14726
- "version": "5.3.0",
14727
- "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.0.tgz",
14728
- "integrity": "sha512-F6JNRUOrZl8FaUCTxPhsOLn2ka7N7Sz9ppxmmEwpybVBDYnhelbNnnlZpeFPc4ULnxbitSi8b0V2C0KT3CjReg==",
14790
+ "version": "5.3.1",
14791
+ "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.3.1.tgz",
14792
+ "integrity": "sha512-cBk172kZKTdvGpJuzCCLg8lJ909wopwsu3Ve9FsL1XsnLBiRT9U3MePcqrgGHgCX2ZgkqZmAGR8taxw+TV6s7A==",
14793
"dev": true,
14794
"license": "MIT",
14795
"dependencies": {
@@ -14740,7 +14804,7 @@
14804
"magic-string": "^0.30.4"
14805
},
14806
"peerDependencies": {
14743
- "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0"
14807
+ "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0"
14808
}
14809
},
14810
"node_modules/vite-svg-loader": {
@@ -15403,13 +15467,13 @@
15467
}
15468
},
15469
"node_modules/vue-i18n": {
15406
- "version": "10.0.4",
15407
- "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.4.tgz",
15408
- "integrity": "sha512-1xkzVxqBLk2ZFOmeI+B5r1J7aD/WtNJ4j9k2mcFcQo5BnOmHBmD7z4/oZohh96AAaRZ4Q7mNQvxc9h+aT+Md3w==",
15470
+ "version": "10.0.5",
15471
+ "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.5.tgz",
15472
+ "integrity": "sha512-9/gmDlCblz3i8ypu/afiIc/SUIfTTE1mr0mZhb9pk70xo2csHAM9mp2gdQ3KD2O0AM3Hz/5ypb+FycTj/lHlPQ==",
15473
"license": "MIT",
15474
"dependencies": {
15411
- "@intlify/core-base": "10.0.4",
15412
- "@intlify/shared": "10.0.4",
15475
+ "@intlify/core-base": "10.0.5",
15476
+ "@intlify/shared": "10.0.5",
15477
"@vue/devtools-api": "^6.5.0"
15478
},
15479
"engines": {
@@ -15930,6 +15994,19 @@
15994
"url": "https://github.com/sponsors/sindresorhus"
15995
}
15996
},
15997
+ "node_modules/yoctocolors": {
15998
+ "version": "2.1.1",
15999
+ "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz",
16000
+ "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==",
16001
+ "dev": true,
16002
+ "license": "MIT",
16003
+ "engines": {
16004
+ "node": ">=18"
16005
+ },
16006
+ "funding": {
16007
+ "url": "https://github.com/sponsors/sindresorhus"
16008
+ }
16009
+ },
16010
"node_modules/zrender": {
16011
"version": "5.6.0",
16012
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.0.tgz",
frontend/package.json
+11
-11
@@ -41,7 +41,7 @@
41
"@fontsource/jetbrains-mono": "^5.1.1",
42
"@fontsource/lexend": "^5.1.1",
43
"@fontsource/public-sans": "^5.1.1",
44
- "@shikijs/markdown-it": "^1.23.1",
44
+ "@shikijs/markdown-it": "^1.24.0",
45
"@tailwindcss/container-queries": "^0.1.1",
46
"@vueuse/core": "^12.0.0",
47
"axios": "^1.7.8",
@@ -59,15 +59,15 @@
59
"naive-ui": "^2.40.2",
60
"nanoid": "^5.0.9",
61
"password-validator": "^5.3.0",
62
- "pinia": "^2.2.7",
62
+ "pinia": "^2.2.8",
63
"pinia-plugin-persistedstate": "^4.1.3",
64
"secure-ls": "^2.0.0",
65
- "shiki": "^1.23.1",
65
+ "shiki": "^1.24.0",
66
"validator": "^13.12.0",
67
"vue": "^3.5.13",
68
"vue-advanced-cropper": "^2.8.9",
69
"vue-highlight-words": "^3.0.1",
70
- "vue-i18n": "^10.0.4",
70
+ "vue-i18n": "^10.0.5",
71
"vue-router": "^4.5.0",
72
"vue-sjv": "^0.0.6",
73
"vue3-apexcharts": "^1.8.0",
@@ -78,7 +78,7 @@
78
"@rollup/rollup-linux-x64-gnu": "^4.27.4"
79
},
80
"devDependencies": {
81
- "@antfu/eslint-config": "^3.11.0",
81
+ "@antfu/eslint-config": "^3.11.2",
82
"@clack/prompts": "^0.8.2",
83
"@iconify/vue": "^4.1.2",
84
"@tsconfig/node20": "^20.1.4",
@@ -87,16 +87,16 @@
87
"@types/fs-extra": "^11.0.4",
88
"@types/jsdom": "^21.1.7",
89
"@types/lodash": "^4.17.13",
90
- "@types/node": "^22.10.0",
90
+ "@types/node": "^22.10.1",
91
"@types/validator": "^13.12.2",
92
"@vitejs/plugin-vue": "^5.2.1",
93
"@vitejs/plugin-vue-jsx": "^4.1.1",
94
"@vue/test-utils": "^2.4.6",
95
- "@vue/tsconfig": "^0.6.0",
95
+ "@vue/tsconfig": "^0.7.0",
96
"autoprefixer": "^10.4.20",
97
"cypress": "^13.16.0",
98
"depcheck": "^1.4.7",
99
- "eslint": "^9.15.0",
99
+ "eslint": "^9.16.0",
100
"flourite": "^1.3.0",
101
"fs-extra": "^11.2.0",
102
"jsdom": "^25.0.1",
@@ -114,7 +114,7 @@
114
"unplugin-vue-components": "^0.27.5",
115
"vite": "^5.4.11",
116
"vite-bundle-visualizer": "^1.2.1",
117
- "vite-plugin-vue-devtools": "^7.6.5",
117
+ "vite-plugin-vue-devtools": "^7.6.7",
118
"vite-svg-loader": "^5.1.0",
119
"vitest": "^2.1.6",
120
"vue-tsc": "^2.1.10"
@@ -130,10 +130,10 @@
130
},
131
"overrides": {
132
"@typescript-eslint/eslint-plugin": {
133
- "eslint": "^9.15.0"
133
+ "eslint": "^9.16.0"
134
},
135
"@typescript-eslint/parser": {
136
- "eslint": "^9.15.0"
136
+ "eslint": "^9.16.0"
137
},
138
"@typescript-eslint/typescript-estree": "^8.16.0"
139
}
frontend/src/api/endpoints/threatIntel.ts
+7
-1
@@ -4,7 +4,8 @@ import type {
4
AiWazuhExclusionRuleResponse,
5
EpssScore,
6
EvaluationData,
7
- ThreatIntelResponse
7
+ ThreatIntelResponse,
8
+ VirusTotalResponse
9
} from "@/types/threatIntel.d"
10
import { HttpClient } from "../httpClient"
11
@@ -44,5 +45,10 @@ export default {
45
index_id: indexId
46
}
47
)
48
+ },
49
+ virusTotalEnrichment(iocValue: string) {
50
+ return HttpClient.post<FlaskBaseResponse & VirusTotalResponse>(`/threat_intel/virustotal`, {
51
+ ioc_value: iocValue
52
+ })
53
}
54
}
frontend/src/assets/scss/index.scss
+1
-1
@@ -18,7 +18,7 @@ body {
18
-moz-osx-font-smoothing: grayscale;
19
text-rendering: optimizeLegibility;
20
line-height: 1.35;
21
- font-size: 15px;
21
+ font-size: 16px;
22
text-wrap: pretty;
23
24
margin: 0;
frontend/src/components/agents/vulnerabilities/VulnerabilityCard.vue
+5
-1
@@ -86,7 +86,7 @@
86
<n-tab-pane name="External references" tab="External references" display-directive="show">
87
<div class="p-7 pt-2">
88
<ul>
89
- <li v-for="refItem of vulnerability.external_references" :key="refItem">
89
+ <li v-for="refItem of externalReferences" :key="refItem">
90
<a :href="refItem" target="_blank">{{ refItem }}</a>
91
</li>
92
</ul>
@@ -109,6 +109,7 @@ import Icon from "@/components/common/Icon.vue"
109
import { useSettingsStore } from "@/stores/settings"
110
import dayjs from "@/utils/dayjs"
111
import { cloneDeep } from "lodash"
112
+import _split from "lodash/split"
113
import _truncate from "lodash/truncate"
114
import { NModal, NTabPane, NTabs, NTooltip } from "naive-ui"
115
import { computed, defineAsyncComponent, ref, toRefs } from "vue"
@@ -129,6 +130,9 @@ const dFormats = useSettingsStore().dateFormat
130
131
const title = computed(() => _truncate(vulnerability.value.title, { length: 20 }))
132
const extract = computed(() => _truncate(vulnerability.value.title, { length: 50 }))
133
+const externalReferences = computed(() =>
134
+ (vulnerability.value.external_references || []).map(ref => _split(ref, ",")).flat()
135
+)
136
137
const vulnerabilitySanitized = computed(() => {
138
const newObj = []
frontend/src/components/common/cards/CardKV.vue
+3
-2
@@ -3,16 +3,17 @@
3
<div v-if="$slots.key" class="key">
4
<slot name="key"></slot>
5
</div>
6
- <div v-if="$slots.value" class="value">
6
+ <div v-if="$slots.value" class="value" :class="valueClass">
7
<slot name="value"></slot>
8
</div>
9
</div>
10
</template>
11
12
<script setup lang="ts">
13
-const { color } = defineProps<{
13
+const { color, size, valueClass } = defineProps<{
14
color?: "danger" | "warning" | "success" | "primary"
15
size?: "lg"
16
+ valueClass?: string
17
}>()
18
</script>
19
frontend/src/components/common/cards/CardStatsBars.vue
+18
-8
@@ -24,7 +24,7 @@
24
</div>
25
<div class="list flex flex-col">
26
<div
27
- v-for="item of sanitizedValues"
27
+ v-for="item of listValues"
28
:key="JSON.stringify(item)"
29
class="item flex items-center gap-3"
30
:class="item.status"
@@ -49,7 +49,7 @@
49
import Icon from "@/components/common/Icon.vue"
50
import _round from "lodash/round"
51
import { NCard } from "naive-ui"
52
-import { computed, toRefs } from "vue"
52
+import { computed } from "vue"
53
54
export interface ItemProps {
55
value: number
@@ -62,19 +62,25 @@ interface ItemPropsExt extends ItemProps {
62
percentage: number
63
}
64
65
-const props = defineProps<{
65
+const {
66
+ title,
67
+ values,
68
+ showTotal = true,
69
+ showZeroItems,
70
+ hovered
71
+} = defineProps<{
72
title: string
73
values: ItemProps[]
74
+ showTotal?: boolean
75
showZeroItems?: boolean
76
hovered?: boolean
77
}>()
71
-const { title, values, showZeroItems, hovered } = toRefs(props)
78
79
const ArrowRightIcon = "carbon:arrow-right"
80
const totItem = computed(
81
() =>
76
- values.value.find(item => item.isTotal) || {
77
- value: values.value.reduce((acc, cur) => {
82
+ values.find(item => item.isTotal) || {
83
+ value: values.reduce((acc, cur) => {
84
return acc + cur.value
85
}, 0),
86
isTotal: true,
@@ -83,14 +89,18 @@ const totItem = computed(
89
)
90
91
const sanitizedValues = computed<ItemPropsExt[]>(() => {
86
- const list: ItemPropsExt[] = values.value
92
+ const list: ItemPropsExt[] = values
93
.filter(o => !o.isTotal)
94
.map(o => ({ ...o, percentage: _round((o.value / totItem.value.value) * 100, 2) }))
89
- .filter(o => o.value || (!o.value && showZeroItems.value))
95
+ .filter(o => o.value || (!o.value && showZeroItems))
96
97
return [{ ...totItem.value, percentage: 100 }, ...list]
98
})
99
100
+const listValues = computed<ItemPropsExt[]>(() =>
101
+ sanitizedValues.value.filter(o => !o.isTotal || (showTotal && o.isTotal))
102
+)
103
+
104
const barValues = computed<ItemPropsExt[]>(() => sanitizedValues.value.filter(o => !o.isTotal && o.percentage))
105
</script>
106
frontend/src/components/incidentManagement/alerts/AlertIoCItem.vue
+19
-14
@@ -8,20 +8,24 @@
8
<p class="mt-2">{{ ioc.description }}</p>
9
</template>
10
<template #footerExtra>
11
- <n-popconfirm
12
- v-model:show="showDeleteConfirm"
13
- trigger="manual"
14
- to="body"
15
- @positive-click="deleteIoc()"
16
- @clickoutside="showDeleteConfirm = false"
17
- >
18
- <template #trigger>
19
- <n-button quaternary size="tiny" :loading="canceling" @click.stop="showDeleteConfirm = true">
20
- Delete
21
- </n-button>
22
- </template>
23
- Are you sure you want to delete this IoC?
24
- </n-popconfirm>
11
+ <div class="flex items-center justify-end gap-3">
12
+ <VirusTotalEnrichmentButton :ioc-value="ioc.value" />
13
+
14
+ <n-popconfirm
15
+ v-model:show="showDeleteConfirm"
16
+ trigger="manual"
17
+ to="body"
18
+ @positive-click="deleteIoc()"
19
+ @clickoutside="showDeleteConfirm = false"
20
+ >
21
+ <template #trigger>
22
+ <n-button quaternary size="tiny" :loading="canceling" @click.stop="showDeleteConfirm = true">
23
+ Delete
24
+ </n-button>
25
+ </template>
26
+ Are you sure you want to delete this IoC?
27
+ </n-popconfirm>
28
+ </div>
29
</template>
30
</CardEntity>
31
</template>
@@ -30,6 +34,7 @@
34
import type { AlertIOC } from "@/types/incidentManagement/alerts"
35
import Api from "@/api"
36
import CardEntity from "@/components/common/cards/CardEntity.vue"
37
+import VirusTotalEnrichmentButton from "@/components/threatIntel/VirusTotalEnrichmentButton.vue"
38
import { NButton, NPopconfirm, useMessage } from "naive-ui"
39
import { ref } from "vue"
40
frontend/src/components/threatIntel/VirusTotalEnrichmentButton.vue
new
+641
@@ -0,0 +1,641 @@
1
+<template>
2
+ <div>
3
+ <n-button :size="size || 'small'" ghost type="primary" :loading @click="analysis()">
4
+ <template #icon>
5
+ <Icon :name="AiIcon" />
6
+ </template>
7
+ <div class="flex items-center gap-2">
8
+ <span>Enrich with VirusTotal</span>
9
+ </div>
10
+ </n-button>
11
+
12
+ <n-modal
13
+ v-model:show="showModal"
14
+ preset="card"
15
+ content-class="!p-0"
16
+ :style="{ maxWidth: 'min(710px, 90vw)', minHeight: 'min(500px, 90vh)' }"
17
+ :bordered="false"
18
+ segmented
19
+ title="Virus Total Enrichment"
20
+ >
21
+ <n-tabs v-if="virusTotalDataResponse" type="line" animated :tabs-padding="24">
22
+ <n-tab-pane name="Overview" tab="Overview" display-directive="show">
23
+ <div class="flex flex-col gap-4 p-7 pt-2">
24
+ <div class="grid-auto-fit-200 grid gap-4">
25
+ <CardKV>
26
+ <template #key>type</template>
27
+ <template #value>{{ virusTotalDataResponse.type }}</template>
28
+ </CardKV>
29
+ <CardKV>
30
+ <template #key>id</template>
31
+ <template #value>{{ virusTotalDataResponse.id }}</template>
32
+ </CardKV>
33
+ </div>
34
+ <CardStatsBars
35
+ title="Total votes"
36
+ class="h-full cursor-pointer"
37
+ :values="totalVotes"
38
+ :show-total="false"
39
+ ></CardStatsBars>
40
+ <CardStatsBars
41
+ title="Last Analysis Stats"
42
+ class="h-full cursor-pointer"
43
+ :values="lastAnalysisStats"
44
+ :show-total="false"
45
+ show-zero-items
46
+ ></CardStatsBars>
47
+ <div
48
+ v-if="virusTotalDataResponse.attributes?.tags?.length"
49
+ class="text-secondary flex flex-wrap gap-4 text-sm"
50
+ >
51
+ <span v-for="tag of virusTotalDataResponse.attributes.tags" :key="tag">#{{ tag }}</span>
52
+ </div>
53
+ <ul v-if="Object.keys(virusTotalDataResponse.links || {}).length">
54
+ <li v-for="link of virusTotalDataResponse.links" :key="link">
55
+ <a :href="link" target="_blank">{{ link }}</a>
56
+ </li>
57
+ </ul>
58
+ </div>
59
+ </n-tab-pane>
60
+ <n-tab-pane name="Analysis Results" tab="Analysis Results" display-directive="show">
61
+ <div class="p-7 pt-2">
62
+ <div
63
+ v-if="Object.keys(virusTotalDataResponse.attributes?.last_analysis_results || {}).length"
64
+ class="flex flex-col gap-3"
65
+ >
66
+ <div
67
+ v-if="virusTotalDataResponse.attributes.last_analysis_date"
68
+ class="text-right font-mono text-sm"
69
+ >
70
+ last analysis date:
71
+ <code>
72
+ {{
73
+ formatDate(
74
+ virusTotalDataResponse.attributes.last_analysis_date,
75
+ dFormats.datetime
76
+ )
77
+ }}
78
+ </code>
79
+ </div>
80
+ <CardEntity
81
+ v-for="(value, key) of virusTotalDataResponse.attributes.last_analysis_results"
82
+ :key
83
+ embedded
84
+ class="@container"
85
+ >
86
+ <template #header>{{ key }}</template>
87
+ <template #footer>
88
+ <div class="@xs:!flex-row flex flex-col justify-between gap-4 text-sm">
89
+ <n-statistic class="grow">
90
+ <template #label>
91
+ <span class="text-sm">method</span>
92
+ </template>
93
+ <span class="text-base">{{ value.method }}</span>
94
+ </n-statistic>
95
+ <n-statistic class="grow">
96
+ <template #label>
97
+ <span class="text-sm">category</span>
98
+ </template>
99
+ <span class="text-base">{{ value.category }}</span>
100
+ </n-statistic>
101
+ <n-statistic class="grow">
102
+ <template #label>
103
+ <span class="text-sm">result</span>
104
+ </template>
105
+ <span class="text-base">{{ value.result }}</span>
106
+ </n-statistic>
107
+ </div>
108
+ </template>
109
+ </CardEntity>
110
+ </div>
111
+ <n-empty v-else description="No items found" class="h-48 justify-center" />
112
+ </div>
113
+ </n-tab-pane>
114
+ <n-tab-pane name="Details" tab="Details" display-directive="show">
115
+ <div class="p-7 pt-4">
116
+ <CodeSource :code="properties" :decode="true" />
117
+ </div>
118
+ </n-tab-pane>
119
+ <n-tab-pane
120
+ v-if="virusTotalDataResponse.attributes.whois"
121
+ name="Whois"
122
+ tab="Whois"
123
+ display-directive="show"
124
+ >
125
+ <div class="p-7 pt-4">
126
+ <div
127
+ v-if="virusTotalDataResponse.attributes.whois_date"
128
+ class="mb-4 text-right font-mono text-sm"
129
+ >
130
+ whois date:
131
+ <code>
132
+ {{ formatDate(virusTotalDataResponse.attributes.whois_date, dFormats.datetime) }}
133
+ </code>
134
+ </div>
135
+ <n-input
136
+ :value="virusTotalDataResponse.attributes.whois"
137
+ type="textarea"
138
+ readonly
139
+ placeholder="Empty"
140
+ size="large"
141
+ :autosize="{
142
+ minRows: 3,
143
+ maxRows: 18
144
+ }"
145
+ />
146
+ </div>
147
+ </n-tab-pane>
148
+ <n-tab-pane name="HTTPS Certificate" tab="HTTPS Certificate" display-directive="show">
149
+ <div class="flex flex-col gap-3 p-7 pt-2">
150
+ <div
151
+ v-if="virusTotalDataResponse.attributes.last_https_certificate_date"
152
+ class="text-right font-mono text-sm"
153
+ >
154
+ last https certificate date:
155
+ <code>
156
+ {{
157
+ formatDate(
158
+ virusTotalDataResponse.attributes.last_https_certificate_date,
159
+ dFormats.datetime
160
+ )
161
+ }}
162
+ </code>
163
+ </div>
164
+
165
+ <CardKV value-class="!p-0 overflow-x-auto overflow-y-hidden">
166
+ <template #key>cert_signature</template>
167
+ <template #value>
168
+ <n-table
169
+ v-if="
170
+ Object.keys(
171
+ virusTotalDataResponse.attributes?.last_https_certificate.cert_signature
172
+ ).length
173
+ "
174
+ :bordered="false"
175
+ single-line
176
+ >
177
+ <tbody>
178
+ <tr
179
+ v-for="(value, key) of virusTotalDataResponse.attributes
180
+ .last_https_certificate.cert_signature"
181
+ :key
182
+ >
183
+ <td class="whitespace-nowrap text-sm">{{ key }}</td>
184
+ <td class="whitespace-nowrap text-sm font-semibold">{{ value }}</td>
185
+ </tr>
186
+ </tbody>
187
+ </n-table>
188
+ </template>
189
+ </CardKV>
190
+
191
+ <CardKV value-class="!p-0 overflow-x-auto overflow-y-hidden">
192
+ <template #key>authority_key_identifier</template>
193
+ <template #value>
194
+ <n-table
195
+ v-if="
196
+ Object.keys(
197
+ virusTotalDataResponse.attributes?.last_https_certificate?.extensions
198
+ ?.authority_key_identifier || {}
199
+ ).length
200
+ "
201
+ :bordered="false"
202
+ single-line
203
+ >
204
+ <tbody>
205
+ <tr
206
+ v-for="(value, key) of virusTotalDataResponse.attributes
207
+ .last_https_certificate.extensions.authority_key_identifier"
208
+ :key
209
+ >
210
+ <td class="whitespace-nowrap text-sm">{{ key }}</td>
211
+ <td class="whitespace-nowrap text-sm font-semibold">{{ value }}</td>
212
+ </tr>
213
+ </tbody>
214
+ </n-table>
215
+ </template>
216
+ </CardKV>
217
+
218
+ <CardKV>
219
+ <template #key>subject_key_identifier</template>
220
+ <template #value>
221
+ {{
222
+ virusTotalDataResponse.attributes.last_https_certificate?.extensions
223
+ ?.subject_key_identifier || "-"
224
+ }}
225
+ </template>
226
+ </CardKV>
227
+
228
+ <CardKV>
229
+ <template #key>subject_alternative_name</template>
230
+ <template #value>
231
+ <span
232
+ v-if="
233
+ !virusTotalDataResponse.attributes.last_https_certificate?.extensions
234
+ ?.subject_alternative_name?.length
235
+ "
236
+ >
237
+ -
238
+ </span>
239
+ <div v-else class="flex flex-wrap gap-2">
240
+ <Badge
241
+ v-for="value of virusTotalDataResponse.attributes.last_https_certificate
242
+ ?.extensions?.subject_alternative_name"
243
+ :key="value"
244
+ >
245
+ <template #value>
246
+ {{ value }}
247
+ </template>
248
+ </Badge>
249
+ </div>
250
+ </template>
251
+ </CardKV>
252
+
253
+ <CardKV>
254
+ <template #key>certificate_policies</template>
255
+ <template #value>
256
+ <span
257
+ v-if="
258
+ !virusTotalDataResponse.attributes.last_https_certificate?.extensions
259
+ ?.certificate_policies?.length
260
+ "
261
+ >
262
+ -
263
+ </span>
264
+ <div v-else class="flex flex-wrap gap-2">
265
+ <Badge
266
+ v-for="value of virusTotalDataResponse.attributes.last_https_certificate
267
+ ?.extensions?.certificate_policies"
268
+ :key="value"
269
+ >
270
+ <template #value>
271
+ {{ value }}
272
+ </template>
273
+ </Badge>
274
+ </div>
275
+ </template>
276
+ </CardKV>
277
+
278
+ <CardKV>
279
+ <template #key>key_usage</template>
280
+ <template #value>
281
+ <span
282
+ v-if="
283
+ !virusTotalDataResponse.attributes.last_https_certificate?.extensions?.key_usage
284
+ ?.length
285
+ "
286
+ >
287
+ -
288
+ </span>
289
+ <div v-else class="flex flex-wrap gap-2">
290
+ <Badge
291
+ v-for="value of virusTotalDataResponse.attributes.last_https_certificate
292
+ ?.extensions?.key_usage"
293
+ :key="value"
294
+ >
295
+ <template #value>
296
+ {{ value }}
297
+ </template>
298
+ </Badge>
299
+ </div>
300
+ </template>
301
+ </CardKV>
302
+
303
+ <CardKV>
304
+ <template #key>extended_key_usage</template>
305
+ <template #value>
306
+ <span
307
+ v-if="
308
+ !virusTotalDataResponse.attributes.last_https_certificate?.extensions
309
+ ?.extended_key_usage?.length
310
+ "
311
+ >
312
+ -
313
+ </span>
314
+ <div v-else class="flex flex-wrap gap-2">
315
+ <Badge
316
+ v-for="value of virusTotalDataResponse.attributes.last_https_certificate
317
+ ?.extensions?.extended_key_usage"
318
+ :key="value"
319
+ >
320
+ <template #value>
321
+ {{ value }}
322
+ </template>
323
+ </Badge>
324
+ </div>
325
+ </template>
326
+ </CardKV>
327
+
328
+ <CardKV>
329
+ <template #key>crl_distribution_points</template>
330
+ <template #value>
331
+ <span
332
+ v-if="
333
+ !virusTotalDataResponse.attributes.last_https_certificate?.extensions
334
+ ?.crl_distribution_points?.length
335
+ "
336
+ >
337
+ -
338
+ </span>
339
+ <ul v-else class="flex flex-wrap gap-2">
340
+ <li
341
+ v-for="value of virusTotalDataResponse.attributes.last_https_certificate
342
+ ?.extensions?.crl_distribution_points"
343
+ :key="value"
344
+ >
345
+ <a :href="value" target="_blank" rel="nofollow noopener noreferrer">
346
+ {{ value }}
347
+ </a>
348
+ </li>
349
+ </ul>
350
+ </template>
351
+ </CardKV>
352
+
353
+ <CardKV value-class="!p-0 overflow-x-auto overflow-y-hidden">
354
+ <template #key>ca_information_access</template>
355
+ <template #value>
356
+ <n-table
357
+ v-if="
358
+ Object.keys(
359
+ virusTotalDataResponse.attributes?.last_https_certificate?.extensions
360
+ ?.ca_information_access || {}
361
+ ).length
362
+ "
363
+ :bordered="false"
364
+ single-line
365
+ >
366
+ <tbody>
367
+ <tr
368
+ v-for="(value, key) of virusTotalDataResponse.attributes
369
+ .last_https_certificate.extensions.ca_information_access"
370
+ :key
371
+ >
372
+ <td class="whitespace-nowrap text-sm">{{ key }}</td>
373
+ <td class="whitespace-nowrap text-sm font-semibold">
374
+ <a :href="value" target="_blank" rel="nofollow noopener noreferrer">
375
+ {{ value }}
376
+ </a>
377
+ </td>
378
+ </tr>
379
+ </tbody>
380
+ </n-table>
381
+ </template>
382
+ </CardKV>
383
+
384
+ <div class="flex gap-3">
385
+ <CardKV class="grow">
386
+ <template #key>CA</template>
387
+ <template #value>
388
+ {{ virusTotalDataResponse.attributes.last_https_certificate.extensions.CA }}
389
+ </template>
390
+ </CardKV>
391
+ <CardKV class="grow">
392
+ <template #key>size</template>
393
+ <template #value>
394
+ {{ virusTotalDataResponse.attributes.last_https_certificate.size }}
395
+ </template>
396
+ </CardKV>
397
+ <CardKV class="grow">
398
+ <template #key>version</template>
399
+ <template #value>
400
+ {{ virusTotalDataResponse.attributes.last_https_certificate.version }}
401
+ </template>
402
+ </CardKV>
403
+ </div>
404
+
405
+ <CardKV value-class="!p-0 overflow-x-auto overflow-y-hidden">
406
+ <template #key>validity</template>
407
+ <template #value>
408
+ <n-table
409
+ v-if="
410
+ Object.keys(
411
+ virusTotalDataResponse.attributes?.last_https_certificate?.validity || {}
412
+ ).length
413
+ "
414
+ :bordered="false"
415
+ single-line
416
+ >
417
+ <tbody>
418
+ <tr
419
+ v-for="(value, key) of virusTotalDataResponse.attributes
420
+ .last_https_certificate.validity"
421
+ :key
422
+ >
423
+ <td class="whitespace-nowrap text-sm">{{ key }}</td>
424
+ <td class="whitespace-nowrap text-sm font-semibold">{{ value }}</td>
425
+ </tr>
426
+ </tbody>
427
+ </n-table>
428
+ </template>
429
+ </CardKV>
430
+
431
+ <CardKV>
432
+ <template #key>public_key</template>
433
+ <template #value>
434
+ <CodeSource
435
+ :code="virusTotalDataResponse.attributes.last_https_certificate.public_key"
436
+ :decode="true"
437
+ />
438
+ </template>
439
+ </CardKV>
440
+
441
+ <CardKV value-class="!p-0 overflow-x-auto overflow-y-hidden">
442
+ <template #value>
443
+ <n-table :bordered="false" single-line>
444
+ <tbody>
445
+ <tr>
446
+ <td class="whitespace-nowrap text-sm">thumbprint_sha256</td>
447
+ <td class="whitespace-nowrap text-sm font-semibold">
448
+ {{
449
+ virusTotalDataResponse.attributes.last_https_certificate
450
+ .thumbprint_sha256
451
+ }}
452
+ </td>
453
+ </tr>
454
+ <tr>
455
+ <td class="whitespace-nowrap text-sm">thumbprint</td>
456
+ <td class="whitespace-nowrap text-sm font-semibold">
457
+ {{
458
+ virusTotalDataResponse.attributes.last_https_certificate.thumbprint
459
+ }}
460
+ </td>
461
+ </tr>
462
+ <tr>
463
+ <td class="whitespace-nowrap text-sm">serial_number</td>
464
+ <td class="whitespace-nowrap text-sm font-semibold">
465
+ {{
466
+ virusTotalDataResponse.attributes.last_https_certificate
467
+ .serial_number
468
+ }}
469
+ </td>
470
+ </tr>
471
+ </tbody>
472
+ </n-table>
473
+ </template>
474
+ </CardKV>
475
+
476
+ <CardKV value-class="!p-0 overflow-x-auto overflow-y-hidden">
477
+ <template #key>issuer</template>
478
+ <template #value>
479
+ <n-table
480
+ v-if="
481
+ Object.keys(
482
+ virusTotalDataResponse.attributes?.last_https_certificate?.issuer || {}
483
+ ).length
484
+ "
485
+ :bordered="false"
486
+ single-line
487
+ >
488
+ <tbody>
489
+ <tr
490
+ v-for="(value, key) of virusTotalDataResponse.attributes
491
+ .last_https_certificate.issuer"
492
+ :key
493
+ >
494
+ <td class="whitespace-nowrap text-sm">{{ key }}</td>
495
+ <td class="whitespace-nowrap text-sm font-semibold">{{ value }}</td>
496
+ </tr>
497
+ </tbody>
498
+ </n-table>
499
+ </template>
500
+ </CardKV>
501
+
502
+ <CardKV value-class="!p-0 overflow-x-auto overflow-y-hidden">
503
+ <template #key>subject</template>
504
+ <template #value>
505
+ <n-table
506
+ v-if="
507
+ Object.keys(
508
+ virusTotalDataResponse.attributes?.last_https_certificate?.subject || {}
509
+ ).length
510
+ "
511
+ :bordered="false"
512
+ single-line
513
+ >
514
+ <tbody>
515
+ <tr
516
+ v-for="(value, key) of virusTotalDataResponse.attributes
517
+ .last_https_certificate.subject"
518
+ :key
519
+ >
520
+ <td class="whitespace-nowrap text-sm">{{ key }}</td>
521
+ <td class="whitespace-nowrap text-sm font-semibold">{{ value }}</td>
522
+ </tr>
523
+ </tbody>
524
+ </n-table>
525
+ </template>
526
+ </CardKV>
527
+ </div>
528
+ </n-tab-pane>
529
+ </n-tabs>
530
+ </n-modal>
531
+ </div>
532
+</template>
533
+
534
+<script setup lang="ts">
535
+import type { ItemProps } from "@/components/common/cards/CardStatsBars.vue"
536
+import type { VirusTotalData } from "@/types/threatIntel.d"
537
+import type { Size } from "naive-ui/es/button/src/interface"
538
+import Api from "@/api"
539
+import Icon from "@/components/common/Icon.vue"
540
+import { useSettingsStore } from "@/stores/settings"
541
+import { formatDate } from "@/utils"
542
+import _pick from "lodash/pick"
543
+import { NButton, NEmpty, NInput, NModal, NStatistic, NTable, NTabPane, NTabs, useMessage } from "naive-ui"
544
+import { computed, defineAsyncComponent, ref } from "vue"
545
+
546
+const { iocValue, size } = defineProps<{
547
+ iocValue: string
548
+ size?: Size
549
+}>()
550
+
551
+const Badge = defineAsyncComponent(() => import("@/components/common/Badge.vue"))
552
+const CardKV = defineAsyncComponent(() => import("@/components/common/cards/CardKV.vue"))
553
+const CardEntity = defineAsyncComponent(() => import("@/components/common/cards/CardEntity.vue"))
554
+const CardStatsBars = defineAsyncComponent(() => import("@/components/common/cards/CardStatsBars.vue"))
555
+const CodeSource = defineAsyncComponent(() => import("@/components/common/CodeSource.vue"))
556
+
557
+const AiIcon = "mage:stars-c"
558
+const showModal = ref<boolean>(false)
559
+const loading = ref<boolean>(false)
560
+const dFormats = useSettingsStore().dateFormat
561
+const message = useMessage()
562
+const virusTotalDataResponse = ref<VirusTotalData | null>(null)
563
+const totalVotes = computed<ItemProps[]>(() =>
564
+ Object.entries(virusTotalDataResponse.value?.attributes.total_votes || {})
565
+ .map(([key, value]) => ({
566
+ value,
567
+ label: key,
568
+ status: getStatusByCategory(key)
569
+ }))
570
+ .sort((a, b) => a.value - b.value)
571
+)
572
+const lastAnalysisStats = computed<ItemProps[]>(() =>
573
+ Object.entries(virusTotalDataResponse.value?.attributes.last_analysis_stats || {})
574
+ .map(([key, value]) => ({
575
+ value,
576
+ label: key,
577
+ status: getStatusByCategory(key)
578
+ }))
579
+ .sort((a, b) => a.value - b.value)
580
+)
581
+const properties = computed(() => {
582
+ return _pick(virusTotalDataResponse.value?.attributes || {}, [
583
+ "regional_internet_registry",
584
+ "continent",
585
+ "last_modification_date",
586
+ "crowdsourced_context",
587
+ "asn",
588
+ "reputation",
589
+ "jarm",
590
+ "country",
591
+ "as_owner",
592
+ "network"
593
+ ])
594
+})
595
+
596
+function getStatusByCategory(category: string) {
597
+ let status: "success" | "warning" | "error" | "muted" | "primary" = "muted"
598
+
599
+ switch (category) {
600
+ case "harmless":
601
+ status = "success"
602
+ break
603
+ case "malicious":
604
+ status = "error"
605
+ break
606
+ case "suspicious":
607
+ status = "warning"
608
+ break
609
+ case "undetected":
610
+ case "timeout":
611
+ status = "muted"
612
+ break
613
+ }
614
+ return status
615
+}
616
+
617
+function openResponse() {
618
+ showModal.value = true
619
+}
620
+
621
+function analysis() {
622
+ loading.value = true
623
+
624
+ Api.threatIntel
625
+ .virusTotalEnrichment(iocValue)
626
+ .then(res => {
627
+ if (res.data.success) {
628
+ virusTotalDataResponse.value = res.data.data.data
629
+ openResponse()
630
+ } else {
631
+ message.warning(res.data?.message || "An error occurred. Please try again later.")
632
+ }
633
+ })
634
+ .catch(err => {
635
+ message.error(err.response?.data?.message || "An error occurred. Please try again later.")
636
+ })
637
+ .finally(() => {
638
+ loading.value = false
639
+ })
640
+}
641
+</script>
frontend/src/types/threatIntel.d.ts
+106
@@ -86,3 +86,109 @@ export interface AiWazuhExclusionRuleResponse {
86
wazuh_exclusion_rule: string
87
wazuh_exclusion_rule_justification: string
88
}
89
+
90
+export interface VirusTotalResponse {
91
+ data: VirusTotal
92
+}
93
+
94
+export interface VirusTotal {
95
+ data: VirusTotalData
96
+}
97
+
98
+export interface VirusTotalData {
99
+ id: string
100
+ type: string
101
+ links: { [key: string]: string }
102
+ attributes: VirusTotalAttributes
103
+}
104
+
105
+export interface VirusTotalAttributes {
106
+ total_votes: { [key in VirusTotalLastAnalysisResultCategory]: number }
107
+ last_analysis_results: { [key: string]: VirusTotalLastAnalysisResult }
108
+ regional_internet_registry: string | null
109
+ continent: string | null
110
+ last_modification_date: number
111
+ crowdsourced_context: string | null
112
+ tags: string[]
113
+ asn: number
114
+ whois: string
115
+ whois_date: number
116
+ reputation: number
117
+ last_analysis_date: number
118
+ jarm: string
119
+ country: string | null
120
+ as_owner: string
121
+ last_analysis_stats: VirusTotalLastAnalysisStats
122
+ last_https_certificate_date: number
123
+ network: string
124
+ last_https_certificate: VirusTotalLastHTTPSCertificate
125
+}
126
+
127
+export interface VirusTotalLastAnalysisResult {
128
+ method: string
129
+ engine_name: string
130
+ category: VirusTotalLastAnalysisResultCategory
131
+ result: VirusTotalLastAnalysisResultResult
132
+}
133
+
134
+export enum VirusTotalLastAnalysisResultCategory {
135
+ Harmless = "harmless",
136
+ Malicious = "malicious",
137
+ Suspicious = "suspicious",
138
+ Undetected = "undetected",
139
+ Timeout = "timeout"
140
+}
141
+
142
+export enum VirusTotalLastAnalysisResultResult {
143
+ Clean = "clean",
144
+ Malicious = "malicious",
145
+ Suspicious = "suspicious",
146
+ Unrated = "unrated"
147
+}
148
+
149
+export interface VirusTotalLastAnalysisStats {
150
+ malicious: number
151
+ suspicious: number
152
+ undetected: number
153
+ harmless: number
154
+ timeout: number
155
+}
156
+
157
+export interface VirusTotalLastHTTPSCertificate {
158
+ cert_signature: {
159
+ signature_algorithm: string
160
+ signature: string
161
+ }
162
+ extensions: VirusTotalLastHTTPSCertificateExtensions
163
+ validity: {
164
+ not_after: Date
165
+ not_before: Date
166
+ }
167
+ size: number
168
+ version: string
169
+ public_key: {
170
+ algorithm: string
171
+ ec: {
172
+ oid: string
173
+ pub: string
174
+ }
175
+ }
176
+ thumbprint_sha256: string
177
+ thumbprint: string
178
+ serial_number: string
179
+ issuer: { [key: string]: string }
180
+ subject: { [key: string]: string }
181
+}
182
+
183
+export interface VirusTotalLastHTTPSCertificateExtensions {
184
+ authority_key_identifier: { [key: string]: string }
185
+ subject_key_identifier: string
186
+ subject_alternative_name: string[]
187
+ certificate_policies: string[]
188
+ key_usage: string[]
189
+ extended_key_usage: string[]
190
+ crl_distribution_points: string[]
191
+ ca_information_access: { [key: string]: string }
192
+ CA: boolean
193
+ "1.3.6.1.4.1.11129.2.4.2": string
194
+}
frontend/src/views/agents/Overview.vue
+7
-19
@@ -37,12 +37,13 @@
37
{{ agent?.hostname }}
38
</h1>
39
40
- <span v-if="isOnline" class="online-badge">ONLINE</span>
41
-
42
- <span v-if="isQuarantined" class="quarantined-badge flex items-center gap-1">
43
- <Icon :name="QuarantinedIcon" :size="15"></Icon>
40
+ <n-tag v-if="isOnline" type="success" round :bordered="false">ONLINE</n-tag>
41
+ <n-tag v-if="isQuarantined" type="warning" round :bordered="false">
42
+ <template #icon>
43
+ <Icon :name="QuarantinedIcon"></Icon>
44
+ </template>
45
<span>QUARANTINED</span>
45
- </span>
46
+ </n-tag>
47
</div>
48
<div class="label text-secondary mt-2">Agent #{{ agent?.agent_id }}</div>
49
</div>
@@ -147,7 +148,7 @@ import CardEntity from "@/components/common/cards/CardEntity.vue"
148
import Icon from "@/components/common/Icon.vue"
149
import { useGoto } from "@/composables/useGoto"
150
import { type Agent, AgentStatus } from "@/types/agents.d"
150
-import { NButton, NCard, NSpin, NTabPane, NTabs, NTooltip, useDialog, useMessage } from "naive-ui"
151
+import { NButton, NCard, NSpin, NTabPane, NTabs, NTag, NTooltip, useDialog, useMessage } from "naive-ui"
152
import { computed, defineAsyncComponent, nextTick, onBeforeMount, ref } from "vue"
153
import { useRoute, useRouter } from "vue-router"
154
@@ -334,19 +335,6 @@ onBeforeMount(() => {
335
display: flex;
336
align-items: center;
337
}
337
- .online-badge,
338
- .quarantined-badge {
339
- border: 2px solid var(--success-color);
340
- color: var(--success-color);
341
- font-weight: bold;
342
- border-radius: var(--border-radius);
343
- @apply px-2 py-1 text-xs;
344
- }
345
-
346
- .quarantined-badge {
347
- border-color: var(--warning-color);
348
- color: var(--warning-color);
349
- }
338
}
339
340
&.critical {