@codacy/tools-trivy-0 0.23.0 → 0.24.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/README.md CHANGED
@@ -12,6 +12,7 @@
12
12
  - [Known divergences](#known-divergences)
13
13
  - [DB update management](#db-update-management)
14
14
  - [Malicious package detection](#malicious-package-detection)
15
+ - [Container image mode](#container-image-mode)
15
16
  - [Updating patterns](#updating-patterns)
16
17
  - [Updating the Trivy version](#updating-the-trivy-version)
17
18
  - [Development](#development)
@@ -177,6 +178,34 @@ is extracted and cross-referenced against the OpenSSF malicious packages index (
177
178
  - On API failure (network error, timeout), logs a warning and skips — never blocks the core scan
178
179
  - Message format: `Malicious package detected: {name}@{version} ({MAL-ID}). {summary}`
179
180
 
181
+ ## Container image mode
182
+
183
+ Besides the repository scan, the adapter implements the optional `containerImage` capability
184
+ (`src/image.ts`), which backs the CLI's `container-scan` and `upload-sbom` commands. The descriptor
185
+ advertises it with `scansContainerImages: true`.
186
+
187
+ | Method | Invocation |
188
+ | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
189
+ | `scan` | `trivy image --format json --cache-dir … [--skip-db-update --skip-java-db-update] --scanners vuln [--ignore-unfixed] --severity … --pkg-types … <image>` |
190
+ | `generateSbom` | `trivy image --format cyclonedx\|spdx-json --output <path> --cache-dir … <image>` |
191
+
192
+ - The image reference is re-validated with `validateImageReference` right before it becomes an argv
193
+ element, even though the runner validated it first.
194
+ - `--exit-code` is deliberately **not** passed. The finding count comes from the parsed JSON, so any
195
+ non-zero exit is a real failure — no stderr heuristics like cli-v2's to decide _whether_ it failed.
196
+ - Findings reuse `describeVulnerability` (`src/findings.ts`), the same mapping the repository scan
197
+ uses: umbrella pattern by severity, `codacy-trivy`'s message format, CVE as `sourceId`, `UNKNOWN`
198
+ dropped. The location is Trivy's `Target` (the OS layer or an in-image manifest) at line 1.
199
+ - `generateSbom` succeeds only if Trivy exits 0 **and** wrote a non-empty file.
200
+ - Every Trivy spawn (both modes) sets `TRIVY_SKIP_VERSION_CHECK=true`, and a failure's stderr goes
201
+ through `summarizeTrivyStderr` (`src/trivy-errors.ts`) rather than being echoed: Trivy's stderr is
202
+ a log stream (INFO lines, an ANSI version notice, then a FATAL Go error-wrap chain), and the text
203
+ formatter truncates errors at 240 characters, which used to cut it off before the cause. The env
204
+ var rather than `--skip-version-check`, because an older global Trivy would reject the flag.
205
+ - `test/fixtures/image-scan.json` is trimmed from a real `trivy image` run against
206
+ `nginx:1.21.0-alpine`, plus one synthetic `UNKNOWN` finding and one synthetic library finding.
207
+ `CODACY_TRIVY_IMAGE_IT=1 pnpm test` runs the opt-in integration test against the real registry.
208
+
180
209
  ## Updating patterns
181
210
 
182
211
  ```bash
@@ -189,7 +218,7 @@ git add src/patterns.json
189
218
 
190
219
  ## Updating the Trivy version
191
220
 
192
- 1. Update `preferredVersion` in `src/adapter.ts`
221
+ 1. Update `preferredVersion` in `src/metadata.ts`
193
222
  2. Update the download URL template if the release format changed
194
223
  3. Run `pnpm test` to verify compatibility
195
224
 
package/dist/index.js CHANGED
@@ -233,18 +233,19 @@ var patterns_default = [
233
233
  // src/metadata.ts
234
234
  var descriptor = {
235
235
  id: "Trivy",
236
+ shortName: "trivy",
236
237
  displayName: "Trivy",
237
238
  preferredVersion: "0.72.0",
238
239
  languages: [
239
240
  "Python",
240
- "JavaScript",
241
+ "Javascript",
241
242
  "TypeScript",
242
243
  "Go",
243
244
  "Java",
244
245
  "Ruby",
245
246
  "Rust",
246
247
  "PHP",
247
- "C#",
248
+ "CSharp",
248
249
  "Dart",
249
250
  "Elixir",
250
251
  "Swift"
@@ -257,7 +258,9 @@ var descriptor = {
257
258
  // issues must upload `sourceId` (the CVE id) or two CVEs on the same manifest
258
259
  // line collapse into a single Codacy result. Matches `needs_source_id_upload`
259
260
  // in the Go CLI's Trivy plugin manifest.
260
- needsSourceIdUpload: true
261
+ needsSourceIdUpload: true,
262
+ // Implements `containerImage` (`trivy image`) for `container-scan` / `upload-sbom`.
263
+ scansContainerImages: true
261
264
  };
262
265
 
263
266
  // src/vuln-mapping.ts
@@ -366,8 +369,7 @@ function comparePrerelease(x, y) {
366
369
  return xs.length < ys.length ? -1 : 1;
367
370
  }
368
371
 
369
- // src/adapter.ts
370
- var TOOL_ID = "Trivy";
372
+ // src/findings.ts
371
373
  var PATTERN_META = new Map(
372
374
  patterns_default.map((p) => [
373
375
  p.id,
@@ -383,6 +385,158 @@ var VULN_SEVERITY_TO_PATTERN = {
383
385
  MEDIUM: "Trivy_vulnerability_medium",
384
386
  LOW: "Trivy_vulnerability_minor"
385
387
  };
388
+ function describeVulnerability(vuln) {
389
+ const patternId = VULN_SEVERITY_TO_PATTERN[vuln.Severity];
390
+ if (!patternId) return void 0;
391
+ const meta = PATTERN_META.get(patternId) ?? {
392
+ category: "Security",
393
+ severity: "Info"
394
+ };
395
+ const fixedVersion = findLeastDisruptiveFixedVersion(
396
+ vuln.InstalledVersion,
397
+ vuln.FixedVersion ?? ""
398
+ );
399
+ const fix = fixedVersion ? `(update to ${fixedVersion})` : "(no fix available)";
400
+ const purl = vuln.PkgIdentifier?.PURL;
401
+ const pkgLabel = purl ? purlPrettyPrint(purl) : `${vuln.PkgName}@${vuln.InstalledVersion}`;
402
+ return {
403
+ patternId,
404
+ message: `Insecure dependency ${pkgLabel} (${vuln.VulnerabilityID}: ${vuln.Title ?? ""}) ${fix}`,
405
+ category: meta.category,
406
+ severity: meta.severity,
407
+ ...vuln.VulnerabilityID ? { sourceId: vuln.VulnerabilityID } : {}
408
+ };
409
+ }
410
+
411
+ // src/image.ts
412
+ function buildImageScanArgs(base, options) {
413
+ const args = [
414
+ "image",
415
+ "--format",
416
+ "json",
417
+ "--cache-dir",
418
+ base.cacheDir,
419
+ ...dbUpdateArgs(base),
420
+ "--scanners",
421
+ "vuln"
422
+ ];
423
+ if (options.ignoreUnfixed) args.push("--ignore-unfixed");
424
+ args.push("--severity", options.severities.join(","));
425
+ args.push("--pkg-types", options.pkgTypes.join(","));
426
+ args.push(options.image);
427
+ return args;
428
+ }
429
+ function buildSbomArgs(base, options) {
430
+ return [
431
+ "image",
432
+ "--format",
433
+ options.format,
434
+ "--output",
435
+ options.outputPath,
436
+ "--cache-dir",
437
+ base.cacheDir,
438
+ options.image
439
+ ];
440
+ }
441
+ function dbUpdateArgs(base) {
442
+ return base.skipDbUpdate ? ["--skip-db-update", "--skip-java-db-update"] : [];
443
+ }
444
+ function parseImageVulnerabilities(output, toolId) {
445
+ const issues = [];
446
+ for (const result of output.Results ?? []) {
447
+ for (const vuln of result.Vulnerabilities ?? []) {
448
+ const described = describeVulnerability(vuln);
449
+ if (!described) continue;
450
+ issues.push({
451
+ ...described,
452
+ filePath: result.Target,
453
+ toolId,
454
+ line: 1,
455
+ column: 1,
456
+ lineContent: ""
457
+ });
458
+ }
459
+ }
460
+ return issues;
461
+ }
462
+
463
+ // src/trivy-errors.ts
464
+ var TRIVY_ENV = { TRIVY_SKIP_VERSION_CHECK: "true" };
465
+ function stripAnsi(text) {
466
+ return text.replace(/\x1b\[[0-9;]*[A-Za-z]/g, "");
467
+ }
468
+ var WRAPPER_PREFIXES = [
469
+ /^run error:\s*/i,
470
+ /^(?:image|fs|filesystem|repository|rootfs|sbom|vm) scan error:\s*/i,
471
+ /^scan error:\s*/i,
472
+ /^unable to initialize (?:a scan service|artifact|container image):\s*/i
473
+ ];
474
+ function stripWrappers(chain) {
475
+ let current = chain.trim();
476
+ let changed = true;
477
+ while (changed) {
478
+ changed = false;
479
+ for (const prefix of WRAPPER_PREFIXES) {
480
+ const next = current.replace(prefix, "");
481
+ if (next !== current) {
482
+ current = next;
483
+ changed = true;
484
+ }
485
+ }
486
+ }
487
+ return current;
488
+ }
489
+ function summarizeTrivyStderr(stderr) {
490
+ const text = stripAnsi(stderr);
491
+ const lines = text.split(/\r?\n/);
492
+ const fatalIndex = lines.findIndex((l) => /\tFATAL\t|\sFATAL\s/.test(l));
493
+ if (fatalIndex !== -1) {
494
+ const chain = lines[fatalIndex].replace(/^.*?\bFATAL\s+(?:Fatal error\s+)?/, "");
495
+ const details = lines.slice(fatalIndex + 1).map((l) => l.trim());
496
+ const missing = chain.match(/unable to find the specified image "([^"]+)"/);
497
+ if (missing) {
498
+ const remote = details.find((l) => /^\*\s*remote error:/i.test(l))?.replace(/^\*\s*remote error:\s*/i, "");
499
+ const base = `Image "${missing[1]}" not found locally (Docker, containerd, Podman) or in its registry`;
500
+ if (!remote) return base;
501
+ if (/UNAUTHORIZED|authentication required|denied/i.test(remote)) {
502
+ return `${base}: access denied \u2014 the image or tag does not exist, or it is private and needs a registry login`;
503
+ }
504
+ return `${base}: ${remote.replace(/\s*\[map\[.*$/, "")}`;
505
+ }
506
+ return stripWrappers(chain).replace(/:\s*\d+ errors? occurred:?$/i, "");
507
+ }
508
+ return withoutNoise(lines).join(" ");
509
+ }
510
+ var TIMESTAMPED = /^\d{4}-\d{2}-\d{2}T\S+\s+/;
511
+ var QUIET_LOG_LINE = /^\d{4}-\d{2}-\d{2}T\S+\s+(?:DEBUG|INFO)\b/;
512
+ function withoutNoise(lines) {
513
+ const kept = [];
514
+ let inNotice = false;
515
+ for (const raw of lines) {
516
+ const line = raw.trim();
517
+ if (/Notices:/.test(line)) {
518
+ inNotice = true;
519
+ continue;
520
+ }
521
+ if (inNotice) {
522
+ if (/--skip-version-check/.test(line)) {
523
+ inNotice = false;
524
+ continue;
525
+ }
526
+ if (!TIMESTAMPED.test(line)) continue;
527
+ inNotice = false;
528
+ }
529
+ if (line.length === 0 || QUIET_LOG_LINE.test(line)) continue;
530
+ kept.push(line.replace(TIMESTAMPED, "").replace(/\t/g, " "));
531
+ }
532
+ return kept;
533
+ }
534
+ function trivyExitMessage(exitCode, stderr) {
535
+ return summarizeTrivyStderr(stderr) || `trivy exited with code ${exitCode}`;
536
+ }
537
+
538
+ // src/adapter.ts
539
+ var TOOL_ID = "Trivy";
386
540
  var PATTERN_TO_SEVERITY = {
387
541
  Trivy_vulnerability_critical: "CRITICAL",
388
542
  Trivy_vulnerability_high: "HIGH",
@@ -486,14 +640,18 @@ async function checkDbFreshness(cacheDir) {
486
640
  return { fresh: false, error: message };
487
641
  }
488
642
  }
489
- async function runTrivyScan(binary, args, cwd, errors) {
490
- const { stdout, stderr, exitCode } = await (0, import_tooling.spawnTool)(binary, args, { cwd });
643
+ async function runTrivyScan(binary, args, cwd, errors, spawnOptions = {}) {
644
+ const { stdout, stderr, exitCode } = await (0, import_tooling.spawnTool)(binary, args, {
645
+ cwd,
646
+ env: TRIVY_ENV,
647
+ ...spawnOptions
648
+ });
491
649
  if (exitCode !== 0) {
492
650
  errors.push({
493
651
  toolId: TOOL_ID,
494
652
  phase: "toolInvoke",
495
653
  kind: "NonZeroExit",
496
- message: `trivy exited with code ${exitCode}${stderr ? `: ${stderr.trim()}` : ""}`,
654
+ message: trivyExitMessage(exitCode, stderr),
497
655
  level: "error"
498
656
  });
499
657
  return null;
@@ -514,7 +672,6 @@ async function runTrivyScan(binary, args, cwd, errors) {
514
672
  return null;
515
673
  }
516
674
  }
517
- var LINE_NUMBERS_NOT_SUPPORTED = "Line numbers not supported";
518
675
  function recordUnlocatableFinding(errors, seenFiles, filePath) {
519
676
  if (seenFiles.has(filePath)) return;
520
677
  seenFiles.add(filePath);
@@ -522,9 +679,10 @@ function recordUnlocatableFinding(errors, seenFiles, filePath) {
522
679
  toolId: TOOL_ID,
523
680
  phase: "outputParse",
524
681
  kind: "LineNumbersNotSupported",
525
- message: LINE_NUMBERS_NOT_SUPPORTED,
682
+ message: import_tooling.LINE_NUMBERS_NOT_SUPPORTED,
526
683
  level: "warning",
527
- filePath
684
+ filePath,
685
+ fileNotAnalyzed: true
528
686
  });
529
687
  }
530
688
  async function parseVulnerabilities(output, enabledPatterns, targetFilesSet, repositoryRoot, unlocatableFiles) {
@@ -556,13 +714,9 @@ async function parseVulnerabilities(output, enabledPatterns, targetFilesSet, rep
556
714
  return found;
557
715
  };
558
716
  for (const vuln of result.Vulnerabilities) {
559
- const patternId = VULN_SEVERITY_TO_PATTERN[vuln.Severity];
560
- if (!patternId) continue;
561
- if (enabledPatterns.size > 0 && !enabledPatterns.has(patternId)) continue;
562
- const meta = PATTERN_META.get(patternId) ?? {
563
- category: "Security",
564
- severity: "Info"
565
- };
717
+ const described = describeVulnerability(vuln);
718
+ if (!described) continue;
719
+ if (enabledPatterns.size > 0 && !enabledPatterns.has(described.patternId)) continue;
566
720
  const purl = vuln.PkgIdentifier?.PURL;
567
721
  const reportedLine = (purl !== void 0 ? lineByPurl.get(purl) : void 0) ?? (vuln.PkgID !== void 0 ? lineByPkgId.get(vuln.PkgID) : void 0) ?? 0;
568
722
  const line = reportedLine > 0 ? reportedLine : resolveFallbackLine(vuln.PkgName);
@@ -570,23 +724,13 @@ async function parseVulnerabilities(output, enabledPatterns, targetFilesSet, rep
570
724
  recordUnlocatableFinding(errors, unlocatableFiles, relPath);
571
725
  continue;
572
726
  }
573
- const fixedVersion = findLeastDisruptiveFixedVersion(
574
- vuln.InstalledVersion,
575
- vuln.FixedVersion ?? ""
576
- );
577
- const fix = fixedVersion ? `(update to ${fixedVersion})` : "(no fix available)";
578
- const pkgLabel = purl ? purlPrettyPrint(purl) : `${vuln.PkgName}@${vuln.InstalledVersion}`;
579
727
  issues.push({
728
+ ...described,
580
729
  filePath: relPath,
581
- patternId,
582
730
  toolId: TOOL_ID,
583
731
  line,
584
732
  column: 1,
585
- message: `Insecure dependency ${pkgLabel} (${vuln.VulnerabilityID}: ${vuln.Title ?? ""}) ${fix}`,
586
- lineContent: (0, import_tooling.getLineContent)(lines, line),
587
- category: meta.category,
588
- severity: meta.severity,
589
- ...vuln.VulnerabilityID ? { sourceId: vuln.VulnerabilityID } : {}
733
+ lineContent: (0, import_tooling.getLineContent)(lines, line)
590
734
  });
591
735
  }
592
736
  }
@@ -737,12 +881,101 @@ async function parseMaliciousPackages(output, enabledPatterns, targetFilesSet, r
737
881
  logger.info("Malicious package check complete", { issuesFound: issues.length });
738
882
  return { issues, errors };
739
883
  }
884
+ async function scanImage(ctx, options) {
885
+ const startedAt = /* @__PURE__ */ new Date();
886
+ const errors = [];
887
+ const fail = (kind, message) => {
888
+ errors.push({ toolId: TOOL_ID, phase: "toolInvoke", kind, message, level: "error" });
889
+ const result = (0, import_tooling.buildResult)(TOOL_ID, startedAt, /* @__PURE__ */ new Date(), [], errors, 0);
890
+ return { ...result, summary: { ...result.summary, status: "failed" } };
891
+ };
892
+ try {
893
+ (0, import_tooling.validateImageReference)(options.image);
894
+ } catch (err) {
895
+ return fail("InvalidImageReference", errorMessage(err));
896
+ }
897
+ ctx.logger.info("Starting image scan", { toolId: TOOL_ID, image: options.image });
898
+ try {
899
+ const binary = await resolveBinaryPath(ctx);
900
+ const cacheDir = await ensureCacheDir(ctx.globalDir);
901
+ const skipDbUpdate = await shouldSkipDbUpdate(ctx, cacheDir);
902
+ const args = buildImageScanArgs({ cacheDir, skipDbUpdate }, options);
903
+ const output = await runTrivyScan(binary, args, ctx.repositoryRoot, errors, {
904
+ signal: ctx.signal,
905
+ logger: ctx.logger
906
+ });
907
+ if (!output) {
908
+ const result = (0, import_tooling.buildResult)(TOOL_ID, startedAt, /* @__PURE__ */ new Date(), [], errors, 0);
909
+ return { ...result, summary: { ...result.summary, status: "failed" } };
910
+ }
911
+ const issues = parseImageVulnerabilities(output, TOOL_ID);
912
+ ctx.logger.info("Image scan complete", { toolId: TOOL_ID, issueCount: issues.length });
913
+ return (0, import_tooling.buildResult)(TOOL_ID, startedAt, /* @__PURE__ */ new Date(), issues, errors, 1);
914
+ } catch (err) {
915
+ if (ctx.signal?.aborted) return fail("Cancelled", "Image scan cancelled by caller");
916
+ return fail("InvocationError", `Failed to run trivy: ${errorMessage(err)}`);
917
+ }
918
+ }
919
+ async function generateImageSbom(ctx, options) {
920
+ const failure = (kind, message) => ({
921
+ success: false,
922
+ error: { toolId: TOOL_ID, phase: "toolInvoke", kind, message, level: "error" }
923
+ });
924
+ try {
925
+ (0, import_tooling.validateImageReference)(options.image);
926
+ } catch (err) {
927
+ return failure("InvalidImageReference", errorMessage(err));
928
+ }
929
+ try {
930
+ const binary = await resolveBinaryPath(ctx);
931
+ const cacheDir = await ensureCacheDir(ctx.globalDir);
932
+ const args = buildSbomArgs({ cacheDir }, options);
933
+ ctx.logger.info("Generating SBOM", { toolId: TOOL_ID, image: options.image });
934
+ const { stderr, exitCode } = await (0, import_tooling.spawnTool)(binary, args, {
935
+ cwd: ctx.repositoryRoot,
936
+ env: TRIVY_ENV,
937
+ logger: ctx.logger,
938
+ signal: ctx.signal
939
+ });
940
+ if (exitCode !== 0) {
941
+ return failure("NonZeroExit", trivyExitMessage(exitCode, stderr));
942
+ }
943
+ const stat = await import_promises.default.stat(options.outputPath).catch(() => null);
944
+ if (!stat || stat.size === 0) {
945
+ return failure(
946
+ "MissingOutput",
947
+ `trivy exited cleanly but wrote no SBOM to ${options.outputPath}`
948
+ );
949
+ }
950
+ return { success: true };
951
+ } catch (err) {
952
+ if (ctx.signal?.aborted) return failure("Cancelled", "SBOM generation cancelled by caller");
953
+ return failure("InvocationError", `Failed to run trivy: ${errorMessage(err)}`);
954
+ }
955
+ }
956
+ async function shouldSkipDbUpdate(ctx, cacheDir) {
957
+ if (ctx.runner === "container") return true;
958
+ const status = await checkDbFreshness(cacheDir);
959
+ if (!status.fresh) {
960
+ ctx.logger.info(
961
+ status.error?.includes("ENOENT") ? "Trivy vulnerability database not found. Downloading (~40MB)..." : "Trivy DB update due, checking for new data..."
962
+ );
963
+ }
964
+ return status.fresh;
965
+ }
966
+ function errorMessage(err) {
967
+ return err instanceof Error ? err.message : String(err);
968
+ }
740
969
  var adapter = {
741
970
  ...descriptor,
742
971
  patternsMetadata: patterns_default,
743
972
  compatibleVersions: ">=0.50.0",
744
973
  website: "https://trivy.dev/",
745
974
  prerequisites: [],
975
+ containerImage: {
976
+ scan: scanImage,
977
+ generateSbom: generateImageSbom
978
+ },
746
979
  /**
747
980
  * Checks whether the `trivy` binary is present and runnable.
748
981
  *
package/dist/metadata.js CHANGED
@@ -25,18 +25,19 @@ __export(metadata_exports, {
25
25
  module.exports = __toCommonJS(metadata_exports);
26
26
  var descriptor = {
27
27
  id: "Trivy",
28
+ shortName: "trivy",
28
29
  displayName: "Trivy",
29
30
  preferredVersion: "0.72.0",
30
31
  languages: [
31
32
  "Python",
32
- "JavaScript",
33
+ "Javascript",
33
34
  "TypeScript",
34
35
  "Go",
35
36
  "Java",
36
37
  "Ruby",
37
38
  "Rust",
38
39
  "PHP",
39
- "C#",
40
+ "CSharp",
40
41
  "Dart",
41
42
  "Elixir",
42
43
  "Swift"
@@ -49,7 +50,9 @@ var descriptor = {
49
50
  // issues must upload `sourceId` (the CVE id) or two CVEs on the same manifest
50
51
  // line collapse into a single Codacy result. Matches `needs_source_id_upload`
51
52
  // in the Go CLI's Trivy plugin manifest.
52
- needsSourceIdUpload: true
53
+ needsSourceIdUpload: true,
54
+ // Implements `containerImage` (`trivy image`) for `container-scan` / `upload-sbom`.
55
+ scansContainerImages: true
53
56
  };
54
57
  // Annotate the CommonJS export names for ESM import in node:
55
58
  0 && (module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/tools-trivy-0",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "Trivy adapter — CLI-mode security scanner",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -8,7 +8,7 @@
8
8
  "dist"
9
9
  ],
10
10
  "dependencies": {
11
- "@codacy/tooling": "0.23.0"
11
+ "@codacy/tooling": "0.24.0"
12
12
  },
13
13
  "license": "MIT",
14
14
  "author": "Codacy <support@codacy.com> (https://www.codacy.com)",