brainblast 0.4.0 → 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 +69 -2
- package/dist/{chunk-WVHGN2HR.js → chunk-Q72MTJXQ.js} +437 -59
- package/dist/cli.js +142 -22
- package/dist/index.d.ts +73 -6
- package/dist/index.js +19 -1
- package/dist/rules/env-secret-leaked-to-sink.yaml +43 -0
- package/dist/rules/env-secrets-committed.yaml +32 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
analyzeCosts,
|
|
4
|
+
applyDiffToFile,
|
|
4
5
|
audit,
|
|
5
6
|
buildTrustGraph,
|
|
6
7
|
cacheSize,
|
|
7
8
|
defaultCachePath,
|
|
9
|
+
getChangedRanges,
|
|
8
10
|
isValidSolanaAddress,
|
|
9
11
|
loadProgramCache,
|
|
12
|
+
parseDiff,
|
|
10
13
|
renderCostReportMd,
|
|
11
14
|
renderTrustGraphMd,
|
|
12
|
-
resolveRules
|
|
13
|
-
|
|
15
|
+
resolveRules,
|
|
16
|
+
startWatch
|
|
17
|
+
} from "./chunk-Q72MTJXQ.js";
|
|
14
18
|
|
|
15
19
|
// src/cli.ts
|
|
16
20
|
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -37,16 +41,16 @@ function loadMemory(targetDir2) {
|
|
|
37
41
|
return { ...EMPTY_MEMORY, lastRun: [], fixHistory: [] };
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
|
-
function saveMemory(targetDir2,
|
|
44
|
+
function saveMemory(targetDir2, memory) {
|
|
41
45
|
mkdirSync(join(targetDir2, ".agent-research"), { recursive: true });
|
|
42
|
-
writeFileSync(memoryPath(targetDir2), JSON.stringify(
|
|
46
|
+
writeFileSync(memoryPath(targetDir2), JSON.stringify(memory, null, 2));
|
|
43
47
|
}
|
|
44
48
|
var snapshotKey = (e) => `${e.ruleId}::${e.file}::${e.exportName}`;
|
|
45
49
|
function precedentKey(c) {
|
|
46
50
|
return `${c.ruleId}::${c.file}`;
|
|
47
51
|
}
|
|
48
|
-
function updateMemory(
|
|
49
|
-
const prevByKey = new Map(
|
|
52
|
+
function updateMemory(memory, checks2, now = /* @__PURE__ */ new Date()) {
|
|
53
|
+
const prevByKey = new Map(memory.lastRun.map((e) => [snapshotKey(e), e]));
|
|
50
54
|
const fixedAt = now.toISOString().slice(0, 10);
|
|
51
55
|
const newFixEvents = [];
|
|
52
56
|
for (const c of checks2) {
|
|
@@ -61,15 +65,15 @@ function updateMemory(memory2, checks2, now = /* @__PURE__ */ new Date()) {
|
|
|
61
65
|
});
|
|
62
66
|
}
|
|
63
67
|
}
|
|
64
|
-
const fixHistory = [...
|
|
65
|
-
const
|
|
68
|
+
const fixHistory = [...memory.fixHistory, ...newFixEvents];
|
|
69
|
+
const precedents = /* @__PURE__ */ new Map();
|
|
66
70
|
for (const c of checks2) {
|
|
67
71
|
if (c.result !== "fail") continue;
|
|
68
72
|
const pk = precedentKey(c);
|
|
69
|
-
if (
|
|
73
|
+
if (precedents.has(pk)) continue;
|
|
70
74
|
const matches = fixHistory.filter((e) => e.ruleId === c.ruleId && e.file !== c.file).sort((a, b) => a.fixedAt < b.fixedAt ? 1 : a.fixedAt > b.fixedAt ? -1 : 0);
|
|
71
75
|
if (matches[0]) {
|
|
72
|
-
|
|
76
|
+
precedents.set(pk, {
|
|
73
77
|
file: matches[0].file,
|
|
74
78
|
exportName: matches[0].exportName,
|
|
75
79
|
fixedAt: matches[0].fixedAt,
|
|
@@ -84,31 +88,72 @@ function updateMemory(memory2, checks2, now = /* @__PURE__ */ new Date()) {
|
|
|
84
88
|
result: c.result,
|
|
85
89
|
detail: c.detail
|
|
86
90
|
}));
|
|
87
|
-
return { memory: { schemaVersion: "1.0", lastRun, fixHistory }, precedents
|
|
91
|
+
return { memory: { schemaVersion: "1.0", lastRun, fixHistory }, precedents };
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
// src/cli.ts
|
|
95
|
+
import { execFileSync } from "child_process";
|
|
91
96
|
var args = process.argv.slice(2);
|
|
92
97
|
if (args[0] === "trust-graph") {
|
|
93
98
|
await runTrustGraph(args.slice(1));
|
|
94
99
|
process.exit(0);
|
|
95
100
|
}
|
|
101
|
+
if (args[0] === "watch") {
|
|
102
|
+
const watchDir = args.find((a, i) => i > 0 && !a.startsWith("--")) ?? process.cwd();
|
|
103
|
+
startWatch(watchDir);
|
|
104
|
+
process.on("SIGINT", () => process.exit(0));
|
|
105
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
106
|
+
await new Promise(() => {
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (args[0] === "fix") {
|
|
110
|
+
await runFix(args.slice(1));
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
96
113
|
var ci = args.includes("--ci");
|
|
97
114
|
var strict = args.includes("--strict");
|
|
98
|
-
var
|
|
115
|
+
var sinceIdx = args.indexOf("--since");
|
|
116
|
+
var since = sinceIdx >= 0 ? args[sinceIdx + 1] : void 0;
|
|
117
|
+
var targetDir = args.find((a, i) => !a.startsWith("--") && args[i - 1] !== "--since") ?? process.cwd();
|
|
118
|
+
if (sinceIdx >= 0 && !since) {
|
|
119
|
+
console.error("error: --since requires a <ref> argument, e.g. --since origin/main");
|
|
120
|
+
process.exit(2);
|
|
121
|
+
}
|
|
99
122
|
var rules = resolveRules(targetDir);
|
|
100
|
-
var
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
123
|
+
var changedRanges;
|
|
124
|
+
if (since) {
|
|
125
|
+
try {
|
|
126
|
+
changedRanges = getChangedRanges(targetDir, since);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error(e.message ?? String(e));
|
|
129
|
+
process.exit(2);
|
|
130
|
+
}
|
|
106
131
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
132
|
+
var { checks, report } = audit(targetDir, rules, changedRanges);
|
|
133
|
+
if (!changedRanges) {
|
|
134
|
+
const memory = loadMemory(targetDir);
|
|
135
|
+
const { memory: nextMemory, precedents } = updateMemory(memory, checks);
|
|
136
|
+
for (const c of checks) {
|
|
137
|
+
const p = precedents.get(precedentKey(c));
|
|
138
|
+
if (p) c.precedent = p;
|
|
139
|
+
}
|
|
140
|
+
for (const rc of report.checks) {
|
|
141
|
+
const p = precedents.get(precedentKey(rc));
|
|
142
|
+
if (p) rc.precedent = p;
|
|
143
|
+
}
|
|
144
|
+
saveMemory(targetDir, nextMemory);
|
|
145
|
+
} else {
|
|
146
|
+
const memory = loadMemory(targetDir);
|
|
147
|
+
const { precedents } = updateMemory(memory, checks);
|
|
148
|
+
for (const c of checks) {
|
|
149
|
+
const p = precedents.get(precedentKey(c));
|
|
150
|
+
if (p) c.precedent = p;
|
|
151
|
+
}
|
|
152
|
+
for (const rc of report.checks) {
|
|
153
|
+
const p = precedents.get(precedentKey(rc));
|
|
154
|
+
if (p) rc.precedent = p;
|
|
155
|
+
}
|
|
110
156
|
}
|
|
111
|
-
saveMemory(targetDir, nextMemory);
|
|
112
157
|
var costReport = analyzeCosts(targetDir);
|
|
113
158
|
report.costAnalysis = costReport;
|
|
114
159
|
var outDir = join2(targetDir, ".agent-research");
|
|
@@ -199,3 +244,78 @@ async function runTrustGraph(argv) {
|
|
|
199
244
|
console.error(` program-cache: ${count} entries (${cp})`);
|
|
200
245
|
}
|
|
201
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
|
@@ -74,6 +74,14 @@ interface RustCandidate {
|
|
|
74
74
|
/** tree-sitter SyntaxNode for the function body — available for precise queries */
|
|
75
75
|
fnBodyNode: any;
|
|
76
76
|
}
|
|
77
|
+
interface ConfigCandidate {
|
|
78
|
+
/** Source file (absolute path) */
|
|
79
|
+
filePath: string;
|
|
80
|
+
/** Raw file contents */
|
|
81
|
+
content: string;
|
|
82
|
+
/** Whether this file is tracked by git / not covered by .gitignore */
|
|
83
|
+
tracked: boolean;
|
|
84
|
+
}
|
|
77
85
|
interface CheckOutcome {
|
|
78
86
|
result: CheckResultKind;
|
|
79
87
|
detail: string;
|
|
@@ -115,8 +123,17 @@ interface Rule {
|
|
|
115
123
|
modules: string[];
|
|
116
124
|
nameRegex: string;
|
|
117
125
|
triggerCalls: string[];
|
|
118
|
-
/**
|
|
119
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Defaults to "typescript". Set to "rust" for Anchor/Rust checker kinds,
|
|
128
|
+
* or "config" for whole-file config/env audits (see `filePatterns`).
|
|
129
|
+
*/
|
|
130
|
+
lang?: "typescript" | "rust" | "config";
|
|
131
|
+
/**
|
|
132
|
+
* Required when `lang: "config"`. Regexes (matched against the file path
|
|
133
|
+
* relative to the scan root) selecting which files this rule audits,
|
|
134
|
+
* e.g. `["(^|/)\\.env(\\.[^/]+)?$"]`. Ignored for "typescript"/"rust".
|
|
135
|
+
*/
|
|
136
|
+
filePatterns?: string[];
|
|
120
137
|
/**
|
|
121
138
|
* When true, a module import from `modules` is a REQUIRED condition for
|
|
122
139
|
* detection: a candidate must be in a file that imports one of the listed
|
|
@@ -140,9 +157,18 @@ interface Rule {
|
|
|
140
157
|
params?: Record<string, any>;
|
|
141
158
|
};
|
|
142
159
|
}
|
|
160
|
+
type Checker = (candidate: Candidate, params: any) => CheckOutcome;
|
|
161
|
+
type RustChecker = (candidate: RustCandidate, params: any) => CheckOutcome;
|
|
162
|
+
type ConfigChecker = (candidate: ConfigCandidate, params: any) => CheckOutcome;
|
|
143
163
|
|
|
144
|
-
|
|
145
|
-
declare function
|
|
164
|
+
type ChangedRanges = Map<string, Array<[number, number]>>;
|
|
165
|
+
declare function getChangedRanges(targetDir: string, ref: string): ChangedRanges;
|
|
166
|
+
declare function getWorkingTreeChanges(targetDir: string): ChangedRanges;
|
|
167
|
+
declare function fileChanged(ranges: ChangedRanges, file: string): boolean;
|
|
168
|
+
declare function rangeChanged(ranges: ChangedRanges, file: string, startLine: number, endLine: number): boolean;
|
|
169
|
+
|
|
170
|
+
declare function auditWithRule(targetDir: string, rule: Rule, changedRanges?: ChangedRanges): CheckResult[];
|
|
171
|
+
declare function audit(targetDir: string, rules: Rule[], changedRanges?: ChangedRanges): {
|
|
146
172
|
checks: CheckResult[];
|
|
147
173
|
report: {
|
|
148
174
|
schemaVersion: string;
|
|
@@ -213,11 +239,52 @@ declare function renderTest(kind: string, opts: {
|
|
|
213
239
|
}): string;
|
|
214
240
|
declare const testKinds: string[];
|
|
215
241
|
|
|
216
|
-
declare function runChecker(kind: string, c: Candidate | RustCandidate, params: any): CheckOutcome;
|
|
242
|
+
declare function runChecker(kind: string, c: Candidate | RustCandidate | ConfigCandidate, params: any): CheckOutcome;
|
|
217
243
|
declare const checkerKinds: string[];
|
|
218
244
|
|
|
219
245
|
declare function findCandidates(targetDir: string, rule: Rule): Candidate[];
|
|
220
246
|
|
|
247
|
+
declare function findConfigCandidates(targetDir: string, rule: Rule): ConfigCandidate[];
|
|
248
|
+
|
|
249
|
+
type WatchEvent = {
|
|
250
|
+
type: "watch_started";
|
|
251
|
+
targetDir: string;
|
|
252
|
+
} | {
|
|
253
|
+
type: "scan_error";
|
|
254
|
+
message: string;
|
|
255
|
+
} | {
|
|
256
|
+
type: "finding";
|
|
257
|
+
ruleId: string;
|
|
258
|
+
severity: string;
|
|
259
|
+
result: "fail" | "cant_tell";
|
|
260
|
+
file: string;
|
|
261
|
+
line: number;
|
|
262
|
+
detail: string;
|
|
263
|
+
fix?: unknown;
|
|
264
|
+
} | {
|
|
265
|
+
type: "scan_complete";
|
|
266
|
+
filesChanged: number;
|
|
267
|
+
findings: number;
|
|
268
|
+
durationMs: number;
|
|
269
|
+
};
|
|
270
|
+
interface WatchOptions {
|
|
271
|
+
debounceMs?: number;
|
|
272
|
+
emit?: (event: WatchEvent) => void;
|
|
273
|
+
}
|
|
274
|
+
declare function runIncrementalScan(targetDir: string, rules: Rule[], emit: (e: WatchEvent) => void): void;
|
|
275
|
+
declare function startWatch(targetDir: string, opts?: WatchOptions): {
|
|
276
|
+
close: () => void;
|
|
277
|
+
};
|
|
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
|
+
|
|
221
288
|
type UpgradeAuthorityKind = "renounced" | "single-key" | "multisig" | "dao" | "unknown";
|
|
222
289
|
type UpgradeAuthoritySource = "directory" | "rpc" | "research";
|
|
223
290
|
interface UpgradeAuthority {
|
|
@@ -347,4 +414,4 @@ declare function isEntryExpired(entry: ProgramCacheEntry, ttlHoursOverride?: num
|
|
|
347
414
|
*/
|
|
348
415
|
declare function cacheSize(cache: ProgramCache, ttlHoursOverride?: number): number;
|
|
349
416
|
|
|
350
|
-
export { type AccountFlow, type AuditRef, type BuildOpts, type Candidate, type CheckOutcome, type CheckResult, type CheckResultKind, type CostReport, DEFAULT_TTL_HOURS, type OnChainProgram, type ParityNote, type PriorityFeePosture, type ProgramCache, type ProgramCacheEntry, type Recoverability, type Rule, type RustAccountField, type RustCandidate, type Severity, type TrustGraph, type UpgradeAuthority, type UpgradeAuthorityKind, type UpgradeAuthoritySource, type VerifiedBuildState, analyzeCosts, audit, auditWithRule, base58Decode, base58Encode, buildTrustGraph, rules as bundledRules, cacheSize, checkerKinds, defaultCachePath, findCandidates, generateTestForResult, getCacheEntry, getCacheEntryMeta, isEntryExpired, isValidSolanaAddress, lamportsToSol, loadDirectory, loadProgramCache, loadRules, putCacheEntry, renderCostReportMd, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, runChecker, saveProgramCache, 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,
|
|
@@ -9,16 +10,22 @@ import {
|
|
|
9
10
|
cacheSize,
|
|
10
11
|
checkerKinds,
|
|
11
12
|
defaultCachePath,
|
|
13
|
+
fileChanged,
|
|
12
14
|
findCandidates,
|
|
15
|
+
findConfigCandidates,
|
|
13
16
|
getCacheEntry,
|
|
14
17
|
getCacheEntryMeta,
|
|
18
|
+
getChangedRanges,
|
|
19
|
+
getWorkingTreeChanges,
|
|
15
20
|
isEntryExpired,
|
|
16
21
|
isValidSolanaAddress,
|
|
17
22
|
lamportsToSol,
|
|
18
23
|
loadDirectory,
|
|
19
24
|
loadProgramCache,
|
|
20
25
|
loadRules,
|
|
26
|
+
parseDiff,
|
|
21
27
|
putCacheEntry,
|
|
28
|
+
rangeChanged,
|
|
22
29
|
renderCostReportMd,
|
|
23
30
|
renderTest,
|
|
24
31
|
renderTrustGraphMd,
|
|
@@ -26,9 +33,11 @@ import {
|
|
|
26
33
|
resolveRules,
|
|
27
34
|
rules,
|
|
28
35
|
runChecker,
|
|
36
|
+
runIncrementalScan,
|
|
29
37
|
saveProgramCache,
|
|
38
|
+
startWatch,
|
|
30
39
|
testKinds
|
|
31
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-Q72MTJXQ.js";
|
|
32
41
|
|
|
33
42
|
// src/generate.ts
|
|
34
43
|
import { writeFileSync, mkdirSync } from "fs";
|
|
@@ -46,6 +55,7 @@ function generateTestForResult(result, rule, outPath) {
|
|
|
46
55
|
export {
|
|
47
56
|
DEFAULT_TTL_HOURS,
|
|
48
57
|
analyzeCosts,
|
|
58
|
+
applyDiffToFile,
|
|
49
59
|
audit,
|
|
50
60
|
auditWithRule,
|
|
51
61
|
base58Decode,
|
|
@@ -55,23 +65,31 @@ export {
|
|
|
55
65
|
cacheSize,
|
|
56
66
|
checkerKinds,
|
|
57
67
|
defaultCachePath,
|
|
68
|
+
fileChanged,
|
|
58
69
|
findCandidates,
|
|
70
|
+
findConfigCandidates,
|
|
59
71
|
generateTestForResult,
|
|
60
72
|
getCacheEntry,
|
|
61
73
|
getCacheEntryMeta,
|
|
74
|
+
getChangedRanges,
|
|
75
|
+
getWorkingTreeChanges,
|
|
62
76
|
isEntryExpired,
|
|
63
77
|
isValidSolanaAddress,
|
|
64
78
|
lamportsToSol,
|
|
65
79
|
loadDirectory,
|
|
66
80
|
loadProgramCache,
|
|
67
81
|
loadRules,
|
|
82
|
+
parseDiff,
|
|
68
83
|
putCacheEntry,
|
|
84
|
+
rangeChanged,
|
|
69
85
|
renderCostReportMd,
|
|
70
86
|
renderTest,
|
|
71
87
|
renderTrustGraphMd,
|
|
72
88
|
rentExemptMinimum,
|
|
73
89
|
resolveRules,
|
|
74
90
|
runChecker,
|
|
91
|
+
runIncrementalScan,
|
|
75
92
|
saveProgramCache,
|
|
93
|
+
startWatch,
|
|
76
94
|
testKinds
|
|
77
95
|
};
|
|
@@ -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
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Pure-data rule (facts). Binds to vetted templates by `check.kind`/`test.kind`.
|
|
2
|
+
id: env-secrets-committed
|
|
3
|
+
severity: critical
|
|
4
|
+
title: Secret-looking values are not committed to source control
|
|
5
|
+
component:
|
|
6
|
+
name: Environment configuration
|
|
7
|
+
type: Config
|
|
8
|
+
version: unversioned
|
|
9
|
+
sourceUrl: https://12factor.net/config
|
|
10
|
+
detect:
|
|
11
|
+
lang: config
|
|
12
|
+
# .env, .env.local, .env.production, etc. — but not .env.example/.sample/.template,
|
|
13
|
+
# which are meant to be committed and document expected keys with placeholders.
|
|
14
|
+
filePatterns:
|
|
15
|
+
- "(^|/)\\.env(\\.(?!example$|sample$|template$)[^/]+)?$"
|
|
16
|
+
check:
|
|
17
|
+
kind: env-secrets-committed
|
|
18
|
+
params:
|
|
19
|
+
# Key names that typically hold credentials/secrets.
|
|
20
|
+
secretKeyPattern: "(SECRET|PRIVATE_KEY|API_KEY|ACCESS_KEY|TOKEN|PASSWORD|CREDENTIAL)"
|
|
21
|
+
# Values that look like placeholders, not real secrets — these are fine to commit.
|
|
22
|
+
placeholderPattern: "^(your[_-]|xxx|changeme|change[_-]?me|replace|example|<|sk_test_|pk_test_|test[_-]|dummy|placeholder|\\*+$|\\.\\.\\.$)"
|
|
23
|
+
ignoredDetail: >-
|
|
24
|
+
File is git-ignored and not committed to source control.
|
|
25
|
+
passDetail: >-
|
|
26
|
+
File is tracked but contains no secret-looking values (placeholders only).
|
|
27
|
+
failDetailPrefix: >-
|
|
28
|
+
This file is committed to source control and contains what look like real
|
|
29
|
+
secret values. Anyone with read access to the repo (including forks) can
|
|
30
|
+
read these credentials
|
|
31
|
+
test:
|
|
32
|
+
kind: none
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainblast",
|
|
3
|
-
"version": "0.4.
|
|
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": [
|