@codacy/tools-trivy-0 0.3.0 → 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 +192 -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",
@@ -471,9 +632,28 @@ var adapter = {
471
632
  const enabledPatterns = new Set(codacyConfig.patterns.map((p) => p.patternId));
472
633
  const targetFilesSet = new Set(ctx.targetFiles);
473
634
  const vulnPatternIds = Object.values(VULN_SEVERITY_TO_PATTERN);
474
- 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;
475
637
  const runSecretScan = enabledPatterns.size === 0 || enabledPatterns.has("Trivy_secret");
476
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
+ }
477
657
  if (codacyConfig.useLocalConfigurationFile && codacyConfig.localConfigurationFile) {
478
658
  baseArgs.push("--config", codacyConfig.localConfigurationFile);
479
659
  }
@@ -511,6 +691,17 @@ var adapter = {
511
691
  ctx.repositoryRoot
512
692
  )
513
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
+ }
514
705
  }
515
706
  }
516
707
  if (runSecretScan) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codacy/tools-trivy-0",
3
- "version": "0.3.0",
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.3.0"
11
+ "@codacy/tooling": "0.4.0"
12
12
  },
13
13
  "license": "MIT",
14
14
  "author": "Codacy <support@codacy.com> (https://www.codacy.com)",