@codacy/tools-trivy-0 0.2.2 → 0.4.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 +47 -19
  2. package/dist/index.js +198 -1
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,8 +1,11 @@
1
1
  # @codacy/tools-trivy-0
2
2
 
3
3
  ## Table of Contents
4
+
4
5
  - [Overview](#overview)
5
6
  - [Umbrella patterns](#umbrella-patterns)
7
+ - [DB update management](#db-update-management)
8
+ - [Malicious package detection](#malicious-package-detection)
6
9
  - [Updating patterns](#updating-patterns)
7
10
  - [Updating the Trivy version](#updating-the-trivy-version)
8
11
  - [Development](#development)
@@ -14,28 +17,52 @@
14
17
 
15
18
  Security scanner using the [Trivy](https://trivy.dev/) binary. Scans for vulnerabilities in dependencies and hardcoded secrets. Uses the CLI execution strategy with two separate scan modes (vulnerability + secret).
16
19
 
17
- | Property | Value |
18
- |----------|-------|
19
- | Tool ID | `Trivy` |
20
- | Codacy UUID | `2fd7fbe0-33f9-4ab3-ab73-e9b62404e2cb` |
21
- | Strategy | CLI |
22
- | Languages | Multi-language (scans dependency manifests) |
23
- | Binary | `trivy` |
24
- | File patterns | `*` (auto-detects dependency manifests) |
25
- | Pattern count | 6 (umbrella patterns) |
20
+ | Property | Value |
21
+ | ------------- | ------------------------------------------- |
22
+ | Tool ID | `Trivy` |
23
+ | Codacy UUID | `2fd7fbe0-33f9-4ab3-ab73-e9b62404e2cb` |
24
+ | Strategy | CLI |
25
+ | Languages | Multi-language (scans dependency manifests) |
26
+ | Binary | `trivy` |
27
+ | File patterns | `*` (auto-detects dependency manifests) |
28
+ | Pattern count | 6 (umbrella patterns) |
26
29
 
27
30
  ## Umbrella patterns
28
31
 
29
32
  Trivy uses umbrella patterns rather than individual CVE patterns:
30
33
 
31
- | Pattern ID | Severity | Description |
32
- |------------|----------|-------------|
33
- | `Trivy_vulnerability_critical` | Error | Critical vulnerabilities |
34
- | `Trivy_vulnerability_high` | High | High-severity vulnerabilities |
35
- | `Trivy_vulnerability_medium` | Warning | Medium-severity vulnerabilities |
36
- | `Trivy_vulnerability_minor` | Info | Low-severity vulnerabilities |
37
- | `Trivy_secret` | Error | Hardcoded secrets |
38
- | `Trivy_malicious_packages` | Error | Malicious packages (reserved) |
34
+ | Pattern ID | Severity | Description |
35
+ | ------------------------------ | -------- | ------------------------------------ |
36
+ | `Trivy_vulnerability_critical` | Error | Critical vulnerabilities |
37
+ | `Trivy_vulnerability_high` | High | High-severity vulnerabilities |
38
+ | `Trivy_vulnerability_medium` | Warning | Medium-severity vulnerabilities |
39
+ | `Trivy_vulnerability_minor` | Info | Low-severity vulnerabilities |
40
+ | `Trivy_secret` | Error | Hardcoded secrets |
41
+ | `Trivy_malicious_packages` | Error | Malicious packages (via OSV.dev API) |
42
+
43
+ ## DB update management
44
+
45
+ The adapter reads Trivy's `metadata.json` (at `{cacheDir}/db/metadata.json`) to
46
+ determine if the vulnerability database is still fresh. The `NextUpdate` field
47
+ tells us when Trivy expects the next DB update.
48
+
49
+ | Scenario | Behavior |
50
+ | ------------------------------ | ------------------------------------------------------------------ |
51
+ | DB fresh (`now < NextUpdate`) | `--skip-db-update --skip-java-db-update` passed — no network check |
52
+ | DB stale (`now >= NextUpdate`) | Trivy checks for updates normally |
53
+ | First run (no metadata) | Trivy downloads the DB (~40MB) |
54
+ | Container mode | Always skips DB updates (DBs are pre-baked) |
55
+
56
+ ## Malicious package detection
57
+
58
+ Detects known malicious packages by querying the [OSV.dev batch API](https://api.osv.dev/v1/querybatch).
59
+ This piggybacks on the vulnerability scan output — the package list from `--list-all-pkgs`
60
+ is extracted and cross-referenced against the OpenSSF malicious packages index (`MAL-*` IDs).
61
+
62
+ - Covers 14 ecosystems: npm, PyPI, Go, RubyGems, crates.io, NuGet, Maven, Packagist, Pub, Hex, ConanCenter, CocoaPods, SwiftURL, conda
63
+ - Uses a 10-second timeout per API call
64
+ - On API failure (network error, timeout), logs a warning and skips — never blocks the core scan
65
+ - Message format: `Malicious package detected: {name}@{version} ({MAL-ID}). {summary}`
39
66
 
40
67
  ## Updating patterns
41
68
 
@@ -62,10 +89,11 @@ pnpm test # Run tests (requires trivy in PATH or auto-install)
62
89
 
63
90
  ## Notes for maintainers
64
91
 
65
- - Trivy runs **two separate scan invocations**: one for vulnerabilities (`--scanners vuln`) and one for secrets (`--scanners secret`). Which scans run depends on which umbrella patterns are enabled.
66
- - The vulnerability database is downloaded lazily on first run to `--cache-dir ~/.codacy/cache/trivy/`. Internet access is required on first use.
92
+ - Trivy runs **two separate scan invocations**: one for vulnerabilities (`--scanners vuln`) and one for secrets (`--scanners secret`). Malicious package detection piggybacks on the vuln scan output (no extra invocation). Which scans run depends on which umbrella patterns are enabled.
93
+ - The vulnerability database is downloaded lazily on first run to `--cache-dir ~/.codacy/cache/trivy/`. Internet access is required on first use. DB staleness is managed via `metadata.json` → `NextUpdate`.
67
94
  - Trivy scans **whole directories**, not individual files. Results are filtered to `ctx.targetFiles` after scanning.
68
95
  - The binary is downloaded from GitHub releases as a `.tar.gz` archive.
69
96
  - Vulnerability severity (CRITICAL/HIGH/MEDIUM/LOW) maps to the corresponding umbrella pattern.
70
97
  - Message format: `Insecure dependency {pkg}@{version} ({CVE}: {title}). Update to {fixedVersion}.`
98
+ - Malicious package detection uses the OSV.dev batch API. On failure, it degrades gracefully (warning only).
71
99
  - Config file: checks for `trivy.yaml` in the repo root.
package/dist/index.js CHANGED
@@ -126,6 +126,52 @@ var PATTERN_TO_SEVERITY = {
126
126
  Trivy_vulnerability_medium: "MEDIUM",
127
127
  Trivy_vulnerability_minor: "LOW"
128
128
  };
129
+ var TRIVY_TYPE_TO_OSV_ECOSYSTEM = {
130
+ npm: "npm",
131
+ pip: "PyPI",
132
+ pipenv: "PyPI",
133
+ poetry: "PyPI",
134
+ conda: "conda",
135
+ gomod: "Go",
136
+ gem: "RubyGems",
137
+ bundler: "RubyGems",
138
+ cargo: "crates.io",
139
+ nuget: "NuGet",
140
+ pom: "Maven",
141
+ gradle: "Maven",
142
+ composer: "Packagist",
143
+ pub: "Pub",
144
+ hex: "Hex",
145
+ conan: "ConanCenter",
146
+ cocoapods: "CocoaPods",
147
+ swift: "SwiftURL"
148
+ };
149
+ var OSV_BATCH_SIZE = 1e3;
150
+ var OSV_TIMEOUT_MS = 1e4;
151
+ async function queryOsvBatch(queries) {
152
+ const allResults = [];
153
+ for (let i = 0; i < queries.length; i += OSV_BATCH_SIZE) {
154
+ const batch = queries.slice(i, i + OSV_BATCH_SIZE);
155
+ const controller = new AbortController();
156
+ const timer = setTimeout(() => controller.abort(), OSV_TIMEOUT_MS);
157
+ try {
158
+ const response = await fetch("https://api.osv.dev/v1/querybatch", {
159
+ method: "POST",
160
+ headers: { "Content-Type": "application/json" },
161
+ body: JSON.stringify({ queries: batch }),
162
+ signal: controller.signal
163
+ });
164
+ if (!response.ok) {
165
+ throw new Error(`OSV API returned HTTP ${response.status}: ${response.statusText}`);
166
+ }
167
+ const data = await response.json();
168
+ allResults.push(...data.results ?? []);
169
+ } finally {
170
+ clearTimeout(timer);
171
+ }
172
+ }
173
+ return { results: allResults };
174
+ }
129
175
  var BINARY = "trivy";
130
176
  var PLATFORM_MAP = {
131
177
  "darwin-arm64": "macOS-ARM64",
@@ -176,6 +222,24 @@ async function ensureCacheDir(globalDir) {
176
222
  await import_promises.default.mkdir(cacheDir, { recursive: true });
177
223
  return cacheDir;
178
224
  }
225
+ async function checkDbFreshness(cacheDir) {
226
+ const metadataPath = import_path.default.join(cacheDir, "db", "metadata.json");
227
+ try {
228
+ const raw = await import_promises.default.readFile(metadataPath, "utf-8");
229
+ const meta = JSON.parse(raw);
230
+ if (!meta.NextUpdate) {
231
+ return { fresh: false, error: "metadata.json missing NextUpdate field" };
232
+ }
233
+ const nextUpdateMs = Date.parse(meta.NextUpdate);
234
+ if (Number.isNaN(nextUpdateMs)) {
235
+ return { fresh: false, error: `Invalid NextUpdate timestamp: ${meta.NextUpdate}` };
236
+ }
237
+ return Date.now() < nextUpdateMs ? { fresh: true, nextUpdate: meta.NextUpdate } : { fresh: false, nextUpdate: meta.NextUpdate };
238
+ } catch (err) {
239
+ const message = err instanceof Error ? err.message : String(err);
240
+ return { fresh: false, error: message };
241
+ }
242
+ }
179
243
  async function runTrivyScan(binary, args, cwd, errors) {
180
244
  const { stdout, stderr, exitCode } = await (0, import_tooling.spawnTool)(binary, args, { cwd });
181
245
  if (exitCode !== 0) {
@@ -286,6 +350,103 @@ async function parseSecrets(output, enabledPatterns, targetFilesSet, repositoryR
286
350
  }
287
351
  return issues;
288
352
  }
353
+ async function parseMaliciousPackages(output, enabledPatterns, targetFilesSet, repositoryRoot, logger) {
354
+ const issues = [];
355
+ const errors = [];
356
+ const patternId = "Trivy_malicious_packages";
357
+ if (enabledPatterns.size > 0 && !enabledPatterns.has(patternId)) {
358
+ return { issues, errors };
359
+ }
360
+ if (!output.Results) return { issues, errors };
361
+ const meta = PATTERN_META.get(patternId) ?? {
362
+ category: "Security",
363
+ severity: "Error"
364
+ };
365
+ const packages = [];
366
+ const seen = /* @__PURE__ */ new Set();
367
+ for (const result of output.Results) {
368
+ if (!result.Packages || !result.Type) continue;
369
+ const ecosystem = TRIVY_TYPE_TO_OSV_ECOSYSTEM[result.Type];
370
+ if (!ecosystem) continue;
371
+ const relPath = result.Target;
372
+ if (targetFilesSet.size > 0 && !targetFilesSet.has(relPath)) continue;
373
+ for (const pkg of result.Packages) {
374
+ const key = `${ecosystem}:${pkg.Name}:${pkg.Version}:${relPath}`;
375
+ if (seen.has(key)) continue;
376
+ seen.add(key);
377
+ const line = pkg.Locations && pkg.Locations.length > 0 && pkg.Locations[0].StartLine > 0 ? pkg.Locations[0].StartLine : 1;
378
+ packages.push({ ecosystem, name: pkg.Name, version: pkg.Version, filePath: relPath, line });
379
+ }
380
+ }
381
+ if (packages.length === 0) return { issues, errors };
382
+ const queryMap = /* @__PURE__ */ new Map();
383
+ for (const pkg of packages) {
384
+ const queryKey = `${pkg.ecosystem}:${pkg.name}:${pkg.version}`;
385
+ if (!queryMap.has(queryKey)) {
386
+ queryMap.set(queryKey, {
387
+ package: { name: pkg.name, ecosystem: pkg.ecosystem },
388
+ version: pkg.version
389
+ });
390
+ }
391
+ }
392
+ const queries = Array.from(queryMap.values());
393
+ let osvResponse;
394
+ try {
395
+ logger.debug("Querying OSV.dev for malicious packages", { packageCount: queries.length });
396
+ osvResponse = await queryOsvBatch(queries);
397
+ } catch (err) {
398
+ const message = err instanceof Error ? err.message : String(err);
399
+ logger.warn(`Malicious package check skipped: ${message}`);
400
+ errors.push({
401
+ toolId: TOOL_ID,
402
+ phase: "toolInvoke",
403
+ kind: "NetworkError",
404
+ message: `OSV.dev API call failed: ${message}. Malicious package detection skipped.`,
405
+ level: "warning"
406
+ });
407
+ return { issues, errors };
408
+ }
409
+ const maliciousByPkg = /* @__PURE__ */ new Map();
410
+ const queryList = Array.from(queryMap.keys());
411
+ for (let i = 0; i < osvResponse.results.length; i++) {
412
+ const result = osvResponse.results[i];
413
+ if (!result.vulns) continue;
414
+ const malVulns = result.vulns.filter((v) => v.id.startsWith("MAL-"));
415
+ if (malVulns.length > 0 && i < queryList.length) {
416
+ maliciousByPkg.set(queryList[i], malVulns);
417
+ }
418
+ }
419
+ if (maliciousByPkg.size === 0) {
420
+ logger.debug("No malicious packages detected");
421
+ return { issues, errors };
422
+ }
423
+ const linesCache = /* @__PURE__ */ new Map();
424
+ for (const pkg of packages) {
425
+ const queryKey = `${pkg.ecosystem}:${pkg.name}:${pkg.version}`;
426
+ const malVulns = maliciousByPkg.get(queryKey);
427
+ if (!malVulns) continue;
428
+ if (!linesCache.has(pkg.filePath)) {
429
+ linesCache.set(pkg.filePath, await readFileLines(import_path.default.join(repositoryRoot, pkg.filePath)));
430
+ }
431
+ const lines = linesCache.get(pkg.filePath);
432
+ for (const vuln of malVulns) {
433
+ const summary = vuln.summary ? ` ${vuln.summary}` : "";
434
+ issues.push({
435
+ filePath: pkg.filePath,
436
+ patternId,
437
+ toolId: TOOL_ID,
438
+ line: pkg.line,
439
+ column: 1,
440
+ message: `Malicious package detected: ${pkg.name}@${pkg.version} (${vuln.id}).${summary}`,
441
+ lineContent: getLineContent(lines, pkg.line),
442
+ category: meta.category,
443
+ severity: meta.severity
444
+ });
445
+ }
446
+ }
447
+ logger.info("Malicious package check complete", { issuesFound: issues.length });
448
+ return { issues, errors };
449
+ }
289
450
  var adapter = {
290
451
  id: TOOL_ID,
291
452
  displayName: "Trivy",
@@ -448,6 +609,12 @@ var adapter = {
448
609
  if (ctx.targetFiles.length === 0) {
449
610
  return buildResult(startedAt, /* @__PURE__ */ new Date(), issues, errors, 0);
450
611
  }
612
+ if (!(codacyConfig.useLocalConfigurationFile && codacyConfig.localConfigurationFile) && codacyConfig.patterns.length === 0) {
613
+ ctx.logger.info("Skipping analysis: no local config and no patterns enabled", {
614
+ toolId: TOOL_ID
615
+ });
616
+ return buildResult(startedAt, /* @__PURE__ */ new Date(), issues, errors, ctx.targetFiles.length);
617
+ }
451
618
  const binary = await resolveBinaryPath(ctx);
452
619
  let cacheDir;
453
620
  try {
@@ -465,9 +632,28 @@ var adapter = {
465
632
  const enabledPatterns = new Set(codacyConfig.patterns.map((p) => p.patternId));
466
633
  const targetFilesSet = new Set(ctx.targetFiles);
467
634
  const vulnPatternIds = Object.values(VULN_SEVERITY_TO_PATTERN);
468
- const runVulnScan = enabledPatterns.size === 0 || vulnPatternIds.some((id) => enabledPatterns.has(id));
635
+ const runMaliciousScan = enabledPatterns.size === 0 || enabledPatterns.has("Trivy_malicious_packages");
636
+ const runVulnScan = enabledPatterns.size === 0 || vulnPatternIds.some((id) => enabledPatterns.has(id)) || runMaliciousScan;
469
637
  const runSecretScan = enabledPatterns.size === 0 || enabledPatterns.has("Trivy_secret");
470
638
  const baseArgs = ["filesystem", "--format", "json", "--cache-dir", cacheDir];
639
+ if (runVulnScan) {
640
+ if (ctx.runner === "container") {
641
+ baseArgs.push("--skip-db-update", "--skip-java-db-update");
642
+ ctx.logger.debug("Container mode: skipping DB updates");
643
+ } else {
644
+ const dbStatus = await checkDbFreshness(cacheDir);
645
+ if (dbStatus.fresh) {
646
+ baseArgs.push("--skip-db-update", "--skip-java-db-update");
647
+ ctx.logger.debug("Trivy DB is fresh, skipping update check", {
648
+ nextUpdate: dbStatus.nextUpdate
649
+ });
650
+ } else {
651
+ ctx.logger.info(
652
+ dbStatus.error?.includes("ENOENT") ? "Trivy vulnerability database not found. Downloading (~40MB)..." : "Trivy DB update due, checking for new data..."
653
+ );
654
+ }
655
+ }
656
+ }
471
657
  if (codacyConfig.useLocalConfigurationFile && codacyConfig.localConfigurationFile) {
472
658
  baseArgs.push("--config", codacyConfig.localConfigurationFile);
473
659
  }
@@ -505,6 +691,17 @@ var adapter = {
505
691
  ctx.repositoryRoot
506
692
  )
507
693
  );
694
+ if (runMaliciousScan && ctx.runner !== "container") {
695
+ const malResult = await parseMaliciousPackages(
696
+ vulnOutput,
697
+ enabledPatterns,
698
+ targetFilesSet,
699
+ ctx.repositoryRoot,
700
+ ctx.logger
701
+ );
702
+ issues.push(...malResult.issues);
703
+ errors.push(...malResult.errors);
704
+ }
508
705
  }
509
706
  }
510
707
  if (runSecretScan) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/tools-trivy-0",
3
- "version": "0.2.2",
3
+ "version": "0.4.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.2.2"
11
+ "@codacy/tooling": "0.4.0"
12
12
  },
13
13
  "license": "MIT",
14
14
  "author": "Codacy <support@codacy.com> (https://www.codacy.com)",