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 @@
+
+
+
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
+
+
+
+
+
+
+
+
+ Todos los tipos
+ {#each Object.entries(SECURITY_POLICY_TYPE_META) as [value, meta]}
+ {meta.label}
+ {/each}
+
+
+ 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}
+
+ {#each filteredPolicies as policy (policy.id)}
+
+ {/each}
+
+ {/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
+
+
+
(editing = !editing)}
+ class="inline-flex items-center gap-2 rounded-full border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50"
+ >
+ {editing ? 'Cancelar edición' : 'Editar'}
+
+
(deleteModalOpen = true)}
+ class="inline-flex items-center gap-2 rounded-full border border-red-200 px-4 py-2 text-sm font-semibold text-red-600 hover:bg-red-50"
+ >
+ Borrar
+
+
+
+
+ {#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
+ (deleteModalOpen = false)}>
+
+
+
+
+ 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"
+ >
+ (deleteModalOpen = false)}
+ class="rounded-full border px-4 py-2 text-sm">Cancelar
+
+ {deleting ? 'Borrando...' : 'Borrar política'}
+
+
+
+
+{/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}
+ import { AlertTriangle, ChevronDown, ChevronUp, ShieldAlert, ShieldCheck, ShieldQuestion } from 'lucide-svelte';
+ import type { PolicyComplianceReport } from '$lib/code-report/policy-evaluation';
+
+ export let report: PolicyComplianceReport;
+ export let policiesHref: string | null = null;
+
+ let expanded = true;
+
+ $: theme =
+ report.status === 'violated'
+ ? {
+ border: 'border-red-300',
+ bg: 'bg-red-50',
+ accent: 'bg-red-600',
+ text: 'text-red-900',
+ muted: 'text-red-700',
+ icon: ShieldAlert,
+ title: 'Incumple las políticas de seguridad',
+ }
+ : report.status === 'compliant'
+ ? {
+ border: 'border-emerald-300',
+ bg: 'bg-emerald-50',
+ accent: 'bg-emerald-600',
+ text: 'text-emerald-900',
+ muted: 'text-emerald-700',
+ icon: ShieldCheck,
+ title: 'Cumple todas las políticas de seguridad',
+ }
+ : {
+ border: 'border-slate-300',
+ bg: 'bg-slate-50',
+ accent: 'bg-slate-500',
+ text: 'text-slate-900',
+ muted: 'text-slate-600',
+ icon: ShieldQuestion,
+ title:
+ report.status === 'no_policies'
+ ? 'Sin políticas de seguridad activas'
+ : 'Ninguna política aplica a este análisis',
+ };
+
+ $: subtitle =
+ report.status === 'violated'
+ ? `${report.failed.length} política(s) incumplida(s) · ${report.totalViolations} regla(s) superada(s)`
+ : report.status === 'compliant'
+ ? `${report.passed.length} política(s) evaluada(s) sin incidencias`
+ : report.status === 'no_policies'
+ ? 'Define políticas para vigilar automáticamente cada análisis.'
+ : 'Las políticas activas no cubren este servicio o faltan análisis.';
+
+
+
+
+
+
+
+
+
+
+
+
+ Security Policies
+
+
{theme.title}
+
{subtitle}
+
+
+
+
+ {#if report.blockingFailures > 0}
+
+
+ {report.blockingFailures} bloqueante(s)
+
+ {/if}
+ {#if policiesHref}
+
+ Ver políticas
+
+ {/if}
+ {#if report.evaluations.length > 0}
+
(expanded = !expanded)}
+ class="inline-flex items-center gap-1 rounded-full border border-slate-300 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50"
+ >
+ {expanded ? 'Ocultar detalle' : 'Ver detalle'}
+ {#if expanded}
+
+ {:else}
+
+ {/if}
+
+ {/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
+
+
+
+
+
+
+
+
+
+
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).
+
+
+
+
+ Critical
+
+
+
+
+ High
+
+
+
+
+ Medium
+
+
+
+
+ Low
+
+
+
+
+
+
+
+
+
+ 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.
+
+
+
+
+
+
+ {loading ? 'Guardando...' : 'Guardar cambios'}
+
+
+
+
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.name}
+ {#if tool.soon}
+
+ Soon
+
+ {:else if tool.enabled}
+
+ Habilitada
+
+ {:else}
+
+ Deshabilitada
+
+ {/if}
+
+
{tool.description}
+
+
+ {/each}
+
+
+
+
+ {loading ? 'Guardando...' : 'Guardar cambios'}
+
+
+
+
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}
+ (activeTab = tab.id)}
+ class={`shrink-0 border-b-2 px-4 py-3 text-sm font-semibold ${
+ activeTab === tab.id
+ ? 'border-slate-900 text-slate-900'
+ : 'border-transparent text-slate-500 hover:text-slate-900'
+ }`}
+ >
+ {tab.label}
+ {#if tab.count !== undefined}{tab.count} {/if}
+
+ {/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)}
+
+ {: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}
+
+
+
+
+
+
+
+
+
+
+
+ Servicio
+ Paquete
+ Version instalada
+ Version corregida
+ Objetivo
+ Ultimo escaneo
+
+
+
+ {#each paginatedAffectedServices as service (service.serviceId + service.target + service.packageName)}
+ {@const target = splitTarget(service.target)}
+
+
+
+ {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() : '-'}
+
+
+ {/each}
+ {#if paginatedAffectedServices.length === 0}
+
+
+ {affectedServices.length === 0
+ ? 'No hay servicios afectados.'
+ : 'Sin resultados para tu busqueda.'}
+
+
+ {/if}
+
+
+
+
+
+
+ Mostrando {affectedServicesRangeStart}-{affectedServicesRangeEnd} de {filteredAffectedServices.length}
+
+ Por pagina
+
+ {#each affectedServicesPerPageOptions as option}
+ {option}
+ {/each}
+
+
+
+
+
+ (affectedServicesPage -= 1)}
+ aria-label="Pagina anterior"
+ class="inline-flex items-center justify-center rounded-full border border-slate-200 p-1.5 text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
+ >
+
+
+
+ Pagina {affectedServicesPage} de {affectedServicesTotalPages}
+
+ (affectedServicesPage += 1)}
+ aria-label="Pagina siguiente"
+ class="inline-flex items-center justify-center rounded-full border border-slate-200 p-1.5 text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
+ >
+
+
+
+
+ {/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 @@
+
+
+
+
+
+
+
+
+
+
+
+ {#if showProjectFilter}
+
+ {/if}
+
+
+
+
+ {#if filteredCves.length === 0}
+
+
+
+ {cves.length === 0 ? emptyMessage : 'Sin resultados para tu busqueda.'}
+
+
+ {:else}
+
+
+
+
+ CVE
+ Severidad
+ CVSS
+ Servicios afectados
+ Apariciones
+
+
+
+ {#each paginatedCves as cve (cve.id)}
+
+
+
+ {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}
+
+ {/each}
+
+
+
+
+
+
+ Mostrando {rangeStart}-{rangeEnd} de {filteredCves.length}
+
+ Por pagina
+
+ {#each perPageOptions as option}
+ {option}
+ {/each}
+
+
+
+
+
+ (currentPage -= 1)}
+ aria-label="Pagina anterior"
+ class="inline-flex items-center justify-center rounded-full border border-slate-200 p-1.5 text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
+ >
+
+
+ Pagina {currentPage} de {totalPages}
+ (currentPage += 1)}
+ aria-label="Pagina siguiente"
+ class="inline-flex items-center justify-center rounded-full border border-slate-200 p-1.5 text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
+ >
+
+
+
+
+ {/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'}
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í.
-
-
-
-
-
-
-
-
-
- Todas las severidades
- Critical
- High
- Medium
- Low
- Unknown
-
-
-
- {#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}
-
-
-
-
- CVE
- Severidad
- CVSS
- Servicios afectados
- Apariciones
-
-
-
- {#each paginatedCves as cve (cve.id)}
-
-
-
- {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}
-
- {/each}
-
-
-
-
-
-
- Mostrando {rangeStart}-{rangeEnd} de {filteredCves.length}
-
- Por página
-
- {#each perPageOptions as option}
- {option}
- {/each}
-
-
-
-
-
- (currentPage -= 1)}
- aria-label="Página anterior"
- class="inline-flex items-center justify-center rounded-full border border-slate-200 p-1.5 text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
- >
-
-
- Página {currentPage} de {totalPages}
- (currentPage += 1)}
- aria-label="Página siguiente"
- class="inline-flex items-center justify-center rounded-full border border-slate-200 p-1.5 text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
- >
-
-
-
-
- {/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}
- (activeTab = tab.id)}
- class={`shrink-0 border-b-2 px-4 py-3 text-sm font-semibold ${
- activeTab === tab.id
- ? 'border-slate-900 text-slate-900'
- : 'border-transparent text-slate-500 hover:text-slate-900'
- }`}
- >
- {tab.label}
- {#if tab.count !== undefined}{tab.count} {/if}
-
- {/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)}
-
- {: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}
-
-
-
-
-
-
-
-
-
-
-
- Servicio
- Paquete
- Versión instalada
- Versión corregida
- Objetivo
- Último escaneo
-
-
-
- {#each paginatedAffectedServices as service (service.serviceId + service.target + service.packageName)}
- {@const target = splitTarget(service.target)}
-
-
-
- {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() : '—'}
-
-
- {/each}
- {#if paginatedAffectedServices.length === 0}
-
-
- {data.affectedServices.length === 0
- ? 'No hay servicios afectados.'
- : 'Sin resultados para tu búsqueda.'}
-
-
- {/if}
-
-
-
-
-
-
- Mostrando {affectedServicesRangeStart}-{affectedServicesRangeEnd} de {filteredAffectedServices.length}
-
- Por página
-
- {#each affectedServicesPerPageOptions as option}
- {option}
- {/each}
-
-
-
-
-
- (affectedServicesPage -= 1)}
- aria-label="Página anterior"
- class="inline-flex items-center justify-center rounded-full border border-slate-200 p-1.5 text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
- >
-
-
-
- Página {affectedServicesPage} de {affectedServicesTotalPages}
-
- (affectedServicesPage += 1)}
- aria-label="Página siguiente"
- class="inline-flex items-center justify-center rounded-full border border-slate-200 p-1.5 text-slate-600 hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-40"
- >
-
-
-
-
- {/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
- Nombre
+ Nombre
Slug
@@ -192,7 +186,7 @@
{saving ? 'Saving...' : 'Save changes'}
-
+
@@ -235,38 +229,40 @@
Delete organization
-
- {#if deleteError}
-
- {deleteError}
-
- {/if}
+
+
+
+ {#if deleteError}
+
+ {deleteError}
+
+ {/if}
-
- Are you sure you want to delete {organization.name} ? This action cannot be undone.
-
-
+
+ Are you sure you want to delete {organization.name} ? This action cannot be undone.
+
+
-
-
- Cancel
-
-
-
- {deleteLoading ? 'Deleting...' : 'Delete organization'}
-
-
+
+
+ Cancel
+
+
+
+ {deleteLoading ? 'Deleting...' : 'Delete organization'}
+
+
+
{/if}
diff --git a/src/routes/org/[org]/projects/[slug]/settings/overview/+page.server.ts b/src/routes/org/[org]/projects/[slug]/settings/overview/+page.server.ts
new file mode 100644
index 0000000..e7c4fbc
--- /dev/null
+++ b/src/routes/org/[org]/projects/[slug]/settings/overview/+page.server.ts
@@ -0,0 +1,107 @@
+import { error, fail, redirect } from '@sveltejs/kit';
+import { cancanService } from '../../../../../../../modules/auth';
+import { projectService } from '../../../../../../../modules/projects';
+
+function errorResponse(error: unknown) {
+ return fail(400, { error: error instanceof Error ? error.message : 'Project action failed.' });
+}
+
+export async function load({ params, locals }) {
+ let project;
+ try {
+ project = await projectService.getProjectBySlug(params.slug);
+ } catch (errorValue: unknown) {
+ throw error(404, 'Project not found');
+ }
+
+ const canRead = await cancanService.canSessionUser(locals.user, 'stateiac:read', {
+ scope: 'project',
+ projectId: project.id,
+ organizationId: project.organization?.id,
+ });
+
+ if (!canRead) throw error(403, 'Forbidden');
+
+ return { project };
+}
+
+export const actions = {
+ async updateProject({ request, params, locals }) {
+ let project;
+ try {
+ const form = await request.formData();
+ const id = String(form.get('id') ?? '');
+ const currentProject = await projectService.getProject(id);
+ const canUpdate = await cancanService.canSessionUser(locals.user, 'stateiac:update', {
+ scope: 'project',
+ projectId: currentProject.id,
+ organizationId: currentProject.organization?.id,
+ });
+
+ if (!canUpdate) return fail(403, { error: 'Forbidden' });
+
+ project = await projectService.updateProject(id, {
+ name: String(form.get('name') ?? ''),
+ slug: String(form.get('slug') ?? ''),
+ description: String(form.get('description') ?? ''),
+ modules: {
+ vault: form.get('moduleVault') === 'on',
+ codereport: form.get('moduleCodeReport') === 'on',
+ stateiac: form.get('moduleStateIac') === 'on',
+ },
+ });
+ } catch (error: unknown) {
+ return errorResponse(error);
+ }
+
+ if (project.slug !== params.slug) {
+ throw redirect(303, `/org/${params.org}/projects/${project.slug}/settings/overview`);
+ }
+
+ return { success: true, project };
+ },
+
+ async updateProjectStatus({ request, locals }) {
+ try {
+ const form = await request.formData();
+ const id = String(form.get('id') ?? '');
+ const currentProject = await projectService.getProject(id);
+ const canUpdate = await cancanService.canSessionUser(locals.user, 'stateiac:update', {
+ scope: 'project',
+ projectId: currentProject.id,
+ organizationId: currentProject.organization?.id,
+ });
+
+ if (!canUpdate) return fail(403, { error: 'Forbidden' });
+
+ const project = await projectService.updateProject(id, {
+ status: String(form.get('status') ?? ''),
+ });
+
+ return { success: true, project };
+ } catch (error: unknown) {
+ return errorResponse(error);
+ }
+ },
+
+ async deleteProject({ request, params, locals }) {
+ try {
+ const form = await request.formData();
+ const id = String(form.get('id') ?? '');
+ const project = await projectService.getProject(id);
+ const canDelete = await cancanService.canSessionUser(locals.user, 'stateiac:delete', {
+ scope: 'project',
+ projectId: project.id,
+ organizationId: project.organization?.id,
+ });
+
+ if (!canDelete) return fail(403, { error: 'Forbidden' });
+
+ await projectService.deleteProject(id);
+ } catch (error: unknown) {
+ return errorResponse(error);
+ }
+
+ throw redirect(303, `/org/${params.org}/settings/projects`);
+ },
+};
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 d68ca6e..4f0e72a 100644
--- a/src/routes/org/[org]/projects/[slug]/settings/overview/+page.svelte
+++ b/src/routes/org/[org]/projects/[slug]/settings/overview/+page.svelte
@@ -1,6 +1,7 @@
@@ -235,7 +215,16 @@
{/if}
-
+
@@ -487,41 +478,45 @@
-
- {#if isArchived}
- Are you sure you want to activate {project.name} ? It will be marked as active again.
- {:else}
- Are you sure you want to archive {project.name} ? It will be marked as inactive.
- {/if}
-
+
+
+
-
-
- Cancel
-
-
+
{#if isArchived}
-
- {archiveLoading ? 'Activating...' : 'Activate project'}
+ Are you sure you want to activate
{project.name} ? It will be marked as active again.
{:else}
-
- {archiveLoading ? 'Archiving...' : 'Archive project'}
+ Are you sure you want to archive
{project.name} ? It will be marked as inactive.
{/if}
-
-
+
+
+
+
+ Cancel
+
+
+ {#if isArchived}
+
+ {archiveLoading ? 'Activating...' : 'Activate project'}
+ {:else}
+
+ {archiveLoading ? 'Archiving...' : 'Archive project'}
+ {/if}
+
+
+
{/if}
@@ -544,30 +539,32 @@
Delete project
-
- Are you sure you want to delete {project.name} ? This action cannot be undone.
-
+
+
+
+ Are you sure you want to delete {project.name} ? This action cannot be undone.
+
-
-
- Cancel
-
-
-
- {deleteLoading ? 'Deleting...' : 'Delete project'}
-
-
+
+
+ Cancel
+
+
+
+ {deleteLoading ? 'Deleting...' : 'Delete project'}
+
+
+
{/if}
diff --git a/src/routes/org/[org]/settings/global/+page.svelte b/src/routes/org/[org]/settings/global/+page.svelte
index dbc591f..fd39e9f 100644
--- a/src/routes/org/[org]/settings/global/+page.svelte
+++ b/src/routes/org/[org]/settings/global/+page.svelte
@@ -1,5 +1,4 @@
diff --git a/src/routes/org/[org]/settings/projects/+page.server.ts b/src/routes/org/[org]/settings/projects/+page.server.ts
index 4e61edf..7c6f6a4 100644
--- a/src/routes/org/[org]/settings/projects/+page.server.ts
+++ b/src/routes/org/[org]/settings/projects/+page.server.ts
@@ -1,7 +1,61 @@
+import { fail } from '@sveltejs/kit';
+import { cancanService } from '../../../../../modules/auth';
import { organizationService } from '../../../../../modules/organization';
+import { projectService } from '../../../../../modules/projects';
+
+function errorResponse(error: unknown) {
+ return fail(400, { error: error instanceof Error ? error.message : 'Project action failed.' });
+}
export async function load({ params }) {
const organization = await organizationService.findBySlug(params.org);
- return { organization };
+ const projects = await projectService.listProjectsByOrganization(organization.id);
+ return { organization, projects };
}
+export const actions = {
+ async createProject({ request, locals }) {
+ try {
+ const form = await request.formData();
+ const organizationId = String(form.get('organizationId') ?? '');
+ const canCreate = await cancanService.canSessionUser(locals.user, 'stateiac:create', {
+ scope: 'organization',
+ organizationId,
+ });
+
+ if (!canCreate) return fail(403, { error: 'Forbidden' });
+
+ const project = await projectService.createProject({
+ organizationId,
+ name: String(form.get('name') ?? ''),
+ slug: String(form.get('slug') ?? '') || undefined,
+ description: String(form.get('description') ?? '') || undefined,
+ status: String(form.get('status') ?? ''),
+ });
+
+ return { success: true, project };
+ } catch (error: unknown) {
+ return errorResponse(error);
+ }
+ },
+
+ async deleteProject({ request, locals }) {
+ try {
+ const form = await request.formData();
+ const id = String(form.get('id') ?? '');
+ const project = await projectService.getProject(id);
+ const canDelete = await cancanService.canSessionUser(locals.user, 'stateiac:delete', {
+ scope: 'project',
+ projectId: project.id,
+ organizationId: project.organization?.id,
+ });
+
+ if (!canDelete) return fail(403, { error: 'Forbidden' });
+
+ await projectService.deleteProject(id);
+ return { success: true };
+ } catch (error: unknown) {
+ return errorResponse(error);
+ }
+ },
+};
diff --git a/src/routes/org/[org]/settings/projects/+page.svelte b/src/routes/org/[org]/settings/projects/+page.svelte
index 04783c0..5dddb1e 100644
--- a/src/routes/org/[org]/settings/projects/+page.svelte
+++ b/src/routes/org/[org]/settings/projects/+page.svelte
@@ -1,8 +1,9 @@
-
-
-
-
-
Storage Backends
-
Configure where your Pulumi state files are retrieved from.
-
-
- Add Backend
-
-
- 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.
-
Add Backend →
-
- {:else}
-
- {#each backends as backend}
-
-
-
-
{backend.name}
-
- {backend.provider === 's3' ? 'AWS S3 / Compatible' : 'Google Cloud Storage'}
-
-
-
- startEdit(backend)}
- class="btn-ghost p-1.5 text-gray-400 hover:text-blue-600 rounded transition-colors"
- title="Edit"
- >
-
-
- openDeleteModal(backend)}
- class="btn-ghost p-1.5 text-gray-400 hover:text-red-600 rounded transition-colors"
- title="Delete"
- >
-
-
-
-
-
-
- Bucket:
- {backend.bucket}
-
-
-
- {/each}
-
- {/if}
-
- {#if isEditing}
-
-
-
-
-
-
-
{editingId ? 'Edit Backend' : 'New Storage Backend'}
-
Enter the connection details for your storage bucket.
-
-
-
-
-
-
-
-
- Connection Name
-
-
-
-
- Provider
-
- AWS S3 / Compatible (MinIO, R2)
- Google Cloud Storage (GCS)
-
-
-
-
- Bucket Name
-
-
-
- {#if provider === 's3'}
-
- {:else}
-
-
- GCP Project ID (Optional)
-
-
-
-
Credentials JSON (Optional)
-
-
- Leave blank to use GOOGLE_APPLICATION_CREDENTIALS or active gcloud session.
-
-
-
- {/if}
-
- {#if configError}
-
- {configError}
-
- {/if}
-
- {#if saveSuccess}
-
- Connection saved successfully.
-
- {/if}
-
-
- Cancel
-
- {#if isConnecting}
- Saving...
- {:else}
- Save Connection
- {/if}
-
-
-
-
-
- {/if}
-
- {#if isDeleteModalOpen}
-
-
-
-
-
-
Delete Storage Backend
-
-
-
-
-
-
-
- This will remove
- {backendToDeleteName || 'this backend'}
- from your configuration.
-
-
This action cannot be undone.
-
-
-
- Cancel
-
-
- {#if isDeleting}
- Deleting...
- {:else}
- Delete Backend
- {/if}
-
-
-
-
-
- {/if}
-