@cryptotaxi247 / CoPilot / commits / 86417c90

Vuln csv export (#280)

* bump docker compose in readme * feat: Add optional assigned_to field to Alert and Case models * feat: Update branch name in Docker workflow * Update branch name in Docker workflow * update dependencies * add agent Vulnerabilities Download feature * feat: Update agent vulnerabilities collection to support multiple severity levels The code changes in this commit modify the `collect_agent_vulnerabilities` function in the `vulnerabilities.py` file to support collecting agent vulnerabilities for multiple severity levels. Previously, the function only collected vulnerabilities for a single severity level. Now, it can collect vulnerabilities for all severity levels or a specific severity level specified by the user. Recent user commits: - Update branch name in Docker workflow - Add alert title payload to handle_customer_notifications function Recent repository commits: - feat: Update branch name in Docker workflow - feat: Add alert title payload to handle_customer_notifications function * update Vulnerability Severity Type * feat: Add Sigma queries collection to scheduler This commit adds the functionality to collect Sigma queries to the scheduler. It modifies the `scheduler.py` file to include a new job for invoking Sigma queries collection. The `invoke_sigma_queries_collect` function is added to the `invoke_sigma_queries.py` file to handle the collection of Sigma enabled queries. This feature allows for the analysis of Sigma queries via the scheduler. Recent user commits: - Update branch name in Docker workflow - Add alert title payload to handle_customer_notifications function Recent repository commits: - feat: Update branch name in Docker workflow - feat: Add alert title payload to handle_customer_notifications function * feat: Disable pre-existing jobs for alert monitoring prior to Graylog Alert Integration This commit modifies the `schedule_enabled_jobs` function in the `scheduler.py` file to disable pre-existing jobs for alert monitoring prior to the Graylog Alert Integration. The list of job IDs to disable includes "invoke_wazuh_monitoring_alert", "invoke_suricata_monitoring_alert", "invoke_office365_exchange_online_alert", and "invoke_office365_threat_intel_alert". This ensures that these jobs are disabled before scheduling any enabled jobs. Recent user commits: - Update branch name in Docker workflow - Add alert title payload to handle_customer_notifications function Recent repository commits: - feat: Update branch name in Docker workflow - feat: Add alert title payload to handle_customer_notifications function * update vue version * add sca Results Download feature * velo collect file debugging --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Sep 3, 2024 at 11:30 UTC 86417c90c19f89eecc4a7c7c75fd109d003cdbf2
15 files changed +355 -142
README.md
+1 -1
@@ -83,7 +83,7 @@ systemctl restart docker
83
84 ```bash
85 # Clone the CoPilot repository
86 -wget https://raw.githubusercontent.com/socfortress/CoPilot/v0.1.0/docker-compose.yml
86 +wget https://raw.githubusercontent.com/socfortress/CoPilot/v0.1.1/docker-compose.yml
87
88 # Edit the docker-compose.yml file to set the server name and/or the services you want to use
89
backend/app/agents/wazuh/schema/agents.py
+1
@@ -12,6 +12,7 @@ class VulnSeverity(Enum):
12 Medium = "Medium"
13 High = "High"
14 Critical = "Critical"
15 + All = "All"
16
17
18 class WazuhAgent(BaseModel):
backend/app/agents/wazuh/services/vulnerabilities.py
+69 -12
@@ -10,6 +10,37 @@ from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_cl
10 from app.connectors.wazuh_manager.utils.universal import send_get_request
11
12
13 +# async def collect_agent_vulnerabilities(agent_id: str, vulnerability_severity: str):
14 +# """
15 +# Collect agent vulnerabilities from Wazuh Manager.
16 +# Used when Wazuh Manager is below 4.8.0
17 +
18 +# Args:
19 +# agent_id (str): The ID of the agent.
20 +
21 +# Returns:
22 +# WazuhAgentVulnerabilitiesResponse: An object containing the collected vulnerabilities.
23 +
24 +# Raises:
25 +# HTTPException: If there is an error collecting the vulnerabilities.
26 +# """
27 +# logger.info(f"Collecting agent {agent_id} vulnerabilities from Wazuh Manager")
28 +# agent_vulnerabilities = await send_get_request(
29 +# endpoint=f"/vulnerability/{agent_id}",
30 +# params={"severity": vulnerability_severity},
31 +# )
32 +# if agent_vulnerabilities["success"] is False:
33 +# raise HTTPException(status_code=500, detail=agent_vulnerabilities["message"])
34 +
35 +# processed_vulnerabilities = process_agent_vulnerabilities(
36 +# agent_vulnerabilities["data"],
37 +# )
38 +# return WazuhAgentVulnerabilitiesResponse(
39 +# vulnerabilities=processed_vulnerabilities,
40 +# success=True,
41 +# message="Vulnerabilities collected successfully",
42 +# )
43 +
44 async def collect_agent_vulnerabilities(agent_id: str, vulnerability_severity: str):
45 """
46 Collect agent vulnerabilities from Wazuh Manager.
@@ -17,6 +48,7 @@ async def collect_agent_vulnerabilities(agent_id: str, vulnerability_severity: s
48
49 Args:
50 agent_id (str): The ID of the agent.
51 + vulnerability_severity (str): The severity of the vulnerabilities to collect.
52
53 Returns:
54 WazuhAgentVulnerabilitiesResponse: An object containing the collected vulnerabilities.
@@ -25,16 +57,21 @@ async def collect_agent_vulnerabilities(agent_id: str, vulnerability_severity: s
57 HTTPException: If there is an error collecting the vulnerabilities.
58 """
59 logger.info(f"Collecting agent {agent_id} vulnerabilities from Wazuh Manager")
28 - agent_vulnerabilities = await send_get_request(
29 - endpoint=f"/vulnerability/{agent_id}",
30 - params={"severity": vulnerability_severity},
31 - )
32 - if agent_vulnerabilities["success"] is False:
33 - raise HTTPException(status_code=500, detail=agent_vulnerabilities["message"])
60
35 - processed_vulnerabilities = process_agent_vulnerabilities(
36 - agent_vulnerabilities["data"],
37 - )
61 + severities = ["Low", "Medium", "High", "Critical"] if vulnerability_severity == "All" else [vulnerability_severity]
62 +
63 + agent_vulnerabilities = []
64 + for severity in severities:
65 + response = await send_get_request(
66 + endpoint=f"/vulnerability/{agent_id}",
67 + params={"severity": severity},
68 + )
69 + if response["success"] is False:
70 + raise HTTPException(status_code=500, detail=response["message"])
71 + agent_vulnerabilities.extend(response["data"])
72 +
73 + processed_vulnerabilities = process_agent_vulnerabilities(agent_vulnerabilities)
74 +
75 return WazuhAgentVulnerabilitiesResponse(
76 vulnerabilities=processed_vulnerabilities,
77 success=True,
@@ -107,9 +144,29 @@ def filter_vulnerabilities_indices(indices_list):
144 async def collect_vulnerabilities(es, vulnerabilities_indices, agent_id, vulnerability_severity="Critical"):
145 agent_vulnerabilities = []
146 for index in vulnerabilities_indices:
110 - query = {
111 - "query": {"bool": {"must": [{"match": {"agent.id": agent_id}}, {"match": {"vulnerability.severity": vulnerability_severity}}]}},
112 - }
147 + if vulnerability_severity == "All":
148 + query = {
149 + "query": {
150 + "bool": {
151 + "must": [
152 + {"match": {"agent.id": agent_id}},
153 + {"terms": {"vulnerability.severity": ["Low", "Medium", "High", "Critical"]}}
154 + ]
155 + }
156 + }
157 + }
158 + else:
159 + query = {
160 + "query": {
161 + "bool": {
162 + "must": [
163 + {"match": {"agent.id": agent_id}},
164 + {"match": {"vulnerability.severity": vulnerability_severity}}
165 + ]
166 + }
167 + }
168 + }
169 +
170 page = es.search(index=index, body=query, scroll="2m")
171 sid = page["_scroll_id"]
172 scroll_size = len(page["hits"]["hits"])
backend/app/connectors/velociraptor/services/artifacts.py
+2 -1
@@ -191,7 +191,7 @@ async def run_file_collection(
191 f" 'artifact': '{collect_artifact_body.artifact_name}',"
192 f" 'parameters': {{"
193 f" 'env': ["
194 - f" {{'key': 'collectionSpec', 'value': 'Glob\n{collect_artifact_body.file}\n'}},"
194 + f" {{'key': 'collectionSpec', 'value': '{collect_artifact_body.file}'}},"
195 f" {{'key': 'Root', 'value': '{collect_artifact_body.root_disk}'}}"
196 f" ]"
197 f" }}"
@@ -241,6 +241,7 @@ async def run_file_collection(
241 )
242
243
244 +
245 async def run_remote_command(run_command_body: RunCommandBody) -> RunCommandResponse:
246 """
247 Run a remote command on a client.
backend/app/connectors/velociraptor/utils/universal.py
+3
@@ -181,6 +181,9 @@ class UniversalService:
181 for response in self.stub.Query(client_request, timeout=30):
182 if response.Response:
183 results += json.loads(response.Response)
184 + elif response.log:
185 + logger.info(f"Log: {response.log}")
186 +
187 return {
188 "success": True,
189 "message": "Successfully executed query",
backend/app/incidents/models.py
+2 -2
@@ -22,7 +22,7 @@ class Alert(SQLModel, table=True):
22 customer_code: str = Field(max_length=50, nullable=False)
23 time_closed: Optional[datetime] = Field(default=None)
24 source: str = Field(max_length=50, nullable=False)
25 - assigned_to: str = Field(max_length=50, nullable=True)
25 + assigned_to: Optional[str] = Field(max_length=50, nullable=True)
26
27 comments: List["Comment"] = Relationship(back_populates="alert")
28 assets: List["Asset"] = Relationship(back_populates="alert")
@@ -125,7 +125,7 @@ class Case(SQLModel, table=True):
125 case_description: str = Field(sa_column=Text)
126 case_creation_time: datetime = Field(default_factory=datetime.utcnow)
127 case_status: str = Field(max_length=50, nullable=False)
128 - assigned_to: str = Field(max_length=50, nullable=True)
128 + assigned_to: Optional[str] = Field(max_length=50, nullable=True)
129
130 alerts: List["CaseAlertLink"] = Relationship(back_populates="case")
131
backend/app/schedulers/scheduler.py
+36 -9
@@ -15,6 +15,7 @@ from app.schedulers.models.scheduler import CreateSchedulerRequest
15 from app.schedulers.models.scheduler import JobMetadata
16 from app.schedulers.services.agent_sync import agent_sync
17 from app.schedulers.services.invoke_alert_creation import invoke_alert_creation_collect
18 +from app.schedulers.services.invoke_sigma_queries import invoke_sigma_queries_collect
19 from app.schedulers.services.invoke_carbonblack import (
20 invoke_carbonblack_integration_collect,
21 )
@@ -137,6 +138,12 @@ async def initialize_job_metadata():
138 "function": invoke_alert_creation_collect,
139 "description": "Invokes alert creation collection.",
140 },
141 + {
142 + "job_id": "invoke_sigma_queries_collect",
143 + "time_interval": 5,
144 + "function": invoke_sigma_queries_collect,
145 + "description": "Invokes Sigma queries collection.",
146 + },
147 # {"job_id": "invoke_mimecast_integration", "time_interval": 5, "function": invoke_mimecast_integration}
148 ]
149 for job in known_jobs:
@@ -158,21 +165,40 @@ async def initialize_job_metadata():
165 job_metadata.enabled = True
166 await session.commit()
167
168 +async def disable_job(session, job_id):
169 + """
170 + Disables a job in the database based on the job ID.
171 +
172 + Args:
173 + session (AsyncSession): The database session.
174 + job_id (str): The ID of the job to disable.
175 + """
176 + stmt = select(JobMetadata).where(JobMetadata.job_id == job_id)
177 + result = await session.execute(stmt)
178 + job_metadata = result.scalars().one_or_none()
179 + logger.info(f"Job Metadata: {job_metadata}")
180 + if job_metadata:
181 + logger.info(f"Disabling job: {job_id}")
182 + job_metadata.enabled = False
183 + await session.commit()
184 +
185
186 async def schedule_enabled_jobs(scheduler):
187 """
188 Schedules jobs that are enabled in the database.
189 """
190 async with AsyncSession(async_engine) as session:
167 - # ! First disable the job of `invoke_wazuh_monitoring_alert` if it is enabled
168 - # TODO ! Inefficient as hell but I will come back to this later
169 - stmt = select(JobMetadata).where(JobMetadata.job_id == "invoke_wazuh_monitoring_alert")
170 - result = await session.execute(stmt)
171 - job_metadata = result.scalars().one_or_none()
172 - if job_metadata:
173 - logger.info("Disabling job: invoke_wazuh_monitoring_alert")
174 - job_metadata.enabled = False
175 - await session.commit()
191 + # ! First prexisiting jobs for alert monitoring prior to Graylog Alert Integration ! #
192 + job_ids_to_disable = [
193 + "invoke_wazuh_monitoring_alert",
194 + "invoke_suricata_monitoring_alert",
195 + "invoke_office365_exchange_online_alert",
196 + "invoke_office365_threat_intel_alert"
197 + ]
198 +
199 + # Disable each job in the list
200 + for job_id in job_ids_to_disable:
201 + await disable_job(session, job_id)
202
203 stmt = select(JobMetadata).where(JobMetadata.enabled == True)
204 result = await session.execute(stmt)
@@ -216,6 +242,7 @@ def get_function_by_name(function_name: str):
242 "agent_sync": agent_sync,
243 "wazuh_index_fields_resize": resize_wazuh_index_fields,
244 "invoke_alert_creation_collect": invoke_alert_creation_collect,
245 + "invoke_sigma_queries_collect": invoke_sigma_queries_collect,
246 "invoke_mimecast_integration": invoke_mimecast_integration,
247 "invoke_mimecast_integration_ttp": invoke_mimecast_integration_ttp,
248 "invoke_sap_siem_integration_collection": invoke_sap_siem_integration_collection,
backend/app/schedulers/services/invoke_sigma_queries.py new
+32
@@ -0,0 +1,32 @@
1 +from datetime import datetime
2 +
3 +from loguru import logger
4 +from sqlalchemy.future import select
5 +
6 +from app.db.db_session import get_db_session
7 +from app.connectors.wazuh_indexer.routes.sigma import run_active_sigma_queries_endpoint
8 +from app.schedulers.models.scheduler import JobMetadata
9 +
10 +
11 +async def invoke_sigma_queries_collect():
12 + """
13 + Invokes the analysis of Sigma enabled queries via the scheduler.
14 +
15 + If the token retrieval fails, it prints a failure message. If the job metadata for
16 + 'invoke_sigma_queries_collect' does not exist, it prints a message indicating the absence of the metadata.
17 + """
18 + logger.info("Invoking sigma queries collection via scheduler...")
19 + async with get_db_session() as session:
20 + await run_active_sigma_queries_endpoint(index_name="wazuh*", db=session)
21 +
22 + stmt = select(JobMetadata).where(JobMetadata.job_id == "invoke_sigma_queries_collect")
23 + result = await session.execute(stmt)
24 + job_metadata = result.scalars().first()
25 +
26 + if job_metadata:
27 + job_metadata.last_success = datetime.utcnow()
28 + session.add(job_metadata)
29 + await session.commit() # Asynchronously commit the transaction
30 + logger.info("Updated job metadata with the last success timestamp.")
31 + else:
32 + logger.warning("JobMetadata for 'invoke_sigma_queries_collect' not found.")
frontend/package-lock.json
+78 -78
@@ -38,7 +38,7 @@
38 "secure-ls": "^2.0.0",
39 "shiki": "^1.16.1",
40 "validator": "^13.12.0",
41 - "vue": "^3.4.38",
41 + "vue": "^3.5.0",
42 "vue-advanced-cropper": "^2.8.9",
43 "vue-highlight-words": "^3.0.1",
44 "vue-i18n": "^9.14.0",
@@ -70,15 +70,15 @@
70 "cypress": "^13.14.1",
71 "eslint": "^9.9.1",
72 "eslint-plugin-cypress": "^3.5.0",
73 - "eslint-plugin-vue": "^9.27.0",
73 + "eslint-plugin-vue": "^9.28.0",
74 "flourite": "^1.3.0",
75 "fs-extra": "^11.2.0",
76 "globals": "^15.9.0",
77 "jsdom": "^25.0.0",
78 "json5": "^2.2.3",
79 "npm-run-all2": "^6.2.2",
80 - "picocolors": "^1.0.1",
81 - "postcss": "^8.4.43",
80 + "picocolors": "^1.1.0",
81 + "postcss": "^8.4.44",
82 "prettier": "^3.3.3",
83 "sass": "^1.77.8",
84 "start-server-and-test": "^2.0.5",
@@ -87,7 +87,7 @@
87 "taze": "^0.16.7",
88 "type-fest": "^4.26.0",
89 "unplugin-vue-components": "^0.27.4",
90 - "vite": "^5.4.2",
90 + "vite": "^5.4.3",
91 "vite-bundle-analyzer": "^0.10.6",
92 "vite-bundle-visualizer": "^1.2.1",
93 "vite-plugin-vue-devtools": "^7.3.9",
@@ -3004,49 +3004,49 @@
3004 }
3005 },
3006 "node_modules/@vue/compiler-core": {
3007 - "version": "3.4.38",
3008 - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.38.tgz",
3009 - "integrity": "sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==",
3007 + "version": "3.5.0",
3008 + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.0.tgz",
3009 + "integrity": "sha512-ja7cpqAOfw4tyFAxgBz70Z42miNDeaqTxExTsnXDLomRpqfyCgyvZvFp482fmsElpfvsoMJUsvzULhvxUTW6Iw==",
3010 "dependencies": {
3011 - "@babel/parser": "^7.24.7",
3012 - "@vue/shared": "3.4.38",
3011 + "@babel/parser": "^7.25.3",
3012 + "@vue/shared": "3.5.0",
3013 "entities": "^4.5.0",
3014 "estree-walker": "^2.0.2",
3015 "source-map-js": "^1.2.0"
3016 }
3017 },
3018 "node_modules/@vue/compiler-dom": {
3019 - "version": "3.4.38",
3020 - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.38.tgz",
3021 - "integrity": "sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==",
3019 + "version": "3.5.0",
3020 + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.0.tgz",
3021 + "integrity": "sha512-xYjUybWZXl+1R/toDy815i4PbeehL2hThiSGkcpmIOCy2HoYyeeC/gAWK/Y/xsoK+GSw198/T5O31bYuQx5uvQ==",
3022 "dependencies": {
3023 - "@vue/compiler-core": "3.4.38",
3024 - "@vue/shared": "3.4.38"
3023 + "@vue/compiler-core": "3.5.0",
3024 + "@vue/shared": "3.5.0"
3025 }
3026 },
3027 "node_modules/@vue/compiler-sfc": {
3028 - "version": "3.4.38",
3029 - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.38.tgz",
3030 - "integrity": "sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==",
3031 - "dependencies": {
3032 - "@babel/parser": "^7.24.7",
3033 - "@vue/compiler-core": "3.4.38",
3034 - "@vue/compiler-dom": "3.4.38",
3035 - "@vue/compiler-ssr": "3.4.38",
3036 - "@vue/shared": "3.4.38",
3028 + "version": "3.5.0",
3029 + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.0.tgz",
3030 + "integrity": "sha512-B9DgLtrqok2GLuaFjLlSL15ZG3ZDBiitUH1ecex9guh/ZcA5MCdwuVE6nsfQxktuZY/QY0awJ35/ripIviCQTQ==",
3031 + "dependencies": {
3032 + "@babel/parser": "^7.25.3",
3033 + "@vue/compiler-core": "3.5.0",
3034 + "@vue/compiler-dom": "3.5.0",
3035 + "@vue/compiler-ssr": "3.5.0",
3036 + "@vue/shared": "3.5.0",
3037 "estree-walker": "^2.0.2",
3038 - "magic-string": "^0.30.10",
3039 - "postcss": "^8.4.40",
3038 + "magic-string": "^0.30.11",
3039 + "postcss": "^8.4.44",
3040 "source-map-js": "^1.2.0"
3041 }
3042 },
3043 "node_modules/@vue/compiler-ssr": {
3044 - "version": "3.4.38",
3045 - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.38.tgz",
3046 - "integrity": "sha512-YXznKFQ8dxYpAz9zLuVvfcXhc31FSPFDcqr0kyujbOwNhlmaNvL2QfIy+RZeJgSn5Fk54CWoEUeW+NVBAogGaw==",
3044 + "version": "3.5.0",
3045 + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.0.tgz",
3046 + "integrity": "sha512-E263QZmA1dqRd7c3u/sWTLRMpQOT0aZ8av/L9SoD/v/BVMZaWFHPUUBswS+bzrfvG2suJF8vSLKx6k6ba5SUdA==",
3047 "dependencies": {
3048 - "@vue/compiler-dom": "3.4.38",
3049 - "@vue/shared": "3.4.38"
3048 + "@vue/compiler-dom": "3.5.0",
3049 + "@vue/shared": "3.5.0"
3050 }
3051 },
3052 "node_modules/@vue/compiler-vue2": {
@@ -3169,49 +3169,49 @@
3169 }
3170 },
3171 "node_modules/@vue/reactivity": {
3172 - "version": "3.4.38",
3173 - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.38.tgz",
3174 - "integrity": "sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==",
3172 + "version": "3.5.0",
3173 + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.0.tgz",
3174 + "integrity": "sha512-Ew3F5riP3B3ZDGjD3ZKb9uZylTTPSqt8hAf4sGbvbjrjDjrFb3Jm15Tk1/w7WwTE5GbQ2Qhwxx1moc9hr8A/OQ==",
3175 "dependencies": {
3176 - "@vue/shared": "3.4.38"
3176 + "@vue/shared": "3.5.0"
3177 }
3178 },
3179 "node_modules/@vue/runtime-core": {
3180 - "version": "3.4.38",
3181 - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.38.tgz",
3182 - "integrity": "sha512-21z3wA99EABtuf+O3IhdxP0iHgkBs1vuoCAsCKLVJPEjpVqvblwBnTj42vzHRlWDCyxu9ptDm7sI2ZMcWrQqlA==",
3180 + "version": "3.5.0",
3181 + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.0.tgz",
3182 + "integrity": "sha512-mQyW0F9FaNRdt8ghkAs+BMG3iQ7LGgWKOpkzUzR5AI5swPNydHGL5hvVTqFaeMzwecF1g0c86H4yFQsSxJhH1w==",
3183 "dependencies": {
3184 - "@vue/reactivity": "3.4.38",
3185 - "@vue/shared": "3.4.38"
3184 + "@vue/reactivity": "3.5.0",
3185 + "@vue/shared": "3.5.0"
3186 }
3187 },
3188 "node_modules/@vue/runtime-dom": {
3189 - "version": "3.4.38",
3190 - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.38.tgz",
3191 - "integrity": "sha512-afZzmUreU7vKwKsV17H1NDThEEmdYI+GCAK/KY1U957Ig2NATPVjCROv61R19fjZNzMmiU03n79OMnXyJVN0UA==",
3189 + "version": "3.5.0",
3190 + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.0.tgz",
3191 + "integrity": "sha512-NQQXjpdXgyYVJ2M56FJ+lSJgZiecgQ2HhxhnQBN95FymXegRNY/N2htI7vOTwpP75pfxhIeYOJ8mE8sW8KAW6A==",
3192 "dependencies": {
3193 - "@vue/reactivity": "3.4.38",
3194 - "@vue/runtime-core": "3.4.38",
3195 - "@vue/shared": "3.4.38",
3193 + "@vue/reactivity": "3.5.0",
3194 + "@vue/runtime-core": "3.5.0",
3195 + "@vue/shared": "3.5.0",
3196 "csstype": "^3.1.3"
3197 }
3198 },
3199 "node_modules/@vue/server-renderer": {
3200 - "version": "3.4.38",
3201 - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.38.tgz",
3202 - "integrity": "sha512-NggOTr82FbPEkkUvBm4fTGcwUY8UuTsnWC/L2YZBmvaQ4C4Jl/Ao4HHTB+l7WnFCt5M/dN3l0XLuyjzswGYVCA==",
3200 + "version": "3.5.0",
3201 + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.0.tgz",
3202 + "integrity": "sha512-HyDIFUg+l7L4PKrEnJlCYWHUOlm6NxZhmSxIefZ5MTYjkIPfDfkwhX7hqxAQHfgIAE1uLMLQZwuNR/ozI0NhZg==",
3203 "dependencies": {
3204 - "@vue/compiler-ssr": "3.4.38",
3205 - "@vue/shared": "3.4.38"
3204 + "@vue/compiler-ssr": "3.5.0",
3205 + "@vue/shared": "3.5.0"
3206 },
3207 "peerDependencies": {
3208 - "vue": "3.4.38"
3208 + "vue": "3.5.0"
3209 }
3210 },
3211 "node_modules/@vue/shared": {
3212 - "version": "3.4.38",
3213 - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.38.tgz",
3214 - "integrity": "sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw=="
3212 + "version": "3.5.0",
3213 + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.0.tgz",
3214 + "integrity": "sha512-m9IgiteBpCkFaMNwCOBkFksA7z8QiKc30ooRuoXWUFRDu0mGyNPlFHmbncF0/Kra1RlX8QrmBbRaIxVvikaR0Q=="
3215 },
3216 "node_modules/@vue/test-utils": {
3217 "version": "2.4.6",
@@ -5426,16 +5426,16 @@
5426 "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
5427 },
5428 "node_modules/eslint-plugin-vue": {
5429 - "version": "9.27.0",
5430 - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.27.0.tgz",
5431 - "integrity": "sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==",
5429 + "version": "9.28.0",
5430 + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz",
5431 + "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==",
5432 "dependencies": {
5433 "@eslint-community/eslint-utils": "^4.4.0",
5434 "globals": "^13.24.0",
5435 "natural-compare": "^1.4.0",
5436 "nth-check": "^2.1.1",
5437 "postcss-selector-parser": "^6.0.15",
5438 - "semver": "^7.6.0",
5438 + "semver": "^7.6.3",
5439 "vue-eslint-parser": "^9.4.3",
5440 "xml-name-validator": "^4.0.0"
5441 },
@@ -8742,9 +8742,9 @@
8742 "dev": true
8743 },
8744 "node_modules/picocolors": {
8745 - "version": "1.0.1",
8746 - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
8747 - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
8745 + "version": "1.1.0",
8746 + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
8747 + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw=="
8748 },
8749 "node_modules/picomatch": {
8750 "version": "2.3.1",
@@ -8889,9 +8889,9 @@
8889 }
8890 },
8891 "node_modules/postcss": {
8892 - "version": "8.4.43",
8893 - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.43.tgz",
8894 - "integrity": "sha512-gJAQVYbh5R3gYm33FijzCZj7CHyQ3hWMgJMprLUlIYqCwTeZhBQ19wp0e9mA25BUbEvY5+EXuuaAjqQsrBxQBQ==",
8892 + "version": "8.4.44",
8893 + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.44.tgz",
8894 + "integrity": "sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==",
8895 "funding": [
8896 {
8897 "type": "opencollective",
@@ -11157,13 +11157,13 @@
11157 }
11158 },
11159 "node_modules/vite": {
11160 - "version": "5.4.2",
11161 - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
11162 - "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
11160 + "version": "5.4.3",
11161 + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.3.tgz",
11162 + "integrity": "sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==",
11163 "dev": true,
11164 "dependencies": {
11165 "esbuild": "^0.21.3",
11166 - "postcss": "^8.4.41",
11166 + "postcss": "^8.4.43",
11167 "rollup": "^4.20.0"
11168 },
11169 "bin": {
@@ -11752,15 +11752,15 @@
11752 "dev": true
11753 },
11754 "node_modules/vue": {
11755 - "version": "3.4.38",
11756 - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.38.tgz",
11757 - "integrity": "sha512-f0ZgN+mZ5KFgVv9wz0f4OgVKukoXtS3nwET4c2vLBGQR50aI8G0cqbFtLlX9Yiyg3LFGBitruPHt2PxwTduJEw==",
11758 - "dependencies": {
11759 - "@vue/compiler-dom": "3.4.38",
11760 - "@vue/compiler-sfc": "3.4.38",
11761 - "@vue/runtime-dom": "3.4.38",
11762 - "@vue/server-renderer": "3.4.38",
11763 - "@vue/shared": "3.4.38"
11755 + "version": "3.5.0",
11756 + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.0.tgz",
11757 + "integrity": "sha512-1t70favYoFijwfWJ7g81aTd32obGaAnKYE9FNyMgnEzn3F4YncRi/kqAHHKloG0VXTD8vBYMhbgLKCA+Sk6QDw==",
11758 + "dependencies": {
11759 + "@vue/compiler-dom": "3.5.0",
11760 + "@vue/compiler-sfc": "3.5.0",
11761 + "@vue/runtime-dom": "3.5.0",
11762 + "@vue/server-renderer": "3.5.0",
11763 + "@vue/shared": "3.5.0"
11764 },
11765 "peerDependencies": {
11766 "typescript": "*"
frontend/package.json
+6 -6
@@ -60,7 +60,7 @@
60 "secure-ls": "^2.0.0",
61 "shiki": "^1.16.1",
62 "validator": "^13.12.0",
63 - "vue": "^3.4.38",
63 + "vue": "^3.5.0",
64 "vue-advanced-cropper": "^2.8.9",
65 "vue-highlight-words": "^3.0.1",
66 "vue-i18n": "^9.14.0",
@@ -92,15 +92,15 @@
92 "cypress": "^13.14.1",
93 "eslint": "^9.9.1",
94 "eslint-plugin-cypress": "^3.5.0",
95 - "eslint-plugin-vue": "^9.27.0",
95 + "eslint-plugin-vue": "^9.28.0",
96 "flourite": "^1.3.0",
97 "fs-extra": "^11.2.0",
98 "globals": "^15.9.0",
99 "jsdom": "^25.0.0",
100 "json5": "^2.2.3",
101 "npm-run-all2": "^6.2.2",
102 - "picocolors": "^1.0.1",
103 - "postcss": "^8.4.43",
102 + "picocolors": "^1.1.0",
103 + "postcss": "^8.4.44",
104 "prettier": "^3.3.3",
105 "sass": "^1.77.8",
106 "start-server-and-test": "^2.0.5",
@@ -109,7 +109,7 @@
109 "taze": "^0.16.7",
110 "type-fest": "^4.26.0",
111 "unplugin-vue-components": "^0.27.4",
112 - "vite": "^5.4.2",
112 + "vite": "^5.4.3",
113 "vite-bundle-analyzer": "^0.10.6",
114 "vite-bundle-visualizer": "^1.2.1",
115 "vite-plugin-vue-devtools": "^7.3.9",
@@ -139,4 +139,4 @@
139 "engines": {
140 "node": ">=18.0.0"
141 }
142 -}
142 +}
\ No newline at end of file
frontend/src/api/endpoints/agents.ts
+10 -3
@@ -13,7 +13,7 @@ export interface AgentPayload {
13 velociraptor_id: string
14 }
15
16 -export type VulnerabilitySeverityType = "Low" | "Medium" | "High" | "Critical"
16 +export type VulnerabilitySeverityType = "Low" | "Medium" | "High" | "Critical" | "All"
17
18 export default {
19 getAgents(agentId?: string) {
@@ -31,11 +31,15 @@ export default {
31 syncAgents() {
32 return HttpClient.post<FlaskBaseResponse>(`/agents/sync`)
33 },
34 - agentVulnerabilities(agentId: string, severity: VulnerabilitySeverityType) {
34 + agentVulnerabilities(agentId: string, severity: VulnerabilitySeverityType, signal?: AbortSignal) {
35 return HttpClient.get<FlaskBaseResponse & { vulnerabilities: AgentVulnerabilities[] }>(
36 - `/agents/${agentId}/vulnerabilities/${severity}`
36 + `/agents/${agentId}/vulnerabilities/${severity}`,
37 + signal ? { signal } : {}
38 )
39 },
40 + agentVulnerabilitiesDownload(agentId: string, severity: VulnerabilitySeverityType) {
41 + return HttpClient.get<string>(`/agents/${agentId}/csv/vulnerabilities/${severity}`)
42 + },
43 getSocCases(agentId: string | number, signal?: AbortSignal) {
44 return HttpClient.get<FlaskBaseResponse & { case_ids: number[] }>(
45 `/agents/${agentId}/soc_cases`,
@@ -54,6 +58,9 @@ export default {
58 signal ? { signal } : {}
59 )
60 },
61 + scaResultsDownload(agentId: string | number, policyId: string) {
62 + return HttpClient.get<string>(`/agents/${agentId}/csv/sca/${policyId}`)
63 + },
64 updateAgent(agentId: string, payload: AgentPayload) {
65 return HttpClient.put<FlaskBaseResponse>(
66 `/agents/${agentId}/update`,
frontend/src/components/agents/AgentCard.vue
+1 -2
@@ -3,6 +3,7 @@
3 class="agent-card py-3 px-4"
4 :class="{ critical: agent.critical_asset, 'bg-secondary': bgSecondary }"
5 content-style="padding:0"
6 + bordered
7 >
8 <n-spin :show="loading">
9 <div class="wrapper">
@@ -144,12 +145,10 @@ function toggleCritical(agentId: string, criticalStatus: boolean) {
145 .agent-card {
146 container-type: inline-size;
147 overflow: hidden;
147 - border: 2px solid transparent;
148 max-width: 100%;
149 box-sizing: border-box;
150 cursor: pointer;
151 transition: all 0.3s;
152 - border: var(--border-small-050);
152
153 &.bg-secondary {
154 background-color: var(--bg-secondary-color);
frontend/src/components/agents/sca/ScaTable.vue
+52 -10
@@ -16,9 +16,25 @@
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>
19 + <div class="flex items-center gap-2">
20 + <n-tooltip trigger="hover">
21 + <template #trigger>
22 + <n-button size="small" @click="showScaDetails(item)">
23 + <template #icon><Icon :name="InfoIcon"></Icon></template>
24 + </n-button>
25 + </template>
26 + Details
27 + </n-tooltip>
28 +
29 + <n-tooltip trigger="hover">
30 + <template #trigger>
31 + <n-button size="small" @click="scaDownload(item)" :loading="item.downloading">
32 + <template #icon><Icon :name="DownloadIcon"></Icon></template>
33 + </n-button>
34 + </template>
35 + Download CSV
36 + </n-tooltip>
37 + </div>
38 </td>
39 <td>
40 <div class="flex flex-col gap-1">
@@ -79,18 +95,20 @@
95
96 <script setup lang="ts">
97 import { ref, onBeforeMount, toRefs } from "vue"
98 +import { NTooltip, NSpin, NEmpty, NScrollbar, NTable, NButton, NPopover, NModal, useMessage } from "naive-ui"
99 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"
100 +import ScaItem from "./ScaItem.vue"
101 import Icon from "@/components/common/Icon.vue"
102 +import { saveAs } from "file-saver"
103 import _truncate from "lodash/truncate"
89 -import ScaItem from "./ScaItem.vue"
104 +import { useSettingsStore } from "@/stores/settings"
105 +import { formatDate } from "@/utils"
106 +import { type Agent, type AgentSca } from "@/types/agents.d"
107
108 interface SCAExt extends AgentSca {
109 end_scan_text?: string
110 extract?: string
111 + downloading: boolean
112 }
113
114 const props = defineProps<{
@@ -98,6 +116,7 @@ const props = defineProps<{
116 }>()
117 const { agent } = toRefs(props)
118
119 +const DownloadIcon = "carbon:document-download"
120 const InfoIcon = "carbon:information"
121 const message = useMessage()
122 const loading = ref(false)
@@ -106,16 +125,17 @@ const scaList = ref<SCAExt[]>([])
125 const dFormats = useSettingsStore().dateFormat
126 const selectedSca = ref<SCAExt | null>(null)
127
109 -function getSCA(id: string) {
128 +function getSCA(agentId: string) {
129 loading.value = true
130
131 Api.agents
113 - .getSCA(id)
132 + .getSCA(agentId)
133 .then(res => {
134 if (res.data.success) {
135 scaList.value = (res.data.sca || []).map(o => {
136 return {
137 ...o,
138 + downloading: false,
139 end_scan_text: formatDate(o.end_scan, dFormats.datetime).toString(),
140 extract: _truncate(o.description, {
141 length: 50,
@@ -140,6 +160,28 @@ function showScaDetails(sca: SCAExt) {
160 selectedSca.value = sca
161 }
162
163 +function scaDownload(sca: SCAExt) {
164 + sca.downloading = true
165 +
166 + const fileName = `sca-${sca.policy_id}_${new Date().getTime()}.csv`
167 +
168 + Api.agents
169 + .scaResultsDownload(agent.value.agent_id, sca.policy_id)
170 + .then(res => {
171 + if (res.data) {
172 + saveAs(new Blob([res.data], { type: "text/csv;charset=utf-8" }), fileName)
173 + } else {
174 + message.warning("An error occurred. Please try again later.")
175 + }
176 + })
177 + .catch(err => {
178 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
179 + })
180 + .finally(() => {
181 + sca.downloading = false
182 + })
183 +}
184 +
185 onBeforeMount(() => {
186 if (agent?.value?.agent_id) getSCA(agent.value.agent_id)
187 })
frontend/src/components/agents/vulnerabilities/VulnerabilitiesGrid.vue
+61 -17
@@ -1,42 +1,58 @@
1 <template>
2 - <n-spin class="vulnerabilities-section" content-class="min-h-48" :show="loading">
3 - <div class="toolbar">
4 - <n-form-item label="Severity" label-placement="left" size="small">
5 - <n-select v-model:value="severity" :options="severityOptions" class="max-w-48" />
2 + <div class="vulnerabilities-section">
3 + <div class="toolbar flex items-center gap-3 mb-8">
4 + <n-form-item label="Severity" label-placement="left" size="small" :show-feedback="false">
5 + <n-select v-model:value="severity" :options="severityOptions" class="!w-28" />
6 </n-form-item>
7 +
8 + <n-button
9 + v-if="vulnerabilities.length && !loading"
10 + :loading="downloading"
11 + size="small"
12 + @click="vulnerabilitiesDownload(agent.agent_id)"
13 + >
14 + Download CSV
15 + </n-button>
16 </div>
8 - <div class="group gap-4 grid grid-auto-fill-200">
9 - <VulnerabilityCard :vulnerability="item" v-for="item of vulnerabilities" :key="item.id" hide-tooltip />
10 - </div>
11 - <n-empty
12 - description="No vulnerabilities detected"
13 - class="justify-center h-48"
14 - v-if="!loading && !vulnerabilities.length"
15 - />
16 - </n-spin>
17 + <n-spin content-class="min-h-48" :show="loading">
18 + <div class="group gap-4 grid grid-auto-fill-200">
19 + <VulnerabilityCard :vulnerability="item" v-for="item of vulnerabilities" :key="item.id" hide-tooltip />
20 + </div>
21 + <n-empty
22 + description="No vulnerabilities detected"
23 + class="justify-center h-48"
24 + v-if="!loading && !vulnerabilities.length"
25 + />
26 + </n-spin>
27 + </div>
28 </template>
29
30 <script setup lang="ts">
31 import { ref, onBeforeMount, toRefs, watch, computed } from "vue"
32 +import { NButton, NSpin, NEmpty, NFormItem, NSelect, useMessage } from "naive-ui"
33 import Api from "@/api"
22 -import { type Agent, type AgentVulnerabilities } from "@/types/agents.d"
34 import VulnerabilityCard from "./VulnerabilityCard.vue"
35 import { nanoid } from "nanoid"
25 -import { useMessage, NSpin, NEmpty, NFormItem, NSelect } from "naive-ui"
36 +import axios from "axios"
37 +import { saveAs } from "file-saver"
38 import type { VulnerabilitySeverityType } from "@/api/endpoints/agents"
39 +import { type Agent, type AgentVulnerabilities } from "@/types/agents.d"
40
41 const props = defineProps<{
42 agent: Agent
43 }>()
44 const { agent } = toRefs(props)
45
46 +let abortController: AbortController | null = null
47 const message = useMessage()
48 const loading = ref(false)
49 +const downloading = ref(false)
50 const severity = ref<VulnerabilitySeverityType>("Critical")
51 const vulnerabilitiesCache = ref<{ [key in VulnerabilitySeverityType | string]: AgentVulnerabilities[] }>({})
52 const vulnerabilities = computed<AgentVulnerabilities[]>(() => vulnerabilitiesCache.value[severity.value] || [])
53
54 const severityOptions: { label: string; value: VulnerabilitySeverityType }[] = [
55 + { label: "All", value: "All" },
56 { label: "Critical", value: "Critical" },
57 { label: "High", value: "High" },
58 { label: "Medium", value: "Medium" },
@@ -52,10 +68,13 @@ function getVulnerabilities(id: string) {
68 return
69 }
70
71 + abortController?.abort()
72 + abortController = new AbortController()
73 +
74 loading.value = true
75
76 Api.agents
58 - .agentVulnerabilities(id, severity.value)
77 + .agentVulnerabilities(id, severity.value, abortController.signal)
78 .then(res => {
79 if (res.data.success) {
80 vulnerabilitiesCache.value[severity.value] = (res.data.vulnerabilities || []).map(o => {
@@ -65,12 +84,37 @@ function getVulnerabilities(id: string) {
84 } else {
85 message.warning(res.data?.message || "An error occurred. Please try again later.")
86 }
87 + loading.value = false
88 + })
89 + .catch(err => {
90 + if (!axios.isCancel(err)) {
91 + vulnerabilitiesCache.value[severity.value] = []
92 +
93 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
94 + loading.value = false
95 + }
96 + })
97 +}
98 +
99 +function vulnerabilitiesDownload(id: string) {
100 + downloading.value = true
101 +
102 + const fileName = `agent-${id}_${severity.value.toLowerCase()}-vulnerabilities.csv`
103 +
104 + Api.agents
105 + .agentVulnerabilitiesDownload(id, severity.value)
106 + .then(res => {
107 + if (res.data) {
108 + saveAs(new Blob([res.data], { type: "text/csv;charset=utf-8" }), fileName)
109 + } else {
110 + message.warning("An error occurred. Please try again later.")
111 + }
112 })
113 .catch(err => {
114 message.error(err.response?.data?.message || "An error occurred. Please try again later.")
115 })
116 .finally(() => {
73 - loading.value = false
117 + downloading.value = false
118 })
119 }
120
frontend/src/views/agents/Overview.vue
+1 -1
@@ -316,9 +316,9 @@ onBeforeMount(() => {
316 }
317
318 .agent-header {
319 - border: 2px solid transparent;
319 border-radius: var(--border-radius);
320 background-color: var(--bg-color);
321 + border: var(--border-small-050);
322
323 .title {
324 display: flex;