brainblast 0.4.1 → 0.4.2

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 CHANGED
@@ -12,6 +12,8 @@ npx brainblast . # scan the repo, write .agent-research/report.json
12
12
  npx brainblast . --ci # exit 1 if a confirmed FAIL remains
13
13
  npx brainblast . --ci --strict # also fail on CANT_TELL (can't statically prove)
14
14
  npx brainblast . --since origin/main # diff-aware: only audit what changed
15
+ npx brainblast fix . # dry run: list mechanical fixes
16
+ npx brainblast fix . --apply # write fixes, re-audit RED -> GREEN
15
17
  ```
16
18
 
17
19
  Exit codes: **0** clean · **1** a confirmed FAIL · CANT_TELL is a warning by
@@ -58,6 +60,22 @@ git work tree). This is the integration point for an agent daemon — tail
58
60
  stdout for structured findings instead of polling `.agent-research/report.json`.
59
61
  Exit with Ctrl-C / SIGTERM.
60
62
 
63
+ ### Auto-fix (`brainblast fix`)
64
+
65
+ ```sh
66
+ npx brainblast fix . # dry run: list available mechanical fixes
67
+ npx brainblast fix . --apply # write each fix.diff to disk, then re-audit
68
+ npx brainblast fix . --apply --branch # also commit to brainblast/auto-fix-<ts>
69
+ ```
70
+
71
+ Every confirmed FAIL that ships a mechanical `fix.diff` (e.g. Stripe raw-body,
72
+ Privy `audience`/`issuer`) can be applied directly. `--apply` writes each diff,
73
+ then re-runs the audit to confirm the finding now passes (RED -> GREEN) — any
74
+ fix that doesn't take is reported, not silently dropped. Findings with only a
75
+ `suggestion` (structural fixes brainblast won't auto-synthesize) are listed as
76
+ guidance, not applied. `--branch` additionally creates a new branch and commits
77
+ the applied changes.
78
+
61
79
  ## What it catches
62
80
 
63
81
  ### Web2 / Node.js
@@ -81,6 +99,7 @@ Exit with Ctrl-C / SIGTERM.
81
99
  | Rule | What's wrong | Consequence |
82
100
  |------|--------------|-------------|
83
101
  | `env-secrets-committed` | A `.env*` file (not `.env.example`/`.sample`/`.template`) is tracked by git and contains a secret-shaped key (`SECRET`, `*_PRIVATE_KEY`, `*_API_KEY`, `*_TOKEN`, `*_PASSWORD`, etc.) with a real-looking (non-placeholder) value | Anyone with read access to the repo — including forks of a public repo — can read the live credential |
102
+ | `env-secret-leaked-to-sink` | A secret-shaped `process.env.X` value (directly, via a local variable, or one hop through a same-file helper) is passed to `console.log`/`res.json`/`res.send`/etc. | Credentials end up in logs, error trackers, or API responses — readable by anyone with log/response access |
84
103
 
85
104
  Each finding lands in `.agent-research/report.json` (stable `schemaVersion: "1.0"`)
86
105
  with a `checks[]` array a CI gate can read. Each confirmed FAIL ships a
@@ -164,7 +183,7 @@ All types are exported: `Rule`, `CheckResult`, `CostReport`, `AccountFlow`,
164
183
 
165
184
  ```sh
166
185
  npm install
167
- npm test # unit suite (164 tests)
186
+ npm test # unit suite (173 tests)
168
187
  npm run prove # end-to-end: generated tests RED on vulnerable, GREEN on fixed
169
188
  npm run build # produce dist/ (the published artifact)
