codesentry 0.2.1 → 0.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.
Files changed (3) hide show
  1. package/README.md +57 -16
  2. package/dist/index.js +1135 -127
  3. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -150,8 +150,8 @@ var commandInjectionRule = {
150
150
  var withScanOptions = (command) => command.option("--json", "exibe o resultado em JSON").option("--tests", "inclui arquivos de teste na an\xE1lise (por padr\xE3o s\xE3o ignorados)");
151
151
 
152
152
  // src/commands/scan/scan-runner.ts
153
- import { writeFile } from "fs/promises";
154
- import { join as join3 } from "path";
153
+ import { writeFile as writeFile2 } from "fs/promises";
154
+ import { join as join4 } from "path";
155
155
  import chalk2 from "chalk";
156
156
  import { Listr } from "listr2";
157
157
 
@@ -186,9 +186,12 @@ var mergeScanResults = (nativeResult, semgrepResult) => ({
186
186
  findings: [...nativeResult.findings, ...semgrepResult.findings],
187
187
  durationMs: nativeResult.durationMs + semgrepResult.durationMs,
188
188
  engines: {
189
+ ...nativeResult.engines,
190
+ ...semgrepResult.engines,
189
191
  codesentry: nativeResult.engines?.codesentry ?? nativeResult.scannedFiles,
190
192
  semgrep: semgrepResult.engines?.semgrep ?? semgrepResult.scannedFiles
191
- }
193
+ },
194
+ warnings: [...nativeResult.warnings ?? [], ...semgrepResult.warnings ?? []]
192
195
  });
