'use strict'; const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { afterEach, test } = require('node:test'); const { ImageOsVulnerabilityPolicyError, auditImageOsVulnerabilityPolicy, parseArguments, renderTrivyIgnore, runCli, } = require('../../scripts/ql3-image-os-vulnerability-policy.cjs'); const NOW = Date.parse('2026-08-01T12:00:00.000Z'); const temporaryDirectories = []; function exception(overrides = {}) { return { id: 'CVE-2026-12345', images: ['admin', 'control'], purls: ['pkg:deb/debian/libssl3@3.0.0-1'], owner: 'security/platform', ticket: 'QLSEC-123', expiresOn: '2026-08-15', rationale: 'Temporary exposure accepted while the fixed base image is qualified.', ...overrides, }; } function policy(exceptions = []) { return { schemaVersion: 1, fixture: 'qinglong/image-os-vulnerability-exceptions@v1', exceptions, }; } afterEach(() => { for (const directory of temporaryDirectories.splice(0)) { fs.rmSync(directory, { recursive: true, force: true }); } }); test('accepts the empty fail-closed production exception policy', () => { const audit = auditImageOsVulnerabilityPolicy(policy(), { now: () => NOW }); assert.deepEqual(audit, { compatible: true, findings: [], exceptionCount: 0, imageExceptionCounts: { admin: 0, control: 0, 'control-ai': 0, local: 0, }, }); assert.equal( renderTrivyIgnore(policy(), 'local', { now: () => NOW }), 'vulnerabilities:\n []\n', ); }); test('renders only one image scoped active exception with lifecycle metadata', () => { const document = policy([exception()]); const admin = renderTrivyIgnore(document, 'admin', { now: () => NOW }); assert.match(admin, /CVE-2026-12345/); assert.match(admin, /pkg:deb\/debian\/libssl3@3\.0\.0-1/); assert.match(admin, /expired_at: 2026-08-15/); assert.match(admin, /owner=security\/platform; ticket=QLSEC-123/); assert.equal( renderTrivyIgnore(document, 'local', { now: () => NOW }), 'vulnerabilities:\n []\n', ); }); test('rejects expired, same-day and overlong exceptions', () => { for (const expiresOn of ['2026-07-31', '2026-08-01', '2026-09-01']) { const audit = auditImageOsVulnerabilityPolicy( policy([exception({ expiresOn })]), { now: () => NOW }, ); assert.equal(audit.compatible, false); assert.equal( audit.findings.some( (finding) => finding.code === 'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_EXPIRY', ), true, ); } }); test('rejects missing ownership, ticket and meaningful rationale', () => { const audit = auditImageOsVulnerabilityPolicy( policy([ exception({ owner: 'UPPER', ticket: 'none', rationale: 'temporary' }), ]), { now: () => NOW }, ); assert.deepEqual( audit.findings.map((finding) => finding.code), [ 'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_OWNER', 'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_TICKET', 'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_RATIONALE', ], ); }); test('rejects unscoped images and non-OS package purls', () => { const audit = auditImageOsVulnerabilityPolicy( policy([ exception({ images: ['unknown'], purls: ['pkg:npm/example@1.0.0'], }), ]), { now: () => NOW }, ); assert.deepEqual( audit.findings.map((finding) => finding.code), [ 'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_IMAGES', 'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_PURLS', ], ); }); test('rejects duplicate, unsorted and extensible exception identities', () => { for (const exceptions of [ [exception(), exception()], [ exception({ id: 'CVE-2026-99999' }), exception({ id: 'CVE-2026-12345' }), ], [{ ...exception(), extra: true }], ]) { const audit = auditImageOsVulnerabilityPolicy(policy(exceptions), { now: () => NOW, }); assert.equal(audit.compatible, false); assert.equal( audit.findings.some( (finding) => finding.code === 'QL3_IMAGE_OS_VULNERABILITY_EXCEPTION_ID', ), true, ); } }); test('creates one private no-replace Trivy ignore file through the exact CLI', () => { const directory = fs.realpathSync( fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-os-policy-')), ); temporaryDirectories.push(directory); const policyDirectory = path.join(directory, 'deploy/containers'); fs.mkdirSync(policyDirectory, { recursive: true }); fs.writeFileSync( path.join(policyDirectory, 'ql3-os-vulnerability-exceptions.json'), `${JSON.stringify(policy([exception()]))}\n`, ); const output = path.join(directory, 'admin.trivyignore.yaml'); runCli([`--image=admin`, `--output=${output}`], directory, { now: () => NOW, }); assert.equal(fs.statSync(output).mode & 0o777, 0o600); assert.match(fs.readFileSync(output, 'utf8'), /QLSEC-123/); assert.throws( () => runCli([`--image=admin`, `--output=${output}`], directory, { now: () => NOW, }), /output path must be unused/, ); }); test('parses only audit mode or exact image/output render arguments', () => { assert.deepEqual(parseArguments([]), { mode: 'audit' }); assert.equal( parseArguments(['--image=control', '--output=/tmp/ignore.yaml']).image, 'control', ); assert.throws( () => parseArguments(['--image=control']), ImageOsVulnerabilityPolicyError, ); assert.throws( () => parseArguments(['--image=control', '--output=/tmp/a', '--extra=x']), ImageOsVulnerabilityPolicyError, ); });