@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.
|
|
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.
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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