cra-audit 2.1.1 → 2.3.0
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/CHANGELOG.md +24 -0
- package/README.md +64 -3
- package/package.json +1 -1
- package/src/cli.js +5 -1
- package/src/commands/audit.js +85 -69
- package/src/commands/readiness.js +17 -6
- package/src/commands/sbom.js +122 -111
- package/src/commands/vex.js +15 -7
- package/src/core/auditor.js +55 -5
- package/src/core/license-checker.js +10 -3
- package/src/core/manifest-reader.js +374 -0
- package/src/core/osv.js +30 -14
- package/src/core/project-source.js +62 -0
- package/src/core/readiness.js +26 -2
- package/src/core/sbom-reader.js +297 -0
- package/src/core/sbom-validator.js +9 -3
- package/src/core/vex.js +3 -3
- package/src/core/vuln-scanner.js +34 -11
- package/src/reporters/console.js +20 -2
- package/src/reporters/sarif.js +40 -14
package/src/reporters/sarif.js
CHANGED
|
@@ -29,10 +29,12 @@ const SECURITY_SEVERITY = { critical: 9.5, high: 8.0, moderate: 5.5, low: 2.0, i
|
|
|
29
29
|
function buildSarif(report, projectRoot, policy = {}) {
|
|
30
30
|
const rules = new Map();
|
|
31
31
|
const results = [];
|
|
32
|
-
const
|
|
32
|
+
const vulns = report.sections.vulnerabilities;
|
|
33
|
+
// An input SBOM is searched for purls; manifest findings carry their own location.
|
|
34
|
+
const inputFile = vulns && vulns.input && vulns.input.format !== 'manifest' ? vulns.input.file : null;
|
|
35
|
+
const lock = lockfileLocator(projectRoot, inputFile);
|
|
33
36
|
const assessments = normalizeAllowlist(policy);
|
|
34
37
|
|
|
35
|
-
const vulns = report.sections.vulnerabilities;
|
|
36
38
|
if (vulns && vulns.ok) {
|
|
37
39
|
for (const finding of vulns.vulnerabilities) {
|
|
38
40
|
for (const source of finding.sources) {
|
|
@@ -46,7 +48,7 @@ function buildSarif(report, projectRoot, policy = {}) {
|
|
|
46
48
|
: finding.malicious || source.kev || ['critical', 'high'].includes(source.severity || finding.severity) ? 'error'
|
|
47
49
|
: (source.severity || finding.severity) === 'moderate' ? 'warning' : 'note',
|
|
48
50
|
message: { text: vulnerabilityMessage(finding, source) },
|
|
49
|
-
locations: [lock.locate(finding.name, finding.version)],
|
|
51
|
+
locations: [finding.location ? lock.at(finding.location) : lock.locate(finding.name, finding.version, finding.purl)],
|
|
50
52
|
partialFingerprints: { 'craAudit/v1': `${ruleId}:${finding.name}@${finding.version || ''}` },
|
|
51
53
|
};
|
|
52
54
|
if (accepted) {
|
|
@@ -70,14 +72,17 @@ function buildSarif(report, projectRoot, policy = {}) {
|
|
|
70
72
|
['cra-audit/license-missing', s.missing, 'warning', (c) => `${c.name}@${c.version} has no documented license (TR-03183-2 §5.2.2).`],
|
|
71
73
|
];
|
|
72
74
|
for (const [ruleId, list, level, text] of groups) {
|
|
73
|
-
|
|
75
|
+
// Manifests (requirements.txt, go.mod…) never declare licenses: that gap is
|
|
76
|
+
// reported once by the audit, not as one alert per dependency.
|
|
77
|
+
const relevant = ruleId === 'cra-audit/license-missing' ? list.filter((c) => !c.location) : list;
|
|
78
|
+
if (!relevant.length) continue;
|
|
74
79
|
rules.set(ruleId, licenseRule(ruleId));
|
|
75
|
-
for (const c of
|
|
80
|
+
for (const c of relevant) {
|
|
76
81
|
results.push({
|
|
77
82
|
ruleId,
|
|
78
83
|
level,
|
|
79
84
|
message: { text: text(c) },
|
|
80
|
-
locations: [lock.locate(c.name, c.version)],
|
|
85
|
+
locations: [c.location ? lock.at(c.location) : lock.locate(c.name, c.version, c.purl)],
|
|
81
86
|
partialFingerprints: { 'craAudit/v1': `${ruleId}:${c.name}@${c.version}` },
|
|
82
87
|
});
|
|
83
88
|
}
|
|
@@ -163,13 +168,18 @@ function licenseRule(ruleId) {
|
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
/**
|
|
166
|
-
* Finds the line of `name@version` in the project lockfile
|
|
167
|
-
* points at the exact entry. Falls back to
|
|
171
|
+
* Finds the line of `name@version` in the project lockfile (or of the purl in
|
|
172
|
+
* an input SBOM) so each alert points at the exact entry. Falls back to line 1.
|
|
168
173
|
*/
|
|
169
|
-
function lockfileLocator(projectRoot) {
|
|
174
|
+
function lockfileLocator(projectRoot, inputFile = null) {
|
|
170
175
|
const repoRoot = findRepoRoot(projectRoot);
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
let file;
|
|
177
|
+
if (inputFile) {
|
|
178
|
+
file = path.resolve(inputFile);
|
|
179
|
+
} else {
|
|
180
|
+
const parsed = parseLockfile(projectRoot);
|
|
181
|
+
file = parsed.lockfileName ? path.join(projectRoot, parsed.lockfileName) : path.join(projectRoot, 'package.json');
|
|
182
|
+
}
|
|
173
183
|
let lines = [];
|
|
174
184
|
try {
|
|
175
185
|
lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
|
|
@@ -178,8 +188,8 @@ function lockfileLocator(projectRoot) {
|
|
|
178
188
|
}
|
|
179
189
|
const uri = path.relative(repoRoot, file).split(path.sep).join('/');
|
|
180
190
|
|
|
181
|
-
const locate = (name, version) => {
|
|
182
|
-
const line = findEntryLine(lines, name, version);
|
|
191
|
+
const locate = (name, version, purl) => {
|
|
192
|
+
const line = inputFile ? findPurlLine(lines, purl) : findEntryLine(lines, name, version);
|
|
183
193
|
return {
|
|
184
194
|
physicalLocation: {
|
|
185
195
|
artifactLocation: { uri, uriBaseId: '%SRCROOT%' },
|
|
@@ -187,7 +197,14 @@ function lockfileLocator(projectRoot) {
|
|
|
187
197
|
},
|
|
188
198
|
};
|
|
189
199
|
};
|
|
190
|
-
|
|
200
|
+
// A known file + line (components read from manifests).
|
|
201
|
+
const at = ({ file: rel, line }) => ({
|
|
202
|
+
physicalLocation: {
|
|
203
|
+
artifactLocation: { uri: path.relative(repoRoot, path.resolve(projectRoot, rel)).split(path.sep).join('/'), uriBaseId: '%SRCROOT%' },
|
|
204
|
+
region: { startLine: line || 1 },
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
return { repoRoot, locate, at };
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
function findEntryLine(lines, name, version) {
|
|
@@ -209,6 +226,15 @@ function findEntryLine(lines, name, version) {
|
|
|
209
226
|
return 1;
|
|
210
227
|
}
|
|
211
228
|
|
|
229
|
+
/** Line of an SBOM that holds the purl (qualifiers may follow it). */
|
|
230
|
+
function findPurlLine(lines, purl) {
|
|
231
|
+
if (!purl) return 1;
|
|
232
|
+
const escaped = purl.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
233
|
+
const exact = new RegExp(`${escaped}(?![\\w.+-])`);
|
|
234
|
+
const index = lines.findIndex((line) => exact.test(line));
|
|
235
|
+
return index === -1 ? 1 : index + 1;
|
|
236
|
+
}
|
|
237
|
+
|
|
212
238
|
/** Nearest folder with a .git entry: SARIF paths must be repository-relative. */
|
|
213
239
|
function findRepoRoot(start) {
|
|
214
240
|
let dir = path.resolve(start);
|