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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ coverage
data/
.gitdb/
.env
.claude/.claude-md-review-state
.claude/.claude-md-review-state
code-report-analysis/
12 changes: 9 additions & 3 deletions src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import { getGitDb } from '$lib/server/gitdb';
getGitDb();

export const handle: Handle = async ({ event, resolve }) => {
if (event.url.pathname === '/login' || event.url.pathname.startsWith('/api/auth/')) {
if (
event.url.pathname === '/login' ||
event.url.pathname.startsWith('/api/auth/') ||
event.url.pathname === '/api/code-report/analyse-result'
) {
return resolve(event);
}

await ensureAuthReady();
await ensureOrganizationReady();


const sessionCookie = event.cookies.get('pos_session');
const currentUser = await authService.resolveAuthenticatedUser(sessionCookie);

Expand All @@ -28,7 +31,10 @@ export const handle: Handle = async ({ event, resolve }) => {
const organizationSettingsMatch = event.url.pathname.match(/^\/org\/([^/]+)\/settings/);
if (organizationSettingsMatch) {
const organization = await organizationService.tryFindBySlug(organizationSettingsMatch[1]);
if (!organization || !(await cancanService.canManageOrganization(currentUser, organization.id))) {
if (
!organization ||
!(await cancanService.canManageOrganization(currentUser, organization.id))
) {
return new Response(null, { status: 302, headers: { location: '/' } });
}
} else if (
Expand Down
72 changes: 72 additions & 0 deletions src/lib/code-report/analysis-summary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Best-effort summary for Trivy-style JSON reports (top-level `Results[]`, each entry may
// carry `Vulnerabilities`/`Secrets`/`Packages`). Other tool formats simply yield all-zero counts.
export type AnalysisSummary = {
vulnerabilities: {
critical: number;
high: number;
medium: number;
low: number;
unknown: number;
};
totalVulnerabilities: number;
exposedSecrets: number;
dependencies: number;
targetsScanned: number;
};

function emptySummary(): AnalysisSummary {
return {
vulnerabilities: { critical: 0, high: 0, medium: 0, low: 0, unknown: 0 },
totalVulnerabilities: 0,
exposedSecrets: 0,
dependencies: 0,
targetsScanned: 0,
};
}

export function summarizeAnalysisResult(result: unknown): AnalysisSummary {
const summary = emptySummary();

if (!result || typeof result !== 'object') return summary;

const results = (result as Record<string, unknown>).Results;
if (!Array.isArray(results)) return summary;

const packageNames = new Set<string>();
summary.targetsScanned = results.length;

for (const entry of results) {
if (!entry || typeof entry !== 'object') continue;
const row = entry as Record<string, unknown>;

const vulnerabilities = Array.isArray(row.Vulnerabilities) ? row.Vulnerabilities : [];
for (const vuln of vulnerabilities) {
if (!vuln || typeof vuln !== 'object') continue;
const vulnRow = vuln as Record<string, unknown>;
summary.totalVulnerabilities += 1;

const severity = String(vulnRow.Severity || '').toLowerCase();
if (severity === 'critical') summary.vulnerabilities.critical += 1;
else if (severity === 'high') summary.vulnerabilities.high += 1;
else if (severity === 'medium') summary.vulnerabilities.medium += 1;
else if (severity === 'low') summary.vulnerabilities.low += 1;
else summary.vulnerabilities.unknown += 1;

if (vulnRow.PkgName) packageNames.add(String(vulnRow.PkgName));
}

const secrets = Array.isArray(row.Secrets) ? row.Secrets : [];
summary.exposedSecrets += secrets.length;

const packages = Array.isArray(row.Packages) ? row.Packages : [];
for (const pkg of packages) {
if (pkg && typeof pkg === 'object' && (pkg as Record<string, unknown>).Name) {
packageNames.add(String((pkg as Record<string, unknown>).Name));
}
}
}

summary.dependencies = packageNames.size;

return summary;
}
12 changes: 6 additions & 6 deletions src/lib/components/AppSidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
export let organizationName: string | null = null;
export let projects: {
slug: string;
modules?: { vault: boolean; openreport: boolean; stateiac: boolean };
modules?: { vault: boolean; codereport: boolean; stateiac: boolean };
}[] = [];

let currentPath = pathname;
Expand Down Expand Up @@ -88,7 +88,7 @@
},
]
: []),
...(currentProject?.modules?.openreport
...(currentProject?.modules?.codereport
? [
{
name: 'Analisis',
Expand All @@ -97,10 +97,10 @@
name: 'Code Report',
icon: BarChart3,
items: [
{ label: 'Services', href: `${projectBase}/report/services`, icon: Layers },
{ label: 'History', href: `${projectBase}/report/history`, icon: GitBranch },
{ label: 'GitOps Report Bot', href: `${projectBase}/report/bot`, icon: Bot },
{ label: 'Settings', href: `${projectBase}/report/settings`, icon: Settings },
{ label: 'Services', href: `${projectBase}/code-report/services`, icon: Layers },
{ 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 },
],
},
],
Expand Down
50 changes: 49 additions & 1 deletion src/lib/database/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export const RoleEntity = entity('roles', {
export const ApiKeyEntity = entity('api_keys', {
id: uuid().primaryKey(),
userId: uuid().notNull(),
projectId: uuid(),
name: text().notNull(),
keyPrefix: text().notNull(),
keyHash: text().notNull(),
Expand Down Expand Up @@ -102,7 +103,41 @@ export const ProjectEntity = entity('projects', {
status: text().notNull().default('active'),
modules: json()
.notNull()
.$defaultFn(() => ({ vault: true, openreport: true, stateiac: true })),
.$defaultFn(() => ({ vault: true, codereport: true, stateiac: true })),
createdAt: timestamp()
.notNull()
.$defaultFn(() => new Date().toISOString()),
updatedAt: timestamp()
.notNull()
.$defaultFn(() => new Date().toISOString()),
});

export const CodeReportServiceEntity = entity('code_report_services', {
id: uuid().primaryKey(),
projectId: uuid().notNull(),
slug: text().notNull().unique(),
name: text().notNull(),
description: text(),
tags: json()
.notNull()
.$defaultFn(() => []),
createdAt: timestamp()
.notNull()
.$defaultFn(() => new Date().toISOString()),
updatedAt: timestamp()
.notNull()
.$defaultFn(() => new Date().toISOString()),
});

export const CodeReportAnalysisEntity = entity('code_report_analyses', {
id: uuid().primaryKey(),
serviceId: uuid().notNull(),
tool: text().notNull(),
status: text().notNull().default('in_progress'),
result: json(),
summary: json(),
error: text(),
gitInfo: json(),
createdAt: timestamp()
.notNull()
.$defaultFn(() => new Date().toISOString()),
Expand All @@ -115,6 +150,19 @@ relations.for(ProjectEntity, ({ one, many }) => ({
roles: many(RoleEntity, { fields: ['id'], references: ['projectId'] }),
organization: one(OrganizationEntity, { fields: ['organizationId'], references: ['id'] }),
access: many(UserAccessEntity, { fields: ['id'], references: ['projectId'] }),
codeReportServices: many(CodeReportServiceEntity, {
fields: ['id'],
references: ['projectId'],
}),
}));

relations.for(CodeReportServiceEntity, ({ one, many }) => ({
project: one(ProjectEntity, { fields: ['projectId'], references: ['id'] }),
analyses: many(CodeReportAnalysisEntity, { fields: ['id'], references: ['serviceId'] }),
}));

relations.for(CodeReportAnalysisEntity, ({ one }) => ({
service: one(CodeReportServiceEntity, { fields: ['serviceId'], references: ['id'] }),
}));

relations.for(RoleEntity, ({ one, many }) => ({
Expand Down
17 changes: 15 additions & 2 deletions src/modules/auth/application/apikeys.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import type { ApiKeyView } from '../domain/entities';
function createRepositoryMock(): any {
return {
listByUser: vi.fn(),
listByProject: vi.fn(),
findValidByHash: vi.fn(),
create: vi.fn(),
findById: vi.fn(),
findByIdAny: vi.fn(),
revoke: vi.fn(),
revokeAny: vi.fn(),
touchLastUsed: vi.fn(),
updateKeyMaterial: vi.fn(),
};
}
Expand All @@ -25,6 +29,7 @@ describe('ApiKeysService', () => {
id: 'key-1',
name: 'Deploy',
keyPrefix: 'gvs_12',
projectId: null,
expiresAt: null,
lastUsedAt: null,
revokedAt: null,
Expand All @@ -46,6 +51,7 @@ describe('ApiKeysService', () => {
id: 'key-1',
name: 'Deploy',
keyPrefix: 'gvs_12',
projectId: null,
expiresAt: null,
lastUsedAt: null,
revokedAt: null,
Expand Down Expand Up @@ -95,6 +101,7 @@ describe('ApiKeysService', () => {
expect(repository.create).toHaveBeenCalledWith({
id: expect.any(String),
userId: 'user-1',
projectId: null,
name: 'Deploy',
keyPrefix: result.token.slice(0, 6),
keyHash: expect.any(String),
Expand All @@ -109,6 +116,7 @@ describe('ApiKeysService', () => {
id: 'key-1',
name: 'Deploy',
keyPrefix: 'gvs_12',
projectId: null,
expiresAt: null,
lastUsedAt: null,
revokedAt: null,
Expand All @@ -118,6 +126,7 @@ describe('ApiKeysService', () => {
id: 'key-1',
name: 'Deploy',
keyPrefix: 'gvs_12',
projectId: null,
expiresAt: null,
lastUsedAt: null,
revokedAt: '2026-08-18T00:10:00.000Z',
Expand All @@ -127,7 +136,9 @@ describe('ApiKeysService', () => {
const service = new ApiKeysService(repository);

await service.revokeApiKey('user-1', 'key-1');
await expect(service.revokeApiKey('user-1', 'key-1')).rejects.toThrow('API key is already revoked');
await expect(service.revokeApiKey('user-1', 'key-1')).rejects.toThrow(
'API key is already revoked',
);
expect(repository.revoke).toHaveBeenCalledTimes(1);
});

Expand All @@ -137,6 +148,7 @@ describe('ApiKeysService', () => {
id: 'key-1',
name: 'Deploy',
keyPrefix: 'gvs_old',
projectId: null,
expiresAt: '2026-12-31T00:00:00.000Z',
lastUsedAt: null,
revokedAt: null,
Expand Down Expand Up @@ -168,6 +180,7 @@ describe('ApiKeysService', () => {
id: 'key-1',
name: 'Deploy',
keyPrefix: 'gvs_old',
projectId: null,
expiresAt: null,
lastUsedAt: null,
revokedAt: '2026-08-18T00:10:00.000Z',
Expand All @@ -179,4 +192,4 @@ describe('ApiKeysService', () => {
await expect(service.regenerateApiKey('user-1', 'key-1')).rejects.toThrow('API key is revoked');
expect(repository.updateKeyMaterial).not.toHaveBeenCalled();
});
});
});
Loading
Loading