sourcecode 4.8.0__py3-none-any.whl → 4.9.1__py3-none-any.whl
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 sourcecode might be problematic. Click here for more details.
- sourcecode/__init__.py +1 -1
- sourcecode/audit_report.py +158 -0
- sourcecode/cli.py +287 -18
- sourcecode/environment_resolution.py +88 -6
- sourcecode/migrate_check.py +307 -1
- sourcecode/non_coverage.py +12 -10
- sourcecode/posture.py +2 -1
- sourcecode/provenance.py +202 -0
- sourcecode/risk.py +234 -16
- sourcecode/rule_catalog.py +6 -1
- sourcecode/sarif.py +12 -0
- sourcecode/spring_security_audit.py +338 -0
- {sourcecode-4.8.0.dist-info → sourcecode-4.9.1.dist-info}/METADATA +24 -12
- {sourcecode-4.8.0.dist-info → sourcecode-4.9.1.dist-info}/RECORD +17 -15
- {sourcecode-4.8.0.dist-info → sourcecode-4.9.1.dist-info}/WHEEL +0 -0
- {sourcecode-4.8.0.dist-info → sourcecode-4.9.1.dist-info}/entry_points.txt +0 -0
- {sourcecode-4.8.0.dist-info → sourcecode-4.9.1.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Signed audit report over existing ASK evidence.
|
|
2
|
+
|
|
3
|
+
This module packages facts other commands already publish. It does not score ROI,
|
|
4
|
+
choose a fix, or introduce a second judgement layer: the report summary is a
|
|
5
|
+
reader-friendly index over the embedded evidence blocks.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import hmac
|
|
12
|
+
import json
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
from sourcecode import __version__
|
|
18
|
+
|
|
19
|
+
AUDIT_REPORT_SCHEMA = "audit-report-v1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _canonical_json(data: dict[str, Any]) -> bytes:
|
|
23
|
+
return json.dumps(data, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _sign(payload: dict[str, Any], key: bytes) -> dict[str, str]:
|
|
27
|
+
canonical = _canonical_json(payload)
|
|
28
|
+
return {
|
|
29
|
+
"algorithm": "hmac-sha256",
|
|
30
|
+
"payload_sha256": hashlib.sha256(canonical).hexdigest(),
|
|
31
|
+
"key_id": hashlib.sha256(key).hexdigest()[:12],
|
|
32
|
+
"signature": hmac.new(key, canonical, hashlib.sha256).hexdigest(),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_audit_report(
|
|
37
|
+
root: Path,
|
|
38
|
+
*,
|
|
39
|
+
profiles: Optional[set[str]] = None,
|
|
40
|
+
sign_key: Optional[bytes] = None,
|
|
41
|
+
risk_limit: int = 10,
|
|
42
|
+
) -> dict[str, Any]:
|
|
43
|
+
"""Build the buyer-readable audit bundle from existing command payloads."""
|
|
44
|
+
from sourcecode.posture import build_posture
|
|
45
|
+
from sourcecode.risk import build_risk
|
|
46
|
+
|
|
47
|
+
root = Path(root).resolve()
|
|
48
|
+
risk = build_risk(root, limit=risk_limit, min_band="low", profiles=profiles)
|
|
49
|
+
posture = build_posture(root, profiles or set())
|
|
50
|
+
top_risks = risk.get("risks", [])[:risk_limit]
|
|
51
|
+
access = ((posture.get("endpoints") or {}).get("effective_access") or {})
|
|
52
|
+
|
|
53
|
+
unsigned: dict[str, Any] = {
|
|
54
|
+
"schema_version": AUDIT_REPORT_SCHEMA,
|
|
55
|
+
"tool": {"name": "ask", "version": __version__},
|
|
56
|
+
"generated_at": datetime.now(timezone.utc).isoformat(),
|
|
57
|
+
"repository": {"path": str(root)},
|
|
58
|
+
**({"profile_set": sorted(profiles)} if profiles else {}),
|
|
59
|
+
"summary": {
|
|
60
|
+
"risk_model": risk.get("model"),
|
|
61
|
+
"total_defects": risk.get("total_defects"),
|
|
62
|
+
"total_findings": risk.get("total_findings"),
|
|
63
|
+
"risk_bands": risk.get("by_band", {}),
|
|
64
|
+
"top_risks_shown": len(top_risks),
|
|
65
|
+
"endpoint_access": access.get("summary", {}),
|
|
66
|
+
"unresolved_beans": len(posture.get("unresolved") or []),
|
|
67
|
+
},
|
|
68
|
+
"top_risks": top_risks,
|
|
69
|
+
"evidence": {
|
|
70
|
+
"risk": risk,
|
|
71
|
+
"posture": posture,
|
|
72
|
+
},
|
|
73
|
+
"limits": {
|
|
74
|
+
"statement": (
|
|
75
|
+
"This report packages existing ASK evidence. It does not certify "
|
|
76
|
+
"compliance, choose fixes, assign ROI, or replace a human audit decision."
|
|
77
|
+
),
|
|
78
|
+
"non_coverage": (risk.get("non_coverage") or {}).get("items", []),
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
from sourcecode.provenance import build_evidence_manifest
|
|
82
|
+
unsigned["evidence_manifest"] = build_evidence_manifest(
|
|
83
|
+
unsigned,
|
|
84
|
+
command="audit-report",
|
|
85
|
+
root=root,
|
|
86
|
+
inputs={
|
|
87
|
+
"profiles": sorted(profiles) if profiles else None,
|
|
88
|
+
"risk_limit": risk_limit,
|
|
89
|
+
"signed": bool(sign_key),
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
# Sign and publish the same JSON-normalized material. Some embedded evidence
|
|
93
|
+
# may contain tuples or other JSON-coercible values; normalizing before signing
|
|
94
|
+
# avoids a signature over an in-memory shape the user never receives.
|
|
95
|
+
normalized = json.loads(_canonical_json(unsigned).decode("utf-8"))
|
|
96
|
+
if sign_key:
|
|
97
|
+
normalized["signature"] = _sign(normalized, sign_key)
|
|
98
|
+
return normalized
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def render_markdown(report: dict[str, Any]) -> str:
|
|
102
|
+
"""Human report renderer over the JSON contract."""
|
|
103
|
+
summary = report.get("summary") or {}
|
|
104
|
+
repo = (report.get("repository") or {}).get("path", "")
|
|
105
|
+
profile = ", ".join(report.get("profile_set") or []) or "default"
|
|
106
|
+
lines = [
|
|
107
|
+
"# ASK Audit Report",
|
|
108
|
+
"",
|
|
109
|
+
f"- Repository: `{repo}`",
|
|
110
|
+
f"- ASK version: `{(report.get('tool') or {}).get('version', '')}`",
|
|
111
|
+
f"- Profile set: `{profile}`",
|
|
112
|
+
f"- Total defects: `{summary.get('total_defects')}`",
|
|
113
|
+
f"- Risk bands: `{summary.get('risk_bands')}`",
|
|
114
|
+
f"- Endpoint access: `{summary.get('endpoint_access')}`",
|
|
115
|
+
"",
|
|
116
|
+
"## Top Risks",
|
|
117
|
+
"",
|
|
118
|
+
]
|
|
119
|
+
risks = report.get("top_risks") or []
|
|
120
|
+
if not risks:
|
|
121
|
+
lines.append("No composed risks were reported.")
|
|
122
|
+
else:
|
|
123
|
+
lines.extend([
|
|
124
|
+
"| Band | Score | Rules | File | Line | Title |",
|
|
125
|
+
"|---|---:|---|---|---:|---|",
|
|
126
|
+
])
|
|
127
|
+
for row in risks:
|
|
128
|
+
rules = ", ".join(str(r) for r in row.get("rule_ids", []))
|
|
129
|
+
lines.append(
|
|
130
|
+
"| "
|
|
131
|
+
+ " | ".join([
|
|
132
|
+
str(row.get("band", "")),
|
|
133
|
+
str(row.get("severity_effective", "")),
|
|
134
|
+
rules,
|
|
135
|
+
str(row.get("source_file", "")),
|
|
136
|
+
str(row.get("first_line", "")),
|
|
137
|
+
str(row.get("title", "")).replace("|", "\\|"),
|
|
138
|
+
])
|
|
139
|
+
+ " |"
|
|
140
|
+
)
|
|
141
|
+
lines.extend([
|
|
142
|
+
"",
|
|
143
|
+
"## Limits",
|
|
144
|
+
"",
|
|
145
|
+
str((report.get("limits") or {}).get("statement", "")),
|
|
146
|
+
])
|
|
147
|
+
signature = report.get("signature")
|
|
148
|
+
if isinstance(signature, dict):
|
|
149
|
+
lines.extend([
|
|
150
|
+
"",
|
|
151
|
+
"## Signature",
|
|
152
|
+
"",
|
|
153
|
+
f"- Algorithm: `{signature.get('algorithm')}`",
|
|
154
|
+
f"- Payload SHA-256: `{signature.get('payload_sha256')}`",
|
|
155
|
+
f"- Key ID: `{signature.get('key_id')}`",
|
|
156
|
+
f"- Signature: `{signature.get('signature')}`",
|
|
157
|
+
])
|
|
158
|
+
return "\n".join(lines) + "\n"
|
sourcecode/cli.py
CHANGED
|
@@ -184,7 +184,8 @@ COMMAND_TIERS: "tuple[tuple[str, str, tuple[str, ...]], ...]" = (
|
|
|
184
184
|
"cache", "auth", "mcp", "telemetry",
|
|
185
185
|
)),
|
|
186
186
|
("experimental", "shape may change in a minor — do not gate CI on it", (
|
|
187
|
-
"risk", "enrich", "data-exposure", "migrate-recipe",
|
|
187
|
+
"risk", "enrich", "audit-report", "data-exposure", "migrate-recipe",
|
|
188
|
+
"posture", "archetype",
|
|
188
189
|
)),
|
|
189
190
|
# `retrieve` publishes 15+ intents whose answers the other commands already
|
|
190
191
|
# give better: measured on the battery, `security-surface` merely re-states
|
|
@@ -332,7 +333,7 @@ def _build_help_text() -> str:
|
|
|
332
333
|
plan_badge = "[yellow]Free[/yellow] · [dim]ask activate <key>[/dim] to unlock Pro"
|
|
333
334
|
|
|
334
335
|
text = f"""\
|
|
335
|
-
[bold]ASK Engine[/bold] [dim]· CLI: ask[/dim] {plan_badge}
|
|
336
|
+
[bold]ASK Engine {__version__}[/bold] [dim]· CLI: ask[/dim] {plan_badge}
|
|
336
337
|
|
|
337
338
|
Deterministic Java/Spring semantics and reusable structural context for AI coding agents.
|
|
338
339
|
|
|
@@ -344,7 +345,7 @@ of files) in minutes. Semantic analysis itself is sub-second; repo indexing domi
|
|
|
344
345
|
posture . --diff dev:prod [dim]# effective access, two profile sets (exp.)[/dim]
|
|
345
346
|
endpoints . [dim]# endpoints + effective path + policy[/dim]
|
|
346
347
|
spring-audit . [dim]# TX anomalies + security surface[/dim]
|
|
347
|
-
migrate-check . --compact [dim]# Boot 2→3
|
|
348
|
+
migrate-check . --compact [dim]# Boot 2→3 + Java LTS/licensing inventory[/dim]
|
|
348
349
|
risk . [dim]# defects ranked by reach × access (exp.)[/dim]
|
|
349
350
|
|
|
350
351
|
{_remedy_help_block()}
|
|
@@ -359,6 +360,7 @@ of files) in minutes. Semantic analysis itself is sub-second; repo indexing domi
|
|
|
359
360
|
impact <Class> . [dim]# reverse deps → endpoints reached[/dim]
|
|
360
361
|
pr-impact . --files - [dim]# same, scoped to a diff on stdin; gating codes[/dim]
|
|
361
362
|
posture . --since main [dim]# did this branch open an endpoint? (exp.)[/dim]
|
|
363
|
+
audit-report . --format markdown [dim]# signed human evidence bundle (exp.)[/dim]
|
|
362
364
|
verify . [dim]# contract gate, baseline-relative[/dim]
|
|
363
365
|
verify-edit . [dim]# did working-tree edits change behaviour?[/dim]
|
|
364
366
|
[dim]modernize · explain <Class> · validation · export · repo-ir[/dim]
|
|
@@ -3873,6 +3875,11 @@ def prepare_context_cmd(
|
|
|
3873
3875
|
ask prepare-context review-pr . --since main --output review.json
|
|
3874
3876
|
ask prepare-context onboard --llm-prompt
|
|
3875
3877
|
ask prepare-context --task-help
|
|
3878
|
+
|
|
3879
|
+
\b
|
|
3880
|
+
Provenance:
|
|
3881
|
+
JSON output includes evidence_manifest: claim IDs for context files and
|
|
3882
|
+
source-backed rows, plus command inputs, ASK version, git HEAD and freshness.
|
|
3876
3883
|
"""
|
|
3877
3884
|
from sourcecode.prepare_context import TASKS, TaskContextBuilder
|
|
3878
3885
|
|
|
@@ -4417,6 +4424,20 @@ def prepare_context_cmd(
|
|
|
4417
4424
|
_skipped.append("test_gap_discovery")
|
|
4418
4425
|
out["skipped_analyzers"] = _skipped
|
|
4419
4426
|
|
|
4427
|
+
from sourcecode.provenance import build_evidence_manifest as _build_evidence_manifest
|
|
4428
|
+
out["evidence_manifest"] = _build_evidence_manifest(
|
|
4429
|
+
out,
|
|
4430
|
+
command="prepare-context",
|
|
4431
|
+
root=target,
|
|
4432
|
+
inputs={
|
|
4433
|
+
"task": task,
|
|
4434
|
+
"since": since,
|
|
4435
|
+
"format": format or "json",
|
|
4436
|
+
"fast": fast,
|
|
4437
|
+
"llm_prompt": llm_prompt,
|
|
4438
|
+
},
|
|
4439
|
+
)
|
|
4440
|
+
|
|
4420
4441
|
# P0-1: Apply output budget per task — safety net for large repos.
|
|
4421
4442
|
_pc_budget = _prepare_context_budget(task)
|
|
4422
4443
|
|
|
@@ -6908,6 +6929,73 @@ def _render_risk_table(
|
|
|
6908
6929
|
return "\n".join(lines)
|
|
6909
6930
|
|
|
6910
6931
|
|
|
6932
|
+
def _render_enrich_table(
|
|
6933
|
+
data: dict[str, Any],
|
|
6934
|
+
*,
|
|
6935
|
+
rule_filter: Optional[str],
|
|
6936
|
+
band_filter: Optional[str],
|
|
6937
|
+
top_n: int,
|
|
6938
|
+
) -> str:
|
|
6939
|
+
"""Render SARIF-enriched findings as a bounded human triage table."""
|
|
6940
|
+
rule = (rule_filter or "").strip().upper()
|
|
6941
|
+
band = (band_filter or "").strip().lower()
|
|
6942
|
+
rows = []
|
|
6943
|
+
for row in data.get("findings", []):
|
|
6944
|
+
if not isinstance(row, dict):
|
|
6945
|
+
continue
|
|
6946
|
+
external = row.get("external", {}) if isinstance(row.get("external"), dict) else {}
|
|
6947
|
+
rule_id = str(external.get("rule_id") or ",".join(str(r) for r in row.get("rule_ids", [])))
|
|
6948
|
+
if rule and rule != rule_id.upper():
|
|
6949
|
+
continue
|
|
6950
|
+
if band and str(row.get("band", "")).lower() != band:
|
|
6951
|
+
continue
|
|
6952
|
+
rows.append(row)
|
|
6953
|
+
total = len(rows)
|
|
6954
|
+
if top_n > 0:
|
|
6955
|
+
rows = rows[:top_n]
|
|
6956
|
+
|
|
6957
|
+
def _cell(value: object, width: int) -> str:
|
|
6958
|
+
text = "" if value is None else str(value)
|
|
6959
|
+
text = text.replace("\n", " ")
|
|
6960
|
+
if len(text) > width:
|
|
6961
|
+
text = text[: max(0, width - 1)] + "..."
|
|
6962
|
+
return text
|
|
6963
|
+
|
|
6964
|
+
lines = [
|
|
6965
|
+
(
|
|
6966
|
+
f"Enriched findings table: showing {len(rows)}/{total} finding(s)"
|
|
6967
|
+
f"{' (rule=' + rule + ')' if rule else ''}"
|
|
6968
|
+
f"{' (band=' + band + ')' if band else ''}"
|
|
6969
|
+
f"{' (top-n=' + str(top_n) + ')' if top_n > 0 else ''}"
|
|
6970
|
+
)
|
|
6971
|
+
]
|
|
6972
|
+
if not rows:
|
|
6973
|
+
lines.append("No matching enriched findings.")
|
|
6974
|
+
return "\n".join(lines)
|
|
6975
|
+
|
|
6976
|
+
headers = ("band", "score", "tool", "rule", "reach", "auth", "file", "line", "title")
|
|
6977
|
+
widths = (8, 7, 12, 18, 7, 14, 34, 6, 48)
|
|
6978
|
+
lines.append(" | ".join(_cell(h, w).ljust(w) for h, w in zip(headers, widths)))
|
|
6979
|
+
lines.append("-+-".join("-" * w for w in widths))
|
|
6980
|
+
for row in rows:
|
|
6981
|
+
factors = row.get("factors", {}) if isinstance(row.get("factors"), dict) else {}
|
|
6982
|
+
auth = factors.get("auth_verdict", {}) if isinstance(factors.get("auth_verdict"), dict) else {}
|
|
6983
|
+
external = row.get("external", {}) if isinstance(row.get("external"), dict) else {}
|
|
6984
|
+
values = (
|
|
6985
|
+
row.get("band", ""),
|
|
6986
|
+
row.get("severity_effective", ""),
|
|
6987
|
+
external.get("tool", ""),
|
|
6988
|
+
external.get("rule_id", ""),
|
|
6989
|
+
row.get("endpoints_reached", ""),
|
|
6990
|
+
auth.get("value", ""),
|
|
6991
|
+
row.get("source_file", ""),
|
|
6992
|
+
row.get("first_line", "-"),
|
|
6993
|
+
row.get("title", ""),
|
|
6994
|
+
)
|
|
6995
|
+
lines.append(" | ".join(_cell(v, w).ljust(w) for v, w in zip(values, widths)))
|
|
6996
|
+
return "\n".join(lines)
|
|
6997
|
+
|
|
6998
|
+
|
|
6911
6999
|
def _render_migrate_check_table(
|
|
6912
7000
|
data: dict[str, Any],
|
|
6913
7001
|
*,
|
|
@@ -7668,9 +7756,10 @@ def risk_cmd(
|
|
|
7668
7756
|
it, and every row publishes every factor with the authority each came from, so
|
|
7669
7757
|
a reader can disagree with one and keep the rest. An axis that could not be
|
|
7670
7758
|
measured is `unknown`, multiplies by 1.0, and is named in `blind_axes`. The
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7759
|
+
input-path axes are bounded: a query may be merely concatenated, or may carry
|
|
7760
|
+
an annotated HTTP input through positional calls to a known query sink; a
|
|
7761
|
+
reached route may accept an unconstrained body. General taint is still NC-001,
|
|
7762
|
+
in the payload's `non_coverage`.
|
|
7674
7763
|
|
|
7675
7764
|
\b
|
|
7676
7765
|
One row is one **defect** — the same identity `spring-audit` groups by, so the
|
|
@@ -7758,6 +7847,28 @@ def enrich_cmd(
|
|
|
7758
7847
|
limit: int = typer.Option(
|
|
7759
7848
|
50, "--limit", help="How many enriched findings to publish (highest first)."
|
|
7760
7849
|
),
|
|
7850
|
+
table: bool = typer.Option(
|
|
7851
|
+
False,
|
|
7852
|
+
"--table",
|
|
7853
|
+
help="Render a human table instead of JSON/YAML. Use with --rule, --band and --top-n.",
|
|
7854
|
+
),
|
|
7855
|
+
rule_filter: Optional[str] = typer.Option(
|
|
7856
|
+
None,
|
|
7857
|
+
"--rule",
|
|
7858
|
+
"--only",
|
|
7859
|
+
help="With --table, show only enriched findings from one external rule id.",
|
|
7860
|
+
),
|
|
7861
|
+
band_filter: Optional[str] = typer.Option(
|
|
7862
|
+
None,
|
|
7863
|
+
"--band",
|
|
7864
|
+
help="With --table, show only one composed band: critical | high | medium | low.",
|
|
7865
|
+
),
|
|
7866
|
+
top_n: int = typer.Option(
|
|
7867
|
+
0,
|
|
7868
|
+
"--top-n",
|
|
7869
|
+
min=0,
|
|
7870
|
+
help="With --table, show only the top N findings after filtering. 0 means all.",
|
|
7871
|
+
),
|
|
7761
7872
|
min_band: str = typer.Option(
|
|
7762
7873
|
"low",
|
|
7763
7874
|
"--min-band",
|
|
@@ -7779,9 +7890,10 @@ def enrich_cmd(
|
|
|
7779
7890
|
|
|
7780
7891
|
\b
|
|
7781
7892
|
This product does not detect injection, CVEs or secrets, and says so in every
|
|
7782
|
-
security payload (NC-001, NC-002).
|
|
7783
|
-
|
|
7784
|
-
|
|
7893
|
+
security payload (NC-001, NC-002). `risk` contributes one bounded HTTP-input
|
|
7894
|
+
to query-sink factor, not a scanner. A scanner has no bean graph, no profile
|
|
7895
|
+
model and no endpoint id it shares with anything, so its output is a list
|
|
7896
|
+
nobody can order. `enrich` reads that list and answers the question the
|
|
7785
7897
|
scanner cannot: which of these findings sits in code an unauthenticated
|
|
7786
7898
|
request reaches.
|
|
7787
7899
|
|
|
@@ -7813,6 +7925,7 @@ def enrich_cmd(
|
|
|
7813
7925
|
ask enrich . --sarif semgrep.sarif
|
|
7814
7926
|
ask enrich . --sarif codeql.sarif --min-band high
|
|
7815
7927
|
ask enrich . --sarif trivy.sarif --profile prod
|
|
7928
|
+
ask enrich . --sarif semgrep.sarif --table --rule java-sqli --band high --top-n 20
|
|
7816
7929
|
ask enrich /path/to/repo --sarif trivy.sarif -o enriched.json
|
|
7817
7930
|
"""
|
|
7818
7931
|
from sourcecode.sarif import enrich_sarif
|
|
@@ -7835,6 +7948,16 @@ def enrich_cmd(
|
|
|
7835
7948
|
expected="critical|high|medium|low",
|
|
7836
7949
|
)
|
|
7837
7950
|
raise typer.Exit(code=1)
|
|
7951
|
+
if band_filter is not None:
|
|
7952
|
+
band_filter = band_filter.strip().lower()
|
|
7953
|
+
if band_filter is not None and band_filter not in ("critical", "high", "medium", "low"):
|
|
7954
|
+
_emit_error_json(
|
|
7955
|
+
INVALID_INPUT_CODE,
|
|
7956
|
+
f"--band expects critical | high | medium | low (got {band_filter!r}).",
|
|
7957
|
+
hint="Example: --table --band high",
|
|
7958
|
+
expected="critical|high|medium|low",
|
|
7959
|
+
)
|
|
7960
|
+
raise typer.Exit(code=1)
|
|
7838
7961
|
if not Path(sarif).is_file():
|
|
7839
7962
|
_emit_error_json(
|
|
7840
7963
|
INVALID_INPUT_CODE,
|
|
@@ -7844,11 +7967,12 @@ def enrich_cmd(
|
|
|
7844
7967
|
)
|
|
7845
7968
|
raise typer.Exit(code=1)
|
|
7846
7969
|
|
|
7970
|
+
enrich_limit = 100000 if table and (rule_filter or band_filter or top_n) else limit
|
|
7847
7971
|
_prog = Progress()
|
|
7848
7972
|
_prog.start("composing risk factors")
|
|
7849
7973
|
try:
|
|
7850
7974
|
data = enrich_sarif(
|
|
7851
|
-
Path(sarif), path, limit=
|
|
7975
|
+
Path(sarif), path, limit=enrich_limit, min_band=min_band,
|
|
7852
7976
|
profiles=_profile_set(profile),
|
|
7853
7977
|
)
|
|
7854
7978
|
except (ValueError, json.JSONDecodeError) as exc:
|
|
@@ -7863,8 +7987,14 @@ def enrich_cmd(
|
|
|
7863
7987
|
finally:
|
|
7864
7988
|
_prog.stop()
|
|
7865
7989
|
|
|
7990
|
+
if table:
|
|
7991
|
+
output = _render_enrich_table(
|
|
7992
|
+
data, rule_filter=rule_filter, band_filter=band_filter, top_n=top_n
|
|
7993
|
+
)
|
|
7994
|
+
else:
|
|
7995
|
+
output = _serialize_dict(data, format)
|
|
7866
7996
|
_emit_command_output(
|
|
7867
|
-
|
|
7997
|
+
output,
|
|
7868
7998
|
output_path,
|
|
7869
7999
|
copy,
|
|
7870
8000
|
success_msg=(
|
|
@@ -7874,6 +8004,103 @@ def enrich_cmd(
|
|
|
7874
8004
|
)
|
|
7875
8005
|
|
|
7876
8006
|
|
|
8007
|
+
@app.command("audit-report")
|
|
8008
|
+
def audit_report_cmd(
|
|
8009
|
+
path: Path = typer.Argument(
|
|
8010
|
+
Path("."),
|
|
8011
|
+
help="Repository path (default: current directory).",
|
|
8012
|
+
),
|
|
8013
|
+
profile: Optional[str] = typer.Option(
|
|
8014
|
+
None,
|
|
8015
|
+
"--profile",
|
|
8016
|
+
help="Active profile set, comma-separated (e.g. prod or prod,metrics).",
|
|
8017
|
+
),
|
|
8018
|
+
sign_key: Optional[Path] = typer.Option(
|
|
8019
|
+
None,
|
|
8020
|
+
"--sign-key",
|
|
8021
|
+
help="Sign the canonical JSON report with this local HMAC key file.",
|
|
8022
|
+
),
|
|
8023
|
+
output_path: Optional[Path] = typer.Option(
|
|
8024
|
+
None, "--output", "-o", help="Write the report to a file instead of stdout."
|
|
8025
|
+
),
|
|
8026
|
+
format: str = typer.Option(
|
|
8027
|
+
"json", "--format", "-f", help="Output format: json, yaml or markdown."
|
|
8028
|
+
),
|
|
8029
|
+
copy: bool = _copy_option(),
|
|
8030
|
+
) -> None:
|
|
8031
|
+
"""[EXPERIMENTAL] Human audit bundle over existing ASK evidence, optionally signed.
|
|
8032
|
+
|
|
8033
|
+
\b
|
|
8034
|
+
This packages the same evidence `risk` and `posture` already publish into a
|
|
8035
|
+
report a buyer can read: top risks, effective runtime posture, source-linked
|
|
8036
|
+
evidence and non-coverage. It does not certify compliance, choose fixes or
|
|
8037
|
+
assign ROI. JSON includes evidence_manifest: claim IDs mapped to source spans,
|
|
8038
|
+
command inputs, ASK version and freshness.
|
|
8039
|
+
|
|
8040
|
+
\b
|
|
8041
|
+
`--sign-key` adds an HMAC-SHA256 signature over the canonical JSON payload.
|
|
8042
|
+
The key is local and explicit; no network trust service or new PKI is implied.
|
|
8043
|
+
|
|
8044
|
+
\b
|
|
8045
|
+
Examples:
|
|
8046
|
+
ask audit-report .
|
|
8047
|
+
ask audit-report . --profile prod --format markdown
|
|
8048
|
+
ask audit-report . --sign-key audit.key -o audit-report.json
|
|
8049
|
+
"""
|
|
8050
|
+
from sourcecode.audit_report import build_audit_report, render_markdown
|
|
8051
|
+
|
|
8052
|
+
path = _admit_path(path)
|
|
8053
|
+
fmt = format.strip().lower()
|
|
8054
|
+
if fmt not in ("json", "yaml", "markdown"):
|
|
8055
|
+
_emit_error_json(
|
|
8056
|
+
INVALID_INPUT_CODE,
|
|
8057
|
+
f"--format expects json | yaml | markdown (got {format!r}).",
|
|
8058
|
+
hint="Example: ask audit-report . --format markdown",
|
|
8059
|
+
expected="json|yaml|markdown",
|
|
8060
|
+
)
|
|
8061
|
+
raise typer.Exit(code=1)
|
|
8062
|
+
key_bytes = None
|
|
8063
|
+
if sign_key is not None:
|
|
8064
|
+
key_path = Path(sign_key).expanduser()
|
|
8065
|
+
if not key_path.is_file():
|
|
8066
|
+
_emit_error_json(
|
|
8067
|
+
INVALID_INPUT_CODE,
|
|
8068
|
+
f"--sign-key must name a readable file (got {str(sign_key)!r}).",
|
|
8069
|
+
hint="Example: ask audit-report . --sign-key audit.key",
|
|
8070
|
+
expected="an existing key file",
|
|
8071
|
+
)
|
|
8072
|
+
raise typer.Exit(code=1)
|
|
8073
|
+
key_bytes = key_path.read_bytes()
|
|
8074
|
+
if not key_bytes:
|
|
8075
|
+
_emit_error_json(
|
|
8076
|
+
INVALID_INPUT_CODE,
|
|
8077
|
+
"--sign-key file is empty; refusing to publish an unverifiable signature.",
|
|
8078
|
+
hint="Write a random local secret to the key file first.",
|
|
8079
|
+
expected="a non-empty key file",
|
|
8080
|
+
)
|
|
8081
|
+
raise typer.Exit(code=1)
|
|
8082
|
+
|
|
8083
|
+
_prog = Progress()
|
|
8084
|
+
_prog.start("packaging audit evidence")
|
|
8085
|
+
try:
|
|
8086
|
+
data = build_audit_report(
|
|
8087
|
+
path, profiles=_profile_set(profile), sign_key=key_bytes,
|
|
8088
|
+
)
|
|
8089
|
+
finally:
|
|
8090
|
+
_prog.stop()
|
|
8091
|
+
output = render_markdown(data) if fmt == "markdown" else _serialize_dict(data, fmt)
|
|
8092
|
+
_emit_command_output(
|
|
8093
|
+
output,
|
|
8094
|
+
output_path,
|
|
8095
|
+
copy,
|
|
8096
|
+
success_msg=f"audit report written to {output_path}",
|
|
8097
|
+
# A signed artifact cannot be mutated after signing. The report carries
|
|
8098
|
+
# tool/version metadata itself, and `signature.payload_sha256` is over
|
|
8099
|
+
# the exact JSON payload the user receives.
|
|
8100
|
+
stamp_envelope=False,
|
|
8101
|
+
)
|
|
8102
|
+
|
|
8103
|
+
|
|
7877
8104
|
@app.command("migrate-recipe")
|
|
7878
8105
|
def migrate_recipe_cmd(
|
|
7879
8106
|
path: Path = typer.Argument(
|
|
@@ -8145,6 +8372,14 @@ def posture_cmd(
|
|
|
8145
8372
|
"by what it leaves unauthenticated."
|
|
8146
8373
|
),
|
|
8147
8374
|
),
|
|
8375
|
+
env_file: Optional[Path] = typer.Option(
|
|
8376
|
+
None,
|
|
8377
|
+
"--env-file",
|
|
8378
|
+
help=(
|
|
8379
|
+
"With --resolve-environments, read SPRING_PROFILES_ACTIVE or "
|
|
8380
|
+
"spring.profiles.active from an explicit deployment env/dotenv file."
|
|
8381
|
+
),
|
|
8382
|
+
),
|
|
8148
8383
|
property_overrides: Optional[list[str]] = typer.Option(
|
|
8149
8384
|
None,
|
|
8150
8385
|
"--property",
|
|
@@ -8192,6 +8427,7 @@ def posture_cmd(
|
|
|
8192
8427
|
ask posture . --since origin/main --fail-on opened
|
|
8193
8428
|
ask posture . --profile prod --property app.security.enabled=true
|
|
8194
8429
|
ask posture . --resolve-environments
|
|
8430
|
+
ask posture . --resolve-environments --env-file prod.env
|
|
8195
8431
|
"""
|
|
8196
8432
|
from sourcecode.posture import (
|
|
8197
8433
|
FAIL_ON_CHOICES,
|
|
@@ -8266,6 +8502,14 @@ def posture_cmd(
|
|
|
8266
8502
|
hint="ask posture . --profile prod --diff-ref origin/main:HEAD",
|
|
8267
8503
|
)
|
|
8268
8504
|
raise typer.Exit(code=1)
|
|
8505
|
+
if env_file and not resolve_environments_flag:
|
|
8506
|
+
_emit_error_json(
|
|
8507
|
+
INVALID_INPUT_CODE,
|
|
8508
|
+
"--env-file supplies deployment evidence for --resolve-environments; it cannot "
|
|
8509
|
+
"be used with a single profile, profile diff or ref comparison.",
|
|
8510
|
+
hint="ask posture . --resolve-environments --env-file prod.env",
|
|
8511
|
+
)
|
|
8512
|
+
raise typer.Exit(code=1)
|
|
8269
8513
|
|
|
8270
8514
|
# One spinner over the whole dispatch: every branch below resolves the
|
|
8271
8515
|
# conditional bean graph, and `--diff-ref` resolves it twice (once per ref).
|
|
@@ -8280,10 +8524,26 @@ def posture_cmd(
|
|
|
8280
8524
|
INVALID_INPUT_CODE,
|
|
8281
8525
|
"--resolve-environments answers which profile set runs; it cannot be "
|
|
8282
8526
|
"combined with --profile, --diff or --diff-ref/--since, which state one.",
|
|
8283
|
-
hint="Run `ask posture . --resolve-environments` on its own
|
|
8527
|
+
hint="Run `ask posture . --resolve-environments` on its own, or add "
|
|
8528
|
+
"`--env-file prod.env` as deployment evidence.",
|
|
8284
8529
|
)
|
|
8285
8530
|
raise typer.Exit(code=1)
|
|
8286
|
-
|
|
8531
|
+
deployment_signals = None
|
|
8532
|
+
if env_file is not None:
|
|
8533
|
+
from sourcecode.environment_resolution import collect_env_file_signals
|
|
8534
|
+
|
|
8535
|
+
env_path = Path(env_file).expanduser()
|
|
8536
|
+
if not env_path.is_file():
|
|
8537
|
+
_emit_error_json(
|
|
8538
|
+
INVALID_INPUT_CODE,
|
|
8539
|
+
f"--env-file must name a readable file (got {str(env_file)!r}).",
|
|
8540
|
+
hint="ask posture . --resolve-environments --env-file prod.env",
|
|
8541
|
+
)
|
|
8542
|
+
raise typer.Exit(code=1)
|
|
8543
|
+
deployment_signals = collect_env_file_signals(env_path, root=path)
|
|
8544
|
+
data = resolve_environments(
|
|
8545
|
+
path, overrides, deployment_signals=deployment_signals,
|
|
8546
|
+
)
|
|
8287
8547
|
elif ref_comparison:
|
|
8288
8548
|
from sourcecode.git_checkout import RefError
|
|
8289
8549
|
|
|
@@ -8484,9 +8744,9 @@ def migrate_check_cmd(
|
|
|
8484
8744
|
None,
|
|
8485
8745
|
"--target-jdk",
|
|
8486
8746
|
help=(
|
|
8487
|
-
"The JDK you are actually moving to (e.g. 11, 17, 21). Rules
|
|
8488
|
-
"later JDK are not your blockers and are not reported.
|
|
8489
|
-
"
|
|
8747
|
+
"The JDK you are actually moving to (e.g. 11, 17, 21, 25). Rules "
|
|
8748
|
+
"for a later JDK are not your blockers and are not reported. Also "
|
|
8749
|
+
"sets java_lts_inventory.target_lts."
|
|
8490
8750
|
),
|
|
8491
8751
|
),
|
|
8492
8752
|
keep_boot: bool = typer.Option(
|
|
@@ -8546,7 +8806,7 @@ def migrate_check_cmd(
|
|
|
8546
8806
|
help="Caller BFS depth for --blast-radius (1-8, default: 4).",
|
|
8547
8807
|
),
|
|
8548
8808
|
) -> None:
|
|
8549
|
-
"""Spring Boot 2→3 migration readiness
|
|
8809
|
+
"""Spring Boot 2→3 migration readiness + Java LTS/licensing inventory.
|
|
8550
8810
|
|
|
8551
8811
|
\b
|
|
8552
8812
|
Detects:
|
|
@@ -8575,6 +8835,7 @@ def migrate_check_cmd(
|
|
|
8575
8835
|
ask migrate-check . --compact bounded decision summary
|
|
8576
8836
|
ask migrate-check /path/to/repo --format text
|
|
8577
8837
|
ask migrate-check . --min-severity high
|
|
8838
|
+
ask migrate-check . --target-jdk 25 Java LTS target inventory
|
|
8578
8839
|
ask migrate-check . --table --rule MIG-001 --band critical --top-n 20
|
|
8579
8840
|
ask migrate-check . --output migration.json
|
|
8580
8841
|
ask migrate-check . --snapshot --ref sprint-12 persist a readiness point
|
|
@@ -8597,6 +8858,13 @@ def migrate_check_cmd(
|
|
|
8597
8858
|
<repo>/.ask/readiness-history). --trend reads that series and reports
|
|
8598
8859
|
first→last movement — no improving/degrading label, and readiness_score is
|
|
8599
8860
|
flagged not-comparable when the applicable dimension set changed.
|
|
8861
|
+
|
|
8862
|
+
\b
|
|
8863
|
+
Java LTS/licensing inventory:
|
|
8864
|
+
The JSON report includes java_lts_inventory: Java 8/11/17/21/25 evidence
|
|
8865
|
+
from Maven/Gradle, local runtime hints and container files; target-LTS
|
|
8866
|
+
blockers; and explicit Oracle JDK/licensing-review signals. This is
|
|
8867
|
+
inventory evidence, not legal advice, and it never changes readiness_score.
|
|
8600
8868
|
"""
|
|
8601
8869
|
from sourcecode.repository_ir import find_java_files
|
|
8602
8870
|
from sourcecode.migrate_check import run_migrate_check
|
|
@@ -11913,7 +12181,8 @@ HELP_PANELS: "tuple[tuple[str, tuple[str, ...]], ...]" = (
|
|
|
11913
12181
|
"cache", "auth", "mcp", "telemetry", "baseline",
|
|
11914
12182
|
)),
|
|
11915
12183
|
("Experimental — shape may change", (
|
|
11916
|
-
"risk", "enrich", "data-exposure", "migrate-recipe",
|
|
12184
|
+
"risk", "enrich", "audit-report", "data-exposure", "migrate-recipe",
|
|
12185
|
+
"archetype", "retrieve",
|
|
11917
12186
|
)),
|
|
11918
12187
|
)
|
|
11919
12188
|
|