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