@metasession.co/devaudit-cli 0.3.24 → 0.3.26

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-cli",
3
- "version": "0.3.24",
3
+ "version": "0.3.26",
4
4
  "description": "DevAudit CLI — installs, syncs, and operates the Metasession SDLC across consumer projects.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^0.8.2",
36
- "@metasession.co/devaudit-plugin-sdk": "^0.3.24",
36
+ "@metasession.co/devaudit-plugin-sdk": "^0.3.26",
37
37
  "ajv": "^8.20.0",
38
38
  "commander": "^12.1.0",
39
39
  "consola": "^3.2.3",
@@ -27,15 +27,23 @@ import re
27
27
  title = os.environ.get("PR_TITLE", "")
28
28
  body = os.environ.get("PR_BODY", "")
29
29
  text = f"{title}\n{body}"
30
+ scope = r"(?:REQ-\d+|v\d+\.\d+\.\d+|v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?)"
31
+
32
+ # An explicit release field is authoritative. Do not let a historical REQ in
33
+ # the surrounding prose override it, and fail closed if its value is invalid.
34
+ explicit_field = re.search(r"^[ \t\-*]*release:\s*(.*)$", text, re.IGNORECASE | re.MULTILINE)
35
+ if explicit_field:
36
+ value = explicit_field.group(1).strip()
37
+ match = re.match(rf"`?({scope})`?(?![A-Za-z0-9_.-])", value, re.IGNORECASE)
38
+ if match:
39
+ print(match.group(1))
40
+ raise SystemExit(0)
41
+
30
42
  patterns = [
31
- r"^[ \t\-*]*Release:\s*((?:REQ-\d+)|(?:v\d+\.\d+\.\d+)|(?:v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?))\b",
32
- r"\b(REQ-\d+)\b",
33
- r"\b(v\d+\.\d+\.\d+)\b",
34
- r"\b(v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?)\b",
43
+ rf"\b({scope})\b",
35
44
  ]
36
- flags = re.MULTILINE
37
45
  for pattern in patterns:
38
- match = re.search(pattern, text, flags)
46
+ match = re.search(pattern, text, re.IGNORECASE)
39
47
  if match:
40
48
  print(match.group(1))
41
49
  break
@@ -223,5 +223,49 @@ assert_exit "bare-date release without exception marker fails" 1 "$CODE"
223
223
  assert_grep "exception marker failure is clear" 'explicit standalone housekeeping exception' "$OUT"
224
224
  echo ""
225
225
 
226
+ echo "--- Test 10: Markdown standalone scope wins over historical REQ prose ---"
227
+ make_fixture "$WORK/test10" "chore: standalone housekeeping release"
228
+ cat > "$WORK/test10/compliance/standalone-housekeeping/STANDALONE-HOUSEKEEPING-${BARE_DATE_VERSION}.json" <<EOF
229
+ {
230
+ "schemaVersion": 1,
231
+ "version": "${BARE_DATE_VERSION}",
232
+ "releaseMode": "standalone_housekeeping",
233
+ "reason": "Reviewed operational change cannot wait for the next tracked requirement release."
234
+ }
235
+ EOF
236
+ OUT="$WORK/test10.out"
237
+ if run_check "$WORK/test10" "$OUT" env PR_TITLE="Standalone housekeeping promotion: ${BARE_DATE_VERSION}" PR_BODY=$"## Scope\n- Release: \`${BARE_DATE_VERSION}\`\n- Historical context: REQ-094 close-out recovery.\n"; then
238
+ CODE=0
239
+ else
240
+ CODE=$?
241
+ fi
242
+ assert_exit "Markdown standalone scope wins over historical REQ prose" 0 "$CODE"
243
+ assert_grep "Markdown standalone scope is verified" "Release Scope Integrity verified for ${BARE_DATE_VERSION}" "$OUT"
244
+ echo ""
245
+
246
+ echo "--- Test 11: Markdown tracked scope passes ---"
247
+ make_fixture "$WORK/test11" "[REQ-093] feat: ship release"
248
+ OUT="$WORK/test11.out"
249
+ if run_check "$WORK/test11" "$OUT" env PR_TITLE="release: REQ-093" PR_BODY=$'## Scope\n- Release: `REQ-093`\n'; then
250
+ CODE=0
251
+ else
252
+ CODE=$?
253
+ fi
254
+ assert_exit "Markdown tracked scope passes" 0 "$CODE"
255
+ assert_grep "Markdown tracked scope is verified" 'Release Scope Integrity verified for REQ-093' "$OUT"
256
+ echo ""
257
+
258
+ echo "--- Test 12: malformed explicit scope fails without falling back ---"
259
+ make_fixture "$WORK/test12" "[REQ-093] feat: ship release"
260
+ OUT="$WORK/test12.out"
261
+ if run_check "$WORK/test12" "$OUT" env PR_TITLE="prepare release" PR_BODY=$'## Scope\n- Release: `not-a-release`\n- Historical context: REQ-093\n'; then
262
+ CODE=0
263
+ else
264
+ CODE=$?
265
+ fi
266
+ assert_exit "malformed explicit scope fails" 1 "$CODE"
267
+ assert_grep "malformed explicit scope has clear error" 'do not declare a release scope' "$OUT"
268
+ echo ""
269
+
226
270
  echo "=== check-release-pr-scope.test.sh: $PASS passed, $FAIL failed ==="
