Files
John Smith 5321a4cea2 fix: fleet audit remediation — RCE/SSRF/traversal fail-closed, catalog re-grounding, CI hardening
- Stage1 P0 security: allowlist introspect (npx,node,python3), argv-only, drop CORS *, per-boot token, SSRF private-range+userinfo block, targetDir absolute/.. block, content_file pattern+containment, test/local-server.test.js 29 tests
- Stage2 P0 catalog: synthetic:true for 14 placeholder entries, real repos/tools for k8s/notion/filesystem/figma/postgres, Codex command/args/env, hyphen-preserving normalize, README npx fix, sst->anomalyco
- Stage3 P1 hardening: dup-id negative tests, check-pr scripts/** fail-closed+wired, ai-review invented-tool/harness/kind/deny-first enforcement, reviewer bare allows, _site clean, engines >=22.12.0, workflows SHA-pinned fetch-depth:1 minimal scopes, CODEOWNERS, ajv->devDeps
- Stage4 P2/P3 polish: install --recipe base64url, SOURCE_DATE_EPOCH idempotency, ? single-char, favicon data-URI, empty warn deny-only, prose fixes, stage4 12 tests

Gates: VALIDATED 63 OK, SITE_BUILT 63, tests 102/102, ai-review REVIEW_OK/SELFTEST_OK
Note: scripts/generate-rigs.py + verify-rigs.sh do not exist; real equivalents used (documented in REMEDIATION_LOG.md)
2026-10-19 11:53:15 -04:00

125 lines
4.3 KiB
JavaScript

// scripts/export-json.js
// Compiles data/**/*.yaml -> dist/registry.json (CommonJS).
const fs = require('fs');
const path = require('path');
const { loadCatalog } = require('./utils/catalog');
const VERSION = '1.0.0';
const REPOSITORY = 'https://github.com/Heretek-AI/Agent-Builder';
const REGISTRY_URL = 'https://heretek-ai.github.io/Agent-Builder/registry.json';
function parseArgs(argv) {
const args = { pretty: false, output: 'dist/registry.json' };
for (let i = 2; i < argv.length; i += 1) {
const a = argv[i];
if (a === '--pretty') {
args.pretty = true;
} else if (a === '--output') {
const next = argv[i + 1];
if (!next || next.startsWith('--')) {
console.error('ERROR: --output requires a <filepath> argument');
process.exit(1);
}
args.output = next;
i += 1;
} else if (a === '--help' || a === '-h') {
console.log('Usage: node scripts/export-json.js [--pretty] [--output <filepath>]');
process.exit(0);
} else {
console.error(`ERROR: unknown argument '${a}'`);
process.exit(1);
}
}
return args;
}
function main() {
const rootDir = path.join(__dirname, '..');
const { pretty, output } = parseArgs(process.argv);
const { entries, errors } = loadCatalog(rootDir);
if (errors.length > 0) {
for (const err of errors) {
console.error(`ERROR: ${err}`);
}
console.error(`EXPORT FAILED: ${errors.length} error(s)`);
process.exit(1);
}
// Raw taxonomy object for output.
const taxonomyPath = path.join(rootDir, 'data', 'taxonomy.json');
const taxonomy = JSON.parse(fs.readFileSync(taxonomyPath, 'utf8'));
const sorted = entries.map((w) => w.entry).sort((a, b) => {
if (a.category < b.category) return -1;
if (a.category > b.category) return 1;
if (a.id < b.id) return -1;
if (a.id > b.id) return 1;
return 0;
});
const byKind = {};
const byCategory = {};
const byHarness = {};
for (const e of sorted) {
byKind[e.kind] = (byKind[e.kind] || 0) + 1;
byCategory[e.category] = (byCategory[e.category] || 0) + 1;
for (const h of e.harnesses || []) {
byHarness[h] = (byHarness[h] || 0) + 1;
}
}
const stats = { total: sorted.length, by_kind: byKind, by_category: byCategory, by_harness: byHarness };
// Content bundler: copy local markdown files referenced by content_file.
const outputAbs = path.isAbsolute(output) ? output : path.join(rootDir, output);
const distDir = path.dirname(outputAbs);
const contentDir = path.join(distDir, 'content');
fs.mkdirSync(distDir, { recursive: true });
fs.mkdirSync(contentDir, { recursive: true });
for (const e of sorted) {
if (!e.content_file) continue;
const cf = String(e.content_file);
// Fail-closed containment: never copy absolute/traversal/URL/non-.md.
// Must match the schema pattern: relative, no `..`, .md suffix.
const isUrl = /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(cf);
const hasTraversal = cf.includes('..');
const isAbsolute = path.isAbsolute(cf);
const isMd = cf.endsWith('.md');
const startsOk = /^[A-Za-z0-9_]/.test(cf);
const noLeadingSlash = !cf.startsWith('/') && !cf.startsWith('\\');
if (isUrl || hasTraversal || isAbsolute || !isMd || !startsOk || !noLeadingSlash) {
console.warn(`WARNING: content_file '${e.content_file}' for entry '${e.id}' rejected (unsafe); skipping.`);
continue;
}
const srcAbs = path.resolve(rootDir, cf);
if (srcAbs !== rootDir && !srcAbs.startsWith(rootDir + path.sep)) {
console.warn(`WARNING: content_file '${e.content_file}' for entry '${e.id}' escapes repo; skipping.`);
continue;
}
const destAbs = path.join(contentDir, `${e.id}.md`);
if (fs.existsSync(srcAbs) && fs.statSync(srcAbs).isFile()) {
fs.copyFileSync(srcAbs, destAbs);
} else {
console.warn(`WARNING: content_file '${e.content_file}' for entry '${e.id}' not found; skipping.`);
}
}
const registry = {
version: VERSION,
repository: REPOSITORY,
registry_url: REGISTRY_URL,
generated_at: new Date().toISOString(),
stats,
taxonomy,
entries: sorted,
};
const json = pretty ? JSON.stringify(registry, null, 2) : JSON.stringify(registry);
fs.writeFileSync(outputAbs, `${json}\n`, 'utf8');
const rel = path.relative(rootDir, outputAbs);
console.log(`EXPORTED ${sorted.length} ENTRIES -> ${rel}`);
}
main();