Skip to content

Commit 15599e6

Browse files
rbjornstadandnorda
andauthored
Vulnz report facelift (#145)
* WIP * refactor: remove unused AdminUsersVariables code from admin page * feat: add isAdmin prop to menuItems for conditional rendering of settings and update related components * still WIP * feat: enhance severityToColor function to support text color differentiation * feat: add TeamVulnerabilitySummary component for displaying team vulnerability details * feat: add support for Ubuntu vulnerability URLs in detailsUrl function * WIP * feat: add vulnerability summary and SBOM support to image queries and components Co-authored-by: Andreas Nordahl <[email protected]> * refactor: simplify ErrorMessage component by removing collapsible prop and adjusting button visibility * refactor: update severityToColor function to accept an object and adjust usage in components --------- Co-authored-by: Andreas Nordahl <[email protected]>
1 parent 6fbd380 commit 15599e6

28 files changed

+581
-561
lines changed

src/lib/components/Image.svelte

-93
This file was deleted.

src/lib/components/Vulnerability.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
{:else if count === PendingValue}
1919
<Skeleton variant="circle" width={size} height={size} />
2020
{:else if count > 0}
21-
<VulnerabilityBadge text={String(count)} color={severityToColor(severity)} {size} />
21+
<VulnerabilityBadge text={String(count)} color={severityToColor({ severity: severity })} {size} />
2222
{:else}
2323
<code class="check">&check;</code>
2424
{/if}

src/lib/components/VulnerabilityBadges.svelte

+39-58
Original file line numberDiff line numberDiff line change
@@ -26,48 +26,52 @@
2626
const categories = ['critical', 'high', 'medium', 'low', 'unassigned'] as const;
2727
</script>
2828

29-
<div class="vulnerability-summary">
29+
<div class="wrapper">
30+
<div class="vulnerability-summary">
31+
{#if summary !== PendingValue}
32+
{#each categories as category (category)}
33+
<BodyShort
34+
class="vulnerability-count"
35+
style="background-color: {severityToColor({ severity: category })}"
36+
>
37+
{summary[category]}
38+
</BodyShort>
39+
<div class="category">{category}</div>
40+
{/each}
41+
{:else}
42+
<Loader />
43+
{/if}
44+
</div>
45+
3046
{#if summary !== PendingValue}
31-
{#each categories as category (category)}
32-
<BodyShort class="vulnerability-count" style="background-color: {severityToColor(category)}">
33-
{summary[category]}
34-
</BodyShort>
35-
<div class="category">{category}</div>
36-
{/each}
37-
{:else}
38-
<Loader />
39-
{/if}
40-
</div>
41-
{#if summary !== PendingValue}
42-
<div class="container">
43-
<dl>
47+
<BodyShort>
4448
{#if summary['riskScore']}
45-
<dt>Risk score:</dt>
46-
<dd>
47-
<span class={summary['riskScore'] > 100 ? 'red' : 'green'}>{summary['riskScore']}</span>
48-
</dd>
49-
<!--HelpText title="Risk score"
50-
>The risk score is a calculated value based on the severity of the vulnerabilities
51-
discovered within the workloads. A higher risk score indicates a higher risk of
52-
exploitation. Algorithms may vary, but a common approach is to assign a score based on the
53-
severity of the vulnerabilities found. The Score is calculated: "((critical * 10) + (high *
54-
5) + (medium * 3) + (low * 1) + (unassigned * 5))".
55-
</HelpText-->
49+
<strong>Risk score:</strong>
50+
{#if summary['coverage']}
51+
{summary['riskScore']}
52+
{:else if summary['riskScore'] > 100}
53+
<span class="red">{summary['riskScore']}</span> (above defined threshold of 100)
54+
{:else}
55+
<span class="green">{summary['riskScore']}</span>
56+
{/if}
5657
{/if}
57-
58+
</BodyShort>
59+
<BodyShort>
5860
{#if summary['coverage']}
59-
<dt>Coverage:</dt>
60-
<dd>
61-
<span class={summary['coverage'] < 100 ? 'red' : 'green'}
62-
>{percentageFormatter(summary['coverage'] ? summary['coverage'] : 0, 0)}</span
63-
>
64-
</dd>
61+
<strong>Coverage:</strong>
62+
<span class={summary['coverage'] < 100 ? 'red' : 'green'}
63+
>{percentageFormatter(summary['coverage'] ? summary['coverage'] : 0, 0)}</span
64+
>
6565
{/if}
66-
</dl>
67-
</div>
68-
{/if}
66+
</BodyShort>
67+
{/if}
68+
</div>
6969

7070
<style>
71+
.wrapper {
72+
display: grid;
73+
row-gap: var(--a-spacing-2);
74+
}
7175
.category {
7276
text-transform: capitalize;
7377
}
@@ -95,34 +99,11 @@
9599
}
96100
}
97101
98-
.container {
99-
display: flex;
100-
gap: 0.5rem;
101-
justify-content: space-between;
102-
}
103-
104102
.red {
105103
color: var(--a-surface-danger);
106104
}
107105
108106
.green {
109107
color: var(--a-surface-success);
110108
}
111-
112-
dl {
113-
display: grid;
114-
grid-template-columns: 90px auto;
115-
margin-block-start: 0;
116-
margin-block-end: 0;
117-
padding-top: var(--a-spacing-4);
118-
}
119-
120-
dt {
121-
font-weight: bold;
122-
text-align: left;
123-
}
124-
125-
dd {
126-
margin: 0;
127-
}
128109
</style>

src/lib/components/WorkloadDeploy.svelte

+49
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
<script lang="ts">
22
import { fragment, graphql, type WorkloadDeploy } from '$houdini';
33
import Time from '$lib/Time.svelte';
4+
import { getImageDisplayName } from '$lib/utils/image';
45
import { isValidSha } from '$lib/utils/isValidSha';
56
import { BodyShort, Heading, Link, Tag } from '@nais/ds-svelte-community';
67
import { ExternalLinkIcon } from '@nais/ds-svelte-community/icons';
8+
import WorkloadLink from './WorkloadLink.svelte';
79
810
interface Props {
911
workload: WorkloadDeploy;
@@ -17,7 +19,29 @@
1719
graphql(`
1820
fragment WorkloadDeploy on Workload {
1921
__typename
22+
id
2023
name
24+
image {
25+
name
26+
tag
27+
workloadReferences {
28+
nodes {
29+
workload {
30+
id
31+
__typename
32+
team {
33+
slug
34+
}
35+
teamEnvironment {
36+
environment {
37+
name
38+
}
39+
}
40+
name
41+
}
42+
}
43+
}
44+
}
2145
deployments(first: 1) {
2246
nodes {
2347
deployerUsername
@@ -40,6 +64,12 @@
4064
let deploymentInfo = $derived(
4165
$data.deployments.nodes.length > 0 ? $data.deployments.nodes[0] : null
4266
);
67+
68+
const relatedWorkloads = $derived(
69+
$data.image.workloadReferences.nodes
70+
.map((node) => node.workload)
71+
.filter((workload) => workload.id !== $data.id)
72+
);
4373
</script>
4474

4575
<div class="wrapper">
@@ -69,6 +99,25 @@
6999
<BodyShort>No deployment metadata found for workload.</BodyShort>
70100
{/if}
71101
</div>
102+
<div class="wrapper">
103+
<Heading level="3" size="small">Image</Heading>
104+
{#if $data.image.name.startsWith('europe-north1-docker.pkg.dev')}
105+
<a href="https://{$data.image.name + ':' + $data.image.tag}">
106+
<span
107+
>{getImageDisplayName($data.image.name)}:{$data.image.tag}
108+
<ExternalLinkIcon /></span
109+
>
110+
</a>
111+
{:else}
112+
{$data.image.name}:{$data.image.tag}
113+
{/if}
114+
{#if relatedWorkloads.length > 0}
115+
<Heading level="4" size="xsmall">Other workloads using this image</Heading>
116+
{#each relatedWorkloads as workload (workload.id)}
117+
<WorkloadLink {workload} />
118+
{/each}
119+
{/if}
120+
</div>
72121

73122
<style>
74123
.wrapper {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<script lang="ts">
2+
import { docURL } from '$lib/doc';
3+
import WarningIcon from '$lib/icons/WarningIcon.svelte';
4+
import { BodyShort, Link } from '@nais/ds-svelte-community';
5+
import VulnerabilityBadges from './VulnerabilityBadges.svelte';
6+
7+
interface Props {
8+
showReportLink?: boolean;
9+
workload: {
10+
__typename: string | null;
11+
name: string;
12+
team: {
13+
slug: string;
14+
};
15+
teamEnvironment: {
16+
environment: {
17+
name: string;
18+
};
19+
};
20+
image: {
21+
hasSBOM: boolean;
22+
vulnerabilitySummary: {
23+
critical: number;
24+
high: number;
25+
medium: number;
26+
low: number;
27+
unassigned: number;
28+
riskScore: number;
29+
} | null;
30+
};
31+
};
32+
}
33+
34+
const { workload, showReportLink = true }: Props = $props();
35+
36+
const { image } = workload;
37+
38+
const imageDetailsUrl = $derived(
39+
`/team/${workload.team.slug}/${workload.teamEnvironment.environment.name}/${workload.__typename === 'Application' ? 'app' : 'job'}/${workload.name}/vulnerability-report`
40+
);
41+
</script>
42+
43+
<div class="wrapper">
44+
{#if image.hasSBOM && image.vulnerabilitySummary}
45+
<VulnerabilityBadges summary={image.vulnerabilitySummary} />
46+
{#if showReportLink}
47+
<Link href={imageDetailsUrl}>View vulnerability report</Link>
48+
{/if}
49+
{:else}
50+
<BodyShort>
51+
<WarningIcon class="text-aligned-icon" /> No vulnerability data available. Learn how to generate
52+
SBOMs and attestations for your workloads in the
53+
<a href={docURL('/services/vulnerabilities/how-to/sbom/')} target="_blank"
54+
>Nais documentation
55+
</a>.
56+
</BodyShort>
57+
{/if}
58+
</div>
59+
60+
<style>
61+
.wrapper {
62+
display: flex;
63+
flex-direction: column;
64+
gap: var(--a-spacing-1);
65+
align-items: start;
66+
}
67+
</style>

0 commit comments

Comments
 (0)