From 603947ec3f89bc14cf47667ba1ca53649012c45f Mon Sep 17 00:00:00 2001 From: ethanmillerinvestments-code Date: Sat, 23 May 2026 01:20:19 -0400 Subject: [PATCH] Add enterprise vendor DPA review guard --- enterprise-vendor-dpa-review-guard/README.md | 29 +++ enterprise-vendor-dpa-review-guard/demo.js | 142 ++++++++++++ enterprise-vendor-dpa-review-guard/index.js | 212 +++++++++++++++++ .../package.json | 12 + .../reports/demo.mp4 | Bin 0 -> 6910 bytes .../reports/summary.svg | 15 ++ .../reports/vendor-dpa-review-packet.json | 215 ++++++++++++++++++ .../reports/vendor-dpa-review-report.md | 20 ++ .../sample-data.js | 65 ++++++ enterprise-vendor-dpa-review-guard/test.js | 160 +++++++++++++ 10 files changed, 870 insertions(+) create mode 100644 enterprise-vendor-dpa-review-guard/README.md create mode 100644 enterprise-vendor-dpa-review-guard/demo.js create mode 100644 enterprise-vendor-dpa-review-guard/index.js create mode 100644 enterprise-vendor-dpa-review-guard/package.json create mode 100644 enterprise-vendor-dpa-review-guard/reports/demo.mp4 create mode 100644 enterprise-vendor-dpa-review-guard/reports/summary.svg create mode 100644 enterprise-vendor-dpa-review-guard/reports/vendor-dpa-review-packet.json create mode 100644 enterprise-vendor-dpa-review-guard/reports/vendor-dpa-review-report.md create mode 100644 enterprise-vendor-dpa-review-guard/sample-data.js create mode 100644 enterprise-vendor-dpa-review-guard/test.js diff --git a/enterprise-vendor-dpa-review-guard/README.md b/enterprise-vendor-dpa-review-guard/README.md new file mode 100644 index 00000000..cb48a3cd --- /dev/null +++ b/enterprise-vendor-dpa-review-guard/README.md @@ -0,0 +1,29 @@ +# Enterprise Vendor DPA Review Guard + +This module adds a focused Enterprise Tooling guard for institutional vendor and integration enablement. It evaluates synthetic third-party vendor requests before admins enable integrations, webhooks, or export destinations. + +It checks: + +- active DPA coverage and accountable owner assignment +- BAA/DUA/SCC agreement coverage for restricted data classes and cross-border subprocessors +- approved subprocessor list and region fit +- breach-notice SLA readiness +- security-review freshness +- deterministic admin actions, webhook event envelopes, and audit digests + +The module is dependency-free and uses synthetic data only. It does not contact live vendors, legal systems, webhooks, payment systems, institutional dashboards, or external APIs. + +## Commands + +```bash +npm run check +npm test +npm run demo +``` + +`npm run demo` writes reviewer artifacts to `reports/`: + +- `vendor-dpa-review-packet.json` +- `vendor-dpa-review-report.md` +- `summary.svg` +- `demo.mp4` diff --git a/enterprise-vendor-dpa-review-guard/demo.js b/enterprise-vendor-dpa-review-guard/demo.js new file mode 100644 index 00000000..ceaf5e15 --- /dev/null +++ b/enterprise-vendor-dpa-review-guard/demo.js @@ -0,0 +1,142 @@ +const { mkdirSync, mkdtempSync, rmSync, writeFileSync } = require("node:fs"); +const { join } = require("node:path"); +const { tmpdir } = require("node:os"); +const { spawnSync } = require("node:child_process"); +const { summarizeVendorPortfolio } = require("./index"); +const { policy, vendorRequests } = require("./sample-data"); + +const reportsDir = join(__dirname, "reports"); +mkdirSync(reportsDir, { recursive: true }); + +const summary = summarizeVendorPortfolio(vendorRequests, policy); + +writeFileSync( + join(reportsDir, "vendor-dpa-review-packet.json"), + `${JSON.stringify(summary, null, 2)}\n` +); +writeFileSync(join(reportsDir, "vendor-dpa-review-report.md"), renderMarkdown(summary)); +writeFileSync(join(reportsDir, "summary.svg"), renderSvg(summary)); +writeDemoVideo(summary); + +console.log( + `status=${Object.entries(summary.byStatus).map(([status, count]) => `${status}:${count}`).join(",")} digest=${summary.auditDigest}` +); + +function renderMarkdown(summary) { + const lines = [ + "# Enterprise Vendor DPA Review Guard", + "", + `Audit digest: \`${summary.auditDigest}\``, + "", + "## Status Summary", + "", + ]; + + for (const [status, count] of Object.entries(summary.byStatus)) { + lines.push(`- ${status}: ${count}`); + } + + lines.push("", "## Admin Actions", ""); + for (const action of summary.adminActions) { + lines.push( + `- ${action.vendorName} (${action.status}): ${action.actionCodes.join(", ")}` + ); + } + + lines.push("", "## Decisions", ""); + for (const decision of summary.decisions) { + const findings = [...decision.blockers, ...decision.warnings].map((item) => item.code); + lines.push( + `- ${decision.vendorName}: ${decision.status}; findings=${findings.length ? findings.join(", ") : "none"}; digest=${decision.auditDigest}` + ); + } + + return `${lines.join("\n")}\n`; +} + +function renderSvg(summary) { + const approve = summary.byStatus.approve_vendor || 0; + const review = summary.byStatus.needs_legal_review || 0; + const hold = summary.byStatus.hold_vendor || 0; + return ` + + Enterprise Vendor DPA Review Guard + Synthetic vendor requests evaluated before institutional enablement. + ${bar(56, 178, approve, summary.totalVendors, "#22c55e", "Approved")} + ${bar(56, 260, review, summary.totalVendors, "#f59e0b", "Legal review")} + ${bar(56, 342, hold, summary.totalVendors, "#ef4444", "Hold")} + digest ${summary.auditDigest.slice(0, 32)} + +`; +} + +function bar(x, y, value, total, color, label) { + const width = Math.max(12, Math.round((value / total) * 620)); + return `${label}: ${value} + + `; +} + +function writeDemoVideo(summary) { + const output = join(reportsDir, "demo.mp4"); + const frameDir = mkdtempSync(join(tmpdir(), "vendor-dpa-demo-")); + + for (let index = 0; index < 60; index += 1) { + writeFileSync( + join(frameDir, `frame-${String(index).padStart(3, "0")}.ppm`), + renderFrame(summary, (index + 1) / 60) + ); + } + + const result = spawnSync("ffmpeg", [ + "-y", + "-framerate", + "15", + "-i", + join(frameDir, "frame-%03d.ppm"), + "-pix_fmt", + "yuv420p", + output, + ], { encoding: "utf8" }); + + rmSync(frameDir, { recursive: true, force: true }); + + if (result.status !== 0) { + throw new Error(`ffmpeg demo generation failed: ${result.stderr}`); + } +} + +function renderFrame(summary, progress) { + const width = 960; + const height = 540; + const pixels = Buffer.alloc(width * height * 3); + fillRect(pixels, width, 0, 0, width, height, [16, 24, 40]); + fillRect(pixels, width, 56, 72, 848, 44, [30, 41, 59]); + fillRect(pixels, width, 56, 128, 520, 18, [71, 85, 105]); + fillRect(pixels, width, 56, 458, 500, 12, [71, 85, 105]); + + drawStatusBar(pixels, width, 56, 178, summary.byStatus.approve_vendor || 0, summary.totalVendors, progress, [34, 197, 94]); + drawStatusBar(pixels, width, 56, 260, summary.byStatus.needs_legal_review || 0, summary.totalVendors, progress, [245, 158, 11]); + drawStatusBar(pixels, width, 56, 342, summary.byStatus.hold_vendor || 0, summary.totalVendors, progress, [239, 68, 68]); + + const header = Buffer.from(`P6\n${width} ${height}\n255\n`); + return Buffer.concat([header, pixels]); +} + +function drawStatusBar(pixels, width, x, y, value, total, progress, color) { + fillRect(pixels, width, x, y - 30, 180, 18, [203, 213, 225]); + fillRect(pixels, width, x, y, 620, 32, [31, 41, 55]); + fillRect(pixels, width, x, y, Math.max(8, Math.round((value / total) * 620 * progress)), 32, color); + fillRect(pixels, width, x + 650, y, 32 + value * 24, 32, color); +} + +function fillRect(pixels, width, x, y, rectWidth, rectHeight, color) { + for (let row = y; row < y + rectHeight; row += 1) { + for (let column = x; column < x + rectWidth; column += 1) { + const offset = (row * width + column) * 3; + pixels[offset] = color[0]; + pixels[offset + 1] = color[1]; + pixels[offset + 2] = color[2]; + } + } +} diff --git a/enterprise-vendor-dpa-review-guard/index.js b/enterprise-vendor-dpa-review-guard/index.js new file mode 100644 index 00000000..98970d35 --- /dev/null +++ b/enterprise-vendor-dpa-review-guard/index.js @@ -0,0 +1,212 @@ +const { createHash } = require("node:crypto"); + +const STATUS_EVENT_TYPES = { + approve_vendor: "enterprise.vendor_dpa.approved", + needs_legal_review: "enterprise.vendor_dpa.review", + hold_vendor: "enterprise.vendor_dpa.hold", +}; + +function evaluateVendorDpaRequest(request, policy) { + const blockers = []; + const warnings = []; + const agreements = new Set((request.agreements || []).map(normalize)); + const dataClasses = request.dataClasses || []; + const subprocessors = request.subprocessors || []; + + if (!request.owner || !request.owner.trim()) { + blockers.push(blocker("missing_owner", "Assign an accountable enterprise owner before enablement.")); + } + + if (!request.dpa || request.dpa.status !== "active") { + blockers.push(blocker("dpa_not_active", "Vendor DPA must be active and signed.")); + } + + if (!policy.allowedRegions.includes(request.region)) { + blockers.push(blocker("region_not_allowed", `Vendor region ${request.region} is outside approved regions.`)); + } + + for (const dataClass of dataClasses) { + const requiredAgreement = policy.requiredRestrictedAgreements[dataClass]; + if (requiredAgreement && !agreements.has(requiredAgreement)) { + blockers.push( + blocker( + `missing_${requiredAgreement}_for_${dataClass}`, + `${dataClass} requires ${requiredAgreement.toUpperCase()} coverage before review.` + ) + ); + } + } + + const unapproved = subprocessors.filter((subprocessor) => !subprocessor.approved); + if (unapproved.length > 0) { + blockers.push( + blocker( + "unapproved_subprocessor", + `Unapproved subprocessors: ${unapproved.map((item) => item.name).join(", ")}.` + ) + ); + } + + const crossBorder = subprocessors.filter( + (subprocessor) => !policy.allowedRegions.includes(subprocessor.region) + ); + if (crossBorder.length > 0 && !agreements.has("scc")) { + warnings.push( + warning( + "subprocessor_region_requires_scc", + `Cross-border subprocessors need SCC review: ${crossBorder.map((item) => item.name).join(", ")}.` + ) + ); + } + + if (request.breachNoticeHours > policy.maxBreachNoticeHours) { + blockers.push( + blocker( + "breach_notice_sla_exceeded", + `Breach notice SLA is ${request.breachNoticeHours}h, above ${policy.maxBreachNoticeHours}h.` + ) + ); + } + + if (securityReviewAgeDays(request) > policy.maxSecurityReviewAgeDays) { + blockers.push( + blocker( + "security_review_stale", + "Security review is stale or missing reviewer evidence." + ) + ); + } + + const status = blockers.length > 0 + ? "hold_vendor" + : warnings.length > 0 + ? "needs_legal_review" + : "approve_vendor"; + + const decision = { + vendorId: request.id, + vendorName: request.vendorName, + status, + riskScore: Math.min(7, blockers.length + warnings.length), + blockers, + warnings, + requiredActions: buildRequiredActions(request, blockers, warnings), + }; + + decision.webhookEvent = buildWebhookEvent(request, decision); + decision.auditDigest = digest({ + vendorId: decision.vendorId, + status: decision.status, + blockers: blockers.map((item) => item.code), + warnings: warnings.map((item) => item.code), + requiredActions: decision.requiredActions.map((item) => item.code), + }); + + return decision; +} + +function summarizeVendorPortfolio(requests, policy) { + const decisions = requests.map((request) => evaluateVendorDpaRequest(request, policy)); + const byStatus = {}; + for (const decision of decisions) { + byStatus[decision.status] = (byStatus[decision.status] || 0) + 1; + } + + const adminActions = decisions + .filter((decision) => decision.status !== "approve_vendor") + .sort((left, right) => severity(right) - severity(left)) + .map((decision) => ({ + vendorId: decision.vendorId, + vendorName: decision.vendorName, + status: decision.status, + actionCodes: decision.requiredActions.map((action) => action.code), + })); + + const summary = { + totalVendors: decisions.length, + byStatus, + decisions, + adminActions, + }; + + summary.auditDigest = digest({ + byStatus, + adminActions, + vendorDigests: decisions.map((decision) => decision.auditDigest), + }); + + return summary; +} + +function buildRequiredActions(request, blockers, warnings) { + const items = []; + for (const item of [...blockers, ...warnings]) { + items.push({ + code: `resolve_${item.code}`, + owner: request.owner && request.owner.trim() ? request.owner : "Enterprise Governance", + description: item.message, + }); + } + return items; +} + +function buildWebhookEvent(request, decision) { + return { + type: STATUS_EVENT_TYPES[decision.status], + vendorId: request.id, + vendorName: request.vendorName, + status: decision.status, + riskScore: decision.riskScore, + owner: request.owner || "Enterprise Governance", + blockerCodes: decision.blockers.map((item) => item.code), + warningCodes: decision.warnings.map((item) => item.code), + }; +} + +function securityReviewAgeDays(request) { + if (!request.securityReview || !request.securityReview.completedAt || !request.securityReview.reviewer) { + return Number.POSITIVE_INFINITY; + } + + const requestedAt = new Date(`${request.requestedAt || new Date().toISOString().slice(0, 10)}T00:00:00Z`); + const completedAt = new Date(`${request.securityReview.completedAt}T00:00:00Z`); + return Math.floor((requestedAt - completedAt) / 86_400_000); +} + +function blocker(code, message) { + return { severity: "blocker", code, message }; +} + +function warning(code, message) { + return { severity: "warning", code, message }; +} + +function severity(decision) { + if (decision.status === "hold_vendor") return 2; + if (decision.status === "needs_legal_review") return 1; + return 0; +} + +function normalize(value) { + return String(value || "").trim().toLowerCase(); +} + +function digest(value) { + return createHash("sha256").update(stableStringify(value)).digest("hex"); +} + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + if (value && typeof value === "object") { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +module.exports = { + evaluateVendorDpaRequest, + summarizeVendorPortfolio, + stableStringify, +}; diff --git a/enterprise-vendor-dpa-review-guard/package.json b/enterprise-vendor-dpa-review-guard/package.json new file mode 100644 index 00000000..06706845 --- /dev/null +++ b/enterprise-vendor-dpa-review-guard/package.json @@ -0,0 +1,12 @@ +{ + "name": "enterprise-vendor-dpa-review-guard", + "version": "1.0.0", + "private": true, + "description": "Synthetic vendor DPA and subprocessor review guard for SCIBASE Enterprise Tooling.", + "scripts": { + "check": "node --check index.js && node --check sample-data.js && node --check test.js && node --check demo.js", + "test": "node test.js", + "demo": "node demo.js" + }, + "license": "MIT" +} diff --git a/enterprise-vendor-dpa-review-guard/reports/demo.mp4 b/enterprise-vendor-dpa-review-guard/reports/demo.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..da6a37b22b1f9d76b1ed7de29feee9945600ecdd GIT binary patch literal 6910 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 ieSaZNC*-N;0_QBcK}PY0sIf + + Enterprise Vendor DPA Review Guard + Synthetic vendor requests evaluated before institutional enablement. + Approved: 1 + + + Legal review: 1 + + + Hold: 1 + + + digest 2bc17eb5d5b748a5eb4982642a8ca9af + diff --git a/enterprise-vendor-dpa-review-guard/reports/vendor-dpa-review-packet.json b/enterprise-vendor-dpa-review-guard/reports/vendor-dpa-review-packet.json new file mode 100644 index 00000000..4734af9c --- /dev/null +++ b/enterprise-vendor-dpa-review-guard/reports/vendor-dpa-review-packet.json @@ -0,0 +1,215 @@ +{ + "totalVendors": 3, + "byStatus": { + "approve_vendor": 1, + "needs_legal_review": 1, + "hold_vendor": 1 + }, + "decisions": [ + { + "vendorId": "vendor-zenith-archive", + "vendorName": "Zenith Archive", + "status": "approve_vendor", + "riskScore": 0, + "blockers": [], + "warnings": [], + "requiredActions": [], + "webhookEvent": { + "type": "enterprise.vendor_dpa.approved", + "vendorId": "vendor-zenith-archive", + "vendorName": "Zenith Archive", + "status": "approve_vendor", + "riskScore": 0, + "owner": "Enterprise Integrations", + "blockerCodes": [], + "warningCodes": [] + }, + "auditDigest": "7ad1ad454c15ab1532003bda61b5568a249d699725546d4309ad04973ce356e2" + }, + { + "vendorId": "vendor-crossborder-notebook", + "vendorName": "Crossborder Notebook", + "status": "needs_legal_review", + "riskScore": 1, + "blockers": [], + "warnings": [ + { + "severity": "warning", + "code": "subprocessor_region_requires_scc", + "message": "Cross-border subprocessors need SCC review: Regional Search." + } + ], + "requiredActions": [ + { + "code": "resolve_subprocessor_region_requires_scc", + "owner": "Research Systems", + "description": "Cross-border subprocessors need SCC review: Regional Search." + } + ], + "webhookEvent": { + "type": "enterprise.vendor_dpa.review", + "vendorId": "vendor-crossborder-notebook", + "vendorName": "Crossborder Notebook", + "status": "needs_legal_review", + "riskScore": 1, + "owner": "Research Systems", + "blockerCodes": [], + "warningCodes": [ + "subprocessor_region_requires_scc" + ] + }, + "auditDigest": "cfaac9ba50cfff1eb07bde67207337e8b2da7ef38db13c3204192178e10a5e53" + }, + { + "vendorId": "vendor-risky-lab-ai", + "vendorName": "Risky Lab AI", + "status": "hold_vendor", + "riskScore": 7, + "blockers": [ + { + "severity": "blocker", + "code": "missing_owner", + "message": "Assign an accountable enterprise owner before enablement." + }, + { + "severity": "blocker", + "code": "dpa_not_active", + "message": "Vendor DPA must be active and signed." + }, + { + "severity": "blocker", + "code": "region_not_allowed", + "message": "Vendor region APAC is outside approved regions." + }, + { + "severity": "blocker", + "code": "missing_baa_for_phi", + "message": "phi requires BAA coverage before review." + }, + { + "severity": "blocker", + "code": "missing_dua_for_student_records", + "message": "student_records requires DUA coverage before review." + }, + { + "severity": "blocker", + "code": "unapproved_subprocessor", + "message": "Unapproved subprocessors: Shadow Analytics." + }, + { + "severity": "blocker", + "code": "breach_notice_sla_exceeded", + "message": "Breach notice SLA is 120h, above 72h." + }, + { + "severity": "blocker", + "code": "security_review_stale", + "message": "Security review is stale or missing reviewer evidence." + } + ], + "warnings": [ + { + "severity": "warning", + "code": "subprocessor_region_requires_scc", + "message": "Cross-border subprocessors need SCC review: Shadow Analytics." + } + ], + "requiredActions": [ + { + "code": "resolve_missing_owner", + "owner": "Enterprise Governance", + "description": "Assign an accountable enterprise owner before enablement." + }, + { + "code": "resolve_dpa_not_active", + "owner": "Enterprise Governance", + "description": "Vendor DPA must be active and signed." + }, + { + "code": "resolve_region_not_allowed", + "owner": "Enterprise Governance", + "description": "Vendor region APAC is outside approved regions." + }, + { + "code": "resolve_missing_baa_for_phi", + "owner": "Enterprise Governance", + "description": "phi requires BAA coverage before review." + }, + { + "code": "resolve_missing_dua_for_student_records", + "owner": "Enterprise Governance", + "description": "student_records requires DUA coverage before review." + }, + { + "code": "resolve_unapproved_subprocessor", + "owner": "Enterprise Governance", + "description": "Unapproved subprocessors: Shadow Analytics." + }, + { + "code": "resolve_breach_notice_sla_exceeded", + "owner": "Enterprise Governance", + "description": "Breach notice SLA is 120h, above 72h." + }, + { + "code": "resolve_security_review_stale", + "owner": "Enterprise Governance", + "description": "Security review is stale or missing reviewer evidence." + }, + { + "code": "resolve_subprocessor_region_requires_scc", + "owner": "Enterprise Governance", + "description": "Cross-border subprocessors need SCC review: Shadow Analytics." + } + ], + "webhookEvent": { + "type": "enterprise.vendor_dpa.hold", + "vendorId": "vendor-risky-lab-ai", + "vendorName": "Risky Lab AI", + "status": "hold_vendor", + "riskScore": 7, + "owner": "Enterprise Governance", + "blockerCodes": [ + "missing_owner", + "dpa_not_active", + "region_not_allowed", + "missing_baa_for_phi", + "missing_dua_for_student_records", + "unapproved_subprocessor", + "breach_notice_sla_exceeded", + "security_review_stale" + ], + "warningCodes": [ + "subprocessor_region_requires_scc" + ] + }, + "auditDigest": "c176a09365c1cd192c881eb6c96b9d73099fad81b4ce397a21edf69b36c1f903" + } + ], + "adminActions": [ + { + "vendorId": "vendor-risky-lab-ai", + "vendorName": "Risky Lab AI", + "status": "hold_vendor", + "actionCodes": [ + "resolve_missing_owner", + "resolve_dpa_not_active", + "resolve_region_not_allowed", + "resolve_missing_baa_for_phi", + "resolve_missing_dua_for_student_records", + "resolve_unapproved_subprocessor", + "resolve_breach_notice_sla_exceeded", + "resolve_security_review_stale", + "resolve_subprocessor_region_requires_scc" + ] + }, + { + "vendorId": "vendor-crossborder-notebook", + "vendorName": "Crossborder Notebook", + "status": "needs_legal_review", + "actionCodes": [ + "resolve_subprocessor_region_requires_scc" + ] + } + ], + "auditDigest": "2bc17eb5d5b748a5eb4982642a8ca9af5bdc90e98916182b577073db55edf7cb" +} diff --git a/enterprise-vendor-dpa-review-guard/reports/vendor-dpa-review-report.md b/enterprise-vendor-dpa-review-guard/reports/vendor-dpa-review-report.md new file mode 100644 index 00000000..4cdd3e82 --- /dev/null +++ b/enterprise-vendor-dpa-review-guard/reports/vendor-dpa-review-report.md @@ -0,0 +1,20 @@ +# Enterprise Vendor DPA Review Guard + +Audit digest: `2bc17eb5d5b748a5eb4982642a8ca9af5bdc90e98916182b577073db55edf7cb` + +## Status Summary + +- approve_vendor: 1 +- needs_legal_review: 1 +- hold_vendor: 1 + +## Admin Actions + +- Risky Lab AI (hold_vendor): resolve_missing_owner, resolve_dpa_not_active, resolve_region_not_allowed, resolve_missing_baa_for_phi, resolve_missing_dua_for_student_records, resolve_unapproved_subprocessor, resolve_breach_notice_sla_exceeded, resolve_security_review_stale, resolve_subprocessor_region_requires_scc +- Crossborder Notebook (needs_legal_review): resolve_subprocessor_region_requires_scc + +## Decisions + +- Zenith Archive: approve_vendor; findings=none; digest=7ad1ad454c15ab1532003bda61b5568a249d699725546d4309ad04973ce356e2 +- Crossborder Notebook: needs_legal_review; findings=subprocessor_region_requires_scc; digest=cfaac9ba50cfff1eb07bde67207337e8b2da7ef38db13c3204192178e10a5e53 +- Risky Lab AI: hold_vendor; findings=missing_owner, dpa_not_active, region_not_allowed, missing_baa_for_phi, missing_dua_for_student_records, unapproved_subprocessor, breach_notice_sla_exceeded, security_review_stale, subprocessor_region_requires_scc; digest=c176a09365c1cd192c881eb6c96b9d73099fad81b4ce397a21edf69b36c1f903 diff --git a/enterprise-vendor-dpa-review-guard/sample-data.js b/enterprise-vendor-dpa-review-guard/sample-data.js new file mode 100644 index 00000000..a6e9b5ca --- /dev/null +++ b/enterprise-vendor-dpa-review-guard/sample-data.js @@ -0,0 +1,65 @@ +const policy = { + allowedRegions: ["US", "EU", "CA"], + maxSecurityReviewAgeDays: 365, + maxBreachNoticeHours: 72, + restrictedDataClasses: ["phi", "student_records", "human_subjects"], + requiredRestrictedAgreements: { + phi: "baa", + student_records: "dua", + human_subjects: "dua", + }, +}; + +const vendorRequests = [ + { + id: "vendor-zenith-archive", + vendorName: "Zenith Archive", + owner: "Enterprise Integrations", + region: "EU", + dataClasses: ["publications", "student_records"], + dpa: { status: "active", signedAt: "2026-01-12" }, + agreements: ["dpa", "dua", "scc"], + subprocessors: [ + { name: "Northwind Storage", approved: true, region: "EU" }, + { name: "Atlas Queue", approved: true, region: "US" }, + ], + breachNoticeHours: 48, + securityReview: { completedAt: "2026-03-01", reviewer: "Security Office" }, + requestedAt: "2026-05-23", + }, + { + id: "vendor-crossborder-notebook", + vendorName: "Crossborder Notebook", + owner: "Research Systems", + region: "US", + dataClasses: ["student_records"], + dpa: { status: "active", signedAt: "2026-02-09" }, + agreements: ["dpa", "dua"], + subprocessors: [ + { name: "Regional Search", approved: true, region: "APAC" }, + ], + breachNoticeHours: 72, + securityReview: { completedAt: "2026-04-15", reviewer: "Security Office" }, + requestedAt: "2026-05-23", + }, + { + id: "vendor-risky-lab-ai", + vendorName: "Risky Lab AI", + owner: "", + region: "APAC", + dataClasses: ["phi", "student_records"], + dpa: { status: "expired", signedAt: "2024-01-01" }, + agreements: ["dpa"], + subprocessors: [ + { name: "Shadow Analytics", approved: false, region: "APAC" }, + ], + breachNoticeHours: 120, + securityReview: { completedAt: "2024-01-01", reviewer: "" }, + requestedAt: "2026-05-23", + }, +]; + +module.exports = { + policy, + vendorRequests, +}; diff --git a/enterprise-vendor-dpa-review-guard/test.js b/enterprise-vendor-dpa-review-guard/test.js new file mode 100644 index 00000000..50af3c9a --- /dev/null +++ b/enterprise-vendor-dpa-review-guard/test.js @@ -0,0 +1,160 @@ +const assert = require("node:assert/strict"); + +const { + evaluateVendorDpaRequest, + summarizeVendorPortfolio, +} = require("./index"); + +const policy = { + allowedRegions: ["US", "EU", "CA"], + maxSecurityReviewAgeDays: 365, + maxBreachNoticeHours: 72, + restrictedDataClasses: ["phi", "student_records", "human_subjects"], + requiredRestrictedAgreements: { + phi: "baa", + student_records: "dua", + human_subjects: "dua", + }, +}; + +const approvedRequest = { + id: "vendor-zenith-archive", + vendorName: "Zenith Archive", + owner: "Enterprise Integrations", + region: "EU", + dataClasses: ["publications", "student_records"], + dpa: { status: "active", signedAt: "2026-01-12" }, + agreements: ["dpa", "dua", "scc"], + subprocessors: [ + { name: "Northwind Storage", approved: true, region: "EU" }, + { name: "Atlas Queue", approved: true, region: "US" }, + ], + breachNoticeHours: 48, + securityReview: { completedAt: "2026-03-01", reviewer: "Security Office" }, + requestedAt: "2026-05-23", +}; + +function test(name, fn) { + try { + fn(); + console.log(`ok - ${name}`); + } catch (error) { + console.error(`not ok - ${name}`); + console.error(error); + process.exitCode = 1; + } +} + +test("approves vendors with active DPA, required agreements, approved subprocessors, and current review", () => { + const decision = evaluateVendorDpaRequest(approvedRequest, policy); + + assert.equal(decision.status, "approve_vendor"); + assert.deepEqual(decision.blockers, []); + assert.equal(decision.riskScore, 0); + assert.equal(decision.webhookEvent.type, "enterprise.vendor_dpa.approved"); + assert.match(decision.auditDigest, /^[a-f0-9]{64}$/); +}); + +test("holds vendors when DPA, restricted-data, subprocessor, residency, SLA, review, and owner controls fail", () => { + const decision = evaluateVendorDpaRequest( + { + ...approvedRequest, + id: "vendor-risky-lab-ai", + vendorName: "Risky Lab AI", + owner: "", + region: "APAC", + dataClasses: ["phi", "student_records"], + dpa: { status: "expired" }, + agreements: ["dpa"], + subprocessors: [ + { name: "Shadow Analytics", approved: false, region: "APAC" }, + ], + breachNoticeHours: 120, + securityReview: { completedAt: "2024-01-01", reviewer: "" }, + }, + policy + ); + + assert.equal(decision.status, "hold_vendor"); + assert.equal(decision.riskScore, 7); + assert.deepEqual( + decision.blockers.map((blocker) => blocker.code), + [ + "missing_owner", + "dpa_not_active", + "region_not_allowed", + "missing_baa_for_phi", + "missing_dua_for_student_records", + "unapproved_subprocessor", + "breach_notice_sla_exceeded", + "security_review_stale", + ] + ); + assert.equal(decision.webhookEvent.type, "enterprise.vendor_dpa.hold"); +}); + +test("marks vendors for legal review when controls pass but subprocessor regions need SCC coverage", () => { + const decision = evaluateVendorDpaRequest( + { + ...approvedRequest, + id: "vendor-crossborder-notebook", + vendorName: "Crossborder Notebook", + agreements: ["dpa", "dua"], + subprocessors: [ + { name: "Regional Search", approved: true, region: "APAC" }, + ], + }, + policy + ); + + assert.equal(decision.status, "needs_legal_review"); + assert.deepEqual(decision.blockers, []); + assert.deepEqual(decision.warnings.map((warning) => warning.code), [ + "subprocessor_region_requires_scc", + ]); + assert.equal(decision.webhookEvent.type, "enterprise.vendor_dpa.review"); +}); + +test("summarizes portfolio decisions for admin dashboard and export packets", () => { + const summary = summarizeVendorPortfolio( + [ + approvedRequest, + { + ...approvedRequest, + id: "vendor-crossborder-notebook", + vendorName: "Crossborder Notebook", + agreements: ["dpa", "dua"], + subprocessors: [ + { name: "Regional Search", approved: true, region: "APAC" }, + ], + }, + { + ...approvedRequest, + id: "vendor-risky-lab-ai", + vendorName: "Risky Lab AI", + owner: "", + dpa: { status: "missing" }, + dataClasses: ["phi"], + agreements: [], + subprocessors: [ + { name: "Shadow Analytics", approved: false, region: "APAC" }, + ], + breachNoticeHours: 96, + securityReview: { completedAt: "2024-01-01" }, + }, + ], + policy + ); + + assert.equal(summary.totalVendors, 3); + assert.deepEqual(summary.byStatus, { + approve_vendor: 1, + needs_legal_review: 1, + hold_vendor: 1, + }); + assert.deepEqual(summary.adminActions.map((action) => action.vendorId), [ + "vendor-risky-lab-ai", + "vendor-crossborder-notebook", + ]); + assert.match(summary.auditDigest, /^[a-f0-9]{64}$/); +});