attackmap 0.4.1__tar.gz → 0.4.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 (98) hide show
  1. {attackmap-0.4.1/src/attackmap.egg-info → attackmap-0.4.2}/PKG-INFO +1 -1
  2. {attackmap-0.4.1 → attackmap-0.4.2}/pyproject.toml +1 -1
  3. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/__init__.py +1 -1
  4. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/anomalies.py +22 -2
  5. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/cli.py +48 -29
  6. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/scanner.py +3 -4
  7. {attackmap-0.4.1 → attackmap-0.4.2/src/attackmap.egg-info}/PKG-INFO +1 -1
  8. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_anomalies.py +26 -0
  9. {attackmap-0.4.1 → attackmap-0.4.2}/LICENSE +0 -0
  10. {attackmap-0.4.1 → attackmap-0.4.2}/README.md +0 -0
  11. {attackmap-0.4.1 → attackmap-0.4.2}/setup.cfg +0 -0
  12. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/analyzer.py +0 -0
  13. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/analyzer_contracts.py +0 -0
  14. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/analyzers.py +0 -0
  15. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/asset_model.py +0 -0
  16. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/attack_taxonomy.py +0 -0
  17. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/authz.py +0 -0
  18. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/config_scanner.py +0 -0
  19. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/context_pack.py +0 -0
  20. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/control_model.py +0 -0
  21. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/crypto.py +0 -0
  22. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/cve.py +0 -0
  23. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/defensive_review.py +0 -0
  24. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/detection_opportunities.py +0 -0
  25. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/diagrams.py +0 -0
  26. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/diff.py +0 -0
  27. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/exploitability.py +0 -0
  28. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/graph.py +0 -0
  29. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/insights.py +0 -0
  30. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/llm_review.py +0 -0
  31. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/merge.py +0 -0
  32. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/models.py +0 -0
  33. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/progress.py +0 -0
  34. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/recon_models.py +0 -0
  35. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/recon_to_analysis.py +0 -0
  36. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/report.py +0 -0
  37. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/review_eval.py +0 -0
  38. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/review_json.py +0 -0
  39. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/review_prompts.py +0 -0
  40. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/sarif.py +0 -0
  41. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/sbom.py +0 -0
  42. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/sdk/__init__.py +0 -0
  43. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/sdk/contracts.py +0 -0
  44. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/sdk/models.py +0 -0
  45. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/security_overlay.py +0 -0
  46. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/srcpaths.py +0 -0
  47. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/suggest.py +0 -0
  48. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/taint.py +0 -0
  49. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/threat_model.py +0 -0
  50. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/topology.py +0 -0
  51. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/weaknesses.py +0 -0
  52. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap/webhardening.py +0 -0
  53. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap.egg-info/SOURCES.txt +0 -0
  54. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap.egg-info/dependency_links.txt +0 -0
  55. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap.egg-info/entry_points.txt +0 -0
  56. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap.egg-info/requires.txt +0 -0
  57. {attackmap-0.4.1 → attackmap-0.4.2}/src/attackmap.egg-info/top_level.txt +0 -0
  58. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_analyzer.py +0 -0
  59. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_analyzer_metadata_schema.py +0 -0
  60. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_analyzers.py +0 -0
  61. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_artifact_schemas.py +0 -0
  62. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_authz.py +0 -0
  63. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_config_scanner.py +0 -0
  64. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_context_pack.py +0 -0
  65. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_crypto.py +0 -0
  66. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_cve.py +0 -0
  67. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_defensive_review.py +0 -0
  68. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_diagrams.py +0 -0
  69. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_diff.py +0 -0
  70. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_exploitability.py +0 -0
  71. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_graph.py +0 -0
  72. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_hunt.py +0 -0
  73. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_injection_sinks.py +0 -0
  74. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_llm_review.py +0 -0
  75. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_merge.py +0 -0
  76. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_pr_comment.py +0 -0
  77. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_progress.py +0 -0
  78. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_provenance.py +0 -0
  79. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_recon_to_analysis.py +0 -0
  80. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_remediate.py +0 -0
  81. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_report.py +0 -0
  82. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_review_eval.py +0 -0
  83. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_review_json.py +0 -0
  84. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_review_prompts.py +0 -0
  85. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_sarif.py +0 -0
  86. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_sbom.py +0 -0
  87. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_scanner.py +0 -0
  88. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_security_overlay.py +0 -0
  89. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_shared_contract_imports.py +0 -0
  90. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_signal_v2.py +0 -0
  91. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_srcpaths.py +0 -0
  92. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_suggest.py +0 -0
  93. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_taint.py +0 -0
  94. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_threat_intel.py +0 -0
  95. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_threat_model.py +0 -0
  96. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_topology.py +0 -0
  97. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_weaknesses.py +0 -0
  98. {attackmap-0.4.1 → attackmap-0.4.2}/tests/test_webhardening.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: attackmap
