attackmap 0.3.0__tar.gz → 0.3.1__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.
- {attackmap-0.3.0/src/attackmap.egg-info → attackmap-0.3.1}/PKG-INFO +3 -3
- {attackmap-0.3.0 → attackmap-0.3.1}/README.md +2 -2
- {attackmap-0.3.0 → attackmap-0.3.1}/pyproject.toml +1 -1
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/__init__.py +1 -1
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/taint.py +57 -0
- {attackmap-0.3.0 → attackmap-0.3.1/src/attackmap.egg-info}/PKG-INFO +3 -3
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_taint.py +76 -3
- {attackmap-0.3.0 → attackmap-0.3.1}/LICENSE +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/setup.cfg +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/analyzer.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/analyzer_contracts.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/analyzers.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/anomalies.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/asset_model.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/attack_taxonomy.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/authz.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/cli.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/config_scanner.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/context_pack.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/control_model.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/crypto.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/cve.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/defensive_review.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/detection_opportunities.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/diagrams.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/diff.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/exploitability.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/graph.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/insights.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/llm_review.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/merge.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/models.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/progress.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/recon_models.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/recon_to_analysis.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/report.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/review_eval.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/review_json.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/review_prompts.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/sarif.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/sbom.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/scanner.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/sdk/__init__.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/sdk/contracts.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/sdk/models.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/security_overlay.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/srcpaths.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/suggest.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/threat_model.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/topology.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/weaknesses.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/webhardening.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap.egg-info/SOURCES.txt +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap.egg-info/dependency_links.txt +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap.egg-info/entry_points.txt +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap.egg-info/requires.txt +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap.egg-info/top_level.txt +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_analyzer.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_analyzer_metadata_schema.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_analyzers.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_anomalies.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_artifact_schemas.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_authz.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_config_scanner.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_context_pack.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_crypto.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_cve.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_defensive_review.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_diagrams.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_diff.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_exploitability.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_graph.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_hunt.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_injection_sinks.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_llm_review.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_merge.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_progress.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_provenance.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_recon_to_analysis.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_report.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_review_eval.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_review_json.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_review_prompts.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_sarif.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_sbom.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_scanner.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_security_overlay.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_shared_contract_imports.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_signal_v2.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_srcpaths.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_suggest.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_threat_intel.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_threat_model.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_topology.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_weaknesses.py +0 -0
- {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_webhardening.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: attackmap
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: AI-assisted defensive security analyzer for codebases — scans a repository, models assets and controls, finds cross-cutting weaknesses, and generates an evidence-grounded review with MITRE ATT&CK mappings and detection-engineering hints.
|
|
5
5
|
Author: AttackMap Contributors
|
|
6
6
|
Author-email: Matthew Davis <matthewd@matthewd.xyz>
|
|
@@ -75,7 +75,7 @@ managers who need to triage an unfamiliar codebase.
|
|
|
75
75
|
[](https://www.python.org/)
|
|
76
76
|
[](https://pypi.org/project/attackmap/)
|
|
77
77
|
|
|
78
|
-
> **Status: beta (v0.3.
|
|
78
|
+
> **Status: beta (v0.3.1).** Core engine and 14 analyzer plugins are published
|
|
79
79
|
> to PyPI, Homebrew, and GHCR and validated against real-world codebases.
|
|
80
80
|
> AttackMap is heuristic by design — findings are confidence-tiered evidence,
|
|
81
81
|
> not proof. See [Project status](#project-status) for what's solid and what's
|
|
@@ -541,7 +541,7 @@ introduces a new HIGH finding.
|
|
|
541
541
|
|
|
542
542
|
## Project status
|
|
543
543
|
|
|
544
|
-
AttackMap is **beta** (v0.3.
|
|
544
|
+
AttackMap is **beta** (v0.3.1) — published and validated on real codebases, but
|
|
545
545
|
pre-1.0 and heuristic.
|
|
546
546
|
|
|
547
547
|
**Solid today:**
|
|
@@ -16,7 +16,7 @@ managers who need to triage an unfamiliar codebase.
|
|
|
16
16
|
[](https://www.python.org/)
|
|
17
17
|
[](https://pypi.org/project/attackmap/)
|
|
18
18
|
|
|
19
|
-
> **Status: beta (v0.3.
|
|
19
|
+
> **Status: beta (v0.3.1).** Core engine and 14 analyzer plugins are published
|
|
20
20
|
> to PyPI, Homebrew, and GHCR and validated against real-world codebases.
|
|
21
21
|
> AttackMap is heuristic by design — findings are confidence-tiered evidence,
|
|
22
22
|
> not proof. See [Project status](#project-status) for what's solid and what's
|
|
@@ -482,7 +482,7 @@ introduces a new HIGH finding.
|
|
|
482
482
|
|
|
483
483
|
## Project status
|
|
484
484
|
|
|
485
|
-
AttackMap is **beta** (v0.3.
|
|
485
|
+
AttackMap is **beta** (v0.3.1) — published and validated on real codebases, but
|
|
486
486
|
pre-1.0 and heuristic.
|
|
487
487
|
|
|
488
488
|
**Solid today:**
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "attackmap"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "AI-assisted defensive security analyzer for codebases — scans a repository, models assets and controls, finds cross-cutting weaknesses, and generates an evidence-grounded review with MITRE ATT&CK mappings and detection-engineering hints."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__all__ = ["__version__"]
|
|
2
|
-
__version__ = "0.3.
|
|
2
|
+
__version__ = "0.3.1"
|
|
@@ -391,6 +391,59 @@ def _resolve_js_target(target: Path, files: dict[str, Path], root: Path) -> str
|
|
|
391
391
|
return None
|
|
392
392
|
|
|
393
393
|
|
|
394
|
+
# Sink families that fire on reachability alone (no request-token gate),
|
|
395
|
+
# because the operation is dangerous regardless of the argument's shape.
|
|
396
|
+
# But that over-fires when the argument is provably STATIC/local — e.g.
|
|
397
|
+
# `yaml.load(fs.readFileSync('./swagger.yml'))` loads a shipped config at
|
|
398
|
+
# boot, not attacker bytes. Left ungated, one such call in a hub module
|
|
399
|
+
# fans out to a chain from every route (observed: 113/123 chains on a real
|
|
400
|
+
# app collapsing to one benign `yaml.load` of a static file). We suppress
|
|
401
|
+
# these kinds when the call argument is a bare string literal or a
|
|
402
|
+
# literal-path file read with no dynamic/attacker component.
|
|
403
|
+
_STATIC_GATED_KINDS = frozenset({"unsafe_deserialization", "eval", "exec"})
|
|
404
|
+
|
|
405
|
+
# A quoted string literal appears in the argument.
|
|
406
|
+
_ARG_STRING_LITERAL = re.compile(r"""['"][^'"]*['"]""")
|
|
407
|
+
# Signals the argument is (or may be) dynamic / attacker-influenced:
|
|
408
|
+
# string concatenation, template/f-string interpolation, or a request /
|
|
409
|
+
# runtime-input container. If any appears, we do NOT treat the arg as static.
|
|
410
|
+
_ARG_DYNAMIC = re.compile(
|
|
411
|
+
r"\+|\$\{|`|\b(?:req|request|body|query|params|payload|argv|input|stdin)\b|process\."
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _extract_call_arg(content: str, open_paren: int, cap: int = 400) -> str:
|
|
416
|
+
"""Return the text inside the balanced parens starting at ``open_paren``
|
|
417
|
+
(the index of the ``(``), bounded to ``cap`` chars."""
|
|
418
|
+
depth = 0
|
|
419
|
+
end = min(len(content), open_paren + cap)
|
|
420
|
+
for i in range(open_paren, end):
|
|
421
|
+
ch = content[i]
|
|
422
|
+
if ch == "(":
|
|
423
|
+
depth += 1
|
|
424
|
+
elif ch == ")":
|
|
425
|
+
depth -= 1
|
|
426
|
+
if depth == 0:
|
|
427
|
+
return content[open_paren + 1 : i]
|
|
428
|
+
return content[open_paren + 1 : end]
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _is_static_local_arg(content: str, match: re.Match[str]) -> bool:
|
|
432
|
+
"""True if the sink call's argument is provably static/local — a string
|
|
433
|
+
literal or a literal-path file read — with no dynamic/attacker component."""
|
|
434
|
+
paren = content.find("(", match.start())
|
|
435
|
+
if paren == -1:
|
|
436
|
+
return False
|
|
437
|
+
arg = _extract_call_arg(content, paren)
|
|
438
|
+
if not _ARG_STRING_LITERAL.search(arg):
|
|
439
|
+
return False # no literal → provenance unknown → keep flagging
|
|
440
|
+
# Strip quoted spans before the dynamic check so a `+`/`${`/token that is
|
|
441
|
+
# DATA inside a literal (e.g. `eval('1 + 1')`, `yaml.load('a: b+c')`)
|
|
442
|
+
# doesn't read as concatenation/interpolation of the argument.
|
|
443
|
+
residual = _ARG_STRING_LITERAL.sub("", arg)
|
|
444
|
+
return _ARG_DYNAMIC.search(residual) is None
|
|
445
|
+
|
|
446
|
+
|
|
394
447
|
def _find_sinks(
|
|
395
448
|
files: dict[str, Path], root: Path
|
|
396
449
|
) -> dict[str, list[tuple[str, int, str]]]:
|
|
@@ -404,6 +457,10 @@ def _find_sinks(
|
|
|
404
457
|
hits: list[tuple[str, int, str]] = []
|
|
405
458
|
for kind, pattern in _SINK_PATTERNS:
|
|
406
459
|
for match in pattern.finditer(content):
|
|
460
|
+
# Suppress ungated "dangerous-regardless" sinks whose argument
|
|
461
|
+
# is a static literal / local-file read (#88 follow-up).
|
|
462
|
+
if kind in _STATIC_GATED_KINDS and _is_static_local_arg(content, match):
|
|
463
|
+
continue
|
|
407
464
|
line = content.count("\n", 0, match.start()) + 1
|
|
408
465
|
snippet = _line_snippet(content, match.start())
|
|
409
466
|
hits.append((kind, line, snippet))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: attackmap
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: AI-assisted defensive security analyzer for codebases — scans a repository, models assets and controls, finds cross-cutting weaknesses, and generates an evidence-grounded review with MITRE ATT&CK mappings and detection-engineering hints.
|
|
5
5
|
Author: AttackMap Contributors
|
|
6
6
|
Author-email: Matthew Davis <matthewd@matthewd.xyz>
|
|
@@ -75,7 +75,7 @@ managers who need to triage an unfamiliar codebase.
|
|
|
75
75
|
[](https://www.python.org/)
|
|
76
76
|
[](https://pypi.org/project/attackmap/)
|
|
77
77
|
|
|
78
|
-
> **Status: beta (v0.3.
|
|
78
|
+
> **Status: beta (v0.3.1).** Core engine and 14 analyzer plugins are published
|
|
79
79
|
> to PyPI, Homebrew, and GHCR and validated against real-world codebases.
|
|
80
80
|
> AttackMap is heuristic by design — findings are confidence-tiered evidence,
|
|
81
81
|
> not proof. See [Project status](#project-status) for what's solid and what's
|
|
@@ -541,7 +541,7 @@ introduces a new HIGH finding.
|
|
|
541
541
|
|
|
542
542
|
## Project status
|
|
543
543
|
|
|
544
|
-
AttackMap is **beta** (v0.3.
|
|
544
|
+
AttackMap is **beta** (v0.3.1) — published and validated on real codebases, but
|
|
545
545
|
pre-1.0 and heuristic.
|
|
546
546
|
|
|
547
547
|
**Solid today:**
|
|
@@ -216,7 +216,7 @@ def test_analyze_taint_uses_scan_routes_directly(tmp_path: Path) -> None:
|
|
|
216
216
|
"import worker\ndef h(): return worker.run()\n", encoding="utf-8"
|
|
217
217
|
)
|
|
218
218
|
(tmp_path / "worker.py").write_text(
|
|
219
|
-
"def run(): eval(
|
|
219
|
+
"def run(x): eval(x)\n", encoding="utf-8"
|
|
220
220
|
)
|
|
221
221
|
scan = ScanResult(
|
|
222
222
|
root=str(tmp_path),
|
|
@@ -228,6 +228,77 @@ def test_analyze_taint_uses_scan_routes_directly(tmp_path: Path) -> None:
|
|
|
228
228
|
assert chains[0].route_path == "/synthetic"
|
|
229
229
|
|
|
230
230
|
|
|
231
|
+
def test_static_local_deserialization_not_flagged(tmp_path: Path) -> None:
|
|
232
|
+
"""`yaml.load` of a static local file (shipped config at boot) is not an
|
|
233
|
+
unsafe deserialization of untrusted data — must not seed a chain. This is
|
|
234
|
+
the juice-shop `yaml.load(readFileSync('./swagger.yml'))` fan-out FP."""
|
|
235
|
+
(tmp_path / "server.js").write_text(
|
|
236
|
+
"const fs = require('fs')\n"
|
|
237
|
+
"const yaml = require('js-yaml')\n"
|
|
238
|
+
"const express = require('express')\n"
|
|
239
|
+
"const app = express()\n"
|
|
240
|
+
"const swaggerDocument = yaml.load(fs.readFileSync('./swagger.yml', 'utf8'))\n"
|
|
241
|
+
"app.get('/status', (req, res) => res.json(swaggerDocument))\n",
|
|
242
|
+
encoding="utf-8",
|
|
243
|
+
)
|
|
244
|
+
scan = scan_repo(tmp_path)
|
|
245
|
+
assert not [c for c in scan.taint_chains if c.sink_kind == "unsafe_deserialization"]
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def test_dynamic_deserialization_still_flagged(tmp_path: Path) -> None:
|
|
249
|
+
# request-derived bytes into yaml.load → real, still flagged.
|
|
250
|
+
(tmp_path / "app.py").write_text(
|
|
251
|
+
"from flask import Flask, request\n"
|
|
252
|
+
"import yaml\n"
|
|
253
|
+
"app = Flask(__name__)\n"
|
|
254
|
+
"@app.route('/load', methods=['POST'])\n"
|
|
255
|
+
"def load():\n"
|
|
256
|
+
" return yaml.load(request.data)\n",
|
|
257
|
+
encoding="utf-8",
|
|
258
|
+
)
|
|
259
|
+
scan = scan_repo(tmp_path)
|
|
260
|
+
assert any(c.sink_kind == "unsafe_deserialization" for c in scan.taint_chains)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_concatenated_path_deserialization_still_flagged(tmp_path: Path) -> None:
|
|
264
|
+
# literal path + a variable (potential traversal) is NOT purely static.
|
|
265
|
+
(tmp_path / "app.py").write_text(
|
|
266
|
+
"from flask import Flask, request\n"
|
|
267
|
+
"import yaml\n"
|
|
268
|
+
"app = Flask(__name__)\n"
|
|
269
|
+
"@app.route('/hint/<key>')\n"
|
|
270
|
+
"def hint(key):\n"
|
|
271
|
+
" return yaml.load(open('./data/' + key + '.yml').read())\n",
|
|
272
|
+
encoding="utf-8",
|
|
273
|
+
)
|
|
274
|
+
scan = scan_repo(tmp_path)
|
|
275
|
+
assert any(c.sink_kind == "unsafe_deserialization" for c in scan.taint_chains)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_constant_eval_not_flagged_but_variable_eval_is(tmp_path: Path) -> None:
|
|
279
|
+
# Separate files so hop-0 file-locality doesn't cross-link the routes.
|
|
280
|
+
(tmp_path / "constant.py").write_text(
|
|
281
|
+
"from flask import Flask\n"
|
|
282
|
+
"app = Flask(__name__)\n"
|
|
283
|
+
"@app.route('/const')\n"
|
|
284
|
+
"def c():\n"
|
|
285
|
+
" return eval('1 + 1')\n", # constant → benign, suppressed
|
|
286
|
+
encoding="utf-8",
|
|
287
|
+
)
|
|
288
|
+
(tmp_path / "dynamic.py").write_text(
|
|
289
|
+
"from flask import Flask, request\n"
|
|
290
|
+
"app = Flask(__name__)\n"
|
|
291
|
+
"@app.route('/dyn')\n"
|
|
292
|
+
"def d():\n"
|
|
293
|
+
" return eval(request.args['x'])\n", # variable → dangerous
|
|
294
|
+
encoding="utf-8",
|
|
295
|
+
)
|
|
296
|
+
scan = scan_repo(tmp_path)
|
|
297
|
+
eval_routes = {c.route_path for c in scan.taint_chains if c.sink_kind == "eval"}
|
|
298
|
+
assert "/dyn" in eval_routes
|
|
299
|
+
assert "/const" not in eval_routes
|
|
300
|
+
|
|
301
|
+
|
|
231
302
|
@pytest.mark.parametrize("sink_kind", ["eval", "exec", "subprocess_shell", "dynamic_open"])
|
|
232
303
|
def test_all_sink_kinds_are_detectable(tmp_path: Path, sink_kind: str) -> None:
|
|
233
304
|
(tmp_path / "route.py").write_text(
|
|
@@ -236,8 +307,10 @@ def test_all_sink_kinds_are_detectable(tmp_path: Path, sink_kind: str) -> None:
|
|
|
236
307
|
encoding="utf-8",
|
|
237
308
|
)
|
|
238
309
|
bodies = {
|
|
239
|
-
|
|
240
|
-
|
|
310
|
+
# eval/exec use a variable arg (not a constant) — a constant eval is
|
|
311
|
+
# benign and is now suppressed by the static-arg gate (see below).
|
|
312
|
+
"eval": "def go(x): eval(x)\n",
|
|
313
|
+
"exec": "def go(x): exec(x)\n",
|
|
241
314
|
"subprocess_shell": "import subprocess\ndef go(): subprocess.run('ls', shell=True)\n",
|
|
242
315
|
"dynamic_open": "def go(req): return open(req.path)\n",
|
|
243
316
|
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|