hackscan 0.3.1__tar.gz → 0.3.2__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.3.1 → hackscan-0.3.2}/PKG-INFO +28 -7
- {hackscan-0.3.1 → hackscan-0.3.2}/PROJECT.md +27 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/README.md +27 -6
- {hackscan-0.3.1 → hackscan-0.3.2}/pyproject.toml +1 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/__init__.py +1 -1
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/analyzers/llm_pass.py +19 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/analyzers/remediate.py +2 -17
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/analyzers/taint_pass.py +534 -36
- hackscan-0.3.2/src/hackscan/taint/sanitizers.py +81 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_llm.py +49 -0
- hackscan-0.3.2/tests/test_quoting_credit.py +926 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_taint.py +3 -1
- hackscan-0.3.1/src/hackscan/taint/sanitizers.py +0 -37
- {hackscan-0.3.1 → hackscan-0.3.2}/.gitattributes +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/.github/workflows/ci.yml +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/.github/workflows/hackscan.yml +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/.github/workflows/release.yml +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/.gitignore +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/.pre-commit-hooks.yaml +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/LICENSE +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/action.yml +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/APPS.json +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/APPS.md +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/README.md +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/RESULTS.json +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/RESULTS.md +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/apps.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/apps_review.json +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/run.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/cmdi/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/cmdi/backup.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/cmdi/ops_tool.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/cmdi/scheduler.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/cmdi/web_tools.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/codei/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/codei/calculator.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/codei/plugins.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/sqli/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/sqli/api.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/sqli/django_views.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/sqli/flask_reports.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/sqli/repository.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/sqli/urls.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/weak_crypto/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/benchmarks/suite/weak_crypto/accounts.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/analyzers/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/analyzers/ast_pass.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/analyzers/interproc.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/analyzers/suppressions.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/cli.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/config.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/core/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/core/dedupe.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/core/fingerprint.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/core/models.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/core/pipeline.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/core/redact.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/core/taxonomy.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/importers/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/importers/common.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/importers/gitleaks.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/importers/runner.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/importers/sarif.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/plugins/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/plugins/base.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/plugins/loader.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/plugins/rules/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/plugins/rules/cmdi.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/plugins/rules/codei.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/plugins/rules/sqli.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/plugins/rules/weak_crypto.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/plugins/scopes.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/sarif/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/sarif/generator.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/taint/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/taint/sources.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/ui/__init__.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/src/hackscan/ui/render.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/conftest.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/README.md +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/cmdi/safe/shell.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/cmdi/vulnerable/shell.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/cmdi/vulnerable/star_import.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/codei/safe/dynamic.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/codei/vulnerable/dynamic.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/frameworks/django/views.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/frameworks/fastapi/main.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/frameworks/flask/app.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/sqli/safe/dbapi.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/sqli/vulnerable/dbapi.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/weak_crypto/safe/hashes.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/corpus/weak_crypto/vulnerable/hashes.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/fixtures/imports/.gitkeep +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/fixtures/imports/README.md +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/fixtures/imports/bandit.sarif +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/fixtures/imports/codeql.sarif +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/fixtures/imports/gitleaks.json +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/fixtures/imports/project/app.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/fixtures/imports/semgrep.sarif +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/fixtures/sarif-schema-2.1.0.json +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_action.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_ast_pass.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_benchmark.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_cli.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_config.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_dedupe.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_fingerprint.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_importers.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_interproc.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_models.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_pipeline.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_redact.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_remediate.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_rules.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_sarif.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_scopes.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_taint_fuzz.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_taxonomy.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/tests/test_ui.py +0 -0
- {hackscan-0.3.1 → hackscan-0.3.2}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: hackscan
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Python SAST orchestrator: own AST + taint engine, imports Semgrep/Bandit/CodeQL/Gitleaks, dedupes, emits SARIF.
|
|
5
5
|
Project-URL: Source, https://github.com/kahledawad2019-netizen/hackscan
|
|
6
6
|
Project-URL: Issues, https://github.com/kahledawad2019-netizen/hackscan/issues
|
|
@@ -202,7 +202,13 @@ HackScan's authors, so treat both tables as a sanity check rather than a ranking
|
|
|
202
202
|
|
|
203
203
|
Sanitizers recognized: numeric/UUID conversions (all classes), `psycopg` `sql.Identifier`
|
|
204
204
|
/`sql.Literal` (SQL). `shlex.quote` is POSIX-only, so quoted commands are kept as
|
|
205
|
-
low-confidence candidates rather than suppressed.
|
|
205
|
+
low-confidence candidates rather than suppressed. That verdict needs the command to start with
|
|
206
|
+
a literal name from the same list of inert programs that command fixes use (plus `echo`, but
|
|
207
|
+
not `printf`, whose shell builtin can evaluate a variable name; not `sh -c`, `ssh`, `xargs`,
|
|
208
|
+
`tar`, interpreters, ...) and command text without shell syntax that starts or groups commands
|
|
209
|
+
(`;`, `|`, `&`, `$(...)`, redirections, braces): a quoted value in the program position, or a
|
|
210
|
+
program name assembled at runtime, gets none; nor does a call that may pick another shell
|
|
211
|
+
(`executable=`, options passed with `**`).
|
|
206
212
|
|
|
207
213
|
### Custom rules
|
|
208
214
|
|
|
@@ -241,7 +247,7 @@ jobs:
|
|
|
241
247
|
runs-on: ubuntu-latest
|
|
242
248
|
steps:
|
|
243
249
|
- uses: actions/checkout@v4
|
|
244
|
-
- uses: kahledawad2019-netizen/hackscan@v0.3.
|
|
250
|
+
- uses: kahledawad2019-netizen/hackscan@v0.3.2
|
|
245
251
|
with:
|
|
246
252
|
path: src
|
|
247
253
|
fail-on: high
|
|
@@ -273,7 +279,7 @@ Python 3.12 on the runner. Without the action:
|
|
|
273
279
|
```yaml
|
|
274
280
|
repos:
|
|
275
281
|
- repo: https://github.com/kahledawad2019-netizen/hackscan
|
|
276
|
-
rev: v0.3.
|
|
282
|
+
rev: v0.3.2
|
|
277
283
|
hooks:
|
|
278
284
|
- id: hackscan
|
|
279
285
|
```
|
|
@@ -298,15 +304,30 @@ apply as usual.
|
|
|
298
304
|
confirmed or left as candidates: an assignment expression can rebind a name in the
|
|
299
305
|
middle of an expression, which the statement-level analysis does not order. Sinks in
|
|
300
306
|
the body of a generator expression are never suppressed either (it runs later), nor
|
|
301
|
-
are values produced by one
|
|
302
|
-
|
|
303
|
-
`exec`, `eval`, `__dict__`,
|
|
307
|
+
are values produced by one, except by a generator that `str.join` consumes right away.
|
|
308
|
+
Module-level constants are trusted only when bound exactly once and the module never
|
|
309
|
+
writes its namespace dynamically (`globals()`, `vars()`, `exec`, `eval`, `__dict__`,
|
|
310
|
+
star imports); `global`/`nonlocal` names are never trusted.
|
|
304
311
|
- Mutable objects (lists, dicts, sets) are tracked by variable name. Mutation through
|
|
305
312
|
direct aliases, method calls (`d.get(k).append(x)`), bound methods
|
|
306
313
|
(`add = parts.append`), conditional receivers and comprehension items is modeled;
|
|
307
314
|
aliases created in less direct ways (stored in another object's attribute or in a
|
|
308
315
|
container and mutated through it later, returned from a helper, reached through
|
|
309
316
|
`getattr`) may be missed, so a constant suppression involving such code deserves review.
|
|
317
|
+
- A sanitizer counts only through a name that cannot be something else in the same file:
|
|
318
|
+
one import or definition, no assignment to an attribute of that name, no `setattr` /
|
|
319
|
+
`delattr`. Replacing a sanitizer at runtime in other ways (from another file, or in the
|
|
320
|
+
same file through an alias of `setattr`, `__setattr__`, `__dict__` writes and the like)
|
|
321
|
+
is not modeled.
|
|
322
|
+
- Sanitizer credit computed in one function is reused by its callers and callees, so a
|
|
323
|
+
transformation in another function that undoes it (slicing a shell-quoted argument,
|
|
324
|
+
for example) can leave such a finding a candidate instead of confirming it.
|
|
325
|
+
- Numbers from `int()`, `float()`, `len()` and similar count as safe in SQL text, and SQL
|
|
326
|
+
token boundaries are not modeled: text whose structure a runtime number decides is not
|
|
327
|
+
detected, such as a `-` right before a possibly negative number (`"n -" + str(int(x))`
|
|
328
|
+
can form a `--` comment), a fragment repeated a runtime number of times (`"-" * int(x)`),
|
|
329
|
+
or a separator repeated between a runtime number of items. Bound query parameters avoid
|
|
330
|
+
all of these.
|
|
310
331
|
- Python only. Imported tools may cover other languages; their findings are passed through.
|
|
311
332
|
- Fix suggestions are deliberately conservative: many vulnerable lines get none. Command
|
|
312
333
|
fixes (argument lists) are only offered for programs whose arguments are inert data
|
|
@@ -459,3 +459,30 @@ hackscan/
|
|
|
459
459
|
fields, positional args used exactly once; never for command argv, where a non-str value would
|
|
460
460
|
crash). Detection is unchanged, so the benchmark tables still show the 0.3.0 measurement (Bandit,
|
|
461
461
|
Semgrep and CodeQL are not installed on the release machine to re-run every row).
|
|
462
|
+
- 2026-10-13 (v0.3.2): soundness fixes found while reviewing an attempt to credit
|
|
463
|
+
datasette's `escape_sqlite` as an SQL identifier sanitizer. That attempt was dropped (Claude + Codex,
|
|
464
|
+
user-confirmed): the helper leaves words missing from its own reserved list bare (`returning`, `true`),
|
|
465
|
+
so it is no sound sanitizer. The review rounds instead found sanitizer-credit bugs that shipped in 0.3.1,
|
|
466
|
+
fixed here (tests/test_quoting_credit.py): shell-quoting credit is allow-listed (it survives only whole
|
|
467
|
+
insertion into plain text; containers, iteration, unpacking, indexing, methods, `!r`/padding, `%c`,
|
|
468
|
+
bytes and decoding drop it); text that may render quotes (bytes, containers, attributes, method results,
|
|
469
|
+
repr-like builtins, sliced quoting output, backslashes) is not quote-free; sanitizer names must be
|
|
470
|
+
stable (one binding, no attribute assignment, no `setattr`) in taint and LLM evidence; number credit
|
|
471
|
+
does not survive conversion to characters (methods other than case/strip/zfill, `%c`/`{:c}`, fill
|
|
472
|
+
specs, unknown templates, `format`/`bytes`/`bytearray`) or attribute access (`uuid.UUID(x).bytes`);
|
|
473
|
+
psycopg `sql.Identifier`/`sql.Literal` keep credit only while passed on as is (also not across calls);
|
|
474
|
+
`.format` field selectors and `*args`/`**kwargs` mappings; constant text indexed at runtime is no
|
|
475
|
+
constant; iteration items are no containers; `abs`/`round` pass their argument's credit through
|
|
476
|
+
(their dunders may return anything) instead of sanitizing; LLM sanitizer evidence is checked with the
|
|
477
|
+
taint engine itself (the credit must survive the sink's use of the value); the POSIX quoting verdict
|
|
478
|
+
needs the command to start with a literal inert program (the fix generator's list, now shared in
|
|
479
|
+
`taint/sanitizers.py`, plus `echo`, minus `printf`: bash's builtin evaluates a `-v` variable name)
|
|
480
|
+
and no shell syntax that starts or groups commands (user choice: allow-list, not a denylist of code
|
|
481
|
+
runners), and a sink that cannot pick another shell (`executable=`, Popen's positional executable,
|
|
482
|
+
`**` options). User decisions: in-file sanitizer replacement beyond the simple checks, credit
|
|
483
|
+
inherited across calls (missed confirmations only) and numbers/constants shaping SQL text
|
|
484
|
+
structure (sign next to `-`, runtime repetition) are documented README limitations (the last
|
|
485
|
+
pinned by strict-xfail tests). Codex review: 13 rounds, final "HS OK". The security agent's
|
|
486
|
+
held-out eval is identical to 0.3.1 on every finding, and the benchmark's HackScan numbers are
|
|
487
|
+
unchanged (`test_published_hackscan_numbers_are_current`), so the tables keep the 0.3.0
|
|
488
|
+
measurement (Bandit, Semgrep and CodeQL are not installed here to re-run every row).
|
|
@@ -176,7 +176,13 @@ HackScan's authors, so treat both tables as a sanity check rather than a ranking
|
|
|
176
176
|
|
|
177
177
|
Sanitizers recognized: numeric/UUID conversions (all classes), `psycopg` `sql.Identifier`
|
|
178
178
|
/`sql.Literal` (SQL). `shlex.quote` is POSIX-only, so quoted commands are kept as
|
|
179
|
-
low-confidence candidates rather than suppressed.
|
|
179
|
+
low-confidence candidates rather than suppressed. That verdict needs the command to start with
|
|
180
|
+
a literal name from the same list of inert programs that command fixes use (plus `echo`, but
|
|
181
|
+
not `printf`, whose shell builtin can evaluate a variable name; not `sh -c`, `ssh`, `xargs`,
|
|
182
|
+
`tar`, interpreters, ...) and command text without shell syntax that starts or groups commands
|
|
183
|
+
(`;`, `|`, `&`, `$(...)`, redirections, braces): a quoted value in the program position, or a
|
|
184
|
+
program name assembled at runtime, gets none; nor does a call that may pick another shell
|
|
185
|
+
(`executable=`, options passed with `**`).
|
|
180
186
|
|
|
181
187
|
### Custom rules
|
|
182
188
|
|
|
@@ -215,7 +221,7 @@ jobs:
|
|
|
215
221
|
runs-on: ubuntu-latest
|
|
216
222
|
steps:
|
|
217
223
|
- uses: actions/checkout@v4
|
|
218
|
-
- uses: kahledawad2019-netizen/hackscan@v0.3.
|
|
224
|
+
- uses: kahledawad2019-netizen/hackscan@v0.3.2
|
|
219
225
|
with:
|
|
220
226
|
path: src
|
|
221
227
|
fail-on: high
|
|
@@ -247,7 +253,7 @@ Python 3.12 on the runner. Without the action:
|
|
|
247
253
|
```yaml
|
|
248
254
|
repos:
|
|
249
255
|
- repo: https://github.com/kahledawad2019-netizen/hackscan
|
|
250
|
-
rev: v0.3.
|
|
256
|
+
rev: v0.3.2
|
|
251
257
|
hooks:
|
|
252
258
|
- id: hackscan
|
|
253
259
|
```
|
|
@@ -272,15 +278,30 @@ apply as usual.
|
|
|
272
278
|
confirmed or left as candidates: an assignment expression can rebind a name in the
|
|
273
279
|
middle of an expression, which the statement-level analysis does not order. Sinks in
|
|
274
280
|
the body of a generator expression are never suppressed either (it runs later), nor
|
|
275
|
-
are values produced by one
|
|
276
|
-
|
|
277
|
-
`exec`, `eval`, `__dict__`,
|
|
281
|
+
are values produced by one, except by a generator that `str.join` consumes right away.
|
|
282
|
+
Module-level constants are trusted only when bound exactly once and the module never
|
|
283
|
+
writes its namespace dynamically (`globals()`, `vars()`, `exec`, `eval`, `__dict__`,
|
|
284
|
+
star imports); `global`/`nonlocal` names are never trusted.
|
|
278
285
|
- Mutable objects (lists, dicts, sets) are tracked by variable name. Mutation through
|
|
279
286
|
direct aliases, method calls (`d.get(k).append(x)`), bound methods
|
|
280
287
|
(`add = parts.append`), conditional receivers and comprehension items is modeled;
|
|
281
288
|
aliases created in less direct ways (stored in another object's attribute or in a
|
|
282
289
|
container and mutated through it later, returned from a helper, reached through
|
|
283
290
|
`getattr`) may be missed, so a constant suppression involving such code deserves review.
|
|
291
|
+
- A sanitizer counts only through a name that cannot be something else in the same file:
|
|
292
|
+
one import or definition, no assignment to an attribute of that name, no `setattr` /
|
|
293
|
+
`delattr`. Replacing a sanitizer at runtime in other ways (from another file, or in the
|
|
294
|
+
same file through an alias of `setattr`, `__setattr__`, `__dict__` writes and the like)
|
|
295
|
+
is not modeled.
|
|
296
|
+
- Sanitizer credit computed in one function is reused by its callers and callees, so a
|
|
297
|
+
transformation in another function that undoes it (slicing a shell-quoted argument,
|
|
298
|
+
for example) can leave such a finding a candidate instead of confirming it.
|
|
299
|
+
- Numbers from `int()`, `float()`, `len()` and similar count as safe in SQL text, and SQL
|
|
300
|
+
token boundaries are not modeled: text whose structure a runtime number decides is not
|
|
301
|
+
detected, such as a `-` right before a possibly negative number (`"n -" + str(int(x))`
|
|
302
|
+
can form a `--` comment), a fragment repeated a runtime number of times (`"-" * int(x)`),
|
|
303
|
+
or a separator repeated between a runtime number of items. Bound query parameters avoid
|
|
304
|
+
all of these.
|
|
284
305
|
- Python only. Imported tools may cover other languages; their findings are passed through.
|
|
285
306
|
- Fix suggestions are deliberately conservative: many vulnerable lines get none. Command
|
|
286
307
|
fixes (argument lists) are only offered for programs whose arguments are inert data
|
|
@@ -386,6 +386,7 @@ def verify_evidence(ctx: FileContext, finding: Finding, line: int, kind: str) ->
|
|
|
386
386
|
_is_constant_value(stmt.value)
|
|
387
387
|
if kind == "constant"
|
|
388
388
|
else _is_class_sanitizer(stmt.value, ctx, finding.vuln_class)
|
|
389
|
+
and _credit_reaches_sink(ctx, func, stmt, call, finding.vuln_class)
|
|
389
390
|
)
|
|
390
391
|
covered = {target.id}
|
|
391
392
|
elif kind == "guard":
|
|
@@ -404,6 +405,21 @@ def verify_evidence(ctx: FileContext, finding: Finding, line: int, kind: str) ->
|
|
|
404
405
|
return ok and not _rebound_after(func, covered, stmt)
|
|
405
406
|
|
|
406
407
|
|
|
408
|
+
def _credit_reaches_sink(
|
|
409
|
+
ctx: FileContext, func: ast.AST, stmt: ast.Assign, call: ast.Call, vuln_class: str
|
|
410
|
+
) -> bool:
|
|
411
|
+
"""Whether the cited sanitizer's credit survives the sink's use of the value, by the
|
|
412
|
+
taint engine's own rules (`"%c" % n` or an f-string of a psycopg object drop it)."""
|
|
413
|
+
from hackscan.analyzers.taint_pass import _FileEngine, _FunctionEngine, combine, source
|
|
414
|
+
|
|
415
|
+
engine = _FunctionEngine(_FileEngine(ctx, {}), func, frozenset())
|
|
416
|
+
name = stmt.targets[0].id # type: ignore[attr-defined]
|
|
417
|
+
value = engine.eval(stmt.value, {name: source("untrusted input", stmt)})
|
|
418
|
+
args = [*call.args, *(k.value for k in call.keywords)]
|
|
419
|
+
result = combine(engine.eval(a, {name: value}) for a in args)
|
|
420
|
+
return vuln_class in result.safe_for
|
|
421
|
+
|
|
422
|
+
|
|
407
423
|
def _sink_value_names(call: ast.Call) -> set[str] | None:
|
|
408
424
|
"""Every name the sink's arguments read, or None if they contain anything else that
|
|
409
425
|
could carry data (calls, attributes, subscripts...). A suppression must vouch for
|
|
@@ -500,10 +516,13 @@ def _is_constant_value(expr: ast.AST) -> bool:
|
|
|
500
516
|
|
|
501
517
|
|
|
502
518
|
def _is_class_sanitizer(expr: ast.AST, ctx: FileContext, vuln_class: str) -> bool:
|
|
519
|
+
from hackscan.analyzers.taint_pass import _FileEngine
|
|
503
520
|
from hackscan.taint.sanitizers import QUOTING_SANITIZERS, SANITIZERS
|
|
504
521
|
|
|
505
522
|
if not isinstance(expr, ast.Call):
|
|
506
523
|
return False
|
|
524
|
+
if not _FileEngine(ctx, {}).stable_callee(expr):
|
|
525
|
+
return False # `int = ...` / `uuid.UUID = ...` elsewhere: the call may be another
|
|
507
526
|
return any(
|
|
508
527
|
vuln_class in SANITIZERS.get(name, frozenset()) and name not in QUOTING_SANITIZERS
|
|
509
528
|
for name in ctx.call_names(expr)
|
|
@@ -24,6 +24,7 @@ from hackscan.core.taxonomy import CMDI, CODEI, SQLI, WEAK_CRYPTO
|
|
|
24
24
|
from hackscan.importers.common import SourceIndex
|
|
25
25
|
from hackscan.plugins.base import FileContext, first_arg, keyword
|
|
26
26
|
from hackscan.plugins.loader import builtin_plugins
|
|
27
|
+
from hackscan.taint.sanitizers import INERT_PROGRAMS, program_name
|
|
27
28
|
|
|
28
29
|
PRODUCER = "template"
|
|
29
30
|
|
|
@@ -474,23 +475,7 @@ def _argv(expr: ast.AST, ctx: FileContext) -> list[str] | None:
|
|
|
474
475
|
return argv
|
|
475
476
|
|
|
476
477
|
|
|
477
|
-
#
|
|
478
|
-
# writes an arbitrary file. An argument list only makes a dynamic value safe for these;
|
|
479
|
-
# countless others execute operands or options (`sh -c`, `npx PKG`, `git -c alias=!cmd`,
|
|
480
|
-
# `tar --to-command`, `sort --compress-program`, `find -exec`, `rg --pre`), so an
|
|
481
|
-
# allowlist, not a denylist.
|
|
482
|
-
INERT_PROGRAMS = {
|
|
483
|
-
"printf", "cat", "tac", "nl", "ls", "head", "tail", "wc", "cut",
|
|
484
|
-
"grep", "egrep", "fgrep", "file", "stat", "du", "df", "basename", "dirname",
|
|
485
|
-
"realpath", "readlink", "which", "whoami", "id", "ping", "host", "dig", "nslookup",
|
|
486
|
-
} # fmt: skip
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
def program_name(literal: str) -> str:
|
|
490
|
-
"""`/bin/grep` or `C:\\Tools\\grep.exe` -> `grep`. Only `.exe` is dropped: `.bat` and
|
|
491
|
-
`.cmd` files run through cmd.exe, which re-parses the arguments."""
|
|
492
|
-
name = literal.replace("\\", "/").rsplit("/", 1)[-1].lower()
|
|
493
|
-
return name[: -len(".exe")] if name.endswith(".exe") else name
|
|
478
|
+
# INERT_PROGRAMS / program_name: see hackscan.taint.sanitizers (shared with taint_pass).
|
|
494
479
|
|
|
495
480
|
|
|
496
481
|
def _argv_is_inert(words: list[str | None]) -> bool:
|