Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vulnz report facelift #145

Merged
merged 11 commits into from
Mar 21, 2025
Merged
93 changes: 0 additions & 93 deletions src/lib/components/Image.svelte

This file was deleted.

2 changes: 1 addition & 1 deletion src/lib/components/Vulnerability.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
{:else if count === PendingValue}
<Skeleton variant="circle" width={size} height={size} />
{:else if count > 0}
<VulnerabilityBadge text={String(count)} color={severityToColor(severity)} {size} />
<VulnerabilityBadge text={String(count)} color={severityToColor({ severity: severity })} {size} />
{:else}
<code class="check">&check;</code>
{/if}
Expand Down
97 changes: 39 additions & 58 deletions src/lib/components/VulnerabilityBadges.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -26,48 +26,52 @@
const categories = ['critical', 'high', 'medium', 'low', 'unassigned'] as const;
</script>

<div class="vulnerability-summary">
<div class="wrapper">
<div class="vulnerability-summary">
{#if summary !== PendingValue}
{#each categories as category (category)}
<BodyShort
class="vulnerability-count"
style="background-color: {severityToColor({ severity: category })}"
>
{summary[category]}
</BodyShort>
<div class="category">{category}</div>
{/each}
{:else}
<Loader />
{/if}
</div>

{#if summary !== PendingValue}
{#each categories as category (category)}
<BodyShort class="vulnerability-count" style="background-color: {severityToColor(category)}">
{summary[category]}
</BodyShort>
<div class="category">{category}</div>
{/each}
{:else}
<Loader />
{/if}
</div>
{#if summary !== PendingValue}
<div class="container">
<dl>
<BodyShort>
{#if summary['riskScore']}
<dt>Risk score:</dt>
<dd>
<span class={summary['riskScore'] > 100 ? 'red' : 'green'}>{summary['riskScore']}</span>
</dd>
<!--HelpText title="Risk score"
>The risk score is a calculated value based on the severity of the vulnerabilities
discovered within the workloads. A higher risk score indicates a higher risk of
exploitation. Algorithms may vary, but a common approach is to assign a score based on the
severity of the vulnerabilities found. The Score is calculated: "((critical * 10) + (high *
5) + (medium * 3) + (low * 1) + (unassigned * 5))".
</HelpText-->
<strong>Risk score:</strong>
{#if summary['coverage']}
{summary['riskScore']}
{:else if summary['riskScore'] > 100}
<span class="red">{summary['riskScore']}</span> (above defined threshold of 100)
{:else}
<span class="green">{summary['riskScore']}</span>
{/if}
{/if}

</BodyShort>
<BodyShort>
{#if summary['coverage']}
<dt>Coverage:</dt>
<dd>
<span class={summary['coverage'] < 100 ? 'red' : 'green'}
>{percentageFormatter(summary['coverage'] ? summary['coverage'] : 0, 0)}</span
>
</dd>
<strong>Coverage:</strong>
<span class={summary['coverage'] < 100 ? 'red' : 'green'}
>{percentageFormatter(summary['coverage'] ? summary['coverage'] : 0, 0)}</span
>
{/if}
</dl>
</div>
{/if}
</BodyShort>
{/if}
</div>

<style>
.wrapper {
display: grid;
row-gap: var(--a-spacing-2);
}
.category {
text-transform: capitalize;
}
Expand Down Expand Up @@ -95,34 +99,11 @@
}
}

.container {
display: flex;
gap: 0.5rem;
justify-content: space-between;
}

.red {
color: var(--a-surface-danger);
}

.green {
color: var(--a-surface-success);
}

dl {
display: grid;
grid-template-columns: 90px auto;
margin-block-start: 0;
margin-block-end: 0;
padding-top: var(--a-spacing-4);
}

dt {
font-weight: bold;
text-align: left;
}

dd {
margin: 0;
}
</style>
49 changes: 49 additions & 0 deletions src/lib/components/WorkloadDeploy.svelte
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<script lang="ts">
import { fragment, graphql, type WorkloadDeploy } from '$houdini';
import Time from '$lib/Time.svelte';
import { getImageDisplayName } from '$lib/utils/image';
import { isValidSha } from '$lib/utils/isValidSha';
import { BodyShort, Heading, Link, Tag } from '@nais/ds-svelte-community';
import { ExternalLinkIcon } from '@nais/ds-svelte-community/icons';
import WorkloadLink from './WorkloadLink.svelte';

interface Props {
workload: WorkloadDeploy;
Expand All @@ -17,7 +19,29 @@
graphql(`
fragment WorkloadDeploy on Workload {
__typename
id
name
image {
name
tag
workloadReferences {
nodes {
workload {
id
__typename
team {
slug
}
teamEnvironment {
environment {
name
}
}
name
}
}
}
}
deployments(first: 1) {
nodes {
deployerUsername
Expand All @@ -40,6 +64,12 @@
let deploymentInfo = $derived(
$data.deployments.nodes.length > 0 ? $data.deployments.nodes[0] : null
);

const relatedWorkloads = $derived(
$data.image.workloadReferences.nodes
.map((node) => node.workload)
.filter((workload) => workload.id !== $data.id)
);
</script>

<div class="wrapper">
Expand Down Expand Up @@ -69,6 +99,25 @@
<BodyShort>No deployment metadata found for workload.</BodyShort>
{/if}
</div>
<div class="wrapper">
<Heading level="3" size="small">Image</Heading>
{#if $data.image.name.startsWith('europe-north1-docker.pkg.dev')}
<a href="https://{$data.image.name + ':' + $data.image.tag}">
<span
>{getImageDisplayName($data.image.name)}:{$data.image.tag}
<ExternalLinkIcon /></span
>
</a>
{:else}
{$data.image.name}:{$data.image.tag}
{/if}
{#if relatedWorkloads.length > 0}
<Heading level="4" size="xsmall">Other workloads using this image</Heading>
{#each relatedWorkloads as workload (workload.id)}
<WorkloadLink {workload} />
{/each}
{/if}
</div>

<style>
.wrapper {
Expand Down
67 changes: 67 additions & 0 deletions src/lib/components/WorkloadVulnerabilitySummary.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<script lang="ts">
import { docURL } from '$lib/doc';
import WarningIcon from '$lib/icons/WarningIcon.svelte';
import { BodyShort, Link } from '@nais/ds-svelte-community';
import VulnerabilityBadges from './VulnerabilityBadges.svelte';

interface Props {
showReportLink?: boolean;
workload: {
__typename: string | null;
name: string;
team: {
slug: string;
};
teamEnvironment: {
environment: {
name: string;
};
};
image: {
hasSBOM: boolean;
vulnerabilitySummary: {
critical: number;
high: number;
medium: number;
low: number;
unassigned: number;
riskScore: number;
} | null;
};
};
}

const { workload, showReportLink = true }: Props = $props();

const { image } = workload;

const imageDetailsUrl = $derived(
`/team/${workload.team.slug}/${workload.teamEnvironment.environment.name}/${workload.__typename === 'Application' ? 'app' : 'job'}/${workload.name}/vulnerability-report`
);
</script>

<div class="wrapper">
{#if image.hasSBOM && image.vulnerabilitySummary}
<VulnerabilityBadges summary={image.vulnerabilitySummary} />
{#if showReportLink}
<Link href={imageDetailsUrl}>View vulnerability report</Link>
{/if}
{:else}
<BodyShort>
<WarningIcon class="text-aligned-icon" /> No vulnerability data available. Learn how to generate
SBOMs and attestations for your workloads in the
<a href={docURL('/services/vulnerabilities/how-to/sbom/')} target="_blank"
>Nais documentation
</a>.
</BodyShort>
{/if}
</div>

<style>
.wrapper {
display: flex;
flex-direction: column;
gap: var(--a-spacing-1);
align-items: start;
}
</style>
Loading