ktg-plugin-marketplace/plugins/llm-security/tests/scanners/ai-bom.test.mjs
Kjell Tore Guttormsen 0439e0f650 feat(scanner): add AI-BOM generator — CycloneDX 1.6 format for AI supply chain transparency
New bom-builder.mjs discovers AI components (models, MCP servers, plugins,
knowledge files, hooks) and builds CycloneDX 1.6 JSON BOMs.
CLI entry point: node scanners/ai-bom-generator.mjs <target>.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 13:29:30 +02:00

123 lines
5 KiB
JavaScript

// ai-bom.test.mjs — Tests for AI-BOM generator (CycloneDX 1.6)
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { discoverComponents, buildAIBOM } from '../../scanners/lib/bom-builder.mjs';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
const GRADE_A_FIXTURE = resolve(__dirname, '../fixtures/posture-scan/grade-a-project');
const GRADE_F_FIXTURE = resolve(__dirname, '../fixtures/posture-scan/grade-f-project');
const PLUGIN_ROOT = resolve(__dirname, '../..');
// ---------------------------------------------------------------------------
// CycloneDX structure tests
// ---------------------------------------------------------------------------
describe('ai-bom: buildAIBOM structure', () => {
it('produces valid CycloneDX 1.6 envelope', () => {
const bom = buildAIBOM([]);
assert.equal(bom.bomFormat, 'CycloneDX');
assert.equal(bom.specVersion, '1.6');
assert.equal(bom.version, 1);
});
it('has metadata with timestamp and tools', () => {
const bom = buildAIBOM([]);
assert.ok(bom.metadata.timestamp.match(/^\d{4}-\d{2}-\d{2}T/));
assert.equal(bom.metadata.tools[0].name, 'ai-bom-generator');
assert.equal(bom.metadata.tools[0].vendor, 'llm-security');
});
it('includes project metadata from arguments', () => {
const bom = buildAIBOM([], { name: 'test-project', version: '1.2.3' });
assert.equal(bom.metadata.component.name, 'test-project');
assert.equal(bom.metadata.component.version, '1.2.3');
});
it('defaults project name to unknown', () => {
const bom = buildAIBOM([]);
assert.equal(bom.metadata.component.name, 'unknown');
assert.equal(bom.metadata.component.version, '0.0.0');
});
it('has components and dependencies arrays', () => {
const bom = buildAIBOM([]);
assert.ok(Array.isArray(bom.components));
assert.ok(Array.isArray(bom.dependencies));
});
it('passes through components array', () => {
const components = [
{ type: 'machine-learning-model', name: 'opus', 'bom-ref': 'model-opus' },
];
const bom = buildAIBOM(components);
assert.equal(bom.components.length, 1);
assert.equal(bom.components[0].name, 'opus');
});
});
// ---------------------------------------------------------------------------
// Component discovery tests
// ---------------------------------------------------------------------------
describe('ai-bom: discoverComponents', () => {
it('discovers hooks from grade-a fixture', async () => {
const components = await discoverComponents(GRADE_A_FIXTURE);
const hooks = components.filter(c => c.name.startsWith('hook:'));
assert.ok(hooks.length >= 4, `Expected >= 4 hooks, got ${hooks.length}`);
});
it('discovers knowledge files from plugin root', async () => {
const components = await discoverComponents(PLUGIN_ROOT);
const knowledge = components.filter(c => c.name.startsWith('knowledge:'));
assert.ok(knowledge.length >= 5, `Expected >= 5 knowledge files, got ${knowledge.length}`);
});
it('returns empty array for minimal project', async () => {
const components = await discoverComponents(GRADE_F_FIXTURE);
// Grade-f has no hooks, no MCP, no knowledge
assert.ok(Array.isArray(components));
});
it('all components have type and name', async () => {
const components = await discoverComponents(PLUGIN_ROOT);
for (const c of components) {
assert.ok(c.type, `Component missing type: ${JSON.stringify(c)}`);
assert.ok(c.name, `Component missing name: ${JSON.stringify(c)}`);
assert.ok(c['bom-ref'], `Component missing bom-ref: ${JSON.stringify(c)}`);
}
});
it('component types are valid CycloneDX types', async () => {
const validTypes = new Set(['machine-learning-model', 'framework', 'library', 'data', 'application']);
const components = await discoverComponents(PLUGIN_ROOT);
for (const c of components) {
assert.ok(validTypes.has(c.type), `Invalid component type: ${c.type} for ${c.name}`);
}
});
});
// ---------------------------------------------------------------------------
// Integration: discover + build
// ---------------------------------------------------------------------------
describe('ai-bom: end-to-end', () => {
it('produces valid CycloneDX BOM from real project', async () => {
const components = await discoverComponents(PLUGIN_ROOT);
const bom = buildAIBOM(components, { name: 'llm-security', version: '6.0.0' });
assert.equal(bom.bomFormat, 'CycloneDX');
assert.equal(bom.specVersion, '1.6');
assert.ok(bom.components.length > 0, 'Expected at least one component');
assert.equal(bom.metadata.component.name, 'llm-security');
});
it('empty project produces valid BOM with zero components', async () => {
const components = await discoverComponents(GRADE_F_FIXTURE);
const bom = buildAIBOM(components);
assert.equal(bom.bomFormat, 'CycloneDX');
assert.ok(Array.isArray(bom.components));
});
});