HackScan
A Python security scanner that verifies what it reports. HackScan finds injection sinks with its own AST engine, then runs a flow-sensitive taint analysis to decide, per finding, whether untrusted input really reaches it. It can also ingest Semgrep, Bandit, CodeQL and Gitleaks results, merge duplicates across tools, and emit one SARIF report.
$ hackscan scan app/
HIGH confirmed views.py:10:9 HS-SQLI-001 [100%]
SQL query is built with string formatting/concatenation.
| cursor.execute("SELECT * FROM sales WHERE year = " + year)
> Untrusted request data (`request.GET`) at line 8.
> Untrusted input reaches the sink.
Why another scanner?
| HackScan | |
|---|---|
| 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. |
| 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. |
| Framework-aware | Sources: Flask request.*, Django/DRF/Starlette/FastAPI request objects (incl. self.request), route-handler parameters, input(), sys.argv. |
| One report from many tools | Imports Semgrep, Bandit, CodeQL (SARIF) and Gitleaks; deduplicates by vulnerability class and location, keeping every tool as provenance. |
| 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. |
Install
pip install hackscan # or: uv tool install hackscan / pipx install hackscan
Python 3.10–3.13, any OS. No network access and no LLM needed.
Usage
hackscan scan . # text report
hackscan scan . --format sarif -o hackscan.sarif # for GitHub code scanning
hackscan scan . --fail-on high # exit 1 on open high/critical findings
hackscan scan . --format json --show-suppressed # everything, machine-readable
hackscan scan . --with bandit,semgrep,gitleaks # also run these tools if installed
hackscan scan . --import codeql=results.sarif # merge a report you already have
hackscan rules # list rules
| Option | Meaning |
|---|---|
--severity, --min-confidence |
Report filters. |
--fail-on SEV |
Exit 1 if an open (candidate/confirmed) finding is at least SEV. Suppressed findings never count. |
--ignore GLOB |
Skip paths (tests/*, **/migrations/**, legacy). .venv, node_modules, build, ... are always skipped. |
--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. |
-o FILE |
Write the report to FILE. Never overwrites anything except a previous HackScan report. |
--plugins DIR |
Load custom rules (see below). |
--allow-incomplete |
Do not exit 2 when some files cannot be analyzed (they are still listed). |
--no-taint |
Pattern matching only. |
--jobs N |
Worker processes (default: automatic). |
Exit codes: 0 OK, 1 --fail-on threshold reached, 2 usage/config/plugin error or
an incomplete scan (a file could not be parsed or read, or an --import report
could not be loaded; use --allow-incomplete to accept). A --with tool that is missing
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
executionSuccessful: false.
Configuration
.hackscan.yml files are discovered from the filesystem root down to the scanned
directory and merged (nearest wins; ignore lists accumulate). CLI options override them.
ignore: [tests/*, "**/migrations/**"]
severity: medium
min-confidence: 40
fail-on: high
with: [bandit]
import:
codeql: codeql-results.sarif
plugins: security/rules
Suppressing a finding
os.system(cmd) # hackscan: ignore[HS-CMDI-001]
Suppressed findings stay in SARIF output (as suppressions) so they remain auditable;
use --sarif-omit-suppressed when uploading to GitHub, which does not honor them.
Secrets are never echoed: findings of class secret (from any tool) have generic
messages and redacted snippets, and values reported by Gitleaks are scrubbed from all
output, including warnings.
Rules
| Rule | Detects | Severity |
|---|---|---|
HS-SQLI-001 |
Non-constant SQL reaching DB-API execute*, Django raw, SQLAlchemy text, pandas read_sql |
high |
HS-CMDI-001 |
Non-constant commands reaching os.system, os.popen, subprocess.*(shell=True), ... |
high |
HS-CODEI-001 |
Non-constant input to eval, exec, compile |
high |
HS-CRYPTO-001 |
MD5/SHA-1 (unless usedforsecurity=False) |
low |
Sanitizers recognized: numeric/UUID conversions (all classes), psycopg sql.Identifier
/sql.Literal (SQL). shlex.quote is POSIX-only, so quoted commands are kept as
low-confidence candidates rather than suppressed.
Custom rules
# security/rules/pickle_rule.py
from hackscan.core.models import Severity
from hackscan.plugins import Match, RulePlugin
class PickleLoads(RulePlugin):
rule_id = "ACME-PICKLE-001" # the HS- prefix is reserved
name = "pickle-loads"
description = "pickle.loads on untrusted data"
severity = Severity.HIGH
cwe = ("CWE-502",)
def check(self, node, ctx):
if "pickle.loads" in ctx.call_names(node):
yield Match(node, "pickle.loads call")
GitHub Actions
- run: pipx install hackscan
- run: hackscan scan . --format sarif --sarif-omit-suppressed -o hackscan.sarif --fail-on high
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: hackscan.sarif
Limitations
- Taint analysis is intra-procedural: values arriving through function parameters are candidates, not confirmations (inter-procedural analysis is on the roadmap).
- Python only. Imported tools may cover other languages; their findings are passed through.
Development
uv sync --group dev
uv run pytest
uv run ruff check . && uv run ruff format --check .
See PROJECT.md for the design, milestones and decision log.
License
MIT
Metadata
Release files for hackscan 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hackscan-0.1.0.tar.gz | 148.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| hackscan-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 214.8 kB
Release files / hackscan-0.1.0.tar.gz
| Download URL | hackscan-0.1.0.tar.gz |
|---|---|
| Size | 148.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
92866cece75e9918a5851dcfef0707cc771575a32de27829884558963fd2eed1
|
|
BLAKE2b-256 checksum How to use checksums |
6f651d6e52455c2e7ce78b701aa46ba46028418b5ed87c87d77522a71b40a970
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logRelease files / hackscan-0.1.0-py3-none-any.whl
| Download URL | hackscan-0.1.0-py3-none-any.whl |
|---|---|
| Size | 66.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
688ce6fbed8009a19720e03a8ec3f2f1871701d552b636fd7a271970cae613cd
|
|
BLAKE2b-256 checksum How to use checksums |
9606c2dbdcf88f08b505819bb90afbc38d8dcb6c0c426b369d5810a3ea433e10
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency log