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,216 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (c) 2026 CommonHuman-Lab
3
+ from __future__ import annotations
4
+
5
+ import io
6
+ import re as _re
7
+ import sys
8
+ import urllib.parse as _up
9
+
10
+ from blackops_cli.colour import BOLD, CYAN, DIM, GREEN, RED, YELLOW
11
+
12
+ _ANSI_RE = _re.compile(r"\x1b\[[0-9;]*m")
13
+
14
+ _MARKER_RE = _re.compile(r"BreachSQL_[A-Za-z0-9]+")
15
+ _CHAR_RE = _re.compile(r"\bchar\(\d[\d,]+\)")
16
+
17
+
18
+ def _ascii_table(columns: list[str], rows: list[list], max_col_w: int = 55) -> list[str]:
19
+ """Return lines for a bordered ASCII table."""
20
+ widths = [len(c) for c in columns]
21
+ for row in rows:
22
+ for i, cell in enumerate(row):
23
+ if i < len(widths):
24
+ widths[i] = min(max_col_w, max(widths[i], len(str(cell))))
25
+
26
+ def border() -> str:
27
+ return "+" + "+".join("-" * (w + 2) for w in widths) + "+"
28
+
29
+ def fmt_row(cells: list) -> str:
30
+ parts = []
31
+ for i, cell in enumerate(cells):
32
+ w = widths[i] if i < len(widths) else 0
33
+ s = str(cell)
34
+ if len(s) > w:
35
+ s = s[:w - 3] + "..."
36
+ parts.append(f" {s.ljust(w)} ")
37
+ return "|" + "|".join(parts) + "|"
38
+
39
+ lines = [border(), fmt_row(columns), border()]
40
+ for row in rows:
41
+ lines.append(fmt_row(row))
42
+ lines.append(border())
43
+ return lines
44
+
45
+
46
+ def _clean_payload(payload: str) -> str:
47
+ """Replace random marker strings with a stable placeholder."""
48
+ s = _MARKER_RE.sub("<marker>", payload)
49
+ s = _CHAR_RE.sub("char(<marker>)", s)
50
+ return s
51
+
52
+
53
+ def _proof_url(url: str, param: str, payload: str, original: str = "1") -> str:
54
+ """
55
+ Build a proof-of-concept URL by injecting *payload* into *param*.
56
+
57
+ The payload is appended to the original param value (matching how the
58
+ scanner injects it) so the link reproduces the exact request that
59
+ triggered the finding. The result is percent-encoded so it is safe
60
+ to paste into a browser address bar or terminal.
61
+ """
62
+ try:
63
+ parsed = _up.urlparse(url)
64
+ qs = _up.parse_qs(parsed.query, keep_blank_values=True)
65
+ orig_val = qs.get(param, [original])[0]
66
+ injected = orig_val + payload
67
+ qs[param] = [injected]
68
+ new_query = _up.urlencode(qs, doseq=True)
69
+ return _up.urlunparse(parsed._replace(query=new_query))
70
+ except Exception:
71
+ return ""
72
+
73
+
74
+ def print_summary(result) -> None:
75
+ print()
76
+ print(BOLD("=" * 60))
77
+ print(BOLD(" BlackOpsSQL — Scan Summary"))
78
+ print(BOLD("=" * 60))
79
+ print(f" Target : {result.target}")
80
+ print(f" Duration : {result.duration_s}s")
81
+ print(f" Requests sent : {result.requests_sent}")
82
+ print(f" URLs crawled : {result.crawled_urls}")
83
+ print(f" Params tested : {result.params_tested}")
84
+ print(f" WAF detected : {result.waf_detected or 'None'}")
85
+ print(f" Evasion used : {result.evasion_applied or 'None'}")
86
+ print(f" DBMS detected : {result.dbms_detected or 'Unknown'}")
87
+ print()
88
+
89
+ if result.total_findings == 0:
90
+ print(DIM(" No findings."))
91
+ else:
92
+ print(GREEN(f" Total findings: {result.total_findings}"))
93
+ print()
94
+ i = 1
95
+
96
+ for f in result.error_based:
97
+ print(f" {i}. {RED('[ERROR-BASED SQLi]')} Confirmed")
98
+ print(f" Param : {f.parameter}")
99
+ print(f" URL : {f.url}")
100
+ print(f" Method : {f.method}")
101
+ print(f" DBMS : {f.dbms}")
102
+ print(f" Payload : {f.payload}")
103
+ if f.method.upper() == "GET":
104
+ print(f" Proof : {CYAN(_proof_url(f.url, f.parameter, f.payload))}")
105
+ print()
106
+ i += 1
107
+
108
+ for f in result.boolean_based:
109
+ status = GREEN("[CONFIRMED]") if f.confirmed else YELLOW("[LIKELY]")
110
+ print(f" {i}. {status} {YELLOW('Boolean-based SQLi')}")
111
+ print(f" Param : {f.parameter}")
112
+ print(f" URL : {f.url}")
113
+ print(f" Method : {f.method}")
114
+ print(f" True payload : {f.payload_true}")
115
+ print(f" False payload : {f.payload_false}")
116
+ print(f" Diff score : {f.diff_score:.2f}")
117
+ if f.method.upper() == "GET":
118
+ print(f" Proof (true) : {CYAN(_proof_url(f.url, f.parameter, f.payload_true))}")
119
+ print(f" Proof (false): {CYAN(_proof_url(f.url, f.parameter, f.payload_false))}")
120
+ print()
121
+ i += 1
122
+
123
+ for f in result.time_based:
124
+ print(f" {i}. {CYAN('[TIME-BASED BLIND SQLi]')}")
125
+ print(f" Param : {f.parameter}")
126
+ print(f" URL : {f.url}")
127
+ print(f" Method : {f.method}")
128
+ print(f" DBMS hint : {f.dbms}")
129
+ print(f" Payload : {f.payload}")
130
+ print(f" Delay : {f.observed_delay:.2f}s (threshold: {f.threshold}s)")
131
+ if f.method.upper() == "GET":
132
+ print(f" Proof : {CYAN(_proof_url(f.url, f.parameter, f.payload))}")
133
+ print()
134
+ i += 1
135
+
136
+ for f in result.union_based:
137
+ print(f" {i}. {RED('[UNION-BASED SQLi]')} Confirmed")
138
+ print(f" Param : {f.parameter}")
139
+ print(f" URL : {f.url}")
140
+ print(f" Method : {f.method}")
141
+ print(f" Columns : {f.column_count}")
142
+ print(f" Payload : {_clean_payload(f.payload)}")
143
+ if f.method.upper() == "GET":
144
+ print(f" Proof : {CYAN(_proof_url(f.url, f.parameter, f.payload))}")
145
+ print()
146
+ i += 1
147
+
148
+ for f in result.oob:
149
+ print(f" {i}. {CYAN('[OOB SQLi]')} Payload injected")
150
+ print(f" Param : {f.parameter}")
151
+ print(f" URL : {f.url}")
152
+ print(f" Callback : {f.callback_url}")
153
+ print(f" Payload : {f.payload}")
154
+ if f.method.upper() == "GET":
155
+ print(f" Proof : {CYAN(_proof_url(f.url, f.parameter, f.payload))}")
156
+ print()
157
+ i += 1
158
+
159
+ for f in result.stacked:
160
+ print(f" {i}. {RED('[STACKED QUERY SQLi]')} Confirmed")
161
+ print(f" Param : {f.parameter}")
162
+ print(f" URL : {f.url}")
163
+ print(f" Method : {f.method}")
164
+ print(f" DBMS : {f.dbms}")
165
+ print(f" Payload : {f.payload}")
166
+ if f.method.upper() == "GET":
167
+ print(f" Proof : {CYAN(_proof_url(f.url, f.parameter, f.payload))}")
168
+ print()
169
+ i += 1
170
+
171
+ if result.extracted:
172
+ print(f" {GREEN('─' * 56)}")
173
+ print(f" {GREEN(BOLD(' Extracted Data'))}")
174
+ print(f" {GREEN('─' * 56)}")
175
+ for f in result.extracted:
176
+ mode_label = f.mode if f.mode == "union" else f"{f.mode}-blind"
177
+ print(f" {i}. {GREEN('[EXTRACTED]')} via {mode_label}")
178
+ print(f" Param : {f.parameter}")
179
+ print(f" URL : {f.url}")
180
+ print(f" Expr : {DIM(f.expr)}")
181
+ print(f" Value : {BOLD(f.value)}")
182
+ print()
183
+ i += 1
184
+
185
+ if result.table_dumps:
186
+ print(f" {GREEN('─' * 56)}")
187
+ print(f" {GREEN(BOLD(' Table Dumps'))}")
188
+ print(f" {GREEN('─' * 56)}")
189
+ _MAX_DISPLAY = 20
190
+ for td in result.table_dumps:
191
+ print(f" {GREEN('[DUMP]')} {BOLD(td.table)}"
192
+ f" ({len(td.rows)} row(s)) param:{td.parameter}")
193
+ display_rows = td.rows[:_MAX_DISPLAY]
194
+ for line in _ascii_table(td.columns, display_rows):
195
+ print(f" {line}")
196
+ if len(td.rows) > _MAX_DISPLAY:
197
+ print(f" {DIM(f' ... {len(td.rows) - _MAX_DISPLAY} more row(s) in dump file')}")
198
+ print()
199
+
200
+ if result.errors:
201
+ print(RED(" Errors:"))
202
+ for e in result.errors:
203
+ print(f" - {e}")
204
+
205
+ print(BOLD("=" * 60))
206
+
207
+
208
+ def format_summary(result) -> str:
209
+ """Return print_summary output as a plain string with ANSI codes stripped."""
210
+ buf = io.StringIO()
211
+ old, sys.stdout = sys.stdout, buf
212
+ try:
213
+ print_summary(result)
214
+ finally:
215
+ sys.stdout = old
216
+ return _ANSI_RE.sub("", buf.getvalue())
@@ -0,0 +1,35 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (c) 2026 CommonHuman-Lab
3
+ """
4
+ BlackOpsSQL — engine/__init__.py
5
+ Public API surface for the BlackOpsSQL engine.
6
+ """
7
+
8
+ from .reporter import (
9
+ ErrorBasedFinding,
10
+ BooleanFinding,
11
+ TimeFinding,
12
+ UnionFinding,
13
+ OOBFinding,
14
+ StackedFinding,
15
+ ExtractionFinding,
16
+ TableDumpFinding,
17
+ FindingType,
18
+ ScanResult,
19
+ )
20
+ from .scanner import ScanOptions, scan
21
+
22
+ __all__ = [
23
+ "scan",
24
+ "ScanOptions",
25
+ "ScanResult",
26
+ "FindingType",
27
+ "ErrorBasedFinding",
28
+ "BooleanFinding",
29
+ "TimeFinding",
30
+ "UnionFinding",
31
+ "OOBFinding",
32
+ "StackedFinding",
33
+ "ExtractionFinding",
34
+ "TableDumpFinding",
35
+ ]
File without changes