/** * P0 Unit Tests: Security Hardening * * Tests security-related utility helpers in isolation: * - Relation type allowlist * - Path traversal detection * - isTestFilePath patterns */ import { describe, it, expect } from 'vitest'; import { VALID_RELATION_TYPES, VALID_NODE_LABELS, isTestFilePath, } from '../../src/mcp/local/local-backend.js'; // ─── Relation type allowlist ────────────────────────────────────────── describe('VALID_RELATION_TYPES', () => { // The expected types are declared once here; the size assertion derives from // the array length so adding a new type only requires appending to this list. const EXPECTED_RELATION_TYPES = [ 'CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS', 'HAS_METHOD', 'HAS_PROPERTY', 'METHOD_OVERRIDES', 'OVERRIDES', 'METHOD_IMPLEMENTS', 'ACCESSES', // USES is an emitted edge type (emit-references.ts) used in the default // impact relTypes + context queries; added to the allowlist in F5. 'USES', 'HANDLES_ROUTE', 'FETCHES', 'HANDLES_TOOL', 'ENTRY_POINT_OF', 'WRAPS', // Spring DI @Autowired collection injection (#2200) 'INJECTS', // Conditional activation and metadata declaration/discovery (#2415) 'CONDITIONAL_ON', 'DECLARES', // Spring proxy/advice evidence (#2416) 'ADVISED_BY', // Spring async messaging: a callable publishes to / consumes from a broker // Destination. Allow-listed so a cypher query may name them; both are // deliberately absent from impact()'s DEFAULT relTypes, exactly as // HANDLES_ROUTE is, so the default blast radius is unchanged. 'PUBLISHES_TO', 'CONSUMES_FROM', ] as const; it('contains all expected relation types', () => { expect(VALID_RELATION_TYPES.size).toBe(EXPECTED_RELATION_TYPES.length); for (const t of EXPECTED_RELATION_TYPES) { expect(VALID_RELATION_TYPES.has(t)).toBe(true); } }); it('rejects invalid relation types', () => { expect(VALID_RELATION_TYPES.has('CONTAINS')).toBe(false); expect(VALID_RELATION_TYPES.has('calls')).toBe(false); // case-sensitive expect(VALID_RELATION_TYPES.has('DROP_TABLE')).toBe(false); }); it('taint edge types stay OUT of the impact allow-list (#2083 M3 KTD9a)', () => { // impact's BFS traverses symbol space; TAINTED/SANITIZES live in // block-space (BasicBlock→BasicBlock) and would be unreachable noise // there. The `explain` tool is the dedicated taint consumer. Pinned // explicitly so a future "add all emitted types" sweep can't drag them in. expect(VALID_RELATION_TYPES.has('TAINTED')).toBe(false); expect(VALID_RELATION_TYPES.has('SANITIZES')).toBe(false); }); it('TAINT_PATH stays OUT of the impact allow-list (#2084 M4 KTD9a)', () => { // Cross-function TAINT_PATH (Function→Function) is the interprocedural // analogue of TAINTED — surfaced ONLY via `explain` (its interprocedural // findings), never impact()'s BFS. Pinned so a future allow-all sweep // can't drag it in — the size assertion tracks EXPECTED_RELATION_TYPES. expect(VALID_RELATION_TYPES.has('TAINT_PATH')).toBe(false); // Size should match the expected types list — not a hardcoded number. expect(VALID_RELATION_TYPES.size).toBe(EXPECTED_RELATION_TYPES.length); }); it('CDG control-dependence edge types stay OUT of the impact allow-list (#2085 M5)', () => { // CDG and POST_DOMINATE are BasicBlock→BasicBlock (block space), like the // taint substrate — they must not enter impact()'s symbol-space BFS. Pinned // explicitly (not just via the EXPECTED_RELATION_TYPES-derived size guard) // so a future "add all emitted types" sweep can't drag them in, mirroring // the TAINTED/TAINT_PATH pins. expect(VALID_RELATION_TYPES.has('CDG')).toBe(false); expect(VALID_RELATION_TYPES.has('POST_DOMINATE')).toBe(false); // REACHING_DEF is the other BasicBlock→BasicBlock PDG edge (#2086 impact // PDG mode traverses it directly, never through impact's symbol-space BFS). // Pinned alongside CDG/POST_DOMINATE so an allow-all sweep can't drag it in. expect(VALID_RELATION_TYPES.has('REACHING_DEF')).toBe(false); }); }); // ─── Valid node labels ─────────────────────────────────────────────── describe('VALID_NODE_LABELS', () => { it('contains core node types', () => { for (const label of [ 'File', 'Folder', 'Function', 'Class', 'Interface', 'Method', 'CodeElement', ]) { expect(VALID_NODE_LABELS.has(label)).toBe(true); } }); it('contains meta node types', () => { for (const label of ['Community', 'Process']) { expect(VALID_NODE_LABELS.has(label)).toBe(true); } }); it('contains multi-language node types', () => { for (const label of ['Struct', 'Enum', 'Macro', 'Trait', 'Impl', 'Namespace']) { expect(VALID_NODE_LABELS.has(label)).toBe(true); } }); it('rejects invalid labels', () => { expect(VALID_NODE_LABELS.has('InvalidType')).toBe(false); expect(VALID_NODE_LABELS.has('function')).toBe(false); // case-sensitive }); }); // ─── Path traversal detection ──────────────────────────────────────── describe('path traversal (isTestFilePath as proxy for path handling)', () => { it('isTestFilePath matches .test. files', () => { expect(isTestFilePath('src/foo.test.ts')).toBe(true); expect(isTestFilePath('src/foo.spec.ts')).toBe(true); }); it('isTestFilePath matches __tests__ directory', () => { expect(isTestFilePath('src/__tests__/foo.ts')).toBe(true); }); it('isTestFilePath matches /test/ directory', () => { expect(isTestFilePath('src/test/foo.ts')).toBe(true); }); it('isTestFilePath handles Windows backslash paths', () => { expect(isTestFilePath('src\\test\\foo.ts')).toBe(true); expect(isTestFilePath('src\\__tests__\\bar.ts')).toBe(true); }); it('isTestFilePath is case-insensitive', () => { expect(isTestFilePath('SRC/TEST/Foo.ts')).toBe(true); expect(isTestFilePath('SRC/Foo.Test.ts')).toBe(true); }); it('isTestFilePath matches Go test files', () => { expect(isTestFilePath('pkg/handler_test.go')).toBe(true); }); it('isTestFilePath matches Python test files', () => { expect(isTestFilePath('tests/test_handler.py')).toBe(true); expect(isTestFilePath('pkg/handler_test.py')).toBe(true); }); it('isTestFilePath returns false for non-test files', () => { expect(isTestFilePath('src/main.ts')).toBe(false); expect(isTestFilePath('src/utils/helper.ts')).toBe(false); }); it('isTestFilePath returns false for nodes without a filePath', () => { // trace BFS visits Community/Process nodes whose rows carry no filePath; // the guard must return false instead of throwing on undefined/null. expect(isTestFilePath(undefined)).toBe(false); expect(isTestFilePath(null)).toBe(false); expect(isTestFilePath('')).toBe(false); }); });