1use crate::analyzer::{
9 DetectedTechnology, HealthEndpoint, HealthEndpointSource, TechnologyCategory,
10};
11use crate::common::file_utils::{is_readable_file, read_file_safe};
12use regex::Regex;
13use std::path::Path;
14
15const COMMON_HEALTH_PATHS: &[&str] = &[
17 "/health",
18 "/healthz",
19 "/ready",
20 "/readyz",
21 "/livez",
22 "/live",
23 "/api/health",
24 "/api/v1/health",
25 "/__health",
26 "/ping",
27 "/status",
28];
29
30pub fn detect_health_endpoints(
32 project_root: &Path,
33 technologies: &[DetectedTechnology],
34 max_file_size: usize,
35) -> Vec<HealthEndpoint> {
36 let mut endpoints = Vec::new();
37
38 for tech in technologies {
40 if let Some(endpoint) = get_framework_health_endpoint(tech) {
41 endpoints.push(endpoint);
42 }
43 }
44
45 let detected_from_code = scan_for_health_routes(project_root, technologies, max_file_size);
47 for endpoint in detected_from_code {
48 if !endpoints.iter().any(|e| e.path == endpoint.path) {
50 endpoints.push(endpoint);
51 } else {
52 if let Some(existing) = endpoints.iter_mut().find(|e| e.path == endpoint.path) {
54 if endpoint.confidence > existing.confidence {
55 *existing = endpoint;
56 }
57 }
58 }
59 }
60
61 endpoints.sort_by(|a, b| {
63 b.confidence
64 .partial_cmp(&a.confidence)
65 .unwrap_or(std::cmp::Ordering::Equal)
66 });
67
68 endpoints
69}
70
71fn get_framework_health_endpoint(tech: &DetectedTechnology) -> Option<HealthEndpoint> {
73 match tech.name.as_str() {
74 "Spring Boot" => Some(HealthEndpoint::from_framework(
76 "/actuator/health",
77 "Spring Boot Actuator",
78 )),
79 "Quarkus" => Some(HealthEndpoint::from_framework(
80 "/q/health",
81 "Quarkus SmallRye Health",
82 )),
83 "Micronaut" => Some(HealthEndpoint::from_framework("/health", "Micronaut")),
84
85 "Express" | "Fastify" | "Koa" | "Hono" | "Elysia" | "NestJS" => {
87 Some(HealthEndpoint {
89 path: "/health".to_string(),
90 confidence: 0.5,
91 source: HealthEndpointSource::FrameworkDefault,
92 description: Some(format!("{} common health pattern", tech.name)),
93 })
94 }
95
96 "FastAPI" => Some(HealthEndpoint::from_framework("/health", "FastAPI")),
98 "Django" => Some(HealthEndpoint {
99 path: "/health/".to_string(), confidence: 0.5,
101 source: HealthEndpointSource::FrameworkDefault,
102 description: Some("Django common health pattern".to_string()),
103 }),
104 "Flask" => Some(HealthEndpoint {
105 path: "/health".to_string(),
106 confidence: 0.5,
107 source: HealthEndpointSource::FrameworkDefault,
108 description: Some("Flask common health pattern".to_string()),
109 }),
110
111 "Gin" | "Echo" | "Fiber" | "Chi" => Some(HealthEndpoint {
113 path: "/health".to_string(),
114 confidence: 0.5,
115 source: HealthEndpointSource::FrameworkDefault,
116 description: Some(format!("{} common health pattern", tech.name)),
117 }),
118
119 "Actix Web" | "Axum" | "Rocket" => Some(HealthEndpoint {
121 path: "/health".to_string(),
122 confidence: 0.5,
123 source: HealthEndpointSource::FrameworkDefault,
124 description: Some(format!("{} common health pattern", tech.name)),
125 }),
126
127 _ => None,
128 }
129}
130
131fn scan_for_health_routes(
133 project_root: &Path,
134 technologies: &[DetectedTechnology],
135 max_file_size: usize,
136) -> Vec<HealthEndpoint> {
137 let mut endpoints = Vec::new();
138
139 let has_js = technologies.iter().any(|t| {
141 matches!(
142 t.category,
143 TechnologyCategory::BackendFramework | TechnologyCategory::MetaFramework
144 ) && (t.name.contains("Express")
145 || t.name.contains("Fastify")
146 || t.name.contains("Koa")
147 || t.name.contains("Hono")
148 || t.name.contains("Elysia")
149 || t.name.contains("NestJS")
150 || t.name.contains("Next")
151 || t.name.contains("Nuxt"))
152 });
153
154 let has_python = technologies.iter().any(|t| {
155 matches!(t.category, TechnologyCategory::BackendFramework)
156 && (t.name.contains("FastAPI") || t.name.contains("Flask") || t.name.contains("Django"))
157 });
158
159 let has_go = technologies.iter().any(|t| {
160 matches!(t.category, TechnologyCategory::BackendFramework)
161 && (t.name.contains("Gin")
162 || t.name.contains("Echo")
163 || t.name.contains("Fiber")
164 || t.name.contains("Chi"))
165 });
166
167 let has_rust = technologies.iter().any(|t| {
168 matches!(t.category, TechnologyCategory::BackendFramework)
169 && (t.name.contains("Actix") || t.name.contains("Axum") || t.name.contains("Rocket"))
170 });
171
172 let has_java = technologies.iter().any(|t| {
173 matches!(t.category, TechnologyCategory::BackendFramework)
174 && (t.name.contains("Spring")
175 || t.name.contains("Quarkus")
176 || t.name.contains("Micronaut"))
177 });
178
179 let locations = [
181 "src/",
182 "app/",
183 "routes/",
184 "api/",
185 "server/",
186 "lib/",
187 "handlers/",
188 "controllers/",
189 ];
190
191 for location in &locations {
192 let dir = project_root.join(location);
193 if dir.is_dir() {
194 if has_js {
195 scan_directory_for_patterns(
196 &dir,
197 &["js", "ts", "mjs"],
198 &js_health_patterns(),
199 max_file_size,
200 &mut endpoints,
201 );
202 }
203 if has_python {
204 scan_directory_for_patterns(
205 &dir,
206 &["py"],
207 &python_health_patterns(),
208 max_file_size,
209 &mut endpoints,
210 );
211 }
212 if has_go {
213 scan_directory_for_patterns(
214 &dir,
215 &["go"],
216 &go_health_patterns(),
217 max_file_size,
218 &mut endpoints,
219 );
220 }
221 if has_rust {
222 scan_directory_for_patterns(
223 &dir,
224 &["rs"],
225 &rust_health_patterns(),
226 max_file_size,
227 &mut endpoints,
228 );
229 }
230 if has_java {
231 scan_directory_for_patterns(
232 &dir,
233 &["java", "kt"],
234 &java_health_patterns(),
235 max_file_size,
236 &mut endpoints,
237 );
238 }
239 }
240 }
241
242 if has_js {
244 for entry in [
245 "index.js",
246 "index.ts",
247 "app.js",
248 "app.ts",
249 "server.js",
250 "server.ts",
251 "main.js",
252 "main.ts",
253 ] {
254 let path = project_root.join(entry);
255 if is_readable_file(&path) {
256 scan_file_for_patterns(&path, &js_health_patterns(), max_file_size, &mut endpoints);
257 }
258 }
259 }
260 if has_python {
261 for entry in ["main.py", "app.py", "wsgi.py", "asgi.py"] {
262 let path = project_root.join(entry);
263 if is_readable_file(&path) {
264 scan_file_for_patterns(
265 &path,
266 &python_health_patterns(),
267 max_file_size,
268 &mut endpoints,
269 );
270 }
271 }
272 }
273 if has_go {
274 let main_go = project_root.join("main.go");
275 if is_readable_file(&main_go) {
276 scan_file_for_patterns(
277 &main_go,
278 &go_health_patterns(),
279 max_file_size,
280 &mut endpoints,
281 );
282 }
283 }
284 if has_rust {
285 let main_rs = project_root.join("src/main.rs");
286 if is_readable_file(&main_rs) {
287 scan_file_for_patterns(
288 &main_rs,
289 &rust_health_patterns(),
290 max_file_size,
291 &mut endpoints,
292 );
293 }
294 }
295
296 endpoints
297}
298
299fn scan_directory_for_patterns(
301 dir: &Path,
302 extensions: &[&str],
303 patterns: &[(&str, f32)],
304 max_file_size: usize,
305 endpoints: &mut Vec<HealthEndpoint>,
306) {
307 if let Ok(entries) = std::fs::read_dir(dir) {
308 for entry in entries.flatten() {
309 let path = entry.path();
310 if path.is_file() {
311 if let Some(ext) = path.extension() {
312 if extensions.iter().any(|e| ext == *e) {
313 scan_file_for_patterns(&path, patterns, max_file_size, endpoints);
314 }
315 }
316 } else if path.is_dir() {
317 let dir_name = path
319 .file_name()
320 .map(|n| n.to_string_lossy().to_string())
321 .unwrap_or_default();
322 if ![
323 "node_modules",
324 ".git",
325 "target",
326 "build",
327 "dist",
328 "__pycache__",
329 ".next",
330 "vendor",
331 ]
332 .contains(&dir_name.as_str())
333 {
334 scan_directory_for_patterns(
335 &path,
336 extensions,
337 patterns,
338 max_file_size,
339 endpoints,
340 );
341 }
342 }
343 }
344 }
345}
346
347fn scan_file_for_patterns(
349 path: &Path,
350 patterns: &[(&str, f32)],
351 max_file_size: usize,
352 endpoints: &mut Vec<HealthEndpoint>,
353) {
354 if let Ok(content) = read_file_safe(path, max_file_size) {
355 for (pattern, confidence) in patterns {
356 if let Ok(regex) = Regex::new(pattern) {
357 for cap in regex.captures_iter(&content) {
358 if let Some(path_match) = cap.get(1) {
359 let health_path = path_match.as_str().to_string();
360 if COMMON_HEALTH_PATHS
362 .iter()
363 .any(|p| health_path.contains(p) || p.contains(&health_path))
364 {
365 if !endpoints.iter().any(|e| e.path == health_path) {
366 endpoints.push(HealthEndpoint {
367 path: health_path,
368 confidence: *confidence,
369 source: HealthEndpointSource::CodePattern,
370 description: Some(format!("Found in {}", path.display())),
371 });
372 }
373 }
374 }
375 }
376 }
377 }
378 }
379}
380
381fn js_health_patterns() -> Vec<(&'static str, f32)> {
383 vec[^'"]*)['"]"#,
387 0.9,
388 ),
389 (
391 r#"@Get\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#,
392 0.9,
393 ),
394 (
396 r#"\.get\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#,
397 0.9,
398 ),
399 ]
400}
401
402fn python_health_patterns() -> Vec<(&'static str, f32)> {
404 vec[^'"]*)['"]"#,
408 0.9,
409 ),
410 (
412 r#"path\s*\(\s*['"]([^'"]*(?:health|ready|live|status|ping)[^'"]*)['"]"#,
413 0.85,
414 ),
415 ]
416}
417
418fn go_health_patterns() -> Vec<(&'static str, f32)> {
420 vec![
421 (
423 r#"HandleFunc\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#,
424 0.9,
425 ),
426 (
428 r#"\.(?:GET|Handle)\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#,
429 0.9,
430 ),
431 ]
432}
433
434fn rust_health_patterns() -> Vec<(&'static str, f32)> {
436 vec![
437 (
439 r#"\.route\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#,
440 0.9,
441 ),
442 (
444 r#"\.route\s*\(\s*"([^"]*(?:health|ready|live|status|ping)[^"]*)"#,
445 0.9,
446 ),
447 ]
448}
449
450fn java_health_patterns() -> Vec<(&'static str, f32)> {
452 vec[^"']*)["']"#,
456 0.9,
457 ),
458 ]
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_spring_boot_health_endpoint() {
467 let tech = DetectedTechnology {
468 name: "Spring Boot".to_string(),
469 version: None,
470 category: TechnologyCategory::BackendFramework,
471 confidence: 0.9,
472 requires: vec![],
473 conflicts_with: vec![],
474 is_primary: true,
475 file_indicators: vec![],
476 };
477
478 let endpoint = get_framework_health_endpoint(&tech).unwrap();
479 assert_eq!(endpoint.path, "/actuator/health");
480 assert_eq!(endpoint.confidence, 0.7);
481 }
482
483 #[test]
484 fn test_express_health_endpoint() {
485 let tech = DetectedTechnology {
486 name: "Express".to_string(),
487 version: None,
488 category: TechnologyCategory::BackendFramework,
489 confidence: 0.9,
490 requires: vec![],
491 conflicts_with: vec![],
492 is_primary: true,
493 file_indicators: vec![],
494 };
495
496 let endpoint = get_framework_health_endpoint(&tech).unwrap();
497 assert_eq!(endpoint.path, "/health");
498 assert_eq!(endpoint.confidence, 0.5); }
500
501 #[test]
502 fn test_unknown_framework_no_endpoint() {
503 let tech = DetectedTechnology {
504 name: "UnknownFramework".to_string(),
505 version: None,
506 category: TechnologyCategory::BackendFramework,
507 confidence: 0.9,
508 requires: vec![],
509 conflicts_with: vec![],
510 is_primary: true,
511 file_indicators: vec![],
512 };
513
514 assert!(get_framework_health_endpoint(&tech).is_none());
515 }
516}