227
271
  [ "$FAIL" -eq 0 ]
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env bash
2
+ # evaluate-npm-audit.sh -- evaluate npm audit high/critical advisories against
3
+ # narrowly governed, repository-owned temporary risk acceptances.
4
+ #
5
+ # The helper intentionally owns both decision logic and machine-readable output.
6
+ # Workflow templates must not parse its human log output or convert evaluation
7
+ # failures into successful "unknown" results.
8
+
9
+ set -euo pipefail
10
+
11
+ AUDIT_PATH="dependency-audit.json"
12
+ LOCK_PATH="package-lock.json"
13
+ EXCEPTIONS_PATH="compliance/security/accepted-vulnerabilities.json"
14
+ OUTPUT_PATH="dependency-risk-evaluation.json"
15
+
16
+ usage() {
17
+ echo "Usage: $0 [--audit <path>] [--lock <path>] [--exceptions <path>] [--output <path>]" >&2
18
+ exit 2
19
+ }
20
+
21
+ while [ "$#" -gt 0 ]; do
22
+ case "$1" in
23
+ --audit) AUDIT_PATH="${2:-}"; shift 2 ;;
24
+ --lock) LOCK_PATH="${2:-}"; shift 2 ;;
25
+ --exceptions) EXCEPTIONS_PATH="${2:-}"; shift 2 ;;
26
+ --output) OUTPUT_PATH="${2:-}"; shift 2 ;;
27
+ -h|--help) usage ;;
28
+ *) echo "Error: unknown argument: $1" >&2; usage ;;
29
+ esac
30
+ done
31
+
32
+ for required_path in "$AUDIT_PATH" "$LOCK_PATH"; do
33
+ [ -n "$required_path" ] && [ -f "$required_path" ] || {
34
+ echo "Error: required input is missing: ${required_path:-<empty>}" >&2
35
+ exit 2
36
+ }
37
+ done
38
+
39
+ python3 - "$AUDIT_PATH" "$LOCK_PATH" "$EXCEPTIONS_PATH" "$OUTPUT_PATH" <<'PY'
40
+ import json
41
+ import os
42
+ import sys
43
+ from datetime import date
44
+
45
+ audit_path, lock_path, exceptions_path, output_path = sys.argv[1:]
46
+
47
+
48
+ def fail(message):
49
+ print(f"Dependency-risk evaluation error: {message}", file=sys.stderr)
50
+ raise SystemExit(2)
51
+
52
+
53
+ def load_json(path, label):
54
+ try:
55
+ with open(path, encoding="utf-8") as handle:
56
+ return json.load(handle)
57
+ except (OSError, json.JSONDecodeError) as error:
58
+ fail(f"cannot read {label} {path}: {error}")
59
+
60
+
61
+ def package_name_from_path(path):
62
+ marker = "node_modules/"
63
+ if marker not in path:
64
+ fail(f"package-lock node path is not a node_modules path: {path}")
65
+ tail = path.rsplit(marker, 1)[1]
66
+ parts = tail.split("/")
67
+ if not parts or not parts[0]:
68
+ fail(f"cannot derive package name from node path: {path}")
69
+ return "/".join(parts[:2]) if parts[0].startswith("@") else parts[0]
70
+
71
+
72
+ def introducer_for(node_path, packages):
73
+ parent_marker = "/node_modules/"
74
+ if parent_marker not in node_path:
75
+ return "direct"
76
+ parent_path = node_path.rsplit(parent_marker, 1)[0]
77
+ parent = packages.get(parent_path)
78
+ if not isinstance(parent, dict) or not isinstance(parent.get("version"), str):
79
+ fail(f"cannot resolve introducing dependency for {node_path}")
80
+ return f"{package_name_from_path(parent_path)}@{parent['version']}"
81
+
82
+
83
+ def advisory_id(advisory):
84
+ url = advisory.get("url")
85
+ if not isinstance(url, str) or "/" not in url:
86
+ fail("high/critical audit advisory is missing a URL")
87
+ value = url.rsplit("/", 1)[-1]
88
+ if not value:
89
+ fail("high/critical audit advisory has an empty advisory ID")
90
+ return value
91
+
92
+
93
+ def valid_date(value, field, index):
94
+ if not isinstance(value, str):
95
+ fail(f"exception {index} has no valid {field}")
96
+ try:
97
+ return date.fromisoformat(value)
98
+ except ValueError:
99
+ fail(f"exception {index} has invalid {field}: {value}")
100
+
101
+
102
+ audit = load_json(audit_path, "npm audit JSON")
103
+ lock = load_json(lock_path, "package-lock JSON")
104
+ if not isinstance(audit, dict) or not isinstance(audit.get("vulnerabilities"), dict):
105
+ fail("npm audit JSON must contain a vulnerabilities object")
106
+ if not isinstance(lock, dict) or not isinstance(lock.get("packages"), dict):
107
+ fail("package-lock JSON must contain a packages object")
108
+
109
+ if os.path.exists(exceptions_path):
110
+ exception_document = load_json(exceptions_path, "risk acceptance JSON")
111
+ if not isinstance(exception_document, dict) or exception_document.get("schemaVersion") != 1:
112
+ fail("risk acceptance JSON must be an object with schemaVersion 1")
113
+ exceptions = exception_document.get("exceptions")
114
+ if not isinstance(exceptions, list):
115
+ fail("risk acceptance JSON must contain an exceptions array")
116
+ else:
117
+ exceptions = []
118
+
119
+ required_fields = (
120
+ "advisoryId",
121
+ "package",
122
+ "vulnerableRange",
123
+ "vulnerableVersion",
124
+ "dependencyPath",
125
+ "introducedBy",
126
+ "approvedAt",
127
+ "expiresAt",
128
+ "approvedBy",
129
+ "reason",
130
+ "remediationIssue",
131
+ )
132
+ today = date.today()
133
+ validated_exceptions = []
134
+ seen_exception_keys = set()
135
+ for index, item in enumerate(exceptions, start=1):
136
+ if not isinstance(item, dict):
137
+ fail(f"exception {index} must be an object")
138
+ missing = [field for field in required_fields if not isinstance(item.get(field), str) or not item[field].strip()]
139
+ if missing:
140
+ fail(f"exception {index} is missing required field(s): {', '.join(missing)}")
141
+ approved_at = valid_date(item["approvedAt"], "approvedAt", index)
142
+ expires_at = valid_date(item["expiresAt"], "expiresAt", index)
143
+ if approved_at > today:
144
+ fail(f"exception {index} approvedAt cannot be in the future")
145
+ if expires_at <= today:
146
+ fail(f"exception {index} is expired on {item['expiresAt']}")
147
+ if not item["remediationIssue"].startswith("https://github.com/"):
148
+ fail(f"exception {index} remediationIssue must be a GitHub issue URL")
149
+ key = (item["advisoryId"], item["dependencyPath"])
150
+ if key in seen_exception_keys:
151
+ fail(f"duplicate exception for {item['advisoryId']} at {item['dependencyPath']}")
152
+ seen_exception_keys.add(key)
153
+ validated_exceptions.append(item)
154
+
155
+ packages = lock["packages"]
156
+ accepted = []
157
+ unresolved = []
158
+ seen_advisories = set()
159
+
160
+ for finding_name, finding in audit["vulnerabilities"].items():
161
+ if not isinstance(finding, dict):
162
+ fail(f"audit vulnerability {finding_name} must be an object")
163
+ via = finding.get("via", [])
164
+ if not isinstance(via, list):
165
+ fail(f"audit vulnerability {finding_name} has invalid via data")
166
+ advisory_objects = [
167
+ item for item in via
168
+ if isinstance(item, dict) and item.get("severity") in ("high", "critical")
169
+ ]
170
+ if finding.get("severity") in ("high", "critical") and not advisory_objects:
171
+ fail(f"high/critical audit vulnerability {finding_name} has no resolvable advisory object")
172
+
173
+ nodes = finding.get("nodes", [])
174
+ if advisory_objects and (not isinstance(nodes, list) or not nodes):
175
+ fail(f"audit vulnerability {finding_name} has no installed package-lock node")
176
+
177
+ for advisory in advisory_objects:
178
+ package = advisory.get("dependency") or finding_name
179
+ advisory_range = advisory.get("range")
180
+ if not isinstance(package, str) or not package:
181
+ fail(f"advisory in {finding_name} has no package name")
182
+ if not isinstance(advisory_range, str) or not advisory_range:
183
+ fail(f"advisory {advisory_id(advisory)} has no vulnerable range")
184
+ identifier = advisory_id(advisory)
185
+ for node_path in nodes:
186
+ if not isinstance(node_path, str):
187
+ fail(f"advisory {identifier} has invalid package-lock node")
188
+ node = packages.get(node_path)
189
+ if not isinstance(node, dict) or not isinstance(node.get("version"), str):
190
+ fail(f"cannot resolve installed version for {node_path}")
191
+ if package_name_from_path(node_path) != package:
192
+ fail(f"audit package {package} does not match package-lock node {node_path}")
193
+ version = node["version"]
194
+ introduced_by = introducer_for(node_path, packages)
195
+ decision_key = (identifier, node_path)
196
+ if decision_key in seen_advisories:
197
+ continue
198
+ seen_advisories.add(decision_key)
199
+ context = {
200
+ "advisoryId": identifier,
201
+ "severity": advisory["severity"],
202
+ "package": package,
203
+ "vulnerableRange": advisory_range,
204
+ "vulnerableVersion": version,
205
+ "dependencyPath": node_path,
206
+ "introducedBy": introduced_by,
207
+ }
208
+ match = next((item for item in validated_exceptions if all(
209
+ item[field] == context[field]
210
+ for field in ("advisoryId", "package", "vulnerableRange", "vulnerableVersion", "dependencyPath", "introducedBy")
211
+ )), None)
212
+ if match is None:
213
+ unresolved.append(context)
214
+ print(
215
+ f"Unaccepted {context['severity']} risk: {identifier} "
216
+ f"({package}@{version} at {node_path})",
217
+ file=sys.stderr,
218
+ )
219
+ else:
220
+ accepted.append({**context, "acceptance": {
221
+ field: match[field]
222
+ for field in ("approvedAt", "expiresAt", "approvedBy", "reason", "remediationIssue")
223
+ }})
224
+ print(
225
+ f"Accepted temporary risk: {identifier} ({package}@{version}) "
226
+ f"until {match['expiresAt']} by {match['approvedBy']}; "
227
+ f"remediation {match['remediationIssue']}",
228
+ )
229
+
230
+ result = {
231
+ "schemaVersion": 1,
232
+ "auditFile": audit_path,
233
+ "lockFile": lock_path,
234
+ "accepted": accepted,
235
+ "unresolved": unresolved,
236
+ "summary": {
237
+ "accepted": len(accepted),
238
+ "unresolved": len(unresolved),
239
+ },
240
+ }
241
+ with open(output_path, "w", encoding="utf-8") as handle:
242
+ json.dump(result, handle, indent=2)
243
+ handle.write("\n")
244
+
245
+ print(f"Dependency risk evaluation: {len(accepted)} accepted, {len(unresolved)} unresolved.")
246
+ raise SystemExit(1 if unresolved else 0)
247
+ PY
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bash
2
+ # Regression coverage for advisory-specific, fail-closed npm risk evaluation.
3
+
4
+ set -euo pipefail
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ HELPER="$SCRIPT_DIR/evaluate-npm-audit.sh"
8
+ WORK="$(mktemp -d)"
9
+ trap 'rm -rf "$WORK"' EXIT
10
+
11
+ mkdir -p "$WORK/compliance/security"
12
+
13
+ write_audit() {
14
+ cat > "$WORK/dependency-audit.json" <<'JSON'
15
+ {
16
+ "vulnerabilities": {
17
+ "postcss": {
18
+ "name": "postcss",
19
+ "severity": "high",
20
+ "via": [
21
+ {
22
+ "name": "postcss",
23
+ "dependency": "postcss",
24
+ "url": "https://github.com/advisories/GHSA-test-high",
25
+ "severity": "high",
26
+ "range": "<=8.5.11"
27
+ }
28
+ ],
29
+ "nodes": ["node_modules/next/node_modules/postcss"]
30
+ }
31
+ }
32
+ }
33
+ JSON
34
+ }
35
+
36
+ write_empty_audit() {
37
+ printf '{"vulnerabilities":{}}\n' > "$WORK/dependency-audit.json"
38
+ }
39
+
40
+ write_lock() {
41
+ cat > "$WORK/package-lock.json" <<'JSON'
42
+ {
43
+ "lockfileVersion": 3,
44
+ "packages": {
45
+ "": {"name": "fixture"},
46
+ "node_modules/next": {"version": "16.2.11"},
47
+ "node_modules/next/node_modules/postcss": {"version": "8.4.31"}
48
+ }
49
+ }
50
+ JSON
51
+ }
52
+
53
+ write_exception() {
54
+ cat > "$WORK/compliance/security/accepted-vulnerabilities.json" <<'JSON'
55
+ {
56
+ "schemaVersion": 1,
57
+ "exceptions": [
58
+ {
59
+ "advisoryId": "GHSA-test-high",
60
+ "package": "postcss",
61
+ "vulnerableRange": "<=8.5.11",
62
+ "vulnerableVersion": "8.4.31",
63
+ "dependencyPath": "node_modules/next/node_modules/postcss",
64
+ "introducedBy": "next@16.2.11",
65
+ "approvedAt": "2026-07-01",
66
+ "expiresAt": "2026-12-31",
67
+ "approvedBy": "reviewer@example.test",
68
+ "reason": "Fixture acceptance while the introducing dependency awaits an upstream repair.",
69
+ "remediationIssue": "https://github.com/example/repo/issues/1"
70
+ }
71
+ ]
72
+ }
73
+ JSON
74
+ }
75
+
76
+ run_helper() {
77
+ bash "$HELPER" \
78
+ --audit "$WORK/dependency-audit.json" \
79
+ --lock "$WORK/package-lock.json" \
80
+ --exceptions "$WORK/compliance/security/accepted-vulnerabilities.json" \
81
+ --output "$WORK/dependency-risk-evaluation.json"
82
+ }
83
+
84
+ expect_success() {
85
+ if ! run_helper; then
86
+ echo "Expected evaluation to succeed" >&2
87
+ exit 1
88
+ fi
89
+ }
90
+
91
+ expect_failure() {
92
+ if run_helper >/dev/null 2>&1; then
93
+ echo "Expected evaluation to fail" >&2
94
+ exit 1
95
+ fi
96
+ }
97
+
98
+ write_audit
99
+ write_lock
100
+ rm -f "$WORK/compliance/security/accepted-vulnerabilities.json"
101
+ expect_failure
102
+
103
+ write_empty_audit
104
+ expect_success
105
+ jq -e '.summary.accepted == 0 and .summary.unresolved == 0' "$WORK/dependency-risk-evaluation.json" >/dev/null
106
+
107
+ write_audit
108
+ write_exception
109
+ expect_success
110
+ jq -e '.summary.accepted == 1 and .summary.unresolved == 0 and .accepted[0].acceptance.remediationIssue == "https://github.com/example/repo/issues/1"' \
111
+ "$WORK/dependency-risk-evaluation.json" >/dev/null
112
+
113
+ jq '.exceptions[0].advisoryId = "GHSA-wrong"' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
114
+ mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
115
+ expect_failure
116
+
117
+ write_exception
118
+ write_audit
119
+ jq '.exceptions[0].vulnerableRange = "<8.5.10"' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
120
+ mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
121
+ expect_failure
122
+
123
+ write_exception
124
+ write_audit
125
+ jq '.exceptions[0].vulnerableVersion = "8.5.15"' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
126
+ mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
127
+ expect_failure
128
+
129
+ write_exception
130
+ write_audit
131
+ jq '.exceptions[0].dependencyPath = "node_modules/postcss"' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
132
+ mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
133
+ expect_failure
134
+
135
+ write_exception
136
+ write_audit
137
+ jq '.exceptions[0].introducedBy = "next@16.2.10"' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
138
+ mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
139
+ expect_failure
140
+
141
+ write_exception
142
+ write_audit
143
+ jq '.exceptions[0].expiresAt = "2026-01-01"' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
144
+ mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
145
+ expect_failure
146
+
147
+ write_exception
148
+ write_audit
149
+ jq '.exceptions[0].expiresAt = "not-a-date"' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
150
+ mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
151
+ expect_failure
152
+
153
+ write_exception
154
+ write_audit
155
+ jq 'del(.exceptions[0].approvedAt)' "$WORK/compliance/security/accepted-vulnerabilities.json" > "$WORK/changed.json"
156
+ mv "$WORK/changed.json" "$WORK/compliance/security/accepted-vulnerabilities.json"
157
+ expect_failure
158
+
159
+ write_exception
160
+ write_audit
161
+ jq '.vulnerabilities.postcss.via += [{"name":"postcss","dependency":"postcss","url":"https://github.com/advisories/GHSA-second-high","severity":"high","range":"<=8.5.11"}]' \
162
+ "$WORK/dependency-audit.json" > "$WORK/changed.json"
163
+ mv "$WORK/changed.json" "$WORK/dependency-audit.json"
164
+ expect_failure
165
+
166
+ write_audit
167
+ write_exception
168
+ printf '{not-json}\n' > "$WORK/dependency-audit.json"
169
+ expect_failure
170
+
171
+ write_audit
172
+ write_exception
173
+ printf '{not-json}\n' > "$WORK/compliance/security/accepted-vulnerabilities.json"
174
+ expect_failure
175
+
176
+ echo "evaluate-npm-audit: PASS"
@@ -19,6 +19,7 @@
19
19
  # - `sast-results.json` (Semgrep) — high/critical findings count
