brainblast 0.4.3 → 0.5.1
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-LOPYKIXE.js} +317 -65
- 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
|
```
|
|
@@ -662,6 +662,44 @@ var taintToSink = (c, p) => {
|
|
|
662
662
|
return { result: "pass", detail: "No tracked source value flows to a sink within the analyzed call graph." };
|
|
663
663
|
};
|
|
664
664
|
|
|
665
|
+
// src/checkers/literalMultiplierWrongConstant.ts
|
|
666
|
+
import { SyntaxKind as SyntaxKind8 } from "ts-morph";
|
|
667
|
+
function callName4(call) {
|
|
668
|
+
const exp = call.getExpression();
|
|
669
|
+
if (exp.getKind() === SyntaxKind8.Identifier) return exp.getText();
|
|
670
|
+
if (exp.getKind() === SyntaxKind8.PropertyAccessExpression) {
|
|
671
|
+
return exp.asKind(SyntaxKind8.PropertyAccessExpression).getName();
|
|
672
|
+
}
|
|
673
|
+
return "";
|
|
674
|
+
}
|
|
675
|
+
function containsIdentifier(node, name) {
|
|
676
|
+
if (node.getKind() === SyntaxKind8.Identifier && node.getText() === name) return true;
|
|
677
|
+
return node.getDescendantsOfKind(SyntaxKind8.Identifier).some((id) => id.getText() === name);
|
|
678
|
+
}
|
|
679
|
+
var literalMultiplierWrongConstant = (c, p) => {
|
|
680
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind8.CallExpression).filter((x) => callName4(x) === p.call);
|
|
681
|
+
if (calls.length === 0) {
|
|
682
|
+
return { result: "cant_tell", detail: p.absentCallDetail };
|
|
683
|
+
}
|
|
684
|
+
const arg = calls[0].getArguments()[p.argIndex];
|
|
685
|
+
if (!arg) {
|
|
686
|
+
return { result: "cant_tell", detail: p.absentCallDetail };
|
|
687
|
+
}
|
|
688
|
+
const forbidden = Array.isArray(p.forbiddenIdentifiers) ? p.forbiddenIdentifiers : [];
|
|
689
|
+
for (const name of forbidden) {
|
|
690
|
+
if (containsIdentifier(arg, name)) {
|
|
691
|
+
return { result: "fail", detail: String(p.failDetail).replace("{got}", name) };
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const expected = Array.isArray(p.expectedIdentifiers) ? p.expectedIdentifiers : [];
|
|
695
|
+
for (const name of expected) {
|
|
696
|
+
if (containsIdentifier(arg, name)) {
|
|
697
|
+
return { result: "pass", detail: String(p.passDetail) };
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return { result: "cant_tell", detail: p.cantTellDetail };
|
|
701
|
+
};
|
|
702
|
+
|
|
665
703
|
// src/checkers/index.ts
|
|
666
704
|
var registry = {
|
|
667
705
|
"positional-arg-identity": positionalArgIdentity,
|
|
@@ -671,7 +709,8 @@ var registry = {
|
|
|
671
709
|
"object-arg-property-literal-equals": objectArgPropertyLiteralEquals,
|
|
672
710
|
"anchor-init-if-needed-guarded": anchorInitIfNeededGuarded,
|
|
673
711
|
"env-secrets-committed": envSecretsCommitted,
|
|
674
|
-
"taint-to-sink": taintToSink
|
|
712
|
+
"taint-to-sink": taintToSink,
|
|
713
|
+
"literal-multiplier-wrong-constant": literalMultiplierWrongConstant
|
|
675
714
|
};
|
|
676
715
|
function runChecker(kind, c, params) {
|
|
677
716
|
const fn = registry[kind];
|
|
@@ -887,7 +926,7 @@ function findRustCandidates(targetDir, rule) {
|
|
|
887
926
|
}
|
|
888
927
|
|
|
889
928
|
// src/fixers/positionalArgIdentity.ts
|
|
890
|
-
import { SyntaxKind as
|
|
929
|
+
import { SyntaxKind as SyntaxKind9 } from "ts-morph";
|
|
891
930
|
|
|
892
931
|
// src/fixers/diffUtil.ts
|
|
893
932
|
function buildDiff(node, replacement) {
|
|
@@ -912,9 +951,9 @@ function buildDiff(node, replacement) {
|
|
|
912
951
|
// src/fixers/positionalArgIdentity.ts
|
|
913
952
|
var fixPositionalArgIdentity = (c, p, outcome) => {
|
|
914
953
|
if (outcome.result !== "fail") return void 0;
|
|
915
|
-
const calls = c.fn.getDescendantsOfKind(
|
|
954
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind9.CallExpression).filter((call) => {
|
|
916
955
|
const exp = call.getExpression();
|
|
917
|
-
return exp.getKind() ===
|
|
956
|
+
return exp.getKind() === SyntaxKind9.PropertyAccessExpression && exp.asKind(SyntaxKind9.PropertyAccessExpression).getName() === p.call;
|
|
918
957
|
});
|
|
919
958
|
if (calls.length === 0) {
|
|
920
959
|
const wantParam2 = c.params[p.paramIndex] ?? "<rawBodyParam>";
|
|
@@ -929,7 +968,7 @@ Do not call JSON.parse() on the body before this verification step.`
|
|
|
929
968
|
}
|
|
930
969
|
const arg = calls[0].getArguments()[p.argIndex];
|
|
931
970
|
const wantParam = c.params[p.paramIndex];
|
|
932
|
-
if (arg && wantParam && arg.getKind() ===
|
|
971
|
+
if (arg && wantParam && arg.getKind() === SyntaxKind9.CallExpression) {
|
|
933
972
|
return {
|
|
934
973
|
summary: `Pass the raw body parameter '${wantParam}' to ${p.call} instead of a parsed value`,
|
|
935
974
|
diff: buildDiff(arg, wantParam)
|
|
@@ -939,12 +978,12 @@ Do not call JSON.parse() on the body before this verification step.`
|
|
|
939
978
|
};
|
|
940
979
|
|
|
941
980
|
// src/fixers/requiredCallWithOptions.ts
|
|
942
|
-
import { SyntaxKind as
|
|
943
|
-
function
|
|
981
|
+
import { SyntaxKind as SyntaxKind10 } from "ts-morph";
|
|
982
|
+
function callName5(call) {
|
|
944
983
|
const exp = call.getExpression();
|
|
945
|
-
if (exp.getKind() ===
|
|
946
|
-
if (exp.getKind() ===
|
|
947
|
-
return exp.asKind(
|
|
984
|
+
if (exp.getKind() === SyntaxKind10.Identifier) return exp.getText();
|
|
985
|
+
if (exp.getKind() === SyntaxKind10.PropertyAccessExpression) {
|
|
986
|
+
return exp.asKind(SyntaxKind10.PropertyAccessExpression).getName();
|
|
948
987
|
}
|
|
949
988
|
return "";
|
|
950
989
|
}
|
|
@@ -962,15 +1001,15 @@ function placeholderFor(propName) {
|
|
|
962
1001
|
}
|
|
963
1002
|
var fixRequiredCallWithOptions = (c, p, outcome) => {
|
|
964
1003
|
if (outcome.result !== "fail") return void 0;
|
|
965
|
-
const calls = c.fn.getDescendantsOfKind(
|
|
966
|
-
const verify = calls.filter((x) => p.verifyCalls.includes(
|
|
1004
|
+
const calls = c.fn.getDescendantsOfKind(SyntaxKind10.CallExpression);
|
|
1005
|
+
const verify = calls.filter((x) => p.verifyCalls.includes(callName5(x)));
|
|
967
1006
|
if (verify.length > 0) {
|
|
968
1007
|
const call = verify[0];
|
|
969
1008
|
const args = call.getArguments();
|
|
970
1009
|
const lastArg = args[args.length - 1];
|
|
971
|
-
const obj = lastArg?.asKind(
|
|
1010
|
+
const obj = lastArg?.asKind(SyntaxKind10.ObjectLiteralExpression);
|
|
972
1011
|
const presentNames = obj ? obj.getProperties().map((pr) => {
|
|
973
|
-
const pa = pr.asKind(
|
|
1012
|
+
const pa = pr.asKind(SyntaxKind10.PropertyAssignment) ?? pr.asKind(SyntaxKind10.ShorthandPropertyAssignment);
|
|
974
1013
|
return pa?.getName() ?? "";
|
|
975
1014
|
}) : [];
|
|
976
1015
|
const missingGroups = p.requiredProps.filter(
|
|
@@ -978,7 +1017,7 @@ var fixRequiredCallWithOptions = (c, p, outcome) => {
|
|
|
978
1017
|
);
|
|
979
1018
|
if (missingGroups.length === 0) return void 0;
|
|
980
1019
|
const newProps = missingGroups.map((g) => placeholderFor(g[0])).join(", ");
|
|
981
|
-
const summary = `Add ${missingGroups.map((g) => g[0]).join(" and ")} to the ${
|
|
1020
|
+
const summary = `Add ${missingGroups.map((g) => g[0]).join(" and ")} to the ${callName5(call)} call`;
|
|
982
1021
|
if (obj) {
|
|
983
1022
|
const inner = obj.getText().slice(1, -1).trim();
|
|
984
1023
|
const newText = inner.length > 0 ? `{ ${inner}, ${newProps} }` : `{ ${newProps} }`;
|
|
@@ -990,7 +1029,7 @@ var fixRequiredCallWithOptions = (c, p, outcome) => {
|
|
|
990
1029
|
}
|
|
991
1030
|
return {
|
|
992
1031
|
summary,
|
|
993
|
-
suggestion: `Add an options object ({ ${newProps} }) as an argument to ${
|
|
1032
|
+
suggestion: `Add an options object ({ ${newProps} }) as an argument to ${callName5(call)}.`
|
|
994
1033
|
};
|
|
995
1034
|
}
|
|
996
1035
|
return {
|
|
@@ -1486,63 +1525,113 @@ function loadRules(dir) {
|
|
|
1486
1525
|
return rules2;
|
|
1487
1526
|
}
|
|
1488
1527
|
|
|
1528
|
+
// src/packs.ts
|
|
1529
|
+
import { existsSync, readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
1530
|
+
import { join as join5 } from "path";
|
|
1531
|
+
import { parse as parse2 } from "yaml";
|
|
1532
|
+
var PACK_MANIFEST_FILE = "brainblast-pack.yaml";
|
|
1533
|
+
function validatePackManifest(m, file) {
|
|
1534
|
+
const errs = [];
|
|
1535
|
+
if (!m || typeof m !== "object") {
|
|
1536
|
+
throw new Error(`invalid pack manifest in ${file}: not a mapping`);
|
|
1537
|
+
}
|
|
1538
|
+
if (!m.id || typeof m.id !== "string") errs.push("missing id");
|
|
1539
|
+
if (!m.name || typeof m.name !== "string") errs.push("missing name");
|
|
1540
|
+
if (!m.version || typeof m.version !== "string") errs.push("missing version");
|
|
1541
|
+
if (!m.author || typeof m.author !== "string") errs.push("missing author");
|
|
1542
|
+
if (errs.length) throw new Error(`invalid pack manifest in ${file}: ${errs.join("; ")}`);
|
|
1543
|
+
}
|
|
1544
|
+
function loadPack(dir) {
|
|
1545
|
+
const manifestPath = join5(dir, PACK_MANIFEST_FILE);
|
|
1546
|
+
const raw = parse2(readFileSync5(manifestPath, "utf8"));
|
|
1547
|
+
validatePackManifest(raw, manifestPath);
|
|
1548
|
+
const manifest = raw;
|
|
1549
|
+
const rulesDir = join5(dir, "rules");
|
|
1550
|
+
const rules2 = existsSync(rulesDir) ? loadRules(rulesDir).map((r) => ({
|
|
1551
|
+
...r,
|
|
1552
|
+
pack: { id: manifest.id, version: manifest.version, author: manifest.author }
|
|
1553
|
+
})) : [];
|
|
1554
|
+
return { manifest, rules: rules2 };
|
|
1555
|
+
}
|
|
1556
|
+
function loadPacksFromDir(packsDir) {
|
|
1557
|
+
if (!existsSync(packsDir)) return [];
|
|
1558
|
+
const out = [];
|
|
1559
|
+
for (const entry of readdirSync4(packsDir).sort()) {
|
|
1560
|
+
const dir = join5(packsDir, entry);
|
|
1561
|
+
if (!statSync3(dir).isDirectory()) continue;
|
|
1562
|
+
if (!existsSync(join5(dir, PACK_MANIFEST_FILE))) continue;
|
|
1563
|
+
out.push(loadPack(dir));
|
|
1564
|
+
}
|
|
1565
|
+
return out;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1489
1568
|
// rules/index.ts
|
|
1490
|
-
import { existsSync } from "fs";
|
|
1491
|
-
import { dirname, join as
|
|
1569
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1570
|
+
import { dirname, join as join6 } from "path";
|
|
1492
1571
|
import { fileURLToPath } from "url";
|
|
1493
1572
|
function bundledRulesDir() {
|
|
1494
1573
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
1495
|
-
if (
|
|
1496
|
-
const sub =
|
|
1497
|
-
if (
|
|
1574
|
+
if (existsSync2(join6(here, "stripe-webhook-raw-body.yaml"))) return here;
|
|
1575
|
+
const sub = join6(here, "rules");
|
|
1576
|
+
if (existsSync2(join6(sub, "stripe-webhook-raw-body.yaml"))) return sub;
|
|
1498
1577
|
return here;
|
|
1499
1578
|
}
|
|
1500
1579
|
var rules = loadRules(bundledRulesDir());
|
|
1501
1580
|
|
|
1502
1581
|
// src/resolveRules.ts
|
|
1503
|
-
import { existsSync as
|
|
1504
|
-
import { join as
|
|
1505
|
-
function resolveRules(targetDir) {
|
|
1582
|
+
import { existsSync as existsSync3 } from "fs";
|
|
1583
|
+
import { join as join7 } from "path";
|
|
1584
|
+
function resolveRules(targetDir, extraPackDirs = []) {
|
|
1506
1585
|
const all = [...rules];
|
|
1507
|
-
const
|
|
1508
|
-
|
|
1509
|
-
const
|
|
1510
|
-
for (const r of loadRules(projDir)) {
|
|
1586
|
+
const seen = new Set(all.map((r) => r.id));
|
|
1587
|
+
const addRules = (rules2, sourceLabel) => {
|
|
1588
|
+
for (const r of rules2) {
|
|
1511
1589
|
if (seen.has(r.id)) {
|
|
1512
|
-
console.warn(`brainblast:
|
|
1590
|
+
console.warn(`brainblast: rule '${r.id}' from ${sourceLabel} shadows an existing rule; keeping the first one loaded.`);
|
|
1513
1591
|
continue;
|
|
1514
1592
|
}
|
|
1515
1593
|
all.push(r);
|
|
1516
1594
|
seen.add(r.id);
|
|
1517
1595
|
}
|
|
1596
|
+
};
|
|
1597
|
+
const projDir = join7(targetDir, ".agent-research", "rules");
|
|
1598
|
+
if (existsSync3(projDir)) {
|
|
1599
|
+
addRules(loadRules(projDir), "project rules");
|
|
1600
|
+
}
|
|
1601
|
+
for (const { manifest, rules: rules2 } of loadPacksFromDir(join7(targetDir, ".agent-research", "packs"))) {
|
|
1602
|
+
addRules(rules2, `pack '${manifest.id}'`);
|
|
1603
|
+
}
|
|
1604
|
+
for (const dir of extraPackDirs) {
|
|
1605
|
+
const { manifest, rules: rules2 } = loadPack(dir);
|
|
1606
|
+
addRules(rules2, `pack '${manifest.id}' (${dir})`);
|
|
1518
1607
|
}
|
|
1519
1608
|
return all;
|
|
1520
1609
|
}
|
|
1521
1610
|
|
|
1522
1611
|
// src/trustGraph/directory.ts
|
|
1523
|
-
import { readFileSync as
|
|
1612
|
+
import { readFileSync as readFileSync6, existsSync as existsSync4 } from "fs";
|
|
1524
1613
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1525
|
-
import { join as
|
|
1526
|
-
import { parse as
|
|
1614
|
+
import { join as join8 } from "path";
|
|
1615
|
+
import { parse as parse3 } from "yaml";
|
|
1527
1616
|
var cache = null;
|
|
1528
1617
|
function bundledPath() {
|
|
1529
1618
|
const here = fileURLToPath2(new URL(".", import.meta.url));
|
|
1530
1619
|
const candidates = [
|
|
1531
|
-
|
|
1620
|
+
join8(here, "programs", "directory.yaml"),
|
|
1532
1621
|
// dist/programs/directory.yaml
|
|
1533
|
-
|
|
1622
|
+
join8(here, "..", "..", "programs", "directory.yaml"),
|
|
1534
1623
|
// src/../../programs/
|
|
1535
|
-
|
|
1624
|
+
join8(here, "..", "programs", "directory.yaml")
|
|
1536
1625
|
// fallback
|
|
1537
1626
|
];
|
|
1538
1627
|
for (const c of candidates) {
|
|
1539
|
-
if (
|
|
1628
|
+
if (existsSync4(c)) return c;
|
|
1540
1629
|
}
|
|
1541
1630
|
return candidates[0];
|
|
1542
1631
|
}
|
|
1543
1632
|
function loadDirectory(path = bundledPath()) {
|
|
1544
1633
|
if (cache && path === bundledPath()) return cache;
|
|
1545
|
-
const raw =
|
|
1634
|
+
const raw = parse3(readFileSync6(path, "utf8"));
|
|
1546
1635
|
if (!raw || !Array.isArray(raw.programs)) {
|
|
1547
1636
|
throw new Error(`invalid program directory at ${path}: missing 'programs' array`);
|
|
1548
1637
|
}
|
|
@@ -1613,23 +1702,23 @@ function isValidSolanaAddress(s) {
|
|
|
1613
1702
|
}
|
|
1614
1703
|
|
|
1615
1704
|
// src/trustGraph/programCache.ts
|
|
1616
|
-
import { readFileSync as
|
|
1617
|
-
import { join as
|
|
1705
|
+
import { readFileSync as readFileSync7, writeFileSync, mkdirSync, existsSync as existsSync5 } from "fs";
|
|
1706
|
+
import { join as join9, dirname as dirname2 } from "path";
|
|
1618
1707
|
import { homedir } from "os";
|
|
1619
1708
|
var DEFAULT_TTL_HOURS = 168;
|
|
1620
1709
|
var SCHEMA_VERSION = "1.0";
|
|
1621
1710
|
function defaultCachePath() {
|
|
1622
1711
|
const envOverride = process.env["BRAINBLAST_CACHE_PATH"];
|
|
1623
|
-
return envOverride ??
|
|
1712
|
+
return envOverride ?? join9(homedir(), ".brainblast", "program-cache.json");
|
|
1624
1713
|
}
|
|
1625
1714
|
function emptyCache() {
|
|
1626
1715
|
return { schemaVersion: SCHEMA_VERSION, entries: {} };
|
|
1627
1716
|
}
|
|
1628
1717
|
function loadProgramCache(cachePath) {
|
|
1629
1718
|
const path = cachePath ?? defaultCachePath();
|
|
1630
|
-
if (!
|
|
1719
|
+
if (!existsSync5(path)) return emptyCache();
|
|
1631
1720
|
try {
|
|
1632
|
-
const raw = JSON.parse(
|
|
1721
|
+
const raw = JSON.parse(readFileSync7(path, "utf8"));
|
|
1633
1722
|
if (raw?.schemaVersion !== SCHEMA_VERSION) {
|
|
1634
1723
|
return emptyCache();
|
|
1635
1724
|
}
|
|
@@ -1906,7 +1995,7 @@ function renderTrustGraphMd(g) {
|
|
|
1906
1995
|
}
|
|
1907
1996
|
|
|
1908
1997
|
// src/costAnalysis.ts
|
|
1909
|
-
import { Project as Project2, SyntaxKind as
|
|
1998
|
+
import { Project as Project2, SyntaxKind as SyntaxKind11 } from "ts-morph";
|
|
1910
1999
|
var LAMPORTS_PER_BYTE_YEAR = 3480;
|
|
1911
2000
|
var EXEMPTION_THRESHOLD = 2;
|
|
1912
2001
|
var OVERHEAD_BYTES = 128;
|
|
@@ -1987,11 +2076,11 @@ var KNOWN_FLOWS = [
|
|
|
1987
2076
|
}
|
|
1988
2077
|
];
|
|
1989
2078
|
var LOOP_NODE_KINDS = /* @__PURE__ */ new Set([
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2079
|
+
SyntaxKind11.ForStatement,
|
|
2080
|
+
SyntaxKind11.ForOfStatement,
|
|
2081
|
+
SyntaxKind11.ForInStatement,
|
|
2082
|
+
SyntaxKind11.WhileStatement,
|
|
2083
|
+
SyntaxKind11.DoStatement
|
|
1995
2084
|
]);
|
|
1996
2085
|
var ARRAY_METHOD_LOOPS = /* @__PURE__ */ new Set(["map", "forEach", "flatMap", "reduce", "filter"]);
|
|
1997
2086
|
function isInsideLoop(node) {
|
|
@@ -1999,12 +2088,12 @@ function isInsideLoop(node) {
|
|
|
1999
2088
|
while (cur) {
|
|
2000
2089
|
const k = cur.getKind?.();
|
|
2001
2090
|
if (k !== void 0 && LOOP_NODE_KINDS.has(k)) {
|
|
2002
|
-
return { scalable: true, note: `call is inside a ${
|
|
2091
|
+
return { scalable: true, note: `call is inside a ${SyntaxKind11[k]} \u2014 cost scales with loop iterations` };
|
|
2003
2092
|
}
|
|
2004
|
-
if (k ===
|
|
2093
|
+
if (k === SyntaxKind11.CallExpression) {
|
|
2005
2094
|
const expr = cur.getExpression?.();
|
|
2006
|
-
if (expr?.getKind?.() ===
|
|
2007
|
-
const name = expr.asKind?.(
|
|
2095
|
+
if (expr?.getKind?.() === SyntaxKind11.PropertyAccessExpression) {
|
|
2096
|
+
const name = expr.asKind?.(SyntaxKind11.PropertyAccessExpression)?.getName?.();
|
|
2008
2097
|
if (name && ARRAY_METHOD_LOOPS.has(name)) {
|
|
2009
2098
|
return { scalable: true, note: `call is inside .${name}() \u2014 cost scales with array length` };
|
|
2010
2099
|
}
|
|
@@ -2018,7 +2107,7 @@ function detectPriorityFee(targetDir) {
|
|
|
2018
2107
|
const project = new Project2({ skipAddingFilesFromTsConfig: true });
|
|
2019
2108
|
for (const file of walk(targetDir)) {
|
|
2020
2109
|
const sf = project.addSourceFileAtPath(file);
|
|
2021
|
-
const calls = sf.getDescendantsOfKind(
|
|
2110
|
+
const calls = sf.getDescendantsOfKind(SyntaxKind11.CallExpression);
|
|
2022
2111
|
for (const ce of calls) {
|
|
2023
2112
|
const expr = ce.getExpression();
|
|
2024
2113
|
const text = expr.getText();
|
|
@@ -2046,22 +2135,22 @@ function detectAccountFlows(targetDir) {
|
|
|
2046
2135
|
const importedModules = new Set(
|
|
2047
2136
|
sf.getImportDeclarations().map((d) => d.getModuleSpecifierValue())
|
|
2048
2137
|
);
|
|
2049
|
-
for (const ce of sf.getDescendantsOfKind(
|
|
2138
|
+
for (const ce of sf.getDescendantsOfKind(SyntaxKind11.CallExpression)) {
|
|
2050
2139
|
const expr = ce.getExpression();
|
|
2051
|
-
let
|
|
2052
|
-
if (expr.getKind() ===
|
|
2053
|
-
|
|
2054
|
-
} else if (expr.getKind() ===
|
|
2055
|
-
|
|
2140
|
+
let callName6 = null;
|
|
2141
|
+
if (expr.getKind() === SyntaxKind11.Identifier) {
|
|
2142
|
+
callName6 = expr.getText();
|
|
2143
|
+
} else if (expr.getKind() === SyntaxKind11.PropertyAccessExpression) {
|
|
2144
|
+
callName6 = expr.asKind(SyntaxKind11.PropertyAccessExpression).getName();
|
|
2056
2145
|
}
|
|
2057
|
-
if (!
|
|
2058
|
-
const known = callIndex.get(
|
|
2146
|
+
if (!callName6) continue;
|
|
2147
|
+
const known = callIndex.get(callName6);
|
|
2059
2148
|
if (!known) continue;
|
|
2060
2149
|
if (!importedModules.has(known.module)) continue;
|
|
2061
2150
|
const lamports = rentExemptMinimum(known.dataLen);
|
|
2062
2151
|
const { scalable, note } = isInsideLoop(ce);
|
|
2063
2152
|
flows.push({
|
|
2064
|
-
call:
|
|
2153
|
+
call: callName6,
|
|
2065
2154
|
module: known.module,
|
|
2066
2155
|
accountType: known.accountType,
|
|
2067
2156
|
file,
|
|
@@ -2206,7 +2295,7 @@ function startWatch(targetDir, opts = {}) {
|
|
|
2206
2295
|
}
|
|
2207
2296
|
|
|
2208
2297
|
// src/fixers/applyDiff.ts
|
|
2209
|
-
import { readFileSync as
|
|
2298
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync2 } from "fs";
|
|
2210
2299
|
function parseDiff(diff) {
|
|
2211
2300
|
const lines = diff.split("\n");
|
|
2212
2301
|
const fileLine = lines.find((l) => l.startsWith("+++ b"));
|
|
@@ -2223,7 +2312,7 @@ function parseDiff(diff) {
|
|
|
2223
2312
|
}
|
|
2224
2313
|
function applyDiffToFile(diff) {
|
|
2225
2314
|
const { filePath, oldStart, oldCount, newLines } = parseDiff(diff);
|
|
2226
|
-
const content =
|
|
2315
|
+
const content = readFileSync8(filePath, "utf8");
|
|
2227
2316
|
const fileLines = content.split("\n");
|
|
2228
2317
|
const removedLines = diff.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).map((l) => l.slice(1));
|
|
2229
2318
|
const actual = fileLines.slice(oldStart - 1, oldStart - 1 + oldCount);
|
|
@@ -2233,6 +2322,156 @@ function applyDiffToFile(diff) {
|
|
|
2233
2322
|
return true;
|
|
2234
2323
|
}
|
|
2235
2324
|
|
|
2325
|
+
// src/pack.ts
|
|
2326
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2327
|
+
import { join as join10 } from "path";
|
|
2328
|
+
function initPack(dir, opts) {
|
|
2329
|
+
if (existsSync6(join10(dir, PACK_MANIFEST_FILE))) {
|
|
2330
|
+
throw new Error(`${dir} already contains a ${PACK_MANIFEST_FILE}`);
|
|
2331
|
+
}
|
|
2332
|
+
const manifest = {
|
|
2333
|
+
id: opts.id,
|
|
2334
|
+
name: opts.name ?? opts.id,
|
|
2335
|
+
version: opts.version ?? "0.1.0",
|
|
2336
|
+
author: opts.author ?? "unknown",
|
|
2337
|
+
...opts.description ? { description: opts.description } : {}
|
|
2338
|
+
};
|
|
2339
|
+
mkdirSync2(dir, { recursive: true });
|
|
2340
|
+
mkdirSync2(join10(dir, "rules"), { recursive: true });
|
|
2341
|
+
mkdirSync2(join10(dir, "fixtures"), { recursive: true });
|
|
2342
|
+
const manifestYaml = [
|
|
2343
|
+
`id: ${manifest.id}`,
|
|
2344
|
+
`name: ${manifest.name}`,
|
|
2345
|
+
`version: ${manifest.version}`,
|
|
2346
|
+
`author: ${manifest.author}`,
|
|
2347
|
+
...manifest.description ? [`description: ${manifest.description}`] : [],
|
|
2348
|
+
""
|
|
2349
|
+
].join("\n");
|
|
2350
|
+
const manifestFile = join10(dir, PACK_MANIFEST_FILE);
|
|
2351
|
+
writeFileSync3(manifestFile, manifestYaml, "utf8");
|
|
2352
|
+
return manifestFile;
|
|
2353
|
+
}
|
|
2354
|
+
function validatePack(dir) {
|
|
2355
|
+
const { manifest, rules: rules2 } = loadPack(dir);
|
|
2356
|
+
const fixturesRoot = join10(dir, "fixtures");
|
|
2357
|
+
const ruleResults = rules2.map((rule) => {
|
|
2358
|
+
const ruleFixturesDir = join10(fixturesRoot, rule.id);
|
|
2359
|
+
const vulnerableDir = join10(ruleFixturesDir, "vulnerable");
|
|
2360
|
+
const fixedDir = join10(ruleFixturesDir, "fixed");
|
|
2361
|
+
if (!existsSync6(vulnerableDir) || !existsSync6(fixedDir)) {
|
|
2362
|
+
return {
|
|
2363
|
+
ruleId: rule.id,
|
|
2364
|
+
status: "missing-fixtures",
|
|
2365
|
+
detail: `no fixtures/${rule.id}/{vulnerable,fixed}/ directory \u2014 prove gate skipped`
|
|
2366
|
+
};
|
|
2367
|
+
}
|
|
2368
|
+
const redChecks = auditWithRule(vulnerableDir, rule);
|
|
2369
|
+
const redFails = redChecks.filter((c) => c.result === "fail");
|
|
2370
|
+
if (redFails.length === 0) {
|
|
2371
|
+
return {
|
|
2372
|
+
ruleId: rule.id,
|
|
2373
|
+
status: "red-failed",
|
|
2374
|
+
detail: `expected at least one FAIL against fixtures/${rule.id}/vulnerable/, got none`
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
const greenChecks = auditWithRule(fixedDir, rule);
|
|
2378
|
+
const greenFails = greenChecks.filter((c) => c.result === "fail");
|
|
2379
|
+
if (greenFails.length > 0) {
|
|
2380
|
+
return {
|
|
2381
|
+
ruleId: rule.id,
|
|
2382
|
+
status: "green-failed",
|
|
2383
|
+
detail: `expected no FAIL against fixtures/${rule.id}/fixed/, got ${greenFails.length}`
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
return { ruleId: rule.id, status: "ok", detail: "RED -> GREEN proven" };
|
|
2387
|
+
});
|
|
2388
|
+
const ok = ruleResults.every((r) => r.status === "ok" || r.status === "missing-fixtures");
|
|
2389
|
+
return { manifest, rules: rules2, ruleResults, ok };
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// src/telemetry.ts
|
|
2393
|
+
import { createHash, randomUUID } from "crypto";
|
|
2394
|
+
import { appendFileSync, existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "fs";
|
|
2395
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2396
|
+
import { homedir as homedir2 } from "os";
|
|
2397
|
+
import { dirname as dirname3, join as join11, resolve } from "path";
|
|
2398
|
+
function sha256Hex(s) {
|
|
2399
|
+
return createHash("sha256").update(s).digest("hex");
|
|
2400
|
+
}
|
|
2401
|
+
function isTelemetryEnabled(targetDir) {
|
|
2402
|
+
const env = process.env.BRAINBLAST_TELEMETRY;
|
|
2403
|
+
if (env === "1" || env === "true") return true;
|
|
2404
|
+
if (env === "0" || env === "false") return false;
|
|
2405
|
+
const configPath = join11(targetDir, ".agent-research", "config.json");
|
|
2406
|
+
if (!existsSync7(configPath)) return false;
|
|
2407
|
+
try {
|
|
2408
|
+
const cfg = JSON.parse(readFileSync9(configPath, "utf8"));
|
|
2409
|
+
return cfg?.telemetry === true;
|
|
2410
|
+
} catch {
|
|
2411
|
+
return false;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
function getUserHash() {
|
|
2415
|
+
const idPath = join11(homedir2(), ".brainblast", "telemetry-id");
|
|
2416
|
+
let id;
|
|
2417
|
+
if (existsSync7(idPath)) {
|
|
2418
|
+
id = readFileSync9(idPath, "utf8").trim();
|
|
2419
|
+
} else {
|
|
2420
|
+
id = randomUUID();
|
|
2421
|
+
mkdirSync3(dirname3(idPath), { recursive: true });
|
|
2422
|
+
writeFileSync4(idPath, id, "utf8");
|
|
2423
|
+
}
|
|
2424
|
+
return sha256Hex(id).slice(0, 16);
|
|
2425
|
+
}
|
|
2426
|
+
function getRepoHash(targetDir) {
|
|
2427
|
+
let key = "";
|
|
2428
|
+
try {
|
|
2429
|
+
key = execFileSync3("git", ["config", "--get", "remote.origin.url"], {
|
|
2430
|
+
cwd: targetDir,
|
|
2431
|
+
encoding: "utf8",
|
|
2432
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2433
|
+
}).trim();
|
|
2434
|
+
} catch {
|
|
2435
|
+
}
|
|
2436
|
+
if (!key) key = resolve(targetDir);
|
|
2437
|
+
return sha256Hex(key).slice(0, 16);
|
|
2438
|
+
}
|
|
2439
|
+
function telemetryFilePath(targetDir) {
|
|
2440
|
+
return join11(targetDir, ".agent-research", "telemetry.ndjson");
|
|
2441
|
+
}
|
|
2442
|
+
var DEFAULT_REGISTRY_URL = "https://registry.brainblast.tech";
|
|
2443
|
+
async function submitTelemetry(targetDir, registryUrl = process.env.BRAINBLAST_REGISTRY_URL || DEFAULT_REGISTRY_URL) {
|
|
2444
|
+
const file = telemetryFilePath(targetDir);
|
|
2445
|
+
if (!existsSync7(file)) {
|
|
2446
|
+
return { submitted: 0, accepted: 0, rejected: 0, graduations: [] };
|
|
2447
|
+
}
|
|
2448
|
+
const events = readFileSync9(file, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
2449
|
+
if (events.length === 0) {
|
|
2450
|
+
return { submitted: 0, accepted: 0, rejected: 0, graduations: [] };
|
|
2451
|
+
}
|
|
2452
|
+
const res = await fetch(`${registryUrl.replace(/\/$/, "")}/api/telemetry`, {
|
|
2453
|
+
method: "POST",
|
|
2454
|
+
headers: { "content-type": "application/json" },
|
|
2455
|
+
body: JSON.stringify({ events })
|
|
2456
|
+
});
|
|
2457
|
+
if (!res.ok) {
|
|
2458
|
+
const body = await res.text().catch(() => "");
|
|
2459
|
+
throw new Error(`telemetry submit failed: ${res.status} ${res.statusText} ${body}`.trim());
|
|
2460
|
+
}
|
|
2461
|
+
const json = await res.json();
|
|
2462
|
+
return { submitted: events.length, ...json };
|
|
2463
|
+
}
|
|
2464
|
+
function recordGraduationEvents(targetDir, events) {
|
|
2465
|
+
if (events.length === 0) return;
|
|
2466
|
+
const file = telemetryFilePath(targetDir);
|
|
2467
|
+
mkdirSync3(dirname3(file), { recursive: true });
|
|
2468
|
+
const repo_hash = getRepoHash(targetDir);
|
|
2469
|
+
const user_hash = getUserHash();
|
|
2470
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
2471
|
+
const lines = events.map((e) => JSON.stringify({ ...e, repo_hash, user_hash, timestamp })).join("\n");
|
|
2472
|
+
appendFileSync(file, lines + "\n", "utf8");
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2236
2475
|
export {
|
|
2237
2476
|
findCandidates,
|
|
2238
2477
|
findConfigCandidates,
|
|
@@ -2247,6 +2486,10 @@ export {
|
|
|
2247
2486
|
renderTest,
|
|
2248
2487
|
testKinds,
|
|
2249
2488
|
loadRules,
|
|
2489
|
+
PACK_MANIFEST_FILE,
|
|
2490
|
+
validatePackManifest,
|
|
2491
|
+
loadPack,
|
|
2492
|
+
loadPacksFromDir,
|
|
2250
2493
|
rules,
|
|
2251
2494
|
resolveRules,
|
|
2252
2495
|
loadDirectory,
|
|
@@ -2271,5 +2514,14 @@ export {
|
|
|
2271
2514
|
runIncrementalScan,
|
|
2272
2515
|
startWatch,
|
|
2273
2516
|
parseDiff,
|
|
2274
|
-
applyDiffToFile
|
|
2517
|
+
applyDiffToFile,
|
|
2518
|
+
initPack,
|
|
2519
|
+
validatePack,
|
|
2520
|
+
isTelemetryEnabled,
|
|
2521
|
+
getUserHash,
|
|
2522
|
+
getRepoHash,
|
|
2523
|
+
telemetryFilePath,
|
|
2524
|
+
DEFAULT_REGISTRY_URL,
|
|
2525
|
+
submitTelemetry,
|
|
2526
|
+
recordGraduationEvents
|
|
2275
2527
|
};
|
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-LOPYKIXE.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-LOPYKIXE.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.1",
|
|
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": [
|