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.
- hackscan-0.1.0/.gitattributes +2 -0
- hackscan-0.1.0/.github/workflows/ci.yml +50 -0
- hackscan-0.1.0/.github/workflows/release.yml +56 -0
- hackscan-0.1.0/.gitignore +13 -0
- hackscan-0.1.0/LICENSE +21 -0
- hackscan-0.1.0/PKG-INFO +181 -0
- hackscan-0.1.0/PROJECT.md +272 -0
- hackscan-0.1.0/README.md +156 -0
- hackscan-0.1.0/pyproject.toml +62 -0
- hackscan-0.1.0/src/hackscan/__init__.py +3 -0
- hackscan-0.1.0/src/hackscan/analyzers/__init__.py +1 -0
- hackscan-0.1.0/src/hackscan/analyzers/ast_pass.py +154 -0
- hackscan-0.1.0/src/hackscan/analyzers/suppressions.py +63 -0
- hackscan-0.1.0/src/hackscan/analyzers/taint_pass.py +808 -0
- hackscan-0.1.0/src/hackscan/cli.py +326 -0
- hackscan-0.1.0/src/hackscan/config.py +184 -0
- hackscan-0.1.0/src/hackscan/core/__init__.py +1 -0
- hackscan-0.1.0/src/hackscan/core/dedupe.py +147 -0
- hackscan-0.1.0/src/hackscan/core/fingerprint.py +76 -0
- hackscan-0.1.0/src/hackscan/core/models.py +270 -0
- hackscan-0.1.0/src/hackscan/core/pipeline.py +164 -0
- hackscan-0.1.0/src/hackscan/core/redact.py +84 -0
- hackscan-0.1.0/src/hackscan/core/taxonomy.py +111 -0
- hackscan-0.1.0/src/hackscan/importers/__init__.py +1 -0
- hackscan-0.1.0/src/hackscan/importers/common.py +198 -0
- hackscan-0.1.0/src/hackscan/importers/gitleaks.py +104 -0
- hackscan-0.1.0/src/hackscan/importers/runner.py +161 -0
- hackscan-0.1.0/src/hackscan/importers/sarif.py +164 -0
- hackscan-0.1.0/src/hackscan/plugins/__init__.py +5 -0
- hackscan-0.1.0/src/hackscan/plugins/base.py +261 -0
- hackscan-0.1.0/src/hackscan/plugins/loader.py +98 -0
- hackscan-0.1.0/src/hackscan/plugins/rules/__init__.py +1 -0
- hackscan-0.1.0/src/hackscan/plugins/rules/cmdi.py +80 -0
- hackscan-0.1.0/src/hackscan/plugins/rules/codei.py +50 -0
- hackscan-0.1.0/src/hackscan/plugins/rules/sqli.py +127 -0
- hackscan-0.1.0/src/hackscan/plugins/rules/weak_crypto.py +53 -0
- hackscan-0.1.0/src/hackscan/plugins/scopes.py +263 -0
- hackscan-0.1.0/src/hackscan/sarif/__init__.py +1 -0
- hackscan-0.1.0/src/hackscan/sarif/generator.py +205 -0
- hackscan-0.1.0/src/hackscan/taint/__init__.py +1 -0
- hackscan-0.1.0/src/hackscan/taint/sanitizers.py +37 -0
- hackscan-0.1.0/src/hackscan/taint/sources.py +99 -0
- hackscan-0.1.0/tests/conftest.py +41 -0
- hackscan-0.1.0/tests/corpus/README.md +28 -0
- hackscan-0.1.0/tests/corpus/cmdi/safe/shell.py +44 -0
- hackscan-0.1.0/tests/corpus/cmdi/vulnerable/shell.py +51 -0
- hackscan-0.1.0/tests/corpus/cmdi/vulnerable/star_import.py +5 -0
- hackscan-0.1.0/tests/corpus/codei/safe/dynamic.py +29 -0
- hackscan-0.1.0/tests/corpus/codei/vulnerable/dynamic.py +36 -0
- hackscan-0.1.0/tests/corpus/frameworks/django/views.py +32 -0
- hackscan-0.1.0/tests/corpus/frameworks/fastapi/main.py +30 -0
- hackscan-0.1.0/tests/corpus/frameworks/flask/app.py +74 -0
- hackscan-0.1.0/tests/corpus/sqli/safe/dbapi.py +51 -0
- hackscan-0.1.0/tests/corpus/sqli/vulnerable/dbapi.py +86 -0
- hackscan-0.1.0/tests/corpus/weak_crypto/safe/hashes.py +17 -0
- hackscan-0.1.0/tests/corpus/weak_crypto/vulnerable/hashes.py +23 -0
- hackscan-0.1.0/tests/fixtures/imports/.gitkeep +0 -0
- hackscan-0.1.0/tests/fixtures/imports/README.md +13 -0
- hackscan-0.1.0/tests/fixtures/imports/bandit.sarif +251 -0
- hackscan-0.1.0/tests/fixtures/imports/codeql.sarif +52 -0
- hackscan-0.1.0/tests/fixtures/imports/gitleaks.json +22 -0
- hackscan-0.1.0/tests/fixtures/imports/project/app.py +24 -0
- hackscan-0.1.0/tests/fixtures/imports/semgrep.sarif +69 -0
- hackscan-0.1.0/tests/fixtures/sarif-schema-2.1.0.json +3389 -0
- hackscan-0.1.0/tests/test_ast_pass.py +243 -0
- hackscan-0.1.0/tests/test_cli.py +264 -0
- hackscan-0.1.0/tests/test_config.py +89 -0
- hackscan-0.1.0/tests/test_dedupe.py +300 -0
- hackscan-0.1.0/tests/test_fingerprint.py +72 -0
- hackscan-0.1.0/tests/test_importers.py +379 -0
- hackscan-0.1.0/tests/test_models.py +87 -0
- hackscan-0.1.0/tests/test_pipeline.py +56 -0
- hackscan-0.1.0/tests/test_rules.py +95 -0
- hackscan-0.1.0/tests/test_sarif.py +116 -0
- hackscan-0.1.0/tests/test_scopes.py +299 -0
- hackscan-0.1.0/tests/test_taint.py +618 -0
- hackscan-0.1.0/tests/test_taint_fuzz.py +179 -0
- hackscan-0.1.0/tests/test_taxonomy.py +47 -0
- hackscan-0.1.0/uv.lock +604 -0
|
@@ -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
|
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.
|
hackscan-0.1.0/PKG-INFO
ADDED
|
@@ -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`).
|