From a9af9a9a436d715853c0d83bbf839cf13e87083a Mon Sep 17 00:00:00 2001
From: taherd <183945978+taherdhanera@users.noreply.github.com>
Date: Sat, 23 May 2026 00:48:02 +0530
Subject: [PATCH] Add webhook payload redaction guard
---
webhook-payload-redaction-guard/README.md | 34 +++
webhook-payload-redaction-guard/demo-video.js | 173 +++++++++++
webhook-payload-redaction-guard/demo.js | 18 ++
webhook-payload-redaction-guard/index.js | 282 ++++++++++++++++++
webhook-payload-redaction-guard/package.json | 14 +
.../reports/demo.webm | Bin 0 -> 80997 bytes
.../reports/reviewer-packet.md | 61 ++++
.../reports/summary.json | 183 ++++++++++++
.../reports/summary.svg | 16 +
.../requirements-map.md | 17 ++
.../sample-data.js | 79 +++++
webhook-payload-redaction-guard/test.js | 75 +++++
12 files changed, 952 insertions(+)
create mode 100644 webhook-payload-redaction-guard/README.md
create mode 100644 webhook-payload-redaction-guard/demo-video.js
create mode 100644 webhook-payload-redaction-guard/demo.js
create mode 100644 webhook-payload-redaction-guard/index.js
create mode 100644 webhook-payload-redaction-guard/package.json
create mode 100644 webhook-payload-redaction-guard/reports/demo.webm
create mode 100644 webhook-payload-redaction-guard/reports/reviewer-packet.md
create mode 100644 webhook-payload-redaction-guard/reports/summary.json
create mode 100644 webhook-payload-redaction-guard/reports/summary.svg
create mode 100644 webhook-payload-redaction-guard/requirements-map.md
create mode 100644 webhook-payload-redaction-guard/sample-data.js
create mode 100644 webhook-payload-redaction-guard/test.js
diff --git a/webhook-payload-redaction-guard/README.md b/webhook-payload-redaction-guard/README.md
new file mode 100644
index 00000000..88c14ac7
--- /dev/null
+++ b/webhook-payload-redaction-guard/README.md
@@ -0,0 +1,34 @@
+# Webhook Payload Redaction Guard
+
+Self-contained Enterprise Tooling slice for `SCIBASE-AI/SCIBASE.AI#19`.
+
+The guard validates outbound institutional webhook/API payloads before delivery.
+It checks schema allowlists, private-project field leakage, PII and direct
+identifier exposure, private storage links, data-residency routing, event
+signature metadata, and dataset access safety. It emits deterministic event
+decisions so unsafe payloads are blocked or redacted before institutional sync.
+
+This is intentionally separate from webhook replay ledgers, admin notification
+escalation, connector certification, API change governance, data export approval,
+deposit reconciliation, SCIM/HRIS deprovisioning, LMS roster passback, usage
+cost allocation, incident response, data residency policy, and secret rotation
+slices. Its job is outbound payload minimization and redaction before delivery.
+
+## Run
+
+```bash
+npm run check
+npm test
+npm run demo
+npm run demo:video
+```
+
+## Outputs
+
+- `reports/summary.json`
+- `reports/reviewer-packet.md`
+- `reports/summary.svg`
+- `reports/demo.webm`
+
+All data is synthetic. The module does not call live webhook delivery,
+repository sync, LMS sync, identity, storage, or external provider systems.
diff --git a/webhook-payload-redaction-guard/demo-video.js b/webhook-payload-redaction-guard/demo-video.js
new file mode 100644
index 00000000..1346a1f8
--- /dev/null
+++ b/webhook-payload-redaction-guard/demo-video.js
@@ -0,0 +1,173 @@
+const fs = require("fs");
+const os = require("os");
+const path = require("path");
+const { execFileSync } = require("child_process");
+
+const reportDir = path.join(__dirname, "reports");
+const outputPath = path.join(reportDir, "demo.webm");
+
+const chromeCandidates = [
+ process.env.CHROME_PATH,
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
+ "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
+ "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe"
+].filter(Boolean);
+
+function findBrowser() {
+ const found = chromeCandidates.find((candidate) => fs.existsSync(candidate));
+ if (!found) {
+ throw new Error("Chrome or Edge was not found. Set CHROME_PATH to generate reports/demo.webm.");
+ }
+ return found;
+}
+
+function fileUrl(filePath) {
+ return `file:///${filePath.replace(/\\/g, "/")}`;
+}
+
+const html = String.raw`
+
+
+
+ Webhook payload redaction guard demo
+
+
+
+
+ recording
+
+
+`;
+
+fs.mkdirSync(reportDir, { recursive: true });
+
+const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "webhook-redaction-demo-"));
+const htmlPath = path.join(tempDir, "demo.html");
+const profileDir = path.join(tempDir, "profile");
+fs.writeFileSync(htmlPath, html, "utf8");
+
+const stdout = execFileSync(
+ findBrowser(),
+ [
+ "--headless=new",
+ "--disable-gpu",
+ "--disable-dev-shm-usage",
+ "--autoplay-policy=no-user-gesture-required",
+ "--run-all-compositor-stages-before-draw",
+ "--virtual-time-budget=7500",
+ `--user-data-dir=${profileDir}`,
+ "--dump-dom",
+ fileUrl(htmlPath)
+ ],
+ { encoding: "utf8", maxBuffer: 30 * 1024 * 1024 }
+);
+
+const match = stdout.match(/data:video\/webm;base64,([A-Za-z0-9+/=]+)/);
+if (!match) {
+ throw new Error(`Demo video generation failed. Browser output ended with: ${stdout.slice(-600)}`);
+}
+
+fs.writeFileSync(outputPath, Buffer.from(match[1], "base64"));
+console.log(`Generated ${path.relative(process.cwd(), outputPath)}`);
diff --git a/webhook-payload-redaction-guard/demo.js b/webhook-payload-redaction-guard/demo.js
new file mode 100644
index 00000000..97c3730c
--- /dev/null
+++ b/webhook-payload-redaction-guard/demo.js
@@ -0,0 +1,18 @@
+const fs = require("fs");
+const path = require("path");
+const { project } = require("./sample-data");
+const { buildReviewPacket, renderMarkdownReport, renderSvgSummary } = require("./index");
+
+const reportDir = path.join(__dirname, "reports");
+fs.mkdirSync(reportDir, { recursive: true });
+
+const packet = buildReviewPacket(project);
+
+fs.writeFileSync(path.join(reportDir, "summary.json"), `${JSON.stringify(packet, null, 2)}\n`, "utf8");
+fs.writeFileSync(path.join(reportDir, "reviewer-packet.md"), renderMarkdownReport(packet), "utf8");
+fs.writeFileSync(path.join(reportDir, "summary.svg"), renderSvgSummary(packet), "utf8");
+
+console.log(`Generated reports for ${packet.guard}`);
+console.log(`Decision: ${packet.decision}`);
+console.log(`Score: ${packet.score}`);
+console.log(`Findings: ${packet.findings.length}`);
diff --git a/webhook-payload-redaction-guard/index.js b/webhook-payload-redaction-guard/index.js
new file mode 100644
index 00000000..157fa913
--- /dev/null
+++ b/webhook-payload-redaction-guard/index.js
@@ -0,0 +1,282 @@
+const SEVERITY_WEIGHTS = {
+ critical: 34,
+ high: 20,
+ medium: 10,
+ low: 4
+};
+
+function addFinding(findings, severity, rule, message, action, refs = []) {
+ findings.push({ severity, rule, message, action, refs });
+}
+
+function isPlainObject(value) {
+ return value && typeof value === "object" && !Array.isArray(value);
+}
+
+function walkObject(value, visitor, path = []) {
+ if (!isPlainObject(value) && !Array.isArray(value)) {
+ return;
+ }
+ const entries = Array.isArray(value) ? value.entries() : Object.entries(value);
+ for (const [key, child] of entries) {
+ const nextPath = path.concat(String(key));
+ visitor(String(key), child, nextPath);
+ walkObject(child, visitor, nextPath);
+ }
+}
+
+function pathString(path) {
+ return path.join(".");
+}
+
+function evaluateEvent(event, project, findings) {
+ if (!project.policy.allowedEventTypes.includes(event.eventType)) {
+ addFinding(
+ findings,
+ "critical",
+ "webhook-event-type-not-allowlisted",
+ `${event.eventId} uses non-allowlisted event type ${event.eventType}.`,
+ "Block delivery until the event type is approved for the destination connector.",
+ [event.eventId, event.eventType]
+ );
+ }
+
+ for (const field of Object.keys(event)) {
+ if (!project.policy.allowedTopLevelFields.includes(field) && field !== "destination") {
+ addFinding(
+ findings,
+ "medium",
+ "webhook-top-level-field-not-allowlisted",
+ `${event.eventId} includes non-allowlisted top-level field ${field}.`,
+ "Drop non-contract fields before webhook delivery.",
+ [event.eventId, field]
+ );
+ }
+ }
+
+ if (!project.policy.allowedRegions.includes(event.destination.region)) {
+ addFinding(
+ findings,
+ "high",
+ "webhook-region-not-allowed",
+ `${event.eventId} targets region ${event.destination.region}.`,
+ "Hold delivery until data-residency routing is approved.",
+ [event.eventId, event.destination.region]
+ );
+ }
+
+ const signature = event.signature || {};
+ for (const field of project.policy.requiredSignatureFields) {
+ if (!signature[field]) {
+ addFinding(
+ findings,
+ "high",
+ "webhook-signature-metadata-incomplete",
+ `${event.eventId} is missing signature field ${field}.`,
+ "Regenerate signed event metadata before delivery.",
+ [event.eventId, field]
+ );
+ }
+ }
+ if (signature.algorithm && signature.algorithm !== "HMAC-SHA256") {
+ addFinding(
+ findings,
+ "critical",
+ "webhook-signature-algorithm-unsafe",
+ `${event.eventId} uses unsafe signature algorithm ${signature.algorithm}.`,
+ "Block delivery until a production signing algorithm is used.",
+ [event.eventId, signature.algorithm]
+ );
+ }
+
+ walkObject(event, (key, value, path) => {
+ if (project.policy.piiFieldNames.includes(key)) {
+ addFinding(
+ findings,
+ "critical",
+ "webhook-pii-field-present",
+ `${event.eventId} includes PII field ${pathString(path)}.`,
+ "Redact direct identifiers before institutional webhook delivery.",
+ [event.eventId, pathString(path)]
+ );
+ }
+ if (project.policy.blockedProjectFields.includes(key)) {
+ addFinding(
+ findings,
+ "high",
+ "webhook-private-project-field-present",
+ `${event.eventId} includes private project field ${pathString(path)}.`,
+ "Remove private workspace fields from outbound payloads.",
+ [event.eventId, pathString(path)]
+ );
+ }
+ if (typeof value === "string" && value.includes("storage.example/private")) {
+ addFinding(
+ findings,
+ "critical",
+ "webhook-private-storage-link-present",
+ `${event.eventId} exposes a private storage URL at ${pathString(path)}.`,
+ "Replace private URLs with DOI/metadata links or suppress the field.",
+ [event.eventId, pathString(path)]
+ );
+ }
+ });
+
+ if (event.project && event.project.visibility !== "public" && event.destination.purpose === "public-metadata") {
+ addFinding(
+ findings,
+ "high",
+ "private-project-routed-to-public-metadata",
+ `${event.eventId} routes ${event.project.visibility} project metadata to a public destination.`,
+ "Hold delivery until the workspace visibility and payload purpose agree.",
+ [event.eventId, event.project.id]
+ );
+ }
+
+ if (event.dataset) {
+ if (!project.policy.publicDatasetAccess.includes(event.dataset.access)) {
+ addFinding(
+ findings,
+ "critical",
+ "webhook-dataset-access-not-public-safe",
+ `${event.eventId} includes dataset ${event.dataset.id} with access ${event.dataset.access}.`,
+ "Suppress dataset delivery or emit metadata-only redacted payload.",
+ [event.eventId, event.dataset.id, event.dataset.access]
+ );
+ }
+ if (event.dataset.access === "embargoed-metadata-only" && event.dataset.downloadUrl) {
+ addFinding(
+ findings,
+ "high",
+ "embargoed-dataset-download-url-present",
+ `${event.eventId} includes a download URL for embargoed dataset ${event.dataset.id}.`,
+ "Remove download links while preserving DOI and metadata.",
+ [event.eventId, event.dataset.id]
+ );
+ }
+ }
+}
+
+function evaluateWebhookPayloads(project) {
+ const findings = [];
+ for (const event of project.outboundEvents) {
+ evaluateEvent(event, project, findings);
+ }
+
+ const eventDecisions = project.outboundEvents.map((event) => {
+ const eventFindings = findings.filter((finding) => finding.refs.includes(event.eventId));
+ const hasCritical = eventFindings.some((finding) => finding.severity === "critical");
+ const hasHigh = eventFindings.some((finding) => finding.severity === "high");
+ return {
+ eventId: event.eventId,
+ decision: hasCritical ? "block-delivery" : hasHigh ? "redact-and-review" : "deliver",
+ rules: eventFindings.map((finding) => finding.rule)
+ };
+ });
+
+ const severitySummary = findings.reduce(
+ (summary, finding) => {
+ summary[finding.severity] += 1;
+ return summary;
+ },
+ { critical: 0, high: 0, medium: 0, low: 0 }
+ );
+ const score = Math.max(0, 100 - findings.reduce((sum, finding) => sum + SEVERITY_WEIGHTS[finding.severity], 0));
+
+ return { findings, eventDecisions, severitySummary, score };
+}
+
+function decisionFromEvaluation(evaluation) {
+ if (evaluation.severitySummary.critical > 0) {
+ return "block-unsafe-webhook-delivery";
+ }
+ if (evaluation.severitySummary.high > 0 || evaluation.score < 75) {
+ return "redact-and-review-before-delivery";
+ }
+ if (evaluation.score < 90) {
+ return "manual-webhook-payload-review";
+ }
+ return "webhook-payload-ready";
+}
+
+function buildReviewPacket(project) {
+ const evaluation = evaluateWebhookPayloads(project);
+ return {
+ guard: "webhook-payload-redaction-guard",
+ issue: "SCIBASE-AI/SCIBASE.AI#19",
+ asOfDate: project.asOfDate,
+ decision: decisionFromEvaluation(evaluation),
+ score: evaluation.score,
+ severitySummary: evaluation.severitySummary,
+ findings: evaluation.findings,
+ eventDecisions: evaluation.eventDecisions,
+ safety: [
+ "Synthetic webhook, project, dataset, review, and connector data only",
+ "No live webhook delivery, repository sync, LMS sync, identity, storage, or external provider calls",
+ "No private institutional payloads, credentials, secrets, real users, or live admin mutations"
+ ]
+ };
+}
+
+function renderMarkdownReport(packet) {
+ const lines = [
+ "# Webhook Payload Redaction Guard",
+ "",
+ `Issue: ${packet.issue}`,
+ `Decision: ${packet.decision}`,
+ `Score: ${packet.score}`,
+ "",
+ "## Event Decisions",
+ ""
+ ];
+
+ for (const decision of packet.eventDecisions) {
+ lines.push(`- ${decision.eventId}: ${decision.decision}`);
+ if (decision.rules.length > 0) {
+ lines.push(` - Rules: ${decision.rules.join(", ")}`);
+ }
+ }
+
+ lines.push("", "## Findings", "");
+ for (const finding of packet.findings) {
+ lines.push(`- **${finding.severity} / ${finding.rule}**: ${finding.message}`);
+ lines.push(` - Action: ${finding.action}`);
+ lines.push(` - Refs: ${finding.refs.join(", ") || "none"}`);
+ }
+
+ lines.push("", "## Safety", "");
+ for (const item of packet.safety) {
+ lines.push(`- ${item}`);
+ }
+
+ return `${lines.join("\n")}\n`;
+}
+
+function renderSvgSummary(packet) {
+ const scoreWidth = Math.max(44, Math.min(760, packet.score * 7.6));
+ return `
+`;
+}
+
+module.exports = {
+ buildReviewPacket,
+ decisionFromEvaluation,
+ evaluateWebhookPayloads,
+ renderMarkdownReport,
+ renderSvgSummary
+};
diff --git a/webhook-payload-redaction-guard/package.json b/webhook-payload-redaction-guard/package.json
new file mode 100644
index 00000000..32c8cbec
--- /dev/null
+++ b/webhook-payload-redaction-guard/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "webhook-payload-redaction-guard",
+ "version": "1.0.0",
+ "description": "Deterministic enterprise guard for webhook payload redaction and minimization.",
+ "main": "index.js",
+ "private": true,
+ "type": "commonjs",
+ "scripts": {
+ "check": "node --check index.js && node --check sample-data.js && node --check demo.js && node --check demo-video.js && node --check test.js",
+ "test": "node test.js",
+ "demo": "node demo.js",
+ "demo:video": "node demo-video.js"
+ }
+}
diff --git a/webhook-payload-redaction-guard/reports/demo.webm b/webhook-payload-redaction-guard/reports/demo.webm
new file mode 100644
index 0000000000000000000000000000000000000000..6c4ca731d3a988f1dc51b5d60c650f0faa96b9cb
GIT binary patch
literal 80997
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