20
20
  # - `dependency-audit.json` (npm audit / pip-audit) — high/critical
21
21
  # vulnerability count
22
+ # - `dependency-risk-evaluation.json` — governed temporary acceptances
22
23
  # - `gate-outcomes.json` (DevAudit-Installer v0.1.29) — per-gate
23
24
  # pass/fail/skip status
24
25
  #
@@ -91,6 +92,25 @@ dep_audit_summary() {
91
92
  echo "$TOTAL total vulnerability/ies · $HIGH high · $CRITICAL critical"
92
93
  }
93
94
 
95
+ risk_acceptance_summary() {
96
+ if [ ! -f dependency-risk-evaluation.json ]; then
97
+ echo "No dependency-risk decision artifact was present for this run."
98
+ return
99
+ fi
100
+ if ! command -v jq >/dev/null 2>&1; then
101
+ echo "REPLACE — \`jq\` not available; inspect \`dependency-risk-evaluation.json\` manually"
102
+ return
103
+ fi
104
+ jq -r '
105
+ if (.accepted | length) == 0 then
106
+ "No temporary high/critical dependency risks were accepted."
107
+ else
108
+ .accepted[] | "- Accepted \(.advisoryId) — \(.package)@\(.vulnerableVersion), introduced by \(.introducedBy), expires \(.acceptance.expiresAt), owner \(.acceptance.approvedBy), remediation \(.acceptance.remediationIssue)"
109
+ end
110
+ ' dependency-risk-evaluation.json 2>/dev/null \
111
+ || echo "REPLACE — could not parse \`dependency-risk-evaluation.json\`"
112
+ }
113
+
94
114
  # Helper: gate-outcomes.json (DevAudit-Installer v0.1.29 onwards).
