Coverage for src/alprina_cli/services/insights_service.py: 11%

104 statements  

« prev     ^ index     » next       coverage.py v7.11.3, created at 2025-11-14 11:27 +0100

1""" 

2Insights Service - Provides security analytics and trend analysis 

3""" 

4from typing import Dict, Any, Optional, List 

5from datetime import datetime, timedelta 

6from ..api.services.neon_service import NeonService 

7 

8 

9class InsightsService: 

10 """Service for generating security insights and recommendations""" 

11 

12 def __init__(self): 

13 self.db = NeonService() 

14 

15 def get_weekly_summary(self, user_id: str) -> Dict[str, Any]: 

16 """ 

17 Get weekly security summary for a user 

18 

19 Returns: 

20 Dictionary with weekly stats including new findings, scan count, etc. 

21 """ 

22 try: 

23 # Get critical findings this week 

24 critical_query = """ 

25 SELECT COUNT(*) as count 

26 FROM alerts 

27 WHERE user_id = %s 

28 AND severity = 'critical' 

29 AND created_at >= NOW() - INTERVAL '7 days' 

30 """ 

31 critical_result = self.db.execute_query(critical_query, (user_id,)) 

32 critical_count = critical_result[0]['count'] if critical_result else 0 

33 

34 # Get high findings this week 

35 high_query = """ 

36 SELECT COUNT(*) as count 

37 FROM alerts 

38 WHERE user_id = %s 

39 AND severity = 'high' 

40 AND created_at >= NOW() - INTERVAL '7 days' 

41 """ 

42 high_result = self.db.execute_query(high_query, (user_id,)) 

43 high_count = high_result[0]['count'] if high_result else 0 

44 

45 # Get total scans this week 

46 scans_query = """ 

47 SELECT COUNT(*) as count 

48 FROM scans 

49 WHERE user_id = %s 

50 AND created_at >= NOW() - INTERVAL '7 days' 

51 """ 

52 scans_result = self.db.execute_query(scans_query, (user_id,)) 

53 scans_count = scans_result[0]['count'] if scans_result else 0 

54 

55 # Get total vulnerabilities found this week 

56 vulns_query = """ 

57 SELECT COALESCE(SUM( 

58 COALESCE(critical_count, 0) + 

59 COALESCE(high_count, 0) + 

60 COALESCE(medium_count, 0) + 

61 COALESCE(low_count, 0) 

62 ), 0) as total_vulnerabilities 

63 FROM scans 

64 WHERE user_id = %s 

65 AND created_at >= NOW() - INTERVAL '7 days' 

66 """ 

67 vulns_result = self.db.execute_query(vulns_query, (user_id,)) 

68 total_vulns = int(vulns_result[0]['total_vulnerabilities']) if vulns_result else 0 

69 

70 return { 

71 'critical_findings': critical_count, 

72 'high_findings': high_count, 

73 'total_scans': scans_count, 

74 'total_vulnerabilities': total_vulns, 

75 'period': 'last_7_days' 

76 } 

77 

78 except Exception as e: 

79 print(f"❌ Error getting weekly summary: {e}") 

80 return { 

81 'critical_findings': 0, 

82 'high_findings': 0, 

83 'total_scans': 0, 

84 'total_vulnerabilities': 0, 

85 'period': 'last_7_days' 

86 } 

87 

88 def get_most_scanned_target(self, user_id: str, days: int = 30) -> Optional[Dict[str, Any]]: 

89 """ 

90 Get the most frequently scanned target 

91 

92 Args: 

93 user_id: User ID 

94 days: Number of days to look back (default: 30) 

95 

96 Returns: 

97 Dictionary with target and scan count, or None 

98 """ 

99 try: 

100 query = """ 

101 SELECT target, COUNT(*) as scan_count 

102 FROM scans 

103 WHERE user_id = %s 

104 AND created_at >= NOW() - INTERVAL '%s days' 

105 AND target IS NOT NULL 

106 AND target != '' 

107 GROUP BY target 

108 ORDER BY scan_count DESC 

109 LIMIT 1 

110 """ 

111 result = self.db.execute_query(query, (user_id, days)) 

112 

113 if result and len(result) > 0: 

114 return { 

115 'target': result[0]['target'], 

116 'scan_count': result[0]['scan_count'] 

117 } 

118 

119 return None 

120 

121 except Exception as e: 

122 print(f"❌ Error getting most scanned target: {e}") 

123 return None 

124 

125 def get_security_trend(self, user_id: str) -> Dict[str, Any]: 

126 """ 

127 Calculate security trend (improving or declining) 

128 Compares current week vs previous week 

129 

130 Returns: 

131 Dictionary with trend direction and percentage change 

132 """ 

133 try: 

134 # Current week (last 7 days) 

