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 `
+`;
+}
+
+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 00000000..da6a37b2
Binary files /dev/null and b/enterprise-vendor-dpa-review-guard/reports/demo.mp4 differ
diff --git a/enterprise-vendor-dpa-review-guard/reports/summary.svg b/enterprise-vendor-dpa-review-guard/reports/summary.svg
new file mode 100644
index 00000000..28b0d9b0
--- /dev/null
+++ b/enterprise-vendor-dpa-review-guard/reports/summary.svg
@@ -0,0 +1,15 @@
+
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}$/);
+});