From 28da9e1642614b5a864e54f0fe2ce60b33493512 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:31:38 +0000 Subject: [PATCH 1/2] Add fuzzing harnesses for Nostr schemas, DM decryptor, and magnet utils - Created `scripts/agent/fuzz-nostrEventSchemas.mjs`, `scripts/agent/fuzz-dmDecryptor.mjs`, and `scripts/agent/fuzz-magnetUtils.mjs`. - Utilized `scripts/agent/fuzz-lib.mjs` for standardized random input generation and reporting. - Removed legacy/duplicate fuzz scripts (`fuzz-nostr-schemas.mjs`, etc.) in favor of the new standardized ones. - Verified all fuzzers run successfully with no crashes using 5000 iterations. - Added huge string injection (1MB+) to stress test inputs. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- scripts/agent/fuzz-dm-decryptor.mjs | 84 ------ scripts/agent/fuzz-dmDecryptor.mjs | 155 +++++------ scripts/agent/fuzz-magnet-utils.mjs | 38 --- scripts/agent/fuzz-magnetUtils.mjs | 129 +++------- scripts/agent/fuzz-nostr-schemas.mjs | 84 ------ scripts/agent/fuzz-nostrEventSchemas.mjs | 312 +++++------------------ scripts/agent/fuzz-utils.mjs | 127 --------- 7 files changed, 181 insertions(+), 748 deletions(-) delete mode 100644 scripts/agent/fuzz-dm-decryptor.mjs delete mode 100644 scripts/agent/fuzz-magnet-utils.mjs delete mode 100644 scripts/agent/fuzz-nostr-schemas.mjs delete mode 100644 scripts/agent/fuzz-utils.mjs diff --git a/scripts/agent/fuzz-dm-decryptor.mjs b/scripts/agent/fuzz-dm-decryptor.mjs deleted file mode 100644 index 8ff7f983..00000000 --- a/scripts/agent/fuzz-dm-decryptor.mjs +++ /dev/null @@ -1,84 +0,0 @@ -import "./setup-test-env.js"; -import { Fuzzer } from "./fuzz-lib.mjs"; -import { decryptDM } from "../../js/dmDecryptor.js"; - -const fuzzer = new Fuzzer("dm-decryptor"); - -class MockDecryptor { - constructor(scheme, shouldFail = false) { - this.scheme = scheme; - this.shouldFail = shouldFail; - this.supportsGiftWrap = scheme === "nip44" || scheme === "nip44_v2"; - } - - async decrypt(pubkey, ciphertext) { - if (this.shouldFail) { - throw new Error("Decryption failed (mock)"); - } - // Return something that might be JSON or might be garbage - if (fuzzer.randBool()) { - // Return valid JSON event string (inner rumor) - return JSON.stringify({ - kind: 1, - content: "Hello world " + fuzzer.randString(10), - pubkey: fuzzer.randString(64, "0123456789abcdef"), - created_at: Math.floor(Date.now() / 1000), - tags: [] - }); - } else { - // Return garbage string - return fuzzer.randString(50); - } - } -} - -async function fuzzTest(fuzzer, state) { - const schemes = ["nip04", "nip44", "nip44_v2", "unknown"]; - - // Create decryptors - const decryptors = []; - const numDecryptors = fuzzer.randInt(0, 3); - for(let i=0; i fuzzer.randString(10), 1, 4)); - } - - // Context - const context = { - actorPubkey: fuzzer.randString(64, "0123456789abcdef"), - decryptors: decryptors - }; - - state.input = { - event, - context - }; - - try { - await decryptDM(event, context); - } catch (err) { - // We expect decryptDM to handle errors gracefully and return { ok: false, errors: ... } - // If it throws, it's a bug (except maybe argument validation if we pass null event) - throw err; - } -} - -fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error); diff --git a/scripts/agent/fuzz-dmDecryptor.mjs b/scripts/agent/fuzz-dmDecryptor.mjs index 5f713a9d..d236e2a8 100644 --- a/scripts/agent/fuzz-dmDecryptor.mjs +++ b/scripts/agent/fuzz-dmDecryptor.mjs @@ -1,82 +1,85 @@ +import "./setup-test-env.js"; +import { Fuzzer } from "./fuzz-lib.mjs"; +import { decryptDM } from "../../js/dmDecryptor.js"; -import fs from "fs"; -import path from "path"; -import { runFuzzer, rng } from "./fuzz-shared.mjs"; +const fuzzer = new Fuzzer("dmDecryptor"); -const originalPath = "../../js/dmDecryptor.js"; -const tempPath = "./temp_dmDecryptor.mjs"; +class MockDecryptor { + constructor(scheme, shouldFail = false) { + this.scheme = scheme; + this.shouldFail = shouldFail; + this.supportsGiftWrap = scheme === "nip44" || scheme === "nip44_v2"; + } -// Read original file -const originalContent = fs.readFileSync(new URL(originalPath, import.meta.url), "utf-8"); - -// 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";' -); - -// Write temp file -fs.writeFileSync(new URL(tempPath, import.meta.url), patchedContent); - -// Import the patched module -const DmDecryptor = await import(tempPath); - -async function fuzzTest(iteration) { - const genEvent = () => { - const tags = rng.array(() => rng.array(() => rng.mixedString(20), 5), 5); - if (rng.bool()) { - // Circular tag structure - const circularTag = ["p"]; - circularTag.push(circularTag); // Circular reference - tags.push(circularTag); + async decrypt(pubkey, ciphertext) { + if (this.shouldFail) { + throw new Error("Decryption failed (mock)"); } - 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 - }; - }; - - 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 event = rng.bool() ? genEvent() : rng.nastyString(); - const context = { - actorPubkey: rng.mixedString(64), - decryptors: rng.array(genDecryptor, 3) - }; - - // Run the target - await DmDecryptor.decryptDM(event, context); - - return { event, context }; + // Return something that might be JSON or might be garbage + if (fuzzer.randBool()) { + // Return valid JSON event string (inner rumor) + return JSON.stringify({ + kind: 1, + content: "Hello world " + fuzzer.randString(10), + pubkey: fuzzer.randString(64, "0123456789abcdef"), + created_at: Math.floor(Date.now() / 1000), + tags: [] + }); + } else { + // Return garbage string + return fuzzer.randString(50); + } + } } -runFuzzer("dmDecryptor", 10000, 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)); - } - }); +async function fuzzTest(fuzzer, state) { + const schemes = ["nip04", "nip44", "nip44_v2", "unknown", null]; + + // Create decryptors + const decryptors = []; + const numDecryptors = fuzzer.randInt(0, 3); + for(let i=0; i fuzzer.randString(10), 1, 4)); + } + + // Context + const context = { + actorPubkey: fuzzer.randString(64, "0123456789abcdef"), + decryptors: decryptors + }; + + state.input = { + event, + context + }; + + try { + await decryptDM(event, context); + } catch (err) { + // We expect decryptDM to handle errors gracefully and return { ok: false, errors: ... } + // If it throws, it's a bug (except maybe argument validation if we pass null event) + throw err; + } +} + +fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error); diff --git a/scripts/agent/fuzz-magnet-utils.mjs b/scripts/agent/fuzz-magnet-utils.mjs deleted file mode 100644 index fca6f43d..00000000 --- a/scripts/agent/fuzz-magnet-utils.mjs +++ /dev/null @@ -1,38 +0,0 @@ -import "./setup-test-env.js"; -import { Fuzzer } from "./fuzz-lib.mjs"; -import { normalizeAndAugmentMagnet } from "../../js/magnetUtils.js"; - -const fuzzer = new Fuzzer("magnet-utils"); - -async function fuzzTest(fuzzer, state) { - // Generate inputs - const rawValue = fuzzer.randBool() ? fuzzer.randString(100) : "magnet:?xt=urn:btih:" + fuzzer.randString(40, "0123456789abcdef"); - - // Inject garbage into magnet link - let malformedValue = rawValue; - if (fuzzer.randBool()) { - // url encode some chars - malformedValue = encodeURIComponent(rawValue); - } - - const options = { - webSeed: fuzzer.randBool() ? fuzzer.randString(50) : [fuzzer.randString(50)], - torrentUrl: fuzzer.randString(50), - xs: fuzzer.randString(50), - extraTrackers: fuzzer.randArray(() => fuzzer.randString(30), 0, 3), - appProtocol: fuzzer.randBool() ? "http:" : "https:" - }; - - state.input = { - rawValue: malformedValue, - options - }; - - try { - normalizeAndAugmentMagnet(malformedValue, options); - } catch (err) { - throw err; - } -} - -fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error); diff --git a/scripts/agent/fuzz-magnetUtils.mjs b/scripts/agent/fuzz-magnetUtils.mjs index 342930b1..4c6add4d 100644 --- a/scripts/agent/fuzz-magnetUtils.mjs +++ b/scripts/agent/fuzz-magnetUtils.mjs @@ -1,107 +1,44 @@ +import "./setup-test-env.js"; +import { Fuzzer } from "./fuzz-lib.mjs"; +import { normalizeAndAugmentMagnet } from "../../js/magnetUtils.js"; -import { runFuzzer, rng } from "./fuzz-shared.mjs"; +const fuzzer = new Fuzzer("magnetUtils"); -// Polyfills -global.window = { - location: { - protocol: "https:" +async function fuzzTest(fuzzer, state) { + // Generate inputs + const rawValue = fuzzer.randBool() ? fuzzer.randString(100) : "magnet:?xt=urn:btih:" + fuzzer.randString(40, "0123456789abcdef"); + + // Inject garbage into magnet link + let malformedValue = rawValue; + if (fuzzer.randBool()) { + // url encode some chars + malformedValue = encodeURIComponent(rawValue); } -}; -// Import the target module -// Note: We are in scripts/agent/ -import * as MagnetUtils from "../../js/magnetUtils.js"; + if (fuzzer.randBool()) { + // Inject huge string + malformedValue += fuzzer.randString(10000); + } -async function fuzzTest(iteration) { - // Randomize window protocol sometimes - global.window.location.protocol = rng.bool() ? "https:" : "http:"; - - const genMagnet = () => { - if (rng.bool()) return rng.mixedString(100); // Random garbage - - // Construct semi-valid magnet - let magnet = "magnet:?"; - const params = []; - - // xt (Exact Topic) - if (rng.bool()) { - const hash = rng.bool() ? rng.mixedString(40) : "urn:btih:" + rng.mixedString(40); - params.push(`xt=${hash}`); - } - - // dn (Display Name) - if (rng.bool()) { - const val = rng.mixedString(20); - try { - params.push(`dn=${encodeURIComponent(val)}`); - } catch { - params.push(`dn=${val}`); - } - } - - // tr (Trackers) - if (rng.bool()) { - const trackers = rng.array(() => { - const proto = rng.oneOf(["udp", "http", "https", "wss", "ws"]); - const val = `${proto}://${rng.mixedString(20)}`; - try { - return encodeURIComponent(val); - } catch { - return val; - } - }, 5); - trackers.forEach(t => params.push(`tr=${t}`)); - } - - // ws (Web Seeds) - if (rng.bool()) { - const val = "https://" + rng.mixedString(20); - try { - params.push(`ws=${encodeURIComponent(val)}`); - } catch { - params.push(`ws=${val}`); - } - } - - // Random params - if (rng.bool()) { - params.push(`${rng.string(5)}=${rng.mixedString(10)}`); - } - - magnet += params.join("&"); - - // Corrupt it - if (Math.random() < 0.2) { - // Inject garbage - const pos = rng.int(0, magnet.length); - magnet = magnet.slice(0, pos) + rng.nastyString() + magnet.slice(pos); - } - - return magnet; + const options = { + webSeed: fuzzer.randBool() ? fuzzer.randString(50) : [fuzzer.randString(50)], + torrentUrl: fuzzer.randString(50), + xs: fuzzer.randString(50), + extraTrackers: fuzzer.randArray(() => fuzzer.randString(30), 0, 3), + appProtocol: fuzzer.randBool() ? "http:" : "https:", + logger: () => {} }; - const rawValue = genMagnet(); - - 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 - }; + state.input = { + rawValue: malformedValue, + options }; - const options = genOptions(); - - MagnetUtils.normalizeAndAugmentMagnet(rawValue, options); - - return { rawValue, options }; + try { + normalizeAndAugmentMagnet(malformedValue, options); + } catch (err) { + throw err; + } } -runFuzzer("magnetUtils", 10000, fuzzTest).catch(err => { - console.error("Fatal fuzzer error:", err); - process.exit(1); -}); +fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error); diff --git a/scripts/agent/fuzz-nostr-schemas.mjs b/scripts/agent/fuzz-nostr-schemas.mjs deleted file mode 100644 index 17450f47..00000000 --- a/scripts/agent/fuzz-nostr-schemas.mjs +++ /dev/null @@ -1,84 +0,0 @@ -import "./setup-test-env.js"; -import { Fuzzer } from "./fuzz-lib.mjs"; -import * as schemas from "../../js/nostrEventSchemas.js"; - -const fuzzer = new Fuzzer("nostr-schemas"); - -// Mappers for build functions -const BUILDERS = [ - { name: "buildVideoPostEvent", fn: schemas.buildVideoPostEvent }, - { name: "buildVideoMirrorEvent", fn: schemas.buildVideoMirrorEvent }, - { name: "buildRepostEvent", fn: schemas.buildRepostEvent }, - { name: "buildRelayListEvent", fn: schemas.buildRelayListEvent }, - { name: "buildDmRelayListEvent", fn: schemas.buildDmRelayListEvent }, - { name: "buildProfileMetadataEvent", fn: schemas.buildProfileMetadataEvent }, - { name: "buildMuteListEvent", fn: schemas.buildMuteListEvent }, - { name: "buildDmAttachmentEvent", fn: schemas.buildDmAttachmentEvent }, - { name: "buildDmReadReceiptEvent", fn: schemas.buildDmReadReceiptEvent }, - { name: "buildDmTypingIndicatorEvent", fn: schemas.buildDmTypingIndicatorEvent }, - { name: "buildViewEvent", fn: schemas.buildViewEvent }, - { name: "buildZapRequestEvent", fn: schemas.buildZapRequestEvent }, - { name: "buildReactionEvent", fn: schemas.buildReactionEvent }, - { name: "buildCommentEvent", fn: schemas.buildCommentEvent }, - { name: "buildWatchHistoryEvent", fn: schemas.buildWatchHistoryEvent }, - { name: "buildSubscriptionListEvent", fn: schemas.buildSubscriptionListEvent }, - { name: "buildBlockListEvent", fn: schemas.buildBlockListEvent }, - { name: "buildHashtagPreferenceEvent", fn: schemas.buildHashtagPreferenceEvent }, -]; - -async function fuzzTest(fuzzer, state) { - // 1. Fuzz Builder Functions - const builder = fuzzer.pick(BUILDERS); - - // Generate random params - const params = fuzzer.randJSON(); - state.input = { - target: builder.name, - params: params - }; - - try { - // Some builders require specific args like buildAdminListEvent(listKey, params) - // but most take a single params object. - // We didn't include buildAdminListEvent in the list above because it takes 2 args. - - const event = builder.fn(params); - - // 2. Validate the generated event - if (event && event.kind) { - // Find schema type for this kind to validate - let type = null; - for (const [key, val] of Object.entries(schemas.NOTE_TYPES)) { - const schema = schemas.getNostrEventSchema(val); - if (schema && schema.kind === event.kind) { - type = val; - break; - } - } - - if (type) { - schemas.validateEventAgainstSchema(type, event); - } - } - } catch (err) { - throw err; - } - - // 3. Fuzz Validation directly with random events - if (fuzzer.randBool()) { - const types = Object.values(schemas.NOTE_TYPES); - const type = fuzzer.pick(types); - const randomEvent = fuzzer.randJSON(); - - state.input = { - target: "validateEventAgainstSchema", - type: type, - event: randomEvent - }; - - schemas.validateEventAgainstSchema(type, randomEvent); - } -} - -// Run for 5000 iterations -fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error); diff --git a/scripts/agent/fuzz-nostrEventSchemas.mjs b/scripts/agent/fuzz-nostrEventSchemas.mjs index 073e9046..b4fbe321 100644 --- a/scripts/agent/fuzz-nostrEventSchemas.mjs +++ b/scripts/agent/fuzz-nostrEventSchemas.mjs @@ -1,254 +1,80 @@ +import "./setup-test-env.js"; +import { Fuzzer } from "./fuzz-lib.mjs"; +import * as schemas from "../../js/nostrEventSchemas.js"; -import { runFuzzer, rng } from "./fuzz-shared.mjs"; -import { TextEncoder, TextDecoder } from "util"; +const fuzzer = new Fuzzer("nostrEventSchemas"); -// Polyfills -global.TextEncoder = TextEncoder; -global.TextDecoder = TextDecoder; -global.window = { - bitvidNostrEventOverrides: {} -}; +// Mappers for build functions +const BUILDERS = Object.keys(schemas) + .filter(k => k.startsWith('build')) + .map(k => ({ name: k, fn: schemas[k] })); -// Import the target module -// Note: We are in scripts/agent/ -import * as Schemas from "../../js/nostrEventSchemas.js"; +async function fuzzTest(fuzzer, state) { + // 1. Fuzz Builder Functions + const builder = fuzzer.pick(BUILDERS); -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. + // Generate random params + const params = fuzzer.randJSON(); - const targets = [ - "buildVideoPostEvent", - "buildViewEvent", - "buildCommentEvent", - "buildReactionEvent", - "buildZapRequestEvent", - "sanitizeAdditionalTags", - "validateEventStructure", - "setNostrEventSchemaOverrides", - "buildRepostEvent", - "buildShareEvent", - "buildRelayListEvent", - "buildDmRelayListEvent", - "buildProfileMetadataEvent", - "buildMuteListEvent", - "buildDeletionEvent", - "buildLegacyDirectMessageEvent", - "buildDmAttachmentEvent", - "buildDmReadReceiptEvent", - "buildDmTypingIndicatorEvent", - "buildWatchHistoryEvent", - "buildSubscriptionListEvent", - "buildBlockListEvent", - "buildHashtagPreferenceEvent", - "buildAdminListEvent", - "buildHttpAuthEvent", - "buildReportEvent", - "buildVideoMirrorEvent" - ]; - - const targetName = rng.oneOf(targets); - let inputs = {}; - - // Helper to generate a comprehensive random params object - const genParams = () => { - // 10% 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; - - // 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(), - }; - - // 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", - // Added fields - "repostKind", "targetKind", "targetEvent", "serializedEvent", - "video", "metadata", "pTags", "encrypted", "encryptionTag", - "eventIds", "addresses", "reason", "ciphertext", "attachment", - "messageKind", "expiresAt", "monthIdentifier", "hexPubkeys", - "payload", "reportType", "address", "addressRelay", "authorPubkey", - "eventRelay", "method", "userId", "relayHint" - ]; - - extraFields.forEach(field => { - if (rng.bool()) { - params[field] = rng.mixedString(50); - } else if (rng.bool()) { - if (field === "targetEvent" || field === "video" || field === "attachment" || field === "metadata") { - params[field] = rng.recursiveObject(2); - } else if (field === "relays" || field === "pTags" || field === "eventIds" || field === "addresses" || field === "hexPubkeys") { - params[field] = rng.array(() => rng.mixedString(64), 5); - } else { - params[field] = rng.recursiveObject(1); // Potentially invalid type - } - } - }); - - 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); + // Inject huge strings occasionally + if (Math.random() < 0.05) { + if (!params || typeof params !== 'object') { + // make it an object } else { - inputs = rng.nastyString(); // or object, etc. + // add huge string field + params.hugeField = "A".repeat(1000000); // 1MB } - 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; - - case "setNostrEventSchemaOverrides": - const randomType = rng.oneOf(Object.values(Schemas.NOTE_TYPES)); - if (rng.bool()) { - // Circular object - const obj = { foo: "bar" }; - obj.self = obj; - inputs = { [randomType]: obj }; - } else { - inputs = { [randomType]: genParams() }; - } - // If we set overrides, we should also trigger usage of them to see if it crashes later - Schemas.setNostrEventSchemaOverrides(inputs); - // Trigger usage - Schemas.getNostrEventSchema(randomType); - break; - - case "buildRepostEvent": - inputs = genParams(); - Schemas.buildRepostEvent(inputs); - break; - case "buildShareEvent": - inputs = genParams(); - Schemas.buildShareEvent(inputs); - break; - case "buildRelayListEvent": - inputs = genParams(); - Schemas.buildRelayListEvent(inputs); - break; - case "buildDmRelayListEvent": - inputs = genParams(); - Schemas.buildDmRelayListEvent(inputs); - break; - case "buildProfileMetadataEvent": - inputs = genParams(); - Schemas.buildProfileMetadataEvent(inputs); - break; - case "buildMuteListEvent": - inputs = genParams(); - Schemas.buildMuteListEvent(inputs); - break; - case "buildDeletionEvent": - inputs = genParams(); - Schemas.buildDeletionEvent(inputs); - break; - case "buildLegacyDirectMessageEvent": - inputs = genParams(); - Schemas.buildLegacyDirectMessageEvent(inputs); - break; - case "buildDmAttachmentEvent": - inputs = genParams(); - Schemas.buildDmAttachmentEvent(inputs); - break; - case "buildDmReadReceiptEvent": - inputs = genParams(); - Schemas.buildDmReadReceiptEvent(inputs); - break; - case "buildDmTypingIndicatorEvent": - inputs = genParams(); - Schemas.buildDmTypingIndicatorEvent(inputs); - break; - case "buildWatchHistoryEvent": - inputs = genParams(); - Schemas.buildWatchHistoryEvent(inputs); - break; - case "buildSubscriptionListEvent": - inputs = genParams(); - Schemas.buildSubscriptionListEvent(inputs); - break; - case "buildBlockListEvent": - inputs = genParams(); - Schemas.buildBlockListEvent(inputs); - break; - case "buildHashtagPreferenceEvent": - inputs = genParams(); - Schemas.buildHashtagPreferenceEvent(inputs); - break; - case "buildAdminListEvent": - inputs = genParams(); - // buildAdminListEvent(listKey, params) - const listKey = rng.oneOf(["moderation", "editors", "whitelist", "blacklist", "communityBlacklistSources", rng.nastyString()]); - Schemas.buildAdminListEvent(listKey, inputs); - break; - case "buildHttpAuthEvent": - inputs = genParams(); - Schemas.buildHttpAuthEvent(inputs); - break; - case "buildReportEvent": - inputs = genParams(); - Schemas.buildReportEvent(inputs); - break; - case "buildVideoMirrorEvent": - inputs = genParams(); - Schemas.buildVideoMirrorEvent(inputs); - break; } - return { target: targetName, inputs }; + state.input = { + target: builder.name, + params: params + }; + + try { + let args = [params]; + if (builder.name === 'buildAdminListEvent') { + const listKeys = ["moderation", "editors", "whitelist", "blacklist", "invalid", null]; + args = [fuzzer.pick(listKeys), params]; + } + + const event = builder.fn(...args); + + // 2. Validate the generated event + if (event && event.kind) { + // Find schema type for this kind to validate + let type = null; + for (const [key, val] of Object.entries(schemas.NOTE_TYPES)) { + const schema = schemas.getNostrEventSchema(val); + if (schema && schema.kind === event.kind) { + type = val; + break; + } + } + + if (type) { + schemas.validateEventAgainstSchema(type, event); + } + } + } catch (err) { + throw err; + } + + // 3. Fuzz Validation directly with random events + if (fuzzer.randBool()) { + const types = Object.values(schemas.NOTE_TYPES); + const type = fuzzer.pick(types); + const randomEvent = fuzzer.randJSON(); + + state.input = { + target: "validateEventStructure", + type: type, + event: randomEvent + }; + + schemas.validateEventStructure(type, randomEvent); + } } -runFuzzer("nostrEventSchemas", 10000, fuzzTest).catch(err => { - console.error("Fatal fuzzer error:", err); - process.exit(1); -}); +// Run for 5000 iterations +fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error); diff --git a/scripts/agent/fuzz-utils.mjs b/scripts/agent/fuzz-utils.mjs deleted file mode 100644 index cf7b25eb..00000000 --- a/scripts/agent/fuzz-utils.mjs +++ /dev/null @@ -1,127 +0,0 @@ -import fs from 'fs'; -import path from 'path'; - -export function fuzzBoolean() { - return Math.random() < 0.5; -} - -export function fuzzInt(min = 0, max = 100) { - return Math.floor(Math.random() * (max - min + 1)) + min; -} - -export function pickOne(options) { - if (!Array.isArray(options) || options.length === 0) return undefined; - return options[Math.floor(Math.random() * options.length)]; -} - -export function fuzzString(length = 10) { - 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 fuzzHexString(length = 64) { - 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 fuzzSurrogatePairString(length = 10) { - // Generate a string that might contain unpaired surrogates - let result = ''; - for (let i = 0; i < length; i++) { - if (Math.random() < 0.1) { - // High surrogate - result += String.fromCharCode(fuzzInt(0xD800, 0xDBFF)); - } else if (Math.random() < 0.1) { - // Low surrogate - result += String.fromCharCode(fuzzInt(0xDC00, 0xDFFF)); - } else { - result += fuzzString(1); - } - } - return result; -} - -export function fuzzJSON(depth = 2) { - if (depth <= 0) { - const type = pickOne(['string', 'number', 'boolean', 'null']); - switch (type) { - case 'string': return fuzzString(); - case 'number': return fuzzInt(); - case 'boolean': return fuzzBoolean(); - case 'null': return null; - } - } - - const type = pickOne(['object', 'array']); - if (type === 'object') { - const obj = {}; - const keys = fuzzInt(1, 5); - for (let i = 0; i < keys; i++) { - obj[fuzzString(5)] = fuzzJSON(depth - 1); - } - return obj; - } else { - const arr = []; - const len = fuzzInt(1, 5); - for (let i = 0; i < len; i++) { - arr.push(fuzzJSON(depth - 1)); - } - return arr; - } -} - -export function saveFuzzReport(targetName, findings) { - const reportDir = 'artifacts'; - if (!fs.existsSync(reportDir)) { - fs.mkdirSync(reportDir, { recursive: true }); - } - const reportPath = path.join(reportDir, `fuzz-report-${targetName}.json`); - const report = { - target: targetName, - date: new Date().toISOString(), - findings: findings - }; - fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); - console.log(`Fuzz report saved to ${reportPath}`); -} - -export function saveReproducer(targetName, id, input, error) { - const reproDir = path.join('examples', 'reproducers', `fuzz-${targetName}-${id}`); - if (!fs.existsSync(reproDir)) { - fs.mkdirSync(reproDir, { recursive: true }); - } - - const readmeContent = `# Fuzz Reproducer: ${targetName} - ${id} - -## Error -\`\`\` -${error.stack || error.message} -\`\`\` - -## Input -See \`input.json\` -`; - - fs.writeFileSync(path.join(reproDir, 'README.md'), readmeContent); - fs.writeFileSync(path.join(reproDir, 'input.json'), JSON.stringify(input, null, 2)); - - // Create a minimal reproduction script - const scriptContent = ` -import fs from 'fs'; -// You would need to adjust imports based on the target -// This is a placeholder template -const input = JSON.parse(fs.readFileSync('input.json', 'utf8')); -console.log('Running reproduction with input:', input); -`; - fs.writeFileSync(path.join(reproDir, 'repro.mjs'), scriptContent); - - console.log(`Reproducer saved to ${reproDir}`); -} From 0669f779cfc354101baff7b9ba8e440dffa397b7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:50:25 +0000 Subject: [PATCH 2/2] Fix CI deploy failure by reverting tracked dist changes and cleaning scripts - Reverted accidental changes to `dist/` directory to prevent deployment conflicts. - Ensured `scripts/agent/fuzz-lib.mjs` and `scripts/agent/setup-test-env.js` are correctly tracked. - Removed legacy fuzz scripts (`fuzz-nostr-schemas.mjs`, etc.) and verified new camelCase naming. - Verified local build (`npm run build`) passes. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com>