170
189
  ```
@@ -487,6 +487,96 @@ var envSecretsCommitted = (c, p) => {
487
487
  return { result: "pass", detail: p.passDetail ?? "No committed secret-looking values found." };
488
488
  };
489
489
 
490
+ // src/checkers/envTaintToSink.ts
491
+ import { SyntaxKind as SyntaxKind7 } from "ts-morph";
492
+ function calleeName(call) {
493
+ const exp = call.getExpression();
494
+ if (exp.getKind() === SyntaxKind7.Identifier) return exp.getText();
495
+ if (exp.getKind() === SyntaxKind7.PropertyAccessExpression) {
496
+ return exp.asKind(SyntaxKind7.PropertyAccessExpression).getName();
497
+ }
498
+ return "";
499
+ }
500
+ function envVarIn(text) {
501
+ const m = text.match(/process\.env\.([A-Za-z0-9_]+)/);
502
+ return m?.[1];
503
+ }
504
+ function wordIn(text, name) {
505
+ return new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(text);
506
+ }
507
+ function findDirectLeak(fn, sinkCalls, secretKeyRe, taintedNames) {
508
+ for (const call of fn.getDescendantsOfKind(SyntaxKind7.CallExpression)) {
509
+ const name = calleeName(call);
510
+ if (!sinkCalls.has(name)) continue;
511
+ for (const arg of call.getArguments()) {
512
+ const text = arg.getText();
513
+ const envVar = envVarIn(text);
514
+ if (envVar && secretKeyRe.test(envVar)) {
515
+ return `process.env.${envVar} is passed directly to ${name}(...) \u2014 secret values must not be logged or returned to clients.`;
516
+ }
517
+ for (const tv of taintedNames) {
518
+ if (wordIn(text, tv)) {
519
+ return `'${tv}' (holding a secret-shaped process.env value) is passed to ${name}(...) \u2014 secret values must not be logged or returned to clients.`;
520
+ }
521
+ }
522
+ }
523
+ }
524
+ return void 0;
525
+ }
526
+ var envTaintToSink = (c, p) => {
527
+ const sinkCalls = new Set(p.sinkCalls ?? []);
528
+ const secretKeyRe = new RegExp(p.secretKeyPattern, "i");
529
+ const fn = c.fn;
530
+ const taintedNames = /* @__PURE__ */ new Set();
531
+ for (const decl of fn.getDescendantsOfKind(SyntaxKind7.VariableDeclaration)) {
532
+ const init = decl.getInitializer();
533
+ if (!init) continue;
534
+ const envVar = envVarIn(init.getText());
535
+ if (envVar && secretKeyRe.test(envVar)) {
536
+ taintedNames.add(decl.getName());
537
+ }
538
+ }
539
+ const direct = findDirectLeak(fn, sinkCalls, secretKeyRe, taintedNames);
540
+ if (direct) return { result: "fail", detail: direct };
541
+ const sourceFile = fn.getSourceFile();
542
+ for (const call of fn.getDescendantsOfKind(SyntaxKind7.CallExpression)) {
543
+ const calleeExp = call.getExpression();
544
+ if (calleeExp.getKind() !== SyntaxKind7.Identifier) continue;
545
+ const calleeFnName = calleeExp.getText();
546
+ if (calleeFnName === (c.fnName ?? "")) continue;
547
+ const args = call.getArguments();
548
+ const taintedArgIndices = [];
549
+ args.forEach((arg, i) => {
550
+ const text = arg.getText();
551
+ const envVar = envVarIn(text);
552
+ if (envVar && secretKeyRe.test(envVar)) {
553
+ taintedArgIndices.push(i);
554
+ return;
555
+ }
556
+ for (const tv of taintedNames) {
557
+ if (wordIn(text, tv)) {
558
+ taintedArgIndices.push(i);
559
+ return;
560
+ }
561
+ }
562
+ });
563
+ if (taintedArgIndices.length === 0) continue;
564
+ const calleeFn = sourceFile.getFunction(calleeFnName);
565
+ if (!calleeFn) continue;
566
+ const params = calleeFn.getParameters().map((pr) => pr.getName());
567
+ const calleeTainted = new Set(taintedArgIndices.map((i) => params[i]).filter((x) => !!x));
568
+ if (calleeTainted.size === 0) continue;
569
+ const hop = findDirectLeak(calleeFn, sinkCalls, secretKeyRe, calleeTainted);
570
+ if (hop) {
571
+ return {
572
+ result: "fail",
573
+ detail: `A secret-shaped process.env value flows into '${calleeFnName}(...)' (called from '${c.fnName}'), where ${hop}`
574
+ };
575
+ }
576
+ }
577
+ return { result: "pass", detail: "No secret-shaped process.env value flows to a logging/response sink." };
578
+ };
579
+
490
580
  // src/checkers/index.ts
491
581
  var registry = {
492
582
  "positional-arg-identity": positionalArgIdentity,
@@ -495,7 +585,8 @@ var registry = {
495
585
  "arg-equals-constant-identifier": argEqualsConstantIdentifier,
496
586
  "object-arg-property-literal-equals": objectArgPropertyLiteralEquals,
497
587
  "anchor-init-if-needed-guarded": anchorInitIfNeededGuarded,
498
- "env-secrets-committed": envSecretsCommitted
588
+ "env-secrets-committed": envSecretsCommitted,
589
+ "env-taint-to-sink": envTaintToSink
499
590
  };
500
591
  function runChecker(kind, c, params) {
501
592
  const fn = registry[kind];
@@ -711,7 +802,7 @@ function findRustCandidates(targetDir, rule) {
711
802
  }
712
803
 
713
804
  // src/fixers/positionalArgIdentity.ts
714
- import { SyntaxKind as SyntaxKind7 } from "ts-morph";
805
+ import { SyntaxKind as SyntaxKind8 } from "ts-morph";
715
806
 
716
807
  // src/fixers/diffUtil.ts
717
808
  function buildDiff(node, replacement) {
@@ -736,9 +827,9 @@ function buildDiff(node, replacement) {
736
827
  // src/fixers/positionalArgIdentity.ts
737
828
  var fixPositionalArgIdentity = (c, p, outcome) => {
738
829
  if (outcome.result !== "fail") return void 0;
739
- const calls = c.fn.getDescendantsOfKind(SyntaxKind7.CallExpression).filter((call) => {
830
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind8.CallExpression).filter((call) => {
740
831
  const exp = call.getExpression();
741
- return exp.getKind() === SyntaxKind7.PropertyAccessExpression && exp.asKind(SyntaxKind7.PropertyAccessExpression).getName() === p.call;
832
+ return exp.getKind() === SyntaxKind8.PropertyAccessExpression && exp.asKind(SyntaxKind8.PropertyAccessExpression).getName() === p.call;
742
833
  });
743
834
  if (calls.length === 0) {
744
835
  const wantParam2 = c.params[p.paramIndex] ?? "<rawBodyParam>";
@@ -753,7 +844,7 @@ Do not call JSON.parse() on the body before this verification step.`
753
844
  }
