Skip to main content

api_scanner/discovery/
common_paths.rs

1use std::collections::HashSet;
2
3use futures::stream::{self, StreamExt};
4use rand::seq::SliceRandom;
5use tracing::{debug, warn};
6
7use crate::{error::CapturedError, http_client::HttpClient};
8
9/// Built-in wordlist of high-value API / admin paths.
10/// Extend this list or load an external wordlist via `Config::wordlist`.
11static COMMON_PATHS: &[&str] = &[
12    "/api",
13    "/api/v1",
14    "/api/v2",
15    "/api/v3",
16    "/graphql",
17    "/graphiql",
18    "/playground",
19    "/swagger",
20    "/swagger.json",
21    "/swagger.yaml",
22    "/swagger-ui",
23    "/swagger-ui.html",
24    "/openapi",
25    "/openapi.json",
26    "/openapi.yaml",
27    "/api-docs",
28    "/api-docs.json",
29    "/docs",
30    "/redoc",
31    "/admin",
32    "/admin/api",
33    "/internal",
34    "/internal/api",
35    "/private",
36    "/debug",
37    "/actuator",
38    "/actuator/health",
39    "/actuator/env",
40    "/actuator/mappings",
41    "/actuator/beans",
42    "/actuator/metrics",
43    "/metrics",
44    "/health",
45    "/healthz",
46    "/readyz",
47    "/livez",
48    "/status",
49    "/.well-known/openid-configuration",
50    "/.well-known/oauth-authorization-server",
51    "/oauth/token",
52    "/oauth/authorize",
53    "/auth/token",
54    "/auth/login",
55    "/auth/refresh",
56    "/login",
57    "/logout",
58    "/register",
59    "/users",
60    "/user",
61    "/account",
62    "/accounts",
63    "/profile",
64    "/me",
65    "/config",
66    "/configuration",
67    "/settings",
68    "/env",
69    "/environment",
70    "/version",
71    "/info",
72    "/ping",
73    "/trace",
74    "/log",
75    "/logs",
76    "/debug/vars",
77    "/server-status",
78    "/server-info",
79    "/phpinfo.php",
80    "/.env",
81    "/.git/config",
82    "/wp-json/wp/v2",
83    "/wp-json",
84    "/jsonapi",
85    "/rest/v1",
86    "/rest/v2",
87    "/api/swagger.json",
88    "/api/openapi.json",
89    "/api/graphql",
90];
91
92pub struct CommonPathDiscovery<'a> {
93    client: &'a HttpClient,
94    base_url: &'a str,
95    concurrency: usize,
96    /// Optional external wordlist to merge with the built-in list
97    extra: Vec<String>,
98}
99
100impl<'a> CommonPathDiscovery<'a> {
101    pub fn new(
102        client: &'a HttpClient,
103        base_url: &'a str,
104        concurrency: usize,
105        extra: Vec<String>,
106    ) -> Self {
107        Self {
108            client,
109            base_url,
110            concurrency,
111            extra,
112        }
113    }
114
115    /// Returns only paths that responded with < 404 (i.e. exist or auth-gated)
116    pub async fn run(&self) -> (HashSet<String>, Vec<CapturedError>) {
117        let base = self.base_url.trim_end_matches('/');
118
119        // Merge built-in + external wordlist, deduplicate, and shuffle to
120        // avoid deterministic probing fingerprints.
121        let mut all_paths: Vec<String> = COMMON_PATHS.iter().map(|p| (*p).to_string()).collect();
122        all_paths.extend(self.extra.iter().cloned());
123        all_paths.sort_unstable();
124        all_paths.dedup();
125        if all_paths.len() > 1 {
126            let mut rng = rand::thread_rng();
127            all_paths.shuffle(&mut rng);
128        }
129
130        let results = stream::iter(all_paths)
131            .map(|path| {
132                let url = format!("{base}{path}");
133                async move {
134                    let result = self.client.head(&url).await;
135                    (path, url, result)
136                }
137            })
138            .buffer_unordered(self.concurrency)
139            .collect::<Vec<_>>()
140            .await;
141
142        let mut found = HashSet::new();
143        let mut errors = Vec::new();
144
145        for (path, url, result) in results {
146            match result {
147                Ok(resp) => {
148                    // Treat anything except 404/410 as "exists"
149                    if resp.status != 404 && resp.status != 410 {
150                        debug!("[common_paths] {} => {}", url, resp.status);
151                        found.insert(path);
152                    }
153                }
154                Err(e) => {
155                    warn!("[common_paths] probe error: {}", e);
156                    errors.push(e);
157                }
158            }
159        }
160
161        debug!("[common_paths] {} live paths found", found.len());
162        (found, errors)
163    }
164}