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.
Files changed (96) hide show
  1. {attackmap-0.3.0/src/attackmap.egg-info → attackmap-0.3.1}/PKG-INFO +3 -3
  2. {attackmap-0.3.0 → attackmap-0.3.1}/README.md +2 -2
  3. {attackmap-0.3.0 → attackmap-0.3.1}/pyproject.toml +1 -1
  4. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/__init__.py +1 -1
  5. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/taint.py +57 -0
  6. {attackmap-0.3.0 → attackmap-0.3.1/src/attackmap.egg-info}/PKG-INFO +3 -3
  7. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_taint.py +76 -3
  8. {attackmap-0.3.0 → attackmap-0.3.1}/LICENSE +0 -0
  9. {attackmap-0.3.0 → attackmap-0.3.1}/setup.cfg +0 -0
  10. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/analyzer.py +0 -0
  11. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/analyzer_contracts.py +0 -0
  12. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/analyzers.py +0 -0
  13. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/anomalies.py +0 -0
  14. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/asset_model.py +0 -0
  15. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/attack_taxonomy.py +0 -0
  16. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/authz.py +0 -0
  17. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/cli.py +0 -0
  18. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/config_scanner.py +0 -0
  19. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/context_pack.py +0 -0
  20. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/control_model.py +0 -0
  21. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/crypto.py +0 -0
  22. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/cve.py +0 -0
  23. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/defensive_review.py +0 -0
  24. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/detection_opportunities.py +0 -0
  25. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/diagrams.py +0 -0
  26. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/diff.py +0 -0
  27. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/exploitability.py +0 -0
  28. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/graph.py +0 -0
  29. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/insights.py +0 -0
  30. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/llm_review.py +0 -0
  31. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/merge.py +0 -0
  32. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/models.py +0 -0
  33. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/progress.py +0 -0
  34. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/recon_models.py +0 -0
  35. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/recon_to_analysis.py +0 -0
  36. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/report.py +0 -0
  37. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/review_eval.py +0 -0
  38. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/review_json.py +0 -0
  39. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/review_prompts.py +0 -0
  40. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/sarif.py +0 -0
  41. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/sbom.py +0 -0
  42. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/scanner.py +0 -0
  43. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/sdk/__init__.py +0 -0
  44. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/sdk/contracts.py +0 -0
  45. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/sdk/models.py +0 -0
  46. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/security_overlay.py +0 -0
  47. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/srcpaths.py +0 -0
  48. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/suggest.py +0 -0
  49. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/threat_model.py +0 -0
  50. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/topology.py +0 -0
  51. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/weaknesses.py +0 -0
  52. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap/webhardening.py +0 -0
  53. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap.egg-info/SOURCES.txt +0 -0
  54. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap.egg-info/dependency_links.txt +0 -0
  55. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap.egg-info/entry_points.txt +0 -0
  56. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap.egg-info/requires.txt +0 -0
  57. {attackmap-0.3.0 → attackmap-0.3.1}/src/attackmap.egg-info/top_level.txt +0 -0
  58. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_analyzer.py +0 -0
  59. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_analyzer_metadata_schema.py +0 -0
  60. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_analyzers.py +0 -0
  61. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_anomalies.py +0 -0
  62. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_artifact_schemas.py +0 -0
  63. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_authz.py +0 -0
  64. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_config_scanner.py +0 -0
  65. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_context_pack.py +0 -0
  66. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_crypto.py +0 -0
  67. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_cve.py +0 -0
  68. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_defensive_review.py +0 -0
  69. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_diagrams.py +0 -0
  70. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_diff.py +0 -0
  71. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_exploitability.py +0 -0
  72. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_graph.py +0 -0
  73. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_hunt.py +0 -0
  74. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_injection_sinks.py +0 -0
  75. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_llm_review.py +0 -0
  76. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_merge.py +0 -0
  77. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_progress.py +0 -0
  78. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_provenance.py +0 -0
  79. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_recon_to_analysis.py +0 -0
  80. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_report.py +0 -0
  81. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_review_eval.py +0 -0
  82. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_review_json.py +0 -0
  83. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_review_prompts.py +0 -0
  84. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_sarif.py +0 -0
  85. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_sbom.py +0 -0
  86. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_scanner.py +0 -0
  87. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_security_overlay.py +0 -0
  88. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_shared_contract_imports.py +0 -0
  89. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_signal_v2.py +0 -0
  90. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_srcpaths.py +0 -0
  91. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_suggest.py +0 -0
  92. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_threat_intel.py +0 -0
  93. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_threat_model.py +0 -0
  94. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_topology.py +0 -0
  95. {attackmap-0.3.0 → attackmap-0.3.1}/tests/test_weaknesses.py +0 -0
  96. {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.0
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
  [![Python: 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/)
76
76
  [![PyPI](https://img.shields.io/pypi/v/attackmap.svg)](https://pypi.org/project/attackmap/)
77
77
 
78
- > **Status: beta (v0.3.0).** Core engine and 14 analyzer plugins are published
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.0) — published and validated on real codebases, but
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
  [![Python: 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/)
17
17
  [![PyPI](https://img.shields.io/pypi/v/attackmap.svg)](https://pypi.org/project/attackmap/)
18
18
 
19
- > **Status: beta (v0.3.0).** Core engine and 14 analyzer plugins are published
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.0) — published and validated on real codebases, but
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.0"
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.0"
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.0
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
  [![Python: 3.11+](https://img.shields.io/badge/Python-3.11%2B-blue.svg)](https://www.python.org/)
76
76
  [![PyPI](https://img.shields.io/pypi/v/attackmap.svg)](https://pypi.org/project/attackmap/)
77
77
 
78
- > **Status: beta (v0.3.0).** Core engine and 14 analyzer plugins are published
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.0) — published and validated on real codebases, but
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('1+1')\n", encoding="utf-8"
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
- "eval": "def go(): eval('1+1')\n",
240
- "exec": "def go(): exec('a=1')\n",
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