754
845
  const arg = calls[0].getArguments()[p.argIndex];
755
846
  const wantParam = c.params[p.paramIndex];
756
- if (arg && wantParam && arg.getKind() === SyntaxKind7.CallExpression) {
847
+ if (arg && wantParam && arg.getKind() === SyntaxKind8.CallExpression) {
757
848
  return {
758
849
  summary: `Pass the raw body parameter '${wantParam}' to ${p.call} instead of a parsed value`,
759
850
  diff: buildDiff(arg, wantParam)
@@ -763,12 +854,12 @@ Do not call JSON.parse() on the body before this verification step.`
763
854
  };
764
855
 
765
856
  // src/fixers/requiredCallWithOptions.ts
766
- import { SyntaxKind as SyntaxKind8 } from "ts-morph";
857
+ import { SyntaxKind as SyntaxKind9 } from "ts-morph";
767
858
  function callName4(call) {
768
859
  const exp = call.getExpression();
769
- if (exp.getKind() === SyntaxKind8.Identifier) return exp.getText();
770
- if (exp.getKind() === SyntaxKind8.PropertyAccessExpression) {
771
- return exp.asKind(SyntaxKind8.PropertyAccessExpression).getName();
860
+ if (exp.getKind() === SyntaxKind9.Identifier) return exp.getText();
861
+ if (exp.getKind() === SyntaxKind9.PropertyAccessExpression) {
862
+ return exp.asKind(SyntaxKind9.PropertyAccessExpression).getName();
772
863
  }
773
864
  return "";
774
865
  }
@@ -786,15 +877,15 @@ function placeholderFor(propName) {
786
877
  }
787
878
  var fixRequiredCallWithOptions = (c, p, outcome) => {
788
879
  if (outcome.result !== "fail") return void 0;
789
- const calls = c.fn.getDescendantsOfKind(SyntaxKind8.CallExpression);
880
+ const calls = c.fn.getDescendantsOfKind(SyntaxKind9.CallExpression);
790
881
  const verify = calls.filter((x) => p.verifyCalls.includes(callName4(x)));
791
882
  if (verify.length > 0) {
792
883
  const call = verify[0];
793
884
  const args = call.getArguments();
794
885
  const lastArg = args[args.length - 1];
795
- const obj = lastArg?.asKind(SyntaxKind8.ObjectLiteralExpression);
886
+ const obj = lastArg?.asKind(SyntaxKind9.ObjectLiteralExpression);
796
887
  const presentNames = obj ? obj.getProperties().map((pr) => {
797
- const pa = pr.asKind(SyntaxKind8.PropertyAssignment) ?? pr.asKind(SyntaxKind8.ShorthandPropertyAssignment);
888
+ const pa = pr.asKind(SyntaxKind9.PropertyAssignment) ?? pr.asKind(SyntaxKind9.ShorthandPropertyAssignment);
798
889
  return pa?.getName() ?? "";
799
890
  }) : [];
800
891
  const missingGroups = p.requiredProps.filter(
@@ -1730,7 +1821,7 @@ function renderTrustGraphMd(g) {
1730
1821
  }
1731
1822
 
1732
1823
  // src/costAnalysis.ts
1733
- import { Project as Project2, SyntaxKind as SyntaxKind9 } from "ts-morph";
1824
+ import { Project as Project2, SyntaxKind as SyntaxKind10 } from "ts-morph";
1734
1825
  var LAMPORTS_PER_BYTE_YEAR = 3480;
1735
1826
  var EXEMPTION_THRESHOLD = 2;
1736
1827
  var OVERHEAD_BYTES = 128;
@@ -1811,11 +1902,11 @@ var KNOWN_FLOWS = [
1811
1902
  }
1812
1903
  ];
1813
1904
  var LOOP_NODE_KINDS = /* @__PURE__ */ new Set([
1814
- SyntaxKind9.ForStatement,
1815
- SyntaxKind9.ForOfStatement,
1816
- SyntaxKind9.ForInStatement,
1817
- SyntaxKind9.WhileStatement,
1818
- SyntaxKind9.DoStatement
1905
+ SyntaxKind10.ForStatement,
1906
+ SyntaxKind10.ForOfStatement,
1907
+ SyntaxKind10.ForInStatement,
1908
+ SyntaxKind10.WhileStatement,
1909
+ SyntaxKind10.DoStatement
1819
1910
  ]);
1820
1911
  var ARRAY_METHOD_LOOPS = /* @__PURE__ */ new Set(["map", "forEach", "flatMap", "reduce", "filter"]);
1821
1912
  function isInsideLoop(node) {
@@ -1823,12 +1914,12 @@ function isInsideLoop(node) {
1823
1914
  while (cur) {
1824
1915
  const k = cur.getKind?.();
1825
1916
  if (k !== void 0 && LOOP_NODE_KINDS.has(k)) {
1826
- return { scalable: true, note: `call is inside a ${SyntaxKind9[k]} \u2014 cost scales with loop iterations` };
1917
+ return { scalable: true, note: `call is inside a ${SyntaxKind10[k]} \u2014 cost scales with loop iterations` };
1827
1918
  }
1828
- if (k === SyntaxKind9.CallExpression) {
1919
+ if (k === SyntaxKind10.CallExpression) {
1829
1920
  const expr = cur.getExpression?.();
1830
- if (expr?.getKind?.() === SyntaxKind9.PropertyAccessExpression) {
1831
- const name = expr.asKind?.(SyntaxKind9.PropertyAccessExpression)?.getName?.();
1921
+ if (expr?.getKind?.() === SyntaxKind10.PropertyAccessExpression) {
1922
+ const name = expr.asKind?.(SyntaxKind10.PropertyAccessExpression)?.getName?.();
1832
1923
  if (name && ARRAY_METHOD_LOOPS.has(name)) {
1833
1924
  return { scalable: true, note: `call is inside .${name}() \u2014 cost scales with array length` };
1834
1925
  }
@@ -1842,7 +1933,7 @@ function detectPriorityFee(targetDir) {
1842
1933
  const project = new Project2({ skipAddingFilesFromTsConfig: true });
1843
1934
  for (const file of walk(targetDir)) {
1844
1935
  const sf = project.addSourceFileAtPath(file);
1845
- const calls = sf.getDescendantsOfKind(SyntaxKind9.CallExpression);
1936
+ const calls = sf.getDescendantsOfKind(SyntaxKind10.CallExpression);
1846
1937
  for (const ce of calls) {
1847
1938
  const expr = ce.getExpression();
1848
1939
  const text = expr.getText();
@@ -1870,13 +1961,13 @@ function detectAccountFlows(targetDir) {
1870
1961
  const importedModules = new Set(
1871
1962
  sf.getImportDeclarations().map((d) => d.getModuleSpecifierValue())
1872
1963
  );
1873
- for (const ce of sf.getDescendantsOfKind(SyntaxKind9.CallExpression)) {
1964
+ for (const ce of sf.getDescendantsOfKind(SyntaxKind10.CallExpression)) {
1874
1965
  const expr = ce.getExpression();
1875
1966
  let callName5 = null;
1876
- if (expr.getKind() === SyntaxKind9.Identifier) {
1967
+ if (expr.getKind() === SyntaxKind10.Identifier) {
1877
1968
  callName5 = expr.getText();
1878
- } else if (expr.getKind() === SyntaxKind9.PropertyAccessExpression) {
1879
- callName5 = expr.asKind(SyntaxKind9.PropertyAccessExpression).getName();
1969
+ } else if (expr.getKind() === SyntaxKind10.PropertyAccessExpression) {
1970
+ callName5 = expr.asKind(SyntaxKind10.PropertyAccessExpression).getName();
1880
1971
  }
1881
1972
  if (!callName5) continue;
1882
1973
  const known = callIndex.get(callName5);
@@ -2029,6 +2120,34 @@ function startWatch(targetDir, opts = {}) {
2029
2120
  };
2030
2121
  }
2031
2122
 
2123
+ // src/fixers/applyDiff.ts
2124
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
2125
+ function parseDiff(diff) {
2126
+ const lines = diff.split("\n");
2127
+ const fileLine = lines.find((l) => l.startsWith("+++ b"));
2128
+ if (!fileLine) throw new Error("parseDiff: no '+++ b<path>' line found");
2129
+ const filePath = fileLine.slice("+++ b".length);
2130
+ const hunkLine = lines.find((l) => l.startsWith("@@"));
2131
+ if (!hunkLine) throw new Error("parseDiff: no hunk header found");
2132
+ const m = hunkLine.match(/^@@ -(\d+),(\d+) \+\d+,\d+ @@/);
2133
+ if (!m) throw new Error(`parseDiff: unrecognized hunk header '${hunkLine}'`);
2134
+ const oldStart = Number(m[1]);
2135
+ const oldCount = Number(m[2]);
2136
+ const newLines = lines.filter((l) => l.startsWith("+") && !l.startsWith("+++")).map((l) => l.slice(1));
2137
+ return { filePath, oldStart, oldCount, newLines };
2138
+ }
2139
+ function applyDiffToFile(diff) {
2140
+ const { filePath, oldStart, oldCount, newLines } = parseDiff(diff);
2141
+ const content = readFileSync7(filePath, "utf8");
2142
+ const fileLines = content.split("\n");
2143
+ const removedLines = diff.split("\n").filter((l) => l.startsWith("-") && !l.startsWith("---")).map((l) => l.slice(1));
2144
+ const actual = fileLines.slice(oldStart - 1, oldStart - 1 + oldCount);
2145
+ if (JSON.stringify(actual) !== JSON.stringify(removedLines)) return false;
2146
+ fileLines.splice(oldStart - 1, oldCount, ...newLines);
2147
+ writeFileSync2(filePath, fileLines.join("\n"));
2148
+ return true;
2149
+ }
2150
+
2032
2151
  export {
2033
2152
  findCandidates,
2034
2153
  findConfigCandidates,
@@ -2065,5 +2184,7 @@ export {
2065
2184
  analyzeCosts,
2066
2185
  renderCostReportMd,
2067
2186
  runIncrementalScan,
2068
- startWatch
2187
+ startWatch,
2188
+ parseDiff,
2189
+ applyDiffToFile
2069
2190
  };
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  analyzeCosts,
4
+ applyDiffToFile,
4
5
  audit,
5
6
  buildTrustGraph,
6
7
  cacheSize,
@@ -8,11 +9,12 @@ import {
8
9
  getChangedRanges,
9
10
  isValidSolanaAddress,
10
11
  loadProgramCache,
12
+ parseDiff,
11
13
  renderCostReportMd,
12
14
  renderTrustGraphMd,
13
15
  resolveRules,
14
16
  startWatch
15
- } from "./chunk-P7K7NRVN.js";
17
+ } from "./chunk-Q72MTJXQ.js";
16
18
 
17
19
  // src/cli.ts
18
20
  import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
@@ -90,6 +92,7 @@ function updateMemory(memory, checks2, now = /* @__PURE__ */ new Date()) {
90
92
  }
91
93
 
92
94
  // src/cli.ts
95
+ import { execFileSync } from "child_process";
93
96
  var args = process.argv.slice(2);
94
97
  if (args[0] === "trust-graph") {
95
98
  await runTrustGraph(args.slice(1));
@@ -103,6 +106,10 @@ if (args[0] === "watch") {
103
106
  await new Promise(() => {
104
107
  });
105
108
  }
109
+ if (args[0] === "fix") {
110
+ await runFix(args.slice(1));
111
+ process.exit(0);
112
+ }
106
113
  var ci = args.includes("--ci");
107
114
  var strict = args.includes("--strict");
108
115
  var sinceIdx = args.indexOf("--since");
@@ -237,3 +244,78 @@ async function runTrustGraph(argv) {
237
244
  console.error(` program-cache: ${count} entries (${cp})`);
238
245
  }
239
246
  }
247
+ async function runFix(argv) {
248
+ const apply = argv.includes("--apply");
249
+ const branch = argv.includes("--branch");
250
+ const targetDir2 = argv.find((a) => !a.startsWith("--")) ?? process.cwd();
251
+ const rules2 = resolveRules(targetDir2);
252
+ const { checks: before } = audit(targetDir2, rules2);
253
+ const fixable = before.filter((c) => c.result === "fail" && c.fix?.diff);
254
+ if (fixable.length === 0) {
255
+ console.log("brainblast fix: no mechanical fixes available (no FAIL ships a fix.diff).");
256
+ const others = before.filter((c) => c.result === "fail" && c.fix?.suggestion);
257
+ for (const c of others) {
258
+ console.log(` [GUIDANCE] ${c.ruleId} ${c.file}:${c.line}`);
259
+ console.log(` ${c.fix.summary}`);
260
+ }
261
+ return;
262
+ }
263
+ console.log(`brainblast fix: ${fixable.length} mechanical fix(es) found.`);
264
+ for (const c of fixable) {
265
+ console.log(` [${apply ? "APPLY" : "DRY-RUN"}] ${c.ruleId} ${c.file}:${c.line} \u2014 ${c.fix.summary}`);
266
+ }
267
+ if (!apply) {
268
+ console.log("\nRe-run with --apply to write these changes to disk.");
269
+ return;
270
+ }
271
+ const byFile = /* @__PURE__ */ new Map();
272
+ for (const c of fixable) {
273
+ const file = parseDiff(c.fix.diff).filePath;
274
+ byFile.set(file, [...byFile.get(file) ?? [], c]);
275
+ }
276
+ let applied = 0;
277
+ let skipped = 0;
278
+ for (const [, group] of byFile) {
279
+ const sorted = [...group].sort((a, b) => parseDiff(b.fix.diff).oldStart - parseDiff(a.fix.diff).oldStart);
280
+ for (const c of sorted) {
281
+ const ok = applyDiffToFile(c.fix.diff);
282
+ if (ok) applied++;
283
+ else {
284
+ skipped++;
285
+ console.log(` [SKIP] ${c.ruleId} ${c.file}:${c.line} \u2014 file no longer matches the fix's expected range`);
286
+ }
287
+ }
288
+ }
289
+ console.log(`
290
+ Applied ${applied} fix(es)${skipped ? `, skipped ${skipped}` : ""}.`);
291
+ const { checks: after } = audit(targetDir2, rules2);
292
+ const stillFailing = fixable.filter((c) => {
293
+ const a = after.find((x) => x.ruleId === c.ruleId && x.file === c.file && x.exportName === c.exportName);
294
+ return a?.result === "fail";
295
+ });
296
+ if (stillFailing.length > 0) {
297
+ console.log(`
298
+ Warning: ${stillFailing.length} fix(es) applied but the rule still fails:`);
299
+ for (const c of stillFailing) console.log(` ${c.ruleId} ${c.file}:${c.line}`);
300
+ } else if (applied > 0) {
301
+ console.log("All applied fixes now pass (or cant_tell) on re-audit. \u2713");
302
+ }
303
+ if (branch && applied > 0) {
304
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
305
+ const branchName = `brainblast/auto-fix-${ts}`;
306
+ try {
307
+ execFileSync("git", ["checkout", "-b", branchName], { cwd: targetDir2, stdio: "ignore" });
308
+ execFileSync("git", ["add", "-A"], { cwd: targetDir2, stdio: "ignore" });
309
+ execFileSync(
310
+ "git",
311
+ ["commit", "-q", "-m", `brainblast fix: apply ${applied} mechanical fix(es)`],
312
+ { cwd: targetDir2, stdio: "ignore" }
313
+ );
314
+ console.log(`
315
+ Committed to new branch '${branchName}'.`);
316
+ } catch (e) {
317
+ console.error(`
318
+ Warning: could not create branch/commit: ${e.message ?? e}`);
319
+ }
320
+ }
321
+ }
package/dist/index.d.ts CHANGED
@@ -276,6 +276,15 @@ declare function startWatch(targetDir: string, opts?: WatchOptions): {
276
276
  close: () => void;
277
277
  };
