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,10 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (c) 2026 CommonHuman-Lab
3
+ """BlackOpsSQL — engine/http/injector.py"""
4
+
5
+ from blackops_core.http import HttpClient, parse_cookie_string, parse_post_data
6
+
7
+ # Alias: Injector IS HttpClient — no subclass needed, all methods already present.
8
+ Injector = HttpClient
9
+
10
+ __all__ = ["Injector", "HttpClient", "parse_cookie_string", "parse_post_data"]
@@ -0,0 +1,51 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (c) 2026 CommonHuman-Lab
3
+ """BlackOpsSQL — WAF detection"""
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import Optional
8
+
9
+ from blackops_payloads.waf import WafResult, detect as _detect
10
+ from blackops_payloads.waf.signatures import WafSignature # noqa: F401 (re-export)
11
+ from blackops_payloads.encoders import (
12
+ EVASION_NONE,
13
+ EVASION_CASE_MIXING,
14
+ EVASION_HTML_ENCODE,
15
+ EVASION_UNICODE,
16
+ EVASION_DOUBLE_ENCODE,
17
+ EVASION_CHUNKED_TAGS,
18
+ EVASION_NULL_BYTE,
19
+ EVASION_NEWLINE,
20
+ EVASION_COMMENT_BREAK,
21
+ EVASION_BACKTICK,
22
+ EVASION_SQL_COMMENT,
23
+ EVASION_SQL_WHITESPACE,
24
+ EVASION_SQL_CASE,
25
+ EVASION_SQL_ENCODE,
26
+ EVASION_SQL_MULTILINE,
27
+ )
28
+
29
+ # SQLi probe — triggers most SQL-aware WAFs
30
+ _PROBE_PAYLOAD = "' OR '1'='1\"-- -"
31
+
32
+ __all__ = [
33
+ "WafResult", "WafSignature", "detect",
34
+ "EVASION_NONE", "EVASION_CASE_MIXING", "EVASION_HTML_ENCODE",
35
+ "EVASION_UNICODE", "EVASION_DOUBLE_ENCODE", "EVASION_CHUNKED_TAGS",
36
+ "EVASION_NULL_BYTE", "EVASION_NEWLINE", "EVASION_COMMENT_BREAK",
37
+ "EVASION_BACKTICK",
38
+ "EVASION_SQL_COMMENT", "EVASION_SQL_WHITESPACE", "EVASION_SQL_CASE",
39
+ "EVASION_SQL_ENCODE", "EVASION_SQL_MULTILINE",
40
+ ]
41
+
42
+
43
+ def detect(injector, url: str, param: Optional[str] = None) -> WafResult:
44
+ """Probe for a WAF using the SQLi probe payload."""
45
+ return _detect(
46
+ injector.get,
47
+ url,
48
+ param,
49
+ probe_payload=_PROBE_PAYLOAD,
50
+ check_reflection=False,
51
+ )
@@ -0,0 +1,7 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (c) 2026 CommonHuman-Lab
3
+ """BlackOpsSQL — engine/log.py — re-exported from blackops_cli.logging."""
4
+
5
+ from blackops_cli.logging import FINDING, StingLogger, get_logger, ScanResultHandler # noqa: F401
6
+
7
+ __all__ = ["FINDING", "get_logger", "ScanResultHandler"]
@@ -0,0 +1,208 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (c) 2026 CommonHuman-Lab
3
+ """BlackOpsSQL — engine/reporter.py — scan result dataclasses and serialisation."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import dataclasses
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from blackops_cli.reporter import ScanResultBase
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Enums
17
+ # ---------------------------------------------------------------------------
18
+
19
+ class FindingType(str, Enum):
20
+ ERROR_BASED = "error_based_sqli"
21
+ BOOLEAN = "boolean_based_sqli"
22
+ TIME_BASED = "time_based_sqli"
23
+ UNION_BASED = "union_based_sqli"
24
+ OOB = "oob_sqli"
25
+ STACKED = "stacked_sqli"
26
+ EXTRACTION = "extraction"
27
+
28
+
29
+ # ---------------------------------------------------------------------------
30
+ # Finding dataclasses
31
+ # ---------------------------------------------------------------------------
32
+
33
+ @dataclass
34
+ class ErrorBasedFinding:
35
+ """DB error pattern visible in response — confirmed SQLi."""
36
+ url: str
37
+ parameter: str
38
+ method: str
39
+ payload: str
40
+ dbms: str
41
+ evidence: str = ""
42
+
43
+
44
+ @dataclass
45
+ class BooleanFinding:
46
+ """True/false response divergence — likely SQLi."""
47
+ url: str
48
+ parameter: str
49
+ method: str
50
+ payload_true: str
51
+ payload_false: str
52
+ diff_score: float
53
+ confirmed: bool
54
+ evidence: str = ""
55
+
56
+
57
+ @dataclass
58
+ class TimeFinding:
59
+ """Response time delta exceeds threshold — time-based blind SQLi."""
60
+ url: str
61
+ parameter: str
62
+ method: str
63
+ payload: str
64
+ dbms: str
65
+ observed_delay: float
66
+ threshold: int
67
+
68
+
69
+ @dataclass
70
+ class UnionFinding:
71
+ """UNION SELECT reflection confirmed — union-based SQLi."""
72
+ url: str
73
+ parameter: str
74
+ method: str
75
+ payload: str
76
+ column_count: int
77
+ extracted: str = ""
78
+
79
+
80
+ @dataclass
81
+ class OOBFinding:
82
+ """Out-of-band payload injected — callback confirmation required externally."""
83
+ url: str
84
+ parameter: str
85
+ method: str
86
+ payload: str
87
+ callback_url: str
88
+ confirmed: bool = False
89
+
90
+
91
+ @dataclass
92
+ class StackedFinding:
93
+ """Stacked (batched) query injection confirmed — second statement was executed."""
94
+ url: str
95
+ parameter: str
96
+ method: str
97
+ payload: str
98
+ dbms: str
99
+ evidence: str = ""
100
+
101
+
102
+ @dataclass
103
+ class ExtractionFinding:
104
+ """Data extracted via blind char-by-char extraction (boolean or time mode)."""
105
+ url: str
106
+ parameter: str
107
+ method: str
108
+ expr: str
109
+ value: str
110
+ mode: str
111
+
112
+
113
+ @dataclass
114
+ class TableDumpFinding:
115
+ """Rows extracted from a table via table dump."""
116
+ table: str
117
+ columns: List[str]
118
+ rows: List[List[str]]
119
+ url: str
120
+ parameter: str
121
+ method: str
122
+
123
+
124
+ # ---------------------------------------------------------------------------
125
+ # Finding type → list attribute mapping
126
+ # ---------------------------------------------------------------------------
127
+
128
+ _FINDING_LISTS: List[tuple[str, FindingType]] = [
129
+ ("error_based", FindingType.ERROR_BASED),
130
+ ("boolean_based", FindingType.BOOLEAN),
131
+ ("time_based", FindingType.TIME_BASED),
132
+ ("union_based", FindingType.UNION_BASED),
133
+ ("oob", FindingType.OOB),
134
+ ("stacked", FindingType.STACKED),
135
+ ("extracted", FindingType.EXTRACTION),
136
+ ]
137
+
138
+ _FINDING_SEVERITY: dict[FindingType, str] = {
139
+ FindingType.ERROR_BASED: "high",
140
+ FindingType.BOOLEAN: "high",
141
+ FindingType.TIME_BASED: "high",
142
+ FindingType.UNION_BASED: "high",
143
+ FindingType.OOB: "high",
144
+ FindingType.STACKED: "critical",
145
+ FindingType.EXTRACTION: "critical",
146
+ }
147
+
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # Top-level ScanResult
151
+ # ---------------------------------------------------------------------------
152
+
153
+ @dataclass
154
+ class ScanResult(ScanResultBase):
155
+ # DBMS (auto-detected during scan) — BlackOpsSQL-specific
156
+ dbms_detected: Optional[str] = None
157
+
158
+ # Findings
159
+ error_based: List[ErrorBasedFinding] = field(default_factory=list)
160
+ boolean_based: List[BooleanFinding] = field(default_factory=list)
161
+ time_based: List[TimeFinding] = field(default_factory=list)
162
+ union_based: List[UnionFinding] = field(default_factory=list)
163
+ oob: List[OOBFinding] = field(default_factory=list)
164
+ stacked: List[StackedFinding] = field(default_factory=list)
165
+ extracted: List[ExtractionFinding] = field(default_factory=list)
166
+ table_dumps: List[TableDumpFinding] = field(default_factory=list)
167
+
168
+ # --- Append helpers -------------------------------------------------------
169
+
170
+ def append_error_based(self, f) -> None: self._append("error_based", f)
171
+ def append_boolean(self, f) -> None: self._append("boolean_based", f)
172
+ def append_time(self, f) -> None: self._append("time_based", f)
173
+ def append_union(self, f) -> None: self._append("union_based", f)
174
+ def append_oob(self, f) -> None: self._append("oob", f)
175
+ def append_stacked(self, f) -> None: self._append("stacked", f)
176
+
177
+ def append_extraction(self, f) -> None:
178
+ with self._lock:
179
+ for existing in self.extracted:
180
+ if existing.parameter == f.parameter and existing.expr == f.expr:
181
+ return
182
+ self.extracted.append(f)
183
+
184
+ # --- Computed properties --------------------------------------------------
185
+
186
+ @property
187
+ def total_findings(self) -> int:
188
+ return sum(len(getattr(self, attr)) for attr, _ in _FINDING_LISTS)
189
+
190
+ def to_dict(self) -> Dict[str, Any]:
191
+ findings: List[Dict[str, Any]] = []
192
+ for attr, ftype in _FINDING_LISTS:
193
+ for item in getattr(self, attr):
194
+ d = dataclasses.asdict(item)
195
+ d["type"] = ftype.value
196
+ d["severity"] = _FINDING_SEVERITY.get(ftype, "info")
197
+ findings.append(d)
198
+
199
+ result = self._base_dict()
200
+ result["dbms_detected"] = self.dbms_detected
201
+ result["total_findings"] = self.total_findings
202
+ result["findings"] = findings
203
+ return result
204
+
205
+ def dumps_to_dict(self) -> Dict[str, Any]:
206
+ return {
207
+ "table_dumps": [dataclasses.asdict(td) for td in self.table_dumps],
208
+ }
@@ -0,0 +1,95 @@
1
+ # SPDX-License-Identifier: AGPL-3.0-or-later
2
+ # Copyright (c) 2026 CommonHuman-Lab
3
+ """
4
+ BlackOpsSQL — engine/scanner.py
5
+ Top-level scan() entry point.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import os
12
+
13
+ from .log import ScanResultHandler, get_logger
14
+ from .http.injector import Injector
15
+ from .reporter import ScanResult
16
+ from .http import waf_detect # noqa: F401 (patch target for tests)
17
+ from ._scanner.options import ScanOptions # noqa: F401 (re-exported)
18
+ from ._scanner.pipeline import run
19
+
20
+ logger = get_logger("blackopssql.scanner")
21
+
22
+
23
+ _OUTPUT_EXTS = {".json", ".txt", ".html"}
24
+
25
+
26
+ def _output_stem(path: str) -> str:
27
+ """Strip extension only if it's one of our known output types (not e.g. '.1' in an IP)."""
28
+ root, ext = os.path.splitext(path)
29
+ return root if ext.lower() in _OUTPUT_EXTS else path
30
+
31
+
32
+ def _unique_stem(stem: str) -> str:
33
+ """Return *stem* unchanged if no collision exists, else append _1, _2, …"""
34
+ suffixes = (".json", ".txt", "_dump.json", ".html")
35
+ if not any(os.path.exists(stem + s) for s in suffixes):
36
+ return stem
37
+ counter = 1
38
+ while any(os.path.exists(f"{stem}_{counter}{s}") for s in suffixes):
39
+ counter += 1
40
+ return f"{stem}_{counter}"
41
+
42
+
43
+ def scan(url: str, options: ScanOptions | None = None) -> ScanResult:
44
+ """Run a full BlackOpsSQL scan against *url* and return a ScanResult."""
45
+ if options is None:
46
+ options = ScanOptions()
47
+
48
+ result = ScanResult(target=url)
49
+ _root = get_logger("blackopssql")
50
+ _handler = ScanResultHandler(result)
51
+ _handler.setFormatter(__import__("logging").Formatter("%(name)s: %(message)s"))
52
+ _root.addHandler(_handler)
53
+
54
+ injector = Injector(
55
+ timeout=options.timeout,
56
+ proxy=options.proxy or None,
57
+ headers=options.headers or None,
58
+ cookies=options.cookies or None,
59
+ delay=options.delay,
60
+ )
61
+
62
+ try:
63
+ run(url, options, injector, result)
64
+ except Exception as exc:
65
+ result.append_error(f"Scan aborted: {exc}")
66
+ logger.exception("BlackOpsSQL scan error")
67
+ finally:
68
+ _root.removeHandler(_handler)
69
+ injector.close()
70
+ result.requests_sent = injector.request_count
71
+ result.finish()
72
+
73
+ if options.output:
74
+ stem = _output_stem(options.output)
75
+ try:
76
+ with open(stem + ".json", "w", encoding="utf-8") as fh:
77
+ json.dump(result.to_dict(), fh, indent=2)
78
+ except OSError as exc:
79
+ result.append_error(f"Failed to write JSON output: {exc}")
80
+ try:
81
+ from blackopssql._cli.summary import format_summary
82
+ with open(stem + ".txt", "w", encoding="utf-8") as fh:
83
+ fh.write(format_summary(result))
84
+ except OSError as exc:
85
+ result.append_error(f"Failed to write text output: {exc}")
86
+
87
+ if options.output and result.table_dumps:
88
+ stem = _output_stem(options.output)
89
+ try:
90
+ with open(stem + "_dump.json", "w", encoding="utf-8") as fh:
91
+ json.dump(result.dumps_to_dict(), fh, indent=2)
92
+ except OSError as exc:
93
+ result.append_error(f"Failed to write dump file: {exc}")
94
+
95
+ return result