@cryptotaxi247 / CoPilot / commits / 16f01439

Wazuh sca (#216)

* chore: Add endpoints for fetching agent SCA and policy results * updated dependencies * updated agent api/types * copilot-ai-testing * chore: Update branch name in Docker workflow from network-connectors to main * feat: Add validation for 'rule_group3' in RuleExcludeRequest The code changes in `rules.py` add validation for the `rule_group3` field in the `RuleExcludeRequest` class. If `rule_group3` is present, it checks that either `rule_group1` or `rule_group3` is set to "windows". If `rule_group3` is not present, it falls back to the previous validation for `rule_group1`. This commit message follows the established convention of using a prefix to indicate the type of change (`feat` for a new feature) and provides a clear and concise description of the changes made. * added sca details * modal refactor * added sca item component * updated VulnerabilityCard * updated dependencies * refactor vulnerability * added sca result list * added sca result item * updated sca result item card * updated sca result details * updated sca result details * chore: Update variable name in Wazuh SCA service * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed May 16, 2024 at 16:21 UTC 16f014396ee3b91784210da15bcae7f59e230d26
44 files changed +1249 -197
.vscode/settings.json
+1
@@ -26,6 +26,7 @@
26 "mimecast",
27 "mynaui",
28 "ntime",
29 + "nums",
30 "ochin",
31 "Osquery",
32 "picocolors",
backend/app/agents/routes/agents.py
+44
@@ -20,8 +20,12 @@ from app.agents.services.status import get_outdated_agents_velociraptor
20 from app.agents.services.status import get_outdated_agents_wazuh
21 from app.agents.services.sync import sync_agents
22 from app.agents.velociraptor.services.agents import delete_agent_velociraptor
23 +from app.agents.wazuh.schema.agents import WazuhAgentScaPolicyResultsResponse
24 +from app.agents.wazuh.schema.agents import WazuhAgentScaResponse
25 from app.agents.wazuh.schema.agents import WazuhAgentVulnerabilitiesResponse
26 from app.agents.wazuh.services.agents import delete_agent_wazuh
27 +from app.agents.wazuh.services.sca import collect_agent_sca
28 +from app.agents.wazuh.services.sca import collect_agent_sca_policy_results
29 from app.agents.wazuh.services.vulnerabilities import collect_agent_vulnerabilities
30
31 # App specific imports
@@ -367,6 +371,46 @@ async def get_agent_vulnerabilities(agent_id: str) -> WazuhAgentVulnerabilitiesR
371 return await collect_agent_vulnerabilities(agent_id)
372
373
374 +@agents_router.get(
375 + "/{agent_id}/sca",
376 + response_model=WazuhAgentScaResponse,
377 + description="Get agent sca results",
378 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
379 +)
380 +async def get_agent_sca(agent_id: str) -> WazuhAgentScaResponse:
381 + """
382 + Fetches the sca results of a specific agent.
383 +
384 + Args:
385 + agent_id (str): The ID of the agent.
386 +
387 + Returns:
388 + WazuhAgentScaResponse: The response containing the agent sca.
389 + """
390 + logger.info(f"Fetching agent {agent_id} sca")
391 + return await collect_agent_sca(agent_id)
392 +
393 +
394 +@agents_router.get(
395 + "/{agent_id}/sca/{policy_id}",
396 + response_model=WazuhAgentScaPolicyResultsResponse,
397 + description="Get agent sca results",
398 + dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
399 +)
400 +async def get_agent_sca_policy_results(agent_id: str, policy_id: str) -> WazuhAgentScaPolicyResultsResponse:
401 + """
402 + Fetches the sca results of a specific agent.
403 +
404 + Args:
405 + agent_id (str): The ID of the agent.
406 +
407 + Returns:
408 + WazuhAgentScaPolicyResultsResponse: The response containing the agent sca.
409 + """
410 + logger.info(f"Fetching agent {agent_id} sca policy results")
411 + return await collect_agent_sca_policy_results(agent_id, policy_id)
412 +
413 +
414 @agents_router.get(
415 "/{agent_id}/soc_cases",
416 # response_model=SocCasesResponse,
backend/app/agents/wazuh/schema/agents.py
+58
@@ -56,3 +56,61 @@ class WazuhAgentVulnerabilitiesResponse(BaseModel):
56 vulnerabilities: Optional[List[WazuhAgentVulnerabilities]]
57 success: bool
58 message: str
59 +
60 +
61 +class WazuhAgentScaResults(BaseModel):
62 + description: str
63 + fail: int
64 + start_scan: str
65 + references: str
66 + name: str
67 + pass_count: int = Field(..., alias="pass")
68 + score: int
69 + end_scan: str
70 + policy_id: str
71 + total_checks: int
72 + hash_file: str
73 + invalid: int
74 +
75 +
76 +class WazuhAgentScaResponse(BaseModel):
77 + sca: Optional[List[WazuhAgentScaResults]]
78 + success: bool
79 + message: str
80 +
81 +
82 +class Compliance(BaseModel):
83 + value: str
84 + key: str
85 +
86 +
87 +class Rules(BaseModel):
88 + type: str
89 + rule: str
90 +
91 +
92 +class WazuhAgentScaPolicyResults(BaseModel):
93 + description: Optional[str] = Field(
94 + "Description not found",
95 + description="Description of the issue",
96 + )
97 + id: int
98 + reason: str
99 + command: Optional[str] = Field(
100 + "Command not found",
101 + description="Command to run to fix the issue",
102 + )
103 + rationale: str
104 + condition: str
105 + title: str
106 + result: str
107 + policy_id: str
108 + remediation: str
109 + compliance: List[Compliance]
110 + rules: List[Rules]
111 +
112 +
113 +class WazuhAgentScaPolicyResultsResponse(BaseModel):
114 + sca_policy_results: Optional[List[WazuhAgentScaPolicyResults]]
115 + success: bool
116 + message: str
backend/app/agents/wazuh/services/sca.py new
+129
@@ -0,0 +1,129 @@
1 +from typing import List
2 +
3 +from fastapi import HTTPException
4 +from loguru import logger
5 +
6 +from app.agents.wazuh.schema.agents import WazuhAgentScaPolicyResults
7 +from app.agents.wazuh.schema.agents import WazuhAgentScaPolicyResultsResponse
8 +from app.agents.wazuh.schema.agents import WazuhAgentScaResponse
9 +from app.agents.wazuh.schema.agents import WazuhAgentScaResults
10 +from app.connectors.wazuh_manager.utils.universal import send_get_request
11 +
12 +
13 +async def collect_agent_sca(agent_id: str):
14 + """
15 + Collect agent sca from Wazuh Manager.
16 +
17 + Args:
18 + agent_id (str): The ID of the agent.
19 +
20 + Returns:
21 + WazuhAgentVulnerabilitiesResponse: An object containing the collected sca.
22 +
23 + Raises:
24 + HTTPException: If there is an error collecting the sca.
25 + """
26 + logger.info(f"Collecting agent {agent_id} sca from Wazuh Manager")
27 + agent_sca = await send_get_request(
28 + endpoint=f"/sca/{agent_id}",
29 + )
30 + if agent_sca["success"] is False:
31 + raise HTTPException(status_code=500, detail=agent_sca["message"])
32 +
33 + processed_sca = process_agent_sca(
34 + agent_sca["data"],
35 + )
36 + logger.info(f"{processed_sca}")
37 + return WazuhAgentScaResponse(
38 + sca=processed_sca,
39 + success=True,
40 + message="SCA collected successfully",
41 + )
42 +
43 +
44 +def process_agent_sca(
45 + agent_sca: dict,
46 +) -> List[WazuhAgentScaResults]:
47 + """
48 + Process agent sca and return a list of WazuhAgentScaResults objects.
49 +
50 + Args:
51 + agent_sca (dict): A dictionary containing agent sca data.
52 +
53 + Returns:
54 + List[WazuhAgentScaResults]: A list of WazuhAgentScaResults objects.
55 +
56 + Raises:
57 + HTTPException: If there is an error processing the agent sca.
58 + """
59 + try:
60 + sca = agent_sca.get("data", {}).get(
61 + "affected_items",
62 + [],
63 + )
64 + return [WazuhAgentScaResults(**sca) for sca in sca]
65 + except Exception as e:
66 + raise HTTPException(
67 + status_code=500,
68 + detail=f"Failed to process agent sca: {e}",
69 + )
70 +
71 +
72 +########## ! SCA POLICY RESULTS ! #########
73 +async def collect_agent_sca_policy_results(agent_id: str, policy_id: str):
74 + """
75 + Collect agent sca from Wazuh Manager.
76 +
77 + Args:
78 + agent_id (str): The ID of the agent.
79 +
80 + Returns:
81 + WazuhAgentScaPolicyResultsResponse: An object containing the collected sca.
82 +
83 + Raises:
84 + HTTPException: If there is an error collecting the sca.
85 + """
86 + logger.info(f"Collecting agent {agent_id} sca from Wazuh Manager")
87 + agent_sca_policy_results = await send_get_request(
88 + endpoint=f"/sca/{agent_id}/checks/{policy_id}",
89 + )
90 + if agent_sca_policy_results["success"] is False:
91 + raise HTTPException(status_code=500, detail=agent_sca_policy_results["message"])
92 +
93 + processed_sca_policy_results = process_agent_sca_policy_results(
94 + agent_sca_policy_results["data"],
95 + )
96 + logger.info(f"{processed_sca_policy_results}")
97 + return WazuhAgentScaPolicyResultsResponse(
98 + sca_policy_results=processed_sca_policy_results,
99 + success=True,
100 + message="SCA Policy results collected successfully",
101 + )
102 +
103 +
104 +def process_agent_sca_policy_results(
105 + agent_sca: dict,
106 +) -> List[WazuhAgentScaPolicyResults]:
107 + """
108 + Process agent sca and return a list of WazuhAgentScaPolicyResults objects.
109 +
110 + Args:
111 + agent_sca (dict): A dictionary containing agent sca data.
112 +
113 + Returns:
114 + List[WazuhAgentScaPolicyResults]: A list of WazuhAgentScaPolicyResults objects.
115 +
116 + Raises:
117 + HTTPException: If there is an error processing the agent sca.
118 + """
119 + try:
120 + sca = agent_sca.get("data", {}).get(
121 + "affected_items",
122 + [],
123 + )
124 + return [WazuhAgentScaPolicyResults(**sca) for sca in sca]
125 + except Exception as e:
126 + raise HTTPException(
127 + status_code=500,
128 + detail=f"Failed to process agent sca: {e}",
129 + )
backend/app/connectors/wazuh_manager/schema/rules.py
+23 -11
@@ -116,17 +116,29 @@ class RuleExcludeRequest(BaseModel):
116 return v
117
118 @validator("prompt")
119 - def check_rule_group1(cls, v):
120 - if "rule_group1" not in v:
121 - raise HTTPException(
122 - status_code=400,
123 - detail="Missing 'rule_group1' in prompt.",
124 - )
125 - if v["rule_group1"] != "windows":
126 - raise HTTPException(
127 - status_code=400,
128 - detail="Invalid 'rule_group1'. Only 'windows' is supported.",
129 - )
119 + def check_rule_group(cls, v):
120 + if "rule_group3" in v:
121 + if "rule_group1" not in v and "rule_group3" not in v:
122 + raise HTTPException(
123 + status_code=400,
124 + detail="Missing 'rule_group1' or 'rule_group3' in prompt.",
125 + )
126 + if ("rule_group1" in v and v["rule_group1"] != "windows") and ("rule_group3" in v and v["rule_group3"] != "windows"):
127 + raise HTTPException(
128 + status_code=400,
129 + detail="Invalid 'rule_group1' or 'rule_group3'. At least one must be 'windows'.",
130 + )
131 + else:
132 + if "rule_group1" not in v:
133 + raise HTTPException(
134 + status_code=400,
135 + detail="Missing 'rule_group1' in prompt.",
136 + )
137 + if v["rule_group1"] != "windows":
138 + raise HTTPException(
139 + status_code=400,
140 + detail="Invalid 'rule_group1'. Only 'windows' is supported.",
141 + )
142 return v
143
144
backend/app/connectors/wazuh_manager/services/rules.py
+2 -2
@@ -251,10 +251,10 @@ async def post_to_copilot_ai_module(data: RuleExcludeRequest) -> RuleExcludeResp
251 data (CollectHuntress): The data to send to the copilot-ai-module Docker container.
252 """
253 logger.info(f"Sending POST request to http://copilot-ai-module/wazuh-rule-exclusion with data: {data.dict()}")
254 - raise HTTPException(status_code=501, detail="Not Implemented Yet")
254 + # raise HTTPException(status_code=501, detail="Not Implemented Yet")
255 async with httpx.AsyncClient() as client:
256 data = await client.post(
257 - "http://127.0.0.1:5001/wazuh-rule-exclusion",
257 + "http://copilot-ai-module/wazuh-rule-exclusion",
258 json=data.dict(),
259 timeout=120,
260 )
frontend/package-lock.json
+89 -74
@@ -16,7 +16,7 @@
16 "@popperjs/core": "^2.11.8",
17 "@vueuse/components": "^10.9.0",
18 "@vueuse/core": "^10.9.0",
19 - "apexcharts": "^3.49.0",
19 + "apexcharts": "^3.49.1",
20 "bytes": "^3.1.2",
21 "colord": "^2.9.3",
22 "crypto-js": "^4.2.0",
@@ -24,7 +24,8 @@
24 "detect-touch-device": "^1.1.6",
25 "echarts": "^5.5.0",
26 "file-saver": "^2.0.5",
27 - "jose": "^5.2.4",
27 + "html-entities": "^2.5.2",
28 + "jose": "^5.3.0",
29 "js-md5": "^0.8.3",
30 "lodash": "^4.17.21",
31 "markdown-it-highlightjs": "^4.0.1",
@@ -34,7 +35,7 @@
35 "pinia": "^2.1.7",
36 "pinia-plugin-persistedstate": "^3.2.1",
37 "secure-ls": "^1.2.6",
37 - "validator": "^13.11.0",
38 + "validator": "^13.12.0",
39 "vue": "^3.4.27",
40 "vue-advanced-cropper": "^2.8.8",
41 "vue-highlight-words": "^3.0.1",
@@ -57,11 +58,11 @@
58 "@types/html2canvas": "^1.0.0",
59 "@types/inquirer": "^9.0.7",
60 "@types/jsdom": "^21.1.6",
60 - "@types/lodash": "^4.17.1",
61 + "@types/lodash": "^4.17.3",
62 "@types/markdown-it": "^14.1.1",
63 "@types/markdown-it-highlightjs": "^3.3.4",
63 - "@types/node": "^20.12.10",
64 - "@types/validator": "^13.11.9",
64 + "@types/node": "^20.12.12",
65 + "@types/validator": "^13.11.10",
66 "@vitejs/plugin-vue": "^5.0.4",
67 "@vitejs/plugin-vue-jsx": "^3.1.0",
68 "@vue/eslint-config-prettier": "^9.0.0",
@@ -69,21 +70,21 @@
70 "@vue/test-utils": "^2.4.6",
71 "@vue/tsconfig": "^0.5.1",
72 "autoprefixer": "^10.4.19",
72 - "cypress": "^13.8.1",
73 + "cypress": "^13.9.0",
74 "eslint": "^8.57.0",
75 "eslint-plugin-cypress": "^3.2.0",
75 - "eslint-plugin-vue": "^9.25.0",
76 + "eslint-plugin-vue": "^9.26.0",
77 "flourite": "^1.2.4",
78 "fs-extra": "^11.2.0",
79 "ip": "^2.0.1",
80 "jsdom": "^24.0.0",
81 "json5": "^2.2.3",
82 "npm-run-all": "^4.1.5",
82 - "picocolors": "^1.0.0",
83 + "picocolors": "^1.0.1",
84 "postcss": "^8.4.38",
85 "prettier": "^3.2.5",
85 - "sass": "^1.77.0",
86 - "shiki": "^1.4.0",
86 + "sass": "^1.77.1",
87 + "shiki": "^1.5.2",
88 "start-server-and-test": "^2.0.3",
89 "tailwind-config-viewer": "^2.0.2",
90 "tailwindcss": "^3.4.3",
@@ -91,10 +92,10 @@
92 "unplugin-vue-components": "^0.27.0",
93 "vite": "^5.2.11",
94 "vite-bundle-analyzer": "^0.9.4",
94 - "vite-bundle-visualizer": "^1.1.0",
95 + "vite-bundle-visualizer": "^1.2.1",
96 "vite-svg-loader": "^5.1.0",
97 "vitest": "^1.6.0",
97 - "vue-tsc": "^2.0.16"
98 + "vue-tsc": "^2.0.19"
99 },
100 "engines": {
101 "node": ">=18.0.0"
@@ -663,7 +664,6 @@
664 },
665 "node_modules/@clack/prompts/node_modules/is-unicode-supported": {
666 "version": "1.3.0",
666 - "extraneous": true,
667 "inBundle": true,
668 "license": "MIT",
669 "engines": {
@@ -1843,9 +1843,9 @@
1843 "dev": true
1844 },
1845 "node_modules/@shikijs/core": {
1846 - "version": "1.4.0",
1847 - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.4.0.tgz",
1848 - "integrity": "sha512-CxpKLntAi64h3j+TwWqVIQObPTED0FyXLHTTh3MKXtqiQNn2JGcMQQ362LftDbc9kYbDtrksNMNoVmVXzKFYUQ==",
1846 + "version": "1.5.2",
1847 + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.5.2.tgz",
1848 + "integrity": "sha512-wSAOgaz48GmhILFElMCeQypSZmj6Ru6DttOOtl3KNkdJ17ApQuGNCfzpk4cClasVrnIu45++2DBwG4LNMQAfaA==",
1849 "dev": true
1850 },
1851 "node_modules/@sideway/address": {
@@ -1985,9 +1985,9 @@
1985 "dev": true
1986 },
1987 "node_modules/@types/lodash": {
1988 - "version": "4.17.1",
1989 - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1.tgz",
1990 - "integrity": "sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q=="
1988 + "version": "4.17.3",
1989 + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.3.tgz",
1990 + "integrity": "sha512-zmNrEJaBvNskZXQWaUQq6bktF4IDGVfDS78M+YEk5aCn9M/b94/mB/6WCyfH2/MjwBdc6QuOor95CIlKWYRL3A=="
1991 },
1992 "node_modules/@types/lodash-es": {
1993 "version": "4.17.12",
@@ -2033,9 +2033,9 @@
2033 "dev": true
2034 },
2035 "node_modules/@types/node": {
2036 - "version": "20.12.10",
2037 - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.10.tgz",
2038 - "integrity": "sha512-Eem5pH9pmWBHoGAT8Dr5fdc5rYA+4NAovdM4EktRPVAAiJhmWWfQrA0cFhAbOsQdSfIHjAud6YdkbL69+zSKjw==",
2036 + "version": "20.12.12",
2037 + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
2038 + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
2039 "dev": true,
2040 "dependencies": {
2041 "undici-types": "~5.26.4"
@@ -2074,9 +2074,9 @@
2074 "dev": true
2075 },
2076 "node_modules/@types/validator": {
2077 - "version": "13.11.9",
2078 - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz",
2079 - "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==",
2077 + "version": "13.11.10",
2078 + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz",
2079 + "integrity": "sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==",
2080 "dev": true
2081 },
2082 "node_modules/@types/web-bluetooth": {
@@ -2473,30 +2473,30 @@
2473 }
2474 },
2475 "node_modules/@volar/language-core": {
2476 - "version": "2.2.1",
2477 - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.2.1.tgz",
2478 - "integrity": "sha512-iHJAZKcYldZgyS8gx6DfIZApViVBeqbf6iPhqoZpG5A6F4zsZiFldKfwaKaBA3/wnOTWE2i8VUbXywI1WywCPg==",
2476 + "version": "2.2.4",
2477 + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.2.4.tgz",
2478 + "integrity": "sha512-7As47GndxGxsqqYnbreLrfB5NDUeQioPM2LJKUuB4/34c0NpEJ2byVl3c9KYdjIdiEstWZ9JLtLKNTaPWb5jtA==",
2479 "dev": true,
2480 "dependencies": {
2481 - "@volar/source-map": "2.2.1"
2481 + "@volar/source-map": "2.2.4"
2482 }
2483 },
2484 "node_modules/@volar/source-map": {
2485 - "version": "2.2.1",
2486 - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.2.1.tgz",
2487 - "integrity": "sha512-w1Bgpguhbp7YTr7VUFu6gb4iAZjeEPsOX4zpgiuvlldbzvIWDWy4t0jVifsIsxZ99HAu+c3swiME7wt+GeNqhA==",
2485 + "version": "2.2.4",
2486 + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.2.4.tgz",
2487 + "integrity": "sha512-m92FLpR9vB1YEZfiZ+bfgpLrToL/DNkOrorWVep3pffHrwwI4Tx2oIQN+sqHJfKkiT5N3J1owC+8crhAEinfjg==",
2488 "dev": true,
2489 "dependencies": {
2490 "muggle-string": "^0.4.0"
2491 }
2492 },
2493 "node_modules/@volar/typescript": {
2494 - "version": "2.2.1",
2495 - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.2.1.tgz",
2496 - "integrity": "sha512-Z/tqluR7Hz5/5dCqQp7wo9C/6tSv/IYl+tTzgzUt2NjTq95bKSsuO4E+V06D0c+3aP9x5S9jggLqw451hpnc6Q==",
2494 + "version": "2.2.4",
2495 + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.2.4.tgz",
2496 + "integrity": "sha512-uAQC53tgEbHO62G8NXMfmBrJAlP2QJ9WxVEEQqqK3I6VSy8frL5LbH3hAWODxiwMWixv74wJLWlKbWXOgdIoRQ==",
2497 "dev": true,
2498 "dependencies": {
2499 - "@volar/language-core": "2.2.1",
2499 + "@volar/language-core": "2.2.4",
2500 "path-browserify": "^1.0.1"
2501 }
2502 },
@@ -2868,12 +2868,12 @@
2868 }
2869 },
2870 "node_modules/@vue/language-core": {
2871 - "version": "2.0.16",
2872 - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.16.tgz",
2873 - "integrity": "sha512-Bc2sexRH99pznOph8mLw2BlRZ9edm7tW51kcBXgx8adAoOcZUWJj3UNSsdQ6H9Y8meGz7BoazVrVo/jUukIsPw==",
2871 + "version": "2.0.19",
2872 + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.19.tgz",
2873 + "integrity": "sha512-A9EGOnvb51jOvnCYoRLnMP+CcoPlbZVxI9gZXE/y2GksRWM6j/PrLEIC++pnosWTN08tFpJgxhSS//E9v/Sg+Q==",
2874 "dev": true,
2875 "dependencies": {
2876 - "@volar/language-core": "~2.2.0",
2876 + "@volar/language-core": "~2.2.4",
2877 "@vue/compiler-dom": "^3.4.0",
2878 "@vue/shared": "^3.4.0",
2879 "computeds": "^0.0.1",
@@ -3239,9 +3239,9 @@
3239 }
3240 },
3241 "node_modules/apexcharts": {
3242 - "version": "3.49.0",
3243 - "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.49.0.tgz",
3244 - "integrity": "sha512-2T9HnbQFLCuYRPndQLmh+bEQFoz0meUbvASaGgiSKDuYhWcLBodJtIpKql2aOtMx4B/sHrWW0dm90HsW4+h2PQ==",
3242 + "version": "3.49.1",
3243 + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.49.1.tgz",
3244 + "integrity": "sha512-MqGtlq/KQuO8j0BBsUJYlRG8VBctKwYdwuBtajHgHTmSgUU3Oai+8oYN/rKCXwXzrUlYA+GiMgotAIbXY2BCGw==",
3245 "dependencies": {
3246 "@yr/monotone-cubic-spline": "^1.0.3",
3247 "svg.draggable.js": "^2.2.2",
@@ -4290,9 +4290,9 @@
4290 "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
4291 },
4292 "node_modules/cypress": {
4293 - "version": "13.8.1",
4294 - "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.8.1.tgz",
4295 - "integrity": "sha512-Uk6ovhRbTg6FmXjeZW/TkbRM07KPtvM5gah1BIMp4Y2s+i/NMxgaLw0+PbYTOdw1+egE0FP3mWRiGcRkjjmhzA==",
4293 + "version": "13.9.0",
4294 + "resolved": "https://registry.npmjs.org/cypress/-/cypress-13.9.0.tgz",
4295 + "integrity": "sha512-atNjmYfHsvTuCaxTxLZr9xGoHz53LLui3266WWxXJHY7+N6OdwJdg/feEa3T+buez9dmUXHT1izCOklqG82uCQ==",
4296 "dev": true,
4297 "hasInstallScript": true,
4298 "dependencies": {
@@ -5269,9 +5269,9 @@
5269 }
5270 },
5271 "node_modules/eslint-plugin-vue": {
5272 - "version": "9.25.0",
5273 - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.25.0.tgz",
5274 - "integrity": "sha512-tDWlx14bVe6Bs+Nnh3IGrD+hb11kf2nukfm6jLsmJIhmiRQ1SUaksvwY9U5MvPB0pcrg0QK0xapQkfITs3RKOA==",
5272 + "version": "9.26.0",
5273 + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.26.0.tgz",
5274 + "integrity": "sha512-eTvlxXgd4ijE1cdur850G6KalZqk65k1JKoOI2d1kT3hr8sPD07j1q98FRFdNnpxBELGPWxZmInxeHGF/GxtqQ==",
5275 "dependencies": {
5276 "@eslint-community/eslint-utils": "^4.4.0",
5277 "globals": "^13.24.0",
@@ -6229,6 +6229,21 @@
6229 "node": ">=18"
6230 }
6231 },
6232 + "node_modules/html-entities": {
6233 + "version": "2.5.2",
6234 + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz",
6235 + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==",
6236 + "funding": [
6237 + {
6238 + "type": "github",
6239 + "url": "https://github.com/sponsors/mdevils"
6240 + },
6241 + {
6242 + "type": "patreon",
6243 + "url": "https://patreon.com/mdevils"
6244 + }
6245 + ]
6246 + },
6247 "node_modules/html-tags": {
6248 "version": "3.3.1",
6249 "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
@@ -6948,9 +6963,9 @@
6963 }
6964 },
6965 "node_modules/jose": {
6951 - "version": "5.2.4",
6952 - "resolved": "https://registry.npmjs.org/jose/-/jose-5.2.4.tgz",
6953 - "integrity": "sha512-6ScbIk2WWCeXkmzF6bRPmEuaqy1m8SbsRFMa/FLrSCkGIhj8OLVG/IH+XHVmNMx/KUo8cVWEE6oKR4dJ+S0Rkg==",
6966 + "version": "5.3.0",
6967 + "resolved": "https://registry.npmjs.org/jose/-/jose-5.3.0.tgz",
6968 + "integrity": "sha512-IChe9AtAE79ru084ow8jzkN2lNrG3Ntfiv65Cvj9uOCE2m5LNsdHG+9EbxWxAoWRF9TgDOqLN5jm08++owDVRg==",
6969 "funding": {
6970 "url": "https://github.com/sponsors/panva"
6971 }
@@ -8561,9 +8576,9 @@
8576 "dev": true
8577 },
8578 "node_modules/picocolors": {
8564 - "version": "1.0.0",
8565 - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
8566 - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
8579 + "version": "1.0.1",
8580 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
8581 + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
8582 },
8583 "node_modules/picomatch": {
8584 "version": "2.3.1",
@@ -9634,9 +9649,9 @@
9649 "dev": true
9650 },
9651 "node_modules/sass": {
9637 - "version": "1.77.0",
9638 - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.0.tgz",
9639 - "integrity": "sha512-eGj4HNfXqBWtSnvItNkn7B6icqH14i3CiCGbzMKs3BAPTq62pp9NBYsBgyN4cA+qssqo9r26lW4JSvlaUUWbgw==",
9652 + "version": "1.77.1",
9653 + "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.1.tgz",
9654 + "integrity": "sha512-OMEyfirt9XEfyvocduUIOlUSkWOXS/LAt6oblR/ISXCTukyavjex+zQNm51pPCOiFKY1QpWvEH1EeCkgyV3I6w==",
9655 "dev": true,
9656 "dependencies": {
9657 "chokidar": ">=3.0.0 <4.0.0",
@@ -9773,12 +9788,12 @@
9788 }
9789 },
9790 "node_modules/shiki": {
9776 - "version": "1.4.0",
9777 - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.4.0.tgz",
9778 - "integrity": "sha512-5WIn0OL8PWm7JhnTwRWXniy6eEDY234mRrERVlFa646V2ErQqwIFd2UML7e0Pq9eqSKLoMa3Ke+xbsF+DAuy+Q==",
9791 + "version": "1.5.2",
9792 + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.5.2.tgz",
9793 + "integrity": "sha512-fpPbuSaatinmdGijE7VYUD3hxLozR3ZZ+iAx8Iy2X6REmJGyF5hQl94SgmiUNTospq346nXUVZx0035dyGvIVw==",
9794 "dev": true,
9795 "dependencies": {
9781 - "@shikijs/core": "1.4.0"
9796 + "@shikijs/core": "1.5.2"
9797 }
9798 },
9799 "node_modules/side-channel": {
@@ -11087,9 +11102,9 @@
11102 }
11103 },
11104 "node_modules/validator": {
11090 - "version": "13.11.0",
11091 - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
11092 - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==",
11105 + "version": "13.12.0",
11106 + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz",
11107 + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==",
11108 "engines": {
11109 "node": ">= 0.10"
11110 }
@@ -11197,9 +11212,9 @@
11212 }
11213 },
11214 "node_modules/vite-bundle-visualizer": {
11200 - "version": "1.1.0",
11201 - "resolved": "https://registry.npmjs.org/vite-bundle-visualizer/-/vite-bundle-visualizer-1.1.0.tgz",
11202 - "integrity": "sha512-cmi5OuS7Eta5keTJmCTEbBBA7gOsUQ4K44W5dbsP+n/X0GIilIIFbJeXF120MQpTxdiZ/GIx4A9zkPEcKpPAog==",
11215 + "version": "1.2.1",
11216 + "resolved": "https://registry.npmjs.org/vite-bundle-visualizer/-/vite-bundle-visualizer-1.2.1.tgz",
11217 + "integrity": "sha512-cwz/Pg6+95YbgIDp+RPwEToc4TKxfsFWSG/tsl2DSZd9YZicUag1tQXjJ5xcL7ydvEoaC2FOZeaXOU60t9BRXw==",
11218 "dev": true,
11219 "dependencies": {
11220 "cac": "^6.7.14",
@@ -11587,13 +11602,13 @@
11602 }
11603 },
11604 "node_modules/vue-tsc": {
11590 - "version": "2.0.16",
11591 - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.16.tgz",
11592 - "integrity": "sha512-/gHAWJa216PeEhfxtAToIbxdWgw01wuQzo48ZUqMYVEyNqDp+OYV9xMO5HaPS2P3Ls0+EsjguMZLY4cGobX4Ew==",
11605 + "version": "2.0.19",
11606 + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.19.tgz",
11607 + "integrity": "sha512-JWay5Zt2/871iodGF72cELIbcAoPyhJxq56mPPh+M2K7IwI688FMrFKc/+DvB05wDWEuCPexQJ6L10zSwzzapg==",
11608 "dev": true,
11609 "dependencies": {
11595 - "@volar/typescript": "~2.2.0",
11596 - "@vue/language-core": "2.0.16",
11610 + "@volar/typescript": "~2.2.4",
11611 + "@vue/language-core": "2.0.19",
11612 "semver": "^7.5.4"
11613 },
11614 "bin": {
frontend/package.json
+15 -13
@@ -23,6 +23,7 @@
23 "tailwind-config-viewer": "tailwind-config-viewer -o",
24 "design-tokens": "node scripts/tokens-tool.js",
25 "start-server-old": "cd ../backend && uvicorn copilot:app --reload --port=5000",
26 + "start-server-venv": "cd ../backend && source .venv/bin/activate && uvicorn copilot:app --port=5000 --log-level debug",
27 "start-server": "cd ../backend && /opt/venv/bin/python copilot.py",
28 "start-vue": "vite --host 0.0.0.0",
29 "start": "concurrently \"npm run start-server\" \"npm run start-vue\"",
@@ -41,7 +42,7 @@
42 "@popperjs/core": "^2.11.8",
43 "@vueuse/components": "^10.9.0",
44 "@vueuse/core": "^10.9.0",
44 - "apexcharts": "^3.49.0",
45 + "apexcharts": "^3.49.1",
46 "bytes": "^3.1.2",
47 "colord": "^2.9.3",
48 "crypto-js": "^4.2.0",
@@ -49,7 +50,8 @@
50 "detect-touch-device": "^1.1.6",
51 "echarts": "^5.5.0",
52 "file-saver": "^2.0.5",
52 - "jose": "^5.2.4",
53 + "html-entities": "^2.5.2",
54 + "jose": "^5.3.0",
55 "js-md5": "^0.8.3",
56 "lodash": "^4.17.21",
57 "markdown-it-highlightjs": "^4.0.1",
@@ -59,7 +61,7 @@
61 "pinia": "^2.1.7",
62 "pinia-plugin-persistedstate": "^3.2.1",
63 "secure-ls": "^1.2.6",
62 - "validator": "^13.11.0",
64 + "validator": "^13.12.0",
65 "vue": "^3.4.27",
66 "vue-advanced-cropper": "^2.8.8",
67 "vue-highlight-words": "^3.0.1",
@@ -82,11 +84,11 @@
84 "@types/html2canvas": "^1.0.0",
85 "@types/inquirer": "^9.0.7",
86 "@types/jsdom": "^21.1.6",
85 - "@types/lodash": "^4.17.1",
87 + "@types/lodash": "^4.17.3",
88 "@types/markdown-it": "^14.1.1",
89 "@types/markdown-it-highlightjs": "^3.3.4",
88 - "@types/node": "^20.12.10",
89 - "@types/validator": "^13.11.9",
90 + "@types/node": "^20.12.12",
91 + "@types/validator": "^13.11.10",
92 "@vitejs/plugin-vue": "^5.0.4",
93 "@vitejs/plugin-vue-jsx": "^3.1.0",
94 "@vue/eslint-config-prettier": "^9.0.0",
@@ -94,21 +96,21 @@
96 "@vue/test-utils": "^2.4.6",
97 "@vue/tsconfig": "^0.5.1",
98 "autoprefixer": "^10.4.19",
97 - "cypress": "^13.8.1",
99 + "cypress": "^13.9.0",
100 "eslint": "^8.57.0",
101 "eslint-plugin-cypress": "^3.2.0",
100 - "eslint-plugin-vue": "^9.25.0",
102 + "eslint-plugin-vue": "^9.26.0",
103 "flourite": "^1.2.4",
104 "fs-extra": "^11.2.0",
105 "ip": "^2.0.1",
106 "jsdom": "^24.0.0",
107 "json5": "^2.2.3",
108 "npm-run-all": "^4.1.5",
107 - "picocolors": "^1.0.0",
109 + "picocolors": "^1.0.1",
110 "postcss": "^8.4.38",
111 "prettier": "^3.2.5",
110 - "sass": "^1.77.0",
111 - "shiki": "^1.4.0",
112 + "sass": "^1.77.1",
113 + "shiki": "^1.5.2",
114 "start-server-and-test": "^2.0.3",
115 "tailwind-config-viewer": "^2.0.2",
116 "tailwindcss": "^3.4.3",
@@ -116,10 +118,10 @@
118 "unplugin-vue-components": "^0.27.0",
119 "vite": "^5.2.11",
120 "vite-bundle-analyzer": "^0.9.4",
119 - "vite-bundle-visualizer": "^1.1.0",
121 + "vite-bundle-visualizer": "^1.2.1",
122 "vite-svg-loader": "^5.1.0",
123 "vitest": "^1.6.0",
122 - "vue-tsc": "^2.0.16"
124 + "vue-tsc": "^2.0.19"
125 },
126 "engines": {
127 "node": ">=18.0.0"
frontend/src/api/agents.ts
+17 -1
@@ -1,6 +1,13 @@
1 import { type FlaskBaseResponse } from "@/types/flask.d"
2 import { HttpClient } from "./httpClient"
3 -import type { Agent, AgentVulnerabilities, OutdatedWazuhAgents, OutdatedVelociraptorAgents } from "@/types/agents.d"
3 +import type {
4 + Agent,
5 + AgentVulnerabilities,
6 + OutdatedWazuhAgents,
7 + OutdatedVelociraptorAgents,
8 + AgentSca,
9 + ScaPolicyResult
10 +} from "@/types/agents.d"
11
12 export default {
13 getAgents(id?: string) {
@@ -29,6 +36,15 @@ export default {
36 signal ? { signal } : {}
37 )
38 },
39 + getSCA(id: string | number, signal?: AbortSignal) {
40 + return HttpClient.get<FlaskBaseResponse & { sca: AgentSca[] }>(`/agents/${id}/sca`, signal ? { signal } : {})
41 + },
42 + getSCAResults(id: string | number, policyId: string, signal?: AbortSignal) {
43 + return HttpClient.get<FlaskBaseResponse & { sca_policy_results: ScaPolicyResult[] }>(
44 + `/agents/${id}/sca/${policyId}`,
45 + signal ? { signal } : {}
46 + )
47 + },
48
49 // IGNORE AT THE MOMENT !
50 agentsWazuhOutdated() {
frontend/src/assets/scss/helpers.scss
+7 -2
@@ -103,6 +103,10 @@
103 background-color: var(--bg-color);
104 }
105
106 +.bg-secondary-color {
107 + background-color: var(--bg-secondary-color);
108 +}
109 +
110 .border-radius {
111 border-radius: var(--border-radius);
112 }
@@ -110,6 +114,9 @@
114 .font-mono {
115 font-family: var(--font-family-mono);
116 }
117 +.\!font-mono {
118 + font-family: var(--font-family-mono) !important;
119 +}
120
121 .text-primary-color {
122 color: var(--primary-color);
@@ -123,11 +130,9 @@
130 .text-secondary-color {
131 color: var(--fg-secondary-color);
132 }
126 -
133 .text-warning-color {
134 color: var(--warning-color);
135 }
130 -
136 .text-error-color {
137 color: var(--error-color);
138 }
frontend/src/components/agents/agentFlow/AgentFlowItem.vue
+1 -1
@@ -63,7 +63,7 @@
63 <n-modal
64 v-model:show="showDetails"
65 preset="card"
66 - content-style="padding:0px"
66 + content-class="!p-0"
67 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
68 :title="'Agent Flow: ' + flow.session_id"
69 :bordered="false"
frontend/src/components/agents/agentFlow/AgentFlowQueryStat.vue
+1 -1
@@ -53,7 +53,7 @@
53 <n-modal
54 v-model:show="showDetails"
55 preset="card"
56 - content-style="padding:0px"
56 + content-class="!p-0"
57 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
58 :title="`Agent Query Stat ${stat.Artifact ? ': ' + stat.Artifact : ''}`"
59 :bordered="false"
frontend/src/components/agents/sca/ScaItem.vue new
+98
@@ -0,0 +1,98 @@
1 +<template>
2 + <n-tabs type="line" animated :tabs-padding="24">
3 + <n-tab-pane name="Details" tab="Details" display-directive="show" class="flex flex-col gap-4 !py-8">
4 + <div class="px-7">
5 + <n-card content-class="bg-secondary-color" class="overflow-hidden">
6 + <div class="flex justify-between gap-8 flex-wrap">
7 + <n-statistic label="Checks" :value="sca.total_checks" tabular-nums />
8 + <n-statistic label="Pass" :value="sca.pass" tabular-nums />
9 + <n-statistic label="Fail" :value="sca.fail" tabular-nums />
10 + <n-statistic label="Invalid" :value="sca.invalid" tabular-nums />
11 + <n-statistic label="Score" :value="sca.score + '%'" tabular-nums />
12 + </div>
13 + </n-card>
14 + </div>
15 + <div class="px-7">
16 + <n-card content-class="bg-secondary-color" class="overflow-hidden">
17 + <div class="flex justify-between gap-8 xs:!flex-row flex-col">
18 + <n-statistic
19 + class="grow"
20 + label="Start scan"
21 + :value="formatDate(sca.start_scan, dFormats.datetime).toString()"
22 + />
23 + <n-statistic
24 + class="grow"
25 + label="End scan"
26 + :value="formatDate(sca.end_scan, dFormats.datetime).toString()"
27 + />
28 + </div>
29 + </n-card>
30 + </div>
31 + <div class="grid gap-2 grid-auto-flow-200 px-7" v-if="properties">
32 + <KVCard v-for="(value, key) of properties" :key="key">
33 + <template #key>{{ key }}</template>
34 + <template #value>
35 + <template v-if="value && key === 'references'">
36 + <a
37 + :href="value"
38 + target="_blank"
39 + alt="references url"
40 + rel="nofollow noopener noreferrer"
41 + class="leading-6"
42 + >
43 + <span>
44 + {{ value }}
45 + </span>
46 + <Icon :name="LinkIcon" :size="14" class="relative top-0.5 ml-2" />
47 + </a>
48 + </template>
49 + <template v-else>
50 + {{ value ?? "-" }}
51 + </template>
52 + </template>
53 + </KVCard>
54 + </div>
55 + </n-tab-pane>
56 + <n-tab-pane name="Description" tab="Description" display-directive="show">
57 + <div class="p-7 pt-4">
58 + <n-input
59 + :value="sca.description"
60 + type="textarea"
61 + readonly
62 + placeholder="Empty"
63 + size="large"
64 + :autosize="{
65 + minRows: 3,
66 + maxRows: 18
67 + }"
68 + />
69 + </div>
70 + </n-tab-pane>
71 + <n-tab-pane name="SCA Results" tab="SCA Results" display-directive="show:lazy">
72 + <div class="p-7 pt-4">
73 + <ScaResults :sca="sca" :agent="agent" />
74 + </div>
75 + </n-tab-pane>
76 + </n-tabs>
77 +</template>
78 +
79 +<script setup lang="ts">
80 +import { NTabs, NTabPane, NInput, NStatistic, NCard } from "naive-ui"
81 +import { useSettingsStore } from "@/stores/settings"
82 +import { formatDate } from "@/utils"
83 +import { type Agent, type AgentSca } from "@/types/agents.d"
84 +import KVCard from "@/components/common/KVCard.vue"
85 +import Icon from "@/components/common/Icon.vue"
86 +import { computed } from "vue"
87 +import ScaResults from "./ScaResults.vue"
88 +import _pick from "lodash/pick"
89 +
90 +const { sca, agent } = defineProps<{ sca: AgentSca; agent: Agent }>()
91 +
92 +const dFormats = useSettingsStore().dateFormat
93 +const LinkIcon = "carbon:launch"
94 +
95 +const properties = computed(() => {
96 + return _pick(sca, ["name", "hash_file", "references"])
97 +})
98 +</script>
frontend/src/components/agents/sca/ScaResultItem.vue new
+118
@@ -0,0 +1,118 @@
1 +<template>
2 + <div class="sca-result-item" :class="{ embedded }">
3 + <div class="px-4 py-3 flex flex-col gap-2">
4 + <div class="header-box flex items-center">
5 + <div class="id">#{{ data.id }}</div>
6 + <div class="grow"></div>
7 + <div class="actions">
8 + <n-button size="small" @click.stop="showDetails = true">
9 + <template #icon>
10 + <Icon :name="DetailsIcon"></Icon>
11 + </template>
12 + Details
13 + </n-button>
14 + </div>
15 + </div>
16 + <div class="main-box flex items-center gap-3">
17 + <div class="content flex flex-col gap-1 grow">
18 + <div class="title">{{ data.title }}</div>
19 + <div class="description">$ {{ data.command }}</div>
20 + </div>
21 + </div>
22 +
23 + <div class="badges-box flex flex-wrap items-center gap-3 mt-2">
24 + <Badge
25 + type="splitted"
26 + :color="
27 + data.result === 'failed' ? 'danger' : data.result === 'not applicable' ? 'warning' : 'success'
28 + "
29 + class="uppercase"
30 + >
31 + <template #label>{{ data.result }}</template>
32 + </Badge>
33 +
34 + <Badge type="splitted">
35 + <template #label>Compliance</template>
36 + <template #value>{{ data.compliance?.length || "-" }}</template>
37 + </Badge>
38 +
39 + <Badge type="splitted">
40 + <template #label>Condition</template>
41 + <template #value>{{ data.condition || "-" }}</template>
42 + </Badge>
43 +
44 + <Badge type="splitted">
45 + <template #label>Rules</template>
46 + <template #value>{{ data.rules?.length || "-" }}</template>
47 + </Badge>
48 + </div>
49 + </div>
50 +
51 + <n-modal
52 + v-model:show="showDetails"
53 + preset="card"
54 + content-class="!p-0"
55 + :style="{ maxWidth: 'min(900px, 90vw)', minHeight: 'min(600px, 90vh)', overflow: 'hidden' }"
56 + :title="data?.title"
57 + :bordered="false"
58 + segmented
59 + >
60 + <ScaResultItemDetails :data="data" />
61 + </n-modal>
62 + </div>
63 +</template>
64 +
65 +<script setup lang="ts">
66 +import Icon from "@/components/common/Icon.vue"
67 +import Badge from "@/components/common/Badge.vue"
68 +import { ref } from "vue"
69 +import { NModal, NButton } from "naive-ui"
70 +import type { ScaPolicyResult } from "@/types/agents"
71 +import ScaResultItemDetails from "./ScaResultItemDetails.vue"
72 +
73 +const { data, embedded } = defineProps<{
74 + data: ScaPolicyResult
75 + embedded?: boolean
76 +}>()
77 +
78 +const DetailsIcon = "carbon:settings-adjust"
79 +const showDetails = ref(false)
80 +</script>
81 +
82 +<style lang="scss" scoped>
83 +.sca-result-item {
84 + border-radius: var(--border-radius);
85 + background-color: var(--bg-color);
86 + transition: all 0.2s var(--bezier-ease);
87 + border: var(--border-small-050);
88 +
89 + .header-box {
90 + font-size: 13px;
91 + .id {
92 + font-family: var(--font-family-mono);
93 + word-break: break-word;
94 + color: var(--fg-secondary-color);
95 + line-height: 1.2;
96 + }
97 + }
98 +
99 + .main-box {
100 + .content {
101 + word-break: break-word;
102 +
103 + .description {
104 + color: var(--fg-secondary-color);
105 + font-size: 13px;
106 + }
107 + }
108 + }
109 +
110 + &.embedded {
111 + background-color: var(--bg-secondary-color);
112 + }
113 +
114 + &:hover {
115 + box-shadow: 0px 0px 0px 1px inset var(--primary-color);
116 + }
117 +}
118 +</style>
frontend/src/components/agents/sca/ScaResultItemDetails.vue new
+159
@@ -0,0 +1,159 @@
1 +<template>
2 + <n-tabs type="line" animated :tabs-padding="24">
3 + <n-tab-pane name="Overview" tab="Overview" display-directive="show:lazy" class="flex flex-col gap-4 !py-8">
4 + <div class="px-7">
5 + <n-card content-class="bg-secondary-color" class="overflow-hidden">
6 + <div class="flex justify-between gap-8 flex-wrap">
7 + <n-statistic label="Result" tabular-nums>
8 + <span
9 + class="uppercase"
10 + :class="
11 + data.result === 'failed'
12 + ? 'text-error-color'
13 + : data.result === 'not applicable'
14 + ? 'text-warning-color'
15 + : 'text-success-color'
16 + "
17 + >
18 + {{ data.result }}
19 + </span>
20 + </n-statistic>
21 + <n-statistic label="Condition" tabular-nums>
22 + <span class="uppercase">{{ data.condition }}</span>
23 + </n-statistic>
24 + <n-statistic label="Compliance" :value="data.compliance.length" tabular-nums />
25 + <n-statistic label="Rules" :value="data.rules.length" tabular-nums />
26 + </div>
27 + </n-card>
28 + </div>
29 +
30 + <div class="px-7">
31 + <n-card content-class="bg-secondary-color !p-0" class="overflow-hidden">
32 + <div
33 + class="scrollbar-styled overflow-hidden"
34 + v-shiki="{ theme: codeTheme, lang: 'shell', decode: true }"
35 + >
36 + <pre v-html="data.command"></pre>
37 + </div>
38 + </n-card>
39 + </div>
40 +
41 + <div class="grid gap-2 grid-auto-flow-200 px-7" v-if="properties">
42 + <KVCard v-for="(value, key) of properties" :key="key">
43 + <template #key>{{ key }}</template>
44 + <template #value>{{ value ?? "-" }}</template>
45 + </KVCard>
46 + </div>
47 + </n-tab-pane>
48 + <n-tab-pane name="Description" tab="Description" display-directive="show:lazy">
49 + <div class="p-7 pt-4">
50 + <n-input
51 + :value="data.description"
52 + type="textarea"
53 + readonly
54 + placeholder="Empty"
55 + size="large"
56 + :autosize="{
57 + minRows: 3,
58 + maxRows: 18
59 + }"
60 + />
61 + </div>
62 + </n-tab-pane>
63 + <n-tab-pane name="Rationale" tab="Rationale" display-directive="show:lazy">
64 + <div class="p-7 pt-4">
65 + <n-input
66 + :value="data.rationale"
67 + type="textarea"
68 + readonly
69 + placeholder="Empty"
70 + size="large"
71 + :autosize="{
72 + minRows: 3,
73 + maxRows: 18
74 + }"
75 + />
76 + </div>
77 + </n-tab-pane>
78 + <n-tab-pane name="Reason" tab="Reason" display-directive="show:lazy">
79 + <div class="p-7 pt-4">
80 + <n-input
81 + :value="data.reason"
82 + type="textarea"
83 + readonly
84 + placeholder="Empty"
85 + size="large"
86 + :autosize="{
87 + minRows: 3,
88 + maxRows: 18
89 + }"
90 + />
91 + </div>
92 + </n-tab-pane>
93 + <n-tab-pane name="Remediation" tab="Remediation" display-directive="show:lazy">
94 + <div class="p-7 pt-4">
95 + <n-input
96 + :value="data.remediation"
97 + type="textarea"
98 + readonly
99 + placeholder="Empty"
100 + size="large"
101 + :autosize="{
102 + minRows: 3,
103 + maxRows: 18
104 + }"
105 + />
106 + </div>
107 + </n-tab-pane>
108 + <n-tab-pane name="Compliance" tab="Compliance" display-directive="show:lazy">
109 + <div class="p-7 pt-4 flex flex-col gap-1">
110 + <n-card
111 + content-class="bg-secondary-color flex flex-col gap-2"
112 + class="overflow-hidden"
113 + size="small"
114 + v-for="item of data.compliance"
115 + :key="item.key"
116 + >
117 + <div>{{ item.key }}</div>
118 + <p>{{ item.value }}</p>
119 + </n-card>
120 + </div>
121 + </n-tab-pane>
122 + <n-tab-pane name="Rules" tab="Rules" display-directive="show:lazy">
123 + <div class="p-7 pt-4 flex flex-col gap-1">
124 + <n-card
125 + content-class="bg-secondary-color flex flex-col gap-2"
126 + class="overflow-hidden"
127 + size="small"
128 + v-for="item of data.rules"
129 + :key="item.type + item.rule"
130 + >
131 + <div>{{ item.type }}</div>
132 + <p>{{ item.rule }}</p>
133 + </n-card>
134 + </div>
135 + </n-tab-pane>
136 + </n-tabs>
137 +</template>
138 +
139 +<script setup lang="ts">
140 +import vShiki from "@/directives/v-shiki"
141 +import _pick from "lodash/pick"
142 +import KVCard from "@/components/common/KVCard.vue"
143 +import { computed } from "vue"
144 +import { NTabs, NTabPane, NStatistic, NInput, NCard } from "naive-ui"
145 +import type { ScaPolicyResult } from "@/types/agents"
146 +import { useThemeStore } from "@/stores/theme"
147 +
148 +const { data } = defineProps<{
149 + data: ScaPolicyResult
150 +}>()
151 +
152 +const themeStore = useThemeStore()
153 +const codeTheme = computed(() => (themeStore.isThemeDark ? "dark" : "light"))
154 +const properties = computed(() => {
155 + return _pick(data, ["id", "policy_id", "title"])
156 +})
157 +</script>
158 +
159 +<style lang="scss" scoped></style>
frontend/src/components/agents/sca/ScaResults.vue new
+184
@@ -0,0 +1,184 @@
1 +<template>
2 + <n-spin :show="loading">
3 + <div class="header flex items-center justify-end gap-2" ref="header">
4 + <div class="info grow flex gap-5">
5 + <n-popover overlap placement="bottom-start">
6 + <template #trigger>
7 + <div class="bg-color border-radius">
8 + <n-button size="small" class="!cursor-help">
9 + <template #icon>
10 + <Icon :name="InfoIcon"></Icon>
11 + </template>
12 + </n-button>
13 + </div>
14 + </template>
15 + <div class="flex flex-col gap-2">
16 + <div class="box">
17 + Total:
18 + <code>{{ total }}</code>
19 + </div>
20 + <div class="box">
21 + Passed:
22 + <code class="text-success-color">{{ totalPassed }}</code>
23 + </div>
24 + <div class="box">
25 + Not applicable:
26 + <code class="text-warning-color">{{ totalNA }}</code>
27 + </div>
28 + <div class="box">
29 + Failed:
30 + <code class="text-error-color">{{ totalFailed }}</code>
31 + </div>
32 + </div>
33 + </n-popover>
34 + </div>
35 + <n-pagination
36 + v-model:page="currentPage"
37 + v-model:page-size="pageSize"
38 + :page-slot="pageSlot"
39 + :show-size-picker="showSizePicker"
40 + :page-sizes="pageSizes"
41 + :item-count="itemsFiltered.length"
42 + :simple="simpleMode"
43 + />
44 + <n-popover overlap placement="right" class="!px-0">
45 + <template #trigger>
46 + <div class="bg-color border-radius">
47 + <n-button size="small">
48 + <template #icon>
49 + <Icon :name="FilterIcon"></Icon>
50 + </template>
51 + </n-button>
52 + </div>
53 + </template>
54 + <div class="py-1">
55 + <div class="px-3">
56 + <div class="text-secondary-color text-sm mb-1">Result:</div>
57 + <n-select
58 + size="small"
59 + v-model:value="resultFilter"
60 + :options="resultOptions"
61 + clearable
62 + placeholder="All"
63 + class="!w-40"
64 + />
65 + </div>
66 + </div>
67 + </n-popover>
68 + </div>
69 + <div class="list my-3">
70 + <template v-if="itemsPaginated.length">
71 + <ScaResultItem v-for="item of itemsPaginated" :key="item.id" :data="item" embedded class="mb-2" />
72 + </template>
73 + <template v-else>
74 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading" />
75 + </template>
76 + </div>
77 + <div class="footer flex justify-end">
78 + <n-pagination
79 + v-model:page="currentPage"
80 + :page-size="pageSize"
81 + :item-count="itemsFiltered.length"
82 + :page-slot="6"
83 + :simple="simpleMode"
84 + v-if="itemsPaginated.length > 3"
85 + />
86 + </div>
87 + </n-spin>
88 +</template>
89 +
90 +<script setup lang="ts">
91 +import { ref, onBeforeMount, computed } from "vue"
92 +import { useMessage, NSpin, NPagination, NPopover, NButton, NSelect, NEmpty } from "naive-ui"
93 +import Api from "@/api"
94 +import Icon from "@/components/common/Icon.vue"
95 +import { useResizeObserver } from "@vueuse/core"
96 +import ScaResultItem from "./ScaResultItem.vue"
97 +import type { Agent, AgentSca, ScaPolicyResult } from "@/types/agents.d"
98 +import { watch } from "vue"
99 +
100 +const { sca, agent } = defineProps<{ sca: AgentSca; agent: Agent }>()
101 +
102 +const FilterIcon = "carbon:filter-edit"
103 +const InfoIcon = "carbon:information"
104 +
105 +const message = useMessage()
106 +const loading = ref(false)
107 +const resultsList = ref<ScaPolicyResult[]>([])
108 +const total = computed(() => resultsList.value.length)
109 +const totalFailed = computed(() => resultsList.value.filter(o => o.result === "failed").length)
110 +const totalNA = computed(() => resultsList.value.filter(o => o.result === "not applicable").length)
111 +const totalPassed = computed(() => resultsList.value.filter(o => o.result === "passed").length)
112 +const pageSize = ref(25)
113 +const currentPage = ref(1)
114 +const simpleMode = ref(false)
115 +const showSizePicker = ref(true)
116 +const pageSizes = [10, 25, 50, 100]
117 +const header = ref()
118 +const pageSlot = ref(8)
119 +const resultFilter = ref<null | string>(null)
120 +const resultOptions = [
121 + { label: "Passed", value: "passed" },
122 + { label: "Not applicable", value: "not applicable" },
123 + { label: "Failed", value: "failed" }
124 +]
125 +
126 +const itemsFiltered = computed(() =>
127 + resultsList.value.filter(o => {
128 + if (!resultFilter.value) {
129 + return true
130 + }
131 + return resultFilter.value === o.result
132 + })
133 +)
134 +
135 +const itemsPaginated = computed(() => {
136 + const from = (currentPage.value - 1) * pageSize.value
137 + const to = currentPage.value * pageSize.value
138 +
139 + return itemsFiltered.value.slice(from, to)
140 +})
141 +
142 +watch(resultFilter, () => {
143 + currentPage.value = 1
144 +})
145 +
146 +function getSCAResults(agentId: string, policyId: string) {
147 + loading.value = true
148 +
149 + Api.agents
150 + .getSCAResults(agentId, policyId)
151 + .then(res => {
152 + if (res.data.success) {
153 + resultsList.value = res.data.sca_policy_results || []
154 + } else {
155 + message.warning(res.data?.message || "An error occurred. Please try again later.")
156 + }
157 + })
158 + .catch(err => {
159 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
160 + })
161 + .finally(() => {
162 + loading.value = false
163 + })
164 +}
165 +
166 +useResizeObserver(header, entries => {
167 + const entry = entries[0]
168 + const { width } = entry.contentRect
169 +
170 + pageSlot.value = width < 650 ? 5 : 8
171 + simpleMode.value = width < 450
172 +})
173 +
174 +onBeforeMount(() => {
175 + if (agent?.agent_id && sca?.policy_id) getSCAResults(agent.agent_id, sca.policy_id)
176 +})
177 +</script>
178 +
179 +<style lang="scss" scoped>
180 +.list {
181 + container-type: inline-size;
182 + min-height: 200px;
183 +}
184 +</style>
frontend/src/components/agents/sca/ScaTable.vue new
+167
@@ -0,0 +1,167 @@
1 +<template>
2 + <n-spin class="sca-section" :show="loading">
3 + <n-scrollbar x-scrollable style="width: 100%">
4 + <n-table :bordered="true" class="min-w-max">
5 + <thead>
6 + <tr>
7 + <th></th>
8 + <th>Policy</th>
9 + <th>End scan</th>
10 + <th>Passed</th>
11 + <th>Failed</th>
12 + <th>Total checks</th>
13 + <th>Score</th>
14 + </tr>
15 + </thead>
16 + <tbody>
17 + <tr v-for="item of scaList" :key="item.policy_id">
18 + <td class="w-6">
19 + <n-button size="small" @click="showScaDetails(item)">
20 + <template #icon><Icon :name="InfoIcon"></Icon></template>
21 + </n-button>
22 + </td>
23 + <td>
24 + <div class="flex flex-col gap-1">
25 + <strong>{{ item.policy_id }}</strong>
26 +
27 + <p class="hidden lg:flex">
28 + {{ item.extract }}
29 +
30 + <n-popover
31 + placement="top-end"
32 + content-class="max-w-96"
33 + scrollable
34 + to="body"
35 + v-if="item.description !== item.extract"
36 + >
37 + <template #trigger>
38 + <span class="cursor-help underline">...</span>
39 + </template>
40 + <div class="flex flex-col py-2 px-1">
41 + {{ item.description }}
42 + </div>
43 + </n-popover>
44 + </p>
45 + </div>
46 + </td>
47 + <td>
48 + {{ item.end_scan_text }}
49 + </td>
50 + <td>
51 + {{ item.pass }}
52 + </td>
53 + <td>
54 + {{ item.fail }}
55 + </td>
56 + <td>
57 + {{ item.total_checks }}
58 + </td>
59 + <td>{{ item.score }}%</td>
60 + </tr>
61 + </tbody>
62 + </n-table>
63 + </n-scrollbar>
64 + <n-empty description="No items found" class="justify-center h-48" v-if="!loading && !scaList.length" />
65 +
66 + <n-modal
67 + v-model:show="showDetails"
68 + preset="card"
69 + :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(400px, 90vh)', overflow: 'hidden' }"
70 + :title="selectedSca?.policy_id || ''"
71 + :bordered="false"
72 + segmented
73 + content-class="!p-0"
74 + >
75 + <ScaItem v-if="selectedSca" :sca="selectedSca" :agent="agent"></ScaItem>
76 + </n-modal>
77 + </n-spin>
78 +</template>
79 +
80 +<script setup lang="ts">
81 +import { ref, onBeforeMount, toRefs } from "vue"
82 +import Api from "@/api"
83 +import { type Agent, type AgentSca } from "@/types/agents.d"
84 +import { useMessage, NSpin, NEmpty, NScrollbar, NTable, NButton, NPopover, NModal } from "naive-ui"
85 +import { useSettingsStore } from "@/stores/settings"
86 +import { formatDate } from "@/utils"
87 +import Icon from "@/components/common/Icon.vue"
88 +import _truncate from "lodash/truncate"
89 +import ScaItem from "./ScaItem.vue"
90 +
91 +interface SCAExt extends AgentSca {
92 + end_scan_text?: string
93 + extract?: string
94 +}
95 +
96 +const props = defineProps<{
97 + agent: Agent
98 +}>()
99 +const { agent } = toRefs(props)
100 +
101 +const InfoIcon = "carbon:information"
102 +const message = useMessage()
103 +const loading = ref(false)
104 +const showDetails = ref(false)
105 +const scaList = ref<SCAExt[]>([])
106 +const dFormats = useSettingsStore().dateFormat
107 +const selectedSca = ref<SCAExt | null>(null)
108 +
109 +function getSCA(id: string) {
110 + loading.value = true
111 +
112 + Api.agents
113 + .getSCA(id)
114 + .then(res => {
115 + if (res.data.success) {
116 + scaList.value = (res.data.sca || []).map(o => {
117 + return {
118 + ...o,
119 + end_scan_text: formatDate(o.end_scan, dFormats.datetime).toString(),
120 + extract: _truncate(o.description, {
121 + length: 50,
122 + omission: ""
123 + })
124 + }
125 + })
126 + } else {
127 + message.warning(res.data?.message || "An error occurred. Please try again later.")
128 + }
129 + })
130 + .catch(err => {
131 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
132 + })
133 + .finally(() => {
134 + loading.value = false
135 + })
136 +}
137 +
138 +function showScaDetails(sca: SCAExt) {
139 + showDetails.value = true
140 + selectedSca.value = sca
141 +}
142 +
143 +onBeforeMount(() => {
144 + if (agent?.value?.agent_id) getSCA(agent.value.agent_id)
145 +})
146 +</script>
147 +
148 +<style lang="scss" scoped>
149 +.sca-section {
150 + container-type: inline-size;
151 + min-height: 100px;
152 +
153 + .group {
154 + @apply gap-4;
155 + width: 100%;
156 + display: grid;
157 + grid-template-columns: repeat(auto-fit, minmax(175px, 1fr));
158 + grid-auto-flow: row dense;
159 + }
160 +
161 + @container (max-width: 500px) {
162 + .group {
163 + grid-template-columns: repeat(auto-fit, 100%);
164 + }
165 + }
166 +}
167 +</style>
frontend/src/components/agents/vulnerabilities/VulnerabilitiesGrid.vue renamed
+7 -3
@@ -3,7 +3,11 @@
3 <div class="group">
4 <VulnerabilityCard :vulnerability="item" v-for="item of vulnerabilities" :key="item.id" />
5 </div>
6 - <div v-if="!loading && !vulnerabilities.length">No vulnerabilities detected</div>
6 + <n-empty
7 + description="No vulnerabilities detected"
8 + class="justify-center h-48"
9 + v-if="!loading && !vulnerabilities.length"
10 + />
11 </n-spin>
12 </template>
13
@@ -11,9 +15,9 @@
15 import { ref, onBeforeMount, toRefs } from "vue"
16 import Api from "@/api"
17 import { type Agent, type AgentVulnerabilities } from "@/types/agents.d"
14 -import VulnerabilityCard from "@/components/agents/VulnerabilityCard.vue"
18 +import VulnerabilityCard from "./VulnerabilityCard.vue"
19 import { nanoid } from "nanoid"
16 -import { useMessage, NSpin } from "naive-ui"
20 +import { useMessage, NSpin, NEmpty } from "naive-ui"
21
22 const props = defineProps<{
23 agent: Agent
frontend/src/components/agents/vulnerabilities/VulnerabilityCard.vue renamed
+21 -38
@@ -46,22 +46,28 @@
46 class="vulnerability-dialog"
47 :title="vulnerability.title"
48 v-model:show="showDialog"
49 + content-class="!p-0"
50 style="width: 90vw; max-width: 1000px"
51 >
51 - <div class="vulnerability-property-group" v-if="vulnerabilitySanitized">
52 - <KVCard v-for="item of vulnerabilitySanitized" :key="item.label">
53 - <template #key>{{ item.label }}</template>
54 - <template #value>{{ item.value ?? "-" }}</template>
55 - </KVCard>
56 - </div>
57 - <div class="vulnerability-references">
58 - <div class="title">External references</div>
59 - <ul class="list">
60 - <li v-for="ref of vulnerability.external_references" :key="ref">
61 - <a :href="ref" target="_blank">{{ ref }}</a>
62 - </li>
63 - </ul>
64 - </div>
52 + <n-tabs type="line" animated :tabs-padding="24">
53 + <n-tab-pane name="Details" tab="Details" display-directive="show">
54 + <div class="grid gap-2 grid-auto-flow-200 p-7 pt-4" v-if="vulnerabilitySanitized">
55 + <KVCard v-for="item of vulnerabilitySanitized" :key="item.label">
56 + <template #key>{{ item.label }}</template>
57 + <template #value>{{ item.value ?? "-" }}</template>
58 + </KVCard>
59 + </div>
60 + </n-tab-pane>
61 + <n-tab-pane name="External references" tab="External references" display-directive="show">
62 + <div class="p-7 pt-2">
63 + <ul>
64 + <li v-for="ref of vulnerability.external_references" :key="ref">
65 + <a :href="ref" target="_blank">{{ ref }}</a>
66 + </li>
67 + </ul>
68 + </div>
69 + </n-tab-pane>
70 + </n-tabs>
71 </n-modal>
72 </template>
73
@@ -70,7 +76,7 @@ import { computed, ref, toRefs } from "vue"
76 import { type AgentVulnerabilities } from "@/types/agents.d"
77 import dayjs from "@/utils/dayjs"
78 import { cloneDeep } from "lodash"
73 -import { NModal, NTooltip } from "naive-ui"
79 +import { NModal, NTooltip, NTabs, NTabPane } from "naive-ui"
80 import Icon from "@/components/common/Icon.vue"
81 import KVCard from "@/components/common/KVCard.vue"
82 import { useSettingsStore } from "@/stores/settings"
@@ -215,27 +221,4 @@ const showDialog = ref(false)
221 &.severity-Untriaged {
222 }
223 }
218 -
219 -.vulnerability-property-group {
220 - width: 100%;
221 - display: grid;
222 - box-sizing: border-box;
223 - @apply gap-2;
224 - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
225 - grid-auto-flow: row dense;
226 -}
227 -
228 -.vulnerability-references {
229 - @apply gap-5 mt-6;
230 - overflow: hidden;
231 -
232 - .list {
233 - padding-left: 16px;
234 - @apply mt-2;
235 -
236 - li {
237 - word-break: break-all;
238 - }
239 - }
240 -}
224 </style>
frontend/src/components/alerts/Alert.vue
+1 -1
@@ -118,7 +118,7 @@
118 <n-modal
119 v-model:show="showDetails"
120 preset="card"
121 - content-style="padding:0px"
121 + content-class="!p-0"
122 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(600px, 90vh)', overflow: 'hidden' }"
123 :title="`Alert: ${alert._id || alert._source.id}`"
124 :bordered="false"
frontend/src/components/alerts/AlertWazuhRules.vue
+1 -1
@@ -33,7 +33,7 @@ const props = defineProps<{ data: WazuhRuleExclude }>()
33 const { data } = toRefs(props)
34
35 const themeStore = useThemeStore()
36 -const codeTheme = computed(() => (themeStore.isThemeDark ? "slack-dark" : "slack-ochin"))
36 +const codeTheme = computed(() => (themeStore.isThemeDark ? "dark" : "light"))
37 const wazuh_rule = computed(() => data.value.wazuh_rule.replace(/\\\\/gim, "\\\\\\\\"))
38 </script>
39
frontend/src/components/artifacts/ArtifactsList.vue
+1 -7
@@ -29,13 +29,7 @@
29 :item-count="totalArtifacts"
30 :simple="simpleMode"
31 />
32 - <n-popover
33 - :show="showFilters"
34 - trigger="manual"
35 - overlap
36 - placement="right"
37 - style="padding-left: 0; padding-right: 0"
38 - >
32 + <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="!px-0">
33 <template #trigger>
34 <div class="bg-color border-radius">
35 <n-badge
frontend/src/components/common/Badge.vue
+13 -2
@@ -23,7 +23,7 @@ const { type, hintCursor, pointCursor, color, href, fluid } = defineProps<{
23 hintCursor?: boolean
24 pointCursor?: boolean
25 fluid?: boolean
26 - color?: "danger" | "warning"
26 + color?: "danger" | "warning" | "success"
27 href?: string
28 }>()
29 </script>
@@ -63,6 +63,10 @@ const { type, hintCursor, pointCursor, color, href, fluid } = defineProps<{
63 }
64 }
65
66 + span:not(:last-child) {
67 + border-right: var(--border-small-100);
68 + }
69 +
70 &.splitted {
71 padding: 0px;
72 gap: 0;
@@ -74,7 +78,6 @@ const { type, hintCursor, pointCursor, color, href, fluid } = defineProps<{
78 line-height: 24px;
79
80 &:first-child {
77 - border-right: var(--border-small-100);
81 background-color: var(--primary-005-color);
82 line-height: 1.1;
83 white-space: nowrap;
@@ -99,6 +102,14 @@ const { type, hintCursor, pointCursor, color, href, fluid } = defineProps<{
102 }
103 }
104 }
105 +
106 + &.success {
107 + & > span {
108 + &:first-child {
109 + background-color: var(--success-005-color);
110 + }
111 + }
112 + }
113 }
114
115 &.fluid {
frontend/src/components/common/ImageCropper.vue
+6 -3
@@ -41,8 +41,7 @@
41 </template>
42
43 <script lang="ts" setup>
44 -import { NButton, NCard, NUpload, NUploadDragger, NModal } from "naive-ui"
45 -import { type FileInfo } from "naive-ui/es/upload/src/interface"
44 +import { NButton, NCard, NUpload, NUploadDragger, NModal, type UploadSettledFileInfo } from "naive-ui"
45 import { computed, ref, toRefs } from "vue"
46 import { Cropper, CircleStencil, RectangleStencil, type CropperResult } from "vue-advanced-cropper"
47 import "vue-advanced-cropper/dist/style.css"
@@ -90,7 +89,11 @@ function openCropper() {
89 img.value = ""
90 }
91
93 -function setImage(data: { file: FileInfo; fileList: FileInfo[]; event: ProgressEvent | Event | undefined }): void {
92 +function setImage(data: {
93 + file: UploadSettledFileInfo
94 + fileList: UploadSettledFileInfo[]
95 + event: ProgressEvent | Event | undefined
96 +}): void {
97 if (data?.file?.file) {
98 const reader = new FileReader()
99 reader.readAsDataURL(data.file.file)
frontend/src/components/customers/CustomerItem.vue
+1 -1
@@ -97,7 +97,7 @@
97 <n-modal
98 v-model:show="showDetails"
99 preset="card"
100 - content-style="padding:0px"
100 + content-class="!p-0"
101 :style="{ maxWidth: 'min(900px, 90vw)', minHeight: 'min(600px, 90vh)', overflow: 'hidden' }"
102 :title="customerInfo?.customer_name"
103 :bordered="false"
frontend/src/components/customers/healthcheck/CustomerHealthcheckItem.vue
+1 -1
@@ -66,7 +66,7 @@
66 <n-modal
67 v-model:show="showDetails"
68 preset="card"
69 - content-style="padding:0px"
69 + content-class="!p-0"
70 :style="{ maxWidth: 'min(800px, 90vw)', overflow: 'hidden' }"
71 :title="`Health check ${source}`"
72 :bordered="false"
frontend/src/components/graylog/Events/Item.vue
+1 -1
@@ -36,7 +36,7 @@
36 <n-modal
37 v-model:show="showDetails"
38 preset="card"
39 - content-style="padding:0px"
39 + content-class="!p-0"
40 :style="{ maxWidth: 'min(600px, 90vw)', overflow: 'hidden' }"
41 :title="event.title"
42 :bordered="false"
frontend/src/components/graylog/Inputs/Item.vue
+1 -1
@@ -68,7 +68,7 @@
68 <n-modal
69 v-model:show="showDetails"
70 preset="card"
71 - content-style="padding:0px"
71 + content-class="!p-0"
72 :style="{ maxWidth: 'min(600px, 90vw)', overflow: 'hidden' }"
73 :title="input.title"
74 :bordered="false"
frontend/src/components/graylog/Pipelines/PipeList.vue
+1 -1
@@ -29,7 +29,7 @@
29 <n-modal
30 v-model:show="showDetails"
31 preset="card"
32 - content-style="padding:0px"
32 + content-class="!p-0"
33 :style="{ maxWidth: 'min(600px, 90vw)', overflow: 'hidden' }"
34 :title="highlightPipe?.title"
35 :bordered="false"
frontend/src/components/graylog/Pipelines/Rule.vue
+1 -1
@@ -45,7 +45,7 @@
45 <n-modal
46 v-model:show="showDetails"
47 preset="card"
48 - content-style="padding:0px"
48 + content-class="!p-0"
49 :style="{ maxWidth: 'min(600px, 90vw)', overflow: 'hidden' }"
50 :title="rule.title"
51 :bordered="false"
frontend/src/components/graylog/Streams/List.vue
+1 -1
@@ -33,7 +33,7 @@
33 :item-count="total"
34 :simple="simpleMode"
35 />
36 - <n-popover overlap placement="right" style="padding-left: 0; padding-right: 0">
36 + <n-popover overlap placement="right" class="!px-0">
37 <template #trigger>
38 <div class="bg-color border-radius">
39 <n-button size="small">
frontend/src/components/logs/LogsList.vue
+1 -7
@@ -44,13 +44,7 @@
44 :item-count="total"
45 :simple="simpleMode"
46 />
47 - <n-popover
48 - :show="showFilters"
49 - trigger="manual"
50 - overlap
51 - placement="right"
52 - style="padding-left: 0; padding-right: 0"
53 - >
47 + <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="!px-0">
48 <template #trigger>
49 <div class="bg-color border-radius">
50 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
frontend/src/components/soc/SocAlerts/SocAlertAssetsItem.vue
+1 -1
@@ -61,7 +61,7 @@
61 <n-modal
62 v-model:show="showDetails"
63 preset="card"
64 - content-style="padding:0px"
64 + content-class="!p-0"
65 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(600px, 90vh)', overflow: 'hidden' }"
66 :title="`Assets #${asset.asset_id} - ${asset.asset_uuid}`"
67 :bordered="false"
frontend/src/components/soc/SocAlerts/SocAlertItem.vue
+1 -1
@@ -188,7 +188,7 @@
188 <n-modal
189 v-model:show="showDetails"
190 preset="card"
191 - content-style="padding:0px"
191 + content-class="!p-0"
192 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
193 :title="`SOC Alert: #${alert?.alert_id} - ${alert?.alert_uuid}`"
194 :bordered="false"
frontend/src/components/soc/SocAlerts/SocAlertItemActions.vue
+1 -1
@@ -30,7 +30,7 @@
30 <n-modal
31 v-model:show="showSocCaseDetails"
32 preset="card"
33 - content-style="padding:0px"
33 + content-class="!p-0"
34 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(250px, 90vh)', overflow: 'hidden' }"
35 :title="`SOC Case: #${caseId}`"
36 :bordered="false"
frontend/src/components/soc/SocCases/SocCaseAssetsItem.vue
+1 -1
@@ -53,7 +53,7 @@
53 <n-modal
54 v-model:show="showDetails"
55 preset="card"
56 - content-style="padding:0px"
56 + content-class="!p-0"
57 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(600px, 90vh)', overflow: 'hidden' }"
58 :title="`Assets #${asset.asset_id} - ${asset.asset_uuid}`"
59 :bordered="false"
frontend/src/components/soc/SocCases/SocCaseItem.vue
+2 -2
@@ -110,7 +110,7 @@
110 <n-modal
111 v-model:show="showSocAlertDetails"
112 preset="card"
113 - content-style="padding:0px"
113 + content-class="!p-0"
114 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(250px, 90vh)', overflow: 'hidden' }"
115 :title="`SOC Alert: #${baseInfo?.case_soc_id}`"
116 :bordered="false"
@@ -131,7 +131,7 @@
131 <n-modal
132 v-model:show="showDetails"
133 preset="card"
134 - content-style="padding:0px"
134 + content-class="!p-0"
135 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(550px, 90vh)', overflow: 'hidden' }"
136 :title="'SOC Case: ' + baseInfo?.case_uuid"
137 :bordered="false"
frontend/src/components/soc/SocCases/SocCaseNote.vue
+1 -1
@@ -55,7 +55,7 @@
55 <n-modal
56 v-model:show="showDetails"
57 preset="card"
58 - content-style="padding:0px"
58 + content-class="!p-0"
59 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(600px, 90vh)', overflow: 'hidden' }"
60 :title="`Note: #${note.note_id} - ${note.note_details.note_uuid}`"
61 :bordered="false"
frontend/src/components/soc/SocCases/SocCasesList.vue
+1 -7
@@ -36,13 +36,7 @@
36 :item-count="total"
37 :simple="simpleMode"
38 />
39 - <n-popover
40 - :show="showFilters"
41 - trigger="manual"
42 - overlap
43 - placement="right"
44 - style="padding-left: 0; padding-right: 0"
45 - >
39 + <n-popover :show="showFilters" trigger="manual" overlap placement="right" class="!px-0">
40 <template #trigger>
41 <div class="bg-color border-radius">
42 <n-badge :show="filtered" dot type="success" :offset="[-4, 0]">
frontend/src/components/soc/SocUsers/SocUserAlerts.vue
+1 -1
@@ -18,7 +18,7 @@
18 <n-modal
19 v-model:show="showSocAlertDetails"
20 preset="card"
21 - content-style="padding:0px"
21 + content-class="!p-0"
22 :style="{ maxWidth: 'min(800px, 90vw)', minHeight: 'min(250px, 90vh)', overflow: 'hidden' }"
23 :title="`SOC Alert: #${selectedAlertId}`"
24 :bordered="false"
frontend/src/directives/v-shiki.ts
+19 -4
@@ -1,11 +1,26 @@
1 import { codeToHtml } from "shiki"
2 import flourite from "flourite"
3 +import { decode } from "html-entities"
4
5 const vShiki = {
5 - created: async (el: HTMLElement, binding: { value: { theme: string; lang?: string } }) => {
6 - const html = await codeToHtml(el.children[0].innerHTML, {
7 - lang: binding?.value?.lang || flourite(el.children[0].innerHTML, { shiki: true }).language || "text",
8 - theme: binding?.value?.theme || ""
6 + created: async (
7 + el: HTMLElement,
8 + binding: { value: { theme?: "dark" | "light"; lang?: string; decode?: boolean } }
9 + ) => {
10 + const code = binding?.value?.decode ? decode(el.children[0].innerHTML) : el.children[0].innerHTML
11 +
12 + let flouriteDetect = null
13 + if (!binding?.value?.lang) {
14 + const fl = flourite(code, { shiki: true }).language
15 + if (fl !== "unknown") {
16 + flouriteDetect = fl
17 + }
18 + }
19 +
20 + const language = binding?.value?.lang || flouriteDetect || "text"
21 + const html = await codeToHtml(code, {
22 + lang: language,
23 + theme: binding?.value?.theme === "light" ? "slack-ochin" : "aurora-x"
24 })
25 el.innerHTML = html
26 }
frontend/src/types/agents.d.ts
+40
@@ -62,3 +62,43 @@ export enum VulnerabilityType {
62 export type OutdatedWazuhAgents = Agent[]
63
64 export type OutdatedVelociraptorAgents = Agent[]
65 +
66 +export interface AgentSca {
67 + description: string
68 + fail: number
69 + start_scan: Date
70 + references: string
71 + name: string
72 + pass: number
73 + score: number
74 + end_scan: Date
75 + policy_id: string
76 + total_checks: number
77 + hash_file: string
78 + invalid: number
79 +}
80 +
81 +export interface ScaPolicyResult {
82 + description: string
83 + id: number
84 + reason: string
85 + command: string
86 + rationale: string
87 + condition: "all" | "any" | "none"
88 + title: string
89 + result: "failed" | "not applicable" | "passed"
90 + policy_id: string
91 + remediation: string
92 + compliance: ScaPolicyResultCompliance[]
93 + rules: ScaPolicyResultRule[]
94 +}
95 +
96 +export interface ScaPolicyResultCompliance {
97 + value: string
98 + key: string
99 +}
100 +
101 +export interface ScaPolicyResultRule {
102 + type: "command" | "directory" | "file" | "numeric" | string
103 + rule: string
104 +}
frontend/src/views/agents/Overview.vue
+9 -3
@@ -46,7 +46,7 @@
46 </n-spin>
47 <n-card class="py-1 px-4 pb-4" content-style="padding:0">
48 <n-spin :show="loadingAgent">
49 - <n-tabs type="line" animated default-value="Overview">
49 + <n-tabs type="line" animated default-value="SCA">
50 <n-tab-pane name="Overview" tab="Overview" display-directive="show">
51 <div class="section">
52 <OverviewSection v-if="agent" :agent="agent" />
@@ -54,7 +54,12 @@
54 </n-tab-pane>
55 <n-tab-pane name="Vulnerabilities" tab="Vulnerabilities" display-directive="show:lazy">
56 <div class="section">
57 - <VulnerabilitiesSection v-if="agent" :agent="agent" />
57 + <VulnerabilitiesGrid v-if="agent" :agent="agent" />
58 + </div>
59 + </n-tab-pane>
60 + <n-tab-pane name="SCA" tab="SCA" display-directive="show:lazy">
61 + <div class="section">
62 + <ScaTable v-if="agent" :agent="agent" />
63 </div>
64 </n-tab-pane>
65 <n-tab-pane name="Cases" tab="Cases" display-directive="show:lazy">
@@ -113,7 +118,8 @@ import Api from "@/api"
118 import { AgentStatus, type Agent } from "@/types/agents.d"
119 import { handleDeleteAgent, toggleAgentCritical } from "@/components/agents/utils"
120 import { useRouter } from "vue-router"
116 -import VulnerabilitiesSection from "@/components/agents/VulnerabilitiesSection.vue"
121 +import VulnerabilitiesGrid from "@/components/agents/vulnerabilities/VulnerabilitiesGrid.vue"
122 +import ScaTable from "@/components/agents/sca/ScaTable.vue"
123 import AlertsList from "@/components/alerts/AlertsList.vue"
124 import OverviewSection from "@/components/agents/OverviewSection.vue"
125 import AgentCases from "@/components/agents/AgentCases.vue"
frontend/src/views/graylog/Pipelines.vue
+1 -1
@@ -14,7 +14,7 @@
14 <n-modal
15 v-model:show="showDetails"
16 preset="card"
17 - content-style="padding:0px"
17 + content-class="!p-0"
18 :style="{ maxWidth: 'min(600px, 90vw)', overflow: 'hidden' }"
19 :title="highlightPipe?.title"
20 :bordered="false"