reconpro 2.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
reconpro/__init__.py ADDED
@@ -0,0 +1,25 @@
1
+ """ReconPro Enterprise — The Security Reconnaissance Platform.
2
+
3
+ Eight Blades. One Target. One Verdict.
4
+
5
+ Modules:
6
+ recon 13-category surface reconnaissance
7
+ auth 15 auth bypass techniques
8
+ chain SSRF + redirect chain hunting
9
+ bot C2 / bot infrastructure detection
10
+ gorgon 15-stage AI red team
11
+ oblivion 23-stage analytical dissolution
12
+ vibesec AI/vibe-coding vulnerability benchmark
13
+ nhi Non-Human Identity & blast-radius mapping
14
+
15
+ Usage:
16
+ reconpro example.com
17
+ reconpro example.com --modules recon,auth,vibesec
18
+ reconpro example.com --all --json -o report.json
19
+ reconpro vibesec example.com
20
+ """
21
+
22
+ __version__ = "2.0.0"
23
+ __all__ = ["scan", "ReconProResult", "__version__"]
24
+
25
+ from .scanner import scan, ReconProResult # noqa: E402
reconpro/cli.py ADDED
@@ -0,0 +1,338 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ import time
7
+
8
+ from rich.console import Console, Group
9
+ from rich.panel import Panel
10
+ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+ from rich.columns import Columns
14
+ from rich.rule import Rule
15
+
16
+ from . import __version__
17
+ from .scanner import scan, MODULE_REGISTRY, ALL_MODULES, DEFAULT_MODULES
18
+
19
+ console = Console()
20
+
21
+ BANNER = r"""[bold bright_white]
22
+ ██████╗ ███████╗ ██████╗██╗ ██╗███████╗██████╗ ███████╗██████╗ ██████╗ ██╗ ██╗
23
+ ██╔══██╗██╔════╝██╔════╝██║ ██║██╔════╝██╔══██╗██╔════╝██╔════╝██║██╗ ██╔╝
24
+ ██████╔╝█████╗ ██║ ███████║█████╗ ██████╔╝█████╗ ██║ ██╔╝██╗██║
25
+ ██╔═══╝ ██╔══╝ ██║ ██╔══██║██╔══╝ ██╔══██╗██╔══╝ ██║ █████╔╝██║
26
+ ██║ ███████╗╚██████╗██║ ██║███████╗██║ ██║███████╗╚██████╗██╔╝██╗██║
27
+ ╚═╝ ╚══════╝ ╚═════╝╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝ ╚═════╝╚═╝ ╚═╝╚═╝
28
+ [/bold bright_white][dim] E I G H T B L A D E S . O N E T A R G E T . O N E V E R D I C T.[/dim]
29
+ """
30
+
31
+
32
+ SEV_COLORS = {
33
+ "critical": "bright_red",
34
+ "high": "red",
35
+ "medium": "yellow",
36
+ "low": "green",
37
+ "info": "dim",
38
+ }
39
+
40
+ GRADE_COLORS = {
41
+ "A+": "bright_green",
42
+ "A": "green",
43
+ "B": "yellow",
44
+ "C": "red",
45
+ "D": "bright_red",
46
+ "F": "bold bright_red",
47
+ }
48
+
49
+
50
+ # ── Rich rendering ──────────────────────────────────────────────────────
51
+
52
+
53
+ def _render_summary(result) -> None:
54
+ score = result.total_score
55
+ grade = result.grade
56
+ grade_color = GRADE_COLORS.get(grade, "bold bright_red")
57
+ sc = result.severity_counts
58
+ total = len(result.findings)
59
+
60
+ bar_width = 50
61
+ filled = int(score / 100 * bar_width)
62
+ bar = "█" * filled + "░" * (bar_width - filled)
63
+
64
+ console.print(Panel(
65
+ Group(
66
+ Text(f"\n {bar} [bold {grade_color}]{score}/100 ({grade})[/{grade_color}]"),
67
+ Text(f"\n Target: [cyan]{result.target}[/]"),
68
+ Text(f" Modules: [dim]{', '.join(result.modules_run)}[/]"),
69
+ Text(""),
70
+ Text(
71
+ f" Findings: [bold]{total}[/] "
72
+ f"[bright_red]{sc.get('critical', 0)} critical[/], "
73
+ f"[red]{sc.get('high', 0)} high[/], "
74
+ f"[yellow]{sc.get('medium', 0)} medium[/], "
75
+ f"[green]{sc.get('low', 0)} low[/], "
76
+ f"[dim]{sc.get('info', 0)} info[/]",
77
+ ),
78
+ *(
79
+ [Text(f"\n VibeSec Score: [bold bright_green]{result.vibesec_score}/100 ({result.vibesec_grade})[/]")]
80
+ if result.vibesec_score is not None else []
81
+ ),
82
+ ),
83
+ border_style=grade_color,
84
+ title="[bold]RECONPRO[/bold]",
85
+ title_align="left",
86
+ padding=(1, 2),
87
+ ))
88
+
89
+
90
+ def _render_findings_table(result) -> None:
91
+ if not result.findings:
92
+ console.print("\n [bright_green]No findings. Target is clean.[/]")
93
+ return
94
+
95
+ table = Table(
96
+ title=f"Findings — {len(result.findings)} detected",
97
+ border_style="bright_white",
98
+ header_style="bold bright_white",
99
+ show_lines=False,
100
+ )
101
+ table.add_column("Severity", style="bold", width=10)
102
+ table.add_column("Module", style="cyan", width=12)
103
+ table.add_column("Category", style="magenta", width=18)
104
+ table.add_column("Finding", style="white")
105
+ table.add_column("Pts", style="yellow", width=5)
106
+
107
+ for f in result.findings[:50]:
108
+ sev = f.get("severity", "info")
109
+ color = SEV_COLORS.get(sev, "white")
110
+ table.add_row(
111
+ f"[{color}]{sev.upper()}[/{color}]",
112
+ f.get("module", ""),
113
+ f.get("category", ""),
114
+ f.get("title", "")[:70],
115
+ str(f.get("points_deducted", 0)),
116
+ )
117
+
118
+ console.print(table)
119
+
120
+
121
+ def _render_module_breakdown(result) -> None:
122
+ if not result.module_results:
123
+ return
124
+
125
+ modules_table = Table(
126
+ title="Module Breakdown",
127
+ border_style="dim",
128
+ header_style="bold dim",
129
+ )
130
+ modules_table.add_column("Module", style="bold", width=16)
131
+ modules_table.add_column("Findings", style="white", width=10)
132
+ modules_table.add_column("Critical", style="bright_red", width=10)
133
+ modules_table.add_column("High", style="red", width=8)
134
+ modules_table.add_column("Medium", style="yellow", width=8)
135
+
136
+ for mod_id, mod_data in result.module_results.items():
137
+ findings = mod_data.get("findings", [])
138
+ crit = sum(1 for f in findings if f.get("severity") == "critical")
139
+ high = sum(1 for f in findings if f.get("severity") == "high")
140
+ med = sum(1 for f in findings if f.get("severity") == "medium")
141
+ modules_table.add_row(
142
+ mod_id.upper(),
143
+ str(len(findings)),
144
+ str(crit),
145
+ str(high),
146
+ str(med),
147
+ )
148
+
149
+ console.print(modules_table)
150
+
151
+
152
+ def _render_badge(result) -> None:
153
+ if result.badge_markdown:
154
+ console.print(Panel(
155
+ Group(
156
+ Text(" GitHub README Badge (copy-paste):", style="bold dim"),
157
+ Text(f" {result.badge_markdown}", style="cyan"),
158
+ ),
159
+ border_style="dim",
160
+ title="[dim]Badge[/dim]",
161
+ title_align="left",
162
+ padding=(0, 2),
163
+ ))
164
+
165
+
166
+ # ── CLI entry point ─────────────────────────────────────────────────────
167
+
168
+
169
+ def _add_scan_args(p: argparse.ArgumentParser) -> None:
170
+ """Add shared scan arguments to a parser."""
171
+ p.add_argument("target", help="Target domain or URL")
172
+ p.add_argument("--json", dest="json_output", action="store_true",
173
+ help="Output as JSON")
174
+ p.add_argument("-o", "--output", dest="output_file", type=str,
175
+ help="Save JSON to file")
176
+ p.add_argument("--timeout", "-t", type=int, default=8,
177
+ help="Per-request timeout in seconds (default: 8)")
178
+ p.add_argument("--insecure", "-k", action="store_true",
179
+ help="Skip TLS verification")
180
+ p.add_argument("--rate-limit", type=float, default=10.0,
181
+ help="Max requests per second (default: 10)")
182
+
183
+
184
+ def main(argv: list[str] | None = None) -> None:
185
+ parser = argparse.ArgumentParser(
186
+ prog="reconpro",
187
+ description=(
188
+ "ReconPro Enterprise — Eight Blades. One Target. One Verdict.\n"
189
+ "The full-spectrum security reconnaissance platform."
190
+ ),
191
+ epilog="Examples:\n"
192
+ " reconpro example.com\n"
193
+ " reconpro example.com --modules recon,auth,vibesec\n"
194
+ " reconpro example.com --all --json -o report.json\n"
195
+ " reconpro vibesec example.com",
196
+ formatter_class=argparse.RawDescriptionHelpFormatter,
197
+ )
198
+
199
+ parser.add_argument("--version", "-v", action="version",
200
+ version=f"ReconPro {__version__}")
201
+
202
+ subparsers = parser.add_subparsers(dest="subcommand")
203
+
204
+ # vibesec quick subcommand
205
+ vibesec_p = subparsers.add_parser("vibesec", help="Quick VibeSec benchmark")
206
+ _add_scan_args(vibesec_p)
207
+
208
+ # scan subcommand (default behavior)
209
+ scan_p = subparsers.add_parser("scan", help="Full scan with module selection")
210
+ scan_p.add_argument("target", help="Target domain or URL")
211
+ scan_p.add_argument("--modules", "-m", type=str, default=None,
212
+ help="Comma-separated module list")
213
+ scan_p.add_argument("--all", "-a", action="store_true",
214
+ help="Run all 8 modules")
215
+ scan_p.add_argument("--json", dest="json_output", action="store_true")
216
+ scan_p.add_argument("-o", "--output", dest="output_file", type=str)
217
+ scan_p.add_argument("--timeout", "-t", type=int, default=8)
218
+ scan_p.add_argument("--insecure", "-k", action="store_true")
219
+ scan_p.add_argument("--rate-limit", type=float, default=10.0)
220
+
221
+ # list subcommand
222
+ subparsers.add_parser("list", help="List available modules")
223
+
224
+ # Catchall: if first positional doesn't match a subcommand, treat as scan
225
+ args, remaining = parser.parse_known_args(argv)
226
+
227
+ # List modules
228
+ if args.subcommand == "list":
229
+ console.print("\n[bold]Available Modules:[/bold]\n")
230
+ for mid, entry in MODULE_REGISTRY.items():
231
+ default = " (default)" if mid in DEFAULT_MODULES else ""
232
+ console.print(f" [cyan]{mid:12}[/] {entry['name']:18}{default}")
233
+ console.print(f"\n [dim]Default: {', '.join(DEFAULT_MODULES)}[/]")
234
+ console.print(f" [dim]Use --all to run all 8 modules[/]\n")
235
+ return
236
+
237
+ # Handle vibesec subcommand
238
+ if args.subcommand == "vibesec":
239
+ if not args.target:
240
+ parser.parse_args(["vibesec", "--help"])
241
+ sys.exit(1)
242
+ target = args.target
243
+ modules = ["vibesec"]
244
+ json_output = args.json_output
245
+ output_file = args.output_file
246
+ timeout = args.timeout
247
+ verify_tls = not args.insecure
248
+ rate_limit = args.rate_limit
249
+ # Handle explicit scan subcommand
250
+ elif args.subcommand == "scan":
251
+ if not args.target:
252
+ parser.parse_args(["scan", "--help"])
253
+ sys.exit(1)
254
+ target = args.target
255
+ modules = ([m.strip().lower() for m in args.modules.split(",")]
256
+ if args.modules else None)
257
+ json_output = args.json_output
258
+ output_file = args.output_file
259
+ timeout = args.timeout
260
+ verify_tls = not args.insecure
261
+ rate_limit = args.rate_limit
262
+ all_modules = args.all
263
+ # Handle implicit scan (no subcommand, first arg is the target)
264
+ elif remaining and not args.subcommand:
265
+ target = remaining[0]
266
+ # Re-parse with explicit scan subcommand to get flags
267
+ scan_args = parser.parse_args(["scan"] + remaining)
268
+ modules = ([m.strip().lower() for m in scan_args.modules.split(",")]
269
+ if scan_args.modules else None)
270
+ json_output = scan_args.json_output
271
+ output_file = scan_args.output_file
272
+ timeout = scan_args.timeout
273
+ verify_tls = not scan_args.insecure
274
+ rate_limit = scan_args.rate_limit
275
+ all_modules = scan_args.all
276
+ else:
277
+ parser.print_help()
278
+ sys.exit(1)
279
+
280
+ target = target.strip()
281
+
282
+ # Banner
283
+ console.print(BANNER)
284
+ console.print(f" [dim]v{__version__} | {target} | modules: {modules or 'default'}[/]\n")
285
+ console.print(f" [bold]Scanning [cyan]{target}[/] ...[/]\n")
286
+
287
+ try:
288
+ with Progress(
289
+ SpinnerColumn(),
290
+ TextColumn("[bold]{task.description}[/]"),
291
+ TimeElapsedColumn(),
292
+ console=console,
293
+ ) as progress:
294
+ task = progress.add_task("ReconPro scanning...", total=None)
295
+ result = scan(
296
+ target,
297
+ modules=modules,
298
+ all_modules=all_modules if args.subcommand == "scan" else False,
299
+ timeout=timeout,
300
+ verify_tls=verify_tls,
301
+ rate_limit=rate_limit,
302
+ )
303
+ progress.update(task, completed=True)
304
+ except KeyboardInterrupt:
305
+ console.print("\n [yellow]Scan interrupted.[/]")
306
+ sys.exit(130)
307
+ except Exception as exc:
308
+ console.print(f" [bright_red]Error:[/] {exc}")
309
+ sys.exit(1)
310
+
311
+ # Output
312
+ if json_output:
313
+ text = json.dumps(result.to_dict(), indent=2, default=str)
314
+ if output_file:
315
+ with open(output_file, "w") as fp:
316
+ fp.write(text)
317
+ console.print(f" [green]JSON report saved to [cyan]{output_file}[/][/]")
318
+ else:
319
+ console.print(text)
320
+ else:
321
+ _render_summary(result)
322
+ console.print()
323
+ _render_module_breakdown(result)
324
+ console.print()
325
+ _render_findings_table(result)
326
+ _render_badge(result)
327
+
328
+ if output_file:
329
+ with open(output_file, "w") as fp:
330
+ fp.write(json.dumps(result.to_dict(), indent=2, default=str))
331
+ console.print(f"\n [green]JSON report saved to [cyan]{output_file}[/][/]")
332
+
333
+ console.print()
334
+ sys.exit(0)
335
+
336
+
337
+ if __name__ == "__main__":
338
+ main()
reconpro/http.py ADDED
@@ -0,0 +1,163 @@
1
+ """Shared HTTP layer used by all ReconPro modules.
2
+
3
+ Thread-safe rate limiter, hardened probe, TLS config.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import ssl
9
+ import time
10
+ import urllib.error
11
+ import urllib.request
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Dict, List, Optional, Tuple
14
+ from threading import Lock
15
+
16
+
17
+ # ── Rate Limiter ────────────────────────────────────────────────────────
18
+
19
+ class RateLimiter:
20
+ """Token-bucket rate limiter. Default: 10 req/s."""
21
+
22
+ def __init__(self, max_per_second: float = 10.0) -> None:
23
+ self._min_interval = 1.0 / max_per_second
24
+ self._lock = Lock()
25
+ self._last = 0.0
26
+
27
+ def acquire(self) -> None:
28
+ with self._lock:
29
+ now = time.monotonic()
30
+ elapsed = now - self._last
31
+ if elapsed < self._min_interval:
32
+ time.sleep(self._min_interval - elapsed)
33
+ self._last = time.monotonic()
34
+
35
+
36
+ default_limiter = RateLimiter(max_per_second=10.0)
37
+
38
+
39
+ # ── HTTP Probe ──────────────────────────────────────────────────────────
40
+
41
+ UA = (
42
+ "ReconPro/2.0 (Enterprise Security Scanner; "
43
+ "+https://github.com/reconpro-security/reconpro)"
44
+ )
45
+
46
+
47
+ def http_probe(
48
+ url: str,
49
+ method: str = "GET",
50
+ body: Optional[bytes] = None,
51
+ headers: Optional[Dict[str, str]] = None,
52
+ timeout: int = 8,
53
+ verify_tls: bool = True,
54
+ limiter: Optional[RateLimiter] = None,
55
+ ) -> Dict[str, Any]:
56
+ """Hardened HTTP probe. All modules delegate through this.
57
+
58
+ Returns dict with keys: ok, status, reason, headers, body.
59
+ """
60
+ if limiter:
61
+ limiter.acquire()
62
+
63
+ h = {
64
+ "User-Agent": UA,
65
+ "Accept": "application/json,text/html,text/plain,*/*",
66
+ "Accept-Language": "en-US,en;q=0.9",
67
+ }
68
+ if headers:
69
+ h.update(headers)
70
+
71
+ req = urllib.request.Request(url, data=body, method=method, headers=h)
72
+
73
+ try:
74
+ if verify_tls:
75
+ ctx = ssl.create_default_context()
76
+ else:
77
+ ctx = ssl._create_unverified_context()
78
+
79
+ with urllib.request.urlopen(req, timeout=timeout, context=ctx) as resp:
80
+ raw = resp.read(16384)
81
+ return {
82
+ "ok": True,
83
+ "status": resp.status,
84
+ "reason": resp.reason,
85
+ "headers": dict(resp.headers.items()),
86
+ "body": raw.decode("utf-8", errors="replace")[:16384],
87
+ }
88
+ except urllib.error.HTTPError as e:
89
+ try:
90
+ raw = e.read(16384)
91
+ body_str = raw.decode("utf-8", errors="replace")[:16384]
92
+ except Exception:
93
+ body_str = ""
94
+ return {
95
+ "ok": False,
96
+ "status": e.code,
97
+ "reason": e.reason,
98
+ "headers": dict(e.headers.items()) if e.headers else {},
99
+ "body": body_str,
100
+ }
101
+ except Exception as e:
102
+ return {"ok": False, "status": 0, "reason": str(e), "headers": {}, "body": ""}
103
+
104
+
105
+ # ── Finding helper ──────────────────────────────────────────────────────
106
+
107
+ @dataclass
108
+ class Finding:
109
+ """A single security finding."""
110
+ title: str
111
+ severity: str # critical, high, medium, low, info
112
+ category: str
113
+ module: str
114
+ description: str
115
+ evidence: str
116
+ asset: str
117
+ points_deducted: int = 0
118
+ remediation: str = ""
119
+
120
+ def to_dict(self) -> Dict[str, Any]:
121
+ return {
122
+ "title": self.title,
123
+ "severity": self.severity,
124
+ "category": self.category,
125
+ "module": self.module,
126
+ "description": self.description,
127
+ "evidence": self.evidence,
128
+ "asset": self.asset,
129
+ "points_deducted": self.points_deducted,
130
+ "remediation": self.remediation,
131
+ }
132
+
133
+
134
+ # ── Grade mapping ───────────────────────────────────────────────────────
135
+
136
+ GRADE_MAP: List[Tuple[int, str]] = [
137
+ (90, "A+"),
138
+ (80, "A"),
139
+ (65, "B"),
140
+ (50, "C"),
141
+ (35, "D"),
142
+ (0, "F"),
143
+ ]
144
+
145
+
146
+ def compute_grade(score: int) -> str:
147
+ for threshold, grade in GRADE_MAP:
148
+ if score >= threshold:
149
+ return grade
150
+ return "F"
151
+
152
+
153
+ def badge_markdown(host: str, grade: str) -> str:
154
+ color_map = {
155
+ "A+": "brightgreen", "A": "green", "B": "yellow",
156
+ "C": "red", "D": "orange", "F": "red",
157
+ }
158
+ c = color_map.get(grade, "lightgrey")
159
+ return (
160
+ f"![ReconPro {grade}]"
161
+ f"(https://img.shields.io/badge/ReconPro-{grade}-{c}"
162
+ f"?style=for-the-badge&labelColor=000000)"
163
+ )
@@ -0,0 +1,15 @@
1
+ """ReconPro scanning modules."""
2
+
3
+ from .recon import run_recon
4
+ from .vibesec import run_vibesec
5
+ from .auth import run_auth
6
+ from .chain import run_chain
7
+ from .bot import run_bot
8
+ from .gorgon import run_gorgon
9
+ from .oblivion import run_oblivion
10
+ from .nhi import run_nhi
11
+
12
+ __all__ = [
13
+ "run_recon", "run_vibesec", "run_auth", "run_chain",
14
+ "run_bot", "run_gorgon", "run_oblivion", "run_nhi",
15
+ ]
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import urllib.parse
5
+ from typing import Any, Dict, List
6
+ from ..http import http_probe, Finding
7
+
8
+
9
+ AUTH_BYPASS_PATHS = [
10
+ "/api/v1/users", "/api/users", "/api/v1/me",
11
+ "/api/v1/admin/users", "/api/admin", "/api/v1/config",
12
+ "/api/v1/secrets", "/api/v1/keys", "/api/v1/tokens",
13
+ "/api/v1/internal", "/api/debug", "/api/status",
14
+ "/api/health", "/.well-known/openid-configuration",
15
+ ]
16
+
17
+ AUTH_HEADERS = [
18
+ {"Authorization": "Bearer eyJhbGciOiJub25lIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9."},
19
+ {"Authorization": "Bearer null"},
20
+ {"Authorization": "Bearer undefined"},
21
+ {"X-Forwarded-For": "127.0.0.1"},
22
+ {"X-Original-URL": "/admin"},
23
+ {"X-Rewrite-URL": "/admin"},
24
+ {"X-Custom-IP-Authorization": "127.0.0.1"},
25
+ {"X-Forwarded-Host": "localhost"},
26
+ {"X-Host": "localhost"},
27
+ {"Referer": "https://localhost/admin"},
28
+ {"X-Real-IP": "127.0.0.1"},
29
+ {"Authorization": "Basic YWRtaW46YWRtaW4="}, # admin:admin
30
+ {"X-Api-Key": ""},
31
+ {"X-Access-Token": ""},
32
+ {"Cookie": "session=admin; role=admin"},
33
+ ]
34
+
35
+
36
+ def run_auth(target: str, base_url: str, timeout: int = 8,
37
+ verify_tls: bool = True) -> List[Finding]:
38
+ """15 auth bypass techniques. Returns list of Findings."""
39
+ findings: List[Finding] = []
40
+ host = target.replace("https://", "").replace("http://", "").split("/")[0]
41
+
42
+ def add(title, severity, desc, evidence, pts=0):
43
+ findings.append(Finding(
44
+ title=title, severity=severity, category="auth_bypass",
45
+ module="auth", description=desc, evidence=evidence,
46
+ asset=host, points_deducted=pts,
47
+ remediation="Implement proper authentication middleware. Validate tokens server-side.",
48
+ ))
49
+
50
+ # Technique 1-12: Header-based bypasses against sensitive endpoints
51
+ for path in AUTH_BYPASS_PATHS[:6]:
52
+ url = base_url.rstrip("/") + path
53
+ for i, headers in enumerate(AUTH_HEADERS):
54
+ resp = http_probe(url, headers=headers, timeout=timeout, verify_tls=verify_tls)
55
+ status = resp.get("status", 0)
56
+ body = resp.get("body", "")[:2048].lower()
57
+
58
+ if status == 200 and len(body) > 20:
59
+ is_protected = any(kw in body for kw in [
60
+ "unauthorized", "forbidden", "401", "403", "login required"
61
+ ])
62
+ if not is_protected:
63
+ header_name = list(headers.keys())[0] if headers else "none"
64
+ add(
65
+ f"Auth bypass via {header_name} on {path}",
66
+ "critical" if "admin" in path.lower() else "high",
67
+ f"Endpoint {path} accessible with suspicious {header_name} header",
68
+ f"{header_name} header bypassed auth -> 200 OK",
69
+ 15 if "admin" in path.lower() else 10,
70
+ )
71
+ break # One bypass per endpoint is enough
72
+
73
+ # Technique 13: HTTP method tampering
74
+ for path in ["/api/v1/users", "/admin", "/api/v1/config"]:
75
+ url = base_url.rstrip("/") + path
76
+ for method in ["OPTIONS", "PUT", "DELETE", "PATCH"]:
77
+ resp = http_probe(url, method=method, timeout=timeout, verify_tls=verify_tls)
78
+ if resp.get("status") == 200:
79
+ add(
80
+ f"Method tampering: {method} {path}",
81
+ "high",
82
+ f"{method} method returns 200 on {path} — may bypass auth checks",
83
+ f"{method} {path} -> 200",
84
+ 10,
85
+ )
86
+ break
87
+
88
+ # Technique 14: Path traversal to admin
89
+ traversal_paths = [
90
+ "/api/v1/../admin", "/api/..%2fadmin",
91
+ "/static/..%2f..%2fadmin", "/%2e%2e/admin",
92
+ ]
93
+ for tp in traversal_paths:
94
+ url = base_url.rstrip("/") + tp
95
+ resp = http_probe(url, timeout=timeout, verify_tls=verify_tls)
96
+ if resp.get("status") == 200 and len(resp.get("body", "")) > 20:
97
+ add(
98
+ f"Path traversal: {tp}",
99
+ "critical",
100
+ f"Path traversal bypassed access controls: {tp}",
101
+ f"GET {tp} -> 200",
102
+ 15,
103
+ )
104
+ break
105
+
106
+ # Technique 15: IDOR probe
107
+ for id_val in ["1", "admin", "me", "0"]:
108
+ for path in ["/api/v1/users/", "/api/users/", "/api/v1/accounts/"]:
109
+ url = base_url.rstrip("/") + path + id_val
110
+ resp = http_probe(url, timeout=timeout, verify_tls=verify_tls)
111
+ if resp.get("status") == 200:
112
+ body = resp.get("body", "")[:1024]
113
+ if any(kw in body.lower() for kw in ["email", "password", "secret", "token", "key"]):
114
+ add(
115
+ f"Potential IDOR: {path}{id_val}",
116
+ "critical",
117
+ f"User data accessible without auth via IDOR: {path}{id_val}",
118
+ f"GET {path}{id_val} -> 200 (sensitive data)",
119
+ 15,
120
+ )
121
+ break
122
+ else:
123
+ continue
124
+ break
125
+
126
+ return findings