From 6c3a1b5401b1ac4d0c7755eaecfc13a0410e928d Mon Sep 17 00:00:00 2001 From: cw <88217123+2bf@users.noreply.github.com> Date: Tue, 19 May 2026 18:34:25 -0700 Subject: [PATCH] Add enterprise incident response governance --- .../README.md | 43 ++++ .../demo.js | 63 +++++ .../demo.mp4 | Bin 0 -> 52553 bytes .../demo.svg | 22 ++ .../index.js | 235 ++++++++++++++++++ .../test.js | 130 ++++++++++ 6 files changed, 493 insertions(+) create mode 100644 enterprise-incident-response-governance/README.md create mode 100644 enterprise-incident-response-governance/demo.js create mode 100644 enterprise-incident-response-governance/demo.mp4 create mode 100644 enterprise-incident-response-governance/demo.svg create mode 100644 enterprise-incident-response-governance/index.js create mode 100644 enterprise-incident-response-governance/test.js diff --git a/enterprise-incident-response-governance/README.md b/enterprise-incident-response-governance/README.md new file mode 100644 index 0000000..acfcd16 --- /dev/null +++ b/enterprise-incident-response-governance/README.md @@ -0,0 +1,43 @@ +# Enterprise Incident Response Governance + +Self-contained milestone for SCIBASE.AI issue #19, Enterprise Tooling. + +This module gives institutional admins an incident governance layer for +integration, export, access, and privacy events. It is synthetic-data-only and +has no network calls, credentials, or external service dependencies. + +## What It Covers + +- Incident severity scoring across privacy, integration, export, access, and + compliance events. +- Breach-notification and escalation clocks for restricted or personal data. +- Owner routing for research offices, security teams, integration owners, and + compliance admins. +- Export and webhook holds when incident evidence is incomplete. +- Dashboard-ready incident metrics for institutional admins. +- Signed, webhook-ready event packets and stable audit digests. + +## Files + +- `index.js` - incident response governance engine. +- `demo.js` - terminal demo for admin incident packets. +- `test.js` - dependency-free regression tests. +- `demo.mp4` - short demo artifact for bounty review. + +## Run + +```sh +node enterprise-incident-response-governance/test.js +node enterprise-incident-response-governance/demo.js +``` + +## Requirement Map + +| Issue #19 Requirement | Implementation | +| --- | --- | +| Admin dashboards | `buildIncidentDashboard()` returns open counts, critical incidents, overdue clocks, team queues, and export holds. | +| Contributor and usage oversight | Incidents include project, department, integration, actor, affected-record, and evidence fields for admin review. | +| API and webhooks | Each packet includes a signed webhook event with deterministic digest and replay-safe incident ID. | +| Export pipelines | Export incidents can hold repository/journal/funder deliveries until evidence and owner approvals are complete. | +| Compliance tracking | Breach clocks, data classification, notification deadlines, and required evidence gates are modeled directly. | +| Enterprise operations | Routing actions assign security, research office, integration owner, and compliance workstreams. | diff --git a/enterprise-incident-response-governance/demo.js b/enterprise-incident-response-governance/demo.js new file mode 100644 index 0000000..58aef7b --- /dev/null +++ b/enterprise-incident-response-governance/demo.js @@ -0,0 +1,63 @@ +const { buildIncidentDashboard } = require("./index") + +const incidents = [ + { + id: "inc-privacy-001", + category: "privacy", + status: "open", + projectId: "project-genomics", + department: "Precision Medicine", + integration: "institutional-repository", + dataClass: "regulated", + affectedRecords: 3200, + detectedAt: "2026-05-19T01:00:00.000Z", + evidence: { + owner: "privacy-office", + affectedRecords: true, + timeline: true, + }, + }, + { + id: "inc-export-014", + category: "export", + status: "investigating", + projectId: "project-climate", + department: "Earth Systems", + integration: "journal-export", + dataClass: "internal", + affectedRecords: 85, + detectedAt: "2026-05-19T18:00:00.000Z", + evidence: { + owner: "research-office", + affectedRecords: true, + timeline: true, + mitigation: true, + }, + }, + { + id: "inc-integration-022", + category: "integration", + status: "mitigated", + projectId: "project-materials", + department: "Materials Lab", + integration: "eln-webhook", + dataClass: "restricted", + affectedRecords: 140, + detectedAt: "2026-05-18T16:30:00.000Z", + evidence: { + owner: "integration-team", + affectedRecords: true, + timeline: true, + mitigation: true, + rootCause: true, + }, + }, +] + +const dashboard = buildIncidentDashboard(incidents, { + nowIso: "2026-05-20T01:30:00.000Z", + generatedFor: "institution_admin_console", +}) + +console.log("Enterprise incident response governance") +console.log(JSON.stringify(dashboard, null, 2)) diff --git a/enterprise-incident-response-governance/demo.mp4 b/enterprise-incident-response-governance/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..0c63e65609001bc908d77ff14a381979efa7c302 GIT binary patch literal 52553 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 R2GSoiKHI-Zq5m?G{67t9NXGyG literal 0 HcmV?d00001 diff --git a/enterprise-incident-response-governance/demo.svg b/enterprise-incident-response-governance/demo.svg new file mode 100644 index 0000000..6aefb35 --- /dev/null +++ b/enterprise-incident-response-governance/demo.svg @@ -0,0 +1,22 @@ + + + + Enterprise Incident Response Governance + Triage privacy, access, export, and integration incidents for institutional admins + + Breach Clock + Regulated data + 24h notification window + Executive bridge + + Admin Routing + Privacy officer + Research office + Integration owner + + Signed Events + Webhook packet + Export hold digest + Audit evidence + Result: institutional admins get severity queues, overdue notifications, held exports, and signed incident evidence. + diff --git a/enterprise-incident-response-governance/index.js b/enterprise-incident-response-governance/index.js new file mode 100644 index 0000000..f973c30 --- /dev/null +++ b/enterprise-incident-response-governance/index.js @@ -0,0 +1,235 @@ +const crypto = require("node:crypto") + +const SEVERITY_WEIGHTS = { + privacy: 0.35, + access: 0.28, + export: 0.22, + integration: 0.18, + compliance: 0.24, +} + +const DATA_CLASS_WEIGHTS = { + public: 0, + internal: 0.18, + restricted: 0.45, + personal: 0.65, + regulated: 0.8, +} + +const STATUS_WEIGHTS = { + open: 0.25, + investigating: 0.18, + mitigated: 0.08, + closed: 0, +} + +function stableJson(value) { + if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]` + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`) + .join(",")}}` + } + return JSON.stringify(value) +} + +function digest(value) { + return crypto.createHash("sha256").update(stableJson(value)).digest("hex") +} + +function signPacket(packet, signingKey = "synthetic-enterprise-incident-demo-key") { + return crypto.createHmac("sha256", signingKey).update(stableJson(packet)).digest("hex") +} + +function clamp(value, min = 0, max = 1) { + return Math.min(Math.max(value, min), max) +} + +function hoursBetween(startIso, endIso) { + return Math.max((Date.parse(endIso) - Date.parse(startIso)) / 36e5, 0) +} + +function addHours(startIso, hours) { + return new Date(Date.parse(startIso) + hours * 36e5).toISOString() +} + +function notificationWindowHours(incident) { + if (incident.dataClass === "regulated") return 24 + if (incident.dataClass === "personal") return 72 + if (incident.dataClass === "restricted") return 96 + return null +} + +function evidenceCompleteness(incident) { + const required = [ + "owner", + "affectedRecords", + "timeline", + "mitigation", + "rootCause", + ] + const present = required.filter((field) => Boolean(incident.evidence?.[field])) + return Number((present.length / required.length).toFixed(4)) +} + +function scoreIncident(incident, nowIso = new Date().toISOString()) { + const categoryScore = SEVERITY_WEIGHTS[incident.category] || 0.15 + const dataClassScore = DATA_CLASS_WEIGHTS[incident.dataClass] || 0.1 + const statusScore = STATUS_WEIGHTS[incident.status] ?? 0.2 + const affectedRecordScore = clamp((incident.affectedRecords || 0) / 5000) + const evidenceGapScore = 1 - evidenceCompleteness(incident) + const clockHours = notificationWindowHours(incident) + const clockScore = + clockHours == null + ? 0 + : clamp(hoursBetween(incident.detectedAt, nowIso) / clockHours) + + return Number( + clamp( + categoryScore + + dataClassScore * 0.28 + + statusScore + + affectedRecordScore * 0.18 + + evidenceGapScore * 0.16 + + clockScore * 0.13, + ).toFixed(4), + ) +} + +function severityTier(score) { + if (score >= 0.85) return "critical" + if (score >= 0.65) return "high" + if (score >= 0.4) return "medium" + return "low" +} + +function notificationClock(incident, nowIso) { + const windowHours = notificationWindowHours(incident) + if (windowHours == null) { + return { + required: false, + dueAt: null, + hoursRemaining: null, + overdue: false, + } + } + const dueAt = addHours(incident.detectedAt, windowHours) + const hoursRemaining = Number(((Date.parse(dueAt) - Date.parse(nowIso)) / 36e5).toFixed(2)) + return { + required: true, + dueAt, + hoursRemaining, + overdue: hoursRemaining < 0, + } +} + +function requiredOwnerQueues(incident, score) { + const owners = new Set() + if (incident.category === "privacy" || incident.dataClass === "regulated" || incident.dataClass === "personal") { + owners.add("privacy_officer") + } + if (incident.category === "access") owners.add("security_admin") + if (incident.category === "integration") owners.add("integration_owner") + if (incident.category === "export") owners.add("research_office") + if (score >= 0.65) owners.add("institution_admin") + if (score >= 0.85) owners.add("executive_sponsor") + return [...owners] +} + +function incidentActions(incident, score, clock) { + const actions = [] + if (clock.required && clock.overdue) actions.push("Escalate overdue breach-notification clock.") + if (clock.required && !clock.overdue) actions.push("Prepare notification decision before deadline.") + if (evidenceCompleteness(incident) < 1) actions.push("Complete incident evidence packet.") + if (incident.category === "export") actions.push("Hold affected export pipeline until review closes.") + if (incident.category === "access") actions.push("Run access review for affected project and actor.") + if (incident.category === "integration") actions.push("Rotate or verify connector credentials before replay.") + if (score >= 0.85) actions.push("Open executive incident bridge.") + return actions +} + +function exportHoldDecision(incident, score) { + if (incident.category === "export" && score >= 0.4) return "hold_export" + if ((incident.dataClass === "regulated" || incident.dataClass === "personal") && score >= 0.65) return "hold_sensitive_exports" + return "allow_exports" +} + +function buildIncidentPacket(incident, options = {}) { + const nowIso = options.nowIso || new Date().toISOString() + const score = scoreIncident(incident, nowIso) + const clock = notificationClock(incident, nowIso) + const packet = { + incidentId: incident.id, + category: incident.category, + status: incident.status, + severityScore: score, + severityTier: severityTier(score), + projectId: incident.projectId, + department: incident.department, + integration: incident.integration, + dataClass: incident.dataClass, + affectedRecords: incident.affectedRecords || 0, + detectedAt: incident.detectedAt, + notificationClock: clock, + evidenceCompleteness: evidenceCompleteness(incident), + ownerQueues: requiredOwnerQueues(incident, score), + actions: incidentActions(incident, score, clock), + exportDecision: exportHoldDecision(incident, score), + } + const auditDigest = digest(packet) + const webhookEvent = { + type: "enterprise.incident.governance", + incidentId: packet.incidentId, + severityTier: packet.severityTier, + exportDecision: packet.exportDecision, + auditDigest, + } + + return { + ...packet, + auditDigest, + webhookEvent: { + ...webhookEvent, + signature: signPacket(webhookEvent, options.signingKey), + }, + } +} + +function buildIncidentDashboard(incidents, options = {}) { + const packets = incidents + .map((incident) => buildIncidentPacket(incident, options)) + .sort((a, b) => b.severityScore - a.severityScore) + const teamQueues = {} + for (const packet of packets) { + for (const queue of packet.ownerQueues) { + teamQueues[queue] = (teamQueues[queue] || 0) + 1 + } + } + + const dashboard = { + generatedFor: options.generatedFor || "enterprise_admin_dashboard", + incidentCount: packets.length, + openIncidents: packets.filter((packet) => packet.status !== "closed").length, + criticalIncidents: packets.filter((packet) => packet.severityTier === "critical").map((packet) => packet.incidentId), + overdueNotifications: packets.filter((packet) => packet.notificationClock.overdue).map((packet) => packet.incidentId), + heldExports: packets.filter((packet) => packet.exportDecision !== "allow_exports").map((packet) => packet.incidentId), + teamQueues, + packets, + } + + return { + ...dashboard, + auditDigest: digest(packets.map((packet) => packet.auditDigest)), + } +} + +module.exports = { + buildIncidentDashboard, + buildIncidentPacket, + digest, + evidenceCompleteness, + scoreIncident, + signPacket, + stableJson, +} diff --git a/enterprise-incident-response-governance/test.js b/enterprise-incident-response-governance/test.js new file mode 100644 index 0000000..e387e34 --- /dev/null +++ b/enterprise-incident-response-governance/test.js @@ -0,0 +1,130 @@ +const assert = require("node:assert/strict") +const { + buildIncidentDashboard, + buildIncidentPacket, + digest, + evidenceCompleteness, + scoreIncident, + signPacket, +} = require("./index") + +const NOW = "2026-05-20T01:30:00.000Z" + +function privacyIncident() { + return { + id: "privacy-1", + category: "privacy", + status: "open", + projectId: "project-a", + department: "Biology", + integration: "dspace", + dataClass: "regulated", + affectedRecords: 5000, + detectedAt: "2026-05-18T00:00:00.000Z", + evidence: { + owner: "privacy", + affectedRecords: true, + timeline: true, + }, + } +} + +function exportIncident() { + return { + id: "export-1", + category: "export", + status: "investigating", + projectId: "project-b", + department: "Physics", + integration: "journal-export", + dataClass: "internal", + affectedRecords: 100, + detectedAt: "2026-05-19T18:00:00.000Z", + evidence: { + owner: "research-office", + affectedRecords: true, + timeline: true, + mitigation: true, + }, + } +} + +function testPrivacyIncidentEscalatesOverdueClock() { + const packet = buildIncidentPacket(privacyIncident(), { nowIso: NOW }) + + assert.equal(packet.severityTier, "critical") + assert.equal(packet.notificationClock.required, true) + assert.equal(packet.notificationClock.overdue, true) + assert.ok(packet.ownerQueues.includes("privacy_officer")) + assert.ok(packet.ownerQueues.includes("executive_sponsor")) + assert.ok(packet.actions.some((action) => action.includes("overdue"))) +} + +function testExportIncidentHoldsPipeline() { + const packet = buildIncidentPacket(exportIncident(), { nowIso: NOW }) + + assert.equal(packet.exportDecision, "hold_export") + assert.ok(packet.ownerQueues.includes("research_office")) + assert.ok(packet.actions.some((action) => action.includes("export pipeline"))) +} + +function testCompleteEvidenceLowersScore() { + const incomplete = scoreIncident(exportIncident(), NOW) + const completeIncident = { + ...exportIncident(), + evidence: { + owner: "research-office", + affectedRecords: true, + timeline: true, + mitigation: true, + rootCause: true, + }, + } + const complete = scoreIncident(completeIncident, NOW) + + assert.equal(evidenceCompleteness(completeIncident), 1) + assert.ok(complete < incomplete) +} + +function testDashboardAggregatesQueuesAndHolds() { + const dashboard = buildIncidentDashboard([privacyIncident(), exportIncident()], { nowIso: NOW }) + + assert.equal(dashboard.incidentCount, 2) + assert.deepEqual(dashboard.criticalIncidents, ["privacy-1"]) + assert.ok(dashboard.heldExports.includes("privacy-1")) + assert.ok(dashboard.heldExports.includes("export-1")) + assert.equal(dashboard.teamQueues.privacy_officer, 1) +} + +function testWebhookSignatureIsDeterministic() { + const packet = buildIncidentPacket(exportIncident(), { nowIso: NOW, signingKey: "demo-key" }) + const event = { + type: packet.webhookEvent.type, + incidentId: packet.webhookEvent.incidentId, + severityTier: packet.webhookEvent.severityTier, + exportDecision: packet.webhookEvent.exportDecision, + auditDigest: packet.webhookEvent.auditDigest, + } + + assert.equal(packet.webhookEvent.signature, signPacket(event, "demo-key")) +} + +function testStableDigestIgnoresObjectKeyOrder() { + assert.equal(digest({ b: 2, a: 1 }), digest({ a: 1, b: 2 })) +} + +const tests = [ + testPrivacyIncidentEscalatesOverdueClock, + testExportIncidentHoldsPipeline, + testCompleteEvidenceLowersScore, + testDashboardAggregatesQueuesAndHolds, + testWebhookSignatureIsDeterministic, + testStableDigestIgnoresObjectKeyOrder, +] + +for (const test of tests) { + test() + console.log(`ok - ${test.name}`) +} + +console.log(`${tests.length} tests passed`)