135 current_query = """ 

136 SELECT 

137 COALESCE(SUM(COALESCE(critical_count, 0)), 0) as critical, 

138 COALESCE(SUM(COALESCE(high_count, 0)), 0) as high, 

139 COALESCE(SUM(COALESCE(medium_count, 0)), 0) as medium 

140 FROM scans 

141 WHERE user_id = %s 

142 AND created_at >= NOW() - INTERVAL '7 days' 

143 """ 

144 current_result = self.db.execute_query(current_query, (user_id,)) 

145 

146 # Previous week (7-14 days ago) 

147 previous_query = """ 

148 SELECT 

149 COALESCE(SUM(COALESCE(critical_count, 0)), 0) as critical, 

150 COALESCE(SUM(COALESCE(high_count, 0)), 0) as high, 

151 COALESCE(SUM(COALESCE(medium_count, 0)), 0) as medium 

152 FROM scans 

153 WHERE user_id = %s 

154 AND created_at >= NOW() - INTERVAL '14 days' 

155 AND created_at < NOW() - INTERVAL '7 days' 

156 """ 

157 previous_result = self.db.execute_query(previous_query, (user_id,)) 

158 

159 if not current_result or not previous_result: 

160 return { 

161 'trend': 'stable', 

162 'direction': 'none', 

163 'change_percentage': 0, 

164 'message': 'Not enough data for trend analysis' 

165 } 

166 

167 # Calculate weighted score (critical=3, high=2, medium=1) 

168 current_score = ( 

169 int(current_result[0]['critical']) * 3 + 

170 int(current_result[0]['high']) * 2 + 

171 int(current_result[0]['medium']) * 1 

172 ) 

173 

174 previous_score = ( 

175 int(previous_result[0]['critical']) * 3 + 

176 int(previous_result[0]['high']) * 2 + 

177 int(previous_result[0]['medium']) * 1 

178 ) 

179 

180 # Calculate change 

181 if previous_score == 0: 

182 if current_score == 0: 

183 return { 

184 'trend': 'stable', 

185 'direction': 'none', 

186 'change_percentage': 0, 

187 'message': 'No security issues detected' 

188 } 

189 else: 

190 return { 

191 'trend': 'declining', 

192 'direction': 'down', 

193 'change_percentage': 100, 

194 'message': 'New security issues detected this week' 

195 } 

196 

197 change_percentage = ((current_score - previous_score) / previous_score) * 100 

198 

199 # Determine trend (negative change = improving, positive = declining) 

200 if change_percentage < -10: 

201 trend = 'improving' 

202 direction = 'up' 

203 message = f"Security improved by {abs(int(change_percentage))}% this week" 

204 elif change_percentage > 10: 

205 trend = 'declining' 

206 direction = 'down' 

207 message = f"Security declined by {int(change_percentage)}% this week" 

208 else: 

209 trend = 'stable' 

210 direction = 'none' 

211 message = "Security posture is stable" 

212 

213 return { 

214 'trend': trend, 

215 'direction': direction, 

216 'change_percentage': abs(int(change_percentage)), 

217 'message': message, 

218 'current_score': current_score, 

219 'previous_score': previous_score 

220 } 

221 

222 except Exception as e: 

223 print(f"❌ Error calculating security trend: {e}") 

224 return { 

225 'trend': 'stable', 

226 'direction': 'none', 

227 'change_percentage': 0, 

228 'message': 'Unable to calculate trend' 

229 } 

230 

231 def get_recommendations(self, user_id: str) -> List[Dict[str, Any]]: 

232 """ 

233 Generate smart security recommendations based on scan history 

234 

235 Returns: 

236 List of recommendation dictionaries 

237 """ 

238 try: 

239 recommendations = [] 

240 

241 # Check for critical findings 

242 critical_query = """ 

243 SELECT COUNT(*) as count 

244 FROM alerts 

245 WHERE user_id = %s 

246 AND severity = 'critical' 

247 AND is_read = false 

248 """ 

249 critical_result = self.db.execute_query(critical_query, (user_id,)) 

250 critical_unread = critical_result[0]['count'] if critical_result else 0 

251 

252 if critical_unread > 0: 

253 recommendations.append({ 

254 'priority': 'critical', 

255 'title': 'Urgent: Address Critical Security Issues', 

256 'description': f'You have {critical_unread} unread critical security finding{"s" if critical_unread != 1 else ""}. These require immediate attention.', 

257 'action': 'View Critical Alerts', 

258 'action_url': '/dashboard/alerts?severity=critical' 

259 }) 

260 

261 # Check scan frequency 

262 recent_scans_query = """ 

263 SELECT COUNT(*) as count 

264 FROM scans 

265 WHERE user_id = %s 

266 AND created_at >= NOW() - INTERVAL '7 days' 

267 """ 

268 recent_result = self.db.execute_query(recent_scans_query, (user_id,)) 

269 recent_scans = recent_result[0]['count'] if recent_result else 0 

270 

271 if recent_scans == 0: 

