attackmap 0.4.4__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.

Files changed (99) hide show
  1. {attackmap-0.4.4/src/attackmap.egg-info → attackmap-0.4.5}/PKG-INFO +1 -1
  2. {attackmap-0.4.4 → attackmap-0.4.5}/pyproject.toml +1 -1
  3. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/__init__.py +1 -1
  4. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/config_scanner.py +46 -5
  5. {attackmap-0.4.4 → attackmap-0.4.5/src/attackmap.egg-info}/PKG-INFO +1 -1
  6. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_config_scanner.py +40 -0
  7. {attackmap-0.4.4 → attackmap-0.4.5}/LICENSE +0 -0
  8. {attackmap-0.4.4 → attackmap-0.4.5}/README.md +0 -0
  9. {attackmap-0.4.4 → attackmap-0.4.5}/setup.cfg +0 -0
  10. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/analyzer.py +0 -0
  11. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/analyzer_contracts.py +0 -0
  12. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/analyzers.py +0 -0
  13. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/anomalies.py +0 -0
  14. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/asset_model.py +0 -0
  15. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/attack_taxonomy.py +0 -0
  16. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/authz.py +0 -0
  17. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/cli.py +0 -0
  18. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/context_pack.py +0 -0
  19. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/control_model.py +0 -0
  20. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/crypto.py +0 -0
  21. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/cve.py +0 -0
  22. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/defensive_review.py +0 -0
  23. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/detection_opportunities.py +0 -0
  24. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/diagrams.py +0 -0
  25. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/diff.py +0 -0
  26. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/exploitability.py +0 -0
  27. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/graph.py +0 -0
  28. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/insights.py +0 -0
  29. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/llm_review.py +0 -0
  30. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/merge.py +0 -0
  31. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/models.py +0 -0
  32. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/progress.py +0 -0
  33. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/recon_models.py +0 -0
  34. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/recon_to_analysis.py +0 -0
  35. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/report.py +0 -0
  36. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/review_eval.py +0 -0
  37. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/review_json.py +0 -0
  38. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/review_prompts.py +0 -0
  39. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/sarif.py +0 -0
  40. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/sbom.py +0 -0
  41. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/scanner.py +0 -0
  42. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/sdk/__init__.py +0 -0
  43. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/sdk/contracts.py +0 -0
  44. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/sdk/models.py +0 -0
  45. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/security_overlay.py +0 -0
  46. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/srcpaths.py +0 -0
  47. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/suggest.py +0 -0
  48. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/taint.py +0 -0
  49. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/threat_model.py +0 -0
  50. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/topology.py +0 -0
  51. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/weaknesses.py +0 -0
  52. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap/webhardening.py +0 -0
  53. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap.egg-info/SOURCES.txt +0 -0
  54. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap.egg-info/dependency_links.txt +0 -0
  55. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap.egg-info/entry_points.txt +0 -0
  56. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap.egg-info/requires.txt +0 -0
  57. {attackmap-0.4.4 → attackmap-0.4.5}/src/attackmap.egg-info/top_level.txt +0 -0
  58. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_analyzer.py +0 -0
  59. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_analyzer_metadata_schema.py +0 -0
  60. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_analyzers.py +0 -0
  61. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_anomalies.py +0 -0
  62. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_artifact_schemas.py +0 -0
  63. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_authz.py +0 -0
  64. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_context_pack.py +0 -0
  65. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_crypto.py +0 -0
  66. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_cve.py +0 -0
  67. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_defensive_review.py +0 -0
  68. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_diagrams.py +0 -0
  69. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_diff.py +0 -0
  70. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_exploitability.py +0 -0
  71. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_graph.py +0 -0
  72. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_hunt.py +0 -0
  73. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_injection_sinks.py +0 -0
  74. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_llm_review.py +0 -0
  75. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_merge.py +0 -0
  76. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_modules_cli.py +0 -0
  77. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_pr_comment.py +0 -0
  78. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_progress.py +0 -0
  79. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_provenance.py +0 -0
  80. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_recon_to_analysis.py +0 -0
  81. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_remediate.py +0 -0
  82. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_report.py +0 -0
  83. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_review_eval.py +0 -0
  84. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_review_json.py +0 -0
  85. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_review_prompts.py +0 -0
  86. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_sarif.py +0 -0
  87. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_sbom.py +0 -0
  88. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_scanner.py +0 -0
  89. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_security_overlay.py +0 -0
  90. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_shared_contract_imports.py +0 -0
  91. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_signal_v2.py +0 -0
  92. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_srcpaths.py +0 -0
  93. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_suggest.py +0 -0
  94. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_taint.py +0 -0
  95. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_threat_intel.py +0 -0
  96. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_threat_model.py +0 -0
  97. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_topology.py +0 -0
  98. {attackmap-0.4.4 → attackmap-0.4.5}/tests/test_weaknesses.py +0 -0
  99. {attackmap-0.4.4 → 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.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.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.4"
2
+ __version__ = "0.4.5"
@@ -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 _line_of, _line_snippet
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=_line_of(content, match.start()),
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=_line_of(content, match.start()),
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=_line_of(content, match.start()),
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 = _line_of(content, match.start())
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.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
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