hackscan 0.1.0__tar.gz

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.
Files changed (79) hide show
  1. hackscan-0.1.0/.gitattributes +2 -0
  2. hackscan-0.1.0/.github/workflows/ci.yml +50 -0
  3. hackscan-0.1.0/.github/workflows/release.yml +56 -0
  4. hackscan-0.1.0/.gitignore +13 -0
  5. hackscan-0.1.0/LICENSE +21 -0
  6. hackscan-0.1.0/PKG-INFO +181 -0
  7. hackscan-0.1.0/PROJECT.md +272 -0
  8. hackscan-0.1.0/README.md +156 -0
  9. hackscan-0.1.0/pyproject.toml +62 -0
  10. hackscan-0.1.0/src/hackscan/__init__.py +3 -0
  11. hackscan-0.1.0/src/hackscan/analyzers/__init__.py +1 -0
  12. hackscan-0.1.0/src/hackscan/analyzers/ast_pass.py +154 -0
  13. hackscan-0.1.0/src/hackscan/analyzers/suppressions.py +63 -0
  14. hackscan-0.1.0/src/hackscan/analyzers/taint_pass.py +808 -0
  15. hackscan-0.1.0/src/hackscan/cli.py +326 -0
  16. hackscan-0.1.0/src/hackscan/config.py +184 -0
  17. hackscan-0.1.0/src/hackscan/core/__init__.py +1 -0
  18. hackscan-0.1.0/src/hackscan/core/dedupe.py +147 -0
  19. hackscan-0.1.0/src/hackscan/core/fingerprint.py +76 -0
  20. hackscan-0.1.0/src/hackscan/core/models.py +270 -0
  21. hackscan-0.1.0/src/hackscan/core/pipeline.py +164 -0
  22. hackscan-0.1.0/src/hackscan/core/redact.py +84 -0
  23. hackscan-0.1.0/src/hackscan/core/taxonomy.py +111 -0
  24. hackscan-0.1.0/src/hackscan/importers/__init__.py +1 -0
  25. hackscan-0.1.0/src/hackscan/importers/common.py +198 -0
  26. hackscan-0.1.0/src/hackscan/importers/gitleaks.py +104 -0
  27. hackscan-0.1.0/src/hackscan/importers/runner.py +161 -0
  28. hackscan-0.1.0/src/hackscan/importers/sarif.py +164 -0
  29. hackscan-0.1.0/src/hackscan/plugins/__init__.py +5 -0
  30. hackscan-0.1.0/src/hackscan/plugins/base.py +261 -0
  31. hackscan-0.1.0/src/hackscan/plugins/loader.py +98 -0
  32. hackscan-0.1.0/src/hackscan/plugins/rules/__init__.py +1 -0
  33. hackscan-0.1.0/src/hackscan/plugins/rules/cmdi.py +80 -0
  34. hackscan-0.1.0/src/hackscan/plugins/rules/codei.py +50 -0
  35. hackscan-0.1.0/src/hackscan/plugins/rules/sqli.py +127 -0
  36. hackscan-0.1.0/src/hackscan/plugins/rules/weak_crypto.py +53 -0
  37. hackscan-0.1.0/src/hackscan/plugins/scopes.py +263 -0
  38. hackscan-0.1.0/src/hackscan/sarif/__init__.py +1 -0
  39. hackscan-0.1.0/src/hackscan/sarif/generator.py +205 -0
  40. hackscan-0.1.0/src/hackscan/taint/__init__.py +1 -0
  41. hackscan-0.1.0/src/hackscan/taint/sanitizers.py +37 -0
  42. hackscan-0.1.0/src/hackscan/taint/sources.py +99 -0
  43. hackscan-0.1.0/tests/conftest.py +41 -0
  44. hackscan-0.1.0/tests/corpus/README.md +28 -0
  45. hackscan-0.1.0/tests/corpus/cmdi/safe/shell.py +44 -0
  46. hackscan-0.1.0/tests/corpus/cmdi/vulnerable/shell.py +51 -0
  47. hackscan-0.1.0/tests/corpus/cmdi/vulnerable/star_import.py +5 -0
  48. hackscan-0.1.0/tests/corpus/codei/safe/dynamic.py +29 -0
  49. hackscan-0.1.0/tests/corpus/codei/vulnerable/dynamic.py +36 -0
  50. hackscan-0.1.0/tests/corpus/frameworks/django/views.py +32 -0
  51. hackscan-0.1.0/tests/corpus/frameworks/fastapi/main.py +30 -0
  52. hackscan-0.1.0/tests/corpus/frameworks/flask/app.py +74 -0
  53. hackscan-0.1.0/tests/corpus/sqli/safe/dbapi.py +51 -0
  54. hackscan-0.1.0/tests/corpus/sqli/vulnerable/dbapi.py +86 -0
  55. hackscan-0.1.0/tests/corpus/weak_crypto/safe/hashes.py +17 -0
  56. hackscan-0.1.0/tests/corpus/weak_crypto/vulnerable/hashes.py +23 -0
  57. hackscan-0.1.0/tests/fixtures/imports/.gitkeep +0 -0
  58. hackscan-0.1.0/tests/fixtures/imports/README.md +13 -0
  59. hackscan-0.1.0/tests/fixtures/imports/bandit.sarif +251 -0
  60. hackscan-0.1.0/tests/fixtures/imports/codeql.sarif +52 -0
  61. hackscan-0.1.0/tests/fixtures/imports/gitleaks.json +22 -0
  62. hackscan-0.1.0/tests/fixtures/imports/project/app.py +24 -0
  63. hackscan-0.1.0/tests/fixtures/imports/semgrep.sarif +69 -0
  64. hackscan-0.1.0/tests/fixtures/sarif-schema-2.1.0.json +3389 -0
  65. hackscan-0.1.0/tests/test_ast_pass.py +243 -0
  66. hackscan-0.1.0/tests/test_cli.py +264 -0
  67. hackscan-0.1.0/tests/test_config.py +89 -0
  68. hackscan-0.1.0/tests/test_dedupe.py +300 -0
  69. hackscan-0.1.0/tests/test_fingerprint.py +72 -0
  70. hackscan-0.1.0/tests/test_importers.py +379 -0
  71. hackscan-0.1.0/tests/test_models.py +87 -0
  72. hackscan-0.1.0/tests/test_pipeline.py +56 -0
  73. hackscan-0.1.0/tests/test_rules.py +95 -0
  74. hackscan-0.1.0/tests/test_sarif.py +116 -0
  75. hackscan-0.1.0/tests/test_scopes.py +299 -0
  76. hackscan-0.1.0/tests/test_taint.py +618 -0
  77. hackscan-0.1.0/tests/test_taint_fuzz.py +179 -0
  78. hackscan-0.1.0/tests/test_taxonomy.py +47 -0
  79. hackscan-0.1.0/uv.lock +604 -0
