@codacy/tools-trivy-0 0.1.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/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +484 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Codacy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# @codacy/tools-trivy-0
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Overview](#overview)
|
|
5
|
+
- [Umbrella patterns](#umbrella-patterns)
|
|
6
|
+
- [Updating patterns](#updating-patterns)
|
|
7
|
+
- [Updating the Trivy version](#updating-the-trivy-version)
|
|
8
|
+
- [Development](#development)
|
|
9
|
+
- [Notes for maintainers](#notes-for-maintainers)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Overview
|
|
14
|
+
|
|
15
|
+
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
|
+
|
|
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) |
|
|
26
|
+
|
|
27
|
+
## Umbrella patterns
|
|
28
|
+
|
|
29
|
+
Trivy uses umbrella patterns rather than individual CVE patterns:
|
|
30
|
+
|
|
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) |
|
|
39
|
+
|
|
40
|
+
## Updating patterns
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Re-fetch pattern metadata from the Codacy API
|
|
44
|
+
pnpm prefetch
|
|
45
|
+
|
|
46
|
+
# Commit the result
|
|
47
|
+
git add src/patterns.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Updating the Trivy version
|
|
51
|
+
|
|
52
|
+
1. Update `preferredVersion` in `src/adapter.ts`
|
|
53
|
+
2. Update the download URL template if the release format changed
|
|
54
|
+
3. Run `pnpm test` to verify compatibility
|
|
55
|
+
|
|
56
|
+
## Development
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pnpm build # Build with tsup
|
|
60
|
+
pnpm test # Run tests (requires trivy in PATH or auto-install)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Notes for maintainers
|
|
64
|
+
|
|
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.
|
|
67
|
+
- Trivy scans **whole directories**, not individual files. Results are filtered to `ctx.targetFiles` after scanning.
|
|
68
|
+
- The binary is downloaded from GitHub releases as a `.tar.gz` archive.
|
|
69
|
+
- Vulnerability severity (CRITICAL/HIGH/MEDIUM/LOW) maps to the corresponding umbrella pattern.
|
|
70
|
+
- Message format: `Insecure dependency {pkg}@{version} ({CVE}: {title}). Update to {fixedVersion}.`
|
|
71
|
+
- Config file: checks for `trivy.yaml` in the repo root.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
default: () => index_default
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
var import_tooling2 = require("@codacy/tooling");
|
|
37
|
+
|
|
38
|
+
// src/adapter.ts
|
|
39
|
+
var import_tooling = require("@codacy/tooling");
|
|
40
|
+
var import_path = __toESM(require("path"));
|
|
41
|
+
var import_promises = __toESM(require("fs/promises"));
|
|
42
|
+
|
|
43
|
+
// src/patterns.json
|
|
44
|
+
var patterns_default = [
|
|
45
|
+
{
|
|
46
|
+
id: "Trivy_malicious_packages",
|
|
47
|
+
category: "Security",
|
|
48
|
+
severityLevel: "Error",
|
|
49
|
+
title: "Detect Malicious Packages",
|
|
50
|
+
enabled: true,
|
|
51
|
+
parameters: []
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: "Trivy_secret",
|
|
55
|
+
category: "Security",
|
|
56
|
+
severityLevel: "Error",
|
|
57
|
+
title: "Detect Secret Exposure",
|
|
58
|
+
enabled: true,
|
|
59
|
+
parameters: []
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "Trivy_vulnerability_critical",
|
|
63
|
+
category: "Security",
|
|
64
|
+
severityLevel: "Error",
|
|
65
|
+
title: "Detect Insecure Dependencies (Critical Severity)",
|
|
66
|
+
enabled: true,
|
|
67
|
+
parameters: []
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "Trivy_vulnerability_high",
|
|
71
|
+
category: "Security",
|
|
72
|
+
severityLevel: "High",
|
|
73
|
+
title: "Detect Insecure Dependencies (High Severity)",
|
|
74
|
+
enabled: true,
|
|
75
|
+
parameters: []
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "Trivy_vulnerability_medium",
|
|
79
|
+
category: "Security",
|
|
80
|
+
severityLevel: "Warning",
|
|
81
|
+
title: "Detect Insecure Dependencies (Medium Severity)",
|
|
82
|
+
enabled: true,
|
|
83
|
+
parameters: []
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "Trivy_vulnerability_minor",
|
|
87
|
+
category: "Security",
|
|
88
|
+
severityLevel: "Info",
|
|
89
|
+
title: "Detect Insecure Dependencies (Minor Severity)",
|
|
90
|
+
enabled: true,
|
|
91
|
+
parameters: []
|
|
92
|
+
}
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// src/adapter.ts
|
|
96
|
+
var TOOL_ID = "Trivy";
|
|
97
|
+
function getLineContent(lines, lineNumber) {
|
|
98
|
+
return lines[lineNumber - 1] ?? "";
|
|
99
|
+
}
|
|
100
|
+
async function readFileLines(filePath) {
|
|
101
|
+
try {
|
|
102
|
+
const content = await import_promises.default.readFile(filePath, "utf-8");
|
|
103
|
+
return content.split("\n");
|
|
104
|
+
} catch {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
var PATTERN_META = new Map(
|
|
109
|
+
patterns_default.map((p) => [
|
|
110
|
+
p.id,
|
|
111
|
+
{
|
|
112
|
+
category: p.category,
|
|
113
|
+
severity: p.severityLevel ?? "Info"
|
|
114
|
+
}
|
|
115
|
+
])
|
|
116
|
+
);
|
|
117
|
+
var VULN_SEVERITY_TO_PATTERN = {
|
|
118
|
+
CRITICAL: "Trivy_vulnerability_critical",
|
|
119
|
+
HIGH: "Trivy_vulnerability_high",
|
|
120
|
+
MEDIUM: "Trivy_vulnerability_medium",
|
|
121
|
+
LOW: "Trivy_vulnerability_minor"
|
|
122
|
+
};
|
|
123
|
+
var PATTERN_TO_SEVERITY = {
|
|
124
|
+
Trivy_vulnerability_critical: "CRITICAL",
|
|
125
|
+
Trivy_vulnerability_high: "HIGH",
|
|
126
|
+
Trivy_vulnerability_medium: "MEDIUM",
|
|
127
|
+
Trivy_vulnerability_minor: "LOW"
|
|
128
|
+
};
|
|
129
|
+
var BINARY = "trivy";
|
|
130
|
+
var PLATFORM_MAP = {
|
|
131
|
+
"darwin-arm64": "macOS-ARM64",
|
|
132
|
+
"darwin-x64": "macOS-64bit",
|
|
133
|
+
"linux-x64": "Linux-64bit",
|
|
134
|
+
"linux-arm64": "Linux-ARM64"
|
|
135
|
+
};
|
|
136
|
+
async function getVersionFromBinary(binaryPath) {
|
|
137
|
+
try {
|
|
138
|
+
const { stdout, exitCode } = await (0, import_tooling.spawnTool)(binaryPath, ["--version"], {
|
|
139
|
+
cwd: process.cwd()
|
|
140
|
+
});
|
|
141
|
+
if (exitCode !== 0) return null;
|
|
142
|
+
const match = stdout.match(/Version:\s*(\S+)/);
|
|
143
|
+
return match ? match[1] : null;
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function resolveBinaryPath(ctx) {
|
|
149
|
+
const codacyBin = import_path.default.join(ctx.toolInstallDir, BINARY);
|
|
150
|
+
const version = await getVersionFromBinary(codacyBin);
|
|
151
|
+
if (version) return codacyBin;
|
|
152
|
+
return BINARY;
|
|
153
|
+
}
|
|
154
|
+
function buildResult(startedAt, completedAt, issues, errors, filesAnalyzed) {
|
|
155
|
+
const durationMs = completedAt.getTime() - startedAt.getTime();
|
|
156
|
+
return {
|
|
157
|
+
metadata: {
|
|
158
|
+
startedAt: startedAt.toISOString(),
|
|
159
|
+
completedAt: completedAt.toISOString(),
|
|
160
|
+
durationMs
|
|
161
|
+
},
|
|
162
|
+
issues,
|
|
163
|
+
errors,
|
|
164
|
+
summary: {
|
|
165
|
+
toolId: TOOL_ID,
|
|
166
|
+
status: errors.some((e) => e.level === "error") ? "partial" : "success",
|
|
167
|
+
issueCount: issues.length,
|
|
168
|
+
errorCount: errors.length,
|
|
169
|
+
durationMs,
|
|
170
|
+
filesAnalyzed
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function ensureCacheDir(globalDir) {
|
|
175
|
+
const cacheDir = import_path.default.join(globalDir, "cache", "trivy");
|
|
176
|
+
await import_promises.default.mkdir(cacheDir, { recursive: true });
|
|
177
|
+
return cacheDir;
|
|
178
|
+
}
|
|
179
|
+
async function runTrivyScan(binary, args, cwd, errors) {
|
|
180
|
+
const { stdout, stderr, exitCode } = await (0, import_tooling.spawnTool)(binary, args, { cwd });
|
|
181
|
+
if (exitCode !== 0) {
|
|
182
|
+
errors.push({
|
|
183
|
+
toolId: TOOL_ID,
|
|
184
|
+
phase: "toolInvoke",
|
|
185
|
+
kind: "NonZeroExit",
|
|
186
|
+
message: `trivy exited with code ${exitCode}${stderr ? `: ${stderr.trim()}` : ""}`,
|
|
187
|
+
level: "error"
|
|
188
|
+
});
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
if (!stdout.trim()) {
|
|
192
|
+
return { Results: [] };
|
|
193
|
+
}
|
|
194
|
+
try {
|
|
195
|
+
return JSON.parse(stdout);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
errors.push({
|
|
198
|
+
toolId: TOOL_ID,
|
|
199
|
+
phase: "outputParse",
|
|
200
|
+
kind: "MalformedOutput",
|
|
201
|
+
message: `Failed to parse trivy JSON output: ${err instanceof Error ? err.message : String(err)}`,
|
|
202
|
+
level: "error"
|
|
203
|
+
});
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
async function parseVulnerabilities(output, enabledPatterns, targetFilesSet, repositoryRoot) {
|
|
208
|
+
const issues = [];
|
|
209
|
+
if (!output.Results) return issues;
|
|
210
|
+
const linesCache = /* @__PURE__ */ new Map();
|
|
211
|
+
for (const result of output.Results) {
|
|
212
|
+
if (!result.Vulnerabilities) continue;
|
|
213
|
+
const relPath = result.Target;
|
|
214
|
+
if (targetFilesSet.size > 0 && !targetFilesSet.has(relPath)) continue;
|
|
215
|
+
if (!linesCache.has(relPath)) {
|
|
216
|
+
linesCache.set(relPath, await readFileLines(import_path.default.join(repositoryRoot, relPath)));
|
|
217
|
+
}
|
|
218
|
+
const lines = linesCache.get(relPath);
|
|
219
|
+
for (const vuln of result.Vulnerabilities) {
|
|
220
|
+
const patternId = VULN_SEVERITY_TO_PATTERN[vuln.Severity];
|
|
221
|
+
if (!patternId) continue;
|
|
222
|
+
if (enabledPatterns.size > 0 && !enabledPatterns.has(patternId)) continue;
|
|
223
|
+
const meta = PATTERN_META.get(patternId) ?? { category: "Security", severity: "Info" };
|
|
224
|
+
const title = vuln.Title ? `: ${vuln.Title}` : "";
|
|
225
|
+
const fix = vuln.FixedVersion ? `Update to ${vuln.FixedVersion}.` : "No fix available.";
|
|
226
|
+
issues.push({
|
|
227
|
+
filePath: relPath,
|
|
228
|
+
patternId,
|
|
229
|
+
toolId: TOOL_ID,
|
|
230
|
+
line: 1,
|
|
231
|
+
column: 1,
|
|
232
|
+
message: `Insecure dependency ${vuln.PkgName}@${vuln.InstalledVersion} (${vuln.VulnerabilityID}${title}). ${fix}`,
|
|
233
|
+
lineContent: getLineContent(lines, 1),
|
|
234
|
+
category: meta.category,
|
|
235
|
+
severity: meta.severity
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return issues;
|
|
240
|
+
}
|
|
241
|
+
async function parseSecrets(output, enabledPatterns, targetFilesSet, repositoryRoot) {
|
|
242
|
+
const issues = [];
|
|
243
|
+
if (!output.Results) return issues;
|
|
244
|
+
const patternId = "Trivy_secret";
|
|
245
|
+
if (enabledPatterns.size > 0 && !enabledPatterns.has(patternId)) return issues;
|
|
246
|
+
const meta = PATTERN_META.get(patternId) ?? { category: "Security", severity: "Error" };
|
|
247
|
+
const linesCache = /* @__PURE__ */ new Map();
|
|
248
|
+
for (const result of output.Results) {
|
|
249
|
+
if (!result.Secrets) continue;
|
|
250
|
+
const relPath = result.Target;
|
|
251
|
+
if (targetFilesSet.size > 0 && !targetFilesSet.has(relPath)) continue;
|
|
252
|
+
if (!linesCache.has(relPath)) {
|
|
253
|
+
linesCache.set(relPath, await readFileLines(import_path.default.join(repositoryRoot, relPath)));
|
|
254
|
+
}
|
|
255
|
+
const lines = linesCache.get(relPath);
|
|
256
|
+
for (const secret of result.Secrets) {
|
|
257
|
+
issues.push({
|
|
258
|
+
filePath: relPath,
|
|
259
|
+
patternId,
|
|
260
|
+
toolId: TOOL_ID,
|
|
261
|
+
line: secret.StartLine,
|
|
262
|
+
column: 1,
|
|
263
|
+
endLine: secret.EndLine,
|
|
264
|
+
message: `Possible hardcoded secret: ${secret.Title} (${secret.Category})`,
|
|
265
|
+
lineContent: getLineContent(lines, secret.StartLine),
|
|
266
|
+
multilineContent: (0, import_tooling.getMultilineContent)(lines, secret.StartLine, secret.EndLine),
|
|
267
|
+
category: meta.category,
|
|
268
|
+
severity: meta.severity
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return issues;
|
|
273
|
+
}
|
|
274
|
+
var adapter = {
|
|
275
|
+
id: TOOL_ID,
|
|
276
|
+
displayName: "Trivy",
|
|
277
|
+
preferredVersion: "0.69.3",
|
|
278
|
+
compatibleVersions: ">=0.50.0",
|
|
279
|
+
website: "https://trivy.dev/",
|
|
280
|
+
languages: ["Python", "JavaScript", "TypeScript", "Go", "Java", "Ruby", "Rust", "PHP", "C#", "Dart", "Elixir", "Swift"],
|
|
281
|
+
filePatterns: ["*"],
|
|
282
|
+
supportedOS: ["linux", "darwin"],
|
|
283
|
+
supportedArch: ["x64", "arm64"],
|
|
284
|
+
supportedRunners: ["local", "container"],
|
|
285
|
+
prerequisites: [],
|
|
286
|
+
/**
|
|
287
|
+
* Checks whether the `trivy` binary is present and runnable.
|
|
288
|
+
*
|
|
289
|
+
* Checks in order:
|
|
290
|
+
* 1. Codacy-managed path (`ctx.toolInstallDir/trivy`) → installation: "codacy"
|
|
291
|
+
* 2. Global PATH (`trivy`) → installation: "global"
|
|
292
|
+
*
|
|
293
|
+
* Parses the version from the output line `Version: X.Y.Z`. Returns
|
|
294
|
+
* `available: false` (rather than throwing) if the binary is not found or
|
|
295
|
+
* returns a non-zero exit code.
|
|
296
|
+
*/
|
|
297
|
+
async checkAvailability(ctx) {
|
|
298
|
+
ctx.logger.debug("Checking availability", { toolId: TOOL_ID });
|
|
299
|
+
const codacyBin = import_path.default.join(ctx.toolInstallDir, BINARY);
|
|
300
|
+
const codacyVersion = await getVersionFromBinary(codacyBin);
|
|
301
|
+
if (codacyVersion) {
|
|
302
|
+
ctx.logger.debug("Found trivy in codacy install dir", { toolId: TOOL_ID, version: codacyVersion });
|
|
303
|
+
return { available: true, version: codacyVersion, path: codacyBin, installation: "codacy" };
|
|
304
|
+
}
|
|
305
|
+
const globalVersion = await getVersionFromBinary(BINARY);
|
|
306
|
+
if (globalVersion) {
|
|
307
|
+
ctx.logger.debug("Found trivy on global PATH", { toolId: TOOL_ID, version: globalVersion });
|
|
308
|
+
return { available: true, version: globalVersion, installation: "global" };
|
|
309
|
+
}
|
|
310
|
+
ctx.logger.debug("trivy not found", { toolId: TOOL_ID });
|
|
311
|
+
return {
|
|
312
|
+
available: false,
|
|
313
|
+
reason: "trivy binary not found in PATH or Codacy install directory",
|
|
314
|
+
installHint: "brew install trivy # macOS\ncodacy-analysis analyze . --install-dependencies # auto-install"
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
/**
|
|
318
|
+
* Downloads trivy from GitHub releases and extracts it to ctx.toolInstallDir.
|
|
319
|
+
*
|
|
320
|
+
* Detects the current platform/arch and downloads the appropriate tar.gz archive.
|
|
321
|
+
* Sets executable permission after extraction.
|
|
322
|
+
*/
|
|
323
|
+
async install(ctx) {
|
|
324
|
+
ctx.logger.info("Starting install", { toolId: TOOL_ID });
|
|
325
|
+
const platformKey = `${ctx.platform.os}-${ctx.platform.arch}`;
|
|
326
|
+
const platformSuffix = PLATFORM_MAP[platformKey];
|
|
327
|
+
if (!platformSuffix) {
|
|
328
|
+
return {
|
|
329
|
+
success: false,
|
|
330
|
+
error: {
|
|
331
|
+
toolId: TOOL_ID,
|
|
332
|
+
phase: "toolInstall",
|
|
333
|
+
kind: "UnsupportedPlatform",
|
|
334
|
+
message: `Unsupported platform: ${platformKey}. Supported: ${Object.keys(PLATFORM_MAP).join(", ")}`,
|
|
335
|
+
level: "error"
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const version = adapter.preferredVersion;
|
|
340
|
+
const url = `https://github.com/aquasecurity/trivy/releases/download/v${version}/trivy_${version}_${platformSuffix}.tar.gz`;
|
|
341
|
+
const destPath = import_path.default.join(ctx.toolInstallDir, BINARY);
|
|
342
|
+
try {
|
|
343
|
+
ctx.logger.info(`Downloading trivy v${version} for ${platformKey}...`);
|
|
344
|
+
await (0, import_tooling.downloadAndExtract)(url, ctx.toolInstallDir, void 0, ctx.logger);
|
|
345
|
+
await import_promises.default.chmod(destPath, 493);
|
|
346
|
+
ctx.logger.info(`Installed trivy v${version} to ${destPath}`);
|
|
347
|
+
return { success: true, installedPath: destPath, installedVersion: version };
|
|
348
|
+
} catch (err) {
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
error: {
|
|
352
|
+
toolId: TOOL_ID,
|
|
353
|
+
phase: "toolInstall",
|
|
354
|
+
kind: "DownloadError",
|
|
355
|
+
message: `Failed to download trivy: ${err instanceof Error ? err.message : String(err)}`,
|
|
356
|
+
level: "error"
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
/**
|
|
362
|
+
* Checks for `trivy.yaml` in the repository root.
|
|
363
|
+
*
|
|
364
|
+
* Trivy can be configured via a `trivy.yaml` file. This method reports
|
|
365
|
+
* whether the file exists so the runner can surface it to the user.
|
|
366
|
+
*/
|
|
367
|
+
async checkLocalConfigurationFile(ctx) {
|
|
368
|
+
ctx.logger.debug("Checking for local config", { toolId: TOOL_ID });
|
|
369
|
+
const configPath = import_path.default.join(ctx.repositoryRoot, "trivy.yaml");
|
|
370
|
+
try {
|
|
371
|
+
await import_promises.default.access(configPath);
|
|
372
|
+
ctx.logger.debug("Found local config", { toolId: TOOL_ID, path: configPath });
|
|
373
|
+
return { found: true, path: configPath };
|
|
374
|
+
} catch {
|
|
375
|
+
return { found: false };
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
/**
|
|
379
|
+
* Runs Trivy scans against the repository and converts the JSON output into
|
|
380
|
+
* Codacy Issue objects.
|
|
381
|
+
*
|
|
382
|
+
* Trivy scans entire directories (not individual files), so the adapter runs
|
|
383
|
+
* against `ctx.repositoryRoot` and then filters results to `ctx.targetFiles`.
|
|
384
|
+
*
|
|
385
|
+
* Two scan modes are supported based on enabled patterns:
|
|
386
|
+
* - **Vulnerability scan**: runs when any `vulnerability_*` pattern is enabled.
|
|
387
|
+
* Uses `trivy filesystem --scanners vuln` with severity filtering.
|
|
388
|
+
* - **Secret scan**: runs when the `secret` pattern is enabled.
|
|
389
|
+
* Uses `trivy filesystem --scanners secret`.
|
|
390
|
+
*
|
|
391
|
+
* Config resolution:
|
|
392
|
+
* - If `useLocalConfigurationFile` is true and `localConfigurationFile` is
|
|
393
|
+
* set, passes `--config <path>` to trivy.
|
|
394
|
+
* - Otherwise, trivy uses its built-in defaults.
|
|
395
|
+
*
|
|
396
|
+
* Exit codes:
|
|
397
|
+
* - 0: success (default behavior — trivy always exits 0 unless there is an error)
|
|
398
|
+
* - Non-zero: genuine error
|
|
399
|
+
*
|
|
400
|
+
* Never throws — all errors are captured in the returned `errors` array.
|
|
401
|
+
*/
|
|
402
|
+
async analyze(ctx, codacyConfig) {
|
|
403
|
+
const startMs = Date.now();
|
|
404
|
+
ctx.logger.info("Starting analysis", { toolId: TOOL_ID, fileCount: ctx.targetFiles.length });
|
|
405
|
+
const startedAt = /* @__PURE__ */ new Date();
|
|
406
|
+
const issues = [];
|
|
407
|
+
const errors = [];
|
|
408
|
+
if (ctx.targetFiles.length === 0) {
|
|
409
|
+
return buildResult(startedAt, /* @__PURE__ */ new Date(), issues, errors, 0);
|
|
410
|
+
}
|
|
411
|
+
const binary = await resolveBinaryPath(ctx);
|
|
412
|
+
let cacheDir;
|
|
413
|
+
try {
|
|
414
|
+
cacheDir = await ensureCacheDir(ctx.globalDir);
|
|
415
|
+
} catch (err) {
|
|
416
|
+
errors.push({
|
|
417
|
+
toolId: TOOL_ID,
|
|
418
|
+
phase: "toolInvoke",
|
|
419
|
+
kind: "FilesystemError",
|
|
420
|
+
message: `Failed to create cache directory: ${err instanceof Error ? err.message : String(err)}`,
|
|
421
|
+
level: "error"
|
|
422
|
+
});
|
|
423
|
+
return buildResult(startedAt, /* @__PURE__ */ new Date(), issues, errors, ctx.targetFiles.length);
|
|
424
|
+
}
|
|
425
|
+
const enabledPatterns = new Set(codacyConfig.patterns.map((p) => p.patternId));
|
|
426
|
+
const targetFilesSet = new Set(ctx.targetFiles);
|
|
427
|
+
const vulnPatternIds = Object.values(VULN_SEVERITY_TO_PATTERN);
|
|
428
|
+
const runVulnScan = enabledPatterns.size === 0 || vulnPatternIds.some((id) => enabledPatterns.has(id));
|
|
429
|
+
const runSecretScan = enabledPatterns.size === 0 || enabledPatterns.has("Trivy_secret");
|
|
430
|
+
const baseArgs = ["filesystem", "--format", "json", "--cache-dir", cacheDir];
|
|
431
|
+
if (codacyConfig.useLocalConfigurationFile && codacyConfig.localConfigurationFile) {
|
|
432
|
+
baseArgs.push("--config", codacyConfig.localConfigurationFile);
|
|
433
|
+
}
|
|
434
|
+
try {
|
|
435
|
+
if (runVulnScan) {
|
|
436
|
+
const vulnArgs = [...baseArgs, "--scanners", "vuln", "--pkg-types", "library"];
|
|
437
|
+
const severities = [];
|
|
438
|
+
if (enabledPatterns.size === 0) {
|
|
439
|
+
severities.push("CRITICAL", "HIGH", "MEDIUM", "LOW");
|
|
440
|
+
} else {
|
|
441
|
+
for (const [patternId, severity] of Object.entries(PATTERN_TO_SEVERITY)) {
|
|
442
|
+
if (enabledPatterns.has(patternId)) {
|
|
443
|
+
severities.push(severity);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (severities.length > 0) {
|
|
448
|
+
vulnArgs.push("--severity", severities.join(","));
|
|
449
|
+
}
|
|
450
|
+
vulnArgs.push(ctx.repositoryRoot);
|
|
451
|
+
const vulnOutput = await runTrivyScan(binary, vulnArgs, ctx.repositoryRoot, errors);
|
|
452
|
+
if (vulnOutput) {
|
|
453
|
+
issues.push(...await parseVulnerabilities(vulnOutput, enabledPatterns, targetFilesSet, ctx.repositoryRoot));
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (runSecretScan) {
|
|
457
|
+
const secretArgs = [...baseArgs, "--scanners", "secret", ctx.repositoryRoot];
|
|
458
|
+
const secretOutput = await runTrivyScan(binary, secretArgs, ctx.repositoryRoot, errors);
|
|
459
|
+
if (secretOutput) {
|
|
460
|
+
issues.push(...await parseSecrets(secretOutput, enabledPatterns, targetFilesSet, ctx.repositoryRoot));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
} catch (err) {
|
|
464
|
+
errors.push({
|
|
465
|
+
toolId: TOOL_ID,
|
|
466
|
+
phase: "toolInvoke",
|
|
467
|
+
kind: "InvocationError",
|
|
468
|
+
message: `Failed to run trivy: ${err instanceof Error ? err.message : String(err)}`,
|
|
469
|
+
level: "error"
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
const result = buildResult(startedAt, /* @__PURE__ */ new Date(), issues, errors, ctx.targetFiles.length);
|
|
473
|
+
ctx.logger.info("Analysis complete", {
|
|
474
|
+
toolId: TOOL_ID,
|
|
475
|
+
issueCount: result.summary.issueCount,
|
|
476
|
+
errorCount: result.summary.errorCount,
|
|
477
|
+
durationMs: Date.now() - startMs
|
|
478
|
+
});
|
|
479
|
+
return result;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// src/index.ts
|
|
484
|
+
var index_default = (0, import_tooling2.defineToolAdapter)(adapter);
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codacy/tools-trivy-0",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Trivy adapter — CLI-mode security scanner",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@codacy/tooling": "0.1.0"
|
|
12
|
+
},
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "Codacy <support@codacy.com> (https://www.codacy.com)",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/codacy/analysis-cli.git",
|
|
18
|
+
"directory": "packages/tools/trivy-0"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/codacy/analysis-cli",
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=20.0.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup src/index.ts --format cjs --dts --clean",
|
|
26
|
+
"test": "vitest run",
|
|
27
|
+
"lint": "eslint src/",
|
|
28
|
+
"clean": "rm -rf dist",
|
|
29
|
+
"prefetch": "node ../scripts/fetch-patterns.mjs 2fd7fbe0-33f9-4ab3-ab73-e9b62404e2cb src/patterns.json"
|
|
30
|
+
}
|
|
31
|
+
}
|