@metasession.co/devaudit-cli 0.3.28 → 0.3.29

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.28",
3
+ "version": "0.3.29",
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.28",
36
+ "@metasession.co/devaudit-plugin-sdk": "^0.3.29",
37
37
  "ajv": "^8.20.0",
38
38
  "commander": "^12.1.0",
39
39
  "consola": "^3.2.3",
@@ -161,78 +161,108 @@ accepted = []
161
161
  unresolved = []
162
162
  seen_advisories = set()
163
163
 
164
- for finding_name, finding in audit["vulnerabilities"].items():
164
+ vulnerabilities = audit["vulnerabilities"]
165
+
166
+
167
+ def concrete_advisories(finding_name, trail=()):
168
+ if finding_name in trail:
169
+ fail(f"audit vulnerability graph contains a cycle: {' -> '.join((*trail, finding_name))}")
170
+ finding = vulnerabilities.get(finding_name)
165
171
  if not isinstance(finding, dict):
166
- fail(f"audit vulnerability {finding_name} must be an object")
172
+ fail(f"audit vulnerability {finding_name} is missing or is not an object")
167
173
  via = finding.get("via", [])
168
174
  if not isinstance(via, list):
169
175
  fail(f"audit vulnerability {finding_name} has invalid via data")
170
- advisory_objects = [
171
- item for item in via
172
- if isinstance(item, dict) and item.get("severity") in ("high", "critical")
173
- ]
174
- if finding.get("severity") in ("high", "critical") and not advisory_objects:
175
- fail(f"high/critical audit vulnerability {finding_name} has no resolvable advisory object")
176
-
177
- nodes = finding.get("nodes", [])
178
- if advisory_objects and (not isinstance(nodes, list) or not nodes):
179
- fail(f"audit vulnerability {finding_name} has no installed package-lock node")
180
-
181
- for advisory in advisory_objects:
182
- package = advisory.get("dependency") or finding_name
183
- advisory_range = advisory.get("range")
184
- if not isinstance(package, str) or not package:
185
- fail(f"advisory in {finding_name} has no package name")
186
- if not isinstance(advisory_range, str) or not advisory_range:
187
- fail(f"advisory {advisory_id(advisory)} has no vulnerable range")
188
- identifier = advisory_id(advisory)
189
- for node_path in nodes:
190
- if not isinstance(node_path, str):
191
- fail(f"advisory {identifier} has invalid package-lock node")
192
- node = packages.get(node_path)
193
- if not isinstance(node, dict) or not isinstance(node.get("version"), str):
194
- fail(f"cannot resolve installed version for {node_path}")
195
- if package_name_from_path(node_path) != package:
196
- fail(f"audit package {package} does not match package-lock node {node_path}")
197
- version = node["version"]
198
- introduced_by = introducer_for(node_path, packages)
199
- decision_key = (identifier, node_path)
200
- if decision_key in seen_advisories:
201
- continue
202
- seen_advisories.add(decision_key)
203
- context = {
204
- "advisoryId": identifier,
205
- "severity": advisory["severity"],
206
- "package": package,
207
- "vulnerableRange": advisory_range,
208
- "vulnerableVersion": version,
209
- "dependencyPath": node_path,
210
- "introducedBy": introduced_by,
176
+
177
+ resolved = []
178
+ for index, item in enumerate(via):
179
+ if isinstance(item, str):
180
+ if item not in vulnerabilities:
181
+ fail(f"audit vulnerability {finding_name} references missing vulnerability {item}")
182
+ resolved.extend(concrete_advisories(item, (*trail, finding_name)))
183
+ elif isinstance(item, dict):
184
+ if item.get("severity") in ("high", "critical"):
185
+ resolved.append((finding_name, index, item))
186
+ else:
187
+ fail(f"audit vulnerability {finding_name} has invalid via entry")
188
+ return resolved
189
+
190
+
191
+ advisory_sources = {}
192
+ for finding_name, finding in vulnerabilities.items():
193
+ if not isinstance(finding, dict):
194
+ fail(f"audit vulnerability {finding_name} must be an object")
195
+ resolved = concrete_advisories(finding_name)
196
+ if finding.get("severity") in ("high", "critical") and not resolved:
197
+ fail(f"high/critical audit vulnerability {finding_name} has no reachable advisory object")
198
+ for source_name, source_index, advisory in resolved:
199
+ advisory_sources.setdefault((source_name, source_index), {
200
+ "advisory": advisory,
201
+ "affectedFindings": set(),
202
+ })["affectedFindings"].add(finding_name)
203
+
204
+ for (source_name, _source_index), source in advisory_sources.items():
205
+ advisory = source["advisory"]
206
+ source_finding = vulnerabilities[source_name]
207
+ nodes = source_finding.get("nodes", [])
208
+ if not isinstance(nodes, list) or not nodes:
209
+ fail(f"audit vulnerability {source_name} has no installed package-lock node")
210
+
211
+ package = advisory.get("dependency") or source_name
212
+ advisory_range = advisory.get("range")
213
+ if not isinstance(package, str) or not package:
214
+ fail(f"advisory in {source_name} has no package name")
215
+ if not isinstance(advisory_range, str) or not advisory_range:
216
+ fail(f"advisory {advisory_id(advisory)} has no vulnerable range")
217
+ identifier = advisory_id(advisory)
218
+ for node_path in nodes:
219
+ if not isinstance(node_path, str):
220
+ fail(f"advisory {identifier} has invalid package-lock node")
221
+ node = packages.get(node_path)
222
+ if not isinstance(node, dict) or not isinstance(node.get("version"), str):
223
+ fail(f"cannot resolve installed version for {node_path}")
224
+ if package_name_from_path(node_path) != package:
225
+ fail(f"audit package {package} does not match package-lock node {node_path}")
226
+ version = node["version"]
227
+ introduced_by = introducer_for(node_path, packages)
228
+ decision_key = (identifier, node_path)
229
+ if decision_key in seen_advisories:
230
+ continue
231
+ seen_advisories.add(decision_key)
232
+ context = {
233
+ "advisoryId": identifier,
234
+ "severity": advisory["severity"],
235
+ "package": package,
236
+ "vulnerableRange": advisory_range,
237
+ "vulnerableVersion": version,
238
+ "dependencyPath": node_path,
239
+ "introducedBy": introduced_by,
240
+ "affectedFindings": sorted(source["affectedFindings"]),
241
+ }
242
+ match = next((item for item in validated_exceptions if all(
243
+ item[field] == context[field]
244
+ for field in ("advisoryId", "package", "vulnerableRange", "vulnerableVersion", "dependencyPath", "introducedBy")
245
+ )), None)
246
+ if match is None:
247
+ unresolved.append(context)
248
+ print(
249
+ f"Unaccepted {context['severity']} risk: {identifier} "
250
+ f"({package}@{version} at {node_path})",
251
+ file=sys.stderr,
252
+ )
253
+ else:
254
+ acceptance = {
255
+ field: match[field]
256
+ for field in ("approvedAt", "expiresAt", "approvedBy", "reason", "remediationIssue")
211
257
  }
212
- match = next((item for item in validated_exceptions if all(
213
- item[field] == context[field]
214
- for field in ("advisoryId", "package", "vulnerableRange", "vulnerableVersion", "dependencyPath", "introducedBy")
215
- )), None)
216
- if match is None:
217
- unresolved.append(context)
218
- print(
219
- f"Unaccepted {context['severity']} risk: {identifier} "
220
- f"({package}@{version} at {node_path})",
221
- file=sys.stderr,
222
- )
223
- else:
224
- acceptance = {
225
- field: match[field]
226
- for field in ("approvedAt", "expiresAt", "approvedBy", "reason", "remediationIssue")
227
- }
228
- if "compensatingControls" in match:
229
- acceptance["compensatingControls"] = match["compensatingControls"]
230
- accepted.append({**context, "acceptance": acceptance})
231
- print(
232
- f"Accepted temporary risk: {identifier} ({package}@{version}) "
233
- f"until {match['expiresAt']} by {match['approvedBy']}; "
234
- f"remediation {match['remediationIssue']}",
235
- )
258
+ if "compensatingControls" in match:
259
+ acceptance["compensatingControls"] = match["compensatingControls"]
260
+ accepted.append({**context, "acceptance": acceptance})
261
+ print(
262
+ f"Accepted temporary risk: {identifier} ({package}@{version}) "
263
+ f"until {match['expiresAt']} by {match['approvedBy']}; "
264
+ f"remediation {match['remediationIssue']}",
265
+ )
236
266
 
237
267
  result = {
238
268
  "schemaVersion": 1,
@@ -40,6 +40,47 @@ write_empty_audit() {
40
40
  printf '{"vulnerabilities":{}}\n' > "$WORK/dependency-audit.json"
41
41
  }
42
42
 
43
+ write_meta_audit() {
44
+ cat > "$WORK/dependency-audit.json" <<'JSON'
45
+ {
46
+ "vulnerabilities": {
47
+ "eslint": {
48
+ "name": "eslint",
49
+ "severity": "high",
50
+ "via": ["@eslint/eslintrc"],
51
+ "nodes": ["node_modules/eslint"]
52
+ },
53
+ "@eslint/eslintrc": {
54
+ "name": "@eslint/eslintrc",
55
+ "severity": "high",
56
+ "via": ["minimatch"],
57
+ "nodes": ["node_modules/@eslint/eslintrc"]
58
+ },
59
+ "minimatch": {
60
+ "name": "minimatch",
61
+ "severity": "high",
62
+ "via": ["brace-expansion"],
63
+ "nodes": ["node_modules/@eslint/eslintrc/node_modules/minimatch"]
64
+ },
65
+ "brace-expansion": {
66
+ "name": "brace-expansion",
67
+ "severity": "high",
68
+ "via": [
69
+ {
70
+ "name": "brace-expansion",
71
+ "dependency": "brace-expansion",
72
+ "url": "https://github.com/advisories/GHSA-meta-chain",
73
+ "severity": "high",
74
+ "range": "<=5.0.7"
75
+ }
76
+ ],
77
+ "nodes": ["node_modules/@eslint/eslintrc/node_modules/brace-expansion"]
78
+ }
79
+ }
80
+ }
81
+ JSON
82
+ }
83
+
43
84
  write_lock() {
44
85
  cat > "$WORK/package-lock.json" <<'JSON'
45
86
  {
@@ -53,6 +94,21 @@ write_lock() {
53
94
  JSON
54
95
  }
55
96
 
97
+ write_meta_lock() {
98
+ cat > "$WORK/package-lock.json" <<'JSON'
99
+ {
100
+ "lockfileVersion": 3,
101
+ "packages": {
102
+ "": {"name": "fixture"},
103
+ "node_modules/eslint": {"version": "9.0.0"},
104
+ "node_modules/@eslint/eslintrc": {"version": "3.0.0"},
105
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {"version": "9.0.0"},
106
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {"version": "2.0.1"}
107
+ }
108
+ }
109
+ JSON
110
+ }
111
+
56
112
  write_exception() {
57
113
  cat > "$WORK/compliance/security/accepted-vulnerabilities.json" <<'JSON'
58
114
  {
@@ -193,4 +249,33 @@ write_exception
193
249
  printf '{not-json}\n' > "$WORK/compliance/security/accepted-vulnerabilities.json"
194
250
  expect_failure
195
251
 
252
+ write_meta_audit
253
+ write_meta_lock
254
+ rm -f "$WORK/compliance/security/accepted-vulnerabilities.json"
255
+ expect_failure
256
+ jq -e '
257
+ .summary.unresolved == 1 and
258
+ .unresolved[0].advisoryId == "GHSA-meta-chain" and
259
+ .unresolved[0].dependencyPath == "node_modules/@eslint/eslintrc/node_modules/brace-expansion" and
260
+ .unresolved[0].affectedFindings == ["@eslint/eslintrc", "brace-expansion", "eslint", "minimatch"]
261
+ ' "$WORK/dependency-risk-evaluation.json" >/dev/null
262
+
263
+ write_meta_audit
264
+ jq '.vulnerabilities.minimatch.via = ["missing-package"]' \
265
+ "$WORK/dependency-audit.json" > "$WORK/changed.json"
266
+ mv "$WORK/changed.json" "$WORK/dependency-audit.json"
267
+ expect_failure
268
+
269
+ write_meta_audit
270
+ jq '.vulnerabilities["brace-expansion"].via = ["eslint"]' \
271
+ "$WORK/dependency-audit.json" > "$WORK/changed.json"
272
+ mv "$WORK/changed.json" "$WORK/dependency-audit.json"
273
+ expect_failure
274
+
275
+ write_meta_audit
276
+ jq '.vulnerabilities["brace-expansion"].via = []' \
277
+ "$WORK/dependency-audit.json" > "$WORK/changed.json"
278
+ mv "$WORK/changed.json" "$WORK/dependency-audit.json"
279
+ expect_failure
280
+
196
281
  echo "evaluate-npm-audit: PASS"
package/sdlc/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metasession.co/devaudit-sdlc",
3
- "version": "0.3.28",
3
+ "version": "0.3.29",
4
4
  "description": "DevAudit SDLC CLI Engine — cross-agent terminal-driven SDLC orchestrator.",
5
5
  "bin": {
6
6
  "devaudit-sdlc": "./src/bin/devaudit-sdlc.js"