diff --git a/scripts/agent/fuzz-dmDecryptor.mjs b/scripts/agent/fuzz-dmDecryptor.mjs index e3a7aa3b..a161b5be 100644 --- a/scripts/agent/fuzz-dmDecryptor.mjs +++ b/scripts/agent/fuzz-dmDecryptor.mjs @@ -1,179 +1,75 @@ -import { decryptDM } from '../../js/dmDecryptor.js'; -import { - fuzzBoolean, - fuzzInt, - fuzzString, - fuzzHexString, - fuzzJSON, - pickOne, - fuzzSurrogatePairString, - saveFuzzReport, - saveReproducer -} from './fuzz-utils.mjs'; -import fs from 'fs'; -import path from 'path'; -const ITERATIONS = 1000; -const FINDINGS = []; +import fs from "fs"; +import path from "path"; +import { runFuzzer, rng } from "./fuzz-shared.mjs"; -function saveReproducerDM(id, input, error) { - const reproDir = path.join('examples', 'reproducers', `fuzz-dmDecryptor-${id}`); - if (!fs.existsSync(reproDir)) { - fs.mkdirSync(reproDir, { recursive: true }); - } +const originalPath = "../../js/dmDecryptor.js"; +const tempPath = "./temp_dmDecryptor.mjs"; - const readmeContent = `# Fuzz Reproducer: dmDecryptor - ${id} +// Read original file +const originalContent = fs.readFileSync(new URL(originalPath, import.meta.url), "utf-8"); -## Error -\`\`\` -${error.stack || error.message} -\`\`\` -`; +// Replace import +// The original file has: import { normalizeActorKey } from "./nostr/watchHistory.js"; +// We want to point to ./mocks/watchHistory.js +const patchedContent = originalContent.replace( + /import \{ normalizeActorKey \} from "\.\/nostr\/watchHistory\.js";/, + 'import { normalizeActorKey } from "./mocks/watchHistory.js";' +); - fs.writeFileSync(path.join(reproDir, 'README.md'), readmeContent); +// Write temp file +fs.writeFileSync(new URL(tempPath, import.meta.url), patchedContent); - // Serialize input parts - const event = input.event; - const context = input.context; +// Import the patched module +const DmDecryptor = await import(tempPath); - // We can't easily capture the 'shouldFail' state unless we stored it on the object, - // but we can infer or just log it. For now, let's just save the structure. - - const serializedContext = { - actorPubkey: context?.actorPubkey, - decryptorConfigs: context?.decryptors && Array.isArray(context.decryptors) ? context.decryptors.map(d => ({ - scheme: d.scheme, - supportsGiftWrap: d.supportsGiftWrap, - })) : context?.decryptors +async function fuzzTest(iteration) { + const genEvent = () => { + return { + kind: rng.oneOf([4, 1059, rng.int(0, 20000)]), + pubkey: rng.mixedString(64), + created_at: rng.int(0, 2000000000), + content: rng.mixedString(100), + tags: rng.array(() => rng.array(() => rng.mixedString(20), 5), 5) }; + }; - fs.writeFileSync(path.join(reproDir, 'input-event.json'), JSON.stringify(event, null, 2)); - fs.writeFileSync(path.join(reproDir, 'input-context-config.json'), JSON.stringify(serializedContext, null, 2)); + const genDecryptor = () => { + return { + scheme: rng.oneOf(["nip04", "nip44", "nip44_v2", rng.mixedString(10)]), + decrypt: async (pubkey, ciphertext) => { + if (rng.bool()) { + throw new Error("Decryption failed " + rng.mixedString(10)); + } + // Return either a string (valid) or garbage + if (rng.bool()) { + return JSON.stringify({ content: "decrypted message" }); + } + return rng.nastyString(); + } + }; + }; - const scriptContent = ` -import { decryptDM } from '../../../../js/dmDecryptor.js'; -import fs from 'fs'; -import path from 'path'; + const event = rng.bool() ? genEvent() : rng.nastyString(); + const context = { + actorPubkey: rng.mixedString(64), + decryptors: rng.array(genDecryptor, 3) + }; -const event = JSON.parse(fs.readFileSync('input-event.json', 'utf8')); -const contextConfig = JSON.parse(fs.readFileSync('input-context-config.json', 'utf8')); + // Run the target + await DmDecryptor.decryptDM(event, context); -// Reconstruct decryptors -const decryptors = Array.isArray(contextConfig.decryptorConfigs) - ? contextConfig.decryptorConfigs.map(config => ({ - scheme: config.scheme, - supportsGiftWrap: config.supportsGiftWrap, - decrypt: async (pubkey, ciphertext) => { - console.log('Mock decrypt called for:', pubkey); - return "mock-decrypted-content"; - } - })) - : contextConfig.decryptorConfigs; - -const context = { - actorPubkey: contextConfig.actorPubkey, - decryptors -}; - -console.log('Running reproduction...'); -try { - await decryptDM(event, context); - console.log('Execution finished without crash.'); -} catch (error) { - console.error('Crash reproduced:', error); -} -`; - fs.writeFileSync(path.join(reproDir, 'repro.mjs'), scriptContent); - console.log(`Reproducer saved to ${reproDir}`); + return { event, context }; } -function logError(target, input, error) { - const id = Date.now().toString() + Math.floor(Math.random() * 1000); - console.error(`[FAIL] ${target} crashed! ID: ${id}`); - FINDINGS.push({ - id, - target, - error: error.message, - stack: error.stack, - input +runFuzzer("dmDecryptor", 5000, fuzzTest) + .catch(err => { + console.error("Fatal fuzzer error:", err); + process.exit(1); + }) + .finally(() => { + // Cleanup + if (fs.existsSync(new URL(tempPath, import.meta.url))) { + fs.unlinkSync(new URL(tempPath, import.meta.url)); + } }); - saveReproducerDM(id, input, error); -} - -function mockDecryptor(shouldFail = false) { - return { - scheme: pickOne(['nip04', 'nip44', 'nip44_v2', fuzzString()]), - decrypt: async (pubkey, ciphertext) => { - if (shouldFail) { - throw new Error('Mock Decryption Failed'); - } - // Return a valid stringified JSON event sometimes, or random garbage - if (fuzzBoolean()) { - return JSON.stringify({ - content: fuzzString(20), - pubkey: fuzzHexString(), - created_at: Math.floor(Date.now() / 1000), - tags: [] - }); - } - return fuzzString(50); - }, - supportsGiftWrap: fuzzBoolean() - }; -} - -function generateRandomEvent() { - const kind = pickOne([4, 1059, fuzzInt(0, 20000)]); - const content = fuzzBoolean() ? fuzzString(100) : fuzzJSON(2); - - return { - kind, - pubkey: fuzzHexString(), - created_at: Math.floor(Date.now() / 1000), - tags: fuzzBoolean() ? [] : [['p', fuzzHexString()], ['encrypted', 'nip44']], - content: typeof content === 'string' ? content : JSON.stringify(content) - }; -} - -function generateRandomContext() { - const decryptors = []; - const count = fuzzInt(0, 3); - for (let i = 0; i < count; i++) { - decryptors.push(mockDecryptor(fuzzBoolean())); // some will fail - } - - return { - actorPubkey: fuzzHexString(), - decryptors - }; -} - -async function fuzzDecryptor() { - console.log('Starting fuzzing for dmDecryptor.js...'); - - for (let i = 0; i < ITERATIONS; i++) { - const event = generateRandomEvent(); - const context = generateRandomContext(); - - // Also test null/undefined/garbage inputs - const inputEvent = pickOne([event, null, undefined, "garbage", {}]); - const inputContext = pickOne([context, null, undefined, {}, { decryptors: "garbage" }]); - - try { - await decryptDM(inputEvent, inputContext); - } catch (error) { - logError('decryptDM', { event: inputEvent, context: inputContext }, error); - } - } - - saveFuzzReport('dmDecryptor', FINDINGS); - console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`); - if (FINDINGS.length > 0) { - process.exit(1); - } -} - -fuzzDecryptor().catch(err => { - console.error('Fuzzing script failed:', err); - process.exit(1); -}); diff --git a/scripts/agent/fuzz-magnetUtils.mjs b/scripts/agent/fuzz-magnetUtils.mjs index 09b89736..8a99abb5 100644 --- a/scripts/agent/fuzz-magnetUtils.mjs +++ b/scripts/agent/fuzz-magnetUtils.mjs @@ -1,90 +1,43 @@ -import { normalizeAndAugmentMagnet } from '../../js/magnetUtils.js'; -import { - fuzzBoolean, - fuzzInt, - fuzzString, - fuzzHexString, - fuzzJSON, - pickOne, - fuzzSurrogatePairString, - saveFuzzReport, - saveReproducer -} from './fuzz-utils.mjs'; -const ITERATIONS = 1000; -const FINDINGS = []; +import { runFuzzer, rng } from "./fuzz-shared.mjs"; -function logError(target, input, error) { - const id = Date.now().toString() + Math.floor(Math.random() * 1000); - console.error(`[FAIL] ${target} crashed! ID: ${id}`); - FINDINGS.push({ - id, - target, - error: error.message, - stack: error.stack, - input - }); - saveReproducer(target, id, input, error); -} - -function generateRandomMagnet() { - const scheme = 'magnet:?'; - const xt = `xt=urn:btih:${fuzzHexString(40)}`; - const dn = `dn=${fuzzString(10)}`; - const tr = `tr=${encodeURIComponent('wss://' + fuzzString(10) + '.com')}`; - - // Mix valid parts with garbage - const parts = [xt, dn, tr]; - if (fuzzBoolean()) parts.push(`ws=${encodeURIComponent('https://' + fuzzString(10) + '.com')}`); - if (fuzzBoolean()) parts.push(fuzzString(5) + '=' + fuzzString(5)); - - // Shuffle - parts.sort(() => Math.random() - 0.5); - - return scheme + parts.join('&'); -} - -function generateRandomOptions() { - return { - webSeed: fuzzBoolean() ? [fuzzString()] : fuzzString(), - torrentUrl: fuzzBoolean() ? fuzzString() : undefined, - xs: fuzzBoolean() ? fuzzString() : undefined, - extraTrackers: fuzzBoolean() ? [fuzzString()] : [], - appProtocol: pickOne(['http:', 'https:', fuzzString(), undefined]), - logger: fuzzBoolean() ? () => {} : undefined - }; -} - -function fuzzMagnetUtils() { - console.log('Starting fuzzing for magnetUtils.js...'); - - for (let i = 0; i < ITERATIONS; i++) { - // Inputs: valid magnet, partial, garbage, huge string, percent encoded mess - const rawValue = pickOne([ - generateRandomMagnet(), - fuzzString(100), - `magnet:?xt=${fuzzString(10)}`, // invalid xt - fuzzSurrogatePairString(50), - null, - undefined, - {}, - 123 - ]); - - const options = fuzzBoolean() ? generateRandomOptions() : undefined; - - try { - normalizeAndAugmentMagnet(rawValue, options); - } catch (error) { - logError('normalizeAndAugmentMagnet', { rawValue, options }, error); - } +// Polyfills +global.window = { + location: { + protocol: "https:" } +}; - saveFuzzReport('magnetUtils', FINDINGS); - console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`); - if (FINDINGS.length > 0) { - process.exit(1); - } +// Import the target module +// Note: We are in scripts/agent/ +import * as MagnetUtils from "../../js/magnetUtils.js"; + +async function fuzzTest(iteration) { + // Randomize window protocol sometimes + global.window.location.protocol = rng.bool() ? "https:" : "http:"; + + const rawValue = rng.mixedString(100); + + const genOptions = () => { + if (rng.bool()) return null; + return { + webSeed: rng.bool() ? rng.mixedString(50) : rng.array(() => rng.mixedString(50), 3), + torrentUrl: rng.mixedString(50), + xs: rng.mixedString(50), + extraTrackers: rng.array(() => rng.mixedString(30), 5), + logger: rng.bool() ? (() => {}) : null, + appProtocol: rng.bool() ? rng.oneOf(["http:", "https:", rng.nastyString()]) : undefined + }; + }; + + const options = genOptions(); + + MagnetUtils.normalizeAndAugmentMagnet(rawValue, options); + + return { rawValue, options }; } -fuzzMagnetUtils(); +runFuzzer("magnetUtils", 5000, fuzzTest).catch(err => { + console.error("Fatal fuzzer error:", err); + process.exit(1); +}); diff --git a/scripts/agent/fuzz-nostrEventSchemas.mjs b/scripts/agent/fuzz-nostrEventSchemas.mjs index 7f77bf12..c92720de 100644 --- a/scripts/agent/fuzz-nostrEventSchemas.mjs +++ b/scripts/agent/fuzz-nostrEventSchemas.mjs @@ -1,173 +1,126 @@ -import { - buildVideoPostEvent, - buildHttpAuthEvent, - buildReportEvent, - buildVideoMirrorEvent, - buildRepostEvent, - buildShareEvent, - buildRelayListEvent, - buildDmRelayListEvent, - buildProfileMetadataEvent, - buildMuteListEvent, - buildDeletionEvent, - buildLegacyDirectMessageEvent, - buildDmAttachmentEvent, - buildDmReadReceiptEvent, - buildDmTypingIndicatorEvent, - buildViewEvent, - buildZapRequestEvent, - buildReactionEvent, - buildCommentEvent, - buildWatchHistoryEvent, - buildSubscriptionListEvent, - buildBlockListEvent, - buildHashtagPreferenceEvent, - buildAdminListEvent, - validateEventStructure, - sanitizeAdditionalTags, - NOTE_TYPES -} from '../../js/nostrEventSchemas.js'; -import { - fuzzBoolean, - fuzzInt, - fuzzString, - fuzzHexString, - fuzzJSON, - pickOne, - fuzzSurrogatePairString, - saveFuzzReport, - saveReproducer -} from './fuzz-utils.mjs'; +import { runFuzzer, rng } from "./fuzz-shared.mjs"; +import { TextEncoder, TextDecoder } from "util"; -const ITERATIONS = 1000; -const FINDINGS = []; +// Polyfills +global.TextEncoder = TextEncoder; +global.TextDecoder = TextDecoder; +global.window = { + bitvidNostrEventOverrides: {} +}; -function logError(target, input, error) { - const id = Date.now().toString() + Math.floor(Math.random() * 1000); - console.error(`[FAIL] ${target} crashed! ID: ${id}`); - FINDINGS.push({ - id, - target, - error: error.message, - stack: error.stack, - input - }); - saveReproducer(target, id, input, error); -} +// Import the target module +// Note: We are in scripts/agent/ +import * as Schemas from "../../js/nostrEventSchemas.js"; -function generateRandomParams() { - const params = {}; - const keys = ['pubkey', 'created_at', 'content', 'tags', 'additionalTags', 'dTagValue', 'url', 'method', 'payload', 'eventId', 'userId', 'reportType', 'relayHint', 'recipientPubkey', 'ciphertext', 'attachment', 'messageKind', 'expiresAt', 'pointerValue', 'pointerTag', 'dedupeTag', 'amountSats', 'lnurl', 'coordinate', 'videoEventId', 'rootKind']; +async function fuzzTest(iteration) { + // We choose a random function to fuzz in each iteration or fuzz all of them? + // Let's pick one random target per iteration to keep it simple and isolate crashes. - // Randomly select some keys to populate - keys.forEach(key => { - if (fuzzBoolean()) { - if (key === 'created_at' || key === 'amountSats' || key === 'messageKind' || key === 'expiresAt') { - params[key] = fuzzBoolean() ? fuzzInt() : fuzzString(); // Mix types - } else if (key === 'pubkey' || key === 'recipientPubkey') { - params[key] = fuzzBoolean() ? fuzzHexString() : fuzzString(); - } else if (key === 'tags' || key === 'additionalTags') { - params[key] = fuzzBoolean() ? [] : fuzzJSON(2); // Can be valid array or random JSON - } else if (key === 'content') { - params[key] = fuzzBoolean() ? fuzzSurrogatePairString(100) : fuzzJSON(2); - } else { - params[key] = fuzzString(); - } - } - }); + const targets = [ + "buildVideoPostEvent", + "buildViewEvent", + "buildCommentEvent", + "buildReactionEvent", + "buildZapRequestEvent", + "sanitizeAdditionalTags", + "validateEventStructure" + ]; - return params; -} + const targetName = rng.oneOf(targets); + let inputs = {}; -function fuzzBuilder(name, builderFn, argsGenerator) { - for (let i = 0; i < ITERATIONS; i++) { - const args = argsGenerator(); - try { - if (Array.isArray(args)) { - builderFn(...args); - } else { - builderFn(args); - } - } catch (error) { - logError(name, args, error); - } - } -} + // Helper to generate a comprehensive random params object + const genParams = () => { + // 50% chance of being undefined/null/non-object to test robustness + if (Math.random() < 0.1) return rng.nastyString(); + if (Math.random() < 0.1) return null; -console.log('Starting fuzzing for nostrEventSchemas.js...'); + // Otherwise generate a rich object + const params = { + pubkey: rng.mixedString(64), + created_at: rng.bool() ? rng.int(0, 2000000000) : rng.nastyString(), + content: rng.bool() ? rng.mixedString(1000) : rng.recursiveObject(2), + additionalTags: rng.bool() ? rng.array(() => rng.array(() => rng.mixedString(20), 5), 10) : rng.nastyString(), + }; -// Fuzz all builders -const builders = [ - { name: 'buildVideoPostEvent', fn: buildVideoPostEvent }, - { name: 'buildHttpAuthEvent', fn: buildHttpAuthEvent }, - { name: 'buildReportEvent', fn: buildReportEvent }, - { name: 'buildVideoMirrorEvent', fn: buildVideoMirrorEvent }, - { name: 'buildRepostEvent', fn: buildRepostEvent }, - { name: 'buildShareEvent', fn: buildShareEvent }, - { name: 'buildRelayListEvent', fn: buildRelayListEvent }, - { name: 'buildDmRelayListEvent', fn: buildDmRelayListEvent }, - { name: 'buildProfileMetadataEvent', fn: buildProfileMetadataEvent }, - { name: 'buildMuteListEvent', fn: buildMuteListEvent }, - { name: 'buildDeletionEvent', fn: buildDeletionEvent }, - { name: 'buildLegacyDirectMessageEvent', fn: buildLegacyDirectMessageEvent }, - { name: 'buildDmAttachmentEvent', fn: buildDmAttachmentEvent }, - { name: 'buildDmReadReceiptEvent', fn: buildDmReadReceiptEvent }, - { name: 'buildDmTypingIndicatorEvent', fn: buildDmTypingIndicatorEvent }, - { name: 'buildViewEvent', fn: buildViewEvent }, - { name: 'buildZapRequestEvent', fn: buildZapRequestEvent }, - { name: 'buildReactionEvent', fn: buildReactionEvent }, - { name: 'buildCommentEvent', fn: buildCommentEvent }, - { name: 'buildWatchHistoryEvent', fn: buildWatchHistoryEvent }, - { name: 'buildSubscriptionListEvent', fn: buildSubscriptionListEvent }, - { name: 'buildBlockListEvent', fn: buildBlockListEvent }, - { name: 'buildHashtagPreferenceEvent', fn: buildHashtagPreferenceEvent }, -]; + // Add specific fields that might be used by specific builders + const extraFields = [ + "dTagValue", "url", "magnet", "thumbnail", "description", "mode", "videoRootId", + "pointerValue", "pointerTag", "pointerTags", "dedupeTag", "includeSessionTag", + "videoEventId", "videoEventRelay", "videoDefinitionAddress", "rootIdentifier", + "parentCommentId", "rootKind", "rootAuthorPubkey", "parentKind", + "recipientPubkey", "relays", "amountSats", "lnurl", "eventId", "coordinate", + "targetPointer", "targetAuthorPubkey" + ]; -builders.forEach(({ name, fn }) => { - fuzzBuilder(name, fn, () => [generateRandomParams()]); -}); - -// Special case for buildAdminListEvent -fuzzBuilder('buildAdminListEvent', buildAdminListEvent, () => { - const listKey = pickOne(['moderation', 'editors', 'whitelist', 'blacklist', fuzzString()]); - return [listKey, generateRandomParams()]; -}); - -// Fuzz validateEventStructure -fuzzBuilder('validateEventStructure', validateEventStructure, () => { - const type = pickOne([...Object.values(NOTE_TYPES), fuzzString()]); - const event = fuzzJSON(2); // Random object/JSON - return [type, event]; -}); - -// Fuzz sanitizeAdditionalTags -fuzzBuilder('sanitizeAdditionalTags', sanitizeAdditionalTags, () => { - // Random input: array of arrays, array of strings, random object, etc. - if (fuzzBoolean()) { - // structured but potentially malformed tags - const tags = []; - const count = fuzzInt(0, 10); - for (let i = 0; i < count; i++) { - if (fuzzBoolean()) { - const tag = [fuzzString()]; - const args = fuzzInt(0, 5); - for (let j = 0; j < args; j++) { - tag.push(fuzzBoolean() ? fuzzString() : fuzzJSON(1)); + extraFields.forEach(field => { + if (rng.bool()) { + params[field] = rng.mixedString(50); + } else if (rng.bool()) { + params[field] = rng.recursiveObject(1); // Potentially invalid type } - tags.push(tag); - } else { - tags.push(pickOne([null, undefined, 123, {}, "string"])); - } - } - return [tags]; - } - // completely random input - return [fuzzJSON(2)]; -}); + }); -saveFuzzReport('nostrEventSchemas', FINDINGS); -console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`); -if (FINDINGS.length > 0) { - process.exit(1); + return params; + }; + + switch (targetName) { + case "buildVideoPostEvent": + inputs = genParams(); + Schemas.buildVideoPostEvent(inputs); + break; + + case "buildViewEvent": + inputs = genParams(); + Schemas.buildViewEvent(inputs); + break; + + case "buildCommentEvent": + inputs = genParams(); + Schemas.buildCommentEvent(inputs); + break; + + case "buildReactionEvent": + inputs = genParams(); + Schemas.buildReactionEvent(inputs); + break; + + case "buildZapRequestEvent": + inputs = genParams(); + Schemas.buildZapRequestEvent(inputs); + break; + + case "sanitizeAdditionalTags": + // sanitizeAdditionalTags expects an array of tags (arrays) + if (rng.bool()) { + inputs = rng.array(() => rng.array(() => rng.mixedString(20), 5), 20); + } else { + inputs = rng.nastyString(); // or object, etc. + } + Schemas.sanitizeAdditionalTags(inputs); + break; + + case "validateEventStructure": + const type = rng.oneOf(Object.values(Schemas.NOTE_TYPES)); + let event = genParams(); // repurpose genParams to make a fake event + // Ensure event is an object for this specific test setup, unless we want to fuzz that too. + // validateEventStructure checks: if (!event || typeof event !== "object") return { valid: false ... } + // So passing a string is valid input for the function (should not crash), + // but my fuzzer crashes trying to assign .kind to it. + if (event && typeof event === 'object') { + event.kind = rng.int(0, 50000); + event.tags = rng.array(() => rng.array(() => rng.mixedString(10), 3), 5); + } + inputs = { type, event }; + Schemas.validateEventStructure(type, event); + break; + } + + return { target: targetName, inputs }; } + +runFuzzer("nostrEventSchemas", 5000, fuzzTest).catch(err => { + console.error("Fatal fuzzer error:", err); + process.exit(1); +}); diff --git a/scripts/agent/fuzz-shared.mjs b/scripts/agent/fuzz-shared.mjs index f968bb05..28f6f165 100644 --- a/scripts/agent/fuzz-shared.mjs +++ b/scripts/agent/fuzz-shared.mjs @@ -5,133 +5,187 @@ import { fileURLToPath } from "url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const REPRODUCERS_DIR = path.resolve(__dirname, "../../examples/reproducers"); -const ARTIFACTS_DIR = path.resolve(__dirname, "../../artifacts"); + +const REPRODUCER_DIR = path.join(process.cwd(), "examples/reproducers"); +const ARTIFACTS_DIR = path.join(process.cwd(), "artifacts"); // Ensure directories exist -if (!fs.existsSync(REPRODUCERS_DIR)) { - fs.mkdirSync(REPRODUCERS_DIR, { recursive: true }); +if (!fs.existsSync(REPRODUCER_DIR)) { + fs.mkdirSync(REPRODUCER_DIR, { recursive: true }); } if (!fs.existsSync(ARTIFACTS_DIR)) { fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); } -export function randomInt(min, max) { - return Math.floor(Math.random() * (max - min + 1)) + min; -} +export const rng = { + bool: () => Math.random() > 0.5, + int: (min = 0, max = 100) => Math.floor(Math.random() * (max - min + 1)) + min, + float: (min = 0, max = 100) => Math.random() * (max - min) + min, + oneOf: (arr) => arr[Math.floor(Math.random() * arr.length)], -export function randomString(length) { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}|:<>?~`-=[]\\;',./"; - let result = ""; - for (let i = 0; i < length; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return result; -} - -export function randomHex(length) { - const chars = "0123456789abcdef"; - let result = ""; - for (let i = 0; i < length; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return result; -} - -export function randomBoolean() { - return Math.random() < 0.5; -} - -export function randomItem(array) { - return array[Math.floor(Math.random() * array.length)]; -} - -export function randomJSON(depth = 3, maxFields = 5) { - if (depth <= 0) { - const type = randomInt(0, 3); - if (type === 0) return randomString(10); - if (type === 1) return randomInt(-1000, 1000); - if (type === 2) return randomBoolean(); - return null; - } - - const type = randomInt(0, 5); - if (type === 0) { - // Array - const arr = []; - const len = randomInt(0, maxFields); + string: (maxLength = 100) => { + const len = Math.floor(Math.random() * maxLength); + const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; for (let i = 0; i < len; i++) { - arr.push(randomJSON(depth - 1, maxFields)); + result += chars.charAt(Math.floor(Math.random() * chars.length)); } - return arr; - } else if (type === 1) { - // Object - const obj = {}; - const len = randomInt(0, maxFields); + return result; + }, + + nastyString: () => { + const candidates = [ + "", + "null", + "undefined", + "NaN", + "\x00", + "\n", + "\r", + "\t", + " ", + " ", + "https://example.com", + "wss://relay.example.com", + "", + "' OR 1=1 --", + "javascript:void(0)", + "data:text/plain;base64,SGVsbG8=", + "😀", + "ñ", + "\uD83D\uDCA9", // Pile of poo + "\uD800", // Lone surrogate (high) + "\uDFFF", // Lone surrogate (low) + "A".repeat(1000), // Long string + "A".repeat(10000), // Very long string + JSON.stringify({ foo: "bar" }), + "{}", + "[]" + ]; + return candidates[Math.floor(Math.random() * candidates.length)]; + }, + + mixedString: (maxLength = 100) => { + if (Math.random() < 0.3) return rng.nastyString(); + return rng.string(maxLength); + }, + + array: (generator, maxLen = 10) => { + const len = rng.int(0, maxLen); + const result = []; for (let i = 0; i < len; i++) { - obj[randomString(5)] = randomJSON(depth - 1, maxFields); + result.push(generator()); + } + return result; + }, + + object: (keyGen, valGen, maxKeys = 10) => { + const len = rng.int(0, maxKeys); + const result = {}; + for (let i = 0; i < len; i++) { + result[keyGen()] = valGen(); + } + return result; + }, + + recursiveObject: (depth = 3) => { + if (depth <= 0) return rng.mixedString(); + + const type = rng.int(0, 4); + if (type === 0) return rng.int(-1000, 1000); + if (type === 1) return rng.mixedString(); + if (type === 2) return rng.bool(); + if (type === 3) return null; + + if (type === 4) { + // Array + const len = rng.int(0, 5); + const arr = []; + for(let i=0; i 0) { + process.stdout.write("."); } } + process.stdout.write("\n"); - console.log("\n"); - const duration = (Date.now() - start) / 1000; - console.log(`[FUZZ] Finished ${name} in ${duration.toFixed(2)}s. Crashes: ${crashes.length}`); + stats.endTime = Date.now(); + stats.duration = stats.endTime - stats.startTime; - const reportPath = path.join(ARTIFACTS_DIR, `fuzz-report-${name}.json`); - const report = { - target: name, - iterations, - duration, - crashes - }; - fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); - console.log(`[FUZZ] Saved report to ${reportPath}`); + console.log(`[FUZZ] Finished. ${stats.passed} passed, ${stats.failed} failed.`); + saveReport(targetName, stats); + + return stats; } diff --git a/scripts/agent/mocks/watchHistory.js b/scripts/agent/mocks/watchHistory.js new file mode 100644 index 00000000..ac73c958 --- /dev/null +++ b/scripts/agent/mocks/watchHistory.js @@ -0,0 +1,6 @@ + +export function normalizeActorKey(key) { + if (typeof key !== 'string') return ''; + // simple hex check or return as is for mocking + return key.toLowerCase().trim(); +}