attackmap 0.4.4__tar.gz → 0.4.6__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.4/src/attackmap.egg-info → attackmap-0.4.6}/PKG-INFO +1 -1
- {attackmap-0.4.4 → attackmap-0.4.6}/pyproject.toml +1 -1
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/__init__.py +1 -1
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/config_scanner.py +46 -5
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/llm_review.py +38 -3
- {attackmap-0.4.4 → attackmap-0.4.6/src/attackmap.egg-info}/PKG-INFO +1 -1
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_config_scanner.py +40 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_llm_review.py +29 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/LICENSE +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/README.md +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/setup.cfg +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/analyzer.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/analyzer_contracts.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/analyzers.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/anomalies.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/asset_model.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/attack_taxonomy.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/authz.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/cli.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/context_pack.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/control_model.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/crypto.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/cve.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/defensive_review.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/detection_opportunities.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/diagrams.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/diff.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/exploitability.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/graph.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/insights.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/merge.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/models.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/progress.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/recon_models.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/recon_to_analysis.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/report.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/review_eval.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/review_json.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/review_prompts.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/sarif.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/sbom.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/scanner.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/sdk/__init__.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/sdk/contracts.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/sdk/models.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/security_overlay.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/srcpaths.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/suggest.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/taint.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/threat_model.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/topology.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/weaknesses.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap/webhardening.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap.egg-info/SOURCES.txt +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap.egg-info/dependency_links.txt +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap.egg-info/entry_points.txt +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap.egg-info/requires.txt +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/src/attackmap.egg-info/top_level.txt +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_analyzer.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_analyzer_metadata_schema.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_analyzers.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_anomalies.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_artifact_schemas.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_authz.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_context_pack.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_crypto.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_cve.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_defensive_review.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_diagrams.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_diff.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_exploitability.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_graph.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_hunt.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_injection_sinks.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_merge.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_modules_cli.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_pr_comment.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_progress.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_provenance.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_recon_to_analysis.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_remediate.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_report.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_review_eval.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_review_json.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_review_prompts.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_sarif.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_sbom.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_scanner.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_security_overlay.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_shared_contract_imports.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_signal_v2.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_srcpaths.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_suggest.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_taint.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_threat_intel.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_threat_model.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_topology.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/tests/test_weaknesses.py +0 -0
- {attackmap-0.4.4 → attackmap-0.4.6}/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.6
|
|
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.6"
|
|
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.6"
|
|
@@ -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.
|
|
@@ -197,6 +197,14 @@ def _claude_cli_available() -> bool:
|
|
|
197
197
|
return shutil.which("claude") is not None
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
def _anthropic_sdk_available() -> bool:
|
|
201
|
+
try:
|
|
202
|
+
import anthropic # noqa: F401
|
|
203
|
+
except ImportError:
|
|
204
|
+
return False
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
|
|
200
208
|
def _run_via_claude_cli(
|
|
201
209
|
rendered_system: str,
|
|
202
210
|
rendered_user: str,
|
|
@@ -412,6 +420,14 @@ def _codex_cli_available() -> bool:
|
|
|
412
420
|
return shutil.which("codex") is not None
|
|
413
421
|
|
|
414
422
|
|
|
423
|
+
def _openai_sdk_available() -> bool:
|
|
424
|
+
try:
|
|
425
|
+
import openai # noqa: F401
|
|
426
|
+
except ImportError:
|
|
427
|
+
return False
|
|
428
|
+
return True
|
|
429
|
+
|
|
430
|
+
|
|
415
431
|
def _run_via_codex_cli(
|
|
416
432
|
rendered_system: str,
|
|
417
433
|
rendered_user: str,
|
|
@@ -514,10 +530,18 @@ def _resolve_openai_backend(
|
|
|
514
530
|
)
|
|
515
531
|
return "cli"
|
|
516
532
|
# auto
|
|
517
|
-
if client is not None
|
|
533
|
+
if client is not None:
|
|
534
|
+
return "api"
|
|
535
|
+
has_key = bool(api_key or os.environ.get("OPENAI_API_KEY"))
|
|
536
|
+
# Prefer the API backend only when the openai SDK is importable; otherwise
|
|
537
|
+
# fall back to the `codex` CLI (needs no SDK) so a key set against an
|
|
538
|
+
# SDK-less install still works via subscription auth.
|
|
539
|
+
if has_key and _openai_sdk_available():
|
|
518
540
|
return "api"
|
|
519
541
|
if _codex_cli_available():
|
|
520
542
|
return "cli"
|
|
543
|
+
if has_key:
|
|
544
|
+
return "api" # no SDK and no CLI — surface the actionable SDK-missing error
|
|
521
545
|
raise LlmReviewError(
|
|
522
546
|
"No OpenAI backend available. Set OPENAI_API_KEY, or install the `codex` "
|
|
523
547
|
"CLI and run `codex login`."
|
|
@@ -539,12 +563,23 @@ def _resolve_backend(
|
|
|
539
563
|
)
|
|
540
564
|
return "cli"
|
|
541
565
|
# auto
|
|
542
|
-
if client is not None
|
|
566
|
+
if client is not None:
|
|
543
567
|
return "api"
|
|
544
|
-
|
|
568
|
+
has_key = bool(
|
|
569
|
+
api_key
|
|
570
|
+
or os.environ.get("ANTHROPIC_API_KEY")
|
|
571
|
+
or os.environ.get("ANTHROPIC_AUTH_TOKEN")
|
|
572
|
+
)
|
|
573
|
+
# Prefer the API backend only when the SDK is actually importable. Otherwise
|
|
574
|
+
# fall back to the `claude` CLI (needs no SDK), so a key set against an
|
|
575
|
+
# SDK-less install (e.g. Homebrew, which doesn't vendor `attackmap[llm]`)
|
|
576
|
+
# still works via subscription auth instead of erroring.
|
|
577
|
+
if has_key and _anthropic_sdk_available():
|
|
545
578
|
return "api"
|
|
546
579
|
if _claude_cli_available():
|
|
547
580
|
return "cli"
|
|
581
|
+
if has_key:
|
|
582
|
+
return "api" # no SDK and no CLI — surface the actionable SDK-missing error
|
|
548
583
|
raise LlmReviewError(
|
|
549
584
|
"No LLM backend available. Set ANTHROPIC_API_KEY, set ANTHROPIC_AUTH_TOKEN, "
|
|
550
585
|
"or install the `claude` CLI and run `claude login`."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: attackmap
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.6
|
|
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
|
|
@@ -424,6 +424,35 @@ def test_auto_backend_falls_back_to_cli_when_no_creds(monkeypatch) -> None:
|
|
|
424
424
|
assert result.backend == "cli"
|
|
425
425
|
|
|
426
426
|
|
|
427
|
+
def test_auto_backend_falls_back_to_cli_when_key_set_but_sdk_missing(monkeypatch) -> None:
|
|
428
|
+
# Homebrew installs attackmap without the anthropic SDK. A key set in that
|
|
429
|
+
# environment must fall back to the `claude` CLI, not error.
|
|
430
|
+
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test")
|
|
431
|
+
monkeypatch.setattr("attackmap.llm_review._anthropic_sdk_available", lambda: False)
|
|
432
|
+
monkeypatch.setattr("attackmap.llm_review._claude_cli_available", lambda: True)
|
|
433
|
+
|
|
434
|
+
payload = {"type": "result", "is_error": False, "result": "hi"}
|
|
435
|
+
runner, _ = _make_cli_runner(json.dumps(payload))
|
|
436
|
+
|
|
437
|
+
result = generate_llm_review(
|
|
438
|
+
_trivial_scan(), _surfaces(), _findings(), [], backend="auto", cli_runner=runner
|
|
439
|
+
)
|
|
440
|
+
assert result.backend == "cli"
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def test_openai_auto_falls_back_to_codex_when_key_set_but_sdk_missing(monkeypatch) -> None:
|
|
444
|
+
monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-test")
|
|
445
|
+
monkeypatch.setattr("attackmap.llm_review._openai_sdk_available", lambda: False)
|
|
446
|
+
monkeypatch.setattr("attackmap.llm_review._codex_cli_available", lambda: True)
|
|
447
|
+
runner, _ = _make_cli_runner("hi")
|
|
448
|
+
|
|
449
|
+
result = generate_llm_review(
|
|
450
|
+
_trivial_scan(), _surfaces(), _findings(), [],
|
|
451
|
+
provider="openai", backend="auto", codex_runner=runner,
|
|
452
|
+
)
|
|
453
|
+
assert result.backend == "cli"
|
|
454
|
+
|
|
455
|
+
|
|
427
456
|
def test_auto_backend_raises_when_no_creds_and_no_cli(monkeypatch) -> None:
|
|
428
457
|
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
|
429
458
|
monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False)
|
|
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
|
|
File without changes
|