api_scanner/discovery/
mod.rs1pub mod common_paths;
2pub mod headers;
3pub mod js;
4pub mod robots;
5pub mod sitemap;
6pub mod swagger;
7
8use std::collections::HashSet;
9
10pub fn normalize_path(raw: &str, target_host: &str) -> Option<String> {
13 let raw = raw.trim();
14 if raw.is_empty() {
15 return None;
16 }
17
18 if raw.starts_with("http://") || raw.starts_with("https://") {
20 if let Ok(parsed) = url::Url::parse(raw) {
21 if parsed.host_str().unwrap_or("") != target_host {
22 return None; }
24 return normalize_path(parsed.path(), target_host);
25 }
26 return None;
27 }
28
29 let mut path = raw.to_string();
30 if !path.starts_with('/') {
31 path = format!("/{path}");
32 }
33
34 if path.len() > 1 && path.ends_with('/') {
36 path.pop();
37 }
38
39 Some(path)
40}
41
42pub fn is_interesting(path: &str) -> bool {
44 const KEYWORDS: &[&str] = &[
45 "api", "graphql", "swagger", "openapi", "admin", "internal", "private", "debug",
46 "actuator", "metrics", "health", "config", "oauth", "auth", "token", "session", "keys",
47 "secret", "rest", "v1", "v2", "v3", "webhook", "upload", "download",
48 ];
49 let lower = path.to_lowercase();
50 KEYWORDS.iter().any(|k| lower.contains(k))
51}
52
53pub fn collect_paths(
55 raws: impl IntoIterator<Item = String>,
56 host: &str,
57 out: &mut HashSet<String>,
58) {
59 for raw in raws {
60 if let Some(p) = normalize_path(&raw, host) {
61 out.insert(p);
62 }
63 }
64}