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.
- {attackmap-0.3.1/src/attackmap.egg-info → attackmap-0.3.2}/PKG-INFO +3 -3
- {attackmap-0.3.1 → attackmap-0.3.2}/README.md +2 -2
- {attackmap-0.3.1 → attackmap-0.3.2}/pyproject.toml +1 -1
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/__init__.py +1 -1
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/scanner.py +22 -4
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/srcpaths.py +38 -1
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/taint.py +7 -1
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/weaknesses.py +23 -2
- {attackmap-0.3.1 → attackmap-0.3.2/src/attackmap.egg-info}/PKG-INFO +3 -3
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_scanner.py +20 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_srcpaths.py +63 -1
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_weaknesses.py +21 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/LICENSE +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/setup.cfg +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/analyzer.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/analyzer_contracts.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/analyzers.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/anomalies.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/asset_model.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/attack_taxonomy.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/authz.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/cli.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/config_scanner.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/context_pack.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/control_model.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/crypto.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/cve.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/defensive_review.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/detection_opportunities.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/diagrams.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/diff.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/exploitability.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/graph.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/insights.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/llm_review.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/merge.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/models.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/progress.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/recon_models.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/recon_to_analysis.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/report.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/review_eval.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/review_json.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/review_prompts.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/sarif.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/sbom.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/sdk/__init__.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/sdk/contracts.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/sdk/models.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/security_overlay.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/suggest.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/threat_model.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/topology.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap/webhardening.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap.egg-info/SOURCES.txt +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap.egg-info/dependency_links.txt +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap.egg-info/entry_points.txt +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap.egg-info/requires.txt +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/src/attackmap.egg-info/top_level.txt +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_analyzer.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_analyzer_metadata_schema.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_analyzers.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_anomalies.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_artifact_schemas.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_authz.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_config_scanner.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_context_pack.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_crypto.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_cve.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_defensive_review.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_diagrams.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_diff.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_exploitability.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_graph.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_hunt.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_injection_sinks.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_llm_review.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_merge.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_progress.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_provenance.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_recon_to_analysis.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_report.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_review_eval.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_review_json.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_review_prompts.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_sarif.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_sbom.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_security_overlay.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_shared_contract_imports.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_signal_v2.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_suggest.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_taint.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_threat_intel.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_threat_model.py +0 -0
- {attackmap-0.3.1 → attackmap-0.3.2}/tests/test_topology.py +0 -0
- {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.
|
|
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
|
[](https://www.python.org/)
|
|
76
76
|
[](https://pypi.org/project/attackmap/)
|
|
77
77
|
|
|
78
|
-
> **Status: beta (v0.3.
|
|
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.
|
|
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
|
[](https://www.python.org/)
|
|
17
17
|
[](https://pypi.org/project/attackmap/)
|
|
18
18
|
|
|
19
|
-
> **Status: beta (v0.3.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
598
|
-
#
|
|
599
|
-
|
|
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
|
-
|
|
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 = {
|
|
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
|
-
|
|
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.
|
|
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
|
[](https://www.python.org/)
|
|
76
76
|
[](https://pypi.org/project/attackmap/)
|
|
77
77
|
|
|
78
|
-
> **Status: beta (v0.3.
|
|
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.
|
|
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
|
|
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
|