164 lines
7.4 KiB
JavaScript
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"/
|
|
);
|
|
});
|
|
});
|