Coverage for src/alprina_cli/api/routes/scans.py: 42%
59 statements
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
« prev ^ index » next coverage.py v7.11.3, created at 2025-11-14 11:27 +0100
1"""
2Scan management endpoints - /v1/scans/*
3"""
5from fastapi import APIRouter, HTTPException, Depends
6from pydantic import BaseModel, Field
7from typing import List, Dict, Any, Optional
9from ..services.neon_service import neon_service
10from ..middleware.auth import get_current_user
11from ..polar_meters import PolarMeterService
13router = APIRouter()
16# Request/Response Models
17class CreateScanRequest(BaseModel):
18 target: str = Field(..., description="Scan target (path, URL, etc.)")
19 scan_type: str = Field(..., description="'local' or 'remote'")
20 profile: str = Field(default="default", description="Scan profile name")
22 class Config:
23 schema_extra = {
24 "example": {
25 "target": "./src",
26 "scan_type": "local",
27 "profile": "code-audit"
28 }
29 }
32class UpdateScanRequest(BaseModel):
33 results: Dict[str, Any] = Field(..., description="Scan results data")
35 class Config:
36 schema_extra = {
37 "example": {
38 "results": {
39 "findings": [
40 {
41 "severity": "HIGH",
42 "title": "SQL Injection vulnerability",
43 "description": "User input not sanitized"
44 }
45 ],
46 "summary": {
47 "critical": 0,
48 "high": 1,
49 "medium": 2,
50 "low": 3,
51 "info": 0
52 }
53 }
54 }
55 }
58@router.post("/scans", status_code=201)
59async def create_scan(
60 request: CreateScanRequest,
61 user: Dict[str, Any] = Depends(get_current_user)
62):
63 """
64 Create a new scan entry (before execution).
66 **Example:**
67 ```bash
68 curl -X POST http://localhost:8000/v1/scans \\
69 -H "Authorization: Bearer alprina_sk_live_..." \\
70 -H "Content-Type: application/json" \\
71 -d '{
72 "target": "./src",
73 "scan_type": "local",
74 "profile": "code-audit"
75 }'
76 ```
78 **Response:**
79 ```json
80 {
81 "scan_id": "uuid-here",
82 "status": "running",
83 "created_at": "2025-11-03T18:00:00Z"
84 }
85 ```
86 """
87 if not neon_service.is_enabled():
88 raise HTTPException(
89 status_code=503,
90 detail="Database not configured"
91 )
93 try:
94 scan = await neon_service.create_scan(
95 user_id=user["id"],
96 target=request.target,
97 scan_type=request.scan_type,
98 profile=request.profile
99 )
101 return {
102 "scan_id": scan["id"],
103 "status": scan["status"],
104 "created_at": scan["created_at"],
105 "message": "Scan created successfully"
106 }
108 except Exception as e:
109 raise HTTPException(
110 status_code=500,
111 detail=f"Failed to create scan: {str(e)}"
112 )
115@router.patch("/scans/{scan_id}")
116async def update_scan(
117 scan_id: str,
118 request: UpdateScanRequest,
119 user: Dict[str, Any] = Depends(get_current_user)
120):
121 """
122 Update scan with results after completion.
124 **Example:**
125 ```bash
126 curl -X PATCH http://localhost:8000/v1/scans/{scan_id} \\
127 -H "Authorization: Bearer alprina_sk_live_..." \\
128 -H "Content-Type: application/json" \\
129 -d '{
130 "results": {
131 "findings": [...],
132 "summary": {...}
133 }
134 }'
135 ```
136 """
137 if not neon_service.is_enabled():
138 raise HTTPException(
139 status_code=503,
140 detail="Database not configured"
141 )
143 # Verify scan belongs to user
144 existing_scan = await neon_service.get_scan(scan_id, user["id"])
145 if not existing_scan:
146 raise HTTPException(404, "Scan not found")
148 try:
149 updated_scan = await neon_service.save_scan(
150 scan_id=scan_id,
151 results=request.results
152 )
154 # Report usage to Polar meter (monthly plans only)
155 if user.get("has_metering") and user.get("email"):
156 await PolarMeterService.report_scan(
157 user_email=user["email"],
158 scan_type=existing_scan.get("scan_type", "standard"),
159 target=existing_scan.get("target", "unknown"),
160 user_id=user["id"]
161 )
163 # Also update local usage counter
164 current_usage = user.get("scans_used_this_period", 0)
165 await neon_service.update_user(
166 user["id"],
167 {"scans_used_this_period": current_usage + 1}
168 )
170 return {
171 "scan_id": scan_id,
172 "status": updated_scan.get("status", "completed"),
173 "findings_count": updated_scan.get("findings_count", 0),
174 "message": "Scan updated successfully"
175 }
177 except Exception as e:
178 raise HTTPException(
179 status_code=500,
180 detail=f"Failed to update scan: {str(e)}"
181 )
184@router.get("/scans")
185async def list_scans(
186 user: Dict[str, Any] = Depends(get_current_user),
187 page: int = 1,
188 limit: int = 20,
189 severity: Optional[str] = None
190):
191 """
192 List all scans for current user with pagination.
194 **Example:**
195 ```bash
196 curl http://localhost:8000/v1/scans?page=1&limit=20 \\
197 -H "Authorization: Bearer alprina_sk_live_..."
198 ```
199 """
200 if not neon_service.is_enabled():
201 raise HTTPException(
202 status_code=503,
203 detail="Database not configured"
204 )
206 try:
207 result = await neon_service.list_scans(
208 user_id=user["id"],
209 page=page,
210 limit=limit,
211 severity=severity
212 )
214 return result
216 except Exception as e:
217 raise HTTPException(
218 status_code=500,
219 detail=f"Failed to list scans: {str(e)}"
220 )
223@router.get("/scans/{scan_id}")
224async def get_scan(
225 scan_id: str,
226 user: Dict[str, Any] = Depends(get_current_user)
227):
228 """
229 Get detailed scan results by ID.
231 **Example:**
232 ```bash
233 curl http://localhost:8000/v1/scans/{scan_id} \\
234 -H "Authorization: Bearer alprina_sk_live_..."
235 ```
236 """
237 if not neon_service.is_enabled():
238 raise HTTPException(
239 status_code=503,
240 detail="Database not configured"
241 )
243 scan = await neon_service.get_scan(scan_id, user["id"])
245 if not scan:
246 raise HTTPException(404, "Scan not found")
248 return scan