Initial Commit
This commit is contained in:
184
scripts/conformance/check-crypto-core-allowlist.ts
Normal file
184
scripts/conformance/check-crypto-core-allowlist.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Import-allowlist conformance gate.
|
||||
*
|
||||
* Scans every workspace package's `src/` for imports of
|
||||
* `@suaveplan/crypto-core`, `@suaveplan/crypto-cipher`,
|
||||
* `@suaveplan/crypto-curve`, or `@suaveplan/crypto-hash` MAC subpaths.
|
||||
* Fails with `error` severity for any package not on the committed
|
||||
* allowlist (`scripts/conformance/crypto-core-allowlist.yml`).
|
||||
*
|
||||
* Open imports (always allowed):
|
||||
* - `@suaveplan/crypto-utils` (encoding helpers)
|
||||
* - `@suaveplan/crypto-hash` for sha256 / sha384 / sha512 / hkdf /
|
||||
* pbkdf2 / argon2id (pure-hash + KDF uses); only the MAC subpath
|
||||
* `@suaveplan/crypto-hash/mac` is gated, because HMAC is the
|
||||
* primitive that should travel through Authority instead.
|
||||
*
|
||||
* Exit codes:
|
||||
* 0 — all packages comply
|
||||
* 1 — at least one package outside the allowlist imports a gated module
|
||||
*/
|
||||
|
||||
import { readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { dirname, join, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||
const REPO_ROOT = join(HERE, "..", "..");
|
||||
const ALLOWLIST_PATH = join(HERE, "crypto-core-allowlist.yml");
|
||||
const PACKAGES_ROOT = join(REPO_ROOT, "packages");
|
||||
|
||||
const GATED_PATTERNS = [
|
||||
/@suaveplan\/crypto-core(\/[^"'`]*)?/,
|
||||
/@suaveplan\/crypto-cipher(\/[^"'`]*)?/,
|
||||
/@suaveplan\/crypto-curve(\/[^"'`]*)?/,
|
||||
/@suaveplan\/crypto-hash\/mac(\/[^"'`]*)?/,
|
||||
];
|
||||
|
||||
interface AllowlistDoc {
|
||||
readonly allowlist: readonly string[];
|
||||
}
|
||||
|
||||
function readAllowlist(path: string): AllowlistDoc {
|
||||
const text = readFileSync(path, "utf8");
|
||||
const allowlist: string[] = [];
|
||||
let inAllowlist = false;
|
||||
for (const raw of text.split(/\r?\n/)) {
|
||||
const line = raw.trimEnd();
|
||||
if (line.startsWith("allowlist:")) {
|
||||
inAllowlist = true;
|
||||
continue;
|
||||
}
|
||||
if (inAllowlist) {
|
||||
// Section ends at the next top-level key
|
||||
if (/^\S/.test(line) && !line.startsWith(" ")) {
|
||||
inAllowlist = false;
|
||||
continue;
|
||||
}
|
||||
const m = line.match(/^\s*-\s+"([^"]+)"\s*$/);
|
||||
if (m?.[1]) allowlist.push(m[1]);
|
||||
}
|
||||
}
|
||||
return { allowlist };
|
||||
}
|
||||
|
||||
function findPackageRoots(start: string): string[] {
|
||||
const out: string[] = [];
|
||||
function walk(dir: string): void {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
if (entry.startsWith(".")) continue;
|
||||
const full = join(dir, entry);
|
||||
const st = statSync(full);
|
||||
if (!st.isDirectory()) continue;
|
||||
if (entry === "node_modules" || entry === "dist") continue;
|
||||
const pj = join(full, "package.json");
|
||||
try {
|
||||
statSync(pj);
|
||||
out.push(full);
|
||||
} catch {
|
||||
walk(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(start);
|
||||
return out;
|
||||
}
|
||||
|
||||
function* walkSrcFiles(packageDir: string): Generator<string> {
|
||||
const src = join(packageDir, "src");
|
||||
try {
|
||||
statSync(src);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
function* recurse(d: string): Generator<string> {
|
||||
for (const entry of readdirSync(d)) {
|
||||
const full = join(d, entry);
|
||||
const st = statSync(full);
|
||||
if (st.isDirectory()) {
|
||||
yield* recurse(full);
|
||||
} else if (
|
||||
full.endsWith(".ts") &&
|
||||
!full.endsWith(".test.ts") &&
|
||||
!full.endsWith(".spec.ts")
|
||||
) {
|
||||
yield full;
|
||||
}
|
||||
}
|
||||
}
|
||||
yield* recurse(src);
|
||||
}
|
||||
|
||||
interface Finding {
|
||||
readonly packageName: string;
|
||||
readonly file: string;
|
||||
readonly line: number;
|
||||
readonly importPath: string;
|
||||
}
|
||||
|
||||
function packageNameOf(packageDir: string): string {
|
||||
const pj = JSON.parse(
|
||||
readFileSync(join(packageDir, "package.json"), "utf8")
|
||||
) as { name?: string };
|
||||
return pj.name ?? `(unknown)@${relative(REPO_ROOT, packageDir)}`;
|
||||
}
|
||||
|
||||
function scanFile(file: string, packageName: string): Finding[] {
|
||||
const findings: Finding[] = [];
|
||||
const text = readFileSync(file, "utf8");
|
||||
const lines = text.split(/\r?\n/);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i] ?? "";
|
||||
if (!line.includes("@suaveplan/crypto-")) continue;
|
||||
for (const pat of GATED_PATTERNS) {
|
||||
const m = line.match(pat);
|
||||
if (m?.[0]) {
|
||||
findings.push({
|
||||
packageName,
|
||||
file: relative(REPO_ROOT, file),
|
||||
line: i + 1,
|
||||
importPath: m[0],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
export interface AllowlistCheckResult {
|
||||
readonly violations: readonly Finding[];
|
||||
}
|
||||
|
||||
export function checkAllowlist(): AllowlistCheckResult {
|
||||
const allow = readAllowlist(ALLOWLIST_PATH);
|
||||
const allowSet = new Set(allow.allowlist);
|
||||
const violations: Finding[] = [];
|
||||
for (const packageDir of findPackageRoots(PACKAGES_ROOT)) {
|
||||
const name = packageNameOf(packageDir);
|
||||
if (allowSet.has(name)) continue;
|
||||
for (const file of walkSrcFiles(packageDir)) {
|
||||
for (const f of scanFile(file, name)) {
|
||||
violations.push(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { violations };
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const result = checkAllowlist();
|
||||
if (result.violations.length === 0) {
|
||||
console.log("crypto-core allowlist: 0 violations");
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(
|
||||
`crypto-core allowlist: ${result.violations.length} violation(s)`
|
||||
);
|
||||
for (const v of result.violations) {
|
||||
console.error(
|
||||
` [error] ${v.packageName} at ${v.file}:${v.line} imports ${v.importPath}`
|
||||
);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user