diff --git a/apps/registry/src/routes/packages.ts b/apps/registry/src/routes/packages.ts index 588502f..0d01b90 100644 --- a/apps/registry/src/routes/packages.ts +++ b/apps/registry/src/routes/packages.ts @@ -23,6 +23,7 @@ import { UnclaimedPackagesResponseSchema, } from '../schemas/generated/api-responses.js'; import { generateMpakJsonExample } from '../schemas/mpak-schema.js'; +import { extractScannerVersion } from '../utils/scanner-version.js'; import { fetchGitHubRepoStats, parseGitHubRepo, verifyPackageClaim } from '../services/github-verifier.js'; import { validateManifest } from '../services/manifest-validator.js'; import { triggerSecurityScan } from '../services/scanner.js'; @@ -607,10 +608,14 @@ export const packageRoutes: FastifyPluginAsync = async (fastify) => { remediation: (f['remediation'] as string) ?? null, })); + // Extract scanner version from report metadata + const scannerVersion = extractScannerVersion(report); + return { status: scan['status'], risk_score: scan['riskScore'], scanned_at: scan['completedAt'], + scanner_version: scannerVersion, certification: scan['certificationLevel'] !== null ? { level: scan['certificationLevel'], level_name: getCertificationLevelName(scan['certificationLevel'] as number | null), diff --git a/apps/registry/src/schemas/generated/api-responses.ts b/apps/registry/src/schemas/generated/api-responses.ts index 88e123d..748c3e0 100644 --- a/apps/registry/src/schemas/generated/api-responses.ts +++ b/apps/registry/src/schemas/generated/api-responses.ts @@ -61,6 +61,7 @@ export const SecurityScanSchema = z.object({ status: z.enum(['pending', 'scanning', 'completed', 'failed']), risk_score: z.enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']).nullable(), scanned_at: z.union([z.string(), z.date()]).nullable(), + scanner_version: z.string().nullable().optional(), certification: CertificationSchema.nullable().optional(), summary: z.object({ components: z.number(), diff --git a/apps/registry/src/utils/scanner-version.ts b/apps/registry/src/utils/scanner-version.ts new file mode 100644 index 0000000..7cf82c8 --- /dev/null +++ b/apps/registry/src/utils/scanner-version.ts @@ -0,0 +1,14 @@ +/** + * Extract the scanner version from a scan report's metadata. + * + * Looks for `report.scan.scanner_version` in the JSON report structure + * produced by mpak-scanner. + * + * Returns the version string, or `null` if missing/malformed. + */ +export function extractScannerVersion( + report: Record | undefined | null, +): string | null { + const scanMeta = report?.['scan'] as Record | undefined; + return (scanMeta?.['scanner_version'] as string) ?? null; +} diff --git a/apps/registry/tests/scanner-version.test.ts b/apps/registry/tests/scanner-version.test.ts new file mode 100644 index 0000000..e80fb46 --- /dev/null +++ b/apps/registry/tests/scanner-version.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { extractScannerVersion } from '../src/utils/scanner-version.js'; + +describe('extractScannerVersion', () => { + it('extracts version from report with scan metadata', () => { + const report = { + scan: { + timestamp: '2025-01-01T00:00:00Z', + scanner: 'mpak-scanner', + scanner_version: '0.4.2', + duration_ms: 1234, + }, + }; + expect(extractScannerVersion(report)).toBe('0.4.2'); + }); + + it('returns null when report is undefined', () => { + expect(extractScannerVersion(undefined)).toBeNull(); + }); + + it('returns null when report is null', () => { + expect(extractScannerVersion(null)).toBeNull(); + }); + + it('returns null when report has no scan key', () => { + expect(extractScannerVersion({ findings: [] })).toBeNull(); + }); + + it('returns null when scan metadata has no scanner_version', () => { + const report = { + scan: { + timestamp: '2025-01-01T00:00:00Z', + scanner: 'mpak-scanner', + }, + }; + expect(extractScannerVersion(report)).toBeNull(); + }); + + it('returns null for empty report object', () => { + expect(extractScannerVersion({})).toBeNull(); + }); +}); diff --git a/apps/web/src/components/SecurityReportSection.tsx b/apps/web/src/components/SecurityReportSection.tsx index 4d8cab8..6391a74 100644 --- a/apps/web/src/components/SecurityReportSection.tsx +++ b/apps/web/src/components/SecurityReportSection.tsx @@ -44,6 +44,7 @@ interface SecurityScan { status: 'pending' | 'scanning' | 'completed' | 'failed'; risk_score: 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | null; scanned_at: string | Date | null; + scanner_version?: string | null; certification?: Certification | null; summary?: SecurityScanSummary; scans?: Record; @@ -227,6 +228,12 @@ export default function SecurityReportSection({ securityScan, version }: Props) )} | Scanned {formatDate(securityScan.scanned_at)} + {securityScan.scanner_version && ( + <> + | + Scanner v{securityScan.scanner_version} + + )} diff --git a/apps/web/src/schemas/generated/api-responses.ts b/apps/web/src/schemas/generated/api-responses.ts index debabdc..d474792 100644 --- a/apps/web/src/schemas/generated/api-responses.ts +++ b/apps/web/src/schemas/generated/api-responses.ts @@ -96,6 +96,7 @@ export const SecurityScanSchema = z.object({ status: z.enum(['pending', 'scanning', 'completed', 'failed']), risk_score: z.enum(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW']).nullable(), scanned_at: z.union([z.string(), z.date()]).nullable(), + scanner_version: z.string().nullable().optional(), certification: CertificationSchema.nullable().optional(), summary: z.object({ components: z.number(), // Total SBOM components diff --git a/packages/schemas/src/api-responses.ts b/packages/schemas/src/api-responses.ts index a8702e2..e212379 100644 --- a/packages/schemas/src/api-responses.ts +++ b/packages/schemas/src/api-responses.ts @@ -111,6 +111,7 @@ export const SecurityScanSchema = z.object({ status: z.enum(["pending", "scanning", "completed", "failed"]), risk_score: z.enum(["CRITICAL", "HIGH", "MEDIUM", "LOW"]).nullable(), scanned_at: z.union([z.string(), z.date()]).nullable(), + scanner_version: z.string().nullable().optional(), certification: CertificationSchema.nullable().optional(), summary: z .object({