attackmap 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.
Files changed (96) hide show
  1. {attackmap-0.3.1/src/attackmap.egg-info → attackmap-0.3.2}/PKG-INFO +3 -3
  2. {attackmap-0.3.1 → attackmap-0.3.2}/README.md +2 -2
  3. {attackmap-0.3.1 → attackmap-0.3.2}/pyproject.toml +1 -1
  4. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/__init__.py +1 -1
  5. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/scanner.py +22 -4
  6. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/srcpaths.py +38 -1
  7. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/taint.py +7 -1
  8. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/weaknesses.py +23 -2
  9. {attackmap-0.3.1 → attackmap-0.3.2/src/attackmap.egg-info}/PKG-INFO +3 -3
  10. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_scanner.py +20 -0
  11. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_srcpaths.py +63 -1
  12. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_weaknesses.py +21 -0
  13. {attackmap-0.3.1 → attackmap-0.3.2}/LICENSE +0 -0
  14. {attackmap-0.3.1 → attackmap-0.3.2}/setup.cfg +0 -0
  15. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/analyzer.py +0 -0
  16. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/analyzer_contracts.py +0 -0
  17. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/analyzers.py +0 -0
  18. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/anomalies.py +0 -0
  19. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/asset_model.py +0 -0
  20. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/attack_taxonomy.py +0 -0
  21. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/authz.py +0 -0
  22. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/cli.py +0 -0
  23. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/config_scanner.py +0 -0
  24. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/context_pack.py +0 -0
  25. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/control_model.py +0 -0
  26. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/crypto.py +0 -0
  27. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/cve.py +0 -0
  28. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/defensive_review.py +0 -0
  29. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/detection_opportunities.py +0 -0
  30. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/diagrams.py +0 -0
  31. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/diff.py +0 -0
  32. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/exploitability.py +0 -0
  33. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/graph.py +0 -0
  34. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/insights.py +0 -0
  35. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/llm_review.py +0 -0
  36. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/merge.py +0 -0
  37. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/models.py +0 -0
  38. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/progress.py +0 -0
  39. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/recon_models.py +0 -0
  40. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/recon_to_analysis.py +0 -0
  41. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/report.py +0 -0
  42. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/review_eval.py +0 -0
  43. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/review_json.py +0 -0
  44. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/review_prompts.py +0 -0
  45. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/sarif.py +0 -0
  46. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/sbom.py +0 -0
  47. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/sdk/__init__.py +0 -0
  48. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/sdk/contracts.py +0 -0
  49. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/sdk/models.py +0 -0
  50. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/security_overlay.py +0 -0
  51. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/suggest.py +0 -0
  52. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/threat_model.py +0 -0
  53. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/topology.py +0 -0
  54. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/webhardening.py +0 -0
  55. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap.egg-info/SOURCES.txt +0 -0
  56. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap.egg-info/dependency_links.txt +0 -0
  57. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap.egg-info/entry_points.txt +0 -0
  58. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap.egg-info/requires.txt +0 -0
  59. {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap.egg-info/top_level.txt +0 -0
  60. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_analyzer.py +0 -0
  61. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_analyzer_metadata_schema.py +0 -0
  62. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_analyzers.py +0 -0
  63. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_anomalies.py +0 -0
  64. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_artifact_schemas.py +0 -0
  65. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_authz.py +0 -0
  66. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_config_scanner.py +0 -0
  67. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_context_pack.py +0 -0
  68. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_crypto.py +0 -0
  69. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_cve.py +0 -0
  70. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_defensive_review.py +0 -0
  71. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_diagrams.py +0 -0
  72. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_diff.py +0 -0
  73. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_exploitability.py +0 -0
  74. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_graph.py +0 -0
  75. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_hunt.py +0 -0
  76. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_injection_sinks.py +0 -0
  77. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_llm_review.py +0 -0
  78. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_merge.py +0 -0
  79. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_progress.py +0 -0
  80. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_provenance.py +0 -0
  81. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_recon_to_analysis.py +0 -0
  82. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_report.py +0 -0
  83. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_review_eval.py +0 -0
  84. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_review_json.py +0 -0
  85. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_review_prompts.py +0 -0
  86. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_sarif.py +0 -0
  87. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_sbom.py +0 -0
  88. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_security_overlay.py +0 -0
  89. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_shared_contract_imports.py +0 -0
  90. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_signal_v2.py +0 -0
  91. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_suggest.py +0 -0
  92. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_taint.py +0 -0
  93. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_threat_intel.py +0 -0
  94. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_threat_model.py +0 -0
  95. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_topology.py +0 -0
  96. {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_webhardening.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: attackmap
3
- Version: 0.3.1
3
+ Version: 0.3.2
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.1).** Core engine and 14 analyzer plugins are published
78
+ > **Status: beta (v0.3.2).** 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.1) — published and validated on real codebases, but
544
+ AttackMap is **beta** (v0.3.2) — 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.1).** Core engine and 14 analyzer plugins are published
19
+ > **Status: beta (v0.3.2).** 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.1) — published and validated on real codebases, but
485
+ AttackMap is **beta** (v0.3.2) — 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.1"
7
+ version = "0.3.2"
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.1"
2
+ __version__ = "0.3.2"
@@ -14,7 +14,7 @@ from .anomalies import find_anomalies
14
14
  from .authz import analyze_authz
