@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.
- package/README.md +47 -19
- package/dist/index.js +198 -1
- 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
|
|
18
|
-
|
|
19
|
-
| Tool ID
|
|
20
|
-
| Codacy UUID
|
|
21
|
-
| Strategy
|
|
22
|
-
| Languages
|
|
23
|
-
| Binary
|
|
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
|
|
32
|
-
|
|
33
|
-
| `Trivy_vulnerability_critical` | Error
|
|
34
|
-
| `Trivy_vulnerability_high`
|
|
35
|
-
| `Trivy_vulnerability_medium`
|
|
36
|
-
| `Trivy_vulnerability_minor`
|
|
37
|
-
| `Trivy_secret`
|
|
38
|
-
| `Trivy_malicious_packages`
|
|
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
|
|
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.
|
|
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.
|
|
11
|
+
"@codacy/tooling": "0.4.0"
|
|
12
12
|
},
|
|
13
13
|
"license": "MIT",
|
|
14
14
|
"author": "Codacy <support@codacy.com> (https://www.codacy.com)",
|