From 4902d03441a8a12dc3804dbc4244536c68138ac5 Mon Sep 17 00:00:00 2001 From: alamb-hex Date: Fri, 22 May 2026 21:31:34 -0400 Subject: [PATCH 01/11] fix(security): cve-lite offline fallback + correct DB-status path (#105, #106) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cve-lite was invoked online-only. When the live OSV API is unreachable (air-gapped/restricted networks), the CLI writes no output file and the scan silently returned zero findings — masking real CVEs (e.g. qs@6.15.1 GHSA-q8mj-m7cp-5q26 / CVE-2026-8723, which the pnpm-audit Patches view flagged but CVE Lite did not). - runCveLiteRaw now tries online first, then falls back to an offline scan against the synced advisory DB (--offline-db). Throws on total failure instead of returning [] so callers surface a real error (the runner marks the source failed; the page route returns 500). Extracted a pure, unit-tested buildCveLiteCommand helper. - readDbStatus() read osv-vulns.json, but `advisories sync` writes advisories.db, so a successful Sync DB never reflected in the status indicator. Point both (and the offline scan) at one shared CVE_LITE_DB_PATH constant = ~/.cache/cve-lite/advisories.db. Verified: tsc clean; 99 tests pass (+5 builder tests); runCveLiteRaw against hexops now returns findingCount 1 surfacing qs@6.15.1 → fixed 6.15.2. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/security/cve-lite-db.ts | 13 +++- src/lib/security/sources/cve-lite.test.ts | 32 +++++++++- src/lib/security/sources/cve-lite.ts | 78 +++++++++++++++++++---- 3 files changed, 107 insertions(+), 16 deletions(-) diff --git a/src/lib/security/cve-lite-db.ts b/src/lib/security/cve-lite-db.ts index 95da589..6f2bc7e 100644 --- a/src/lib/security/cve-lite-db.ts +++ b/src/lib/security/cve-lite-db.ts @@ -6,7 +6,14 @@ import { homedir } from 'os'; const execAsync = promisify(exec); const BIN = join(process.cwd(), 'node_modules', '.bin', 'cve-lite'); -const DB_FILE = join(homedir(), '.cache', 'cve-lite', 'osv-vulns.json'); + +/** + * The local advisory database that `cve-lite advisories sync` writes (its + * `--output` default) and that `cve-lite --offline-db` reads. The status check, + * the sync, and the offline scan fallback must all agree on this one path — + * the legacy `osv-vulns.json` is a query-cache artifact `sync` never updates. + */ +export const CVE_LITE_DB_PATH = join(homedir(), '.cache', 'cve-lite', 'advisories.db'); export interface DbStatus { ok: boolean; synced: boolean; builtAt?: string; ageDays?: number } @@ -18,8 +25,8 @@ export function dbStatusFromMtime(mtimeMs: number | null): DbStatus { export function readDbStatus(): DbStatus { try { - if (!existsSync(DB_FILE)) return dbStatusFromMtime(null); - return dbStatusFromMtime(statSync(DB_FILE).mtimeMs); + if (!existsSync(CVE_LITE_DB_PATH)) return dbStatusFromMtime(null); + return dbStatusFromMtime(statSync(CVE_LITE_DB_PATH).mtimeMs); } catch { return dbStatusFromMtime(null); } diff --git a/src/lib/security/sources/cve-lite.test.ts b/src/lib/security/sources/cve-lite.test.ts index 8227f6a..5fc9e4e 100644 --- a/src/lib/security/sources/cve-lite.test.ts +++ b/src/lib/security/sources/cve-lite.test.ts @@ -1,7 +1,8 @@ import { describe, it, expect } from 'vitest'; import { readFileSync } from 'fs'; import { join } from 'path'; -import { parseCveLiteJson, buildScanFlags } from './cve-lite'; +import { parseCveLiteJson, buildScanFlags, buildCveLiteCommand } from './cve-lite'; +import { CVE_LITE_DB_PATH } from '../cve-lite-db'; const fixture = JSON.parse(readFileSync(join(__dirname, '__fixtures__/cve-lite-sample.json'), 'utf-8')); @@ -77,3 +78,32 @@ describe('buildScanFlags', () => { expect(buildScanFlags({ minSeverity: 'critical', all: true })).toEqual(['--min-severity', 'critical', '--all']); }); }); + +describe('buildCveLiteCommand', () => { + it('online (default) emits --json --usage and the project path, no offline flags', () => { + const cmd = buildCveLiteCommand('/bin/cve-lite', '/proj', []); + expect(cmd).toBe('"/bin/cve-lite" "--json" "--usage" "/proj"'); + expect(cmd).not.toContain('--offline'); + }); + + it('offline appends --offline-db pointing at the synced advisory DB', () => { + const cmd = buildCveLiteCommand('/bin/cve-lite', '/proj', [], { offline: true }); + expect(cmd).toContain('"--offline-db"'); + expect(cmd).toContain(JSON.stringify(CVE_LITE_DB_PATH)); + }); + + it('offline honors an explicit dbPath override', () => { + const cmd = buildCveLiteCommand('/bin/cve-lite', '/proj', [], { offline: true, dbPath: '/tmp/db' }); + expect(cmd).toContain('"--offline-db" "/tmp/db"'); + }); + + it('threads scan flags through before the project path', () => { + const cmd = buildCveLiteCommand('/bin/cve-lite', '/proj', ['--min-severity', 'high']); + expect(cmd).toBe('"/bin/cve-lite" "--json" "--usage" "--min-severity" "high" "/proj"'); + }); + + it('quotes every argument (paths with spaces are safe)', () => { + const cmd = buildCveLiteCommand('/bin/cve-lite', '/my proj', []); + expect(cmd).toContain('"/my proj"'); + }); +}); diff --git a/src/lib/security/sources/cve-lite.ts b/src/lib/security/sources/cve-lite.ts index a57ee57..9fd25de 100644 --- a/src/lib/security/sources/cve-lite.ts +++ b/src/lib/security/sources/cve-lite.ts @@ -7,6 +7,7 @@ import type { ScanSource, Finding, Severity, Remediation } from '../types'; import type { ProjectConfig } from '../../types'; import { readCveLiteCache, writeCveLiteCache } from '../cve-lite-cache'; import { deriveReachable } from '../cve-lite-view'; +import { CVE_LITE_DB_PATH } from '../cve-lite-db'; const execAsync = promisify(exec); @@ -145,22 +146,75 @@ async function probe(): Promise { return availableCache; } -/** Runs cve-lite once and returns the full report (no caching). */ +/** + * Builds the cve-lite scan command. With `offline: true` it appends + * `--offline-db ` so cve-lite reads the synced local advisory DB + * instead of querying the live OSV API. + */ +export function buildCveLiteCommand( + bin: string, + projectPath: string, + flags: string[], + opts: { offline?: boolean; dbPath?: string } = {}, +): string { + const parts = ['--json', '--usage', ...flags]; + if (opts.offline) parts.push('--offline-db', opts.dbPath ?? CVE_LITE_DB_PATH); + parts.push(projectPath); + return [bin, ...parts].map((p) => JSON.stringify(p)).join(' '); +} + +/** + * Runs one cve-lite invocation in `tmpDir`. Returns the parsed report, or null + * if no output file was written — which is the signal that the scan failed + * (e.g. the live OSV fetch failed). cve-lite also exits non-zero when findings + * exist, but in that case the JSON file IS written, so the file's presence — + * not the exit code — is what distinguishes success from failure. + */ +async function runScanAttempt( + tmpDir: string, + projectPath: string, + flags: string[], + opts: { offline?: boolean }, +): Promise { + try { + await execAsync(buildCveLiteCommand(binPath(), projectPath, flags, opts), { + cwd: tmpDir, + timeout: 170_000, + maxBuffer: 64 * 1024 * 1024, + }); + } catch { + // Non-zero exit is expected when findings exist (file still written) and + // when the OSV fetch fails (no file written). Disambiguated below. + } + const outFile = readdirSync(tmpDir).find((f) => f.startsWith('cve-lite-scan-') && f.endsWith('.json')); + if (!outFile) return null; + return JSON.parse(readFileSync(join(tmpDir, outFile), 'utf-8')) as CveLiteOutput; +} + +/** + * Runs cve-lite once and returns the full report (no caching). Tries online + * first (freshest OSV data); if that yields no output — which happens when the + * live OSV API is unreachable (offline/air-gapped/restricted networks) — it + * falls back to an offline scan against the synced advisory DB. Throws if + * neither attempt produces output, rather than silently reporting zero + * findings, so the caller surfaces a real error instead of a false all-clear. + */ export async function runCveLiteRaw(project: ProjectConfig, options: ScanOptions = {}): Promise { const tmp = mkdtempSync(join(tmpdir(), 'hexops-cve-lite-')); try { - const flags = buildScanFlags(options).map((f) => JSON.stringify(f)).join(' '); - try { - await execAsync( - `${JSON.stringify(binPath())} --json --usage ${flags} ${JSON.stringify(project.path)}`, - { cwd: tmp, timeout: 170_000, maxBuffer: 64 * 1024 * 1024 }, - ); - } catch { - // cve-lite may exit non-zero when findings exist; the JSON file is still written. + const flags = buildScanFlags(options); + const online = await runScanAttempt(tmp, project.path, flags, { offline: false }); + if (online) return online; + const haveDb = existsSync(CVE_LITE_DB_PATH); + if (haveDb) { + const offline = await runScanAttempt(tmp, project.path, flags, { offline: true }); + if (offline) return offline; } - const outFile = readdirSync(tmp).find((f) => f.startsWith('cve-lite-scan-') && f.endsWith('.json')); - if (!outFile) return { findings: [] }; - return JSON.parse(readFileSync(join(tmp, outFile), 'utf-8')) as CveLiteOutput; + throw new Error( + haveDb + ? 'cve-lite produced no output (online OSV query and offline DB scan both failed)' + : 'cve-lite produced no output: OSV API unreachable and no local advisory DB — run Sync DB first', + ); } finally { try { rmSync(tmp, { recursive: true, force: true }); } catch { /* best effort */ } } From 8dc59ef106d4af601b5ee6cc03cffa408448fb1a Mon Sep 17 00:00:00 2001 From: alamb-hex Date: Fri, 22 May 2026 22:24:43 -0400 Subject: [PATCH 02/11] =?UTF-8?q?feat(security):=20remediation-commit=20he?= =?UTF-8?q?lper=20for=20cve-lite=20apply=E2=86=92commit=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/security/remediation-commit.test.ts | 67 +++++++++++++++++++++ src/lib/security/remediation-commit.ts | 55 +++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 src/lib/security/remediation-commit.test.ts create mode 100644 src/lib/security/remediation-commit.ts diff --git a/src/lib/security/remediation-commit.test.ts b/src/lib/security/remediation-commit.test.ts new file mode 100644 index 0000000..e0d3b67 --- /dev/null +++ b/src/lib/security/remediation-commit.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; +import { remediationFromRow, remediationFromRows } from './remediation-commit'; +import type { FindingRow } from './cve-lite-view'; + +function row(over: Partial = {}): FindingRow { + return { + package: 'qs', + version: '6.15.1', + severity: 'medium', + relationship: 'transitive', + validatedFixVersion: '6.15.2', + advisoryIds: ['GHSA-q8mj-m7cp-5q26', 'CVE-2026-8723'], + ...over, + }; +} + +describe('remediationFromRow', () => { + it('builds one security UpdatedPackage from a row', () => { + const rc = remediationFromRow(row()); + expect(rc.packages).toEqual([ + { name: 'qs', fromVersion: '6.15.1', toVersion: '6.15.2', isSecurityFix: true, vulnCount: 2 }, + ]); + expect(rc.advisories).toEqual(['GHSA-q8mj-m7cp-5q26', 'CVE-2026-8723']); + expect(rc.severity).toBe('medium'); + }); + + it('de-dupes advisory ids', () => { + const rc = remediationFromRow(row({ advisoryIds: ['CVE-1', 'CVE-1', 'GHSA-x'] })); + expect(rc.advisories).toEqual(['CVE-1', 'GHSA-x']); + expect(rc.packages[0].vulnCount).toBe(3); + }); + + it('tolerates missing versions', () => { + const rc = remediationFromRow(row({ version: undefined, validatedFixVersion: undefined })); + expect(rc.packages[0].fromVersion).toBe(''); + expect(rc.packages[0].toVersion).toBe(''); + }); +}); + +describe('remediationFromRows', () => { + it('keeps only direct, fixable rows', () => { + const rows = [ + row({ package: 'a', relationship: 'direct', validatedFixVersion: '2.0.0', advisoryIds: ['CVE-A'], severity: 'high' }), + row({ package: 'b', relationship: 'transitive', validatedFixVersion: '2.0.0' }), + row({ package: 'c', relationship: 'direct', validatedFixVersion: undefined }), + ]; + const rc = remediationFromRows(rows); + expect(rc.packages.map((p) => p.name)).toEqual(['a']); + }); + + it('unions advisories and takes the max severity', () => { + const rows = [ + row({ package: 'a', relationship: 'direct', validatedFixVersion: '2.0.0', advisoryIds: ['CVE-A'], severity: 'medium' }), + row({ package: 'd', relationship: 'direct', validatedFixVersion: '3.0.0', advisoryIds: ['CVE-D', 'CVE-A'], severity: 'critical' }), + ]; + const rc = remediationFromRows(rows); + expect(rc.advisories).toEqual(['CVE-A', 'CVE-D']); + expect(rc.severity).toBe('critical'); + }); + + it('returns empty when nothing is fixable', () => { + const rc = remediationFromRows([row({ relationship: 'transitive' })]); + expect(rc.packages).toEqual([]); + expect(rc.advisories).toEqual([]); + expect(rc.severity).toBeUndefined(); + }); +}); diff --git a/src/lib/security/remediation-commit.ts b/src/lib/security/remediation-commit.ts new file mode 100644 index 0000000..ce77b51 --- /dev/null +++ b/src/lib/security/remediation-commit.ts @@ -0,0 +1,55 @@ +import type { FindingRow, FixSeverity } from './cve-lite-view'; +import type { UpdatedPackage } from '@/lib/patch-commit-message'; + +export interface RemediationCommit { + /** Feeds generatePatchCommitMessage. */ + packages: UpdatedPackage[]; + /** De-duped advisory IDs (GHSA + CVE) for the audit trail. */ + advisories: string[]; + /** Highest severity among the included rows. */ + severity?: FixSeverity; +} + +const SEVERITY_ORDER: FixSeverity[] = ['critical', 'high', 'medium', 'low']; + +function maxSeverity(severities: FixSeverity[]): FixSeverity | undefined { + let best: FixSeverity | undefined; + for (const s of severities) { + if (best === undefined || SEVERITY_ORDER.indexOf(s) < SEVERITY_ORDER.indexOf(best)) { + best = s; + } + } + return best; +} + +function pkgFromRow(row: FindingRow): UpdatedPackage { + return { + name: row.package, + fromVersion: row.version ?? '', + toVersion: row.validatedFixVersion ?? '', + isSecurityFix: true, + vulnCount: row.advisoryIds.length, + }; +} + +/** Build a remediation commit from a single applied finding row (applyOne). */ +export function remediationFromRow(row: FindingRow): RemediationCommit { + return { + packages: [pkgFromRow(row)], + advisories: [...new Set(row.advisoryIds)], + severity: row.severity, + }; +} + +/** + * Build a remediation commit from all displayed rows, filtered to what + * `cve-lite --fix` (mode `all`) addresses: direct deps with a validated fix. + */ +export function remediationFromRows(rows: FindingRow[]): RemediationCommit { + const fixable = rows.filter((r) => r.relationship === 'direct' && r.validatedFixVersion); + return { + packages: fixable.map(pkgFromRow), + advisories: [...new Set(fixable.flatMap((r) => r.advisoryIds))], + severity: maxSeverity(fixable.map((r) => r.severity)), + }; +} From 109feb4ad00c8976e8084dab45200c9122d11c6a Mon Sep 17 00:00:00 2001 From: alamb-hex Date: Fri, 22 May 2026 22:28:44 -0400 Subject: [PATCH 03/11] refactor(security): share FIX_SEVERITY_ORDER between cve-lite-view and remediation-commit (#107) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/security/cve-lite-view.ts | 4 ++-- src/lib/security/remediation-commit.ts | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/lib/security/cve-lite-view.ts b/src/lib/security/cve-lite-view.ts index 7290ca7..16aac65 100644 --- a/src/lib/security/cve-lite-view.ts +++ b/src/lib/security/cve-lite-view.ts @@ -1,7 +1,7 @@ import type { CveLiteOutput, CveLiteFinding } from './sources/cve-lite'; export type FixSeverity = 'critical' | 'high' | 'medium' | 'low'; -const ORDER: FixSeverity[] = ['critical', 'high', 'medium', 'low']; +export const FIX_SEVERITY_ORDER: FixSeverity[] = ['critical', 'high', 'medium', 'low']; function normSeverity(s?: string): FixSeverity { const v = (s ?? 'low').toLowerCase(); @@ -53,7 +53,7 @@ export function selectFixPlan(report: CveLiteOutput): FixPlanGroup[] { } } const groups: FixPlanGroup[] = []; - for (const sev of ORDER) { + for (const sev of FIX_SEVERITY_ORDER) { const g = bySeverity.get(sev); if (g && g.size > 0) groups.push({ severity: sev, items: Array.from(g.values()) }); } diff --git a/src/lib/security/remediation-commit.ts b/src/lib/security/remediation-commit.ts index ce77b51..27ee71c 100644 --- a/src/lib/security/remediation-commit.ts +++ b/src/lib/security/remediation-commit.ts @@ -1,4 +1,5 @@ import type { FindingRow, FixSeverity } from './cve-lite-view'; +import { FIX_SEVERITY_ORDER } from './cve-lite-view'; import type { UpdatedPackage } from '@/lib/patch-commit-message'; export interface RemediationCommit { @@ -10,12 +11,10 @@ export interface RemediationCommit { severity?: FixSeverity; } -const SEVERITY_ORDER: FixSeverity[] = ['critical', 'high', 'medium', 'low']; - function maxSeverity(severities: FixSeverity[]): FixSeverity | undefined { let best: FixSeverity | undefined; for (const s of severities) { - if (best === undefined || SEVERITY_ORDER.indexOf(s) < SEVERITY_ORDER.indexOf(best)) { + if (best === undefined || FIX_SEVERITY_ORDER.indexOf(s) < FIX_SEVERITY_ORDER.indexOf(best)) { best = s; } } From 24a0a11c3b0d70ecc72a7ccdc9ace1a47cf82c13 Mon Sep 17 00:00:00 2001 From: alamb-hex Date: Fri, 22 May 2026 22:30:41 -0400 Subject: [PATCH 04/11] feat(git): tag commit log with source/advisories when provided (#107) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/projects/[id]/git-commit/route.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/app/api/projects/[id]/git-commit/route.ts b/src/app/api/projects/[id]/git-commit/route.ts index 03b606c..fe14350 100644 --- a/src/app/api/projects/[id]/git-commit/route.ts +++ b/src/app/api/projects/[id]/git-commit/route.ts @@ -12,6 +12,9 @@ export async function POST( ) { const { id } = await params; + let source: string | undefined; + let advisories: string[] | undefined; + try { const project = getProject(id); @@ -24,6 +27,10 @@ export async function POST( const body = await request.json(); const message = body.message?.trim(); + source = typeof body.source === 'string' ? body.source : undefined; + advisories = Array.isArray(body.advisories) + ? body.advisories.filter((a: unknown): a is string => typeof a === 'string') + : undefined; if (!message) { return NextResponse.json( @@ -65,7 +72,11 @@ export async function POST( // Log success logger.info('git', 'commit_created', `Committed changes: ${message.split('\n')[0]}`, { projectId: id, - meta: { message: message.split('\n')[0] }, + meta: { + message: message.split('\n')[0], + ...(source ? { source } : {}), + ...(advisories ? { advisories } : {}), + }, }); return NextResponse.json({ @@ -79,7 +90,11 @@ export async function POST( // Log failure logger.error('git', 'commit_failed', `Commit failed: ${errorMessage}`, { projectId: id, - meta: { error: errorMessage }, + meta: { + error: errorMessage, + ...(source ? { source } : {}), + ...(advisories ? { advisories } : {}), + }, }); return NextResponse.json( From b22a6f08bd913eacd15caf6ada24f4a0d2ecba29 Mon Sep 17 00:00:00 2001 From: alamb-hex Date: Fri, 22 May 2026 22:32:52 -0400 Subject: [PATCH 05/11] feat(git): tag push log with source/advisories when provided (#107) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/projects/[id]/git-push/route.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/app/api/projects/[id]/git-push/route.ts b/src/app/api/projects/[id]/git-push/route.ts index 50691e2..297ca12 100644 --- a/src/app/api/projects/[id]/git-push/route.ts +++ b/src/app/api/projects/[id]/git-push/route.ts @@ -7,10 +7,15 @@ import { logger } from '@/lib/logger'; const execFileAsync = promisify(execFile); export async function POST( - _request: NextRequest, + request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; + const body = (await request.json().catch(() => ({}))) as Record; + const source = typeof body.source === 'string' ? body.source : undefined; + const advisories = Array.isArray(body.advisories) + ? (body.advisories as unknown[]).filter((a): a is string => typeof a === 'string') + : undefined; try { const project = getProject(id); @@ -36,7 +41,10 @@ export async function POST( // Remote has commits we don't have (e.g. Dependabot merged between our commit and push). // Rebase our patch commit on top of whatever landed remotely, then push again. - logger.info('git', 'push_rebase', 'Push rejected (non-fast-forward) — pulling with rebase', { projectId: id }); + logger.info('git', 'push_rebase', 'Push rejected (non-fast-forward) — pulling with rebase', { + projectId: id, + meta: { ...(source ? { source } : {}), ...(advisories ? { advisories } : {}) }, + }); try { await execFileAsync('git', ['pull', '--rebase', '--autostash'], { cwd, timeout: 60000 }); } catch (pullErr) { @@ -50,6 +58,7 @@ export async function POST( // Log success logger.info('git', 'push_completed', 'Pushed changes to remote', { projectId: id, + meta: { ...(source ? { source } : {}), ...(advisories ? { advisories } : {}) }, }); return NextResponse.json({ @@ -64,7 +73,11 @@ export async function POST( // Log failure logger.error('git', 'push_failed', `Push failed: ${errorMessage}`, { projectId: id, - meta: { error: errorMessage }, + meta: { + error: errorMessage, + ...(source ? { source } : {}), + ...(advisories ? { advisories } : {}), + }, }); return NextResponse.json( From 15c24ff4a6423143ecc3c2a29669e30874e61a0c Mon Sep 17 00:00:00 2001 From: alamb-hex Date: Fri, 22 May 2026 22:35:00 -0400 Subject: [PATCH 06/11] feat(patches): log security_remediation_applied when auditContext present (#107) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/projects/[id]/update/route.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/app/api/projects/[id]/update/route.ts b/src/app/api/projects/[id]/update/route.ts index 37e4098..9ca5015 100644 --- a/src/app/api/projects/[id]/update/route.ts +++ b/src/app/api/projects/[id]/update/route.ts @@ -32,6 +32,7 @@ interface UpdateRequestBody { fixByParent?: { name: string; version: string }; }>; lockfileResolution?: LockfileResolutionMode; + auditContext?: { source?: string; advisories?: string[]; severity?: string }; } export async function POST( @@ -324,6 +325,22 @@ export async function POST( } catch { /* non-fatal */ } } + // Origin-tagged remediation audit trail (e.g. applied from the CVE Lite dashboard). + if (anySucceeded && body.auditContext?.source) { + const successfulNames = results.filter(r => r.success).map(r => r.package).filter(n => n !== '*'); + logger.info('patches', 'security_remediation_applied', + `Applied security fix in ${id}: ${successfulNames.join(', ') || '(reconcile)'}`, + { + projectId: id, + meta: { + source: body.auditContext.source, + advisories: body.auditContext.advisories ?? [], + severity: body.auditContext.severity, + packages: successfulNames, + }, + }); + } + const allSucceeded = results.every(r => r.success); const output = results.map(r => r.output).join('\n\n'); From a0e90a0a073aca2ab666b548211f402299c4c0a6 Mon Sep 17 00:00:00 2001 From: alamb-hex Date: Fri, 22 May 2026 22:37:36 -0400 Subject: [PATCH 07/11] feat(security): log security_remediation_applied on cve-lite --fix (#107) Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/security/cve-lite/[id]/fix/route.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/app/api/security/cve-lite/[id]/fix/route.ts b/src/app/api/security/cve-lite/[id]/fix/route.ts index 2c26d0a..28fc455 100644 --- a/src/app/api/security/cve-lite/[id]/fix/route.ts +++ b/src/app/api/security/cve-lite/[id]/fix/route.ts @@ -31,6 +31,7 @@ export async function POST( if (body.mode !== 'all') { return NextResponse.json({ error: "mode must be 'all'" }, { status: 400 }); } + const auditContext = body.auditContext as { source?: string; advisories?: string[]; severity?: string } | undefined; let summary = ''; let ok = true; try { @@ -48,5 +49,15 @@ export async function POST( await runCveLite(project, { force: true }).catch(() => {}); await runSecurityScan(project).catch(() => {}); logger.info('api', 'cve_lite_fix', `cve-lite --fix on ${id} (ok=${ok})`, { projectId: id }); + if (ok && auditContext?.source) { + logger.info('patches', 'security_remediation_applied', `Applied cve-lite --fix in ${id}`, { + projectId: id, + meta: { + source: auditContext.source, + advisories: auditContext.advisories ?? [], + severity: auditContext.severity, + }, + }); + } return NextResponse.json({ ok, summary, rescanned: true }); } From e9d85c094178e81d890b1b3faa103017310da117 Mon Sep 17 00:00:00 2001 From: alamb-hex Date: Fri, 22 May 2026 22:39:37 -0400 Subject: [PATCH 08/11] =?UTF-8?q?feat(security):=20PendingCommitBanner=20f?= =?UTF-8?q?or=20cve-lite=20apply=E2=86=92commit=E2=86=92push=20(#107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../cve-lite/pending-commit-banner.tsx | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/components/security/cve-lite/pending-commit-banner.tsx diff --git a/src/components/security/cve-lite/pending-commit-banner.tsx b/src/components/security/cve-lite/pending-commit-banner.tsx new file mode 100644 index 0000000..7bfb5e8 --- /dev/null +++ b/src/components/security/cve-lite/pending-commit-banner.tsx @@ -0,0 +1,96 @@ +'use client'; +import type { UpdatedPackage } from '@/lib/patch-commit-message'; + +export interface PendingCommitBannerProps { + packages: UpdatedPackage[]; + message: string; + isEditing: boolean; + ahead: number; + committed: boolean; + isCommitting: boolean; + isPushing: boolean; + onMessageChange: (msg: string) => void; + onToggleEdit: () => void; + onCommit: () => void; + onPush: () => void; + onDismiss: () => void; +} + +export function PendingCommitBanner({ + packages, + message, + isEditing, + ahead, + committed, + isCommitting, + isPushing, + onMessageChange, + onToggleEdit, + onCommit, + onPush, + onDismiss, +}: PendingCommitBannerProps) { + const securityCount = packages.filter((p) => p.isSecurityFix).length; + + return ( +
+
+ + {committed ? 'Committed — ready to push' : 'Fix applied — ready to commit'} + {securityCount > 0 && ( + + ({securityCount} security fix{securityCount !== 1 ? 'es' : ''}) + + )} + + +
+ + {isEditing ? ( +