Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-security-gdpr-docs.mjs
T

175 lines
10 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable no-console */
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
export const REQUIRED_DOCUMENTS = [
{
path: "SECURITY.md",
label: "security disclosure policy",
sections: [
{ id: "security-title", description: "Security Policy title", pattern: /^#\s+Security Policy\s*$/im },
{ id: "supported-scope", description: "supported vulnerability scope", pattern: /^##\s+Supported scope\s*$/im },
{ id: "reporting", description: "vulnerability reporting instructions", pattern: /^##\s+Reporting a vulnerability\s*$/im },
{ id: "safe-evidence", description: "safe evidence guidance", pattern: /^##\s+Safe evidence\s*$/im },
{ id: "do-not-include", description: "unsafe disclosure and redaction rules", pattern: /^##\s+Do not include\s*$/im },
{ id: "triage-expectations", description: "maintainer triage expectations", pattern: /^##\s+Triage expectations\s*$/im },
],
terms: [
{ id: "runbook-link", description: "link to docs/security/runbook.md", pattern: /docs\/security\/runbook\.md/ },
{ id: "security-overview-link", description: "link to docs/security.md", pattern: /docs\/security\.md/ },
{ id: "private-reporting", description: "private reporting channel guidance", pattern: /private (?:maintainer contact path|issue process|channel)/i },
{ id: "redaction", description: "redaction guidance for sensitive evidence", pattern: /redact(?:ed|ion)?/i },
{ id: "secrets", description: "secret and credential exclusion", pattern: /secrets?|credentials?|tokens?|API keys?/i },
{ id: "tenant-customer-data", description: "tenant and customer data exclusion", pattern: /tenant identifiers?|customer data/i },
],
},
{
path: "docs/data-retention.md",
label: "data retention defaults",
sections: [
{ id: "retention-title", description: "Data Retention Defaults title", pattern: /^#\s+Data Retention Defaults\s*$/im },
{ id: "scope", description: "retention defaults scope", pattern: /^##\s+Scope\s*$/im },
{ id: "default-classes", description: "default retention classes", pattern: /^##\s+Default retention classes\s*$/im },
{ id: "surface-defaults", description: "surface-by-surface defaults", pattern: /^##\s+Surface-by-surface defaults\s*$/im },
{ id: "erasure-handling", description: "erasure and deletion handling", pattern: /^##\s+Erasure and deletion handling\s*$/im },
{ id: "operational-evidence", description: "operational evidence guidance", pattern: /^##\s+Operational evidence\s*$/im },
{ id: "manual-boundaries", description: "manual approval boundaries", pattern: /^##\s+Manual approval boundaries\s*$/im },
{ id: "implementation-followups", description: "implementation follow-ups", pattern: /^##\s+Implementation follow-ups\s*$/im },
],
terms: [
{ id: "export-artifacts", description: "export artifact expiration and deletion coverage", pattern: /export(?: files as sensitive generated artifacts| artifact expiration|s?[^\n]*(?:artifact|expiration|deletion))/i },
{ id: "import-reports", description: "import report and error artifact deletion coverage", pattern: /import[^\n]*(?:reports?|error artifacts?|error-report deletion|artifact TTL)/i },
{ id: "object-store-removal", description: "object-store blob deletion coverage", pattern: /object-store[^\n]*(?:blobs?|deletion|artifacts?|cleanup|remov(?:al|ed))/i },
{ id: "provider-deletion", description: "external provider deletion or minimization coverage", pattern: /provider[^\n]*(?:deletion|messages?|cleanup|sync|reconciliation|minimization)/i },
{ id: "queue-removal", description: "queue payload purge or dead-letter cleanup coverage", pattern: /(?:queue[^\n]*(?:TTLs|dead-letter cleanup|purge|payloads?|remov(?:al|ed))|purge stale payloads)/i },
{ id: "billing-exceptions", description: "billing/accounting exception coverage", pattern: /(?:billing|accounting|chargeback|tax|legal-hold)[^\n]*(?:exception|requirement|limits?|minimization)/i },
{ id: "audit-minimization", description: "audit evidence minimization coverage", pattern: /(?:audit[^\n]*minimi[sz]e|minimi[sz]e[^\n]*audit|minimal non-secret audit references)/i },
{ id: "plugin-candidate-classification", description: "plugin-candidate classification coverage", pattern: /plugin-candidate[^\n]*(?:review|classification|surfaces?|ownership)/i },
{ id: "safe-evidence", description: "safe evidence and redaction guidance", pattern: /Safe evidence includes/i },
{ id: "unsafe-evidence", description: "unsafe evidence exclusion guidance", pattern: /Unsafe evidence includes/i },
],
},
{
path: "docs/compliance/gdpr.md",
label: "GDPR data inventory",
sections: [
{ id: "gdpr-title", description: "GDPR Data Inventory title", pattern: /^#\s+GDPR Data Inventory\s*$/im },
{ id: "scope", description: "inventory scope", pattern: /^##\s+Scope\s*$/im },
{ id: "data-categories", description: "personal data categories", pattern: /^##\s+Data categories\s*$/im },
{ id: "processing-surfaces", description: "processing surfaces", pattern: /^##\s+Processing surfaces\s*$/im },
{ id: "storage-evidence", description: "storage and schema evidence", pattern: /^##\s+Storage evidence\s*$/im },
{ id: "export-surfaces", description: "export surfaces and hooks", pattern: /^##\s+Export surfaces\s*$/im },
{ id: "deletion-erasure", description: "deletion and erasure surfaces", pattern: /^##\s+Deletion and erasure surfaces\s*$/im },
{ id: "retention-status", description: "retention status and gaps", pattern: /^##\s+Retention status\s*$/im },
{ id: "redaction-rules", description: "operational redaction rules", pattern: /^##\s+Operational redaction rules\s*$/im },
{ id: "downstream-followups", description: "manual downstream follow-up boundaries", pattern: /^##\s+Downstream follow-ups\s*$/im },
],
terms: [
{ id: "schema-references", description: "Prisma schema storage references", pattern: /prisma\/schema\/[\w-]+\.prisma/ },
{ id: "export-hook", description: "data export hook coverage", pattern: /export (?:surfaces?|artifacts?|scope|job|inputs?)/i },
{ id: "deletion-hook", description: "deletion or erasure hook coverage", pattern: /(?:deletion|erasure) (?:hooks?|surfaces?|workflows?|evidence)/i },
{ id: "retention-gap", description: "explicit retention implementation gap", pattern: /retention (?:status|periods?|duration|gap|policy)/i },
{ id: "safe-evidence", description: "safe evidence boundaries", pattern: /safe evidence/i },
{ id: "unsafe-evidence", description: "unsafe evidence boundaries", pattern: /unsafe evidence/i },
{ id: "manual-runtime-boundary", description: "manual boundary for live/runtime checks", pattern: /manual|human explicitly approves/i },
],
},
];
export const FORBIDDEN_PLACEHOLDER_PATTERN = /\b(?:TODO|TBD|FIXME)\b/g;
function normalizePath(relativePath) {
return relativePath.split(path.sep).join("/");
}
function lineAndColumnForIndex(content, index) {
const prefix = content.slice(0, index);
const lines = prefix.split(/\r?\n/);
return { line: lines.length, column: lines.at(-1).length + 1 };
}
function readRequiredDocument(rootDir, relativePath) {
const normalized = normalizePath(relativePath);
const absolutePath = path.join(rootDir, relativePath);
if (!fs.existsSync(absolutePath)) {
return { relativePath: normalized, content: null, failures: [`${normalized}: missing required documentation file`] };
}
const content = fs.readFileSync(absolutePath, "utf8");
if (content.trim().length === 0) {
return { relativePath: normalized, content, failures: [`${normalized}: documentation file is empty`] };
}
return { relativePath: normalized, content, failures: [] };
}
function checkRequiredPatterns(relativePath, content, contractKey, patterns) {
return patterns
.filter(({ pattern }) => !pattern.test(content))
.map(({ id, description }) => `${relativePath}: missing ${contractKey} "${id}" (${description})`);
}
function checkForbiddenPlaceholders(relativePath, content) {
const failures = [];
const pattern = new RegExp(FORBIDDEN_PLACEHOLDER_PATTERN.source, FORBIDDEN_PLACEHOLDER_PATTERN.flags);
let match;
while ((match = pattern.exec(content)) !== null) {
const { line, column } = lineAndColumnForIndex(content, match.index);
failures.push(`${relativePath}:${line}:${column}: forbidden placeholder marker "${match[0]}"`);
}
return failures;
}
export function verifyComplianceRetentionSecurityGdprDocs(rootDir = process.cwd(), documents = REQUIRED_DOCUMENTS) {
const failures = [];
const checkedFiles = [];
for (const document of documents) {
const { relativePath, content, failures: readFailures } = readRequiredDocument(rootDir, document.path);
checkedFiles.push(relativePath);
failures.push(...readFailures);
if (content === null || content.trim().length === 0) continue;
failures.push(...checkForbiddenPlaceholders(relativePath, content));
failures.push(...checkRequiredPatterns(relativePath, content, "section", document.sections));
failures.push(...checkRequiredPatterns(relativePath, content, "contract field", document.terms));
}
return {
passes: failures.length === 0 ? [`${checkedFiles.length} documentation files satisfy the Compliance Retention security/GDPR contract`] : [],
failures,
observations: [`checked files: ${checkedFiles.join(", ")}`],
};
}
export function formatComplianceRetentionSecurityGdprDocsResult(result) {
const status = result.failures.length > 0 ? "FAIL" : "PASS";
return [
`${status} Compliance Retention security/GDPR documentation verification`,
...result.passes.map((pass) => `PASS ${pass}`),
...result.observations.map((observation) => `OBSERVE ${observation}`),
...result.failures.map((failure) => `FAIL ${failure}`),
`Summary: ${result.passes.length} passed, ${result.observations.length} observed, ${result.failures.length} failed`,
"",
].join("\n");
}
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
if (isCli) {
const result = verifyComplianceRetentionSecurityGdprDocs(process.cwd());
const output = formatComplianceRetentionSecurityGdprDocsResult(result);
if (result.failures.length > 0) {
console.error(output);
process.exitCode = 1;
} else {
console.log(output);
}
}