@cryptotaxi247 / CoPilot / commits / 88524d19

364 ability to edit comments and delete (#367)

* Add comment editing and deletion endpoints with corresponding schema updates * chore: update dependencies in frontend * feat: add edit/delete actions on alert comments * Add endpoint to sync agent vulnerabilities and implement corresponding logic * fix: alert comment popconfirm * refactor: update check_vulnerability_exists function to use index prefix * precommit fixes --------- Co-authored-by: Davide Di Modica <webmaster.ddm@gmail.com>

taylor_socfortress committed Dec 12, 2024 at 09:28 UTC 88524d195f3aa1ccde0109a9d21838020033e7a2
11 files changed +409 -48
backend/app/agents/routes/agents.py
+23
@@ -36,6 +36,7 @@ from app.agents.wazuh.services.sca import collect_agent_sca
36 from app.agents.wazuh.services.sca import collect_agent_sca_policy_results
37 from app.agents.wazuh.services.vulnerabilities import collect_agent_vulnerabilities
38 from app.agents.wazuh.services.vulnerabilities import collect_agent_vulnerabilities_new
39 +from app.agents.wazuh.services.vulnerabilities import sync_agent_vulnerabilities
40
41 # App specific imports
42 from app.auth.routes.auth import AuthHandler
@@ -794,6 +795,28 @@ async def delete_agent(
795 )
796
797
798 +@agents_router.get(
799 + "/sync/vulnerabilities",
800 + description="Sync agent vulnerabilities",
801 +)
802 +async def sync_vulnerabilities_route(
803 + session: AsyncSession = Depends(get_db),
804 +):
805 + """
806 + Only applies to Wazuh Manager Version 4.8.1 or higher.
807 + 1. Loops through all agents in the database to collect their agent_name and customer code.
808 + 2. Queries the `wazuh-states-vulnerabilities-*` index in Wazuh Indexer to get vulnerabilities based on the agent_name.
809 + 3. Checks the `wazuh-vulnerabilities-*customer_code*` index in Wazuh Indexer to get vulnerabilities based on the
810 + agent_name and checks to see if a vulnerability_id already exists.
811 + 4. If the vulnerability_id does not exist, it is sent to the Graylog GELF Input.
812 + """
813 + logger.info("Syncing agent vulnerabilities")
814 + agents = await get_agents(session)
815 + for agent in agents.agents:
816 + await sync_agent_vulnerabilities(agent.hostname, agent.customer_code)
817 + return {"success": True, "message": "Agent vulnerabilities synced successfully"}
818 +
819 +
820 # ! TODO: CURRENTLY UPDATES IN THE DB BUT NEED TO UPDATE IN WAZUH # !
821 # @agents_router.put(
822 # "/{agent_id}/update-customer-code",
backend/app/agents/wazuh/services/vulnerabilities.py
+123
@@ -8,6 +8,8 @@ from app.agents.wazuh.schema.agents import WazuhAgentVulnerabilitiesResponse
8 from app.connectors.wazuh_indexer.utils.universal import collect_indices
9 from app.connectors.wazuh_indexer.utils.universal import create_wazuh_indexer_client
10 from app.connectors.wazuh_manager.utils.universal import send_get_request
11 +from app.integrations.utils.event_shipper import event_shipper
12 +from app.integrations.utils.schema import EventShipperPayload
13
14
15 async def collect_agent_vulnerabilities(agent_id: str, vulnerability_severity: str):
@@ -108,6 +110,14 @@ def filter_vulnerabilities_indices(indices_list):
110 return [index for index in indices_list if index.startswith("wazuh-states-vulnerabilities")]
111
112
113 +def filter_vulnerabilities_indices_sync(indices_list, customer_code):
114 + """
115 + Filter the indices list to only include the vulnerability indices which are relevant to the customer.
116 + Notice the missing `states` in the index name.
117 + """
118 + return [index for index in indices_list if index.startswith(f"wazuh-vulnerabilities-{customer_code}")]
119 +
120 +
121 async def collect_vulnerabilities(es, vulnerabilities_indices, agent_id, vulnerability_severity="Critical"):
122 agent_vulnerabilities = []
123 for index in vulnerabilities_indices:
@@ -145,6 +155,45 @@ async def collect_vulnerabilities(es, vulnerabilities_indices, agent_id, vulnera
155 return agent_vulnerabilities
156
157
158 +async def collect_vulnerabilities_sync(es, vulnerabilities_indices, agent_name, vulnerability_severity="All"):
159 + agent_vulnerabilities = []
160 + for index in vulnerabilities_indices:
161 + if vulnerability_severity == "All":
162 + query = {
163 + "query": {
164 + "bool": {
165 + "must": [
166 + {"match": {"agent.name": agent_name}},
167 + {"terms": {"vulnerability.severity": ["Low", "Medium", "High", "Critical"]}},
168 + ],
169 + },
170 + },
171 + }
172 + else:
173 + query = {
174 + "query": {
175 + "bool": {
176 + "must": [{"match": {"agent.name": agent_name}}, {"match": {"vulnerability.severity": vulnerability_severity}}],
177 + },
178 + },
179 + }
180 +
181 + page = es.search(index=index, body=query, scroll="2m")
182 + sid = page["_scroll_id"]
183 + scroll_size = len(page["hits"]["hits"])
184 +
185 + while scroll_size > 0:
186 + for hit in page["hits"]["hits"]:
187 + vulnerability = hit["_source"]
188 + agent_vulnerabilities.append(vulnerability)
189 +
190 + page = es.scroll(scroll_id=sid, scroll="2m")
191 + sid = page["_scroll_id"]
192 + scroll_size = len(page["hits"]["hits"])
193 +
194 + return agent_vulnerabilities
195 +
196 +
197 def process_agent_vulnerabilities_new(agent_vulnerabilities: List[dict]) -> List[WazuhAgentVulnerabilities]:
198 logger.info(f"Processing agent vulnerabilities: {agent_vulnerabilities}")
199
@@ -178,3 +227,77 @@ def ensure_list(value):
227 if not isinstance(value, list):
228 return [value]
229 return value
230 +
231 +
232 +async def check_vulnerability_exists(es, vulnerability_cve, agent_name, index_prefix):
233 + query = {
234 + "query": {
235 + "bool": {
236 + "must": [
237 + {"match": {"agent_name": agent_name}},
238 + {"match": {"cve": vulnerability_cve}},
239 + ],
240 + },
241 + },
242 + }
243 + index_pattern = f"{index_prefix}*"
244 + response = es.search(index=index_pattern, body=query)
245 + return response["hits"]["total"]["value"] > 0
246 +
247 +
248 +async def sync_agent_vulnerabilities(agent_name: str, customer_code: str):
249 + """
250 + 1. Loops through all agents in the database to collect their agent_name and customer code.
251 + 2. Queries the `wazuh-states-vulnerabilities-*` index in Wazuh Indexer to get vulnerabilities based on the agent_name.
252 + 3. Checks the `wazuh-vulnerabilities-*customer_code*` index in Wazuh Indexer to get vulnerabilities based on the
253 + agent_name and checks to see if a vulnerability_id already exists.
254 + 4. If the vulnerability_id does not exist, it is sent to the Graylog GELF Input.
255 + """
256 + logger.info(f"Syncing agent {agent_name} with customer code {customer_code} vulnerabilities")
257 +
258 + es = await create_wazuh_indexer_client("Wazuh-Indexer")
259 + indices = await collect_indices(all_indices=True)
260 +
261 + vulnerabilities_indices = filter_vulnerabilities_indices(indices.indices_list)
262 +
263 + agent_vulnerabilities = await collect_vulnerabilities_sync(es, vulnerabilities_indices, agent_name, vulnerability_severity="All")
264 +
265 + processed_vulnerabilities = process_agent_vulnerabilities_new(agent_vulnerabilities)
266 +
267 + customer_vulnerabilities_indices = filter_vulnerabilities_indices_sync(indices.indices_list, customer_code)
268 + logger.info(f"Customer vulnerabilities indices: {customer_vulnerabilities_indices}")
269 +
270 + if customer_vulnerabilities_indices:
271 + logger.info("Customer vulnerabilities index already exists")
272 + # ! Check to see if the vulnerability exists in the customer's index and send to Graylog if it does not exist in the customer's index ! #
273 + for vulnerability in processed_vulnerabilities:
274 + vulnerability_exists = await check_vulnerability_exists(
275 + es,
276 + vulnerability_cve=vulnerability.cve,
277 + agent_name=agent_name,
278 + index_prefix=f"wazuh-vulnerabilities-{customer_code}",
279 + )
280 +
281 + if not vulnerability_exists:
282 + await event_shipper(
283 + EventShipperPayload(
284 + integration="vulnerabilities",
285 + customer_code=customer_code,
286 + agent_name=agent_name,
287 + **vulnerability.dict(),
288 + ),
289 + )
290 + return True
291 +
292 + logger.info("Customer vulnerabilities index does not exist")
293 + # ! Send all vulnerabilities to Graylog ! #
294 + for vulnerability in processed_vulnerabilities:
295 + await event_shipper(
296 + EventShipperPayload(
297 + integration="vulnerabilities",
298 + customer_code=customer_code,
299 + agent_name=agent_name,
300 + **vulnerability.dict(),
301 + ),
302 + )
303 + return True
backend/app/incidents/routes/db_operations.py
+14
@@ -59,6 +59,7 @@ from app.incidents.schema.db_operations import CaseReportTemplateDataStoreListRe
59 from app.incidents.schema.db_operations import CaseReportTemplateDataStoreResponse
60 from app.incidents.schema.db_operations import CaseResponse
61 from app.incidents.schema.db_operations import CommentCreate
62 +from app.incidents.schema.db_operations import CommentEdit
63 from app.incidents.schema.db_operations import CommentResponse
64 from app.incidents.schema.db_operations import ConfiguredSourcesResponse
65 from app.incidents.schema.db_operations import DefaultReportTemplateFileNames
@@ -136,6 +137,7 @@ from app.incidents.services.db_operations import delete_alert_tag
137 from app.incidents.services.db_operations import delete_alert_title_name
138 from app.incidents.services.db_operations import delete_asset_name
139 from app.incidents.services.db_operations import delete_case
140 +from app.incidents.services.db_operations import delete_comment
141 from app.incidents.services.db_operations import delete_field_name
142 from app.incidents.services.db_operations import delete_file_from_case
143 from app.incidents.services.db_operations import delete_ioc_name
@@ -143,6 +145,7 @@ from app.incidents.services.db_operations import delete_report_template
145 from app.incidents.services.db_operations import delete_timefield_name
146 from app.incidents.services.db_operations import download_file_from_case
147 from app.incidents.services.db_operations import download_report_template
148 +from app.incidents.services.db_operations import edit_comment
149 from app.incidents.services.db_operations import file_exists
150 from app.incidents.services.db_operations import get_alert_by_id
151 from app.incidents.services.db_operations import get_alert_context_by_id
@@ -402,6 +405,17 @@ async def create_comment_endpoint(comment: CommentCreate, db: AsyncSession = Dep
405 return CommentResponse(comment=await create_comment(comment, db), success=True, message="Comment created successfully")
406
407
408 +@incidents_db_operations_router.put("/alert/comment", response_model=CommentResponse)
409 +async def edit_comment_endpoint(comment: CommentEdit, db: AsyncSession = Depends(get_db)):
410 + return CommentResponse(comment=await edit_comment(comment, db), success=True, message="Comment edited successfully")
411 +
412 +
413 +@incidents_db_operations_router.delete("/alert/comment/{comment_id}")
414 +async def delete_comment_endpoint(comment_id: int, db: AsyncSession = Depends(get_db)):
415 + await delete_comment(comment_id, db)
416 + return {"message": "Comment deleted successfully", "success": True}
417 +
418 +
419 @incidents_db_operations_router.get("/alert/available-users", response_model=AvailableUsersResponse)
420 async def get_available_users(db: AsyncSession = Depends(get_db)):
421 all_users = await select_all_users()
backend/app/incidents/schema/db_operations.py
+8
@@ -249,6 +249,14 @@ class CommentCreate(BaseModel):
249 created_at: datetime
250
251
252 +class CommentEdit(BaseModel):
253 + alert_id: int
254 + comment_id: int
255 + comment: str
256 + user_name: str
257 + created_at: datetime
258 +
259 +
260 class AlertContextCreate(BaseModel):
261 source: str
262 context: Dict
backend/app/incidents/services/db_operations.py
+22
@@ -60,6 +60,7 @@ from app.incidents.schema.db_operations import CaseOut
60 from app.incidents.schema.db_operations import CaseReportTemplateDataStoreListResponse
61 from app.incidents.schema.db_operations import CommentBase
62 from app.incidents.schema.db_operations import CommentCreate
63 +from app.incidents.schema.db_operations import CommentEdit
64 from app.incidents.schema.db_operations import IoCBase
65 from app.incidents.schema.db_operations import LinkedCaseCreate
66 from app.incidents.schema.db_operations import PutNotification
@@ -833,6 +834,27 @@ async def create_comment(comment: CommentCreate, db: AsyncSession) -> Comment:
834 return db_comment
835
836
837 +async def edit_comment(comment: CommentEdit, db: AsyncSession) -> Comment:
838 + result = await db.execute(select(Comment).where(Comment.id == comment.comment_id))
839 + db_comment = result.scalars().first()
840 + if not db_comment:
841 + raise HTTPException(status_code=404, detail="Comment not found")
842 + db_comment.comment = comment.comment
843 + db_comment.user_name = comment.user_name
844 + await db.commit()
845 + return db_comment
846 +
847 +
848 +async def delete_comment(comment_id: int, db: AsyncSession) -> Comment:
849 + result = await db.execute(select(Comment).where(Comment.id == comment_id))
850 + comment = result.scalars().first()
851 + if not comment:
852 + raise HTTPException(status_code=404, detail="Comment not found")
853 + await db.execute(delete(Comment).where(Comment.id == comment_id))
854 + await db.commit()
855 + return comment
856 +
857 +
858 async def create_asset(asset: AssetCreate, db: AsyncSession) -> Asset:
859 # Check if the alert exists
860 result = await db.execute(select(Alert).options(selectinload(Alert.assets)).where(Alert.id == asset.alert_linked))
frontend/package-lock.json
+39 -39
@@ -56,7 +56,7 @@
56 "@types/fs-extra": "^11.0.4",
57 "@types/jsdom": "^21.1.7",
58 "@types/lodash": "^4.17.13",
59 - "@types/node": "^22.10.1",
59 + "@types/node": "^22.10.2",
60 "@types/validator": "^13.12.2",
61 "@vitejs/plugin-vue": "^5.2.1",
62 "@vitejs/plugin-vue-jsx": "^4.1.1",
@@ -74,7 +74,7 @@
74 "prettier": "^3.4.2",
75 "prettier-plugin-tailwindcss": "^0.6.9",
76 "sass": "^1.82.0",
77 - "start-server-and-test": "^2.0.8",
77 + "start-server-and-test": "^2.0.9",
78 "tailwind-config-viewer": "^2.0.4",
79 "tailwindcss": "^3.4.16",
80 "taze": "^0.18.0",
@@ -83,7 +83,7 @@
83 "unplugin-vue-components": "^0.27.5",
84 "vite": "^5.4.11",
85 "vite-bundle-visualizer": "^1.2.1",
86 - "vite-plugin-vue-devtools": "^7.6.7",
86 + "vite-plugin-vue-devtools": "^7.6.8",
87 "vite-svg-loader": "^5.1.0",
88 "vitest": "^2.1.8",
89 "vue-tsc": "^2.1.10"
@@ -2919,9 +2919,9 @@
2919 "license": "MIT"
2920 },
2921 "node_modules/@types/node": {
2922 - "version": "22.10.1",
2923 - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz",
2924 - "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==",
2922 + "version": "22.10.2",
2923 + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
2924 + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
2925 "dev": true,
2926 "license": "MIT",
2927 "dependencies": {
@@ -3552,14 +3552,14 @@
3552 "license": "MIT"
3553 },
3554 "node_modules/@vue/devtools-core": {
3555 - "version": "7.6.7",
3556 - "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.6.7.tgz",
3557 - "integrity": "sha512-6fW8Q0H1NHDXdEcuV6dylT5U2Yxg3SdMnVCey99Y6S4R2PNgFL2vC+VU9U9rHIiaoEUkeza42S7FfHxV4VI3Jg==",
3555 + "version": "7.6.8",
3556 + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.6.8.tgz",
3557 + "integrity": "sha512-8X4roysTwzQ94o7IobjVcOd1aZF5iunikrMrHPI2uUdigZCi2kFTQc7ffYiFiTNaLElCpjOhCnM7bo7aK1yU7A==",
3558 "dev": true,
3559 "license": "MIT",
3560 "dependencies": {
3561 - "@vue/devtools-kit": "^7.6.7",
3562 - "@vue/devtools-shared": "^7.6.7",
3561 + "@vue/devtools-kit": "^7.6.8",
3562 + "@vue/devtools-shared": "^7.6.8",
3563 "mitt": "^3.0.1",
3564 "nanoid": "^5.0.9",
3565 "pathe": "^1.1.2",
@@ -3570,13 +3570,13 @@
3570 }
3571 },
3572 "node_modules/@vue/devtools-kit": {
3573 - "version": "7.6.7",
3574 - "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.7.tgz",
3575 - "integrity": "sha512-V8/jrXY/swHgnblABG9U4QCbE60c6RuPasmv2d9FvVqc5d94t1vDiESuvRmdNJBdWz4/D3q6ffgyAfRVjwHYEw==",
3573 + "version": "7.6.8",
3574 + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.6.8.tgz",
3575 + "integrity": "sha512-JhJ8M3sPU+v0P2iZBF2DkdmR9L0dnT5RXJabJqX6o8KtFs3tebdvfoXV2Dm3BFuqeECuMJIfF1aCzSt+WQ4wrw==",
3576 "dev": true,
3577 "license": "MIT",
3578 "dependencies": {
3579 - "@vue/devtools-shared": "^7.6.7",
3579 + "@vue/devtools-shared": "^7.6.8",
3580 "birpc": "^0.2.19",
3581 "hookable": "^5.5.3",
3582 "mitt": "^3.0.1",
@@ -3586,9 +3586,9 @@
3586 }
3587 },
3588 "node_modules/@vue/devtools-shared": {
3589 - "version": "7.6.7",
3590 - "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.7.tgz",
3591 - "integrity": "sha512-QggO6SviAsolrePAXZ/sA1dSicSPt4TueZibCvydfhNDieL1lAuyMTgQDGst7TEvMGb4vgYv2I+1sDkO4jWNnw==",
3589 + "version": "7.6.8",
3590 + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.6.8.tgz",
3591 + "integrity": "sha512-9MBPO5Z3X1nYGFqTJyohl6Gmf/J7UNN1oicHdyzBVZP4jnhZ4c20MgtaHDIzWmHDHCMYVS5bwKxT3jxh7gOOKA==",
3592 "dev": true,
3593 "license": "MIT",
3594 "dependencies": {
@@ -5353,9 +5353,9 @@
5353 "license": "MIT"
5354 },
5355 "node_modules/debug": {
5356 - "version": "4.3.7",
5357 - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
5358 - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
5356 + "version": "4.4.0",
5357 + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
5358 + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
5359 "license": "MIT",
5360 "dependencies": {
5361 "ms": "^2.1.3"
@@ -13021,16 +13021,16 @@
13021 "license": "MIT"
13022 },
13023 "node_modules/start-server-and-test": {
13024 - "version": "2.0.8",
13025 - "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.8.tgz",
13026 - "integrity": "sha512-v2fV6NV2F7tL1ocwfI4Wpait+IKjRbT5l3ZZ+ZikXdMLmxYsS8ynGAsCQAUVXkVyGyS+UibsRnvgHkMvJIvCsw==",
13024 + "version": "2.0.9",
13025 + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.0.9.tgz",
13026 + "integrity": "sha512-DDceIvc4wdpr+z3Aqkot2QMho8TcUBh5qH0wEHDpEexBTzlheOcmh53d3dExABY4J5C7qS2UbSXqRWLtxpbWIQ==",
13027 "dev": true,
13028 "license": "MIT",
13029 "dependencies": {
13030 "arg": "^5.0.2",
13031 "bluebird": "3.7.2",
13032 "check-more-types": "2.24.0",
13033 - "debug": "4.3.7",
13033 + "debug": "4.4.0",
13034 "execa": "5.1.1",
13035 "lazy-ass": "1.6.0",
13036 "ps-tree": "1.2.0",
@@ -13272,9 +13272,9 @@
13272 }
13273 },
13274 "node_modules/superjson": {
13275 - "version": "2.2.1",
13276 - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz",
13277 - "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==",
13275 + "version": "2.2.2",
13276 + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
13277 + "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
13278 "dev": true,
13279 "license": "MIT",
13280 "dependencies": {
@@ -14538,9 +14538,9 @@
14538 }
14539 },
14540 "node_modules/vite-plugin-inspect": {
14541 - "version": "0.8.8",
14542 - "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.8.tgz",
14543 - "integrity": "sha512-aZlBuXsWUPJFmMK92GIv6lH7LrwG2POu4KJ+aEdcqnu92OAf+rhBnfMDQvxIJPEB7hE2t5EyY/PMgf5aDLT8EA==",
14541 + "version": "0.8.9",
14542 + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.9.tgz",
14543 + "integrity": "sha512-22/8qn+LYonzibb1VeFZmISdVao5kC22jmEKm24vfFE8siEn47EpVcCLYMv6iKOYMJfjSvSJfueOwcFCkUnV3A==",
14544 "dev": true,
14545 "license": "MIT",
14546 "dependencies": {
@@ -14561,7 +14561,7 @@
14561 "url": "https://github.com/sponsors/antfu"
14562 },
14563 "peerDependencies": {
14564 - "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0"
14564 + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.1"
14565 },
14566 "peerDependenciesMeta": {
14567 "@nuxt/kit": {
@@ -14618,18 +14618,18 @@
14618 }
14619 },
14620 "node_modules/vite-plugin-vue-devtools": {
14621 - "version": "7.6.7",
14622 - "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.6.7.tgz",
14623 - "integrity": "sha512-H1ZyjtpWjP5mHA5R15sQeYgAARuh2Myg3TDFXWZK6QOQRy8s3XjTIt319DogVjU/x3rC3L/jJQjIasRU04mWXA==",
14621 + "version": "7.6.8",
14622 + "resolved": "https://registry.npmjs.org/vite-plugin-vue-devtools/-/vite-plugin-vue-devtools-7.6.8.tgz",
14623 + "integrity": "sha512-32aIps8C1Y7UEoqyWf+ES3J1OozsCYMIqTqd+I5qass+R0Tcf8SaA2bX1/rskAzkcKCteVoBjEENmqwTcMebbw==",
14624 "dev": true,
14625 "license": "MIT",
14626 "dependencies": {
14627 - "@vue/devtools-core": "^7.6.7",
14628 - "@vue/devtools-kit": "^7.6.7",
14629 - "@vue/devtools-shared": "^7.6.7",
14627 + "@vue/devtools-core": "^7.6.8",
14628 + "@vue/devtools-kit": "^7.6.8",
14629 + "@vue/devtools-shared": "^7.6.8",
14630 "execa": "^9.5.1",
14631 "sirv": "^3.0.0",
14632 - "vite-plugin-inspect": "0.8.8",
14632 + "vite-plugin-inspect": "~0.8.9",
14633 "vite-plugin-vue-inspector": "^5.3.1"
14634 },
14635 "engines": {
frontend/package.json
+3 -3
@@ -87,7 +87,7 @@
87 "@types/fs-extra": "^11.0.4",
88 "@types/jsdom": "^21.1.7",
89 "@types/lodash": "^4.17.13",
90 - "@types/node": "^22.10.1",
90 + "@types/node": "^22.10.2",
91 "@types/validator": "^13.12.2",
92 "@vitejs/plugin-vue": "^5.2.1",
93 "@vitejs/plugin-vue-jsx": "^4.1.1",
@@ -105,7 +105,7 @@
105 "prettier": "^3.4.2",
106 "prettier-plugin-tailwindcss": "^0.6.9",
107 "sass": "^1.82.0",
108 - "start-server-and-test": "^2.0.8",
108 + "start-server-and-test": "^2.0.9",
109 "tailwind-config-viewer": "^2.0.4",
110 "tailwindcss": "^3.4.16",
111 "taze": "^0.18.0",
@@ -114,7 +114,7 @@
114 "unplugin-vue-components": "^0.27.5",
115 "vite": "^5.4.11",
116 "vite-bundle-visualizer": "^1.2.1",
117 - "vite-plugin-vue-devtools": "^7.6.7",
117 + "vite-plugin-vue-devtools": "^7.6.8",
118 "vite-svg-loader": "^5.1.0",
119 "vitest": "^2.1.8",
120 "vue-tsc": "^2.1.10"
frontend/src/api/endpoints/incidentManagement.ts
+11
@@ -46,6 +46,8 @@ export type CasesFilterTypes = KeysOfUnion<CasesFilter>
46
47 export type AlertCommentPayload = Omit<AlertComment, "id">
48
49 +export type AlertCommentUpdatePayload = Omit<AlertComment, "id"> & { comment_id: number }
50 +
51 export interface AlertIocPayload {
52 alert_id: number
53 ioc_value: string
@@ -250,6 +252,15 @@ export default {
252 payload
253 )
254 },
255 + updateAlertComment(payload: AlertCommentUpdatePayload) {
256 + return HttpClient.put<FlaskBaseResponse & { comment: AlertComment }>(
257 + `/incidents/db_operations/alert/comment`,
258 + payload
259 + )
260 + },
261 + deleteAlertComment(commentId: number) {
262 + return HttpClient.delete<FlaskBaseResponse>(`/incidents/db_operations/alert/comment/${commentId}`)
263 + },
264 newAlertTag(alertId: number, tag: string) {
265 return HttpClient.post<FlaskBaseResponse & { alert_tag: AlertTag }>(`/incidents/db_operations/alert/tag`, {
266 alert_id: alertId,
frontend/src/components/auth/SignUp.vue
-1
@@ -142,7 +142,6 @@
142
143 <script lang="ts" setup>
144 import type { RegisterPayload } from "@/types/auth.d"
145 -
145 import Api from "@/api"
146 import Icon from "@/components/common/Icon.vue"
147 import {
frontend/src/components/incidentManagement/alerts/AlertComment.vue
+143 -4
@@ -12,24 +12,163 @@
12 {{ formatDate(comment.created_at, dFormats.datetime) }}
13 </div>
14 </div>
15 - <div class="comment-message" v-html="message"></div>
15 + <div v-if="mode === 'view'" class="comment-message">
16 + <Suspense>
17 + <Markdown :source="comment.comment" />
18 + </Suspense>
19 + </div>
20 +
21 + <n-input
22 + v-if="mode === 'edit'"
23 + v-model:value="commentModel"
24 + type="textarea"
25 + :disabled="saving"
26 + placeholder="Insert the updated comment"
27 + size="large"
28 + :autosize="{
29 + minRows: 3,
30 + maxRows: 18
31 + }"
32 + />
33 +
34 + <div class="comment-actions flex justify-end gap-1">
35 + <template v-if="mode === 'view'">
36 + <n-button size="tiny" secondary :disabled="canceling" @click="editComment()">
37 + <template #icon>
38 + <Icon :name="EditIcon" :size="12"></Icon>
39 + </template>
40 + <span>Edit</span>
41 + </n-button>
42 + <n-popconfirm to="body" @positive-click="deleteAlertComment()">
43 + <template #trigger>
44 + <n-button size="tiny" secondary type="error" :loading="canceling">
45 + <template #icon>
46 + <Icon :name="DeleteIcon" :size="12"></Icon>
47 + </template>
48 + <span>Delete</span>
49 + </n-button>
50 + </template>
51 + Are you sure you want to delete the comment?
52 + </n-popconfirm>
53 + </template>
54 + <template v-if="mode === 'edit'">
55 + <n-button size="tiny" secondary :disabled="saving" @click="setMode('view')">
56 + <template #icon>
57 + <Icon :name="ArrowLeftIcon" :size="12"></Icon>
58 + </template>
59 + <span>Cancel</span>
60 + </n-button>
61 +
62 + <n-button
63 + size="tiny"
64 + secondary
65 + type="success"
66 + :loading="saving"
67 + :disabled="!commentModel"
68 + @click="updateAlertComment()"
69 + >
70 + <template #icon>
71 + <Icon :name="SaveIcon" :size="13"></Icon>
72 + </template>
73 + <span>Save</span>
74 + </n-button>
75 + </template>
76 + </div>
77 </div>
78 </div>
79 </template>
80
81 <script setup lang="ts">
82 import type { AlertComment } from "@/types/incidentManagement/alerts.d"
83 +import Api from "@/api"
84 +import Icon from "@/components/common/Icon.vue"
85 import { useSettingsStore } from "@/stores/settings"
86 import { formatDate, getAvatar, getNameInitials } from "@/utils"
24 -import { NAvatar } from "naive-ui"
25 -import { onBeforeMount, ref, toRefs } from "vue"
87 +import { NAvatar, NButton, NInput, NPopconfirm, useMessage } from "naive-ui"
88 +import { defineAsyncComponent, onBeforeMount, ref, toRefs } from "vue"
89 +
90 +type Mode = "view" | "edit"
91
92 const props = defineProps<{ comment: AlertComment; embedded?: boolean }>()
93 +
94 +const emit = defineEmits<{
95 + (e: "deleted"): void
96 + (e: "updated", value: AlertComment): void
97 +}>()
98 +
99 +const Markdown = defineAsyncComponent(() => import("@/components/common/Markdown.vue"))
100 +
101 const { comment, embedded } = toRefs(props)
102
103 +const ArrowLeftIcon = "carbon:arrow-left"
104 +const SaveIcon = "carbon:save"
105 +const EditIcon = "uil:edit-alt"
106 +const DeleteIcon = "ph:trash"
107 +const mode = ref<Mode>("view")
108 +const canceling = ref(false)
109 +const saving = ref(false)
110 const dFormats = useSettingsStore().dateFormat
111 const userPic = ref("")
32 -const message = ref(comment.value.comment.replace(/\n/g, "<br/>"))
112 +const commentModel = ref(comment.value.comment)
113 +const message = useMessage()
114 +
115 +function setMode(newMode: Mode) {
116 + mode.value = newMode
117 +}
118 +
119 +function editComment() {
120 + setMode("edit")
121 + commentModel.value = comment.value.comment
122 +}
123 +
124 +function updateAlertComment() {
125 + saving.value = true
126 +
127 + Api.incidentManagement
128 + .updateAlertComment({
129 + alert_id: comment.value.alert_id,
130 + comment_id: comment.value.id,
131 + comment: commentModel.value,
132 + created_at: new Date(),
133 + user_name: comment.value.user_name
134 + })
135 + .then(res => {
136 + if (res.data.success) {
137 + message.success(res.data?.message || "Comment updated successfully")
138 + setMode("view")
139 + emit("updated", res.data.comment)
140 + } else {
141 + message.warning(res.data?.message || "An error occurred. Please try again later.")
142 + }
143 + })
144 + .catch(err => {
145 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
146 + })
147 + .finally(() => {
148 + saving.value = false
149 + })
150 +}
151 +
152 +function deleteAlertComment() {
153 + canceling.value = true
154 +
155 + Api.incidentManagement
156 + .deleteAlertComment(comment.value.id)
157 + .then(res => {
158 + if (res.data.success) {
159 + message.success(res.data?.message || "Comment deleted successfully")
160 + emit("deleted")
161 + } else {
162 + message.warning(res.data?.message || "An error occurred. Please try again later.")
163 + }
164 + })
165 + .catch(err => {
166 + message.error(err.response?.data?.message || "An error occurred. Please try again later.")
167 + })
168 + .finally(() => {
169 + canceling.value = false
170 + })
171 +}
172
173 onBeforeMount(() => {
174 const initials = getNameInitials(comment.value.user_name)
frontend/src/components/incidentManagement/alerts/AlertCommentsList.vue
+23 -1
@@ -1,7 +1,14 @@
1 <template>
2 <div class="flex flex-col gap-6">
3 <template v-if="commentsList.length">
4 - <AlertCommentItem v-for="comment of commentsList" :key="comment.id" :comment embedded />
4 + <AlertCommentItem
5 + v-for="comment of commentsList"
6 + :key="comment.id"
7 + :comment
8 + embedded
9 + @deleted="removeComment(comment)"
10 + @updated="updateComment($event)"
11 + />
12 </template>
13 <template v-else>
14 <n-empty description="No comments found" class="h-48 justify-center" />
@@ -67,6 +74,21 @@ function reset() {
74 commentMessage.value = ""
75 }
76
77 +function updateComment(newComment: AlertComment) {
78 + const comment = commentsList.value.find(o => o.id === newComment.id)
79 + if (comment) {
80 + comment.created_at = newComment.created_at
81 + comment.comment = newComment.comment
82 + }
83 +}
84 +
85 +function removeComment(comment: AlertComment) {
86 + commentsList.value.splice(
87 + commentsList.value.findIndex(o => o.id === comment.id),
88 + 1
89 + )
90 +}
91 +
92 function submit() {
93 if (trimmedValue.value) {
94 submitting.value = true