brainblast 0.5.3 → 0.6.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/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/{chunk-LHP6HFMS.js → chunk-A56IF3UX.js} +3 -897
- package/dist/chunk-EAKU3L7F.js +183 -0
- package/dist/chunk-SC6RNNDW.js +160 -0
- package/dist/chunk-ZZ6LBZV5.js +909 -0
- package/dist/cli.js +124 -4
- package/dist/diff-KIR4PCBC.js +13 -0
- package/dist/drift-JQ7DDZIF.js +11 -0
- package/dist/index.d.ts +61 -1
- package/dist/index.js +42 -19
- package/dist/mcp-AFYJQ7K6.js +173 -0
- package/dist/rules/jsonwebtoken-algorithm-pinned.yaml +36 -0
- package/dist/rules/open-redirect.yaml +32 -0
- package/dist/rules/prisma-raw-injection.yaml +35 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -2,11 +2,9 @@
|
|
|
2
2
|
import {
|
|
3
3
|
analyzeCosts,
|
|
4
4
|
applyDiffToFile,
|
|
5
|
-
audit,
|
|
6
5
|
buildTrustGraph,
|
|
7
6
|
cacheSize,
|
|
8
7
|
defaultCachePath,
|
|
9
|
-
getChangedRanges,
|
|
10
8
|
initPack,
|
|
11
9
|
isTelemetryEnabled,
|
|
12
10
|
isValidSolanaAddress,
|
|
@@ -15,12 +13,17 @@ import {
|
|
|
15
13
|
recordGraduationEvents,
|
|
16
14
|
renderCostReportMd,
|
|
17
15
|
renderTrustGraphMd,
|
|
18
|
-
resolveRules,
|
|
19
16
|
startWatch,
|
|
20
17
|
submitTelemetry,
|
|
21
18
|
telemetryFilePath,
|
|
22
19
|
validatePack
|
|
23
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-ZZ6LBZV5.js";
|
|
21
|
+
import {
|
|
22
|
+
audit,
|
|
23
|
+
getChangedRanges,
|
|
24
|
+
resolveRules
|
|
25
|
+
} from "./chunk-A56IF3UX.js";
|
|
26
|
+
import "./chunk-3RG5ZIWI.js";
|
|
24
27
|
|
|
25
28
|
// src/cli.ts
|
|
26
29
|
import { writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -107,6 +110,19 @@ function parsePackDirs(argv) {
|
|
|
107
110
|
if (!value || value.startsWith("--")) return [];
|
|
108
111
|
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
109
112
|
}
|
|
113
|
+
if (args[0] === "diff") {
|
|
114
|
+
await runDiff(args.slice(1));
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
if (args[0] === "drift") {
|
|
118
|
+
await runDrift(args.slice(1));
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
if (args[0] === "mcp") {
|
|
122
|
+
const { startMcpServer } = await import("./mcp-AFYJQ7K6.js");
|
|
123
|
+
await startMcpServer();
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
110
126
|
if (args[0] === "trust-graph") {
|
|
111
127
|
await runTrustGraph(args.slice(1));
|
|
112
128
|
process.exit(0);
|
|
@@ -418,3 +434,107 @@ Warning: could not create branch/commit: ${e.message ?? e}`);
|
|
|
418
434
|
}
|
|
419
435
|
}
|
|
420
436
|
}
|
|
437
|
+
async function runDrift(argv) {
|
|
438
|
+
const updateBaseline = argv.includes("--update-baseline");
|
|
439
|
+
const jsonOut = argv.includes("--json");
|
|
440
|
+
const targetDir2 = argv.find((a) => !a.startsWith("--")) ?? process.cwd();
|
|
441
|
+
const { checkDrift, renderDriftText } = await import("./drift-JQ7DDZIF.js");
|
|
442
|
+
let result;
|
|
443
|
+
try {
|
|
444
|
+
result = await checkDrift(targetDir2, { updateBaseline });
|
|
445
|
+
} catch (e) {
|
|
446
|
+
console.error(`brainblast drift: ${e.message ?? String(e)}`);
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
if (jsonOut) {
|
|
450
|
+
console.log(JSON.stringify(result, null, 2));
|
|
451
|
+
if (result.newAdvisories.length > 0) process.exit(1);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
console.log(renderDriftText(result));
|
|
455
|
+
if (result.newAdvisories.length > 0) process.exit(1);
|
|
456
|
+
}
|
|
457
|
+
function splitPkgVersion(arg) {
|
|
458
|
+
if (arg.startsWith("@")) {
|
|
459
|
+
const rest = arg.slice(1);
|
|
460
|
+
const at2 = rest.lastIndexOf("@");
|
|
461
|
+
if (at2 < 0) return [arg, ""];
|
|
462
|
+
return [`@${rest.slice(0, at2)}`, rest.slice(at2 + 1)];
|
|
463
|
+
}
|
|
464
|
+
const at = arg.lastIndexOf("@");
|
|
465
|
+
if (at <= 0) return [arg, ""];
|
|
466
|
+
return [arg.slice(0, at), arg.slice(at + 1)];
|
|
467
|
+
}
|
|
468
|
+
function guessEcosystem(name) {
|
|
469
|
+
if (name.includes("/") && !name.startsWith("@")) return "Go";
|
|
470
|
+
return "npm";
|
|
471
|
+
}
|
|
472
|
+
async function runDiff(argv) {
|
|
473
|
+
const flag = (n) => {
|
|
474
|
+
const i = argv.indexOf(`--${n}`);
|
|
475
|
+
return i >= 0 ? argv[i + 1] : void 0;
|
|
476
|
+
};
|
|
477
|
+
const ecoFlag = flag("ecosystem");
|
|
478
|
+
const fromFlag = flag("from");
|
|
479
|
+
const toFlag = flag("to");
|
|
480
|
+
const jsonOut = argv.includes("--json");
|
|
481
|
+
const skipValues = new Set([ecoFlag, fromFlag, toFlag].filter(Boolean));
|
|
482
|
+
const positional = argv.filter((a) => !a.startsWith("--") && !skipValues.has(a));
|
|
483
|
+
let pkgName;
|
|
484
|
+
let fromVersion;
|
|
485
|
+
let toVersion;
|
|
486
|
+
let ecosystem;
|
|
487
|
+
if (positional.length === 2 && positional[0].includes("@") && positional[1].includes("@")) {
|
|
488
|
+
const [n1, v1] = splitPkgVersion(positional[0]);
|
|
489
|
+
const [n2, v2] = splitPkgVersion(positional[1]);
|
|
490
|
+
if (n1 !== n2) {
|
|
491
|
+
console.error(`error: package names must match ('${n1}' vs '${n2}')`);
|
|
492
|
+
process.exit(2);
|
|
493
|
+
}
|
|
494
|
+
if (!v1 || !v2) {
|
|
495
|
+
console.error("error: could not parse versions from arguments");
|
|
496
|
+
process.exit(2);
|
|
497
|
+
}
|
|
498
|
+
pkgName = n1;
|
|
499
|
+
fromVersion = v1;
|
|
500
|
+
toVersion = v2;
|
|
501
|
+
ecosystem = ecoFlag ?? guessEcosystem(pkgName);
|
|
502
|
+
} else if (positional.length >= 1 && fromFlag && toFlag) {
|
|
503
|
+
pkgName = positional[0];
|
|
504
|
+
fromVersion = fromFlag;
|
|
505
|
+
toVersion = toFlag;
|
|
506
|
+
ecosystem = ecoFlag ?? guessEcosystem(pkgName);
|
|
507
|
+
} else {
|
|
508
|
+
console.error("usage: brainblast diff <pkg>@<from> <pkg>@<to> [--ecosystem <eco>]");
|
|
509
|
+
console.error(" or: brainblast diff <pkg> --from <v1> --to <v2> [--ecosystem <eco>]");
|
|
510
|
+
console.error(" e.g.: brainblast diff lodash@4.17.20 lodash@4.17.21");
|
|
511
|
+
process.exit(2);
|
|
512
|
+
}
|
|
513
|
+
const { diffVersions, renderDiffText, renderDiffMd, riskScore } = await import("./diff-KIR4PCBC.js");
|
|
514
|
+
let result;
|
|
515
|
+
try {
|
|
516
|
+
result = await diffVersions(ecosystem, pkgName, fromVersion, toVersion);
|
|
517
|
+
} catch (e) {
|
|
518
|
+
console.error(`brainblast diff: ${e.message ?? String(e)}`);
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
if (jsonOut) {
|
|
522
|
+
console.log(JSON.stringify(result, null, 2));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
console.log(renderDiffText(result));
|
|
526
|
+
const score = riskScore(result);
|
|
527
|
+
if (score > 0) {
|
|
528
|
+
console.error(`
|
|
529
|
+
Upgrade INCREASES risk (score: +${score}). Review introduced advisories before bumping.`);
|
|
530
|
+
process.exit(1);
|
|
531
|
+
} else if (score < 0) {
|
|
532
|
+
console.log(`
|
|
533
|
+
Upgrade DECREASES risk (score: ${score}). Upgrade recommended.`);
|
|
534
|
+
} else if (result.unchanged.length > 0) {
|
|
535
|
+
console.log(`
|
|
536
|
+
Risk profile unchanged (${result.unchanged.length} advisory${result.unchanged.length !== 1 ? "ies" : ""} persist in both versions).`);
|
|
537
|
+
} else {
|
|
538
|
+
console.log("\nNo known advisories for either version.");
|
|
539
|
+
}
|
|
540
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -495,4 +495,64 @@ declare function isEntryExpired(entry: ProgramCacheEntry, ttlHoursOverride?: num
|
|
|
495
495
|
*/
|
|
496
496
|
declare function cacheSize(cache: ProgramCache, ttlHoursOverride?: number): number;
|
|
497
497
|
|
|
498
|
-
|
|
498
|
+
interface OsvAdvisory {
|
|
499
|
+
id: string;
|
|
500
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
501
|
+
summary: string;
|
|
502
|
+
url: string;
|
|
503
|
+
}
|
|
504
|
+
declare function queryOsv(ecosystem: string, name: string, version: string): Promise<OsvAdvisory[]>;
|
|
505
|
+
|
|
506
|
+
interface DiffResult {
|
|
507
|
+
package: string;
|
|
508
|
+
ecosystem: string;
|
|
509
|
+
fromVersion: string;
|
|
510
|
+
toVersion: string;
|
|
511
|
+
/** Advisories present in fromVersion that are gone in toVersion. */
|
|
512
|
+
resolved: OsvAdvisory[];
|
|
513
|
+
/** Advisories that appear in toVersion but were not in fromVersion. */
|
|
514
|
+
introduced: OsvAdvisory[];
|
|
515
|
+
/** Advisories present in both versions. */
|
|
516
|
+
unchanged: OsvAdvisory[];
|
|
517
|
+
}
|
|
518
|
+
declare function diffVersions(ecosystem: string, packageName: string, fromVersion: string, toVersion: string): Promise<DiffResult>;
|
|
519
|
+
declare function riskScore(result: DiffResult): number;
|
|
520
|
+
declare function renderDiffText(result: DiffResult): string;
|
|
521
|
+
declare function renderDiffMd(result: DiffResult): string;
|
|
522
|
+
|
|
523
|
+
interface DriftPackage {
|
|
524
|
+
name: string;
|
|
525
|
+
version: string;
|
|
526
|
+
ecosystem: string;
|
|
527
|
+
source: string;
|
|
528
|
+
}
|
|
529
|
+
interface DriftAdvisory extends OsvAdvisory {
|
|
530
|
+
package: string;
|
|
531
|
+
ecosystem: string;
|
|
532
|
+
version: string;
|
|
533
|
+
}
|
|
534
|
+
interface DriftBaseline {
|
|
535
|
+
createdAt: string;
|
|
536
|
+
packages: number;
|
|
537
|
+
/** Map from "ecosystem:name@version" to array of advisory IDs. */
|
|
538
|
+
advisoryIds: Record<string, string[]>;
|
|
539
|
+
}
|
|
540
|
+
interface DriftResult {
|
|
541
|
+
newAdvisories: DriftAdvisory[];
|
|
542
|
+
resolvedAdvisories: DriftAdvisory[];
|
|
543
|
+
baselineExists: boolean;
|
|
544
|
+
baselineDate: string | null;
|
|
545
|
+
packagesChecked: number;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Seed packages from lockfiles in the project directory.
|
|
549
|
+
* Shells out to scripts/seed-inventory.sh for parity with the skill.
|
|
550
|
+
*/
|
|
551
|
+
declare function seedPackages(dir: string): DriftPackage[];
|
|
552
|
+
declare function checkDrift(dir: string, opts?: {
|
|
553
|
+
updateBaseline?: boolean;
|
|
554
|
+
packages?: DriftPackage[];
|
|
555
|
+
}): Promise<DriftResult>;
|
|
556
|
+
declare function renderDriftText(result: DriftResult): string;
|
|
557
|
+
|
|
558
|
+
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 DiffResult, type DriftAdvisory, type DriftBaseline, type DriftPackage, type DriftResult, type GraduationEvent, type OnChainProgram, type OsvAdvisory, 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, checkDrift, checkerKinds, defaultCachePath, diffVersions, fileChanged, findCandidates, findConfigCandidates, generateTestForResult, getCacheEntry, getCacheEntryMeta, getChangedRanges, getRepoHash, getUserHash, getWorkingTreeChanges, initPack, isEntryExpired, isTelemetryEnabled, isValidSolanaAddress, lamportsToSol, loadDirectory, loadPack, loadPacksFromDir, loadProgramCache, loadRules, parseDiff, putCacheEntry, queryOsv, rangeChanged, recordGraduationEvents, renderCostReportMd, renderDiffMd, renderDiffText, renderDriftText, renderTest, renderTrustGraphMd, rentExemptMinimum, resolveRules, riskScore, runChecker, runIncrementalScan, saveProgramCache, seedPackages, startWatch, submitTelemetry, telemetryFilePath, testKinds, validatePack, validatePackManifest };
|
package/dist/index.js
CHANGED
|
@@ -1,56 +1,71 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DEFAULT_REGISTRY_URL,
|
|
3
3
|
DEFAULT_TTL_HOURS,
|
|
4
|
-
PACK_MANIFEST_FILE,
|
|
5
4
|
analyzeCosts,
|
|
6
5
|
applyDiffToFile,
|
|
7
|
-
audit,
|
|
8
|
-
auditWithRule,
|
|
9
6
|
base58Decode,
|
|
10
7
|
base58Encode,
|
|
11
8
|
buildTrustGraph,
|
|
12
9
|
cacheSize,
|
|
13
|
-
checkerKinds,
|
|
14
10
|
defaultCachePath,
|
|
15
|
-
fileChanged,
|
|
16
|
-
findCandidates,
|
|
17
|
-
findConfigCandidates,
|
|
18
11
|
getCacheEntry,
|
|
19
12
|
getCacheEntryMeta,
|
|
20
|
-
getChangedRanges,
|
|
21
13
|
getRepoHash,
|
|
22
14
|
getUserHash,
|
|
23
|
-
getWorkingTreeChanges,
|
|
24
15
|
initPack,
|
|
25
16
|
isEntryExpired,
|
|
26
17
|
isTelemetryEnabled,
|
|
27
18
|
isValidSolanaAddress,
|
|
28
19
|
lamportsToSol,
|
|
29
20
|
loadDirectory,
|
|
30
|
-
loadPack,
|
|
31
|
-
loadPacksFromDir,
|
|
32
21
|
loadProgramCache,
|
|
33
|
-
loadRules,
|
|
34
22
|
parseDiff,
|
|
35
23
|
putCacheEntry,
|
|
36
|
-
rangeChanged,
|
|
37
24
|
recordGraduationEvents,
|
|
38
25
|
renderCostReportMd,
|
|
39
|
-
renderTest,
|
|
40
26
|
renderTrustGraphMd,
|
|
41
27
|
rentExemptMinimum,
|
|
42
|
-
resolveRules,
|
|
43
|
-
rules,
|
|
44
|
-
runChecker,
|
|
45
28
|
runIncrementalScan,
|
|
46
29
|
saveProgramCache,
|
|
47
30
|
startWatch,
|
|
48
31
|
submitTelemetry,
|
|
49
32
|
telemetryFilePath,
|
|
33
|
+
validatePack
|
|
34
|
+
} from "./chunk-ZZ6LBZV5.js";
|
|
35
|
+
import {
|
|
36
|
+
PACK_MANIFEST_FILE,
|
|
37
|
+
audit,
|
|
38
|
+
auditWithRule,
|
|
39
|
+
checkerKinds,
|
|
40
|
+
fileChanged,
|
|
41
|
+
findCandidates,
|
|
42
|
+
findConfigCandidates,
|
|
43
|
+
getChangedRanges,
|
|
44
|
+
getWorkingTreeChanges,
|
|
45
|
+
loadPack,
|
|
46
|
+
loadPacksFromDir,
|
|
47
|
+
loadRules,
|
|
48
|
+
rangeChanged,
|
|
49
|
+
renderTest,
|
|
50
|
+
resolveRules,
|
|
51
|
+
rules,
|
|
52
|
+
runChecker,
|
|
50
53
|
testKinds,
|
|
51
|
-
validatePack,
|
|
52
54
|
validatePackManifest
|
|
53
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-A56IF3UX.js";
|
|
56
|
+
import {
|
|
57
|
+
diffVersions,
|
|
58
|
+
queryOsv,
|
|
59
|
+
renderDiffMd,
|
|
60
|
+
renderDiffText,
|
|
61
|
+
riskScore
|
|
62
|
+
} from "./chunk-SC6RNNDW.js";
|
|
63
|
+
import {
|
|
64
|
+
checkDrift,
|
|
65
|
+
renderDriftText,
|
|
66
|
+
seedPackages
|
|
67
|
+
} from "./chunk-EAKU3L7F.js";
|
|
68
|
+
import "./chunk-3RG5ZIWI.js";
|
|
54
69
|
|
|
55
70
|
// src/generate.ts
|
|
56
71
|
import { writeFileSync, mkdirSync } from "fs";
|
|
@@ -78,8 +93,10 @@ export {
|
|
|
78
93
|
buildTrustGraph,
|
|
79
94
|
rules as bundledRules,
|
|
80
95
|
cacheSize,
|
|
96
|
+
checkDrift,
|
|
81
97
|
checkerKinds,
|
|
82
98
|
defaultCachePath,
|
|
99
|
+
diffVersions,
|
|
83
100
|
fileChanged,
|
|
84
101
|
findCandidates,
|
|
85
102
|
findConfigCandidates,
|
|
@@ -102,16 +119,22 @@ export {
|
|
|
102
119
|
loadRules,
|
|
103
120
|
parseDiff,
|
|
104
121
|
putCacheEntry,
|
|
122
|
+
queryOsv,
|
|
105
123
|
rangeChanged,
|
|
106
124
|
recordGraduationEvents,
|
|
107
125
|
renderCostReportMd,
|
|
126
|
+
renderDiffMd,
|
|
127
|
+
renderDiffText,
|
|
128
|
+
renderDriftText,
|
|
108
129
|
renderTest,
|
|
109
130
|
renderTrustGraphMd,
|
|
110
131
|
rentExemptMinimum,
|
|
111
132
|
resolveRules,
|
|
133
|
+
riskScore,
|
|
112
134
|
runChecker,
|
|
113
135
|
runIncrementalScan,
|
|
114
136
|
saveProgramCache,
|
|
137
|
+
seedPackages,
|
|
115
138
|
startWatch,
|
|
116
139
|
submitTelemetry,
|
|
117
140
|
telemetryFilePath,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
audit,
|
|
3
|
+
resolveRules
|
|
4
|
+
} from "./chunk-A56IF3UX.js";
|
|
5
|
+
import {
|
|
6
|
+
diffVersions,
|
|
7
|
+
queryOsv
|
|
8
|
+
} from "./chunk-SC6RNNDW.js";
|
|
9
|
+
import "./chunk-3RG5ZIWI.js";
|
|
10
|
+
|
|
11
|
+
// src/mcp.ts
|
|
12
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
13
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
14
|
+
import {
|
|
15
|
+
CallToolRequestSchema,
|
|
16
|
+
ListToolsRequestSchema
|
|
17
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
18
|
+
var VERSION = "0.6.0";
|
|
19
|
+
var TOOLS = [
|
|
20
|
+
{
|
|
21
|
+
name: "brainblast_audit",
|
|
22
|
+
description: "Run the brainblast deterministic static auditor on a local project directory. Returns per-rule check results (pass / fail / cant_tell) and severity totals. Catches catastrophic AI-integration traps: Stripe raw-body signing, JWT audience/issuer, SPL token-program identity, command-injection sinks, committed secrets, and more.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {
|
|
26
|
+
dir: {
|
|
27
|
+
type: "string",
|
|
28
|
+
description: "Absolute path to the project directory to audit. Defaults to the current working directory if omitted."
|
|
29
|
+
},
|
|
30
|
+
packs: {
|
|
31
|
+
type: "array",
|
|
32
|
+
items: { type: "string" },
|
|
33
|
+
description: "Optional list of absolute paths to pluggable rule-pack directories (each must contain a brainblast-pack.yaml manifest)."
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
required: []
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "brainblast_diff",
|
|
41
|
+
description: "Compare the OSV security-advisory risk profile between two versions of a package. Shows which advisories were introduced by the upgrade, which were resolved, and which are present in both versions. Returns a risk score: positive means the upgrade increases risk, negative means it decreases risk.",
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
ecosystem: {
|
|
46
|
+
type: "string",
|
|
47
|
+
description: "OSV ecosystem name \u2014 e.g. npm, PyPI, crates.io, Go, RubyGems, Packagist, Maven, NuGet."
|
|
48
|
+
},
|
|
49
|
+
package: { type: "string", description: "Package / module name." },
|
|
50
|
+
from_version: { type: "string", description: "The version currently in use (upgrading from)." },
|
|
51
|
+
to_version: { type: "string", description: "The candidate version (upgrading to)." }
|
|
52
|
+
},
|
|
53
|
+
required: ["ecosystem", "package", "from_version", "to_version"]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "brainblast_osv_check",
|
|
58
|
+
description: "Query OSV.dev for all known security advisories affecting a specific package at a specific version. Returns severity (critical/high/medium/low), advisory ID, human-readable summary, and advisory URL. An empty array means no advisories found \u2014 not that the package is safe.",
|
|
59
|
+
inputSchema: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
ecosystem: {
|
|
63
|
+
type: "string",
|
|
64
|
+
description: "OSV ecosystem \u2014 e.g. npm, PyPI, crates.io, Go, RubyGems."
|
|
65
|
+
},
|
|
66
|
+
package: { type: "string", description: "Package name." },
|
|
67
|
+
version: { type: "string", description: "Exact version string to check." }
|
|
68
|
+
},
|
|
69
|
+
required: ["ecosystem", "package", "version"]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
];
|
|
73
|
+
async function startMcpServer() {
|
|
74
|
+
const server = new Server(
|
|
75
|
+
{ name: "brainblast", version: VERSION },
|
|
76
|
+
{ capabilities: { tools: {} } }
|
|
77
|
+
);
|
|
78
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
79
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
80
|
+
const { name, arguments: args = {} } = req.params;
|
|
81
|
+
if (name === "brainblast_audit") {
|
|
82
|
+
const { dir = process.cwd(), packs = [] } = args;
|
|
83
|
+
try {
|
|
84
|
+
const rules = resolveRules(dir, packs);
|
|
85
|
+
const { checks, report } = audit(dir, rules);
|
|
86
|
+
return {
|
|
87
|
+
content: [
|
|
88
|
+
{
|
|
89
|
+
type: "text",
|
|
90
|
+
text: JSON.stringify(
|
|
91
|
+
{
|
|
92
|
+
dir,
|
|
93
|
+
rulesLoaded: rules.length,
|
|
94
|
+
checks,
|
|
95
|
+
riskTotals: report.riskTotals
|
|
96
|
+
},
|
|
97
|
+
null,
|
|
98
|
+
2
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
};
|
|
103
|
+
} catch (e) {
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: `Error running audit: ${e.message ?? String(e)}`
|
|
109
|
+
}
|
|
110
|
+
],
|
|
111
|
+
isError: true
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (name === "brainblast_osv_check") {
|
|
116
|
+
const { ecosystem, package: pkg, version } = args;
|
|
117
|
+
try {
|
|
118
|
+
const advisories = await queryOsv(ecosystem, pkg, version);
|
|
119
|
+
return {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: JSON.stringify(advisories, null, 2)
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
};
|
|
127
|
+
} catch (e) {
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: "text",
|
|
132
|
+
text: `OSV query failed: ${e.message ?? String(e)}`
|
|
133
|
+
}
|
|
134
|
+
],
|
|
135
|
+
isError: true
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (name === "brainblast_diff") {
|
|
140
|
+
const { ecosystem, package: pkg, from_version, to_version } = args;
|
|
141
|
+
try {
|
|
142
|
+
const result = await diffVersions(ecosystem, pkg, from_version, to_version);
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: "text",
|
|
147
|
+
text: JSON.stringify(result, null, 2)
|
|
148
|
+
}
|
|
149
|
+
]
|
|
150
|
+
};
|
|
151
|
+
} catch (e) {
|
|
152
|
+
return {
|
|
153
|
+
content: [
|
|
154
|
+
{
|
|
155
|
+
type: "text",
|
|
156
|
+
text: `Diff failed: ${e.message ?? String(e)}`
|
|
157
|
+
}
|
|
158
|
+
],
|
|
159
|
+
isError: true
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
165
|
+
isError: true
|
|
166
|
+
};
|
|
167
|
+
});
|
|
168
|
+
const transport = new StdioServerTransport();
|
|
169
|
+
await server.connect(transport);
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
startMcpServer
|
|
173
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Pure-data rule (facts). Binds to vetted templates by `check.kind`/`test.kind`.
|
|
2
|
+
id: jsonwebtoken-algorithm-pinned
|
|
3
|
+
severity: critical
|
|
4
|
+
title: jsonwebtoken verify() must pin the accepted algorithm(s)
|
|
5
|
+
component:
|
|
6
|
+
name: jsonwebtoken
|
|
7
|
+
type: Auth
|
|
8
|
+
version: unversioned
|
|
9
|
+
sourceUrl: https://github.com/auth0/node-jsonwebtoken/blob/master/README.md#jwtverifytoken-secretorpublickey-options-callback
|
|
10
|
+
detect:
|
|
11
|
+
lang: typescript
|
|
12
|
+
modules: [jsonwebtoken]
|
|
13
|
+
# Functions that import jsonwebtoken and contain verification logic.
|
|
14
|
+
nameRegex: "auth|jwt|token|verify|middleware"
|
|
15
|
+
triggerCalls: [verify]
|
|
16
|
+
requiresImport: true
|
|
17
|
+
check:
|
|
18
|
+
kind: required-call-with-options
|
|
19
|
+
params:
|
|
20
|
+
verifyCalls: [verify]
|
|
21
|
+
decodeCalls: [decode]
|
|
22
|
+
requiredProps:
|
|
23
|
+
- [algorithms]
|
|
24
|
+
passDetail: >-
|
|
25
|
+
jwt.verify() pins the accepted algorithm(s) via the 'algorithms' option —
|
|
26
|
+
algorithm-confusion attacks are blocked.
|
|
27
|
+
missingPropsDetail: >-
|
|
28
|
+
jwt.verify() is called without an 'algorithms' option. An attacker who controls
|
|
29
|
+
the token header can switch to 'none' (no signature) or to an asymmetric algorithm
|
|
30
|
+
using the public key as the HMAC secret, bypassing signature verification entirely.
|
|
31
|
+
Always pass algorithms: ['HS256'] (or whichever algorithm you use).
|
|
32
|
+
decodeOnlyDetail: >-
|
|
33
|
+
Token is decoded with jwt.decode() (no signature verification). Any token —
|
|
34
|
+
forged or expired — is accepted. Use jwt.verify() with a pinned algorithms list.
|
|
35
|
+
test:
|
|
36
|
+
kind: none
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Pure-data rule (facts). Binds to vetted templates by `check.kind`/`test.kind`.
|
|
2
|
+
id: open-redirect
|
|
3
|
+
severity: high
|
|
4
|
+
title: Untrusted request input used as a redirect destination
|
|
5
|
+
component:
|
|
6
|
+
name: Node.js HTTP (Express / Next.js)
|
|
7
|
+
type: API
|
|
8
|
+
version: unversioned
|
|
9
|
+
sourceUrl: https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html
|
|
10
|
+
detect:
|
|
11
|
+
lang: typescript
|
|
12
|
+
modules: []
|
|
13
|
+
# Trigger on any function that calls res.redirect / router.redirect / NextResponse.redirect.
|
|
14
|
+
nameRegex: "(?!)"
|
|
15
|
+
triggerCalls:
|
|
16
|
+
- redirect
|
|
17
|
+
- setHeader
|
|
18
|
+
requiresImport: false
|
|
19
|
+
check:
|
|
20
|
+
kind: taint-to-sink
|
|
21
|
+
params:
|
|
22
|
+
sources:
|
|
23
|
+
- name: request-input
|
|
24
|
+
# req.query / req.params are the most common entry points for open-redirect attacks.
|
|
25
|
+
# req.body included for completeness (e.g. POST-based login redirects).
|
|
26
|
+
pattern: "\\b(req|request)\\.(query|params|body|headers)\\b"
|
|
27
|
+
sinkCalls:
|
|
28
|
+
- redirect
|
|
29
|
+
- setHeader
|
|
30
|
+
maxHops: 2
|
|
31
|
+
test:
|
|
32
|
+
kind: none
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Pure-data rule (facts). Binds to vetted templates by `check.kind`/`test.kind`.
|
|
2
|
+
id: prisma-raw-injection
|
|
3
|
+
severity: critical
|
|
4
|
+
title: Untrusted request input flows into a Prisma raw query
|
|
5
|
+
component:
|
|
6
|
+
name: Prisma ORM
|
|
7
|
+
type: SDK
|
|
8
|
+
version: unversioned
|
|
9
|
+
sourceUrl: https://www.prisma.io/docs/orm/prisma-client/using-raw-sql/raw-queries#sql-injection-prevention
|
|
10
|
+
detect:
|
|
11
|
+
lang: typescript
|
|
12
|
+
modules: ["@prisma/client", "prisma"]
|
|
13
|
+
# Never matches by name alone — only the sink triggerCalls below select candidates.
|
|
14
|
+
nameRegex: "(?!)"
|
|
15
|
+
triggerCalls:
|
|
16
|
+
- $queryRaw
|
|
17
|
+
- $executeRaw
|
|
18
|
+
- $queryRawUnsafe
|
|
19
|
+
- $executeRawUnsafe
|
|
20
|
+
requiresImport: false
|
|
21
|
+
check:
|
|
22
|
+
kind: taint-to-sink
|
|
23
|
+
params:
|
|
24
|
+
sources:
|
|
25
|
+
- name: request-input
|
|
26
|
+
# req.body / req.query / req.params / req.headers — canonical untrusted HTTP input.
|
|
27
|
+
pattern: "\\b(req|request)\\.(body|query|params|headers)\\b"
|
|
28
|
+
sinkCalls:
|
|
29
|
+
- $queryRaw
|
|
30
|
+
- $executeRaw
|
|
31
|
+
- $queryRawUnsafe
|
|
32
|
+
- $executeRawUnsafe
|
|
33
|
+
maxHops: 2
|
|
34
|
+
test:
|
|
35
|
+
kind: none
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainblast",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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": [
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
66
67
|
"tree-sitter": "^0.22.4",
|
|
67
68
|
"tree-sitter-rust": "^0.24.0",
|
|
68
69
|
"ts-morph": "^23",
|