diff --git a/.ralph/.loop_start_sha b/.ralph/.loop_start_sha
index cdceb86..c2592a2 100644
--- a/.ralph/.loop_start_sha
+++ b/.ralph/.loop_start_sha
@@ -1 +1 @@
-96bf6e50979e4cc152e9715b965f27eeb2decbc1
+d62bd615bf6b93b982e19e35b0b49591c648e5d2
diff --git a/.ralph/fix_plan.md b/.ralph/fix_plan.md
index c2c782c..8dd606f 100644
--- a/.ralph/fix_plan.md
+++ b/.ralph/fix_plan.md
@@ -89,40 +89,40 @@ Spec: `.ralph/specs/phase-04-crawling-infrastructure.md`
---
-## Phase 5: Findings Module [PENDIENTE]
+## Phase 5: Findings Module [COMPLETO]
Spec: `.ralph/specs/phase-05-findings-module.md`
-- [ ] 5.1: Crear `domain/entities/Finding.ts` — AggregateRoot con severity, type, evidence, status, actionTrace
-- [ ] 5.2: Crear value objects: `Severity.ts` (low/medium/high/critical), `FindingType.ts`, `Evidence.ts`, `FindingStatus.ts` (open/investigating/resolved/closed)
-- [ ] 5.3: Crear events: `FindingCreated.ts`, `FindingResolved.ts`, `FindingEnriched.ts`
-- [ ] 5.4: Crear ports: `IFindingRepository.ts`, `IAIEnricher.ts`
-- [ ] 5.5: Crear commands: `CreateFindingCommand.ts`, `EnrichFindingCommand.ts`, `ResolveFindingCommand.ts`
-- [ ] 5.6: Crear queries: `GetFindingQuery.ts`, `ListFindingsQuery.ts` (filtros: severity, type, session, status, search), `FindingStatsQuery.ts`
-- [ ] 5.7: Crear `event-handlers/OnAnomalyDetected.ts` — escucha eventos crawling → crea Finding
-- [ ] 5.8: Crear `infrastructure/repositories/KyselyFindingRepository.ts`
-- [ ] 5.9: Migrar exporters existentes → `infrastructure/exporters/` (MarkdownExporter, JSONExporter)
-- [ ] 5.10: Crear `infrastructure/exporters/PlaywrightScriptExporter.ts` — genera test Playwright reproducible desde actionTrace
-- [ ] 5.11: Crear `infrastructure/http/FindingsController.ts` — routes para anomalies existentes + nuevas
-- [ ] 5.12: Migración Kysely: tabla findings con columnas status, browser, ai_enrichment_json
-- [ ] 5.13: Tests: Finding aggregate, CreateFinding, ListFindings con filtros
-- [ ] 5.14: Verificar build + commit: `fase(5): findings module complete`
+- [x] 5.1: Crear `domain/entities/Finding.ts` — AggregateRoot con severity, type, evidence, status, actionTrace
+- [x] 5.2: Crear value objects: `Severity.ts` (low/medium/high/critical), `FindingType.ts`, `Evidence.ts`, `FindingStatus.ts` (open/investigating/resolved/closed)
+- [x] 5.3: Crear events: `FindingCreated.ts`, `FindingResolved.ts`, `FindingEnriched.ts`
+- [x] 5.4: Crear ports: `IFindingRepository.ts`, `IAIEnricher.ts`
+- [x] 5.5: Crear commands: `CreateFindingCommand.ts`, `EnrichFindingCommand.ts`, `ResolveFindingCommand.ts`
+- [x] 5.6: Crear queries: `GetFindingQuery.ts`, `ListFindingsQuery.ts` (filtros: severity, type, session, status, search), `FindingStatsQuery.ts`
+- [x] 5.7: Crear `event-handlers/OnAnomalyDetected.ts` — escucha eventos crawling → crea Finding
+- [x] 5.8: Crear `infrastructure/repositories/KyselyFindingRepository.ts`
+- [x] 5.9: Migrar exporters existentes → `infrastructure/exporters/` (MarkdownExporter, JSONExporter)
+- [x] 5.10: Crear `infrastructure/exporters/PlaywrightScriptExporter.ts` — genera test Playwright reproducible desde actionTrace
+- [x] 5.11: Crear `infrastructure/http/FindingsController.ts` — routes para anomalies existentes + nuevas
+- [x] 5.12: Migración Kysely: tabla findings con columnas status, browser, ai_enrichment_json
+- [x] 5.13: Tests: Finding aggregate, CreateFinding, ListFindings con filtros
+- [x] 5.14: Verificar build + commit: `fase(5): findings module complete`
---
-## Phase 6: Fuzzing Module [PENDIENTE]
+## Phase 6: Fuzzing Module [COMPLETO]
Spec: `.ralph/specs/phase-06-fuzzing-module.md`
-- [ ] 6.1: Crear domain: `FuzzSession.ts` (AggregateRoot), `FuzzResult.ts` (Entity)
-- [ ] 6.2: Crear value objects: `FuzzStrategy.ts`, `FuzzPayload.ts`, `Seed.ts`, `FuzzIntensity.ts`
-- [ ] 6.3: Crear events: `FuzzStarted.ts`, `VulnerabilityDetected.ts`, `FuzzCompleted.ts`
-- [ ] 6.4: Crear port: `IFuzzerEngine.ts`
-- [ ] 6.5: Crear `commands/RunFuzzCommand.ts`
-- [ ] 6.6: Crear `event-handlers/OnActionExecuted.ts` — escucha crawling → trigger fuzzing
-- [ ] 6.7: Migrar las 5 estrategias existentes → `infrastructure/strategies/` (Empty, Oversized, SpecialChars, TypeMismatch, Boundary)
-- [ ] 6.8: Migrar `FuzzingEngine.ts` y `InputTypeDetector.ts` → `infrastructure/adapters/`
-- [ ] 6.9: Crear `infrastructure/http/FuzzingController.ts`
-- [ ] 6.10: Tests: cada estrategia de fuzzing genera payloads válidos
-- [ ] 6.11: Verificar build + commit: `fase(6): fuzzing module complete`
+- [x] 6.1: Crear domain: `FuzzSession.ts` (AggregateRoot), `FuzzResult.ts` (Entity)
+- [x] 6.2: Crear value objects: `FuzzStrategy.ts`, `FuzzPayload.ts`, `Seed.ts`, `FuzzIntensity.ts`
+- [x] 6.3: Crear events: `FuzzStarted.ts`, `VulnerabilityDetected.ts`, `FuzzCompleted.ts`
+- [x] 6.4: Crear port: `IFuzzerEngine.ts`
+- [x] 6.5: Crear `commands/RunFuzzCommand.ts`
+- [x] 6.6: Crear `event-handlers/OnActionExecuted.ts` — escucha crawling → trigger fuzzing
+- [x] 6.7: Migrar las 5 estrategias existentes → `infrastructure/strategies/` (Empty, Oversized, SpecialChars, TypeMismatch, Boundary)
+- [x] 6.8: Migrar `FuzzingEngine.ts` y `InputTypeDetector.ts` → `infrastructure/adapters/`
+- [x] 6.9: Crear `infrastructure/http/FuzzingController.ts`
+- [x] 6.10: Tests: cada estrategia de fuzzing genera payloads válidos
+- [x] 6.11: Verificar build + commit: `fase(6): fuzzing module complete`
---
diff --git a/.ralph/progress.json b/.ralph/progress.json
index 5463e49..9476d27 100644
--- a/.ralph/progress.json
+++ b/.ralph/progress.json
@@ -1,7 +1 @@
-{
- "status": "executing",
- "indicator": "⠹",
- "elapsed_seconds": 30,
- "last_output": "",
- "timestamp": "2026-03-05 03:53:30"
-}
+{"status": "failed", "timestamp": "2026-03-05 04:07:02"}
diff --git a/dist/modules/fuzzing/application/commands/RunFuzzCommand.js b/dist/modules/fuzzing/application/commands/RunFuzzCommand.js
new file mode 100644
index 0000000..f45a027
--- /dev/null
+++ b/dist/modules/fuzzing/application/commands/RunFuzzCommand.js
@@ -0,0 +1,39 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.RunFuzzCommand = void 0;
+const Result_1 = require("../../../../shared/domain/Result");
+const FuzzSession_1 = require("../../domain/entities/FuzzSession");
+class RunFuzzCommand {
+ constructor(fuzzerEngine, repository, eventBus) {
+ this.fuzzerEngine = fuzzerEngine;
+ this.repository = repository;
+ this.eventBus = eventBus;
+ }
+ async execute(request) {
+ const sessionResult = FuzzSession_1.FuzzSession.create({
+ crawlSessionId: request.crawlSessionId,
+ intensity: request.intensity,
+ seed: request.seed,
+ });
+ if (!sessionResult.ok) {
+ return (0, Result_1.Err)(sessionResult.error);
+ }
+ const session = sessionResult.value;
+ await this.repository.save(session);
+ const actions = this.fuzzerEngine.generateFuzzActions(request.state.domSnapshot, request.state);
+ for (const _action of actions) {
+ session.incrementActions();
+ }
+ session.complete();
+ await this.repository.update(session);
+ for (const event of session.domainEvents) {
+ await this.eventBus.publish(event);
+ }
+ session.clearEvents();
+ return (0, Result_1.Ok)({
+ fuzzSessionId: session.id.toString(),
+ actionsGenerated: actions.length,
+ });
+ }
+}
+exports.RunFuzzCommand = RunFuzzCommand;
diff --git a/dist/modules/fuzzing/application/event-handlers/OnActionExecuted.js b/dist/modules/fuzzing/application/event-handlers/OnActionExecuted.js
new file mode 100644
index 0000000..8adf64e
--- /dev/null
+++ b/dist/modules/fuzzing/application/event-handlers/OnActionExecuted.js
@@ -0,0 +1,26 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.OnActionExecuted = void 0;
+/**
+ * Listens for action_executed events from crawling module
+ * and triggers fuzzing on the resulting state's DOM.
+ */
+class OnActionExecuted {
+ constructor(runFuzz) {
+ this.runFuzz = runFuzz;
+ }
+ async handle(event) {
+ const payload = event.payload;
+ if (!payload.state || !payload.sessionId)
+ return;
+ if (!payload.state.domSnapshot)
+ return;
+ await this.runFuzz.execute({
+ crawlSessionId: payload.sessionId,
+ intensity: payload.intensity ?? 'low',
+ seed: payload.action?.seed ?? Date.now(),
+ state: payload.state,
+ });
+ }
+}
+exports.OnActionExecuted = OnActionExecuted;
diff --git a/dist/modules/fuzzing/domain/entities/FuzzResult.js b/dist/modules/fuzzing/domain/entities/FuzzResult.js
new file mode 100644
index 0000000..d430c84
--- /dev/null
+++ b/dist/modules/fuzzing/domain/entities/FuzzResult.js
@@ -0,0 +1,22 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.FuzzResult = void 0;
+const Entity_1 = require("../../../../shared/domain/Entity");
+class FuzzResult extends Entity_1.Entity {
+ static create(props, id) {
+ return new FuzzResult({ ...props, detectedAt: new Date() }, id);
+ }
+ static reconstitute(props, id) {
+ return new FuzzResult(props, id);
+ }
+ get sessionId() { return this.props.sessionId; }
+ get stateId() { return this.props.stateId; }
+ get selector() { return this.props.selector; }
+ get payload() { return this.props.payload; }
+ get strategy() { return this.props.strategy; }
+ get anomalyType() { return this.props.anomalyType; }
+ get severity() { return this.props.severity; }
+ get description() { return this.props.description; }
+ get detectedAt() { return this.props.detectedAt; }
+}
+exports.FuzzResult = FuzzResult;
diff --git a/dist/modules/fuzzing/domain/entities/FuzzSession.js b/dist/modules/fuzzing/domain/entities/FuzzSession.js
new file mode 100644
index 0000000..1ab540e
--- /dev/null
+++ b/dist/modules/fuzzing/domain/entities/FuzzSession.js
@@ -0,0 +1,96 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.FuzzSession = void 0;
+const AggregateRoot_1 = require("../../../../shared/domain/AggregateRoot");
+const Result_1 = require("../../../../shared/domain/Result");
+const FuzzIntensity_1 = require("../value-objects/FuzzIntensity");
+const Seed_1 = require("../value-objects/Seed");
+const FuzzStarted_1 = require("../events/FuzzStarted");
+const FuzzCompleted_1 = require("../events/FuzzCompleted");
+const VulnerabilityDetected_1 = require("../events/VulnerabilityDetected");
+class FuzzSession extends AggregateRoot_1.AggregateRoot {
+ constructor(props, id) {
+ super(props, id);
+ }
+ static reconstitute(props, id) {
+ return new FuzzSession(props, id);
+ }
+ static create(request) {
+ let intensity;
+ try {
+ intensity = FuzzIntensity_1.FuzzIntensity.fromString(request.intensity);
+ }
+ catch (e) {
+ return (0, Result_1.Err)(e.message);
+ }
+ let seed;
+ try {
+ seed = Seed_1.Seed.create(request.seed);
+ }
+ catch (e) {
+ return (0, Result_1.Err)(e.message);
+ }
+ const props = {
+ crawlSessionId: request.crawlSessionId,
+ intensity,
+ seed,
+ status: 'running',
+ actionsExecuted: 0,
+ vulnerabilitiesFound: 0,
+ startedAt: new Date(),
+ };
+ const session = new FuzzSession(props);
+ session.addDomainEvent(new FuzzStarted_1.FuzzStarted(session.id.toString(), {
+ crawlSessionId: request.crawlSessionId,
+ intensity: request.intensity,
+ seed: request.seed,
+ }));
+ return (0, Result_1.Ok)(session);
+ }
+ get crawlSessionId() { return this.props.crawlSessionId; }
+ get intensity() { return this.props.intensity; }
+ get seed() { return this.props.seed; }
+ get status() { return this.props.status; }
+ get actionsExecuted() { return this.props.actionsExecuted; }
+ get vulnerabilitiesFound() { return this.props.vulnerabilitiesFound; }
+ get startedAt() { return this.props.startedAt; }
+ get completedAt() { return this.props.completedAt; }
+ recordVulnerability(result) {
+ this.props = {
+ ...this.props,
+ actionsExecuted: this.props.actionsExecuted + 1,
+ vulnerabilitiesFound: this.props.vulnerabilitiesFound + 1,
+ };
+ this.addDomainEvent(new VulnerabilityDetected_1.VulnerabilityDetected(this.id.toString(), {
+ crawlSessionId: this.props.crawlSessionId,
+ stateId: result.stateId,
+ anomalyType: result.anomalyType,
+ severity: result.severity,
+ selector: result.selector,
+ payload: result.payload,
+ strategy: result.strategy,
+ }));
+ }
+ incrementActions() {
+ this.props = { ...this.props, actionsExecuted: this.props.actionsExecuted + 1 };
+ }
+ complete() {
+ this.props = { ...this.props, status: 'completed', completedAt: new Date() };
+ this.addDomainEvent(new FuzzCompleted_1.FuzzCompleted(this.id.toString(), {
+ crawlSessionId: this.props.crawlSessionId,
+ actionsExecuted: this.props.actionsExecuted,
+ vulnerabilitiesFound: this.props.vulnerabilitiesFound,
+ }));
+ }
+ fail(reason) {
+ this.props = { ...this.props, status: 'failed', completedAt: new Date() };
+ this.addDomainEvent(new FuzzCompleted_1.FuzzCompleted(this.id.toString(), {
+ crawlSessionId: this.props.crawlSessionId,
+ actionsExecuted: this.props.actionsExecuted,
+ vulnerabilitiesFound: this.props.vulnerabilitiesFound,
+ failed: true,
+ reason,
+ }));
+ }
+}
+exports.FuzzSession = FuzzSession;
diff --git a/dist/modules/fuzzing/domain/events/FuzzCompleted.js b/dist/modules/fuzzing/domain/events/FuzzCompleted.js
new file mode 100644
index 0000000..e56a2ea
--- /dev/null
+++ b/dist/modules/fuzzing/domain/events/FuzzCompleted.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.FuzzCompleted = void 0;
+const crypto_1 = require("crypto");
+class FuzzCompleted {
+ constructor(aggregateId, payload) {
+ this.aggregateId = aggregateId;
+ this.payload = payload;
+ this.eventId = (0, crypto_1.randomUUID)();
+ this.eventName = 'fuzz.completed';
+ this.occurredOn = new Date();
+ }
+}
+exports.FuzzCompleted = FuzzCompleted;
diff --git a/dist/modules/fuzzing/domain/events/FuzzStarted.js b/dist/modules/fuzzing/domain/events/FuzzStarted.js
new file mode 100644
index 0000000..24fbdff
--- /dev/null
+++ b/dist/modules/fuzzing/domain/events/FuzzStarted.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.FuzzStarted = void 0;
+const crypto_1 = require("crypto");
+class FuzzStarted {
+ constructor(aggregateId, payload) {
+ this.aggregateId = aggregateId;
+ this.payload = payload;
+ this.eventId = (0, crypto_1.randomUUID)();
+ this.eventName = 'fuzz.started';
+ this.occurredOn = new Date();
+ }
+}
+exports.FuzzStarted = FuzzStarted;
diff --git a/dist/modules/fuzzing/domain/events/VulnerabilityDetected.js b/dist/modules/fuzzing/domain/events/VulnerabilityDetected.js
new file mode 100644
index 0000000..921120d
--- /dev/null
+++ b/dist/modules/fuzzing/domain/events/VulnerabilityDetected.js
@@ -0,0 +1,14 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.VulnerabilityDetected = void 0;
+const crypto_1 = require("crypto");
+class VulnerabilityDetected {
+ constructor(aggregateId, payload) {
+ this.aggregateId = aggregateId;
+ this.payload = payload;
+ this.eventId = (0, crypto_1.randomUUID)();
+ this.eventName = 'fuzz.vulnerability_detected';
+ this.occurredOn = new Date();
+ }
+}
+exports.VulnerabilityDetected = VulnerabilityDetected;
diff --git a/dist/modules/fuzzing/domain/ports/IFuzzerEngine.js b/dist/modules/fuzzing/domain/ports/IFuzzerEngine.js
new file mode 100644
index 0000000..c8ad2e5
--- /dev/null
+++ b/dist/modules/fuzzing/domain/ports/IFuzzerEngine.js
@@ -0,0 +1,2 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
diff --git a/dist/modules/fuzzing/domain/value-objects/FuzzIntensity.js b/dist/modules/fuzzing/domain/value-objects/FuzzIntensity.js
new file mode 100644
index 0000000..f1ed031
--- /dev/null
+++ b/dist/modules/fuzzing/domain/value-objects/FuzzIntensity.js
@@ -0,0 +1,18 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.FuzzIntensity = void 0;
+const ValueObject_1 = require("../../../../shared/domain/ValueObject");
+class FuzzIntensity extends ValueObject_1.ValueObject {
+ static low() { return new FuzzIntensity({ value: 'low' }); }
+ static medium() { return new FuzzIntensity({ value: 'medium' }); }
+ static high() { return new FuzzIntensity({ value: 'high' }); }
+ static fromString(s) {
+ if (!FuzzIntensity.LEVELS.includes(s)) {
+ throw new Error(`Invalid intensity: ${s}. Must be one of: ${FuzzIntensity.LEVELS.join(', ')}`);
+ }
+ return new FuzzIntensity({ value: s });
+ }
+ get value() { return this.props.value; }
+}
+exports.FuzzIntensity = FuzzIntensity;
+FuzzIntensity.LEVELS = ['low', 'medium', 'high'];
diff --git a/dist/modules/fuzzing/domain/value-objects/FuzzPayload.js b/dist/modules/fuzzing/domain/value-objects/FuzzPayload.js
new file mode 100644
index 0000000..38ab71e
--- /dev/null
+++ b/dist/modules/fuzzing/domain/value-objects/FuzzPayload.js
@@ -0,0 +1,12 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.FuzzPayload = void 0;
+const ValueObject_1 = require("../../../../shared/domain/ValueObject");
+class FuzzPayload extends ValueObject_1.ValueObject {
+ static create(value, strategy) {
+ return new FuzzPayload({ value, strategy });
+ }
+ get value() { return this.props.value; }
+ get strategy() { return this.props.strategy; }
+}
+exports.FuzzPayload = FuzzPayload;
diff --git a/dist/modules/fuzzing/domain/value-objects/FuzzStrategy.js b/dist/modules/fuzzing/domain/value-objects/FuzzStrategy.js
new file mode 100644
index 0000000..90bd7ee
--- /dev/null
+++ b/dist/modules/fuzzing/domain/value-objects/FuzzStrategy.js
@@ -0,0 +1,20 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.FuzzStrategy = void 0;
+const ValueObject_1 = require("../../../../shared/domain/ValueObject");
+class FuzzStrategy extends ValueObject_1.ValueObject {
+ static empty() { return new FuzzStrategy({ value: 'empty' }); }
+ static oversized() { return new FuzzStrategy({ value: 'oversized' }); }
+ static specialChars() { return new FuzzStrategy({ value: 'special_chars' }); }
+ static typeMismatch() { return new FuzzStrategy({ value: 'type_mismatch' }); }
+ static boundary() { return new FuzzStrategy({ value: 'boundary' }); }
+ static fromString(s) {
+ if (!FuzzStrategy.ALL.includes(s)) {
+ throw new Error(`Invalid fuzz strategy: ${s}`);
+ }
+ return new FuzzStrategy({ value: s });
+ }
+ get value() { return this.props.value; }
+}
+exports.FuzzStrategy = FuzzStrategy;
+FuzzStrategy.ALL = ['empty', 'oversized', 'special_chars', 'type_mismatch', 'boundary'];
diff --git a/dist/modules/fuzzing/domain/value-objects/Seed.js b/dist/modules/fuzzing/domain/value-objects/Seed.js
new file mode 100644
index 0000000..2c5ed71
--- /dev/null
+++ b/dist/modules/fuzzing/domain/value-objects/Seed.js
@@ -0,0 +1,17 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.Seed = void 0;
+const ValueObject_1 = require("../../../../shared/domain/ValueObject");
+class Seed extends ValueObject_1.ValueObject {
+ static create(value) {
+ if (!Number.isInteger(value) || value < 0) {
+ throw new Error(`Seed must be a non-negative integer, got: ${value}`);
+ }
+ return new Seed({ value });
+ }
+ static fromTimestamp() {
+ return new Seed({ value: Date.now() });
+ }
+ get value() { return this.props.value; }
+}
+exports.Seed = Seed;
diff --git a/dist/modules/fuzzing/index.js b/dist/modules/fuzzing/index.js
new file mode 100644
index 0000000..87bfa3e
--- /dev/null
+++ b/dist/modules/fuzzing/index.js
@@ -0,0 +1,26 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.createFuzzingRouter = exports.FuzzingEngineAdapter = exports.OnActionExecuted = exports.RunFuzzCommand = exports.Seed = exports.FuzzPayload = exports.FuzzStrategy = exports.FuzzIntensity = exports.FuzzResult = exports.FuzzSession = void 0;
+// Domain
+var FuzzSession_1 = require("./domain/entities/FuzzSession");
+Object.defineProperty(exports, "FuzzSession", { enumerable: true, get: function () { return FuzzSession_1.FuzzSession; } });
+var FuzzResult_1 = require("./domain/entities/FuzzResult");
+Object.defineProperty(exports, "FuzzResult", { enumerable: true, get: function () { return FuzzResult_1.FuzzResult; } });
+var FuzzIntensity_1 = require("./domain/value-objects/FuzzIntensity");
+Object.defineProperty(exports, "FuzzIntensity", { enumerable: true, get: function () { return FuzzIntensity_1.FuzzIntensity; } });
+var FuzzStrategy_1 = require("./domain/value-objects/FuzzStrategy");
+Object.defineProperty(exports, "FuzzStrategy", { enumerable: true, get: function () { return FuzzStrategy_1.FuzzStrategy; } });
+var FuzzPayload_1 = require("./domain/value-objects/FuzzPayload");
+Object.defineProperty(exports, "FuzzPayload", { enumerable: true, get: function () { return FuzzPayload_1.FuzzPayload; } });
+var Seed_1 = require("./domain/value-objects/Seed");
+Object.defineProperty(exports, "Seed", { enumerable: true, get: function () { return Seed_1.Seed; } });
+// Application
+var RunFuzzCommand_1 = require("./application/commands/RunFuzzCommand");
+Object.defineProperty(exports, "RunFuzzCommand", { enumerable: true, get: function () { return RunFuzzCommand_1.RunFuzzCommand; } });
+var OnActionExecuted_1 = require("./application/event-handlers/OnActionExecuted");
+Object.defineProperty(exports, "OnActionExecuted", { enumerable: true, get: function () { return OnActionExecuted_1.OnActionExecuted; } });
+// Infrastructure
+var FuzzingEngineAdapter_1 = require("./infrastructure/adapters/FuzzingEngineAdapter");
+Object.defineProperty(exports, "FuzzingEngineAdapter", { enumerable: true, get: function () { return FuzzingEngineAdapter_1.FuzzingEngineAdapter; } });
+var FuzzingController_1 = require("./infrastructure/http/FuzzingController");
+Object.defineProperty(exports, "createFuzzingRouter", { enumerable: true, get: function () { return FuzzingController_1.createFuzzingRouter; } });
diff --git a/dist/modules/fuzzing/infrastructure/adapters/FuzzingEngineAdapter.js b/dist/modules/fuzzing/infrastructure/adapters/FuzzingEngineAdapter.js
new file mode 100644
index 0000000..f319c9f
--- /dev/null
+++ b/dist/modules/fuzzing/infrastructure/adapters/FuzzingEngineAdapter.js
@@ -0,0 +1,127 @@
+"use strict";
+/**
+ * FuzzingEngineAdapter — implements IFuzzerEngine port using the 5 fuzzing strategies.
+ * Adapts the legacy FuzzingEngine logic to the hexagonal architecture.
+ */
+var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ var desc = Object.getOwnPropertyDescriptor(m, k);
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
+ desc = { enumerable: true, get: function() { return m[k]; } };
+ }
+ Object.defineProperty(o, k2, desc);
+}) : (function(o, m, k, k2) {
+ if (k2 === undefined) k2 = k;
+ o[k2] = m[k];
+}));
+var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
+}) : function(o, v) {
+ o["default"] = v;
+});
+var __importStar = (this && this.__importStar) || (function () {
+ var ownKeys = function(o) {
+ ownKeys = Object.getOwnPropertyNames || function (o) {
+ var ar = [];
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
+ return ar;
+ };
+ return ownKeys(o);
+ };
+ return function (mod) {
+ if (mod && mod.__esModule) return mod;
+ var result = {};
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
+ __setModuleDefault(result, mod);
+ return result;
+ };
+})();
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.FuzzingEngineAdapter = void 0;
+const crypto = __importStar(require("crypto"));
+const InputTypeDetector_1 = require("./InputTypeDetector");
+const EmptyValueStrategy_1 = require("../strategies/EmptyValueStrategy");
+const OversizedStringStrategy_1 = require("../strategies/OversizedStringStrategy");
+const SpecialCharsStrategy_1 = require("../strategies/SpecialCharsStrategy");
+const TypeMismatchStrategy_1 = require("../strategies/TypeMismatchStrategy");
+const BoundaryValueStrategy_1 = require("../strategies/BoundaryValueStrategy");
+const INPUT_RE = /<(input|textarea|select)[^>]*>/gi;
+const ATTR_RE = (name) => new RegExp(`${name}="([^"]*)"`, 'i');
+function extractFields(domSnapshot) {
+ const fields = [];
+ let match;
+ while ((match = INPUT_RE.exec(domSnapshot)) !== null) {
+ const tag = match[0] ?? '';
+ const tagName = match[1] ?? 'input';
+ const idMatch = ATTR_RE('id').exec(tag);
+ const nameMatch = ATTR_RE('name').exec(tag);
+ const typeMatch = ATTR_RE('type').exec(tag);
+ const placeholderMatch = ATTR_RE('placeholder').exec(tag);
+ const ariaMatch = ATTR_RE('aria-label').exec(tag);
+ const selector = idMatch?.[1]
+ ? `#${idMatch[1]}`
+ : nameMatch?.[1]
+ ? `[name="${nameMatch[1]}"]`
+ : tagName;
+ fields.push({
+ selector,
+ tagName,
+ inputType: typeMatch?.[1],
+ name: nameMatch?.[1],
+ placeholder: placeholderMatch?.[1],
+ ariaLabel: ariaMatch?.[1],
+ });
+ }
+ return fields;
+}
+class FuzzingEngineAdapter {
+ constructor(config) {
+ this.intensity = config.intensity;
+ this.seed = config.seed;
+ }
+ generateFuzzActions(domSnapshot, state) {
+ const fields = extractFields(domSnapshot);
+ const actions = [];
+ const now = Date.now();
+ const strategies = this.selectStrategies();
+ for (const field of fields) {
+ const detectedType = (0, InputTypeDetector_1.detectInputType)({
+ tagName: field.tagName,
+ inputType: field.inputType,
+ name: field.name,
+ placeholder: field.placeholder,
+ ariaLabel: field.ariaLabel,
+ });
+ for (const strategy of strategies) {
+ if (!strategy.appliesTo(detectedType))
+ continue;
+ const values = strategy.values(detectedType);
+ for (const value of values) {
+ actions.push({
+ id: crypto.randomUUID(),
+ type: 'fill',
+ selector: field.selector,
+ value,
+ timestamp: now,
+ seed: this.seed,
+ stateId: state.id,
+ });
+ }
+ }
+ }
+ return actions;
+ }
+ selectStrategies() {
+ const empty = new EmptyValueStrategy_1.EmptyValueStrategy();
+ const typeMismatch = new TypeMismatchStrategy_1.TypeMismatchStrategy();
+ const oversized = new OversizedStringStrategy_1.OversizedStringStrategy(this.intensity);
+ const boundary = new BoundaryValueStrategy_1.BoundaryValueStrategy();
+ const special = new SpecialCharsStrategy_1.SpecialCharsStrategy();
+ switch (this.intensity) {
+ case 'low': return [empty, typeMismatch];
+ case 'medium': return [empty, typeMismatch, oversized, boundary];
+ case 'high': return [empty, typeMismatch, oversized, boundary, special];
+ }
+ }
+}
+exports.FuzzingEngineAdapter = FuzzingEngineAdapter;
diff --git a/dist/modules/fuzzing/infrastructure/adapters/InputTypeDetector.js b/dist/modules/fuzzing/infrastructure/adapters/InputTypeDetector.js
new file mode 100644
index 0000000..5c0a208
--- /dev/null
+++ b/dist/modules/fuzzing/infrastructure/adapters/InputTypeDetector.js
@@ -0,0 +1,47 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.detectInputType = detectInputType;
+function detectInputType(attrs) {
+ const tag = (attrs.tagName ?? '').toLowerCase();
+ if (tag === 'textarea')
+ return 'textarea';
+ if (tag === 'select')
+ return 'select';
+ const inputType = (attrs.inputType ?? '').toLowerCase();
+ if (inputType === 'email')
+ return 'email';
+ if (inputType === 'password')
+ return 'password';
+ if (inputType === 'number')
+ return 'number';
+ if (inputType === 'date')
+ return 'date';
+ if (inputType === 'tel')
+ return 'phone';
+ if (inputType === 'url')
+ return 'url';
+ if (inputType === 'search')
+ return 'search';
+ if (inputType === 'file')
+ return 'file';
+ const hints = [
+ (attrs.name ?? '').toLowerCase(),
+ (attrs.placeholder ?? '').toLowerCase(),
+ (attrs.ariaLabel ?? '').toLowerCase(),
+ ].join(' ');
+ if (/email/.test(hints))
+ return 'email';
+ if (/password|pass/.test(hints))
+ return 'password';
+ if (/phone|tel|mobile/.test(hints))
+ return 'phone';
+ if (/date|birth|dob/.test(hints))
+ return 'date';
+ if (/number|qty|quantity|age/.test(hints))
+ return 'number';
+ if (/search/.test(hints))
+ return 'search';
+ if (/url|website|link/.test(hints))
+ return 'url';
+ return 'text';
+}
diff --git a/dist/modules/fuzzing/infrastructure/http/FuzzingController.js b/dist/modules/fuzzing/infrastructure/http/FuzzingController.js
new file mode 100644
index 0000000..7aeb2a7
--- /dev/null
+++ b/dist/modules/fuzzing/infrastructure/http/FuzzingController.js
@@ -0,0 +1,50 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.createFuzzingRouter = createFuzzingRouter;
+const express_1 = require("express");
+function createFuzzingRouter(deps) {
+ const router = (0, express_1.Router)();
+ // POST /api/fuzz/run — trigger fuzzing for a given state
+ router.post('/run', async (req, res) => {
+ const { crawlSessionId, intensity, seed, state } = req.body;
+ if (!crawlSessionId || !state) {
+ res.status(400).json({ error: 'crawlSessionId and state are required' });
+ return;
+ }
+ const result = await deps.runFuzz.execute({
+ crawlSessionId,
+ intensity: intensity ?? 'low',
+ seed: seed ?? Date.now(),
+ state,
+ });
+ if (!result.ok) {
+ res.status(422).json({ error: result.error });
+ return;
+ }
+ res.status(201).json(result.value);
+ });
+ // GET /api/fuzz/sessions/:id — get fuzz session
+ router.get('/sessions/:id', async (req, res) => {
+ const sessionId = req.params['id'];
+ const session = await deps.repository.findById(sessionId);
+ if (!session) {
+ res.status(404).json({ error: 'Fuzz session not found' });
+ return;
+ }
+ res.json(toDTO(session));
+ });
+ return router;
+}
+function toDTO(s) {
+ return {
+ id: s.id.toString(),
+ crawlSessionId: s.crawlSessionId,
+ intensity: s.intensity.value,
+ seed: s.seed.value,
+ status: s.status,
+ actionsExecuted: s.actionsExecuted,
+ vulnerabilitiesFound: s.vulnerabilitiesFound,
+ startedAt: s.startedAt.toISOString(),
+ completedAt: s.completedAt?.toISOString() ?? null,
+ };
+}
diff --git a/dist/modules/fuzzing/infrastructure/strategies/BoundaryValueStrategy.js b/dist/modules/fuzzing/infrastructure/strategies/BoundaryValueStrategy.js
new file mode 100644
index 0000000..395c869
--- /dev/null
+++ b/dist/modules/fuzzing/infrastructure/strategies/BoundaryValueStrategy.js
@@ -0,0 +1,19 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.BoundaryValueStrategy = void 0;
+class BoundaryValueStrategy {
+ constructor() {
+ this.name = 'BoundaryValueStrategy';
+ }
+ appliesTo(type) {
+ return type === 'number' || type === 'date';
+ }
+ values(type) {
+ switch (type) {
+ case 'number': return ['0', '-1', '2147483647', '2147483648', '-2147483648'];
+ case 'date': return ['1900-01-01', '2099-12-31', '1970-01-01'];
+ default: return [];
+ }
+ }
+}
+exports.BoundaryValueStrategy = BoundaryValueStrategy;
diff --git a/dist/modules/fuzzing/infrastructure/strategies/EmptyValueStrategy.js b/dist/modules/fuzzing/infrastructure/strategies/EmptyValueStrategy.js
new file mode 100644
index 0000000..cb536df
--- /dev/null
+++ b/dist/modules/fuzzing/infrastructure/strategies/EmptyValueStrategy.js
@@ -0,0 +1,15 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.EmptyValueStrategy = void 0;
+class EmptyValueStrategy {
+ constructor() {
+ this.name = 'EmptyValueStrategy';
+ }
+ appliesTo(_type) {
+ return true;
+ }
+ values() {
+ return ['', ' ', '\t'];
+ }
+}
+exports.EmptyValueStrategy = EmptyValueStrategy;
diff --git a/dist/modules/fuzzing/infrastructure/strategies/OversizedStringStrategy.js b/dist/modules/fuzzing/infrastructure/strategies/OversizedStringStrategy.js
new file mode 100644
index 0000000..c72f54b
--- /dev/null
+++ b/dist/modules/fuzzing/infrastructure/strategies/OversizedStringStrategy.js
@@ -0,0 +1,21 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.OversizedStringStrategy = void 0;
+const APPLICABLE_TYPES = ['text', 'email', 'password', 'textarea'];
+class OversizedStringStrategy {
+ constructor(intensity) {
+ this.intensity = intensity;
+ this.name = 'OversizedStringStrategy';
+ }
+ appliesTo(type) {
+ return APPLICABLE_TYPES.includes(type);
+ }
+ values() {
+ switch (this.intensity) {
+ case 'low': return ['A'.repeat(256)];
+ case 'medium': return ['A'.repeat(1024)];
+ case 'high': return ['A'.repeat(10000) + '日本語テスト𠮷野家'];
+ }
+ }
+}
+exports.OversizedStringStrategy = OversizedStringStrategy;
diff --git a/dist/modules/fuzzing/infrastructure/strategies/SpecialCharsStrategy.js b/dist/modules/fuzzing/infrastructure/strategies/SpecialCharsStrategy.js
new file mode 100644
index 0000000..9941d72
--- /dev/null
+++ b/dist/modules/fuzzing/infrastructure/strategies/SpecialCharsStrategy.js
@@ -0,0 +1,22 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.SpecialCharsStrategy = void 0;
+const APPLICABLE_TYPES = ['text', 'email', 'search', 'textarea'];
+class SpecialCharsStrategy {
+ constructor() {
+ this.name = 'SpecialCharsStrategy';
+ }
+ appliesTo(type) {
+ return APPLICABLE_TYPES.includes(type);
+ }
+ values() {
+ return [
+ "' OR 1=1 --",
+ '',
+ '../../etc/passwd',
+ '${7*7}',
+ '\x00\x01\x02',
+ ];
+ }
+}
+exports.SpecialCharsStrategy = SpecialCharsStrategy;
diff --git a/dist/modules/fuzzing/infrastructure/strategies/TypeMismatchStrategy.js b/dist/modules/fuzzing/infrastructure/strategies/TypeMismatchStrategy.js
new file mode 100644
index 0000000..f098ac3
--- /dev/null
+++ b/dist/modules/fuzzing/infrastructure/strategies/TypeMismatchStrategy.js
@@ -0,0 +1,22 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", { value: true });
+exports.TypeMismatchStrategy = void 0;
+class TypeMismatchStrategy {
+ constructor() {
+ this.name = 'TypeMismatchStrategy';
+ }
+ appliesTo(type) {
+ return ['email', 'number', 'date', 'url', 'phone'].includes(type);
+ }
+ values(type) {
+ switch (type) {
+ case 'email': return ['not-an-email', '12345', '@@@'];
+ case 'number': return ['abc', '-999999', '9.9.9', 'NaN'];
+ case 'date': return ['yesterday', '32/13/2025', '0000-00-00'];
+ case 'url': return ['javascript:alert(1)', 'not a url'];
+ case 'phone': return ['000', '++++', 'abcdefghij'];
+ default: return [];
+ }
+ }
+}
+exports.TypeMismatchStrategy = TypeMismatchStrategy;
diff --git a/src/modules/fuzzing/application/commands/RunFuzzCommand.ts b/src/modules/fuzzing/application/commands/RunFuzzCommand.ts
new file mode 100644
index 0000000..b96e331
--- /dev/null
+++ b/src/modules/fuzzing/application/commands/RunFuzzCommand.ts
@@ -0,0 +1,66 @@
+import { UseCase } from '../../../../shared/application/UseCase';
+import { EventBus } from '../../../../shared/application/EventBus';
+import { Result, Ok, Err } from '../../../../shared/domain/Result';
+import { FuzzSession } from '../../domain/entities/FuzzSession';
+import { IFuzzerEngine } from '../../domain/ports/IFuzzerEngine';
+import { IState } from '../../../../core/interfaces';
+
+export interface IFuzzSessionRepository {
+ save(session: FuzzSession): Promise;
+ findById(id: string): Promise;
+ update(session: FuzzSession): Promise;
+}
+
+interface RunFuzzRequest {
+ crawlSessionId: string;
+ intensity: 'low' | 'medium' | 'high';
+ seed: number;
+ state: IState;
+}
+
+interface RunFuzzResponse {
+ fuzzSessionId: string;
+ actionsGenerated: number;
+}
+
+export class RunFuzzCommand implements UseCase {
+ constructor(
+ private readonly fuzzerEngine: IFuzzerEngine,
+ private readonly repository: IFuzzSessionRepository,
+ private readonly eventBus: EventBus
+ ) {}
+
+ async execute(request: RunFuzzRequest): Promise> {
+ const sessionResult = FuzzSession.create({
+ crawlSessionId: request.crawlSessionId,
+ intensity: request.intensity,
+ seed: request.seed,
+ });
+
+ if (!sessionResult.ok) {
+ return Err(sessionResult.error);
+ }
+
+ const session = sessionResult.value;
+ await this.repository.save(session);
+
+ const actions = this.fuzzerEngine.generateFuzzActions(request.state.domSnapshot, request.state);
+
+ for (const _action of actions) {
+ session.incrementActions();
+ }
+
+ session.complete();
+ await this.repository.update(session);
+
+ for (const event of session.domainEvents) {
+ await this.eventBus.publish(event);
+ }
+ session.clearEvents();
+
+ return Ok({
+ fuzzSessionId: session.id.toString(),
+ actionsGenerated: actions.length,
+ });
+ }
+}
diff --git a/src/modules/fuzzing/application/event-handlers/OnActionExecuted.ts b/src/modules/fuzzing/application/event-handlers/OnActionExecuted.ts
new file mode 100644
index 0000000..3486d8b
--- /dev/null
+++ b/src/modules/fuzzing/application/event-handlers/OnActionExecuted.ts
@@ -0,0 +1,31 @@
+import { EventHandler } from '../../../../shared/application/EventHandler';
+import { DomainEvent } from '../../../../shared/domain/DomainEvent';
+import { RunFuzzCommand } from '../commands/RunFuzzCommand';
+import { IAction, IState } from '../../../../core/interfaces';
+
+/**
+ * Listens for action_executed events from crawling module
+ * and triggers fuzzing on the resulting state's DOM.
+ */
+export class OnActionExecuted implements EventHandler {
+ constructor(private readonly runFuzz: RunFuzzCommand) {}
+
+ async handle(event: DomainEvent): Promise {
+ const payload = event.payload as {
+ action?: IAction;
+ state?: IState;
+ sessionId?: string;
+ intensity?: 'low' | 'medium' | 'high';
+ };
+
+ if (!payload.state || !payload.sessionId) return;
+ if (!payload.state.domSnapshot) return;
+
+ await this.runFuzz.execute({
+ crawlSessionId: payload.sessionId,
+ intensity: payload.intensity ?? 'low',
+ seed: payload.action?.seed ?? Date.now(),
+ state: payload.state,
+ });
+ }
+}
diff --git a/src/modules/fuzzing/domain/entities/FuzzResult.ts b/src/modules/fuzzing/domain/entities/FuzzResult.ts
new file mode 100644
index 0000000..e7ee602
--- /dev/null
+++ b/src/modules/fuzzing/domain/entities/FuzzResult.ts
@@ -0,0 +1,34 @@
+import { Entity } from '../../../../shared/domain/Entity';
+import { UniqueId } from '../../../../shared/domain/UniqueId';
+
+export interface FuzzResultProps {
+ sessionId: string;
+ stateId: string;
+ selector: string;
+ payload: string;
+ strategy: string;
+ anomalyType: string;
+ severity: 'low' | 'medium' | 'high' | 'critical';
+ description: string;
+ detectedAt: Date;
+}
+
+export class FuzzResult extends Entity {
+ static create(props: Omit, id?: UniqueId): FuzzResult {
+ return new FuzzResult({ ...props, detectedAt: new Date() }, id);
+ }
+
+ static reconstitute(props: FuzzResultProps, id: UniqueId): FuzzResult {
+ return new FuzzResult(props, id);
+ }
+
+ get sessionId(): string { return this.props.sessionId; }
+ get stateId(): string { return this.props.stateId; }
+ get selector(): string { return this.props.selector; }
+ get payload(): string { return this.props.payload; }
+ get strategy(): string { return this.props.strategy; }
+ get anomalyType(): string { return this.props.anomalyType; }
+ get severity(): 'low' | 'medium' | 'high' | 'critical' { return this.props.severity; }
+ get description(): string { return this.props.description; }
+ get detectedAt(): Date { return this.props.detectedAt; }
+}
diff --git a/src/modules/fuzzing/domain/entities/FuzzSession.ts b/src/modules/fuzzing/domain/entities/FuzzSession.ts
new file mode 100644
index 0000000..4c79eee
--- /dev/null
+++ b/src/modules/fuzzing/domain/entities/FuzzSession.ts
@@ -0,0 +1,131 @@
+import { AggregateRoot } from '../../../../shared/domain/AggregateRoot';
+import { UniqueId } from '../../../../shared/domain/UniqueId';
+import { Result, Ok, Err } from '../../../../shared/domain/Result';
+import { FuzzIntensity } from '../value-objects/FuzzIntensity';
+import { Seed } from '../value-objects/Seed';
+import { FuzzStarted } from '../events/FuzzStarted';
+import { FuzzCompleted } from '../events/FuzzCompleted';
+import { VulnerabilityDetected } from '../events/VulnerabilityDetected';
+import { FuzzResult } from './FuzzResult';
+
+type FuzzSessionStatus = 'running' | 'completed' | 'failed';
+
+export interface FuzzSessionProps {
+ crawlSessionId: string;
+ intensity: FuzzIntensity;
+ seed: Seed;
+ status: FuzzSessionStatus;
+ actionsExecuted: number;
+ vulnerabilitiesFound: number;
+ startedAt: Date;
+ completedAt?: Date;
+}
+
+export interface CreateFuzzSessionRequest {
+ crawlSessionId: string;
+ intensity: 'low' | 'medium' | 'high';
+ seed: number;
+}
+
+export class FuzzSession extends AggregateRoot {
+ private constructor(props: FuzzSessionProps, id?: UniqueId) {
+ super(props, id);
+ }
+
+ static reconstitute(props: FuzzSessionProps, id: UniqueId): FuzzSession {
+ return new FuzzSession(props, id);
+ }
+
+ static create(request: CreateFuzzSessionRequest): Result {
+ let intensity: FuzzIntensity;
+ try {
+ intensity = FuzzIntensity.fromString(request.intensity);
+ } catch (e) {
+ return Err((e as Error).message);
+ }
+
+ let seed: Seed;
+ try {
+ seed = Seed.create(request.seed);
+ } catch (e) {
+ return Err((e as Error).message);
+ }
+
+ const props: FuzzSessionProps = {
+ crawlSessionId: request.crawlSessionId,
+ intensity,
+ seed,
+ status: 'running',
+ actionsExecuted: 0,
+ vulnerabilitiesFound: 0,
+ startedAt: new Date(),
+ };
+
+ const session = new FuzzSession(props);
+ session.addDomainEvent(
+ new FuzzStarted(session.id.toString(), {
+ crawlSessionId: request.crawlSessionId,
+ intensity: request.intensity,
+ seed: request.seed,
+ })
+ );
+
+ return Ok(session);
+ }
+
+ get crawlSessionId(): string { return this.props.crawlSessionId; }
+ get intensity(): FuzzIntensity { return this.props.intensity; }
+ get seed(): Seed { return this.props.seed; }
+ get status(): FuzzSessionStatus { return this.props.status; }
+ get actionsExecuted(): number { return this.props.actionsExecuted; }
+ get vulnerabilitiesFound(): number { return this.props.vulnerabilitiesFound; }
+ get startedAt(): Date { return this.props.startedAt; }
+ get completedAt(): Date | undefined { return this.props.completedAt; }
+
+ recordVulnerability(result: FuzzResult): void {
+ this.props = {
+ ...this.props,
+ actionsExecuted: this.props.actionsExecuted + 1,
+ vulnerabilitiesFound: this.props.vulnerabilitiesFound + 1,
+ };
+ this.addDomainEvent(
+ new VulnerabilityDetected(this.id.toString(), {
+ crawlSessionId: this.props.crawlSessionId,
+ stateId: result.stateId,
+ anomalyType: result.anomalyType,
+ severity: result.severity,
+ selector: result.selector,
+ payload: result.payload,
+ strategy: result.strategy,
+ })
+ );
+ }
+
+ incrementActions(): void {
+ this.props = { ...this.props, actionsExecuted: this.props.actionsExecuted + 1 };
+ }
+
+ complete(): void {
+ this.props = { ...this.props, status: 'completed', completedAt: new Date() };
+ this.addDomainEvent(
+ new FuzzCompleted(this.id.toString(), {
+ crawlSessionId: this.props.crawlSessionId,
+ actionsExecuted: this.props.actionsExecuted,
+ vulnerabilitiesFound: this.props.vulnerabilitiesFound,
+ })
+ );
+ }
+
+ fail(reason: string): void {
+ this.props = { ...this.props, status: 'failed', completedAt: new Date() };
+ this.addDomainEvent(
+ new FuzzCompleted(this.id.toString(), {
+ crawlSessionId: this.props.crawlSessionId,
+ actionsExecuted: this.props.actionsExecuted,
+ vulnerabilitiesFound: this.props.vulnerabilitiesFound,
+ failed: true,
+ reason,
+ })
+ );
+ }
+}
diff --git a/src/modules/fuzzing/domain/events/FuzzCompleted.ts b/src/modules/fuzzing/domain/events/FuzzCompleted.ts
new file mode 100644
index 0000000..0173625
--- /dev/null
+++ b/src/modules/fuzzing/domain/events/FuzzCompleted.ts
@@ -0,0 +1,13 @@
+import { randomUUID } from 'crypto';
+import { DomainEvent } from '../../../../shared/domain/DomainEvent';
+
+export class FuzzCompleted implements DomainEvent {
+ readonly eventId = randomUUID();
+ readonly eventName = 'fuzz.completed';
+ readonly occurredOn = new Date();
+
+ constructor(
+ readonly aggregateId: string,
+ readonly payload: Record
+ ) {}
+}
diff --git a/src/modules/fuzzing/domain/events/FuzzStarted.ts b/src/modules/fuzzing/domain/events/FuzzStarted.ts
new file mode 100644
index 0000000..a41a13b
--- /dev/null
+++ b/src/modules/fuzzing/domain/events/FuzzStarted.ts
@@ -0,0 +1,13 @@
+import { randomUUID } from 'crypto';
+import { DomainEvent } from '../../../../shared/domain/DomainEvent';
+
+export class FuzzStarted implements DomainEvent {
+ readonly eventId = randomUUID();
+ readonly eventName = 'fuzz.started';
+ readonly occurredOn = new Date();
+
+ constructor(
+ readonly aggregateId: string,
+ readonly payload: Record
+ ) {}
+}
diff --git a/src/modules/fuzzing/domain/events/VulnerabilityDetected.ts b/src/modules/fuzzing/domain/events/VulnerabilityDetected.ts
new file mode 100644
index 0000000..d9dfc86
--- /dev/null
+++ b/src/modules/fuzzing/domain/events/VulnerabilityDetected.ts
@@ -0,0 +1,13 @@
+import { randomUUID } from 'crypto';
+import { DomainEvent } from '../../../../shared/domain/DomainEvent';
+
+export class VulnerabilityDetected implements DomainEvent {
+ readonly eventId = randomUUID();
+ readonly eventName = 'fuzz.vulnerability_detected';
+ readonly occurredOn = new Date();
+
+ constructor(
+ readonly aggregateId: string,
+ readonly payload: Record
+ ) {}
+}
diff --git a/src/modules/fuzzing/domain/ports/IFuzzerEngine.ts b/src/modules/fuzzing/domain/ports/IFuzzerEngine.ts
new file mode 100644
index 0000000..0a35ce2
--- /dev/null
+++ b/src/modules/fuzzing/domain/ports/IFuzzerEngine.ts
@@ -0,0 +1,6 @@
+import { IAction, IState } from '../../../../core/interfaces';
+
+export interface IFuzzerEngine {
+ /** Generate fuzz fill actions for a DOM snapshot at a given state */
+ generateFuzzActions(domSnapshot: string, state: IState): IAction[];
+}
diff --git a/src/modules/fuzzing/domain/value-objects/FuzzIntensity.ts b/src/modules/fuzzing/domain/value-objects/FuzzIntensity.ts
new file mode 100644
index 0000000..b0375a1
--- /dev/null
+++ b/src/modules/fuzzing/domain/value-objects/FuzzIntensity.ts
@@ -0,0 +1,24 @@
+import { ValueObject } from '../../../../shared/domain/ValueObject';
+
+type IntensityLevel = 'low' | 'medium' | 'high';
+
+interface FuzzIntensityProps {
+ value: IntensityLevel;
+}
+
+export class FuzzIntensity extends ValueObject {
+ static readonly LEVELS: IntensityLevel[] = ['low', 'medium', 'high'];
+
+ static low(): FuzzIntensity { return new FuzzIntensity({ value: 'low' }); }
+ static medium(): FuzzIntensity { return new FuzzIntensity({ value: 'medium' }); }
+ static high(): FuzzIntensity { return new FuzzIntensity({ value: 'high' }); }
+
+ static fromString(s: string): FuzzIntensity {
+ if (!FuzzIntensity.LEVELS.includes(s as IntensityLevel)) {
+ throw new Error(`Invalid intensity: ${s}. Must be one of: ${FuzzIntensity.LEVELS.join(', ')}`);
+ }
+ return new FuzzIntensity({ value: s as IntensityLevel });
+ }
+
+ get value(): IntensityLevel { return this.props.value; }
+}
diff --git a/src/modules/fuzzing/domain/value-objects/FuzzPayload.ts b/src/modules/fuzzing/domain/value-objects/FuzzPayload.ts
new file mode 100644
index 0000000..8f87a28
--- /dev/null
+++ b/src/modules/fuzzing/domain/value-objects/FuzzPayload.ts
@@ -0,0 +1,15 @@
+import { ValueObject } from '../../../../shared/domain/ValueObject';
+
+interface FuzzPayloadProps {
+ value: string;
+ strategy: string;
+}
+
+export class FuzzPayload extends ValueObject {
+ static create(value: string, strategy: string): FuzzPayload {
+ return new FuzzPayload({ value, strategy });
+ }
+
+ get value(): string { return this.props.value; }
+ get strategy(): string { return this.props.strategy; }
+}
diff --git a/src/modules/fuzzing/domain/value-objects/FuzzStrategy.ts b/src/modules/fuzzing/domain/value-objects/FuzzStrategy.ts
new file mode 100644
index 0000000..51a5ef7
--- /dev/null
+++ b/src/modules/fuzzing/domain/value-objects/FuzzStrategy.ts
@@ -0,0 +1,26 @@
+import { ValueObject } from '../../../../shared/domain/ValueObject';
+
+type StrategyName = 'empty' | 'oversized' | 'special_chars' | 'type_mismatch' | 'boundary';
+
+interface FuzzStrategyProps {
+ value: StrategyName;
+}
+
+export class FuzzStrategy extends ValueObject {
+ static readonly ALL: StrategyName[] = ['empty', 'oversized', 'special_chars', 'type_mismatch', 'boundary'];
+
+ static empty(): FuzzStrategy { return new FuzzStrategy({ value: 'empty' }); }
+ static oversized(): FuzzStrategy { return new FuzzStrategy({ value: 'oversized' }); }
+ static specialChars(): FuzzStrategy { return new FuzzStrategy({ value: 'special_chars' }); }
+ static typeMismatch(): FuzzStrategy { return new FuzzStrategy({ value: 'type_mismatch' }); }
+ static boundary(): FuzzStrategy { return new FuzzStrategy({ value: 'boundary' }); }
+
+ static fromString(s: string): FuzzStrategy {
+ if (!FuzzStrategy.ALL.includes(s as StrategyName)) {
+ throw new Error(`Invalid fuzz strategy: ${s}`);
+ }
+ return new FuzzStrategy({ value: s as StrategyName });
+ }
+
+ get value(): StrategyName { return this.props.value; }
+}
diff --git a/src/modules/fuzzing/domain/value-objects/Seed.ts b/src/modules/fuzzing/domain/value-objects/Seed.ts
new file mode 100644
index 0000000..1a8c5cc
--- /dev/null
+++ b/src/modules/fuzzing/domain/value-objects/Seed.ts
@@ -0,0 +1,20 @@
+import { ValueObject } from '../../../../shared/domain/ValueObject';
+
+interface SeedProps {
+ value: number;
+}
+
+export class Seed extends ValueObject {
+ static create(value: number): Seed {
+ if (!Number.isInteger(value) || value < 0) {
+ throw new Error(`Seed must be a non-negative integer, got: ${value}`);
+ }
+ return new Seed({ value });
+ }
+
+ static fromTimestamp(): Seed {
+ return new Seed({ value: Date.now() });
+ }
+
+ get value(): number { return this.props.value; }
+}
diff --git a/src/modules/fuzzing/index.ts b/src/modules/fuzzing/index.ts
new file mode 100644
index 0000000..41bf71b
--- /dev/null
+++ b/src/modules/fuzzing/index.ts
@@ -0,0 +1,17 @@
+// Domain
+export { FuzzSession } from './domain/entities/FuzzSession';
+export { FuzzResult } from './domain/entities/FuzzResult';
+export { FuzzIntensity } from './domain/value-objects/FuzzIntensity';
+export { FuzzStrategy } from './domain/value-objects/FuzzStrategy';
+export { FuzzPayload } from './domain/value-objects/FuzzPayload';
+export { Seed } from './domain/value-objects/Seed';
+export type { IFuzzerEngine } from './domain/ports/IFuzzerEngine';
+
+// Application
+export { RunFuzzCommand } from './application/commands/RunFuzzCommand';
+export type { IFuzzSessionRepository } from './application/commands/RunFuzzCommand';
+export { OnActionExecuted } from './application/event-handlers/OnActionExecuted';
+
+// Infrastructure
+export { FuzzingEngineAdapter } from './infrastructure/adapters/FuzzingEngineAdapter';
+export { createFuzzingRouter } from './infrastructure/http/FuzzingController';
diff --git a/src/modules/fuzzing/infrastructure/adapters/FuzzingEngineAdapter.ts b/src/modules/fuzzing/infrastructure/adapters/FuzzingEngineAdapter.ts
new file mode 100644
index 0000000..4fa62a4
--- /dev/null
+++ b/src/modules/fuzzing/infrastructure/adapters/FuzzingEngineAdapter.ts
@@ -0,0 +1,121 @@
+/**
+ * FuzzingEngineAdapter — implements IFuzzerEngine port using the 5 fuzzing strategies.
+ * Adapts the legacy FuzzingEngine logic to the hexagonal architecture.
+ */
+
+import * as crypto from 'crypto';
+import { IAction, IState } from '../../../../core/interfaces';
+import { IFuzzerEngine } from '../../domain/ports/IFuzzerEngine';
+import { detectInputType, DetectedInputType } from './InputTypeDetector';
+import { EmptyValueStrategy } from '../strategies/EmptyValueStrategy';
+import { OversizedStringStrategy } from '../strategies/OversizedStringStrategy';
+import { SpecialCharsStrategy } from '../strategies/SpecialCharsStrategy';
+import { TypeMismatchStrategy } from '../strategies/TypeMismatchStrategy';
+import { BoundaryValueStrategy } from '../strategies/BoundaryValueStrategy';
+
+interface FormField {
+ selector: string;
+ tagName: string;
+ inputType?: string;
+ name?: string;
+ placeholder?: string;
+ ariaLabel?: string;
+}
+
+const INPUT_RE = /<(input|textarea|select)[^>]*>/gi;
+const ATTR_RE = (name: string): RegExp => new RegExp(`${name}="([^"]*)"`, 'i');
+
+function extractFields(domSnapshot: string): FormField[] {
+ const fields: FormField[] = [];
+ let match: RegExpExecArray | null;
+ while ((match = INPUT_RE.exec(domSnapshot)) !== null) {
+ const tag = match[0] ?? '';
+ const tagName = match[1] ?? 'input';
+ const idMatch = ATTR_RE('id').exec(tag);
+ const nameMatch = ATTR_RE('name').exec(tag);
+ const typeMatch = ATTR_RE('type').exec(tag);
+ const placeholderMatch = ATTR_RE('placeholder').exec(tag);
+ const ariaMatch = ATTR_RE('aria-label').exec(tag);
+
+ const selector = idMatch?.[1]
+ ? `#${idMatch[1]}`
+ : nameMatch?.[1]
+ ? `[name="${nameMatch[1]}"]`
+ : tagName;
+
+ fields.push({
+ selector,
+ tagName,
+ inputType: typeMatch?.[1],
+ name: nameMatch?.[1],
+ placeholder: placeholderMatch?.[1],
+ ariaLabel: ariaMatch?.[1],
+ });
+ }
+ return fields;
+}
+
+type FuzzingStrategy = {
+ name: string;
+ appliesTo(type: DetectedInputType): boolean;
+ values(type?: DetectedInputType): string[];
+};
+
+export class FuzzingEngineAdapter implements IFuzzerEngine {
+ private readonly intensity: 'low' | 'medium' | 'high';
+ private readonly seed: number;
+
+ constructor(config: { intensity: 'low' | 'medium' | 'high'; seed: number }) {
+ this.intensity = config.intensity;
+ this.seed = config.seed;
+ }
+
+ generateFuzzActions(domSnapshot: string, state: IState): IAction[] {
+ const fields = extractFields(domSnapshot);
+ const actions: IAction[] = [];
+ const now = Date.now();
+ const strategies = this.selectStrategies();
+
+ for (const field of fields) {
+ const detectedType = detectInputType({
+ tagName: field.tagName,
+ inputType: field.inputType,
+ name: field.name,
+ placeholder: field.placeholder,
+ ariaLabel: field.ariaLabel,
+ });
+
+ for (const strategy of strategies) {
+ if (!strategy.appliesTo(detectedType)) continue;
+ const values = strategy.values(detectedType);
+ for (const value of values) {
+ actions.push({
+ id: crypto.randomUUID(),
+ type: 'fill',
+ selector: field.selector,
+ value,
+ timestamp: now,
+ seed: this.seed,
+ stateId: state.id,
+ });
+ }
+ }
+ }
+
+ return actions;
+ }
+
+ private selectStrategies(): FuzzingStrategy[] {
+ const empty = new EmptyValueStrategy();
+ const typeMismatch = new TypeMismatchStrategy();
+ const oversized = new OversizedStringStrategy(this.intensity);
+ const boundary = new BoundaryValueStrategy();
+ const special = new SpecialCharsStrategy();
+
+ switch (this.intensity) {
+ case 'low': return [empty, typeMismatch];
+ case 'medium': return [empty, typeMismatch, oversized, boundary];
+ case 'high': return [empty, typeMismatch, oversized, boundary, special];
+ }
+ }
+}
diff --git a/src/modules/fuzzing/infrastructure/adapters/InputTypeDetector.ts b/src/modules/fuzzing/infrastructure/adapters/InputTypeDetector.ts
new file mode 100644
index 0000000..c8f4663
--- /dev/null
+++ b/src/modules/fuzzing/infrastructure/adapters/InputTypeDetector.ts
@@ -0,0 +1,41 @@
+import { DetectedInputType } from '../strategies/EmptyValueStrategy';
+
+export type { DetectedInputType };
+
+export function detectInputType(attrs: {
+ tagName?: string;
+ inputType?: string;
+ name?: string;
+ placeholder?: string;
+ ariaLabel?: string;
+}): DetectedInputType {
+ const tag = (attrs.tagName ?? '').toLowerCase();
+ if (tag === 'textarea') return 'textarea';
+ if (tag === 'select') return 'select';
+
+ const inputType = (attrs.inputType ?? '').toLowerCase();
+ if (inputType === 'email') return 'email';
+ if (inputType === 'password') return 'password';
+ if (inputType === 'number') return 'number';
+ if (inputType === 'date') return 'date';
+ if (inputType === 'tel') return 'phone';
+ if (inputType === 'url') return 'url';
+ if (inputType === 'search') return 'search';
+ if (inputType === 'file') return 'file';
+
+ const hints = [
+ (attrs.name ?? '').toLowerCase(),
+ (attrs.placeholder ?? '').toLowerCase(),
+ (attrs.ariaLabel ?? '').toLowerCase(),
+ ].join(' ');
+
+ if (/email/.test(hints)) return 'email';
+ if (/password|pass/.test(hints)) return 'password';
+ if (/phone|tel|mobile/.test(hints)) return 'phone';
+ if (/date|birth|dob/.test(hints)) return 'date';
+ if (/number|qty|quantity|age/.test(hints)) return 'number';
+ if (/search/.test(hints)) return 'search';
+ if (/url|website|link/.test(hints)) return 'url';
+
+ return 'text';
+}
diff --git a/src/modules/fuzzing/infrastructure/http/FuzzingController.ts b/src/modules/fuzzing/infrastructure/http/FuzzingController.ts
new file mode 100644
index 0000000..d4b8512
--- /dev/null
+++ b/src/modules/fuzzing/infrastructure/http/FuzzingController.ts
@@ -0,0 +1,71 @@
+import { Router, Request, Response } from 'express';
+import { RunFuzzCommand, IFuzzSessionRepository } from '../../application/commands/RunFuzzCommand';
+import { FuzzSession } from '../../domain/entities/FuzzSession';
+import { IState } from '../../../../core/interfaces';
+
+export interface FuzzingControllerDeps {
+ runFuzz: RunFuzzCommand;
+ repository: IFuzzSessionRepository;
+}
+
+export function createFuzzingRouter(deps: FuzzingControllerDeps): Router {
+ const router = Router();
+
+ // POST /api/fuzz/run — trigger fuzzing for a given state
+ router.post('/run', async (req: Request, res: Response) => {
+ const { crawlSessionId, intensity, seed, state } = req.body as {
+ crawlSessionId?: string;
+ intensity?: 'low' | 'medium' | 'high';
+ seed?: number;
+ state?: IState;
+ };
+
+ if (!crawlSessionId || !state) {
+ res.status(400).json({ error: 'crawlSessionId and state are required' });
+ return;
+ }
+
+ const result = await deps.runFuzz.execute({
+ crawlSessionId,
+ intensity: intensity ?? 'low',
+ seed: seed ?? Date.now(),
+ state,
+ });
+
+ if (!result.ok) {
+ res.status(422).json({ error: result.error });
+ return;
+ }
+
+ res.status(201).json(result.value);
+ });
+
+ // GET /api/fuzz/sessions/:id — get fuzz session
+ router.get('/sessions/:id', async (req: Request, res: Response) => {
+ const sessionId = req.params['id'] as string;
+ const session = await deps.repository.findById(sessionId);
+
+ if (!session) {
+ res.status(404).json({ error: 'Fuzz session not found' });
+ return;
+ }
+
+ res.json(toDTO(session));
+ });
+
+ return router;
+}
+
+function toDTO(s: FuzzSession) {
+ return {
+ id: s.id.toString(),
+ crawlSessionId: s.crawlSessionId,
+ intensity: s.intensity.value,
+ seed: s.seed.value,
+ status: s.status,
+ actionsExecuted: s.actionsExecuted,
+ vulnerabilitiesFound: s.vulnerabilitiesFound,
+ startedAt: s.startedAt.toISOString(),
+ completedAt: s.completedAt?.toISOString() ?? null,
+ };
+}
diff --git a/src/modules/fuzzing/infrastructure/strategies/BoundaryValueStrategy.ts b/src/modules/fuzzing/infrastructure/strategies/BoundaryValueStrategy.ts
new file mode 100644
index 0000000..080bfc6
--- /dev/null
+++ b/src/modules/fuzzing/infrastructure/strategies/BoundaryValueStrategy.ts
@@ -0,0 +1,17 @@
+import { DetectedInputType } from './EmptyValueStrategy';
+
+export class BoundaryValueStrategy {
+ readonly name = 'BoundaryValueStrategy';
+
+ appliesTo(type: DetectedInputType): boolean {
+ return type === 'number' || type === 'date';
+ }
+
+ values(type: DetectedInputType): string[] {
+ switch (type) {
+ case 'number': return ['0', '-1', '2147483647', '2147483648', '-2147483648'];
+ case 'date': return ['1900-01-01', '2099-12-31', '1970-01-01'];
+ default: return [];
+ }
+ }
+}
diff --git a/src/modules/fuzzing/infrastructure/strategies/EmptyValueStrategy.ts b/src/modules/fuzzing/infrastructure/strategies/EmptyValueStrategy.ts
new file mode 100644
index 0000000..287dd6f
--- /dev/null
+++ b/src/modules/fuzzing/infrastructure/strategies/EmptyValueStrategy.ts
@@ -0,0 +1,15 @@
+export type DetectedInputType =
+ | 'email' | 'password' | 'number' | 'date' | 'phone'
+ | 'url' | 'search' | 'text' | 'textarea' | 'select' | 'file';
+
+export class EmptyValueStrategy {
+ readonly name = 'EmptyValueStrategy';
+
+ appliesTo(_type: DetectedInputType): boolean {
+ return true;
+ }
+
+ values(): string[] {
+ return ['', ' ', '\t'];
+ }
+}
diff --git a/src/modules/fuzzing/infrastructure/strategies/OversizedStringStrategy.ts b/src/modules/fuzzing/infrastructure/strategies/OversizedStringStrategy.ts
new file mode 100644
index 0000000..5f9553a
--- /dev/null
+++ b/src/modules/fuzzing/infrastructure/strategies/OversizedStringStrategy.ts
@@ -0,0 +1,21 @@
+import { DetectedInputType } from './EmptyValueStrategy';
+
+const APPLICABLE_TYPES: DetectedInputType[] = ['text', 'email', 'password', 'textarea'];
+
+export class OversizedStringStrategy {
+ readonly name = 'OversizedStringStrategy';
+
+ constructor(private readonly intensity: 'low' | 'medium' | 'high') {}
+
+ appliesTo(type: DetectedInputType): boolean {
+ return APPLICABLE_TYPES.includes(type);
+ }
+
+ values(): string[] {
+ switch (this.intensity) {
+ case 'low': return ['A'.repeat(256)];
+ case 'medium': return ['A'.repeat(1024)];
+ case 'high': return ['A'.repeat(10000) + '日本語テスト𠮷野家'];
+ }
+ }
+}
diff --git a/src/modules/fuzzing/infrastructure/strategies/SpecialCharsStrategy.ts b/src/modules/fuzzing/infrastructure/strategies/SpecialCharsStrategy.ts
new file mode 100644
index 0000000..f5a99a3
--- /dev/null
+++ b/src/modules/fuzzing/infrastructure/strategies/SpecialCharsStrategy.ts
@@ -0,0 +1,21 @@
+import { DetectedInputType } from './EmptyValueStrategy';
+
+const APPLICABLE_TYPES: DetectedInputType[] = ['text', 'email', 'search', 'textarea'];
+
+export class SpecialCharsStrategy {
+ readonly name = 'SpecialCharsStrategy';
+
+ appliesTo(type: DetectedInputType): boolean {
+ return APPLICABLE_TYPES.includes(type);
+ }
+
+ values(): string[] {
+ return [
+ "' OR 1=1 --",
+ '',
+ '../../etc/passwd',
+ '${7*7}',
+ '\x00\x01\x02',
+ ];
+ }
+}
diff --git a/src/modules/fuzzing/infrastructure/strategies/TypeMismatchStrategy.ts b/src/modules/fuzzing/infrastructure/strategies/TypeMismatchStrategy.ts
new file mode 100644
index 0000000..7e3d5cd
--- /dev/null
+++ b/src/modules/fuzzing/infrastructure/strategies/TypeMismatchStrategy.ts
@@ -0,0 +1,20 @@
+import { DetectedInputType } from './EmptyValueStrategy';
+
+export class TypeMismatchStrategy {
+ readonly name = 'TypeMismatchStrategy';
+
+ appliesTo(type: DetectedInputType): boolean {
+ return ['email', 'number', 'date', 'url', 'phone'].includes(type);
+ }
+
+ values(type: DetectedInputType): string[] {
+ switch (type) {
+ case 'email': return ['not-an-email', '12345', '@@@'];
+ case 'number': return ['abc', '-999999', '9.9.9', 'NaN'];
+ case 'date': return ['yesterday', '32/13/2025', '0000-00-00'];
+ case 'url': return ['javascript:alert(1)', 'not a url'];
+ case 'phone': return ['000', '++++', 'abcdefghij'];
+ default: return [];
+ }
+ }
+}
diff --git a/tests/modules/fuzzing.test.ts b/tests/modules/fuzzing.test.ts
new file mode 100644
index 0000000..ab2c52b
--- /dev/null
+++ b/tests/modules/fuzzing.test.ts
@@ -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('');
+ });
+});
+
+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 = ``;
+ 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 = ``;
+ 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 = ``;
+ const state = makeState(dom);
+ expect(engine.generateFuzzActions(dom, state)).toHaveLength(0);
+ });
+});