main
py 806 lines 32.5 KB
Raw
1 import io
2 import json
3 from datetime import datetime
4 from typing import List
5 from typing import Optional
6
7 from fastapi import APIRouter
8 from fastapi import BackgroundTasks
9 from fastapi import Depends
10 from fastapi import HTTPException
11 from fastapi import Query
12 from fastapi import Security
13 from fastapi.responses import StreamingResponse
14 from loguru import logger
15 from sqlalchemy.ext.asyncio import AsyncSession
16 from sqlalchemy.future import select
17
18 from app.agents.vulnerabilities.schema.vulnerabilities import (
19 AgentVulnerabilitiesResponse,
20 )
21 from app.agents.vulnerabilities.schema.vulnerabilities import (
22 VulnerabilityDeleteResponse,
23 )
24 from app.agents.vulnerabilities.schema.vulnerabilities import (
25 VulnerabilityReportGenerateRequest,
26 )
27 from app.agents.vulnerabilities.schema.vulnerabilities import (
28 VulnerabilityReportGenerateResponse,
29 )
30 from app.agents.vulnerabilities.schema.vulnerabilities import (
31 VulnerabilityReportListResponse,
32 )
33 from app.agents.vulnerabilities.schema.vulnerabilities import (
34 VulnerabilitySearchResponse,
35 )
36 from app.agents.vulnerabilities.schema.vulnerabilities import VulnerabilityStatsResponse
37 from app.agents.vulnerabilities.schema.vulnerabilities import VulnerabilitySyncRequest
38 from app.agents.vulnerabilities.schema.vulnerabilities import VulnerabilitySyncResponse
39 from app.agents.vulnerabilities.services.vulnerabilities import delete_vulnerabilities
40 from app.agents.vulnerabilities.services.vulnerabilities import (
41 generate_vulnerability_csv_report,
42 )
43 from app.agents.vulnerabilities.services.vulnerabilities import (
44 get_vulnerabilities_by_agent,
45 )
46 from app.agents.vulnerabilities.services.vulnerabilities import (
47 get_vulnerability_report_download,
48 )
49 from app.agents.vulnerabilities.services.vulnerabilities import (
50 get_vulnerability_statistics,
51 )
52 from app.agents.vulnerabilities.services.vulnerabilities import (
53 list_vulnerability_reports,
54 )
55 from app.agents.vulnerabilities.services.vulnerabilities import (
56 search_vulnerabilities_from_indexer,
57 )
58 from app.agents.vulnerabilities.services.vulnerabilities import sync_all_vulnerabilities
59 from app.agents.vulnerabilities.services.vulnerabilities import (
60 sync_vulnerabilities_for_agent,
61 )
62 from app.auth.models.users import User
63 from app.auth.routes.auth import AuthHandler
64 from app.db.db_session import get_db
65 from app.db.db_session import get_db_session
66 from app.db.universal_models import VulnerabilityReport
67
68 # Create router for vulnerability endpoints
69 vulnerabilities_router = APIRouter()
70
71
72 @vulnerabilities_router.post(
73 "/sync",
74 response_model=VulnerabilitySyncResponse,
75 description="Sync vulnerabilities from Wazuh Indexer indices to database for all agents with performance options",
76 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst", "scheduler"))],
77 deprecated=True, # Marking this endpoint as deprecated in favor of more specific ones
78 )
79 async def sync_vulnerabilities(
80 sync_request: Optional[VulnerabilitySyncRequest] = None,
81 batch_size: int = Query(100, description="Batch size for processing (1-1000)", ge=1, le=1000),
82 use_bulk_mode: bool = Query(False, description="Use ultra-fast bulk mode for large datasets"),
83 db: AsyncSession = Depends(get_db),
84 ) -> VulnerabilitySyncResponse:
85 """
86 Sync vulnerabilities from Wazuh Indexer indices to the database for all agents.
87
88 This endpoint fetches vulnerability data from the 'wazuh-states-vulnerabilities-*'
89 indices for all agents in the database, processes them, and stores them in the
90 agent_vulnerabilities table.
91
92 **Performance Modes:**
93 - **Batch Mode** (default): Processes vulnerabilities in configurable batches with individual error handling
94 - **Bulk Mode**: Ultra-fast processing using bulk database operations for large datasets
95
96 The endpoint automatically discovers all agents from the database and syncs
97 vulnerabilities for each one using their hostname and customer_code.
98
99 Args:
100 sync_request: Optional request parameters for vulnerability sync
101 batch_size: Number of vulnerabilities to process per batch (1-1000, default: 100)
102 use_bulk_mode: Enable ultra-fast bulk processing mode for large datasets
103 db: Database session
104
105 Returns:
106 VulnerabilitySyncResponse: Status of the sync operation
107 """
108 logger.info(f"Starting vulnerability sync for all agents from database (batch_size={batch_size}, bulk_mode={use_bulk_mode})")
109
110 try:
111 # Use the standalone function directly with performance options
112 result = await sync_all_vulnerabilities(db_session=db, customer_code=None, batch_size=batch_size, use_bulk_mode=use_bulk_mode)
113 return result
114
115 except Exception as e:
116 logger.error(f"Error in vulnerability sync endpoint: {e}")
117 raise HTTPException(status_code=500, detail=f"Vulnerability sync failed: {e}")
118
119
120 @vulnerabilities_router.post(
121 "/sync/background",
122 response_model=dict,
123 description="Start background vulnerability sync for all agents",
124 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
125 deprecated=True, # Marking this endpoint as deprecated in favor of more specific ones
126 )
127 async def sync_vulnerabilities_background(
128 background_tasks: BackgroundTasks,
129 sync_request: Optional[VulnerabilitySyncRequest] = None,
130 db: AsyncSession = Depends(get_db),
131 ):
132 """
133 Start vulnerability sync as a background task for all agents in the database.
134
135 This endpoint automatically discovers all agents from the database and starts
136 a background task to sync vulnerabilities for each one. This is useful for
137 large sync operations that might take a long time.
138
139 Args:
140 background_tasks: FastAPI background tasks
141 sync_request: Optional request parameters
142 db: Database session
143 """
144 logger.info("Starting background vulnerability sync for all agents from database")
145
146 async def background_sync():
147 try:
148 # Create a new database session for the background task
149 async with get_db_session() as bg_db:
150 # Use the standalone function directly with default performance settings
151 await sync_all_vulnerabilities(db_session=bg_db, customer_code=None, batch_size=100, use_bulk_mode=False)
152 except Exception as e:
153 logger.error(f"Background vulnerability sync failed: {e}")
154
155 background_tasks.add_task(background_sync)
156
157 return {"success": True, "message": "Vulnerability sync started in background for all agents"}
158
159
160 @vulnerabilities_router.get(
161 "/agent/{agent_id}",
162 response_model=AgentVulnerabilitiesResponse,
163 description="Get vulnerabilities for a specific agent",
164 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
165 deprecated=True, # Marking this endpoint as deprecated in favor of more specific ones
166 )
167 async def get_agent_vulnerabilities(
168 agent_id: str,
169 severity: Optional[List[str]] = Query(None, description="Filter by severity levels"),
170 db: AsyncSession = Depends(get_db),
171 ) -> AgentVulnerabilitiesResponse:
172 """
173 Retrieve vulnerabilities for a specific agent from the database.
174
175 Args:
176 agent_id: ID of the agent to get vulnerabilities for
177 severity: Optional list of severity levels to filter by (Critical, High, Medium, Low)
178 db: Database session
179
180 Returns:
181 AgentVulnerabilitiesResponse: List of vulnerabilities for the agent
182 """
183 logger.info(f"Getting vulnerabilities for agent: {agent_id}")
184
185 try:
186 # Use the standalone function directly
187 return await get_vulnerabilities_by_agent(db_session=db, agent_id=agent_id, severity_filter=severity)
188
189 except Exception as e:
190 logger.error(f"Error getting vulnerabilities for agent {agent_id}: {e}")
191 raise HTTPException(status_code=500, detail=f"Failed to get vulnerabilities for agent {agent_id}: {e}")
192
193
194 @vulnerabilities_router.get(
195 "/stats",
196 response_model=VulnerabilityStatsResponse,
197 description="Get vulnerability statistics",
198 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
199 deprecated=True, # Marking this endpoint as deprecated in favor of more specific ones
200 )
201 async def get_vulnerability_stats(
202 customer_code: Optional[str] = Query(None, description="Filter by customer code"),
203 db: AsyncSession = Depends(get_db),
204 ) -> VulnerabilityStatsResponse:
205 """
206 Get vulnerability statistics across all agents or for a specific customer.
207
208 Args:
209 customer_code: Optional customer code to filter statistics by
210 db: Database session
211
212 Returns:
213 VulnerabilityStatsResponse: Vulnerability statistics
214 """
215 logger.info(f"Getting vulnerability statistics for customer: {customer_code}")
216
217 try:
218 # Use the standalone function directly
219 return await get_vulnerability_statistics(db_session=db, customer_code=customer_code)
220
221 except Exception as e:
222 logger.error(f"Error getting vulnerability statistics: {e}")
223 raise HTTPException(status_code=500, detail=f"Failed to get vulnerability statistics: {e}")
224
225
226 @vulnerabilities_router.post(
227 "/sync/customer/{customer_code}",
228 response_model=VulnerabilitySyncResponse,
229 description="Sync vulnerabilities for all agents of a specific customer",
230 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
231 deprecated=True, # Marking this endpoint as deprecated in favor of more specific ones
232 )
233 async def sync_customer_vulnerabilities(
234 customer_code: str,
235 background_tasks: BackgroundTasks,
236 force_refresh: bool = Query(False, description="Force refresh of existing vulnerabilities"),
237 batch_size: int = Query(100, description="Batch size for processing vulnerabilities"),
238 use_bulk_mode: bool = Query(False, description="Use ultra-fast bulk operations for large datasets"),
239 db: AsyncSession = Depends(get_db),
240 ) -> VulnerabilitySyncResponse:
241 """
242 Sync vulnerabilities for all agents belonging to a specific customer with performance options.
243
244 Args:
245 customer_code: Customer code to sync vulnerabilities for
246 background_tasks: FastAPI background tasks
247 force_refresh: Whether to force refresh of existing vulnerabilities
248 batch_size: Number of vulnerabilities to process in each batch
249 use_bulk_mode: Use ultra-fast bulk operations for large datasets
250 db: Database session
251
252 Returns:
253 VulnerabilitySyncResponse: Status of the sync operation
254 """
255 logger.info(f"Starting vulnerability sync for customer: {customer_code} (batch_size={batch_size}, bulk_mode={use_bulk_mode})")
256
257 try:
258 # Use the standalone function directly with performance options
259 result = await sync_all_vulnerabilities(
260 db_session=db,
261 customer_code=customer_code,
262 batch_size=batch_size,
263 use_bulk_mode=use_bulk_mode,
264 )
265 return result
266
267 except Exception as e:
268 logger.error(f"Error syncing vulnerabilities for customer {customer_code}: {e}")
269 raise HTTPException(status_code=500, detail=f"Failed to sync vulnerabilities for customer {customer_code}: {e}")
270
271
272 @vulnerabilities_router.post(
273 "/sync/agent/{agent_name}",
274 response_model=VulnerabilitySyncResponse,
275 description="Sync vulnerabilities for a specific agent with performance options",
276 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
277 deprecated=True, # Marking this endpoint as deprecated in favor of more specific ones
278 )
279 async def sync_agent_vulnerabilities(
280 agent_name: str,
281 customer_code: Optional[str] = Query(None, description="Override customer code"),
282 batch_size: int = Query(100, description="Batch size for processing (1-1000)", ge=1, le=1000),
283 use_bulk_mode: bool = Query(False, description="Use ultra-fast bulk mode for large datasets"),
284 db: AsyncSession = Depends(get_db),
285 ) -> VulnerabilitySyncResponse:
286 """
287 Sync vulnerabilities for a specific agent with performance optimization options.
288
289 **Performance Modes:**
290 - **Batch Mode** (default): Processes vulnerabilities in configurable batches with individual error handling
291 - **Bulk Mode**: Ultra-fast processing using bulk database operations for large datasets
292
293 Args:
294 agent_name: Name/hostname of the agent to sync vulnerabilities for
295 customer_code: Optional customer code override
296 batch_size: Number of vulnerabilities to process per batch (1-1000, default: 100)
297 use_bulk_mode: Enable ultra-fast bulk processing mode for large datasets
298 db: Database session
299
300 Returns:
301 VulnerabilitySyncResponse: Status of the sync operation
302 """
303 logger.info(f"Starting vulnerability sync for agent: {agent_name} (batch_size={batch_size}, bulk_mode={use_bulk_mode})")
304
305 try:
306 # Use the standalone function directly with performance options
307 result = await sync_vulnerabilities_for_agent(
308 db_session=db,
309 agent_name=agent_name,
310 customer_code=customer_code,
311 batch_size=batch_size,
312 use_bulk_mode=use_bulk_mode,
313 )
314 return result
315
316 except Exception as e:
317 logger.error(f"Error syncing vulnerabilities for agent {agent_name}: {e}")
318 raise HTTPException(status_code=500, detail=f"Failed to sync vulnerabilities for agent {agent_name}: {e}")
319
320
321 @vulnerabilities_router.delete(
322 "/delete",
323 response_model=VulnerabilityDeleteResponse,
324 description="Delete vulnerabilities based on scope",
325 dependencies=[Security(AuthHandler().require_any_scope("admin"))],
326 )
327 async def delete_vulnerabilities_endpoint(
328 agent_name: Optional[str] = Query(None, description="Delete vulnerabilities for specific agent"),
329 customer_code: Optional[str] = Query(None, description="Delete vulnerabilities for specific customer"),
330 confirm_delete_all: bool = Query(False, description="Required confirmation to delete ALL vulnerabilities"),
331 db: AsyncSession = Depends(get_db),
332 ) -> VulnerabilityDeleteResponse:
333 """
334 Delete vulnerabilities based on scope:
335
336 - If neither agent_name nor customer_code provided: Delete ALL vulnerabilities (requires confirm_delete_all=true)
337 - If agent_name provided: Delete vulnerabilities for that specific agent
338 - If customer_code provided: Delete vulnerabilities for all agents of that customer
339
340 **WARNING**: Deleting all vulnerabilities is irreversible. Use with caution.
341
342 Args:
343 agent_name: Optional agent name to delete vulnerabilities for
344 customer_code: Optional customer code to delete vulnerabilities for
345 confirm_delete_all: Required confirmation when deleting ALL vulnerabilities
346 db: Database session
347
348 Returns:
349 VulnerabilityDeleteResponse: Status of the delete operation
350 """
351
352 # Safety check for deleting all vulnerabilities
353 if not agent_name and not customer_code:
354 if not confirm_delete_all:
355 raise HTTPException(status_code=400, detail="To delete ALL vulnerabilities, you must set confirm_delete_all=true")
356 logger.warning("Request to delete ALL vulnerabilities received with confirmation")
357
358 # Validate that both agent_name and customer_code are not provided
359 if agent_name and customer_code:
360 raise HTTPException(status_code=400, detail="Cannot specify both agent_name and customer_code. Choose one scope.")
361
362 try:
363 result = await delete_vulnerabilities(db_session=db, agent_name=agent_name, customer_code=customer_code)
364 return result
365
366 except Exception as e:
367 logger.error(f"Error in delete vulnerabilities endpoint: {e}")
368 raise HTTPException(status_code=500, detail=f"Failed to delete vulnerabilities: {e}")
369
370
371 @vulnerabilities_router.get(
372 "/search",
373 response_model=VulnerabilitySearchResponse,
374 description="Search vulnerabilities directly from Wazuh indexer with filtering and pagination",
375 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
376 )
377 async def search_vulnerabilities(
378 customer_code: Optional[str] = Query(None, description="Filter by customer code"),
379 agent_name: Optional[str] = Query(None, description="Filter by agent hostname"),
380 severity: Optional[str] = Query(None, description="Filter by severity (Critical, High, Medium, Low)"),
381 cve_id: Optional[str] = Query(None, description="Filter by specific CVE ID"),
382 package_name: Optional[str] = Query(None, description="Filter by package name"),
383 page: int = Query(1, description="Page number for pagination", ge=1),
384 page_size: int = Query(50, description="Number of vulnerabilities per page", ge=1, le=1000),
385 include_epss: bool = Query(True, description="Include EPSS scores (may impact performance)"),
386 current_user: User = Depends(AuthHandler().get_current_user),
387 db: AsyncSession = Depends(get_db),
388 ) -> VulnerabilitySearchResponse:
389 """
390 Search vulnerabilities directly from Wazuh indexer without storing them in database.
391
392 This endpoint provides fast, real-time vulnerability data with advanced filtering
393 and pagination capabilities. Perfect for exploring vulnerability data without
394 the overhead of database synchronization.
395
396 **Customer Access Control:**
397 - Admin/analyst users: Can access vulnerabilities for all customers
398 - Customer users: Can only access vulnerabilities for their assigned customers
399 - Customer filtering is automatically applied based on user permissions
400
401 **Features:**
402 - Real-time data directly from Wazuh indexer
403 - Advanced filtering by customer, agent, severity, CVE, or package
404 - Efficient pagination for large result sets
405 - No database storage required
406 - Optional EPSS scoring integration
407 - Automatic customer access filtering based on user role
408
409 **Performance:**
410 - Handles large datasets efficiently with pagination
411 - Optimized Elasticsearch queries for fast response times
412 - Automatic sorting by EPSS score (highest to lowest) when include_epss=True
413 - Falls back to detection date and severity sorting when include_epss=False
414 - EPSS scoring can be disabled for faster response times
415
416 **Filtering Options:**
417 - **customer_code**: Filter by specific customer (subject to user access permissions)
418 - **agent_name**: Filter by specific agent hostname
419 - **severity**: Filter by vulnerability severity (Critical, High, Medium, Low)
420 - **cve_id**: Search for specific CVE identifier
421 - **package_name**: Filter by package name (supports partial matching)
422
423 **EPSS Integration:**
424 - **include_epss**: Include EPSS scores and percentiles for vulnerabilities
425 - Provides risk assessment data from FIRST.org
426 - Results are automatically sorted by EPSS score (highest to lowest)
427 - May impact response time due to external API calls
428
429 **Pagination:**
430 - **page**: Page number (starts at 1)
431 - **page_size**: Results per page (1-1000, default: 50)
432
433 **Sorting Behavior:**
434 - When **include_epss=True**: Results sorted by EPSS score (highest to lowest), then by severity, then by CVE ID
435 - When **include_epss=False**: Results sorted by detection date (newest first), then by severity
436
437 Args:
438 customer_code: Optional customer code filter (filtered by user access)
439 agent_name: Optional agent hostname filter
440 severity: Optional severity filter
441 cve_id: Optional CVE ID filter
442 package_name: Optional package name filter (partial matching)
443 page: Page number for pagination
444 page_size: Number of results per page
445 current_user: Current authenticated user (automatically injected)
446 db: Database session
447
448 Returns:
449 VulnerabilitySearchResponse: Paginated vulnerability search results filtered by user access
450 """
451 logger.info(
452 f"Searching vulnerabilities from indexer with filters: "
453 f"customer_code={customer_code}, agent_name={agent_name}, "
454 f"severity={severity}, cve_id={cve_id}, package_name={package_name}, "
455 f"page={page}, page_size={page_size}, include_epss={include_epss}",
456 )
457
458 try:
459 result = await search_vulnerabilities_from_indexer(
460 db_session=db,
461 current_user=current_user,
462 customer_code=customer_code,
463 agent_name=agent_name,
464 severity=severity,
465 cve_id=cve_id,
466 package_name=package_name,
467 page=page,
468 page_size=page_size,
469 include_epss=include_epss,
470 )
471 return result
472
473 except Exception as e:
474 logger.error(f"Error in vulnerability search endpoint: {e}")
475 raise HTTPException(status_code=500, detail=f"Failed to search vulnerabilities: {e}")
476
477
478 @vulnerabilities_router.post(
479 "/reports/generate",
480 response_model=VulnerabilityReportGenerateResponse,
481 description="Generate a CSV vulnerability report for a specific customer",
482 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
483 )
484 async def generate_report(
485 request: VulnerabilityReportGenerateRequest,
486 current_user: User = Depends(AuthHandler().get_current_user),
487 db: AsyncSession = Depends(get_db),
488 ) -> VulnerabilityReportGenerateResponse:
489 """
490 Generate a CSV vulnerability report for a specific customer.
491
492 The report will include all vulnerabilities matching the specified filters
493 and will be stored in MinIO for later retrieval.
494
495 **Features:**
496 - Generates comprehensive CSV report with all vulnerability details
497 - Applies same filtering as search endpoint
498 - Stores report in MinIO for persistent access
499 - Tracks report metadata in database
500 - Optional EPSS scoring inclusion
501
502 **Report Contents:**
503 - CVE ID, Severity, Title
504 - Agent Name, Customer Code
505 - Package details (name, version, architecture)
506 - Detection and publication dates
507 - EPSS scores (if enabled)
508 - References
509
510 Args:
511 request: Report generation request with filters
512 current_user: Current authenticated user
513 db: Database session
514
515 Returns:
516 VulnerabilityReportGenerateResponse with report details and download URL
517 """
518 return await generate_vulnerability_csv_report(db, current_user, request)
519
520
521 @vulnerabilities_router.post(
522 "/reports/generate/background",
523 response_model=dict,
524 description="Generate a CSV vulnerability report as a background task (recommended for large datasets)",
525 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
526 )
527 async def generate_report_background(
528 request: VulnerabilityReportGenerateRequest,
529 background_tasks: BackgroundTasks,
530 current_user: User = Depends(AuthHandler().get_current_user),
531 db: AsyncSession = Depends(get_db),
532 ) -> dict:
533 """
534 Generate a CSV vulnerability report as a background task.
535
536 This endpoint is recommended for large datasets that may take significant time
537 to process. The report will be generated asynchronously and can be retrieved
538 later via the list/download endpoints.
539
540 **Workflow:**
541 1. Submit report generation request (returns immediately with report_id)
542 2. Poll the `/reports` endpoint to check for completion
543 3. Download the completed report via `/reports/{report_id}/download`
544
545 **Features:**
546 - Non-blocking operation (immediate response)
547 - Generates comprehensive CSV report with all vulnerability details
548 - Applies same filtering as search endpoint
549 - Stores report in MinIO for later retrieval
550 - Tracks report metadata and status in database
551 - Handles large datasets efficiently
552
553 **Status Tracking:**
554 - Reports are created with status "processing"
555 - Check status via `/reports` endpoint
556 - Status changes to "completed" when done
557 - If errors occur, status becomes "failed" with error message
558
559 Args:
560 request: Report generation request with filters
561 background_tasks: FastAPI background tasks
562 current_user: Current authenticated user
563 db: Database session
564
565 Returns:
566 dict: Confirmation with queued report details
567 """
568 logger.info(f"Queueing vulnerability report generation for customer: {request.customer_code}")
569
570 try:
571 # Create a "processing" report record immediately
572 timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
573 report_name = request.report_name or f"vulnerability_report_{timestamp}"
574
575 # Build filters JSON
576 filters = {}
577 if request.agent_name:
578 filters["agent_name"] = request.agent_name
579 if request.severity:
580 filters["severity"] = request.severity
581 if request.cve_id:
582 filters["cve_id"] = request.cve_id
583 if request.package_name:
584 filters["package_name"] = request.package_name
585 filters["include_epss"] = request.include_epss
586
587 # Create placeholder report record
588 report_record = VulnerabilityReport(
589 report_name=report_name,
590 customer_code=request.customer_code,
591 bucket_name="vulnerability-reports",
592 object_key=f"{request.customer_code}/{report_name}.csv",
593 file_name=f"{report_name}.csv",
594 file_size=0,
595 file_hash="pending",
596 generated_by=current_user.id,
597 filters_json=json.dumps(filters),
598 status="processing",
599 )
600
601 db.add(report_record)
602 await db.commit()
603 await db.refresh(report_record)
604
605 report_id = report_record.id
606
607 # Define the background task
608 async def generate_report_task():
609 try:
610 # Create a new database session for the background task
611 async with get_db_session() as bg_db:
612 result = await generate_vulnerability_csv_report(bg_db, current_user, request, report_id=report_id)
613
614 if result.success:
615 logger.info(f"Successfully completed background report generation (ID: {report_id})")
616 else:
617 logger.error(f"Background report generation failed (ID: {report_id}): {result.error}")
618
619 except Exception as e:
620 logger.error(f"Background report generation failed (ID: {report_id}): {e}")
621 # Update report status to failed
622 try:
623 async with get_db_session() as bg_db:
624 stmt = select(VulnerabilityReport).filter(VulnerabilityReport.id == report_id)
625 result_db = await bg_db.execute(stmt)
626 report = result_db.scalars().first()
627 if report:
628 report.status = "failed"
629 report.error_message = str(e)
630 await bg_db.commit()
631 except Exception as update_error:
632 logger.error(f"Failed to update report status: {update_error}")
633
634 # Add the task to background tasks
635 background_tasks.add_task(generate_report_task)
636
637 return {
638 "success": True,
639 "message": "Report generation queued successfully",
640 "report_id": report_id,
641 "report_name": report_name,
642 "customer_code": request.customer_code,
643 "status": "processing",
644 "check_status_url": "/api/v1/vulnerabilities/reports",
645 "download_url": f"/api/v1/vulnerabilities/reports/{report_id}/download",
646 }
647
648 except Exception as e:
649 logger.error(f"Error queueing report generation: {e}")
650 raise HTTPException(status_code=500, detail=f"Failed to queue report generation: {e}")
651
652
653 @vulnerabilities_router.get(
654 "/reports",
655 response_model=VulnerabilityReportListResponse,
656 description="List available vulnerability reports",
657 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
658 )
659 async def list_reports(
660 customer_code: Optional[str] = Query(None, description="Filter by customer code"),
661 current_user: User = Depends(AuthHandler().get_current_user),
662 db: AsyncSession = Depends(get_db),
663 ) -> VulnerabilityReportListResponse:
664 """
665 List all available vulnerability reports.
666
667 Reports are filtered based on user access permissions.
668
669 **Features:**
670 - Lists all reports accessible to the user
671 - Includes report metadata and statistics
672 - Provides download URLs for each report
673 - Filters by customer if specified
674
675 Args:
676 customer_code: Optional filter by customer code
677 current_user: Current authenticated user
678 db: Database session
679
680 Returns:
681 VulnerabilityReportListResponse with list of available reports
682 """
683 return await list_vulnerability_reports(db, current_user, customer_code)
684
685
686 @vulnerabilities_router.get(
687 "/reports/{report_id}/download",
688 description="Download a vulnerability report CSV file",
689 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
690 )
691 async def download_report(
692 report_id: int,
693 current_user: User = Depends(AuthHandler().get_current_user),
694 db: AsyncSession = Depends(get_db),
695 ):
696 """
697 Download a vulnerability report CSV file.
698
699 **Features:**
700 - Retrieves report from MinIO storage
701 - Verifies user has access to the customer
702 - Returns CSV file as downloadable attachment
703
704 Args:
705 report_id: ID of the report to download
706 current_user: Current authenticated user
707 db: Database session
708
709 Returns:
710 StreamingResponse with CSV file
711 """
712 report_data = await get_vulnerability_report_download(db, current_user, report_id)
713
714 return StreamingResponse(
715 io.BytesIO(report_data["file_content"]),
716 media_type=report_data["content_type"],
717 headers={"Content-Disposition": f'attachment; filename="{report_data["file_name"]}"'},
718 )
719
720
721 @vulnerabilities_router.delete(
722 "/reports/{report_id}",
723 response_model=dict,
724 description="Delete a vulnerability report",
725 dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))],
726 )
727 async def delete_report(
728 report_id: int,
729 current_user: User = Depends(AuthHandler().get_current_user),
730 db: AsyncSession = Depends(get_db),
731 ) -> dict:
732 """
733 Delete a vulnerability report and its associated file from MinIO.
734
735 This endpoint removes both the database record and the CSV file stored in MinIO.
736 Users can only delete reports for customers they have access to.
737
738 **Features:**
739 - Deletes report metadata from database
740 - Removes CSV file from MinIO storage
741 - Verifies user has access to the customer
742 - Provides detailed error messages
743
744 **Access Control:**
745 - Admin/analyst users: Can delete reports for customers they have access to
746 - Customer users: Can only delete reports for their assigned customers
747
748 Args:
749 report_id: ID of the report to delete
750 current_user: Current authenticated user
751 db: Database session
752
753 Returns:
754 dict: Confirmation of successful deletion
755 """
756 from app.data_store.data_store_operations import delete_file_from_minio
757 from app.middleware.customer_access import customer_access_handler
758
759 try:
760 # Get the report record
761 stmt = select(VulnerabilityReport).filter(VulnerabilityReport.id == report_id)
762 result = await db.execute(stmt)
763 report = result.scalars().first()
764
765 if not report:
766 raise HTTPException(status_code=404, detail=f"Report with ID {report_id} not found")
767
768 # Verify customer access
769 accessible_customers = await customer_access_handler.get_user_accessible_customers(current_user, db)
770
771 if "*" not in accessible_customers and report.customer_code not in accessible_customers:
772 raise HTTPException(status_code=403, detail=f"Access denied to delete report for customer {report.customer_code}")
773
774 logger.info(f"Deleting vulnerability report ID {report_id} for customer {report.customer_code}")
775
776 # Delete file from MinIO
777 minio_result = await delete_file_from_minio(
778 bucket_name=report.bucket_name,
779 object_key=report.object_key,
780 )
781
782 if not minio_result["success"]:
783 logger.warning(
784 f"Failed to delete file from MinIO for report {report_id}: {minio_result.get('error')}. "
785 "Proceeding with database deletion.",
786 )
787
788 # Delete database record
789 await db.delete(report)
790 await db.commit()
791
792 logger.info(f"Successfully deleted vulnerability report ID {report_id}")
793
794 return {
795 "success": True,
796 "message": f"Report '{report.report_name}' deleted successfully",
797 "report_id": report_id,
798 "report_name": report.report_name,
799 "customer_code": report.customer_code,
800 }
801
802 except HTTPException:
803 raise
804 except Exception as e:
805 logger.error(f"Error deleting vulnerability report {report_id}: {e}")
806 raise HTTPException(status_code=500, detail=f"Failed to delete report: {e}")