diff --git a/src/hostd/privileged-commands.test.ts b/src/hostd/privileged-commands.test.ts index 179de40..5bf6250 100644 --- a/src/hostd/privileged-commands.test.ts +++ b/src/hostd/privileged-commands.test.ts @@ -485,6 +485,39 @@ describe('handleOp — spawnSync args (RC services)', () => { ); }); + it('pkg-audit treats exit code 1 with output as a successful audit (vulnerabilities found)', () => { + mockSpawnSync.mockReturnValueOnce({ + status: 1, + stdout: 'curl-8.7.1 is vulnerable:\n CVE-2024-1234\n\n1 problem(s) found.', + stderr: '', + error: undefined, + }); + const result = handleOp('pkg-audit', {}); + expect(result.ok).toBe(true); + expect(result.exitCode).toBe(1); + expect(result.output).toContain('1 problem(s) found'); + }); + + it('pkg-audit reports failure when exit code > 1 (real error, not vuln list)', () => { + mockSpawnSync.mockReturnValueOnce({ + status: 2, + stdout: '', + stderr: 'pkg: vulndb fetch failed', + error: undefined, + }); + const result = handleOp('pkg-audit', {}); + expect(result.ok).toBe(false); + }); + + it('pkg-version dispatches via bastille cmd when jail is provided', () => { + handleOp('pkg-version', { jail: 'git' }); + expect(mockSpawnSync).toHaveBeenCalledWith( + 'bastille', + ['cmd', 'git', 'pkg', 'version', '-vRUL='], + expect.any(Object), + ); + }); + it('freebsd-update-status uses updatesready (no fetch, no install)', () => { handleOp('freebsd-update-status', {}); expect(mockSpawnSync).toHaveBeenCalledWith( diff --git a/src/hostd/privileged-commands.ts b/src/hostd/privileged-commands.ts index a8f9689..bac5e85 100644 --- a/src/hostd/privileged-commands.ts +++ b/src/hostd/privileged-commands.ts @@ -369,19 +369,30 @@ const OPS: Record = { handler: (p) => exec('pkg', ['install', '-y', String(p.package)]), }, - 'pkg-version': { - schema: z.object({}), - handler: () => exec('pkg', ['version', '-vRUL=']), - }, - // ── Read-only update audits ────────────────────────────────────────────── // These let sysadmin (and any tenant agent diagnosing its own jail) answer // "any updates available?" without root. None of them mutate state. - 'pkg-audit': { + 'pkg-version': { schema: z.object({ jail: jailName.optional() }), handler: (p) => p.jail - ? exec('bastille', ['cmd', String(p.jail), 'pkg', 'audit', '-F']) - : exec('pkg', ['audit', '-F']), + ? exec('bastille', ['cmd', String(p.jail), 'pkg', 'version', '-vRUL=']) + : exec('pkg', ['version', '-vRUL=']), + }, + 'pkg-audit': { + schema: z.object({ jail: jailName.optional() }), + handler: (p) => { + const result = p.jail + ? exec('bastille', ['cmd', String(p.jail), 'pkg', 'audit', '-F']) + : exec('pkg', ['audit', '-F']); + // pkg audit exits 1 when vulnerabilities ARE found — that's a + // successful audit producing a useful report, not a transport + // failure. 0 = clean; 1 = vulns reported; >1 = real error + // (network, vuln-db corrupt, etc.). + if (result.exitCode === 1 && (result.output ?? '').length > 0) { + return { ...result, ok: true }; + } + return result; + }, }, 'freebsd-update-status': { // updatesready exits 0 (none ready) or 2 (ready) — no fetching, no