From d75c51c7363a3a2f53bff0209b67de5f054b741b Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 23 Aug 2026 00:03:21 +0200 Subject: [PATCH 01/15] analsys wip --- src/lib/code-report/analysis-summary.ts | 76 ++++++ .../services/[serviceSlug]/+page.svelte | 258 +++++++++++++++++- 2 files changed, 327 insertions(+), 7 deletions(-) diff --git a/src/lib/code-report/analysis-summary.ts b/src/lib/code-report/analysis-summary.ts index 9a92031..8d265d4 100644 --- a/src/lib/code-report/analysis-summary.ts +++ b/src/lib/code-report/analysis-summary.ts @@ -14,6 +14,27 @@ export type AnalysisSummary = { targetsScanned: number; }; +export type VulnerabilityFinding = { + id: string; + packageName: string; + installedVersion: string; + fixedVersion: string; + severity: 'critical' | 'high' | 'medium' | 'low' | 'unknown'; + status: string; + target: string; + packagePath: string; + packageIdentifier: string; + lineStart: number | null; + lineEnd: number | null; + codeSnippet: string; + title: string; + description: string; + primaryUrl: string; + cveUrl: string; + cvssScore: number | null; + cweIds: string[]; +}; + function emptySummary(): AnalysisSummary { return { vulnerabilities: { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 }, @@ -70,3 +91,58 @@ export function summarizeAnalysisResult(result: unknown): AnalysisSummary { return summary; } + +export function extractVulnerabilities(result: unknown): VulnerabilityFinding[] { + if (!result || typeof result !== 'object') return []; + + const results = (result as Record).Results; + if (!Array.isArray(results)) return []; + + return results.flatMap((entry) => { + if (!entry || typeof entry !== 'object') return []; + const row = entry as Record; + const vulnerabilities = Array.isArray(row.Vulnerabilities) ? row.Vulnerabilities : []; + + return vulnerabilities.flatMap((vulnerability) => { + if (!vulnerability || typeof vulnerability !== 'object') return []; + const vuln = vulnerability as Record; + const severity = String(vuln.Severity || '').toLowerCase(); + const cvss = vuln.CVSS; + const scores = cvss && typeof cvss === 'object' ? Object.values(cvss) : []; + const score = scores.reduce((highest, source) => { + if (!source || typeof source !== 'object') return highest; + const value = Number((source as Record).V3Score); + return Number.isFinite(value) && (highest === null || value > highest) ? value : highest; + }, null); + + return [ + { + id: String(vuln.VulnerabilityID || `${vuln.PkgName || 'unknown'}-${row.Target || ''}`), + packageName: String(vuln.PkgName || 'Paquete desconocido'), + installedVersion: String(vuln.InstalledVersion || 'desconocida'), + fixedVersion: String(vuln.FixedVersion || ''), + severity: + severity === 'critical' || + severity === 'high' || + severity === 'medium' || + severity === 'low' + ? severity + : 'unknown', + status: String(vuln.Status || 'unknown').toLowerCase(), + target: String(row.Target || 'Target no especificado'), + packagePath: String(vuln.PkgPath || row.Target || 'Ruta no especificada'), + packageIdentifier: String(vuln.PkgIdentifier || ''), + lineStart: Number.isFinite(Number(vuln.StartLine)) ? Number(vuln.StartLine) : null, + lineEnd: Number.isFinite(Number(vuln.EndLine)) ? Number(vuln.EndLine) : null, + codeSnippet: String(vuln.CodeSnippet || vuln.Snippet || ''), + 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 || '')}`, + cvssScore: score, + cweIds: Array.isArray(vuln.CweIDs) ? vuln.CweIDs.map(String) : [], + }, + ]; + }); + }); +} diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte index 2590700..9686b1e 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte @@ -9,12 +9,17 @@ Github, Gitlab, History, + Search, ShieldAlert, Trash2, Upload, X, } from 'lucide-svelte'; - import { summarizeAnalysisResult } from '$lib/code-report/analysis-summary'; + import { + extractVulnerabilities, + summarizeAnalysisResult, + type VulnerabilityFinding, + } from '$lib/code-report/analysis-summary'; type ServiceRow = { id: string; @@ -60,6 +65,53 @@ $: analysisSummary = data.latestAnalysis ? summarizeAnalysisResult(data.latestAnalysis.result) : null; + $: vulnerabilities = data.latestAnalysis + ? extractVulnerabilities(data.latestAnalysis.result) + : []; + + let vulnerabilityQuery = ''; + let severityFilter = 'all'; + let statusFilter = 'all'; + + const severityRank: Record = { critical: 4, high: 3, medium: 2, low: 1 }; + + $: filteredVulnerabilities = vulnerabilities + .filter((vulnerability) => { + const query = vulnerabilityQuery.trim().toLowerCase(); + const matchesQuery = + !query || + [ + vulnerability.id, + vulnerability.packageName, + vulnerability.target, + vulnerability.title, + ].some((value) => value.toLowerCase().includes(query)); + const matchesSeverity = severityFilter === 'all' || vulnerability.severity === severityFilter; + const matchesStatus = statusFilter === 'all' || vulnerability.status === statusFilter; + return matchesQuery && matchesSeverity && matchesStatus; + }) + .sort((a, b) => { + const severityDifference = (severityRank[b.severity] ?? 0) - (severityRank[a.severity] ?? 0); + return ( + severityDifference || (b.cvssScore ?? 0) - (a.cvssScore ?? 0) || a.id.localeCompare(b.id) + ); + }); + + $: statuses = [...new Set(vulnerabilities.map((vulnerability) => vulnerability.status))]; + + function findingStatus(vulnerability: VulnerabilityFinding) { + if (vulnerability.status === 'fixed' || vulnerability.fixedVersion) return 'Actualizar'; + if (vulnerability.status === 'will_not_fix') return 'Excepción'; + return vulnerability.status === 'unknown' ? 'Revisar' : 'Afectada'; + } + + function findingStatusClass(vulnerability: VulnerabilityFinding) { + if (vulnerability.status === 'fixed' || vulnerability.fixedVersion) { + return 'bg-emerald-50 text-emerald-700'; + } + if (vulnerability.status === 'will_not_fix') return 'bg-slate-100 text-slate-600'; + return 'bg-red-50 text-red-700'; + } $: repositoryUrl = data.latestAnalysis?.gitInfo?.repositoryUrl ?? null; @@ -83,6 +135,7 @@ high: 'bg-orange-50 text-orange-700 border-orange-200', medium: 'bg-amber-50 text-amber-700 border-amber-200', low: 'bg-slate-50 text-slate-600 border-slate-200', + unknown: 'bg-slate-50 text-slate-600 border-slate-200', }; let deleteModalOpen = false; @@ -344,13 +397,204 @@ {data.latestAnalysis.error} + {:else if vulnerabilities.length > 0} +
+
+ + + +
+ +
+ + Mostrando {filteredVulnerabilities.length} de + {vulnerabilities.length} CVEs + + +
+ + {#if filteredVulnerabilities.length === 0} +

+ No hay vulnerabilidades que coincidan con esos filtros. +

+ {:else} +
+ {#each filteredVulnerabilities as vulnerability (vulnerability.id + vulnerability.target)} +
+ +
+
+ + {vulnerability.severity} + + {vulnerability.id} +
+

{vulnerability.title}

+
+
+

+ {vulnerability.packageName} +

+

{vulnerability.target}

+
+ {vulnerability.installedVersion} + + {findingStatus(vulnerability)} + + + {vulnerability.cvssScore !== null + ? `CVSS ${vulnerability.cvssScore.toFixed(1)}` + : 'Sin CVSS'} + + + CVE ↗ + +
+ +
+
+
+

Por qué no cumple

+

+ {vulnerability.description || vulnerability.title} +

+
+

+ Ubicación detectada +

+

+ {vulnerability.packagePath} +

+ {#if vulnerability.packageIdentifier} +

+ Identificador: {vulnerability.packageIdentifier} +

+ {/if} + {#if vulnerability.lineStart !== null} +

+ Línea{vulnerability.lineEnd !== null && + vulnerability.lineEnd !== vulnerability.lineStart + ? `s ${vulnerability.lineStart}-${vulnerability.lineEnd}` + : ` ${vulnerability.lineStart}`} +

+ {:else} +

+ Este informe no incluye línea ni fragmento de código. +

+ {/if} + {#if vulnerability.codeSnippet} +
{vulnerability.codeSnippet}
+ {/if} +
+
+
+
+
Versión instalada
+
+ {vulnerability.installedVersion} +
+
+
+
Versión corregida
+
+ {vulnerability.fixedVersion || 'No indicada'} +
+
+
+
CWE
+
+ {vulnerability.cweIds.length > 0 + ? vulnerability.cweIds.join(', ') + : 'No indicado'} +
+
+
+
Estado Trivy
+
+ {vulnerability.status} +
+
+
+
+ +
+
+ {/each} +
+ {/if} +
{:else} -
{JSON.stringify(
-            data.latestAnalysis.result ?? {},
-            null,
-            2,
-          )}
+
+ No se han detectado vulnerabilidades en este análisis. +
{/if} {/if} From 9a376c3b8b74f00422c8710cf0d24dcc8350275c Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 23 Aug 2026 00:08:53 +0200 Subject: [PATCH 02/15] filter cves --- .../services/[serviceSlug]/+page.svelte | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte index 9686b1e..2294bad 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte @@ -71,33 +71,43 @@ let vulnerabilityQuery = ''; let severityFilter = 'all'; - let statusFilter = 'all'; const severityRank: Record = { critical: 4, high: 3, medium: 2, low: 1 }; - $: filteredVulnerabilities = vulnerabilities - .filter((vulnerability) => { - const query = vulnerabilityQuery.trim().toLowerCase(); - const matchesQuery = - !query || - [ - vulnerability.id, - vulnerability.packageName, - vulnerability.target, - vulnerability.title, - ].some((value) => value.toLowerCase().includes(query)); - const matchesSeverity = severityFilter === 'all' || vulnerability.severity === severityFilter; - const matchesStatus = statusFilter === 'all' || vulnerability.status === statusFilter; - return matchesQuery && matchesSeverity && matchesStatus; - }) - .sort((a, b) => { - const severityDifference = (severityRank[b.severity] ?? 0) - (severityRank[a.severity] ?? 0); - return ( - severityDifference || (b.cvssScore ?? 0) - (a.cvssScore ?? 0) || a.id.localeCompare(b.id) - ); - }); - - $: statuses = [...new Set(vulnerabilities.map((vulnerability) => vulnerability.status))]; + function filterVulnerabilities( + findings: VulnerabilityFinding[], + queryValue: string, + severityValue: string, + ) { + const query = queryValue.trim().toLowerCase(); + + return findings + .filter((vulnerability) => { + const matchesQuery = + !query || + [ + vulnerability.id, + vulnerability.packageName, + vulnerability.target, + vulnerability.title, + ].some((value) => value.toLowerCase().includes(query)); + const matchesSeverity = severityValue === 'all' || vulnerability.severity === severityValue; + return matchesQuery && matchesSeverity; + }) + .sort((a, b) => { + const severityDifference = + (severityRank[b.severity] ?? 0) - (severityRank[a.severity] ?? 0); + return ( + severityDifference || (b.cvssScore ?? 0) - (a.cvssScore ?? 0) || a.id.localeCompare(b.id) + ); + }); + } + + $: filteredVulnerabilities = filterVulnerabilities( + vulnerabilities, + vulnerabilityQuery, + severityFilter, + ); function findingStatus(vulnerability: VulnerabilityFinding) { if (vulnerability.status === 'fixed' || vulnerability.fixedVersion) return 'Actualizar'; @@ -420,16 +430,6 @@ -
@@ -446,7 +446,7 @@

{:else}
- {#each filteredVulnerabilities as vulnerability (vulnerability.id + vulnerability.target)} + {#each filteredVulnerabilities as vulnerability, index (vulnerability.id + vulnerability.target + vulnerability.packageName + vulnerability.installedVersion + index)}
Date: Sun, 23 Aug 2026 00:38:14 +0200 Subject: [PATCH 03/15] resumen vulnerabilidades --- bun.lock | 5 + package.json | 1 + .../services/[serviceSlug]/+page.server.ts | 11 +- .../services/[serviceSlug]/+page.svelte | 1096 ++++++++++++----- 4 files changed, 807 insertions(+), 306 deletions(-) diff --git a/bun.lock b/bun.lock index 911687d..91db460 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@aws-sdk/client-s3": "^3.500.0", "@getgitops/gitdb": "^0.2.0", "@google-cloud/storage": "^7.21.0", + "chart.js": "^4.5.1", "lucide-svelte": "^0.320.0", }, "devDependencies": { @@ -185,6 +186,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="], + "@nodable/entities": ["@nodable/entities@2.2.0", "", {}, "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg=="], "@oxc-project/types": ["@oxc-project/types@0.144.0", "", {}, "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg=="], @@ -443,6 +446,8 @@ "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="], + "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "code-red": ["code-red@1.0.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", "acorn": "^8.10.0", "estree-walker": "^3.0.3", "periscopic": "^3.1.0" } }, "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw=="], diff --git a/package.json b/package.json index a7ca65c..47d67ba 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@aws-sdk/client-s3": "^3.500.0", "@getgitops/gitdb": "^0.2.0", "@google-cloud/storage": "^7.21.0", + "chart.js": "^4.5.1", "lucide-svelte": "^0.320.0" } } diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts index c4f5054..a32a5d9 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts @@ -23,8 +23,15 @@ export async function load({ parent, params, locals }) { const service = await codeReportService.getByProjectAndSlug(project.id, params.serviceSlug); const analyses = await codeReportAnalysisService.listByService(service.id); const latestAnalysis = analyses[0] ?? null; - - return { service, latestAnalysis }; + const analysisHistory = analyses + .filter((analysis) => analysis.status === 'completed') + .map((analysis) => ({ + id: analysis.id, + createdAt: analysis.createdAt, + result: analysis.result, + })); + + return { service, latestAnalysis, analysisHistory }; } catch { throw error(404, 'Service not found'); } diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte index 2294bad..e697d06 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte @@ -9,6 +9,7 @@ Github, Gitlab, History, + Info, Search, ShieldAlert, Trash2, @@ -49,7 +50,22 @@ updatedAt: string; } | null; - export let data: { service: ServiceRow; latestAnalysis: AnalysisRow }; + type HistoryRow = { + id: string; + createdAt: string; + result: unknown; + }; + + type FileGroup = { + path: string; + vulnerabilities: VulnerabilityFinding[]; + }; + + export let data: { + service: ServiceRow; + latestAnalysis: AnalysisRow; + analysisHistory: HistoryRow[]; + }; export let form: { error?: string; uploadError?: string; @@ -65,14 +81,104 @@ $: analysisSummary = data.latestAnalysis ? summarizeAnalysisResult(data.latestAnalysis.result) : null; + $: historyPoints = data.analysisHistory + .slice() + .reverse() + .map((analysis) => ({ + date: new Date(analysis.createdAt).toLocaleDateString(), + summary: summarizeAnalysisResult(analysis.result), + })); + $: currentRiskScore = analysisSummary ? calculateRiskScore(analysisSummary) : 0; + $: currentRiskLevel = getRiskLevel(analysisSummary, currentRiskScore); $: vulnerabilities = data.latestAnalysis ? extractVulnerabilities(data.latestAnalysis.result) : []; + $: fileGroups = groupVulnerabilitiesByFile(vulnerabilities); + $: selectedFile = fileGroups.find((file) => file.path === selectedFilePath) ?? null; + let activeReportTab = 'summary'; + let riskInfoModalOpen = false; + let selectedFilePath = ''; + let fileQuery = ''; let vulnerabilityQuery = ''; let severityFilter = 'all'; + const riskWeights = { critical: 10, high: 6, medium: 3, low: 1 }; + + function calculateRiskScore(summary: NonNullable) { + return ( + summary.vulnerabilities.critical * riskWeights.critical + + summary.vulnerabilities.high * riskWeights.high + + summary.vulnerabilities.medium * riskWeights.medium + + summary.vulnerabilities.low * riskWeights.low + ); + } + + function getRiskLevel(summary: NonNullable | null, score: number) { + if (!summary || score === 0) return { label: 'Sin riesgo', className: 'safe' }; + if (summary.vulnerabilities.critical > 0 || score >= 40) { + return { label: 'Riesgo crítico', className: 'critical' }; + } + if (score >= 20) return { label: 'Riesgo alto', className: 'high' }; + if (score >= 8) return { label: 'Riesgo medio', className: 'medium' }; + return { label: 'Riesgo bajo', className: 'low' }; + } + + function setupRiskChart(canvas: HTMLCanvasElement) { + let disposed = false; + let chart: { destroy: () => void } | null = null; + + import('chart.js').then(({ Chart, registerables }) => { + if (disposed || historyPoints.length === 0) return; + Chart.register(...registerables); + + chart = new Chart(canvas, { + type: 'line', + data: { + labels: historyPoints.map((point) => point.date), + datasets: [ + { + label: 'Riesgo ponderado', + data: historyPoints.map((point) => calculateRiskScore(point.summary)), + borderColor: '#ef4444', + backgroundColor: 'rgba(239, 68, 68, 0.12)', + fill: true, + tension: 0.35, + pointRadius: 4, + pointHoverRadius: 6, + }, + { + label: 'Vulnerabilidades', + data: historyPoints.map((point) => point.summary.totalVulnerabilities), + borderColor: '#2457ff', + backgroundColor: 'transparent', + fill: false, + tension: 0.35, + pointRadius: 4, + pointHoverRadius: 6, + }, + ], + }, + options: { + responsive: true, + maintainAspectRatio: false, + interaction: { mode: 'index', intersect: false }, + plugins: { legend: { position: 'bottom' } }, + scales: { y: { beginAtZero: true, ticks: { precision: 0 } } }, + }, + }); + }); + + return { + destroy() { + disposed = true; + chart?.destroy(); + }, + }; + } + const severityRank: Record = { critical: 4, high: 3, medium: 2, low: 1 }; + const fileSeverityOrder = ['critical', 'high', 'medium', 'low'] as const; function filterVulnerabilities( findings: VulnerabilityFinding[], @@ -108,6 +214,39 @@ vulnerabilityQuery, severityFilter, ); + $: filteredFileGroups = fileGroups.filter((file) => + getFileName(file.path).toLowerCase().includes(fileQuery.trim().toLowerCase()), + ); + + function groupVulnerabilitiesByFile(findings: VulnerabilityFinding[]): FileGroup[] { + const groups = new Map(); + for (const finding of findings) { + const current = groups.get(finding.packagePath) ?? []; + groups.set(finding.packagePath, [...current, finding]); + } + + return [...groups.entries()] + .map(([path, groupedFindings]) => ({ + path, + vulnerabilities: filterVulnerabilities(groupedFindings, '', 'all'), + })) + .sort( + (a, b) => + b.vulnerabilities.length - a.vulnerabilities.length || a.path.localeCompare(b.path), + ); + } + + function getFileName(path: string) { + return path.split(/[\\/]/).pop() || path; + } + + function countSeverity(findings: VulnerabilityFinding[], severity: string) { + return findings.filter((finding) => finding.severity === severity).length; + } + + function severityLabel(severity: string) { + return severity.charAt(0).toUpperCase() + severity.slice(1); + } function findingStatus(vulnerability: VulnerabilityFinding) { if (vulnerability.status === 'fixed' || vulnerability.fixedVersion) return 'Actualizar'; @@ -262,343 +401,692 @@
-
-
-

{data.service.name}

-

- Slug: - {data.service.slug} -

- {#if data.service.description} -

Description

-

- {data.service.description} -

- {/if} - {#if data.service.tags.length > 0} -
- {#each data.service.tags as tag} - - {tag} - - {/each} -
- {/if} - - {#if repositoryUrl && repositoryIcon} - - - - {/if} -
+
+ + + +
-
-

- - Vulnerabilidades detectadas -

- {#if analysisSummary} -
-
-

{analysisSummary.vulnerabilities.critical}

-

Critical

-
-
-

{analysisSummary.vulnerabilities.high}

-

High

-
-
-

{analysisSummary.vulnerabilities.medium}

-

Medium

+ {#if activeReportTab === 'summary'} + {#if analysisSummary} +
+
+
+

+ Estado de seguridad +

+

{currentRiskLevel.label}

+

+ {analysisSummary.vulnerabilities.critical > 0 + ? 'Hay vulnerabilidades críticas que requieren atención prioritaria.' + : 'Puntuación calculada según la severidad de las vulnerabilidades detectadas.'} +

-
-

{analysisSummary.vulnerabilities.low}

-

Low

+
+

Riesgo

+

{currentRiskScore}

+

puntos ponderados

- {:else} -

Sin datos todavía.

- {/if} -
+ +
-
-

Resumen

- {#if analysisSummary} -
-
-
- Total vulnerabilidades -
-
- {analysisSummary.totalVulnerabilities} -
-
-
-
- Secretos expuestos -
-
{analysisSummary.exposedSecrets}
-
-
-
- Dependencias -
-
{analysisSummary.dependencies}
+
+
+
+
+

Evolución del riesgo

+

+ {historyPoints.length} análisis completado{historyPoints.length === 1 ? '' : 's'} +

+
+ + Riesgo + vulnerabilidades +
-
-
- Archivos analizados -
-
{analysisSummary.targetsScanned}
+
+ {#if historyPoints.length > 0} + + {:else} +
+ Todavía no hay historial suficiente para mostrar evolución. +
+ {/if}
-
- {:else} -

Sin datos todavía.

- {/if} -
-
- -
-

Último análisis

+
- {#if !data.latestAnalysis} -

Todavía no se ha ejecutado ningún análisis.

- {:else} -
- - {data.latestAnalysis.status} - - {data.latestAnalysis.tool} - - - {new Date(data.latestAnalysis.createdAt).toLocaleString()} - +
+

Indicadores clave

+
+
+

+ {analysisSummary.vulnerabilities.critical} +

+

+ Critical +

+
+
+

+ {analysisSummary.vulnerabilities.high} +

+

High

+
+
+

+ {analysisSummary.vulnerabilities.medium} +

+

+ Medium +

+
+
+

{analysisSummary.vulnerabilities.low}

+

Low

+
+
+
+
+
Dependencias
+
{analysisSummary.dependencies}
+
+
+
Archivos afectados
+
{fileGroups.length}
+
+
+
+ {/if} - {#if data.latestAnalysis.gitInfo} -
- - {#if data.latestAnalysis.gitInfo.repositoryUrl} - {data.latestAnalysis.gitInfo.repositoryUrl} - {/if} - {#if data.latestAnalysis.gitInfo.branch} - @ {data.latestAnalysis.gitInfo.branch} - {/if} - {#if data.latestAnalysis.gitInfo.commit} - {data.latestAnalysis.gitInfo.commit.slice(0, 7)} +
+
+

Servicio

+

{data.service.name}

+

{data.service.slug}

+ {#if data.service.description} +

+ {data.service.description} +

+ {/if} + {#if data.service.tags.length > 0} +
+ {#each data.service.tags as tag} + + {tag} + + {/each} +
+ {/if} +
+ +
+
+
+

Repositorio

+

+ {repositoryUrl ? 'Repositorio conectado' : 'Sin repositorio conectado'} +

+
+ {#if repositoryUrl && repositoryIcon} + {/if}
- {/if} + {#if repositoryUrl} + + {repositoryUrl} + + {:else} +

Este servicio no tiene repositorio configurado.

+ {/if} + {#if data.latestAnalysis?.gitInfo?.branch} +

+ Rama {data.latestAnalysis.gitInfo.branch} +

+ {/if} +
- {#if data.latestAnalysis.status === 'failed' && data.latestAnalysis.error} -
+

Ejecuciones

+

Histórico de ejecuciones

+

+ {data.analysisHistory.length} análisis completado{data.analysisHistory.length === 1 + ? '' + : 's'} +

+ {#if data.latestAnalysis} +

+ Último: {data.latestAnalysis.tool} + · + {new Date(data.latestAnalysis.createdAt).toLocaleString()} +

+ {/if} + - - {data.latestAnalysis.error} + Ver histórico completo ↗ + + +
+ {/if} + + {#if activeReportTab !== 'summary'} +
+

Último análisis

+ + {#if !data.latestAnalysis} +

Todavía no se ha ejecutado ningún análisis.

+ {:else} +
+ + {data.latestAnalysis.status} + + {data.latestAnalysis.tool} + + + {new Date(data.latestAnalysis.createdAt).toLocaleString()} +
- {:else if vulnerabilities.length > 0} -
-
- - -
-
- - Mostrando {filteredVulnerabilities.length} de - {vulnerabilities.length} CVEs - - + {#if data.latestAnalysis.gitInfo} +
+ + {#if data.latestAnalysis.gitInfo.repositoryUrl} + {data.latestAnalysis.gitInfo.repositoryUrl} + {/if} + {#if data.latestAnalysis.gitInfo.branch} + @ {data.latestAnalysis.gitInfo.branch} + {/if} + {#if data.latestAnalysis.gitInfo.commit} + {data.latestAnalysis.gitInfo.commit.slice(0, 7)} + {/if}
+ {/if} - {#if filteredVulnerabilities.length === 0} -

- No hay vulnerabilidades que coincidan con esos filtros. -

- {:else} -
- {#each filteredVulnerabilities as vulnerability, index (vulnerability.id + vulnerability.target + vulnerability.packageName + vulnerability.installedVersion + index)} -
- -
-
+ {#if activeReportTab === 'vulnerabilities'} + {#if data.latestAnalysis.status === 'failed' && data.latestAnalysis.error} +
+ + {data.latestAnalysis.error} +
+ {:else if vulnerabilities.length > 0} +
+
+ + +
+ +
+ + Mostrando {filteredVulnerabilities.length} + de + {vulnerabilities.length} CVEs + + +
+ + {#if filteredVulnerabilities.length === 0} +

+ No hay vulnerabilidades que coincidan con esos filtros. +

+ {:else} +
+ {#each filteredVulnerabilities as vulnerability, index (vulnerability.id + vulnerability.target + vulnerability.packageName + vulnerability.installedVersion + index)} +
+ +
+
+ + {vulnerability.severity} + + {vulnerability.id} +
+

{vulnerability.title}

+
+
+

+ {vulnerability.packageName} +

+

{vulnerability.target}

+
+ {vulnerability.installedVersion} - {vulnerability.severity} + {findingStatus(vulnerability)} + + + {vulnerability.cvssScore !== null + ? `CVSS ${vulnerability.cvssScore.toFixed(1)}` + : 'Sin CVSS'} - {vulnerability.id} -
-

{vulnerability.title}

-
-
-

- {vulnerability.packageName} -

-

{vulnerability.target}

-
- {vulnerability.installedVersion} - - {findingStatus(vulnerability)} - - - {vulnerability.cvssScore !== null - ? `CVSS ${vulnerability.cvssScore.toFixed(1)}` - : 'Sin CVSS'} - - - CVE ↗ - -
- -
-
-
-

Por qué no cumple

-

- {vulnerability.description || vulnerability.title} -

-
-

- Ubicación detectada -

-

- {vulnerability.packagePath} -

- {#if vulnerability.packageIdentifier} -

- Identificador: {vulnerability.packageIdentifier} -

- {/if} - {#if vulnerability.lineStart !== null} -

- Línea{vulnerability.lineEnd !== null && - vulnerability.lineEnd !== vulnerability.lineStart - ? `s ${vulnerability.lineStart}-${vulnerability.lineEnd}` - : ` ${vulnerability.lineStart}`} -

- {:else} -

- Este informe no incluye línea ni fragmento de código. + CVE ↗ + + + +

+
+
+

Por qué no cumple

+

+ {vulnerability.description || vulnerability.title}

- {/if} - {#if vulnerability.codeSnippet} -
{vulnerability.codeSnippet}
+
+

+ Ubicación detectada +

+

+ {vulnerability.packagePath} +

+ {#if vulnerability.packageIdentifier} +

+ Identificador: {vulnerability.packageIdentifier} +

+ {/if} + {#if vulnerability.lineStart !== null} +

+ Línea{vulnerability.lineEnd !== null && + vulnerability.lineEnd !== vulnerability.lineStart + ? `s ${vulnerability.lineStart}-${vulnerability.lineEnd}` + : ` ${vulnerability.lineStart}`} +

+ {:else} +

+ Este informe no incluye línea ni fragmento de código. +

+ {/if} + {#if vulnerability.codeSnippet} +
{vulnerability.codeSnippet}
+ {/if} +
+
+
+
+
Versión instalada
+
+ {vulnerability.installedVersion} +
+
+
+
Versión corregida
+
+ {vulnerability.fixedVersion || 'No indicada'} +
+
+
+
CWE
+
+ {vulnerability.cweIds.length > 0 + ? vulnerability.cweIds.join(', ') + : 'No indicado'} +
+
+
+
Estado Trivy
+
+ {vulnerability.status} +
+
+
+
+
-
-
-
Versión instalada
-
- {vulnerability.installedVersion} -
-
-
-
Versión corregida
-
- {vulnerability.fixedVersion || 'No indicada'} -
-
-
-
CWE
-
- {vulnerability.cweIds.length > 0 - ? vulnerability.cweIds.join(', ') - : 'No indicado'} -
-
-
-
Estado Trivy
-
- {vulnerability.status} -
-
-
-
- + {/if} +
+ {:else} +
+ No se han detectado vulnerabilidades en este análisis. +
+ {/if} + {:else if activeReportTab === 'files'} + {#if fileGroups.length === 0} +
+ No se han detectado vulnerabilidades asociadas a archivos. +
+ {:else} +
- {/each} + {:else} +
+

+ Archivo seleccionado +

+

+ {selectedFile.path} +

+

+ {selectedFile.vulnerabilities.length} vulnerabilidades detectadas en este archivo +

+
+
+ {#each selectedFile.vulnerabilities as vulnerability, vulnerabilityIndex (vulnerability.id + vulnerability.packageName + vulnerability.installedVersion + vulnerabilityIndex)} +
+
+
+ + {vulnerability.severity} + + + {vulnerability.id} + +
+ + Ver CVE ↗ + +
+

+ {vulnerability.packageName} +

+

+ {vulnerability.installedVersion} → {vulnerability.fixedVersion || + 'Sin versión corregida'} +

+

+ {vulnerability.description || vulnerability.title} +

+ {#if vulnerability.lineStart !== null || vulnerability.codeSnippet} +
+ {#if vulnerability.lineStart !== null} +

+ Línea{vulnerability.lineEnd !== null && + vulnerability.lineEnd !== vulnerability.lineStart + ? `s ${vulnerability.lineStart}-${vulnerability.lineEnd}` + : ` ${vulnerability.lineStart}`} +

+ {/if} + {#if vulnerability.codeSnippet} +
{vulnerability.codeSnippet}
+ {/if} +
+ {/if} +
+ {/each} +
+ {/if} +
{/if} + {/if} + {/if} + + {/if} +
+ +{#if riskInfoModalOpen} + +
+

High × 6

+

+ {analysisSummary?.vulnerabilities.high ?? 0} detectadas +

+
+
+

Medium × 3

+

+ {analysisSummary?.vulnerabilities.medium ?? 0} detectadas +

+
+
+

Low × 1

+

+ {analysisSummary?.vulnerabilities.low ?? 0} detectadas +

+
+ + +
+

Fórmula aplicada

+

+ (Critical × 10) + (High × 6) + (Medium × 3) + (Low × 1) = {currentRiskScore} puntos +

+

+ Riesgo bajo: 1-7 · medio: 8-19 · alto: 20-39 · crítico: 40+ o cualquier Critical. +

+
+ + +{/if} {#if deleteModalOpen}
From 9326ae07c28a07eec8e0349012d157083f614290 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 23 Aug 2026 01:23:21 +0200 Subject: [PATCH 04/15] history working --- .../components/CodeReportVisualization.svelte | 390 ++++++ .../code-report-analysis.service.ts | 11 + .../code-report/history/+page.server.ts | 30 + .../[slug]/code-report/history/+page.svelte | 88 ++ .../history/[analysisID]/+page.server.ts | 27 + .../history/[analysisID]/+page.svelte | 32 + .../services/[serviceSlug]/+page.server.ts | 6 + .../services/[serviceSlug]/+page.svelte | 1150 +---------------- 8 files changed, 652 insertions(+), 1082 deletions(-) create mode 100644 src/lib/components/CodeReportVisualization.svelte create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/history/+page.server.ts create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.server.ts create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte diff --git a/src/lib/components/CodeReportVisualization.svelte b/src/lib/components/CodeReportVisualization.svelte new file mode 100644 index 0000000..6d564ff --- /dev/null +++ b/src/lib/components/CodeReportVisualization.svelte @@ -0,0 +1,390 @@ + + +
+
+
+

{heading}

+ {#if analysis}
+ {analysis.status}{analysis.tool}{new Date(analysis.createdAt).toLocaleString()} +
{/if} +
+ {#if analysis?.gitInfo?.repositoryUrl}{@const ProviderIcon = providerIcon( + analysis.gitInfo.repositoryUrl, + )}Repositorio{/if} +
+
+ {#each [{ id: 'summary', label: 'Resumen' }, { id: 'vulnerabilities', label: 'Vulnerabilidades', count: vulnerabilities.length }, { id: 'files', label: 'Archivos', count: fileGroups.length }] as tab}{/each} +
+ {#if !analysis}
+ Todavía no se ha ejecutado ningún análisis. +
{:else if activeTab === 'summary'}
+
+
+

+ Estado de seguridad +

+

{riskLevel.label}

+

+ Puntuación calculada según la severidad de las vulnerabilidades detectadas. +

+
+
+

Riesgo

+

{riskScore}

+

puntos ponderados

+
+
+ +
+
+
+

Evolución del riesgo

+
+ {#if historyPoints.length > 0}{:else}
+ Todavía no hay historial suficiente. +
{/if} +
+
+
+

Indicadores clave

+
+ {#each severityKeys as severity}
+

{summary?.vulnerabilities[severity]}

+

+ {severity} +

+
{/each} +
+
+
+
Dependencias
+
{summary?.dependencies}
+
+
+
Archivos afectados
+
{fileGroups.length}
+
+
+
+
{:else if activeTab === 'vulnerabilities'}
+ {#if analysis.status === 'failed' && analysis.error}
+ {analysis.error} +
{:else if vulnerabilities.length === 0}

+ No se han detectado vulnerabilidades en este análisis. +

{:else}
+ +
+
+ {#each filteredVulnerabilities as finding}
+ {finding.severity}{finding.id}{finding.title}{findingStatus(finding)} +
+

{finding.description || finding.title}

+

{finding.packagePath}

+ {#if finding.codeSnippet}
{finding.codeSnippet}
{/if} +
+
{/each} +
{/if} +
{:else}
+ {#if filteredFileGroups.length === 0}

+ No se han detectado vulnerabilidades asociadas a archivos. +

{:else}
+
+ {#each filteredFileGroups as file}{/each} +
+
+ {#if selectedFile}

+ {selectedFile.path} +

+ {#each selectedFile.vulnerabilities as finding}
+

{finding.id}

+

{finding.title}

+
{/each}{:else}
+ Selecciona un archivo para inspeccionar. +
{/if} +
+
{/if} +
{/if} +
+{#if riskInfoModalOpen}{/if} diff --git a/src/modules/code-report/application/code-report-analysis.service.ts b/src/modules/code-report/application/code-report-analysis.service.ts index 4c3aadc..757447f 100644 --- a/src/modules/code-report/application/code-report-analysis.service.ts +++ b/src/modules/code-report/application/code-report-analysis.service.ts @@ -17,6 +17,17 @@ export class CodeReportAnalysisService { return analyses.map((analysis) => analysis.toJson()); } + async listByProject(serviceIds: string[]) { + const analyses = await Promise.all( + serviceIds.map((serviceId) => this.listByService(serviceId)), + ); + return analyses + .flat() + .sort( + (left, right) => new Date(right.createdAt).getTime() - new Date(left.createdAt).getTime(), + ); + } + async getById(id: string) { const analysis = await this.repository.findById(id); if (!analysis) { diff --git a/src/routes/org/[org]/projects/[slug]/code-report/history/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/history/+page.server.ts new file mode 100644 index 0000000..48bbe6c --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/history/+page.server.ts @@ -0,0 +1,30 @@ +import { error } from '@sveltejs/kit'; +import { cancanService } from '../../../../../../../modules/auth'; +import { + codeReportAnalysisService, + codeReportService, +} from '../../../../../../../modules/code-report'; + +export async function load({ parent, locals }) { + const { project } = await parent(); + if ( + !(await cancanService.canSessionUser(locals.user, 'openreport:read', { + scope: 'project', + projectId: project.id, + organizationId: project.organization?.id, + })) + ) + throw error(403, 'Forbidden'); + const services = await codeReportService.listByProject(project.id); + const analyses = await codeReportAnalysisService.listByProject( + services.map((service) => service.id), + ); + const serviceById = new Map(services.map((service) => [service.id, service])); + return { + services, + analyses: analyses.map((analysis) => ({ + ...analysis, + service: serviceById.get(analysis.serviceId), + })), + }; +} diff --git a/src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte new file mode 100644 index 0000000..24c4235 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte @@ -0,0 +1,88 @@ + + +Histórico de Code Report - GitVault Suite +
+ Volver a servicios +
+

Code Report

+

Histórico de análisis

+

+ Consulta todos los informes del proyecto y abre cualquier ejecución. +

+
+
+ +
+

+ {filteredAnalyses.length} informe{filteredAnalyses.length === 1 ? '' : 's'} +

+
+ {#if filteredAnalyses.length === 0}

+ No hay informes que coincidan con los filtros. +

{:else}{#each filteredAnalyses as analysis}
+ {analysis.service?.name ?? 'Servicio eliminado'}{analysis.status}{analysis.tool}{new Date(analysis.createdAt).toLocaleString()} +
{/each}{/if} +
+
diff --git a/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.server.ts new file mode 100644 index 0000000..286f164 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.server.ts @@ -0,0 +1,27 @@ +import { error } from '@sveltejs/kit'; +import { cancanService } from '../../../../../../../../modules/auth'; +import { + codeReportAnalysisService, + codeReportService, +} from '../../../../../../../../modules/code-report'; + +export async function load({ parent, params, locals }) { + const { project } = await parent(); + if ( + !(await cancanService.canSessionUser(locals.user, 'openreport:read', { + scope: 'project', + projectId: project.id, + organizationId: project.organization?.id, + })) + ) + throw error(403, 'Forbidden'); + const analysis = await codeReportAnalysisService.getById(params.analysisID); + const services = await codeReportService.listByProject(project.id); + const service = services.find((item) => item.id === analysis.serviceId); + if (!service) throw error(404, 'Analysis not found'); + return { + service, + analysis, + analysisHistory: await codeReportAnalysisService.listByService(service.id), + }; +} diff --git a/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte new file mode 100644 index 0000000..f359d13 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte @@ -0,0 +1,32 @@ + + +{data.service.name} - Histórico de Code Report +
+ Volver al histórico +
+

Informe consultado

+

{data.service.name}

+
+ {data.analysis.id}{data.analysis.tool}{new Date(data.analysis.createdAt).toLocaleString()}{data.analysis.status} +
+
+ +
diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts index a32a5d9..7af56a2 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts @@ -29,6 +29,12 @@ export async function load({ parent, params, locals }) { id: analysis.id, createdAt: analysis.createdAt, result: analysis.result, + summary: analysis.summary, + status: analysis.status, + tool: analysis.tool, + gitInfo: analysis.gitInfo, + error: analysis.error, + updatedAt: analysis.updatedAt, })); return { service, latestAnalysis, analysisHistory }; diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte index e697d06..d6d0805 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte @@ -1,70 +1,13 @@ - - {data.service.name} - Code Report - GitVault Suite - +{data.service.name} - Code Report - GitVault Suite
-
+
- - Volver a servicios + Volver a servicios - -
+
- - Ver histórico + Ver histórico
-
- - - -
- - {#if activeReportTab === 'summary'} - {#if analysisSummary} -
-
-
-

- Estado de seguridad -

-

{currentRiskLevel.label}

-

- {analysisSummary.vulnerabilities.critical > 0 - ? 'Hay vulnerabilidades críticas que requieren atención prioritaria.' - : 'Puntuación calculada según la severidad de las vulnerabilidades detectadas.'} -

-
-
-

Riesgo

-

{currentRiskScore}

-

puntos ponderados

-
-
- -
- -
-
-
-
-

Evolución del riesgo

-

- {historyPoints.length} análisis completado{historyPoints.length === 1 ? '' : 's'} -

-
- - Riesgo + vulnerabilidades - -
-
- {#if historyPoints.length > 0} - - {:else} -
- Todavía no hay historial suficiente para mostrar evolución. -
- {/if} -
-
- -
-

Indicadores clave

-
-
-

- {analysisSummary.vulnerabilities.critical} -

-

- Critical -

-
-
-

- {analysisSummary.vulnerabilities.high} -

-

High

-
-
-

- {analysisSummary.vulnerabilities.medium} -

-

- Medium -

-
-
-

{analysisSummary.vulnerabilities.low}

-

Low

-
-
-
-
-
Dependencias
-
{analysisSummary.dependencies}
-
-
-
Archivos afectados
-
{fileGroups.length}
-
-
-
-
- {/if} - -
-
-

Servicio

-

{data.service.name}

-

{data.service.slug}

- {#if data.service.description} -

- {data.service.description} -

- {/if} - {#if data.service.tags.length > 0} -
- {#each data.service.tags as tag} - - {tag} - - {/each} -
- {/if} -
- -
-
-
-

Repositorio

-

- {repositoryUrl ? 'Repositorio conectado' : 'Sin repositorio conectado'} -

-
- {#if repositoryUrl && repositoryIcon} - - {/if} -
- {#if repositoryUrl} - - {repositoryUrl} - - {:else} -

Este servicio no tiene repositorio configurado.

- {/if} - {#if data.latestAnalysis?.gitInfo?.branch} -

- Rama {data.latestAnalysis.gitInfo.branch} -

- {/if} -
- -
-

Ejecuciones

-

Histórico de ejecuciones

-

- {data.analysisHistory.length} análisis completado{data.analysisHistory.length === 1 - ? '' - : 's'} -

- {#if data.latestAnalysis} -

- Último: {data.latestAnalysis.tool} - · - {new Date(data.latestAnalysis.createdAt).toLocaleString()} -

- {/if} - - Ver histórico completo ↗ - -
-
- {/if} - - {#if activeReportTab !== 'summary'} -
-

Último análisis

- - {#if !data.latestAnalysis} -

Todavía no se ha ejecutado ningún análisis.

- {:else} -
- - {data.latestAnalysis.status} - - {data.latestAnalysis.tool} - - - {new Date(data.latestAnalysis.createdAt).toLocaleString()} - -
- - {#if data.latestAnalysis.gitInfo} -
- - {#if data.latestAnalysis.gitInfo.repositoryUrl} - {data.latestAnalysis.gitInfo.repositoryUrl} - {/if} - {#if data.latestAnalysis.gitInfo.branch} - @ {data.latestAnalysis.gitInfo.branch} - {/if} - {#if data.latestAnalysis.gitInfo.commit} - {data.latestAnalysis.gitInfo.commit.slice(0, 7)} - {/if} -
- {/if} - - {#if activeReportTab === 'vulnerabilities'} - {#if data.latestAnalysis.status === 'failed' && data.latestAnalysis.error} -
- - {data.latestAnalysis.error} -
- {:else if vulnerabilities.length > 0} -
-
- - -
- -
- - Mostrando {filteredVulnerabilities.length} - de - {vulnerabilities.length} CVEs - - -
- - {#if filteredVulnerabilities.length === 0} -

- No hay vulnerabilidades que coincidan con esos filtros. -

- {:else} -
- {#each filteredVulnerabilities as vulnerability, index (vulnerability.id + vulnerability.target + vulnerability.packageName + vulnerability.installedVersion + index)} -
- -
-
- - {vulnerability.severity} - - {vulnerability.id} -
-

{vulnerability.title}

-
-
-

- {vulnerability.packageName} -

-

{vulnerability.target}

-
- {vulnerability.installedVersion} - - {findingStatus(vulnerability)} - - - {vulnerability.cvssScore !== null - ? `CVSS ${vulnerability.cvssScore.toFixed(1)}` - : 'Sin CVSS'} - - - CVE ↗ - -
- -
-
-
-

Por qué no cumple

-

- {vulnerability.description || vulnerability.title} -

-
-

- Ubicación detectada -

-

- {vulnerability.packagePath} -

- {#if vulnerability.packageIdentifier} -

- Identificador: {vulnerability.packageIdentifier} -

- {/if} - {#if vulnerability.lineStart !== null} -

- Línea{vulnerability.lineEnd !== null && - vulnerability.lineEnd !== vulnerability.lineStart - ? `s ${vulnerability.lineStart}-${vulnerability.lineEnd}` - : ` ${vulnerability.lineStart}`} -

- {:else} -

- Este informe no incluye línea ni fragmento de código. -

- {/if} - {#if vulnerability.codeSnippet} -
{vulnerability.codeSnippet}
- {/if} -
-
-
-
-
Versión instalada
-
- {vulnerability.installedVersion} -
-
-
-
Versión corregida
-
- {vulnerability.fixedVersion || 'No indicada'} -
-
-
-
CWE
-
- {vulnerability.cweIds.length > 0 - ? vulnerability.cweIds.join(', ') - : 'No indicado'} -
-
-
-
Estado Trivy
-
- {vulnerability.status} -
-
-
-
- -
-
- {/each} -
- {/if} -
- {:else} -
- No se han detectado vulnerabilidades en este análisis. -
- {/if} - {:else if activeReportTab === 'files'} - {#if fileGroups.length === 0} -
- No se han detectado vulnerabilidades asociadas a archivos. -
- {:else} -
- - -
- {#if !selectedFile} -
-
-

- Selecciona un archivo para inspeccionar -

-

- Elige un archivo del listado para ver sus vulnerabilidades. -

-
-
- {:else} -
-

- Archivo seleccionado -

-

- {selectedFile.path} -

-

- {selectedFile.vulnerabilities.length} vulnerabilidades detectadas en este archivo -

-
-
- {#each selectedFile.vulnerabilities as vulnerability, vulnerabilityIndex (vulnerability.id + vulnerability.packageName + vulnerability.installedVersion + vulnerabilityIndex)} -
-
-
- - {vulnerability.severity} - - - {vulnerability.id} - -
- - Ver CVE ↗ - -
-

- {vulnerability.packageName} -

-

- {vulnerability.installedVersion} → {vulnerability.fixedVersion || - 'Sin versión corregida'} -

-

- {vulnerability.description || vulnerability.title} -

- {#if vulnerability.lineStart !== null || vulnerability.codeSnippet} -
- {#if vulnerability.lineStart !== null} -

- Línea{vulnerability.lineEnd !== null && - vulnerability.lineEnd !== vulnerability.lineStart - ? `s ${vulnerability.lineStart}-${vulnerability.lineEnd}` - : ` ${vulnerability.lineStart}`} -

- {/if} - {#if vulnerability.codeSnippet} -
{vulnerability.codeSnippet}
- {/if} -
- {/if} -
- {/each} -
- {/if} -
-
- {/if} - {/if} - {/if} -
- {/if} +
-{#if riskInfoModalOpen} - -{/if} - {#if deleteModalOpen}
-

Borrar servicio

- - -
-

- Vas a borrar {data.service.name} y - todos sus análisis. Esta acción no se - puede deshacer. + Vas a borrar {data.service.name} y todos sus análisis. Esta acción no se puede + deshacer.

- - {#if deleteError} -

{deleteError}

- {/if} - + {#if deleteError}

+ {deleteError} +

{/if}
- Cancelar - - {deleting ? 'Borrando...' : 'Borrar servicio y análisis'} -
@@ -1147,16 +151,11 @@
-

Subir análisis

- - -
-
- {#if uploadError} -

{uploadError}

- {/if} - + {#if uploadError}

+ {uploadError} +

{/if} - + /> - -
+
- - {uploading ? 'Subiendo...' : 'Subir análisis'} -
From 13f2805ec16f85b61eea4fb3f3f41698d984b50e Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 23 Aug 2026 01:31:21 +0200 Subject: [PATCH 05/15] fixes codeReport --- .../components/CodeReportVisualization.svelte | 466 ++++++++++++++---- .../history/[analysisID]/+page.svelte | 2 +- .../services/[serviceSlug]/+page.svelte | 2 +- 3 files changed, 385 insertions(+), 85 deletions(-) diff --git a/src/lib/components/CodeReportVisualization.svelte b/src/lib/components/CodeReportVisualization.svelte index 6d564ff..d092897 100644 --- a/src/lib/components/CodeReportVisualization.svelte +++ b/src/lib/components/CodeReportVisualization.svelte @@ -1,6 +1,6 @@
-
-
-

{heading}

- {#if analysis}
- {analysis.status}{analysis.tool}{new Date(analysis.createdAt).toLocaleString()} -
{/if} -
- {#if analysis?.gitInfo?.repositoryUrl}{@const ProviderIcon = providerIcon( - analysis.gitInfo.repositoryUrl, - )}Repositorio{/if} -
{#each [{ id: 'summary', label: 'Resumen' }, { id: 'vulnerabilities', label: 'Vulnerabilidades', count: vulnerabilities.length }, { id: 'files', label: 'Archivos', count: fileGroups.length }] as tab} +
+
+

+ Información del servicio +

+ {#if service} +

{service.name}

+

{service.slug}

+ {#if service.description} +

{service.description}

+ {/if} + {#if service.tags && service.tags.length > 0} +
+ {#each service.tags as tag} + + {tag} + + {/each} +
+ {/if} + {:else} +

Información no disponible.

+ {/if} +
+ +
+
+
+

+ Información del repositorio +

+

+ {analysis?.gitInfo?.repositoryUrl + ? 'Repositorio conectado' + : 'Sin repositorio conectado'} +

+
+ {#if analysis?.gitInfo?.repositoryUrl} + {@const ProviderIcon = providerIcon(analysis.gitInfo.repositoryUrl)} + + {/if} +
+ {#if analysis?.gitInfo?.repositoryUrl} + + {analysis.gitInfo.repositoryUrl} + + {:else} +

+ Este análisis no incluye un repositorio configurado. +

+ {/if} + {#if analysis?.gitInfo?.branch} +

+ Rama {analysis.gitInfo.branch} +

+ {/if} +
+ +
+

+ Histórico de ejecuciones +

+

+ {analysisHistory.length} análisis completado{analysisHistory.length === 1 ? '' : 's'} +

+ {#if analysis} +

+ Último: {analysis.tool} + ·{new Date(analysis.createdAt).toLocaleString()} +

+ {/if} +
+

Evolución del riesgo

@@ -305,23 +367,113 @@ >
- {#each filteredVulnerabilities as finding}
- + + {finding.severity}{finding.id}{finding.title}{findingStatus(finding)} + > + {finding.severity} + + {finding.id} + {finding.title} + {findingStatus(finding)} + + {finding.cvssScore !== null ? `CVSS ${finding.cvssScore.toFixed(1)}` : 'Sin CVSS'} + + + CVE ↗ + +

{finding.description || finding.title}

-

{finding.packagePath}

- {#if finding.codeSnippet}
{finding.codeSnippet}
{/if} +
+
+

+ Ubicación detectada +

+

+ {finding.packagePath} +

+ {#if finding.packageIdentifier} +

+ Identificador: {finding.packageIdentifier} +

+ {/if} + {#if finding.lineStart !== null} +

+ Línea{finding.lineEnd !== null && finding.lineEnd !== finding.lineStart + ? `s ${finding.lineStart}-${finding.lineEnd}` + : ` ${finding.lineStart}`} +

+ {:else} +

+ Este informe no incluye línea ni fragmento de código. +

+ {/if} + {#if finding.codeSnippet} +
{finding.codeSnippet}
+ {/if} +
+
+
+
Paquete
+
{finding.packageName}
+
+
+
Estado Trivy
+
{finding.status}
+
+
+
Versión instalada
+
+ {finding.installedVersion} +
+
+
+
Versión corregida
+
+ {finding.fixedVersion || 'No indicada'} +
+
+
+
CWE
+
+ {finding.cweIds.length > 0 ? finding.cweIds.join(', ') : 'No indicado'} +
+
+
+
+
+ + Abrir {finding.id} en NVD ↗ + + {#if finding.primaryUrl} + + Ver advisory ↗ + + {/if} +
-
{/each} + + {/each}
{/if} {:else}
{#if filteredFileGroups.length === 0}

No se han detectado vulnerabilidades asociadas a archivos.

{:else}
-
- {#each filteredFileGroups as file}{/each} -
-
- {#if selectedFile}

- {selectedFile.path} -

- {#each selectedFile.vulnerabilities as finding}
+
+

Archivos

+

{fileGroups.length} archivos con hallazgos

+
+
+ {#each filteredFileGroups as file, index (file.path + index)} +
{/each}{:else}
- Selecciona un archivo para inspeccionar. -
{/if} +

+ {getFileName(file.path)} +

+
+ {#each fileSeverityOrder as severity} + {@const count = countSeverity(file.vulnerabilities, severity)} + {#if count > 0} + + {severityLabel(severity)} + {count} + + {/if} + {/each} +
+

+ {file.vulnerabilities.length} CVE{file.vulnerabilities.length === 1 ? '' : 's'} +

+ + {/each} +
+ +
+ {#if selectedFile} +
+

+ Archivo seleccionado +

+

+ {selectedFile.path} +

+

+ {selectedFile.vulnerabilities.length} vulnerabilidades detectadas en este archivo +

+
+
+ {#each selectedFile.vulnerabilities as finding, index (finding.id + finding.packageName + finding.installedVersion + index)} +
+
+
+ {finding.severity} + {finding.id} +
+ Ver CVE ↗ +
+

{finding.packageName}

+

+ {finding.installedVersion} → {finding.fixedVersion || 'Sin versión corregida'} +

+

+ {finding.description || finding.title} +

+
+ {#if finding.lineStart !== null} +

+ Línea{finding.lineEnd !== null && finding.lineEnd !== finding.lineStart + ? `s ${finding.lineStart}-${finding.lineEnd}` + : ` ${finding.lineStart}`} +

+ {:else} +

Este informe no incluye línea ni fragmento de código.

+ {/if} + {#if finding.codeSnippet}
{finding.codeSnippet}
{/if} +
+
+ CWE: {finding.cweIds.length > 0 + ? finding.cweIds.join(', ') + : 'No indicado'} + {#if finding.primaryUrl}Ver advisory ↗{/if} +
+
+ {/each} +
+ {:else} +
+
+

+ Selecciona un archivo para inspeccionar +

+

+ Elige un archivo del listado para ver sus vulnerabilidades. +

+
+
+ {/if}
{/if}
{/if}
-{#if riskInfoModalOpen}{/if} +
+{/if} diff --git a/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte index f359d13..9d0b6ad 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte @@ -25,8 +25,8 @@
diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte index d6d0805..bcfaa54 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte @@ -97,9 +97,9 @@
From 58b09207b1db80d9f3075860a2765c93e927a19b Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 23 Aug 2026 01:37:39 +0200 Subject: [PATCH 06/15] refactor components code report --- .../components/CodeReportVisualization.svelte | 505 ++---------------- .../code-report/CodeReportFiles.svelte | 134 +++++ .../code-report/CodeReportSummary.svelte | 149 ++++++ .../CodeReportVulnerabilities.svelte | 129 +++++ 4 files changed, 458 insertions(+), 459 deletions(-) create mode 100644 src/lib/components/code-report/CodeReportFiles.svelte create mode 100644 src/lib/components/code-report/CodeReportSummary.svelte create mode 100644 src/lib/components/code-report/CodeReportVulnerabilities.svelte diff --git a/src/lib/components/CodeReportVisualization.svelte b/src/lib/components/CodeReportVisualization.svelte index d092897..86617bb 100644 --- a/src/lib/components/CodeReportVisualization.svelte +++ b/src/lib/components/CodeReportVisualization.svelte @@ -1,11 +1,14 @@
@@ -192,429 +191,41 @@ class="rounded-2xl border border-slate-200 bg-white p-8 text-sm text-slate-600" > Todavía no se ha ejecutado ningún análisis. -
{:else if activeTab === 'summary'}
-
-
-

- Estado de seguridad -

-

{riskLevel.label}

-

- Puntuación calculada según la severidad de las vulnerabilidades detectadas. -

-
-
-

Riesgo

-

{riskScore}

-

puntos ponderados

-
-
- -
-
-
-

- Información del servicio -

- {#if service} -

{service.name}

-

{service.slug}

- {#if service.description} -

{service.description}

- {/if} - {#if service.tags && service.tags.length > 0} -
- {#each service.tags as tag} - - {tag} - - {/each} -
- {/if} - {:else} -

Información no disponible.

- {/if} -
- -
-
-
-

- Información del repositorio -

-

- {analysis?.gitInfo?.repositoryUrl - ? 'Repositorio conectado' - : 'Sin repositorio conectado'} -

-
- {#if analysis?.gitInfo?.repositoryUrl} - {@const ProviderIcon = providerIcon(analysis.gitInfo.repositoryUrl)} - - {/if} -
- {#if analysis?.gitInfo?.repositoryUrl} - - {analysis.gitInfo.repositoryUrl} - - {:else} -

- Este análisis no incluye un repositorio configurado. -

- {/if} - {#if analysis?.gitInfo?.branch} -

- Rama {analysis.gitInfo.branch} -

- {/if} -
- -
-

- Histórico de ejecuciones -

-

- {analysisHistory.length} análisis completado{analysisHistory.length === 1 ? '' : 's'} -

- {#if analysis} -

- Último: {analysis.tool} - ·{new Date(analysis.createdAt).toLocaleString()} -

- {/if} -
-
-
-
-

Evolución del riesgo

-
- {#if historyPoints.length > 0}{:else}
- Todavía no hay historial suficiente. -
{/if} -
-
-
-

Indicadores clave

-
- {#each severityKeys as severity}
-

{summary?.vulnerabilities[severity]}

-

- {severity} -

-
{/each} -
-
-
-
Dependencias
-
{summary?.dependencies}
-
-
-
Archivos afectados
-
{fileGroups.length}
-
-
-
-
{:else if activeTab === 'vulnerabilities'}
- {#if analysis.status === 'failed' && analysis.error}
- {analysis.error} -
{:else if vulnerabilities.length === 0}

- No se han detectado vulnerabilidades en este análisis. -

{:else}
- -
-
- {#each filteredVulnerabilities as finding, index (finding.id + finding.target + finding.packageName + index)} -
- - - {finding.severity} - - {finding.id} - {finding.title} - {findingStatus(finding)} - - {finding.cvssScore !== null ? `CVSS ${finding.cvssScore.toFixed(1)}` : 'Sin CVSS'} - - - CVE ↗ - - -
-

{finding.description || finding.title}

-
-
-

- Ubicación detectada -

-

- {finding.packagePath} -

- {#if finding.packageIdentifier} -

- Identificador: {finding.packageIdentifier} -

- {/if} - {#if finding.lineStart !== null} -

- Línea{finding.lineEnd !== null && finding.lineEnd !== finding.lineStart - ? `s ${finding.lineStart}-${finding.lineEnd}` - : ` ${finding.lineStart}`} -

- {:else} -

- Este informe no incluye línea ni fragmento de código. -

- {/if} - {#if finding.codeSnippet} -
{finding.codeSnippet}
- {/if} -
-
-
-
Paquete
-
{finding.packageName}
-
-
-
Estado Trivy
-
{finding.status}
-
-
-
Versión instalada
-
- {finding.installedVersion} -
-
-
-
Versión corregida
-
- {finding.fixedVersion || 'No indicada'} -
-
-
-
CWE
-
- {finding.cweIds.length > 0 ? finding.cweIds.join(', ') : 'No indicado'} -
-
-
-
-
- - Abrir {finding.id} en NVD ↗ - - {#if finding.primaryUrl} - - Ver advisory ↗ - - {/if} -
-
-
- {/each} -
{/if} -
{:else}
- {#if filteredFileGroups.length === 0}

- No se han detectado vulnerabilidades asociadas a archivos. -

{:else}
- -
- {#if selectedFile} -
-

- Archivo seleccionado -

-

- {selectedFile.path} -

-

- {selectedFile.vulnerabilities.length} vulnerabilidades detectadas en este archivo -

-
-
- {#each selectedFile.vulnerabilities as finding, index (finding.id + finding.packageName + finding.installedVersion + index)} -
-
-
- {finding.severity} - {finding.id} -
- Ver CVE ↗ -
-

{finding.packageName}

-

- {finding.installedVersion} → {finding.fixedVersion || 'Sin versión corregida'} -

-

- {finding.description || finding.title} -

-
- {#if finding.lineStart !== null} -

- Línea{finding.lineEnd !== null && finding.lineEnd !== finding.lineStart - ? `s ${finding.lineStart}-${finding.lineEnd}` - : ` ${finding.lineStart}`} -

- {:else} -

Este informe no incluye línea ni fragmento de código.

- {/if} - {#if finding.codeSnippet}
{finding.codeSnippet}
{/if} -
-
- CWE: {finding.cweIds.length > 0 - ? finding.cweIds.join(', ') - : 'No indicado'} - {#if finding.primaryUrl}Ver advisory ↗{/if} -
-
- {/each} -
- {:else} -
-
-

- Selecciona un archivo para inspeccionar -

-

- Elige un archivo del listado para ver sus vulnerabilidades. -

-
-
- {/if} -
-
{/if} -
{/if} +
{:else if activeTab === 'summary'} (riskInfoModalOpen = true)} + />{:else if activeTab === 'vulnerabilities'}{:else}{/if} -{#if riskInfoModalOpen} - -

La puntuación suma el peso de cada vulnerabilidad encontrada en el último análisis. Cuanto mayor sea el resultado, mayor es la prioridad de remediación.

-
-
-

Critical × 10

-

- {summary?.vulnerabilities.critical ?? 0} detectadas -

-
-
-

High × 6

-

- {summary?.vulnerabilities.high ?? 0} detectadas -

-
-
-

Medium × 3

-

- {summary?.vulnerabilities.medium ?? 0} detectadas -

-
-
-

Low × 1

-

- {summary?.vulnerabilities.low ?? 0} detectadas -

-
+ {#each [{ name: 'Critical', weight: 10, count: summary?.vulnerabilities.critical ?? 0, style: 'border-red-200 bg-red-50 text-red-700' }, { name: 'High', weight: 6, count: summary?.vulnerabilities.high ?? 0, style: 'border-orange-200 bg-orange-50 text-orange-700' }, { name: 'Medium', weight: 3, count: summary?.vulnerabilities.medium ?? 0, style: 'border-amber-200 bg-amber-50 text-amber-700' }, { name: 'Low', weight: 1, count: summary?.vulnerabilities.low ?? 0, style: 'border-slate-200 bg-slate-50 text-slate-700' }] as item}
+

{item.name} × {item.weight}

+

{item.count} detectadas

+
{/each}
-

Fórmula aplicada

@@ -686,5 +274,4 @@

- -{/if} + {/if} diff --git a/src/lib/components/code-report/CodeReportFiles.svelte b/src/lib/components/code-report/CodeReportFiles.svelte new file mode 100644 index 0000000..0f198bf --- /dev/null +++ b/src/lib/components/code-report/CodeReportFiles.svelte @@ -0,0 +1,134 @@ + + +
+ {#if filteredFileGroups.length === 0}

+ No se han detectado vulnerabilidades asociadas a archivos. +

{:else}
+ +
+ {#if selectedFile}
+

+ Archivo seleccionado +

+

+ {selectedFile.path} +

+

+ {selectedFile.vulnerabilities.length} vulnerabilidades detectadas en este archivo +

+
+
+ {#each selectedFile.vulnerabilities as finding, index (finding.id + finding.packageName + finding.installedVersion + index)}
+
+
+ {finding.severity}{finding.id} +
+ Ver CVE ↗ +
+

{finding.packageName}

+

+ {finding.installedVersion} → {finding.fixedVersion || 'Sin versión corregida'} +

+

+ {finding.description || finding.title} +

+
+ {#if finding.lineStart !== null}

+ Línea{finding.lineEnd !== null && finding.lineEnd !== finding.lineStart + ? `s ${finding.lineStart}-${finding.lineEnd}` + : ` ${finding.lineStart}`} +

{:else}

+ Este informe no incluye línea ni fragmento de código. +

{/if}{#if finding.codeSnippet}
{finding.codeSnippet}
{/if} +
+
+ CWE: {finding.cweIds.length > 0 + ? finding.cweIds.join(', ') + : 'No indicado'}{#if finding.primaryUrl}Ver advisory ↗{/if} +
+
{/each} +
{:else}
+
+

+ Selecciona un archivo para inspeccionar +

+

+ Elige un archivo del listado para ver sus vulnerabilidades. +

+
+
{/if} +
+
{/if} +
diff --git a/src/lib/components/code-report/CodeReportSummary.svelte b/src/lib/components/code-report/CodeReportSummary.svelte new file mode 100644 index 0000000..1cfa506 --- /dev/null +++ b/src/lib/components/code-report/CodeReportSummary.svelte @@ -0,0 +1,149 @@ + + +
+
+
+

+ Estado de seguridad +

+

{riskLevel.label}

+

+ Puntuación calculada según la severidad de las vulnerabilidades detectadas. +

+
+
+

Riesgo

+

{riskScore}

+

puntos ponderados

+
+
+ +
+
+
+

+ Información del servicio +

+ {#if service}

{service.name}

+

{service.slug}

+ {#if service.description}

+ {service.description} +

{/if}{#if service.tags?.length}
+ {#each service.tags as tag}{tag}{/each} +
{/if}{:else}

Información no disponible.

{/if} +
+
+
+
+

+ Información del repositorio +

+

+ {analysis?.gitInfo?.repositoryUrl ? 'Repositorio conectado' : 'Sin repositorio conectado'} +

+
+ {#if analysis?.gitInfo?.repositoryUrl}{@const ProviderIcon = providerIcon( + analysis.gitInfo.repositoryUrl, + )}{/if} +
+ {#if analysis?.gitInfo?.repositoryUrl}{analysis.gitInfo.repositoryUrl}{:else}

+ Este análisis no incluye un repositorio configurado. +

{/if}{#if analysis?.gitInfo?.branch}

+ Rama {analysis.gitInfo.branch} +

{/if} +
+
+

+ Histórico de ejecuciones +

+

+ {analysisHistoryLength} análisis completado{analysisHistoryLength === 1 ? '' : 's'} +

+ {#if analysis}

+ Último: {analysis.tool}·{new Date(analysis.createdAt).toLocaleString()} +

{/if} +
+
+
+
+

Evolución del riesgo

+
+ {#if historyPoints.length > 0}{:else}
+ Todavía no hay historial suficiente. +
{/if} +
+
+
+

Indicadores clave

+
+ {#each severityKeys as severity}
+

{summary?.vulnerabilities[severity]}

+

+ {severity} +

+
{/each} +
+
+
+
Dependencias
+
{summary?.dependencies}
+
+
+
Archivos afectados
+
{fileCount}
+
+
+
+
diff --git a/src/lib/components/code-report/CodeReportVulnerabilities.svelte b/src/lib/components/code-report/CodeReportVulnerabilities.svelte new file mode 100644 index 0000000..300e937 --- /dev/null +++ b/src/lib/components/code-report/CodeReportVulnerabilities.svelte @@ -0,0 +1,129 @@ + + +
+ {#if analysis.status === 'failed' && analysis.error}
+ {analysis.error} +
{:else if vulnerabilities.length === 0}

+ No se han detectado vulnerabilidades en este análisis. +

{:else}
+ +
+
+ {#each filteredVulnerabilities as finding, index (finding.id + finding.target + finding.packageName + index)}
+ {finding.severity}{finding.id}{finding.title}{findingStatus(finding)}{finding.cvssScore !== null + ? `CVSS ${finding.cvssScore.toFixed(1)}` + : 'Sin CVSS'}CVE ↗ +
+

{finding.description || finding.title}

+
+
+

+ Ubicación detectada +

+

{finding.packagePath}

+ {#if finding.packageIdentifier}

+ Identificador: {finding.packageIdentifier} +

{/if}{#if finding.lineStart !== null}

+ Línea{finding.lineEnd !== null && finding.lineEnd !== finding.lineStart + ? `s ${finding.lineStart}-${finding.lineEnd}` + : ` ${finding.lineStart}`} +

{:else}

+ Este informe no incluye línea ni fragmento de código. +

{/if}{#if finding.codeSnippet}
{finding.codeSnippet}
{/if} +
+
+
+
Paquete
+
{finding.packageName}
+
+
+
Estado Trivy
+
{finding.status}
+
+
+
Versión instalada
+
+ {finding.installedVersion} +
+
+
+
Versión corregida
+
+ {finding.fixedVersion || 'No indicada'} +
+
+
+
CWE
+
+ {finding.cweIds.length > 0 ? finding.cweIds.join(', ') : 'No indicado'} +
+
+
+
+
+ Abrir {finding.id} en NVD ↗{#if finding.primaryUrl}Ver advisory ↗{/if} +
+
+
{/each} +
{/if} +
From f9865303d388ed2032cecc8780db77c07ed3400c Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Sun, 23 Aug 2026 01:48:58 +0200 Subject: [PATCH 07/15] fixes --- .../[slug]/code-report/history/+page.svelte | 199 +++++++++++++++--- 1 file changed, 172 insertions(+), 27 deletions(-) diff --git a/src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte index 24c4235..3862856 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte @@ -1,11 +1,74 @@
-
- {#each [{ id: 'summary', label: 'Resumen' }, { id: 'vulnerabilities', label: 'Vulnerabilidades', count: vulnerabilities.length }, { id: 'files', label: 'Archivos', count: fileGroups.length }] as tab}{/each} @@ -192,7 +220,7 @@ > Todavía no se ha ejecutado ningún análisis.
{:else if activeTab === 'summary'} (riskInfoModalOpen = true)} />{:else if activeTab === 'vulnerabilities'}{:else if activeTab === 'secrets'}{:else if activeTab === 'sbom'}{:else} + import { AlertCircle, Package, Search } from 'lucide-svelte'; + import type { SbomComponent } from '$lib/code-report/analysis-summary'; + + type Analysis = { tool: string; status: string; createdAt: string; error?: string | null } | null; + + export let analysis: Analysis = null; + export let components: SbomComponent[] = []; + export let sbomQuery = ''; + export let sbomTypeFilter = 'all'; + + $: componentTypes = [...new Set(components.map((component) => component.type))].sort(); + $: filteredComponents = components + .filter((component) => { + const query = sbomQuery.trim().toLowerCase(); + return ( + (!query || + [component.name, component.purl, component.version].some((value) => + value.toLowerCase().includes(query), + )) && + (sbomTypeFilter === 'all' || component.type === sbomTypeFilter) + ); + }) + .sort((left, right) => left.name.localeCompare(right.name)); + $: licensedCount = components.filter((component) => component.licenses.length > 0).length; + + +
+ {#if !analysis} +

+ Todavía no se ha generado ningún SBOM con syft en este servicio. +

+ {:else if analysis.status === 'failed'} +
+ {analysis.error || 'La generación del SBOM falló.'} +
+ {:else if analysis.status === 'in_progress'} +

+ La generación del SBOM está en curso. +

+ {:else if components.length === 0} +

+ El SBOM no contiene componentes o el formato no es reconocido. +

+ {:else} +
+
+

Componentes

+

{components.length}

+
+
+

Ecosistemas

+

{componentTypes.length}

+
+
+

Con licencia

+

+ {licensedCount}/{components.length} +

+
+
+
+ + +
+
+ + + + + + + + + + + + {#each filteredComponents as component, index (component.purl + component.name + index)} + + + + + + + + {/each} + +
ComponenteVersiónTipoLicenciasUbicación
+ + {component.name} + + {#if component.purl} + {component.purl} + {/if} + {component.version} + {component.type} + + {component.licenses.length > 0 ? component.licenses.join(', ') : 'No declarada'} + + {component.locations[0] || '—'} +
+
+ {/if} +
diff --git a/src/lib/components/code-report/CodeReportSecrets.svelte b/src/lib/components/code-report/CodeReportSecrets.svelte new file mode 100644 index 0000000..3958991 --- /dev/null +++ b/src/lib/components/code-report/CodeReportSecrets.svelte @@ -0,0 +1,135 @@ + + +
+ {#if !analysis} +

+ Todavía no se ha ejecutado ningún análisis de secretos con gitleaks en este servicio. +

+ {:else if analysis.status === 'failed'} +
+ {analysis.error || 'El análisis de secretos falló.'} +
+ {:else if analysis.status === 'in_progress'} +

+ El análisis de secretos está en curso. +

+ {:else if secrets.length === 0} +

+ No se han detectado secretos expuestos en este análisis. +

+ {:else} +
+
+ {secrets.length} secreto{secrets.length === 1 + ? '' + : 's'} expuesto{secrets.length === 1 ? '' : 's'} +
+ +
+
+ {#each filteredSecrets as secret, index (secret.id + index)} +
+ + {secret.severity} + {secret.ruleId} + {secret.title} + {secret.file}{secret.lineStart !== null ? `:${secret.lineStart}` : ''} + +
+
+
+
Archivo
+
+ {secret.file} +
+
+
+
Líneas
+
+ {secret.lineStart !== null + ? secret.lineEnd !== null && secret.lineEnd !== secret.lineStart + ? `${secret.lineStart}-${secret.lineEnd}` + : secret.lineStart + : 'No indicadas'} +
+
+
+
Entropía
+
+ {secret.entropy !== null ? secret.entropy.toFixed(2) : 'No indicada'} +
+
+ {#if secret.commit} +
+
Commit
+
+ {secret.commit.slice(0, 10)} +
+
+ {/if} + {#if secret.author} +
+
Autor
+
{secret.author}
+
+ {/if} + {#if secret.date} +
+
Fecha
+
+ {new Date(secret.date).toLocaleString()} +
+
+ {/if} +
+ {#if secret.match} +

+ Coincidencia (enmascarada) +

+
{secret.match}
+ {/if} +

+ Rota esta credencial aunque la elimines del código: sigue presente en el historial de + git. +

+
+
+ {/each} +
+ {/if} +
diff --git a/src/modules/code-report/application/code-report-analysis.service.ts b/src/modules/code-report/application/code-report-analysis.service.ts index 757447f..e0484fa 100644 --- a/src/modules/code-report/application/code-report-analysis.service.ts +++ b/src/modules/code-report/application/code-report-analysis.service.ts @@ -1,5 +1,6 @@ import crypto from 'crypto'; import { CodeReportAnalysisRepository } from '../infrastructure/repositories/code-report-analysis.repository'; +import type { CodeReportAnalysisDomain } from '../domain/code-report-analysis.domain'; import type { CodeReportGitInfo } from '../domain/code-report-analysis.domain'; type ServiceLookup = { @@ -17,6 +18,21 @@ export class CodeReportAnalysisService { return analyses.map((analysis) => analysis.toJson()); } + async getLatestByTool(serviceId: string, tools: string[]) { + const analyses = await Promise.all( + tools.map((tool) => this.repository.findLatestByServiceIdAndTool(serviceId, tool)), + ); + + return tools.reduce | null>>( + (accumulator, tool, index) => { + const analysis = analyses[index]; + accumulator[tool] = analysis ? analysis.toJson() : null; + return accumulator; + }, + {}, + ); + } + async listByProject(serviceIds: string[]) { const analyses = await Promise.all( serviceIds.map((serviceId) => this.listByService(serviceId)), @@ -29,6 +45,7 @@ export class CodeReportAnalysisService { } async getById(id: string) { + console.log('🔍 Fetching analysis by ID:', id); const analysis = await this.repository.findById(id); if (!analysis) { throw new Error('Analysis not found'); @@ -38,15 +55,15 @@ export class CodeReportAnalysisService { // called when a scan tool starts running against a service, reports 'in_progress' with no result yet async startAnalysis(input: { serviceId: string; tool: string; gitInfo?: CodeReportGitInfo }) { - const serviceId = input.serviceId?.trim(); - if (!serviceId) { - throw new Error('Service is required'); - } + // const serviceId = input.serviceId?.trim(); + // if (!serviceId) { + // throw new Error('Service is required'); + // } - const service = await this.serviceLookup.findById(serviceId); - if (!service) { - throw new Error('Service not found'); - } + // const service = await this.serviceLookup.findById(serviceId); + // if (!service) { + // throw new Error('Service not found'); + // } const tool = input.tool?.trim(); if (!tool) { @@ -56,13 +73,13 @@ export class CodeReportAnalysisService { const id = crypto.randomUUID(); await this.repository.create({ id, - serviceId, + serviceId: input.serviceId, tool, status: 'in_progress', gitInfo: input.gitInfo, }); - - return this.getById(id); + return { id, serviceId: input.serviceId, tool, status: 'in_progress', gitInfo: input.gitInfo }; + // return this.getById(id); } // called when the tool finishes successfully with the raw JSON result diff --git a/src/modules/code-report/application/code-report.service.ts b/src/modules/code-report/application/code-report.service.ts index 663e113..e2ae81c 100644 --- a/src/modules/code-report/application/code-report.service.ts +++ b/src/modules/code-report/application/code-report.service.ts @@ -24,7 +24,13 @@ export class CodeReportService { } return service.toJson(); } - + async getByProjectIdAndSlug(projectId: string, serviceSlug: string) { + const service = await this.repository.findBySlug(projectId, serviceSlug); + if (!service) { + throw new Error('Service not found'); + } + return service.toJson(); + } async getByProjectAndSlug(projectSlug: string, serviceSlug: string) { const project = await this.projectService.getProjectBySlug(projectSlug); if (!project) { diff --git a/src/modules/code-report/infrastructure/repositories/code-report-analysis.repository.ts b/src/modules/code-report/infrastructure/repositories/code-report-analysis.repository.ts index ff0fb08..dc3201d 100644 --- a/src/modules/code-report/infrastructure/repositories/code-report-analysis.repository.ts +++ b/src/modules/code-report/infrastructure/repositories/code-report-analysis.repository.ts @@ -22,6 +22,20 @@ export class CodeReportAnalysisRepository extends Repository { return row ? new CodeReportAnalysisDomain(row) : null; } + async findLatestByServiceIdAndTool( + serviceId: string, + tool: string, + ): Promise { + const result = await this.db + .select() + .from(CodeReportAnalysisEntity) + .where({ serviceId, tool }) + .orderBy('createdAt', 'desc') + .limit(1); + const row = result.rows[0]; + return row ? new CodeReportAnalysisDomain(row) : null; + } + async create(input: { id: string; serviceId: string; diff --git a/src/routes/api/code-report/scan/+server.ts b/src/routes/api/code-report/scan/+server.ts index b9da487..b8d553d 100644 --- a/src/routes/api/code-report/scan/+server.ts +++ b/src/routes/api/code-report/scan/+server.ts @@ -94,6 +94,7 @@ export async function POST({ request }) { if(status === 'start') { message = 'Scan started'; } else if(status === 'in_progress') { + console.log('Starting analysis for service:', serviceCodeReport?.id, 'with tool:', body.tool); analysis = await codeReportAnalysisService.startAnalysis({ serviceId: serviceCodeReport?.id || '', tool: body.tool, diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts index 7af56a2..5ade227 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.server.ts @@ -20,7 +20,7 @@ export async function load({ parent, params, locals }) { } try { - const service = await codeReportService.getByProjectAndSlug(project.id, params.serviceSlug); + const service = await codeReportService.getByProjectIdAndSlug(project.id, params.serviceSlug); const analyses = await codeReportAnalysisService.listByService(service.id); const latestAnalysis = analyses[0] ?? null; const analysisHistory = analyses @@ -37,7 +37,12 @@ export async function load({ parent, params, locals }) { updatedAt: analysis.updatedAt, })); - return { service, latestAnalysis, analysisHistory }; + const latestByTool = await codeReportAnalysisService.getLatestByTool( + service.id, + service.tools ?? [], + ); + + return { service, latestAnalysis, latestByTool, analysisHistory }; } catch { throw error(404, 'Service not found'); } diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte index bcfaa54..a79248f 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte @@ -7,6 +7,7 @@ export let data: { service: { name: string; slug: string }; latestAnalysis: any; + latestByTool: Record; analysisHistory: any[]; }; export let form: { @@ -99,6 +100,7 @@
From 4698b37a3e76fc4befe09ad7b5c347d5f3ccaabb Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 11:50:23 +0200 Subject: [PATCH 10/15] fixes UI --- .../components/CodeReportVisualization.svelte | 87 ++++++++++++------- .../code-report/CodeReportSummary.svelte | 31 ++++++- .../code-report/CodeReportToolBadge.svelte | 37 ++++++++ .../[slug]/code-report/history/+page.svelte | 4 +- .../history/[analysisID]/+page.svelte | 8 +- .../code-report/services/+page.server.ts | 20 ++++- .../[slug]/code-report/services/+page.svelte | 42 +++++++++ 7 files changed, 188 insertions(+), 41 deletions(-) create mode 100644 src/lib/components/code-report/CodeReportToolBadge.svelte diff --git a/src/lib/components/CodeReportVisualization.svelte b/src/lib/components/CodeReportVisualization.svelte index 6b456da..6416268 100644 --- a/src/lib/components/CodeReportVisualization.svelte +++ b/src/lib/components/CodeReportVisualization.svelte @@ -41,6 +41,7 @@ export let latestByTool: Record = {}; export let service: ServiceData = null; let activeTab = 'summary'; + let activeVulnerabilityTab = 'cve'; let riskInfoModalOpen = false; let fileQuery = ''; let vulnerabilityQuery = ''; @@ -61,6 +62,7 @@ const severityRank: Record = { critical: 4, high: 3, medium: 2, low: 1 }; const severityKeys = ['critical', 'high', 'medium', 'low', 'unknown'] as const; const fileSeverityOrder = ['critical', 'high', 'medium', 'low'] as const; + const knownTools = ['trivy', 'sbom', 'gitleaks']; // manual uploads are stored under other tool names, so fall back to the latest analysis $: trivyAnalysis = latestByTool.trivy ?? analysis; $: gitleaksAnalysis = latestByTool.gitleaks ?? null; @@ -109,16 +111,19 @@ ); $: tabs = [ { id: 'summary', label: 'Resumen' }, - { - id: 'vulnerabilities', - label: 'Vulnerabilidades', - tool: 'trivy', - count: vulnerabilities.length, - }, - { id: 'secrets', label: 'Secretos Expuestos', tool: 'gitleaks', count: secrets.length }, - { id: 'sbom', label: 'SBOM', tool: 'syft', count: sbomComponents.length }, + { id: 'vulnerabilities', label: 'Vulnerabilidades', count: vulnerabilities.length }, + { id: 'sbom', label: 'Inventario', count: sbomComponents.length }, + { id: 'secrets', label: 'Secretos Expuestos', count: secrets.length }, + ] as { id: string; label: string; count?: number }[]; + $: vulnerabilitySubTabs = [ + { id: 'cve', label: 'CVE', count: vulnerabilities.length }, { id: 'files', label: 'Archivos', count: fileGroups.length }, - ] as { id: string; label: string; tool?: string; count?: number }[]; + ]; + $: toolRuns = knownTools.map((tool) => ({ + tool, + status: latestByTool[tool]?.status ?? null, + createdAt: latestByTool[tool]?.createdAt ?? null, + })); function calculateRiskScore(value: ReturnType) { return ( value.vulnerabilities.critical * riskWeights.critical + @@ -208,9 +213,7 @@ aria-selected={activeTab === tab.id} on:click={() => (activeTab = tab.id)} class={`shrink-0 border-b-2 px-4 py-3 text-sm font-semibold ${activeTab === tab.id ? 'border-slate-900 text-slate-900' : 'border-transparent text-slate-500 hover:text-slate-900'}`} - >{tab.label}{#if tab.tool}({tab.tool}){/if}{#if tab.count !== undefined}{tab.label}{#if tab.count !== undefined}{tab.count}{/if}{/each} @@ -222,6 +225,7 @@ {:else if activeTab === 'summary'} (riskInfoModalOpen = true)} - />{:else if activeTab === 'vulnerabilities'}{:else if activeTab === 'secrets'}{:else if activeTab === 'vulnerabilities'}
+
+ {#each vulnerabilitySubTabs as subTab}{/each} +
+ {#if activeVulnerabilityTab === 'cve'}{:else}{/if} +
{:else if activeTab === 'secrets'}{:else if activeTab === 'sbom'}{:else}{:else}{/if} diff --git a/src/lib/components/code-report/CodeReportSummary.svelte b/src/lib/components/code-report/CodeReportSummary.svelte index 1cfa506..e6f1a03 100644 --- a/src/lib/components/code-report/CodeReportSummary.svelte +++ b/src/lib/components/code-report/CodeReportSummary.svelte @@ -1,6 +1,7 @@
{analysisHistoryLength} análisis completado{analysisHistoryLength === 1 ? '' : 's'} - {#if analysis}

+ {#if toolRuns.length > 0}

    + {#each toolRuns as run (run.tool)}
  • +
    + +

    + {run.createdAt ? new Date(run.createdAt).toLocaleString() : 'Nunca ejecutado'} +

    +
    + {run.status ? (runStatusLabels[run.status] ?? run.status) : 'Sin datos'} +
  • {/each} +
{:else if analysis}

Último: {analysis.tool}·{new Date(analysis.createdAt).toLocaleString()} diff --git a/src/lib/components/code-report/CodeReportToolBadge.svelte b/src/lib/components/code-report/CodeReportToolBadge.svelte new file mode 100644 index 0000000..dff6a44 --- /dev/null +++ b/src/lib/components/code-report/CodeReportToolBadge.svelte @@ -0,0 +1,37 @@ + + + + + {preset.label} + diff --git a/src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte index 3862856..25723c9 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/history/+page.svelte @@ -2,6 +2,7 @@ import { page } from '$app/stores'; import { ArrowLeft, Check, ChevronDown, Clock, Search } from 'lucide-svelte'; import { summarizeAnalysisResult } from '$lib/code-report/analysis-summary'; + import CodeReportToolBadge from '$lib/components/code-report/CodeReportToolBadge.svelte'; export let data: { services: { id: string; slug: string; name: string }[]; analyses: any[] }; let serviceFilter = $page.url.searchParams.get('service') ?? 'all'; let statusFilter = 'all'; @@ -198,8 +199,7 @@ ID: {analysis.id.slice(0, 8)}

- {analysis.tool} - · + {new Date( analysis.createdAt, diff --git a/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte index 9d0b6ad..054a43d 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/history/[analysisID]/+page.svelte @@ -2,6 +2,7 @@ import { page } from '$app/stores'; import { ArrowLeft } from 'lucide-svelte'; import CodeReportVisualization from '$lib/components/CodeReportVisualization.svelte'; + import CodeReportToolBadge from '$lib/components/code-report/CodeReportToolBadge.svelte'; export let data: { service: { name: string; slug: string }; analysis: any; @@ -18,9 +19,10 @@

Informe consultado

{data.service.name}

-
- {data.analysis.id}{data.analysis.tool}{new Date(data.analysis.createdAt).toLocaleString()} + {data.analysis.id}{new Date(data.analysis.createdAt).toLocaleString()}{data.analysis.status}
diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/services/+page.server.ts index 3e1d10f..7cb97b9 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/+page.server.ts +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/+page.server.ts @@ -1,7 +1,8 @@ import { error, fail } from '@sveltejs/kit'; -import { codeReportService } from '../../../../../../../modules/code-report'; +import { codeReportService, codeReportAnalysisService } from '../../../../../../../modules/code-report'; import { projectService } from '../../../../../../../modules/projects'; import { cancanService } from '../../../../../../../modules/auth'; +import { summarizeAnalysisResult } from '$lib/code-report/analysis-summary'; export async function load({ parent, locals }) { const { project } = await parent(); @@ -18,7 +19,22 @@ export async function load({ parent, locals }) { const services = await codeReportService.listByProject(project.id); - return { services }; + const servicesWithSeverity = await Promise.all( + services.map(async (service) => { + const latest = await codeReportAnalysisService.getLatestByTool(service.id, ['trivy']); + const analysis = latest.trivy; + const summary = + analysis?.status === 'completed' ? summarizeAnalysisResult(analysis.result) : null; + + return { + ...service, + lastScanAt: analysis?.createdAt ?? null, + severity: summary?.vulnerabilities ?? null, + }; + }), + ); + + return { services: servicesWithSeverity }; } export const actions = { diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/services/+page.svelte index c93e06f..d0a8df8 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/+page.svelte @@ -3,14 +3,31 @@ import { enhance } from '$app/forms'; import { Boxes, Plus, Search, X } from 'lucide-svelte'; + type SeverityCounts = { + critical: number; + high: number; + medium: number; + low: number; + unknown: number; + }; + type ServiceRow = { id: string; slug: string; name: string; description?: string | null; tags: string[]; + lastScanAt?: string | null; + severity?: SeverityCounts | null; }; + const severityStyles: { key: keyof SeverityCounts; label: string; className: string }[] = [ + { key: 'critical', label: 'Critical', className: 'border-red-200 bg-red-50 text-red-700' }, + { key: 'high', label: 'High', className: 'border-orange-200 bg-orange-50 text-orange-700' }, + { key: 'medium', label: 'Medium', className: 'border-amber-200 bg-amber-50 text-amber-700' }, + { key: 'low', label: 'Low', className: 'border-slate-200 bg-slate-50 text-slate-600' }, + ]; + export let data: { services: ServiceRow[] }; export let form: { success?: boolean; @@ -136,6 +153,31 @@ {/each}
{/if} +
+ {#if service.severity} +
+ {#each severityStyles as severity} +
+

+ {service.severity[severity.key]} +

+

+ {severity.label} +

+
+ {/each} +
+ {#if service.lastScanAt} +

+ Último escaneo: {new Date(service.lastScanAt).toLocaleString()} +

+ {/if} + {:else} +

Sin análisis de vulnerabilidades todavía.

+ {/if} +
{/each} From 8f569150404ab6fb4fa401df54f1a8967806d350 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 12:27:50 +0200 Subject: [PATCH 11/15] CVE pages --- src/lib/code-report/cve-aggregation.ts | 90 +++++++++ src/lib/components/AppSidebar.svelte | 2 + .../application/code-report-cve.service.ts | 26 +++ src/modules/code-report/index.ts | 5 + .../[slug]/code-report/cves/+page.server.ts | 22 +++ .../[slug]/code-report/cves/+page.svelte | 122 ++++++++++++ .../code-report/cves/[cve]/+page.server.ts | 53 ++++++ .../code-report/cves/[cve]/+page.svelte | 179 ++++++++++++++++++ 8 files changed, 499 insertions(+) create mode 100644 src/lib/code-report/cve-aggregation.ts create mode 100644 src/modules/code-report/application/code-report-cve.service.ts create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/cves/+page.server.ts create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/cves/+page.svelte create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.server.ts create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.svelte diff --git a/src/lib/code-report/cve-aggregation.ts b/src/lib/code-report/cve-aggregation.ts new file mode 100644 index 0000000..16e3fc4 --- /dev/null +++ b/src/lib/code-report/cve-aggregation.ts @@ -0,0 +1,90 @@ +import { extractVulnerabilities, type VulnerabilityFinding } from './analysis-summary'; + +export type CveOccurrence = { + serviceId: string; + serviceSlug: string; + serviceName: string; + scannedAt: string | null; + finding: VulnerabilityFinding; +}; + +export type CveSummary = { + id: string; + title: string; + severity: VulnerabilityFinding['severity']; + cvssScore: number | null; + affectedServiceCount: number; + occurrenceCount: number; +}; + +const severityRank: Record = { + critical: 4, + high: 3, + medium: 2, + low: 1, + unknown: 0, +}; + +export type CompletedAnalysis = { result: unknown; createdAt: string } | null | undefined; + +// groups every vulnerability finding across a project's services by CVE id, using the +// latest completed analysis available for each service +export function collectCveOccurrences( + services: { id: string; slug: string; name: string }[], + analysisByServiceId: Map, +): Map { + const occurrencesByCve = new Map(); + + for (const service of services) { + const analysis = analysisByServiceId.get(service.id); + if (!analysis) continue; + + for (const finding of extractVulnerabilities(analysis.result)) { + const occurrences = occurrencesByCve.get(finding.id) ?? []; + occurrences.push({ + serviceId: service.id, + serviceSlug: service.slug, + serviceName: service.name, + scannedAt: analysis.createdAt, + finding, + }); + occurrencesByCve.set(finding.id, occurrences); + } + } + + return occurrencesByCve; +} + +export function highestSeverity(occurrences: CveOccurrence[]): VulnerabilityFinding['severity'] { + return occurrences.reduce( + (highest, occurrence) => + severityRank[occurrence.finding.severity] > severityRank[highest] + ? occurrence.finding.severity + : highest, + 'unknown', + ); +} + +export function highestCvssScore(occurrences: CveOccurrence[]): number | null { + return occurrences.reduce((highest, occurrence) => { + const score = occurrence.finding.cvssScore; + return score !== null && (highest === null || score > highest) ? score : highest; + }, null); +} + +export function summarizeCves(occurrencesByCve: Map): CveSummary[] { + return [...occurrencesByCve.entries()] + .map(([id, occurrences]) => ({ + id, + title: occurrences[0]?.finding.title ?? '', + severity: highestSeverity(occurrences), + cvssScore: highestCvssScore(occurrences), + affectedServiceCount: new Set(occurrences.map((occurrence) => occurrence.serviceId)).size, + occurrenceCount: occurrences.length, + })) + .sort( + (left, right) => + severityRank[right.severity] - severityRank[left.severity] || + right.affectedServiceCount - left.affectedServiceCount, + ); +} diff --git a/src/lib/components/AppSidebar.svelte b/src/lib/components/AppSidebar.svelte index cad3432..0a90bbb 100644 --- a/src/lib/components/AppSidebar.svelte +++ b/src/lib/components/AppSidebar.svelte @@ -22,6 +22,7 @@ Layers, HardDrive, Bot, + ShieldAlert, } from 'lucide-svelte'; type NavItem = { label: string; href: string; icon: ComponentType }; @@ -98,6 +99,7 @@ icon: BarChart3, items: [ { label: 'Services', href: `${projectBase}/code-report/services`, icon: Layers }, + { label: 'CVEs', href: `${projectBase}/code-report/cves`, icon: ShieldAlert }, { label: 'History', href: `${projectBase}/code-report/history`, icon: GitBranch }, { label: 'GitOps Report Bot', href: `${projectBase}/code-report/bot`, icon: Bot }, { label: 'Settings', href: `${projectBase}/code-report/settings`, icon: Settings }, diff --git a/src/modules/code-report/application/code-report-cve.service.ts b/src/modules/code-report/application/code-report-cve.service.ts new file mode 100644 index 0000000..0f23bf9 --- /dev/null +++ b/src/modules/code-report/application/code-report-cve.service.ts @@ -0,0 +1,26 @@ +import { collectCveOccurrences, type CompletedAnalysis, type CveOccurrence } from '$lib/code-report/cve-aggregation'; +import type { CodeReportService } from './code-report.service'; +import type { CodeReportAnalysisService } from './code-report-analysis.service'; + +export class CodeReportCveService { + constructor( + private readonly codeReportService: CodeReportService, + private readonly codeReportAnalysisService: CodeReportAnalysisService, + ) {} + + // only trivy analyses carry CVE-style vulnerability findings today + async getProjectCveOccurrences(projectId: string): Promise> { + const services = await this.codeReportService.listByProject(projectId); + + const analysisEntries = await Promise.all( + services.map(async (service) => { + const latest = await this.codeReportAnalysisService.getLatestByTool(service.id, ['trivy']); + const analysis = latest.trivy; + const completed: CompletedAnalysis = analysis?.status === 'completed' ? analysis : null; + return [service.id, completed] as const; + }), + ); + + return collectCveOccurrences(services, new Map(analysisEntries)); + } +} diff --git a/src/modules/code-report/index.ts b/src/modules/code-report/index.ts index a8fb35b..d5a1654 100644 --- a/src/modules/code-report/index.ts +++ b/src/modules/code-report/index.ts @@ -1,5 +1,6 @@ import { CodeReportService } from './application/code-report.service'; import { CodeReportAnalysisService } from './application/code-report-analysis.service'; +import { CodeReportCveService } from './application/code-report-cve.service'; import { CodeReportServiceRepository } from './infrastructure/repositories/code-report-service.repository'; import { CodeReportAnalysisRepository } from './infrastructure/repositories/code-report-analysis.repository'; import { projectService } from '../projects'; @@ -16,3 +17,7 @@ export const codeReportService = new CodeReportService( projectService, codeReportAnalysisService, ); +export const codeReportCveService = new CodeReportCveService( + codeReportService, + codeReportAnalysisService, +); diff --git a/src/routes/org/[org]/projects/[slug]/code-report/cves/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/cves/+page.server.ts new file mode 100644 index 0000000..b949012 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/cves/+page.server.ts @@ -0,0 +1,22 @@ +import { error } from '@sveltejs/kit'; +import { cancanService } from '../../../../../../../modules/auth'; +import { codeReportCveService } from '../../../../../../../modules/code-report'; +import { summarizeCves } from '$lib/code-report/cve-aggregation'; + +export async function load({ parent, locals }) { + const { project } = await parent(); + + const canRead = await cancanService.canSessionUser(locals.user, 'openreport:read', { + scope: 'project', + projectId: project.id, + organizationId: project.organization?.id, + }); + + if (!canRead) { + throw error(403, 'Forbidden'); + } + + const occurrencesByCve = await codeReportCveService.getProjectCveOccurrences(project.id); + + return { cves: summarizeCves(occurrencesByCve) }; +} diff --git a/src/routes/org/[org]/projects/[slug]/code-report/cves/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/cves/+page.svelte new file mode 100644 index 0000000..5c7290f --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/cves/+page.svelte @@ -0,0 +1,122 @@ + + + + Code Report - CVEs - GitVault Suite + + +
+
+ + +
+ + {#if filteredCves.length === 0} +
+ +

+ {data.cves.length === 0 + ? 'Todavía no se han detectado CVEs en este proyecto.' + : 'Sin resultados para tu búsqueda.'} +

+
+ {:else} +
+ + + + + + + + + + + + {#each filteredCves as cve (cve.id)} + + + + + + + + {/each} + +
CVESeveridadCVSSServicios afectadosApariciones
+ + {cve.id} + + {#if cve.title} +

{cve.title}

+ {/if} +
+ + {#if cve.severity === 'critical'}{/if} + {cve.severity} + + + {cve.cvssScore !== null ? cve.cvssScore.toFixed(1) : '—'} + {cve.affectedServiceCount}{cve.occurrenceCount}
+
+ {/if} +
diff --git a/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.server.ts new file mode 100644 index 0000000..a928fe2 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.server.ts @@ -0,0 +1,53 @@ +import { error } from '@sveltejs/kit'; +import { cancanService } from '../../../../../../../../modules/auth'; +import { codeReportCveService } from '../../../../../../../../modules/code-report'; +import { highestCvssScore, highestSeverity } from '$lib/code-report/cve-aggregation'; + +export async function load({ parent, locals, params }) { + const { project } = await parent(); + + const canRead = await cancanService.canSessionUser(locals.user, 'openreport:read', { + scope: 'project', + projectId: project.id, + organizationId: project.organization?.id, + }); + + if (!canRead) { + throw error(403, 'Forbidden'); + } + + const occurrencesByCve = await codeReportCveService.getProjectCveOccurrences(project.id); + const occurrences = occurrencesByCve.get(params.cve); + + if (!occurrences || occurrences.length === 0) { + throw error(404, 'CVE not found'); + } + + const first = occurrences[0].finding; + + const cve = { + id: params.cve, + title: first.title, + description: first.description, + severity: highestSeverity(occurrences), + cvssScore: highestCvssScore(occurrences), + primaryUrl: first.primaryUrl, + cveUrl: first.cveUrl, + cweIds: first.cweIds, + }; + + const affectedServices = occurrences.map((occurrence) => ({ + serviceId: occurrence.serviceId, + serviceSlug: occurrence.serviceSlug, + serviceName: occurrence.serviceName, + packageName: occurrence.finding.packageName, + installedVersion: occurrence.finding.installedVersion, + fixedVersion: occurrence.finding.fixedVersion, + target: occurrence.finding.target, + severity: occurrence.finding.severity, + status: occurrence.finding.status, + scannedAt: occurrence.scannedAt, + })); + + return { cve, affectedServices }; +} diff --git a/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.svelte new file mode 100644 index 0000000..8c0db46 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.svelte @@ -0,0 +1,179 @@ + + + + {data.cve.id} - Code Report - GitVault Suite + + +
+ + Volver a CVEs + + +
+
+

{data.cve.id}

+ + {data.cve.severity} + + {#if data.cve.cvssScore !== null} + CVSS {data.cve.cvssScore.toFixed(1)} + {/if} +
+ {#if data.cve.title} +

{data.cve.title}

+ {/if} +
+ +
+ {#each tabs as tab} + + {/each} +
+ + {#if activeTab === 'info'} +
+

{data.cve.description || 'Sin descripción disponible.'}

+ +
+
+
CWE
+
+ {data.cve.cweIds.length > 0 ? data.cve.cweIds.join(', ') : 'No indicado'} +
+
+
+
CVSS
+
+ {data.cve.cvssScore !== null ? data.cve.cvssScore.toFixed(1) : 'No indicado'} +
+
+
+ +
+ + Abrir {data.cve.id} en NVD ↗ + + {#if data.cve.primaryUrl} + + Ver advisory ↗ + + {/if} +
+
+ {:else} +
+ + + + + + + + + + + + + {#each data.affectedServices as service (service.serviceId + service.target + service.packageName)} + + + + + + + + + {/each} + +
ServicioPaqueteVersión instaladaVersión corregidaObjetivoÚltimo escaneo
+ + {service.serviceName} + + {service.packageName} + {service.installedVersion} + + {service.fixedVersion || 'No indicada'} + {service.target} + {service.scannedAt ? new Date(service.scannedAt).toLocaleString() : '—'} +
+
+ {/if} +
From 2fa3a124a0f5e2a89164e4eeca25146034c553a6 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 12:29:53 +0200 Subject: [PATCH 12/15] cve info --- .../projects/[slug]/code-report/cves/+page.svelte | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/routes/org/[org]/projects/[slug]/code-report/cves/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/cves/+page.svelte index 5c7290f..10acc4b 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/cves/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/cves/+page.svelte @@ -1,6 +1,6 @@ @@ -98,7 +109,7 @@ - {#each filteredCves as cve (cve.id)} + {#each paginatedCves as cve (cve.id)} @@ -126,5 +137,45 @@ + +
+
+ Mostrando {rangeStart}-{rangeEnd} de {filteredCves.length} + +
+ +
+ + Página {currentPage} de {totalPages} + +
+
{/if} diff --git a/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.server.ts index a928fe2..e615a7d 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.server.ts +++ b/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.server.ts @@ -1,7 +1,12 @@ import { error } from '@sveltejs/kit'; import { cancanService } from '../../../../../../../../modules/auth'; import { codeReportCveService } from '../../../../../../../../modules/code-report'; -import { highestCvssScore, highestSeverity } from '$lib/code-report/cve-aggregation'; +import { + highestCvssScore, + highestEpssPercentile, + highestEpssScore, + highestSeverity, +} from '$lib/code-report/cve-aggregation'; export async function load({ parent, locals, params }) { const { project } = await parent(); @@ -24,6 +29,10 @@ export async function load({ parent, locals, params }) { } const first = occurrences[0].finding; + const publishedDate = + occurrences.map((occurrence) => occurrence.finding.publishedDate).find(Boolean) ?? null; + const lastModifiedDate = + occurrences.map((occurrence) => occurrence.finding.lastModifiedDate).find(Boolean) ?? null; const cve = { id: params.cve, @@ -31,11 +40,31 @@ export async function load({ parent, locals, params }) { description: first.description, severity: highestSeverity(occurrences), cvssScore: highestCvssScore(occurrences), + epssScore: highestEpssScore(occurrences), + epssPercentile: highestEpssPercentile(occurrences), primaryUrl: first.primaryUrl, cveUrl: first.cveUrl, cweIds: first.cweIds, + references: [...new Set(occurrences.flatMap((occurrence) => occurrence.finding.references))], + publishedDate, + lastModifiedDate, }; + // one remediation entry per distinct package, listing the fixed version(s) trivy reported + const remediations = [ + ...new Map( + occurrences.map((occurrence) => [ + occurrence.finding.packageName, + { + packageName: occurrence.finding.packageName, + installedVersion: occurrence.finding.installedVersion, + fixedVersion: occurrence.finding.fixedVersion, + status: occurrence.finding.status, + }, + ]), + ).values(), + ]; + const affectedServices = occurrences.map((occurrence) => ({ serviceId: occurrence.serviceId, serviceSlug: occurrence.serviceSlug, @@ -49,5 +78,5 @@ export async function load({ parent, locals, params }) { scannedAt: occurrence.scannedAt, })); - return { cve, affectedServices }; + return { cve, remediations, affectedServices }; } diff --git a/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.svelte index 8c0db46..0816c12 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/cves/[cve]/+page.svelte @@ -1,6 +1,18 @@ @@ -96,46 +192,201 @@ {#if activeTab === 'info'} +
+
+ +
+

Severidad

+

{data.cve.severity}

+
+
+ {#if data.cve.publishedDate} +
+ +
+

Publicado

+

{new Date(data.cve.publishedDate).toLocaleDateString()}

+
+
+ {/if} +
+
-

{data.cve.description || 'Sin descripción disponible.'}

+

Overview

+

{data.cve.description || 'Sin descripción disponible.'}

+ {#if data.cve.lastModifiedDate} +

+ Última actualización: {new Date(data.cve.lastModifiedDate).toLocaleDateString()} +

+ {/if} +
-
-
- -
- - Abrir {data.cve.id} en NVD ↗ - - {#if data.cve.primaryUrl} - - Ver advisory ↗ - - {/if} +

+ {data.cve.cvssScore !== null ? data.cve.cvssScore.toFixed(1) : '—'} +

+
+
+
+

Escala 0-10

+
+ +
+
+ + EPSS Score + + + +
+

+ {data.cve.epssScore !== null ? `${(data.cve.epssScore * 100).toFixed(3)}%` : '—'} +

+
+
+
+

Probabilidad de explotación

+
+ +
+
+ + EPSS Percentil + + + +
+

+ {data.cve.epssPercentile !== null ? `${(data.cve.epssPercentile * 100).toFixed(1)}%` : '—'} +

+
+
+
+

Frente al resto de CVEs conocidas

+ + +
+

+ Cómo solucionarlo +

+ {#if data.remediations.some((remediation) => remediation.fixedVersion)} +
    + {#each data.remediations as remediation (remediation.packageName)} +
  • + {remediation.packageName} + {#if remediation.fixedVersion} +
    + {remediation.installedVersion} + + {#each splitVersions(remediation.fixedVersion) as version (version)} + + {version} + + {/each} +
    + {:else} + — todavía no hay una versión corregida publicada. + {/if} +
  • + {/each} +
+ {:else} +

+ Ninguno de los paquetes afectados tiene aún una versión corregida publicada. Revisa el advisory + para posibles mitigaciones alternativas. +

+ {/if} +
+ +
+

References

+ {#if data.cve.references.length === 0 && !data.cve.primaryUrl} +

No hay referencias disponibles para este CVE.

+ {:else} + + {/if}
{:else} +
+ +
+
@@ -149,7 +400,8 @@ - {#each data.affectedServices as service (service.serviceId + service.target + service.packageName)} + {#each paginatedAffectedServices as service (service.serviceId + service.target + service.packageName)} + {@const target = splitTarget(service.target)} - + - {/each} + {#if paginatedAffectedServices.length === 0} + + + + {/if}
{service.installedVersion} - {service.fixedVersion || 'No indicada'} + + {#if service.fixedVersion} +
+ {#each splitVersions(service.fixedVersion) as version (version)} + + {version} + + {/each} +
+ {:else} + No indicada + {/if} +
+ {#if target.dirParts.length > 0} +

+ {#each target.dirParts as part, i (i)}{part}{#if i < target.dirParts.length - 1}/{/if}{/each}/ +

+ {/if} +

{target.file}

{service.target} {service.scannedAt ? new Date(service.scannedAt).toLocaleString() : '—'}
+ {data.affectedServices.length === 0 + ? 'No hay servicios afectados.' + : 'Sin resultados para tu búsqueda.'} +
+ +
+
+ Mostrando {affectedServicesRangeStart}-{affectedServicesRangeEnd} de {filteredAffectedServices.length} + +
+ +
+ + + Página {affectedServicesPage} de {affectedServicesTotalPages} + + +
+
{/if} From ef2a8d46efad4c0e918dfdc9902ae588354e982e Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 13:07:52 +0200 Subject: [PATCH 14/15] dashboard code-report --- src/lib/components/AppSidebar.svelte | 5 + .../org/[org]/projects/[slug]/+layout.svelte | 2 +- .../[slug]/code-report/+page.server.ts | 2 +- .../code-report/dashboard/+page.server.ts | 119 +++++++ .../[slug]/code-report/dashboard/+page.svelte | 333 ++++++++++++++++++ 5 files changed, 459 insertions(+), 2 deletions(-) create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.server.ts create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.svelte diff --git a/src/lib/components/AppSidebar.svelte b/src/lib/components/AppSidebar.svelte index 0a90bbb..92a5c59 100644 --- a/src/lib/components/AppSidebar.svelte +++ b/src/lib/components/AppSidebar.svelte @@ -98,6 +98,11 @@ name: 'Code Report', icon: BarChart3, items: [ + { + label: 'Dashboard', + href: `${projectBase}/code-report/dashboard`, + icon: LayoutDashboard, + }, { label: 'Services', href: `${projectBase}/code-report/services`, icon: Layers }, { label: 'CVEs', href: `${projectBase}/code-report/cves`, icon: ShieldAlert }, { label: 'History', href: `${projectBase}/code-report/history`, icon: GitBranch }, diff --git a/src/routes/org/[org]/projects/[slug]/+layout.svelte b/src/routes/org/[org]/projects/[slug]/+layout.svelte index e3a0656..514f486 100644 --- a/src/routes/org/[org]/projects/[slug]/+layout.svelte +++ b/src/routes/org/[org]/projects/[slug]/+layout.svelte @@ -34,7 +34,7 @@ }, { label: 'Open Report', - href: `/org/${orgSlug}/projects/${project.slug}/code-report/services`, + href: `/org/${orgSlug}/projects/${project.slug}/code-report/dashboard`, icon: BarChart3, subtitle: 'Reportes de vulnerabilidades y dependencias del proyecto.', }, diff --git a/src/routes/org/[org]/projects/[slug]/code-report/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/+page.server.ts index a81e7d2..96b57f2 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/+page.server.ts +++ b/src/routes/org/[org]/projects/[slug]/code-report/+page.server.ts @@ -1,5 +1,5 @@ import { redirect } from '@sveltejs/kit'; export function load({ params }) { - throw redirect(302, `/org/${params.org}/projects/${params.slug}/code-report/services`); + throw redirect(302, `/org/${params.org}/projects/${params.slug}/code-report/dashboard`); } diff --git a/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.server.ts new file mode 100644 index 0000000..d900807 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.server.ts @@ -0,0 +1,119 @@ +import { error } from '@sveltejs/kit'; +import { cancanService } from '../../../../../../../modules/auth'; +import { + codeReportAnalysisService, + codeReportCveService, + codeReportService, +} from '../../../../../../../modules/code-report'; +import { extractSecrets, summarizeAnalysisResult } from '$lib/code-report/analysis-summary'; +import { summarizeCves } from '$lib/code-report/cve-aggregation'; + +const STALE_AFTER_DAYS = 30; +const riskWeights = { critical: 10, high: 6, medium: 3, low: 1, unknown: 0 }; + +export async function load({ parent, locals }) { + const { project } = await parent(); + + const canRead = await cancanService.canSessionUser(locals.user, 'openreport:read', { + scope: 'project', + projectId: project.id, + organizationId: project.organization?.id, + }); + + if (!canRead) { + throw error(403, 'Forbidden'); + } + + const services = await codeReportService.listByProject(project.id); + + const serviceStats = await Promise.all( + services.map(async (service) => { + const latest = await codeReportAnalysisService.getLatestByTool(service.id, [ + 'trivy', + 'gitleaks', + ]); + const trivyAnalysis = latest.trivy?.status === 'completed' ? latest.trivy : null; + const gitleaksAnalysis = latest.gitleaks?.status === 'completed' ? latest.gitleaks : null; + + const summary = trivyAnalysis + ? summarizeAnalysisResult(trivyAnalysis.result) + : summarizeAnalysisResult(null); + const exposedSecrets = gitleaksAnalysis ? extractSecrets(gitleaksAnalysis.result).length : 0; + + const lastScanAt = [latest.trivy?.createdAt, latest.gitleaks?.createdAt] + .filter((value): value is string => Boolean(value)) + .sort((left, right) => new Date(right).getTime() - new Date(left).getTime())[0] ?? null; + + return { + id: service.id, + slug: service.slug, + name: service.name, + scanned: trivyAnalysis !== null, + severity: summary.vulnerabilities, + exposedSecrets, + lastScanAt, + }; + }), + ); + + const occurrencesByCve = await codeReportCveService.getProjectCveOccurrences(project.id); + const cves = summarizeCves(occurrencesByCve); + + const remediableCves = [...occurrencesByCve.values()].filter((occurrences) => + occurrences.some((occurrence) => occurrence.finding.fixedVersion), + ).length; + + const severityBreakdown = serviceStats.reduce( + (totals, service) => ({ + critical: totals.critical + service.severity.critical, + high: totals.high + service.severity.high, + medium: totals.medium + service.severity.medium, + low: totals.low + service.severity.low, + unknown: totals.unknown + service.severity.unknown, + }), + { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 }, + ); + + const staleCutoff = Date.now() - STALE_AFTER_DAYS * 24 * 60 * 60 * 1000; + const staleServices = serviceStats + .filter((service) => !service.lastScanAt || new Date(service.lastScanAt).getTime() < staleCutoff) + .sort((left, right) => { + if (!left.lastScanAt) return -1; + if (!right.lastScanAt) return 1; + return new Date(left.lastScanAt).getTime() - new Date(right.lastScanAt).getTime(); + }); + + const riskiestServices = serviceStats + .map((service) => ({ + ...service, + riskScore: + service.severity.critical * riskWeights.critical + + service.severity.high * riskWeights.high + + service.severity.medium * riskWeights.medium + + service.severity.low * riskWeights.low, + })) + .filter((service) => service.riskScore > 0) + .sort((left, right) => right.riskScore - left.riskScore) + .slice(0, 5); + + const topCves = cves.slice(0, 8); + + return { + project, + kpis: { + totalServices: services.length, + scannedServices: serviceStats.filter((service) => service.scanned).length, + totalCves: cves.length, + criticalCount: severityBreakdown.critical, + highCount: severityBreakdown.high, + exposedSecrets: serviceStats.reduce((total, service) => total + service.exposedSecrets, 0), + remediationCoveragePercent: + cves.length > 0 ? Math.round((remediableCves / cves.length) * 100) : null, + staleServicesCount: staleServices.length, + }, + severityBreakdown, + topCves, + riskiestServices, + staleServices: staleServices.slice(0, 5), + }; +} diff --git a/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.svelte new file mode 100644 index 0000000..36f38fa --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.svelte @@ -0,0 +1,333 @@ + + + + Code Report - Dashboard - GitVault Suite + + +
+
+
+
+ Servicios +
+

{data.kpis.totalServices}

+

+ {data.kpis.scannedServices} con al menos un análisis completado +

+
+ +
+
+ CVEs críticos + altos +
+

+ {data.kpis.criticalCount + data.kpis.highCount} +

+

de {data.kpis.totalCves} CVEs detectados en total

+
+ +
+
+ Secretos expuestos +
+

{data.kpis.exposedSecrets}

+

detectados en los últimos análisis de gitleaks

+
+ +
+
+ Cobertura de remediación +
+

+ {data.kpis.remediationCoveragePercent !== null + ? `${data.kpis.remediationCoveragePercent}%` + : '—'} +

+

CVEs con una versión corregida disponible

+
+
+ +
+
+
+

Distribución de severidad

+

Vulnerabilidades del último análisis de cada servicio.

+ {#if totalSeverity === 0} +

No se han detectado vulnerabilidades.

+ {:else} +
+
+ +
+ {totalSeverity} + Total +
+
+
    + {#each Object.entries(severityColors) as [key, color] (key)} +
  • + + + {key} + + {data.severityBreakdown[key]} +
  • + {/each} +
+
+ {/if} +
+ +
+

Security Policy

+

+ Umbrales de aceptación y políticas de seguridad para este proyecto. +

+
+ Próximamente +
+
+
+ +
+

+ CVEs más críticos +

+ {#if data.topCves.length === 0} +

No se han detectado CVEs en este proyecto.

+ {:else} + + + Ver todos los CVEs → + + {/if} +
+
+ +
+
+

+ Servicios con más riesgo +

+ {#if data.riskiestServices.length === 0} +

Ningún servicio tiene vulnerabilidades detectadas.

+ {:else} + + {/if} +
+ +
+

+ Servicios sin análisis recientes +

+

Sin un escaneo completado en los últimos 30 días.

+ {#if data.staleServices.length === 0} +

Todos los servicios tienen análisis recientes.

+ {:else} + + {/if} +
+
+
From 5551d654250dddf57e6848ac25a1995e28ca736f Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 13:25:55 +0200 Subject: [PATCH 15/15] remove upload service --- .../services/[serviceSlug]/+page.svelte | 102 +----------------- 1 file changed, 1 insertion(+), 101 deletions(-) diff --git a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte index a79248f..565b732 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/services/[serviceSlug]/+page.svelte @@ -1,7 +1,7 @@ @@ -74,16 +42,6 @@ > Ver histórico - - -
{ - uploading = true; - return async ({ update }) => { - await update(); - uploading = false; - }; - }} - class="mt-4 space-y-4" - > - {#if uploadError}

- {uploadError} -

{/if} - - -
- -
-
- - -{/if}