95
115
  gate_outcomes_summary() {
96
116
  if [ ! -f gate-outcomes.json ]; then
@@ -107,6 +127,7 @@ gate_outcomes_summary() {
107
127
 
108
128
  SAST_SUMMARY=$(sast_summary)
109
129
  DEP_SUMMARY=$(dep_audit_summary)
130
+ RISK_ACCEPTANCE_SUMMARY=$(risk_acceptance_summary)
110
131
  GATES=$(gate_outcomes_summary)
111
132
 
112
133
  cat <<EOF
@@ -133,7 +154,7 @@ generated_by: "generate-security-summary.sh (DevAudit-Installer#116)"
133
154
 
134
155
  **Release shape:** $SHAPE
135
156
  **Generated:** $TODAY
136
- **Source data:** \`sast-results.json\` + \`dependency-audit.json\` + \`gate-outcomes.json\` (this CI run)
157
+ **Source data:** \`sast-results.json\` + \`dependency-audit.json\` + \`dependency-risk-evaluation.json\` + \`gate-outcomes.json\` (this CI run)
137
158
 
138
159
  ## SAST findings (Semgrep)
139
160
 
@@ -145,7 +166,11 @@ $SAST_SUMMARY
145
166
 
146
167
  $DEP_SUMMARY
147
168
 
148
- > **Policy:** the dependency-audit gate fails the build at \`high\` or \`critical\` severity. If this release shipped, both are zero.
169
+ > **Policy:** the dependency-audit gate fails at \`high\` or \`critical\` severity unless every affected advisory has an exact, unexpired, reviewer-attributed acceptance. The raw audit count may therefore be non-zero only when the governed decision record below identifies the accepted risk.
170
+
171
+ ## Governed temporary dependency risks
172
+
173
+ $RISK_ACCEPTANCE_SUMMARY
149
174
 
150
175
  ## Gate outcomes (per CI run)
151
176
 
@@ -53,5 +53,38 @@ assert_contains "spaced separator in sign-off table" '| --- | --- | --- | --- |'
53
53
  assert_not_contains_regex "no compact separators remain" '^\|[-:|]+\|$' "$OUTPUT"
54
54
  echo
55
55
 
56
+ DIR2="$TMPDIR_BASE/test2"
57
+ mkdir -p "$DIR2"
58
+ cd "$DIR2"
59
+ printf '{ "results": [] }\n' > sast-results.json
60
+ printf '{ "vulnerabilities": { "postcss": { "severity": "high" } } }\n' > dependency-audit.json
61
+ printf '{ "sast": "PASS", "dependency_audit": "PASS" }\n' > gate-outcomes.json
62
+ cat > dependency-risk-evaluation.json <<'JSON'
63
+ {
64
+ "schemaVersion": 1,
65
+ "accepted": [
66
+ {
67
+ "advisoryId": "GHSA-fixture",
68
+ "package": "postcss",
69
+ "vulnerableVersion": "8.4.31",
70
+ "introducedBy": "next@16.2.11",
71
+ "acceptance": {
72
+ "expiresAt": "2026-12-31",
73
+ "approvedBy": "reviewer@example.test",
74
+ "remediationIssue": "https://github.com/example/repo/issues/1"
75
+ }
76
+ }
77
+ ],
78
+ "unresolved": []
79
+ }
80
+ JSON
81
+
82
+ echo "Test 2: accepted dependency risk is identified in generated evidence"
83
+ OUTPUT=$(bash "$HELPER" "REQ-322" 2>&1)
84
+ assert_contains "accepted advisory emitted" "Accepted GHSA-fixture" "$OUTPUT"
85
+ assert_contains "accepted risk owner emitted" "owner reviewer@example.test" "$OUTPUT"
86
+ assert_contains "accepted risk remediation emitted" "https://github.com/example/repo/issues/1" "$OUTPUT"
87
+ echo
88
+
56
89
  echo "Result: $PASS passed, $FAIL failed"
57
90
  [ "$FAIL" = "0" ]