3
- Version: 0.4.1
3
+ Version: 0.4.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>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "attackmap"
7
- version = "0.4.1"
7
+ version = "0.4.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.4.1"
2
+ __version__ = "0.4.2"
@@ -36,10 +36,16 @@ from __future__ import annotations
36
36
  import re
37
37
  from dataclasses import dataclass
38
38
  from pathlib import Path
39
+ from typing import TYPE_CHECKING
39
40
 
40
41
  from .models import Anomaly, Route, ScanResult
41
42
  from .srcpaths import is_test_file
42
43
 
44
+ if TYPE_CHECKING:
45
+ from .progress import JsonScanProgress, ScanProgress
46
+
47
+ _Progress = ScanProgress | JsonScanProgress
48
+
43
49
 
44
50
  @dataclass
45
51
  class _SignalCtx:
@@ -106,8 +112,17 @@ _MIN_GROUP = 3
106
112
  _SCAFFOLD_RE = re.compile(r"^(?:api|rest|graphql|gql|v\d+(?:beta\d*|alpha\d*)?)$", re.IGNORECASE)
107
113
 
108
114
 
109
- def find_anomalies(scan: ScanResult, root: str | Path | None = None) -> list[Anomaly]:
110
- """Return within-repo consistency outliers for the routes in ``scan``."""
115
+ def find_anomalies(
116
+ scan: ScanResult,
117
+ root: str | Path | None = None,
118
+ progress: "_Progress | None" = None,
119
+ ) -> list[Anomaly]:
120
+ """Return within-repo consistency outliers for the routes in ``scan``.
121
+
122
+ On a large route surface the per-cohort source-window reads dominate this
123
+ pass, so when a ``progress`` reporter is supplied it drives a determinate
124
+ bar over the cohorts (rather than an open-ended spinner).
125
+ """
111
126
  root_path = Path(root or scan.root).resolve()
112
127
 
113
128
  routes = [r for r in scan.routes if not is_test_file(r.file)]
