@sanity-labs/backstage-plugin-trivy 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +230 -0
- package/dist/esm/index-e137a1bb.esm.js +104 -0
- package/dist/esm/index-e137a1bb.esm.js.map +1 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.esm.js +269 -0
- package/dist/index.esm.js.map +1 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Trivy Frontend Plugin
|
|
2
|
+
|
|
3
|
+
This frontend plugin displays Trivy security scan results in Backstage.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Security Scans Page**: View all security scans across repositories
|
|
8
|
+
- **Entity Card**: Display latest scan results on catalog entity pages
|
|
9
|
+
- **Severity-based filtering**: See critical, high, medium, and low severity findings
|
|
10
|
+
- **Links to GitHub PRs and CircleCI builds**
|
|
11
|
+
- **Expandable finding details** with remediation guidance
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### 1. Add to your Backstage app dependencies
|
|
16
|
+
|
|
17
|
+
Add to `packages/app/package.json`:
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@internal/backstage-plugin-trivy": "^0.1.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### 2. Configure the plugin (optional)
|
|
27
|
+
|
|
28
|
+
Add to your `app-config.yaml`:
|
|
29
|
+
```yaml
|
|
30
|
+
trivy:
|
|
31
|
+
# GCS bucket name (default: sanity-trivy-logs)
|
|
32
|
+
bucketName: sanity-trivy-logs
|
|
33
|
+
|
|
34
|
+
# Cache times in minutes
|
|
35
|
+
cacheTime: 30 # Frontend cache duration
|
|
36
|
+
staleTime: 5 # When to refetch in background
|
|
37
|
+
|
|
38
|
+
# Severity filter (default: CRITICAL, HIGH, MEDIUM)
|
|
39
|
+
severityFilter:
|
|
40
|
+
- CRITICAL
|
|
41
|
+
- HIGH
|
|
42
|
+
- MEDIUM
|
|
43
|
+
# - LOW # Uncomment to include low severity
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 3. Add the route to your app
|
|
47
|
+
|
|
48
|
+
In `packages/app/src/App.tsx`:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import { TrivyPage } from '@internal/backstage-plugin-trivy';
|
|
52
|
+
|
|
53
|
+
// ... inside <FlatRoutes>
|
|
54
|
+
<Route path="/trivy" element={<TrivyPage />} />
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 4. Add to navigation (optional)
|
|
58
|
+
|
|
59
|
+
In `packages/app/src/components/Root/Root.tsx`:
|
|
60
|
+
|
|
61
|
+
```tsx
|
|
62
|
+
import SecurityIcon from '@material-ui/icons/Security';
|
|
63
|
+
|
|
64
|
+
// ... inside <SidebarGroup>
|
|
65
|
+
<SidebarItem icon={SecurityIcon} to="trivy" text="Security" />
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 5. Add Entity Card (optional)
|
|
69
|
+
|
|
70
|
+
To show scan results on entity pages, add to your entity page layout in `packages/app/src/components/catalog/EntityPage.tsx`:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
import { EntityTrivyCard } from '@internal/backstage-plugin-trivy';
|
|
74
|
+
|
|
75
|
+
// For service entities
|
|
76
|
+
const serviceEntityPage = (
|
|
77
|
+
<EntityLayout>
|
|
78
|
+
<EntityLayout.Route path="/" title="Overview">
|
|
79
|
+
<Grid container spacing={3}>
|
|
80
|
+
{/* ... other cards ... */}
|
|
81
|
+
<Grid item md={6}>
|
|
82
|
+
<EntityTrivyCard />
|
|
83
|
+
</Grid>
|
|
84
|
+
</Grid>
|
|
85
|
+
</EntityLayout.Route>
|
|
86
|
+
</EntityLayout>
|
|
87
|
+
);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 6. Use Standalone Card (optional)
|
|
91
|
+
|
|
92
|
+
For custom pages without entity context:
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
import { StandaloneTrivyCard } from '@internal/backstage-plugin-trivy';
|
|
96
|
+
|
|
97
|
+
// Use anywhere with explicit repository
|
|
98
|
+
<StandaloneTrivyCard repository="alert-relay" />
|
|
99
|
+
// or
|
|
100
|
+
<StandaloneTrivyCard repository="sanity-io/alert-relay" title="Custom Title" />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Usage
|
|
104
|
+
|
|
105
|
+
### Viewing All Scans
|
|
106
|
+
|
|
107
|
+
Navigate to `/trivy` to see a table of all security scans across your repositories. The table shows:
|
|
108
|
+
|
|
109
|
+
- Repository name and branch
|
|
110
|
+
- Finding counts by severity
|
|
111
|
+
- Git commit hash
|
|
112
|
+
- Links to GitHub PRs and CircleCI builds
|
|
113
|
+
- Scan timestamp
|
|
114
|
+
|
|
115
|
+
### Entity Card
|
|
116
|
+
|
|
117
|
+
The `EntityTrivyCard` automatically displays the latest scan for the entity's repository. It reads the repository name from:
|
|
118
|
+
|
|
119
|
+
- `github.com/project-slug` annotation, or
|
|
120
|
+
- `backstage.io/source-location` annotation
|
|
121
|
+
|
|
122
|
+
Example entity:
|
|
123
|
+
```yaml
|
|
124
|
+
apiVersion: backstage.io/v1alpha1
|
|
125
|
+
kind: Component
|
|
126
|
+
metadata:
|
|
127
|
+
name: my-service
|
|
128
|
+
annotations:
|
|
129
|
+
github.com/project-slug: sanity-io/alert-relay
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Interpreting Results
|
|
133
|
+
|
|
134
|
+
Findings are color-coded by severity:
|
|
135
|
+
- 🔴 **CRITICAL**: Red - Requires immediate attention
|
|
136
|
+
- 🟠**HIGH**: Orange - Should be fixed soon
|
|
137
|
+
- 🟡 **MEDIUM**: Yellow - Should be addressed
|
|
138
|
+
- 🔵 **LOW**: Blue - Consider fixing
|
|
139
|
+
- ⚪ **UNKNOWN**: Gray - Needs review
|
|
140
|
+
|
|
141
|
+
Each finding includes:
|
|
142
|
+
- Affected file and line numbers
|
|
143
|
+
- Description of the issue
|
|
144
|
+
- Link to detailed remediation guidance
|
|
145
|
+
|
|
146
|
+
## API & Hooks
|
|
147
|
+
|
|
148
|
+
### React Query Hooks (Recommended)
|
|
149
|
+
|
|
150
|
+
The plugin provides React Query hooks with built-in caching:
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import { useTrivyScans, useTrivyLatestScan, useTrivyScan } from '@internal/backstage-plugin-trivy';
|
|
154
|
+
|
|
155
|
+
const MyComponent = () => {
|
|
156
|
+
// Get all scans with caching
|
|
157
|
+
const { data, isLoading, error } = useTrivyScans();
|
|
158
|
+
|
|
159
|
+
// Get latest scan for a repo
|
|
160
|
+
const { data: latest } = useTrivyLatestScan('alert-relay');
|
|
161
|
+
|
|
162
|
+
// Get specific scan
|
|
163
|
+
const { data: scan } = useTrivyScan('repo', 'branch', 'scanId');
|
|
164
|
+
};
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Direct API (Low-level)
|
|
168
|
+
|
|
169
|
+
For custom implementations without React Query:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { useApi } from '@backstage/core-plugin-api';
|
|
173
|
+
import { trivyApiRef } from '@internal/backstage-plugin-trivy';
|
|
174
|
+
|
|
175
|
+
const MyComponent = () => {
|
|
176
|
+
const trivyApi = useApi(trivyApiRef);
|
|
177
|
+
|
|
178
|
+
// Get all scans
|
|
179
|
+
const scans = await trivyApi.getScans();
|
|
180
|
+
|
|
181
|
+
// Get specific scan
|
|
182
|
+
const scan = await trivyApi.getScan('repo', 'branch', 'scanId');
|
|
183
|
+
|
|
184
|
+
// Get latest scan for a repo
|
|
185
|
+
const latest = await trivyApi.getLatestScan('repo');
|
|
186
|
+
};
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Configuration
|
|
190
|
+
|
|
191
|
+
### Severity Filtering
|
|
192
|
+
|
|
193
|
+
Control which severity levels are displayed:
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
trivy:
|
|
197
|
+
severityFilter:
|
|
198
|
+
- CRITICAL # Always recommended
|
|
199
|
+
- HIGH # Always recommended
|
|
200
|
+
- MEDIUM # Recommended for most teams
|
|
201
|
+
# - LOW # Uncomment for comprehensive view
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Caching
|
|
205
|
+
|
|
206
|
+
The plugin uses two-level caching:
|
|
207
|
+
|
|
208
|
+
1. **Frontend Cache** (React Query):
|
|
209
|
+
- `cacheTime`: How long data stays in memory (default: 30 min)
|
|
210
|
+
- `staleTime`: When to refetch in background (default: 5 min)
|
|
211
|
+
|
|
212
|
+
2. **Backend Cache** (In-memory):
|
|
213
|
+
- Caches GCS responses to reduce API calls
|
|
214
|
+
- Configurable via `trivy.cacheTime` (default: 5 min)
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
trivy:
|
|
218
|
+
cacheTime: 30 # Frontend: keep in cache for 30 minutes
|
|
219
|
+
staleTime: 5 # Frontend: consider stale after 5 minutes
|
|
220
|
+
# Backend cache uses the same cacheTime value
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Custom Bucket
|
|
224
|
+
|
|
225
|
+
If using a different GCS bucket:
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
trivy:
|
|
229
|
+
bucketName: my-custom-bucket
|
|
230
|
+
```
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Progress, Table, Link, Page, Header, Content } from '@backstage/core-components';
|
|
3
|
+
import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
|
4
|
+
import { getTrivyConfig, useTrivyScans, TrivyQueryProvider } from '../index.esm.js';
|
|
5
|
+
import { Chip, Box, Typography } from '@material-ui/core';
|
|
6
|
+
import Alert from '@material-ui/lab/Alert';
|
|
7
|
+
import '@material-ui/icons/ExpandMore';
|
|
8
|
+
import '@material-ui/icons/Warning';
|
|
9
|
+
import '@material-ui/icons/Error';
|
|
10
|
+
import '@material-ui/icons/Info';
|
|
11
|
+
import '@backstage/plugin-catalog-react';
|
|
12
|
+
import '@tanstack/react-query';
|
|
13
|
+
import '@tanstack/query-sync-storage-persister';
|
|
14
|
+
import '@tanstack/react-query-persist-client';
|
|
15
|
+
|
|
16
|
+
const getSeverityCounts = (scan) => {
|
|
17
|
+
const counts = {
|
|
18
|
+
CRITICAL: 0,
|
|
19
|
+
HIGH: 0,
|
|
20
|
+
MEDIUM: 0,
|
|
21
|
+
LOW: 0,
|
|
22
|
+
UNKNOWN: 0
|
|
23
|
+
};
|
|
24
|
+
scan.findings.forEach((finding) => {
|
|
25
|
+
counts[finding.severity] = (counts[finding.severity] || 0) + 1;
|
|
26
|
+
});
|
|
27
|
+
return counts;
|
|
28
|
+
};
|
|
29
|
+
const TrivyScansTable = () => {
|
|
30
|
+
const configApi = useApi(configApiRef);
|
|
31
|
+
const config = getTrivyConfig(configApi);
|
|
32
|
+
const { data, isLoading, error } = useTrivyScans();
|
|
33
|
+
if (isLoading) {
|
|
34
|
+
return /* @__PURE__ */ React.createElement(Progress, null);
|
|
35
|
+
}
|
|
36
|
+
if (error) {
|
|
37
|
+
return /* @__PURE__ */ React.createElement(Alert, { severity: "error" }, "Failed to load scans: ", error instanceof Error ? error.message : "Unknown error");
|
|
38
|
+
}
|
|
39
|
+
const filteredScans = (data || []).map((scan) => ({
|
|
40
|
+
...scan,
|
|
41
|
+
findings: scan.findings.filter(
|
|
42
|
+
(finding) => config.severityFilter.includes(finding.severity)
|
|
43
|
+
)
|
|
44
|
+
}));
|
|
45
|
+
const columns = [
|
|
46
|
+
{
|
|
47
|
+
title: "Repository",
|
|
48
|
+
field: "repo",
|
|
49
|
+
highlight: true
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
title: "Branch",
|
|
53
|
+
field: "branch"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
title: "Findings",
|
|
57
|
+
render: (row) => {
|
|
58
|
+
const counts = getSeverityCounts(row);
|
|
59
|
+
const total = row.findings.length;
|
|
60
|
+
if (total === 0) {
|
|
61
|
+
return /* @__PURE__ */ React.createElement(Chip, { label: "\u2713 No issues", size: "small", style: { backgroundColor: "#4caf50", color: "white" } });
|
|
62
|
+
}
|
|
63
|
+
return /* @__PURE__ */ React.createElement(Box, { display: "flex", style: { gap: 4 } }, counts.CRITICAL > 0 && /* @__PURE__ */ React.createElement(Chip, { label: `${counts.CRITICAL} Critical`, size: "small", color: "secondary" }), counts.HIGH > 0 && /* @__PURE__ */ React.createElement(Chip, { label: `${counts.HIGH} High`, size: "small", color: "secondary" }), counts.MEDIUM > 0 && /* @__PURE__ */ React.createElement(Chip, { label: `${counts.MEDIUM} Medium`, size: "small", color: "primary" }), counts.LOW > 0 && /* @__PURE__ */ React.createElement(Chip, { label: `${counts.LOW} Low`, size: "small", color: "default" }));
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: "Commit",
|
|
68
|
+
render: (row) => {
|
|
69
|
+
if (row.metadata.gitCommit) {
|
|
70
|
+
return /* @__PURE__ */ React.createElement(Typography, { variant: "body2", style: { fontFamily: "monospace" } }, row.metadata.gitCommit.substring(0, 7));
|
|
71
|
+
}
|
|
72
|
+
return "-";
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
title: "Links",
|
|
77
|
+
render: (row) => {
|
|
78
|
+
return /* @__PURE__ */ React.createElement(Box, { display: "flex", style: { gap: 8 } }, row.metadata.githubPr && /* @__PURE__ */ React.createElement(Link, { to: row.metadata.githubPr, target: "_blank" }, "PR"), row.metadata.circleciUrl && /* @__PURE__ */ React.createElement(Link, { to: row.metadata.circleciUrl, target: "_blank" }, "Build"));
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
title: "Timestamp",
|
|
83
|
+
field: "timestamp",
|
|
84
|
+
type: "datetime"
|
|
85
|
+
}
|
|
86
|
+
];
|
|
87
|
+
return /* @__PURE__ */ React.createElement(
|
|
88
|
+
Table,
|
|
89
|
+
{
|
|
90
|
+
title: "Trivy Security Scans",
|
|
91
|
+
subtitle: `Showing findings: ${config.severityFilter.join(", ")}`,
|
|
92
|
+
options: { search: true, paging: true, pageSize: 20 },
|
|
93
|
+
columns,
|
|
94
|
+
data: filteredScans
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const TrivyPage = () => {
|
|
100
|
+
return /* @__PURE__ */ React.createElement(TrivyQueryProvider, null, /* @__PURE__ */ React.createElement(Page, { themeId: "tool" }, /* @__PURE__ */ React.createElement(Header, { title: "Security Scans", subtitle: "Trivy vulnerability and misconfiguration findings" }), /* @__PURE__ */ React.createElement(Content, null, /* @__PURE__ */ React.createElement(TrivyScansTable, null))));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export { TrivyPage };
|
|
104
|
+
//# sourceMappingURL=index-e137a1bb.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index-e137a1bb.esm.js","sources":["../../src/components/TrivyScansTable/TrivyScansTable.tsx","../../src/components/TrivyPage/TrivyPage.tsx"],"sourcesContent":["import React from 'react';\nimport { Table, TableColumn, Progress, Link } from '@backstage/core-components';\nimport { useApi, configApiRef } from '@backstage/core-plugin-api';\nimport { TrivyScanResult } from '../../api/TrivyApi';\nimport { useTrivyScans } from '../../api/hooks';\nimport { getTrivyConfig } from '../../api/config';\nimport {\n Box,\n Chip,\n Typography,\n} from '@material-ui/core';\nimport Alert from '@material-ui/lab/Alert';\n\nconst getSeverityCounts = (scan: TrivyScanResult) => {\n const counts = {\n CRITICAL: 0,\n HIGH: 0,\n MEDIUM: 0,\n LOW: 0,\n UNKNOWN: 0,\n };\n\n scan.findings.forEach(finding => {\n counts[finding.severity] = (counts[finding.severity] || 0) + 1;\n });\n\n return counts;\n};\n\nexport const TrivyScansTable = () => {\n const configApi = useApi(configApiRef);\n const config = getTrivyConfig(configApi);\n const { data, isLoading, error } = useTrivyScans();\n\n if (isLoading) {\n return <Progress />;\n }\n\n if (error) {\n return <Alert severity=\"error\">Failed to load scans: {error instanceof Error ? error.message : 'Unknown error'}</Alert>;\n }\n\n // Filter scans by configured severity levels\n const filteredScans = (data || []).map(scan => ({\n ...scan,\n findings: scan.findings.filter(finding =>\n config.severityFilter.includes(finding.severity),\n ),\n }));\n\n const columns: TableColumn<TrivyScanResult>[] = [\n {\n title: 'Repository',\n field: 'repo',\n highlight: true,\n },\n {\n title: 'Branch',\n field: 'branch',\n },\n {\n title: 'Findings',\n render: (row: TrivyScanResult) => {\n const counts = getSeverityCounts(row);\n const total = row.findings.length;\n\n if (total === 0) {\n return <Chip label=\"✓ No issues\" size=\"small\" style={{ backgroundColor: '#4caf50', color: 'white' }} />;\n }\n\n return (\n <Box display=\"flex\" style={{ gap: 4 }}>\n {counts.CRITICAL > 0 && (\n <Chip label={`${counts.CRITICAL} Critical`} size=\"small\" color=\"secondary\" />\n )}\n {counts.HIGH > 0 && (\n <Chip label={`${counts.HIGH} High`} size=\"small\" color=\"secondary\" />\n )}\n {counts.MEDIUM > 0 && (\n <Chip label={`${counts.MEDIUM} Medium`} size=\"small\" color=\"primary\" />\n )}\n {counts.LOW > 0 && (\n <Chip label={`${counts.LOW} Low`} size=\"small\" color=\"default\" />\n )}\n </Box>\n );\n },\n },\n {\n title: 'Commit',\n render: (row: TrivyScanResult) => {\n if (row.metadata.gitCommit) {\n return (\n <Typography variant=\"body2\" style={{ fontFamily: 'monospace' }}>\n {row.metadata.gitCommit.substring(0, 7)}\n </Typography>\n );\n }\n return '-';\n },\n },\n {\n title: 'Links',\n render: (row: TrivyScanResult) => {\n return (\n <Box display=\"flex\" style={{ gap: 8 }}>\n {row.metadata.githubPr && (\n <Link to={row.metadata.githubPr} target=\"_blank\">\n PR\n </Link>\n )}\n {row.metadata.circleciUrl && (\n <Link to={row.metadata.circleciUrl} target=\"_blank\">\n Build\n </Link>\n )}\n </Box>\n );\n },\n },\n {\n title: 'Timestamp',\n field: 'timestamp',\n type: 'datetime',\n },\n ];\n\n return (\n <Table\n title=\"Trivy Security Scans\"\n subtitle={`Showing findings: ${config.severityFilter.join(', ')}`}\n options={{ search: true, paging: true, pageSize: 20 }}\n columns={columns}\n data={filteredScans}\n />\n );\n};\n","import React from 'react';\nimport { Page, Header, Content } from '@backstage/core-components';\nimport { TrivyScansTable } from '../TrivyScansTable';\nimport { TrivyQueryProvider } from '../../api/QueryProvider';\n\nexport const TrivyPage = () => {\n return (\n <TrivyQueryProvider>\n <Page themeId=\"tool\">\n <Header title=\"Security Scans\" subtitle=\"Trivy vulnerability and misconfiguration findings\" />\n <Content>\n <TrivyScansTable />\n </Content>\n </Page>\n </TrivyQueryProvider>\n );\n};\n"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import React__default from 'react';
|
|
3
|
+
import * as _backstage_core_plugin_api from '@backstage/core-plugin-api';
|
|
4
|
+
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
5
|
+
import { UseQueryOptions } from '@tanstack/react-query';
|
|
6
|
+
import { Config } from '@backstage/config';
|
|
7
|
+
|
|
8
|
+
declare const trivyPlugin: _backstage_core_plugin_api.BackstagePlugin<{
|
|
9
|
+
root: _backstage_core_plugin_api.RouteRef<undefined>;
|
|
10
|
+
}, {}, {}>;
|
|
11
|
+
declare const TrivyPage: () => React.JSX.Element;
|
|
12
|
+
|
|
13
|
+
interface TrivyMetadata {
|
|
14
|
+
circleciUrl?: string;
|
|
15
|
+
githubPr?: string;
|
|
16
|
+
gitCommit?: string;
|
|
17
|
+
committer?: string;
|
|
18
|
+
}
|
|
19
|
+
interface TrivyFinding {
|
|
20
|
+
file: string;
|
|
21
|
+
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'UNKNOWN';
|
|
22
|
+
title: string;
|
|
23
|
+
description: string;
|
|
24
|
+
lines: string;
|
|
25
|
+
avdId?: string;
|
|
26
|
+
}
|
|
27
|
+
interface TrivyScanResult {
|
|
28
|
+
repo: string;
|
|
29
|
+
branch: string;
|
|
30
|
+
scanId: string;
|
|
31
|
+
metadata: TrivyMetadata;
|
|
32
|
+
findings: TrivyFinding[];
|
|
33
|
+
timestamp: string;
|
|
34
|
+
}
|
|
35
|
+
interface TrivyApi {
|
|
36
|
+
getScans(): Promise<TrivyScanResult[]>;
|
|
37
|
+
getScan(repo: string, branch: string, scanId: string): Promise<TrivyScanResult>;
|
|
38
|
+
getLatestScan(repo: string): Promise<TrivyScanResult>;
|
|
39
|
+
}
|
|
40
|
+
declare const trivyApiRef: _backstage_core_plugin_api.ApiRef<TrivyApi>;
|
|
41
|
+
|
|
42
|
+
interface TrivyFindingsCardProps {
|
|
43
|
+
findings: TrivyFinding[];
|
|
44
|
+
title?: string;
|
|
45
|
+
}
|
|
46
|
+
declare const TrivyFindingsCard: ({ findings, title }: TrivyFindingsCardProps) => React__default.JSX.Element;
|
|
47
|
+
|
|
48
|
+
declare const EntityTrivyCard: () => React__default.JSX.Element;
|
|
49
|
+
|
|
50
|
+
interface StandaloneTrivyCardProps {
|
|
51
|
+
/** Repository identifier (e.g., "alert-relay" or "sanity-io/alert-relay") */
|
|
52
|
+
repository: string;
|
|
53
|
+
/** Optional title override */
|
|
54
|
+
title?: string;
|
|
55
|
+
}
|
|
56
|
+
declare const StandaloneTrivyCard: ({ repository, title }: StandaloneTrivyCardProps) => React__default.JSX.Element;
|
|
57
|
+
|
|
58
|
+
declare const TrivyQueryProvider: ({ children }: {
|
|
59
|
+
children: React__default.ReactNode;
|
|
60
|
+
}) => React__default.JSX.Element;
|
|
61
|
+
declare const SimpleTrivyQueryProvider: ({ children }: {
|
|
62
|
+
children: React__default.ReactNode;
|
|
63
|
+
}) => React__default.JSX.Element;
|
|
64
|
+
|
|
65
|
+
declare const useTrivyScans: (options?: Omit<UseQueryOptions<TrivyScanResult[]>, "queryKey" | "queryFn">) => _tanstack_react_query.UseQueryResult<TrivyScanResult[], Error>;
|
|
66
|
+
declare const useTrivyScan: (repo: string, branch: string, scanId: string, options?: Omit<UseQueryOptions<TrivyScanResult>, "queryKey" | "queryFn">) => _tanstack_react_query.UseQueryResult<TrivyScanResult, Error>;
|
|
67
|
+
declare const useTrivyLatestScan: (repo: string | undefined, options?: Omit<UseQueryOptions<TrivyScanResult>, "queryKey" | "queryFn">) => _tanstack_react_query.UseQueryResult<TrivyScanResult, Error>;
|
|
68
|
+
|
|
69
|
+
interface TrivyConfig {
|
|
70
|
+
bucketName: string;
|
|
71
|
+
severityFilter: string[];
|
|
72
|
+
cacheTime: number;
|
|
73
|
+
staleTime: number;
|
|
74
|
+
}
|
|
75
|
+
declare const DEFAULT_TRIVY_CONFIG: TrivyConfig;
|
|
76
|
+
declare function getTrivyConfig(config: Config): TrivyConfig;
|
|
77
|
+
|
|
78
|
+
export { DEFAULT_TRIVY_CONFIG, EntityTrivyCard, SimpleTrivyQueryProvider, StandaloneTrivyCard, TrivyApi, TrivyConfig, TrivyFinding, TrivyFindingsCard, TrivyMetadata, TrivyPage, TrivyQueryProvider, TrivyScanResult, getTrivyConfig, trivyApiRef, trivyPlugin, useTrivyLatestScan, useTrivyScan, useTrivyScans };
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { createRouteRef, createApiRef, createPlugin, createApiFactory, discoveryApiRef, fetchApiRef, createRoutableExtension, useApi, configApiRef } from '@backstage/core-plugin-api';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { InfoCard, Link, Progress } from '@backstage/core-components';
|
|
4
|
+
import { makeStyles, Box, Chip, Accordion, AccordionSummary, Typography, AccordionDetails } from '@material-ui/core';
|
|
5
|
+
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
|
6
|
+
import WarningIcon from '@material-ui/icons/Warning';
|
|
7
|
+
import ErrorIcon from '@material-ui/icons/Error';
|
|
8
|
+
import InfoIcon from '@material-ui/icons/Info';
|
|
9
|
+
import { useEntity } from '@backstage/plugin-catalog-react';
|
|
10
|
+
import { useQuery, QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
11
|
+
import Alert from '@material-ui/lab/Alert';
|
|
12
|
+
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
|
|
13
|
+
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
|
|
14
|
+
|
|
15
|
+
const rootRouteRef = createRouteRef({
|
|
16
|
+
id: "trivy"
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
var __defProp = Object.defineProperty;
|
|
20
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
21
|
+
var __publicField = (obj, key, value) => {
|
|
22
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
const trivyApiRef = createApiRef({
|
|
26
|
+
id: "plugin.trivy.service"
|
|
27
|
+
});
|
|
28
|
+
class TrivyClient {
|
|
29
|
+
constructor(options) {
|
|
30
|
+
__publicField(this, "discoveryApi");
|
|
31
|
+
__publicField(this, "fetchApi");
|
|
32
|
+
this.discoveryApi = options.discoveryApi;
|
|
33
|
+
this.fetchApi = options.fetchApi;
|
|
34
|
+
}
|
|
35
|
+
async fetch(path) {
|
|
36
|
+
const baseUrl = await this.discoveryApi.getBaseUrl("trivy");
|
|
37
|
+
const response = await this.fetchApi.fetch(`${baseUrl}${path}`);
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new Error(`Failed to fetch ${path}: ${response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
return response.json();
|
|
42
|
+
}
|
|
43
|
+
async getScans() {
|
|
44
|
+
return this.fetch("/scans");
|
|
45
|
+
}
|
|
46
|
+
async getScan(repo, branch, scanId) {
|
|
47
|
+
return this.fetch(`/scans/${repo}/${branch}/${scanId}`);
|
|
48
|
+
}
|
|
49
|
+
async getLatestScan(repo) {
|
|
50
|
+
return this.fetch(`/scans/${repo}/latest`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const trivyPlugin = createPlugin({
|
|
55
|
+
id: "trivy",
|
|
56
|
+
routes: {
|
|
57
|
+
root: rootRouteRef
|
|
58
|
+
},
|
|
59
|
+
apis: [
|
|
60
|
+
createApiFactory({
|
|
61
|
+
api: trivyApiRef,
|
|
62
|
+
deps: {
|
|
63
|
+
discoveryApi: discoveryApiRef,
|
|
64
|
+
fetchApi: fetchApiRef
|
|
65
|
+
},
|
|
66
|
+
factory: ({ discoveryApi, fetchApi }) => new TrivyClient({ discoveryApi, fetchApi })
|
|
67
|
+
})
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
const TrivyPage = trivyPlugin.provide(
|
|
71
|
+
createRoutableExtension({
|
|
72
|
+
name: "TrivyPage",
|
|
73
|
+
component: () => import('./esm/index-e137a1bb.esm.js').then((m) => m.TrivyPage),
|
|
74
|
+
mountPoint: rootRouteRef
|
|
75
|
+
})
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const useStyles = makeStyles((theme) => ({
|
|
79
|
+
finding: {
|
|
80
|
+
marginBottom: theme.spacing(1)
|
|
81
|
+
},
|
|
82
|
+
critical: {
|
|
83
|
+
borderLeft: `4px solid ${theme.palette.error.main}`
|
|
84
|
+
},
|
|
85
|
+
high: {
|
|
86
|
+
borderLeft: `4px solid ${theme.palette.error.light}`
|
|
87
|
+
},
|
|
88
|
+
medium: {
|
|
89
|
+
borderLeft: `4px solid ${theme.palette.warning.main}`
|
|
90
|
+
},
|
|
91
|
+
low: {
|
|
92
|
+
borderLeft: `4px solid ${theme.palette.info.main}`
|
|
93
|
+
},
|
|
94
|
+
unknown: {
|
|
95
|
+
borderLeft: `4px solid ${theme.palette.grey[400]}`
|
|
96
|
+
}
|
|
97
|
+
}));
|
|
98
|
+
const SeverityIcon = ({ severity }) => {
|
|
99
|
+
switch (severity) {
|
|
100
|
+
case "CRITICAL":
|
|
101
|
+
return /* @__PURE__ */ React.createElement(ErrorIcon, { style: { color: "#d32f2f" } });
|
|
102
|
+
case "HIGH":
|
|
103
|
+
return /* @__PURE__ */ React.createElement(ErrorIcon, { style: { color: "#f44336" } });
|
|
104
|
+
case "MEDIUM":
|
|
105
|
+
return /* @__PURE__ */ React.createElement(WarningIcon, { style: { color: "#ff9800" } });
|
|
106
|
+
case "LOW":
|
|
107
|
+
return /* @__PURE__ */ React.createElement(InfoIcon, { style: { color: "#2196f3" } });
|
|
108
|
+
default:
|
|
109
|
+
return /* @__PURE__ */ React.createElement(InfoIcon, { style: { color: "#9e9e9e" } });
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
const TrivyFindingsCard = ({ findings, title = "Security Findings" }) => {
|
|
113
|
+
const classes = useStyles();
|
|
114
|
+
const sortedFindings = [...findings].sort((a, b) => {
|
|
115
|
+
const severityOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3, UNKNOWN: 4 };
|
|
116
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
117
|
+
});
|
|
118
|
+
const getSeverityClass = (severity) => {
|
|
119
|
+
switch (severity) {
|
|
120
|
+
case "CRITICAL":
|
|
121
|
+
return classes.critical;
|
|
122
|
+
case "HIGH":
|
|
123
|
+
return classes.high;
|
|
124
|
+
case "MEDIUM":
|
|
125
|
+
return classes.medium;
|
|
126
|
+
case "LOW":
|
|
127
|
+
return classes.low;
|
|
128
|
+
default:
|
|
129
|
+
return classes.unknown;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
if (findings.length === 0) {
|
|
133
|
+
return /* @__PURE__ */ React.createElement(InfoCard, { title }, /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", style: { gap: 8 } }, /* @__PURE__ */ React.createElement(Chip, { label: "\u2713 No security issues found", style: { backgroundColor: "#4caf50", color: "white" } })));
|
|
134
|
+
}
|
|
135
|
+
return /* @__PURE__ */ React.createElement(InfoCard, { title }, /* @__PURE__ */ React.createElement(Box, null, sortedFindings.map((finding, index) => /* @__PURE__ */ React.createElement(Accordion, { key: index, className: `${classes.finding} ${getSeverityClass(finding.severity)}` }, /* @__PURE__ */ React.createElement(AccordionSummary, { expandIcon: /* @__PURE__ */ React.createElement(ExpandMoreIcon, null) }, /* @__PURE__ */ React.createElement(Box, { display: "flex", alignItems: "center", style: { gap: 8 }, width: "100%" }, /* @__PURE__ */ React.createElement(SeverityIcon, { severity: finding.severity }), /* @__PURE__ */ React.createElement(Chip, { label: finding.severity, size: "small" }), /* @__PURE__ */ React.createElement(Typography, { variant: "subtitle2", style: { flex: 1 } }, finding.title), /* @__PURE__ */ React.createElement(Typography, { variant: "caption", color: "textSecondary" }, finding.file, finding.lines && `:${finding.lines}`))), /* @__PURE__ */ React.createElement(AccordionDetails, null, /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Typography, { variant: "body2", paragraph: true }, finding.description), finding.avdId && /* @__PURE__ */ React.createElement(Box, { mt: 1 }, /* @__PURE__ */ React.createElement(Link, { to: finding.avdId, target: "_blank" }, "View documentation \u2192"))))))));
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const DEFAULT_TRIVY_CONFIG = {
|
|
139
|
+
bucketName: "sanity-trivy-logs",
|
|
140
|
+
severityFilter: ["CRITICAL", "HIGH", "MEDIUM"],
|
|
141
|
+
cacheTime: 30,
|
|
142
|
+
staleTime: 5
|
|
143
|
+
};
|
|
144
|
+
function getTrivyConfig(config) {
|
|
145
|
+
const trivyConfig = config.getOptionalConfig("trivy");
|
|
146
|
+
return {
|
|
147
|
+
bucketName: (trivyConfig == null ? void 0 : trivyConfig.getOptionalString("bucketName")) || DEFAULT_TRIVY_CONFIG.bucketName,
|
|
148
|
+
severityFilter: (trivyConfig == null ? void 0 : trivyConfig.getOptionalStringArray("severityFilter")) || DEFAULT_TRIVY_CONFIG.severityFilter,
|
|
149
|
+
cacheTime: (trivyConfig == null ? void 0 : trivyConfig.getOptionalNumber("cacheTime")) || DEFAULT_TRIVY_CONFIG.cacheTime,
|
|
150
|
+
staleTime: (trivyConfig == null ? void 0 : trivyConfig.getOptionalNumber("staleTime")) || DEFAULT_TRIVY_CONFIG.staleTime
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const useTrivyScans = (options) => {
|
|
155
|
+
const trivyApi = useApi(trivyApiRef);
|
|
156
|
+
const configApi = useApi(configApiRef);
|
|
157
|
+
const config = getTrivyConfig(configApi);
|
|
158
|
+
return useQuery({
|
|
159
|
+
queryKey: ["trivy", "scans"],
|
|
160
|
+
queryFn: () => trivyApi.getScans(),
|
|
161
|
+
staleTime: config.staleTime * 60 * 1e3,
|
|
162
|
+
gcTime: config.cacheTime * 60 * 1e3,
|
|
163
|
+
...options
|
|
164
|
+
});
|
|
165
|
+
};
|
|
166
|
+
const useTrivyScan = (repo, branch, scanId, options) => {
|
|
167
|
+
const trivyApi = useApi(trivyApiRef);
|
|
168
|
+
const configApi = useApi(configApiRef);
|
|
169
|
+
const config = getTrivyConfig(configApi);
|
|
170
|
+
return useQuery({
|
|
171
|
+
queryKey: ["trivy", "scan", repo, branch, scanId],
|
|
172
|
+
queryFn: () => trivyApi.getScan(repo, branch, scanId),
|
|
173
|
+
staleTime: config.staleTime * 60 * 1e3,
|
|
174
|
+
gcTime: config.cacheTime * 60 * 1e3,
|
|
175
|
+
enabled: !!(repo && branch && scanId),
|
|
176
|
+
...options
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
const useTrivyLatestScan = (repo, options) => {
|
|
180
|
+
const trivyApi = useApi(trivyApiRef);
|
|
181
|
+
const configApi = useApi(configApiRef);
|
|
182
|
+
const config = getTrivyConfig(configApi);
|
|
183
|
+
return useQuery({
|
|
184
|
+
queryKey: ["trivy", "latest", repo],
|
|
185
|
+
queryFn: () => trivyApi.getLatestScan(repo),
|
|
186
|
+
staleTime: config.staleTime * 60 * 1e3,
|
|
187
|
+
gcTime: config.cacheTime * 60 * 1e3,
|
|
188
|
+
enabled: !!repo,
|
|
189
|
+
...options
|
|
190
|
+
});
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const EntityTrivyCard = () => {
|
|
194
|
+
var _a, _b, _c;
|
|
195
|
+
const { entity } = useEntity();
|
|
196
|
+
const configApi = useApi(configApiRef);
|
|
197
|
+
const config = getTrivyConfig(configApi);
|
|
198
|
+
const repoAnnotation = ((_a = entity.metadata.annotations) == null ? void 0 : _a["github.com/project-slug"]) || ((_b = entity.metadata.annotations) == null ? void 0 : _b["backstage.io/source-location"]);
|
|
199
|
+
const repoName = (_c = repoAnnotation == null ? void 0 : repoAnnotation.split("/").pop()) == null ? void 0 : _c.replace(/^url:.*\//, "");
|
|
200
|
+
const { data, isLoading, error } = useTrivyLatestScan(repoName);
|
|
201
|
+
if (!repoName) {
|
|
202
|
+
return /* @__PURE__ */ React.createElement(Alert, { severity: "info" }, "No repository configured for this entity. Add a", " ", /* @__PURE__ */ React.createElement("code", null, "github.com/project-slug"), " annotation to enable security scanning.");
|
|
203
|
+
}
|
|
204
|
+
if (isLoading) {
|
|
205
|
+
return /* @__PURE__ */ React.createElement(Progress, null);
|
|
206
|
+
}
|
|
207
|
+
if (error) {
|
|
208
|
+
return /* @__PURE__ */ React.createElement(Alert, { severity: "warning" }, "No security scans found for repository: ", repoName);
|
|
209
|
+
}
|
|
210
|
+
if (!data) {
|
|
211
|
+
return /* @__PURE__ */ React.createElement(Alert, { severity: "info" }, "No security scans available for ", repoName);
|
|
212
|
+
}
|
|
213
|
+
const filteredFindings = data.findings.filter(
|
|
214
|
+
(finding) => config.severityFilter.includes(finding.severity)
|
|
215
|
+
);
|
|
216
|
+
return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(TrivyFindingsCard, { findings: filteredFindings, title: "Latest Security Scan" }), /* @__PURE__ */ React.createElement(Box, { mt: 2, display: "flex", justifyContent: "space-between", alignItems: "center" }, /* @__PURE__ */ React.createElement(Typography, { variant: "caption", color: "textSecondary" }, "Scanned: ", new Date(data.timestamp).toLocaleString(), " | Showing: ", config.severityFilter.join(", ")), /* @__PURE__ */ React.createElement(Box, { display: "flex", style: { gap: 8 } }, data.metadata.githubPr && /* @__PURE__ */ React.createElement(Link, { to: data.metadata.githubPr, target: "_blank" }, "View PR"), data.metadata.circleciUrl && /* @__PURE__ */ React.createElement(Link, { to: data.metadata.circleciUrl, target: "_blank" }, "View Build"))));
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
const StandaloneTrivyCard = ({ repository, title }) => {
|
|
220
|
+
const configApi = useApi(configApiRef);
|
|
221
|
+
const config = getTrivyConfig(configApi);
|
|
222
|
+
const repoName = repository.split("/").pop() || repository;
|
|
223
|
+
const { data, isLoading, error } = useTrivyLatestScan(repoName);
|
|
224
|
+
if (isLoading) {
|
|
225
|
+
return /* @__PURE__ */ React.createElement(Progress, null);
|
|
226
|
+
}
|
|
227
|
+
if (error) {
|
|
228
|
+
return /* @__PURE__ */ React.createElement(Alert, { severity: "warning" }, "No security scans found for repository: ", repoName);
|
|
229
|
+
}
|
|
230
|
+
if (!data) {
|
|
231
|
+
return /* @__PURE__ */ React.createElement(Alert, { severity: "info" }, "No security scans available for ", repoName);
|
|
232
|
+
}
|
|
233
|
+
const filteredFindings = data.findings.filter(
|
|
234
|
+
(finding) => config.severityFilter.includes(finding.severity)
|
|
235
|
+
);
|
|
236
|
+
return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(
|
|
237
|
+
TrivyFindingsCard,
|
|
238
|
+
{
|
|
239
|
+
findings: filteredFindings,
|
|
240
|
+
title: title || `Security Scan - ${repoName}`
|
|
241
|
+
}
|
|
242
|
+
), /* @__PURE__ */ React.createElement(Box, { mt: 2, display: "flex", justifyContent: "space-between", alignItems: "center" }, /* @__PURE__ */ React.createElement(Typography, { variant: "caption", color: "textSecondary" }, "Scanned: ", new Date(data.timestamp).toLocaleString()), /* @__PURE__ */ React.createElement(Box, { display: "flex", style: { gap: 8 } }, data.metadata.githubPr && /* @__PURE__ */ React.createElement(Link, { to: data.metadata.githubPr, target: "_blank" }, "View PR"), data.metadata.circleciUrl && /* @__PURE__ */ React.createElement(Link, { to: data.metadata.circleciUrl, target: "_blank" }, "View Build"))));
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const queryClient = new QueryClient({
|
|
246
|
+
defaultOptions: {
|
|
247
|
+
queries: {
|
|
248
|
+
gcTime: 1e3 * 60 * 30,
|
|
249
|
+
// 30 minutes
|
|
250
|
+
staleTime: 1e3 * 60 * 5,
|
|
251
|
+
// 5 minutes
|
|
252
|
+
retry: 1,
|
|
253
|
+
refetchOnWindowFocus: false
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
const persister = createSyncStoragePersister({
|
|
258
|
+
storage: window.localStorage,
|
|
259
|
+
key: "trivy-query-cache"
|
|
260
|
+
});
|
|
261
|
+
const TrivyQueryProvider = ({ children }) => {
|
|
262
|
+
return /* @__PURE__ */ React.createElement(PersistQueryClientProvider, { client: queryClient, persistOptions: { persister } }, children);
|
|
263
|
+
};
|
|
264
|
+
const SimpleTrivyQueryProvider = ({ children }) => {
|
|
265
|
+
return /* @__PURE__ */ React.createElement(QueryClientProvider, { client: queryClient }, children);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export { DEFAULT_TRIVY_CONFIG, EntityTrivyCard, SimpleTrivyQueryProvider, StandaloneTrivyCard, TrivyFindingsCard, TrivyPage, TrivyQueryProvider, getTrivyConfig, trivyApiRef, trivyPlugin, useTrivyLatestScan, useTrivyScan, useTrivyScans };
|
|
269
|
+
//# sourceMappingURL=index.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/routes.ts","../src/api/TrivyApi.ts","../src/plugin.ts","../src/components/TrivyFindingsCard/TrivyFindingsCard.tsx","../src/api/config.ts","../src/api/hooks.ts","../src/components/EntityTrivyCard/EntityTrivyCard.tsx","../src/components/StandaloneTrivyCard/StandaloneTrivyCard.tsx","../src/api/QueryProvider.tsx"],"sourcesContent":["import { createRouteRef } from '@backstage/core-plugin-api';\n\nexport const rootRouteRef = createRouteRef({\n id: 'trivy',\n});\n","import { createApiRef, DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';\n\nexport interface TrivyMetadata {\n circleciUrl?: string;\n githubPr?: string;\n gitCommit?: string;\n committer?: string;\n}\n\nexport interface TrivyFinding {\n file: string;\n severity: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'UNKNOWN';\n title: string;\n description: string;\n lines: string;\n avdId?: string;\n}\n\nexport interface TrivyScanResult {\n repo: string;\n branch: string;\n scanId: string;\n metadata: TrivyMetadata;\n findings: TrivyFinding[];\n timestamp: string;\n}\n\nexport interface TrivyApi {\n getScans(): Promise<TrivyScanResult[]>;\n getScan(repo: string, branch: string, scanId: string): Promise<TrivyScanResult>;\n getLatestScan(repo: string): Promise<TrivyScanResult>;\n}\n\nexport const trivyApiRef = createApiRef<TrivyApi>({\n id: 'plugin.trivy.service',\n});\n\nexport class TrivyClient implements TrivyApi {\n private readonly discoveryApi: DiscoveryApi;\n private readonly fetchApi: FetchApi;\n\n constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) {\n this.discoveryApi = options.discoveryApi;\n this.fetchApi = options.fetchApi;\n }\n\n private async fetch<T>(path: string): Promise<T> {\n const baseUrl = await this.discoveryApi.getBaseUrl('trivy');\n const response = await this.fetchApi.fetch(`${baseUrl}${path}`);\n\n if (!response.ok) {\n throw new Error(`Failed to fetch ${path}: ${response.statusText}`);\n }\n\n return response.json();\n }\n\n async getScans(): Promise<TrivyScanResult[]> {\n return this.fetch<TrivyScanResult[]>('/scans');\n }\n\n async getScan(repo: string, branch: string, scanId: string): Promise<TrivyScanResult> {\n return this.fetch<TrivyScanResult>(`/scans/${repo}/${branch}/${scanId}`);\n }\n\n async getLatestScan(repo: string): Promise<TrivyScanResult> {\n return this.fetch<TrivyScanResult>(`/scans/${repo}/latest`);\n }\n}\n","import {\n createPlugin,\n createRoutableExtension,\n createApiFactory,\n discoveryApiRef,\n fetchApiRef,\n} from '@backstage/core-plugin-api';\nimport { rootRouteRef } from './routes';\nimport { trivyApiRef, TrivyClient } from './api/TrivyApi';\n\nexport const trivyPlugin = createPlugin({\n id: 'trivy',\n routes: {\n root: rootRouteRef,\n },\n apis: [\n createApiFactory({\n api: trivyApiRef,\n deps: {\n discoveryApi: discoveryApiRef,\n fetchApi: fetchApiRef,\n },\n factory: ({ discoveryApi, fetchApi }) =>\n new TrivyClient({ discoveryApi, fetchApi }),\n }),\n ],\n});\n\nexport const TrivyPage = trivyPlugin.provide(\n createRoutableExtension({\n name: 'TrivyPage',\n component: () =>\n import('./components/TrivyPage').then(m => m.TrivyPage),\n mountPoint: rootRouteRef,\n }),\n);\n","import React from 'react';\nimport { InfoCard, Link } from '@backstage/core-components';\nimport { TrivyFinding } from '../../api/TrivyApi';\nimport {\n Box,\n Chip,\n Typography,\n Accordion,\n AccordionSummary,\n AccordionDetails,\n makeStyles,\n} from '@material-ui/core';\nimport ExpandMoreIcon from '@material-ui/icons/ExpandMore';\nimport WarningIcon from '@material-ui/icons/Warning';\nimport ErrorIcon from '@material-ui/icons/Error';\nimport InfoIcon from '@material-ui/icons/Info';\n\nconst useStyles = makeStyles(theme => ({\n finding: {\n marginBottom: theme.spacing(1),\n },\n critical: {\n borderLeft: `4px solid ${theme.palette.error.main}`,\n },\n high: {\n borderLeft: `4px solid ${theme.palette.error.light}`,\n },\n medium: {\n borderLeft: `4px solid ${theme.palette.warning.main}`,\n },\n low: {\n borderLeft: `4px solid ${theme.palette.info.main}`,\n },\n unknown: {\n borderLeft: `4px solid ${theme.palette.grey[400]}`,\n },\n}));\n\nconst SeverityIcon = ({ severity }: { severity: string }) => {\n switch (severity) {\n case 'CRITICAL':\n return <ErrorIcon style={{ color: '#d32f2f' }} />;\n case 'HIGH':\n return <ErrorIcon style={{ color: '#f44336' }} />;\n case 'MEDIUM':\n return <WarningIcon style={{ color: '#ff9800' }} />;\n case 'LOW':\n return <InfoIcon style={{ color: '#2196f3' }} />;\n default:\n return <InfoIcon style={{ color: '#9e9e9e' }} />;\n }\n};\n\ninterface TrivyFindingsCardProps {\n findings: TrivyFinding[];\n title?: string;\n}\n\nexport const TrivyFindingsCard = ({ findings, title = 'Security Findings' }: TrivyFindingsCardProps) => {\n const classes = useStyles();\n\n const sortedFindings = [...findings].sort((a, b) => {\n const severityOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3, UNKNOWN: 4 };\n return severityOrder[a.severity] - severityOrder[b.severity];\n });\n\n const getSeverityClass = (severity: string) => {\n switch (severity) {\n case 'CRITICAL':\n return classes.critical;\n case 'HIGH':\n return classes.high;\n case 'MEDIUM':\n return classes.medium;\n case 'LOW':\n return classes.low;\n default:\n return classes.unknown;\n }\n };\n\n if (findings.length === 0) {\n return (\n <InfoCard title={title}>\n <Box display=\"flex\" alignItems=\"center\" style={{ gap: 8 }}>\n <Chip label=\"✓ No security issues found\" style={{ backgroundColor: '#4caf50', color: 'white' }} />\n </Box>\n </InfoCard>\n );\n }\n\n return (\n <InfoCard title={title}>\n <Box>\n {sortedFindings.map((finding, index) => (\n <Accordion key={index} className={`${classes.finding} ${getSeverityClass(finding.severity)}`}>\n <AccordionSummary expandIcon={<ExpandMoreIcon />}>\n <Box display=\"flex\" alignItems=\"center\" style={{ gap: 8 }} width=\"100%\">\n <SeverityIcon severity={finding.severity} />\n <Chip label={finding.severity} size=\"small\" />\n <Typography variant=\"subtitle2\" style={{ flex: 1 }}>\n {finding.title}\n </Typography>\n <Typography variant=\"caption\" color=\"textSecondary\">\n {finding.file}\n {finding.lines && `:${finding.lines}`}\n </Typography>\n </Box>\n </AccordionSummary>\n <AccordionDetails>\n <Box>\n <Typography variant=\"body2\" paragraph>\n {finding.description}\n </Typography>\n {finding.avdId && (\n <Box mt={1}>\n <Link to={finding.avdId} target=\"_blank\">\n View documentation →\n </Link>\n </Box>\n )}\n </Box>\n </AccordionDetails>\n </Accordion>\n ))}\n </Box>\n </InfoCard>\n );\n};\n","import { Config } from '@backstage/config';\n\nexport interface TrivyConfig {\n bucketName: string;\n severityFilter: string[];\n cacheTime: number;\n staleTime: number;\n}\n\nexport const DEFAULT_TRIVY_CONFIG: TrivyConfig = {\n bucketName: 'sanity-trivy-logs',\n severityFilter: ['CRITICAL', 'HIGH', 'MEDIUM'],\n cacheTime: 30,\n staleTime: 5,\n};\n\nexport function getTrivyConfig(config: Config): TrivyConfig {\n const trivyConfig = config.getOptionalConfig('trivy');\n\n return {\n bucketName: trivyConfig?.getOptionalString('bucketName') || DEFAULT_TRIVY_CONFIG.bucketName,\n severityFilter: trivyConfig?.getOptionalStringArray('severityFilter') || DEFAULT_TRIVY_CONFIG.severityFilter,\n cacheTime: trivyConfig?.getOptionalNumber('cacheTime') || DEFAULT_TRIVY_CONFIG.cacheTime,\n staleTime: trivyConfig?.getOptionalNumber('staleTime') || DEFAULT_TRIVY_CONFIG.staleTime,\n };\n}\n","import { useApi, configApiRef } from '@backstage/core-plugin-api';\nimport { useQuery, UseQueryOptions } from '@tanstack/react-query';\nimport { trivyApiRef, TrivyScanResult } from './TrivyApi';\nimport { getTrivyConfig } from './config';\n\nexport const useTrivyScans = (options?: Omit<UseQueryOptions<TrivyScanResult[]>, 'queryKey' | 'queryFn'>) => {\n const trivyApi = useApi(trivyApiRef);\n const configApi = useApi(configApiRef);\n const config = getTrivyConfig(configApi);\n\n return useQuery({\n queryKey: ['trivy', 'scans'],\n queryFn: () => trivyApi.getScans(),\n staleTime: config.staleTime * 60 * 1000,\n gcTime: config.cacheTime * 60 * 1000,\n ...options,\n });\n};\n\nexport const useTrivyScan = (\n repo: string,\n branch: string,\n scanId: string,\n options?: Omit<UseQueryOptions<TrivyScanResult>, 'queryKey' | 'queryFn'>,\n) => {\n const trivyApi = useApi(trivyApiRef);\n const configApi = useApi(configApiRef);\n const config = getTrivyConfig(configApi);\n\n return useQuery({\n queryKey: ['trivy', 'scan', repo, branch, scanId],\n queryFn: () => trivyApi.getScan(repo, branch, scanId),\n staleTime: config.staleTime * 60 * 1000,\n gcTime: config.cacheTime * 60 * 1000,\n enabled: !!(repo && branch && scanId),\n ...options,\n });\n};\n\nexport const useTrivyLatestScan = (\n repo: string | undefined,\n options?: Omit<UseQueryOptions<TrivyScanResult>, 'queryKey' | 'queryFn'>,\n) => {\n const trivyApi = useApi(trivyApiRef);\n const configApi = useApi(configApiRef);\n const config = getTrivyConfig(configApi);\n\n return useQuery({\n queryKey: ['trivy', 'latest', repo],\n queryFn: () => trivyApi.getLatestScan(repo!),\n staleTime: config.staleTime * 60 * 1000,\n gcTime: config.cacheTime * 60 * 1000,\n enabled: !!repo,\n ...options,\n });\n};\n","import React from 'react';\nimport { Progress, Link } from '@backstage/core-components';\nimport { useApi, configApiRef } from '@backstage/core-plugin-api';\nimport { useEntity } from '@backstage/plugin-catalog-react';\nimport { useTrivyLatestScan } from '../../api/hooks';\nimport { getTrivyConfig } from '../../api/config';\nimport Alert from '@material-ui/lab/Alert';\nimport { TrivyFindingsCard } from '../TrivyFindingsCard';\nimport { Box, Typography } from '@material-ui/core';\n\nexport const EntityTrivyCard = () => {\n const { entity } = useEntity();\n const configApi = useApi(configApiRef);\n const config = getTrivyConfig(configApi);\n\n // Extract repo name from entity annotations\n const repoAnnotation =\n entity.metadata.annotations?.['github.com/project-slug'] ||\n entity.metadata.annotations?.['backstage.io/source-location'];\n\n const repoName = repoAnnotation?.split('/').pop()?.replace(/^url:.*\\//, '');\n\n const { data, isLoading, error } = useTrivyLatestScan(repoName);\n\n if (!repoName) {\n return (\n <Alert severity=\"info\">\n No repository configured for this entity. Add a{' '}\n <code>github.com/project-slug</code> annotation to enable security scanning.\n </Alert>\n );\n }\n\n if (isLoading) {\n return <Progress />;\n }\n\n if (error) {\n return (\n <Alert severity=\"warning\">\n No security scans found for repository: {repoName}\n </Alert>\n );\n }\n\n if (!data) {\n return (\n <Alert severity=\"info\">\n No security scans available for {repoName}\n </Alert>\n );\n }\n\n // Filter findings by configured severity levels\n const filteredFindings = data.findings.filter(finding =>\n config.severityFilter.includes(finding.severity),\n );\n\n return (\n <Box>\n <TrivyFindingsCard findings={filteredFindings} title=\"Latest Security Scan\" />\n <Box mt={2} display=\"flex\" justifyContent=\"space-between\" alignItems=\"center\">\n <Typography variant=\"caption\" color=\"textSecondary\">\n Scanned: {new Date(data.timestamp).toLocaleString()} | Showing: {config.severityFilter.join(', ')}\n </Typography>\n <Box display=\"flex\" style={{ gap: 8 }}>\n {data.metadata.githubPr && (\n <Link to={data.metadata.githubPr} target=\"_blank\">\n View PR\n </Link>\n )}\n {data.metadata.circleciUrl && (\n <Link to={data.metadata.circleciUrl} target=\"_blank\">\n View Build\n </Link>\n )}\n </Box>\n </Box>\n </Box>\n );\n};\n","import React from 'react';\nimport { Progress, Link } from '@backstage/core-components';\nimport { useApi, configApiRef } from '@backstage/core-plugin-api';\nimport { TrivyFindingsCard } from '../TrivyFindingsCard';\nimport { useTrivyLatestScan } from '../../api/hooks';\nimport { getTrivyConfig } from '../../api/config';\nimport { Box, Typography } from '@material-ui/core';\nimport Alert from '@material-ui/lab/Alert';\n\ninterface StandaloneTrivyCardProps {\n /** Repository identifier (e.g., \"alert-relay\" or \"sanity-io/alert-relay\") */\n repository: string;\n /** Optional title override */\n title?: string;\n}\n\nexport const StandaloneTrivyCard = ({ repository, title }: StandaloneTrivyCardProps) => {\n const configApi = useApi(configApiRef);\n const config = getTrivyConfig(configApi);\n\n // Extract just the repo name if full slug provided\n const repoName = repository.split('/').pop() || repository;\n\n const { data, isLoading, error } = useTrivyLatestScan(repoName);\n\n if (isLoading) {\n return <Progress />;\n }\n\n if (error) {\n return (\n <Alert severity=\"warning\">\n No security scans found for repository: {repoName}\n </Alert>\n );\n }\n\n if (!data) {\n return (\n <Alert severity=\"info\">\n No security scans available for {repoName}\n </Alert>\n );\n }\n\n // Filter findings by configured severity levels\n const filteredFindings = data.findings.filter(finding =>\n config.severityFilter.includes(finding.severity),\n );\n\n return (\n <Box>\n <TrivyFindingsCard\n findings={filteredFindings}\n title={title || `Security Scan - ${repoName}`}\n />\n <Box mt={2} display=\"flex\" justifyContent=\"space-between\" alignItems=\"center\">\n <Typography variant=\"caption\" color=\"textSecondary\">\n Scanned: {new Date(data.timestamp).toLocaleString()}\n </Typography>\n <Box display=\"flex\" style={{ gap: 8 }}>\n {data.metadata.githubPr && (\n <Link to={data.metadata.githubPr} target=\"_blank\">\n View PR\n </Link>\n )}\n {data.metadata.circleciUrl && (\n <Link to={data.metadata.circleciUrl} target=\"_blank\">\n View Build\n </Link>\n )}\n </Box>\n </Box>\n </Box>\n );\n};\n","import React from 'react';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';\nimport { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';\n\nconst queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n gcTime: 1000 * 60 * 30, // 30 minutes\n staleTime: 1000 * 60 * 5, // 5 minutes\n retry: 1,\n refetchOnWindowFocus: false,\n },\n },\n});\n\nconst persister = createSyncStoragePersister({\n storage: window.localStorage,\n key: 'trivy-query-cache',\n});\n\nexport const TrivyQueryProvider = ({ children }: { children: React.ReactNode }) => {\n return (\n <PersistQueryClientProvider client={queryClient} persistOptions={{ persister }}>\n {children}\n </PersistQueryClientProvider>\n );\n};\n\n// For simpler usage without persistence\nexport const SimpleTrivyQueryProvider = ({ children }: { children: React.ReactNode }) => {\n return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;\n};\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sanity-labs/backstage-plugin-trivy",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "dist/index.esm.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"backstage": {
|
|
11
|
+
"role": "frontend-plugin"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@backstage/config": "^1.1.1",
|
|
15
|
+
"@backstage/core-components": "^0.13.10",
|
|
16
|
+
"@backstage/core-plugin-api": "^1.8.2",
|
|
17
|
+
"@backstage/plugin-catalog-react": "^1.9.3",
|
|
18
|
+
"@backstage/theme": "^0.5.0",
|
|
19
|
+
"@material-ui/core": "^4.12.4",
|
|
20
|
+
"@material-ui/icons": "^4.11.3",
|
|
21
|
+
"@material-ui/lab": "^4.0.0-alpha.61",
|
|
22
|
+
"@tanstack/query-sync-storage-persister": "^5.85.5",
|
|
23
|
+
"@tanstack/react-query": "^5.85.5",
|
|
24
|
+
"@tanstack/react-query-persist-client": "^5.85.5",
|
|
25
|
+
"react": "^18.0.2",
|
|
26
|
+
"react-dom": "^18.0.2",
|
|
27
|
+
"react-router-dom": "^6.3.0",
|
|
28
|
+
"react-use": "^17.2.4"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@backstage/cli": "^0.25.0",
|
|
32
|
+
"@backstage/core-app-api": "^1.11.3",
|
|
33
|
+
"@backstage/dev-utils": "^1.0.28",
|
|
34
|
+
"@backstage/test-utils": "^1.4.7",
|
|
35
|
+
"@testing-library/jest-dom": "^6.0.0",
|
|
36
|
+
"@testing-library/react": "^14.0.0",
|
|
37
|
+
"@testing-library/user-event": "^14.0.0",
|
|
38
|
+
"@types/jest": "^30.0.0",
|
|
39
|
+
"@types/node": "*",
|
|
40
|
+
"@types/react": "^18",
|
|
41
|
+
"@types/react-dom": "^18",
|
|
42
|
+
"@types/webpack-env": "^1.18.8"
|
|
43
|
+
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"start": "backstage-cli package start",
|
|
49
|
+
"build": "backstage-cli package build",
|
|
50
|
+
"lint": "backstage-cli package lint",
|
|
51
|
+
"test": "backstage-cli package test",
|
|
52
|
+
"clean": "backstage-cli package clean",
|
|
53
|
+
"tsc": "tsc --project tsconfig.json --declaration --emitDeclarationOnly --outDir dist-types/src"
|
|
54
|
+
}
|
|
55
|
+
}
|