@@ -0,0 +1,2 @@
1
+ * text=auto eol=lf
2
+ *.bat text eol=crlf
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ lint:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: astral-sh/setup-uv@v6
17
+ - run: uv run --group dev ruff check .
18
+ - run: uv run --group dev ruff format --check .
19
+
20
+ test:
21
+ runs-on: ${{ matrix.os }}
22
+ strategy:
23
+ fail-fast: false
24
+ matrix:
25
+ os: [ubuntu-latest]
26
+ python: ["3.10", "3.11", "3.12", "3.13"]
27
+ include:
28
+ - os: windows-latest
29
+ python: "3.12"
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ - uses: astral-sh/setup-uv@v6
33
+ with:
34
+ python-version: ${{ matrix.python }}
35
+ - run: uv run --group dev pytest
36
+
37
+ package:
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@v4
41
+ - uses: astral-sh/setup-uv@v6
42
+ - run: uv build
43
+ - name: Install the wheel in a clean environment and scan
44
+ run: |
45
+ uv venv /tmp/smoke
46
+ uv pip install --python /tmp/smoke dist/*.whl
47
+ /tmp/smoke/bin/hackscan --version
48
+ /tmp/smoke/bin/hackscan scan tests/corpus --format sarif -o /tmp/corpus.sarif
49
+ - name: Dogfood - HackScan scans its own source
50
+ run: /tmp/smoke/bin/hackscan scan src --fail-on high
@@ -0,0 +1,56 @@
1
+ name: Release
2
+
3
+ # Publishes to PyPI when a tag like v0.1.0 is pushed. Uses PyPI trusted publishing
4
+ # (OIDC): no API token is stored in the repository. One-time setup on pypi.org:
5
+ # add a "pending publisher" for project `hackscan`, owner/repo of this repository,
6
+ # workflow `release.yml`, environment `pypi`.
7
+
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ with:
21
+ fetch-depth: 0
22
+ - uses: astral-sh/setup-uv@v6
23
+ - name: Check the tagged commit is on main
24
+ run: |
25
+ git fetch origin main
26
+ git merge-base --is-ancestor "$GITHUB_SHA" origin/main || {
27
+ echo "tag ${GITHUB_REF_NAME} points to a commit that is not on main"; exit 1; }
28
+ - name: Check tag matches package version
29
+ run: |
30
+ version=$(uv run --no-project python -c "import re,pathlib;print(re.search(r'__version__ = \"(.+)\"', pathlib.Path('src/hackscan/__init__.py').read_text()).group(1))")
31
+ test "v${version}" = "${GITHUB_REF_NAME}" || { echo "tag ${GITHUB_REF_NAME} != v${version}"; exit 1; }
32
+ - run: uv run --group dev pytest
33
+ - run: uv build
34
+ - name: Smoke-test the wheel in a clean environment
35
+ run: |
36
+ uv venv /tmp/smoke
37
+ uv pip install --python /tmp/smoke dist/*.whl
38
+ /tmp/smoke/bin/hackscan --version
39
+ /tmp/smoke/bin/hackscan scan tests/corpus --format sarif -o /tmp/out.sarif
40
+ - uses: actions/upload-artifact@v4
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+
45
+ publish:
46
+ needs: build
47
+ runs-on: ubuntu-latest
48
+ environment: pypi
49
+ permissions:
50
+ id-token: write # trusted publishing
51
+ steps:
52
+ - uses: actions/download-artifact@v4
53
+ with:
54
+ name: dist
55
+ path: dist/
56
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ build/
5
+ dist/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ htmlcov/
11
+ *.sarif
12
+ !tests/fixtures/**/*.sarif
13
+ .hackscan-cache/
hackscan-0.1.0/LICENSE ADDED
@@ -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.
@@ -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,272 @@
1
+ # Project: HackScan (plan v2)
2
+
3
+ > v2 supersedes the Antigravity draft (`~/.gemini/antigravity/scratch/vulnhawk/PROJECT.md`).
4
+ > Changes driven by a Codex + Claude review on 2026-10-08: usable scanner first, LLM optional,
5
+ > reuse mature scanners as inputs/baselines instead of re-implementing them.
6
+
7
+ ## Positioning
8
+ HackScan is a Python SAST **orchestrator + verifier**, not a Semgrep/CodeQL competitor.
9
+ Value = (1) own AST+taint engine for core injection classes, (2) ingest & dedupe findings from
10
+ mature scanners, (3) taint-based confirmation, (4) optional local-LLM triage and fix suggestions,
11
+ (5) clean SARIF for GitHub Code Scanning.
12
+
13
+ ## Architecture
14
+ ```
15
+ own engine (AST + taint) ──┐
16
+ Semgrep CE SARIF (opt) ──┤
17
+ Gitleaks JSON (secrets) ──┼─► normalize → dedupe → taint-confirm → [LLM triage] → [fix gen] → CLI / SARIF
18
+ CodeQL SARIF (opt, BYO) ──┘
19
+ ```
20
+ 1. **Pass 1 – AST rules** (`analyzers/ast_pass`): candidate sinks per rule, plugin-based.
21
+ 2. **Pass 2 – Taint** (`analyzers/taint_pass`): intra-procedural in MVP; framework-aware sources
22
+ (Flask `request.*`, Django `request.GET/POST/body`, FastAPI handler params), rule-specific sanitizers.
23
+ 3. **Importers** (`importers/`): SARIF (Semgrep, CodeQL, Bandit) and Gitleaks JSON → `Finding`.
24
+ External tools are invoked only if present on PATH; never bundled.
25
+ 4. **Dedupe** (`core/dedupe`): merge findings sharing (file, region overlap, CWE); keep provenance list.
26
+ 5. **Pass 3 – LLM triage** (`analyzers/llm_pass`, optional): Ollama, structured JSON output, schema-validated.
27
+ 6. **Pass 4 – Remediation** (`analyzers/remediate`, optional): fixes as exact edits, validated by re-parse.
28
+ 7. **Output** (`cli`, `ui`, `sarif`): Click CLI, Rich output (plain mode for CI), SARIF 2.1.0.
29
+
30
+ ## Core Contract: `Finding` (schema_version = 1)
31
+ | Field | Type | Notes |
32
+ |---|---|---|
33
+ | `id` | str | Stable fingerprint: sha256(`vuln_class` + rel_path + enclosing function qualname + normalized sink expression). Vendor-independent, survives line shifts and importer order. |
34
+ | `vuln_class` | enum | Canonical class: `sqli`, `cmdi`, `codei`, `weak_crypto`, `secret`, `other:<cwe>`. Mapped from rule_id/CWE via `core/taxonomy.py`; unmappable imports get `other:<cwe or tool:rule>`. |
35
+ | `rule_id` | str | Primary rule, e.g. `HS-SQLI-001`, or `semgrep:<id>` for imported. All contributing rule ids kept in `related_rules`. |
36
+ | `cwe` | list[str] | Used for dedupe and SARIF taxa |
37
+ | `severity` | enum | critical/high/medium/low |
38
+ | `location` | Region | rel path, start/end line+col (1-based lines, 1-based cols per SARIF) |
39
+ | `snippet` | str | Display only; never used for identity |
40
+ | `sink` | str | Source text of the sink at `location`, read from the file by the producer; input to the fingerprint (falls back to `snippet` only if empty) |
41
+ | `sources` | list[str] | Provenance: `hackscan`, `semgrep`, `codeql`, `gitleaks`, ... |
42
+ | `status` | enum | `candidate`, `confirmed`, `suppressed` |
43
+ | `suppression` | Optional[str] | Reason: `taint:constant_input`, `taint:sanitized`, `llm:false_positive`, `inline:hackscan-ignore` |
44
+ | `confidence` | int 0-100 | Rule-specific; see policy below |
45
+ | `evidence` | list[Evidence] | Taint trace steps, LLM rationale (each tagged with producer) |
46
+ | `fix` | Optional[Fix] | `edits: list[{region, replacement}]` + derived unified diff; only if validated |
47
+
48
+ **Dedupe / merge policy** (deterministic, independent of importer order)
49
+ - Merge key: (`vuln_class`, rel_path, overlapping region — column-aware, `end_column` exclusive,
50
+ missing columns = whole line). Clustering is **not transitive** — invariant: every pair of findings
51
+ in a cluster overlaps. Findings are visited smallest region first (own engine breaks ties) and join
52
+ the first cluster in which they overlap every member, so two disjoint sinks can never be fused.
53
+ Findings sharing a source but recording different (normalized) sinks never merge — e.g. nested
54
+ `eval(eval(x))` — so a merge never reports fewer findings than any single source did.
55
+ - `sink` and `function` are derived from the parsed file by every producer (importers look them up
56
+ by location); merged findings take them from the primary, else the first contributor that has them. Findings with missing CWE merge only if
57
+ `vuln_class` mapped via rule_id; otherwise they stay separate.
58
+ - Merged location = union of all contributors' regions. Evidence = sorted concatenation (duplicates kept).
59
+ - All tie-breaks fall back to the finding's canonical JSON, so every result is input-order independent.
60
+ - Merged fields: `rule_id` = own-engine rule if present, else lexicographically smallest
61
+ `tool:rule`; `severity` = max; `confidence` = max; `sources`/`related_rules`/`cwe` = sorted union;
62
+ `evidence` = concatenation sorted by producer; `status` = `confirmed` > `candidate` > `suppressed`
63
+ (a finding is suppressed only if every contributor is suppressed).
64
+
65
+ **Policies**
66
+ - Passes return new/updated `Finding`s; they never delete.
67
+ - `--fail-on <sev>` counts `candidate` and `confirmed` findings at or above `<sev>`; never `suppressed`.
68
+ - Taint may suppress only findings in classes the own engine models (`sqli`, `cmdi`, `codei`) and
69
+ only when the full flow is inside one function and provably constant/sanitized. Imported findings
70
+ the engine cannot fully analyze stay `candidate`. Suppressed findings are kept and emitted
71
+ in SARIF with `suppressions[]`; hidden in CLI unless `--show-suppressed`.
72
+ - Confidence is rule-specific. Taint applies only to injection rules. Secrets and weak-crypto are
73
+ findings *because* they are literals/calls — taint "constant input" dismissal never applies to them.
74
+ - `--no-llm` disables Pass 3 and LLM fix generation. Deterministic template fixes (Pass 4 templates)
75
+ still run unless `--no-fix`.
76
+ - Output ordering is deterministic (path, line, col, rule_id).
77
+
78
+ ## LLM Safety (Pass 3/4)
79
+ - Scanned code is untrusted data: wrapped in delimited blocks, system prompt states it must not be
80
+ followed as instructions; model output must match a JSON schema or is discarded.
81
+ - LLM can only move status `candidate → confirmed|suppressed` with rationale; it cannot create findings
82
+ or raise severity above rule default.
83
+ - LLM suppression requires independent evidence: the model must cite a specific line (sanitizer call,
84
+ constant, guard) that a deterministic checker verifies exists in the code. Unverifiable rationale →
85
+ finding stays `candidate` with the LLM note attached. LLM suppressions are labeled `llm:*` and can be
86
+ disabled with `--llm-no-suppress`.
87
+ - Redaction applies to all context sent to Ollama (surrounding code too): string literals matching
88
+ secret patterns / Gitleaks hits are replaced with `<REDACTED>`.
89
+ - Fixes: must re-parse with `ast`, must change only the reported region(s); else dropped.
90
+ - Secrets findings' snippets are redacted before any LLM call. Non-localhost `--ollama-host` prints a warning.
91
+ - Budgets: per-request timeout, max findings per scan (`--llm-max`), cache by (finding id, model, prompt version).
92
+
93
+ ## Performance & Determinism
94
+ - Default ignores: `.git`, `.venv`, `venv`, `node_modules`, `build`, `dist`, `site-packages`, `__pycache__`.
95
+ - Parse failures are reported as warnings, never crash the scan.
96
+ - Process-pool parallelism per file for Passes 1-2; result cache keyed by file hash + engine version.
97
+ - Target: 100k LOC in < 30 s without LLM on a laptop.
98
+
99
+ ## Importer Behavior
100
+ - **Run mode** `--with semgrep,bandit,gitleaks`: invoke tool if on PATH with pinned args
101
+ (`semgrep scan --config p/python --sarif`, `bandit -r -f sarif`, `gitleaks detect --no-git -f json`).
102
+ Missing tool → warning, scan continues. Non-zero exit with valid output → accept; invalid/empty
103
+ output or timeout (`--tool-timeout`, default 300 s) → warning, that source skipped.
104
+ `--strict-tools` turns any tool failure into exit code 2.
105
+ - **BYO mode** `--import codeql=results.sarif --import sarif=other.sarif`: parse only, never execute.
106
+ - **Path normalization**: all locations resolved to POSIX paths relative to scan root; SARIF
107
+ `originalUriBaseIds`/`file://` URIs resolved; results outside the scan root are dropped with a warning.
108
+ - **Region normalization**: 1-based lines/cols; missing end → same as start; missing column → whole line.
109
+ - **Exit codes**: 0 = no failing findings, 1 = `--fail-on` threshold hit, 2 = usage/tool/internal error.
110
+ - **Tests**: one recorded fixture output per tool (Semgrep, Bandit, CodeQL, Gitleaks) in
111
+ `tests/fixtures/imports/`, plus malformed-input cases.
112
+
113
+ ## Feature Inventory (MVP = M1–M3)
114
+ | # | Feature | Milestone |
115
+ |---|---|---|
116
+ | 1 | Rules: SQL injection, command injection, code injection (`eval`/`exec`) | M1 |
117
+ | 2 | Rule: weak crypto (`md5`/`sha1` in security context) — low severity | M1 |
118
+ | 3 | Plugin ABC + loader (`--plugins <dir>`) | M1 |
119
+ | 4 | Intra-procedural taint + Flask/Django/FastAPI sources + sanitizers | M2 |
120
+ | 5 | Inline suppression `# hackscan: ignore[RULE]` | M2 |
121
+ | 6 | `hackscan scan <path>` with `--format text|json|sarif`, `--output` | M3 |
122
+ | 7 | `--severity`, `--min-confidence`, `--ignore`, `--fail-on`, `--quiet`, `--show-suppressed` | M3 |
123
+ | 8 | `.hackscan.yml` hierarchical config | M3 |
124
+ | 9 | SARIF 2.1.0: rules, locations, snippets, properties, suppressions; `fixes[]` emitted only when `Finding.fix` exists (populated from M4) ; schema-validated in tests | M3 |
125
+ | 10 | Importers (see Importer Behavior): `--with semgrep,bandit,gitleaks` runs tools; `--import <tool>=<file>` ingests BYO reports (CodeQL, any SARIF) | M3 |
126
+ | 11 | Dedupe across sources | M3 |
127
+ | 12 | Packaging (`pyproject.toml`, `hackscan` entry point), PyPI release via GitHub Actions trusted publishing | M3 |
128
+ | 13 | Ollama triage pass with graceful offline fallback | M4 |
129
+ | 14 | Template + LLM remediation as validated edits; diff view | M4 |
130
+ | 15 | Rich "Matrix" TUI (banner, spinners, tables); auto-off in CI / `--quiet` | M4 |
131
+ | 16 | Inter-procedural & cross-file taint (own call graph from `ast`) | M5 |
132
+ | 17 | Benchmark harness vs Bandit / Semgrep CE / CodeQL; published precision/recall | M5 |
133
+ | 18 | GitHub Action (uploads SARIF to Code Scanning), pre-commit hook | M5 |
134
+ | 19 | Auth bypass / IDOR heuristics (only if benchmark shows acceptable precision) | Later |
135
+ | 20 | Docker image | Later, on demand |
136
+
137
+ ## Milestones
138
+ | # | Name | Exit criteria | Depends |
139
+ |---|---|---|---|
140
+ | M0 ✅ | Skeleton & contracts | Repo, `pyproject`, CI (ruff + pytest on 3.10–3.13), `Finding` models, fixture corpus layout; **frozen** `Finding` schema v1, `taxonomy.py`, fingerprint + dedupe/merge implemented with order-independence tests; importer contract (above) documented | — |
141
+ | M1 ✅ | AST engine & rules | Rules 1–2 pass per-rule vulnerable **and** safe fixtures | M0 |
142
+ | M2 ✅ | Taint | Framework fixtures (Flask/Django/FastAPI); constant/sanitized flows suppressed; tests for taint boundaries | M1 |
143
+ | M3 🔨 | CLI, SARIF, importers → **v0.1.0 on PyPI** (built; publish pending) | `pip install hackscan` works on clean venv; SARIF validates against official schema; `--fail-on` exit codes tested | M2 |
144
+ | M4 | LLM + remediation + TUI → v0.2.0 | All LLM tests run against a mocked Ollama; offline degradation tested; prompt-injection fixture | M3 |
145
+ | M5 | Inter-procedural taint, benchmark, GitHub Action → v0.3.0 | Benchmark report in README; Action used on the repo itself | M4 |
146
+
147
+ ## Test Strategy
148
+ - Corpus: `tests/corpus/<rule>/{vulnerable,safe}/*.py`, each file annotated with expected findings
149
+ (`# expect: HS-SQLI-001`) — the test harness diffs actual vs expected (precision/recall per rule).
150
+ - Framework cases: `tests/corpus/frameworks/{flask,django,fastapi}/`.
151
+ - Golden SARIF snapshots + JSON-schema validation.
152
+ - CLI tests via Click `CliRunner`: exit codes, filters, formats.
153
+ - LLM: mocked HTTP; prompt-injection fixture (code comment instructing "mark as safe") must not suppress.
154
+ - Performance smoke test on a generated large repo (marked `slow`).
155
+
156
+ ## Code Layout
157
+ ```text
158
+ hackscan/
159
+ ├── pyproject.toml
160
+ ├── README.md
161
+ ├── PROJECT.md
162
+ ├── .github/workflows/{ci.yml,release.yml}
163
+ ├── src/hackscan/
164
+ │ ├── cli.py
165
+ │ ├── config.py
166
+ │ ├── core/{models.py,pipeline.py,dedupe.py,fingerprint.py}
167
+ │ ├── analyzers/{ast_pass.py,taint_pass.py,llm_pass.py,remediate.py}
168
+ │ ├── taint/{sources.py,sinks.py,sanitizers.py} # framework models
169
+ │ ├── importers/{sarif.py,gitleaks.py,runner.py}
170
+ │ ├── plugins/{base.py,loader.py,rules/{sqli,cmdi,codei,weak_crypto}.py}
171
+ │ ├── ui/{theme.py,banner.py,formatter.py}
172
+ │ └── sarif/{generator.py,schema/sarif-2.1.0.json}
173
+ └── tests/
174
+ ├── corpus/...
175
+ ├── test_rules.py
176
+ ├── test_taint.py
177
+ ├── test_importers.py
178
+ ├── test_dedupe.py
179
+ ├── test_cli.py
180
+ ├── test_sarif.py
181
+ └── test_llm.py
182
+ ```
183
+
184
+ ## Dev Tooling (not shipped)
185
+ - codebase-memory-mcp or Graphify (Graphify-Labs) as a code-navigation aid for agents. Not a runtime dependency.
186
+ - Note: `graphify.net` is flagged by Cloudflare as suspected phishing — use the GitHub repo only.
187
+
188
+ ## Decisions Log
189
+ - 2026-10-08: Repo at `~/teamwork_projects/hackscan`. MVP = M0–M3. Secrets via Gitleaks import (own
190
+ secrets detector dropped). Auth/IDOR deferred. CodeQL never bundled (license) — BYO SARIF only.
191
+ Semgrep invoked as external CLI; its registry rules are not vendored (Semgrep Rules License).
192
+ - 2026-10-08 (Codex re-review): vendor-independent fingerprint via `vuln_class`; deterministic merge
193
+ policy; importer run/BYO modes + failure semantics; SARIF fixes optional until M4; `--fail-on`
194
+ excludes suppressed; LLM suppression requires verifiable evidence; full-context redaction.
195
+ Verdict: ready for M0.
196
+ - 2026-10-08 (M0 impl): pipeline order is collect → `merge_findings` → `assign_ids` (ids are
197
+ computed on merged findings). Identical sinks in one function get an occurrence suffix (`#n`,
198
+ ordered by line), so inserting an identical sink earlier in the same function shifts later ids —
199
+ accepted trade-off. Fingerprint ids prefixed `hs1-` (bump if the recipe changes).
200
+ - 2026-10-08 (Codex M0 code review): anchor-based column-aware clustering (no transitive fusion),
201
+ `sink` field for fingerprints, union locations, evidence concatenation, canonical-JSON tie-breaks,
202
+ end-column validation. Round 2: precision-first anchors, union on effective ends, merged
203
+ findings keep a recorded `sink`; randomized order-independence test.
204
+ - 2026-10-08 (Codex M1 review): **rules report candidates, taint decides.** Rules no longer
205
+ dismiss findings via flow-insensitive assignment lookups (e.g. a constant assigned under
206
+ `if False`); every non-constant value at a sink is a candidate (lower confidence when
207
+ unresolved), and constant/sanitized flows are suppressed by the M2 taint pass. Corpus marks
208
+ these with `# expect-suppressed:`; the harness enforces the suppression once
209
+ `TAINT_PASS_ENABLED` is flipped in M2. Name resolution is now scope-aware
210
+ (`plugins/scopes.py`: function/class/global rules; decorators and defaults evaluate in the
211
+ enclosing scope). SQLi requires a DB-looking receiver or SQL text in the literal parts.
212
+ Plugins are type-validated; bad matches and declared source encodings are handled.
213
+ - 2026-10-08 (M2 impl): taint is flow-sensitive within a function — abstract interpretation
214
+ over statements (branch joins, loop fixpoint, try/except, match, walrus, comprehensions,
215
+ container mutators) with a `Taint(sources, unknown, safe_for)` lattice. Sinks inside
216
+ decorators/defaults, lambdas and class bodies are analyzed in their own scopes. Module-level
217
+ names bound only to constants (and never rebound via `global`) count as constants inside
218
+ functions — the one deliberate exception to "flow inside one function". Sources: Flask
219
+ `request.*`, `request`/`req` parameters (Django/DRF/Starlette/FastAPI), route-handler
220
+ parameters, `input()`, `sys.argv`, `sys.stdin`. Confirmed = +35 confidence with a source
221
+ trace. Inline suppression reason renamed to `inline:hackscan-ignore` (no spaces).
222
+ - 2026-10-08 (Codex M2 review): fixed five unsound suppressions. Taint values now track
223
+ mutability (a mutable object that is aliased, mutated, passed to an unmodeled call, stored
224
+ elsewhere or captured by a nested scope joins `unknown`), shell-quote context
225
+ (`shlex.quote` safety is dropped when the surrounding string may open a quote), and
226
+ statements are scanned in evaluation order (walrus/mutation/sink ordering). Module values
227
+ count as constants only if immutable. `ast.literal_eval` removed from sanitizers.
228
+ `self.request` (class-based views) is a source. Each case is a regression test that fails
229
+ on the previous engine.
230
+ - 2026-10-08 (Codex M2 re-review): `except`/`finally` see the join of every state the `try`
231
+ body passed through; aliases are tracked through calls/constructors (`dict(cmd=parts)`);
232
+ any transformation of a shell-quoted value (strip/replace/slicing/repr) drops quoting
233
+ safety. **Policy:** `shlex.quote` protects POSIX shells only (not Windows `cmd.exe`), and
234
+ the target platform is unknown, so POSIX-quoted commands are never suppressed; they stay
235
+ candidates at confidence 20 with an explanatory note. (A `--shell posix` option to restore
236
+ suppression can be added in M3 config.)
237
+ - 2026-10-08 (Codex M2 round 3): `break`/`continue` modeled (paths end at the jump; break
238
+ states join the loop exit and skip `else`), inner `finally` state propagates to outer
239
+ handlers. Added `tests/test_taint_fuzz.py`: differential soundness fuzzing — random
240
+ programs (branches, while/for with break/continue, try/except/finally, raises, early
241
+ returns, list aliasing, dict wrappers, closures, walrus) are executed with an instrumented
242
+ `os.system`; any marker reaching a sink the scanner suppressed fails the test. It catches
243
+ every engine version before this one; a 4,500-program stress run checked 6,743 suppressed
244
+ sinks with zero unsound results. M2 closed on this evidence.
245
+ - 2026-10-08: **Renamed VulnHawk → HackScan** (PyPI `vulnhawk` is taken by an unrelated
246
+ project). Package/CLI `hackscan`, rule ids `HS-*`, fingerprint prefix `hs1-`, inline
247
+ `# hackscan: ignore`, config `.hackscan.yml`, GitHub repo `hackscan`.
248
+ - 2026-10-08 (M3 impl): `hackscan scan` CLI (text/json/sarif, filters, `--fail-on`, exit
249
+ codes 0/1/2), hierarchical `.hackscan.yml`, process-pool analysis (serial below 16 files;
250
+ output identical either way), importers for SARIF (Semgrep/Bandit/CodeQL/any) and Gitleaks
251
+ with run/BYO modes, source-derived `sink`/`function` enrichment and secret redaction,
252
+ SARIF 2.1.0 export validated against the OASIS schema in tests. Bandit fixture is recorded
253
+ tool output; Semgrep/CodeQL/Gitleaks fixtures are hand-written to their documented formats
254
+ (tools unavailable locally). Release via tag-triggered trusted publishing (`release.yml`);
255
+ CI builds the wheel, installs it in a clean venv, and dogfoods `hackscan scan src`.
256
+ - 2026-10-08 (Codex M3 review + stop-gate): incomplete scans (unparseable/unreadable files,
257
+ crashing rules) exit 2 unless `--allow-incomplete`, and SARIF sets
258
+ `executionSuccessful: false`; secrets are redacted after merging from every field of every
259
+ finding (known Gitleaks values everywhere, all string literals for `secret`-class
260
+ findings); a start column without an end is a point (no rest-of-line widening);
261
+ imported findings obey default ignores; remote `file://host/` URIs are rejected;
262
+ `--output` never overwrites source files and creates parent directories; config keys are
263
+ an explicit allow-list; malformed Gitleaks entries are skipped with a warning; releases
264
+ require the tagged commit to be on `main`. SARIF URIs are repository-root-relative (GitHub
265
+ resolves them that way) and URI-escaped; `--sarif-omit-suppressed` for GitHub uploads,
266
+ since GitHub does not document `suppressions` support and only reads
267
+ `primaryLocationLineHash` (computed by `upload-sarif`).
268
+ - 2026-10-08 (Codex final release check): secret-class findings never carry tool text
269
+ (generic message/evidence; literals and assigned values redacted in snippet/sink);
270
+ known secrets scrubbed from warnings/errors; `-o` only overwrites previous HackScan
271
+ reports; failed `--import` reports make the scan incomplete (exit 2); `--with` tools
272
+ exiting with an error code are `partial` (warning, fails `--strict-tools`).