From 672e9856792b029acbfb011cca4bafa83aa3a91c Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 14:27:33 +0200 Subject: [PATCH 1/9] security policy crud --- src/lib/code-report/security-policy.ts | 163 ++++++ src/lib/components/AppSidebar.svelte | 5 + .../code-report/SecurityPolicyForm.svelte | 519 ++++++++++++++++++ src/lib/database/schemas.ts | 35 ++ .../code-report-security-policy.service.ts | 246 +++++++++ .../code-report-security-policy.domain.ts | 54 ++ src/modules/code-report/index.ts | 6 + .../code-report-security-policy.repository.ts | 87 +++ .../security-policy/+page.server.ts | 51 ++ .../code-report/security-policy/+page.svelte | 156 ++++++ .../security-policy/[id]/+page.server.ts | 102 ++++ .../security-policy/[id]/+page.svelte | 212 +++++++ .../security-policy/new/+page.server.ts | 67 +++ .../security-policy/new/+page.svelte | 29 + 14 files changed, 1732 insertions(+) create mode 100644 src/lib/code-report/security-policy.ts create mode 100644 src/lib/components/code-report/SecurityPolicyForm.svelte create mode 100644 src/modules/code-report/application/code-report-security-policy.service.ts create mode 100644 src/modules/code-report/domain/code-report-security-policy.domain.ts create mode 100644 src/modules/code-report/infrastructure/repositories/code-report-security-policy.repository.ts create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.server.ts create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.server.ts create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.server.ts create mode 100644 src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte diff --git a/src/lib/code-report/security-policy.ts b/src/lib/code-report/security-policy.ts new file mode 100644 index 0000000..ebacbcc --- /dev/null +++ b/src/lib/code-report/security-policy.ts @@ -0,0 +1,163 @@ +export const SECURITY_POLICY_TYPES = [ + 'vulnerabilities', + 'license', + 'code_coverage', + 'secrets', +] as const; + +export type SecurityPolicyType = (typeof SECURITY_POLICY_TYPES)[number]; + +export const SECURITY_POLICY_ENFORCEMENTS = ['warn', 'block'] as const; +export type SecurityPolicyEnforcement = (typeof SECURITY_POLICY_ENFORCEMENTS)[number]; + +export const SECURITY_POLICY_SCOPE_MODES = ['all', 'services', 'tags'] as const; +export type SecurityPolicyScopeMode = (typeof SECURITY_POLICY_SCOPE_MODES)[number]; + +export type SecurityPolicyScope = { + mode: SecurityPolicyScopeMode; + services: string[]; + tags: string[]; +}; + +export type VulnerabilitiesRules = { + maxCritical: number | null; + maxHigh: number | null; + maxMedium: number | null; + maxLow: number | null; + minCvssScore: number | null; + ignoreUnfixed: boolean; + maxAgeDays: number | null; + ignoredCves: string[]; +}; + +export type LicenseRules = { + mode: 'allowlist' | 'denylist'; + licenses: string[]; + allowUnknown: boolean; +}; + +export type CodeCoverageRules = { + minTotalCoverage: number | null; + minPatchCoverage: number | null; + allowCoverageDrop: boolean; +}; + +export type SecretsRules = { + maxSecrets: number | null; + blockVerifiedOnly: boolean; + ignoredRules: string[]; +}; + +export type SecurityPolicyRules = + | VulnerabilitiesRules + | LicenseRules + | CodeCoverageRules + | SecretsRules + | Record; + +export type SecurityPolicy = { + id: string; + projectId: string; + slug: string; + name: string; + description: string | null; + type: SecurityPolicyType; + enabled: boolean; + enforcement: SecurityPolicyEnforcement; + scope: SecurityPolicyScope; + rules: SecurityPolicyRules; + createdAt: string; + updatedAt: string; +}; + +export const SECURITY_POLICY_TYPE_META: Record< + SecurityPolicyType, + { label: string; description: string; available: boolean } +> = { + vulnerabilities: { + label: 'Vulnerabilidades', + description: 'Umbrales por severidad, CVSS y CVEs ignorados.', + available: true, + }, + license: { + label: 'Licencias', + description: 'Licencias permitidas o denegadas en las dependencias.', + available: false, + }, + code_coverage: { + label: 'Cobertura de código', + description: 'Mínimos de cobertura total y de los cambios.', + available: false, + }, + secrets: { + label: 'Secretos', + description: 'Secretos expuestos detectados en el repositorio.', + available: false, + }, +}; + +export const SECURITY_POLICY_ENFORCEMENT_META: Record< + SecurityPolicyEnforcement, + { label: string; description: string; available: boolean } +> = { + warn: { + label: 'Avisar', + description: 'Marca el análisis como incumplido pero no bloquea el pipeline.', + available: true, + }, + block: { + label: 'Bloquear', + description: 'Falla el pipeline cuando el análisis incumple la política.', + available: false, + }, +}; + +export function defaultRulesFor(type: SecurityPolicyType): SecurityPolicyRules { + switch (type) { + case 'vulnerabilities': + return { + maxCritical: 0, + maxHigh: null, + maxMedium: null, + maxLow: null, + minCvssScore: null, + ignoreUnfixed: false, + maxAgeDays: null, + ignoredCves: [], + } satisfies VulnerabilitiesRules; + case 'license': + return { mode: 'denylist', licenses: [], allowUnknown: true } satisfies LicenseRules; + case 'code_coverage': + return { + minTotalCoverage: 80, + minPatchCoverage: null, + allowCoverageDrop: false, + } satisfies CodeCoverageRules; + case 'secrets': + return { + maxSecrets: 0, + blockVerifiedOnly: false, + ignoredRules: [], + } satisfies SecretsRules; + } +} + +export function defaultScope(): SecurityPolicyScope { + return { mode: 'all', services: [], tags: [] }; +} + +export function isSecurityPolicyType(value: unknown): value is SecurityPolicyType { + return SECURITY_POLICY_TYPES.includes(value as SecurityPolicyType); +} + +export function describeScope(scope: SecurityPolicyScope): string { + if (scope.mode === 'services') { + return scope.services.length > 0 + ? `${scope.services.length} servicio(s)` + : 'Sin servicios seleccionados'; + } + if (scope.mode === 'tags') { + return scope.tags.length > 0 ? scope.tags.join(', ') : 'Sin tags seleccionados'; + } + return 'Todos los servicios'; +} diff --git a/src/lib/components/AppSidebar.svelte b/src/lib/components/AppSidebar.svelte index 92a5c59..a0d0a38 100644 --- a/src/lib/components/AppSidebar.svelte +++ b/src/lib/components/AppSidebar.svelte @@ -105,6 +105,11 @@ }, { label: 'Services', href: `${projectBase}/code-report/services`, icon: Layers }, { label: 'CVEs', href: `${projectBase}/code-report/cves`, icon: ShieldAlert }, + { + label: 'Security Policies', + href: `${projectBase}/code-report/security-policy`, + icon: Shield, + }, { 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/lib/components/code-report/SecurityPolicyForm.svelte b/src/lib/components/code-report/SecurityPolicyForm.svelte new file mode 100644 index 0000000..dfeeca2 --- /dev/null +++ b/src/lib/components/code-report/SecurityPolicyForm.svelte @@ -0,0 +1,519 @@ + + +
{ + submitting = true; + return async ({ update }) => { + await update(); + submitting = false; + }; + }} + class="space-y-4" +> + + + {#if errorMessage} +

{errorMessage}

+ {/if} + +
+

1. Información general

+

Identifica la política dentro del proyecto.

+ +
+ + +
+ + +
+ +
+

2. Tipo de política

+

Determina qué reglas puedes configurar.

+ +
+ {#each SECURITY_POLICY_TYPES as value} + + {/each} +
+
+ +
+

3. Alcance

+

A qué servicios del proyecto se aplica.

+ +
+ {#each scopeModes as option} + + {/each} +
+ + {#if scope.mode === 'services'} +
+ {#each services as service (service.id)} + + {:else} +

No hay servicios en este proyecto.

+ {/each} +
+ {:else if scope.mode === 'tags'} +
+ {#each tags as tag} + + {:else} +

No hay tags definidos en los servicios.

+ {/each} +
+ {/if} +
+ +
+

4. Reglas

+

+ Deja un campo vacío para no aplicar ese límite. +

+ + {#if type === 'vulnerabilities'} +
+ {#each severityFields as field} + + {/each} +
+ +
+ + +
+ + + +
+ CVEs ignorados +
+ { + if (event.key === 'Enter') { + event.preventDefault(); + addToRuleList('ignoredCves', ignoredCveInput); + ignoredCveInput = ''; + } + }} + class="flex-1 rounded-lg border border-slate-200 px-3 py-2 text-sm focus:border-slate-400 focus:outline-none" + /> + +
+
+ {#each rulesByType.vulnerabilities.ignoredCves as cve} + + {cve} + + + {/each} +
+
+ {:else if type === 'license'} +
+ {#each licenseModes as option} + + {/each} +
+
+ + +
+
+ {#each rulesByType.license.licenses as license} + + {license} + + + {/each} +
+ + {:else if type === 'code_coverage'} +
+ + +
+ + {:else if type === 'secrets'} + + +
+ + +
+
+ {#each rulesByType.secrets.ignoredRules as rule} + + {rule} + + + {/each} +
+ {/if} +
+ +
+

5. Aplicación

+

Qué ocurre cuando un análisis incumple la política.

+ +
+ {#each SECURITY_POLICY_ENFORCEMENTS as value} + + {/each} +
+ + +
+ +
+ + +
+
diff --git a/src/lib/database/schemas.ts b/src/lib/database/schemas.ts index fd0d3c4..c382ad7 100644 --- a/src/lib/database/schemas.ts +++ b/src/lib/database/schemas.ts @@ -149,6 +149,33 @@ export const CodeReportAnalysisEntity = entity('code_report_analyses', { .$defaultFn(() => new Date().toISOString()), }); +export const CodeReportSecurityPolicyEntity = entity('code_report_security_policies', { + id: uuid().primaryKey(), + projectId: uuid().notNull(), + slug: text().notNull(), + name: text().notNull(), + description: text(), + // vulnerabilities | license | code_coverage | secrets + type: text().notNull().default('vulnerabilities'), + enabled: bool().notNull().default(true), + // warn | block + enforcement: text().notNull().default('warn'), + // { mode: 'all' | 'services' | 'tags', services: string[], tags: string[] } + scope: json() + .notNull() + .$defaultFn(() => ({ mode: 'all', services: [], tags: [] })), + // type-specific configuration, see $lib/code-report/security-policy + rules: json() + .notNull() + .$defaultFn(() => ({})), + createdAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), + updatedAt: timestamp() + .notNull() + .$defaultFn(() => new Date().toISOString()), +}); + relations.for(ProjectEntity, ({ one, many }) => ({ roles: many(RoleEntity, { fields: ['id'], references: ['projectId'] }), organization: one(OrganizationEntity, { fields: ['organizationId'], references: ['id'] }), @@ -157,6 +184,14 @@ relations.for(ProjectEntity, ({ one, many }) => ({ fields: ['id'], references: ['projectId'], }), + codeReportSecurityPolicies: many(CodeReportSecurityPolicyEntity, { + fields: ['id'], + references: ['projectId'], + }), +})); + +relations.for(CodeReportSecurityPolicyEntity, ({ one }) => ({ + project: one(ProjectEntity, { fields: ['projectId'], references: ['id'] }), })); relations.for(CodeReportServiceEntity, ({ one, many }) => ({ diff --git a/src/modules/code-report/application/code-report-security-policy.service.ts b/src/modules/code-report/application/code-report-security-policy.service.ts new file mode 100644 index 0000000..4170f31 --- /dev/null +++ b/src/modules/code-report/application/code-report-security-policy.service.ts @@ -0,0 +1,246 @@ +import crypto from 'crypto'; +import { CodeReportSecurityPolicyRepository } from '../infrastructure/repositories/code-report-security-policy.repository'; +import { + defaultRulesFor, + defaultScope, + isSecurityPolicyType, + SECURITY_POLICY_ENFORCEMENTS, + SECURITY_POLICY_SCOPE_MODES, + type SecurityPolicyEnforcement, + type SecurityPolicyRules, + type SecurityPolicyScope, + type SecurityPolicyType, +} from '$lib/code-report/security-policy'; + +export type SecurityPolicyInput = { + name: string; + slug?: string; + description?: string; + type: string; + enabled?: boolean; + enforcement?: string; + scope?: Partial; + rules?: Record; +}; + +export class CodeReportSecurityPolicyService { + constructor(private readonly repository: CodeReportSecurityPolicyRepository) {} + + async listByProject(projectId: string) { + const policies = await this.repository.findByProjectId(projectId); + return policies.map((policy) => policy.toJson()); + } + + async getById(id: string) { + const policy = await this.repository.findById(id); + if (!policy) { + throw new Error('Security policy not found'); + } + return policy.toJson(); + } + + async create(projectId: string, input: SecurityPolicyInput) { + const name = input.name?.trim(); + if (!name) { + throw new Error('El nombre de la política es obligatorio'); + } + + const type = this.normalizeType(input.type); + const slug = this.normalizeSlug(input.slug || name); + if (!slug) { + throw new Error('El slug de la política es obligatorio'); + } + + const existing = await this.repository.findBySlug(projectId, slug); + if (existing) { + throw new Error('Ya existe una política con este slug en el proyecto'); + } + + const id = crypto.randomUUID(); + await this.repository.create({ + id, + projectId, + slug, + name, + description: input.description?.trim() || undefined, + type, + enabled: input.enabled ?? true, + enforcement: this.normalizeEnforcement(input.enforcement), + scope: this.normalizeScope(input.scope), + rules: this.normalizeRules(type, input.rules), + }); + + return this.getById(id); + } + + async update(id: string, changes: Partial) { + const policy = await this.repository.findById(id); + if (!policy) { + throw new Error('Security policy not found'); + } + + const patch: Parameters[1] = {}; + + if (changes.name !== undefined) { + const name = changes.name.trim(); + if (!name) { + throw new Error('El nombre de la política es obligatorio'); + } + patch.name = name; + } + + if (changes.slug !== undefined) { + const slug = this.normalizeSlug(changes.slug); + if (!slug) { + throw new Error('El slug de la política es obligatorio'); + } + const existing = await this.repository.findBySlug(policy.projectId, slug); + if (existing && existing.id !== id) { + throw new Error('Ya existe una política con este slug en el proyecto'); + } + patch.slug = slug; + } + + if (changes.description !== undefined) { + patch.description = changes.description.trim(); + } + + const type = changes.type !== undefined ? this.normalizeType(changes.type) : policy.type; + if (changes.type !== undefined) { + patch.type = type; + } + + if (changes.enabled !== undefined) { + patch.enabled = Boolean(changes.enabled); + } + + if (changes.enforcement !== undefined) { + patch.enforcement = this.normalizeEnforcement(changes.enforcement); + } + + if (changes.scope !== undefined) { + patch.scope = this.normalizeScope(changes.scope); + } + + if (changes.rules !== undefined || changes.type !== undefined) { + patch.rules = this.normalizeRules(type, changes.rules ?? policy.rules); + } + + await this.repository.update(id, patch); + return this.getById(id); + } + + async setEnabled(id: string, enabled: boolean) { + return this.update(id, { enabled }); + } + + async delete(id: string) { + const policy = await this.repository.findById(id); + if (!policy) { + throw new Error('Security policy not found'); + } + await this.repository.deleteById(id); + } + + private normalizeType(value: unknown): SecurityPolicyType { + if (!isSecurityPolicyType(value)) { + throw new Error('Tipo de política no soportado'); + } + return value; + } + + private normalizeEnforcement(value: unknown): SecurityPolicyEnforcement { + return SECURITY_POLICY_ENFORCEMENTS.includes(value as SecurityPolicyEnforcement) + ? (value as SecurityPolicyEnforcement) + : 'warn'; + } + + private normalizeScope(value: Partial | undefined): SecurityPolicyScope { + const scope = { ...defaultScope(), ...(value ?? {}) }; + const mode = SECURITY_POLICY_SCOPE_MODES.includes(scope.mode) ? scope.mode : 'all'; + return { + mode, + services: mode === 'services' ? this.normalizeStringList(scope.services) : [], + tags: mode === 'tags' ? this.normalizeStringList(scope.tags) : [], + }; + } + + private normalizeRules( + type: SecurityPolicyType, + rules: Record | SecurityPolicyRules | undefined, + ): SecurityPolicyRules { + const input = (rules ?? {}) as Record; + + if (type === 'vulnerabilities') { + return { + maxCritical: this.normalizeCount(input.maxCritical), + maxHigh: this.normalizeCount(input.maxHigh), + maxMedium: this.normalizeCount(input.maxMedium), + maxLow: this.normalizeCount(input.maxLow), + minCvssScore: this.normalizeRange(input.minCvssScore, 0, 10), + ignoreUnfixed: Boolean(input.ignoreUnfixed), + maxAgeDays: this.normalizeCount(input.maxAgeDays), + ignoredCves: this.normalizeStringList(input.ignoredCves).map((cve) => cve.toUpperCase()), + }; + } + + if (type === 'license') { + return { + mode: input.mode === 'allowlist' ? 'allowlist' : 'denylist', + licenses: this.normalizeStringList(input.licenses), + allowUnknown: Boolean(input.allowUnknown), + }; + } + + if (type === 'code_coverage') { + return { + minTotalCoverage: this.normalizeRange(input.minTotalCoverage, 0, 100), + minPatchCoverage: this.normalizeRange(input.minPatchCoverage, 0, 100), + allowCoverageDrop: Boolean(input.allowCoverageDrop), + }; + } + + if (type === 'secrets') { + return { + maxSecrets: this.normalizeCount(input.maxSecrets), + blockVerifiedOnly: Boolean(input.blockVerifiedOnly), + ignoredRules: this.normalizeStringList(input.ignoredRules), + }; + } + + return defaultRulesFor(type); + } + + private normalizeCount(value: unknown): number | null { + if (value === null || value === undefined || value === '') return null; + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed < 0) return null; + return Math.floor(parsed); + } + + private normalizeRange(value: unknown, min: number, max: number): number | null { + if (value === null || value === undefined || value === '') return null; + const parsed = Number(value); + if (!Number.isFinite(parsed)) return null; + return Math.min(max, Math.max(min, parsed)); + } + + private normalizeStringList(value: unknown): string[] { + if (!Array.isArray(value)) return []; + return [ + ...new Set( + value + .map((item) => String(item).trim()) + .filter((item) => item.length > 0), + ), + ]; + } + + private normalizeSlug(value: string): string { + return value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + } +} diff --git a/src/modules/code-report/domain/code-report-security-policy.domain.ts b/src/modules/code-report/domain/code-report-security-policy.domain.ts new file mode 100644 index 0000000..1df557d --- /dev/null +++ b/src/modules/code-report/domain/code-report-security-policy.domain.ts @@ -0,0 +1,54 @@ +import { Domain } from '$lib/server/domain/domain'; +import { ProjectDomain } from '../../projects/domain/project.domain'; +import { + defaultScope, + type SecurityPolicyEnforcement, + type SecurityPolicyRules, + type SecurityPolicyScope, + type SecurityPolicyType, +} from '$lib/code-report/security-policy'; + +export class CodeReportSecurityPolicyDomain extends Domain { + public projectId: string = ''; + public slug: string = ''; + public name: string = ''; + public description?: string | null = null; + public type: SecurityPolicyType = 'vulnerabilities'; + public enabled: boolean = true; + public enforcement: SecurityPolicyEnforcement = 'warn'; + public scope: SecurityPolicyScope = defaultScope(); + public rules: SecurityPolicyRules = {}; + public project: ProjectDomain | null = null; + + constructor(data: any) { + super(data); + this.projectId = data.projectId; + this.slug = data.slug; + this.name = data.name; + this.description = data.description; + this.type = data.type ?? 'vulnerabilities'; + this.enabled = data.enabled ?? true; + this.enforcement = data.enforcement ?? 'warn'; + this.scope = { ...defaultScope(), ...(data.scope ?? {}) }; + this.rules = data.rules ?? {}; + this.project = data.project ? new ProjectDomain(data.project) : null; + } + + toJson() { + return { + id: this.id, + projectId: this.projectId, + slug: this.slug, + name: this.name, + description: this.description ?? null, + type: this.type, + enabled: this.enabled, + enforcement: this.enforcement, + scope: this.scope, + rules: this.rules, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + project: this.project ? this.project.toJson() : null, + }; + } +} diff --git a/src/modules/code-report/index.ts b/src/modules/code-report/index.ts index d5a1654..1ef39b7 100644 --- a/src/modules/code-report/index.ts +++ b/src/modules/code-report/index.ts @@ -1,12 +1,15 @@ import { CodeReportService } from './application/code-report.service'; import { CodeReportAnalysisService } from './application/code-report-analysis.service'; import { CodeReportCveService } from './application/code-report-cve.service'; +import { CodeReportSecurityPolicyService } from './application/code-report-security-policy.service'; import { CodeReportServiceRepository } from './infrastructure/repositories/code-report-service.repository'; import { CodeReportAnalysisRepository } from './infrastructure/repositories/code-report-analysis.repository'; +import { CodeReportSecurityPolicyRepository } from './infrastructure/repositories/code-report-security-policy.repository'; import { projectService } from '../projects'; const codeReportServiceRepository = new CodeReportServiceRepository(); const codeReportAnalysisRepository = new CodeReportAnalysisRepository(); +const codeReportSecurityPolicyRepository = new CodeReportSecurityPolicyRepository(); export const codeReportAnalysisService = new CodeReportAnalysisService( codeReportAnalysisRepository, @@ -21,3 +24,6 @@ export const codeReportCveService = new CodeReportCveService( codeReportService, codeReportAnalysisService, ); +export const codeReportSecurityPolicyService = new CodeReportSecurityPolicyService( + codeReportSecurityPolicyRepository, +); diff --git a/src/modules/code-report/infrastructure/repositories/code-report-security-policy.repository.ts b/src/modules/code-report/infrastructure/repositories/code-report-security-policy.repository.ts new file mode 100644 index 0000000..4309540 --- /dev/null +++ b/src/modules/code-report/infrastructure/repositories/code-report-security-policy.repository.ts @@ -0,0 +1,87 @@ +import { Repository } from '$lib/server/infra/repository'; +import { CodeReportSecurityPolicyDomain } from '../../domain/code-report-security-policy.domain'; +import { CodeReportSecurityPolicyEntity } from '$lib/database/schemas'; +import type { + SecurityPolicyEnforcement, + SecurityPolicyRules, + SecurityPolicyScope, + SecurityPolicyType, +} from '$lib/code-report/security-policy'; + +export class CodeReportSecurityPolicyRepository extends Repository { + async findByProjectId(projectId: string): Promise { + const result = await this.db + .select() + .from(CodeReportSecurityPolicyEntity) + .where({ projectId }) + .orderBy('createdAt', 'desc'); + return result.rows.map((row: any) => new CodeReportSecurityPolicyDomain(row)); + } + + async findById(id: string): Promise { + const result = await this.db + .with({ project: true }) + .select() + .from(CodeReportSecurityPolicyEntity) + .where({ id }) + .limit(1); + const row = result.rows[0]; + return row ? new CodeReportSecurityPolicyDomain(row) : null; + } + + async findBySlug( + projectId: string, + slug: string, + ): Promise { + const result = await this.db + .select() + .from(CodeReportSecurityPolicyEntity) + .where({ projectId, slug }) + .limit(1); + const row = result.rows[0]; + return row ? new CodeReportSecurityPolicyDomain(row) : null; + } + + async create(input: { + id: string; + projectId: string; + slug: string; + name: string; + description?: string; + type: SecurityPolicyType; + enabled: boolean; + enforcement: SecurityPolicyEnforcement; + scope: SecurityPolicyScope; + rules: SecurityPolicyRules; + }): Promise { + await this.db.insert(CodeReportSecurityPolicyEntity).values({ + ...input, + description: input.description, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + } + + async update( + id: string, + changes: { + name?: string; + slug?: string; + description?: string; + type?: SecurityPolicyType; + enabled?: boolean; + enforcement?: SecurityPolicyEnforcement; + scope?: SecurityPolicyScope; + rules?: SecurityPolicyRules; + }, + ): Promise { + await this.db + .update(CodeReportSecurityPolicyEntity) + .set({ ...changes, updatedAt: new Date().toISOString() }) + .where({ id }); + } + + async deleteById(id: string): Promise { + await this.db.delete(CodeReportSecurityPolicyEntity).where({ id }); + } +} diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.server.ts new file mode 100644 index 0000000..899f312 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.server.ts @@ -0,0 +1,51 @@ +import { error, fail } from '@sveltejs/kit'; +import { cancanService } from '../../../../../../../modules/auth'; +import { codeReportSecurityPolicyService } from '../../../../../../../modules/code-report'; +import { projectService } from '../../../../../../../modules/projects'; + +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'); + } + + return { policies: await codeReportSecurityPolicyService.listByProject(project.id) }; +} + +export const actions = { + toggle: async ({ request, params, locals }) => { + const project = await projectService.getProjectBySlug(params.slug); + + const canUpdate = await cancanService.canSessionUser(locals.user, 'openreport:update', { + scope: 'project', + projectId: project.id, + organizationId: project.organization?.id, + }); + + if (!canUpdate) { + return fail(403, { error: 'Forbidden' }); + } + + const formData = await request.formData(); + const id = String(formData.get('id') || ''); + const enabled = String(formData.get('enabled') || '') === 'true'; + + try { + const policy = await codeReportSecurityPolicyService.getById(id); + if (policy.projectId !== project.id) { + return fail(404, { error: 'Security policy not found' }); + } + await codeReportSecurityPolicyService.setEnabled(id, enabled); + return { success: true }; + } catch (err) { + return fail(400, { error: err instanceof Error ? err.message : 'No se pudo actualizar' }); + } + }, +}; diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte new file mode 100644 index 0000000..ab3a347 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte @@ -0,0 +1,156 @@ + + +Security Policies - Code Report - GitVault Suite + +
+
+
+ + +
+ + + Nueva política + +
+ + {#if form?.error} +

{form.error}

+ {/if} + + {#if filteredPolicies.length === 0} +
+ +

+ {data.policies.length === 0 + ? 'Todavía no hay políticas de seguridad en este proyecto.' + : 'Sin resultados para tu búsqueda.'} +

+ {#if data.policies.length === 0} + + Crear la primera política + + {/if} +
+ {:else} + + {/if} +
diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.server.ts new file mode 100644 index 0000000..2168203 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.server.ts @@ -0,0 +1,102 @@ +import { error, fail, redirect } from '@sveltejs/kit'; +import { cancanService } from '../../../../../../../../modules/auth'; +import { + codeReportSecurityPolicyService, + codeReportService, +} from '../../../../../../../../modules/code-report'; +import { projectService } from '../../../../../../../../modules/projects'; + +export async function load({ parent, params, 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 policy = await codeReportSecurityPolicyService.getById(params.id).catch(() => null); + if (!policy || policy.projectId !== project.id) { + throw error(404, 'Security policy not found'); + } + + const services = await codeReportService.listByProject(project.id); + + return { + policy, + services: services.map((service) => ({ + id: service.id, + slug: service.slug, + name: service.name, + tags: service.tags, + })), + tags: [...new Set(services.flatMap((service) => service.tags))].sort(), + }; +} + +async function resolvePolicy(slugParam: string, id: string) { + const project = await projectService.getProjectBySlug(slugParam); + const policy = await codeReportSecurityPolicyService.getById(id).catch(() => null); + if (!policy || policy.projectId !== project.id) { + return { project, policy: null }; + } + return { project, policy }; +} + +export const actions = { + update: async ({ request, params, locals }) => { + const { project, policy } = await resolvePolicy(params.slug, params.id); + + const canUpdate = await cancanService.canSessionUser(locals.user, 'openreport:update', { + scope: 'project', + projectId: project.id, + organizationId: project.organization?.id, + }); + + if (!canUpdate) { + return fail(403, { error: 'Forbidden' }); + } + + if (!policy) { + return fail(404, { error: 'Security policy not found' }); + } + + const formData = await request.formData(); + + try { + const input = JSON.parse(String(formData.get('payload') || '')); + await codeReportSecurityPolicyService.update(params.id, input); + return { success: true }; + } catch (err) { + return fail(400, { + error: err instanceof Error ? err.message : 'No se pudo actualizar la política', + }); + } + }, + + delete: async ({ params, locals }) => { + const { project, policy } = await resolvePolicy(params.slug, params.id); + + const canDelete = await cancanService.canSessionUser(locals.user, 'openreport:delete', { + scope: 'project', + projectId: project.id, + organizationId: project.organization?.id, + }); + + if (!canDelete) { + return fail(403, { error: 'Forbidden' }); + } + + if (!policy) { + return fail(404, { error: 'Security policy not found' }); + } + + await codeReportSecurityPolicyService.delete(params.id); + + throw redirect(303, `/org/${params.org}/projects/${params.slug}/code-report/security-policy`); + }, +}; diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte new file mode 100644 index 0000000..8bc6532 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte @@ -0,0 +1,212 @@ + + +{policy.name} - Security Policies - GitVault Suite + +
+
+ + Volver a políticas + +
+ + +
+
+ + {#if editing} + {#key policy.updatedAt} + + {/key} + {:else} +
+
+
+

{policy.name}

+

{policy.slug}

+ {#if policy.description} +

{policy.description}

+ {/if} +
+ + {policy.enabled ? 'Activa' : 'Inactiva'} + +
+ +
+
+

Tipo

+

+ {SECURITY_POLICY_TYPE_META[policy.type].label} +

+
+
+

Aplicación

+

+ {SECURITY_POLICY_ENFORCEMENT_META[policy.enforcement].label} +

+

+ {SECURITY_POLICY_ENFORCEMENT_META[policy.enforcement].description} +

+
+
+

Alcance

+

{describeScope(policy.scope)}

+ {#if scopedServices.length > 0} +
+ {#each scopedServices as service} + + {service.name} + + {/each} +
+ {/if} +
+
+
+ +
+

Reglas

+
+ {#each ruleEntries(policy.rules) as [key, value]} +
+
+ {ruleLabels[key] ?? key} +
+
{formatRuleValue(value)}
+
+ {/each} +
+
+ +

+ Creada el {new Date(policy.createdAt).toLocaleString()} · Actualizada el + {new Date(policy.updatedAt).toLocaleString()} +

+ {/if} +
+ +{#if deleteModalOpen} +
+
+
+

Borrar política

+ +
+

+ Vas a borrar {policy.name}. Esta acción no se puede deshacer. +

+
{ + deleting = true; + return async ({ update }) => { + await update(); + deleting = false; + }; + }} + class="mt-5 flex justify-end gap-2" + > + + +
+
+
+{/if} diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.server.ts new file mode 100644 index 0000000..17d192a --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.server.ts @@ -0,0 +1,67 @@ +import { error, fail, redirect } from '@sveltejs/kit'; +import { cancanService } from '../../../../../../../../modules/auth'; +import { + codeReportSecurityPolicyService, + codeReportService, +} from '../../../../../../../../modules/code-report'; +import { projectService } from '../../../../../../../../modules/projects'; + +export async function load({ parent, locals }) { + const { project } = await parent(); + + const canCreate = await cancanService.canSessionUser(locals.user, 'openreport:create', { + scope: 'project', + projectId: project.id, + organizationId: project.organization?.id, + }); + + if (!canCreate) { + throw error(403, 'Forbidden'); + } + + const services = await codeReportService.listByProject(project.id); + + return { + services: services.map((service) => ({ + id: service.id, + slug: service.slug, + name: service.name, + tags: service.tags, + })), + tags: [...new Set(services.flatMap((service) => service.tags))].sort(), + }; +} + +export const actions = { + create: async ({ request, params, locals }) => { + const project = await projectService.getProjectBySlug(params.slug); + + const canCreate = await cancanService.canSessionUser(locals.user, 'openreport:create', { + scope: 'project', + projectId: project.id, + organizationId: project.organization?.id, + }); + + if (!canCreate) { + return fail(403, { error: 'Forbidden' }); + } + + const formData = await request.formData(); + const payload = String(formData.get('payload') || ''); + + let policy; + try { + const input = JSON.parse(payload); + policy = await codeReportSecurityPolicyService.create(project.id, input); + } catch (err) { + return fail(400, { + error: err instanceof Error ? err.message : 'No se pudo crear la política', + }); + } + + throw redirect( + 303, + `/org/${params.org}/projects/${params.slug}/code-report/security-policy/${policy.id}`, + ); + }, +}; diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte new file mode 100644 index 0000000..2ee993a --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte @@ -0,0 +1,29 @@ + + +Nueva política de seguridad - GitVault Suite + + From cc627a6ed35422dd7dcd3e35db9af8e9d76a3ea9 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 14:38:36 +0200 Subject: [PATCH 2/9] policy violation in analysis --- src/lib/code-report/policy-evaluation.ts | 236 ++++++++++++++++++ .../components/CodeReportVisualization.svelte | 20 ++ .../SecurityPolicyComplianceCard.svelte | 177 +++++++++++++ .../history/[analysisID]/+page.server.ts | 2 + .../history/[analysisID]/+page.svelte | 6 +- .../services/[serviceSlug]/+page.server.ts | 5 +- .../services/[serviceSlug]/+page.svelte | 6 +- 7 files changed, 449 insertions(+), 3 deletions(-) create mode 100644 src/lib/code-report/policy-evaluation.ts create mode 100644 src/lib/components/code-report/SecurityPolicyComplianceCard.svelte diff --git a/src/lib/code-report/policy-evaluation.ts b/src/lib/code-report/policy-evaluation.ts new file mode 100644 index 0000000..8d0af44 --- /dev/null +++ b/src/lib/code-report/policy-evaluation.ts @@ -0,0 +1,236 @@ +import type { SecretFinding, VulnerabilityFinding } from './analysis-summary'; +import type { + SecurityPolicy, + SecurityPolicyType, + VulnerabilitiesRules, +} from './security-policy'; + +export type PolicyCheck = { + key: string; + label: string; + actual: number; + limit: number | null; + passed: boolean; + message: string; + samples: string[]; +}; + +export type PolicyEvaluation = { + policyId: string; + policyName: string; + policySlug: string; + type: SecurityPolicyType; + enforcement: SecurityPolicy['enforcement']; + applies: boolean; + evaluable: boolean; + skippedReason: string | null; + passed: boolean; + checks: PolicyCheck[]; + violations: PolicyCheck[]; +}; + +export type PolicyComplianceReport = { + status: 'no_policies' | 'not_applicable' | 'compliant' | 'violated'; + evaluations: PolicyEvaluation[]; + passed: PolicyEvaluation[]; + failed: PolicyEvaluation[]; + totalViolations: number; + blockingFailures: number; +}; + +export type PolicyEvaluationContext = { + serviceId: string; + serviceTags: string[]; + vulnerabilities: VulnerabilityFinding[]; + secrets: SecretFinding[]; + hasVulnerabilityScan: boolean; + hasSecretScan: boolean; +}; + +const DAY_MS = 24 * 60 * 60 * 1000; + +export function policyApplies(policy: SecurityPolicy, context: PolicyEvaluationContext): boolean { + if (policy.scope.mode === 'services') return policy.scope.services.includes(context.serviceId); + if (policy.scope.mode === 'tags') + return policy.scope.tags.some((tag) => context.serviceTags.includes(tag)); + return true; +} + +function check( + key: string, + label: string, + actual: number, + limit: number | null, + samples: string[], + message: string, +): PolicyCheck { + return { + key, + label, + actual, + limit, + passed: limit === null || actual <= limit, + message, + samples: samples.slice(0, 5), + }; +} + +function relevantVulnerabilities(rules: VulnerabilitiesRules, findings: VulnerabilityFinding[]) { + const ignored = new Set(rules.ignoredCves.map((cve) => cve.toUpperCase())); + return findings.filter((finding) => { + if (ignored.has(finding.id.toUpperCase())) return false; + if (rules.ignoreUnfixed && !finding.fixedVersion) return false; + if (rules.minCvssScore !== null && (finding.cvssScore ?? 0) < rules.minCvssScore) return false; + return true; + }); +} + +function evaluateVulnerabilities( + rules: VulnerabilitiesRules, + findings: VulnerabilityFinding[], +): PolicyCheck[] { + const scoped = relevantVulnerabilities(rules, findings); + const bySeverity = (severity: VulnerabilityFinding['severity']) => + scoped.filter((finding) => finding.severity === severity); + + const severityChecks: { key: keyof VulnerabilitiesRules; label: string; severity: VulnerabilityFinding['severity'] }[] = + [ + { key: 'maxCritical', label: 'Vulnerabilidades críticas', severity: 'critical' }, + { key: 'maxHigh', label: 'Vulnerabilidades altas', severity: 'high' }, + { key: 'maxMedium', label: 'Vulnerabilidades medias', severity: 'medium' }, + { key: 'maxLow', label: 'Vulnerabilidades bajas', severity: 'low' }, + ]; + + const checks = severityChecks + .filter(({ key }) => rules[key] !== null && rules[key] !== undefined) + .map(({ key, label, severity }) => { + const matches = bySeverity(severity); + const limit = rules[key] as number; + return check( + String(key), + label, + matches.length, + limit, + matches.map((finding) => finding.id), + `${matches.length} encontradas · máximo permitido ${limit}`, + ); + }); + + if (rules.maxAgeDays !== null) { + const cutoff = Date.now() - rules.maxAgeDays * DAY_MS; + const stale = scoped.filter((finding) => { + if (!finding.publishedDate) return false; + const published = new Date(finding.publishedDate).getTime(); + return Number.isFinite(published) && published < cutoff; + }); + checks.push( + check( + 'maxAgeDays', + `Hallazgos con más de ${rules.maxAgeDays} días`, + stale.length, + 0, + stale.map((finding) => finding.id), + `${stale.length} vulnerabilidades publicadas hace más de ${rules.maxAgeDays} días`, + ), + ); + } + + return checks; +} + +function evaluateSecrets(rules: any, secrets: SecretFinding[]): PolicyCheck[] { + const ignored = new Set((rules.ignoredRules ?? []).map((rule: string) => rule.toLowerCase())); + const scoped = secrets.filter((secret) => !ignored.has(secret.ruleId.toLowerCase())); + + if (rules.maxSecrets === null || rules.maxSecrets === undefined) return []; + + return [ + check( + 'maxSecrets', + 'Secretos expuestos', + scoped.length, + rules.maxSecrets, + scoped.map((secret) => `${secret.ruleId} · ${secret.file}`), + `${scoped.length} secretos detectados · máximo permitido ${rules.maxSecrets}`, + ), + ]; +} + +export function evaluatePolicy( + policy: SecurityPolicy, + context: PolicyEvaluationContext, +): PolicyEvaluation { + const base = { + policyId: policy.id, + policyName: policy.name, + policySlug: policy.slug, + type: policy.type, + enforcement: policy.enforcement, + applies: policyApplies(policy, context), + }; + + const notEvaluable = (reason: string): PolicyEvaluation => ({ + ...base, + evaluable: false, + skippedReason: reason, + passed: true, + checks: [], + violations: [], + }); + + if (!base.applies) return notEvaluable('Fuera del alcance de este servicio'); + + let checks: PolicyCheck[]; + + if (policy.type === 'vulnerabilities') { + if (!context.hasVulnerabilityScan) return notEvaluable('Sin análisis de vulnerabilidades'); + checks = evaluateVulnerabilities(policy.rules as VulnerabilitiesRules, context.vulnerabilities); + } else if (policy.type === 'secrets') { + if (!context.hasSecretScan) return notEvaluable('Sin análisis de secretos'); + checks = evaluateSecrets(policy.rules, context.secrets); + } else { + return notEvaluable('Tipo de política todavía no evaluable'); + } + + if (checks.length === 0) return notEvaluable('La política no define reglas aplicables'); + + const violations = checks.filter((item) => !item.passed); + + return { + ...base, + evaluable: true, + skippedReason: null, + passed: violations.length === 0, + checks, + violations, + }; +} + +export function evaluatePolicies( + policies: SecurityPolicy[], + context: PolicyEvaluationContext, +): PolicyComplianceReport { + const active = policies.filter((policy) => policy.enabled); + const evaluations = active.map((policy) => evaluatePolicy(policy, context)); + const evaluable = evaluations.filter((evaluation) => evaluation.evaluable); + const failed = evaluable.filter((evaluation) => !evaluation.passed); + const passed = evaluable.filter((evaluation) => evaluation.passed); + + const status: PolicyComplianceReport['status'] = + active.length === 0 + ? 'no_policies' + : evaluable.length === 0 + ? 'not_applicable' + : failed.length > 0 + ? 'violated' + : 'compliant'; + + return { + status, + evaluations, + passed, + failed, + totalViolations: failed.reduce((total, evaluation) => total + evaluation.violations.length, 0), + blockingFailures: failed.filter((evaluation) => evaluation.enforcement === 'block').length, + }; +} diff --git a/src/lib/components/CodeReportVisualization.svelte b/src/lib/components/CodeReportVisualization.svelte index 6416268..a24e3cd 100644 --- a/src/lib/components/CodeReportVisualization.svelte +++ b/src/lib/components/CodeReportVisualization.svelte @@ -8,11 +8,14 @@ summarizeAnalysisResult, type VulnerabilityFinding, } from '$lib/code-report/analysis-summary'; + import { evaluatePolicies } from '$lib/code-report/policy-evaluation'; + import type { SecurityPolicy } from '$lib/code-report/security-policy'; import CodeReportFiles from './code-report/CodeReportFiles.svelte'; import CodeReportSbom from './code-report/CodeReportSbom.svelte'; import CodeReportSecrets from './code-report/CodeReportSecrets.svelte'; import CodeReportSummary from './code-report/CodeReportSummary.svelte'; import CodeReportVulnerabilities from './code-report/CodeReportVulnerabilities.svelte'; + import SecurityPolicyComplianceCard from './code-report/SecurityPolicyComplianceCard.svelte'; type AnalysisData = { id: string; @@ -31,6 +34,7 @@ }; type Analysis = AnalysisData | null; type ServiceData = { + id?: string; name: string; slug: string; description?: string | null; @@ -40,6 +44,8 @@ export let analysisHistory: AnalysisData[] = []; export let latestByTool: Record = {}; export let service: ServiceData = null; + export let securityPolicies: SecurityPolicy[] = []; + export let securityPoliciesHref: string | null = null; let activeTab = 'summary'; let activeVulnerabilityTab = 'cve'; let riskInfoModalOpen = false; @@ -71,6 +77,14 @@ $: vulnerabilities = trivyAnalysis ? extractVulnerabilities(trivyAnalysis.result) : []; $: secrets = gitleaksAnalysis ? extractSecrets(gitleaksAnalysis.result) : []; $: sbomComponents = sbomAnalysis ? extractSbomComponents(sbomAnalysis.result) : []; + $: complianceReport = evaluatePolicies(securityPolicies, { + serviceId: service?.id ?? '', + serviceTags: service?.tags ?? [], + vulnerabilities, + secrets, + hasVulnerabilityScan: trivyAnalysis?.status === 'completed', + hasSecretScan: gitleaksAnalysis?.status === 'completed', + }); $: fileGroups = groupByFile(vulnerabilities); $: selectedFile = fileGroups.find((file) => file.path === selectedFilePath) ?? null; $: historyPoints = analysisHistory @@ -206,6 +220,12 @@
+ {#if analysis} + + {/if}
{#each tabs as tab} + {/if} +
+
+ + {#if expanded && report.evaluations.length > 0} +
+ {#each report.evaluations as evaluation (evaluation.policyId)} +
+
+
+ {#if !evaluation.evaluable} + + {:else if evaluation.passed} + + {:else} + + {/if} +

{evaluation.policyName}

+ + {evaluation.enforcement === 'block' ? 'Bloquear' : 'Avisar'} + +
+ + {!evaluation.evaluable ? 'No evaluada' : evaluation.passed ? 'Cumple' : 'Incumple'} + +
+ + {#if !evaluation.evaluable} +

{evaluation.skippedReason}

+ {:else} +
    + {#each evaluation.checks as item (item.key)} +
  • +
    +

    + {item.label} +

    +

    {item.message}

    + {#if !item.passed && item.samples.length > 0} +

    + Ej.: {item.samples.join(', ')} + {#if item.actual > item.samples.length} + y {item.actual - item.samples.length} más + {/if} +

    + {/if} +
    + + {item.actual}{item.limit !== null ? ` / ${item.limit}` : ''} + +
  • + {/each} +
+ {/if} +
+ {/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 index 286f164..f9104ce 100644 --- 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 @@ -2,6 +2,7 @@ import { error } from '@sveltejs/kit'; import { cancanService } from '../../../../../../../../modules/auth'; import { codeReportAnalysisService, + codeReportSecurityPolicyService, codeReportService, } from '../../../../../../../../modules/code-report'; @@ -23,5 +24,6 @@ export async function load({ parent, params, locals }) { service, analysis, analysisHistory: await codeReportAnalysisService.listByService(service.id), + securityPolicies: await codeReportSecurityPolicyService.listByProject(project.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 index 054a43d..d0f2ecb 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 @@ -3,10 +3,12 @@ import { ArrowLeft } from 'lucide-svelte'; import CodeReportVisualization from '$lib/components/CodeReportVisualization.svelte'; import CodeReportToolBadge from '$lib/components/code-report/CodeReportToolBadge.svelte'; + import type { SecurityPolicy } from '$lib/code-report/security-policy'; export let data: { - service: { name: string; slug: string }; + service: { id: string; name: string; slug: string; tags?: string[] }; analysis: any; analysisHistory: any[]; + securityPolicies: SecurityPolicy[]; }; $: historyHref = `/org/${$page.params.org}/projects/${$page.params.slug}/code-report/history`; @@ -30,5 +32,7 @@ service={data.service} analysis={data.analysis} analysisHistory={data.analysisHistory} + securityPolicies={data.securityPolicies} + securityPoliciesHref={`/org/${$page.params.org}/projects/${$page.params.slug}/code-report/security-policy`} /> 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 5ade227..826a48e 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 @@ -2,6 +2,7 @@ import { error, fail, redirect } from '@sveltejs/kit'; import { codeReportService, codeReportAnalysisService, + codeReportSecurityPolicyService, } from '../../../../../../../../modules/code-report'; import { projectService } from '../../../../../../../../modules/projects'; import { cancanService } from '../../../../../../../../modules/auth'; @@ -42,7 +43,9 @@ export async function load({ parent, params, locals }) { service.tools ?? [], ); - return { service, latestAnalysis, latestByTool, analysisHistory }; + const securityPolicies = await codeReportSecurityPolicyService.listByProject(project.id); + + return { service, latestAnalysis, latestByTool, analysisHistory, securityPolicies }; } 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 565b732..a4968c9 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 @@ -3,12 +3,14 @@ import { enhance } from '$app/forms'; import { ArrowLeft, History, Trash2, X } from 'lucide-svelte'; import CodeReportVisualization from '$lib/components/CodeReportVisualization.svelte'; + import type { SecurityPolicy } from '$lib/code-report/security-policy'; export let data: { - service: { name: string; slug: string }; + service: { id: string; name: string; slug: string; tags?: string[] }; latestAnalysis: any; latestByTool: Record; analysisHistory: any[]; + securityPolicies: SecurityPolicy[]; }; export let form: { error?: string; @@ -60,6 +62,8 @@ analysis={data.latestAnalysis} latestByTool={data.latestByTool} analysisHistory={data.analysisHistory} + securityPolicies={data.securityPolicies} + securityPoliciesHref={`/org/${orgSlug}/projects/${projectSlug}/code-report/security-policy`} /> From 24fe2573326be90ac900f3f80d793e52d274ef22 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 15:09:35 +0200 Subject: [PATCH 3/9] policy evaluation --- src/lib/code-report/policy-evaluation.ts | 40 +++++++ src/lib/code-report/security-policy.ts | 4 +- .../components/CodeReportVisualization.svelte | 22 ++-- .../SecurityPolicyComplianceCard.svelte | 6 +- src/lib/database/schemas.ts | 2 + .../code-report-analysis.service.ts | 85 ++++++++++++++- .../domain/code-report-analysis.domain.ts | 4 + src/modules/code-report/index.ts | 7 +- .../code-report-analysis.repository.ts | 1 + .../code-report/dashboard/+page.server.ts | 53 +++++++++ .../[slug]/code-report/dashboard/+page.svelte | 102 ++++++++++++++++-- .../history/[analysisID]/+page.server.ts | 2 - .../history/[analysisID]/+page.svelte | 3 - .../security-policy/[id]/+page.server.ts | 36 +++++++ .../security-policy/[id]/+page.svelte | 47 +++++++- .../services/[serviceSlug]/+page.server.ts | 5 +- .../services/[serviceSlug]/+page.svelte | 3 - 17 files changed, 378 insertions(+), 44 deletions(-) diff --git a/src/lib/code-report/policy-evaluation.ts b/src/lib/code-report/policy-evaluation.ts index 8d0af44..c112597 100644 --- a/src/lib/code-report/policy-evaluation.ts +++ b/src/lib/code-report/policy-evaluation.ts @@ -234,3 +234,43 @@ export function evaluatePolicies( blockingFailures: failed.filter((evaluation) => evaluation.enforcement === 'block').length, }; } + +// each tool stores its own partial report, so views combine them into a single one +export function mergeComplianceReports( + reports: (PolicyComplianceReport | null | undefined)[], +): PolicyComplianceReport | null { + const available = reports.filter((report): report is PolicyComplianceReport => Boolean(report)); + if (available.length === 0) return null; + + const byPolicy = new Map(); + for (const report of available) { + for (const evaluation of report.evaluations) { + const existing = byPolicy.get(evaluation.policyId); + // an evaluable result always wins over a skipped one + if (!existing || (!existing.evaluable && evaluation.evaluable)) { + byPolicy.set(evaluation.policyId, evaluation); + } + } + } + + const evaluations = [...byPolicy.values()]; + const evaluable = evaluations.filter((evaluation) => evaluation.evaluable); + const failed = evaluable.filter((evaluation) => !evaluation.passed); + const passed = evaluable.filter((evaluation) => evaluation.passed); + + return { + status: + evaluations.length === 0 + ? 'no_policies' + : evaluable.length === 0 + ? 'not_applicable' + : failed.length > 0 + ? 'violated' + : 'compliant', + evaluations, + passed, + failed, + totalViolations: failed.reduce((total, evaluation) => total + evaluation.violations.length, 0), + blockingFailures: failed.filter((evaluation) => evaluation.enforcement === 'block').length, + }; +} diff --git a/src/lib/code-report/security-policy.ts b/src/lib/code-report/security-policy.ts index ebacbcc..285c537 100644 --- a/src/lib/code-report/security-policy.ts +++ b/src/lib/code-report/security-policy.ts @@ -66,8 +66,8 @@ export type SecurityPolicy = { enforcement: SecurityPolicyEnforcement; scope: SecurityPolicyScope; rules: SecurityPolicyRules; - createdAt: string; - updatedAt: string; + createdAt: string | Date; + updatedAt: string | Date; }; export const SECURITY_POLICY_TYPE_META: Record< diff --git a/src/lib/components/CodeReportVisualization.svelte b/src/lib/components/CodeReportVisualization.svelte index a24e3cd..6008324 100644 --- a/src/lib/components/CodeReportVisualization.svelte +++ b/src/lib/components/CodeReportVisualization.svelte @@ -8,8 +8,8 @@ summarizeAnalysisResult, type VulnerabilityFinding, } from '$lib/code-report/analysis-summary'; - import { evaluatePolicies } from '$lib/code-report/policy-evaluation'; - import type { SecurityPolicy } from '$lib/code-report/security-policy'; + import type { PolicyComplianceReport } from '$lib/code-report/policy-evaluation'; + import { mergeComplianceReports } from '$lib/code-report/policy-evaluation'; import CodeReportFiles from './code-report/CodeReportFiles.svelte'; import CodeReportSbom from './code-report/CodeReportSbom.svelte'; import CodeReportSecrets from './code-report/CodeReportSecrets.svelte'; @@ -23,6 +23,7 @@ status: 'in_progress' | 'completed' | 'failed'; result: unknown; summary?: unknown; + securityPolicies?: PolicyComplianceReport | null; error?: string | null; gitInfo?: { repositoryUrl?: string | null; @@ -44,7 +45,6 @@ export let analysisHistory: AnalysisData[] = []; export let latestByTool: Record = {}; export let service: ServiceData = null; - export let securityPolicies: SecurityPolicy[] = []; export let securityPoliciesHref: string | null = null; let activeTab = 'summary'; let activeVulnerabilityTab = 'cve'; @@ -77,14 +77,12 @@ $: vulnerabilities = trivyAnalysis ? extractVulnerabilities(trivyAnalysis.result) : []; $: secrets = gitleaksAnalysis ? extractSecrets(gitleaksAnalysis.result) : []; $: sbomComponents = sbomAnalysis ? extractSbomComponents(sbomAnalysis.result) : []; - $: complianceReport = evaluatePolicies(securityPolicies, { - serviceId: service?.id ?? '', - serviceTags: service?.tags ?? [], - vulnerabilities, - secrets, - hasVulnerabilityScan: trivyAnalysis?.status === 'completed', - hasSecretScan: gitleaksAnalysis?.status === 'completed', - }); + $: complianceReport = mergeComplianceReports([ + trivyAnalysis?.securityPolicies, + gitleaksAnalysis?.securityPolicies, + sbomAnalysis?.securityPolicies, + analysis?.securityPolicies, + ]); $: fileGroups = groupByFile(vulnerabilities); $: selectedFile = fileGroups.find((file) => file.path === selectedFilePath) ?? null; $: historyPoints = analysisHistory @@ -220,7 +218,7 @@
- {#if analysis} + {#if analysis && complianceReport} 0} - {report.blockingFailures} bloqueante(s) + {report.blockingFailures} bloqueantes {/if} {#if policiesHref} diff --git a/src/lib/database/schemas.ts b/src/lib/database/schemas.ts index c382ad7..cc4bcc9 100644 --- a/src/lib/database/schemas.ts +++ b/src/lib/database/schemas.ts @@ -139,6 +139,8 @@ export const CodeReportAnalysisEntity = entity('code_report_analyses', { status: text().notNull().default('in_progress'), result: json(), summary: json(), + // compliance report evaluated when the analysis is completed + securityPolicies: json(), error: text(), gitInfo: json(), createdAt: timestamp() 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 e0484fa..2097678 100644 --- a/src/modules/code-report/application/code-report-analysis.service.ts +++ b/src/modules/code-report/application/code-report-analysis.service.ts @@ -2,15 +2,36 @@ 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'; +import { extractSecrets, extractVulnerabilities } from '$lib/code-report/analysis-summary'; +import { evaluatePolicies, type PolicyComplianceReport } from '$lib/code-report/policy-evaluation'; +import type { SecurityPolicy, SecurityPolicyType } from '$lib/code-report/security-policy'; type ServiceLookup = { - findById(id: string): Promise<{ id: string } | null>; + findById(id: string): Promise<{ id: string; projectId?: string; tags?: string[] } | null>; }; +type PolicyLookup = { + listByProject(projectId: string): Promise; +}; + +// each tool only produces evidence for some policy types +const TOOL_POLICY_TYPES: Record = { + trivy: ['vulnerabilities', 'license'], + grype: ['vulnerabilities'], + sbom: ['license'], + syft: ['license'], + gitleaks: ['secrets'], + trufflehog: ['secrets'], + coverage: ['code_coverage'], + 'code-coverage': ['code_coverage'], +}; +const DEFAULT_POLICY_TYPES: SecurityPolicyType[] = ['vulnerabilities', 'license']; + export class CodeReportAnalysisService { constructor( private readonly repository: CodeReportAnalysisRepository, private readonly serviceLookup: ServiceLookup, + private readonly policyLookup?: PolicyLookup, ) {} async listByService(serviceId: string) { @@ -100,6 +121,7 @@ export class CodeReportAnalysisService { status: 'completed', result: input.result, summary: input.summary, + securityPolicies: await this.evaluateSecurityPolicies(analysis.serviceId, analysis.tool, input.result), gitInfo: input.gitInfo, error: null, }); @@ -107,6 +129,67 @@ export class CodeReportAnalysisService { return this.getById(id); } + // compliance is frozen at completion time so the UI never re-evaluates on read + private async evaluateSecurityPolicies( + serviceId: string, + tool: string, + result: unknown, + ): Promise { + if (!this.policyLookup) return null; + + const service = await this.serviceLookup.findById(serviceId); + if (!service?.projectId) return null; + + const policies = await this.policyLookup.listByProject(service.projectId); + const types = TOOL_POLICY_TYPES[tool.toLowerCase()] ?? DEFAULT_POLICY_TYPES; + const scopedPolicies = policies.filter((policy) => types.includes(policy.type)); + if (scopedPolicies.length === 0) return null; + + const checksVulnerabilities = types.includes('vulnerabilities'); + const checksSecrets = types.includes('secrets'); + + return evaluatePolicies(scopedPolicies, { + serviceId: service.id, + serviceTags: service.tags ?? [], + vulnerabilities: checksVulnerabilities ? extractVulnerabilities(result) : [], + secrets: checksSecrets ? extractSecrets(result) : [], + hasVulnerabilityScan: checksVulnerabilities, + hasSecretScan: checksSecrets, + }); + } + + // re-runs the policy evaluation over the latest stored analysis of each service + async revalidateLatestByServices(services: { id: string; tools?: string[] }[]) { + let analysesUpdated = 0; + const reports: { serviceId: string; report: PolicyComplianceReport }[] = []; + + for (const service of services) { + const tools = service.tools?.length ? service.tools : ['trivy']; + const latest = await this.getLatestByTool(service.id, tools); + + for (const analysis of Object.values(latest)) { + if (!analysis || analysis.status !== 'completed') continue; + + const report = await this.evaluateSecurityPolicies( + analysis.serviceId, + analysis.tool, + analysis.result, + ); + if (!report) continue; + + await this.repository.update(analysis.id, { securityPolicies: report }); + analysesUpdated += 1; + reports.push({ serviceId: service.id, report }); + } + } + + return { + servicesEvaluated: new Set(reports.map((entry) => entry.serviceId)).size, + analysesUpdated, + reports, + }; + } + // called when the tool could not run/complete, records the reason instead of a result async failAnalysis(id: string, input: { error: string; gitInfo?: CodeReportGitInfo }) { const analysis = await this.repository.findById(id); diff --git a/src/modules/code-report/domain/code-report-analysis.domain.ts b/src/modules/code-report/domain/code-report-analysis.domain.ts index a0fe708..ee463d5 100644 --- a/src/modules/code-report/domain/code-report-analysis.domain.ts +++ b/src/modules/code-report/domain/code-report-analysis.domain.ts @@ -1,4 +1,5 @@ import { Domain } from '$lib/server/domain/domain'; +import type { PolicyComplianceReport } from '$lib/code-report/policy-evaluation'; export type CodeReportAnalysisStatus = 'in_progress' | 'completed' | 'failed'; @@ -16,6 +17,7 @@ export class CodeReportAnalysisDomain extends Domain { public status: CodeReportAnalysisStatus = 'in_progress'; public result: unknown = null; public summary?: unknown = null; + public securityPolicies: PolicyComplianceReport | null = null; public error?: string | null = null; public gitInfo?: CodeReportGitInfo | null = null; @@ -26,6 +28,7 @@ export class CodeReportAnalysisDomain extends Domain { this.status = data.status; this.result = data.result ?? null; this.summary = data.summary ?? null; + this.securityPolicies = data.securityPolicies ?? null; this.error = data.error ?? null; this.gitInfo = data.gitInfo ?? null; } @@ -38,6 +41,7 @@ export class CodeReportAnalysisDomain extends Domain { status: this.status, result: this.result, summary: this.summary, + securityPolicies: this.securityPolicies, error: this.error, gitInfo: this.gitInfo, createdAt: this.createdAt, diff --git a/src/modules/code-report/index.ts b/src/modules/code-report/index.ts index 1ef39b7..5b129fd 100644 --- a/src/modules/code-report/index.ts +++ b/src/modules/code-report/index.ts @@ -11,9 +11,13 @@ const codeReportServiceRepository = new CodeReportServiceRepository(); const codeReportAnalysisRepository = new CodeReportAnalysisRepository(); const codeReportSecurityPolicyRepository = new CodeReportSecurityPolicyRepository(); +export const codeReportSecurityPolicyService = new CodeReportSecurityPolicyService( + codeReportSecurityPolicyRepository, +); export const codeReportAnalysisService = new CodeReportAnalysisService( codeReportAnalysisRepository, codeReportServiceRepository, + codeReportSecurityPolicyService, ); export const codeReportService = new CodeReportService( codeReportServiceRepository, @@ -24,6 +28,3 @@ export const codeReportCveService = new CodeReportCveService( codeReportService, codeReportAnalysisService, ); -export const codeReportSecurityPolicyService = new CodeReportSecurityPolicyService( - codeReportSecurityPolicyRepository, -); 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 dc3201d..10e77d4 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 @@ -60,6 +60,7 @@ export class CodeReportAnalysisRepository extends Repository { status?: CodeReportAnalysisStatus; result?: unknown; summary?: unknown; + securityPolicies?: unknown; error?: string | null; gitInfo?: CodeReportGitInfo; }, 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 index d900807..53592e9 100644 --- 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 @@ -3,10 +3,13 @@ import { cancanService } from '../../../../../../../modules/auth'; import { codeReportAnalysisService, codeReportCveService, + codeReportSecurityPolicyService, codeReportService, } from '../../../../../../../modules/code-report'; import { extractSecrets, summarizeAnalysisResult } from '$lib/code-report/analysis-summary'; import { summarizeCves } from '$lib/code-report/cve-aggregation'; +import type { PolicyComplianceReport } from '$lib/code-report/policy-evaluation'; +import { mergeComplianceReports } from '$lib/code-report/policy-evaluation'; const STALE_AFTER_DAYS = 30; const riskWeights = { critical: 10, high: 6, medium: 3, low: 1, unknown: 0 }; @@ -25,6 +28,10 @@ export async function load({ parent, locals }) { } const services = await codeReportService.listByProject(project.id); + const securityPolicies = await codeReportSecurityPolicyService.listByProject(project.id); + + const policyEvaluations: { service: (typeof services)[number]; report: PolicyComplianceReport }[] = + []; const serviceStats = await Promise.all( services.map(async (service) => { @@ -40,6 +47,15 @@ export async function load({ parent, locals }) { : summarizeAnalysisResult(null); const exposedSecrets = gitleaksAnalysis ? extractSecrets(gitleaksAnalysis.result).length : 0; + // compliance was evaluated and stored when the analysis completed + const report = mergeComplianceReports([ + trivyAnalysis?.securityPolicies as PolicyComplianceReport | null, + gitleaksAnalysis?.securityPolicies as PolicyComplianceReport | null, + ]); + if (report) { + policyEvaluations.push({ service, report }); + } + 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; @@ -98,6 +114,31 @@ export async function load({ parent, locals }) { const topCves = cves.slice(0, 8); + const evaluatedServices = policyEvaluations.filter( + (entry) => entry.report.status === 'compliant' || entry.report.status === 'violated', + ); + const failingServices = evaluatedServices.filter((entry) => entry.report.status === 'violated'); + + const violatedPolicies = new Map< + string, + { id: string; name: string; enforcement: string; services: string[] } + >(); + for (const entry of failingServices) { + for (const evaluation of entry.report.failed) { + const existing = violatedPolicies.get(evaluation.policyId); + if (existing) { + existing.services.push(entry.service.name); + } else { + violatedPolicies.set(evaluation.policyId, { + id: evaluation.policyId, + name: evaluation.policyName, + enforcement: evaluation.enforcement, + services: [entry.service.name], + }); + } + } + } + return { project, kpis: { @@ -111,6 +152,18 @@ export async function load({ parent, locals }) { cves.length > 0 ? Math.round((remediableCves / cves.length) * 100) : null, staleServicesCount: staleServices.length, }, + securityPolicies: { + total: securityPolicies.length, + active: securityPolicies.filter((policy) => policy.enabled).length, + evaluatedServices: evaluatedServices.length, + failingServices: failingServices.length, + compliantServices: evaluatedServices.length - failingServices.length, + totalViolations: failingServices.reduce( + (total, entry) => total + entry.report.totalViolations, + 0, + ), + violatedPolicies: [...violatedPolicies.values()], + }, severityBreakdown, topCves, riskiestServices, 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 index 36f38fa..71a2847 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.svelte @@ -56,6 +56,15 @@ remediationCoveragePercent: number | null; staleServicesCount: number; }; + securityPolicies: { + total: number; + active: number; + evaluatedServices: number; + failingServices: number; + compliantServices: number; + totalViolations: number; + violatedPolicies: { id: string; name: string; enforcement: string; services: string[] }[]; + }; severityBreakdown: SeverityCounts; topCves: CveRow[]; riskiestServices: ServiceRisk[]; @@ -122,6 +131,16 @@ `/org/${orgSlug}/projects/${projectSlug}/code-report/cves${id ? `/${id}` : ''}`; $: servicesHref = (slug: string) => `/org/${orgSlug}/projects/${projectSlug}/code-report/services/${slug}`; + $: securityPolicyHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/security-policy`; + + $: policyState = + data.securityPolicies.active === 0 + ? 'none' + : data.securityPolicies.failingServices > 0 + ? 'violated' + : data.securityPolicies.evaluatedServices === 0 + ? 'pending' + : 'compliant'; $: totalSeverity = data.severityBreakdown.critical + @@ -213,16 +232,81 @@ {/if} -
-

Security Policy

-

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

-
- Próximamente +
+
+
+

+ {#if policyState === 'violated'} + + {:else if policyState === 'compliant'} + + {:else} + + {/if} + Security Policy +

+

+ {data.securityPolicies.active} políticas activas sobre {data.securityPolicies + .evaluatedServices} servicios evaluados. +

+
+ + Gestionar +
+ + {#if policyState === 'violated'} +

+ {data.securityPolicies.failingServices} +

+

+ servicios incumpliendo · {data.securityPolicies.totalViolations} reglas superadas +

+
    + {#each data.securityPolicies.violatedPolicies as policy (policy.id)} +
  • + + {policy.name} + + + {policy.services.length} servicios + +
  • + {/each} +
+ {:else if policyState === 'compliant'} +

OK

+

+ {data.securityPolicies.compliantServices} servicios cumplen todas las políticas +

+ {:else} +
+ {#if policyState === 'none'} + Todavía no hay políticas activas. + Crea la primera + {:else} + Sin análisis suficientes para evaluar las políticas activas. + {/if} +
+ {/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 index f9104ce..286f164 100644 --- 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 @@ -2,7 +2,6 @@ import { error } from '@sveltejs/kit'; import { cancanService } from '../../../../../../../../modules/auth'; import { codeReportAnalysisService, - codeReportSecurityPolicyService, codeReportService, } from '../../../../../../../../modules/code-report'; @@ -24,6 +23,5 @@ export async function load({ parent, params, locals }) { service, analysis, analysisHistory: await codeReportAnalysisService.listByService(service.id), - securityPolicies: await codeReportSecurityPolicyService.listByProject(project.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 index d0f2ecb..e0f7096 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 @@ -3,12 +3,10 @@ import { ArrowLeft } from 'lucide-svelte'; import CodeReportVisualization from '$lib/components/CodeReportVisualization.svelte'; import CodeReportToolBadge from '$lib/components/code-report/CodeReportToolBadge.svelte'; - import type { SecurityPolicy } from '$lib/code-report/security-policy'; export let data: { service: { id: string; name: string; slug: string; tags?: string[] }; analysis: any; analysisHistory: any[]; - securityPolicies: SecurityPolicy[]; }; $: historyHref = `/org/${$page.params.org}/projects/${$page.params.slug}/code-report/history`; @@ -32,7 +30,6 @@ service={data.service} analysis={data.analysis} analysisHistory={data.analysisHistory} - securityPolicies={data.securityPolicies} securityPoliciesHref={`/org/${$page.params.org}/projects/${$page.params.slug}/code-report/security-policy`} />
diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.server.ts index 2168203..6472227 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.server.ts +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.server.ts @@ -1,6 +1,7 @@ import { error, fail, redirect } from '@sveltejs/kit'; import { cancanService } from '../../../../../../../../modules/auth'; import { + codeReportAnalysisService, codeReportSecurityPolicyService, codeReportService, } from '../../../../../../../../modules/code-report'; @@ -78,6 +79,41 @@ export const actions = { } }, + evaluate: async ({ params, locals }) => { + const { project, policy } = await resolvePolicy(params.slug, params.id); + + const canUpdate = await cancanService.canSessionUser(locals.user, 'openreport:update', { + scope: 'project', + projectId: project.id, + organizationId: project.organization?.id, + }); + + if (!canUpdate) { + return fail(403, { error: 'Forbidden' }); + } + + if (!policy) { + return fail(404, { error: 'Security policy not found' }); + } + + const services = await codeReportService.listByProject(project.id); + const result = await codeReportAnalysisService.revalidateLatestByServices(services); + + const failingServices = new Set( + result.reports + .filter((entry) => entry.report.failed.some((item) => item.policyId === params.id)) + .map((entry) => entry.serviceId), + ); + + return { + evaluated: { + servicesEvaluated: result.servicesEvaluated, + analysesUpdated: result.analysesUpdated, + failingServices: failingServices.size, + }, + }; + }, + delete: async ({ params, locals }) => { const { project, policy } = await resolvePolicy(params.slug, params.id); diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte index 8bc6532..24cae69 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte @@ -1,7 +1,7 @@ + +
+ + +
+ + \ No newline at end of file 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 index 53592e9..b621039 100644 --- 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 @@ -12,7 +12,6 @@ import type { PolicyComplianceReport } from '$lib/code-report/policy-evaluation' import { mergeComplianceReports } from '$lib/code-report/policy-evaluation'; 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(); @@ -27,6 +26,10 @@ export async function load({ parent, locals }) { throw error(403, 'Forbidden'); } + const projectSettings = project.settings?.['code-report'] || {}; + const riskWeights = projectSettings.securityRiskMultipliers || { critical: 10, high: 6, medium: 3, low: 1 }; + + const services = await codeReportService.listByProject(project.id); const securityPolicies = await codeReportSecurityPolicyService.listByProject(project.id); 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 index 48bbe6c..de9620a 100644 --- 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 @@ -15,6 +15,10 @@ export async function load({ parent, locals }) { })) ) throw error(403, 'Forbidden'); + + const projectSettings = project.settings?.['code-report'] || {}; + const riskWeights = projectSettings.securityRiskMultipliers || { critical: 10, high: 6, medium: 3, low: 1 }; + const services = await codeReportService.listByProject(project.id); const analyses = await codeReportAnalysisService.listByProject( services.map((service) => service.id), @@ -22,6 +26,7 @@ export async function load({ parent, locals }) { const serviceById = new Map(services.map((service) => [service.id, service])); return { services, + riskWeights, 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 index 25723c9..f30d479 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 @@ -3,13 +3,17 @@ 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[] }; + export let data: { + services: { id: string; slug: string; name: string }[]; + analyses: any[]; + riskWeights: { critical: number; high: number; medium: number; low: number }; + }; let serviceFilter = $page.url.searchParams.get('service') ?? 'all'; let statusFilter = 'all'; let dateFilter = ''; let query = ''; let openDropdown = ''; - const riskWeights = { critical: 10, high: 6, medium: 3, low: 1 }; + $: riskWeights = data.riskWeights; const statusOptions = [ { value: 'all', label: 'Todos los estados' }, 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 index 286f164..c996e60 100644 --- 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 @@ -19,9 +19,14 @@ export async function load({ parent, params, locals }) { const services = await codeReportService.listByProject(project.id); const service = services.find((item) => item.id === analysis.serviceId); if (!service) throw error(404, 'Analysis not found'); + + const projectSettings = project.settings?.['code-report'] || {}; + const riskWeights = projectSettings.securityRiskMultipliers || { critical: 10, high: 6, medium: 3, low: 1 }; + return { service, analysis, + riskWeights, 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 index e0f7096..1dfcfa5 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 @@ -7,6 +7,7 @@ service: { id: string; name: string; slug: string; tags?: string[] }; analysis: any; analysisHistory: any[]; + riskWeights: { critical: number; high: number; medium: number; low: number }; }; $: historyHref = `/org/${$page.params.org}/projects/${$page.params.slug}/code-report/history`; @@ -30,6 +31,7 @@ service={data.service} analysis={data.analysis} analysisHistory={data.analysisHistory} + riskWeights={data.riskWeights} securityPoliciesHref={`/org/${$page.params.org}/projects/${$page.params.slug}/code-report/security-policy`} /> 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 5ade227..fe7597d 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 @@ -42,7 +42,10 @@ export async function load({ parent, params, locals }) { service.tools ?? [], ); - return { service, latestAnalysis, latestByTool, analysisHistory }; + const projectSettings = project.settings?.['code-report'] || {}; + const riskWeights = projectSettings.securityRiskMultipliers || { critical: 10, high: 6, medium: 3, low: 1 }; + + return { service, latestAnalysis, latestByTool, analysisHistory, riskWeights }; } 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 d41c7b6..0c37475 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 @@ latestAnalysis: any; latestByTool: Record; analysisHistory: any[]; + riskWeights: { critical: number; high: number; medium: number; low: number }; }; export let form: { error?: string; @@ -60,6 +61,7 @@ analysis={data.latestAnalysis} latestByTool={data.latestByTool} analysisHistory={data.analysisHistory} + riskWeights={data.riskWeights} securityPoliciesHref={`/org/${orgSlug}/projects/${projectSlug}/code-report/security-policy`} /> diff --git a/src/routes/org/[org]/projects/[slug]/code-report/settings/+layout.svelte b/src/routes/org/[org]/projects/[slug]/code-report/settings/+layout.svelte new file mode 100644 index 0000000..6b31844 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/settings/+layout.svelte @@ -0,0 +1,75 @@ + + +
+
+
+

Code Report Settings

+
+ {#each tabs as tab} + {#if tab.soon} + + {:else} + + + {tab.label} + + {/if} + {/each} +
+
+ +
+ +
+
+
+ + diff --git a/src/routes/org/[org]/projects/[slug]/code-report/settings/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/settings/+page.server.ts new file mode 100644 index 0000000..c1e0867 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/settings/+page.server.ts @@ -0,0 +1,56 @@ +import { error } from '@sveltejs/kit'; +import { projectService } from '../../../../../../../modules/projects'; + +export async function load({ params }) { + const project = await projectService.getProjectBySlug(params.slug); + if (!project) { + throw error(404, 'Proyecto no encontrado'); + } + + const codeReportSettings = project.settings?.['code-report'] || { + securityRiskMultipliers: { + critical: 10, + high: 6, + medium: 3, + low: 1, + } + }; + + return { + settings: codeReportSettings, + }; +} + +export const actions = { + updateRiskMultipliers: async ({ request, params }) => { + const data = await request.formData(); + const critical = Number(data.get('critical')); + const high = Number(data.get('high')); + const medium = Number(data.get('medium')); + const low = Number(data.get('low')); + + const project = await projectService.getProjectBySlug(params.slug); + + const currentSettings = project.settings || {}; + const codeReportSettings = currentSettings['code-report'] || {}; + + const updatedSettings = { + ...currentSettings, + 'code-report': { + ...codeReportSettings, + securityRiskMultipliers: { + critical, + high, + medium, + low, + }, + }, + }; + + await projectService.updateProject(project.id, { + settings: updatedSettings, + }); + + return { success: true }; + } +}; diff --git a/src/routes/org/[org]/projects/[slug]/code-report/settings/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/settings/+page.svelte new file mode 100644 index 0000000..6c04a75 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/settings/+page.svelte @@ -0,0 +1,139 @@ + + + + General Settings - Code Report + + +
+
+
+ +

General

+
+

+ Parámetros globales del módulo para puntuar riesgo y clasificar findings en este proyecto. +

+
+ +
{ + loading = true; + return async ({ update }) => { + await update(); + loading = false; + }; + }} + class="space-y-6" + > +
+
+ +

Security Risk Multipliers

+
+ +

+ Fórmula aplicada en el detalle de servicio: (Critical × M_c) + (High × M_h) + (Medium × M_m) + + (Low × M_l). +

+ +
+ + + + + + + +
+ +
+
+ +
+

+ Ejemplo actual: (Critical × 10) + (High × 6) + (Medium × 3) + (Low × 1) = 561 puntos. +

+

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

+
+
+
+
+ +
+

Más ajustes

+

+ Próximamente: baseline por servicio, severidades mínimas para alertas, y excepciones temporales de CVEs. +

+
+ +
+ +
+
+
diff --git a/src/routes/org/[org]/projects/[slug]/code-report/settings/tools/+page.server.ts b/src/routes/org/[org]/projects/[slug]/code-report/settings/tools/+page.server.ts new file mode 100644 index 0000000..bd83e2d --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/settings/tools/+page.server.ts @@ -0,0 +1,88 @@ +import { projectService } from '../../../../../../../../modules/projects'; + +const DEFAULT_CODE_REPORT_TOOLS = [ + { + id: 'trivy', + name: 'Vulnerabilidades', + description: 'Escaneo de vulnerabilidades y severidad de dependencias con Trivy.', + enabled: true, + scanner: 'trivy', + }, + { + id: 'syft', + name: 'Dependencias, SBOM y Licencias', + description: 'Inventario de dependencias, generación de SBOM y análisis de licencias con Syft.', + enabled: true, + scanner: 'syft', + }, + { + id: 'gitleaks', + name: 'Secretos Expuestos', + description: 'Detección de secretos expuestos en el repositorio con Gitleaks.', + enabled: true, + scanner: 'gitleaks', + }, + { + id: 'code-coverage', + name: 'Code Coverage', + description: 'Métricas de cobertura de código por servicio.', + enabled: false, + scanner: 'code-coverage', + soon: true, + }, +]; + +export async function load({ params }) { + const project = await projectService.getProjectBySlug(params.slug); + + const codeReportSettings = project.settings?.['code-report'] || {}; + const persistedTools = Array.isArray(codeReportSettings.tools) ? codeReportSettings.tools : []; + const persistedById = new Map( + persistedTools + .filter((tool: any) => tool && typeof tool.id === 'string') + .map((tool: any) => [tool.id, tool]), + ); + + const tools = DEFAULT_CODE_REPORT_TOOLS.map((tool) => { + const persisted = persistedById.get(tool.id); + return { + ...tool, + enabled: persisted?.enabled ?? tool.enabled, + }; + }); + + return { + tools, + }; +} + +export const actions = { + updateTools: async ({ request, params }) => { + const data = await request.formData(); + const enabledToolIds = data.getAll('tools'); + + const project = await projectService.getProjectBySlug(params.slug); + + const currentSettings = project.settings || {}; + const codeReportSettings = currentSettings['code-report'] || {}; + const enabledSet = new Set(enabledToolIds.map((id) => String(id))); + const updatedTools = DEFAULT_CODE_REPORT_TOOLS.map((tool) => ({ + ...tool, + enabled: tool.soon ? false : enabledSet.has(tool.id), + })); + + const updatedSettings = { + ...currentSettings, + 'code-report': { + ...codeReportSettings, + tools: updatedTools, + }, + }; + + await projectService.updateProject(project.id, { + settings: updatedSettings, + }); + + return { success: true }; + } +}; diff --git a/src/routes/org/[org]/projects/[slug]/code-report/settings/tools/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/settings/tools/+page.svelte new file mode 100644 index 0000000..88c4613 --- /dev/null +++ b/src/routes/org/[org]/projects/[slug]/code-report/settings/tools/+page.svelte @@ -0,0 +1,98 @@ + + + + Tools - Code Report + + +
+
+
+ +

Herramientas de Análisis

+
+

+ Gestiona las categorías de análisis habilitadas para este proyecto. Lo que actives aquí se guarda + en settings del proyecto y se devolverá en la API de scan. +

+
+ +
{ + loading = true; + return async ({ update }) => { + await update(); + loading = false; + }; + }} + > +
+ {#each localTools as tool} +
+
+ +
+
+ +

{tool.description}

+
+
+ {/each} +
+ +
+ +
+
+
From cddf58c3c3037dcd8e34d4daac051512bca2e84c Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 18:06:04 +0200 Subject: [PATCH 6/9] settings project --- src/lib/components/AppSidebar.svelte | 1 - .../components/CodeReportVisualization.svelte | 4 +- .../application/code-report.service.ts | 55 +++++++++++++++++++ .../[slug]/code-report/+layout.svelte | 16 +----- .../code-report/dashboard/+page.server.ts | 13 ++--- .../code-report/history/+page.server.ts | 3 +- .../history/[analysisID]/+page.server.ts | 3 +- .../services/[serviceSlug]/+page.server.ts | 3 +- .../code-report/settings/+layout.svelte | 13 ++++- 9 files changed, 77 insertions(+), 34 deletions(-) diff --git a/src/lib/components/AppSidebar.svelte b/src/lib/components/AppSidebar.svelte index a0d0a38..77c9650 100644 --- a/src/lib/components/AppSidebar.svelte +++ b/src/lib/components/AppSidebar.svelte @@ -111,7 +111,6 @@ icon: Shield, }, { 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/lib/components/CodeReportVisualization.svelte b/src/lib/components/CodeReportVisualization.svelte index 0727316..8e5a5c0 100644 --- a/src/lib/components/CodeReportVisualization.svelte +++ b/src/lib/components/CodeReportVisualization.svelte @@ -335,7 +335,7 @@ mayor sea el resultado, mayor es la prioridad de remediación.

- {#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}

@@ -345,7 +345,7 @@

Fórmula aplicada

- (Critical × 10) + (High × 6) + (Medium × 3) + (Low × 1) = {riskScore} puntos + (Critical × {riskWeights.critical}) + (High × {riskWeights.high}) + (Medium × {riskWeights.medium}) + (Low × {riskWeights.low}) = {riskScore} puntos

Riesgo bajo: 1-7 · medio: 8-19 · alto: 20-39 · crítico: 40+ o cualquier Critical. diff --git a/src/modules/code-report/application/code-report.service.ts b/src/modules/code-report/application/code-report.service.ts index e2ae81c..6672bea 100644 --- a/src/modules/code-report/application/code-report.service.ts +++ b/src/modules/code-report/application/code-report.service.ts @@ -1,6 +1,28 @@ import crypto from 'crypto'; import { CodeReportServiceRepository } from '../infrastructure/repositories/code-report-service.repository'; import { ProjectService } from '../../projects/application/project.service'; + +export type RiskWeights = { + critical: number; + high: number; + medium: number; + low: number; +}; + +export type VulnerabilityTotals = { + critical: number; + high: number; + medium: number; + low: number; +}; + +export const DEFAULT_RISK_WEIGHTS: RiskWeights = { + critical: 10, + high: 6, + medium: 3, + low: 1, +}; + type AnalysisCleanup = { deleteAllByService(serviceId: string): Promise; }; @@ -17,6 +39,23 @@ export class CodeReportService { return services.map((service) => service.toJson()); } + async getRiskWeightsByProjectId(projectId: string): Promise { + const project = await this.projectService.getProject(projectId); + return this.resolveRiskWeights(project.settings); + } + + calculateRiskScore( + vulnerabilities: VulnerabilityTotals, + riskWeights: RiskWeights = DEFAULT_RISK_WEIGHTS, + ): number { + return ( + vulnerabilities.critical * riskWeights.critical + + vulnerabilities.high * riskWeights.high + + vulnerabilities.medium * riskWeights.medium + + vulnerabilities.low * riskWeights.low + ); + } + async getById(id: string) { const service = await this.repository.findById(id); if (!service) { @@ -164,4 +203,20 @@ export class CodeReportService { .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, ''); } + + private resolveRiskWeights(settings: any): RiskWeights { + const multipliers = settings?.['code-report']?.securityRiskMultipliers; + + return { + critical: this.toPositiveNumberOrDefault(multipliers?.critical, DEFAULT_RISK_WEIGHTS.critical), + high: this.toPositiveNumberOrDefault(multipliers?.high, DEFAULT_RISK_WEIGHTS.high), + medium: this.toPositiveNumberOrDefault(multipliers?.medium, DEFAULT_RISK_WEIGHTS.medium), + low: this.toPositiveNumberOrDefault(multipliers?.low, DEFAULT_RISK_WEIGHTS.low), + }; + } + + private toPositiveNumberOrDefault(value: unknown, fallback: number): number { + const parsed = Number(value); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; + } } diff --git a/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte b/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte index 1db6de1..99ee45b 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte @@ -2,8 +2,8 @@ import { page } from '$app/stores'; import { Settings, ShieldAlert, Activity, FileKey, CheckSquare, Boxes, History } from 'lucide-svelte'; - $: orgSlug = $page.params.org; - $: projectSlug = $page.params.slug; + $: orgSlug = $page?.params?.org ?? ''; + $: projectSlug = $page?.params?.slug ?? ''; $: basePath = `/org/${orgSlug}/projects/${projectSlug}/code-report`; @@ -16,20 +16,10 @@ { label: 'Ajustes', href: `${basePath}/settings`, icon: Settings }, ]; - $: currentPath = $page.url.pathname; + $: currentPath = $page?.url?.pathname ?? '';

- - \ No newline at end of file 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 index b621039..571c150 100644 --- 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 @@ -26,8 +26,7 @@ export async function load({ parent, locals }) { throw error(403, 'Forbidden'); } - const projectSettings = project.settings?.['code-report'] || {}; - const riskWeights = projectSettings.securityRiskMultipliers || { critical: 10, high: 6, medium: 3, low: 1 }; + const riskWeights = await codeReportService.getRiskWeightsByProjectId(project.id); const services = await codeReportService.listByProject(project.id); @@ -60,8 +59,8 @@ export async function load({ parent, locals }) { } 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; + .filter((value): value is Date => Boolean(value)) + .sort((left, right) => right.getTime() - left.getTime())[0] ?? null; return { id: service.id, @@ -105,11 +104,7 @@ export async function load({ parent, locals }) { 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, + riskScore: codeReportService.calculateRiskScore(service.severity, riskWeights), })) .filter((service) => service.riskScore > 0) .sort((left, right) => right.riskScore - left.riskScore) 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 index de9620a..8c746c6 100644 --- 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 @@ -16,8 +16,7 @@ export async function load({ parent, locals }) { ) throw error(403, 'Forbidden'); - const projectSettings = project.settings?.['code-report'] || {}; - const riskWeights = projectSettings.securityRiskMultipliers || { critical: 10, high: 6, medium: 3, low: 1 }; + const riskWeights = await codeReportService.getRiskWeightsByProjectId(project.id); const services = await codeReportService.listByProject(project.id); const analyses = await codeReportAnalysisService.listByProject( 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 index c996e60..7cd0e05 100644 --- 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 @@ -20,8 +20,7 @@ export async function load({ parent, params, locals }) { const service = services.find((item) => item.id === analysis.serviceId); if (!service) throw error(404, 'Analysis not found'); - const projectSettings = project.settings?.['code-report'] || {}; - const riskWeights = projectSettings.securityRiskMultipliers || { critical: 10, high: 6, medium: 3, low: 1 }; + const riskWeights = await codeReportService.getRiskWeightsByProjectId(project.id); return { service, 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 fe7597d..27855ff 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 @@ -42,8 +42,7 @@ export async function load({ parent, params, locals }) { service.tools ?? [], ); - const projectSettings = project.settings?.['code-report'] || {}; - const riskWeights = projectSettings.securityRiskMultipliers || { critical: 10, high: 6, medium: 3, low: 1 }; + const riskWeights = await codeReportService.getRiskWeightsByProjectId(project.id); return { service, latestAnalysis, latestByTool, analysisHistory, riskWeights }; } catch { diff --git a/src/routes/org/[org]/projects/[slug]/code-report/settings/+layout.svelte b/src/routes/org/[org]/projects/[slug]/code-report/settings/+layout.svelte index 6b31844..0276433 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/settings/+layout.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/settings/+layout.svelte @@ -2,8 +2,15 @@ import { page } from '$app/stores'; import { Bot, ListChecks, Settings, Webhook } from 'lucide-svelte'; - $: orgSlug = $page.params.org; - $: projectSlug = $page.params.slug; + export let data: { + project?: { + slug?: string | null; + organization?: { slug?: string | null } | null; + }; + }; + + $: orgSlug = data?.project?.organization?.slug ?? $page?.params?.org ?? ''; + $: projectSlug = data?.project?.slug ?? $page?.params?.slug ?? ''; $: basePath = `/org/${orgSlug}/projects/${projectSlug}/code-report/settings`; $: tabs = [ @@ -14,7 +21,7 @@ { label: 'Webhooks', href: `${basePath}/webhooks`, icon: Webhook, soon: true }, ]; - $: currentPath = $page.url.pathname; + $: currentPath = $page?.url?.pathname ?? '';
From ba5aaae713ca81dd7e42244f89a0f7fd44d14f9a Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 18:41:23 +0200 Subject: [PATCH 7/9] cves moved to organization --- src/lib/components/AppSidebar.svelte | 19 +- .../code-report/CveDetailView.svelte | 507 +++++++++++++++++ .../components/code-report/CveListView.svelte | 214 +++++++ .../application/code-report-cve.service.ts | 42 +- src/routes/+layout.server.ts | 3 + src/routes/+layout.svelte | 9 +- src/routes/org/+page.svelte | 2 +- src/routes/org/[org]/cves/+page.server.ts | 44 ++ src/routes/org/[org]/cves/+page.svelte | 34 ++ .../org/[org]/cves/[cve]/+page.server.ts | 98 ++++ src/routes/org/[org]/cves/[cve]/+page.svelte | 57 ++ src/routes/org/[org]/overview/+page.svelte | 2 +- .../org/[org]/projects/[slug]/+layout.svelte | 4 +- .../[slug]/code-report/+layout.svelte | 2 +- .../[slug]/code-report/cves/+page.server.ts | 23 +- .../[slug]/code-report/cves/+page.svelte | 169 +----- .../code-report/cves/[cve]/+page.server.ts | 83 +-- .../code-report/cves/[cve]/+page.svelte | 532 ++---------------- .../[slug]/code-report/dashboard/+page.svelte | 6 +- .../[slug]/code-report/history/+page.svelte | 4 +- .../history/[analysisID]/+page.svelte | 4 +- .../code-report/security-policy/+page.svelte | 4 +- .../security-policy/[id]/+page.svelte | 4 +- .../security-policy/new/+page.svelte | 2 +- .../[slug]/code-report/services/+page.svelte | 4 +- .../services/[serviceSlug]/+page.svelte | 4 +- .../projects/[slug]/overview/+page.svelte | 2 +- .../[slug]/settings/overview/+page.svelte | 8 +- 28 files changed, 1103 insertions(+), 783 deletions(-) create mode 100644 src/lib/components/code-report/CveDetailView.svelte create mode 100644 src/lib/components/code-report/CveListView.svelte create mode 100644 src/routes/org/[org]/cves/+page.server.ts create mode 100644 src/routes/org/[org]/cves/+page.svelte create mode 100644 src/routes/org/[org]/cves/[cve]/+page.server.ts create mode 100644 src/routes/org/[org]/cves/[cve]/+page.svelte diff --git a/src/lib/components/AppSidebar.svelte b/src/lib/components/AppSidebar.svelte index 77c9650..2143062 100644 --- a/src/lib/components/AppSidebar.svelte +++ b/src/lib/components/AppSidebar.svelte @@ -34,6 +34,7 @@ export let collapsed = true; export let organizationSlug: string | null = null; export let organizationName: string | null = null; + export let currentProjectSlug: string | null = null; export let projects: { slug: string; modules?: { vault: boolean; codereport: boolean; stateiac: boolean }; @@ -42,9 +43,10 @@ let currentPath = pathname; let openModules: Record = {}; - $: currentPath = $page.url.pathname || pathname; - $: currentProjectOrgSlug = ($page.params.org as string | undefined) ?? organizationSlug; - $: currentProjectSlug = $page.params.slug as string | undefined; + $: currentPath = $page?.url?.pathname || pathname; + // sourced from the server load (parsed from the URL), so it never lags behind or + // blanks out during client-side navigation like $page.params could + $: currentProjectOrgSlug = organizationSlug; $: currentProject = projects.find((project) => project.slug === currentProjectSlug) ?? null; $: projectBase = `/org/${currentProjectOrgSlug}/projects/${currentProjectSlug}`; @@ -64,6 +66,11 @@ href: `/org/${organizationSlug}/overview`, icon: LayoutDashboard, }, + { + label: 'CVEs', + href: `/org/${organizationSlug}/cves`, + icon: ShieldAlert, + }, ] : [ { @@ -104,7 +111,11 @@ icon: LayoutDashboard, }, { label: 'Services', href: `${projectBase}/code-report/services`, icon: Layers }, - { label: 'CVEs', href: `${projectBase}/code-report/cves`, icon: ShieldAlert }, + { + label: 'CVEs', + href: `/org/${currentProjectOrgSlug}/cves?project=${currentProjectSlug}`, + icon: ShieldAlert, + }, { label: 'Security Policies', href: `${projectBase}/code-report/security-policy`, diff --git a/src/lib/components/code-report/CveDetailView.svelte b/src/lib/components/code-report/CveDetailView.svelte new file mode 100644 index 0000000..08631a0 --- /dev/null +++ b/src/lib/components/code-report/CveDetailView.svelte @@ -0,0 +1,507 @@ + + +
+ + Volver a CVEs + + +
+
+

{cve.id}

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

{cve.title}

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

Severidad

+

{cve.severity}

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

Publicado

+

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

+
+
+ {/if} +
+ +
+

Overview

+

{cve.description || 'Sin descripcion disponible.'}

+ {#if cve.lastModifiedDate} +

+ Ultima actualizacion: {new Date(cve.lastModifiedDate).toLocaleDateString()} +

+ {/if} +
+ +
+

CWE

+ {#if cve.cweIds.length === 0} +

No se ha indicado una clasificacion CWE para este CVE.

+ {:else} +
+ {#each cve.cweIds as cweId (cweId)} + + {cweId} + + {/each} +
+ {/if} +
+ +
+
+
+ + CVSS + + + +
+

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

+
+
+
+

Escala 0-10

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

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

+
+
+
+

Probabilidad de explotacion

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

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

+
+
+
+

Frente al resto de CVEs conocidas

+
+
+ +
+

+ Como solucionarlo +

+ {#if remediations.some((remediation) => remediation.fixedVersion)} +
    + {#each remediations as remediation (remediation.packageName)} +
  • + {remediation.packageName} + {#if remediation.fixedVersion} +
    + {remediation.installedVersion} + -> + {#each splitVersions(remediation.fixedVersion) as version (version)} + + {version} + + {/each} +
    + {:else} + - todavia no hay una version corregida publicada. + {/if} +
  • + {/each} +
+ {:else} +

+ Ninguno de los paquetes afectados tiene aun una version corregida publicada. Revisa el advisory + para posibles mitigaciones alternativas. +

+ {/if} +
+ +
+

References

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

No hay referencias disponibles para este CVE.

+ {:else} + + {/if} +
+ {:else} +
+ +
+ +
+ + + + + + + + + + + + + {#each paginatedAffectedServices as service (service.serviceId + service.target + service.packageName)} + {@const target = splitTarget(service.target)} + + + + + + + + + {/each} + {#if paginatedAffectedServices.length === 0} + + + + {/if} + +
ServicioPaqueteVersion instaladaVersion corregidaObjetivoUltimo escaneo
+ + {service.serviceName} + + {#if service.projectName || service.projectSlug} +

+ Proyecto: + {service.projectName || service.projectSlug} +

+ {/if} +
{service.packageName} + {service.installedVersion} + + {#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.scannedAt ? new Date(service.scannedAt).toLocaleString() : '-'} +
+ {affectedServices.length === 0 + ? 'No hay servicios afectados.' + : 'Sin resultados para tu busqueda.'} +
+
+ +
+
+ Mostrando {affectedServicesRangeStart}-{affectedServicesRangeEnd} de {filteredAffectedServices.length} + +
+ +
+ + + Pagina {affectedServicesPage} de {affectedServicesTotalPages} + + +
+
+ {/if} +
diff --git a/src/lib/components/code-report/CveListView.svelte b/src/lib/components/code-report/CveListView.svelte new file mode 100644 index 0000000..659e6e7 --- /dev/null +++ b/src/lib/components/code-report/CveListView.svelte @@ -0,0 +1,214 @@ + + +
+
+ +

{infoMessage}

+
+ +
+ + + {#if showProjectFilter} + + {/if} + + +
+ + {#if filteredCves.length === 0} +
+ +

+ {cves.length === 0 ? emptyMessage : 'Sin resultados para tu busqueda.'} +

+
+ {:else} +
+ + + + + + + + + + + + {#each paginatedCves 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}
+
+ +
+
+ Mostrando {rangeStart}-{rangeEnd} de {filteredCves.length} + +
+ +
+ + Pagina {currentPage} de {totalPages} + +
+
+ {/if} +
diff --git a/src/modules/code-report/application/code-report-cve.service.ts b/src/modules/code-report/application/code-report-cve.service.ts index 0f23bf9..3ea39f6 100644 --- a/src/modules/code-report/application/code-report-cve.service.ts +++ b/src/modules/code-report/application/code-report-cve.service.ts @@ -2,6 +2,12 @@ import { collectCveOccurrences, type CompletedAnalysis, type CveOccurrence } fro import type { CodeReportService } from './code-report.service'; import type { CodeReportAnalysisService } from './code-report-analysis.service'; +export type OrganizationCveOccurrence = CveOccurrence & { + projectId: string; + projectSlug: string; + projectName: string; +}; + export class CodeReportCveService { constructor( private readonly codeReportService: CodeReportService, @@ -16,11 +22,45 @@ export class CodeReportCveService { 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; + const completed: CompletedAnalysis = + analysis?.status === 'completed' + ? { + result: analysis.result, + createdAt: + analysis.createdAt instanceof Date + ? analysis.createdAt.toISOString() + : String(analysis.createdAt), + } + : null; return [service.id, completed] as const; }), ); return collectCveOccurrences(services, new Map(analysisEntries)); } + + async getOrganizationCveOccurrences( + projects: { id: string; slug: string; name: string }[], + ): Promise> { + const occurrencesByCve = new Map(); + + for (const project of projects) { + const projectOccurrences = await this.getProjectCveOccurrences(project.id); + + for (const [cveId, occurrences] of projectOccurrences.entries()) { + const merged = occurrencesByCve.get(cveId) ?? []; + merged.push( + ...occurrences.map((occurrence) => ({ + ...occurrence, + projectId: project.id, + projectSlug: project.slug, + projectName: project.name, + })), + ); + occurrencesByCve.set(cveId, merged); + } + } + + return occurrencesByCve; + } } diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 1764cea..e6a7e53 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -20,6 +20,8 @@ export async function load({ locals, url }) { ? await organizationService.tryFindBySlug(orgSlugFromUrl) : await organizationService.getDefaultOrganization(); + const currentProjectSlug = url.pathname.match(/\/projects\/([^/]+)/)?.[1] ?? null; + const projects = locals.user ? ( await Promise.all( @@ -47,5 +49,6 @@ export async function load({ locals, url }) { user: locals.user, organization, projects, + currentProjectSlug, }; } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 9cd36f5..44bdcb0 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -25,12 +25,12 @@ class="min-h-screen bg-slate-50 text-slate-900 font-sans flex flex-col" style={`--sidebar-width:${sidebarCollapsed ? '96px' : '340px'}`} > - {#if $page.url.pathname !== '/login'} + {#if $page?.url?.pathname !== '/login'}
@@ -57,7 +58,7 @@ {/if} - {#if $page.url.pathname !== '/login'} + {#if $page?.url?.pathname !== '/login'}
{/if}
diff --git a/src/routes/org/+page.svelte b/src/routes/org/+page.svelte index 70f913c..153738d 100644 --- a/src/routes/org/+page.svelte +++ b/src/routes/org/+page.svelte @@ -12,7 +12,7 @@ export let data: { organizations: OrganizationRow[] }; $: errorMessage = - $page.url.searchParams.get('error') === 'organization-not-found' + $page?.url?.searchParams?.get('error') === 'organization-not-found' ? 'Organization not found.' : ''; diff --git a/src/routes/org/[org]/cves/+page.server.ts b/src/routes/org/[org]/cves/+page.server.ts new file mode 100644 index 0000000..7473e55 --- /dev/null +++ b/src/routes/org/[org]/cves/+page.server.ts @@ -0,0 +1,44 @@ +import { error } from '@sveltejs/kit'; +import { cancanService } from '../../../../modules/auth'; +import { codeReportCveService } from '../../../../modules/code-report'; +import { projectService } from '../../../../modules/projects'; +import { summarizeCves } from '$lib/code-report/cve-aggregation'; + +export async function load({ parent, locals, url }) { + const { organization } = await parent(); + + const canRead = await cancanService.canSessionUser(locals.user, 'openreport:read', { + scope: 'organization', + organizationId: organization.id, + }); + + if (!canRead) { + throw error(403, 'Forbidden'); + } + + const projects = (await projectService.listProjectsByOrganization(organization.id)).filter( + (project) => project.status === 'active' && project.modules?.codereport, + ); + + const occurrencesByCve = await codeReportCveService.getOrganizationCveOccurrences( + projects.map((project) => ({ + id: project.id, + slug: project.slug, + name: project.name, + })), + ); + + const cves = summarizeCves( + new Map([...occurrencesByCve.entries()].map(([id, occurrences]) => [id, occurrences])), + ).map((cve) => ({ + ...cve, + projectSlugs: [...new Set((occurrencesByCve.get(cve.id) ?? []).map((entry) => entry.projectSlug))], + })); + + return { + orgSlug: organization.slug, + cves, + projects: projects.map((project) => ({ slug: project.slug, name: project.name })), + initialProjectFilter: url.searchParams.get('project') ?? 'all', + }; +} diff --git a/src/routes/org/[org]/cves/+page.svelte b/src/routes/org/[org]/cves/+page.svelte new file mode 100644 index 0000000..516e9eb --- /dev/null +++ b/src/routes/org/[org]/cves/+page.svelte @@ -0,0 +1,34 @@ + + + + Organization CVEs - GitVault Suite + + + diff --git a/src/routes/org/[org]/cves/[cve]/+page.server.ts b/src/routes/org/[org]/cves/[cve]/+page.server.ts new file mode 100644 index 0000000..62b1704 --- /dev/null +++ b/src/routes/org/[org]/cves/[cve]/+page.server.ts @@ -0,0 +1,98 @@ +import { error } from '@sveltejs/kit'; +import { cancanService } from '../../../../../modules/auth'; +import { codeReportCveService } from '../../../../../modules/code-report'; +import { projectService } from '../../../../../modules/projects'; +import { + highestCvssScore, + highestEpssPercentile, + highestEpssScore, + highestSeverity, +} from '$lib/code-report/cve-aggregation'; + +export async function load({ parent, locals, params }) { + const { organization } = await parent(); + + const canRead = await cancanService.canSessionUser(locals.user, 'openreport:read', { + scope: 'organization', + organizationId: organization.id, + }); + + if (!canRead) { + throw error(403, 'Forbidden'); + } + + const projects = (await projectService.listProjectsByOrganization(organization.id)).filter( + (project) => project.status === 'active' && project.modules?.codereport, + ); + + const occurrencesByCve = await codeReportCveService.getOrganizationCveOccurrences( + projects.map((project) => ({ + id: project.id, + slug: project.slug, + name: project.name, + })), + ); + + const occurrences = occurrencesByCve.get(params.cve); + + if (!occurrences || occurrences.length === 0) { + throw error(404, 'CVE not found'); + } + + 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, + title: first.title, + 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, + }; + + const remediations = [ + ...new Map( + occurrences.map((occurrence) => [ + `${occurrence.projectSlug}:${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, + serviceName: occurrence.serviceName, + projectName: occurrence.projectName, + projectSlug: occurrence.projectSlug, + 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 { + orgSlug: organization.slug, + cve, + remediations, + affectedServices, + }; +} diff --git a/src/routes/org/[org]/cves/[cve]/+page.svelte b/src/routes/org/[org]/cves/[cve]/+page.svelte new file mode 100644 index 0000000..c79ef33 --- /dev/null +++ b/src/routes/org/[org]/cves/[cve]/+page.svelte @@ -0,0 +1,57 @@ + + + + {data.cve.id} - Organization CVEs - GitVault Suite + + + diff --git a/src/routes/org/[org]/overview/+page.svelte b/src/routes/org/[org]/overview/+page.svelte index cb59b19..963b954 100644 --- a/src/routes/org/[org]/overview/+page.svelte +++ b/src/routes/org/[org]/overview/+page.svelte @@ -24,7 +24,7 @@ export let data: { projects: ProjectRow[] }; - $: orgSlug = $page.params.org; + $: orgSlug = $page?.params?.org; let searchQuery = ''; diff --git a/src/routes/org/[org]/projects/[slug]/+layout.svelte b/src/routes/org/[org]/projects/[slug]/+layout.svelte index 514f486..987e39d 100644 --- a/src/routes/org/[org]/projects/[slug]/+layout.svelte +++ b/src/routes/org/[org]/projects/[slug]/+layout.svelte @@ -15,7 +15,7 @@ export let data: { project: { id: string; name: string; slug: string } }; $: project = data.project; - $: orgSlug = $page.params.org; + $: orgSlug = $page?.params?.org; $: overviewHref = `/org/${orgSlug}/projects/${project.slug}/overview`; @@ -76,7 +76,7 @@ }, ]; - $: currentPath = $page.url.pathname; + $: currentPath = $page?.url?.pathname; $: currentTab = tabs.find((tab) => tab.href === currentPath) ?? tabs[0]; diff --git a/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte b/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte index 99ee45b..c5a8b78 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte @@ -10,7 +10,7 @@ $: tabs = [ { label: 'Dashboard', href: `${basePath}/dashboard`, icon: Activity }, { label: 'Servicios', href: `${basePath}/services`, icon: Boxes }, - { label: 'CVEs', href: `${basePath}/cves`, icon: ShieldAlert }, + { label: 'CVEs', href: `/org/${orgSlug}/cves`, icon: ShieldAlert }, { label: 'Security Policy', href: `${basePath}/security-policy`, icon: CheckSquare }, { label: 'Historial', href: `${basePath}/history`, icon: History }, { label: 'Ajustes', href: `${basePath}/settings`, icon: Settings }, 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 index b949012..8e0c316 100644 --- 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 @@ -1,22 +1,5 @@ -import { error } from '@sveltejs/kit'; -import { cancanService } from '../../../../../../../modules/auth'; -import { codeReportCveService } from '../../../../../../../modules/code-report'; -import { summarizeCves } from '$lib/code-report/cve-aggregation'; +import { redirect } from '@sveltejs/kit'; -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) }; +export async function load({ params }) { + throw redirect(307, `/org/${params.org}/cves`); } 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 454c56d..c0523d1 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 @@ Code Report - CVEs - GitVault Suite -
-
- -

- Este listado muestra los CVEs detectados en el último análisis de cada servicio. - Los servicios sin un análisis reciente completado no aparecen reflejados aquí. -

-
- -
- - -
- - {#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 paginatedCves 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}
-
- -
-
- 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 e615a7d..cf9d563 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,82 +1,5 @@ -import { error } from '@sveltejs/kit'; -import { cancanService } from '../../../../../../../../modules/auth'; -import { codeReportCveService } from '../../../../../../../../modules/code-report'; -import { - highestCvssScore, - highestEpssPercentile, - highestEpssScore, - highestSeverity, -} from '$lib/code-report/cve-aggregation'; +import { redirect } from '@sveltejs/kit'; -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 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, - title: first.title, - 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, - 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, remediations, affectedServices }; +export async function load({ params }) { + throw redirect(307, `/org/${params.org}/cves/${params.cve}`); } 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 0816c12..799853a 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,501 +1,57 @@ {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'} -
-
- -
-

Severidad

-

{data.cve.severity}

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

Publicado

-

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

-
-
- {/if} -
- -
-

Overview

-

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

- {#if data.cve.lastModifiedDate} -

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

- {/if} -
- -
-

CWE

- {#if data.cve.cweIds.length === 0} -

No se ha indicado una clasificación CWE para este CVE.

- {:else} -
- {#each data.cve.cweIds as cweId (cweId)} - - {cweId} - - {/each} -
- {/if} -
- -
-
-
- - CVSS - - - -
-

- {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} -
- -
- -
- - - - - - - - - - - - - {#each paginatedAffectedServices as service (service.serviceId + service.target + service.packageName)} - {@const target = splitTarget(service.target)} - - - - - - - - - {/each} - {#if paginatedAffectedServices.length === 0} - - - - {/if} - -
ServicioPaqueteVersión instaladaVersión corregidaObjetivoÚltimo escaneo
- - {service.serviceName} - - {service.packageName} - {service.installedVersion} - - {#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.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} -
+ 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 index 71a2847..45b306a 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.svelte @@ -125,10 +125,10 @@ onDestroy(() => chart?.destroy()); - $: orgSlug = $page.params.org; - $: projectSlug = $page.params.slug; + $: orgSlug = $page?.params?.org ?? ''; + $: projectSlug = $page?.params?.slug ?? ''; $: cvesHref = (id?: string) => - `/org/${orgSlug}/projects/${projectSlug}/code-report/cves${id ? `/${id}` : ''}`; + `/org/${orgSlug}/cves${id ? `/${id}` : ''}`; $: servicesHref = (slug: string) => `/org/${orgSlug}/projects/${projectSlug}/code-report/services/${slug}`; $: securityPolicyHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/security-policy`; 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 f30d479..13de117 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 @@ -8,7 +8,7 @@ analyses: any[]; riskWeights: { critical: number; high: number; medium: number; low: number }; }; - let serviceFilter = $page.url.searchParams.get('service') ?? 'all'; + let serviceFilter = $page?.url?.searchParams?.get('service') ?? 'all'; let statusFilter = 'all'; let dateFilter = ''; let query = ''; @@ -84,7 +84,7 @@ (statusFilter === 'all' || analysis.status === statusFilter) && (!dateFilter || analysis.createdAt.slice(0, 10) === dateFilter), ); - $: base = `/org/${$page.params.org}/projects/${$page.params.slug}/code-report`; + $: base = `/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report`; Histórico de Code Report - GitVault Suite 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 1dfcfa5..833476f 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 @@ -9,7 +9,7 @@ analysisHistory: any[]; riskWeights: { critical: number; high: number; medium: number; low: number }; }; - $: historyHref = `/org/${$page.params.org}/projects/${$page.params.slug}/code-report/history`; + $: historyHref = `/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report/history`; {data.service.name} - Histórico de Code Report @@ -32,6 +32,6 @@ analysis={data.analysis} analysisHistory={data.analysisHistory} riskWeights={data.riskWeights} - securityPoliciesHref={`/org/${$page.params.org}/projects/${$page.params.slug}/code-report/security-policy`} + securityPoliciesHref={`/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report/security-policy`} />
diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte index ab3a347..a3576e5 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte @@ -15,8 +15,8 @@ let searchQuery = ''; let typeFilter: 'all' | SecurityPolicy['type'] = 'all'; - $: orgSlug = $page.params.org; - $: projectSlug = $page.params.slug; + $: orgSlug = $page?.params?.org; + $: projectSlug = $page?.params?.slug; $: baseHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/security-policy`; $: filteredPolicies = data.policies.filter((policy) => { diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte index 2d8b5f9..e2c31c7 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte @@ -42,8 +42,8 @@ let activeTab: 'detail' | 'services' = 'detail'; $: policy = data.policy; - $: baseHref = `/org/${$page.params.org}/projects/${$page.params.slug}/code-report/security-policy`; - $: servicesHref = `/org/${$page.params.org}/projects/${$page.params.slug}/code-report/services`; + $: baseHref = `/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report/security-policy`; + $: servicesHref = `/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report/services`; $: failingServicesCount = data.affectedServices.filter((service) => !service.passing).length; $: if (form?.success) editing = false; diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte index 2ee993a..070b142 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte @@ -9,7 +9,7 @@ }; export let form: { error?: string } | null; - $: baseHref = `/org/${$page.params.org}/projects/${$page.params.slug}/code-report/security-policy`; + $: baseHref = `/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report/security-policy`; Nueva política de seguridad - GitVault Suite 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 d0a8df8..bf8b548 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 @@ -40,8 +40,8 @@ let searchQuery = ''; - $: orgSlug = $page.params.org; - $: projectSlug = $page.params.slug; + $: orgSlug = $page?.params?.org; + $: projectSlug = $page?.params?.slug; $: baseHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/services`; $: filteredServices = data.services.filter((service) => { 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 0c37475..256314d 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 @@ -15,8 +15,8 @@ error?: string; } | null; - $: orgSlug = $page.params.org; - $: projectSlug = $page.params.slug; + $: orgSlug = $page?.params?.org; + $: projectSlug = $page?.params?.slug; $: servicesHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/services`; $: historyHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/history?service=${data.service.slug}`; diff --git a/src/routes/org/[org]/projects/[slug]/overview/+page.svelte b/src/routes/org/[org]/projects/[slug]/overview/+page.svelte index 905242c..fde47ad 100644 --- a/src/routes/org/[org]/projects/[slug]/overview/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/overview/+page.svelte @@ -36,7 +36,7 @@ export let data: { project: ProjectRow }; $: project = data.project; - $: orgSlug = $page.params.org; + $: orgSlug = $page?.params?.org; $: moduleInfo = [ { diff --git a/src/routes/org/[org]/projects/[slug]/settings/overview/+page.svelte b/src/routes/org/[org]/projects/[slug]/settings/overview/+page.svelte index ef5fdbd..59eec2f 100644 --- a/src/routes/org/[org]/projects/[slug]/settings/overview/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/settings/overview/+page.svelte @@ -37,7 +37,7 @@ export let data: { project: ProjectRow }; $: project = data.project; - $: orgSlug = $page.params.org; + $: orgSlug = $page?.params?.org; $: isArchived = project.status === 'inactive'; let saving = false; @@ -60,19 +60,19 @@ key: 'vault', label: 'Vault', icon: Shield, - href: `/org/${$page.params.org}/projects/${data.project.slug}/vault`, + href: `/org/${$page?.params?.org}/projects/${data.project.slug}/vault`, }, { key: 'codereport', label: 'Code Report', icon: BarChart3, - href: `/org/${$page.params.org}/projects/${data.project.slug}/code-report`, + href: `/org/${$page?.params?.org}/projects/${data.project.slug}/code-report`, }, { key: 'stateiac', label: 'State IaC', icon: GitBranch, - href: `/org/${$page.params.org}/projects/${data.project.slug}/state-iac`, + href: `/org/${$page?.params?.org}/projects/${data.project.slug}/state-iac`, }, ]; From bfbeae17e3ef7259085261bd9ed8cf2eba5c1930 Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 19:32:27 +0200 Subject: [PATCH 8/9] fix urls --- src/routes/org/[org]/overview/+page.svelte | 4 ++-- src/routes/org/[org]/projects/[slug]/+layout.svelte | 11 +++++++++-- .../[org]/projects/[slug]/code-report/+layout.svelte | 11 +++++++++-- .../[slug]/code-report/dashboard/+page.svelte | 5 +++-- .../projects/[slug]/code-report/history/+page.svelte | 3 ++- .../code-report/history/[analysisID]/+page.svelte | 7 +++++-- .../[slug]/code-report/security-policy/+page.svelte | 9 ++++++--- .../code-report/security-policy/[id]/+page.svelte | 7 +++++-- .../code-report/security-policy/new/+page.svelte | 5 ++++- .../projects/[slug]/code-report/services/+page.svelte | 9 ++++++--- .../code-report/services/[serviceSlug]/+page.svelte | 5 +++-- .../org/[org]/projects/[slug]/overview/+page.svelte | 2 +- .../projects/[slug]/settings/overview/+page.svelte | 8 ++++---- 13 files changed, 59 insertions(+), 27 deletions(-) diff --git a/src/routes/org/[org]/overview/+page.svelte b/src/routes/org/[org]/overview/+page.svelte index 963b954..d991486 100644 --- a/src/routes/org/[org]/overview/+page.svelte +++ b/src/routes/org/[org]/overview/+page.svelte @@ -22,9 +22,9 @@ updatedAt: string; }; - export let data: { projects: ProjectRow[] }; + export let data: { projects: ProjectRow[]; organization?: { slug?: string | null } }; - $: orgSlug = $page?.params?.org; + $: orgSlug = data.organization?.slug ?? $page?.params?.org ?? ''; let searchQuery = ''; diff --git a/src/routes/org/[org]/projects/[slug]/+layout.svelte b/src/routes/org/[org]/projects/[slug]/+layout.svelte index 987e39d..cc94d0f 100644 --- a/src/routes/org/[org]/projects/[slug]/+layout.svelte +++ b/src/routes/org/[org]/projects/[slug]/+layout.svelte @@ -12,10 +12,17 @@ Users, } from 'lucide-svelte'; - export let data: { project: { id: string; name: string; slug: string } }; + export let data: { + project: { + id: string; + name: string; + slug: string; + organization?: { slug?: string | null } | null; + }; + }; $: project = data.project; - $: orgSlug = $page?.params?.org; + $: orgSlug = project.organization?.slug ?? $page?.params?.org ?? ''; $: overviewHref = `/org/${orgSlug}/projects/${project.slug}/overview`; diff --git a/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte b/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte index c5a8b78..39fa6d0 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/+layout.svelte @@ -2,8 +2,15 @@ import { page } from '$app/stores'; import { Settings, ShieldAlert, Activity, FileKey, CheckSquare, Boxes, History } from 'lucide-svelte'; - $: orgSlug = $page?.params?.org ?? ''; - $: projectSlug = $page?.params?.slug ?? ''; + export let data: { + project?: { + slug?: string | null; + organization?: { slug?: string | null } | null; + }; + }; + + $: orgSlug = data?.project?.organization?.slug ?? $page?.params?.org ?? ''; + $: projectSlug = data?.project?.slug ?? $page?.params?.slug ?? ''; $: basePath = `/org/${orgSlug}/projects/${projectSlug}/code-report`; 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 index 45b306a..da824a6 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/dashboard/+page.svelte @@ -69,6 +69,7 @@ topCves: CveRow[]; riskiestServices: ServiceRisk[]; staleServices: StaleService[]; + project?: { slug?: string; organization?: { slug?: string | null } | null }; }; const severityStyles: Record = { @@ -125,8 +126,8 @@ onDestroy(() => chart?.destroy()); - $: orgSlug = $page?.params?.org ?? ''; - $: projectSlug = $page?.params?.slug ?? ''; + $: orgSlug = data.project?.organization?.slug ?? $page?.params?.org ?? ''; + $: projectSlug = data.project?.slug ?? $page?.params?.slug ?? ''; $: cvesHref = (id?: string) => `/org/${orgSlug}/cves${id ? `/${id}` : ''}`; $: servicesHref = (slug: string) => 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 13de117..a230a78 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 @@ -7,6 +7,7 @@ services: { id: string; slug: string; name: string }[]; analyses: any[]; riskWeights: { critical: number; high: number; medium: number; low: number }; + project?: { slug?: string; organization?: { slug?: string | null } | null }; }; let serviceFilter = $page?.url?.searchParams?.get('service') ?? 'all'; let statusFilter = 'all'; @@ -84,7 +85,7 @@ (statusFilter === 'all' || analysis.status === statusFilter) && (!dateFilter || analysis.createdAt.slice(0, 10) === dateFilter), ); - $: base = `/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report`; + $: base = `/org/${data.project?.organization?.slug ?? $page?.params?.org ?? ''}/projects/${data.project?.slug ?? $page?.params?.slug ?? ''}/code-report`; Histórico de Code Report - GitVault Suite 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 833476f..b7a348d 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 @@ -8,8 +8,11 @@ analysis: any; analysisHistory: any[]; riskWeights: { critical: number; high: number; medium: number; low: number }; + project?: { slug?: string; organization?: { slug?: string | null } | null }; }; - $: historyHref = `/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report/history`; + $: orgSlug = data.project?.organization?.slug ?? $page?.params?.org ?? ''; + $: projectSlug = data.project?.slug ?? $page?.params?.slug ?? ''; + $: historyHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/history`; {data.service.name} - Histórico de Code Report @@ -32,6 +35,6 @@ analysis={data.analysis} analysisHistory={data.analysisHistory} riskWeights={data.riskWeights} - securityPoliciesHref={`/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report/security-policy`} + securityPoliciesHref={`/org/${orgSlug}/projects/${projectSlug}/code-report/security-policy`} />
diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte index a3576e5..991fd8a 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/+page.svelte @@ -9,14 +9,17 @@ type SecurityPolicy, } from '$lib/code-report/security-policy'; - export let data: { policies: SecurityPolicy[] }; + export let data: { + policies: SecurityPolicy[]; + project?: { slug?: string; organization?: { slug?: string | null } | null }; + }; export let form: { error?: string } | null; let searchQuery = ''; let typeFilter: 'all' | SecurityPolicy['type'] = 'all'; - $: orgSlug = $page?.params?.org; - $: projectSlug = $page?.params?.slug; + $: orgSlug = data.project?.organization?.slug ?? $page?.params?.org ?? ''; + $: projectSlug = data.project?.slug ?? $page?.params?.slug ?? ''; $: baseHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/security-policy`; $: filteredPolicies = data.policies.filter((policy) => { diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte index e2c31c7..5441cea 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/[id]/+page.svelte @@ -28,6 +28,7 @@ affectedServices: AffectedService[]; services: { id: string; slug: string; name: string; tags: string[] }[]; tags: string[]; + project?: { slug?: string; organization?: { slug?: string | null } | null }; }; export let form: { error?: string; @@ -42,8 +43,10 @@ let activeTab: 'detail' | 'services' = 'detail'; $: policy = data.policy; - $: baseHref = `/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report/security-policy`; - $: servicesHref = `/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report/services`; + $: orgSlug = data.project?.organization?.slug ?? $page?.params?.org ?? ''; + $: projectSlug = data.project?.slug ?? $page?.params?.slug ?? ''; + $: baseHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/security-policy`; + $: servicesHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/services`; $: failingServicesCount = data.affectedServices.filter((service) => !service.passing).length; $: if (form?.success) editing = false; diff --git a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte index 070b142..4b65c7d 100644 --- a/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/code-report/security-policy/new/+page.svelte @@ -6,10 +6,13 @@ export let data: { services: { id: string; slug: string; name: string; tags: string[] }[]; tags: string[]; + project?: { slug?: string; organization?: { slug?: string | null } | null }; }; export let form: { error?: string } | null; - $: baseHref = `/org/${$page?.params?.org}/projects/${$page?.params?.slug}/code-report/security-policy`; + $: orgSlug = data.project?.organization?.slug ?? $page?.params?.org ?? ''; + $: projectSlug = data.project?.slug ?? $page?.params?.slug ?? ''; + $: baseHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/security-policy`; Nueva política de seguridad - GitVault Suite 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 bf8b548..b14bdd1 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 @@ -28,7 +28,10 @@ { key: 'low', label: 'Low', className: 'border-slate-200 bg-slate-50 text-slate-600' }, ]; - export let data: { services: ServiceRow[] }; + export let data: { + services: ServiceRow[]; + project?: { slug?: string; organization?: { slug?: string | null } | null }; + }; export let form: { success?: boolean; error?: string; @@ -40,8 +43,8 @@ let searchQuery = ''; - $: orgSlug = $page?.params?.org; - $: projectSlug = $page?.params?.slug; + $: orgSlug = data.project?.organization?.slug ?? $page?.params?.org ?? ''; + $: projectSlug = data.project?.slug ?? $page?.params?.slug ?? ''; $: baseHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/services`; $: filteredServices = data.services.filter((service) => { 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 256314d..7186a05 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 @@ -10,13 +10,14 @@ latestByTool: Record; analysisHistory: any[]; riskWeights: { critical: number; high: number; medium: number; low: number }; + project?: { slug?: string; organization?: { slug?: string | null } | null }; }; export let form: { error?: string; } | null; - $: orgSlug = $page?.params?.org; - $: projectSlug = $page?.params?.slug; + $: orgSlug = data.project?.organization?.slug ?? $page?.params?.org ?? ''; + $: projectSlug = data.project?.slug ?? $page?.params?.slug ?? ''; $: servicesHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/services`; $: historyHref = `/org/${orgSlug}/projects/${projectSlug}/code-report/history?service=${data.service.slug}`; diff --git a/src/routes/org/[org]/projects/[slug]/overview/+page.svelte b/src/routes/org/[org]/projects/[slug]/overview/+page.svelte index fde47ad..ce17dfb 100644 --- a/src/routes/org/[org]/projects/[slug]/overview/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/overview/+page.svelte @@ -36,7 +36,7 @@ export let data: { project: ProjectRow }; $: project = data.project; - $: orgSlug = $page?.params?.org; + $: orgSlug = $page?.params?.org ?? ''; $: moduleInfo = [ { diff --git a/src/routes/org/[org]/projects/[slug]/settings/overview/+page.svelte b/src/routes/org/[org]/projects/[slug]/settings/overview/+page.svelte index 59eec2f..d68ca6e 100644 --- a/src/routes/org/[org]/projects/[slug]/settings/overview/+page.svelte +++ b/src/routes/org/[org]/projects/[slug]/settings/overview/+page.svelte @@ -37,7 +37,7 @@ export let data: { project: ProjectRow }; $: project = data.project; - $: orgSlug = $page?.params?.org; + $: orgSlug = $page?.params?.org ?? ''; $: isArchived = project.status === 'inactive'; let saving = false; @@ -60,19 +60,19 @@ key: 'vault', label: 'Vault', icon: Shield, - href: `/org/${$page?.params?.org}/projects/${data.project.slug}/vault`, + href: `/org/${orgSlug}/projects/${data.project.slug}/vault`, }, { key: 'codereport', label: 'Code Report', icon: BarChart3, - href: `/org/${$page?.params?.org}/projects/${data.project.slug}/code-report`, + href: `/org/${orgSlug}/projects/${data.project.slug}/code-report`, }, { key: 'stateiac', label: 'State IaC', icon: GitBranch, - href: `/org/${$page?.params?.org}/projects/${data.project.slug}/state-iac`, + href: `/org/${orgSlug}/projects/${data.project.slug}/state-iac`, }, ]; From 6d993d70778433017cdf8084f4ee8d28e87860fa Mon Sep 17 00:00:00 2001 From: Carlos Lopez Date: Tue, 25 Aug 2026 19:50:17 +0200 Subject: [PATCH 9/9] remove apis unused --- src/hooks.server.ts | 12 +- src/routes/api/backends/+server.ts | 45 -- src/routes/api/backends/[id]/+server.ts | 17 - src/routes/api/organizations/+server.ts | 42 -- src/routes/api/organizations/[id]/+server.ts | 56 -- src/routes/api/projects/+server.ts | 58 --- src/routes/api/projects/[id]/+server.ts | 82 --- src/routes/api/projects/sync/+server.ts | 30 -- src/routes/api/roles/+server.ts | 51 -- src/routes/api/roles/[id]/+server.ts | 36 -- src/routes/api/users/+server.ts | 59 --- src/routes/api/users/[id]/+server.ts | 45 -- .../cluster-settings/orgs/+page.server.ts | 44 ++ src/routes/cluster-settings/orgs/+page.svelte | 291 +++++------ .../orgs/[org]/+page.server.ts | 46 +- .../cluster-settings/orgs/[org]/+page.svelte | 150 +++--- .../[slug]/settings/overview/+page.server.ts | 107 ++++ .../[slug]/settings/overview/+page.svelte | 241 +++++---- .../org/[org]/settings/global/+page.svelte | 56 +- .../[org]/settings/projects/+page.server.ts | 56 +- .../org/[org]/settings/projects/+page.svelte | 320 ++++++------ .../[org]/settings/storage/+page.server.ts | 2 +- .../org/[org]/settings/storage/+page.svelte | 483 ------------------ 23 files changed, 739 insertions(+), 1590 deletions(-) delete mode 100644 src/routes/api/backends/+server.ts delete mode 100644 src/routes/api/backends/[id]/+server.ts delete mode 100644 src/routes/api/organizations/+server.ts delete mode 100644 src/routes/api/organizations/[id]/+server.ts delete mode 100644 src/routes/api/projects/+server.ts delete mode 100644 src/routes/api/projects/[id]/+server.ts delete mode 100644 src/routes/api/projects/sync/+server.ts delete mode 100644 src/routes/api/roles/+server.ts delete mode 100644 src/routes/api/roles/[id]/+server.ts delete mode 100644 src/routes/api/users/+server.ts delete mode 100644 src/routes/api/users/[id]/+server.ts create mode 100644 src/routes/cluster-settings/orgs/+page.server.ts create mode 100644 src/routes/org/[org]/projects/[slug]/settings/overview/+page.server.ts delete mode 100644 src/routes/org/[org]/settings/storage/+page.svelte diff --git a/src/hooks.server.ts b/src/hooks.server.ts index c416505..3651215 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -10,14 +10,12 @@ const authWithToken = async (token: string) => { }; export const handle: Handle = async ({ event, resolve }) => { - if ( - event.url.pathname === '/login' - ) { + if (event.url.pathname === '/login') { return resolve(event); } if (event.url.pathname.startsWith('/api/')) { - const token = event.request.headers.get('Authorization') || ""; + const token = event.request.headers.get('Authorization') || ''; if (!token || token.trim() === '') { return new Response(null, { status: 401 }); } @@ -54,11 +52,7 @@ export const handle: Handle = async ({ event, resolve }) => { ) { return new Response(null, { status: 302, headers: { location: '/' } }); } - } else if ( - event.url.pathname.startsWith('/cluster-settings') || - event.url.pathname.startsWith('/api/system') || - event.url.pathname.startsWith('/api/organizations') - ) { + } else if (event.url.pathname.startsWith('/cluster-settings')) { if (!cancanService.canAccessAdminArea(currentUser)) { return new Response(null, { status: 302, headers: { location: '/' } }); } diff --git a/src/routes/api/backends/+server.ts b/src/routes/api/backends/+server.ts deleted file mode 100644 index 7d3de8b..0000000 --- a/src/routes/api/backends/+server.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { storageBackendService } from '../../../modules/config'; -import { cancanService } from '../../../modules/auth'; - -export async function GET({ locals }) { - if (!(await cancanService.canSessionUser(locals.user, 'stateiac:read', { scope: 'cluster' }))) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - return json({ backends: storageBackendService.list() }); -} - -export async function POST({ request, locals }) { - try { - const data = await request.json(); - - if ( - !(await cancanService.canSessionUser( - locals.user, - data.id ? 'stateiac:update' : 'stateiac:create', - { scope: 'cluster' }, - )) - ) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - const id = storageBackendService.upsert({ - id: data.id, - name: data.name, - provider: data.provider, - bucket: data.bucket, - region: data.region || null, - accessKeyId: data.accessKeyId || null, - secretAccessKey: data.secretAccessKey || null, - endpoint: data.endpoint || null, - gcpProjectId: data.gcpProjectId || null, - gcpCredentials: data.gcpCredentials || null, - }); - - return json({ success: true, id }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 400 }); - } -} diff --git a/src/routes/api/backends/[id]/+server.ts b/src/routes/api/backends/[id]/+server.ts deleted file mode 100644 index 9bfff35..0000000 --- a/src/routes/api/backends/[id]/+server.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { storageBackendService } from '../../../../modules/config'; -import { cancanService } from '../../../../modules/auth'; - -export async function DELETE({ params, locals }) { - if (!(await cancanService.canSessionUser(locals.user, 'stateiac:delete', { scope: 'cluster' }))) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - storageBackendService.deleteById(params.id); - return json({ success: true }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 400 }); - } -} diff --git a/src/routes/api/organizations/+server.ts b/src/routes/api/organizations/+server.ts deleted file mode 100644 index bd9ca2f..0000000 --- a/src/routes/api/organizations/+server.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { organizationService } from '../../../modules/organization'; -import { cancanService } from '../../../modules/auth'; - -export async function GET({ locals }) { - if (!cancanService.canAccessAdminArea(locals.user)) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const organizations = await organizationService.listOrganizations(); - return json({ organizations }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 500 }); - } -} - -export async function POST({ request, locals }) { - if (!cancanService.canAccessAdminArea(locals.user)) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const data = (await request.json()) as { - name?: string; - slug?: string; - description?: string; - }; - - const organization = await organizationService.createOrganization({ - name: String(data.name || ''), - slug: data.slug ? String(data.slug) : undefined, - description: data.description ? String(data.description) : undefined, - }); - - return json({ success: true, organization }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 400 }); - } -} diff --git a/src/routes/api/organizations/[id]/+server.ts b/src/routes/api/organizations/[id]/+server.ts deleted file mode 100644 index fc43873..0000000 --- a/src/routes/api/organizations/[id]/+server.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { organizationService } from '../../../../modules/organization'; -import { cancanService } from '../../../../modules/auth'; - -export async function GET({ params, locals }) { - if (!cancanService.canAccessAdminArea(locals.user)) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const organization = await organizationService.getOrganization(params.id); - return json({ organization }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 404 }); - } -} - -export async function PATCH({ request, params, locals }) { - if (!cancanService.canAccessAdminArea(locals.user)) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const data = (await request.json()) as { - name?: string; - slug?: string; - description?: string; - }; - - const organization = await organizationService.updateOrganization(params.id, { - name: data.name, - slug: data.slug, - description: data.description, - }); - - return json({ success: true, organization }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 400 }); - } -} - -export async function DELETE({ params, locals }) { - if (!cancanService.canAccessAdminArea(locals.user)) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - await organizationService.deleteOrganization(params.id); - return json({ success: true }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 400 }); - } -} diff --git a/src/routes/api/projects/+server.ts b/src/routes/api/projects/+server.ts deleted file mode 100644 index f94f721..0000000 --- a/src/routes/api/projects/+server.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { projectService } from '../../../modules/projects'; -import { cancanService } from '../../../modules/auth'; - -export async function GET({ url, locals }) { - if (!(await cancanService.canSessionUser(locals.user, 'stateiac:read', { scope: 'cluster' }))) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const organizationId = url.searchParams.get('organizationId'); - const projects = organizationId - ? await projectService.listProjectsByOrganization(organizationId) - : await projectService.listProjects(); - return json({ projects }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 500 }); - } -} - -export async function POST({ request, locals }) { - try { - const data = (await request.json()) as { - organizationId?: string; - name?: string; - slug?: string; - description?: string; - status?: string; - modules?: { vault?: boolean; openreport?: boolean; stateiac?: boolean }; - }; - - const canCreate = data.organizationId - ? await cancanService.canSessionUser(locals.user, 'stateiac:create', { - scope: 'organization', - organizationId: data.organizationId, - }) - : await cancanService.canSessionUser(locals.user, 'stateiac:create', { scope: 'cluster' }); - - if (!canCreate) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - const project = await projectService.createProject({ - organizationId: String(data.organizationId || ''), - name: String(data.name || ''), - slug: data.slug ? String(data.slug) : undefined, - description: data.description ? String(data.description) : undefined, - status: data.status ? String(data.status) : undefined, - modules: data.modules, - }); - - return json({ success: true, project }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 400 }); - } -} diff --git a/src/routes/api/projects/[id]/+server.ts b/src/routes/api/projects/[id]/+server.ts deleted file mode 100644 index 211c975..0000000 --- a/src/routes/api/projects/[id]/+server.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { projectService } from '../../../../modules/projects'; -import { cancanService } from '../../../../modules/auth'; - -export async function GET({ params, locals }) { - try { - const project = await projectService.getProject(params.id); - if ( - !(await cancanService.canSessionUser(locals.user, 'stateiac:read', { - scope: 'project', - projectId: project.id, - organizationId: project.organization?.id, - })) - ) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - return json({ project }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 404 }); - } -} - -export async function PATCH({ request, params, locals }) { - try { - const currentProject = await projectService.getProject(params.id); - if ( - !(await cancanService.canSessionUser(locals.user, 'stateiac:update', { - scope: 'project', - projectId: currentProject.id, - organizationId: currentProject.organization?.id, - })) - ) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - const data = (await request.json()) as { - organizationId?: string; - name?: string; - slug?: string; - description?: string; - status?: string; - modules?: { vault?: boolean; openreport?: boolean; stateiac?: boolean }; - }; - - const project = await projectService.updateProject(params.id, { - organizationId: data.organizationId, - name: data.name, - slug: data.slug, - description: data.description, - status: data.status, - modules: data.modules, - }); - - return json({ success: true, project }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 400 }); - } -} - -export async function DELETE({ params, locals }) { - try { - const project = await projectService.getProject(params.id); - if ( - !(await cancanService.canSessionUser(locals.user, 'stateiac:delete', { - scope: 'project', - projectId: project.id, - organizationId: project.organization?.id, - })) - ) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - await projectService.deleteProject(params.id); - return json({ success: true }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 400 }); - } -} diff --git a/src/routes/api/projects/sync/+server.ts b/src/routes/api/projects/sync/+server.ts deleted file mode 100644 index 0567557..0000000 --- a/src/routes/api/projects/sync/+server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { storageBackendService } from '../../../../modules/config'; -import { projectService } from '../../../../modules/projects'; -import { storageService } from '../../../../modules/storage'; -import { cancanService } from '../../../../modules/auth'; - -export async function POST({ cookies, locals }) { - if (!(await cancanService.canSessionUser(locals.user, 'stateiac:update', { scope: 'cluster' }))) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const backends = storageBackendService.list(); - if (!backends.length) throw new Error('No storage configured'); - - const activeId = cookies.get('active_backend') || backends[0].id; - const config = - storageBackendService.getById(activeId) || storageBackendService.getById(backends[0].id); - - if (!config) throw new Error('No storage configured'); - - const { states: files } = await storageService.listPulumiStates(config); - const count = projectService.syncFromPulumiStateKeys(files); - - return json({ success: true, count }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Unknown error'; - return json({ error: message }, { status: 500 }); - } -} diff --git a/src/routes/api/roles/+server.ts b/src/routes/api/roles/+server.ts deleted file mode 100644 index 57f216a..0000000 --- a/src/routes/api/roles/+server.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { roleService, cancanService } from '../../../modules/auth'; - -export async function GET({ locals, url }) { - if (!locals.user) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const scope = (url.searchParams.get('scope') ?? 'cluster') as - 'cluster' | 'organization' | 'project'; - const scopeId = - url.searchParams.get('organizationId') ?? url.searchParams.get('projectId') ?? undefined; - const roles = await roleService.listRoles(scope, scopeId); - return json({ roles }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to list roles'; - return json({ error: message }, { status: 500 }); - } -} - -export async function POST({ request, locals }) { - if (!cancanService.canAccessAdminArea(locals.user)) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const data = (await request.json()) as { - name?: string; - slug?: string; - permissions?: string[]; - scope?: 'cluster' | 'organization' | 'project'; - organizationId?: string; - projectId?: string; - }; - - const role = await roleService.createRole({ - name: data.name ?? '', - slug: data.slug ?? '', - permissions: Array.isArray(data.permissions) ? data.permissions : [], - scope: data.scope, - organizationId: data.organizationId, - projectId: data.projectId, - }); - - return json({ success: true, role }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to create role'; - return json({ error: message }, { status: 400 }); - } -} diff --git a/src/routes/api/roles/[id]/+server.ts b/src/routes/api/roles/[id]/+server.ts deleted file mode 100644 index c01eca5..0000000 --- a/src/routes/api/roles/[id]/+server.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { roleService, cancanService } from '../../../../modules/auth'; - -export async function PATCH({ request, params, locals }) { - if (!cancanService.canAccessAdminArea(locals.user)) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const data = (await request.json()) as { name?: string; permissions?: string[] }; - - const role = await roleService.updateRole(params.id, { - name: data.name, - permissions: data.permissions, - }); - - return json({ success: true, role }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to update role'; - return json({ error: message }, { status: 400 }); - } -} - -export async function DELETE({ params, locals }) { - if (!cancanService.canAccessAdminArea(locals.user)) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - await roleService.deleteRole(params.id); - return json({ success: true }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to delete role'; - return json({ error: message }, { status: 400 }); - } -} diff --git a/src/routes/api/users/+server.ts b/src/routes/api/users/+server.ts deleted file mode 100644 index 7023aef..0000000 --- a/src/routes/api/users/+server.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { userService } from '../../../modules/auth'; - -export async function GET({ locals }) { - if (!locals.user) { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const users = await userService.listUsers(); - - return json({ users }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to list users'; - return json({ error: message }, { status: 500 }); - } -} - -export async function POST({ request, locals }) { - if (!locals.user || locals.user.role !== 'admin') { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const data = (await request.json()) as { username?: string; password?: string; role?: string }; - - const username = data.username?.trim() || ''; - const password = data.password?.trim() || ''; - const role = data.role === 'admin' ? 'admin' : 'developer'; - - if (!username) { - return json({ error: 'Username is required' }, { status: 400 }); - } - - if (!password) { - return json({ error: 'Password is required' }, { status: 400 }); - } - - const user = await userService.createUser({ - username, - password, - role, - email: null, - }); - - return json({ - success: true, - user, - }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to create user'; - - if (message.includes('UNIQUE constraint failed')) { - return json({ error: 'Username already exists' }, { status: 400 }); - } - - return json({ error: message }, { status: 400 }); - } -} \ No newline at end of file diff --git a/src/routes/api/users/[id]/+server.ts b/src/routes/api/users/[id]/+server.ts deleted file mode 100644 index f792534..0000000 --- a/src/routes/api/users/[id]/+server.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { json } from '@sveltejs/kit'; -import { userManagementService } from '../../../../modules/auth'; - -export async function PATCH({ request, params, locals }) { - if (!locals.user || locals.user.role !== 'admin') { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - try { - const data = (await request.json()) as { password?: string; role?: string }; - const id = params.id; - - await userManagementService.updateUser({ - actorUserId: locals.user.id, - targetUserId: id, - password: data.password, - role: data.role === 'admin' ? 'admin' : data.role === 'developer' ? 'developer' : undefined, - }); - - return json({ success: true }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to update user'; - return json({ error: message }, { status: 400 }); - } -} - -export async function DELETE({ params, locals }) { - if (!locals.user || locals.user.role !== 'admin') { - return json({ error: 'Forbidden' }, { status: 403 }); - } - - const id = params.id; - - if (id === locals.user.id) { - return json({ error: 'You cannot delete your own account.' }, { status: 400 }); - } - - try { - await userManagementService.deleteUser(locals.user.id, id); - return json({ success: true }); - } catch (error: unknown) { - const message = error instanceof Error ? error.message : 'Failed to delete user'; - return json({ error: message }, { status: 400 }); - } -} \ No newline at end of file diff --git a/src/routes/cluster-settings/orgs/+page.server.ts b/src/routes/cluster-settings/orgs/+page.server.ts new file mode 100644 index 0000000..553e657 --- /dev/null +++ b/src/routes/cluster-settings/orgs/+page.server.ts @@ -0,0 +1,44 @@ +import { fail } from '@sveltejs/kit'; +import { cancanService } from '../../../modules/auth'; +import { organizationService } from '../../../modules/organization'; + +function errorResponse(error: unknown) { + return fail(400, { + error: error instanceof Error ? error.message : 'Organization action failed.', + }); +} + +export async function load() { + const organizations = await organizationService.listOrganizations(); + return { organizations }; +} + +export const actions = { + async createOrganization({ request, locals }) { + if (!cancanService.canAccessAdminArea(locals.user)) return fail(403, { error: 'Forbidden' }); + + try { + const form = await request.formData(); + const organization = await organizationService.createOrganization({ + name: String(form.get('name') ?? ''), + slug: String(form.get('slug') ?? '') || undefined, + description: String(form.get('description') ?? '') || undefined, + }); + return { success: true, organization }; + } catch (error: unknown) { + return errorResponse(error); + } + }, + + async deleteOrganization({ request, locals }) { + if (!cancanService.canAccessAdminArea(locals.user)) return fail(403, { error: 'Forbidden' }); + + try { + const form = await request.formData(); + await organizationService.deleteOrganization(String(form.get('id') ?? '')); + return { success: true }; + } catch (error: unknown) { + return errorResponse(error); + } + }, +}; diff --git a/src/routes/cluster-settings/orgs/+page.svelte b/src/routes/cluster-settings/orgs/+page.svelte index a481aac..0aaa9f1 100644 --- a/src/routes/cluster-settings/orgs/+page.svelte +++ b/src/routes/cluster-settings/orgs/+page.svelte @@ -1,5 +1,6 @@ @@ -130,7 +117,13 @@
{/if} -
+
+

Información

-
- -
- - -
+ {/if} @@ -425,38 +405,40 @@
Delete project
- {#if deleteError} -
-
- {deleteError} +
+ + {#if deleteError} +
+
+ {deleteError} +
-
- {/if} + {/if} -
- Are you sure you want to delete {deleteModalProject.name}? This action cannot be undone. -
+
+ Are you sure you want to delete {deleteModalProject.name}? This action cannot be undone. +
-
- - -
+
+ + +
+
{/if} diff --git a/src/routes/org/[org]/settings/storage/+page.server.ts b/src/routes/org/[org]/settings/storage/+page.server.ts index 2b26af8..0a44aac 100644 --- a/src/routes/org/[org]/settings/storage/+page.server.ts +++ b/src/routes/org/[org]/settings/storage/+page.server.ts @@ -2,4 +2,4 @@ import { redirect } from '@sveltejs/kit'; export function load() { throw redirect(302, '/pulumi-state/backends'); -} \ No newline at end of file +} diff --git a/src/routes/org/[org]/settings/storage/+page.svelte b/src/routes/org/[org]/settings/storage/+page.svelte deleted file mode 100644 index e95fcee..0000000 --- a/src/routes/org/[org]/settings/storage/+page.svelte +++ /dev/null @@ -1,483 +0,0 @@ - - -
-
-
-

Storage Backends

-

Configure where your Pulumi state files are retrieved from.

-
- -
- class="btn-ghost p-1.5 text-gray-400 hover:text-red-600 rounded transition-colors" - {#if isLoading} -
- -
- {:else if backends.length === 0} -
- -

No backends configured

-

Add your first S3 or GCS bucket to start syncing states.

- -
- {:else} -
- {#each backends as backend} -
-
-
-

{backend.name}

-

- {backend.provider === 's3' ? 'AWS S3 / Compatible' : 'Google Cloud Storage'} -

-
-
- - -
-
-
-

- Bucket: - {backend.bucket} -

-
-
- {/each} -
- {/if} - - {#if isEditing} - - -
- -
- {/if} - - {#if isDeleteModalOpen} - - -
- -
- {/if} -