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

164 lines
7.4 KiB
JavaScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { afterEach, describe, it } from "vitest";
import { formatComplianceRetentionSecurityGdprDocsResult, verifyComplianceRetentionSecurityGdprDocs } from "./verify-security-gdpr-docs.mjs";
const fixtures = [];
const VALID_SECURITY = [
"# Security Policy",
"This entrypoint links to [docs/security.md](docs/security.md) and [docs/security/runbook.md](docs/security/runbook.md).",
"## Supported scope",
"Web, API, worker, and documentation contracts are supported.",
"## Reporting a vulnerability",
"Use a private maintainer contact path and never include secrets, credentials, tokens, API keys, tenant identifiers, or customer data.",
"## Safe evidence",
"Safe evidence uses request IDs and redacted diagnostics only.",
"## Do not include",
"Do not include customer data, tenant identifiers, raw secrets, tokens, credentials, or API keys.",
"## Triage expectations",
"Maintainers preserve redaction boundaries during triage.",
].join("\n");
const VALID_GDPR = [
"# GDPR Data Inventory",
"## Scope",
"This inventory defines personal data boundaries and manual runtime checks when a human explicitly approves them.",
"## Data categories",
"User and tenant records can contain personal data.",
"## Processing surfaces",
"Processing surfaces include web, API, worker, and provider flows.",
"## Storage evidence",
"Schema references include prisma/schema/accounts.prisma and prisma/schema/core.prisma.",
"## Export surfaces",
"Export surfaces list export job inputs and export artifacts.",
"## Deletion and erasure surfaces",
"Deletion and erasure surfaces identify deletion hooks and erasure workflows.",
"## Retention status",
"Retention status documents a retention gap without inventing a retention policy.",
"## Operational redaction rules",
"Safe evidence is allowed while unsafe evidence is rejected.",
"## Downstream follow-ups",
"Follow-up work defines manual boundaries for later implementation.",
].join("\n");
const VALID_RETENTION = [
"# Data Retention Defaults",
"## Scope",
"This document defines repository-level default retention classes.",
"## Default retention classes",
"Default classes cover account, tenant, telemetry, artifacts, audit, billing, and plugin review data.",
"## Surface-by-surface defaults",
"Surface defaults map data surfaces to default retention behavior, including export files as sensitive generated artifacts and import error artifacts.",
"## Erasure and deletion handling",
"Erasure workflows separate active records, artifacts, provider data, and audit exceptions, with object-store deletion, provider deletion, and queue payload removal tracked explicitly.",
"Billing exception handling covers accounting requirements and local identifier minimization.",
"Audit evidence must minimize personal fields while preserving restricted accountability records.",
"Plugin-candidate classification is required before migration, export, deletion, or provider sync work.",
"## Operational evidence",
"Safe evidence includes surface labels, request IDs, job IDs, counts, statuses, and redacted diagnostic codes.",
"Unsafe evidence includes secrets, tenant identifiers, object keys, signed URLs, provider payloads, queue payloads, import/export contents, and message bodies.",
"## Manual approval boundaries",
"Runtime checks remain manual unless explicitly approved.",
"## Implementation follow-ups",
"Follow-up work adds jobs, provider cleanup, and redacted evidence.",
].join("\n");
function writeFixtureFile(rootDir, relativePath, content) {
const absolutePath = path.join(rootDir, relativePath);
fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
fs.writeFileSync(absolutePath, content);
}
function createFixture(overrides = {}) {
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "complianceRetention-security-gdpr-docs-"));
fixtures.push(rootDir);
const files = {
"SECURITY.md": VALID_SECURITY,
"docs/data-retention.md": VALID_RETENTION,
"docs/compliance/gdpr.md": VALID_GDPR,
...overrides,
};
for (const [relativePath, content] of Object.entries(files)) {
if (content === null) continue;
writeFixtureFile(rootDir, relativePath, content);
}
return rootDir;
}
afterEach(() => {
while (fixtures.length > 0) {
fs.rmSync(fixtures.pop(), { recursive: true, force: true });
}
});
describe("verify-security-gdpr-docs", () => {
it("passes for documentation that satisfies the Compliance Retention SECURITY/GDPR contract", () => {
const result = verifyComplianceRetentionSecurityGdprDocs(createFixture());
const output = formatComplianceRetentionSecurityGdprDocsResult(result);
assert.deepEqual(result.failures, []);
assert.match(output, /^PASS Compliance Retention security\/GDPR documentation verification/);
assert.match(output, /SECURITY\.md/);
assert.match(output, /docs\/data-retention\.md/);
assert.match(output, /docs\/compliance\/gdpr\.md/);
});
it("fails when the retention defaults document is missing", () => {
const result = verifyComplianceRetentionSecurityGdprDocs(createFixture({ "docs/data-retention.md": null }));
assert.match(result.failures.join("\n"), /docs\/data-retention\.md: missing required documentation file/);
});
it("fails when a required document is missing", () => {
const result = verifyComplianceRetentionSecurityGdprDocs(createFixture({ "docs/compliance/gdpr.md": null }));
assert.match(result.failures.join("\n"), /docs\/compliance\/gdpr\.md: missing required documentation file/);
});
it("fails when a required section is missing", () => {
const result = verifyComplianceRetentionSecurityGdprDocs(createFixture({ "SECURITY.md": VALID_SECURITY.replace("## Safe evidence\n", "") }));
assert.match(result.failures.join("\n"), /SECURITY\.md: missing section "safe-evidence"/);
});
it("fails when a required contract field is missing", () => {
const result = verifyComplianceRetentionSecurityGdprDocs(
createFixture({ "docs/compliance/gdpr.md": VALID_GDPR.replace(/prisma\/schema\/[\w-]+\.prisma/g, "schema evidence") })
);
assert.match(result.failures.join("\n"), /docs\/compliance\/gdpr\.md: missing contract field "schema-references"/);
});
it("fails when a required retention contract field is missing", () => {
const result = verifyComplianceRetentionSecurityGdprDocs(
createFixture({ "docs/data-retention.md": VALID_RETENTION.replace("Plugin-candidate", "Candidate") })
);
assert.match(result.failures.join("\n"), /docs\/data-retention\.md: missing contract field "plugin-candidate-classification"/);
});
it("fails when forbidden placeholder markers are present", () => {
const result = verifyComplianceRetentionSecurityGdprDocs(createFixture({ "SECURITY.md": `${VALID_SECURITY}\nFIXME redact later` }));
assert.match(result.failures.join("\n"), /SECURITY\.md:\d+:\d+: forbidden placeholder marker "FIXME"/);
});
it("keeps CLI mode import-safe and exits non-zero for invalid docs", () => {
const rootDir = createFixture({ "SECURITY.md": `${VALID_SECURITY}\nTODO later` });
const scriptPath = path.resolve("scripts/verify-security-gdpr-docs.mjs");
assert.throws(
() => execFileSync(process.execPath, [scriptPath], { cwd: rootDir, encoding: "utf8", stdio: "pipe" }),
/forbidden placeholder marker "TODO"/
);
});
});