brainblast 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{chunk-XQUQOBXZ.js → chunk-5LJXC66F.js} +242 -29
- package/dist/cli.js +103 -4
- package/dist/index.d.ts +83 -2
- package/dist/index.js +29 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -184,7 +184,7 @@ All types are exported: `Rule`, `CheckResult`, `CostReport`, `AccountFlow`,
|
|
|
184
184
|
|
|
185
185
|
```sh
|
|
186
186
|
npm install
|
|
187
|
-
npm test # unit suite (
|
|
187
|
+
npm test # unit suite (214 tests)
|
|
188
188
|
npm run prove # end-to-end: generated tests RED on vulnerable, GREEN on fixed
|
|
189
189
|
npm run build # produce dist/ (the published artifact)
|
|
190
190
|
```
|
|
@@ -1486,63 +1486,113 @@ function loadRules(dir) {
|
|
|
1486
1486
|
return rules2;
|
|
1487
1487
|
}
|
|
1488
1488
|
|
|
1489
|
+
// src/packs.ts
|
|
1490
|
+
import { existsSync, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
1491
|
+
import { join as join5 } from "path";
|
|
1492
|
+
import { parse as parse2 } from "yaml";
|
|
1493
|
+
var PACK_MANIFEST_FILE = "brainblast-pack.yaml";
|
|
1494
|
+
function validatePackManifest(m, file) {
|
|
1495
|
+
const errs = [];
|
|
1496
|
+
if (!m || typeof m !== "object") {
|
|
1497
|
+
throw new Error(`invalid pack manifest in ${file}: not a mapping`);
|
|
1498
|
+
}
|
|
1499
|
+
if (!m.id || typeof m.id !== "string") errs.push("missing id");
|
|
1500
|
+
if (!m.name || typeof m.name !== "string") errs.push("missing name");
|
|
1501
|
+
if (!m.version || typeof m.version !== "string") errs.push("missing version");
|
|
1502
|
+
if (!m.author || typeof m.author !== "string") errs.push("missing author");
|
|
1503
|
+
if (errs.length) throw new Error(`invalid pack manifest in ${file}: ${errs.join("; ")}`);
|
|
1504
|
+
}
|
|
1505
|
+
function loadPack(dir) {
|
|
1506
|
+
const manifestPath = join5(dir, PACK_MANIFEST_FILE);
|
|
1507
|
+
const raw = parse2(readFileSync5(manifestPath, "utf8"));
|
|
1508
|
+
validatePackManifest(raw, manifestPath);
|
|
1509
|
+
const manifest = raw;
|
|
1510
|
+
const rulesDir = join5(dir, "rules");
|
|
1511
|
+
const rules2 = existsSync(rulesDir) ? loadRules(rulesDir).map((r) => ({
|
|
1512
|
+
...r,
|
|
1513
|
+
pack: { id: manifest.id, version: manifest.version, author: manifest.author }
|
|
1514
|
+
})) : [];
|
|
1515
|
+
return { manifest, rules: rules2 };
|
|
1516
|
+
}
|
|
1517
|
+
function loadPacksFromDir(packsDir) {
|
|
1518
|
+
if (!existsSync(packsDir)) return [];
|
|
1519
|
+
const out = [];
|
|
1520
|
+
for (const entry of readdirSync4(packsDir).sort()) {
|
|
1521
|
+
const dir = join5(packsDir, entry);
|
|
1522
|
+
if (!statSync3(dir).isDirectory()) continue;
|
|
1523
|
+
if (!existsSync(join5(dir, PACK_MANIFEST_FILE))) continue;
|
|
1524
|
+
out.push(loadPack(dir));
|
|
1525
|
+
}
|
|
1526
|
+
return out;
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1489
1529
|
// rules/index.ts
|
|
1490
|
-
import { existsSync } from "fs";
|
|
1491
|
-
import { dirname, join as
|
|
1530
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1531
|
+
import { dirname, join as join6 } from "path";
|
|
1492
1532
|
import { fileURLToPath } from "url";
|
|
1493
1533
|
function bundledRulesDir() {
|
|
1494
1534
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1495
|
-
if (
|
|
1496
|
-
const sub =
|
|
1497
|
-
if (
|
|
1535
|
+
if (existsSync2(join6(here, "stripe-webhook-raw-body.yaml"))) return here;
|
|
1536
|
+
const sub = join6(here, "rules");
|
|
1537
|
+
if (existsSync2(join6(sub, "stripe-webhook-raw-body.yaml"))) return sub;
|
|
1498
1538
|
return here;
|
|
1499
1539
|
}
|
|
1500
1540
|
var rules = loadRules(bundledRulesDir());
|
|
1501
1541
|
|
|
1502
1542
|
// src/resolveRules.ts
|
|
1503
|
-
import { existsSync as
|
|
1504
|
-
import { join as
|
|
1505
|
-
function resolveRules(targetDir) {
|
|
1543
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1544
|
+
import { join as join7 } from "path";
|
|
1545
|
+
function resolveRules(targetDir, extraPackDirs = []) {
|
|
1506
1546
|
const all = [...rules];
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
const
|
|
1510
|
-
for (const r of loadRules(projDir)) {
|
|
1547
|
+
const seen = new Set(all.map((r) => r.id));
|
|
1548
|
+
const addRules = (rules2, sourceLabel) => {
|
|
1549
|
+
for (const r of rules2) {
|
|
1511
1550
|
if (seen.has(r.id)) {
|
|
1512
|
-
console.warn(`brainblast:
|
|
1551
|
+
console.warn(`brainblast: rule '${r.id}' from ${sourceLabel} shadows an existing rule; keeping the first one loaded.`);
|
|
1513
1552
|
continue;
|
|
1514
1553
|
}
|
|
1515
1554
|
all.push(r);
|
|
1516
1555
|
seen.add(r.id);
|
|
1517
1556
|
}
|
|
1557
|
+
};
|
|
1558
|
+
const projDir = join7(targetDir, ".agent-research", "rules");
|
|
1559
|
+
if (existsSync3(projDir)) {
|
|
1560
|
+
addRules(loadRules(projDir), "project rules");
|
|
1561
|
+
}
|
|
1562
|
+
for (const { manifest, rules: rules2 } of loadPacksFromDir(join7(targetDir, ".agent-research", "packs"))) {
|
|
1563
|
+
addRules(rules2, `pack '${manifest.id}'`);
|
|
1564
|
+
}
|
|
1565
|
+
for (const dir of extraPackDirs) {
|
|
1566
|
+
const { manifest, rules: rules2 } = loadPack(dir);
|
|
1567
|
+
addRules(rules2, `pack '${manifest.id}' (${dir})`);
|
|
1518
1568
|
}
|
|
1519
1569
|
return all;
|
|
1520
1570
|
}
|
|
1521
1571
|
|
|
1522
1572
|
// src/trustGraph/directory.ts
|
|
1523
|
-
import { readFileSync as
|
|
1573
|
+
import { readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
|
|
1524
1574
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1525
|
-
import { join as
|
|
1526
|
-
import { parse as
|
|
1575
|
+
import { join as join8 } from "path";
|
|
1576
|
+
import { parse as parse3 } from "yaml";
|
|
1527
1577
|
var cache = null;
|
|
1528
1578
|
function bundledPath() {
|
|
1529
1579
|
const here = fileURLToPath2(new URL(".", import.meta.url));
|
|
1530
1580
|
const candidates = [
|
|
1531
|
-
|
|
1581
|
+
join8(here, "programs", "directory.yaml"),
|
|
1532
1582
|
// dist/programs/directory.yaml
|
|
1533
|
-
|
|
1583
|
+
join8(here, "..", "..", "programs", "directory.yaml"),
|
|
1534
1584
|
// src/../../programs/
|
|
1535
|
-
|
|
1585
|
+
join8(here, "..", "programs", "directory.yaml")
|
|
1536
1586
|
// fallback
|
|
1537
1587
|
];
|
|
1538
1588
|
for (const c of candidates) {
|
|
1539
|
-
if (
|
|
1589
|
+
if (existsSync4(c)) return c;
|
|
1540
1590
|
}
|
|
1541
1591
|
return candidates[0];
|
|
1542
1592
|
}
|
|
1543
1593
|
function loadDirectory(path = bundledPath()) {
|
|
1544
1594
|
if (cache && path === bundledPath()) return cache;
|
|
1545
|
-
const raw =
|
|
1595
|
+
const raw = parse3(readFileSync6(path, "utf8"));
|
|
1546
1596
|
if (!raw || !Array.isArray(raw.programs)) {
|
|
1547
1597
|
throw new Error(`invalid program directory at ${path}: missing 'programs' array`);
|
|
1548
1598
|
}
|
|
@@ -1613,23 +1663,23 @@ function isValidSolanaAddress(s) {
|
|
|
1613
1663
|
}
|
|
1614
1664
|
|
|
1615
1665
|
// src/trustGraph/programCache.ts
|
|
1616
|
-
import { readFileSync as
|
|
1617
|
-
import { join as
|
|
1666
|
+
import { readFileSync as readFileSync7, writeFileSync, mkdirSync, existsSync as existsSync5 } from "fs";
|
|
1667
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
1618
1668
|
import { homedir } from "os";
|
|
1619
1669
|
var DEFAULT_TTL_HOURS = 168;
|
|
1620
1670
|
var SCHEMA_VERSION = "1.0";
|
|
1621
1671
|
function defaultCachePath() {
|
|
1622
1672
|
const envOverride = process.env["BRAINBLAST_CACHE_PATH"];
|
|
1623
|
-
return envOverride ??
|
|
1673
|
+
return envOverride ?? join9(homedir(), ".brainblast", "program-cache.json");
|
|
1624
1674
|
}
|
|
1625
1675
|
function emptyCache() {
|
|
1626
1676
|
return { schemaVersion: SCHEMA_VERSION, entries: {} };
|
|
1627
1677
|
}
|
|
1628
1678
|
function loadProgramCache(cachePath) {
|
|
1629
1679
|
const path = cachePath ?? defaultCachePath();
|
|
1630
|
-
if (!
|
|
1680
|
+
if (!existsSync5(path)) return emptyCache();
|
|
1631
1681
|
try {
|
|
1632
|
-
const raw = JSON.parse(
|
|
1682
|
+
const raw = JSON.parse(readFileSync7(path, "utf8"));
|
|
1633
1683
|
if (raw?.schemaVersion !== SCHEMA_VERSION) {
|
|
1634
1684
|
return emptyCache();
|
|
1635
1685
|
}
|
|
@@ -2206,7 +2256,7 @@ function startWatch(targetDir, opts = {}) {
|
|
|
2206
2256
|
}
|
|
2207
2257
|
|
|
2208
2258
|
// src/fixers/applyDiff.ts
|
|
2209
|
-
import { readFileSync as
|
|
2259
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
|
|
2210
2260
|
function parseDiff(diff) {
|
|
2211
2261
|
const lines = diff.split("\n");
|
|
2212
2262
|
const fileLine = lines.find((l) => l.startsWith("+++ b"));
|
|
@@ -2223,7 +2273,7 @@ function parseDiff(diff) {
|
|
|
2223
2273
|
}
|
|
2224
2274
|
function applyDiffToFile(diff) {
|
|
2225
2275
|
const { filePath, oldStart, oldCount, newLines } = parseDiff(diff);
|
|
2226
|
-
const content =
|
|
2276
|
+
const content = readFileSync8(filePath, "utf8");
|
|
2227
2277
|
const fileLines = content.split("\n");
|
|
2228
2278
|
const removedLines = diff.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).map((l) => l.slice(1));
|
|
2229
2279
|
const actual = fileLines.slice(oldStart - 1, oldStart - 1 + oldCount);
|
|
@@ -2233,6 +2283,156 @@ function applyDiffToFile(diff) {
|
|
|
2233
2283
|
return true;
|
|
2234
2284
|
}
|
|
2235
2285
|
|
|
2286
|
+
// src/pack.ts
|
|
2287
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2288
|
+
import { join as join10 } from "path";
|
|
2289
|
+
function initPack(dir, opts) {
|
|
2290
|
+
if (existsSync6(join10(dir, PACK_MANIFEST_FILE))) {
|
|
2291
|
+
throw new Error(`${dir} already contains a ${PACK_MANIFEST_FILE}`);
|
|
2292
|
+
}
|
|
2293
|
+
const manifest = {
|
|
2294
|
+
id: opts.id,
|
|
2295
|
+
name: opts.name ?? opts.id,
|
|
2296
|
+
version: opts.version ?? "0.1.0",
|
|
2297
|
+
author: opts.author ?? "unknown",
|
|
2298
|
+
...opts.description ? { description: opts.description } : {}
|
|
2299
|
+
};
|
|
2300
|
+
mkdirSync2(dir, { recursive: true });
|
|
2301
|
+
mkdirSync2(join10(dir, "rules"), { recursive: true });
|
|
2302
|
+
mkdirSync2(join10(dir, "fixtures"), { recursive: true });
|
|
2303
|
+
const manifestYaml = [
|
|
2304
|
+
`id: ${manifest.id}`,
|
|
2305
|
+
`name: ${manifest.name}`,
|
|
2306
|
+
`version: ${manifest.version}`,
|
|
2307
|
+
`author: ${manifest.author}`,
|
|
2308
|
+
...manifest.description ? [`description: ${manifest.description}`] : [],
|
|
2309
|
+
""
|
|
2310
|
+
].join("\n");
|
|
2311
|
+
const manifestFile = join10(dir, PACK_MANIFEST_FILE);
|
|
2312
|
+
writeFileSync3(manifestFile, manifestYaml, "utf8");
|
|
2313
|
+
return manifestFile;
|
|
2314
|
+
}
|
|
2315
|
+
function validatePack(dir) {
|
|
2316
|
+
const { manifest, rules: rules2 } = loadPack(dir);
|
|
2317
|
+
const fixturesRoot = join10(dir, "fixtures");
|
|
2318
|
+
const ruleResults = rules2.map((rule) => {
|
|
2319
|
+
const ruleFixturesDir = join10(fixturesRoot, rule.id);
|
|
2320
|
+
const vulnerableDir = join10(ruleFixturesDir, "vulnerable");
|
|
2321
|
+
const fixedDir = join10(ruleFixturesDir, "fixed");
|
|
2322
|
+
if (!existsSync6(vulnerableDir) || !existsSync6(fixedDir)) {
|
|
2323
|
+
return {
|
|
2324
|
+
ruleId: rule.id,
|
|
2325
|
+
status: "missing-fixtures",
|
|
2326
|
+
detail: `no fixtures/${rule.id}/{vulnerable,fixed}/ directory \u2014 prove gate skipped`
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
const redChecks = auditWithRule(vulnerableDir, rule);
|
|
2330
|
+
const redFails = redChecks.filter((c) => c.result === "fail");
|
|
2331
|
+
if (redFails.length === 0) {
|
|
2332
|
+
return {
|
|
2333
|
+
ruleId: rule.id,
|
|
2334
|
+
status: "red-failed",
|
|
2335
|
+
detail: `expected at least one FAIL against fixtures/${rule.id}/vulnerable/, got none`
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
const greenChecks = auditWithRule(fixedDir, rule);
|
|
2339
|
+
const greenFails = greenChecks.filter((c) => c.result === "fail");
|
|
2340
|
+
if (greenFails.length > 0) {
|
|
2341
|
+
return {
|
|
2342
|
+
ruleId: rule.id,
|
|
2343
|
+
status: "green-failed",
|
|
2344
|
+
detail: `expected no FAIL against fixtures/${rule.id}/fixed/, got ${greenFails.length}`
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
return { ruleId: rule.id, status: "ok", detail: "RED -> GREEN proven" };
|
|
2348
|
+
});
|
|
2349
|
+
const ok = ruleResults.every((r) => r.status === "ok" || r.status === "missing-fixtures");
|
|
2350
|
+
return { manifest, rules: rules2, ruleResults, ok };
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
// src/telemetry.ts
|
|
2354
|
+
import { createHash, randomUUID } from "crypto";
|
|
2355
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
2356
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2357
|
+
import { homedir as homedir2 } from "os";
|
|
2358
|
+
import { dirname as dirname3, join as join11, resolve } from "path";
|
|
2359
|
+
function sha256Hex(s) {
|
|
2360
|
+
return createHash("sha256").update(s).digest("hex");
|
|
2361
|
+
}
|
|
2362
|
+
function isTelemetryEnabled(targetDir) {
|
|
2363
|
+
const env = process.env.BRAINBLAST_TELEMETRY;
|
|
2364
|
+
if (env === "1" || env === "true") return true;
|
|
2365
|
+
if (env === "0" || env === "false") return false;
|
|
2366
|
+
const configPath = join11(targetDir, ".agent-research", "config.json");
|
|
2367
|
+
if (!existsSync7(configPath)) return false;
|
|
2368
|
+
try {
|
|
2369
|
+
const cfg = JSON.parse(readFileSync9(configPath, "utf8"));
|
|
2370
|
+
return cfg?.telemetry === true;
|
|
2371
|
+
} catch {
|
|
2372
|
+
return false;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
function getUserHash() {
|
|
2376
|
+
const idPath = join11(homedir2(), ".brainblast", "telemetry-id");
|
|
2377
|
+
let id;
|
|
2378
|
+
if (existsSync7(idPath)) {
|
|
2379
|
+
id = readFileSync9(idPath, "utf8").trim();
|
|
2380
|
+
} else {
|
|
2381
|
+
id = randomUUID();
|
|
2382
|
+
mkdirSync3(dirname3(idPath), { recursive: true });
|
|
2383
|
+
writeFileSync4(idPath, id, "utf8");
|
|
2384
|
+
}
|
|
2385
|
+
return sha256Hex(id).slice(0, 16);
|
|
2386
|
+
}
|
|
2387
|
+
function getRepoHash(targetDir) {
|
|
2388
|
+
let key = "";
|
|
2389
|
+
try {
|
|
2390
|
+
key = execFileSync3("git", ["config", "--get", "remote.origin.url"], {
|
|
2391
|
+
cwd: targetDir,
|
|
2392
|
+
encoding: "utf8",
|
|
2393
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2394
|
+
}).trim();
|
|
2395
|
+
} catch {
|
|
2396
|
+
}
|
|
2397
|
+
if (!key) key = resolve(targetDir);
|
|
2398
|
+
return sha256Hex(key).slice(0, 16);
|
|
2399
|
+
}
|
|
2400
|
+
function telemetryFilePath(targetDir) {
|
|
2401
|
+
return join11(targetDir, ".agent-research", "telemetry.ndjson");
|
|
2402
|
+
}
|
|
2403
|
+
var DEFAULT_REGISTRY_URL = "https://registry.brainblast.tech";
|
|
2404
|
+
async function submitTelemetry(targetDir, registryUrl = process.env.BRAINBLAST_REGISTRY_URL || DEFAULT_REGISTRY_URL) {
|
|
2405
|
+
const file = telemetryFilePath(targetDir);
|
|
2406
|
+
if (!existsSync7(file)) {
|
|
2407
|
+
return { submitted: 0, accepted: 0, rejected: 0, graduations: [] };
|
|
2408
|
+
}
|
|
2409
|
+
const events = readFileSync9(file, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
2410
|
+
if (events.length === 0) {
|
|
2411
|
+
return { submitted: 0, accepted: 0, rejected: 0, graduations: [] };
|
|
2412
|
+
}
|
|
2413
|
+
const res = await fetch(`${registryUrl.replace(/\/$/, "")}/api/telemetry`, {
|
|
2414
|
+
method: "POST",
|
|
2415
|
+
headers: { "content-type": "application/json" },
|
|
2416
|
+
body: JSON.stringify({ events })
|
|
2417
|
+
});
|
|
2418
|
+
if (!res.ok) {
|
|
2419
|
+
const body = await res.text().catch(() => "");
|
|
2420
|
+
throw new Error(`telemetry submit failed: ${res.status} ${res.statusText} ${body}`.trim());
|
|
2421
|
+
}
|
|
2422
|
+
const json = await res.json();
|
|
2423
|
+
return { submitted: events.length, ...json };
|
|
2424
|
+
}
|
|
2425
|
+
function recordGraduationEvents(targetDir, events) {
|
|
2426
|
+
if (events.length === 0) return;
|
|
2427
|
+
const file = telemetryFilePath(targetDir);
|
|
2428
|
+
mkdirSync3(dirname3(file), { recursive: true });
|
|
2429
|
+
const repo_hash = getRepoHash(targetDir);
|
|
2430
|
+
const user_hash = getUserHash();
|
|
2431
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2432
|
+
const lines = events.map((e) => JSON.stringify({ ...e, repo_hash, user_hash, timestamp })).join("\n");
|
|
2433
|
+
appendFileSync(file, lines + "\n", "utf8");
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2236
2436
|
export {
|
|
2237
2437
|
findCandidates,
|
|
2238
2438
|
findConfigCandidates,
|
|
@@ -2247,6 +2447,10 @@ export {
|
|
|
2247
2447
|
renderTest,
|
|
2248
2448
|
testKinds,
|
|
2249
2449
|
loadRules,
|
|
2450
|
+
PACK_MANIFEST_FILE,
|
|
2451
|
+
validatePackManifest,
|
|
2452
|
+
loadPack,
|
|
2453
|
+
loadPacksFromDir,
|
|
2250
2454
|
rules,
|
|
2251
2455
|
resolveRules,
|
|
2252
2456
|
loadDirectory,
|
|
@@ -2271,5 +2475,14 @@ export {
|
|
|
2271
2475
|
runIncrementalScan,
|
|
2272
2476
|
startWatch,
|
|
2273
2477
|
parseDiff,
|
|
2274
|
-
applyDiffToFile
|
|
2478
|
+
applyDiffToFile,
|
|
2479
|
+
initPack,
|
|
2480
|
+
validatePack,
|
|
2481
|
+
isTelemetryEnabled,
|
|
2482
|
+
getUserHash,
|
|
2483
|
+
getRepoHash,
|
|
2484
|
+
telemetryFilePath,
|
|
2485
|
+
DEFAULT_REGISTRY_URL,
|
|
2486
|
+
submitTelemetry,
|
|
2487
|
+
recordGraduationEvents
|
|
2275
2488
|
};
|
package/dist/cli.js
CHANGED
|
@@ -7,14 +7,20 @@ import {
|
|
|
7
7
|
cacheSize,
|
|
8
8
|
defaultCachePath,
|
|
9
9
|
getChangedRanges,
|
|
10
|
+
initPack,
|
|
11
|
+
isTelemetryEnabled,
|
|
10
12
|
isValidSolanaAddress,
|
|
11
13
|
loadProgramCache,
|
|
12
14
|
parseDiff,
|
|
15
|
+
recordGraduationEvents,
|
|
13
16
|
renderCostReportMd,
|
|
14
17
|
renderTrustGraphMd,
|
|
15
18
|
resolveRules,
|
|
16
|
-
startWatch
|
|
17
|
-
|
|
19
|
+
startWatch,
|
|
20
|
+
submitTelemetry,
|
|
21
|
+
telemetryFilePath,
|
|
22
|
+
validatePack
|
|
23
|
+
} from "./chunk-5LJXC66F.js";
|
|
18
24
|
|
|
19
25
|
// src/cli.ts
|
|
20
26
|
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -94,10 +100,25 @@ function updateMemory(memory, checks2, now = /* @__PURE__ */ new Date()) {
|
|
|
94
100
|
// src/cli.ts
|
|
95
101
|
import { execFileSync } from "child_process";
|
|
96
102
|
var args = process.argv.slice(2);
|
|
103
|
+
function parsePackDirs(argv) {
|
|
104
|
+
const idx = argv.indexOf("--packs");
|
|
105
|
+
if (idx < 0) return [];
|
|
106
|
+
const value = argv[idx + 1];
|
|
107
|
+
if (!value || value.startsWith("--")) return [];
|
|
108
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
109
|
+
}
|
|
97
110
|
if (args[0] === "trust-graph") {
|
|
98
111
|
await runTrustGraph(args.slice(1));
|
|
99
112
|
process.exit(0);
|
|
100
113
|
}
|
|
114
|
+
if (args[0] === "pack") {
|
|
115
|
+
runPack(args.slice(1));
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
if (args[0] === "telemetry") {
|
|
119
|
+
await runTelemetry(args.slice(1));
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
101
122
|
if (args[0] === "watch") {
|
|
102
123
|
const watchDir = args.find((a, i) => i > 0 && !a.startsWith("--")) ?? process.cwd();
|
|
103
124
|
startWatch(watchDir);
|
|
@@ -119,7 +140,7 @@ if (sinceIdx >= 0 && !since) {
|
|
|
119
140
|
console.error("error: --since requires a <ref> argument, e.g. --since origin/main");
|
|
120
141
|
process.exit(2);
|
|
121
142
|
}
|
|
122
|
-
var rules = resolveRules(targetDir);
|
|
143
|
+
var rules = resolveRules(targetDir, parsePackDirs(args));
|
|
123
144
|
var changedRanges;
|
|
124
145
|
if (since) {
|
|
125
146
|
try {
|
|
@@ -211,6 +232,75 @@ if (ci) {
|
|
|
211
232
|
const gateFail = fails > 0 || strict && cantTell > 0;
|
|
212
233
|
process.exit(gateFail ? 1 : 0);
|
|
213
234
|
}
|
|
235
|
+
function runPack(argv) {
|
|
236
|
+
const sub = argv[0];
|
|
237
|
+
if (sub === "init") {
|
|
238
|
+
const dir = argv.find((a, i) => i > 0 && !a.startsWith("--") && argv[i - 1] !== "--id" && argv[i - 1] !== "--name" && argv[i - 1] !== "--author" && argv[i - 1] !== "--version" && argv[i - 1] !== "--description");
|
|
239
|
+
const flag = (name) => {
|
|
240
|
+
const idx = argv.indexOf(`--${name}`);
|
|
241
|
+
return idx >= 0 ? argv[idx + 1] : void 0;
|
|
242
|
+
};
|
|
243
|
+
const id = flag("id");
|
|
244
|
+
if (!dir || !id) {
|
|
245
|
+
console.error("usage: brainblast pack init <dir> --id <pack-id> [--name <name>] [--author <author>] [--version <semver>] [--description <text>]");
|
|
246
|
+
process.exit(2);
|
|
247
|
+
}
|
|
248
|
+
const manifestFile = initPack(dir, {
|
|
249
|
+
id,
|
|
250
|
+
name: flag("name"),
|
|
251
|
+
author: flag("author"),
|
|
252
|
+
version: flag("version"),
|
|
253
|
+
description: flag("description")
|
|
254
|
+
});
|
|
255
|
+
console.log(`brainblast pack init: wrote ${manifestFile}`);
|
|
256
|
+
console.log(` rules: ${join2(dir, "rules")}/`);
|
|
257
|
+
console.log(` fixtures: ${join2(dir, "fixtures")}/`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (sub === "validate") {
|
|
261
|
+
const dir = argv.find((a, i) => i > 0 && !a.startsWith("--"));
|
|
262
|
+
if (!dir) {
|
|
263
|
+
console.error("usage: brainblast pack validate <dir>");
|
|
264
|
+
process.exit(2);
|
|
265
|
+
}
|
|
266
|
+
const result = validatePack(dir);
|
|
267
|
+
console.log(`pack: ${result.manifest.id} v${result.manifest.version} (${result.manifest.author})`);
|
|
268
|
+
console.log(` ${result.rules.length} rule(s)`);
|
|
269
|
+
for (const r of result.ruleResults) {
|
|
270
|
+
const marker = r.status === "ok" ? "OK" : r.status === "missing-fixtures" ? "WARN" : "FAIL";
|
|
271
|
+
console.log(` [${marker}] ${r.ruleId}: ${r.detail}`);
|
|
272
|
+
}
|
|
273
|
+
process.exit(result.ok ? 0 : 1);
|
|
274
|
+
}
|
|
275
|
+
console.error("usage: brainblast pack <init|validate> ...");
|
|
276
|
+
process.exit(2);
|
|
277
|
+
}
|
|
278
|
+
async function runTelemetry(argv) {
|
|
279
|
+
const sub = argv[0];
|
|
280
|
+
if (sub !== "submit") {
|
|
281
|
+
console.error("usage: brainblast telemetry submit [targetDir]");
|
|
282
|
+
process.exit(2);
|
|
283
|
+
}
|
|
284
|
+
const targetDir2 = argv.find((a, i) => i > 0 && !a.startsWith("--")) ?? process.cwd();
|
|
285
|
+
try {
|
|
286
|
+
const result = await submitTelemetry(targetDir2);
|
|
287
|
+
if (result.submitted === 0) {
|
|
288
|
+
console.log(`brainblast telemetry submit: no events to submit (${telemetryFilePath(targetDir2)} is empty or missing)`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
console.log(`brainblast telemetry submit: sent ${result.submitted} event(s) \u2014 ${result.accepted} accepted, ${result.rejected} rate-limited`);
|
|
292
|
+
for (const g of result.graduations) {
|
|
293
|
+
if (g.graduated) {
|
|
294
|
+
console.log(` [GRADUATED] ${g.pack_id}/${g.rule_id} (${g.distinct_pairs} distinct repo/user pairs)`);
|
|
295
|
+
} else {
|
|
296
|
+
console.log(` [PROGRESS] ${g.pack_id}/${g.rule_id} ${g.distinct_pairs}/5 distinct repo/user pairs`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} catch (e) {
|
|
300
|
+
console.error(`brainblast telemetry submit: ${e.message ?? String(e)}`);
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
214
304
|
async function runTrustGraph(argv) {
|
|
215
305
|
const rpcIdx = argv.indexOf("--rpc");
|
|
216
306
|
const rpcUrl = rpcIdx >= 0 ? argv[rpcIdx + 1] : void 0;
|
|
@@ -248,7 +338,7 @@ async function runFix(argv) {
|
|
|
248
338
|
const apply = argv.includes("--apply");
|
|
249
339
|
const branch = argv.includes("--branch");
|
|
250
340
|
const targetDir2 = argv.find((a) => !a.startsWith("--")) ?? process.cwd();
|
|
251
|
-
const rules2 = resolveRules(targetDir2);
|
|
341
|
+
const rules2 = resolveRules(targetDir2, parsePackDirs(argv));
|
|
252
342
|
const { checks: before } = audit(targetDir2, rules2);
|
|
253
343
|
const fixable = before.filter((c) => c.result === "fail" && c.fix?.diff);
|
|
254
344
|
if (fixable.length === 0) {
|
|
@@ -300,6 +390,15 @@ Warning: ${stillFailing.length} fix(es) applied but the rule still fails:`);
|
|
|
300
390
|
} else if (applied > 0) {
|
|
301
391
|
console.log("All applied fixes now pass (or cant_tell) on re-audit. \u2713");
|
|
302
392
|
}
|
|
393
|
+
if (isTelemetryEnabled(targetDir2)) {
|
|
394
|
+
const graduated = fixable.filter((c) => !stillFailing.includes(c));
|
|
395
|
+
const events = graduated.map((c) => rules2.find((r) => r.id === c.ruleId)).filter((r) => !!r?.pack).map((r) => ({ pack_id: r.pack.id, rule_id: r.id }));
|
|
396
|
+
if (events.length > 0) {
|
|
397
|
+
recordGraduationEvents(targetDir2, events);
|
|
398
|
+
console.log(`
|
|
399
|
+
Telemetry: recorded ${events.length} graduation event(s) to ${telemetryFilePath(targetDir2)}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
303
402
|
if (branch && applied > 0) {
|
|
304
403
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
305
404
|
const branchName = `brainblast/auto-fix-${ts}`;
|
package/dist/index.d.ts
CHANGED
|
@@ -156,6 +156,23 @@ interface Rule {
|
|
|
156
156
|
kind: string;
|
|
157
157
|
params?: Record<string, any>;
|
|
158
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* Provenance for rules loaded from a third-party rule pack (see
|
|
161
|
+
* src/packs.ts). Absent for bundled rules. Stamped by the pack loader from
|
|
162
|
+
* the pack's brainblast-pack.yaml manifest, not set by the rule author.
|
|
163
|
+
*/
|
|
164
|
+
pack?: {
|
|
165
|
+
id: string;
|
|
166
|
+
version: string;
|
|
167
|
+
author?: string;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
interface PackManifest {
|
|
171
|
+
id: string;
|
|
172
|
+
name: string;
|
|
173
|
+
version: string;
|
|
174
|
+
author: string;
|
|
175
|
+
description?: string;
|
|
159
176
|
}
|
|
160
177
|
type Checker = (candidate: Candidate, params: any) => CheckOutcome;
|
|
161
178
|
type RustChecker = (candidate: RustCandidate, params: any) => CheckOutcome;
|
|
@@ -224,7 +241,7 @@ declare function audit(targetDir: string, rules: Rule[], changedRanges?: Changed
|
|
|
224
241
|
};
|
|
225
242
|
};
|
|
226
243
|
|
|
227
|
-
declare function resolveRules(targetDir: string): Rule[];
|
|
244
|
+
declare function resolveRules(targetDir: string, extraPackDirs?: string[]): Rule[];
|
|
228
245
|
|
|
229
246
|
declare function loadRules(dir: string): Rule[];
|
|
230
247
|
|
|
@@ -285,6 +302,70 @@ interface ParsedDiff {
|
|
|
285
302
|
declare function parseDiff(diff: string): ParsedDiff;
|
|
286
303
|
declare function applyDiffToFile(diff: string): boolean;
|
|
287
304
|
|
|
305
|
+
declare const PACK_MANIFEST_FILE = "brainblast-pack.yaml";
|
|
306
|
+
declare function validatePackManifest(m: any, file: string): void;
|
|
307
|
+
declare function loadPack(dir: string): {
|
|
308
|
+
manifest: PackManifest;
|
|
309
|
+
rules: Rule[];
|
|
310
|
+
};
|
|
311
|
+
declare function loadPacksFromDir(packsDir: string): {
|
|
312
|
+
manifest: PackManifest;
|
|
313
|
+
rules: Rule[];
|
|
314
|
+
}[];
|
|
315
|
+
|
|
316
|
+
interface PackInitOptions {
|
|
317
|
+
id: string;
|
|
318
|
+
name?: string;
|
|
319
|
+
author?: string;
|
|
320
|
+
version?: string;
|
|
321
|
+
description?: string;
|
|
322
|
+
}
|
|
323
|
+
declare function initPack(dir: string, opts: PackInitOptions): string;
|
|
324
|
+
interface PackValidateResult {
|
|
325
|
+
manifest: PackManifest;
|
|
326
|
+
rules: Rule[];
|
|
327
|
+
/** Per-rule prove-gate results. */
|
|
328
|
+
ruleResults: PackRuleValidation[];
|
|
329
|
+
/** True if the manifest is valid, every rule loaded cleanly, and every rule with fixtures passed RED->GREEN. */
|
|
330
|
+
ok: boolean;
|
|
331
|
+
}
|
|
332
|
+
interface PackRuleValidation {
|
|
333
|
+
ruleId: string;
|
|
334
|
+
/** "ok" | "missing-fixtures" | "red-failed" | "green-failed" */
|
|
335
|
+
status: "ok" | "missing-fixtures" | "red-failed" | "green-failed";
|
|
336
|
+
detail: string;
|
|
337
|
+
}
|
|
338
|
+
declare function validatePack(dir: string): PackValidateResult;
|
|
339
|
+
|
|
340
|
+
interface GraduationEvent {
|
|
341
|
+
pack_id: string;
|
|
342
|
+
rule_id: string;
|
|
343
|
+
repo_hash: string;
|
|
344
|
+
user_hash: string;
|
|
345
|
+
timestamp: string;
|
|
346
|
+
}
|
|
347
|
+
declare function isTelemetryEnabled(targetDir: string): boolean;
|
|
348
|
+
declare function getUserHash(): string;
|
|
349
|
+
declare function getRepoHash(targetDir: string): string;
|
|
350
|
+
declare function telemetryFilePath(targetDir: string): string;
|
|
351
|
+
declare const DEFAULT_REGISTRY_URL = "https://registry.brainblast.tech";
|
|
352
|
+
interface TelemetrySubmitResult {
|
|
353
|
+
submitted: number;
|
|
354
|
+
accepted: number;
|
|
355
|
+
rejected: number;
|
|
356
|
+
graduations: {
|
|
357
|
+
pack_id: string;
|
|
358
|
+
rule_id: string;
|
|
359
|
+
distinct_pairs: number;
|
|
360
|
+
graduated: boolean;
|
|
361
|
+
}[];
|
|
362
|
+
}
|
|
363
|
+
declare function submitTelemetry(targetDir: string, registryUrl?: string): Promise<TelemetrySubmitResult>;
|
|
364
|
+
declare function recordGraduationEvents(targetDir: string, events: {
|
|
365
|
+
pack_id: string;
|
|
366
|
+
rule_id: string;
|
|
367
|
+
}[]): void;
|
|
368
|
+
|
|
288
369
|
type UpgradeAuthorityKind = "renounced" | "single-key" | "multisig" | "dao" | "unknown";
|
|
289
370
|
type UpgradeAuthoritySource = "directory" | "rpc" | "research";
|
|
290
371
|
interface UpgradeAuthority {
|
|
@@ -414,4 +495,4 @@ declare function isEntryExpired(entry: ProgramCacheEntry, ttlHoursOverride?: num
|
|
|
414
495
|
*/
|
|
415
496
|
declare function cacheSize(cache: ProgramCache, ttlHoursOverride?: number): number;
|
|
416
497
|
|
|
417
|
-
export { type AccountFlow, type AuditRef, type BuildOpts, type Candidate, type ChangedRanges, type CheckOutcome, type CheckResult, type CheckResultKind, type Checker, type ConfigCandidate, type ConfigChecker, type CostReport, DEFAULT_TTL_HOURS, type OnChainProgram, type ParityNote, type ParsedDiff, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type Rule, type RustAccountField, type RustCandidate, type RustChecker, type Severity, type TrustGraph, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, type WatchEvent, type WatchOptions, analyzeCosts, applyDiffToFile, audit, auditWithRule, base58Decode, base58Encode, buildTrustGraph, rules as bundledRules, cacheSize, checkerKinds, defaultCachePath, fileChanged, findCandidates, findConfigCandidates, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, getWorkingTreeChanges, isEntryExpired, isValidSolanaAddress, lamportsToSol, loadDirectory, loadProgramCache, loadRules, parseDiff, putCacheEntry, rangeChanged, renderCostReportMd, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, runChecker, runIncrementalScan, saveProgramCache, startWatch, testKinds };
|
|
498
|
+
export { type AccountFlow, type AuditRef, type BuildOpts, type Candidate, type ChangedRanges, type CheckOutcome, type CheckResult, type CheckResultKind, type Checker, type ConfigCandidate, type ConfigChecker, type CostReport, DEFAULT_REGISTRY_URL, DEFAULT_TTL_HOURS, type GraduationEvent, type OnChainProgram, PACK_MANIFEST_FILE, type PackInitOptions, type PackManifest, type PackRuleValidation, type PackValidateResult, type ParityNote, type ParsedDiff, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type Rule, type RustAccountField, type RustCandidate, type RustChecker, type Severity, type TelemetrySubmitResult, type TrustGraph, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, type WatchEvent, type WatchOptions, analyzeCosts, applyDiffToFile, audit, auditWithRule, base58Decode, base58Encode, buildTrustGraph, rules as bundledRules, cacheSize, checkerKinds, defaultCachePath, fileChanged, findCandidates, findConfigCandidates, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, getRepoHash, getUserHash, getWorkingTreeChanges, initPack, isEntryExpired, isTelemetryEnabled, isValidSolanaAddress, lamportsToSol, loadDirectory, loadPack, loadPacksFromDir, loadProgramCache, loadRules, parseDiff, putCacheEntry, rangeChanged, recordGraduationEvents, renderCostReportMd, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, runChecker, runIncrementalScan, saveProgramCache, startWatch, submitTelemetry, telemetryFilePath, testKinds, validatePack, validatePackManifest };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_REGISTRY_URL,
|
|
2
3
|
DEFAULT_TTL_HOURS,
|
|
4
|
+
PACK_MANIFEST_FILE,
|
|
3
5
|
analyzeCosts,
|
|
4
6
|
applyDiffToFile,
|
|
5
7
|
audit,
|
|
@@ -16,16 +18,23 @@ import {
|
|
|
16
18
|
getCacheEntry,
|
|
17
19
|
getCacheEntryMeta,
|
|
18
20
|
getChangedRanges,
|
|
21
|
+
getRepoHash,
|
|
22
|
+
getUserHash,
|
|
19
23
|
getWorkingTreeChanges,
|
|
24
|
+
initPack,
|
|
20
25
|
isEntryExpired,
|
|
26
|
+
isTelemetryEnabled,
|
|
21
27
|
isValidSolanaAddress,
|
|
22
28
|
lamportsToSol,
|
|
23
29
|
loadDirectory,
|
|
30
|
+
loadPack,
|
|
31
|
+
loadPacksFromDir,
|
|
24
32
|
loadProgramCache,
|
|
25
33
|
loadRules,
|
|
26
34
|
parseDiff,
|
|
27
35
|
putCacheEntry,
|
|
28
36
|
rangeChanged,
|
|
37
|
+
recordGraduationEvents,
|
|
29
38
|
renderCostReportMd,
|
|
30
39
|
renderTest,
|
|
31
40
|
renderTrustGraphMd,
|
|
@@ -36,8 +45,12 @@ import {
|
|
|
36
45
|
runIncrementalScan,
|
|
37
46
|
saveProgramCache,
|
|
38
47
|
startWatch,
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
submitTelemetry,
|
|
49
|
+
telemetryFilePath,
|
|
50
|
+
testKinds,
|
|
51
|
+
validatePack,
|
|
52
|
+
validatePackManifest
|
|
53
|
+
} from "./chunk-5LJXC66F.js";
|
|
41
54
|
|
|
42
55
|
// src/generate.ts
|
|
43
56
|
import { writeFileSync, mkdirSync } from "fs";
|
|
@@ -53,7 +66,9 @@ function generateTestForResult(result, rule, outPath) {
|
|
|
53
66
|
return outPath;
|
|
54
67
|
}
|
|
55
68
|
export {
|
|
69
|
+
DEFAULT_REGISTRY_URL,
|
|
56
70
|
DEFAULT_TTL_HOURS,
|
|
71
|
+
PACK_MANIFEST_FILE,
|
|
57
72
|
analyzeCosts,
|
|
58
73
|
applyDiffToFile,
|
|
59
74
|
audit,
|
|
@@ -72,16 +87,23 @@ export {
|
|
|
72
87
|
getCacheEntry,
|
|
73
88
|
getCacheEntryMeta,
|
|
74
89
|
getChangedRanges,
|
|
90
|
+
getRepoHash,
|
|
91
|
+
getUserHash,
|
|
75
92
|
getWorkingTreeChanges,
|
|
93
|
+
initPack,
|
|
76
94
|
isEntryExpired,
|
|
95
|
+
isTelemetryEnabled,
|
|
77
96
|
isValidSolanaAddress,
|
|
78
97
|
lamportsToSol,
|
|
79
98
|
loadDirectory,
|
|
99
|
+
loadPack,
|
|
100
|
+
loadPacksFromDir,
|
|
80
101
|
loadProgramCache,
|
|
81
102
|
loadRules,
|
|
82
103
|
parseDiff,
|
|
83
104
|
putCacheEntry,
|
|
84
105
|
rangeChanged,
|
|
106
|
+
recordGraduationEvents,
|
|
85
107
|
renderCostReportMd,
|
|
86
108
|
renderTest,
|
|
87
109
|
renderTrustGraphMd,
|
|
@@ -91,5 +113,9 @@ export {
|
|
|
91
113
|
runIncrementalScan,
|
|
92
114
|
saveProgramCache,
|
|
93
115
|
startWatch,
|
|
94
|
-
|
|
116
|
+
submitTelemetry,
|
|
117
|
+
telemetryFilePath,
|
|
118
|
+
testKinds,
|
|
119
|
+
validatePack,
|
|
120
|
+
validatePackManifest
|
|
95
121
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainblast",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic auditor for catastrophic AI-integration bugs: scan a repo, find the silent money/auth traps, and generate the behavioral test that proves they're fixed.",
|
|
6
6
|
"keywords": [
|