redforge-sec 0.2.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.
- redforge/__init__.py +9 -0
- redforge/cli.py +512 -0
- redforge/compliance/__init__.py +6 -0
- redforge/compliance/cis.py +103 -0
- redforge/compliance/password.py +74 -0
- redforge/core/__init__.py +5 -0
- redforge/core/config.py +72 -0
- redforge/core/report.py +105 -0
- redforge/core/runner.py +153 -0
- redforge/core/scheduler.py +61 -0
- redforge/core/store.py +94 -0
- redforge/defense/__init__.py +6 -0
- redforge/defense/ioc.py +35 -0
- redforge/defense/log_analyzer.py +100 -0
- redforge/pentest/__init__.py +49 -0
- redforge/pentest/auth_tester.py +66 -0
- redforge/pentest/exploit_sdk.py +243 -0
- redforge/pentest/web_attacks.py +741 -0
- redforge/recon/__init__.py +32 -0
- redforge/recon/dns.py +122 -0
- redforge/recon/endpoints.py +61 -0
- redforge/recon/headers.py +86 -0
- redforge/recon/ports.py +65 -0
- redforge/recon/subdomains.py +53 -0
- redforge/recon/takeover.py +47 -0
- redforge/vuln/__init__.py +8 -0
- redforge/vuln/config_audit.py +70 -0
- redforge/vuln/cve.py +158 -0
- redforge/vuln/secrets.py +94 -0
- redforge/vuln/tls_scan.py +79 -0
- redforge/webapp/__init__.py +5 -0
- redforge/webapp/app.py +254 -0
- redforge_sec-0.2.0.dist-info/METADATA +263 -0
- redforge_sec-0.2.0.dist-info/RECORD +37 -0
- redforge_sec-0.2.0.dist-info/WHEEL +5 -0
- redforge_sec-0.2.0.dist-info/entry_points.txt +3 -0
- redforge_sec-0.2.0.dist-info/top_level.txt +1 -0
redforge/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""RedForge — Security Operations Platform.
|
|
2
|
+
|
|
3
|
+
A legitimate offensive + defensive security platform: penetration testing,
|
|
4
|
+
vulnerability assessment, red-team automation, bug-bounty tooling, compliance
|
|
5
|
+
auditing, and defensive monitoring. Scope is strictly limited to assets the
|
|
6
|
+
user owns or is explicitly authorized to test.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.2.0"
|
redforge/cli.py
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""RedForge command-line interface.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
redforge recon subdomains <domain> --scope scope.json
|
|
6
|
+
redforge recon headers <url> --scope scope.json
|
|
7
|
+
redforge recon ports <host> --scope scope.json
|
|
8
|
+
redforge vuln cve <name>
|
|
9
|
+
redforge vuln config <file>
|
|
10
|
+
redforge vuln tls <host>
|
|
11
|
+
redforge compliance cis
|
|
12
|
+
redforge compliance password <password>
|
|
13
|
+
redforge defense logs <file>
|
|
14
|
+
redforge defense ioc <text>
|
|
15
|
+
redforge report <target> --scope scope.json
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
from .core.config import Config
|
|
24
|
+
from .core.report import build_markdown_report, write_report
|
|
25
|
+
from .core.store import Store
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _clean_output(findings: list, target: str, tool: str) -> list:
|
|
29
|
+
"""Attach target/tool and drop internal keys for display."""
|
|
30
|
+
out = []
|
|
31
|
+
for f in findings:
|
|
32
|
+
f = dict(f)
|
|
33
|
+
f.setdefault("target", target)
|
|
34
|
+
f.setdefault("tool", tool)
|
|
35
|
+
out.append(f)
|
|
36
|
+
return out
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_recon(args, cfg: Config):
|
|
40
|
+
tool = args.sub
|
|
41
|
+
if tool == "subdomains":
|
|
42
|
+
from .recon import enum_subdomains, resolve_subdomains
|
|
43
|
+
cfg.scope.require(args.domain)
|
|
44
|
+
subs = enum_subdomains(args.domain)
|
|
45
|
+
print(f"Found {len(subs)} subdomains for {args.domain}")
|
|
46
|
+
for s in subs:
|
|
47
|
+
print(" " + s)
|
|
48
|
+
if args.resolve:
|
|
49
|
+
resolved = resolve_subdomains(subs)
|
|
50
|
+
for r in resolved:
|
|
51
|
+
print(f" → {r['host']} = {r['ip']}")
|
|
52
|
+
elif tool == "headers":
|
|
53
|
+
from .recon import scan_headers
|
|
54
|
+
import urllib.parse
|
|
55
|
+
host = urllib.parse.urlparse(args.url).netloc
|
|
56
|
+
cfg.scope.require(host)
|
|
57
|
+
findings = _clean_output(scan_headers(args.url), host, "header-scan")
|
|
58
|
+
_print_findings(findings)
|
|
59
|
+
if args.save and cfg.db_path:
|
|
60
|
+
_save(args, findings, cfg)
|
|
61
|
+
elif tool == "ports":
|
|
62
|
+
from .recon import scan_common_ports
|
|
63
|
+
cfg.scope.require(args.host)
|
|
64
|
+
ports = scan_common_ports(args.host, banner=getattr(args, "banner", False))
|
|
65
|
+
for p in ports:
|
|
66
|
+
b = f" [{p.get('banner', '')}]" if p.get("banner") else ""
|
|
67
|
+
print(f" {p['port']:<6} {p['service']}{b}")
|
|
68
|
+
print(f"Open ports: {len(ports)}")
|
|
69
|
+
elif tool == "dns":
|
|
70
|
+
from .recon import resolve_records, enum_subdomains_robust, brute_subdomains
|
|
71
|
+
cfg.scope.require(args.domain)
|
|
72
|
+
records = resolve_records(args.domain)
|
|
73
|
+
for rtype, vals in records.items():
|
|
74
|
+
if vals:
|
|
75
|
+
print(f"{rtype}: {', '.join(vals)}")
|
|
76
|
+
subs = enum_subdomains_robust(args.domain)
|
|
77
|
+
print(f"Subdomains ({len(subs)}): {', '.join(subs[:20])}")
|
|
78
|
+
if getattr(args, "brute", False):
|
|
79
|
+
print("Brute-forcing common subdomains...")
|
|
80
|
+
brute = brute_subdomains(args.domain)
|
|
81
|
+
print(f"Brute found ({len(brute)}): {', '.join(brute)}")
|
|
82
|
+
elif tool == "endpoints":
|
|
83
|
+
import urllib.parse
|
|
84
|
+
if "://" not in args.url:
|
|
85
|
+
host = args.url
|
|
86
|
+
url = f"https://{args.url}"
|
|
87
|
+
else:
|
|
88
|
+
host = urllib.parse.urlparse(args.url).netloc
|
|
89
|
+
url = args.url
|
|
90
|
+
cfg.scope.require(host)
|
|
91
|
+
from .recon import discover_endpoints
|
|
92
|
+
found = discover_endpoints(url)
|
|
93
|
+
for e in found:
|
|
94
|
+
print(f" {e['status']:<4} {e['path']} ({e['size']} bytes)")
|
|
95
|
+
print(f"Endpoints found: {len(found)}")
|
|
96
|
+
elif tool == "takeover":
|
|
97
|
+
from .recon import detect_takeover
|
|
98
|
+
cfg.scope.require(args.host)
|
|
99
|
+
findings = detect_takeover(args.host)
|
|
100
|
+
_print_findings(findings)
|
|
101
|
+
else:
|
|
102
|
+
print(f"Unknown recon tool: {tool}")
|
|
103
|
+
sys.exit(2)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def cmd_vuln(args, cfg: Config):
|
|
107
|
+
tool = args.sub
|
|
108
|
+
if tool == "cve":
|
|
109
|
+
from .vuln import lookup_cve, search_cves
|
|
110
|
+
result = lookup_cve(args.name)
|
|
111
|
+
if result:
|
|
112
|
+
print(f"{result['cve']} — {args.name} [{result['severity']}]")
|
|
113
|
+
print(f" Affected: {result['affected']}")
|
|
114
|
+
print(f" Note: {result['note']}")
|
|
115
|
+
print(f" Remediation: {result['remediation']}")
|
|
116
|
+
else:
|
|
117
|
+
matches = search_cves(args.name)
|
|
118
|
+
if matches:
|
|
119
|
+
print(f"No exact match. Similar CVEs:")
|
|
120
|
+
for m in matches:
|
|
121
|
+
print(f" {m['name']} ({m['cve']}) [{m['severity']}]")
|
|
122
|
+
else:
|
|
123
|
+
print(f"Unknown CVE: {args.name}")
|
|
124
|
+
elif tool == "config":
|
|
125
|
+
from .vuln import audit_config
|
|
126
|
+
findings = _clean_output(audit_config(args.file), args.file, "config-audit")
|
|
127
|
+
_print_findings(findings)
|
|
128
|
+
if args.save:
|
|
129
|
+
_save(args, findings, cfg)
|
|
130
|
+
elif tool == "tls":
|
|
131
|
+
from .vuln import scan_tls
|
|
132
|
+
cfg.scope.require(args.host)
|
|
133
|
+
findings = _clean_output(scan_tls(args.host, args.port), args.host, "tls-scan")
|
|
134
|
+
_print_findings(findings)
|
|
135
|
+
if args.save:
|
|
136
|
+
_save(args, findings, cfg)
|
|
137
|
+
elif tool == "secrets":
|
|
138
|
+
from .vuln import scan_repo
|
|
139
|
+
findings = _clean_output(scan_repo(args.path), args.path, "secret-scan")
|
|
140
|
+
_print_findings(findings)
|
|
141
|
+
if args.save:
|
|
142
|
+
_save(args, findings, cfg)
|
|
143
|
+
else:
|
|
144
|
+
print(f"Unknown vuln tool: {tool}")
|
|
145
|
+
sys.exit(2)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def cmd_pentest(args, cfg: Config):
|
|
149
|
+
tool = args.sub
|
|
150
|
+
if tool == "cors":
|
|
151
|
+
from .pentest import CorsScanner
|
|
152
|
+
import urllib.parse
|
|
153
|
+
host = urllib.parse.urlparse(args.url).netloc or args.url
|
|
154
|
+
cfg.scope.require(host)
|
|
155
|
+
findings = CorsScanner(args.url, host, cfg.scope.targets).scan()
|
|
156
|
+
_print_findings(findings)
|
|
157
|
+
elif tool == "verbs":
|
|
158
|
+
from .pentest import HttpVerbTester
|
|
159
|
+
import urllib.parse
|
|
160
|
+
host = urllib.parse.urlparse(args.url).netloc or args.url
|
|
161
|
+
cfg.scope.require(host)
|
|
162
|
+
findings = HttpVerbTester(args.url, host, cfg.scope.targets).scan()
|
|
163
|
+
_print_findings(findings)
|
|
164
|
+
elif tool == "jwt":
|
|
165
|
+
from .pentest import JwtAuditor
|
|
166
|
+
findings = JwtAuditor(args.token).audit()
|
|
167
|
+
_print_findings(findings)
|
|
168
|
+
elif tool == "redir":
|
|
169
|
+
from .pentest import OpenRedirectProber
|
|
170
|
+
payloads = OpenRedirectProber().payloads()
|
|
171
|
+
for k, v in payloads.items():
|
|
172
|
+
print(f" {k}: {v}")
|
|
173
|
+
print("Inert payload strings — apply manually to authorized targets.")
|
|
174
|
+
elif tool == "ssrf":
|
|
175
|
+
from .pentest import SsrfProber
|
|
176
|
+
if args.url:
|
|
177
|
+
import urllib.parse
|
|
178
|
+
host = urllib.parse.urlparse(args.url).netloc or args.url
|
|
179
|
+
cfg.scope.require(host)
|
|
180
|
+
hits = SsrfProber.find_candidate_params(args.url)
|
|
181
|
+
if hits:
|
|
182
|
+
print("SSRF-prone parameters found:")
|
|
183
|
+
for h in hits:
|
|
184
|
+
print(f" ?{h['param']}={h['value']}")
|
|
185
|
+
else:
|
|
186
|
+
print("No SSRF-prone parameters detected in the query string.")
|
|
187
|
+
print("\nSSRF payload candidates (inert, manual review):")
|
|
188
|
+
for k, v in SsrfProber().payloads().items():
|
|
189
|
+
print(f" {k}: {v}")
|
|
190
|
+
elif tool == "ratelimit":
|
|
191
|
+
from .pentest import RateLimitTester
|
|
192
|
+
import urllib.parse
|
|
193
|
+
host = urllib.parse.urlparse(args.url).netloc or args.url
|
|
194
|
+
cfg.scope.require(host)
|
|
195
|
+
result = RateLimitTester(args.url, host, cfg.scope.targets).test()
|
|
196
|
+
_print_findings(result["findings"])
|
|
197
|
+
print(f"Statuses observed: {result['statuses']}")
|
|
198
|
+
elif tool == "idor":
|
|
199
|
+
from .pentest import IdorGuide
|
|
200
|
+
guide = IdorGuide(getattr(args, "base_id", "1000"))
|
|
201
|
+
print(guide.guidance())
|
|
202
|
+
print(f"\nCandidate IDs: {', '.join(guide.adjacent_ids())}")
|
|
203
|
+
if getattr(args, "url", None):
|
|
204
|
+
print("Candidate URLs (manual review only):")
|
|
205
|
+
for u in guide.candidate_urls(args.url):
|
|
206
|
+
print(f" {u}")
|
|
207
|
+
elif tool == "hdrinj":
|
|
208
|
+
from .pentest import HeaderInjectionTester
|
|
209
|
+
import urllib.parse
|
|
210
|
+
host = urllib.parse.urlparse(args.url).netloc or args.url
|
|
211
|
+
cfg.scope.require(host)
|
|
212
|
+
tester = HeaderInjectionTester(args.url, host, cfg.scope.targets)
|
|
213
|
+
print("CRLF payloads (inert):")
|
|
214
|
+
for p in tester.payloads():
|
|
215
|
+
print(f" {p}")
|
|
216
|
+
print("\nProbing header reflection...")
|
|
217
|
+
_print_findings(tester.probe_reflection())
|
|
218
|
+
elif tool == "graphql":
|
|
219
|
+
from .pentest import GraphqlIntrospector
|
|
220
|
+
import urllib.parse
|
|
221
|
+
host = urllib.parse.urlparse(args.url).netloc or args.url
|
|
222
|
+
cfg.scope.require(host)
|
|
223
|
+
endpoint = args.url if args.url.endswith("/graphql") else f"{args.url.rstrip('/')}/graphql"
|
|
224
|
+
_print_findings(GraphqlIntrospector(endpoint, host, cfg.scope.targets).probe())
|
|
225
|
+
elif tool == "hosthdr":
|
|
226
|
+
from .pentest import HostHeaderTester
|
|
227
|
+
import urllib.parse
|
|
228
|
+
host = urllib.parse.urlparse(args.url).netloc or args.url
|
|
229
|
+
cfg.scope.require(host)
|
|
230
|
+
_print_findings(HostHeaderTester(args.url, host, cfg.scope.targets).scan())
|
|
231
|
+
elif tool == "cachep":
|
|
232
|
+
from .pentest import CachePoisonTester
|
|
233
|
+
import urllib.parse
|
|
234
|
+
host = urllib.parse.urlparse(args.url).netloc or args.url
|
|
235
|
+
cfg.scope.require(host)
|
|
236
|
+
result = CachePoisonTester(args.url, host, cfg.scope.targets).probe()
|
|
237
|
+
_print_findings(result["findings"])
|
|
238
|
+
print(f"Statuses: base={result.get('base_status')} busted={result.get('busted_status')}")
|
|
239
|
+
elif tool == "backups":
|
|
240
|
+
from .pentest import BackupFileFinder
|
|
241
|
+
import urllib.parse
|
|
242
|
+
host = urllib.parse.urlparse(args.url).netloc or args.url
|
|
243
|
+
cfg.scope.require(host)
|
|
244
|
+
findings = BackupFileFinder(args.url, host, cfg.scope.targets).scan()
|
|
245
|
+
_print_findings(findings)
|
|
246
|
+
print(f"{len(findings)} backup candidates found.")
|
|
247
|
+
else:
|
|
248
|
+
print(f"Unknown pentest tool: {tool}")
|
|
249
|
+
sys.exit(2)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def cmd_compliance(args, cfg: Config):
|
|
253
|
+
if args.sub == "cis":
|
|
254
|
+
from .compliance import run_cis_checks
|
|
255
|
+
findings = _clean_output(run_cis_checks(), "localhost", "cis")
|
|
256
|
+
_print_findings(findings)
|
|
257
|
+
if args.save:
|
|
258
|
+
_save(args, findings, cfg)
|
|
259
|
+
elif args.sub == "password":
|
|
260
|
+
from .compliance import check_password_strength
|
|
261
|
+
r = check_password_strength(args.password)
|
|
262
|
+
print(f"Score: {r['score']}/9 Strength: {r['strength']}")
|
|
263
|
+
for issue in r["issues"]:
|
|
264
|
+
print(f" ! {issue}")
|
|
265
|
+
else:
|
|
266
|
+
print(f"Unknown compliance tool: {args.sub}")
|
|
267
|
+
sys.exit(2)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def cmd_defense(args, cfg: Config):
|
|
271
|
+
if args.sub == "logs":
|
|
272
|
+
from .defense import analyze_log_file
|
|
273
|
+
findings = _clean_output(analyze_log_file(args.file), "<logs>", "log-analyzer")
|
|
274
|
+
_print_findings(findings)
|
|
275
|
+
if args.save:
|
|
276
|
+
_save(args, findings, cfg)
|
|
277
|
+
elif args.sub == "ioc":
|
|
278
|
+
from .defense import match_iocs
|
|
279
|
+
findings = _clean_output(match_iocs(args.text), "<text>", "ioc-matcher")
|
|
280
|
+
_print_findings(findings)
|
|
281
|
+
else:
|
|
282
|
+
print(f"Unknown defense tool: {args.sub}")
|
|
283
|
+
sys.exit(2)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def cmd_report(args, cfg: Config):
|
|
287
|
+
store = Store(cfg.db_path)
|
|
288
|
+
findings = store.findings(target=args.target)
|
|
289
|
+
if not findings:
|
|
290
|
+
print(f"No findings stored for {args.target}. Run a scan with --save first.")
|
|
291
|
+
sys.exit(1)
|
|
292
|
+
if args.format == "csv":
|
|
293
|
+
from .core.report import export_csv
|
|
294
|
+
path = f"{cfg.report_dir}/{args.target.replace('/', '_')}-report.csv"
|
|
295
|
+
write_report(path, export_csv(findings))
|
|
296
|
+
print(f"CSV report written: {path}")
|
|
297
|
+
store.close()
|
|
298
|
+
return
|
|
299
|
+
if args.format == "json":
|
|
300
|
+
from .core.report import export_json
|
|
301
|
+
path = f"{cfg.report_dir}/{args.target.replace('/', '_')}-report.json"
|
|
302
|
+
write_report(path, export_json(findings))
|
|
303
|
+
print(f"JSON report written: {path}")
|
|
304
|
+
store.close()
|
|
305
|
+
return
|
|
306
|
+
md = build_markdown_report(
|
|
307
|
+
args.target, findings,
|
|
308
|
+
scope_note=cfg.scope.note,
|
|
309
|
+
engagement=args.engagement,
|
|
310
|
+
)
|
|
311
|
+
path = f"{cfg.report_dir}/{args.target.replace('/', '_')}-report.md"
|
|
312
|
+
p = write_report(path, md)
|
|
313
|
+
print(f"Report written: {p}")
|
|
314
|
+
store.close()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def cmd_scan(args, cfg: Config):
|
|
318
|
+
from .core.runner import run_against
|
|
319
|
+
tools = [t.strip() for t in args.tools.split(",") if t.strip()]
|
|
320
|
+
try:
|
|
321
|
+
result = run_against(args.target, tools, cfg)
|
|
322
|
+
except PermissionError as e:
|
|
323
|
+
print(f"BLOCKED: {e}")
|
|
324
|
+
sys.exit(3)
|
|
325
|
+
print(f"Scan complete: {result['target']} ({', '.join(result['tools'])})")
|
|
326
|
+
print(f" Findings saved: {result['findings_saved']}")
|
|
327
|
+
print(f" Report: {result['report_path']}")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def cmd_schedule(args, cfg: Config):
|
|
331
|
+
from .core.scheduler import ScanScheduler, make_job
|
|
332
|
+
from .core.runner import run_against
|
|
333
|
+
tools = [t.strip() for t in args.tools.split(",") if t.strip()]
|
|
334
|
+
# Verify scope up-front so a misconfigured target fails fast
|
|
335
|
+
try:
|
|
336
|
+
run_against(args.target, tools, cfg)
|
|
337
|
+
except PermissionError as e:
|
|
338
|
+
print(f"BLOCKED: {e}")
|
|
339
|
+
sys.exit(3)
|
|
340
|
+
job = make_job(args.target, tools, cfg)
|
|
341
|
+
sched = ScanScheduler(args.interval, job, max_iterations=args.runs)
|
|
342
|
+
print(f"Scheduling scan of {args.target} every {args.interval}s "
|
|
343
|
+
f"{'(max ' + str(args.runs) + ' runs)' if args.runs else '(until interrupted)'}...")
|
|
344
|
+
try:
|
|
345
|
+
sched.run_forever()
|
|
346
|
+
except KeyboardInterrupt:
|
|
347
|
+
print("\nStopped.")
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _save(args, findings: list, cfg: Config):
|
|
351
|
+
store = Store(cfg.db_path)
|
|
352
|
+
target = getattr(args, "target", None) or "local"
|
|
353
|
+
for f in findings:
|
|
354
|
+
store.add_finding(
|
|
355
|
+
target=target,
|
|
356
|
+
tool=f.get("tool", "unknown"),
|
|
357
|
+
severity=f.get("severity", "info"),
|
|
358
|
+
title=f.get("title", ""),
|
|
359
|
+
detail=f.get("detail", ""),
|
|
360
|
+
evidence=f.get("evidence", ""),
|
|
361
|
+
remediation=f.get("remediation", ""),
|
|
362
|
+
)
|
|
363
|
+
store.close()
|
|
364
|
+
print(f"Saved {len(findings)} findings to {cfg.db_path}")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _print_findings(findings: list):
|
|
368
|
+
if not findings:
|
|
369
|
+
print("No findings.")
|
|
370
|
+
return
|
|
371
|
+
for f in findings:
|
|
372
|
+
sev = f.get("severity", "info").upper()
|
|
373
|
+
print(f"[{sev}] {f.get('title')}")
|
|
374
|
+
if f.get("detail"):
|
|
375
|
+
print(f" {f['detail']}")
|
|
376
|
+
if f.get("remediation"):
|
|
377
|
+
print(f" Fix: {f['remediation']}")
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
381
|
+
parser = argparse.ArgumentParser(prog="redforge", description="Security Operations Platform")
|
|
382
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
383
|
+
|
|
384
|
+
p_recon = sub.add_parser("recon", help="Reconnaissance tools")
|
|
385
|
+
recon_sub = p_recon.add_subparsers(dest="sub", required=True)
|
|
386
|
+
p_sub = recon_sub.add_parser("subdomains", help="Enumerate subdomains (crt.sh)")
|
|
387
|
+
p_sub.add_argument("domain")
|
|
388
|
+
p_sub.add_argument("--resolve", action="store_true")
|
|
389
|
+
p_hdr = recon_sub.add_parser("headers", help="Scan HTTP security headers")
|
|
390
|
+
p_hdr.add_argument("url")
|
|
391
|
+
p_hdr.add_argument("--save", action="store_true")
|
|
392
|
+
p_hdr.add_argument("--target")
|
|
393
|
+
p_port = recon_sub.add_parser("ports", help="Scan common ports")
|
|
394
|
+
p_port.add_argument("host")
|
|
395
|
+
p_port.add_argument("--banner", action="store_true", help="Grab service banners")
|
|
396
|
+
p_dns = recon_sub.add_parser("dns", help="Enumerate DNS records + subdomains")
|
|
397
|
+
p_dns.add_argument("domain")
|
|
398
|
+
p_dns.add_argument("--brute", action="store_true", help="Brute-force common subdomains")
|
|
399
|
+
p_ep = recon_sub.add_parser("endpoints", help="Discover common endpoints")
|
|
400
|
+
p_ep.add_argument("url")
|
|
401
|
+
p_tk = recon_sub.add_parser("takeover", help="Detect potential subdomain takeover")
|
|
402
|
+
p_tk.add_argument("host")
|
|
403
|
+
|
|
404
|
+
p_vuln = sub.add_parser("vuln", help="Vulnerability assessment")
|
|
405
|
+
vuln_sub = p_vuln.add_subparsers(dest="sub", required=True)
|
|
406
|
+
p_cve = vuln_sub.add_parser("cve", help="Look up a CVE")
|
|
407
|
+
p_cve.add_argument("name")
|
|
408
|
+
p_cfg = vuln_sub.add_parser("config", help="Audit a config file")
|
|
409
|
+
p_cfg.add_argument("file")
|
|
410
|
+
p_cfg.add_argument("--save", action="store_true")
|
|
411
|
+
p_tls = vuln_sub.add_parser("tls", help="Scan TLS config")
|
|
412
|
+
p_tls.add_argument("host")
|
|
413
|
+
p_tls.add_argument("--port", type=int, default=443)
|
|
414
|
+
p_tls.add_argument("--save", action="store_true")
|
|
415
|
+
p_sec = vuln_sub.add_parser("secrets", help="Recursively scan a repo for secrets")
|
|
416
|
+
p_sec.add_argument("path")
|
|
417
|
+
p_sec.add_argument("--save", action="store_true")
|
|
418
|
+
|
|
419
|
+
p_comp = sub.add_parser("compliance", help="Compliance auditing")
|
|
420
|
+
comp_sub = p_comp.add_subparsers(dest="sub", required=True)
|
|
421
|
+
p_cis = comp_sub.add_parser("cis", help="Run CIS-style checks")
|
|
422
|
+
p_cis.add_argument("--save", action="store_true")
|
|
423
|
+
p_pw = comp_sub.add_parser("password", help="Check password strength")
|
|
424
|
+
p_pw.add_argument("password")
|
|
425
|
+
|
|
426
|
+
p_pt = sub.add_parser("pentest", help="Web attack-surface testing (authorized)")
|
|
427
|
+
pt_sub = p_pt.add_subparsers(dest="sub", required=True)
|
|
428
|
+
p_cors = pt_sub.add_parser("cors", help="Scan CORS misconfiguration")
|
|
429
|
+
p_cors.add_argument("url")
|
|
430
|
+
p_verbs = pt_sub.add_parser("verbs", help="Test HTTP methods")
|
|
431
|
+
p_verbs.add_argument("url")
|
|
432
|
+
p_jwt = pt_sub.add_parser("jwt", help="Audit a JWT token")
|
|
433
|
+
p_jwt.add_argument("token")
|
|
434
|
+
p_redir = pt_sub.add_parser("redir", help="Generate open-redirect payloads")
|
|
435
|
+
p_redir.add_argument("--url", help="Optional target base (informational)")
|
|
436
|
+
p_ssrf = pt_sub.add_parser("ssrf", help="SSRF payloads + URL param detection")
|
|
437
|
+
p_ssrf.add_argument("--url", help="Target URL to analyze for SSRF-prone params")
|
|
438
|
+
p_rl = pt_sub.add_parser("ratelimit", help="Bounded rate-limit assessment")
|
|
439
|
+
p_rl.add_argument("url")
|
|
440
|
+
p_idor = pt_sub.add_parser("idor", help="IDOR candidate generation (guidance)")
|
|
441
|
+
p_idor.add_argument("--url", help="Base API URL for candidate review")
|
|
442
|
+
p_idor.add_argument("--base-id", default="1000", help="Known object ID")
|
|
443
|
+
p_hdr = pt_sub.add_parser("hdrinj", help="Header-injection payloads + reflection probe")
|
|
444
|
+
p_hdr.add_argument("url")
|
|
445
|
+
p_gql = pt_sub.add_parser("graphql", help="GraphQL introspection check")
|
|
446
|
+
p_gql.add_argument("url")
|
|
447
|
+
p_hh = pt_sub.add_parser("hosthdr", help="Host-header / cache-poisoning signals")
|
|
448
|
+
p_hh.add_argument("url")
|
|
449
|
+
p_cp = pt_sub.add_parser("cachep", help="Cache-key poisoning probe")
|
|
450
|
+
p_cp.add_argument("url")
|
|
451
|
+
p_bk = pt_sub.add_parser("backups", help="Probe for exposed backup files")
|
|
452
|
+
p_bk.add_argument("url")
|
|
453
|
+
|
|
454
|
+
p_def = sub.add_parser("defense", help="Defensive monitoring")
|
|
455
|
+
def_sub = p_def.add_subparsers(dest="sub", required=True)
|
|
456
|
+
p_logs = def_sub.add_parser("logs", help="Analyze an access log")
|
|
457
|
+
p_logs.add_argument("file")
|
|
458
|
+
p_logs.add_argument("--save", action="store_true")
|
|
459
|
+
p_ioc = def_sub.add_parser("ioc", help="Match IOCs in text")
|
|
460
|
+
p_ioc.add_argument("text")
|
|
461
|
+
|
|
462
|
+
p_rep = sub.add_parser("report", help="Generate a report")
|
|
463
|
+
p_rep.add_argument("target")
|
|
464
|
+
p_rep.add_argument("--engagement", default="")
|
|
465
|
+
p_rep.add_argument("--format", choices=["md", "csv", "json"], default="md")
|
|
466
|
+
|
|
467
|
+
p_scan = sub.add_parser("scan", help="Run a combined scan against a scoped target")
|
|
468
|
+
p_scan.add_argument("target")
|
|
469
|
+
p_scan.add_argument("--tools", default="headers,ports,tls",
|
|
470
|
+
help="Comma-separated: headers,ports,tls,cis,dns,endpoints,takeover,cors,verbs")
|
|
471
|
+
|
|
472
|
+
p_sched = sub.add_parser("schedule", help="Run a scan on an interval")
|
|
473
|
+
p_sched.add_argument("target")
|
|
474
|
+
p_sched.add_argument("--tools", default="headers,ports,tls")
|
|
475
|
+
p_sched.add_argument("--interval", type=int, default=3600, help="Seconds between runs (>=5)")
|
|
476
|
+
p_sched.add_argument("--runs", type=int, default=None, help="Max runs")
|
|
477
|
+
|
|
478
|
+
parser.add_argument("--scope", help="Path to scope.json")
|
|
479
|
+
parser.add_argument("--db", help="SQLite db path")
|
|
480
|
+
return parser
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def main(argv=None):
|
|
484
|
+
parser = build_parser()
|
|
485
|
+
args = parser.parse_args(argv)
|
|
486
|
+
cfg = Config.load(args.scope)
|
|
487
|
+
if args.db:
|
|
488
|
+
cfg.db_path = args.db
|
|
489
|
+
|
|
490
|
+
if args.command == "recon":
|
|
491
|
+
cmd_recon(args, cfg)
|
|
492
|
+
elif args.command == "vuln":
|
|
493
|
+
cmd_vuln(args, cfg)
|
|
494
|
+
elif args.command == "compliance":
|
|
495
|
+
cmd_compliance(args, cfg)
|
|
496
|
+
elif args.command == "pentest":
|
|
497
|
+
cmd_pentest(args, cfg)
|
|
498
|
+
elif args.command == "defense":
|
|
499
|
+
cmd_defense(args, cfg)
|
|
500
|
+
elif args.command == "report":
|
|
501
|
+
cmd_report(args, cfg)
|
|
502
|
+
elif args.command == "scan":
|
|
503
|
+
cmd_scan(args, cfg)
|
|
504
|
+
elif args.command == "schedule":
|
|
505
|
+
cmd_schedule(args, cfg)
|
|
506
|
+
else:
|
|
507
|
+
parser.print_help()
|
|
508
|
+
sys.exit(2)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
if __name__ == "__main__":
|
|
512
|
+
main()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""CIS-style benchmark checks for the user's own systems.
|
|
2
|
+
|
|
3
|
+
Runs a set of read-only configuration and system checks against the local
|
|
4
|
+
host (the operator's own machine/server) to assess baseline hardening against
|
|
5
|
+
common CIS benchmark controls. Read-only — no changes are made.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import stat
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _check_file_perms(path: str, mode: int) -> bool:
|
|
18
|
+
try:
|
|
19
|
+
st = os.stat(path)
|
|
20
|
+
return (stat.S_IMODE(st.st_mode) & mode) == 0
|
|
21
|
+
except OSError:
|
|
22
|
+
return True # file absent = not a finding
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _is_installed(bin: str) -> bool:
|
|
26
|
+
return shutil.which(bin) is not None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def run_cis_checks() -> List[dict]:
|
|
30
|
+
"""Evaluate a battery of CIS-style hardening checks on the local host."""
|
|
31
|
+
findings = []
|
|
32
|
+
|
|
33
|
+
# World-writable sensitive files
|
|
34
|
+
for p in ["/etc/passwd", "/etc/shadow", "/etc/ssh/sshd_config",
|
|
35
|
+
os.path.expanduser("~/.ssh/authorized_keys")]:
|
|
36
|
+
if os.path.exists(p) and _check_file_perms(p, stat.S_IWOTH):
|
|
37
|
+
findings.append({
|
|
38
|
+
"tool": "cis",
|
|
39
|
+
"severity": "medium",
|
|
40
|
+
"title": f"World-writable file: {p}",
|
|
41
|
+
"detail": f"{p} is writable by other users.",
|
|
42
|
+
"remediation": f"chmod 600/644 {p}",
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
# Root login via SSH
|
|
46
|
+
sshd = "/etc/ssh/sshd_config"
|
|
47
|
+
if os.path.exists(sshd):
|
|
48
|
+
try:
|
|
49
|
+
cfg = Path(sshd).read_text()
|
|
50
|
+
if "PermitRootLogin yes" in cfg and not cfg.lower().startswith("#"):
|
|
51
|
+
findings.append({
|
|
52
|
+
"tool": "cis",
|
|
53
|
+
"severity": "high",
|
|
54
|
+
"title": "SSH root login permitted",
|
|
55
|
+
"detail": "PermitRootLogin is set to yes.",
|
|
56
|
+
"remediation": "Set PermitRootLogin no (or prohibit-password).",
|
|
57
|
+
})
|
|
58
|
+
except OSError:
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
# Firewall present
|
|
62
|
+
if not (_is_installed("ufw") or _is_installed("firewalld") or _is_installed("iptables")):
|
|
63
|
+
findings.append({
|
|
64
|
+
"tool": "cis",
|
|
65
|
+
"severity": "medium",
|
|
66
|
+
"title": "No firewall tool detected",
|
|
67
|
+
"detail": "None of ufw/firewalld/iptables found on PATH.",
|
|
68
|
+
"remediation": "Enable a host firewall.",
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
# Passwordless sudo (indicative)
|
|
72
|
+
sudoers = "/etc/sudoers"
|
|
73
|
+
if os.path.exists(sudoers):
|
|
74
|
+
try:
|
|
75
|
+
if "NOPASSWD" in Path(sudoers).read_text():
|
|
76
|
+
findings.append({
|
|
77
|
+
"tool": "cis",
|
|
78
|
+
"severity": "medium",
|
|
79
|
+
"title": "Passwordless sudo configured",
|
|
80
|
+
"detail": "NOPASSWD directive found in sudoers.",
|
|
81
|
+
"remediation": "Require password for sudo.",
|
|
82
|
+
})
|
|
83
|
+
except OSError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
# Core dump limit
|
|
87
|
+
if not os.path.exists("/proc/sys/fs/suid_dumpable"):
|
|
88
|
+
pass # non-Linux; skip
|
|
89
|
+
else:
|
|
90
|
+
try:
|
|
91
|
+
val = Path("/proc/sys/fs/suid_dumpable").read_text().strip()
|
|
92
|
+
if val != "0":
|
|
93
|
+
findings.append({
|
|
94
|
+
"tool": "cis",
|
|
95
|
+
"severity": "info",
|
|
96
|
+
"title": "SUID core dumps enabled",
|
|
97
|
+
"detail": f"suid_dumpable = {val}.",
|
|
98
|
+
"remediation": "Set fs.suid_dumpable = 0.",
|
|
99
|
+
})
|
|
100
|
+
except OSError:
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
return findings
|