blackops-sql 0.1.6__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.
@@ -0,0 +1,27 @@
1
+ BlackOpsSQL
2
+ ===========
3
+
4
+ BlackOpsSQL is a modified fork of BreachSQL by CommonHuman-Lab.
5
+ Original project: https://github.com/CommonHuman-Lab/breachsql
6
+ License: AGPL-3.0-or-later
7
+
8
+ BlackOpsSQL is an AGPL-3.0-or-later modified fork of BreachSQL by CommonHuman-Lab.
9
+
10
+ Original copyright (c) 2026 CommonHuman-Lab
11
+ Modifications copyright (c) 2026 roc1t1z3not
12
+
13
+ This program is free software: you can redistribute it and/or modify
14
+ it under the terms of the GNU Affero General Public License as published
15
+ by the Free Software Foundation, either version 3 of the License, or
16
+ (at your option) any later version.
17
+
18
+ Upstream dependency notices
19
+ ----------------------------
20
+ BlackOpsSQL depends on the following upstream libraries from CommonHuman-Lab,
21
+ used as external PyPI packages (not bundled source):
22
+
23
+ - commonhuman-core — HTTP client, crawler, OpenAPI loader, auth helpers
24
+ - commonhuman-payloads — SQL injection payload library and WAF signatures
25
+ - commonhuman-cli — CLI framework, logging, colour helpers, entry point utils
26
+
27
+ These libraries remain unmodified and are governed by their own licenses.
@@ -0,0 +1,111 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (c) 2026 CommonHuman-Lab (original BreachSQL)
3
+ # Modifications copyright (c) 2026 roc1t1z3not (BlackOpsSQL)
4
+ """
5
+ BlackOpsSQL — API-aware SQL injection reconnaissance and validation engine.
6
+
7
+ BlackOpsSQL is an AGPL-3.0-or-later modified fork of BreachSQL by CommonHuman-Lab.
8
+
9
+ Quick start:
10
+
11
+ from blackopssql import scan, ScanOptions
12
+
13
+ result = scan("https://target.com/search?q=test")
14
+ print(result.total_findings)
15
+
16
+ opts = ScanOptions(level=2, crawl=True, dbms="mysql")
17
+ result = scan("https://target.com/search?q=test", opts)
18
+ """
19
+
20
+ from blackopssql.engine import scan, ScanOptions
21
+ from blackopssql.engine.reporter import (
22
+ ScanResult,
23
+ ErrorBasedFinding,
24
+ BooleanFinding,
25
+ TimeFinding,
26
+ UnionFinding,
27
+ OOBFinding,
28
+ FindingType,
29
+ )
30
+
31
+ __version__ = "0.1.6"
32
+
33
+
34
+ def _make_banner() -> str:
35
+ import re
36
+ import sys
37
+
38
+ _tty = sys.stdout.isatty()
39
+
40
+ def _r(t: str) -> str:
41
+ return f"\033[31;1m{t}\033[0m" if _tty else t
42
+
43
+ def _g(t: str) -> str:
44
+ return f"\033[38;5;46m{t}\033[0m" if _tty else t
45
+
46
+ def _d(t: str) -> str:
47
+ return f"\033[2m{t}\033[0m" if _tty else t
48
+
49
+ def _vlen(s: str) -> int:
50
+ return len(re.sub(r"\033\[[0-9;]*m", "", s))
51
+
52
+ # slant font: "BlackOps" (green) + "SQL" (red) merged side-by-side
53
+ _BO_W = 44
54
+ _BO = (
55
+ " ____ __ __ ____ ",
56
+ " / __ )/ /___ ______/ /__/ __ \\____ _____",
57
+ " / __ / / __ `/ ___/ //_/ / / / __ \\/ ___/",
58
+ " / /_/ / / /_/ / /__/ ,< / /_/ / /_/ (__ ) ",
59
+ "/_____/_/\\__,_/\\___/_/|_|\\____/ .___/____/ ",
60
+ " /_/ ",
61
+ )
62
+ _SQL = (
63
+ " _____ ____ __ ",
64
+ " / ___// __ \\ / / ",
65
+ " \\__ \\/ / / / / / ",
66
+ " ___/ / /_/ / / /___",
67
+ "/____/\\___\\_\\/_____/",
68
+ " ",
69
+ )
70
+
71
+ art_colored = [_g(bo.ljust(_BO_W)) + _r(sql) for bo, sql in zip(_BO, _SQL)]
72
+ art_raw = [bo.ljust(_BO_W) + sql for bo, sql in zip(_BO, _SQL)]
73
+
74
+ info = [
75
+ _d(" API-aware SQL injection reconnaissance and validation engine"),
76
+ _d(" fork of BreachSQL by CommonHuman-Lab | AGPL-3.0-or-later"),
77
+ _d(" Big thanks to Deorsa for Graphical and Visual design."),
78
+ ]
79
+
80
+ art_raw_widths = [len(l) for l in art_raw]
81
+ info_raw_widths = [_vlen(l) for l in info]
82
+ inner_w = max(art_raw_widths + info_raw_widths)
83
+ frame_w = inner_w + 4 # "# " + content + " #"
84
+
85
+ top_bot = _r("#" * frame_w)
86
+ empty = _r("#") + " " * (frame_w - 2) + _r("#")
87
+
88
+ rows = ["", top_bot, empty]
89
+ for colored, raw_w in zip(art_colored, art_raw_widths):
90
+ rows.append(_r("#") + " " + colored + " " * (inner_w - raw_w) + " " + _r("#"))
91
+ rows.append(empty)
92
+ for colored, raw_w in zip(info, info_raw_widths):
93
+ rows.append(_r("#") + " " + colored + " " * (inner_w - raw_w) + " " + _r("#"))
94
+ rows += [empty, top_bot, ""]
95
+ return "\n".join(rows)
96
+
97
+
98
+ BANNER = _make_banner()
99
+
100
+ __all__ = [
101
+ "__version__",
102
+ "scan",
103
+ "ScanOptions",
104
+ "ScanResult",
105
+ "ErrorBasedFinding",
106
+ "BooleanFinding",
107
+ "TimeFinding",
108
+ "UnionFinding",
109
+ "OOBFinding",
110
+ "FindingType",
111
+ ]
@@ -0,0 +1,287 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (c) 2026 CommonHuman-Lab (original BreachSQL)
3
+ # Modifications copyright (c) 2026 roc1t1z3not (BlackOpsSQL fork)
4
+ """
5
+ BlackOpsSQL — __main__.py
6
+ CLI entry point.
7
+
8
+ Usage:
9
+ python -m blackopssql -u https://target.com/search?q=test
10
+ blackops-sql -u https://target.com/search?q=test
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import copy
16
+ import json
17
+ import os
18
+ import re
19
+ import sys
20
+ import urllib.parse as _up
21
+
22
+ _HERE = os.path.dirname(os.path.abspath(__file__))
23
+ _PARENT = os.path.dirname(_HERE)
24
+ for _p in (_PARENT, _HERE):
25
+ if _p not in sys.path:
26
+ sys.path.insert(0, _p)
27
+
28
+ from blackopssql import BANNER
29
+ from blackopssql.engine import scan, ScanOptions
30
+ from blackopssql.engine.scanner import _unique_stem, _output_stem
31
+ from blackopssql.engine.log import get_logger
32
+ from blackopssql._cli.args import build_parser, interactive_prompts
33
+ from blackopssql._cli.summary import print_summary
34
+ from blackops_cli.colour import BOLD, CYAN
35
+ from blackops_cli.logging import setup_logging
36
+ from blackops_cli.entrypoint import (
37
+ load_url_list, compile_exclude_patterns, parse_headers, validate_timeout,
38
+ )
39
+
40
+ _cli_logger = get_logger("blackopssql")
41
+
42
+ _AUTH_PATH_RE = re.compile(r'/(login|signin|authenticate|session|token)\b', re.IGNORECASE)
43
+ _AUTH_SYNTH_BODY = '{"email":"test@test.com","password":"test","username":"test"}'
44
+
45
+
46
+ def _split_param_list(val) -> list[str]:
47
+ """Accept either a list (from interactive mode) or a comma-separated string."""
48
+ if isinstance(val, list):
49
+ return [p.strip() for p in val if str(p).strip()]
50
+ return [p.strip() for p in str(val).split(",") if p.strip()]
51
+
52
+
53
+ def main() -> None:
54
+ parser = build_parser()
55
+ args = parser.parse_args()
56
+
57
+ setup_logging(verbose=args.verbose, quiet=args.quiet or args.json_output, logger_name="blackopssql")
58
+
59
+ # Collect target URLs
60
+ urls: list[str] = []
61
+ if args.url:
62
+ urls.append(args.url)
63
+ if args.url_list:
64
+ urls.extend(load_url_list(args.url_list))
65
+
66
+ # No URL supplied → interactive mode
67
+ if not urls:
68
+ args = interactive_prompts()
69
+ urls = [args.url]
70
+ elif not args.json_output:
71
+ print(CYAN(BANNER))
72
+
73
+ validate_timeout(args.timeout)
74
+
75
+ exclude_patterns = compile_exclude_patterns(args.exclude)
76
+ headers = parse_headers(args.header)
77
+
78
+ # Form login
79
+ if getattr(args, "login_url", "") and getattr(args, "login_user", ""):
80
+ from blackops_core.auth import form_login as _form_login
81
+ if not args.quiet and not args.json_output:
82
+ print(f"[*] Authenticating via {args.login_url} ...")
83
+ auth = _form_login(
84
+ login_url=args.login_url,
85
+ username=args.login_user,
86
+ password=getattr(args, "login_pass", ""),
87
+ username_field=getattr(args, "login_user_field", "username"),
88
+ password_field=getattr(args, "login_pass_field", "password"),
89
+ )
90
+ if auth.cookies and not args.cookie:
91
+ args.cookie = auth.cookies
92
+ headers.update(auth.headers)
93
+
94
+ # OpenAPI spec import
95
+ if getattr(args, "openapi", ""):
96
+ from blackops_core.openapi import load_openapi as _load_openapi
97
+ if not args.quiet and not args.json_output:
98
+ print(f"[*] Loading OpenAPI spec from {args.openapi} ...")
99
+ api_eps = _load_openapi(args.openapi, base_url=getattr(args, "base_url", ""))
100
+ seen_oa = set(urls)
101
+ for ep in api_eps:
102
+ if ep.url not in seen_oa:
103
+ urls.append(ep.url)
104
+ seen_oa.add(ep.url)
105
+ if not args.quiet and not args.json_output:
106
+ print(f"[*] OpenAPI: {len(api_eps)} endpoint(s) added")
107
+
108
+ # Per-URL POST body overrides: populated by JS discovery for auth endpoints.
109
+ url_data_overrides: dict[str, str] = {}
110
+
111
+ # JS API discovery — parse SPA bundles for REST/API endpoints
112
+ if (args.crawl or getattr(args, "browser_crawl", False)) and urls:
113
+ from blackops_core.js_api_discover import js_api_discover as _js_discover
114
+ seed = urls[0]
115
+ if not args.quiet and not args.json_output:
116
+ print(f"[*] JS API discovery on {seed} ...")
117
+ seen_js = set(urls)
118
+ js_found = _js_discover(seed)
119
+ new_js: list[str] = []
120
+ for _method, js_url, _tmpl in js_found:
121
+ if js_url not in seen_js:
122
+ seen_js.add(js_url)
123
+ new_js.append(js_url)
124
+ urls.append(js_url)
125
+ # Synthesise a JSON body for discovered POST auth endpoints so
126
+ # the scanner can build injectable surfaces (email/password fields).
127
+ if _method == "POST" and _AUTH_PATH_RE.search(_up.urlparse(js_url).path):
128
+ url_data_overrides[js_url] = _AUTH_SYNTH_BODY
129
+ if not args.quiet and not args.json_output:
130
+ print(f"[*] JS discovery: {len(new_js)} endpoint(s) found")
131
+
132
+ # Browser crawl — headless JS endpoint discovery
133
+ if getattr(args, "browser_crawl", False) and urls:
134
+ from blackops_core.browser_crawler import browser_crawl as _browser_crawl
135
+ seed = urls[0]
136
+ if not args.quiet and not args.json_output:
137
+ print(f"[*] Browser-crawling {seed} ...")
138
+ bc_found = _browser_crawl(
139
+ start_url=seed,
140
+ max_pages=args.max_pages,
141
+ max_depth=args.max_depth,
142
+ cookies=args.cookie or "",
143
+ )
144
+ seen_bc = set(urls)
145
+ new_bc = [u for u in bc_found if u not in seen_bc]
146
+ urls.extend(new_bc)
147
+ if not args.quiet and not args.json_output:
148
+ print(f"[*] Browser crawl: {len(new_bc)} additional endpoint(s) queued")
149
+
150
+ # --exploit implies --dump-all + auto-named outputs under <host>/ directory
151
+ if getattr(args, "exploit", False):
152
+ args.dump_all = True
153
+ if not args.output and urls:
154
+ _host = _up.urlparse(urls[0]).hostname or "target"
155
+ os.makedirs(_host, exist_ok=True)
156
+ args.output = os.path.join(_host, _host)
157
+ if not getattr(args, "report_html", "") and args.output:
158
+ args.report_html = args.output + ".html"
159
+
160
+ # Resolve collision-free stem once for the whole session so that every
161
+ # scan() call in the crawl loop writes to the same base name.
162
+ if args.output:
163
+ args.output = _unique_stem(_output_stem(args.output))
164
+ if getattr(args, "report_html", ""):
165
+ args.report_html = _unique_stem(_output_stem(args.report_html)) + ".html"
166
+
167
+ opts = ScanOptions(
168
+ crawl=args.crawl,
169
+ data=args.data,
170
+ headers=headers,
171
+ cookies=args.cookie,
172
+ proxy=args.proxy,
173
+ threads=args.threads,
174
+ timeout=args.timeout,
175
+ delay=args.delay,
176
+ level=args.level,
177
+ max_pages=args.max_pages,
178
+ max_depth=args.max_depth,
179
+ output=args.output,
180
+ exclude_patterns=exclude_patterns,
181
+ dbms=args.dbms,
182
+ technique=args.technique,
183
+ oob_callback=args.oob,
184
+ time_threshold=args.time_threshold,
185
+ risk=args.risk,
186
+ second_url=getattr(args, "second_url", ""),
187
+ path_params=_split_param_list(getattr(args, "path_params", "")),
188
+ cookie_params=_split_param_list(getattr(args, "cookie_params", "")),
189
+ header_params=_split_param_list(getattr(args, "header_params", "")),
190
+ exploit=(
191
+ getattr(args, "exploit", False)
192
+ or bool(getattr(args, "dump", ""))
193
+ or getattr(args, "dump_all", False)
194
+ ),
195
+ dump=getattr(args, "dump", ""),
196
+ dump_all=getattr(args, "dump_all", False),
197
+ )
198
+
199
+ all_results = []
200
+ any_findings = False
201
+ multi = len(urls) > 1
202
+
203
+ for target_url in urls:
204
+ if any(p.search(target_url) for p in exclude_patterns):
205
+ _cli_logger.info("Skipping excluded URL: %s", target_url)
206
+ continue
207
+
208
+ if not args.json_output and not args.quiet:
209
+ if not multi:
210
+ print(BOLD(f"[*] Target : {target_url}"))
211
+ print(BOLD(f"[*] Level : {args.level} Threads: {args.threads} Crawl: {args.crawl}"))
212
+ print(BOLD(f"[*] DBMS hint : {args.dbms} Techniques: {args.technique} Risk: {args.risk}"))
213
+ print()
214
+
215
+ _scan_opts = opts
216
+ if target_url in url_data_overrides and not opts.data:
217
+ _scan_opts = copy.copy(opts)
218
+ _scan_opts.data = url_data_overrides[target_url]
219
+
220
+ result = scan(target_url, _scan_opts)
221
+ all_results.append(result)
222
+ if result.total_findings > 0:
223
+ any_findings = True
224
+
225
+ if args.json_output:
226
+ print(json.dumps(result.to_dict(), indent=2))
227
+ continue
228
+
229
+ if multi:
230
+ # In multi-URL mode: only show a line when findings exist
231
+ if result.total_findings > 0:
232
+ print(f" [+] {result.total_findings} finding(s) — {target_url}")
233
+ else:
234
+ print_summary(result)
235
+
236
+ if not args.json_output and multi:
237
+ # Merge all results into one combined summary
238
+ from .engine.reporter import ScanResult
239
+ combined = ScanResult(target=urls[0])
240
+ combined.duration_s = sum(r.duration_s for r in all_results)
241
+ combined.requests_sent = sum(r.requests_sent for r in all_results)
242
+ combined.crawled_urls = sum(r.crawled_urls for r in all_results)
243
+ combined.params_tested = sum(r.params_tested for r in all_results)
244
+ combined.waf_detected = next((r.waf_detected for r in all_results if r.waf_detected), None)
245
+ combined.evasion_applied = next((r.evasion_applied for r in all_results if r.evasion_applied), None)
246
+ combined.dbms_detected = next((r.dbms_detected for r in all_results if r.dbms_detected), None)
247
+ for r in all_results:
248
+ combined.error_based.extend(r.error_based)
249
+ combined.boolean_based.extend(r.boolean_based)
250
+ combined.time_based.extend(r.time_based)
251
+ combined.union_based.extend(r.union_based)
252
+ combined.oob.extend(r.oob)
253
+ combined.stacked.extend(r.stacked)
254
+ combined.extracted.extend(r.extracted)
255
+ combined.errors.extend(r.errors)
256
+ combined.target = f"{urls[0]} (+{len(urls)-1} more)" if len(urls) > 1 else urls[0]
257
+ print()
258
+ print_summary(combined)
259
+
260
+ # ── HTML report ───────────────────────────────────────────────────────────
261
+ _report_html = getattr(args, "report_html", "")
262
+ if _report_html and all_results:
263
+ from blackops_cli.report_html import render_html as _render_html
264
+ try:
265
+ _html_str = _render_html(
266
+ results=[
267
+ {**r.to_dict(), "table_dumps": r.dumps_to_dict()["table_dumps"]}
268
+ for r in all_results
269
+ ],
270
+ tool_name="BlackOpsSQL",
271
+ tool_version=__import__("blackopssql").__version__,
272
+ )
273
+ with open(_report_html, "w", encoding="utf-8") as _fh:
274
+ _fh.write(_html_str)
275
+ if not args.json_output and not args.quiet:
276
+ print(f"[+] HTML report written to {_report_html}")
277
+ except OSError as _exc:
278
+ print(f"[!] Cannot write HTML report: {_exc}", file=sys.stderr)
279
+
280
+ if args.json_output:
281
+ sys.exit(0 if not any_findings else 1)
282
+
283
+ sys.exit(1 if any_findings else 0)
284
+
285
+
286
+ if __name__ == "__main__":
287
+ main()
File without changes
@@ -0,0 +1,229 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (c) 2026 CommonHuman-Lab
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from blackops_cli.colour import CYAN, DIM, YELLOW
8
+ from blackops_cli.prompts import (
9
+ safe_int as _safe_int,
10
+ prompt as _prompt,
11
+ prompt_bool as _prompt_bool,
12
+ section as _section,
13
+ )
14
+
15
+ try:
16
+ from blackopssql import __version__, BANNER
17
+ except ImportError:
18
+ __version__ = "0.1.0"
19
+ BANNER = ""
20
+
21
+
22
+ def interactive_prompts() -> argparse.Namespace:
23
+ """Walk the user through all scan options interactively."""
24
+ print(CYAN(BANNER))
25
+ print(DIM(" No arguments supplied — entering interactive mode."))
26
+ print(DIM(" Press Enter to accept defaults. Ctrl+C to exit.\n"))
27
+
28
+ _section("Target")
29
+ url = ""
30
+ while not url:
31
+ url = _prompt(" Target URL", hint="e.g. https://target.com/search?q=test")
32
+ if not url:
33
+ print(YELLOW(" [!] URL is required."))
34
+ elif not url.startswith(("http://", "https://")):
35
+ print(YELLOW(" [!] URL must start with http:// or https://"))
36
+ url = ""
37
+
38
+ _section("Authentication (optional)")
39
+ login_url = _prompt(" Login URL", hint="https://target.com/login (blank to skip)")
40
+ if login_url:
41
+ login_user = _prompt(" Username")
42
+ login_pass = _prompt(" Password")
43
+ else:
44
+ login_user = login_pass = ""
45
+ cookie = _prompt(" Cookies", hint="name=val; name2=val2 (blank if using --login-url)")
46
+ headers_raw: list[str] = []
47
+ while True:
48
+ h = _prompt(" Header", hint="KEY:VALUE (blank to finish)")
49
+ if not h:
50
+ break
51
+ headers_raw.append(h)
52
+
53
+ _section("Request")
54
+ data = _prompt(" POST body", hint="form-encoded or JSON (blank = GET)")
55
+ proxy = _prompt(" Proxy", hint="http://127.0.0.1:8080")
56
+
57
+ _section("SQLi options")
58
+ dbms = _prompt(" Target DBMS", default="auto",
59
+ hint="mysql | mariadb | mssql | postgres | sqlite | oracle | auto")
60
+ technique = _prompt(" Techniques", default="EBTUO",
61
+ hint="E=error B=bool T=time U=union O=oob (e.g. EBT)")
62
+ oob = _prompt(" OOB callback URL", hint="https://your.interactsh.io/x (blank to skip)")
63
+ time_thr = _prompt(" Time threshold", default="4", hint="seconds to flag time-based hit")
64
+ risk_str = _prompt(" Risk level", default="1", hint="1=safe 2=moderate 3=aggressive")
65
+ second_url = _prompt(" Second URL", hint="read SQLi response from this URL (blank to skip)")
66
+ path_params = _prompt(" Path params", hint="comma-separated names e.g. id,slug (blank=auto)")
67
+ cookie_params = _prompt(" Cookie params", hint="comma-separated cookie names to inject (blank=skip)")
68
+ header_params = _prompt(" Header params", hint="comma-separated header names to inject (blank=skip)")
69
+
70
+ _section("Scan options")
71
+ level_str = _prompt(" Scan level", default="1", hint="1=fast 2=thorough 3=deep")
72
+ threads_str = _prompt(" Threads", default="5")
73
+ timeout_str = _prompt(" Timeout", default="15", hint="seconds per request")
74
+
75
+ crawl = _prompt_bool(" Enable crawler", default=False)
76
+ if crawl:
77
+ max_pages_str = _prompt(" Max pages", default="100")
78
+ max_depth_str = _prompt(" Max depth", default="3")
79
+ else:
80
+ max_pages_str = "100"
81
+ max_depth_str = "3"
82
+
83
+ _section("Exploitation (optional)")
84
+ exploit = _prompt_bool(" Extract data after finding SQLi", default=False)
85
+ dump = _prompt(" Dump table", hint="table name to dump rows from (blank to skip)") if exploit else ""
86
+ print()
87
+
88
+ return argparse.Namespace(
89
+ url=url,
90
+ url_list="",
91
+ crawl=crawl,
92
+ data=data,
93
+ header=headers_raw,
94
+ cookie=cookie,
95
+ proxy=proxy,
96
+ threads=_safe_int(threads_str, 5, 1, 20),
97
+ timeout=_safe_int(timeout_str, 15, 5, 120),
98
+ delay=0.0,
99
+ level=_safe_int(level_str, 1, 1, 3),
100
+ max_pages=_safe_int(max_pages_str, 100, 1, 500),
101
+ max_depth=_safe_int(max_depth_str, 3, 1, 10),
102
+ exclude=[],
103
+ output="",
104
+ json_output=False,
105
+ quiet=False,
106
+ verbose=False,
107
+ dbms=dbms,
108
+ technique=technique.upper(),
109
+ oob=oob,
110
+ time_threshold=_safe_int(time_thr, 4, 1, 30),
111
+ risk=_safe_int(risk_str, 1, 1, 3),
112
+ second_url=second_url,
113
+ path_params=[p.strip() for p in path_params.split(",") if p.strip()],
114
+ cookie_params=[p.strip() for p in cookie_params.split(",") if p.strip()],
115
+ header_params=[p.strip() for p in header_params.split(",") if p.strip()],
116
+ login_url=login_url,
117
+ login_user=login_user,
118
+ login_pass=login_pass,
119
+ login_user_field="username",
120
+ login_pass_field="password",
121
+ openapi="",
122
+ base_url="",
123
+ browser_crawl=False,
124
+ exploit=exploit,
125
+ dump=dump,
126
+ dump_all=False,
127
+ report_html="",
128
+ )
129
+
130
+
131
+ def build_parser() -> argparse.ArgumentParser:
132
+ p = argparse.ArgumentParser(
133
+ prog="blackops-sql",
134
+ description="BlackOpsSQL — API-aware SQL injection reconnaissance and validation engine",
135
+ formatter_class=argparse.RawDescriptionHelpFormatter,
136
+ )
137
+ p.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
138
+
139
+ # --- Target ---
140
+ p.add_argument("-u", "--url", default="", help="Target URL")
141
+ p.add_argument("-L", "--url-list", default="", metavar="FILE",
142
+ help="File of target URLs (one per line)")
143
+
144
+ # --- Request ---
145
+ p.add_argument("--crawl", action="store_true", help="Enable BFS crawler")
146
+ p.add_argument("-d", "--data", default="", help="POST body")
147
+ p.add_argument("-H", "--header", action="append", default=[], metavar="KEY:VALUE",
148
+ help="Custom header (repeatable)")
149
+ p.add_argument("-c", "--cookie", default="", help="Cookie string")
150
+ p.add_argument("--proxy", default="", help="HTTP proxy URL")
151
+ p.add_argument("-t", "--threads", type=int, default=5,
152
+ help="Worker threads 1-20 (default 5)")
153
+ p.add_argument("--timeout", type=int, default=15,
154
+ help="Request timeout seconds 5-120 (default 15)")
155
+ p.add_argument("--delay", type=float, default=0.0,
156
+ help="Seconds between requests (default 0)")
157
+
158
+ # --- Scan depth ---
159
+ p.add_argument("--level", type=int, default=1, choices=[1, 2, 3],
160
+ help="Scan depth: 1=fast 2=thorough 3=deep (default 1)")
161
+ p.add_argument("--max-pages", type=int, default=100,
162
+ help="Max pages to crawl (default 100)")
163
+ p.add_argument("--max-depth", type=int, default=3,
164
+ help="Max crawl depth (default 3)")
165
+ p.add_argument("--exclude", action="append", default=[], metavar="PATTERN",
166
+ help="Regex pattern of URLs to skip (repeatable)")
167
+
168
+ # --- SQLi-specific ---
169
+ p.add_argument("--dbms", default="auto",
170
+ choices=["auto", "mysql", "mariadb", "mssql", "postgres", "sqlite", "oracle"],
171
+ help="Target DBMS hint (default: auto-detect)")
172
+ p.add_argument("--technique", default="EBTUO", metavar="TECHNIQUES",
173
+ help="Techniques to use: E=error B=bool T=time U=union O=oob (default: EBTUO)")
174
+ p.add_argument("--oob", default="", metavar="URL",
175
+ help="Out-of-band callback URL for OOB detection")
176
+ p.add_argument("--time-threshold", type=int, default=4, dest="time_threshold",
177
+ help="Seconds delta to flag time-based SQLi (default 4)")
178
+ p.add_argument("--risk", type=int, default=1, choices=[1, 2, 3],
179
+ help="Risk level: 1=safe 2=moderate 3=aggressive (default 1)")
180
+ p.add_argument("--second-url", default="", dest="second_url", metavar="URL",
181
+ help="Read SQLi response from this URL after injecting into target "
182
+ "(e.g. DVWA high: inject to session-input.php, read from sqli/)")
183
+ p.add_argument("--path-params", default="", dest="path_params", metavar="NAMES",
184
+ help="Comma-separated path segment names to inject "
185
+ "(auto-detected from :name/{name} patterns if omitted)")
186
+ p.add_argument("--cookie-params", default="", dest="cookie_params", metavar="NAMES",
187
+ help="Comma-separated cookie names to inject as SQLi surfaces")
188
+ p.add_argument("--header-params", default="", dest="header_params", metavar="NAMES",
189
+ help="Comma-separated HTTP header names to inject as SQLi surfaces")
190
+
191
+ # --- Output ---
192
+ p.add_argument("-o", "--output", default="", metavar="NAME",
193
+ help="Output stem — writes <name>.json and <name>.txt")
194
+ p.add_argument("--json", action="store_true", dest="json_output",
195
+ help="Output raw JSON")
196
+ p.add_argument("-q", "--quiet", action="store_true",
197
+ help="Suppress live log output")
198
+ p.add_argument("-v", "--verbose", action="store_true",
199
+ help="Show all checks including clean ones")
200
+ p.add_argument("--login-url", default="", dest="login_url",
201
+ help="Login form URL — authenticates before scanning")
202
+ p.add_argument("--login-user", default="", dest="login_user",
203
+ help="Username for form login")
204
+ p.add_argument("--login-pass", default="", dest="login_pass",
205
+ help="Password for form login")
206
+ p.add_argument("--login-user-field", default="username", dest="login_user_field",
207
+ help="Username field name (default: username)")
208
+ p.add_argument("--login-pass-field", default="password", dest="login_pass_field",
209
+ help="Password field name (default: password)")
210
+ p.add_argument("--openapi", default="", dest="openapi",
211
+ help="OpenAPI/Swagger spec file path or URL — imports endpoints to scan")
212
+ p.add_argument("--base-url", default="", dest="base_url",
213
+ help="Base URL override for OpenAPI spec")
214
+ p.add_argument("--browser-crawl", action="store_true", dest="browser_crawl",
215
+ help="Use headless Chromium for endpoint discovery (requires selenium)")
216
+
217
+ # --- Exploitation ---
218
+ p.add_argument("--exploit", action="store_true", default=False,
219
+ help="After finding SQLi, dump all tables and write <host>.txt, <host>.json, <host>.html")
220
+ p.add_argument("--dump", default="", metavar="TABLE",
221
+ help="Dump all rows from TABLE using a confirmed injection point (implies --exploit)")
222
+ p.add_argument("--dump-all", action="store_true", default=False, dest="dump_all",
223
+ help="Dump every table discovered during extraction (implies --exploit)")
224
+
225
+ # --- Reports ---
226
+ p.add_argument("--report-html", default="", dest="report_html", metavar="FILE",
227
+ help="Write a self-contained HTML report to this file")
228
+
229
+ return p