@@ -129,8 +144,13 @@ def find_anomalies(scan: ScanResult, root: str | Path | None = None) -> list[Ano
129
144
 
130
145
  ctx = _SignalCtx(root_path, line_cache, file_route_lines)
131
146
 
147
+ if progress is not None:
148
+ progress.begin(len(groups), "Anomaly / outlier detection")
149
+
132
150
  out: list[Anomaly] = []
133
151
  for key, members in groups.items():
152
+ if progress is not None:
153
+ progress.advance(key)
134
154
  if len(members) < _MIN_GROUP:
135
155
  continue
136
156
  if not _is_route_like_cohort(key, members):
@@ -160,7 +160,11 @@ def analyze(
160
160
  scan = analyze_repository(repo_path, analyzers=active_analyzers, progress=scan_progress)
161
161
  if cve and scan.dependencies:
162
162
  typer.echo(f"Checking {len(scan.dependencies)} dependencies against OSV.dev…")
163
- vulns, cve_summary = query_vulnerabilities(scan.dependencies)
163
+ scan_progress.stage(f"Checking {len(scan.dependencies)} dependencies against OSV.dev")
164
+ try:
165
+ vulns, cve_summary = query_vulnerabilities(scan.dependencies)
166
+ finally:
167
+ scan_progress.done()
164
168
  scan.vulnerabilities = vulns
165
169
  typer.echo(
166
170
  f"CVE lookup: {len(vulns)} vulnerabilit"
@@ -257,15 +261,19 @@ def analyze(
257
261
  typer.echo(
258
262
  f"Generating narrative review via Claude (backend={llm_backend}, may take a minute)..."
259
263
  )
260
- result = generate_llm_review(
261
- scan,
262
- attack_surfaces,
263
- findings,
264
- attack_paths,
265
- model=llm_model,
266
- effort=effort_value, # type: ignore[arg-type]
267
- backend=llm_backend, # type: ignore[arg-type]
268
- )
264
+ scan_progress.stage(f"Claude is writing the defensive review (backend={llm_backend})")
265
+ try:
266
+ result = generate_llm_review(
267
+ scan,
268
+ attack_surfaces,
269
+ findings,
270
+ attack_paths,
271
+ model=llm_model,
272
+ effort=effort_value, # type: ignore[arg-type]
273
+ backend=llm_backend, # type: ignore[arg-type]
274
+ )
275
+ finally:
276
+ scan_progress.done()
269
277
  except LlmReviewError as exc:
270
278
  typer.echo(f"LLM review skipped: {exc}", err=True)
271
279
  else:
@@ -307,16 +315,23 @@ def analyze(
307
315
  f"Hunting for vulnerability hypotheses via Claude "
308
316
  f"({'adjudicated against source, ' if verify else ''}backend={llm_backend}, may take a minute)..."
309
317
  )
310
- hunt_result = generate_llm_review(
311
- scan,
312
- attack_surfaces,
313
- findings,
314
- attack_paths,
315
- model=llm_model,
316
- effort=hunt_effort_value, # type: ignore[arg-type]
317
- backend=llm_backend, # type: ignore[arg-type]
318
- mode=hunt_mode, # type: ignore[arg-type]
318
+ scan_progress.stage(
319
+ f"Claude is hunting exploit-chain hypotheses"
320
+ f"{' + verifying against source' if verify else ''} (backend={llm_backend})"
319
321
  )
322
+ try:
323
+ hunt_result = generate_llm_review(
324
+ scan,
325
+ attack_surfaces,
326
+ findings,
327
+ attack_paths,
328
+ model=llm_model,
329
+ effort=hunt_effort_value, # type: ignore[arg-type]
330
+ backend=llm_backend, # type: ignore[arg-type]
331
+ mode=hunt_mode, # type: ignore[arg-type]
332
+ )
333
+ finally:
334
+ scan_progress.done()
320
335
  except LlmReviewError as exc:
321
336
  typer.echo(f"Vulnerability hunt skipped: {exc}", err=True)
322
337
  else:
@@ -359,16 +374,20 @@ def analyze(
359
374
  typer.echo(
360
375
  f"Generating remediation suggestions via Claude (backend={llm_backend}, may take a minute)..."
361
376
  )
362
- rem_result = generate_llm_review(
363
- scan,
364
- attack_surfaces,
365
- findings,
366
- attack_paths,
367
- model=llm_model,
368
- effort=rem_effort_value, # type: ignore[arg-type]
369
- backend=llm_backend, # type: ignore[arg-type]
370
- mode="remediate",
371
- )
377
+ scan_progress.stage(f"Claude is drafting remediation suggestions (backend={llm_backend})")
378
+ try:
379
+ rem_result = generate_llm_review(
380
+ scan,
381
+ attack_surfaces,
382
+ findings,
383
+ attack_paths,
384
+ model=llm_model,
385
+ effort=rem_effort_value, # type: ignore[arg-type]
386
+ backend=llm_backend, # type: ignore[arg-type]
387
+ mode="remediate",
388
+ )
389
+ finally:
390
+ scan_progress.done()
372
391
  except LlmReviewError as exc:
373
392
  typer.echo(f"Remediation skipped: {exc}", err=True)
374
393
  else:
@@ -736,10 +736,9 @@ def scan_repo(
736
736
  progress.stage("Authorization (BOLA/IDOR)")
737
737
  result.authz_candidates = analyze_authz(result, root_path)
738
738
  # Anomaly / outlier pass (#78): the odd-one-out among sibling routes.
739
- # Runs last so the full route list is assembled into cohorts.
740
- if progress is not None:
741
- progress.stage("Anomaly / outlier detection")
742
- result.anomalies = find_anomalies(result, root_path)
739
+ # Runs last so the full route list is assembled into cohorts. It drives its
740
+ # own determinate per-cohort progress (the slow tail on big route surfaces).
741
+ result.anomalies = find_anomalies(result, root_path, progress=progress)
743
742
  if progress is not None:
744
743
  progress.done()
745
744
  return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: attackmap
3
- Version: 0.4.1
3
+ Version: 0.4.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>
@@ -369,3 +369,29 @@ def test_auth_outlier_becomes_tagged_finding(tmp_path: Path) -> None:
369
369
  assert auth.severity == "high"
370
370
  assert auth.attack_techniques
371
371
  assert any("/api/users/export" in e for e in auth.evidence)
372
+
373
+
374
+ def test_progress_reported_per_cohort(tmp_path: Path) -> None:
375
+ """find_anomalies drives a determinate begin/advance over cohorts (#status)."""
376
+ import io
377
+ import json
378
+
379
+ from attackmap.progress import JsonScanProgress
380
+
381
+ scan = _scan_with_routes(
382
+ tmp_path,
383
+ "views.py",
384
+ _AUTH_COHORT,
385
+ [
386
+ ("/api/users", "GET", '"/api/users")'),
387
+ ("/api/users/<id>", "GET", '"/api/users/<id>")'),
388
+ ("/api/users/export", "GET", '"/api/users/export")'),
389
+ ],
390
+ )
391
+ stream = io.StringIO()
392
+ find_anomalies(scan, progress=JsonScanProgress(stream=stream, min_interval=0.0))
393
+ events = [json.loads(line) for line in stream.getvalue().splitlines() if line.strip()]
394
+ begin = next(e for e in events if e["event"] == "begin")
395
+ assert begin["label"] == "Anomaly / outlier detection"
396
+ assert begin["total"] >= 1
397
+ assert any(e["event"] == "advance" for e in events)
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