hackscan 0.1.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.
@@ -0,0 +1,37 @@
1
+ """Sanitizers: calls whose result is safe for some (or all) injection classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from hackscan.core.taxonomy import CMDI, SQLI, TAINT_CLASSES
6
+
7
+ ALL = TAINT_CLASSES
8
+
9
+ # Fully-qualified call name -> vuln classes the result is safe for.
10
+ SANITIZERS: dict[str, frozenset[str]] = {
11
+ # Numeric / boolean / identifier conversions cannot carry injection payloads.
12
+ "int": ALL,
13
+ "float": ALL,
14
+ "bool": ALL,
15
+ "abs": ALL,
16
+ "len": ALL,
17
+ "round": ALL,
18
+ "builtins.int": ALL,
19
+ "builtins.float": ALL,
20
+ "uuid.UUID": ALL,
21
+ "decimal.Decimal": ALL,
22
+ # Shell quoting. Context-sensitive: see QUOTING_SANITIZERS.
23
+ "shlex.quote": frozenset({CMDI}),
24
+ "shlex.join": frozenset({CMDI}),
25
+ "pipes.quote": frozenset({CMDI}),
26
+ # SQL identifier/literal quoting helpers.
27
+ "psycopg2.sql.Identifier": frozenset({SQLI}),
28
+ "psycopg2.sql.Literal": frozenset({SQLI}),
29
+ "psycopg.sql.Identifier": frozenset({SQLI}),
30
+ "psycopg.sql.Literal": frozenset({SQLI}),
31
+ }
32
+
33
+ # Sanitizers whose safety depends on where the result is placed: a shell-quoted value
34
+ # embedded inside another quote context (`'echo "' + shlex.quote(x) + '"'`) is unsafe.
35
+ # Note: `ast.literal_eval` is deliberately absent: it can return a string, which is
36
+ # still dangerous when passed on to eval/exec.
37
+ QUOTING_SANITIZERS = frozenset({"shlex.quote", "shlex.join", "pipes.quote"})
@@ -0,0 +1,99 @@
1
+ """Untrusted input sources: web frameworks, CLI and stdin.
2
+
3
+ Sources are matched three ways:
4
+ 1. Resolved dotted names (`flask.request.args`, `sys.argv`, builtin `input`).
5
+ 2. Request-object parameters: a parameter named `request`/`req` (Django, DRF, Starlette,
6
+ FastAPI `Request`) whose request-data attributes are read.
7
+ 3. Route handler parameters: every parameter of a function decorated with a routing
8
+ decorator (`@app.route`, `@app.get`, `@router.post`, ...) is client-controlled
9
+ (Flask path variables, FastAPI path/query/body parameters).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import ast
15
+
16
+ # Attributes of `flask.request` that carry client data.
17
+ FLASK_REQUEST_ATTRS = frozenset(
18
+ {
19
+ "args",
20
+ "form",
21
+ "values",
22
+ "json",
23
+ "data",
24
+ "cookies",
25
+ "headers",
26
+ "files",
27
+ "get_json",
28
+ "get_data",
29
+ "view_args",
30
+ "path",
31
+ "full_path",
32
+ "url",
33
+ "query_string",
34
+ "stream",
35
+ }
36
+ )
37
+
38
+ # Attributes of a request parameter (Django/DRF/Starlette/FastAPI) that carry client data.
39
+ REQUEST_PARAM_ATTRS = frozenset(
40
+ {
41
+ # Django
42
+ "GET",
43
+ "POST",
44
+ "body",
45
+ "COOKIES",
46
+ "META",
47
+ "FILES",
48
+ "headers",
49
+ "path",
50
+ "path_info",
51
+ # DRF
52
+ "data",
53
+ "query_params",
54
+ # Starlette / FastAPI
55
+ "path_params",
56
+ "cookies",
57
+ "json",
58
+ "form",
59
+ "url",
60
+ # Flask-style, when a request object is passed around
61
+ "args",
62
+ "values",
63
+ "get_json",
64
+ "get_data",
65
+ }
66
+ )
67
+ REQUEST_PARAM_NAMES = frozenset({"request", "req"})
68
+
69
+ # Fully-qualified names whose value (attribute) or return value (call) is untrusted.
70
+ SOURCE_NAMES = {
71
+ "sys.argv": "command-line arguments (sys.argv)",
72
+ "sys.stdin": "standard input (sys.stdin)",
73
+ "input": "user input (input())",
74
+ "builtins.input": "user input (input())",
75
+ }
76
+
77
+ # Decorator method names that register an HTTP route handler.
78
+ ROUTE_DECORATORS = frozenset(
79
+ {"route", "get", "post", "put", "patch", "delete", "head", "options", "api_route", "websocket"}
80
+ )
81
+
82
+
83
+ def flask_request_source(resolved: str) -> str | None:
84
+ """`flask.request.args` -> description, for any resolved dotted name."""
85
+ prefix = "flask.request."
86
+ if resolved.startswith(prefix):
87
+ attr = resolved[len(prefix) :].split(".", 1)[0]
88
+ if attr in FLASK_REQUEST_ATTRS:
89
+ return f"Flask request data (request.{attr})"
90
+ return None
91
+
92
+
93
+ def is_route_handler(func: ast.AST) -> bool:
94
+ """Decorated with `@<obj>.route(...)`, `@<obj>.get(...)`, etc."""
95
+ for deco in getattr(func, "decorator_list", ()):
96
+ target = deco.func if isinstance(deco, ast.Call) else deco
97
+ if isinstance(target, ast.Attribute) and target.attr in ROUTE_DECORATORS:
98
+ return True
99
+ return False
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.5
2
+ Name: hackscan
3
+ Version: 0.1.0
4
+ Summary: Python SAST orchestrator: own AST + taint engine, imports Semgrep/Bandit/CodeQL/Gitleaks, dedupes, emits SARIF.
5
+ Project-URL: Source, https://github.com/kahledawad2019-netizen/hackscan
6
+ Project-URL: Issues, https://github.com/kahledawad2019-netizen/hackscan/issues
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: sarif,sast,security,static-analysis,taint-analysis,vulnerability-scanner
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: click>=8.1
23
+ Requires-Dist: pyyaml>=6.0.3
24
+ Description-Content-Type: text/markdown
25
+
26
+ # HackScan
27
+
28
+ **A Python security scanner that verifies what it reports.** HackScan finds injection
29
+ sinks with its own AST engine, then runs a flow-sensitive taint analysis to decide, per
30
+ finding, whether untrusted input really reaches it. It can also ingest Semgrep, Bandit,
31
+ CodeQL and Gitleaks results, merge duplicates across tools, and emit one SARIF report.
32
+
33
+ ```text
34
+ $ hackscan scan app/
35
+ HIGH confirmed views.py:10:9 HS-SQLI-001 [100%]
36
+ SQL query is built with string formatting/concatenation.
37
+ | cursor.execute("SELECT * FROM sales WHERE year = " + year)
38
+ > Untrusted request data (`request.GET`) at line 8.
39
+ > Untrusted input reaches the sink.
40
+ ```
41
+
42
+ ## Why another scanner?
43
+
44
+ | | HackScan |
45
+ |---|---|
46
+ | **Verdicts, not just matches** | Every injection candidate is **confirmed** (untrusted source reaches it, with a trace), **suppressed** (provably constant or sanitized on every path), or left as a **candidate** for review. |
47
+ | **Sound suppression** | A finding is only dismissed if *every* path is safe. Branches, loops (`break`/`continue`), `try`/`except`/`finally`, aliasing and mutation of lists/dicts, closures and `:=` are modeled. A differential fuzzer executes thousands of random programs to check that no dismissed sink ever receives attacker input. |
48
+ | **Framework-aware** | Sources: Flask `request.*`, Django/DRF/Starlette/FastAPI request objects (incl. `self.request`), route-handler parameters, `input()`, `sys.argv`. |
49
+ | **One report from many tools** | Imports Semgrep, Bandit, CodeQL (SARIF) and Gitleaks; deduplicates by vulnerability class and location, keeping every tool as provenance. |
50
+ | **CI-ready** | SARIF 2.1.0 (validated against the OASIS schema) for GitHub code scanning, `--fail-on` exit codes, stable fingerprints that survive code moves. |
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install hackscan # or: uv tool install hackscan / pipx install hackscan
56
+ ```
57
+
58
+ Python 3.10–3.13, any OS. No network access and no LLM needed.
59
+
60
+ ## Usage
61
+
62
+ ```bash
63
+ hackscan scan . # text report
64
+ hackscan scan . --format sarif -o hackscan.sarif # for GitHub code scanning
65
+ hackscan scan . --fail-on high # exit 1 on open high/critical findings
66
+ hackscan scan . --format json --show-suppressed # everything, machine-readable
67
+ hackscan scan . --with bandit,semgrep,gitleaks # also run these tools if installed
68
+ hackscan scan . --import codeql=results.sarif # merge a report you already have
69
+ hackscan rules # list rules
70
+ ```
71
+
72
+ | Option | Meaning |
73
+ |---|---|
74
+ | `--severity`, `--min-confidence` | Report filters. |
75
+ | `--fail-on SEV` | Exit 1 if an open (candidate/confirmed) finding is at least `SEV`. Suppressed findings never count. |
76
+ | `--ignore GLOB` | Skip paths (`tests/*`, `**/migrations/**`, `legacy`). `.venv`, `node_modules`, `build`, ... are always skipped. |
77
+ | `--with TOOLS` / `--import FMT=FILE` | Run external tools / import reports (`sarif`, `semgrep`, `bandit`, `codeql`, `gitleaks`). A missing or failing tool is a warning unless `--strict-tools`. |
78
+ | `-o FILE` | Write the report to `FILE`. Never overwrites anything except a previous HackScan report. |
79
+ | `--plugins DIR` | Load custom rules (see below). |
80
+ | `--allow-incomplete` | Do not exit 2 when some files cannot be analyzed (they are still listed). |
81
+ | `--no-taint` | Pattern matching only. |
82
+ | `--jobs N` | Worker processes (default: automatic). |
83
+
84
+ Exit codes: `0` OK, `1` `--fail-on` threshold reached, `2` usage/config/plugin error or
85
+ an **incomplete scan** (a file could not be parsed or read, or an `--import` report
86
+ could not be loaded; use `--allow-incomplete` to accept). A `--with` tool that is missing
87
+ or exits with an error is a warning, or exit `2` with `--strict-tools`. An incomplete scan is never reported as a pass, and SARIF marks it with
88
+ `executionSuccessful: false`.
89
+
90
+ ### Configuration
91
+
92
+ `.hackscan.yml` files are discovered from the filesystem root down to the scanned
93
+ directory and merged (nearest wins; `ignore` lists accumulate). CLI options override them.
94
+
95
+ ```yaml
96
+ ignore: [tests/*, "**/migrations/**"]
97
+ severity: medium
98
+ min-confidence: 40
99
+ fail-on: high
100
+ with: [bandit]
101
+ import:
102
+ codeql: codeql-results.sarif
103
+ plugins: security/rules
104
+ ```
105
+
106
+ ### Suppressing a finding
107
+
108
+ ```python
109
+ os.system(cmd) # hackscan: ignore[HS-CMDI-001]
110
+ ```
111
+
112
+ Suppressed findings stay in SARIF output (as `suppressions`) so they remain auditable;
113
+ use `--sarif-omit-suppressed` when uploading to GitHub, which does not honor them.
114
+
115
+ Secrets are never echoed: findings of class `secret` (from any tool) have generic
116
+ messages and redacted snippets, and values reported by Gitleaks are scrubbed from all
117
+ output, including warnings.
118
+
119
+ ## Rules
120
+
121
+ | Rule | Detects | Severity |
122
+ |---|---|---|
123
+ | `HS-SQLI-001` | Non-constant SQL reaching DB-API `execute*`, Django `raw`, SQLAlchemy `text`, pandas `read_sql` | high |
124
+ | `HS-CMDI-001` | Non-constant commands reaching `os.system`, `os.popen`, `subprocess.*(shell=True)`, ... | high |
125
+ | `HS-CODEI-001` | Non-constant input to `eval`, `exec`, `compile` | high |
126
+ | `HS-CRYPTO-001` | MD5/SHA-1 (unless `usedforsecurity=False`) | low |
127
+
128
+ Sanitizers recognized: numeric/UUID conversions (all classes), `psycopg` `sql.Identifier`
129
+ /`sql.Literal` (SQL). `shlex.quote` is POSIX-only, so quoted commands are kept as
130
+ low-confidence candidates rather than suppressed.
131
+
132
+ ### Custom rules
133
+
134
+ ```python
135
+ # security/rules/pickle_rule.py
136
+ from hackscan.core.models import Severity
137
+ from hackscan.plugins import Match, RulePlugin
138
+
139
+
140
+ class PickleLoads(RulePlugin):
141
+ rule_id = "ACME-PICKLE-001" # the HS- prefix is reserved
142
+ name = "pickle-loads"
143
+ description = "pickle.loads on untrusted data"
144
+ severity = Severity.HIGH
145
+ cwe = ("CWE-502",)
146
+
147
+ def check(self, node, ctx):
148
+ if "pickle.loads" in ctx.call_names(node):
149
+ yield Match(node, "pickle.loads call")
150
+ ```
151
+
152
+ ## GitHub Actions
153
+
154
+ ```yaml
155
+ - run: pipx install hackscan
156
+ - run: hackscan scan . --format sarif --sarif-omit-suppressed -o hackscan.sarif --fail-on high
157
+ - uses: github/codeql-action/upload-sarif@v3
158
+ if: always()
159
+ with:
160
+ sarif_file: hackscan.sarif
161
+ ```
162
+
163
+ ## Limitations
164
+
165
+ - Taint analysis is intra-procedural: values arriving through function parameters are
166
+ candidates, not confirmations (inter-procedural analysis is on the roadmap).
167
+ - Python only. Imported tools may cover other languages; their findings are passed through.
168
+
169
+ ## Development
170
+
171
+ ```bash
172
+ uv sync --group dev
173
+ uv run pytest
174
+ uv run ruff check . && uv run ruff format --check .
175
+ ```
176
+
177
+ See [PROJECT.md](PROJECT.md) for the design, milestones and decision log.
178
+
179
+ ## License
180
+
181
+ MIT
@@ -0,0 +1,38 @@
1
+ hackscan/__init__.py,sha256=GcNLXUbJFNBS3K-SvHhkLUK9cOMWVGY1ZeZt48qjt2w,78
2
+ hackscan/cli.py,sha256=3UJJrJ6YHsTZsfve08xlgFecuU8FsV9mA3imd7pWcik,12878
3
+ hackscan/config.py,sha256=mzer8CtjwdxQutrtmMS14k66USLjxDXZzLzSSWRycBs,6547
4
+ hackscan/analyzers/__init__.py,sha256=elBA2v6nafQUQNdepIOzhnF1dfJNpdIM1r7_u7TYkII,23
5
+ hackscan/analyzers/ast_pass.py,sha256=z9m7zhJyILLNWOWn3kmNHy8yn6rwYjR-4kiBx79pWag,6056
6
+ hackscan/analyzers/suppressions.py,sha256=hcKBlK6729DZT3cFmW3p42RzFtvi3Nh93tneQjtkAKQ,2303
7
+ hackscan/analyzers/taint_pass.py,sha256=xxU39JD7m1hZu3mzj41SCd2warHMaFUzjh5_hzBfQlc,34350
8
+ hackscan/core/__init__.py,sha256=PCIydZsEkHbAAwx6huzlLKZIjZVwdN3zmYzzDGUiApo,60
9
+ hackscan/core/dedupe.py,sha256=Fs58QHoTUIqvhwTGDHWM_y8exDfQXJWXhE9k_tu7VCQ,5787
10
+ hackscan/core/fingerprint.py,sha256=dnzay8zrqXi7RZzJzE-J8a535Gf3HBwgFtLYD691wx8,2561
11
+ hackscan/core/models.py,sha256=Drv92BkGGP161f7eHrkYk3Bc0iYnJ6VNz5uW6T80f7I,9284
12
+ hackscan/core/pipeline.py,sha256=Okjh1IybZgjh86h7DBJ5GG78DZ2nNbre8myv5uIQfCo,6241
13
+ hackscan/core/redact.py,sha256=dJN7RC01yVP_1IrOHeJ0IzobNQJlBCSdQ2mVgeuGArg,3405
14
+ hackscan/core/taxonomy.py,sha256=ry_x7K3z9rh1AY5W7q7Fjib9OSMG2YYeAKEC9gEEwLs,3268
15
+ hackscan/importers/__init__.py,sha256=fuGmHD8qS_xDk57KLCkDO4fZLXHraaNnAtE3MdV5pb0,49
16
+ hackscan/importers/common.py,sha256=JE1BhKNTTHnJefadNMqUcjca01DwbhkUVad6s9qCZTg,7577
17
+ hackscan/importers/gitleaks.py,sha256=aWzi2zjHy1VCoVaB6QxruW0U2qzbl9MiVRgBd3z53tM,3789
18
+ hackscan/importers/runner.py,sha256=w66aWDa_lECGekNOsKDIzu1fzATJeuceY92IPIEUJ4A,5937
19
+ hackscan/importers/sarif.py,sha256=L0bvKQUqCxw3weEsLgNoRFXVU-xVzH0QGapPV_uJ5Vo,5931
20
+ hackscan/plugins/__init__.py,sha256=ftL30V67HM7IlVwdZ75Diajl6O0KlW896xQzuS6UsGM,197
21
+ hackscan/plugins/base.py,sha256=nmXhLmApn3iaHId8sovOUU5WjwBiVmc8rg5hufYjhTY,10198
22
+ hackscan/plugins/loader.py,sha256=cWS79msmrMV1SBqj8coIqj6UI3UCM7Hbn1WMy0V6uGU,3926
23
+ hackscan/plugins/scopes.py,sha256=4GDUAdxalzcq8pEuoXxu9ougMA_YMh0UYipzmUjvGBg,11484
24
+ hackscan/plugins/rules/__init__.py,sha256=QlFidHeq30_8JfoB4PMxjrGQfQGeYohdZQdQ3cMUTgw,32
25
+ hackscan/plugins/rules/cmdi.py,sha256=KtU0DZIxLHdyP7tc6_SVS2Q3F7y8Lf4MDLo9kYuO5jo,2420
26
+ hackscan/plugins/rules/codei.py,sha256=9uBSpWdU1l_eoOEWhU5jBCpJSjKb00HWT5WwYXDIciw,1503
27
+ hackscan/plugins/rules/sqli.py,sha256=7-5Ab-wKGf3E4Pl7OwwJfEP9q8NjlKli8P5tzergghA,4184
28
+ hackscan/plugins/rules/weak_crypto.py,sha256=Ikfbkf1iMXi4YG319FFwXjR_-n3M30KyP6YpyZIy1Ak,1952
29
+ hackscan/sarif/__init__.py,sha256=pvckjpJyukYgJmh_K2k8McH0xr5sVOSdQ2ExwnhHPXM,26
30
+ hackscan/sarif/generator.py,sha256=RadNx_1-dL6uDptfHQXSuIO1EjuBjm_WJbXC4n-PzVg,7898
31
+ hackscan/taint/__init__.py,sha256=LtqvgHY01Ya-lIis87zVxBbaz8OwGm-_SR6b8tz-YIg,44
32
+ hackscan/taint/sanitizers.py,sha256=Edqvr5UFyAFBKWilhxGgz2es2z8aXhjd37nGhtOPjNo,1396
33
+ hackscan/taint/sources.py,sha256=LSskC0JKwxbIsiyXU2Prz0sCyYQmGfsZQLqdBUQ11zU,2883
34
+ hackscan-0.1.0.dist-info/METADATA,sha256=J3YB65Kqr-VpQaDLQI-c-DU4F6TefIJr2XkKGCB0Rh4,7702
35
+ hackscan-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
36
+ hackscan-0.1.0.dist-info/entry_points.txt,sha256=MwyZQj1nZ-7l0tUv-x1CtLeryBbtnOleUZihDy6Fgxs,47
37
+ hackscan-0.1.0.dist-info/licenses/LICENSE,sha256=KkO2hUXEuGbXW1_AnaHLlx2GHIdQA8UmxOXLNay7yW8,1068
38
+ hackscan-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ hackscan = hackscan.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Khaled Awad
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.