247 lines
9.2 KiB
TypeScript
247 lines
9.2 KiB
TypeScript
/**
|
|
* Unit tests for fuzzing strategies and FuzzingEngine.
|
|
*/
|
|
|
|
import { detectInputType } from '../../src/plugins/fuzzers/InputTypeDetector';
|
|
import { EmptyValueStrategy } from '../../src/plugins/fuzzers/strategies/EmptyValueStrategy';
|
|
import { OversizedStringStrategy } from '../../src/plugins/fuzzers/strategies/OversizedStringStrategy';
|
|
import { SpecialCharsStrategy } from '../../src/plugins/fuzzers/strategies/SpecialCharsStrategy';
|
|
import { TypeMismatchStrategy } from '../../src/plugins/fuzzers/strategies/TypeMismatchStrategy';
|
|
import { BoundaryValueStrategy } from '../../src/plugins/fuzzers/strategies/BoundaryValueStrategy';
|
|
import { FuzzingEngine } from '../../src/plugins/fuzzers/FuzzingEngine';
|
|
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,
|
|
};
|
|
}
|
|
|
|
// ─── InputTypeDetector ────────────────────────────────────────────────────────
|
|
|
|
describe('detectInputType', () => {
|
|
it('detects email from inputType', () => {
|
|
expect(detectInputType({ inputType: 'email' })).toBe('email');
|
|
});
|
|
|
|
it('detects password from inputType', () => {
|
|
expect(detectInputType({ inputType: 'password' })).toBe('password');
|
|
});
|
|
|
|
it('detects number from inputType', () => {
|
|
expect(detectInputType({ inputType: 'number' })).toBe('number');
|
|
});
|
|
|
|
it('detects email from name attribute', () => {
|
|
expect(detectInputType({ name: 'email_address' })).toBe('email');
|
|
});
|
|
|
|
it('detects phone from placeholder', () => {
|
|
expect(detectInputType({ placeholder: 'Enter phone number' })).toBe('phone');
|
|
});
|
|
|
|
it('detects textarea from tagName', () => {
|
|
expect(detectInputType({ tagName: 'textarea' })).toBe('textarea');
|
|
});
|
|
|
|
it('falls back to text for unknown', () => {
|
|
expect(detectInputType({})).toBe('text');
|
|
});
|
|
});
|
|
|
|
// ─── EmptyValueStrategy ───────────────────────────────────────────────────────
|
|
|
|
describe('EmptyValueStrategy', () => {
|
|
const strategy = new EmptyValueStrategy();
|
|
|
|
it('applies to all types', () => {
|
|
expect(strategy.appliesTo('email')).toBe(true);
|
|
expect(strategy.appliesTo('number')).toBe(true);
|
|
expect(strategy.appliesTo('text')).toBe(true);
|
|
});
|
|
|
|
it('returns empty/whitespace values', () => {
|
|
expect(strategy.values()).toContain('');
|
|
expect(strategy.values()).toContain(' ');
|
|
expect(strategy.values()).toContain('\t');
|
|
});
|
|
});
|
|
|
|
// ─── OversizedStringStrategy ──────────────────────────────────────────────────
|
|
|
|
describe('OversizedStringStrategy', () => {
|
|
it('applies to text types', () => {
|
|
const s = new OversizedStringStrategy('medium');
|
|
expect(s.appliesTo('text')).toBe(true);
|
|
expect(s.appliesTo('email')).toBe(true);
|
|
expect(s.appliesTo('number')).toBe(false);
|
|
});
|
|
|
|
it('returns low-intensity 256 chars', () => {
|
|
const s = new OversizedStringStrategy('low');
|
|
expect(s.values()[0]?.length).toBe(256);
|
|
});
|
|
|
|
it('returns medium-intensity 1024 chars', () => {
|
|
const s = new OversizedStringStrategy('medium');
|
|
expect(s.values()[0]?.length).toBe(1024);
|
|
});
|
|
|
|
it('returns high-intensity 10000+ chars', () => {
|
|
const s = new OversizedStringStrategy('high');
|
|
expect(s.values()[0]!.length).toBeGreaterThan(10000);
|
|
});
|
|
});
|
|
|
|
// ─── SpecialCharsStrategy ─────────────────────────────────────────────────────
|
|
|
|
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>');
|
|
});
|
|
});
|
|
|
|
// ─── TypeMismatchStrategy ─────────────────────────────────────────────────────
|
|
|
|
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 mismatched values for email', () => {
|
|
expect(s.values('email')).toContain('not-an-email');
|
|
});
|
|
|
|
it('returns mismatched values for number', () => {
|
|
expect(s.values('number')).toContain('abc');
|
|
});
|
|
|
|
it('returns empty for unhandled type', () => {
|
|
expect(s.values('text')).toEqual([]);
|
|
});
|
|
});
|
|
|
|
// ─── BoundaryValueStrategy ────────────────────────────────────────────────────
|
|
|
|
describe('BoundaryValueStrategy', () => {
|
|
const s = new BoundaryValueStrategy();
|
|
|
|
it('applies to number and date', () => {
|
|
expect(s.appliesTo('number')).toBe(true);
|
|
expect(s.appliesTo('date')).toBe(true);
|
|
expect(s.appliesTo('text')).toBe(false);
|
|
});
|
|
|
|
it('returns boundary numbers', () => {
|
|
expect(s.values('number')).toContain('0');
|
|
expect(s.values('number')).toContain('2147483647');
|
|
});
|
|
|
|
it('returns boundary dates', () => {
|
|
expect(s.values('date')).toContain('1900-01-01');
|
|
});
|
|
});
|
|
|
|
// ─── FuzzingEngine ────────────────────────────────────────────────────────────
|
|
|
|
describe('FuzzingEngine', () => {
|
|
it('generates actions from DOM snapshot with input fields', () => {
|
|
const engine = new FuzzingEngine({ 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 FuzzingEngine({ intensity: 'low', seed: 1 });
|
|
const high = new FuzzingEngine({ 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 array for DOM with no inputs', () => {
|
|
const engine = new FuzzingEngine({ intensity: 'medium', seed: 1 });
|
|
const dom = `<div><p>No forms here</p></div>`;
|
|
const state = makeState(dom);
|
|
expect(engine.generateFuzzActions(dom, state)).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
// ─── AnomalyDetector fuzzing rules ────────────────────────────────────────────
|
|
|
|
describe('AnomalyDetector fuzzing anomaly types', () => {
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const { AnomalyDetector } = require('../../src/core/AnomalyDetector');
|
|
const detector = new AnomalyDetector();
|
|
|
|
const baseObs = {
|
|
id: 'obs1',
|
|
actionId: 'act1',
|
|
newStateId: 's1',
|
|
httpResponses: [],
|
|
consoleErrors: [],
|
|
jsExceptions: [],
|
|
timestamp: Date.now(),
|
|
};
|
|
|
|
it('detects validation_bypass on 200 response to empty input', () => {
|
|
const obs = { ...baseObs, httpResponses: [{ url: '/', status: 200, method: 'POST', durationMs: 10 }] };
|
|
const result = detector.checkValidationBypass(obs, [], '');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.type).toBe('validation_bypass');
|
|
});
|
|
|
|
it('does not detect validation_bypass without 2xx', () => {
|
|
const obs = { ...baseObs, httpResponses: [{ url: '/', status: 400, method: 'POST', durationMs: 10 }] };
|
|
const result = detector.checkValidationBypass(obs, [], '');
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
it('detects server_error_on_fuzz on 500', () => {
|
|
const obs = { ...baseObs, httpResponses: [{ url: '/', status: 500, method: 'POST', durationMs: 10 }] };
|
|
const result = detector.checkServerErrorOnFuzz(obs, []);
|
|
expect(result).not.toBeNull();
|
|
expect(result!.type).toBe('server_error_on_fuzz');
|
|
expect(result!.severity).toBe('high');
|
|
});
|
|
|
|
it('detects xss_reflection when script tag in DOM', () => {
|
|
const result = detector.checkXssReflection(baseObs, [], '<script>alert(1)</script>');
|
|
expect(result).not.toBeNull();
|
|
expect(result!.type).toBe('xss_reflection');
|
|
expect(result!.severity).toBe('critical');
|
|
});
|
|
|
|
it('does not detect xss_reflection without payload in DOM', () => {
|
|
const result = detector.checkXssReflection(baseObs, [], '<div>clean</div>');
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|