15
15
  from .crypto import find_crypto_weaknesses
16
16
  from .sbom import analyze_sbom
17
- from .srcpaths import is_test_file
17
+ from .srcpaths import is_test_file, is_vendored_file
18
18
  from .weaknesses import find_code_weaknesses
19
19
  from .webhardening import find_web_hardening_issues
20
20
  from .sdk.models import AuthHint, DatabaseHint, ExternalCall, Route, ScanResult, SecretHint
@@ -594,9 +594,11 @@ def scan_repo(
594
594
 
595
595
  _append_hardcoded_secret_hints(result, relative, content)
596
596
 
597
- # The weakness passes below are noise in test scaffolding, so skip
598
- # test/spec files by default (#67; ATTACKMAP_INCLUDE_TESTS opts in).
599
- if not is_test_file(relative):
597
+ # The weakness passes below are noise in test scaffolding (#67) and in
598
+ # vendored/minified third-party code (#95) a bug in a dependency the
599
+ # project doesn't maintain isn't this repo's finding. Skip both by
600
+ # default (ATTACKMAP_INCLUDE_TESTS / ATTACKMAP_INCLUDE_VENDORED opt in).
601
+ if not is_test_file(relative) and not is_vendored_file(relative):
600
602
  # Insecure crypto / weak randomness (#70). Content is already
601
603
  # read, so this rides the per-file pass rather than re-walking.
602
604
  result.crypto_weaknesses.extend(find_crypto_weaknesses(content, relative))
@@ -724,6 +726,8 @@ def _append_hardcoded_secret_hints(result: ScanResult, relative: str, content: s
724
726
  continue
725
727
  if not _looks_like_secret_candidate(literal):
726
728
  continue
729
+ if _looks_like_charset(literal):
730
+ continue # base64/base32/hex alphabet constant, not a secret (#96)
727
731
  if _shannon_entropy(literal) < _ENTROPY_THRESHOLD:
728
732
  continue
729
733
  line = _line_of(content, pos)
@@ -743,6 +747,20 @@ def _append_hardcoded_secret_hints(result: ScanResult, relative: str, content: s
743
747
  )
744
748
 
745
749
 
750
+ # Sequential alphabet runs that mark a well-known charset/alphabet constant
751
+ # (base64/base32/base16 digit strings) — high-entropy but not a secret (#96).
752
+ _CHARSET_MARKERS = (
753
+ "abcdefghijklmnop", # lowercase alphabet run (base64/base32/base36)
754
+ "ABCDEFGHIJKLMNOP", # uppercase alphabet run
755
+ "0123456789abcdef", # lower hex
756
+ "0123456789ABCDEF", # upper hex
757
+ )
758
+
759
+
760
+ def _looks_like_charset(value: str) -> bool:
761
+ return any(marker in value for marker in _CHARSET_MARKERS)
762
+
763
+
746
764
  def _is_jwt_shape(literal: str) -> bool:
747
765
  """Verify the header segment of a candidate JWT decodes to a JSON
748
766
  object with an `alg` field. Cheap and precise enough to keep the
@@ -80,4 +80,41 @@ def is_infra_route(path: str) -> bool:
80
80
  return _INFRA_ROUTE_RE.search(path) is not None
81
81
 
82
82
 
83
- __all__ = ["is_test_file", "is_infra_route"]
83
+ # Directory segments that hold third-party / vendored code the project doesn't
84
+ # maintain. Flagging weaknesses here is noise about someone else's dependency.
85
+ _VENDORED_DIR_SEGMENTS = frozenset(
86
+ {
87
+ "node_modules",
88
+ "bower_components",
89
+ "vendor",
90
+ "vendored",
91
+ "third_party",
92
+ "third-party",
93
+ "thirdparty",
94
+ "external",
95
+ "externals",
96
+ "site-packages",
97
+ "jspm_packages",
98
+ }
99
+ )
100
+
101
+ # Minified / bundled build artifacts — not human-authored source.
102
+ _MINIFIED_FILE_RE = re.compile(r"\.min\.(?:js|css|mjs|cjs)$|\.bundle\.js$", re.IGNORECASE)
103
+
104
+
105
+ def is_vendored_file(rel_path: str) -> bool:
106
+ """Return True if ``rel_path`` is vendored third-party or minified/bundled
107
+ code (e.g. ``UserInterface/External/three.js``, ``vendor/…``, ``*.min.js``).
108
+
109
+ Honors ``ATTACKMAP_INCLUDE_VENDORED`` to scan it anyway (e.g. auditing a
110
+ pinned/forked dependency)."""
111
+ if os.environ.get("ATTACKMAP_INCLUDE_VENDORED"):
112
+ return False
113
+ norm = rel_path.replace("\\", "/")
114
+ parts = norm.split("/")
115
+ if any(segment.lower() in _VENDORED_DIR_SEGMENTS for segment in parts[:-1]):
116
+ return True
117
+ return _MINIFIED_FILE_RE.search(parts[-1]) is not None
118
+
119
+
120
+ __all__ = ["is_test_file", "is_infra_route", "is_vendored_file"]
@@ -268,7 +268,13 @@ def analyze_taint(scan: ScanResult, root: str | Path | None = None) -> list[Tain
268
268
  def _index_repo(root: Path) -> dict[str, Path]:
269
269
  """Return {rel_path: abs_path} for every taint-relevant source file."""
270
270
  out: dict[str, Path] = {}
271
- skip_dirs = {".git", "node_modules", "__pycache__", "dist", "build", ".venv", "venv"}
271
+ skip_dirs = {
272
+ ".git", "node_modules", "__pycache__", "dist", "build", ".venv", "venv",
273
+ # Vendored third-party trees (#95) — import-walking someone else's
274
+ # bundled library adds sinks the project doesn't own.
275
+ "bower_components", "vendor", "third_party", "third-party",
276
+ "external", "externals", "jspm_packages", "site-packages",
277
+ }
272
278
  for path in root.rglob("*"):
273
279
  if not path.is_file() or path.suffix not in _SUPPORTED_SUFFIXES:
274
280
  continue
@@ -32,8 +32,19 @@ _PATTERNS: tuple[tuple[str, str, re.Pattern[str]], ...] = (
32
32
  (
33
33
  "prototype_pollution",
34
34
  "high",
35
- # Literal __proto__ / constructor.prototype write.
36
- _rx(r"\[\s*['\"]__proto__['\"]\s*\]|\.__proto__\s*[=.]|['\"]constructor['\"]\s*\]\s*\[\s*['\"]prototype['\"]"),
35
+ # Literal __proto__ / constructor.prototype *write* (assignment target).
36
+ # Requiring a trailing `=` (not `==`) avoids flagging prototype-chain
37
+ # *reads* like `x.prototype.__proto__.constructor` (#94).
38
+ _rx(
39
+ r"(?:"
40
+ r"\.__proto__"
41
+ r"|\[\s*['\"]__proto__['\"]\s*\]"
42
+ r"|\[\s*['\"]constructor['\"]\s*\]\s*\[\s*['\"]prototype['\"]\s*\]"
43
+ r"|\.constructor\s*\.\s*prototype"
44
+ r")"
45
+ r"(?:\s*\.\s*\w+|\s*\[[^\]]*\])*" # optional further member/subscript access
46
+ r"\s*=(?!=)"
47
+ ),
37
48
  ),
38
49
  (
39
50
  "prototype_pollution",
@@ -158,6 +169,8 @@ def find_code_weaknesses(content: str, rel_file: str) -> list[CodeWeakness]:
158
169
  out: list[CodeWeakness] = []
159
170
  for kind, severity, pattern in _PATTERNS:
160
171
  for match in pattern.finditer(content):
172
+ if _in_line_comment(content, match.start()):
173
+ continue # a pattern inside a `//` comment isn't real code (#94)
161
174
  line = content.count("\n", 0, match.start()) + 1
162
175
  key = (kind, line)
163
176
  if key in seen:
@@ -176,6 +189,14 @@ def find_code_weaknesses(content: str, rel_file: str) -> list[CodeWeakness]:
176
189
  return out
177
190
 
178
191
 
192
+ def _in_line_comment(content: str, offset: int) -> bool:
193
+ """True if ``offset`` falls after a `//` line comment on its line. Cheap
194
+ heuristic (ignores `//` inside strings), used only to drop obvious
195
+ comment-embedded matches like `// x.__proto__.y = z`."""
196
+ line_start = content.rfind("\n", 0, offset) + 1
197
+ return "//" in content[line_start:offset]
198
+
199
+
179
200
  def _snippet(content: str, offset: int, radius: int = 120) -> str:
180
201
  start = max(0, content.rfind("\n", 0, offset) + 1)
181
202
  end = content.find("\n", offset)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: attackmap
3
- Version: 0.3.1
3
+ Version: 0.3.2
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.1).** Core engine and 14 analyzer plugins are published
78
+ > **Status: beta (v0.3.2).** 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.1) — published and validated on real codebases, but
544
+ AttackMap is **beta** (v0.3.2) — published and validated on real codebases, but
545
545
  pre-1.0 and heuristic.
546
546
 
547
547
  **Solid today:**
@@ -803,3 +803,23 @@ def refresh():
803
803
  # separate exposures). But the same line must not double-emit.
804
804
  lines = [h.line for h in aws_hints]
805
805
  assert len(lines) == len(set(lines))
806
+
807
+
808
+ def test_base64_alphabet_constant_not_flagged_as_secret(tmp_path: Path) -> None:
809
+ """A base64/hex alphabet constant is high-entropy but not a secret (#96)."""
810
+ (tmp_path / "sourcemap.js").write_text(
811
+ 'var base64Digits = '
812
+ '"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";\n',
813
+ encoding="utf-8",
814
+ )
815
+ result = scan_repo(tmp_path)
816
+ assert not [s for s in result.secret_hints if s.kind == "high_entropy"]
817
+
818
+
819
+ def test_real_high_entropy_secret_still_flagged(tmp_path: Path) -> None:
820
+ (tmp_path / "cfg.js").write_text(
821
+ 'const apiToken = "s3cr3tR4nd0mK3yZ9xQvB2mNw8LpT4hJ7";\n',
822
+ encoding="utf-8",
823
+ )
824
+ result = scan_repo(tmp_path)
825
+ assert any(s.kind == "high_entropy" for s in result.secret_hints)
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  import pytest
8
8
 
9
9
  from attackmap.scanner import scan_repo
10
- from attackmap.srcpaths import is_test_file
10
+ from attackmap.srcpaths import is_test_file, is_vendored_file
11
11
 
12
12
 
13
13
  # ---------------------------------------------------------------------------
@@ -58,6 +58,68 @@ def test_include_tests_env_disables_exclusion(monkeypatch: pytest.MonkeyPatch) -
58
58
  assert is_test_file("pkg/foo.test.ts") is False
59
59
 
60
60
 
61
+ # ---------------------------------------------------------------------------
62
+ # is_vendored_file classification (#95)
63
+ # ---------------------------------------------------------------------------
64
+
65
+
66
+ @pytest.mark.parametrize(
67
+ "rel",
68
+ [
69
+ "node_modules/lodash/merge.js",
70
+ "UserInterface/External/three.js/three.js",
71
+ "vendor/github.com/x/y.go",
72
+ "third_party/zlib/zlib.js",
73
+ "src/bower_components/jquery/jquery.js",
74
+ "app/static/app.min.js",
75
+ "app/main.bundle.js",
76
+ ],
77
+ )
78
+ def test_vendored_paths_detected(rel: str) -> None:
79
+ assert is_vendored_file(rel) is True
80
+
81
+
82
+ @pytest.mark.parametrize(
83
+ "rel",
84
+ [
85
+ "src/app.js",
86
+ "UserInterface/Models/PropertyPath.js", # not under External/
87
+ "services/external_api.py", # 'external_api' file, not an 'external' dir
88
+ "vendored_notes.md", # not a dir segment
89
+ ],
90
+ )
91
+ def test_non_vendored_paths_not_detected(rel: str) -> None:
92
+ assert is_vendored_file(rel) is False
93
+
94
+
95
+ def test_include_vendored_env_disables_exclusion(monkeypatch: pytest.MonkeyPatch) -> None:
96
+ monkeypatch.setenv("ATTACKMAP_INCLUDE_VENDORED", "1")
97
+ assert is_vendored_file("node_modules/x/y.js") is False
98
+ assert is_vendored_file("a/b.min.js") is False
99
+
100
+
101
+ def test_weakness_in_vendored_file_excluded_by_default(tmp_path: Path) -> None:
102
+ ext = tmp_path / "External" / "three.js"
103
+ ext.mkdir(parents=True)
104
+ # A ReDoS-shaped regex living in a vendored library.
105
+ (ext / "three.js").write_text(
106
+ "const re = /((?:WC+[\\/:])*)/\n", encoding="utf-8"
107
+ )
108
+ scan = scan_repo(tmp_path)
109
+ assert not [w for w in scan.code_weaknesses if w.kind == "redos"]
110
+
111
+
112
+ def test_weakness_in_vendored_file_included_with_env(
113
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
114
+ ) -> None:
115
+ monkeypatch.setenv("ATTACKMAP_INCLUDE_VENDORED", "1")
116
+ ext = tmp_path / "External"
117
+ ext.mkdir(parents=True)
118
+ (ext / "lib.js").write_text("const re = /^(a+)+$/\n", encoding="utf-8")
119
+ scan = scan_repo(tmp_path)
120
+ assert any(w.kind == "redos" for w in scan.code_weaknesses)
121
+
122
+
61
123
  # ---------------------------------------------------------------------------
62
124
  # Integration: weakness passes skip test files by default
63
125
  # ---------------------------------------------------------------------------
@@ -40,6 +40,27 @@ def test_safe_merge_of_constant_not_flagged() -> None:
40
40
  assert "prototype_pollution" not in _kinds("_.merge(target, { a: 1 })\n")
41
41
 
42
42
 
43
+ def test_prototype_chain_read_not_flagged() -> None:
44
+ # Reading up the prototype chain (parent-constructor lookup) is not a write.
45
+ assert "prototype_pollution" not in _kinds(
46
+ "constructor = constructor.prototype.__proto__.constructor;\n"
47
+ )
48
+ assert "prototype_pollution" not in _kinds("const p = obj.__proto__.foo\n")
49
+
50
+
51
+ def test_proto_in_comment_not_flagged() -> None:
52
+ assert "prototype_pollution" not in _kinds(
53
+ '// The path for "foo.__proto__.bar.__proto__.x" is shown as "foo.bar.x".\n'
54
+ )
55
+ # even a write-shaped example inside a line comment is ignored
56
+ assert "prototype_pollution" not in _kinds("// obj.__proto__.polluted = true\n")
57
+
58
+
59
+ def test_proto_write_still_flagged() -> None:
60
+ assert "prototype_pollution" in _kinds("target.__proto__.polluted = true\n")
61
+ assert "prototype_pollution" in _kinds("obj['__proto__']['x'] = 1\n")
62
+
63
+
43
64
  # ---------------------------------------------------------------------------
44
65
  # Mass assignment
45
66
  # ---------------------------------------------------------------------------
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