fase(6): fuzzing module complete
This commit is contained in:
319
tests/modules/fuzzing.test.ts
Normal file
319
tests/modules/fuzzing.test.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
/**
|
||||
* Unit tests for the fuzzing module.
|
||||
*/
|
||||
|
||||
import { FuzzSession } from '../../src/modules/fuzzing/domain/entities/FuzzSession';
|
||||
import { FuzzResult } from '../../src/modules/fuzzing/domain/entities/FuzzResult';
|
||||
import { FuzzIntensity } from '../../src/modules/fuzzing/domain/value-objects/FuzzIntensity';
|
||||
import { FuzzStrategy } from '../../src/modules/fuzzing/domain/value-objects/FuzzStrategy';
|
||||
import { Seed } from '../../src/modules/fuzzing/domain/value-objects/Seed';
|
||||
import { FuzzPayload } from '../../src/modules/fuzzing/domain/value-objects/FuzzPayload';
|
||||
import { EmptyValueStrategy } from '../../src/modules/fuzzing/infrastructure/strategies/EmptyValueStrategy';
|
||||
import { OversizedStringStrategy } from '../../src/modules/fuzzing/infrastructure/strategies/OversizedStringStrategy';
|
||||
import { SpecialCharsStrategy } from '../../src/modules/fuzzing/infrastructure/strategies/SpecialCharsStrategy';
|
||||
import { TypeMismatchStrategy } from '../../src/modules/fuzzing/infrastructure/strategies/TypeMismatchStrategy';
|
||||
import { BoundaryValueStrategy } from '../../src/modules/fuzzing/infrastructure/strategies/BoundaryValueStrategy';
|
||||
import { FuzzingEngineAdapter } from '../../src/modules/fuzzing/infrastructure/adapters/FuzzingEngineAdapter';
|
||||
import { detectInputType } from '../../src/modules/fuzzing/infrastructure/adapters/InputTypeDetector';
|
||||
import { IState } from '../../src/core/interfaces';
|
||||
|
||||
function makeState(domSnapshot = ''): IState {
|
||||
return {
|
||||
id: 'state1',
|
||||
url: 'http://test.com',
|
||||
title: 'Test',
|
||||
timestamp: Date.now(),
|
||||
domSnapshot,
|
||||
visitCount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Value Objects ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('FuzzIntensity', () => {
|
||||
it('creates low intensity', () => {
|
||||
expect(FuzzIntensity.low().value).toBe('low');
|
||||
});
|
||||
|
||||
it('creates from string', () => {
|
||||
expect(FuzzIntensity.fromString('high').value).toBe('high');
|
||||
});
|
||||
|
||||
it('throws for invalid intensity', () => {
|
||||
expect(() => FuzzIntensity.fromString('extreme')).toThrow();
|
||||
});
|
||||
|
||||
it('equality works', () => {
|
||||
expect(FuzzIntensity.medium().equals(FuzzIntensity.medium())).toBe(true);
|
||||
expect(FuzzIntensity.low().equals(FuzzIntensity.high())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FuzzStrategy', () => {
|
||||
it('creates all strategies', () => {
|
||||
expect(FuzzStrategy.empty().value).toBe('empty');
|
||||
expect(FuzzStrategy.specialChars().value).toBe('special_chars');
|
||||
expect(FuzzStrategy.boundary().value).toBe('boundary');
|
||||
});
|
||||
|
||||
it('throws for invalid strategy', () => {
|
||||
expect(() => FuzzStrategy.fromString('invalid')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Seed', () => {
|
||||
it('creates valid seed', () => {
|
||||
expect(Seed.create(42).value).toBe(42);
|
||||
});
|
||||
|
||||
it('throws for negative seed', () => {
|
||||
expect(() => Seed.create(-1)).toThrow();
|
||||
});
|
||||
|
||||
it('throws for non-integer', () => {
|
||||
expect(() => Seed.create(1.5)).toThrow();
|
||||
});
|
||||
|
||||
it('creates from timestamp', () => {
|
||||
const seed = Seed.fromTimestamp();
|
||||
expect(seed.value).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FuzzPayload', () => {
|
||||
it('creates payload with value and strategy', () => {
|
||||
const p = FuzzPayload.create("' OR 1=1 --", 'special_chars');
|
||||
expect(p.value).toBe("' OR 1=1 --");
|
||||
expect(p.strategy).toBe('special_chars');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── FuzzSession Aggregate ───────────────────────────────────────────────────
|
||||
|
||||
describe('FuzzSession', () => {
|
||||
it('creates a fuzz session with domain event', () => {
|
||||
const result = FuzzSession.create({
|
||||
crawlSessionId: 'crawl-1',
|
||||
intensity: 'medium',
|
||||
seed: 12345,
|
||||
});
|
||||
|
||||
if (!result.ok) throw new Error('Expected Ok');
|
||||
const session = result.value;
|
||||
expect(session.intensity.value).toBe('medium');
|
||||
expect(session.seed.value).toBe(12345);
|
||||
expect(session.status).toBe('running');
|
||||
expect(session.actionsExecuted).toBe(0);
|
||||
expect(session.domainEvents).toHaveLength(1);
|
||||
expect(session.domainEvents[0]!.eventName).toBe('fuzz.started');
|
||||
});
|
||||
|
||||
it('fails with invalid intensity', () => {
|
||||
const result = FuzzSession.create({
|
||||
crawlSessionId: 'crawl-1',
|
||||
intensity: 'extreme' as 'low',
|
||||
seed: 1,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it('fails with negative seed', () => {
|
||||
const result = FuzzSession.create({
|
||||
crawlSessionId: 'crawl-1',
|
||||
intensity: 'low',
|
||||
seed: -1,
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
});
|
||||
|
||||
it('tracks vulnerability detection', () => {
|
||||
const r1 = FuzzSession.create({ crawlSessionId: 'c1', intensity: 'high', seed: 1 });
|
||||
if (!r1.ok) throw new Error('Expected Ok');
|
||||
const session = r1.value;
|
||||
session.clearEvents();
|
||||
|
||||
const fuzzResult = FuzzResult.create({
|
||||
sessionId: session.id.toString(),
|
||||
stateId: 'state1',
|
||||
selector: '#email',
|
||||
payload: "' OR 1=1 --",
|
||||
strategy: 'special_chars',
|
||||
anomalyType: 'xss_reflection',
|
||||
severity: 'critical',
|
||||
description: 'XSS reflected',
|
||||
});
|
||||
|
||||
session.recordVulnerability(fuzzResult);
|
||||
expect(session.vulnerabilitiesFound).toBe(1);
|
||||
expect(session.actionsExecuted).toBe(1);
|
||||
expect(session.domainEvents[0]!.eventName).toBe('fuzz.vulnerability_detected');
|
||||
});
|
||||
|
||||
it('completes session with event', () => {
|
||||
const r2 = FuzzSession.create({ crawlSessionId: 'c1', intensity: 'low', seed: 1 });
|
||||
if (!r2.ok) throw new Error('Expected Ok');
|
||||
const session = r2.value;
|
||||
session.clearEvents();
|
||||
session.complete();
|
||||
expect(session.status).toBe('completed');
|
||||
expect(session.completedAt).toBeDefined();
|
||||
expect(session.domainEvents[0]!.eventName).toBe('fuzz.completed');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Strategies ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('EmptyValueStrategy', () => {
|
||||
const s = new EmptyValueStrategy();
|
||||
|
||||
it('applies to all types', () => {
|
||||
expect(s.appliesTo('email')).toBe(true);
|
||||
expect(s.appliesTo('number')).toBe(true);
|
||||
expect(s.appliesTo('select')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns empty/whitespace values', () => {
|
||||
const vals = s.values();
|
||||
expect(vals).toContain('');
|
||||
expect(vals).toContain(' ');
|
||||
expect(vals).toContain('\t');
|
||||
});
|
||||
});
|
||||
|
||||
describe('OversizedStringStrategy', () => {
|
||||
it('applies to text, email, password, textarea', () => {
|
||||
const s = new OversizedStringStrategy('medium');
|
||||
expect(s.appliesTo('text')).toBe(true);
|
||||
expect(s.appliesTo('email')).toBe(true);
|
||||
expect(s.appliesTo('number')).toBe(false);
|
||||
});
|
||||
|
||||
it('low = 256 chars', () => {
|
||||
const s = new OversizedStringStrategy('low');
|
||||
expect(s.values()[0]?.length).toBe(256);
|
||||
});
|
||||
|
||||
it('medium = 1024 chars', () => {
|
||||
const s = new OversizedStringStrategy('medium');
|
||||
expect(s.values()[0]?.length).toBe(1024);
|
||||
});
|
||||
|
||||
it('high = 10000+ chars', () => {
|
||||
const s = new OversizedStringStrategy('high');
|
||||
expect(s.values()[0]!.length).toBeGreaterThan(10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SpecialCharsStrategy', () => {
|
||||
const s = new SpecialCharsStrategy();
|
||||
|
||||
it('applies to text, email, search, textarea', () => {
|
||||
expect(s.appliesTo('text')).toBe(true);
|
||||
expect(s.appliesTo('email')).toBe(true);
|
||||
expect(s.appliesTo('number')).toBe(false);
|
||||
});
|
||||
|
||||
it('includes SQL injection payload', () => {
|
||||
expect(s.values()).toContain("' OR 1=1 --");
|
||||
});
|
||||
|
||||
it('includes XSS payload', () => {
|
||||
expect(s.values()).toContain('<script>alert(1)</script>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TypeMismatchStrategy', () => {
|
||||
const s = new TypeMismatchStrategy();
|
||||
|
||||
it('applies to typed fields', () => {
|
||||
expect(s.appliesTo('email')).toBe(true);
|
||||
expect(s.appliesTo('number')).toBe(true);
|
||||
expect(s.appliesTo('text')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns mismatch for email', () => {
|
||||
expect(s.values('email')).toContain('not-an-email');
|
||||
});
|
||||
|
||||
it('returns mismatch for number', () => {
|
||||
expect(s.values('number')).toContain('abc');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BoundaryValueStrategy', () => {
|
||||
const s = new BoundaryValueStrategy();
|
||||
|
||||
it('applies to number and date only', () => {
|
||||
expect(s.appliesTo('number')).toBe(true);
|
||||
expect(s.appliesTo('date')).toBe(true);
|
||||
expect(s.appliesTo('text')).toBe(false);
|
||||
});
|
||||
|
||||
it('returns boundary numbers', () => {
|
||||
const vals = s.values('number');
|
||||
expect(vals).toContain('0');
|
||||
expect(vals).toContain('2147483647');
|
||||
expect(vals).toContain('-2147483648');
|
||||
});
|
||||
|
||||
it('returns boundary dates', () => {
|
||||
const vals = s.values('date');
|
||||
expect(vals).toContain('1900-01-01');
|
||||
expect(vals).toContain('2099-12-31');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── InputTypeDetector ────────────────────────────────────────────────────────
|
||||
|
||||
describe('detectInputType (module adapter)', () => {
|
||||
it('detects email from inputType', () => {
|
||||
expect(detectInputType({ inputType: 'email' })).toBe('email');
|
||||
});
|
||||
|
||||
it('detects password', () => {
|
||||
expect(detectInputType({ inputType: 'password' })).toBe('password');
|
||||
});
|
||||
|
||||
it('detects textarea', () => {
|
||||
expect(detectInputType({ tagName: 'textarea' })).toBe('textarea');
|
||||
});
|
||||
|
||||
it('infers email from name', () => {
|
||||
expect(detectInputType({ name: 'email_address' })).toBe('email');
|
||||
});
|
||||
|
||||
it('falls back to text', () => {
|
||||
expect(detectInputType({})).toBe('text');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── FuzzingEngineAdapter ─────────────────────────────────────────────────────
|
||||
|
||||
describe('FuzzingEngineAdapter', () => {
|
||||
it('generates fuzz actions from DOM with input fields', () => {
|
||||
const engine = new FuzzingEngineAdapter({ intensity: 'low', seed: 42 });
|
||||
const dom = `<form><input type="email" name="email" /><input type="password" name="pass" /></form>`;
|
||||
const state = makeState(dom);
|
||||
const actions = engine.generateFuzzActions(dom, state);
|
||||
expect(actions.length).toBeGreaterThan(0);
|
||||
expect(actions.every((a) => a.type === 'fill')).toBe(true);
|
||||
expect(actions.every((a) => a.stateId === 'state1')).toBe(true);
|
||||
});
|
||||
|
||||
it('generates more actions at high intensity', () => {
|
||||
const low = new FuzzingEngineAdapter({ intensity: 'low', seed: 1 });
|
||||
const high = new FuzzingEngineAdapter({ intensity: 'high', seed: 1 });
|
||||
const dom = `<input type="text" name="q" />`;
|
||||
const state = makeState(dom);
|
||||
expect(high.generateFuzzActions(dom, state).length).toBeGreaterThan(
|
||||
low.generateFuzzActions(dom, state).length
|
||||
);
|
||||
});
|
||||
|
||||
it('returns empty for DOM with no inputs', () => {
|
||||
const engine = new FuzzingEngineAdapter({ intensity: 'medium', seed: 1 });
|
||||
const dom = `<div><p>No forms here</p></div>`;
|
||||
const state = makeState(dom);
|
||||
expect(engine.generateFuzzActions(dom, state)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user