shieldx/tests/unit/behavioral/KillChainMapper.test.ts
Rene Fichtmueller 1c4c034483 feat: ShieldX v0.3.0 — UnicodeScanner (L5), DNS Covert Channel rules, ATLAS v5.4 mappings
- Layer 4 EntropyScanner: Shannon entropy, Base32/Base64 detection, CVE-2025-55284
  ping/nslookup exfil, EchoLeak markdown pattern, DNS tunneling (iodine/dnscat)
- Layer 5 UnicodeScanner: ASCII Smuggling (U+E0000 Tags Block), Variant Selectors,
  Zero-Width steganography, CamoLeak image-ordering (CVE-2025-53773), homoglyphs,
  BiDi override, high-entropy URL params
- 30 DNS covert channel rules (dns-001 to dns-030)
- ATLASMapper: 29 techniques (ATLAS v5.4.0 Feb 2026), added AML.T0062 (Agent Tool
  Invocation), AML.TA0015 (C2 tactic), memory poisoning, multi-agent trust,
  CamoLeak, Unicode steganography mappings
- Rule count: 72 → 102
- Build: tsup 316ms, zero TypeScript errors
2026-03-31 16:32:16 +02:00

178 lines
7.1 KiB
TypeScript

import { describe, it, expect, beforeEach } from 'vitest'
import { KillChainMapper, KILL_CHAIN_PHASES } from '../../../src/behavioral/KillChainMapper.js'
import type { ScanResult, KillChainPhase } from '../../../src/types/detection.js'
function makeScanResult(overrides: Partial<ScanResult> = {}): ScanResult {
return {
scannerId: 'test-scanner',
scannerType: 'rule',
detected: true,
confidence: 0.8,
threatLevel: 'high',
killChainPhase: 'initial_access',
matchedPatterns: ['test-pattern'],
latencyMs: 1,
...overrides,
}
}
describe('KillChainMapper', () => {
let mapper: KillChainMapper
beforeEach(() => {
mapper = new KillChainMapper()
})
describe('classify()', () => {
it('should return "none" for empty scan results', () => {
const classification = mapper.classify([])
expect(classification.primaryPhase).toBe('none')
expect(classification.allPhases).toHaveLength(0)
expect(classification.isMultiPhase).toBe(false)
expect(classification.confidence).toBe(1.0)
})
it('should return "none" when no results are detected', () => {
const results = [makeScanResult({ detected: false })]
const classification = mapper.classify(results)
expect(classification.primaryPhase).toBe('none')
expect(classification.allPhases).toHaveLength(0)
})
describe('phase classification for each of 7 phases', () => {
const testCases: Array<{ phase: KillChainPhase; label: string }> = [
{ phase: 'initial_access', label: 'Initial Access' },
{ phase: 'privilege_escalation', label: 'Privilege Escalation' },
{ phase: 'reconnaissance', label: 'Reconnaissance' },
{ phase: 'persistence', label: 'Persistence' },
{ phase: 'command_and_control', label: 'Command and Control' },
{ phase: 'lateral_movement', label: 'Lateral Movement' },
{ phase: 'actions_on_objective', label: 'Actions on Objective' },
]
for (const { phase, label } of testCases) {
it(`should classify ${label} phase correctly`, () => {
const results = [makeScanResult({ killChainPhase: phase })]
const classification = mapper.classify(results)
expect(classification.primaryPhase).toBe(phase)
expect(classification.allPhases.length).toBeGreaterThan(0)
expect(classification.allPhases[0]!.phase).toBe(phase)
})
}
})
describe('priority ordering', () => {
it('should prioritize actions_on_objective over initial_access', () => {
const results = [
makeScanResult({ killChainPhase: 'initial_access', confidence: 0.9 }),
makeScanResult({ killChainPhase: 'actions_on_objective', confidence: 0.7 }),
]
const classification = mapper.classify(results)
expect(classification.primaryPhase).toBe('actions_on_objective')
})
it('should prioritize lateral_movement over reconnaissance', () => {
const results = [
makeScanResult({ killChainPhase: 'reconnaissance', confidence: 0.95 }),
makeScanResult({ killChainPhase: 'lateral_movement', confidence: 0.6 }),
]
const classification = mapper.classify(results)
expect(classification.primaryPhase).toBe('lateral_movement')
})
it('should prioritize command_and_control over privilege_escalation', () => {
const results = [
makeScanResult({ killChainPhase: 'privilege_escalation', confidence: 0.9 }),
makeScanResult({ killChainPhase: 'command_and_control', confidence: 0.7 }),
]
const classification = mapper.classify(results)
expect(classification.primaryPhase).toBe('command_and_control')
})
})
describe('multi-phase detection', () => {
it('should detect multi-phase attack (2+ phases)', () => {
const results = [
makeScanResult({ killChainPhase: 'initial_access' }),
makeScanResult({ killChainPhase: 'privilege_escalation' }),
]
const classification = mapper.classify(results)
expect(classification.isMultiPhase).toBe(true)
expect(classification.allPhases.length).toBe(2)
})
it('should not flag single-phase as multi-phase', () => {
const results = [
makeScanResult({ killChainPhase: 'initial_access', scannerId: 'scanner-1' }),
makeScanResult({ killChainPhase: 'initial_access', scannerId: 'scanner-2' }),
]
const classification = mapper.classify(results)
expect(classification.isMultiPhase).toBe(false)
})
it('should include description of attack chain', () => {
const results = [
makeScanResult({ killChainPhase: 'initial_access' }),
makeScanResult({ killChainPhase: 'persistence' }),
makeScanResult({ killChainPhase: 'actions_on_objective' }),
]
const classification = mapper.classify(results)
expect(classification.isMultiPhase).toBe(true)
expect(classification.attackChainDescription).toContain('Multi-phase')
expect(classification.attackChainDescription).toContain('3 phases')
})
})
describe('confidence scoring', () => {
it('should return confidence > 0 for detected phases', () => {
const results = [makeScanResult({ killChainPhase: 'initial_access', confidence: 0.8 })]
const classification = mapper.classify(results)
expect(classification.confidence).toBeGreaterThan(0)
expect(classification.confidence).toBeLessThanOrEqual(1.0)
})
it('should include matched rule IDs in phase mappings', () => {
const results = [makeScanResult({ killChainPhase: 'reconnaissance', scannerId: 'pe-001' })]
const classification = mapper.classify(results)
expect(classification.allPhases[0]!.matchedRuleIds).toContain('pe-001')
})
})
describe('rule prefix classification', () => {
it('should classify by scanner ID prefix when killChainPhase is none', () => {
const results = [makeScanResult({
killChainPhase: 'none',
scannerId: 'io-001',
matchedPatterns: ['io-injection'],
})]
const classification = mapper.classify(results)
// io- prefix should map to initial_access
expect(classification.primaryPhase).not.toBe('none')
})
})
})
describe('KILL_CHAIN_PHASES', () => {
it('should define all 7 phases', () => {
const phases = Object.keys(KILL_CHAIN_PHASES)
expect(phases).toHaveLength(7)
expect(phases).toContain('initial_access')
expect(phases).toContain('privilege_escalation')
expect(phases).toContain('reconnaissance')
expect(phases).toContain('persistence')
expect(phases).toContain('command_and_control')
expect(phases).toContain('lateral_movement')
expect(phases).toContain('actions_on_objective')
})
it('should have name, description, and mitigations for each phase', () => {
for (const detail of Object.values(KILL_CHAIN_PHASES)) {
expect(detail.name).toBeTruthy()
expect(detail.description).toBeTruthy()
expect(detail.mitigations.length).toBeGreaterThan(0)
expect(detail.rulePatterns.length).toBeGreaterThan(0)
}
})
})
})