mirror of
https://github.com/przeprogramowani/10x-cli.git
synced 2026-09-19 03:30:01 +08:00
2f4ebd3b75
* docs: add corporate network allowlist for security/sysadmin teams Polish sysadmin spec plus a machine-readable host list and a source-scan test so new CLI destinations cannot land undocumented. * chore(release): prepare v1.23.1 * docs: rewrite corporate allowlist in sysadmin language (PL+EN) Plain host/port/protocol tables, matching English document, and an allowlist test that covers both language files. * docs: drop localhost from the corporate network allowlist Public DNS names only; the source scan skips IP literals and single-label hosts so local-dev URLs stay out of the sysadmin spec. --------- Co-authored-by: Claude <noreply@anthropic.com>
153 lines
4.7 KiB
TypeScript
153 lines
4.7 KiB
TypeScript
/**
|
|
* Fails when src/ or the sysadmin docs mention a hostname that is not listed
|
|
* in docs/network-allowlist.json. Hosts with inDocs: true must appear in both
|
|
* the Polish and English documents.
|
|
*/
|
|
import { describe, expect, it } from "bun:test";
|
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
import { join, relative, resolve } from "node:path";
|
|
|
|
const ROOT = resolve(import.meta.dir, "..");
|
|
const SRC = join(ROOT, "src");
|
|
const ALLOWLIST_PATH = join(ROOT, "docs/network-allowlist.json");
|
|
const DOC_PATHS = [
|
|
"docs/wymagania-sieciowe.md",
|
|
"docs/network-requirements.md",
|
|
] as const;
|
|
const HOST_RE = /https?:\/\/([a-zA-Z0-9.-]+)/g;
|
|
/** Vendor documentation links in the AI-tool table — not 10x-cli destinations. */
|
|
const VENDOR_DOC_HOSTS = new Set([
|
|
"docs.claude.com",
|
|
"cursor.com",
|
|
"docs.github.com",
|
|
"developers.openai.com",
|
|
"developers.google.com",
|
|
"kiro.dev",
|
|
"docs.devin.ai",
|
|
]);
|
|
|
|
interface AllowlistHost {
|
|
hostname: string;
|
|
inDocs?: boolean;
|
|
}
|
|
|
|
interface AllowlistFile {
|
|
version: number;
|
|
hosts: AllowlistHost[];
|
|
}
|
|
|
|
function walkTsFiles(dir: string): string[] {
|
|
const out: string[] = [];
|
|
for (const name of readdirSync(dir)) {
|
|
const path = join(dir, name);
|
|
const st = statSync(path);
|
|
if (st.isDirectory()) out.push(...walkTsFiles(path));
|
|
else if (name.endsWith(".ts")) out.push(path);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Drop block comments, then line comments that are not inside http(s)://. */
|
|
function stripComments(source: string): string {
|
|
const withoutBlocks = source.replace(/\/\*[\s\S]*?\*\//g, " ");
|
|
return withoutBlocks
|
|
.split("\n")
|
|
.map((line) => {
|
|
let i = 0;
|
|
while (i < line.length) {
|
|
if (line.startsWith("https://", i)) {
|
|
i += "https://".length;
|
|
continue;
|
|
}
|
|
if (line.startsWith("http://", i)) {
|
|
i += "http://".length;
|
|
continue;
|
|
}
|
|
if (line[i] === "/" && line[i + 1] === "/") return line.slice(0, i);
|
|
i += 1;
|
|
}
|
|
return line;
|
|
})
|
|
.join("\n");
|
|
}
|
|
|
|
function hostsInText(text: string): Set<string> {
|
|
const found = new Set<string>();
|
|
HOST_RE.lastIndex = 0;
|
|
let match: RegExpExecArray | null;
|
|
while ((match = HOST_RE.exec(text)) !== null) {
|
|
const host = match[1]?.replace(/\.+$/, "");
|
|
if (host) found.add(host);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
/** Allowlist only public DNS names (a dot, not an IP literal). */
|
|
function isPublicHostname(host: string): boolean {
|
|
if (!host.includes(".")) return false;
|
|
if (/^\d{1,3}(?:\.\d{1,3}){3}$/.test(host)) return false;
|
|
return true;
|
|
}
|
|
|
|
function hostsInSource(): Map<string, string[]> {
|
|
const found = new Map<string, string[]>();
|
|
for (const file of walkTsFiles(SRC)) {
|
|
const text = stripComments(readFileSync(file, "utf8"));
|
|
for (const host of hostsInText(text)) {
|
|
if (!isPublicHostname(host)) continue;
|
|
const rel = relative(ROOT, file);
|
|
const list = found.get(host) ?? [];
|
|
if (!list.includes(rel)) list.push(rel);
|
|
found.set(host, list);
|
|
}
|
|
}
|
|
return found;
|
|
}
|
|
|
|
describe("docs/network-allowlist.json", () => {
|
|
const raw = JSON.parse(readFileSync(ALLOWLIST_PATH, "utf8")) as AllowlistFile;
|
|
const listed = new Set(raw.hosts.map((h) => h.hostname));
|
|
const inDocs = raw.hosts.filter((h) => h.inDocs).map((h) => h.hostname);
|
|
const docTexts = Object.fromEntries(
|
|
DOC_PATHS.map((path) => [path, readFileSync(join(ROOT, path), "utf8")]),
|
|
);
|
|
|
|
it("is a versioned host list", () => {
|
|
expect(raw.version).toBe(1);
|
|
expect(raw.hosts.length).toBeGreaterThan(0);
|
|
expect(listed.has("10x-toolkit-api.przeprogramowani.workers.dev")).toBe(true);
|
|
expect(listed.has("registry.npmjs.org")).toBe(true);
|
|
});
|
|
|
|
it("lists every public hostname referenced from src/", () => {
|
|
const missing: string[] = [];
|
|
for (const [host, files] of hostsInSource()) {
|
|
if (!listed.has(host)) missing.push(`${host} (${files.join(", ")})`);
|
|
}
|
|
expect(missing).toEqual([]);
|
|
});
|
|
|
|
it("lists every non-vendor hostname referenced from both sysadmin docs", () => {
|
|
const missing: string[] = [];
|
|
for (const path of DOC_PATHS) {
|
|
const text = docTexts[path] ?? "";
|
|
for (const host of hostsInText(text)) {
|
|
if (!isPublicHostname(host) || VENDOR_DOC_HOSTS.has(host)) continue;
|
|
if (!listed.has(host)) missing.push(`${host} (${path})`);
|
|
}
|
|
}
|
|
expect(missing).toEqual([]);
|
|
});
|
|
|
|
it("requires inDocs hosts in both the Polish and English documents", () => {
|
|
const missing: string[] = [];
|
|
for (const path of DOC_PATHS) {
|
|
const text = docTexts[path] ?? "";
|
|
for (const host of inDocs) {
|
|
if (!text.includes(host)) missing.push(`${host} missing from ${path}`);
|
|
}
|
|
}
|
|
expect(missing).toEqual([]);
|
|
});
|
|
});
|