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.
@@ -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 lock = lockfileLocator(projectRoot);
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
- if (!list.length) continue;
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 list) {
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 so each alert
167
- * points at the exact entry. Falls back to package.json line 1.
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
- const parsed = parseLockfile(projectRoot);
172
- const file = parsed.lockfileName ? path.join(projectRoot, parsed.lockfileName) : path.join(projectRoot, 'package.json');
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
- return { repoRoot, locate };
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);