diff --git a/enterprise-trust-center/README.md b/enterprise-trust-center/README.md new file mode 100644 index 0000000..1d3896d --- /dev/null +++ b/enterprise-trust-center/README.md @@ -0,0 +1,75 @@ +# Enterprise Trust Center + +Self-contained enterprise tooling milestone for [SCIBASE.AI issue #19](https://github.com/SCIBASE-AI/SCIBASE.AI/issues/19). + +The module gives institutional administrators a deterministic trust-center workflow for compliance analytics, secure API cataloging, webhook events, and audit-ready exports. It is credential-free and uses only Node.js built-ins so reviewers can run it locally without setup friction. + +## What It Adds + +- Admin dashboard summary for workspace status, active users, projects, integrations, and next actions. +- Compliance analytics for MFA coverage, audit-log coverage, webhook health, open incidents, and overdue data requests. +- Secure API catalog showing integration scopes, owners, active key counts, rotation timestamps, risk, and webhook endpoints. +- HMAC-signed trust-center webhook events. +- Export pipeline catalog for repositories, journal submission packages, and funder portals with required metadata checks. +- Procurement readiness report for SAML, DPA, security questionnaire, SLA, webhook health, API-key rotation, export readiness, blockers, and approval route. +- Compliance export bundle with evidence manifest and audit summary. +- Sample workspace data, tests, and a deterministic CLI demo. + +## Run + +```bash +cd enterprise-trust-center +npm run check +npm test +npm run demo +``` + +Expected demo shape: + +```json +{ + "workspace": "Borealis Research Institute", + "status": "blocked", + "activeUsers": 3, + "integrations": 2, + "exportPipelines": [ + { + "id": "journal-submission", + "formats": ["jats", "docx", "latex"], + "readyProjects": 1, + "blockedProjects": 2 + } + ], + "procurement": { + "status": "blocked", + "buyer": "Borealis IT Procurement" + }, + "nextActions": [ + { + "checkId": "mfa-coverage", + "title": "MFA coverage", + "remediation": "Require MFA for remaining active users before renewing institutional access." + } + ], + "firstSignedWebhook": { + "type": "trust_center.compliance_evaluated", + "signature": "sha256=..." + } +} +``` + +## Demo Artifact + +See [docs/demo.gif](docs/demo.gif) for a short visual walkthrough of the dashboard, export, and signed webhook flow. The SVG source is also included at [docs/demo.svg](docs/demo.svg). + +## Files + +- `src/trust-center.js` - core enterprise trust-center functions, including export pipeline and procurement readiness checks. +- `data/sample-workspace.json` - reviewable institutional workspace fixture. +- `test/trust-center.test.js` - dependency-free Node assertions. +- `scripts/demo.js` - CLI demo for reviewer smoke testing. +- `docs/issue-19-requirement-map.md` - maps the implementation to the bounty requirements. + +## AI-Assisted Disclosure + +This contribution was produced with AI assistance and manually verified with the local commands above. diff --git a/enterprise-trust-center/data/sample-workspace.json b/enterprise-trust-center/data/sample-workspace.json new file mode 100644 index 0000000..c1860b0 --- /dev/null +++ b/enterprise-trust-center/data/sample-workspace.json @@ -0,0 +1,136 @@ +{ + "id": "inst-borealis", + "name": "Borealis Research Institute", + "plan": "enterprise", + "users": [ + { "id": "u-001", "status": "active", "role": "admin", "mfaEnabled": true }, + { "id": "u-002", "status": "active", "role": "principal-investigator", "mfaEnabled": true }, + { "id": "u-003", "status": "active", "role": "researcher", "mfaEnabled": false }, + { "id": "u-004", "status": "invited", "role": "reviewer", "mfaEnabled": false } + ], + "projects": [ + { + "id": "p-101", + "title": "Longitudinal microbiome atlas", + "auditLogEnabled": true, + "exportMetadata": { + "doi": "10.5555/borealis.microbiome", + "orcid": "0000-0002-1825-0097", + "license": "CC-BY-4.0", + "versionHistory": true, + "citationIds": ["pmid:123456"], + "grantId": "NIH-R01-42", + "openAccessStatus": "compliant" + } + }, + { + "id": "p-102", + "title": "Climate model reproducibility pack", + "auditLogEnabled": true, + "exportMetadata": { + "doi": "10.5555/borealis.climate", + "license": "CC-BY-4.0", + "versionHistory": true, + "openAccessStatus": "pending" + } + }, + { + "id": "p-103", + "title": "Retrospective cohort review", + "auditLogEnabled": false, + "exportMetadata": { + "orcid": "0000-0003-1415-9265" + } + } + ], + "exportTargets": [ + { + "id": "zenodo", + "name": "Zenodo deposition", + "category": "indexed-repository", + "formats": ["datacite-json", "archive-zip"], + "requiredFields": ["doi", "license", "versionHistory"], + "preservedIdentifiers": ["doi", "citationIds", "versionHistory"] + }, + { + "id": "journal-submission", + "name": "Journal submission package", + "category": "journal", + "formats": ["jats", "docx", "latex"], + "requiredFields": ["doi", "orcid", "license"], + "preservedIdentifiers": ["doi", "orcid", "citationIds"] + }, + { + "id": "funder-portal", + "name": "Funder compliance report", + "category": "grant-portal", + "formats": ["grant-report-json", "csv"], + "requiredFields": ["grantId", "openAccessStatus"], + "preservedIdentifiers": ["doi", "orcid"] + } + ], + "integrations": [ + { + "id": "semantic-scholar", + "name": "Semantic Scholar export", + "category": "literature", + "scopes": ["read:projects", "write:citations"], + "owners": ["u-002"], + "risk": "low", + "webhookUrl": "https://example.edu/hooks/semantic-scholar" + }, + { + "id": "institutional-archive", + "name": "Institutional archive", + "category": "repository", + "scopes": ["read:projects", "write:projects"], + "owners": ["u-001"], + "risk": "medium", + "webhookUrl": "https://archive.example.edu/scibase/hooks" + } + ], + "apiKeys": [ + { + "id": "key-001", + "integrationId": "semantic-scholar", + "status": "active", + "rotatedAt": "2026-05-01T10:00:00Z" + }, + { + "id": "key-002", + "integrationId": "institutional-archive", + "status": "active", + "rotatedAt": "2026-04-17T14:30:00Z" + } + ], + "events": [ + { "id": "evt-001", "status": "delivered", "type": "project.exported" }, + { "id": "evt-002", "status": "delivered", "type": "citation.synced" }, + { "id": "evt-003", "status": "failed", "type": "dataset.archived" } + ], + "dataRequests": [ + { "id": "dr-001", "status": "closed", "type": "export" }, + { "id": "dr-002", "status": "overdue", "type": "deletion" } + ], + "incidents": [ + { "id": "inc-001", "severity": "medium", "status": "open" }, + { "id": "inc-002", "severity": "critical", "status": "resolved" } + ], + "auditLog": [ + { "id": "audit-001", "action": "project.created", "actorId": "u-002" }, + { "id": "audit-002", "action": "integration.connected", "actorId": "u-001" }, + { "id": "audit-003", "action": "export.generated", "actorId": "u-001" } + ], + "procurement": { + "asOf": "2026-05-14T00:00:00Z", + "buyer": "Borealis IT Procurement", + "renewalDate": "2026-09-01", + "evidence": { + "samlConfigured": true, + "dpaSigned": true, + "securityQuestionnaireComplete": false, + "slaHours": 48, + "activeKeyRotationDaysMax": 30 + } + } +} diff --git a/enterprise-trust-center/docs/demo.gif b/enterprise-trust-center/docs/demo.gif new file mode 100644 index 0000000..66e465d Binary files /dev/null and b/enterprise-trust-center/docs/demo.gif differ diff --git a/enterprise-trust-center/docs/demo.mp4 b/enterprise-trust-center/docs/demo.mp4 new file mode 100644 index 0000000..149913e Binary files /dev/null and b/enterprise-trust-center/docs/demo.mp4 differ diff --git a/enterprise-trust-center/docs/demo.svg b/enterprise-trust-center/docs/demo.svg new file mode 100644 index 0000000..7167d0d --- /dev/null +++ b/enterprise-trust-center/docs/demo.svg @@ -0,0 +1,39 @@ + + SCIBASE Enterprise Trust Center Demo + Animated overview of the enterprise trust-center dashboard, compliance export, and signed webhook flow. + + + Enterprise Trust Center + Borealis Research Institute · issue #19 milestone + + Compliance status + blocked + 4 action items + + Enterprise analytics + MFA coverage: 66.67% + Audit logs: 66.67% + + API catalog + 2 integrations + HMAC webhooks ready + + Demo output + { "status": "blocked", "integrations": 2, + "firstSignedWebhook": "sha256=5dd7…", + "exportId": "inst-borealis-trust-center-2026-05-14" } + + + + diff --git a/enterprise-trust-center/docs/issue-19-requirement-map.md b/enterprise-trust-center/docs/issue-19-requirement-map.md new file mode 100644 index 0000000..9bf7559 --- /dev/null +++ b/enterprise-trust-center/docs/issue-19-requirement-map.md @@ -0,0 +1,21 @@ +# Issue #19 Requirement Map + +This module is a focused milestone for SCIBASE issue #19, Enterprise Tooling. It avoids external credentials and keeps all behavior deterministic for review. + +| Issue requirement | Implementation | +| --- | --- | +| Institutional admin dashboards | `buildAdminDashboard()` produces workspace headline metrics, risk status, compliance checks, integrations, and next actions. | +| Contributor, usage, and compliance analytics | `computeUsageAnalytics()` derives MFA coverage, audit-log coverage, webhook health, incidents, data requests, project totals, and integration counts. | +| Secure API catalog | `buildApiCatalog()` lists connected integrations, scopes, owners, key rotation status, risk, and webhook endpoints. | +| Webhooks | `generateWebhookEvents()` creates trust-center events and `signWebhookEvent()` signs payloads with HMAC SHA-256. | +| Export pipelines | `buildExportPipelineCatalog()` models repository, journal, and funder-portal targets with formats, required metadata, ready projects, blocked projects, and preserved identifiers. `packageComplianceExport()` includes that catalog in the audit-ready export bundle. | +| Enterprise procurement readiness | `buildProcurementReadinessReport()` checks SAML, DPA, security questionnaire, SLA, webhook health, API key rotation, export readiness, blockers, and approval route metadata for institutional review. | +| Reviewer-friendly demo | `npm run demo` prints a deterministic trust-center summary from `data/sample-workspace.json`. | +| Local verification | `npm run check` and `npm test` validate syntax and behavior without network calls. | + +## Review Notes + +- The module is isolated under `enterprise-trust-center/`. +- It uses only Node.js built-ins. +- It is designed as a mergeable enterprise-tooling slice rather than a placeholder integration. +- The compliance policy is intentionally configurable through `evaluateCompliance(workspace, policy)`. diff --git a/enterprise-trust-center/package.json b/enterprise-trust-center/package.json new file mode 100644 index 0000000..4bbb901 --- /dev/null +++ b/enterprise-trust-center/package.json @@ -0,0 +1,12 @@ +{ + "name": "scibase-enterprise-trust-center", + "version": "0.1.0", + "private": true, + "description": "Self-contained enterprise tooling milestone for SCIBASE institutional trust center workflows.", + "type": "commonjs", + "scripts": { + "check": "node --check src/trust-center.js && node --check scripts/demo.js && node --check test/trust-center.test.js", + "demo": "node scripts/demo.js", + "test": "node test/trust-center.test.js" + } +} diff --git a/enterprise-trust-center/scripts/demo.js b/enterprise-trust-center/scripts/demo.js new file mode 100644 index 0000000..5c21316 --- /dev/null +++ b/enterprise-trust-center/scripts/demo.js @@ -0,0 +1,36 @@ +"use strict"; + +const path = require("path"); +const sampleWorkspace = require(path.join("..", "data", "sample-workspace.json")); +const { buildEnterpriseTrustCenter } = require("../src/trust-center"); + +const trustCenter = buildEnterpriseTrustCenter(sampleWorkspace, { + webhookSecret: "demo-secret", +}); + +const summary = { + workspace: trustCenter.dashboard.workspace.name, + status: trustCenter.dashboard.headline.status, + activeUsers: trustCenter.dashboard.headline.activeUsers, + integrations: trustCenter.dashboard.headline.apiIntegrations, + exportPipelines: trustCenter.dashboard.exportPipelines.map((pipeline) => ({ + id: pipeline.id, + formats: pipeline.formats, + readyProjects: pipeline.readyProjectIds.length, + blockedProjects: pipeline.blockedProjects.length, + })), + procurement: { + status: trustCenter.dashboard.procurement.status, + buyer: trustCenter.dashboard.procurement.buyer, + blockers: trustCenter.dashboard.procurement.blockers, + approvalRoute: trustCenter.dashboard.procurement.approvalRoute, + }, + nextActions: trustCenter.dashboard.nextActions, + firstSignedWebhook: { + type: trustCenter.signedWebhookEvents[0].type, + signature: trustCenter.signedWebhookEvents[0].signature.slice(0, 24) + "...", + }, + exportId: trustCenter.complianceExport.exportId, +}; + +console.log(JSON.stringify(summary, null, 2)); diff --git a/enterprise-trust-center/src/trust-center.js b/enterprise-trust-center/src/trust-center.js new file mode 100644 index 0000000..979f9d6 --- /dev/null +++ b/enterprise-trust-center/src/trust-center.js @@ -0,0 +1,401 @@ +"use strict"; + +const crypto = require("crypto"); + +const DEFAULT_POLICY = { + minMfaCoverage: 0.9, + maxOpenCriticalIncidents: 0, + maxOverdueDataRequests: 0, + maxWebhookFailureRate: 0.02, + minAuditLogCoverage: 0.95, +}; + +function asArray(value) { + return Array.isArray(value) ? value : []; +} + +function toNumber(value, fallback = 0) { + const number = Number(value); + return Number.isFinite(number) ? number : fallback; +} + +function percent(numerator, denominator) { + const top = toNumber(numerator); + const bottom = toNumber(denominator); + if (bottom <= 0) return 0; + return Number((top / bottom).toFixed(4)); +} + +function normalizeWorkspace(workspace) { + if (!workspace || typeof workspace !== "object") { + throw new TypeError("workspace must be an object"); + } + + return { + id: String(workspace.id || "workspace-unknown"), + name: String(workspace.name || "Unnamed workspace"), + plan: String(workspace.plan || "enterprise"), + users: asArray(workspace.users), + projects: asArray(workspace.projects), + integrations: asArray(workspace.integrations), + events: asArray(workspace.events), + dataRequests: asArray(workspace.dataRequests), + incidents: asArray(workspace.incidents), + apiKeys: asArray(workspace.apiKeys), + auditLog: asArray(workspace.auditLog), + exportTargets: asArray(workspace.exportTargets), + procurement: workspace.procurement || {}, + }; +} + +function buildApiCatalog(workspaceInput) { + const workspace = normalizeWorkspace(workspaceInput); + + return workspace.integrations.map((integration) => { + const scopes = asArray(integration.scopes); + const owners = asArray(integration.owners); + const activeKeys = workspace.apiKeys.filter( + (key) => key.integrationId === integration.id && key.status === "active", + ); + + return { + id: integration.id, + name: integration.name, + category: integration.category || "custom", + scopes, + owners, + activeKeyCount: activeKeys.length, + lastRotatedAt: activeKeys + .map((key) => key.rotatedAt) + .filter(Boolean) + .sort() + .at(-1) || null, + risk: integration.risk || (scopes.includes("write:projects") ? "medium" : "low"), + webhookUrl: integration.webhookUrl || null, + }; + }); +} + +function computeUsageAnalytics(workspaceInput) { + const workspace = normalizeWorkspace(workspaceInput); + const activeUsers = workspace.users.filter((user) => user.status === "active"); + const mfaUsers = activeUsers.filter((user) => user.mfaEnabled); + const auditCoveredProjects = workspace.projects.filter((project) => project.auditLogEnabled); + const deliveredWebhooks = workspace.events.filter((event) => event.status === "delivered"); + const failedWebhooks = workspace.events.filter((event) => event.status === "failed"); + const criticalIncidents = workspace.incidents.filter( + (incident) => incident.severity === "critical" && incident.status !== "resolved", + ); + const overdueRequests = workspace.dataRequests.filter((request) => request.status === "overdue"); + + return { + activeUsers: activeUsers.length, + totalProjects: workspace.projects.length, + mfaCoverage: percent(mfaUsers.length, activeUsers.length), + auditLogCoverage: percent(auditCoveredProjects.length, workspace.projects.length), + webhookDeliveryRate: percent(deliveredWebhooks.length, workspace.events.length), + webhookFailureRate: percent(failedWebhooks.length, workspace.events.length), + openCriticalIncidents: criticalIncidents.length, + overdueDataRequests: overdueRequests.length, + apiIntegrations: workspace.integrations.length, + }; +} + +function buildExportPipelineCatalog(workspaceInput) { + const workspace = normalizeWorkspace(workspaceInput); + const defaultTargets = [ + { + id: "zenodo", + name: "Zenodo deposition", + category: "indexed-repository", + formats: ["datacite-json", "archive-zip"], + requiredFields: ["doi", "license", "versionHistory"], + }, + { + id: "journal-submission", + name: "Journal submission package", + category: "journal", + formats: ["jats", "docx", "latex"], + requiredFields: ["doi", "orcid", "license"], + }, + { + id: "funder-portal", + name: "Funder compliance report", + category: "grant-portal", + formats: ["grant-report-json", "csv"], + requiredFields: ["grantId", "openAccessStatus"], + }, + ]; + const targets = workspace.exportTargets.length ? workspace.exportTargets : defaultTargets; + + return targets.map((target) => { + const requiredFields = asArray(target.requiredFields); + const projects = workspace.projects.map((project) => { + const exportMetadata = project.exportMetadata || {}; + const missingFields = requiredFields.filter((field) => !exportMetadata[field]); + return { + projectId: project.id, + title: project.title, + ready: missingFields.length === 0, + missingFields, + }; + }); + + return { + id: target.id, + name: target.name, + category: target.category || "custom", + formats: asArray(target.formats), + requiredFields, + route: `/enterprise/${workspace.id}/exports/${target.id}`, + readyProjectIds: projects.filter((project) => project.ready).map((project) => project.projectId), + blockedProjects: projects.filter((project) => !project.ready), + preservedIdentifiers: ["doi", "orcid", "citationIds", "versionHistory"].filter((field) => + requiredFields.includes(field) || asArray(target.preservedIdentifiers).includes(field), + ), + }; + }); +} + +function evaluateCompliance(workspaceInput, policyInput = {}) { + const policy = { ...DEFAULT_POLICY, ...policyInput }; + const analytics = computeUsageAnalytics(workspaceInput); + const checks = [ + { + id: "mfa-coverage", + label: "MFA coverage", + value: analytics.mfaCoverage, + threshold: policy.minMfaCoverage, + passed: analytics.mfaCoverage >= policy.minMfaCoverage, + remediation: "Require MFA for remaining active users before renewing institutional access.", + }, + { + id: "critical-incidents", + label: "Open critical incidents", + value: analytics.openCriticalIncidents, + threshold: policy.maxOpenCriticalIncidents, + passed: analytics.openCriticalIncidents <= policy.maxOpenCriticalIncidents, + remediation: "Resolve or formally risk-accept critical incidents before export approval.", + }, + { + id: "data-requests", + label: "Overdue data requests", + value: analytics.overdueDataRequests, + threshold: policy.maxOverdueDataRequests, + passed: analytics.overdueDataRequests <= policy.maxOverdueDataRequests, + remediation: "Close overdue privacy and data export requests with audit notes.", + }, + { + id: "webhook-failures", + label: "Webhook failure rate", + value: analytics.webhookFailureRate, + threshold: policy.maxWebhookFailureRate, + passed: analytics.webhookFailureRate <= policy.maxWebhookFailureRate, + remediation: "Rotate webhook credentials or disable unhealthy endpoints.", + }, + { + id: "audit-log-coverage", + label: "Project audit log coverage", + value: analytics.auditLogCoverage, + threshold: policy.minAuditLogCoverage, + passed: analytics.auditLogCoverage >= policy.minAuditLogCoverage, + remediation: "Enable immutable audit logging on projects that host manuscripts or datasets.", + }, + ]; + + const failed = checks.filter((check) => !check.passed); + + return { + status: failed.length === 0 ? "pass" : failed.length <= 2 ? "needs-attention" : "blocked", + checks, + failedChecks: failed, + }; +} + +function generateWebhookEvents(workspaceInput) { + const workspace = normalizeWorkspace(workspaceInput); + const compliance = evaluateCompliance(workspace); + const apiCatalog = buildApiCatalog(workspace); + + return [ + { + type: "trust_center.compliance_evaluated", + workspaceId: workspace.id, + payload: { + status: compliance.status, + failedCheckIds: compliance.failedChecks.map((check) => check.id), + }, + }, + ...apiCatalog + .filter((integration) => integration.webhookUrl) + .map((integration) => ({ + type: "trust_center.integration_ready", + workspaceId: workspace.id, + target: integration.webhookUrl, + payload: { + integrationId: integration.id, + scopes: integration.scopes, + risk: integration.risk, + }, + })), + ]; +} + +function signWebhookEvent(event, secret) { + if (!secret) { + throw new Error("secret is required to sign webhook events"); + } + const body = JSON.stringify(event); + const signature = crypto.createHmac("sha256", secret).update(body).digest("hex"); + return { + ...event, + body, + signature: `sha256=${signature}`, + }; +} + +function buildAdminDashboard(workspaceInput) { + const workspace = normalizeWorkspace(workspaceInput); + const analytics = computeUsageAnalytics(workspace); + const compliance = evaluateCompliance(workspace); + const apiCatalog = buildApiCatalog(workspace); + const exportPipelines = buildExportPipelineCatalog(workspace); + const procurement = buildProcurementReadinessReport(workspace); + + return { + workspace: { + id: workspace.id, + name: workspace.name, + plan: workspace.plan, + }, + headline: { + status: compliance.status, + activeUsers: analytics.activeUsers, + totalProjects: analytics.totalProjects, + apiIntegrations: analytics.apiIntegrations, + }, + analytics, + compliance, + integrations: apiCatalog, + exportPipelines, + procurement, + nextActions: compliance.failedChecks.map((check) => ({ + checkId: check.id, + title: check.label, + remediation: check.remediation, + })), + }; +} + +function buildProcurementReadinessReport(workspaceInput) { + const workspace = normalizeWorkspace(workspaceInput); + const analytics = computeUsageAnalytics(workspace); + const apiCatalog = buildApiCatalog(workspace); + const exportPipelines = buildExportPipelineCatalog(workspace); + const evidence = workspace.procurement.evidence || {}; + const requirements = { + samlConfigured: Boolean(evidence.samlConfigured), + dpaSigned: Boolean(evidence.dpaSigned), + securityQuestionnaireComplete: Boolean(evidence.securityQuestionnaireComplete), + slaHours: Number(evidence.slaHours || 0), + auditLogCoverage: analytics.auditLogCoverage, + webhookFailureRate: analytics.webhookFailureRate, + activeKeyRotationDaysMax: Number(evidence.activeKeyRotationDaysMax || 90), + exportTargetsReady: exportPipelines.every((pipeline) => pipeline.blockedProjects.length === 0), + }; + const staleKeys = apiCatalog.filter((integration) => { + if (!integration.lastRotatedAt) return true; + const rotatedAt = new Date(integration.lastRotatedAt); + if (Number.isNaN(rotatedAt.getTime())) return true; + const asOf = workspace.procurement.asOf ? new Date(workspace.procurement.asOf) : new Date(); + const ageDays = Math.floor((asOf.getTime() - rotatedAt.getTime()) / (1000 * 60 * 60 * 24)); + return ageDays > requirements.activeKeyRotationDaysMax; + }); + const blockers = []; + if (!requirements.samlConfigured) blockers.push("saml-not-configured"); + if (!requirements.dpaSigned) blockers.push("dpa-not-signed"); + if (!requirements.securityQuestionnaireComplete) blockers.push("security-questionnaire-incomplete"); + if (requirements.slaHours > 24 || requirements.slaHours <= 0) blockers.push("sla-missing-or-too-slow"); + if (requirements.auditLogCoverage < DEFAULT_POLICY.minAuditLogCoverage) blockers.push("audit-log-coverage-low"); + if (requirements.webhookFailureRate > DEFAULT_POLICY.maxWebhookFailureRate) blockers.push("webhook-failure-rate-high"); + if (staleKeys.length) blockers.push("api-key-rotation-stale"); + if (!requirements.exportTargetsReady) blockers.push("export-pipeline-metadata-incomplete"); + + return { + status: blockers.length ? "blocked" : "ready-for-procurement-review", + buyer: workspace.procurement.buyer || null, + renewalDate: workspace.procurement.renewalDate || null, + requirements, + staleIntegrations: staleKeys.map((integration) => integration.id), + blockers, + approvalRoute: `/enterprise/${workspace.id}/procurement/approve`, + procurementHash: crypto + .createHash("sha256") + .update(JSON.stringify({ workspaceId: workspace.id, requirements, blockers })) + .digest("hex") + .slice(0, 18), + }; +} + +function packageComplianceExport(workspaceInput) { + const workspace = normalizeWorkspace(workspaceInput); + const dashboard = buildAdminDashboard(workspace); + const events = generateWebhookEvents(workspace); + const exportPipelines = buildExportPipelineCatalog(workspace); + const auditSummary = workspace.auditLog.reduce((summary, entry) => { + const action = entry.action || "unknown"; + summary[action] = (summary[action] || 0) + 1; + return summary; + }, {}); + + return { + exportId: `${workspace.id}-trust-center-${new Date().toISOString().slice(0, 10)}`, + generatedAt: new Date().toISOString(), + workspace: dashboard.workspace, + complianceStatus: dashboard.compliance.status, + dashboard, + exportPipelines, + webhookEvents: events, + auditSummary, + evidenceManifest: { + users: workspace.users.length, + projects: workspace.projects.length, + integrations: workspace.integrations.length, + dataRequests: workspace.dataRequests.length, + incidents: workspace.incidents.length, + auditEntries: workspace.auditLog.length, + exportTargets: exportPipelines.length, + procurementStatus: dashboard.procurement.status, + }, + }; +} + +function buildEnterpriseTrustCenter(workspaceInput, options = {}) { + const workspace = normalizeWorkspace(workspaceInput); + const dashboard = buildAdminDashboard(workspace); + const complianceExport = packageComplianceExport(workspace); + const signedWebhookEvents = generateWebhookEvents(workspace).map((event) => + options.webhookSecret ? signWebhookEvent(event, options.webhookSecret) : event, + ); + + return { + dashboard, + complianceExport, + signedWebhookEvents, + }; +} + +module.exports = { + DEFAULT_POLICY, + buildAdminDashboard, + buildApiCatalog, + buildEnterpriseTrustCenter, + buildExportPipelineCatalog, + buildProcurementReadinessReport, + computeUsageAnalytics, + evaluateCompliance, + generateWebhookEvents, + normalizeWorkspace, + packageComplianceExport, + signWebhookEvent, +}; diff --git a/enterprise-trust-center/test/trust-center.test.js b/enterprise-trust-center/test/trust-center.test.js new file mode 100644 index 0000000..5216c70 --- /dev/null +++ b/enterprise-trust-center/test/trust-center.test.js @@ -0,0 +1,145 @@ +"use strict"; + +const assert = require("assert"); +const sampleWorkspace = require("../data/sample-workspace.json"); +const { + buildAdminDashboard, + buildApiCatalog, + buildEnterpriseTrustCenter, + buildExportPipelineCatalog, + buildProcurementReadinessReport, + computeUsageAnalytics, + evaluateCompliance, + packageComplianceExport, + signWebhookEvent, +} = require("../src/trust-center"); + +function testAnalytics() { + const analytics = computeUsageAnalytics(sampleWorkspace); + + assert.strictEqual(analytics.activeUsers, 3); + assert.strictEqual(analytics.totalProjects, 3); + assert.strictEqual(analytics.mfaCoverage, 0.6667); + assert.strictEqual(analytics.auditLogCoverage, 0.6667); + assert.strictEqual(analytics.webhookFailureRate, 0.3333); + assert.strictEqual(analytics.openCriticalIncidents, 0); + assert.strictEqual(analytics.overdueDataRequests, 1); +} + +function testCompliance() { + const compliance = evaluateCompliance(sampleWorkspace); + + assert.strictEqual(compliance.status, "blocked"); + assert.deepStrictEqual( + compliance.failedChecks.map((check) => check.id), + ["mfa-coverage", "data-requests", "webhook-failures", "audit-log-coverage"], + ); +} + +function testApiCatalog() { + const catalog = buildApiCatalog(sampleWorkspace); + + assert.strictEqual(catalog.length, 2); + assert.strictEqual(catalog[0].activeKeyCount, 1); + assert.strictEqual(catalog[1].risk, "medium"); + assert.ok(catalog[1].scopes.includes("write:projects")); +} + +function testExportPipelineCatalog() { + const pipelines = buildExportPipelineCatalog(sampleWorkspace); + const journal = pipelines.find((pipeline) => pipeline.id === "journal-submission"); + + assert.strictEqual(pipelines.length, 3); + assert.ok(journal.formats.includes("jats")); + assert.ok(journal.formats.includes("docx")); + assert.ok(journal.formats.includes("latex")); + assert.deepStrictEqual(journal.readyProjectIds, ["p-101"]); + assert.ok(journal.blockedProjects.some((project) => project.projectId === "p-102" && project.missingFields.includes("orcid"))); + assert.ok(journal.preservedIdentifiers.includes("doi")); +} + +function testDashboardAndExport() { + const dashboard = buildAdminDashboard(sampleWorkspace); + const complianceExport = packageComplianceExport(sampleWorkspace); + + assert.strictEqual(dashboard.workspace.id, "inst-borealis"); + assert.strictEqual(dashboard.exportPipelines.length, 3); + assert.strictEqual(dashboard.nextActions.length, 4); + assert.strictEqual(complianceExport.complianceStatus, "blocked"); + assert.strictEqual(complianceExport.evidenceManifest.exportTargets, 3); + assert.strictEqual(complianceExport.evidenceManifest.procurementStatus, "blocked"); + assert.strictEqual(complianceExport.evidenceManifest.auditEntries, 3); + assert.strictEqual(complianceExport.auditSummary["integration.connected"], 1); +} + +function testProcurementReadiness() { + const report = buildProcurementReadinessReport(sampleWorkspace); + const ready = buildProcurementReadinessReport({ + ...sampleWorkspace, + projects: sampleWorkspace.projects.map((project) => ({ + ...project, + auditLogEnabled: true, + exportMetadata: { + doi: project.exportMetadata.doi || "10.5555/ready", + orcid: project.exportMetadata.orcid || "0000-0000-0000-0000", + license: project.exportMetadata.license || "CC-BY-4.0", + versionHistory: project.exportMetadata.versionHistory || true, + grantId: project.exportMetadata.grantId || "GRANT-READY", + openAccessStatus: "compliant", + }, + })), + events: sampleWorkspace.events.map((event) => ({ ...event, status: "delivered" })), + procurement: { + ...sampleWorkspace.procurement, + evidence: { + samlConfigured: true, + dpaSigned: true, + securityQuestionnaireComplete: true, + slaHours: 12, + activeKeyRotationDaysMax: 90, + }, + }, + }); + + assert.strictEqual(report.status, "blocked"); + assert.ok(report.blockers.includes("webhook-failure-rate-high")); + assert.ok(report.blockers.includes("export-pipeline-metadata-incomplete")); + assert.ok(report.approvalRoute.includes("/procurement/approve")); + assert.strictEqual(ready.status, "ready-for-procurement-review"); + assert.deepStrictEqual(ready.blockers, []); + assert.ok(ready.procurementHash.length >= 12); +} + +function testWebhookSigning() { + const event = { + type: "trust_center.test", + workspaceId: "inst-borealis", + payload: { ok: true }, + }; + const signed = signWebhookEvent(event, "secret"); + + assert.ok(signed.body.includes("trust_center.test")); + assert.match(signed.signature, /^sha256=[a-f0-9]{64}$/); +} + +function testFullBuild() { + const trustCenter = buildEnterpriseTrustCenter(sampleWorkspace, { + webhookSecret: "secret", + }); + + assert.strictEqual(trustCenter.dashboard.headline.status, "blocked"); + assert.ok(trustCenter.complianceExport.exportId.startsWith("inst-borealis-trust-center")); + assert.ok(trustCenter.signedWebhookEvents.length >= 2); + assert.match(trustCenter.signedWebhookEvents[0].signature, /^sha256=/); +} + +testAnalytics(); +testCompliance(); +testApiCatalog(); +testExportPipelineCatalog(); +testDashboardAndExport(); +testProcurementReadiness(); +testWebhookSigning(); +testFullBuild(); + +console.log("enterprise-trust-center tests passed");