193
196
  var finalizeScanResult = (result, dependencyAuditCoverage2 = false) => ({
194
197
  ...result,
@@ -204,9 +207,14 @@ var SEVERITY_COLOR = {
204
207
  critical: (text2) => chalk.bgRed.white(text2)
205
208
  };
206
209
  var semgrepCoverage = (result) => result.engines?.semgrep === void 0 ? void 0 : `CodeSentry: ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep: ${result.engines.semgrep} arquivo(s)`;
207
- var dependencyAuditCoverage = (result) => typeof result.engines?.dependencyAudit === "number" ? `Dependency audit: ${result.engines.dependencyAudit} pacote(s) via npm audit` : void 0;
210
+ var dependencyAuditCoverage = (result) => typeof result.engines?.dependencyAudit === "number" ? `Dependency audit: ${result.engines.dependencyAudit} pacote(s) considerado(s)` : void 0;
208
211
  var osvCoverage = (result) => result.engines?.osv ? `OSV.dev: ${result.engines.osv.checked}/${result.engines.osv.total} verificados` : void 0;
209
- var coverageParts = (result) => [semgrepCoverage(result), dependencyAuditCoverage(result), osvCoverage(result)].filter(
212
+ var nvdCoverage = (result) => {
213
+ const nvd = result.engines?.nvd;
214
+ if (!nvd || nvd.total === 0) return void 0;
215
+ return `NVD: ${nvd.total} CVE(s); ${nvd.enriched} enriquecido(s); ${nvd.notFound} sem resultado; ${nvd.failed} falha(s); ${nvd.cacheHits} cache hit(s)`;
216
+ };
217
+ var coverageParts = (result) => [semgrepCoverage(result), dependencyAuditCoverage(result), osvCoverage(result), nvdCoverage(result)].filter(
210
218
  (part) => part !== void 0
211
219
  );
212
220
  var coverageText = (result) => {
@@ -225,9 +233,9 @@ var printCleanReport = (result, coverage) => {
225
233
  console.log(chalk.green(`Nenhum problema encontrado (${result.scannedFiles} arquivos analisados).${coverage}`));
226
234
  printNotes(result);
227
235
  };
228
- var findingsTable = (result) => {
236
+ var findingsTable = (findings) => {
229
237
  const table = new Table({ head: ["Severity", "Rule", "File", "Line", "Message"] });
230
- for (const finding of result.findings) {
238
+ for (const finding of findings) {
231
239
  const colorize = SEVERITY_COLOR[finding.severity];
232
240
  table.push([
233
241
  colorize(finding.severity),
@@ -239,8 +247,132 @@ var findingsTable = (result) => {
239
247
  }
240
248
  return table;
241
249
  };
250
+ var titleCaseMetric = (value) => value.toLowerCase().split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
251
+ var labeledLines = (pairs, format) => pairs.filter((pair) => Boolean(pair[1])).map(([label, value]) => format(label, value));
252
+ var cvssMetricLines = (cvss) => labeledLines(
253
+ [
254
+ ["Vetor de ataque", cvss.attackVector],
255
+ ["Complexidade", cvss.attackComplexity],
256
+ ["Privil\xE9gios necess\xE1rios", cvss.privilegesRequired],
257
+ ["Intera\xE7\xE3o do usu\xE1rio", cvss.userInteraction],
258
+ ["Escopo", cvss.scope],
259
+ ["Confidentiality", cvss.confidentialityImpact],
260
+ ["Integrity", cvss.integrityImpact],
261
+ ["Availability", cvss.availabilityImpact]
262
+ ],
263
+ (label, value) => ` ${label}: ${titleCaseMetric(value)}`
264
+ );
265
+ var cvssLines = (cvss) => [
266
+ ` Severidade: ${cvss.severity ?? "n\xE3o informada"}`,
267
+ ` CVSS: ${cvss.score}`,
268
+ ` Vers\xE3o CVSS: ${cvss.version}`,
269
+ ` Vetor: ${cvss.vectorString}`,
270
+ ...cvssMetricLines(cvss)
271
+ ];
272
+ var cvssBlock = (data) => data.cvss ? cvssLines(data.cvss) : [" CVSS: registro encontrado, ainda sem m\xE9trica CVSS"];
273
+ var metadataLines = (data, summary) => labeledLines(
274
+ [
275
+ ["CWE", data.cwes.join(", ") || void 0],
276
+ ["Descri\xE7\xE3o NVD", data.description !== summary ? data.description : void 0],
277
+ ["Publicado", data.published],
278
+ ["Modificado", data.lastModified]
279
+ ],
280
+ (label, value) => ` ${label}: ${value}`
281
+ );
282
+ var cisaKevLines = (kev) => [
283
+ ` CISA KEV desde: ${kev.addedAt}`,
284
+ ...labeledLines(
285
+ [
286
+ ["Nome CISA", kev.vulnerabilityName],
287
+ ["Prazo CISA", kev.actionDue],
288
+ ["A\xE7\xE3o CISA", kev.requiredAction]
289
+ ],
290
+ (label, value) => ` ${label}: ${value}`
291
+ )
292
+ ];
293
+ var cisaSsvcLines = (ssvc) => {
294
+ const parts = labeledLines(
295
+ [
296
+ ["explora\xE7\xE3o", ssvc.exploitation],
297
+ ["automatiz\xE1vel", ssvc.automatable],
298
+ ["impacto t\xE9cnico", ssvc.technicalImpact]
299
+ ],
300
+ (label, value) => `${label}=${value}`
301
+ );
302
+ return labeledLines(
303
+ [
304
+ ["CISA SSVC", parts.length > 0 ? parts.join("; ") : void 0],
305
+ ["CISA SSVC atualizado", ssvc.timestamp]
306
+ ],
307
+ (label, value) => ` ${label}: ${value}`
308
+ );
309
+ };
310
+ var cisaLines = (cisa) => [
311
+ ...cisa?.kev ? cisaKevLines(cisa.kev) : [],
312
+ ...cisa?.ssvc ? cisaSsvcLines(cisa.ssvc) : []
313
+ ];
314
+ var referencesLines = (references) => {
315
+ const shown = references.slice(0, 5);
316
+ const extraCount = references.length - shown.length;
317
+ return labeledLines(
318
+ [
319
+ ["Refer\xEAncias", shown.length > 0 ? shown.map((reference) => reference.url).join(", ") : void 0],
320
+ ["Refer\xEAncias adicionais", extraCount > 0 ? `${extraCount}` : void 0]
321
+ ],
322
+ (label, value) => ` ${label}: ${value}`
323
+ );
324
+ };
325
+ var foundNvdLines = (result, summary) => {
326
+ const { data } = result;
327
+ return [
328
+ ` CVE: ${result.cveId}`,
329
+ ` Status NVD: ${data.vulnerabilityStatus ?? "n\xE3o informado"}`,
330
+ ...cvssBlock(data),
331
+ ...metadataLines(data, summary),
332
+ ...cisaLines(data.cisa),
333
+ ...referencesLines(data.references)
334
+ ];
335
+ };
336
+ var httpStatusSuffix = (httpStatus) => httpStatus ? ` HTTP ${httpStatus}` : "";
337
+ var nvdLines = (result, summary) => {
338
+ if (result.status === "found") return foundNvdLines(result, summary);
339
+ if (result.status === "not-found") return [` CVE: ${result.cveId}`, " NVD: sem resultado"];
340
+ return [
341
+ ` CVE: ${result.cveId}`,
342
+ ` NVD: falha ao consultar (${result.error.kind}${httpStatusSuffix(result.error.httpStatus)})`
343
+ ];
344
+ };
345
+ var aliasesLine = (aliases) => aliases.length > 0 ? [`Aliases: ${aliases.join(", ")}`] : [];
346
+ var summaryLine = (summary, label) => summary ? [`${label}: ${summary}`] : [];
347
+ var nvdResultLines = (results, summary) => (results ?? []).flatMap((result) => ["", ...nvdLines(result, summary)]);
348
+ var sourcesLabel = (source, foundNvd) => source === "osv" ? `OSV${foundNvd ? ", NVD" : ""}` : "npm";
349
+ var dependencyBlock = (finding) => {
350
+ const dependency = finding.dependency;
351
+ if (!dependency) return "";
352
+ const foundNvd = (dependency.nvd ?? []).some((result) => result.status === "found");
353
+ const lines = [
354
+ `Pacote: ${dependency.package.name}`,
355
+ `Vers\xE3o instalada: ${dependency.package.installedVersion}`,
356
+ `Vers\xE3o corrigida: ${dependency.package.fixedVersions.join(" ou ") || "n\xE3o informada"}`,
357
+ `${dependency.advisory.source.toUpperCase()}: ${dependency.advisory.id ?? "identificador n\xE3o informado"}`,
358
+ ...aliasesLine(dependency.advisory.aliases),
359
+ `Severidade do finding: ${finding.severity.toUpperCase()}`,
360
+ ...summaryLine(dependency.advisory.summary, "Descri\xE7\xE3o OSV/npm"),
361
+ ...nvdResultLines(dependency.nvd, dependency.advisory.summary),
362
+ `Fontes: ${sourcesLabel(dependency.advisory.source, foundNvd)}`
363
+ ];
364
+ return lines.join("\n");
365
+ };
366
+ var printDependencyFindings = (findings) => {
367
+ if (findings.length === 0) return;
368
+ console.log(chalk.bold("\nDepend\xEAncias vulner\xE1veis"));
369
+ console.log(findings.map(dependencyBlock).join("\n\n"));
370
+ };
242
371
  var printFindingsReport = (result, coverage) => {
243
- console.log(findingsTable(result).toString());
372
+ const codeFindings = result.findings.filter((finding) => !finding.dependency);
373
+ const dependencyFindings = result.findings.filter((finding) => finding.dependency);
374
+ if (codeFindings.length) console.log(findingsTable(codeFindings).toString());
375
+ printDependencyFindings(dependencyFindings);
244
376
  console.log(
245
377
  chalk.bold(
246
378
  `
@@ -264,6 +396,7 @@ var toJsonReport = (result) => {
264
396
  };
265
397
 
266
398
  // src/reporters/markdown.reporter.ts
399
+ var LOGO_URL = "https://raw.githubusercontent.com/Ivan-ReisDev/code-sentry/main/docs/assets/logo.png";
267
400
  var SEVERITY_ORDER = ["critical", "high", "medium", "low"];
268
401
  var SEVERITY_LABEL = /* @__PURE__ */ new Map([
269
402
  ["critical", "Critical"],
@@ -271,7 +404,14 @@ var SEVERITY_LABEL = /* @__PURE__ */ new Map([
271
404
  ["medium", "Medium"],
272
405
  ["low", "Low"]
273
406
  ]);
407
+ var SEVERITY_EMOJI = /* @__PURE__ */ new Map([
408
+ ["critical", "\u{1F534}"],
409
+ ["high", "\u{1F7E0}"],
410
+ ["medium", "\u{1F7E1}"],
411
+ ["low", "\u{1F535}"]
412
+ ]);
274
413
  var severityLabel = (severity) => SEVERITY_LABEL.get(severity) ?? severity;
414
+ var severityBadge = (severity) => `${SEVERITY_EMOJI.get(severity) ?? ""} ${severityLabel(severity)}`;
275
415
  var escapeCell = (text2) => text2.replaceAll("|", "\\|");
276
416
  var groupBy = (items, keyOf) => {
277
417
  const map = /* @__PURE__ */ new Map();
@@ -290,7 +430,7 @@ var findingsTable2 = (findings) => {
290
430
  return lines;
291
431
  };
292
432
  var severitySection = (severity, findings) => {
293
- const lines = [`## ${severityLabel(severity)} (${findings.length})`, ""];
433
+ const lines = [`## ${severityBadge(severity)} (${findings.length})`, ""];
294
434
  const byRule = groupBy(findings, (f) => f.ruleId);
295
435
  for (const ruleId of [...byRule.keys()].sort()) {
296
436
  const ruleFindings = byRule.get(ruleId) ?? [];
@@ -298,24 +438,38 @@ var severitySection = (severity, findings) => {
298
438
  }
299
439
  return lines;
300
440
  };
441
+ var semgrepCoverageLine = (result) => result.engines?.semgrep === void 0 ? [] : [
442
+ `- **Cobertura por motor:** CodeSentry ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep ${result.engines.semgrep} arquivos`
443
+ ];
444
+ var dependencyAuditConsideredLine = (result) => typeof result.engines?.dependencyAudit === "number" ? [`- **Depend\xEAncias consideradas:** ${result.engines.dependencyAudit}`] : [];
445
+ var nvdCoverageLine = (result) => {
446
+ const nvd = result.engines?.nvd;
447
+ if (!nvd || nvd.total === 0) return [];
448
+ return [
449
+ `- **Cobertura NVD:** ${nvd.enriched}/${nvd.total} enriquecidos; ${nvd.notFound} sem resultado; ${nvd.failed} falhas; ${nvd.cacheHits} cache hits`
450
+ ];
451
+ };
452
+ var dependencyAuditNoteLine = (result) => result.engines?.dependencyAudit === false ? [`- **Nota:** ${DEPENDENCY_AUDIT_NOTE}`] : [];
301
453
  var reportHeader = (result, generatedAt) => [
454
+ `<p align="center"><img src="${LOGO_URL}" alt="CodeSentry" width="320"></p>`,
455
+ "",
302
456
  "# Relat\xF3rio CodeSentry",
303
457
  "",
304
458
  `- **Gerado em:** ${generatedAt.toISOString()}`,
305
459
  `- **Arquivos analisados:** ${result.scannedFiles}`,
306
- ...result.engines?.semgrep === void 0 ? [] : [
307
- `- **Cobertura por motor:** CodeSentry ${result.engines.codesentry ?? result.scannedFiles} JS/TS; Semgrep ${result.engines.semgrep} arquivos`
308
- ],
460
+ ...semgrepCoverageLine(result),
461
+ ...dependencyAuditConsideredLine(result),
462
+ ...nvdCoverageLine(result),
309
463
  `- **Dura\xE7\xE3o:** ${result.durationMs}ms`,
310
464
  `- **Total de problemas:** ${result.findings.length}`,
311
- ...result.engines?.dependencyAudit === false ? [`- **Nota:** ${DEPENDENCY_AUDIT_NOTE}`] : [],
465
+ ...dependencyAuditNoteLine(result),
312
466
  ""
313
467
  ];
314
468
  var warningsSection = (result) => (result.warnings ?? []).length === 0 ? [] : ["## Avisos", "", ...(result.warnings ?? []).map((warning) => `- ${warning}`), ""];
315
469
  var summaryTable = (bySeverity) => {
316
470
  const lines = ["## Resumo por severidade", "", "| Severidade | Quantidade |", "| --- | --- |"];
317
471
  for (const severity of SEVERITY_ORDER) {
318
- lines.push(`| ${severityLabel(severity)} | ${(bySeverity.get(severity) ?? []).length} |`);
472
+ lines.push(`| ${severityBadge(severity)} | ${(bySeverity.get(severity) ?? []).length} |`);
319
473
  }
320
474
  lines.push("");
321
475
  return lines;
@@ -330,6 +484,147 @@ var severitySections = (bySeverity) => {
330
484
  }
331
485
  return lines;
332
486
  };
487
+ var labeledLines2 = (pairs, format) => pairs.filter((pair) => Boolean(pair[1])).map(([label, value]) => format(label, value));
488
+ var mdCvssMetricLines = (cvss) => labeledLines2(
489
+ [
490
+ ["Attack Vector", cvss.attackVector],
491
+ ["Attack Complexity", cvss.attackComplexity],
492
+ ["Privileges Required", cvss.privilegesRequired],
493
+ ["User Interaction", cvss.userInteraction],
494
+ ["Scope", cvss.scope],
495
+ ["Confidentiality", cvss.confidentialityImpact],
496
+ ["Integrity", cvss.integrityImpact],
497
+ ["Availability", cvss.availabilityImpact]
498
+ ],
499
+ (key, value) => `${key}=${value}`
500
+ );
501
+ var mdCvssLines = (cvss) => {
502
+ const lines = [
503
+ `- **CVSS:** ${cvss.score} (${cvss.severity ?? "sem severidade"}), vers\xE3o ${cvss.version}`,
504
+ `- **Vetor:** \`${cvss.vectorString}\``
505
+ ];
506
+ const metrics = mdCvssMetricLines(cvss);
507
+ return metrics.length ? [...lines, `- **M\xE9tricas:** ${metrics.join("; ")}`] : lines;
508
+ };
509
+ var mdCvssBlock = (data) => data.cvss ? mdCvssLines(data.cvss) : ["- **CVSS:** registro encontrado, ainda sem m\xE9trica CVSS"];
510
+ var mdMetadataLines = (data, summary) => labeledLines2(
511
+ [
512
+ ["CWE", data.cwes.join(", ") || void 0],
513
+ ["Descri\xE7\xE3o NVD", data.description !== summary ? data.description : void 0],
514
+ ["Publicado", data.published],
515
+ ["Modificado", data.lastModified]
516
+ ],
517
+ (label, value) => `- **${label}:** ${value}`
518
+ );
519
+ var mdCisaKevLines = (kev) => [
520
+ `- **CISA KEV:** \u26A0\uFE0F inclu\xEDdo em ${kev.addedAt}`,
521
+ ...labeledLines2(
522
+ [
523
+ ["Nome CISA", kev.vulnerabilityName],
524
+ ["Prazo CISA", kev.actionDue],
525
+ ["A\xE7\xE3o requerida", kev.requiredAction]
526
+ ],
527
+ (label, value) => `- **${label}:** ${value}`
528
+ )
529
+ ];
530
+ var mdCisaSsvcLines = (ssvc) => {
531
+ const parts = labeledLines2(
532
+ [
533
+ ["explora\xE7\xE3o", ssvc.exploitation],
534
+ ["automatiz\xE1vel", ssvc.automatable],
535
+ ["impacto t\xE9cnico", ssvc.technicalImpact]
536
+ ],
537
+ (label, value) => `${label}=${value}`
538
+ );
539
+ return labeledLines2(
540
+ [
541
+ ["CISA SSVC", parts.length > 0 ? parts.join("; ") : void 0],
542
+ ["CISA SSVC atualizado", ssvc.timestamp]
543
+ ],
544
+ (label, value) => `- **${label}:** ${value}`
545
+ );
546
+ };
547
+ var mdCisaLines = (cisa) => [
548
+ ...cisa?.kev ? mdCisaKevLines(cisa.kev) : [],
549
+ ...cisa?.ssvc ? mdCisaSsvcLines(cisa.ssvc) : []
550
+ ];
551
+ var mdReferencesBlock = (references) => references.length ? [
552
+ "",
553
+ "**Refer\xEAncias:**",
554
+ "",
555
+ ...references.map((reference) => {
556
+ const metadata = [reference.source, ...reference.tags].filter(Boolean);
557
+ return `- ${reference.url}${metadata.length ? ` \u2014 ${metadata.join(", ")}` : ""}`;
558
+ })
559
+ ] : [];
560
+ var nvdHttpStatusSuffix = (httpStatus) => httpStatus ? `, HTTP ${httpStatus}` : "";
561
+ var nvdMarkdown = (result, advisorySummary) => {
562
+ if (result.status === "not-found") return [`- **${result.cveId}:** sem resultado no NVD`];
563
+ if (result.status === "error") {
564
+ return [
565
+ `- **${result.cveId}:** falha ao consultar (${result.error.kind}${nvdHttpStatusSuffix(result.error.httpStatus)})`
566
+ ];
567
+ }
568
+ const { data } = result;
569
+ return [
570
+ `#### ${result.cveId}`,
571
+ "",
572
+ `- **Status NVD:** ${data.vulnerabilityStatus ?? "n\xE3o informado"}`,
573
+ ...mdCvssBlock(data),
574
+ ...mdMetadataLines(data, advisorySummary),
575
+ ...mdCisaLines(data.cisa),
576
+ ...mdReferencesBlock(data.references)
577
+ ];
578
+ };
579
+ var mdSourcesLabel = (source, foundNvd) => source === "osv" ? `OSV${foundNvd ? ", NVD" : ""}` : "npm";
580
+ var mdSummaryLine = (summary) => summary ? [`- **Descri\xE7\xE3o OSV/npm:** ${summary}`] : [];
581
+ var firstCvssScore = (nvd) => {
582
+ const withCvss = (nvd ?? []).find(
583
+ (result) => result.status === "found" && Boolean(result.data.cvss)
584
+ );
585
+ return withCvss?.data.cvss ? `${withCvss.data.cvss.score}` : "\u2014";
586
+ };
587
+ var dependencySummaryRow = (finding, dependency) => `| ${dependency.package.name}@${dependency.package.installedVersion} | ${severityBadge(finding.severity)} | ${dependency.advisory.id ?? "\u2014"} | ${dependency.advisory.aliases.join(", ") || "\u2014"} | ${firstCvssScore(dependency.nvd)} | ${dependency.package.fixedVersions.join(" ou ") || "\u2014"} |`;
588
+ var dependencySummaryTable = (findings) => {
589
+ const rows = findings.filter((finding) => Boolean(finding.dependency)).map((finding) => dependencySummaryRow(finding, finding.dependency));
590
+ if (!rows.length) return [];
591
+ return [
592
+ "| Pacote | Severidade | Advisory | CVE(s) | CVSS | Corrigir para |",
593
+ "| --- | --- | --- | --- | --- | --- |",
594
+ ...rows,
595
+ ""
596
+ ];
597
+ };
598
+ var dependencySection = (finding) => {
599
+ const dependency = finding.dependency;
600
+ if (!dependency) return [];
601
+ const foundNvd = (dependency.nvd ?? []).some((result) => result.status === "found");
602
+ return [
603
+ "<details>",
604
+ `<summary>${severityBadge(finding.severity)} <strong>${dependency.package.name}@${dependency.package.installedVersion}</strong></summary>`,
605
+ "",
606
+ `- **Fonte principal:** ${dependency.advisory.source.toUpperCase()}`,
607
+ `- **Advisory:** ${dependency.advisory.id ?? "n\xE3o informado"}`,
608
+ `- **Aliases:** ${dependency.advisory.aliases.join(", ") || "nenhum"}`,
609
+ `- **Vers\xF5es corrigidas:** ${dependency.package.fixedVersions.join(" ou ") || "n\xE3o informadas"}`,
610
+ `- **Fontes:** ${mdSourcesLabel(dependency.advisory.source, foundNvd)}`,
611
+ ...mdSummaryLine(dependency.advisory.summary),
612
+ ...(dependency.nvd ?? []).flatMap((result) => ["", ...nvdMarkdown(result, dependency.advisory.summary)]),
613
+ "",
614
+ "</details>",
615
+ ""
616
+ ];
617
+ };
618
+ var dependencySections = (findings) => {
619
+ const dependencies = findings.filter((finding) => finding.dependency);
620
+ if (dependencies.length === 0) return [];
621
+ return [
622
+ "## Depend\xEAncias vulner\xE1veis",
623
+ "",
624
+ ...dependencySummaryTable(findings),
625
+ ...dependencies.flatMap(dependencySection)
626
+ ];
627
+ };
333
628
  var osvCheckedSection = (result) => {
334
629
  const { osvCheckedPackages } = result;
335
630
  if (!osvCheckedPackages || osvCheckedPackages.length === 0) {
@@ -340,27 +635,618 @@ var osvCheckedSection = (result) => {
340
635
  return [
341
636
  `## Depend\xEAncias verificadas no OSV.dev (${checked}/${total})`,
342
637
  "",
638
+ "<details>",
639
+ "<summary>Ver lista completa</summary>",
640
+ "",
343
641
  ...osvCheckedPackages.map((pkg) => `- ${pkg}`),
642
+ "",
643
+ "</details>",
344
644
  ""
345
645
  ];
346
646
  };
347
647
  var toMarkdownReport = (result, generatedAt = /* @__PURE__ */ new Date()) => {
648
+ const codeFindings = result.findings.filter((finding) => !finding.dependency);
348
649
  const bySeverity = groupBy(result.findings, (f) => f.severity);
650
+ const codeBySeverity = groupBy(codeFindings, (f) => f.severity);
349
651
  return [
350
652
  ...reportHeader(result, generatedAt),
351
653
  ...warningsSection(result),
352
654
  ...summaryTable(bySeverity),
353
- ...severitySections(bySeverity),
655
+ ...severitySections(codeBySeverity),
656
+ ...dependencySections(result.findings),
354
657
  ...osvCheckedSection(result)
355
658
  ].join("\n");
356
659
  };
357
660
 
358
661
  // src/scanner/dependency-audit.ts
359
662
  import { execFile } from "child_process";
360
- import { readFile } from "fs/promises";
361
- import { join } from "path";
663
+ import { readFile as readFile2 } from "fs/promises";
664
+ import { join as join2 } from "path";
362
665
  import { promisify } from "util";
363
666
 
667
+ // src/scanner/nvd-cache.ts
668
+ import { homedir } from "os";
669
+ import { dirname, join } from "path";
670
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
671
+ var CACHE_SCHEMA_VERSION = 1;
672
+ var FOUND_TTL_MS = 24 * 60 * 60 * 1e3;
673
+ var NOT_FOUND_TTL_MS = 60 * 60 * 1e3;
674
+ var isRecord = (value) => typeof value === "object" && value !== null;
675
+ var defaultCacheFile = () => {
676
+ if (process.platform === "win32") {
677
+ return join(process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"), "CodeSentry", "nvd-v1.json");
678
+ }
679
+ if (process.platform === "darwin") {
680
+ return join(homedir(), "Library", "Caches", "CodeSentry", "nvd-v1.json");
681
+ }
682
+ return join(process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache"), "codesentry", "nvd-v1.json");
683
+ };
684
+ var isValidReference = (value) => isRecord(value) && typeof value.url === "string";
685
+ var isNvdData = (value, cveId) => isRecord(value) && value.id === cveId && Array.isArray(value.cwes) && value.cwes.every((item) => typeof item === "string") && Array.isArray(value.references) && value.references.every(isValidReference);
686
+ var isStoredEnvelope = (value) => isRecord(value) && typeof value.expiresAt === "number" && isRecord(value.result);
687
+ var parseFoundResult = (cveId, result) => result.status === "found" && isNvdData(result.data, cveId) ? { status: "found", cveId, data: result.data } : void 0;
688
+ var parseStoredResult = (cveId, result) => {
689
+ if (result.cveId !== cveId) return void 0;
690
+ if (result.status === "not-found") return { status: "not-found", cveId };
691
+ return parseFoundResult(cveId, result);
692
+ };
693
+ var parseStoredEntry = (cveId, value) => {
694
+ if (!isStoredEnvelope(value)) return void 0;
695
+ const result = parseStoredResult(cveId, value.result);
696
+ return result ? { expiresAt: value.expiresAt, result } : void 0;
697
+ };
698
+ var parseCacheFileBody = (parsed, now) => {
699
+ if (!isRecord(parsed) || parsed.schemaVersion !== CACHE_SCHEMA_VERSION || !isRecord(parsed.entries)) {
700
+ return { entries: /* @__PURE__ */ new Map(), incompatible: true };
701
+ }
702
+ const entries = /* @__PURE__ */ new Map();
703
+ for (const [cveId, value] of Object.entries(parsed.entries)) {
704
+ const entry = parseStoredEntry(cveId, value);
705
+ if (entry && entry.expiresAt > now) entries.set(cveId, entry);
706
+ }
707
+ return { entries, incompatible: false };
708
+ };
709
+ var cleanCacheableResult = (result) => result.status === "found" ? { status: "found", cveId: result.cveId, data: result.data } : { status: "not-found", cveId: result.cveId };
710
+ var toStoredEntry = (result, now) => ({
711
+ expiresAt: now + (result.status === "found" ? FOUND_TTL_MS : NOT_FOUND_TTL_MS),
712
+ result
713
+ });
714
+ var PersistentNvdCache = class {
715
+ #filePath;
716
+ #now;
717
+ #entries = /* @__PURE__ */ new Map();
718
+ #warnings = [];
719
+ #loadPromise;
720
+ #writeCounter = 0;
721
+ #writeQueue = Promise.resolve();
722
+ constructor(options) {
723
+ this.#filePath = options.filePath ?? defaultCacheFile();
724
+ this.#now = options.now ?? Date.now;
725
+ }
726
+ async #loadFromDisk() {
727
+ try {
728
+ const raw = await readFile(this.#filePath, "utf-8");
729
+ const parsed = JSON.parse(raw);
730
+ const { entries, incompatible } = parseCacheFileBody(parsed, this.#now());
731
+ if (incompatible) {
732
+ this.#warnings.push("O cache do NVD possui vers\xE3o ou formato incompat\xEDvel e foi ignorado.");
733
+ return;
734
+ }
735
+ entries.forEach((entry, cveId) => this.#entries.set(cveId, entry));
736
+ } catch (error) {
737
+ if (error.code !== "ENOENT") {
738
+ this.#warnings.push("N\xE3o foi poss\xEDvel ler o cache do NVD; as consultas continuar\xE3o sem ele.");
739
+ }
740
+ }
741
+ }
742
+ async #load() {
743
+ this.#loadPromise ??= this.#loadFromDisk();
744
+ await this.#loadPromise;
745
+ }
746
+ async get(cveId) {
747
+ await this.#load();
748
+ const entry = this.#entries.get(cveId);
749
+ if (!entry) return void 0;
750
+ if (entry.expiresAt <= this.#now()) {
751
+ this.#entries.delete(cveId);
752
+ return void 0;
753
+ }
754
+ return { ...entry.result, fromCache: true };
755
+ }
756
+ async set(result) {
757
+ if (result.status === "error") return;
758
+ await this.#load();
759
+ const entry = toStoredEntry(cleanCacheableResult(result), this.#now());
760
+ this.#entries.set(result.cveId, entry);
761
+ this.#writeQueue = this.#writeQueue.then(() => this.#persist()).catch(() => void 0);
762
+ await this.#writeQueue;
763
+ }
764
+ async #persist() {
765
+ try {
766
+ const directory = dirname(this.#filePath);
767
+ await mkdir(directory, { recursive: true });
768
+ const entries = Object.fromEntries(this.#entries);
769
+ const body = { schemaVersion: CACHE_SCHEMA_VERSION, entries };
770
+ const temporary = `${this.#filePath}.${process.pid}.${this.#writeCounter++}.tmp`;
771
+ await writeFile(temporary, JSON.stringify(body, null, 2), { encoding: "utf-8", mode: 384 });
772
+ await rename(temporary, this.#filePath);
773
+ } catch {
774
+ this.#warnings.push("N\xE3o foi poss\xEDvel gravar o cache do NVD; o scan continuar\xE1 normalmente.");
775
+ }
776
+ }
777
+ consumeWarnings() {
778
+ return this.#warnings.splice(0);
779
+ }
780
+ };
781
+ var createNvdCache = (options = {}) => new PersistentNvdCache(options);
782
+
783
+ // src/scanner/nvd-normalizer.ts
784
+ var isRecord2 = (value) => typeof value === "object" && value !== null;
785
+ var asString = (value) => typeof value === "string" && value.trim() ? value.trim() : void 0;
786
+ var asArray = (value) => Array.isArray(value) ? value : [];
787
+ var pickBy = (condition, whenTrue, whenFalse) => condition ? whenTrue : whenFalse;
788
+ var pickEnum = (value, accepted) => {
789
+ const candidate = asString(value)?.toUpperCase();
790
+ return candidate && accepted.includes(candidate) ? candidate : void 0;
791
+ };
792
+ var SEVERITIES = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
793
+ var ATTACK_VECTORS = ["NETWORK", "ADJACENT", "LOCAL", "PHYSICAL"];
794
+ var ATTACK_COMPLEXITIES = ["LOW", "MEDIUM", "HIGH"];
795
+ var PRIVILEGES = ["NONE", "LOW", "HIGH"];
796
+ var USER_INTERACTIONS = ["NONE", "REQUIRED", "PASSIVE", "ACTIVE"];
797
+ var SCOPES = ["UNCHANGED", "CHANGED"];
798
+ var IMPACTS = ["NONE", "LOW", "HIGH", "PARTIAL", "COMPLETE"];
799
+ var METRIC_BUCKETS = [
800
+ { key: "cvssMetricV40", version: "4.0" },
801
+ { key: "cvssMetricV31", version: "3.1" },
802
+ { key: "cvssMetricV30", version: "3.0" },
803
+ { key: "cvssMetricV2", version: "2.0" }
804
+ ];
805
+ var isNvdSource = (metric) => asString(metric.source)?.toLowerCase() === "nvd@nist.gov";
806
+ var isPrimaryType = (metric) => asString(metric.type) === "Primary";
807
+ var fallbackMetricPriority = (metric) => {
808
+ if (isPrimaryType(metric)) return 1;
809
+ if (isNvdSource(metric)) return 2;
810
+ return 3;
811
+ };
812
+ var metricPriority = (metric) => isNvdSource(metric) && isPrimaryType(metric) ? 0 : fallbackMetricPriority(metric);
813
+ var isValidCvssScore = (score, version) => score >= 0 && score <= 10 && (score !== 0 || version === "2.0");
814
+ var classifyHighOrCritical = (score, version) => version === "2.0" || score < 9 ? "HIGH" : "CRITICAL";
815
+ var classifySeverity = (score, version) => {
816
+ if (score < 4) return "LOW";
817
+ if (score < 7) return "MEDIUM";
818
+ return classifyHighOrCritical(score, version);
819
+ };
820
+ var deriveSeverity = (score, version) => isValidCvssScore(score, version) ? classifySeverity(score, version) : void 0;
821
+ var normalizeAttackVector = (value) => {
822
+ const normalized = asString(value)?.toUpperCase();
823
+ return pickEnum(normalized === "ADJACENT_NETWORK" ? "ADJACENT" : normalized, ATTACK_VECTORS);
824
+ };
825
+ var validatedCvssData = (metric, version) => {
826
+ if (!isRecord2(metric.cvssData)) return void 0;
827
+ const data = metric.cvssData;
828
+ const dataVersion = asString(data.version);
829
+ const vectorString = asString(data.vectorString);
830
+ const score = data.baseScore;
831
+ const scoreIsValidNumber = typeof score === "number" && Number.isFinite(score) && score >= 0 && score <= 10;
832
+ if (dataVersion !== version || !vectorString || !scoreIsValidNumber) return void 0;
833
+ return { data, vectorString, score };
834
+ };
835
+ var metricSeverity = (metric, data, score, version) => {
836
+ const wrapperSeverity = pickBy(version === "2.0", metric.baseSeverity, data.baseSeverity);
837
+ const rawSeverity = asString(wrapperSeverity);
838
+ return rawSeverity === void 0 ? deriveSeverity(score, version) : pickEnum(rawSeverity, SEVERITIES);
839
+ };
840
+ var metricUserInteraction = (metric, data, version) => {
841
+ if (version === "2.0" && typeof metric.userInteractionRequired === "boolean") {
842
+ return metric.userInteractionRequired ? "REQUIRED" : "NONE";
843
+ }
844
+ return pickEnum(data.userInteraction, USER_INTERACTIONS);
845
+ };
846
+ var metricType = (rawType) => rawType === "Primary" || rawType === "Secondary" ? rawType : void 0;
847
+ var privilegesRequiredValue = (data, version) => version === "2.0" ? void 0 : pickEnum(data.privilegesRequired, PRIVILEGES);
848
+ var scopeValue = (data, version) => version === "4.0" || version === "2.0" ? void 0 : pickEnum(data.scope, SCOPES);
849
+ var computeMetricFields = (metric, data, score, version) => ({
850
+ severity: metricSeverity(metric, data, score, version),
851
+ source: asString(metric.source),
852
+ type: metricType(asString(metric.type)),
853
+ attackVector: normalizeAttackVector(pickBy(version === "2.0", data.accessVector, data.attackVector)),
854
+ attackComplexity: pickEnum(
855
+ pickBy(version === "2.0", data.accessComplexity, data.attackComplexity),
856
+ ATTACK_COMPLEXITIES
857
+ ),
858
+ privilegesRequired: privilegesRequiredValue(data, version),
859
+ userInteraction: metricUserInteraction(metric, data, version),
860
+ scope: scopeValue(data, version),
861
+ confidentialityImpact: pickEnum(
862
+ pickBy(version === "4.0", data.vulnConfidentialityImpact, data.confidentialityImpact),
863
+ IMPACTS
864
+ ),
865
+ integrityImpact: pickEnum(pickBy(version === "4.0", data.vulnIntegrityImpact, data.integrityImpact), IMPACTS),
866
+ availabilityImpact: pickEnum(
867
+ pickBy(version === "4.0", data.vulnAvailabilityImpact, data.availabilityImpact),
868
+ IMPACTS
869
+ )
870
+ });
871
+ var withOptionalFields = (base, fields) => ({
872
+ ...base,
873
+ ...fields.severity && { severity: fields.severity },
874
+ ...fields.source && { source: fields.source },
875
+ ...fields.type && { type: fields.type },
876
+ ...fields.attackVector && { attackVector: fields.attackVector },
877
+ ...fields.attackComplexity && { attackComplexity: fields.attackComplexity },
878
+ ...fields.privilegesRequired && { privilegesRequired: fields.privilegesRequired },
879
+ ...fields.userInteraction && { userInteraction: fields.userInteraction },
880
+ ...fields.scope && { scope: fields.scope },
881
+ ...fields.confidentialityImpact && { confidentialityImpact: fields.confidentialityImpact },
882
+ ...fields.integrityImpact && { integrityImpact: fields.integrityImpact },
883
+ ...fields.availabilityImpact && { availabilityImpact: fields.availabilityImpact }
884
+ });
885
+ var normalizeMetric = (metric, version) => {
886
+ const validated = validatedCvssData(metric, version);
887
+ if (!validated) return void 0;
888
+ const { data, vectorString, score } = validated;
889
+ const fields = computeMetricFields(metric, data, score, version);
890
+ return withOptionalFields({ score, version, vectorString }, fields);
891
+ };
892
+ var rankedCandidates = (metrics, key) => (
893
+ // codesentry-disable-next-line security/detect-object-injection -- key always comes from the hardcoded METRIC_BUCKETS list, never attacker input.
894
+ asArray(metrics[key]).filter(isRecord2).map((metric, index) => ({ metric, index, priority: metricPriority(metric) })).sort((a, b) => a.priority - b.priority || a.index - b.index).map(({ metric }) => metric)
895
+ );
896
+ var firstNormalizedCandidate = (candidates, version) => {
897
+ for (const metric of candidates) {
898
+ const normalized = normalizeMetric(metric, version);
899
+ if (normalized) return normalized;
900
+ }
901
+ return void 0;
902
+ };
903
+ var selectCvss = (metrics) => {
904
+ if (!isRecord2(metrics)) return void 0;
905
+ for (const bucket of METRIC_BUCKETS) {
906
+ const normalized = firstNormalizedCandidate(rankedCandidates(metrics, bucket.key), bucket.version);
907
+ if (normalized) return normalized;
908
+ }
909
+ return void 0;
910
+ };
911
+ var selectDescription = (descriptions) => {
912
+ const candidates = asArray(descriptions).filter(isRecord2);
913
+ const selected = candidates.find((item) => asString(item.lang)?.toLowerCase() === "en") ?? candidates.find((item) => asString(item.lang)?.toLowerCase().startsWith("en-")) ?? candidates[0];
914
+ if (!selected) return {};
915
+ return { description: asString(selected.value), descriptionLanguage: asString(selected.lang) };
916
+ };
917
+ var normalizeCwes = (weaknesses) => {
918
+ const values = asArray(weaknesses).filter(isRecord2).flatMap((weakness) => asArray(weakness.description)).filter(isRecord2).map((description) => asString(description.value)?.toUpperCase()).filter((value) => value !== void 0 && /^CWE-\d+$/.test(value));
919
+ return [...new Set(values)];
920
+ };
921
+ var normalizeReferences = (references) => {
922
+ const deduped = /* @__PURE__ */ new Map();
923
+ for (const reference of asArray(references).filter(isRecord2)) {
924
+ const url = asString(reference.url);
925
+ if (!url || deduped.has(url)) continue;
926
+ deduped.set(url, {
927
+ url,
928
+ source: asString(reference.source),
929
+ tags: asArray(reference.tags).map(asString).filter((tag) => tag !== void 0)
930
+ });
931
+ }
932
+ return [...deduped.values()];
933
+ };
934
+ var ssvcOption = (options, key) => {
935
+ for (const option of asArray(options).filter(isRecord2)) {
936
+ const value = asString(option[key]);
937
+ if (value) return value;
938
+ }
939
+ return void 0;
940
+ };
941
+ var normalizeKev = (cve) => {
942
+ const addedAt = asString(cve.cisaExploitAdd);
943
+ return addedAt ? {
944
+ addedAt,
945
+ actionDue: asString(cve.cisaActionDue),
946
+ requiredAction: asString(cve.cisaRequiredAction),
947
+ vulnerabilityName: asString(cve.cisaVulnerabilityName)
948
+ } : void 0;
949
+ };
950
+ var findSsvcMetric = (cve) => {
951
+ const metrics = isRecord2(cve.metrics) ? cve.metrics : void 0;
952
+ return asArray(metrics?.ssvcV203).filter(isRecord2).map((entry) => entry.ssvcData).find((data) => isRecord2(data) && asString(data.role) === "CISA Coordinator");
953
+ };
954
+ var normalizeSsvc = (ssvcMetric) => ssvcMetric ? {
955
+ exploitation: ssvcOption(ssvcMetric.options, "exploitation"),
956
+ automatable: ssvcOption(ssvcMetric.options, "automatable"),
957
+ technicalImpact: ssvcOption(ssvcMetric.options, "technicalImpact"),
958
+ timestamp: asString(ssvcMetric.timestamp)
959
+ } : void 0;
960
+ var normalizeCisa = (cve) => {
961
+ const kev = normalizeKev(cve);
962
+ const ssvc = normalizeSsvc(findSsvcMetric(cve));
963
+ return kev || ssvc ? { kev, ssvc } : void 0;
964
+ };
965
+ var normalizeNvdCve = (value, expectedCveId) => {
966
+ if (!isRecord2(value)) return void 0;
967
+ const id = asString(value.id)?.toUpperCase();
968
+ if (!id || id !== expectedCveId.toUpperCase()) return void 0;
969
+ return {
970
+ id,
971
+ vulnerabilityStatus: asString(value.vulnStatus),
972
+ ...selectDescription(value.descriptions),
973
+ published: asString(value.published),
974
+ lastModified: asString(value.lastModified),
975
+ cvss: selectCvss(value.metrics),
976
+ cwes: normalizeCwes(value.weaknesses),
977
+ references: normalizeReferences(value.references),
978
+ cisa: normalizeCisa(value)
979
+ };
980
+ };
981
+
982
+ // src/scanner/nvd-client.ts
983
+ var NVD_API_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0";
984
+ var DEFAULT_TIMEOUT_MS = 1e4;
985
+ var DEFAULT_MAX_ATTEMPTS = 3;
986
+ var TRANSIENT_FAILURE_CIRCUIT_THRESHOLD = 3;
987
+ var CVE_PATTERN = /^CVE-\d{4}-\d{4,}$/;
988
+ var isRecord3 = (value) => typeof value === "object" && value !== null;
989
+ var defaultSleep = (milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds));
990
+ var isAttemptFailure = (outcome) => "result" in outcome;
991
+ var errorResult = (cveId, kind, httpStatus) => ({
992
+ status: "error",
993
+ cveId,
994
+ error: { kind, ...httpStatus === void 0 ? {} : { httpStatus } }
995
+ });
996
+ var parseRetryAfterDate = (value, now) => {
997
+ const date = Date.parse(value);
998
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
999
+ };
1000
+ var retryAfterMilliseconds = (value, now) => {
1001
+ if (!value) return void 0;
1002
+ const seconds = Number(value);
1003
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
1004
+ return parseRetryAfterDate(value, now);
1005
+ };
1006
+ var extractCveEntries = (body) => {
1007
+ if (!isRecord3(body) || !Array.isArray(body.vulnerabilities)) return void 0;
1008
+ return body.vulnerabilities;
1009
+ };
1010
+ var buildRequestHeaders = (apiKey) => {
1011
+ const headers = { Accept: "application/json" };
1012
+ if (apiKey) headers.apiKey = apiKey;
1013
+ return headers;
1014
+ };
1015
+ var errorKindForStatus = (status) => {
1016
+ if (status === 429) return "rate-limit";
1017
+ if (status >= 500) return "server";
1018
+ return "http";
1019
+ };
1020
+ var isRetryableStatus = (status) => status === 408 || status === 429 || status >= 500;
1021
+ var isEmptyResultBody = (body, entries) => entries.length === 0 || isRecord3(body) && body.totalResults === 0;
1022
+ var outcomeFromEntries = (entries, cveId) => {
1023
+ for (const entry of entries) {
1024
+ if (!isRecord3(entry)) continue;
1025
+ const normalized = normalizeNvdCve(entry.cve, cveId);
1026
+ if (normalized) return { status: "found", cveId, data: normalized };
1027
+ }
1028
+ return { result: errorResult(cveId, "invalid-response"), retryable: false };
1029
+ };
1030
+ var parseSuccessBody = (body, cveId) => {
1031
+ const entries = extractCveEntries(body);
1032
+ if (!entries) return { result: errorResult(cveId, "invalid-response"), retryable: false };
1033
+ if (isEmptyResultBody(body, entries)) return { status: "not-found", cveId };
1034
+ return outcomeFromEntries(entries, cveId);
1035
+ };
1036
+ var isTimeoutError = (error, signal) => signal.aborted || error instanceof Error && error.name === "AbortError";
1037
+ var attemptCatchResult = (error, signal, cveId) => ({
1038
+ result: errorResult(cveId, isTimeoutError(error, signal) ? "timeout" : "network"),
1039
+ retryable: true
1040
+ });
1041
+ var DefaultNvdClient = class {
1042
+ #fetch;
1043
+ #apiKey;
1044
+ #cache;
1045
+ #timeoutMs;
1046
+ #maxAttempts;
1047
+ #minIntervalMs;
1048
+ #sleep;
1049
+ #now;
1050
+ #random;
1051
+ #lookups = /* @__PURE__ */ new Map();
1052
+ #warnings = [];
1053
+ #requestQueue = Promise.resolve();
1054
+ #nextRequestAt = 0;
1055
+ #circuitOpen = false;
1056
+ #consecutiveTransientFailures = 0;
1057
+ constructor(options) {
1058
+ this.#fetch = options.fetchImpl ?? fetch;
1059
+ this.#apiKey = options.apiKey?.trim() || void 0;
1060
+ this.#cache = options.cache ?? createNvdCache();
1061
+ this.#timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1062
+ this.#maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
1063
+ this.#minIntervalMs = options.minIntervalMs ?? (this.#apiKey ? 610 : 6100);
1064
+ this.#sleep = options.sleep ?? defaultSleep;
1065
+ this.#now = options.now ?? Date.now;
1066
+ this.#random = options.random ?? Math.random;
1067
+ }
1068
+ lookupCve(cveId) {
1069
+ const normalizedId = cveId.trim().toUpperCase();
1070
+ if (!CVE_PATTERN.test(normalizedId)) {
1071
+ return Promise.resolve(errorResult(normalizedId, "invalid-response"));
1072
+ }
1073
+ const existing = this.#lookups.get(normalizedId);
1074
+ if (existing) return existing;
1075
+ const lookup = this.#lookup(normalizedId);
1076
+ this.#lookups.set(normalizedId, lookup);
1077
+ return lookup;
1078
+ }
1079
+ async #shortCircuit(cveId) {
1080
+ const cached = await this.#cache.get(cveId);
1081
+ if (cached) return cached;
1082
+ if (this.#circuitOpen) return errorResult(cveId, "unavailable");
1083
+ return void 0;
1084
+ }
1085
+ async #retryUntilSuccess(cveId) {
1086
+ let finalFailure;
1087
+ for (let attempt = 0; attempt < this.#maxAttempts; attempt += 1) {
1088
+ const outcome = await this.#scheduleAttempt(cveId);
1089
+ if (!isAttemptFailure(outcome)) {
1090
+ this.#consecutiveTransientFailures = 0;
1091
+ await this.#cache.set(outcome);
1092
+ return { success: outcome };
1093
+ }
1094
+ finalFailure = outcome;
1095
+ if (!outcome.retryable || attempt === this.#maxAttempts - 1) break;
1096
+ const baseBackoff = Math.min(3e4, 1e3 * 2 ** attempt);
1097
+ const jitteredBackoff = baseBackoff * (0.8 + this.#random() * 0.4);
1098
+ await this.#sleep(Math.max(jitteredBackoff, outcome.retryAfterMs ?? 0));
1099
+ }
1100
+ return { failure: finalFailure ?? { result: errorResult(cveId, "unavailable"), retryable: false } };
1101
+ }
1102
+ #recordFailure(failure) {
1103
+ if (failure.retryable) {
1104
+ this.#consecutiveTransientFailures += 1;
1105
+ if (failure.result.error.kind === "rate-limit" || this.#consecutiveTransientFailures >= TRANSIENT_FAILURE_CIRCUIT_THRESHOLD) {
1106
+ this.#circuitOpen = true;
1107
+ }
1108
+ }
1109
+ return failure.result;
1110
+ }
1111
+ async #lookup(cveId) {
1112
+ const shortCircuited = await this.#shortCircuit(cveId);
1113
+ if (shortCircuited) return shortCircuited;
1114
+ const outcome = await this.#retryUntilSuccess(cveId);
1115
+ return "success" in outcome ? outcome.success : this.#recordFailure(outcome.failure);
1116
+ }
1117
+ #scheduleAttempt(cveId) {
1118
+ const scheduled = this.#requestQueue.then(async () => {
1119
+ if (this.#circuitOpen) return errorResult(cveId, "unavailable");
1120
+ const waitMs = Math.max(0, this.#nextRequestAt - this.#now());
1121
+ if (waitMs > 0) await this.#sleep(waitMs);
1122
+ this.#nextRequestAt = this.#now() + this.#minIntervalMs;
1123
+ return this.#attempt(cveId);
1124
+ }).catch(() => ({ result: errorResult(cveId, "unavailable"), retryable: true }));
1125
+ this.#requestQueue = scheduled.then(
1126
+ () => void 0,
1127
+ () => void 0
1128
+ );
1129
+ return scheduled;
1130
+ }
1131
+ #failureForStatus(cveId, status, response) {
1132
+ if (status === 401 || status === 403) {
1133
+ this.#circuitOpen = true;
1134
+ return { result: errorResult(cveId, "forbidden", status), retryable: false };
1135
+ }
1136
+ return {
1137
+ result: errorResult(cveId, errorKindForStatus(status), status),
1138
+ retryable: isRetryableStatus(status),
1139
+ retryAfterMs: retryAfterMilliseconds(response.headers?.get("Retry-After"), this.#now())
1140
+ };
1141
+ }
1142
+ async #safeFetch(url, headers, signal, cveId) {
1143
+ try {
1144
+ return await this.#fetch(url, { method: "GET", headers, signal });
1145
+ } catch (error) {
1146
+ return attemptCatchResult(error, signal, cveId);
1147
+ }
1148
+ }
1149
+ async #parseBodySafely(response, cveId) {
1150
+ try {
1151
+ return parseSuccessBody(await response.json(), cveId);
1152
+ } catch {
1153
+ return { result: errorResult(cveId, "invalid-response"), retryable: false };
1154
+ }
1155
+ }
1156
+ async #handleResponse(response, cveId) {
1157
+ const status = response.status ?? 0;
1158
+ if (status === 404) return { status: "not-found", cveId };
1159
+ if (!response.ok) return this.#failureForStatus(cveId, status, response);
1160
+ return this.#parseBodySafely(response, cveId);
1161
+ }
1162
+ async #handleFetchOutcome(fetched, cveId) {
1163
+ if ("result" in fetched) return fetched;
1164
+ return this.#handleResponse(fetched, cveId);
1165
+ }
1166
+ async #attempt(cveId) {
1167
+ const url = new URL(NVD_API_URL);
1168
+ url.searchParams.set("cveId", cveId);
1169
+ const headers = buildRequestHeaders(this.#apiKey);
1170
+ const signal = AbortSignal.timeout(this.#timeoutMs);
1171
+ const fetched = await this.#safeFetch(url.toString(), headers, signal, cveId);
1172
+ return this.#handleFetchOutcome(fetched, cveId);
1173
+ }
1174
+ consumeWarnings() {
1175
+ return [...this.#warnings.splice(0), ...this.#cache.consumeWarnings()];
1176
+ }
1177
+ };
1178
+ var createNvdClient = (options = {}) => new DefaultNvdClient(options);
1179
+
1180
+ // src/scanner/nvd-enrichment.ts
1181
+ var CVE_PATTERN2 = /^CVE-\d{4}-\d{4,}$/;
1182
+ var extractCveAliases = (aliases) => {
1183
+ const cves = (aliases ?? []).map((alias) => alias.trim().toUpperCase()).filter((alias) => CVE_PATTERN2.test(alias));
1184
+ return [...new Set(cves)];
1185
+ };
1186
+ var coverageFrom = (results) => ({
1187
+ total: results.length,
1188
+ enriched: results.filter((result) => result.status === "found").length,
1189
+ notFound: results.filter((result) => result.status === "not-found").length,
1190
+ failed: results.filter((result) => result.status === "error").length,
1191
+ cacheHits: results.filter((result) => result.status !== "error" && result.fromCache).length
1192
+ });
1193
+ var resolveLookups = async (lookups) => {
1194
+ const resolved = /* @__PURE__ */ new Map();
1195
+ let unexpectedFailure = false;
1196
+ try {
1197
+ await Promise.all(
1198
+ [...lookups].map(async ([cveId, lookup]) => {
1199
+ try {
1200
+ resolved.set(cveId, await lookup);
1201
+ } catch {
1202
+ resolved.set(cveId, { status: "error", cveId, error: { kind: "unavailable" } });
1203
+ }
1204
+ })
1205
+ );
1206
+ } catch {
1207
+ unexpectedFailure = true;
1208
+ }
1209
+ return { resolved, unexpectedFailure };
1210
+ };
1211
+ var enrichMatch = (match, cveIds, resolved) => ({
1212
+ ...match,
1213
+ cveIds,
1214
+ nvd: cveIds.map((cveId) => resolved.get(cveId)).filter((item) => item !== void 0)
1215
+ });
1216
+ var enrichmentWarnings = (client, coverage, unexpectedFailure) => [
1217
+ ...client.consumeWarnings(),
1218
+ ...coverage.failed > 0 ? [`N\xE3o foi poss\xEDvel consultar o NVD para ${coverage.failed} CVE(s); os findings OSV foram preservados.`] : [],
1219
+ ...unexpectedFailure ? [
1220
+ "Falha interna inesperada ao consolidar as consultas do NVD; alguns CVEs podem n\xE3o ter sido enriquecidos."
1221
+ ] : []
1222
+ ];
1223
+ var enrichOsvMatchesWithNvd = async (matches, client) => {
1224
+ const cveIdsByMatch = matches.map((match) => extractCveAliases(match.vuln.aliases));
1225
+ const uniqueCveIds = [...new Set(cveIdsByMatch.flat())];
1226
+ const lookups = new Map(
1227
+ uniqueCveIds.map((cveId) => [cveId, client.lookupCve(cveId)])
1228
+ );
1229
+ let resolution;
1230
+ try {
1231
+ resolution = await resolveLookups(lookups);
1232
+ } catch {
1233
+ resolution = { resolved: /* @__PURE__ */ new Map(), unexpectedFailure: true };
1234
+ }
1235
+ const { resolved, unexpectedFailure } = resolution;
1236
+ const results = uniqueCveIds.map((cveId) => resolved.get(cveId)).filter((item) => Boolean(item));
1237
+ const coverage = coverageFrom(results);
1238
+ return {
1239
+ matches: matches.map(
1240
+ (match, index) => (
1241
+ // codesentry-disable-next-line security/detect-object-injection -- index originates from map over the same cveIdsByMatch array.
1242
+ enrichMatch(match, cveIdsByMatch[index] ?? [], resolved)
1243
+ )
1244
+ ),
1245
+ coverage,
1246
+ warnings: enrichmentWarnings(client, coverage, unexpectedFailure)
1247
+ };
1248
+ };
1249
+
364
1250
  // src/scanner/run-with-concurrency-limit.ts
365
1251
  import pLimit from "p-limit";
366
1252
  var runWithConcurrencyLimit = async (items, concurrency, task) => {
@@ -521,77 +1407,131 @@ var SEVERITY_MAP2 = {
521
1407
  high: "high",
522
1408
  critical: "critical"
523
1409
  };
524
- var vulnerabilityTitle = (vulnerability) => {
525
- const firstVia = vulnerability.via[0];
526
- if (typeof firstVia === "object" && firstVia?.title) {
527
- return firstVia.title;
528
- }
529
- return 'ver "npm audit" para detalhes';
1410
+ var NVD_SEVERITY_MAP = {
1411
+ LOW: "low",
1412
+ MEDIUM: "medium",
1413
+ HIGH: "high",
1414
+ CRITICAL: "critical"
530
1415
  };
1416
+ var SEVERITY_WEIGHT = { low: 0, medium: 1, high: 2, critical: 3 };
1417
+ var CANONICAL_ADVISORY_PATTERN = /(?:CVE-\d{4}-\d{4,}|GHSA-[0-9A-Z]{4}-[0-9A-Z]{4}-[0-9A-Z]{4})/gi;
531
1418
  var npmFixSuggestion = (vulnerability) => {
532
1419
  const { fixAvailable } = vulnerability;
533
- if (fixAvailable === false) {
534
- return "sem corre\xE7\xE3o dispon\xEDvel ainda";
535
- }
536
- if (fixAvailable === true) {
1420
+ if (fixAvailable === false) return "sem corre\xE7\xE3o dispon\xEDvel ainda";
1421
+ if (fixAvailable === true)
537
1422
  return `atualize para uma vers\xE3o fora do intervalo vulner\xE1vel (${vulnerability.range})`;
538
- }
539
1423
  return `atualize para ${fixAvailable.name}@${fixAvailable.version}`;
540
1424
  };
541
- var mapAuditReportToFindings = (report) => {
542
- return Object.values(report.vulnerabilities).map((vulnerability) => ({
543
- ruleId: "dependency-audit",
544
- message: `Depend\xEAncia vulner\xE1vel: ${vulnerability.name} (${vulnerability.severity}) \u2014 ${vulnerabilityTitle(vulnerability)} \u2014 ${npmFixSuggestion(vulnerability)}`,
545
- file: "package.json",
546
- line: 1,
547
- severity: SEVERITY_MAP2[vulnerability.severity]
548
- }));
1425
+ var canonicalIdsFrom = (...values) => [
1426
+ ...new Set(
1427
+ values.flatMap((value) => String(value ?? "").match(CANONICAL_ADVISORY_PATTERN) ?? []).map((id) => id.toUpperCase())
1428
+ )
1429
+ ];
1430
+ var npmAdvisoryTitle = (advisory) => {
1431
+ if (typeof advisory === "object" && advisory.title) return advisory.title;
1432
+ if (typeof advisory === "string") return `vulnerabilidade transitiva via ${advisory}`;
1433
+ return 'ver "npm audit" para detalhes';
549
1434
  };
550
- var osvFixSuggestion = (fixedVersions) => fixedVersions.length ? `atualize para ${fixedVersions.join(" ou ")}` : "nenhuma vers\xE3o corrigida publicada pelo OSV.dev ainda";
551
- var buildOsvFinding = (pkg, vuln) => {
552
- const fixedVersions = extractFixedVersions(vuln, pkg.name);
1435
+ var npmFixedVersions = (fixAvailable) => typeof fixAvailable === "object" ? [fixAvailable.version] : [];
1436
+ var npmFinding = (vulnerability, advisory, ids) => {
1437
+ const title = npmAdvisoryTitle(advisory);
553
1438
  return {
554
1439
  ruleId: "dependency-audit",
555
- message: `OSV ${vuln.id}: ${pkg.name}@${pkg.version} (${mapOsvSeverity(vuln)}) \u2014 ${vuln.summary ?? "ver OSV.dev para detalhes"} \u2014 ${osvFixSuggestion(fixedVersions)}`,
556
- file: "package-lock.json",
1440
+ message: `Depend\xEAncia vulner\xE1vel: ${vulnerability.name} (${vulnerability.severity}) \u2014 ${title} \u2014 ${npmFixSuggestion(vulnerability)}`,
1441
+ file: "package.json",
557
1442
  line: 1,
558
- severity: mapOsvSeverity(vuln)
1443
+ // codesentry-disable-next-line security/detect-object-injection -- vulnerability.severity is npm audit's own NpmAuditSeverity union, not attacker input; unknown values just look up as undefined.
1444
+ severity: SEVERITY_MAP2[vulnerability.severity],
1445
+ dependency: {
1446
+ package: {
1447
+ name: vulnerability.name,
1448
+ installedVersion: "n\xE3o informada pelo npm audit",
1449
+ fixedVersions: npmFixedVersions(vulnerability.fixAvailable)
1450
+ },
1451
+ advisory: { source: "npm", id: ids[0], aliases: ids.slice(1), summary: title }
1452
+ }
559
1453
  };
560
1454
  };
561
- var findingsForLockedPackage = (pkg, vulnIdsByPackage, detailsById) => {
562
- const ids = vulnIdsByPackage.get(`${pkg.name}@${pkg.version}`) ?? [];
563
- return ids.map((id) => detailsById.get(id)).filter((vuln) => vuln !== void 0).map((vuln) => buildOsvFinding(pkg, vuln));
1455
+ var advisoryIds = (advisory) => typeof advisory === "object" ? canonicalIdsFrom(advisory.source, advisory.url, advisory.title) : canonicalIdsFrom(advisory);
1456
+ var advisoryIdentity = (advisory, ids) => {
1457
+ if (ids.length) return `ids:${[...ids].sort().join(",")}`;
1458
+ if (typeof advisory === "object")
1459
+ return `fields:${advisory.source ?? ""}|${advisory.url ?? ""}|${advisory.title ?? ""}`;
1460
+ return `via:${advisory ?? "unknown"}`;
564
1461
  };
565
- var mapOsvFindingsToRuleFindings = (lockedPackages, vulnIdsByPackage, detailsById, npmFlaggedNames) => lockedPackages.filter((pkg) => !npmFlaggedNames.has(pkg.name)).flatMap((pkg) => findingsForLockedPackage(pkg, vulnIdsByPackage, detailsById));
1462
+ var npmAdvisoryEntries = (vulnerability) => {
1463
+ const structured = vulnerability.via.filter(
1464
+ (advisory) => typeof advisory === "object"
1465
+ );
1466
+ const candidates = structured.length ? structured : [vulnerability.via.find((advisory) => typeof advisory === "string")];
1467
+ const seen = /* @__PURE__ */ new Set();
1468
+ return candidates.filter((advisory) => {
1469
+ const identity = advisoryIdentity(advisory, advisoryIds(advisory));
1470
+ if (seen.has(identity)) return false;
1471
+ seen.add(identity);
1472
+ return true;
1473
+ });
1474
+ };
1475
+ var mapAuditReportToFindings = (report, osvIdsByPackage = /* @__PURE__ */ new Map()) => Object.values(report.vulnerabilities).flatMap((vulnerability) => {
1476
+ const osvIds = osvIdsByPackage.get(vulnerability.name) ?? /* @__PURE__ */ new Set();
1477
+ return npmAdvisoryEntries(vulnerability).flatMap((advisory) => {
1478
+ const ids = advisoryIds(advisory);
1479
+ return ids.some((id) => osvIds.has(id)) ? [] : [npmFinding(vulnerability, advisory, ids)];
1480
+ });
1481
+ });
1482
+ var osvFixSuggestion = (fixedVersions) => fixedVersions.length ? `atualize para ${fixedVersions.join(" ou ")}` : "nenhuma vers\xE3o corrigida publicada pelo OSV.dev ainda";
1483
+ var normalizeOsvAliases = (aliases) => {
1484
+ const normalized = /* @__PURE__ */ new Map();
1485
+ for (const alias of aliases ?? []) {
1486
+ const trimmed = alias.trim();
1487
+ if (!trimmed) continue;
1488
+ const value = extractCveAliases([trimmed])[0] ?? trimmed;
1489
+ const key = value.toUpperCase();
1490
+ if (!normalized.has(key)) normalized.set(key, value);
1491
+ }
1492
+ return [...normalized.values()];
1493
+ };
1494
+ var highestNvdSeverity = (results) => results.filter((result) => result.status === "found").map((result) => result.data.cvss?.severity).filter((severity) => severity !== void 0).map((severity) => NVD_SEVERITY_MAP[severity]).filter((severity) => severity !== void 0).sort((a, b) => SEVERITY_WEIGHT[b] - SEVERITY_WEIGHT[a])[0];
1495
+ var buildOsvFinding = (match) => ({
1496
+ ruleId: "dependency-audit",
1497
+ message: `OSV ${match.vuln.id}: ${match.pkg.name}@${match.pkg.version} \u2014 ${match.vuln.summary ?? "ver OSV.dev para detalhes"} \u2014 ${osvFixSuggestion(match.fixedVersions)}`,
1498
+ file: "package-lock.json",
1499
+ line: 1,
1500
+ severity: highestNvdSeverity(match.nvd) ?? mapOsvSeverity(match.vuln),
1501
+ dependency: {
1502
+ package: {
1503
+ name: match.pkg.name,
1504
+ installedVersion: match.pkg.version,
1505
+ fixedVersions: match.fixedVersions
1506
+ },
1507
+ advisory: {
1508
+ source: "osv",
1509
+ id: match.vuln.id,
1510
+ aliases: normalizeOsvAliases(match.vuln.aliases),
1511
+ summary: match.vuln.summary
1512
+ },
1513
+ nvd: match.nvd
1514
+ }
1515
+ });
1516
+ var unenrichedMatch = (match) => ({
1517
+ ...match,
1518
+ cveIds: extractCveAliases(match.vuln.aliases),
1519
+ nvd: []
1520
+ });
1521
+ var collectMatches = (lockedPackages, vulnIdsByPackage, detailsById) => lockedPackages.flatMap(
1522
+ (pkg) => [...new Set(vulnIdsByPackage.get(`${pkg.name}@${pkg.version}`) ?? [])].map((id) => detailsById.get(id)).filter((vuln) => vuln !== void 0).map((vuln) => ({ pkg, vuln, fixedVersions: extractFixedVersions(vuln, pkg.name) }))
1523
+ );
566
1524
  var errorMessage = (error) => error instanceof Error ? error.message : "erro desconhecido";
567
1525
  var hasVulnerabilitiesRecord = (value) => typeof value === "object" && value !== null && typeof value.vulnerabilities === "object" && value.vulnerabilities !== null;
568
1526
  var normalizeNpmAuditReport = (raw) => {
569
- if (hasVulnerabilitiesRecord(raw)) {
570
- return { report: { vulnerabilities: raw.vulnerabilities } };
571
- }
1527
+ if (hasVulnerabilitiesRecord(raw)) return { report: { vulnerabilities: raw.vulnerabilities } };
572
1528
  const errorSummary = typeof raw === "object" && raw !== null && "error" in raw ? raw.error?.summary ?? "formato de resposta inesperado" : "formato de resposta inesperado";
573
1529
  return {
574
1530
  report: { vulnerabilities: {} },
575
1531
  warning: `"npm audit" n\xE3o retornou um relat\xF3rio v\xE1lido: ${errorSummary}.`
576
1532
  };
577
1533
  };
578
- var combineWarnings = (...warnings) => {
579
- const present = warnings.filter((warning) => Boolean(warning));
580
- return present.length ? present.join(" ") : void 0;
581
- };
582
- var fetchDetailsForIds = (ids, fetchImpl) => ids.length ? fetchOsvVulnerabilityDetails(ids, fetchImpl) : Promise.resolve({ detailsById: /* @__PURE__ */ new Map(), warning: void 0 });
583
- var auditPackagesWithOsv = async (lockedPackages, npmFlaggedNames, fetchImpl = fetch) => {
584
- try {
585
- return await collectOsvAuditResult(lockedPackages, npmFlaggedNames, fetchImpl);
586
- } catch (error) {
587
- return {
588
- findings: [],
589
- checkedPackages: [],
590
- warning: `N\xE3o foi poss\xEDvel consultar o OSV.dev: ${errorMessage(error)}.`
591
- };
592
- }
593
- };
594
- var collectOsvAuditResult = async (lockedPackages, npmFlaggedNames, fetchImpl) => {
1534
+ var collectOsvMatches = async (lockedPackages, fetchImpl) => {
595
1535
  try {
596
1536
  const {
597
1537
  vulnIdsByPackage,
@@ -599,19 +1539,20 @@ var collectOsvAuditResult = async (lockedPackages, npmFlaggedNames, fetchImpl) =
599
1539
  warning: batchWarning
600
1540
  } = await queryOsvBatch(lockedPackages, fetchImpl);
601
1541
  const ids = [...new Set([...vulnIdsByPackage.values()].flat())];
602
- const { detailsById, warning: detailsWarning } = await fetchDetailsForIds(ids, fetchImpl);
1542
+ const detailResult = ids.length ? await fetchOsvVulnerabilityDetails(ids, fetchImpl) : { detailsById: /* @__PURE__ */ new Map(), warning: void 0 };
603
1543
  return {
604
- findings: mapOsvFindingsToRuleFindings(
605
- lockedPackages,
606
- vulnIdsByPackage,
607
- detailsById,
608
- npmFlaggedNames
609
- ),
1544
+ matches: collectMatches(lockedPackages, vulnIdsByPackage, detailResult.detailsById),
610
1545
  checkedPackages,
611
- warning: combineWarnings(batchWarning, detailsWarning)
1546
+ warnings: [batchWarning, detailResult.warning].filter(
1547
+ (warning) => Boolean(warning)
1548
+ )
612
1549
  };
613
1550
  } catch (error) {
614
- throw new Error("N\xE3o foi poss\xEDvel consolidar os resultados do OSV.dev.", { cause: error });
1551
+ return {
1552
+ matches: [],
1553
+ checkedPackages: [],
1554
+ warnings: [`N\xE3o foi poss\xEDvel consultar o OSV.dev: ${errorMessage(error)}.`]
1555
+ };
615
1556
  }
616
1557
  };
617
1558
  var runNpmAudit = async (targetDir) => {
@@ -623,64 +1564,124 @@ var runNpmAudit = async (targetDir) => {
623
1564
  }));
624
1565
  } catch (error) {
625
1566
  const stdoutFromError = error.stdout;
626
- if (!stdoutFromError) {
1567
+ if (!stdoutFromError)
627
1568
  throw new Error(`N\xE3o foi poss\xEDvel executar "npm audit" em "${targetDir}".`, { cause: error });
628
- }
629
1569
  stdout = stdoutFromError;
630
1570
  }
631
1571
  return JSON.parse(stdout);
632
1572
  };
633
- var auditPackagesFromLockfile = async (targetDir, npmFlaggedNames, fetchImpl) => {
1573
+ var osvIdentityIndex = (matches) => {
1574
+ const index = /* @__PURE__ */ new Map();
1575
+ matches.forEach((match) => {
1576
+ const ids = index.get(match.pkg.name) ?? /* @__PURE__ */ new Set();
1577
+ [match.vuln.id, ...match.vuln.aliases ?? []].forEach((id) => ids.add(id.trim().toUpperCase()));
1578
+ index.set(match.pkg.name, ids);
1579
+ });
1580
+ return index;
1581
+ };
1582
+ var emptyNvdCoverage = (total, failed) => ({
1583
+ total,
1584
+ enriched: 0,
1585
+ notFound: 0,
1586
+ failed,
1587
+ cacheHits: 0
1588
+ });
1589
+ var resolveNvdClient = (options) => options.nvdClient ?? createNvdClient({
1590
+ fetchImpl: options.fetchImpl,
1591
+ apiKey: options.nvdApiKey ?? process.env.NVD_API_KEY,
1592
+ cache: options.nvdCache ?? createNvdCache()
1593
+ });
1594
+ var enrichWithNvdIfEnabled = async (collected, options) => {
1595
+ if (!options.nvdEnabled) {
1596
+ return { matches: collected.matches.map(unenrichedMatch), nvd: false, warnings: [] };
1597
+ }
1598
+ const uniqueCveIds = new Set(collected.matches.flatMap((match) => extractCveAliases(match.vuln.aliases)));
1599
+ if (uniqueCveIds.size === 0) {
1600
+ return { matches: collected.matches.map(unenrichedMatch), nvd: emptyNvdCoverage(0, 0), warnings: [] };
1601
+ }
634
1602
  try {
635
- const raw = await readFile(join(targetDir, "package-lock.json"), "utf-8");
636
- const lockedPackages = parsePackageLock(raw);
637
- const osvResult = await auditPackagesWithOsv(lockedPackages, npmFlaggedNames, fetchImpl);
1603
+ const enrichment = await enrichOsvMatchesWithNvd(collected.matches, resolveNvdClient(options));
1604
+ return { matches: enrichment.matches, nvd: enrichment.coverage, warnings: enrichment.warnings };
1605
+ } catch {
1606
+ const total = uniqueCveIds.size;
638
1607
  return {
639
- findings: osvResult.findings,
640
- packagesAudited: lockedPackages.length,
641
- osvChecked: { checked: osvResult.checkedPackages.length, total: lockedPackages.length },
642
- osvCheckedPackages: osvResult.checkedPackages.map((pkg) => `${pkg.name}@${pkg.version}`).sort(),
643
- warning: osvResult.warning
1608
+ matches: collected.matches.map(unenrichedMatch),
1609
+ nvd: emptyNvdCoverage(total, total),
1610
+ warnings: [
1611
+ `N\xE3o foi poss\xEDvel consultar o NVD para ${total} CVE(s); os findings OSV foram preservados.`
1612
+ ]
644
1613
  };
1614
+ }
1615
+ };
1616
+ var successfulLockfileAudit = (lockedPackages, collected, enrichment) => ({
1617
+ findings: enrichment.matches.map(buildOsvFinding),
1618
+ matches: collected.matches,
1619
+ packagesAudited: lockedPackages.length,
1620
+ osvChecked: { checked: collected.checkedPackages.length, total: lockedPackages.length },
1621
+ osvCheckedPackages: collected.checkedPackages.map((pkg) => `${pkg.name}@${pkg.version}`).sort(),
1622
+ nvd: enrichment.nvd,
1623
+ warnings: [...collected.warnings, ...enrichment.warnings]
1624
+ });
1625
+ var auditPackagesFromLockfile = async (targetDir, options) => {
1626
+ try {
1627
+ const raw = await readFile2(join2(targetDir, "package-lock.json"), "utf-8");
1628
+ const lockedPackages = parsePackageLock(raw);
1629
+ const collected = await collectOsvMatches(lockedPackages, options.fetchImpl);
1630
+ const enrichment = await enrichWithNvdIfEnabled(collected, options);
1631
+ return successfulLockfileAudit(lockedPackages, collected, enrichment);
645
1632
  } catch (error) {
646
1633
  return {
647
1634
  findings: [],
1635
+ matches: [],
648
1636
  packagesAudited: false,
649
- warning: `N\xE3o foi poss\xEDvel checar o OSV.dev: ${errorMessage(error)}.`
1637
+ nvd: options.nvdEnabled ? { total: 0, enriched: 0, notFound: 0, failed: 0, cacheHits: 0 } : false,
1638
+ warnings: [`N\xE3o foi poss\xEDvel checar o OSV.dev: ${errorMessage(error)}.`]
650
1639
  };
651
1640
  }
652
1641
  };
653
- var runDependencyAudit = async (targetDir, fetchImpl = fetch) => {
1642
+ var normalizeOptions = (optionsOrFetch) => typeof optionsOrFetch === "function" ? { fetchImpl: optionsOrFetch } : optionsOrFetch ?? {};
1643
+ var buildDependencyAuditResult = (startedAt, npmReport, npmWarning, osv) => {
1644
+ const npmFindings = mapAuditReportToFindings(npmReport, osvIdentityIndex(osv.matches));
1645
+ const warnings = [npmWarning, ...osv.warnings].filter((warning) => Boolean(warning));
1646
+ return {
1647
+ scannedFiles: 1,
1648
+ findings: [...npmFindings, ...osv.findings],
1649
+ durationMs: Date.now() - startedAt,
1650
+ engines: {
1651
+ dependencyAudit: osv.packagesAudited === false ? Object.keys(npmReport.vulnerabilities).length : osv.packagesAudited,
1652
+ osv: osv.osvChecked,
1653
+ nvd: osv.nvd
1654
+ },
1655
+ osvCheckedPackages: osv.osvCheckedPackages,
1656
+ warnings: warnings.length ? warnings : void 0
1657
+ };
1658
+ };
1659
+ var runDependencyAudit = async (targetDir, optionsOrFetch) => {
654
1660
  const startedAt = Date.now();
1661
+ const supplied = normalizeOptions(optionsOrFetch);
1662
+ const options = {
1663
+ ...supplied,
1664
+ fetchImpl: supplied.fetchImpl ?? fetch,
1665
+ nvdEnabled: supplied.nvdEnabled ?? true
1666
+ };
655
1667
  try {
656
- const { report: npmReport, warning: npmWarning } = normalizeNpmAuditReport(await runNpmAudit(targetDir));
657
- const npmFindings = mapAuditReportToFindings(npmReport);
658
- const npmFlaggedNames = new Set(Object.keys(npmReport.vulnerabilities));
659
- const osv = await auditPackagesFromLockfile(targetDir, npmFlaggedNames, fetchImpl);
660
- const warnings = combineWarnings(npmWarning, osv.warning);
661
- return {
662
- scannedFiles: 1,
663
- findings: [...npmFindings, ...osv.findings],
664
- durationMs: Date.now() - startedAt,
665
- engines: {
666
- dependencyAudit: osv.packagesAudited === false ? npmFlaggedNames.size : osv.packagesAudited,
667
- osv: osv.osvChecked
668
- },
669
- osvCheckedPackages: osv.osvCheckedPackages,
670
- warnings: warnings ? [warnings] : void 0
671
- };
1668
+ const { report: npmReport, warning: npmWarning } = normalizeNpmAuditReport(
1669
+ await (options.npmAuditRunner ?? runNpmAudit)(targetDir)
1670
+ );
1671
+ const osv = await auditPackagesFromLockfile(targetDir, options);
1672
+ return buildDependencyAuditResult(startedAt, npmReport, npmWarning, osv);
672
1673
  } catch (error) {
673
1674
  throw new Error(`N\xE3o foi poss\xEDvel auditar depend\xEAncias em "${targetDir}".`, { cause: error });
674
1675
  }
675
1676
  };
676
1677
 
677
1678
  // src/scanner/scanner.ts
678
- import { readFile as readFile2 } from "fs/promises";
1679
+ import { readFile as readFile3 } from "fs/promises";
679
1680
  import { availableParallelism } from "os";
680
1681
 
681
1682
  // src/scanner/file-finder.ts
682
1683
  import { readdir } from "fs/promises";
683
- import { join as join2 } from "path";
1684
+ import { join as join3 } from "path";
684
1685
 
685
1686
  // src/scanner/ignore-patterns.ts
686
1687
  var ALWAYS_IGNORED_DIR_NAMES = ["node_modules", ".git", "dist", ".next", ".angular"];
@@ -694,10 +1695,10 @@ var isTestFileName = (fileName) => TEST_FILE_NAME_PATTERN.test(fileName);
694
1695
  var SCANNABLE_EXTENSIONS = [".js", ".ts", ".jsx", ".tsx"];
695
1696
  var isScannable = (fileName) => SCANNABLE_EXTENSIONS.some((ext) => fileName.endsWith(ext));
696
1697
  var filesFromDirectory = async (currentDir, entry, includeTests) => {
697
- return isIgnoredDirName(entry.name, includeTests) ? [] : walk(join2(currentDir, entry.name), includeTests);
1698
+ return isIgnoredDirName(entry.name, includeTests) ? [] : walk(join3(currentDir, entry.name), includeTests);
698
1699
  };
699
1700
  var filesFromFile = (currentDir, entry, includeTests) => {
700
- return entry.isFile() && isScannable(entry.name) && (includeTests || !isTestFileName(entry.name)) ? [join2(currentDir, entry.name)] : [];
1701
+ return entry.isFile() && isScannable(entry.name) && (includeTests || !isTestFileName(entry.name)) ? [join3(currentDir, entry.name)] : [];
701
1702
  };
702
1703
  var filesFromEntry = async (currentDir, entry, includeTests) => {
703
1704
  if (entry.isDirectory()) {
@@ -733,7 +1734,7 @@ var parseErrorFinding = (filePath, error) => ({
733
1734
  });
734
1735
  var readFileContent = async (filePath) => {
735
1736
  try {
736
- return await readFile2(filePath, "utf-8");
1737
+ return await readFile3(filePath, "utf-8");
737
1738
  } catch (error) {
738
1739
  throw new Error(`N\xE3o foi poss\xEDvel ler o arquivo "${filePath}".`, { cause: error });
739
1740
  }
@@ -781,13 +1782,13 @@ var runScan = async (targetDir, rules, concurrency = DEFAULT_SCAN_CONCURRENCY, i
781
1782
 
782
1783
  // src/scanner/semgrep.ts
783
1784
  import { execFile as execFile2 } from "child_process";
784
- import { delimiter, dirname as dirname2 } from "path";
1785
+ import { delimiter, dirname as dirname3 } from "path";
785
1786
  import { promisify as promisify2 } from "util";
786
1787
 
787
1788
  // src/scanner/semgrep-runtime.ts
788
1789
  import { existsSync, readFileSync } from "fs";
789
1790
  import { createRequire } from "module";
790
- import { dirname, resolve } from "path";
1791
+ import { dirname as dirname2, resolve } from "path";
791
1792
  var require2 = createRequire(import.meta.url);
792
1793
  var packageForPlatform = (platform, architecture) => {
793
1794
  if (platform === "linux" && architecture === "x64") {
@@ -809,7 +1810,7 @@ var resolveBundledSemgrepRuntime = (platform = process.platform, architecture =
809
1810
  const manifestPath = resolveManifestPath(packageName);
810
1811
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
811
1812
  const runtime = {
812
- semgrep: resolve(dirname(manifestPath), manifest.semgrep)
1813
+ semgrep: resolve(dirname2(manifestPath), manifest.semgrep)
813
1814
  };
814
1815
  if (!existsSync(runtime.semgrep)) {
815
1816
  throw new Error("artefatos do runtime ausentes");
@@ -880,11 +1881,11 @@ var semgrepArgs = (ruleset, includeTests) => [
880
1881
  "."
881
1882
  ];
882
1883
  var semgrepEnvironment = (runtime) => {
883
- const semgrepDir = dirname2(runtime.semgrep);
1884
+ const semgrepDir = dirname3(runtime.semgrep);
884
1885
  const systemPathFallback = process.platform === "win32" ? "C:\\Windows\\System32;C:\\Windows" : "/usr/local/bin:/usr/bin:/bin";
885
1886
  return {
886
1887
  ...process.env,
887
- PATH: `${semgrepDir}${delimiter}${dirname2(semgrepDir)}${delimiter}${process.env.PATH ?? ""}${delimiter}${systemPathFallback}`
1888
+ PATH: `${semgrepDir}${delimiter}${dirname3(semgrepDir)}${delimiter}${process.env.PATH ?? ""}${delimiter}${systemPathFallback}`
888
1889
  };
889
1890
  };
890
1891
  var executeSemgrep = async (targetDir, runtime, ruleset, execute, includeTests) => {
@@ -943,9 +1944,9 @@ var writeMarkdownReportIfNeeded = async (result, targetDir) => {
943
1944
  if (result.findings.length <= MARKDOWN_REPORT_FINDINGS_THRESHOLD) {
944
1945
  return;
945
1946
  }
946
- const filePath = join3(targetDir, generateMarkdownReportFilename());
1947
+ const filePath = join4(targetDir, generateMarkdownReportFilename());
947
1948
  try {
948
- await writeFile(filePath, toMarkdownReport(result), "utf-8");
1949
+ await writeFile2(filePath, toMarkdownReport(result), "utf-8");
949
1950
  console.log(chalk2.cyan(`
950
1951
  Relat\xF3rio detalhado gerado em: ${filePath}`));
951
1952
  } catch (error) {
@@ -964,15 +1965,20 @@ var runNativeAndSemgrep = async (path, rules, options) => {
964
1965
  throw new Error(`Falha durante a an\xE1lise: ${errorMessage3(error)}`, { cause: error });
965
1966
  }
966
1967
  };
967
- var runOptionalDependencyAudit = async (path, merged) => {
1968
+ var runOptionalDependencyAudit = async (path, merged, options) => {
968
1969
  try {
969
- const auditResult = await runDependencyAudit(path);
1970
+ const auditResult = await runDependencyAudit(path, { nvdEnabled: options.nvd ?? true });
970
1971
  return finalizeScanResult(
971
1972
  {
972
1973
  ...merged,
973
1974
  findings: [...merged.findings, ...auditResult.findings],
1975
+ durationMs: merged.durationMs + auditResult.durationMs,
974
1976
  warnings: [...merged.warnings ?? [], ...auditResult.warnings ?? []],
975
- engines: { ...merged.engines, osv: auditResult.engines?.osv },
1977
+ engines: {
1978
+ ...merged.engines,
1979
+ osv: auditResult.engines?.osv,
1980
+ nvd: auditResult.engines?.nvd
1981
+ },
976
1982
  osvCheckedPackages: auditResult.osvCheckedPackages
977
1983
  },
978
1984
  auditResult.engines?.dependencyAudit ?? false
@@ -980,14 +1986,15 @@ var runOptionalDependencyAudit = async (path, merged) => {
980
1986
  } catch (error) {
981
1987
  return finalizeScanResult({
982
1988
  ...merged,
983
- warnings: [...merged.warnings ?? [], `Auditoria de depend\xEAncias falhou: ${errorMessage3(error)}.`]
1989
+ warnings: [...merged.warnings ?? [], `Auditoria de depend\xEAncias falhou: ${errorMessage3(error)}.`],
1990
+ engines: { ...merged.engines, nvd: false }
984
1991
  });
985
1992
  }
986
1993
  };
987
1994
  var runScanEngines = async (path, rules, options) => {
988
1995
  try {
989
1996
  const merged = await runNativeAndSemgrep(path, rules, options);
990
- return options.deps ? runOptionalDependencyAudit(path, merged) : finalizeScanResult(merged);
1997
+ return options.deps ? runOptionalDependencyAudit(path, merged, options) : finalizeScanResult(merged);
991
1998
  } catch (error) {
992
1999
  throw error;
993
2000
  }
@@ -1143,7 +2150,10 @@ var printAuditResult = (result, json) => {
1143
2150
  };
1144
2151
  var auditAndReport = async (path, options) => {
1145
2152
  try {
1146
- printAuditResult(await runDependencyAudit(path), options.json ?? false);
2153
+ printAuditResult(
2154
+ await runDependencyAudit(path, { nvdEnabled: options.nvd ?? true }),
2155
+ options.json ?? false
2156
+ );
1147
2157
  } catch (error) {
1148
2158
  const message = error instanceof Error ? error.message : "erro desconhecido";
1149
2159
  process.exitCode = 1;
@@ -1151,9 +2161,7 @@ var auditAndReport = async (path, options) => {
1151
2161
  }
1152
2162
  };
1153
2163
  var registerDependencyAuditCommand = (program) => {
1154
- program.command("dependency-audit").description(
1155
- 'Audita as depend\xEAncias do projeto contra vulnerabilidades conhecidas (via "npm audit" e OSV.dev; requer npm no PATH e acesso \xE0 rede)'
1156
- ).argument("[path]", "diret\xF3rio do projeto a ser auditado", ".").option("--json", "exibe o resultado em JSON").action((path, options) => auditAndReport(path, options));
2164
+ program.command("dependency-audit").description("Audita depend\xEAncias via npm audit e OSV.dev, com enriquecimento opcional do NVD").argument("[path]", "diret\xF3rio do projeto a ser auditado", ".").option("--json", "exibe o resultado em JSON").option("--no-nvd", "n\xE3o enriquece os resultados OSV com dados do NVD").action((path, options) => auditAndReport(path, options));
1157
2165
  };
1158
2166
 
1159
2167
  // src/rules/empty-catch.rule.ts
@@ -2952,9 +3960,9 @@ var registerScanCommand = (program) => {
2952
3960
  withScanOptions(
2953
3961
  program.command("scan").description("Analisa um diret\xF3rio em busca de vulnerabilidades e problemas de qualidade").argument("[path]", "diret\xF3rio a ser analisado", ".").option("--concurrency <n>", "limita arquivos processados em paralelo", parseConcurrency).option("--config <file>", "usa um ruleset Semgrep YAML local", parseLocalSemgrepConfig).option(
2954
3962
  "--no-deps",
2955
- "n\xE3o inclui a auditoria de depend\xEAncias (npm audit + OSV.dev) neste scan \u2014 permite rodar offline"
3963
+ "n\xE3o inclui a auditoria de depend\xEAncias (npm audit + OSV.dev + NVD) neste scan \u2014 permite rodar offline"
2956
3964
  )
2957
- ).action(
3965
+ ).option("--no-nvd", "n\xE3o enriquece os resultados OSV com dados do NVD").action(
2958
3966
  (path, options) => scanAndReport(path, allRules, "Scanning files...", { ...options, semgrep: true })
2959
3967
  );
2960
3968
  };