attackmap 0.4.3__tar.gz → 0.4.5__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.
Potentially problematic release.
This version of attackmap might be problematic. Click here for more details.
- {attackmap-0.4.3/src/attackmap.egg-info → attackmap-0.4.5}/PKG-INFO +1 -1
- {attackmap-0.4.3 → attackmap-0.4.5}/pyproject.toml +1 -1
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/__init__.py +1 -1
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/cli.py +25 -1
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/config_scanner.py +46 -5
- {attackmap-0.4.3 → attackmap-0.4.5/src/attackmap.egg-info}/PKG-INFO +1 -1
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap.egg-info/SOURCES.txt +1 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_config_scanner.py +40 -0
- attackmap-0.4.5/tests/test_modules_cli.py +44 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/LICENSE +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/README.md +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/setup.cfg +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/analyzer.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/analyzer_contracts.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/analyzers.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/anomalies.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/asset_model.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/attack_taxonomy.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/authz.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/context_pack.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/control_model.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/crypto.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/cve.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/defensive_review.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/detection_opportunities.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/diagrams.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/diff.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/exploitability.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/graph.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/insights.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/llm_review.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/merge.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/models.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/progress.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/recon_models.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/recon_to_analysis.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/report.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/review_eval.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/review_json.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/review_prompts.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/sarif.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/sbom.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/scanner.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/sdk/__init__.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/sdk/contracts.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/sdk/models.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/security_overlay.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/srcpaths.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/suggest.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/taint.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/threat_model.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/topology.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/weaknesses.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap/webhardening.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap.egg-info/dependency_links.txt +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap.egg-info/entry_points.txt +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap.egg-info/requires.txt +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/src/attackmap.egg-info/top_level.txt +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_analyzer.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_analyzer_metadata_schema.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_analyzers.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_anomalies.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_artifact_schemas.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_authz.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_context_pack.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_crypto.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_cve.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_defensive_review.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_diagrams.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_diff.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_exploitability.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_graph.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_hunt.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_injection_sinks.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_llm_review.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_merge.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_pr_comment.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_progress.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_provenance.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_recon_to_analysis.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_remediate.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_report.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_review_eval.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_review_json.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_review_prompts.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_sarif.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_sbom.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_scanner.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_security_overlay.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_shared_contract_imports.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_signal_v2.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_srcpaths.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_suggest.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_taint.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_threat_intel.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_threat_model.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_topology.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_weaknesses.py +0 -0
- {attackmap-0.4.3 → attackmap-0.4.5}/tests/test_webhardening.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: attackmap
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
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>
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "attackmap"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.5"
|
|
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.4.
|
|
2
|
+
__version__ = "0.4.5"
|
|
@@ -449,8 +449,32 @@ def analyze(
|
|
|
449
449
|
|
|
450
450
|
|
|
451
451
|
@app.command("modules")
|
|
452
|
-
def modules(
|
|
452
|
+
def modules(
|
|
453
|
+
json_output: bool = typer.Option(
|
|
454
|
+
False,
|
|
455
|
+
"--json",
|
|
456
|
+
help="Emit the installed analyzer modules as a JSON array "
|
|
457
|
+
"(name/display_name/description/scope/ecosystems/enabled_by_default). "
|
|
458
|
+
"Network-free — the remote module-repository section is omitted. "
|
|
459
|
+
"Intended for tool / GUI front-ends that offer analyzer selection.",
|
|
460
|
+
),
|
|
461
|
+
) -> None:
|
|
453
462
|
available_modules = get_available_modules()
|
|
463
|
+
if json_output:
|
|
464
|
+
payload = [
|
|
465
|
+
{
|
|
466
|
+
"name": meta.name,
|
|
467
|
+
"display_name": meta.display_name or meta.name,
|
|
468
|
+
"description": meta.description,
|
|
469
|
+
"scope": meta.scope,
|
|
470
|
+
"ecosystems": list(meta.ecosystems),
|
|
471
|
+
"enabled_by_default": meta.enabled_by_default,
|
|
472
|
+
}
|
|
473
|
+
for meta in available_modules
|
|
474
|
+
]
|
|
475
|
+
typer.echo(json.dumps(payload, indent=2))
|
|
476
|
+
return
|
|
477
|
+
|
|
454
478
|
if not available_modules:
|
|
455
479
|
typer.echo("No analyzer modules are currently available.")
|
|
456
480
|
else:
|
|
@@ -15,11 +15,45 @@ want structural analysis can extend on top; this layer stays regex-only.
|
|
|
15
15
|
|
|
16
16
|
from __future__ import annotations
|
|
17
17
|
|
|
18
|
+
import bisect
|
|
18
19
|
import re
|
|
19
20
|
from pathlib import Path
|
|
20
21
|
|
|
21
22
|
from .models import DatabaseHint, ExternalCall, ScanResult, SecretHint
|
|
22
|
-
from .scanner import
|
|
23
|
+
from .scanner import _line_snippet
|
|
24
|
+
|
|
25
|
+
# Config files larger than this are skipped — at that size they're invariably
|
|
26
|
+
# generated (lock files, minified bundles, or AttackMap's own JSON reports)
|
|
27
|
+
# rather than hand-written config, and scanning multi-megabyte text for URL /
|
|
28
|
+
# secret regexes is pointlessly slow.
|
|
29
|
+
_MAX_CONFIG_BYTES = 5_000_000
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _LineIndex:
|
|
33
|
+
"""O(log n) line-number lookups for a file's text.
|
|
34
|
+
|
|
35
|
+
`scanner._line_of` counts newlines from the start on every call — O(n) each,
|
|
36
|
+
which degrades to O(n^2) when a large file yields many matches (the cause of
|
|
37
|
+
a scan hang on multi-megabyte JSON). Precomputing the line-start offsets once
|
|
38
|
+
and binary-searching keeps extraction linear.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
__slots__ = ("_starts",)
|
|
42
|
+
|
|
43
|
+
def __init__(self, content: str) -> None:
|
|
44
|
+
starts = [0]
|
|
45
|
+
find = content.find
|
|
46
|
+
idx = find("\n")
|
|
47
|
+
while idx != -1:
|
|
48
|
+
starts.append(idx + 1)
|
|
49
|
+
idx = find("\n", idx + 1)
|
|
50
|
+
self._starts = starts
|
|
51
|
+
|
|
52
|
+
def line_of(self, offset: int) -> int:
|
|
53
|
+
"""1-indexed line number for a character offset."""
|
|
54
|
+
if offset <= 0:
|
|
55
|
+
return 1
|
|
56
|
+
return bisect.bisect_right(self._starts, offset)
|
|
23
57
|
|
|
24
58
|
|
|
25
59
|
CONFIG_SUFFIXES = {".yaml", ".yml", ".toml", ".json", ".ini", ".cfg", ".env"}
|
|
@@ -48,6 +82,8 @@ CONFIG_EXCLUDE_FILENAMES = frozenset({
|
|
|
48
82
|
_SKIP_DIRS = frozenset({
|
|
49
83
|
"node_modules",
|
|
50
84
|
".git",
|
|
85
|
+
".attackmap-gui", # the macOS GUI's report output dir — never scan our own output
|
|
86
|
+
".attackmap", # CLI cache / output dir
|
|
51
87
|
".venv",
|
|
52
88
|
"venv",
|
|
53
89
|
"dist",
|
|
@@ -183,6 +219,8 @@ def scan_config_repo(root: str | Path) -> ScanResult:
|
|
|
183
219
|
if not should_scan_config_file(path):
|
|
184
220
|
continue
|
|
185
221
|
try:
|
|
222
|
+
if path.stat().st_size > _MAX_CONFIG_BYTES:
|
|
223
|
+
continue
|
|
186
224
|
content = path.read_text(encoding="utf-8")
|
|
187
225
|
except (UnicodeDecodeError, OSError):
|
|
188
226
|
continue
|
|
@@ -196,6 +234,7 @@ def scan_config_repo(root: str | Path) -> ScanResult:
|
|
|
196
234
|
|
|
197
235
|
def _extract_db_urls(content: str, relative: str, result: ScanResult) -> None:
|
|
198
236
|
seen: set[str] = {(h.kind, h.file) for h in result.databases if h.file == relative}
|
|
237
|
+
index = _LineIndex(content)
|
|
199
238
|
for match in _DB_URL_PATTERN.finditer(content):
|
|
200
239
|
scheme = match.group("scheme").lower()
|
|
201
240
|
kind = _DB_SCHEME_TO_KIND.get(scheme, scheme)
|
|
@@ -205,7 +244,7 @@ def _extract_db_urls(content: str, relative: str, result: ScanResult) -> None:
|
|
|
205
244
|
DatabaseHint(
|
|
206
245
|
kind=kind,
|
|
207
246
|
file=relative,
|
|
208
|
-
line=
|
|
247
|
+
line=index.line_of(match.start()),
|
|
209
248
|
evidence_text=_line_snippet(content, match.start()),
|
|
210
249
|
)
|
|
211
250
|
)
|
|
@@ -221,7 +260,7 @@ def _extract_db_urls(content: str, relative: str, result: ScanResult) -> None:
|
|
|
221
260
|
SecretHint(
|
|
222
261
|
name=_redact(password),
|
|
223
262
|
file=relative,
|
|
224
|
-
line=
|
|
263
|
+
line=index.line_of(match.start()),
|
|
225
264
|
evidence_text=_line_snippet(content, match.start()),
|
|
226
265
|
confidence=1.0,
|
|
227
266
|
kind=f"{kind}_url_password",
|
|
@@ -231,6 +270,7 @@ def _extract_db_urls(content: str, relative: str, result: ScanResult) -> None:
|
|
|
231
270
|
|
|
232
271
|
def _extract_http_targets(content: str, relative: str, result: ScanResult) -> None:
|
|
233
272
|
seen_targets = {(c.target, c.file) for c in result.external_calls if c.file == relative}
|
|
273
|
+
index = _LineIndex(content)
|
|
234
274
|
for match in _HTTP_URL_PATTERN.finditer(content):
|
|
235
275
|
target = match.group(0)
|
|
236
276
|
# Trim trailing punctuation that looks like URL end
|
|
@@ -244,7 +284,7 @@ def _extract_http_targets(content: str, relative: str, result: ScanResult) -> No
|
|
|
244
284
|
ExternalCall(
|
|
245
285
|
target=target,
|
|
246
286
|
file=relative,
|
|
247
|
-
line=
|
|
287
|
+
line=index.line_of(match.start()),
|
|
248
288
|
evidence_text=_line_snippet(content, match.start()),
|
|
249
289
|
)
|
|
250
290
|
)
|
|
@@ -258,12 +298,13 @@ def _extract_host(url: str) -> str | None:
|
|
|
258
298
|
|
|
259
299
|
def _extract_secret_kvs(content: str, relative: str, result: ScanResult) -> None:
|
|
260
300
|
seen: set[tuple[str, int]] = {(h.name, h.line or 0) for h in result.secret_hints if h.file == relative}
|
|
301
|
+
index = _LineIndex(content)
|
|
261
302
|
for match in _SECRET_KEY_RE.finditer(content):
|
|
262
303
|
key = match.group("key")
|
|
263
304
|
value = match.group("value") or match.group("bare") or ""
|
|
264
305
|
if not value or _is_placeholder(value):
|
|
265
306
|
continue
|
|
266
|
-
line =
|
|
307
|
+
line = index.line_of(match.start())
|
|
267
308
|
# Store `key` as the SecretHint.name so consumers see WHICH secret
|
|
268
309
|
# was leaked (auth vs stripe vs db). Value itself is redacted into
|
|
269
310
|
# evidence_text via the same helper hardcoded-secret detection uses.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: attackmap
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
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>
|
|
@@ -263,3 +263,43 @@ def test_analyze_repository_preserves_provenance_on_config_signals(tmp_path: Pat
|
|
|
263
263
|
config_dbs = [d for d in result.databases if d.file == "app.yaml"]
|
|
264
264
|
assert config_dbs
|
|
265
265
|
assert config_dbs[0].source_analyzer == "config"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
# ---------------------------------------------------------------------------
|
|
269
|
+
# Large-file / feedback-loop hang regression (config_scanner _line_of O(n^2))
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
import attackmap.config_scanner as _cfg
|
|
273
|
+
from attackmap.config_scanner import _LineIndex
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_line_index_matches_naive_line_numbers() -> None:
|
|
277
|
+
content = "a\nbb\n\nccc\nd"
|
|
278
|
+
expected = {0: 1, 1: 1, 2: 2, 4: 2, 5: 3, 6: 4, 9: 4, 10: 5}
|
|
279
|
+
index = _LineIndex(content)
|
|
280
|
+
for offset, line in expected.items():
|
|
281
|
+
assert index.line_of(offset) == line, (offset, index.line_of(offset))
|
|
282
|
+
assert index.line_of(-5) == 1
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_skips_attackmap_output_dir(tmp_path: Path) -> None:
|
|
286
|
+
# AttackMap writes reports into .attackmap-gui/ inside scanned repos; scanning
|
|
287
|
+
# them back is a feedback loop that hung on the multi-MB report (#gui-hang).
|
|
288
|
+
(tmp_path / "app.json").write_text('{"api": "https://api.real-service.com/v1"}')
|
|
289
|
+
reports = tmp_path / ".attackmap-gui" / "reports"
|
|
290
|
+
reports.mkdir(parents=True)
|
|
291
|
+
(reports / "attackmap-report.json").write_text('{"u": "https://leaked.internal-host.net/x"}')
|
|
292
|
+
|
|
293
|
+
files = {c.file for c in scan_config_repo(tmp_path).external_calls}
|
|
294
|
+
assert not any(".attackmap-gui" in f for f in files)
|
|
295
|
+
assert any("app.json" in f for f in files)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_skips_oversized_config_file(tmp_path: Path, monkeypatch) -> None:
|
|
299
|
+
monkeypatch.setattr(_cfg, "_MAX_CONFIG_BYTES", 200)
|
|
300
|
+
(tmp_path / "big.json").write_text('{"u":"https://api.real-service.com/"}' + " " * 500)
|
|
301
|
+
(tmp_path / "small.json").write_text('{"u":"https://api.real-service.com/"}')
|
|
302
|
+
|
|
303
|
+
files = {c.file for c in scan_config_repo(tmp_path).external_calls}
|
|
304
|
+
assert "big.json" not in files
|
|
305
|
+
assert "small.json" in files
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from typer.testing import CliRunner
|
|
6
|
+
|
|
7
|
+
from attackmap.cli import app
|
|
8
|
+
|
|
9
|
+
runner = CliRunner()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_modules_json_emits_installed_modules_as_array() -> None:
|
|
13
|
+
result = runner.invoke(app, ["modules", "--json"])
|
|
14
|
+
assert result.exit_code == 0, result.output
|
|
15
|
+
|
|
16
|
+
payload = json.loads(result.output)
|
|
17
|
+
assert isinstance(payload, list) and payload, "expected a non-empty JSON array"
|
|
18
|
+
|
|
19
|
+
for entry in payload:
|
|
20
|
+
assert set(entry) >= {
|
|
21
|
+
"name",
|
|
22
|
+
"display_name",
|
|
23
|
+
"description",
|
|
24
|
+
"scope",
|
|
25
|
+
"ecosystems",
|
|
26
|
+
"enabled_by_default",
|
|
27
|
+
}
|
|
28
|
+
assert isinstance(entry["name"], str) and entry["name"]
|
|
29
|
+
assert isinstance(entry["ecosystems"], list)
|
|
30
|
+
assert isinstance(entry["enabled_by_default"], bool)
|
|
31
|
+
|
|
32
|
+
# The built-in Python web analyzer ships in-tree and runs by default.
|
|
33
|
+
by_name = {e["name"]: e for e in payload}
|
|
34
|
+
assert "python-web" in by_name
|
|
35
|
+
assert by_name["python-web"]["enabled_by_default"] is True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_modules_json_is_network_free_and_omits_remote_section() -> None:
|
|
39
|
+
# --json must not print the "module repositories (mlaify GitHub org)" block
|
|
40
|
+
# (that section does network I/O); the output is pure JSON.
|
|
41
|
+
result = runner.invoke(app, ["modules", "--json"])
|
|
42
|
+
assert result.exit_code == 0
|
|
43
|
+
assert "GitHub org" not in result.output
|
|
44
|
+
json.loads(result.output) # parses cleanly as a whole
|
|
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
|
|
File without changes
|