wulf-pulse/lib/services/campaign-classifier.test.ts
lorentz f4e6baf505 test(19-01): add failing tests + synthetic fixtures for classifier pure rule functions
- campaign-classifier.test.ts: describe blocks for domainMatchesAllowlist,
  isKnownSimulationSender, effectiveAuthResults, hasHardAuthFail,
  computeConfidence, mapVerdictToActions, computeRequiresApproval
- campaign-classifier.fixtures.ts: synthetic KnowBe4/BSN simulation fixtures
  plus non-simulation threat/clean-spam/suspicious-unwanted fixtures
- Covers CLASSIFY-01/02/03/04/06 pure-function behavior (T-19-01, Pitfall 1/3)
2026-07-16 08:14:51 -04:00

189 lines
6.7 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
KNOWN_SIMULATION_SENDERS,
domainMatchesAllowlist,
isKnownSimulationSender,
effectiveAuthResults,
hasHardAuthFail,
computeConfidence,
mapVerdictToActions,
computeRequiresApproval,
} from './campaign-classifier';
import {
knowbe4SimMessage,
bsnSimMessage,
threatMessage,
cleanSpamMessage,
} from './campaign-classifier.fixtures';
describe('KNOWN_SIMULATION_SENDERS', () => {
it('includes both the KnowBe4 and Breach Secure Now sender domains', () => {
const allDomains = KNOWN_SIMULATION_SENDERS.flatMap((entry) => entry.domains);
expect(allDomains).toContain('it-support.care');
expect(allDomains).toContain('breachsecurenow.com');
});
});
describe('domainMatchesAllowlist', () => {
it('matches an exact allowlisted domain', () => {
expect(domainMatchesAllowlist('it-support.care')).toBe(true);
});
it('matches a proper subdomain of an allowlisted domain', () => {
expect(domainMatchesAllowlist('sub.it-support.care')).toBe(true);
expect(domainMatchesAllowlist('em8721.breachsecurenow.com')).toBe(true);
});
it('does NOT match a bare substring / suffix-spoofed domain (T-19-01)', () => {
expect(domainMatchesAllowlist('it-support.care.attacker.net')).toBe(false);
expect(domainMatchesAllowlist('evil-it-support.care')).toBe(false);
});
});
describe('isKnownSimulationSender', () => {
it('matches on From domain (knowbe4SimMessage)', () => {
expect(isKnownSimulationSender(knowbe4SimMessage)).toBe(true);
});
it('matches on Return-Path domain when From.domain is null (Pitfall 3, bsnSimMessage)', () => {
expect(bsnSimMessage.from.domain).toBeNull();
expect(isKnownSimulationSender(bsnSimMessage)).toBe(true);
});
it('does not match a non-allowlisted sender', () => {
expect(isKnownSimulationSender(threatMessage)).toBe(false);
expect(isKnownSimulationSender(cleanSpamMessage)).toBe(false);
});
});
describe('effectiveAuthResults', () => {
it('returns authResultsOriginal when present (Pitfall 1)', () => {
expect(effectiveAuthResults(knowbe4SimMessage)).toEqual(knowbe4SimMessage.authResultsOriginal);
});
it('falls back to authResults when authResultsOriginal is null', () => {
expect(threatMessage.authResultsOriginal).toBeNull();
expect(effectiveAuthResults(threatMessage)).toEqual(threatMessage.authResults);
});
});
describe('hasHardAuthFail', () => {
it('is true when spf is fail', () => {
expect(hasHardAuthFail({ spf: 'fail' })).toBe(true);
});
it('is true when dkim is fail', () => {
expect(hasHardAuthFail({ dkim: 'fail' })).toBe(true);
});
it('is true when dmarc is fail', () => {
expect(hasHardAuthFail({ dmarc: 'fail' })).toBe(true);
});
it('is false for none/neutral/undefined verdicts', () => {
expect(hasHardAuthFail({ spf: 'none', dkim: 'neutral' })).toBe(false);
expect(hasHardAuthFail({})).toBe(false);
});
it('is false when all verdicts pass', () => {
expect(hasHardAuthFail({ spf: 'pass', dkim: 'pass', dmarc: 'pass' })).toBe(false);
});
});
describe('computeConfidence', () => {
it('is 1.0 with no deductions and no reasons when all evidence is present (confidence deduction baseline)', () => {
const result = computeConfidence({
hasAnyMessage: true,
blastRadiusStatus: 'ok',
hasAttachmentOrUrlIndicators: true,
});
expect(result.confidence).toBe(1.0);
expect(result.reasons).toHaveLength(0);
});
it('deducts 0.4 and names the reason when no message was parsed (confidence deduction)', () => {
const result = computeConfidence({
hasAnyMessage: false,
blastRadiusStatus: 'ok',
hasAttachmentOrUrlIndicators: true,
});
expect(result.confidence).toBe(0.6);
expect(result.reasons).toHaveLength(1);
expect(result.reasons[0]).toMatch(/message/i);
});
it('deducts 0.3 and names the reason when blast-radius is unavailable (confidence deduction)', () => {
const result = computeConfidence({
hasAnyMessage: true,
blastRadiusStatus: 'unavailable',
hasAttachmentOrUrlIndicators: true,
});
expect(result.confidence).toBe(0.7);
expect(result.reasons[0]).toMatch(/mimecast|blast/i);
});
it('deducts 0.2 and names the reason when no attachment/url indicators are found (confidence deduction)', () => {
const result = computeConfidence({
hasAnyMessage: true,
blastRadiusStatus: 'ok',
hasAttachmentOrUrlIndicators: false,
});
expect(result.confidence).toBe(0.8);
expect(result.reasons[0]).toMatch(/indicator/i);
});
it('floors at 0.10 with all three named reasons when every evidence source is missing (confidence deduction)', () => {
const result = computeConfidence({
hasAnyMessage: false,
blastRadiusStatus: 'unavailable',
hasAttachmentOrUrlIndicators: false,
});
expect(result.confidence).toBe(0.1);
expect(result.reasons).toHaveLength(3);
});
});
describe('mapVerdictToActions', () => {
it('maps SPAM to no_action', () => {
expect(mapVerdictToActions('SPAM', { clicked: 0 })).toEqual(['no_action']);
});
it('maps UNWANTED to warn_user', () => {
expect(mapVerdictToActions('UNWANTED', { clicked: 0 })).toEqual(['warn_user']);
});
it('maps THREAT with no clicks to block_sender + purge_message', () => {
expect(mapVerdictToActions('THREAT', { clicked: 0 })).toEqual(['block_sender', 'purge_message']);
});
it('maps THREAT with clicks to also include reset_password/isolate_endpoint/disable_forwarding_rule', () => {
const actions = mapVerdictToActions('THREAT', { clicked: 1 });
expect(actions).toContain('block_sender');
expect(actions).toContain('purge_message');
expect(actions).toContain('reset_password');
expect(actions).toContain('isolate_endpoint');
expect(actions).toContain('disable_forwarding_rule');
});
});
describe('computeRequiresApproval', () => {
it('is false for disable_forwarding_rule alone (requires_approval invariant)', () => {
expect(computeRequiresApproval(['disable_forwarding_rule'])).toBe(false);
});
it('is true when disable_forwarding_rule is combined with a destructive action (requires_approval invariant)', () => {
expect(computeRequiresApproval(['disable_forwarding_rule', 'block_sender'])).toBe(true);
});
it.each(['block_sender', 'purge_message', 'reset_password', 'isolate_endpoint'])(
'is true for %s alone (requires_approval invariant)',
(action) => {
expect(computeRequiresApproval([action])).toBe(true);
}
);
it('is false for no_action and warn_user (requires_approval invariant)', () => {
expect(computeRequiresApproval(['no_action'])).toBe(false);
expect(computeRequiresApproval(['warn_user'])).toBe(false);
});
});