272 recommendations.append({ 

273 'priority': 'medium', 

274 'title': 'No Recent Security Scans', 

275 'description': 'You haven\'t run any security scans this week. Regular scanning helps identify vulnerabilities early.', 

276 'action': 'Run a Scan Now', 

277 'action_url': '/dashboard' 

278 }) 

279 elif recent_scans < 3: 

280 recommendations.append({ 

281 'priority': 'low', 

282 'title': 'Increase Scan Frequency', 

283 'description': 'Consider running security scans more frequently to catch issues early. Aim for at least weekly scans.', 

284 'action': 'Schedule Regular Scans', 

285 'action_url': '/dashboard/settings' 

286 }) 

287 

288 # Check for most common vulnerability types 

289 vuln_types_query = """ 

290 SELECT 

291 COALESCE(SUM(COALESCE(critical_count, 0)), 0) as critical, 

292 COALESCE(SUM(COALESCE(high_count, 0)), 0) as high, 

293 COALESCE(SUM(COALESCE(medium_count, 0)), 0) as medium 

294 FROM scans 

295 WHERE user_id = %s 

296 AND created_at >= NOW() - INTERVAL '30 days' 

297 """ 

298 vuln_result = self.db.execute_query(vuln_types_query, (user_id,)) 

299 

300 if vuln_result: 

301 total_vulns = ( 

302 int(vuln_result[0]['critical']) + 

303 int(vuln_result[0]['high']) + 

304 int(vuln_result[0]['medium']) 

305 ) 

306 

307 if total_vulns > 20: 

308 recommendations.append({ 

309 'priority': 'high', 

310 'title': 'High Volume of Vulnerabilities', 

311 'description': f'Detected {total_vulns} vulnerabilities in the last 30 days. Consider implementing automated security controls.', 

312 'action': 'View Scan History', 

313 'action_url': '/dashboard/scans' 

314 }) 

315 

316 # If no recommendations, add a positive one 

317 if len(recommendations) == 0: 

318 recommendations.append({ 

319 'priority': 'info', 

320 'title': 'Security Looking Good!', 

321 'description': 'Your security posture is strong. Keep up the regular scanning and monitoring.', 

322 'action': 'View Analytics', 

323 'action_url': '/dashboard/analytics' 

324 }) 

325 

326 return recommendations 

327 

328 except Exception as e: 

329 print(f"❌ Error generating recommendations: {e}") 

330 return [{ 

331 'priority': 'info', 

332 'title': 'Welcome to Alprina', 

333 'description': 'Start by running your first security scan to get personalized recommendations.', 

334 'action': 'Get Started', 

335 'action_url': '/dashboard' 

336 }] 

337 

338 def get_top_vulnerable_targets(self, user_id: str, limit: int = 5) -> List[Dict[str, Any]]: 

339 """ 

340 Get targets with the most vulnerabilities 

341 

342 Args: 

343 user_id: User ID 

344 limit: Number of targets to return 

345 

346 Returns: 

347 List of targets with vulnerability counts 

348 """ 

349 try: 

350 query = """ 

351 SELECT 

352 target, 

353 COALESCE(SUM(COALESCE(critical_count, 0)), 0) as critical, 

354 COALESCE(SUM(COALESCE(high_count, 0)), 0) as high, 

355 COALESCE(SUM(COALESCE(medium_count, 0)), 0) as medium, 

356 COALESCE(SUM(COALESCE(low_count, 0)), 0) as low, 

357 COUNT(*) as scan_count, 

358 MAX(created_at) as last_scan 

359 FROM scans 

360 WHERE user_id = %s 

361 AND created_at >= NOW() - INTERVAL '30 days' 

362 AND target IS NOT NULL 

363 AND target != '' 

364 GROUP BY target 

365 ORDER BY ( 

366 COALESCE(SUM(COALESCE(critical_count, 0)), 0) * 3 + 

367 COALESCE(SUM(COALESCE(high_count, 0)), 0) * 2 + 

368 COALESCE(SUM(COALESCE(medium_count, 0)), 0) * 1 

369 ) DESC 

370 LIMIT %s 

371 """ 

372 results = self.db.execute_query(query, (user_id, limit)) 

373 

374 if not results: 

375 return [] 

376 

377 targets = [] 

378 for row in results: 

379 targets.append({ 

380 'target': row['target'], 

381 'critical': int(row['critical']), 

382 'high': int(row['high']), 

383 'medium': int(row['medium']), 

384 'low': int(row['low']), 

385 'total_vulnerabilities': int(row['critical']) + int(row['high']) + int(row['medium']) + int(row['low']), 

386 'scan_count': int(row['scan_count']), 

387 'last_scan': row['last_scan'].isoformat() if row['last_scan'] else None 

388 }) 

389 

390 return targets 

391 

392 except Exception as e: 

393 print(f"❌ Error getting vulnerable targets: {e}") 

394 return []