@sanity-labs/backstage-plugin-trivy-backend 1.0.2 → 1.0.3
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/dist/index.cjs.js +2 -348
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/plugin.cjs.js +32 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/service/cache.cjs.js +59 -0
- package/dist/service/cache.cjs.js.map +1 -0
- package/dist/service/router.cjs.js +303 -0
- package/dist/service/router.cjs.js.map +1 -0
- package/package.json +24 -9
package/dist/index.cjs.js
CHANGED
|
@@ -2,355 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
|
-
var
|
|
6
|
-
var backendCommon = require('@backstage/backend-common');
|
|
7
|
-
var express = require('express');
|
|
8
|
-
var Router = require('express-promise-router');
|
|
9
|
-
var storage = require('@google-cloud/storage');
|
|
5
|
+
var plugin = require('./plugin.cjs.js');
|
|
10
6
|
|
|
11
|
-
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
12
7
|
|
|
13
|
-
var express__default = /*#__PURE__*/_interopDefaultLegacy(express);
|
|
14
|
-
var Router__default = /*#__PURE__*/_interopDefaultLegacy(Router);
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
18
|
-
var __publicField = (obj, key, value) => {
|
|
19
|
-
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
20
|
-
return value;
|
|
21
|
-
};
|
|
22
|
-
class CacheManager {
|
|
23
|
-
constructor(ttlMinutes = 5, logger) {
|
|
24
|
-
__publicField(this, "cache");
|
|
25
|
-
__publicField(this, "ttlMs");
|
|
26
|
-
__publicField(this, "logger");
|
|
27
|
-
this.cache = /* @__PURE__ */ new Map();
|
|
28
|
-
this.ttlMs = ttlMinutes * 60 * 1e3;
|
|
29
|
-
this.logger = logger;
|
|
30
|
-
}
|
|
31
|
-
get(key) {
|
|
32
|
-
const entry = this.cache.get(key);
|
|
33
|
-
if (!entry) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
const age = Date.now() - entry.timestamp;
|
|
37
|
-
if (age > this.ttlMs) {
|
|
38
|
-
this.cache.delete(key);
|
|
39
|
-
this.logger.debug(`Cache miss (expired): ${key}`);
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
this.logger.debug(`Cache hit: ${key}`);
|
|
43
|
-
return entry.data;
|
|
44
|
-
}
|
|
45
|
-
set(key, data) {
|
|
46
|
-
this.cache.set(key, {
|
|
47
|
-
data,
|
|
48
|
-
timestamp: Date.now()
|
|
49
|
-
});
|
|
50
|
-
this.logger.debug(`Cache set: ${key}`);
|
|
51
|
-
}
|
|
52
|
-
has(key) {
|
|
53
|
-
const entry = this.cache.get(key);
|
|
54
|
-
if (!entry)
|
|
55
|
-
return false;
|
|
56
|
-
const age = Date.now() - entry.timestamp;
|
|
57
|
-
if (age > this.ttlMs) {
|
|
58
|
-
this.cache.delete(key);
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
clear() {
|
|
64
|
-
this.cache.clear();
|
|
65
|
-
this.logger.info("Cache cleared");
|
|
66
|
-
}
|
|
67
|
-
size() {
|
|
68
|
-
return this.cache.size;
|
|
69
|
-
}
|
|
70
|
-
stats() {
|
|
71
|
-
return {
|
|
72
|
-
size: this.cache.size,
|
|
73
|
-
keys: Array.from(this.cache.keys())
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function parseMetadata(content) {
|
|
79
|
-
const lines = content.split("\n");
|
|
80
|
-
const metadata = {};
|
|
81
|
-
for (const line of lines) {
|
|
82
|
-
if (line.startsWith("CircleCI Build URL:")) {
|
|
83
|
-
metadata.circleciUrl = line.substring("CircleCI Build URL:".length).trim() || void 0;
|
|
84
|
-
} else if (line.startsWith("GitHub PR:")) {
|
|
85
|
-
metadata.githubPr = line.substring("GitHub PR:".length).trim() || void 0;
|
|
86
|
-
} else if (line.startsWith("Git Commit:")) {
|
|
87
|
-
metadata.gitCommit = line.substring("Git Commit:".length).trim() || void 0;
|
|
88
|
-
} else if (line.startsWith("Committer:")) {
|
|
89
|
-
metadata.committer = line.substring("Committer:".length).trim() || void 0;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return metadata;
|
|
93
|
-
}
|
|
94
|
-
function parseTrivyTableFormat(content, currentFile) {
|
|
95
|
-
const findings = [];
|
|
96
|
-
const lines = content.split("\n");
|
|
97
|
-
for (let i = 0; i < lines.length; i++) {
|
|
98
|
-
const line = lines[i];
|
|
99
|
-
if (line.includes("\u250C") || line.includes("\u251C") || line.includes("\u2514") || line.includes("Library") || line.includes("Vulnerability")) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
if (line.includes("\u2502")) {
|
|
103
|
-
const parts = line.split("\u2502").map((p) => p.trim()).filter((p) => p);
|
|
104
|
-
if (parts.length >= 3) {
|
|
105
|
-
const library = parts[0];
|
|
106
|
-
const vulnerability = parts[1];
|
|
107
|
-
const severity = parts[2];
|
|
108
|
-
if (!["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"].includes(severity)) {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
const title = parts[parts.length - 1] || vulnerability;
|
|
112
|
-
let avdId;
|
|
113
|
-
const urlMatch = line.match(/https:\/\/avd\.aquasec\.com\/[^\s│]+/);
|
|
114
|
-
if (urlMatch) {
|
|
115
|
-
avdId = urlMatch[0];
|
|
116
|
-
} else if (i + 1 < lines.length) {
|
|
117
|
-
const nextLineUrlMatch = lines[i + 1].match(
|
|
118
|
-
/https:\/\/avd\.aquasec\.com\/[^\s│]+/
|
|
119
|
-
);
|
|
120
|
-
if (nextLineUrlMatch) {
|
|
121
|
-
avdId = nextLineUrlMatch[0];
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
findings.push({
|
|
125
|
-
file: library || currentFile,
|
|
126
|
-
severity,
|
|
127
|
-
title: title.split("\n")[0],
|
|
128
|
-
// Take first line of title
|
|
129
|
-
description: vulnerability,
|
|
130
|
-
lines: "",
|
|
131
|
-
avdId
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return findings;
|
|
137
|
-
}
|
|
138
|
-
function parseTrivyTextFormat(content, currentFile) {
|
|
139
|
-
const findings = [];
|
|
140
|
-
const sections = content.split(/\n(?=[^\s])/);
|
|
141
|
-
for (const section of sections) {
|
|
142
|
-
const severityMatch = section.match(
|
|
143
|
-
/^(CRITICAL|HIGH|MEDIUM|LOW|UNKNOWN):\s+(.+)/
|
|
144
|
-
);
|
|
145
|
-
if (severityMatch && currentFile) {
|
|
146
|
-
const severity = severityMatch[1];
|
|
147
|
-
const title = severityMatch[2];
|
|
148
|
-
const descMatch = section.match(/═{40}\n([\s\S]+?)\n(?:See|─{40})/);
|
|
149
|
-
const description = descMatch ? descMatch[1].trim() : "";
|
|
150
|
-
const avdMatch = section.match(
|
|
151
|
-
/See\s+(https:\/\/avd\.aquasec\.com\/[^\s]+)/
|
|
152
|
-
);
|
|
153
|
-
const avdId = avdMatch ? avdMatch[1] : void 0;
|
|
154
|
-
const linesMatch = section.match(/─{40}\n\s*(.+?):(\d+)(?:-(\d+))?/);
|
|
155
|
-
let lines = "";
|
|
156
|
-
if (linesMatch) {
|
|
157
|
-
lines = linesMatch[3] ? `${linesMatch[2]}-${linesMatch[3]}` : linesMatch[2];
|
|
158
|
-
}
|
|
159
|
-
findings.push({
|
|
160
|
-
file: currentFile,
|
|
161
|
-
severity,
|
|
162
|
-
title,
|
|
163
|
-
description,
|
|
164
|
-
lines,
|
|
165
|
-
avdId
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return findings;
|
|
170
|
-
}
|
|
171
|
-
function parseTrivyOutput(content) {
|
|
172
|
-
const findings = [];
|
|
173
|
-
const sections = content.split(/\n(?=\S.*\s+\([^)]+\)\s*\n=+)/);
|
|
174
|
-
for (const section of sections) {
|
|
175
|
-
const fileMatch = section.match(/^(.+)\s+\(([^)]+)\)\s*\n=+/);
|
|
176
|
-
if (!fileMatch)
|
|
177
|
-
continue;
|
|
178
|
-
const currentFile = fileMatch[1];
|
|
179
|
-
if (section.includes("\u250C") || section.includes("\u2502")) {
|
|
180
|
-
findings.push(...parseTrivyTableFormat(section, currentFile));
|
|
181
|
-
}
|
|
182
|
-
findings.push(...parseTrivyTextFormat(section, currentFile));
|
|
183
|
-
}
|
|
184
|
-
return findings;
|
|
185
|
-
}
|
|
186
|
-
function getBackendConfig(config) {
|
|
187
|
-
const trivyConfig = config == null ? void 0 : config.getOptionalConfig("trivy");
|
|
188
|
-
return {
|
|
189
|
-
bucketName: (trivyConfig == null ? void 0 : trivyConfig.getOptionalString("bucketName")) || "sanity-trivy-logs",
|
|
190
|
-
cacheTime: (trivyConfig == null ? void 0 : trivyConfig.getOptionalNumber("cacheTime")) || 5
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
async function createRouter(options) {
|
|
194
|
-
const { logger, config } = options;
|
|
195
|
-
const router = Router__default["default"]();
|
|
196
|
-
router.use(express__default["default"].json());
|
|
197
|
-
const backendConfig = getBackendConfig(config);
|
|
198
|
-
const storage$1 = new storage.Storage();
|
|
199
|
-
const bucketName = backendConfig.bucketName;
|
|
200
|
-
const cache = new CacheManager(backendConfig.cacheTime, logger);
|
|
201
|
-
router.get("/health", (_, response) => {
|
|
202
|
-
logger.info("PONG!");
|
|
203
|
-
response.json({
|
|
204
|
-
status: "ok",
|
|
205
|
-
cache: cache.stats(),
|
|
206
|
-
config: {
|
|
207
|
-
bucketName,
|
|
208
|
-
cacheTime: backendConfig.cacheTime
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
router.get("/scans/:repo/:branch/:scanId", async (request, response) => {
|
|
213
|
-
try {
|
|
214
|
-
const { repo, branch, scanId } = request.params;
|
|
215
|
-
const cacheKey = `scan-${repo}-${branch}-${scanId}`;
|
|
216
|
-
const cached = cache.get(cacheKey);
|
|
217
|
-
if (cached) {
|
|
218
|
-
logger.info(`Returning cached scan: ${cacheKey}`);
|
|
219
|
-
response.json(cached);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const prefix = `repos/${repo}/${branch}/${scanId}/`;
|
|
223
|
-
const [files] = await storage$1.bucket(bucketName).getFiles({ prefix });
|
|
224
|
-
const scan = {
|
|
225
|
-
repo,
|
|
226
|
-
branch,
|
|
227
|
-
scanId,
|
|
228
|
-
metadata: {},
|
|
229
|
-
findings: [],
|
|
230
|
-
timestamp: /* @__PURE__ */ new Date()
|
|
231
|
-
};
|
|
232
|
-
for (const file of files) {
|
|
233
|
-
const fileName = file.name.split("/").pop();
|
|
234
|
-
if (fileName === "trivy-metadata.txt") {
|
|
235
|
-
const [content] = await file.download();
|
|
236
|
-
scan.metadata = parseMetadata(content.toString());
|
|
237
|
-
} else if (fileName === "trivy-fs-output.txt" || fileName === "trivy-image-output.txt") {
|
|
238
|
-
const [content] = await file.download();
|
|
239
|
-
scan.findings.push(...parseTrivyOutput(content.toString()));
|
|
240
|
-
}
|
|
241
|
-
if (file.metadata.timeCreated) {
|
|
242
|
-
scan.timestamp = new Date(file.metadata.timeCreated);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
cache.set(cacheKey, scan);
|
|
246
|
-
response.json(scan);
|
|
247
|
-
} catch (error) {
|
|
248
|
-
logger.error("Error fetching scan:", error);
|
|
249
|
-
response.status(500).json({ error: "Failed to fetch scan" });
|
|
250
|
-
}
|
|
251
|
-
});
|
|
252
|
-
router.get("/scans/:repo/latest", async (request, response) => {
|
|
253
|
-
var _a;
|
|
254
|
-
try {
|
|
255
|
-
const { repo } = request.params;
|
|
256
|
-
const branch = "main";
|
|
257
|
-
const cacheKey = `latest-${repo}-${branch}`;
|
|
258
|
-
const cached = cache.get(cacheKey);
|
|
259
|
-
if (cached) {
|
|
260
|
-
logger.info(`Returning cached latest scan: ${cacheKey}`);
|
|
261
|
-
response.json(cached);
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
const prefix = `repos/${repo}/${branch}/`;
|
|
265
|
-
const [files] = await storage$1.bucket(bucketName).getFiles({ prefix });
|
|
266
|
-
const scanInfo = /* @__PURE__ */ new Map();
|
|
267
|
-
for (const file of files) {
|
|
268
|
-
const pathParts = file.name.split("/");
|
|
269
|
-
if (pathParts.length < 5)
|
|
270
|
-
continue;
|
|
271
|
-
const scanId = pathParts[3];
|
|
272
|
-
const fileName = pathParts[4];
|
|
273
|
-
const fileTimestamp = file.metadata.timeCreated ? new Date(file.metadata.timeCreated) : /* @__PURE__ */ new Date(0);
|
|
274
|
-
if (!scanInfo.has(scanId)) {
|
|
275
|
-
scanInfo.set(scanId, {
|
|
276
|
-
timestamp: fileTimestamp,
|
|
277
|
-
hasOutputFiles: false
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
const info = scanInfo.get(scanId);
|
|
281
|
-
if (fileName === "trivy-fs-output.txt" || fileName === "trivy-image-output.txt") {
|
|
282
|
-
info.hasOutputFiles = true;
|
|
283
|
-
}
|
|
284
|
-
if (fileTimestamp < info.timestamp) {
|
|
285
|
-
info.timestamp = fileTimestamp;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
const latestScanId = (_a = Array.from(scanInfo.entries()).filter(([_, info]) => info.hasOutputFiles).sort(
|
|
289
|
-
(a, b) => b[1].timestamp.getTime() - a[1].timestamp.getTime()
|
|
290
|
-
)[0]) == null ? void 0 : _a[0];
|
|
291
|
-
if (!latestScanId) {
|
|
292
|
-
response.status(404).json({
|
|
293
|
-
error: `No scans found for repo ${repo} on ${branch} branch`
|
|
294
|
-
});
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
const latestScan = {
|
|
298
|
-
repo,
|
|
299
|
-
branch,
|
|
300
|
-
scanId: latestScanId,
|
|
301
|
-
metadata: {},
|
|
302
|
-
findings: [],
|
|
303
|
-
timestamp: scanInfo.get(latestScanId).timestamp
|
|
304
|
-
};
|
|
305
|
-
const latestScanFiles = files.filter((file) => {
|
|
306
|
-
const pathParts = file.name.split("/");
|
|
307
|
-
return pathParts.length >= 5 && pathParts[3] === latestScanId;
|
|
308
|
-
});
|
|
309
|
-
for (const file of latestScanFiles) {
|
|
310
|
-
const fileName = file.name.split("/")[4];
|
|
311
|
-
if (fileName === "trivy-metadata.txt") {
|
|
312
|
-
const [content] = await file.download();
|
|
313
|
-
latestScan.metadata = parseMetadata(content.toString());
|
|
314
|
-
} else if (fileName === "trivy-fs-output.txt" || fileName === "trivy-image-output.txt") {
|
|
315
|
-
const [content] = await file.download();
|
|
316
|
-
latestScan.findings.push(...parseTrivyOutput(content.toString()));
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
cache.set(cacheKey, latestScan);
|
|
320
|
-
response.json(latestScan);
|
|
321
|
-
} catch (error) {
|
|
322
|
-
logger.error("Error fetching latest scan:", error);
|
|
323
|
-
response.status(500).json({ error: "Failed to fetch latest scan" });
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
router.use(backendCommon.errorHandler());
|
|
327
|
-
return router;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
const trivyPlugin = backendPluginApi.createBackendPlugin({
|
|
331
|
-
pluginId: "trivy",
|
|
332
|
-
register(env) {
|
|
333
|
-
env.registerInit({
|
|
334
|
-
deps: {
|
|
335
|
-
httpRouter: backendPluginApi.coreServices.httpRouter,
|
|
336
|
-
logger: backendPluginApi.coreServices.logger,
|
|
337
|
-
config: backendPluginApi.coreServices.rootConfig
|
|
338
|
-
},
|
|
339
|
-
async init({ httpRouter, logger, config }) {
|
|
340
|
-
httpRouter.use(
|
|
341
|
-
await createRouter({
|
|
342
|
-
logger,
|
|
343
|
-
config
|
|
344
|
-
})
|
|
345
|
-
);
|
|
346
|
-
httpRouter.addAuthPolicy({
|
|
347
|
-
path: "/health",
|
|
348
|
-
allow: "unauthenticated"
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
exports["default"] = trivyPlugin;
|
|
9
|
+
exports.default = plugin.trivyPlugin;
|
|
356
10
|
//# sourceMappingURL=index.cjs.js.map
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs.js","sources":["../src/service/cache.ts","../src/service/router.ts","../src/plugin.ts"],"sourcesContent":["import { LoggerService } from \"@backstage/backend-plugin-api\";\n\ninterface CacheEntry<T> {\n data: T;\n timestamp: number;\n}\n\nexport class CacheManager {\n private cache: Map<string, CacheEntry<any>>;\n private ttlMs: number;\n private logger: LoggerService;\n\n constructor(ttlMinutes: number = 5, logger: LoggerService) {\n this.cache = new Map();\n this.ttlMs = ttlMinutes * 60 * 1000;\n this.logger = logger;\n }\n\n get<T>(key: string): T | null {\n const entry = this.cache.get(key);\n\n if (!entry) {\n return null;\n }\n\n const age = Date.now() - entry.timestamp;\n\n if (age > this.ttlMs) {\n this.cache.delete(key);\n this.logger.debug(`Cache miss (expired): ${key}`);\n return null;\n }\n\n this.logger.debug(`Cache hit: ${key}`);\n return entry.data as T;\n }\n\n set<T>(key: string, data: T): void {\n this.cache.set(key, {\n data,\n timestamp: Date.now(),\n });\n this.logger.debug(`Cache set: ${key}`);\n }\n\n has(key: string): boolean {\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n const age = Date.now() - entry.timestamp;\n if (age > this.ttlMs) {\n this.cache.delete(key);\n return false;\n }\n\n return true;\n }\n\n clear(): void {\n this.cache.clear();\n this.logger.info(\"Cache cleared\");\n }\n\n size(): number {\n return this.cache.size;\n }\n\n stats(): { size: number; keys: string[] } {\n return {\n size: this.cache.size,\n keys: Array.from(this.cache.keys()),\n };\n }\n}\n","import { errorHandler } from \"@backstage/backend-common\";\nimport { LoggerService } from \"@backstage/backend-plugin-api\";\nimport { Config } from \"@backstage/config\";\nimport express from \"express\";\nimport Router from \"express-promise-router\";\nimport { Storage } from \"@google-cloud/storage\";\nimport { CacheManager } from \"./cache\";\n\nexport interface RouterOptions {\n logger: LoggerService;\n config?: Config;\n}\n\ninterface TrivyBackendConfig {\n bucketName: string;\n cacheTime: number;\n}\n\ninterface TrivyMetadata {\n circleciUrl?: string;\n githubPr?: string;\n gitCommit?: string;\n committer?: string;\n}\n\ninterface TrivyFinding {\n file: string;\n severity: string;\n title: string;\n description: string;\n lines: string;\n avdId?: string;\n}\n\ninterface TrivyScanResult {\n repo: string;\n branch: string;\n scanId: string;\n metadata: TrivyMetadata;\n findings: TrivyFinding[];\n timestamp: Date;\n}\n\nfunction parseMetadata(content: string): TrivyMetadata {\n const lines = content.split(\"\\n\");\n const metadata: TrivyMetadata = {};\n\n for (const line of lines) {\n if (line.startsWith(\"CircleCI Build URL:\")) {\n metadata.circleciUrl =\n line.substring(\"CircleCI Build URL:\".length).trim() || undefined;\n } else if (line.startsWith(\"GitHub PR:\")) {\n metadata.githubPr =\n line.substring(\"GitHub PR:\".length).trim() || undefined;\n } else if (line.startsWith(\"Git Commit:\")) {\n metadata.gitCommit =\n line.substring(\"Git Commit:\".length).trim() || undefined;\n } else if (line.startsWith(\"Committer:\")) {\n metadata.committer =\n line.substring(\"Committer:\".length).trim() || undefined;\n }\n }\n\n return metadata;\n}\n\nfunction parseTrivyTableFormat(\n content: string,\n currentFile: string,\n): TrivyFinding[] {\n const findings: TrivyFinding[] = [];\n const lines = content.split(\"\\n\");\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Skip table borders and headers\n if (\n line.includes(\"┌\") ||\n line.includes(\"├\") ||\n line.includes(\"└\") ||\n line.includes(\"Library\") ||\n line.includes(\"Vulnerability\")\n ) {\n continue;\n }\n\n // Parse data rows (contain │ separators)\n if (line.includes(\"│\")) {\n const parts = line\n .split(\"│\")\n .map((p) => p.trim())\n .filter((p) => p);\n\n if (parts.length >= 3) {\n const library = parts[0];\n const vulnerability = parts[1];\n const severity = parts[2];\n\n // Skip if not a valid severity or if it's continuation of previous row\n if (\n ![\"CRITICAL\", \"HIGH\", \"MEDIUM\", \"LOW\", \"UNKNOWN\"].includes(severity)\n ) {\n continue;\n }\n\n // Get title from the last column if available\n const title = parts[parts.length - 1] || vulnerability;\n\n // Look for URL in the next line or current line\n let avdId: string | undefined;\n const urlMatch = line.match(/https:\\/\\/avd\\.aquasec\\.com\\/[^\\s│]+/);\n if (urlMatch) {\n avdId = urlMatch[0];\n } else if (i + 1 < lines.length) {\n const nextLineUrlMatch = lines[i + 1].match(\n /https:\\/\\/avd\\.aquasec\\.com\\/[^\\s│]+/,\n );\n if (nextLineUrlMatch) {\n avdId = nextLineUrlMatch[0];\n }\n }\n\n findings.push({\n file: library || currentFile,\n severity,\n title: title.split(\"\\n\")[0], // Take first line of title\n description: vulnerability,\n lines: \"\",\n avdId,\n });\n }\n }\n }\n\n return findings;\n}\n\nfunction parseTrivyTextFormat(\n content: string,\n currentFile: string,\n): TrivyFinding[] {\n const findings: TrivyFinding[] = [];\n const sections = content.split(/\\n(?=[^\\s])/);\n\n for (const section of sections) {\n // Check if this is a finding in text format\n const severityMatch = section.match(\n /^(CRITICAL|HIGH|MEDIUM|LOW|UNKNOWN):\\s+(.+)/,\n );\n if (severityMatch && currentFile) {\n const severity = severityMatch[1];\n const title = severityMatch[2];\n\n // Extract description\n const descMatch = section.match(/═{40}\\n([\\s\\S]+?)\\n(?:See|─{40})/);\n const description = descMatch ? descMatch[1].trim() : \"\";\n\n // Extract AVD ID\n const avdMatch = section.match(\n /See\\s+(https:\\/\\/avd\\.aquasec\\.com\\/[^\\s]+)/,\n );\n const avdId = avdMatch ? avdMatch[1] : undefined;\n\n // Extract line numbers\n const linesMatch = section.match(/─{40}\\n\\s*(.+?):(\\d+)(?:-(\\d+))?/);\n let lines = \"\";\n if (linesMatch) {\n lines = linesMatch[3]\n ? `${linesMatch[2]}-${linesMatch[3]}`\n : linesMatch[2];\n }\n\n findings.push({\n file: currentFile,\n severity,\n title,\n description,\n lines,\n avdId,\n });\n }\n }\n\n return findings;\n}\n\nfunction parseTrivyOutput(content: string): TrivyFinding[] {\n const findings: TrivyFinding[] = [];\n\n // Split content into sections by file headers\n const sections = content.split(/\\n(?=\\S.*\\s+\\([^)]+\\)\\s*\\n=+)/);\n\n for (const section of sections) {\n // Extract file header\n const fileMatch = section.match(/^(.+)\\s+\\(([^)]+)\\)\\s*\\n=+/);\n if (!fileMatch) continue;\n\n const currentFile = fileMatch[1];\n\n // Check if this section contains a table (has box-drawing characters)\n if (section.includes(\"┌\") || section.includes(\"│\")) {\n findings.push(...parseTrivyTableFormat(section, currentFile));\n }\n\n // Also parse text format findings (misconfigs, secrets)\n findings.push(...parseTrivyTextFormat(section, currentFile));\n }\n\n return findings;\n}\n\nfunction getBackendConfig(config?: Config): TrivyBackendConfig {\n const trivyConfig = config?.getOptionalConfig(\"trivy\");\n return {\n bucketName:\n trivyConfig?.getOptionalString(\"bucketName\") || \"sanity-trivy-logs\",\n cacheTime: trivyConfig?.getOptionalNumber(\"cacheTime\") || 5,\n };\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const { logger, config } = options;\n const router = Router();\n router.use(express.json());\n\n const backendConfig = getBackendConfig(config);\n const storage = new Storage();\n const bucketName = backendConfig.bucketName;\n const cache = new CacheManager(backendConfig.cacheTime, logger);\n\n router.get(\"/health\", (_, response) => {\n logger.info(\"PONG!\");\n response.json({\n status: \"ok\",\n cache: cache.stats(),\n config: {\n bucketName,\n cacheTime: backendConfig.cacheTime,\n },\n });\n });\n\n // Get scan by ID\n router.get(\"/scans/:repo/:branch/:scanId\", async (request, response) => {\n try {\n const { repo, branch, scanId } = request.params;\n const cacheKey = `scan-${repo}-${branch}-${scanId}`;\n const cached = cache.get<TrivyScanResult>(cacheKey);\n\n if (cached) {\n logger.info(`Returning cached scan: ${cacheKey}`);\n response.json(cached);\n return;\n }\n\n const prefix = `repos/${repo}/${branch}/${scanId}/`;\n\n const [files] = await storage.bucket(bucketName).getFiles({ prefix });\n\n const scan: TrivyScanResult = {\n repo,\n branch,\n scanId,\n metadata: {},\n findings: [],\n timestamp: new Date(),\n };\n\n for (const file of files) {\n const fileName = file.name.split(\"/\").pop();\n\n if (fileName === \"trivy-metadata.txt\") {\n const [content] = await file.download();\n scan.metadata = parseMetadata(content.toString());\n } else if (\n fileName === \"trivy-fs-output.txt\" ||\n fileName === \"trivy-image-output.txt\"\n ) {\n const [content] = await file.download();\n scan.findings.push(...parseTrivyOutput(content.toString()));\n }\n\n if (file.metadata.timeCreated) {\n scan.timestamp = new Date(file.metadata.timeCreated);\n }\n }\n\n cache.set(cacheKey, scan);\n response.json(scan);\n } catch (error) {\n logger.error(\"Error fetching scan:\", error as Error);\n response.status(500).json({ error: \"Failed to fetch scan\" });\n }\n });\n\n // Get latest scan for a repo (main branch only)\n router.get(\"/scans/:repo/latest\", async (request, response) => {\n try {\n const { repo } = request.params;\n const branch = \"main\"; // Focus on main branch only\n const cacheKey = `latest-${repo}-${branch}`;\n const cached = cache.get<TrivyScanResult>(cacheKey);\n\n if (cached) {\n logger.info(`Returning cached latest scan: ${cacheKey}`);\n response.json(cached);\n return;\n }\n\n const prefix = `repos/${repo}/${branch}/`;\n\n const [files] = await storage.bucket(bucketName).getFiles({ prefix });\n\n // Group files by scanId and track timestamps\n const scanInfo = new Map<\n string,\n { timestamp: Date; hasOutputFiles: boolean }\n >();\n\n for (const file of files) {\n const pathParts = file.name.split(\"/\");\n if (pathParts.length < 5) continue;\n\n const scanId = pathParts[3];\n const fileName = pathParts[4];\n const fileTimestamp = file.metadata.timeCreated\n ? new Date(file.metadata.timeCreated)\n : new Date(0);\n\n if (!scanInfo.has(scanId)) {\n scanInfo.set(scanId, {\n timestamp: fileTimestamp,\n hasOutputFiles: false,\n });\n }\n\n const info = scanInfo.get(scanId)!;\n // Track if this scan has actual trivy output files\n if (\n fileName === \"trivy-fs-output.txt\" ||\n fileName === \"trivy-image-output.txt\"\n ) {\n info.hasOutputFiles = true;\n }\n // Use earliest file timestamp for the scan\n if (fileTimestamp < info.timestamp) {\n info.timestamp = fileTimestamp;\n }\n }\n\n // Find the most recent scan that has output files\n const latestScanId = Array.from(scanInfo.entries())\n .filter(([_, info]) => info.hasOutputFiles)\n .sort(\n (a, b) => b[1].timestamp.getTime() - a[1].timestamp.getTime(),\n )[0]?.[0];\n\n if (!latestScanId) {\n response.status(404).json({\n error: `No scans found for repo ${repo} on ${branch} branch`,\n });\n return;\n }\n\n // Now only download files for the latest scan\n const latestScan: TrivyScanResult = {\n repo,\n branch,\n scanId: latestScanId,\n metadata: {},\n findings: [],\n timestamp: scanInfo.get(latestScanId)!.timestamp,\n };\n\n const latestScanFiles = files.filter((file) => {\n const pathParts = file.name.split(\"/\");\n return pathParts.length >= 5 && pathParts[3] === latestScanId;\n });\n\n for (const file of latestScanFiles) {\n const fileName = file.name.split(\"/\")[4];\n\n if (fileName === \"trivy-metadata.txt\") {\n const [content] = await file.download();\n latestScan.metadata = parseMetadata(content.toString());\n } else if (\n fileName === \"trivy-fs-output.txt\" ||\n fileName === \"trivy-image-output.txt\"\n ) {\n const [content] = await file.download();\n latestScan.findings.push(...parseTrivyOutput(content.toString()));\n }\n }\n\n cache.set(cacheKey, latestScan);\n response.json(latestScan);\n } catch (error) {\n logger.error(\"Error fetching latest scan:\", error as Error);\n response.status(500).json({ error: \"Failed to fetch latest scan\" });\n }\n });\n\n router.use(errorHandler() as any);\n return router;\n}\n","import {\n coreServices,\n createBackendPlugin,\n} from \"@backstage/backend-plugin-api\";\nimport { createRouter } from \"./service/router\";\n\nexport const trivyPlugin = createBackendPlugin({\n pluginId: \"trivy\",\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n },\n async init({ httpRouter, logger, config }) {\n httpRouter.use(\n (await createRouter({\n logger,\n config,\n })) as any,\n );\n httpRouter.addAuthPolicy({\n path: \"/health\",\n allow: \"unauthenticated\",\n });\n },\n });\n },\n});\n"],"names":["Router","express","storage","Storage","errorHandler","createBackendPlugin","coreServices"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
|
|
2
2
|
|
|
3
|
-
declare const trivyPlugin: _backstage_backend_plugin_api.
|
|
3
|
+
declare const trivyPlugin: _backstage_backend_plugin_api.BackendFeature;
|
|
4
4
|
|
|
5
5
|
export { trivyPlugin as default };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var backendPluginApi = require('@backstage/backend-plugin-api');
|
|
4
|
+
var router = require('./service/router.cjs.js');
|
|
5
|
+
|
|
6
|
+
const trivyPlugin = backendPluginApi.createBackendPlugin({
|
|
7
|
+
pluginId: "trivy",
|
|
8
|
+
register(env) {
|
|
9
|
+
env.registerInit({
|
|
10
|
+
deps: {
|
|
11
|
+
httpRouter: backendPluginApi.coreServices.httpRouter,
|
|
12
|
+
logger: backendPluginApi.coreServices.logger,
|
|
13
|
+
config: backendPluginApi.coreServices.rootConfig
|
|
14
|
+
},
|
|
15
|
+
async init({ httpRouter, logger, config }) {
|
|
16
|
+
httpRouter.use(
|
|
17
|
+
await router.createRouter({
|
|
18
|
+
logger,
|
|
19
|
+
config
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
httpRouter.addAuthPolicy({
|
|
23
|
+
path: "/health",
|
|
24
|
+
allow: "unauthenticated"
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
exports.trivyPlugin = trivyPlugin;
|
|
32
|
+
//# sourceMappingURL=plugin.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n coreServices,\n createBackendPlugin,\n} from \"@backstage/backend-plugin-api\";\nimport { createRouter } from \"./service/router\";\n\nexport const trivyPlugin = createBackendPlugin({\n pluginId: \"trivy\",\n register(env) {\n env.registerInit({\n deps: {\n httpRouter: coreServices.httpRouter,\n logger: coreServices.logger,\n config: coreServices.rootConfig,\n },\n async init({ httpRouter, logger, config }) {\n httpRouter.use(\n (await createRouter({\n logger,\n config,\n })) as any,\n );\n httpRouter.addAuthPolicy({\n path: \"/health\",\n allow: \"unauthenticated\",\n });\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","createRouter"],"mappings":";;;;;AAMO,MAAM,cAAcA,oCAAA,CAAoB;AAAA,EAC7C,QAAA,EAAU,OAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,YAAYC,6BAAA,CAAa,UAAA;AAAA,QACzB,QAAQA,6BAAA,CAAa,MAAA;AAAA,QACrB,QAAQA,6BAAA,CAAa;AAAA,OACvB;AAAA,MACA,MAAM,IAAA,CAAK,EAAE,UAAA,EAAY,MAAA,EAAQ,QAAO,EAAG;AACzC,QAAA,UAAA,CAAW,GAAA;AAAA,UACR,MAAMC,mBAAA,CAAa;AAAA,YAClB,MAAA;AAAA,YACA;AAAA,WACD;AAAA,SACH;AACA,QAAA,UAAA,CAAW,aAAA,CAAc;AAAA,UACvB,IAAA,EAAM,SAAA;AAAA,UACN,KAAA,EAAO;AAAA,SACR,CAAA;AAAA,MACH;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class CacheManager {
|
|
4
|
+
cache;
|
|
5
|
+
ttlMs;
|
|
6
|
+
logger;
|
|
7
|
+
constructor(ttlMinutes = 5, logger) {
|
|
8
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
9
|
+
this.ttlMs = ttlMinutes * 60 * 1e3;
|
|
10
|
+
this.logger = logger;
|
|
11
|
+
}
|
|
12
|
+
get(key) {
|
|
13
|
+
const entry = this.cache.get(key);
|
|
14
|
+
if (!entry) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const age = Date.now() - entry.timestamp;
|
|
18
|
+
if (age > this.ttlMs) {
|
|
19
|
+
this.cache.delete(key);
|
|
20
|
+
this.logger.debug(`Cache miss (expired): ${key}`);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
this.logger.debug(`Cache hit: ${key}`);
|
|
24
|
+
return entry.data;
|
|
25
|
+
}
|
|
26
|
+
set(key, data) {
|
|
27
|
+
this.cache.set(key, {
|
|
28
|
+
data,
|
|
29
|
+
timestamp: Date.now()
|
|
30
|
+
});
|
|
31
|
+
this.logger.debug(`Cache set: ${key}`);
|
|
32
|
+
}
|
|
33
|
+
has(key) {
|
|
34
|
+
const entry = this.cache.get(key);
|
|
35
|
+
if (!entry) return false;
|
|
36
|
+
const age = Date.now() - entry.timestamp;
|
|
37
|
+
if (age > this.ttlMs) {
|
|
38
|
+
this.cache.delete(key);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
clear() {
|
|
44
|
+
this.cache.clear();
|
|
45
|
+
this.logger.info("Cache cleared");
|
|
46
|
+
}
|
|
47
|
+
size() {
|
|
48
|
+
return this.cache.size;
|
|
49
|
+
}
|
|
50
|
+
stats() {
|
|
51
|
+
return {
|
|
52
|
+
size: this.cache.size,
|
|
53
|
+
keys: Array.from(this.cache.keys())
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
exports.CacheManager = CacheManager;
|
|
59
|
+
//# sourceMappingURL=cache.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.cjs.js","sources":["../../src/service/cache.ts"],"sourcesContent":["import { LoggerService } from \"@backstage/backend-plugin-api\";\n\ninterface CacheEntry<T> {\n data: T;\n timestamp: number;\n}\n\nexport class CacheManager {\n private cache: Map<string, CacheEntry<any>>;\n private ttlMs: number;\n private logger: LoggerService;\n\n constructor(ttlMinutes: number = 5, logger: LoggerService) {\n this.cache = new Map();\n this.ttlMs = ttlMinutes * 60 * 1000;\n this.logger = logger;\n }\n\n get<T>(key: string): T | null {\n const entry = this.cache.get(key);\n\n if (!entry) {\n return null;\n }\n\n const age = Date.now() - entry.timestamp;\n\n if (age > this.ttlMs) {\n this.cache.delete(key);\n this.logger.debug(`Cache miss (expired): ${key}`);\n return null;\n }\n\n this.logger.debug(`Cache hit: ${key}`);\n return entry.data as T;\n }\n\n set<T>(key: string, data: T): void {\n this.cache.set(key, {\n data,\n timestamp: Date.now(),\n });\n this.logger.debug(`Cache set: ${key}`);\n }\n\n has(key: string): boolean {\n const entry = this.cache.get(key);\n if (!entry) return false;\n\n const age = Date.now() - entry.timestamp;\n if (age > this.ttlMs) {\n this.cache.delete(key);\n return false;\n }\n\n return true;\n }\n\n clear(): void {\n this.cache.clear();\n this.logger.info(\"Cache cleared\");\n }\n\n size(): number {\n return this.cache.size;\n }\n\n stats(): { size: number; keys: string[] } {\n return {\n size: this.cache.size,\n keys: Array.from(this.cache.keys()),\n };\n }\n}\n"],"names":[],"mappings":";;AAOO,MAAM,YAAA,CAAa;AAAA,EAChB,KAAA;AAAA,EACA,KAAA;AAAA,EACA,MAAA;AAAA,EAER,WAAA,CAAY,UAAA,GAAqB,CAAA,EAAG,MAAA,EAAuB;AACzD,IAAA,IAAA,CAAK,KAAA,uBAAY,GAAA,EAAI;AACrB,IAAA,IAAA,CAAK,KAAA,GAAQ,aAAa,EAAA,GAAK,GAAA;AAC/B,IAAA,IAAA,CAAK,MAAA,GAAS,MAAA;AAAA,EAChB;AAAA,EAEA,IAAO,GAAA,EAAuB;AAC5B,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAEhC,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,CAAM,SAAA;AAE/B,IAAA,IAAI,GAAA,GAAM,KAAK,KAAA,EAAO;AACpB,MAAA,IAAA,CAAK,KAAA,CAAM,OAAO,GAAG,CAAA;AACrB,MAAA,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,CAAA,sBAAA,EAAyB,GAAG,CAAA,CAAE,CAAA;AAChD,MAAA,OAAO,IAAA;AAAA,IACT;AAEA,IAAA,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,CAAA,WAAA,EAAc,GAAG,CAAA,CAAE,CAAA;AACrC,IAAA,OAAO,KAAA,CAAM,IAAA;AAAA,EACf;AAAA,EAEA,GAAA,CAAO,KAAa,IAAA,EAAe;AACjC,IAAA,IAAA,CAAK,KAAA,CAAM,IAAI,GAAA,EAAK;AAAA,MAClB,IAAA;AAAA,MACA,SAAA,EAAW,KAAK,GAAA;AAAI,KACrB,CAAA;AACD,IAAA,IAAA,CAAK,MAAA,CAAO,KAAA,CAAM,CAAA,WAAA,EAAc,GAAG,CAAA,CAAE,CAAA;AAAA,EACvC;AAAA,EAEA,IAAI,GAAA,EAAsB;AACxB,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,GAAG,CAAA;AAChC,IAAA,IAAI,CAAC,OAAO,OAAO,KAAA;AAEnB,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,GAAA,EAAI,GAAI,KAAA,CAAM,SAAA;AAC/B,IAAA,IAAI,GAAA,GAAM,KAAK,KAAA,EAAO;AACpB,MAAA,IAAA,CAAK,KAAA,CAAM,OAAO,GAAG,CAAA;AACrB,MAAA,OAAO,KAAA;AAAA,IACT;AAEA,IAAA,OAAO,IAAA;AAAA,EACT;AAAA,EAEA,KAAA,GAAc;AACZ,IAAA,IAAA,CAAK,MAAM,KAAA,EAAM;AACjB,IAAA,IAAA,CAAK,MAAA,CAAO,KAAK,eAAe,CAAA;AAAA,EAClC;AAAA,EAEA,IAAA,GAAe;AACb,IAAA,OAAO,KAAK,KAAA,CAAM,IAAA;AAAA,EACpB;AAAA,EAEA,KAAA,GAA0C;AACxC,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,KAAK,KAAA,CAAM,IAAA;AAAA,MACjB,MAAM,KAAA,CAAM,IAAA,CAAK,IAAA,CAAK,KAAA,CAAM,MAAM;AAAA,KACpC;AAAA,EACF;AACF;;;;"}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var express = require('express');
|
|
4
|
+
var Router = require('express-promise-router');
|
|
5
|
+
var storage = require('@google-cloud/storage');
|
|
6
|
+
var cache = require('./cache.cjs.js');
|
|
7
|
+
|
|
8
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var express__default = /*#__PURE__*/_interopDefaultCompat(express);
|
|
11
|
+
var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
|
|
12
|
+
|
|
13
|
+
function parseMetadata(content) {
|
|
14
|
+
const lines = content.split("\n");
|
|
15
|
+
const metadata = {};
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
if (line.startsWith("CircleCI Build URL:")) {
|
|
18
|
+
metadata.circleciUrl = line.substring("CircleCI Build URL:".length).trim() || void 0;
|
|
19
|
+
} else if (line.startsWith("GitHub PR:")) {
|
|
20
|
+
metadata.githubPr = line.substring("GitHub PR:".length).trim() || void 0;
|
|
21
|
+
} else if (line.startsWith("Git Commit:")) {
|
|
22
|
+
metadata.gitCommit = line.substring("Git Commit:".length).trim() || void 0;
|
|
23
|
+
} else if (line.startsWith("Committer:")) {
|
|
24
|
+
metadata.committer = line.substring("Committer:".length).trim() || void 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return metadata;
|
|
28
|
+
}
|
|
29
|
+
function parseTrivyTableFormat(content, currentFile) {
|
|
30
|
+
const findings = [];
|
|
31
|
+
const lines = content.split("\n");
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const line = lines[i];
|
|
34
|
+
if (line.includes("\u250C") || line.includes("\u251C") || line.includes("\u2514") || line.includes("Library") || line.includes("Vulnerability")) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (line.includes("\u2502")) {
|
|
38
|
+
const parts = line.split("\u2502").map((p) => p.trim()).filter((p) => p);
|
|
39
|
+
if (parts.length >= 3) {
|
|
40
|
+
const library = parts[0];
|
|
41
|
+
const vulnerability = parts[1];
|
|
42
|
+
const severity = parts[2];
|
|
43
|
+
if (!["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"].includes(severity)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const title = parts[parts.length - 1] || vulnerability;
|
|
47
|
+
let avdId;
|
|
48
|
+
const urlMatch = line.match(/https:\/\/avd\.aquasec\.com\/[^\s│]+/);
|
|
49
|
+
if (urlMatch) {
|
|
50
|
+
avdId = urlMatch[0];
|
|
51
|
+
} else if (i + 1 < lines.length) {
|
|
52
|
+
const nextLineUrlMatch = lines[i + 1].match(
|
|
53
|
+
/https:\/\/avd\.aquasec\.com\/[^\s│]+/
|
|
54
|
+
);
|
|
55
|
+
if (nextLineUrlMatch) {
|
|
56
|
+
avdId = nextLineUrlMatch[0];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
findings.push({
|
|
60
|
+
file: library || currentFile,
|
|
61
|
+
severity,
|
|
62
|
+
title: title.split("\n")[0],
|
|
63
|
+
// Take first line of title
|
|
64
|
+
description: vulnerability,
|
|
65
|
+
lines: "",
|
|
66
|
+
avdId
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return findings;
|
|
72
|
+
}
|
|
73
|
+
function parseTrivyTextFormat(content, currentFile) {
|
|
74
|
+
const findings = [];
|
|
75
|
+
const sections = content.split(/\n(?=[^\s])/);
|
|
76
|
+
for (const section of sections) {
|
|
77
|
+
const severityMatch = section.match(
|
|
78
|
+
/^(CRITICAL|HIGH|MEDIUM|LOW|UNKNOWN):\s+(.+)/
|
|
79
|
+
);
|
|
80
|
+
if (severityMatch && currentFile) {
|
|
81
|
+
const severity = severityMatch[1];
|
|
82
|
+
const title = severityMatch[2];
|
|
83
|
+
const descMatch = section.match(/═{40}\n([\s\S]+?)\n(?:See|─{40})/);
|
|
84
|
+
const description = descMatch ? descMatch[1].trim() : "";
|
|
85
|
+
const avdMatch = section.match(
|
|
86
|
+
/See\s+(https:\/\/avd\.aquasec\.com\/[^\s]+)/
|
|
87
|
+
);
|
|
88
|
+
const avdId = avdMatch ? avdMatch[1] : void 0;
|
|
89
|
+
const linesMatch = section.match(/─{40}\n\s*(.+?):(\d+)(?:-(\d+))?/);
|
|
90
|
+
let lines = "";
|
|
91
|
+
if (linesMatch) {
|
|
92
|
+
lines = linesMatch[3] ? `${linesMatch[2]}-${linesMatch[3]}` : linesMatch[2];
|
|
93
|
+
}
|
|
94
|
+
findings.push({
|
|
95
|
+
file: currentFile,
|
|
96
|
+
severity,
|
|
97
|
+
title,
|
|
98
|
+
description,
|
|
99
|
+
lines,
|
|
100
|
+
avdId
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return findings;
|
|
105
|
+
}
|
|
106
|
+
function parseTrivyOutput(content) {
|
|
107
|
+
const findings = [];
|
|
108
|
+
const sections = content.split(/\n(?=\S.*\s+\([^)]+\)\s*\n=+)/);
|
|
109
|
+
for (const section of sections) {
|
|
110
|
+
const fileMatch = section.match(/^(.+)\s+\(([^)]+)\)\s*\n=+/);
|
|
111
|
+
if (!fileMatch) continue;
|
|
112
|
+
const currentFile = fileMatch[1];
|
|
113
|
+
if (section.includes("\u250C") || section.includes("\u2502")) {
|
|
114
|
+
findings.push(...parseTrivyTableFormat(section, currentFile));
|
|
115
|
+
}
|
|
116
|
+
findings.push(...parseTrivyTextFormat(section, currentFile));
|
|
117
|
+
}
|
|
118
|
+
return findings;
|
|
119
|
+
}
|
|
120
|
+
function getBackendConfig(config) {
|
|
121
|
+
const trivyConfig = config?.getOptionalConfig("trivy");
|
|
122
|
+
return {
|
|
123
|
+
bucketName: trivyConfig?.getOptionalString("bucketName") || "sanity-trivy-logs",
|
|
124
|
+
cacheTime: trivyConfig?.getOptionalNumber("cacheTime") || 5
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
async function createRouter(options) {
|
|
128
|
+
const { logger, config } = options;
|
|
129
|
+
const router = Router__default.default();
|
|
130
|
+
router.use(express__default.default.json());
|
|
131
|
+
const backendConfig = getBackendConfig(config);
|
|
132
|
+
const storage$1 = new storage.Storage();
|
|
133
|
+
const bucketName = backendConfig.bucketName;
|
|
134
|
+
const cache$1 = new cache.CacheManager(backendConfig.cacheTime, logger);
|
|
135
|
+
router.get("/health", (_, response) => {
|
|
136
|
+
logger.info("PONG!");
|
|
137
|
+
response.json({
|
|
138
|
+
status: "ok",
|
|
139
|
+
cache: cache$1.stats(),
|
|
140
|
+
config: {
|
|
141
|
+
bucketName,
|
|
142
|
+
cacheTime: backendConfig.cacheTime
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
router.get("/scans/:repo/:branch/:scanId", async (request, response) => {
|
|
147
|
+
try {
|
|
148
|
+
const repo = decodeURIComponent(request.params.repo);
|
|
149
|
+
const { branch, scanId } = request.params;
|
|
150
|
+
const cacheKey = `scan-${repo}-${branch}-${scanId}`;
|
|
151
|
+
const cached = cache$1.get(cacheKey);
|
|
152
|
+
if (cached) {
|
|
153
|
+
logger.info(`Returning cached scan: ${cacheKey}`);
|
|
154
|
+
response.json(cached);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const prefix = `repos/${repo}/${branch}/${scanId}/`;
|
|
158
|
+
const [files] = await storage$1.bucket(bucketName).getFiles({ prefix });
|
|
159
|
+
const scan = {
|
|
160
|
+
repo,
|
|
161
|
+
branch,
|
|
162
|
+
scanId,
|
|
163
|
+
metadata: {},
|
|
164
|
+
findings: [],
|
|
165
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
166
|
+
};
|
|
167
|
+
for (const file of files) {
|
|
168
|
+
const fileName = file.name.split("/").pop();
|
|
169
|
+
if (fileName === "trivy-metadata.txt") {
|
|
170
|
+
const [content] = await file.download();
|
|
171
|
+
scan.metadata = parseMetadata(content.toString());
|
|
172
|
+
} else if (fileName === "trivy-fs-output.txt" || fileName === "trivy-image-output.txt") {
|
|
173
|
+
const [content] = await file.download();
|
|
174
|
+
scan.findings.push(...parseTrivyOutput(content.toString()));
|
|
175
|
+
}
|
|
176
|
+
if (file.metadata.timeCreated) {
|
|
177
|
+
scan.timestamp = new Date(file.metadata.timeCreated);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
cache$1.set(cacheKey, scan);
|
|
181
|
+
response.json(scan);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
logger.error("Error fetching scan:", error);
|
|
184
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
185
|
+
if (error.code === 403) {
|
|
186
|
+
response.status(403).json({
|
|
187
|
+
error: "Permission denied accessing GCS bucket",
|
|
188
|
+
details: "Valid credentials but no bucket access. Check IAM permissions for Storage Object Viewer role.",
|
|
189
|
+
bucket: bucketName
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
response.status(500).json({ error: "Failed to fetch scan" });
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
router.get("/scans/:repo/latest", async (request, response) => {
|
|
198
|
+
try {
|
|
199
|
+
const fullRepo = decodeURIComponent(request.params.repo);
|
|
200
|
+
const branch = "main";
|
|
201
|
+
const repoName = fullRepo.includes("/") ? fullRepo.split("/")[1] : fullRepo;
|
|
202
|
+
const cacheKey = `latest-${fullRepo}-${branch}`;
|
|
203
|
+
logger.info(`Fetching latest scan for repo: ${fullRepo} (using path: ${repoName}), branch: ${branch}`);
|
|
204
|
+
const cached = cache$1.get(cacheKey);
|
|
205
|
+
if (cached) {
|
|
206
|
+
logger.info(`Returning cached latest scan: ${cacheKey}`);
|
|
207
|
+
response.json(cached);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const prefix = `repos/${repoName}/${branch}/`;
|
|
211
|
+
logger.info(`Searching GCS bucket '${bucketName}' with prefix: ${prefix}`);
|
|
212
|
+
const [files] = await storage$1.bucket(bucketName).getFiles({ prefix });
|
|
213
|
+
logger.info(`Found ${files.length} files in GCS bucket for prefix: ${prefix}`);
|
|
214
|
+
const scanInfo = /* @__PURE__ */ new Map();
|
|
215
|
+
for (const file of files) {
|
|
216
|
+
const pathParts = file.name.split("/");
|
|
217
|
+
if (pathParts.length < 5) continue;
|
|
218
|
+
const scanId = pathParts[3];
|
|
219
|
+
const fileName = pathParts[4];
|
|
220
|
+
const fileTimestamp = file.metadata.timeCreated ? new Date(file.metadata.timeCreated) : /* @__PURE__ */ new Date(0);
|
|
221
|
+
if (!scanInfo.has(scanId)) {
|
|
222
|
+
scanInfo.set(scanId, {
|
|
223
|
+
timestamp: fileTimestamp,
|
|
224
|
+
hasOutputFiles: false
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const info = scanInfo.get(scanId);
|
|
228
|
+
if (fileName === "trivy-fs-output.txt" || fileName === "trivy-image-output.txt") {
|
|
229
|
+
info.hasOutputFiles = true;
|
|
230
|
+
}
|
|
231
|
+
if (fileTimestamp < info.timestamp) {
|
|
232
|
+
info.timestamp = fileTimestamp;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const latestScanId = Array.from(scanInfo.entries()).filter(([_, info]) => info.hasOutputFiles).sort(
|
|
236
|
+
(a, b) => b[1].timestamp.getTime() - a[1].timestamp.getTime()
|
|
237
|
+
)[0]?.[0];
|
|
238
|
+
if (!latestScanId) {
|
|
239
|
+
logger.warn(`No scans found for repo ${fullRepo} on ${branch} branch in bucket ${bucketName}`);
|
|
240
|
+
response.status(404).json({
|
|
241
|
+
error: `No scans found for repo ${fullRepo} on ${branch} branch`
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
logger.info(`Found latest scan ID: ${latestScanId} for ${fullRepo}/${branch}`);
|
|
246
|
+
const latestScan = {
|
|
247
|
+
repo: fullRepo,
|
|
248
|
+
branch,
|
|
249
|
+
scanId: latestScanId,
|
|
250
|
+
metadata: {},
|
|
251
|
+
findings: [],
|
|
252
|
+
timestamp: scanInfo.get(latestScanId).timestamp
|
|
253
|
+
};
|
|
254
|
+
const latestScanFiles = files.filter((file) => {
|
|
255
|
+
const pathParts = file.name.split("/");
|
|
256
|
+
return pathParts.length >= 5 && pathParts[3] === latestScanId;
|
|
257
|
+
});
|
|
258
|
+
for (const file of latestScanFiles) {
|
|
259
|
+
const fileName = file.name.split("/")[4];
|
|
260
|
+
if (fileName === "trivy-metadata.txt") {
|
|
261
|
+
const [content] = await file.download();
|
|
262
|
+
latestScan.metadata = parseMetadata(content.toString());
|
|
263
|
+
} else if (fileName === "trivy-fs-output.txt" || fileName === "trivy-image-output.txt") {
|
|
264
|
+
const [content] = await file.download();
|
|
265
|
+
latestScan.findings.push(...parseTrivyOutput(content.toString()));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
logger.info(`Successfully loaded scan for ${fullRepo}/${branch}/${latestScanId} with ${latestScan.findings.length} findings`);
|
|
269
|
+
cache$1.set(cacheKey, latestScan);
|
|
270
|
+
response.json(latestScan);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
273
|
+
const fullRepo = decodeURIComponent(request.params.repo);
|
|
274
|
+
logger.error(`Error fetching latest scan for ${fullRepo}:`, error);
|
|
275
|
+
logger.error(`Error details: ${errorMessage}`);
|
|
276
|
+
if (error instanceof Error && error.stack) {
|
|
277
|
+
logger.error("Stack trace:", { stack: error.stack });
|
|
278
|
+
}
|
|
279
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
280
|
+
if (error.code === 403) {
|
|
281
|
+
response.status(403).json({
|
|
282
|
+
error: "Permission denied accessing GCS bucket",
|
|
283
|
+
details: "Valid credentials but no bucket access. Check IAM permissions for Storage Object Viewer role.",
|
|
284
|
+
bucket: bucketName,
|
|
285
|
+
repo: fullRepo,
|
|
286
|
+
branch: "main"
|
|
287
|
+
});
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
response.status(500).json({
|
|
292
|
+
error: "Failed to fetch latest scan",
|
|
293
|
+
details: errorMessage,
|
|
294
|
+
repo: fullRepo,
|
|
295
|
+
branch: "main"
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
return router;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
exports.createRouter = createRouter;
|
|
303
|
+
//# sourceMappingURL=router.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["import { LoggerService } from \"@backstage/backend-plugin-api\";\nimport { Config } from \"@backstage/config\";\nimport express from \"express\";\nimport Router from \"express-promise-router\";\nimport { Storage } from \"@google-cloud/storage\";\nimport { CacheManager } from \"./cache\";\n\nexport interface RouterOptions {\n logger: LoggerService;\n config?: Config;\n}\n\ninterface TrivyBackendConfig {\n bucketName: string;\n cacheTime: number;\n}\n\ninterface TrivyMetadata {\n circleciUrl?: string;\n githubPr?: string;\n gitCommit?: string;\n committer?: string;\n}\n\ninterface TrivyFinding {\n file: string;\n severity: string;\n title: string;\n description: string;\n lines: string;\n avdId?: string;\n}\n\ninterface TrivyScanResult {\n repo: string;\n branch: string;\n scanId: string;\n metadata: TrivyMetadata;\n findings: TrivyFinding[];\n timestamp: Date;\n}\n\nfunction parseMetadata(content: string): TrivyMetadata {\n const lines = content.split(\"\\n\");\n const metadata: TrivyMetadata = {};\n\n for (const line of lines) {\n if (line.startsWith(\"CircleCI Build URL:\")) {\n metadata.circleciUrl =\n line.substring(\"CircleCI Build URL:\".length).trim() || undefined;\n } else if (line.startsWith(\"GitHub PR:\")) {\n metadata.githubPr =\n line.substring(\"GitHub PR:\".length).trim() || undefined;\n } else if (line.startsWith(\"Git Commit:\")) {\n metadata.gitCommit =\n line.substring(\"Git Commit:\".length).trim() || undefined;\n } else if (line.startsWith(\"Committer:\")) {\n metadata.committer =\n line.substring(\"Committer:\".length).trim() || undefined;\n }\n }\n\n return metadata;\n}\n\nfunction parseTrivyTableFormat(\n content: string,\n currentFile: string,\n): TrivyFinding[] {\n const findings: TrivyFinding[] = [];\n const lines = content.split(\"\\n\");\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i];\n\n // Skip table borders and headers\n if (\n line.includes(\"┌\") ||\n line.includes(\"├\") ||\n line.includes(\"└\") ||\n line.includes(\"Library\") ||\n line.includes(\"Vulnerability\")\n ) {\n continue;\n }\n\n // Parse data rows (contain │ separators)\n if (line.includes(\"│\")) {\n const parts = line\n .split(\"│\")\n .map((p) => p.trim())\n .filter((p) => p);\n\n if (parts.length >= 3) {\n const library = parts[0];\n const vulnerability = parts[1];\n const severity = parts[2];\n\n // Skip if not a valid severity or if it's continuation of previous row\n if (\n ![\"CRITICAL\", \"HIGH\", \"MEDIUM\", \"LOW\", \"UNKNOWN\"].includes(severity)\n ) {\n continue;\n }\n\n // Get title from the last column if available\n const title = parts[parts.length - 1] || vulnerability;\n\n // Look for URL in the next line or current line\n let avdId: string | undefined;\n const urlMatch = line.match(/https:\\/\\/avd\\.aquasec\\.com\\/[^\\s│]+/);\n if (urlMatch) {\n avdId = urlMatch[0];\n } else if (i + 1 < lines.length) {\n const nextLineUrlMatch = lines[i + 1].match(\n /https:\\/\\/avd\\.aquasec\\.com\\/[^\\s│]+/,\n );\n if (nextLineUrlMatch) {\n avdId = nextLineUrlMatch[0];\n }\n }\n\n findings.push({\n file: library || currentFile,\n severity,\n title: title.split(\"\\n\")[0], // Take first line of title\n description: vulnerability,\n lines: \"\",\n avdId,\n });\n }\n }\n }\n\n return findings;\n}\n\nfunction parseTrivyTextFormat(\n content: string,\n currentFile: string,\n): TrivyFinding[] {\n const findings: TrivyFinding[] = [];\n const sections = content.split(/\\n(?=[^\\s])/);\n\n for (const section of sections) {\n // Check if this is a finding in text format\n const severityMatch = section.match(\n /^(CRITICAL|HIGH|MEDIUM|LOW|UNKNOWN):\\s+(.+)/,\n );\n if (severityMatch && currentFile) {\n const severity = severityMatch[1];\n const title = severityMatch[2];\n\n // Extract description\n const descMatch = section.match(/═{40}\\n([\\s\\S]+?)\\n(?:See|─{40})/);\n const description = descMatch ? descMatch[1].trim() : \"\";\n\n // Extract AVD ID\n const avdMatch = section.match(\n /See\\s+(https:\\/\\/avd\\.aquasec\\.com\\/[^\\s]+)/,\n );\n const avdId = avdMatch ? avdMatch[1] : undefined;\n\n // Extract line numbers\n const linesMatch = section.match(/─{40}\\n\\s*(.+?):(\\d+)(?:-(\\d+))?/);\n let lines = \"\";\n if (linesMatch) {\n lines = linesMatch[3]\n ? `${linesMatch[2]}-${linesMatch[3]}`\n : linesMatch[2];\n }\n\n findings.push({\n file: currentFile,\n severity,\n title,\n description,\n lines,\n avdId,\n });\n }\n }\n\n return findings;\n}\n\nfunction parseTrivyOutput(content: string): TrivyFinding[] {\n const findings: TrivyFinding[] = [];\n\n // Split content into sections by file headers\n const sections = content.split(/\\n(?=\\S.*\\s+\\([^)]+\\)\\s*\\n=+)/);\n\n for (const section of sections) {\n // Extract file header\n const fileMatch = section.match(/^(.+)\\s+\\(([^)]+)\\)\\s*\\n=+/);\n if (!fileMatch) continue;\n\n const currentFile = fileMatch[1];\n\n // Check if this section contains a table (has box-drawing characters)\n if (section.includes(\"┌\") || section.includes(\"│\")) {\n findings.push(...parseTrivyTableFormat(section, currentFile));\n }\n\n // Also parse text format findings (misconfigs, secrets)\n findings.push(...parseTrivyTextFormat(section, currentFile));\n }\n\n return findings;\n}\n\nfunction getBackendConfig(config?: Config): TrivyBackendConfig {\n const trivyConfig = config?.getOptionalConfig(\"trivy\");\n return {\n bucketName:\n trivyConfig?.getOptionalString(\"bucketName\") || \"sanity-trivy-logs\",\n cacheTime: trivyConfig?.getOptionalNumber(\"cacheTime\") || 5,\n };\n}\n\nexport async function createRouter(\n options: RouterOptions,\n): Promise<express.Router> {\n const { logger, config } = options;\n const router = Router();\n router.use(express.json());\n\n const backendConfig = getBackendConfig(config);\n const storage = new Storage();\n const bucketName = backendConfig.bucketName;\n const cache = new CacheManager(backendConfig.cacheTime, logger);\n\n router.get(\"/health\", (_, response) => {\n logger.info(\"PONG!\");\n response.json({\n status: \"ok\",\n cache: cache.stats(),\n config: {\n bucketName,\n cacheTime: backendConfig.cacheTime,\n },\n });\n });\n\n // Get scan by ID\n router.get(\"/scans/:repo/:branch/:scanId\", async (request, response) => {\n try {\n // Decode the URL-encoded repo name (e.g., org%2Frepo -> org/repo)\n const repo = decodeURIComponent(request.params.repo);\n const { branch, scanId } = request.params;\n const cacheKey = `scan-${repo}-${branch}-${scanId}`;\n const cached = cache.get<TrivyScanResult>(cacheKey);\n\n if (cached) {\n logger.info(`Returning cached scan: ${cacheKey}`);\n response.json(cached);\n return;\n }\n\n const prefix = `repos/${repo}/${branch}/${scanId}/`;\n\n const [files] = await storage.bucket(bucketName).getFiles({ prefix });\n\n const scan: TrivyScanResult = {\n repo,\n branch,\n scanId,\n metadata: {},\n findings: [],\n timestamp: new Date(),\n };\n\n for (const file of files) {\n const fileName = file.name.split(\"/\").pop();\n\n if (fileName === \"trivy-metadata.txt\") {\n const [content] = await file.download();\n scan.metadata = parseMetadata(content.toString());\n } else if (\n fileName === \"trivy-fs-output.txt\" ||\n fileName === \"trivy-image-output.txt\"\n ) {\n const [content] = await file.download();\n scan.findings.push(...parseTrivyOutput(content.toString()));\n }\n\n if (file.metadata.timeCreated) {\n scan.timestamp = new Date(file.metadata.timeCreated);\n }\n }\n\n cache.set(cacheKey, scan);\n response.json(scan);\n } catch (error) {\n logger.error(\"Error fetching scan:\", error as Error);\n\n // Check for GCS permission errors\n if (error && typeof error === 'object' && 'code' in error) {\n if (error.code === 403) {\n response.status(403).json({\n error: \"Permission denied accessing GCS bucket\",\n details: \"Valid credentials but no bucket access. Check IAM permissions for Storage Object Viewer role.\",\n bucket: bucketName\n });\n return;\n }\n }\n\n response.status(500).json({ error: \"Failed to fetch scan\" });\n }\n });\n\n // Get latest scan for a repo (main branch only)\n router.get(\"/scans/:repo/latest\", async (request, response) => {\n try {\n // Decode the URL-encoded repo name (e.g., org%2Frepo -> org/repo)\n const fullRepo = decodeURIComponent(request.params.repo);\n const branch = \"main\"; // Focus on main branch only\n\n // Extract just the repo name from org/repo format\n const repoName = fullRepo.includes('/') ? fullRepo.split('/')[1] : fullRepo;\n\n const cacheKey = `latest-${fullRepo}-${branch}`;\n\n logger.info(`Fetching latest scan for repo: ${fullRepo} (using path: ${repoName}), branch: ${branch}`);\n\n const cached = cache.get<TrivyScanResult>(cacheKey);\n\n if (cached) {\n logger.info(`Returning cached latest scan: ${cacheKey}`);\n response.json(cached);\n return;\n }\n\n const prefix = `repos/${repoName}/${branch}/`;\n logger.info(`Searching GCS bucket '${bucketName}' with prefix: ${prefix}`);\n\n const [files] = await storage.bucket(bucketName).getFiles({ prefix });\n\n logger.info(`Found ${files.length} files in GCS bucket for prefix: ${prefix}`);\n\n // Group files by scanId and track timestamps\n const scanInfo = new Map<\n string,\n { timestamp: Date; hasOutputFiles: boolean }\n >();\n\n for (const file of files) {\n const pathParts = file.name.split(\"/\");\n if (pathParts.length < 5) continue;\n\n const scanId = pathParts[3];\n const fileName = pathParts[4];\n const fileTimestamp = file.metadata.timeCreated\n ? new Date(file.metadata.timeCreated)\n : new Date(0);\n\n if (!scanInfo.has(scanId)) {\n scanInfo.set(scanId, {\n timestamp: fileTimestamp,\n hasOutputFiles: false,\n });\n }\n\n const info = scanInfo.get(scanId)!;\n // Track if this scan has actual trivy output files\n if (\n fileName === \"trivy-fs-output.txt\" ||\n fileName === \"trivy-image-output.txt\"\n ) {\n info.hasOutputFiles = true;\n }\n // Use earliest file timestamp for the scan\n if (fileTimestamp < info.timestamp) {\n info.timestamp = fileTimestamp;\n }\n }\n\n // Find the most recent scan that has output files\n const latestScanId = Array.from(scanInfo.entries())\n .filter(([_, info]) => info.hasOutputFiles)\n .sort(\n (a, b) => b[1].timestamp.getTime() - a[1].timestamp.getTime(),\n )[0]?.[0];\n\n if (!latestScanId) {\n logger.warn(`No scans found for repo ${fullRepo} on ${branch} branch in bucket ${bucketName}`);\n response.status(404).json({\n error: `No scans found for repo ${fullRepo} on ${branch} branch`,\n });\n return;\n }\n\n logger.info(`Found latest scan ID: ${latestScanId} for ${fullRepo}/${branch}`);\n\n // Now only download files for the latest scan\n const latestScan: TrivyScanResult = {\n repo: fullRepo,\n branch,\n scanId: latestScanId,\n metadata: {},\n findings: [],\n timestamp: scanInfo.get(latestScanId)!.timestamp,\n };\n\n const latestScanFiles = files.filter((file) => {\n const pathParts = file.name.split(\"/\");\n return pathParts.length >= 5 && pathParts[3] === latestScanId;\n });\n\n for (const file of latestScanFiles) {\n const fileName = file.name.split(\"/\")[4];\n\n if (fileName === \"trivy-metadata.txt\") {\n const [content] = await file.download();\n latestScan.metadata = parseMetadata(content.toString());\n } else if (\n fileName === \"trivy-fs-output.txt\" ||\n fileName === \"trivy-image-output.txt\"\n ) {\n const [content] = await file.download();\n latestScan.findings.push(...parseTrivyOutput(content.toString()));\n }\n }\n\n logger.info(`Successfully loaded scan for ${fullRepo}/${branch}/${latestScanId} with ${latestScan.findings.length} findings`);\n\n cache.set(cacheKey, latestScan);\n response.json(latestScan);\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : String(error);\n const fullRepo = decodeURIComponent(request.params.repo);\n logger.error(`Error fetching latest scan for ${fullRepo}:`, error as Error);\n logger.error(`Error details: ${errorMessage}`);\n if (error instanceof Error && error.stack) {\n logger.error('Stack trace:', { stack: error.stack });\n }\n\n // Check for GCS permission errors\n if (error && typeof error === 'object' && 'code' in error) {\n if (error.code === 403) {\n response.status(403).json({\n error: \"Permission denied accessing GCS bucket\",\n details: \"Valid credentials but no bucket access. Check IAM permissions for Storage Object Viewer role.\",\n bucket: bucketName,\n repo: fullRepo,\n branch: \"main\",\n });\n return;\n }\n }\n\n response.status(500).json({\n error: \"Failed to fetch latest scan\",\n details: errorMessage,\n repo: fullRepo,\n branch: \"main\",\n });\n }\n });\n\n return router;\n}\n"],"names":["Router","express","storage","Storage","cache","CacheManager"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanity-labs/backstage-plugin-trivy-backend",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"main": "dist/index.cjs.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/sanity-labs/backstage-
|
|
9
|
+
"url": "git+https://github.com/sanity-labs/backstage-plugins.git",
|
|
10
|
+
"directory": "plugins/trivy-backend"
|
|
10
11
|
},
|
|
11
12
|
"publishConfig": {
|
|
12
13
|
"access": "public",
|
|
@@ -14,7 +15,14 @@
|
|
|
14
15
|
"types": "dist/index.d.ts"
|
|
15
16
|
},
|
|
16
17
|
"backstage": {
|
|
17
|
-
"role": "backend-plugin"
|
|
18
|
+
"role": "backend-plugin",
|
|
19
|
+
"pluginId": "trivy",
|
|
20
|
+
"pluginPackages": [
|
|
21
|
+
"@sanity-labs/backstage-plugin-trivy-backend"
|
|
22
|
+
],
|
|
23
|
+
"features": {
|
|
24
|
+
".": "@backstage/BackendFeature"
|
|
25
|
+
}
|
|
18
26
|
},
|
|
19
27
|
"scripts": {
|
|
20
28
|
"start": "backstage-cli package start",
|
|
@@ -27,10 +35,10 @@
|
|
|
27
35
|
"tsc": "tsc --project tsconfig.json --declaration --emitDeclarationOnly --outDir dist-types/src"
|
|
28
36
|
},
|
|
29
37
|
"dependencies": {
|
|
30
|
-
"@backstage/backend-
|
|
31
|
-
"@backstage/backend-plugin-api": "
|
|
32
|
-
"@backstage/catalog-model": "
|
|
33
|
-
"@backstage/config": "
|
|
38
|
+
"@backstage/backend-defaults": "backstage:^",
|
|
39
|
+
"@backstage/backend-plugin-api": "backstage:^",
|
|
40
|
+
"@backstage/catalog-model": "backstage:^",
|
|
41
|
+
"@backstage/config": "backstage:^",
|
|
34
42
|
"@google-cloud/storage": "^7.7.0",
|
|
35
43
|
"express": "^4.18.2",
|
|
36
44
|
"express-promise-router": "^4.1.1",
|
|
@@ -39,7 +47,7 @@
|
|
|
39
47
|
"yn": "^4.0.0"
|
|
40
48
|
},
|
|
41
49
|
"devDependencies": {
|
|
42
|
-
"@backstage/cli": "
|
|
50
|
+
"@backstage/cli": "backstage:^",
|
|
43
51
|
"@types/express": "*",
|
|
44
52
|
"@types/jest": "^30.0.0",
|
|
45
53
|
"@types/node": "*",
|
|
@@ -47,5 +55,12 @@
|
|
|
47
55
|
},
|
|
48
56
|
"files": [
|
|
49
57
|
"dist"
|
|
50
|
-
]
|
|
58
|
+
],
|
|
59
|
+
"typesVersions": {
|
|
60
|
+
"*": {
|
|
61
|
+
"package.json": [
|
|
62
|
+
"package.json"
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
}
|
|
51
66
|
}
|