278
278
 
279
+ interface ParsedDiff {
280
+ filePath: string;
281
+ oldStart: number;
282
+ oldCount: number;
283
+ newLines: string[];
284
+ }
285
+ declare function parseDiff(diff: string): ParsedDiff;
286
+ declare function applyDiffToFile(diff: string): boolean;
287
+
279
288
  type UpgradeAuthorityKind = "renounced" | "single-key" | "multisig" | "dao" | "unknown";
280
289
  type UpgradeAuthoritySource = "directory" | "rpc" | "research";
281
290
  interface UpgradeAuthority {
@@ -405,4 +414,4 @@ declare function isEntryExpired(entry: ProgramCacheEntry, ttlHoursOverride?: num
405
414
  */
406
415
  declare function cacheSize(cache: ProgramCache, ttlHoursOverride?: number): number;
407
416
 
408
- 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 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, 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, putCacheEntry, rangeChanged, renderCostReportMd, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, runChecker, runIncrementalScan, saveProgramCache, startWatch, testKinds };
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 };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  DEFAULT_TTL_HOURS,
3
3
  analyzeCosts,
4
+ applyDiffToFile,
4
5
  audit,
5
6
  auditWithRule,
6
7
  base58Decode,
@@ -22,6 +23,7 @@ import {
22
23
  loadDirectory,
23
24
  loadProgramCache,
24
25
  loadRules,
26
+ parseDiff,
25
27
  putCacheEntry,
26
28
  rangeChanged,
27
29
  renderCostReportMd,
@@ -35,7 +37,7 @@ import {
35
37
  saveProgramCache,
36
38
  startWatch,
37
39
  testKinds
38
- } from "./chunk-P7K7NRVN.js";
40
+ } from "./chunk-Q72MTJXQ.js";
39
41
 
