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

1""" 

2Scan management endpoints - /v1/scans/* 

3""" 

4 

5from fastapi import APIRouter, HTTPException, Depends 

6from pydantic import BaseModel, Field 

7from typing import List, Dict, Any, Optional 

8 

9from ..services.neon_service import neon_service 

10from ..middleware.auth import get_current_user 

11from ..polar_meters import PolarMeterService 

12 

13router = APIRouter() 

14 

15 

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") 

21 

22 class Config: 

23 schema_extra = { 

24 "example": { 

25 "target": "./src", 

26 "scan_type": "local", 

27 "profile": "code-audit" 

28 } 

29 } 

30 

31 

32class UpdateScanRequest(BaseModel): 

33 results: Dict[str, Any] = Field(..., description="Scan results data") 

34 

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 } 

56 

57 

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). 

65 

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 ``` 

77 

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 ) 

92 

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 ) 

100 

101 return { 

102 "scan_id": scan["id"], 

103 "status": scan["status"], 

104 "created_at": scan["created_at"], 

105 "message": "Scan created successfully" 

106 } 

107 

108 except Exception as e: 

109 raise HTTPException( 

110 status_code=500, 

111 detail=f"Failed to create scan: {str(e)}" 

112 ) 

113 

114 

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. 

123 

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 ) 

142 

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") 

147 

148 try: 

149 updated_scan = await neon_service.save_scan( 

150 scan_id=scan_id, 

151 results=request.results 

152 ) 

153 

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 ) 

162 

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 ) 

169 

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 } 

176 

177 except Exception as e: 

178 raise HTTPException( 

179 status_code=500, 

180 detail=f"Failed to update scan: {str(e)}" 

181 ) 

182 

183 

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. 

193 

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 ) 

205 

206 try: 

207 result = await neon_service.list_scans( 

208 user_id=user["id"], 

209 page=page, 

210 limit=limit, 

211 severity=severity 

212 ) 

213 

214 return result 

215 

216 except Exception as e: 

217 raise HTTPException( 

218 status_code=500, 

219 detail=f"Failed to list scans: {str(e)}" 

220 ) 

221 

222 

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. 

230 

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 ) 

242 

243 scan = await neon_service.get_scan(scan_id, user["id"]) 

244 

245 if not scan: 

246 raise HTTPException(404, "Scan not found") 

247 

248 return scan