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 +25 -0
- reconpro/cli.py +338 -0
- reconpro/http.py +163 -0
- reconpro/modules/__init__.py +15 -0
- reconpro/modules/auth.py +126 -0
- reconpro/modules/bot.py +127 -0
- reconpro/modules/chain.py +176 -0
- reconpro/modules/gorgon.py +174 -0
- reconpro/modules/nhi.py +159 -0
- reconpro/modules/oblivion.py +210 -0
- reconpro/modules/recon.py +307 -0
- reconpro/modules/vibesec.py +206 -0
- reconpro/scanner.py +150 -0
- reconpro-2.0.0.dist-info/METADATA +109 -0
- reconpro-2.0.0.dist-info/RECORD +19 -0
- reconpro-2.0.0.dist-info/WHEEL +5 -0
- reconpro-2.0.0.dist-info/entry_points.txt +2 -0
- reconpro-2.0.0.dist-info/licenses/LICENSE +21 -0
- reconpro-2.0.0.dist-info/top_level.txt +1 -0
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
|
+
]
|
reconpro/modules/auth.py
ADDED
|
@@ -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
|