40
42
  // src/generate.ts
41
43
  import { writeFileSync, mkdirSync } from "fs";
@@ -53,6 +55,7 @@ function generateTestForResult(result, rule, outPath) {
53
55
  export {
54
56
  DEFAULT_TTL_HOURS,
55
57
  analyzeCosts,
58
+ applyDiffToFile,
56
59
  audit,
57
60
  auditWithRule,
58
61
  base58Decode,
@@ -76,6 +79,7 @@ export {
76
79
  loadDirectory,
77
80
  loadProgramCache,
78
81
  loadRules,
82
+ parseDiff,
79
83
  putCacheEntry,
80
84
  rangeChanged,
81
85
  renderCostReportMd,
@@ -0,0 +1,43 @@
1
+ # Pure-data rule (facts). Binds to vetted templates by `check.kind`/`test.kind`.
2
+ id: env-secret-leaked-to-sink
3
+ severity: high
4
+ title: Secret-shaped environment value flows to a logging/response sink
5
+ component:
6
+ name: Environment configuration
7
+ type: Config
8
+ version: unversioned
9
+ sourceUrl: https://12factor.net/config
10
+ detect:
11
+ lang: typescript
12
+ modules: []
13
+ # Never matches by name alone — only the triggerCalls (sink calls) below
14
+ # select candidates. Keeps this from firing on every function in a repo.
15
+ nameRegex: "(?!)"
16
+ triggerCalls:
17
+ - log
18
+ - error
19
+ - warn
20
+ - info
21
+ - debug
22
+ - json
23
+ - send
24
+ - write
25
+ - end
26
+ requiresImport: false
27
+ check:
28
+ kind: env-taint-to-sink
29
+ params:
30
+ sinkCalls:
31
+ - log
32
+ - error
33
+ - warn
34
+ - info
35
+ - debug
36
+ - json
37
+ - send
38
+ - write
39
+ - end
40
+ # Key names that typically hold credentials/secrets.
41
+ secretKeyPattern: "(SECRET|PRIVATE_KEY|API_KEY|ACCESS_KEY|TOKEN|PASSWORD|CREDENTIAL)"
42
+ test:
43
+ kind: none
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainblast",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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": [