Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@google-cloud/storage": "^8.0.1",
"@lucide/svelte": "^1.34.0",
"chart.js": "^4.5.1",
"marked": "^18.0.13",
"nodemailer": "^9.0.6",
"pino": "^10.3.1"
}
Expand Down
8 changes: 6 additions & 2 deletions src/lib/code-report/analysis-summary.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Best-effort summary for Trivy-style JSON reports (top-level `Results[]`, each entry may
// carry `Vulnerabilities`/`Secrets`/`Packages`). Other tool formats simply yield all-zero counts.
import { vulnerabilityUrl } from './vulnerability-format';

export type AnalysisSummary = {
vulnerabilities: {
critical: number;
Expand Down Expand Up @@ -156,9 +158,11 @@ export function extractVulnerabilities(result: unknown): VulnerabilityFinding[]
? Number((epss as Record<string, unknown>).Percentile)
: null;

const vulnerabilityId = String(vuln.VulnerabilityID || `${vuln.PkgName || 'unknown'}-${row.Target || ''}`);

return [
{
id: String(vuln.VulnerabilityID || `${vuln.PkgName || 'unknown'}-${row.Target || ''}`),
id: vulnerabilityId,
packageName: String(vuln.PkgName || 'Paquete desconocido'),
installedVersion: String(vuln.InstalledVersion || 'desconocida'),
fixedVersion: String(vuln.FixedVersion || ''),
Expand All @@ -179,7 +183,7 @@ export function extractVulnerabilities(result: unknown): VulnerabilityFinding[]
title: String(vuln.Title || 'Vulnerabilidad sin título'),
description: String(vuln.Description || ''),
primaryUrl: String(vuln.PrimaryURL || ''),
cveUrl: `https://nvd.nist.gov/vuln/detail/${String(vuln.VulnerabilityID || '')}`,
cveUrl: vulnerabilityUrl(vulnerabilityId, String(vuln.PrimaryURL || '')),
cvssScore: score,
cweIds: Array.isArray(vuln.CweIDs) ? vuln.CweIDs.map(String) : [],
references: Array.isArray(vuln.References) ? vuln.References.map(String) : [],
Expand Down
28 changes: 28 additions & 0 deletions src/lib/code-report/vulnerability-format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest';
import {
renderVulnerabilityDescription,
vulnerabilityUrl,
} from './vulnerability-format';

describe('vulnerability format', () => {
it('uses GitHub Advisories for GHSA identifiers and NVD for CVEs', () => {
expect(vulnerabilityUrl('GHSA-2x7j-588g-ccc2')).toBe(
'https://github.com/advisories/GHSA-2X7J-588G-CCC2',
);
expect(vulnerabilityUrl('CVE-2026-1234')).toBe(
'https://nvd.nist.gov/vuln/detail/CVE-2026-1234',
);
});

it('renders Markdown while escaping raw HTML and unsafe links', () => {
const rendered = renderVulnerabilityDescription(
'### Summary\n\n**Important** [safe](https://example.com) [unsafe](javascript:alert(1))\n\n<script>alert(1)</script>',
);

expect(rendered).toContain('<h3>Summary</h3>');
expect(rendered).toContain('<strong>Important</strong>');
expect(rendered).toContain('href="https://example.com"');
expect(rendered).not.toContain('<script>');
expect(rendered).not.toContain('javascript:');
});
});
41 changes: 41 additions & 0 deletions src/lib/code-report/vulnerability-format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { marked, Renderer } from 'marked';

function escapeHtml(value: string) {
return value
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}

function isSafeUrl(value: string) {
return /^(https?:|mailto:|#|\/)/i.test(value);
}

export function vulnerabilityUrl(id: string, primaryUrl = '') {
const normalizedId = id.trim().toUpperCase();
if (normalizedId.startsWith('GHSA-')) {
return `https://github.com/advisories/${normalizedId}`;
}
if (normalizedId.startsWith('CVE-')) {
return `https://nvd.nist.gov/vuln/detail/${normalizedId}`;
}
return primaryUrl;
}

export function vulnerabilitySourceLabel(id: string) {
return id.trim().toUpperCase().startsWith('GHSA-') ? 'GitHub Advisory' : 'NVD';
}

export function renderVulnerabilityDescription(value: string) {
const renderer = new Renderer();
renderer.html = ({ raw }) => escapeHtml(raw);
renderer.link = ({ href, title, text }) => {
if (!isSafeUrl(href)) return text;
const titleAttribute = title ? ` title="${escapeHtml(title)}"` : '';
return `<a href="${escapeHtml(href)}"${titleAttribute} target="_blank" rel="noreferrer noopener">${text}</a>`;
};

return marked.parse(value, { renderer, async: false });
}
76 changes: 74 additions & 2 deletions src/lib/components/code-report/CveDetailView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
TrendingUp,
Wrench,
} from '@lucide/svelte';
import {
renderVulnerabilityDescription,
vulnerabilitySourceLabel,
} from '$lib/code-report/vulnerability-format';

type CveDetail = {
id: string;
Expand Down Expand Up @@ -118,6 +122,7 @@
$: epssScorePercent = cve.epssScore !== null ? Math.min(100, cve.epssScore * 100) : 0;
$: epssPercentilePercent =
cve.epssPercentile !== null ? Math.min(100, cve.epssPercentile * 100) : 0;
$: renderedDescription = cve.description ? renderVulnerabilityDescription(cve.description) : '';

$: filteredAffectedServices = affectedServices.filter((service) => {
const query = affectedServicesQuery.trim().toLowerCase();
Expand Down Expand Up @@ -218,7 +223,11 @@

<section class="rounded-[28px] border border-slate-200 bg-white p-6 shadow-sm sm:p-8">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Overview</h2>
<p class="mt-2 text-sm text-slate-600">{cve.description || 'Sin descripcion disponible.'}</p>
{#if renderedDescription}
<div class="cve-description mt-2 text-sm text-slate-600">{@html renderedDescription}</div>
{:else}
<p class="mt-2 text-sm text-slate-600">Sin descripcion disponible.</p>
{/if}
{#if cve.lastModifiedDate}
<p class="mt-3 text-xs text-slate-400">
Ultima actualizacion: {new Date(cve.lastModifiedDate).toLocaleDateString()}
Expand Down Expand Up @@ -347,7 +356,7 @@
rel="noreferrer noopener"
class="inline-flex items-center gap-1.5 text-sm font-semibold text-blue-700 hover:text-blue-900"
>
<ExternalLink class="h-3.5 w-3.5" />NVD: {cve.id}
<ExternalLink class="h-3.5 w-3.5" />{vulnerabilitySourceLabel(cve.id)}: {cve.id}
</a>
</li>
{#if cve.primaryUrl}
Expand Down Expand Up @@ -505,3 +514,66 @@
</div>
{/if}
</div>

<style>
.cve-description :global(h1),
.cve-description :global(h2),
.cve-description :global(h3),
.cve-description :global(h4) {
margin-top: 1.25rem;
margin-bottom: 0.5rem;
color: rgb(15 23 42);
font-weight: 700;
}

.cve-description :global(h3:first-child) {
margin-top: 0;
}

.cve-description :global(p),
.cve-description :global(ul),
.cve-description :global(ol),
.cve-description :global(pre) {
margin-top: 0.75rem;
}

.cve-description :global(ul),
.cve-description :global(ol) {
padding-left: 1.5rem;
}

.cve-description :global(ul) {
list-style: disc;
}

.cve-description :global(ol) {
list-style: decimal;
}

.cve-description :global(a) {
color: rgb(29 78 216);
text-decoration: underline;
}

.cve-description :global(code) {
border-radius: 0.25rem;
background: rgb(51 65 85);
padding: 0.125rem 0.25rem;
color: rgb(241 245 249);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.875em;
}

.cve-description :global(pre) {
overflow-x: auto;
border-radius: 0.5rem;
background: rgb(15 23 42);
padding: 0.75rem;
color: rgb(241 245 249);
}

.cve-description :global(pre code) {
background: transparent;
padding: 0;
}
</style>
Loading