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.
@@ -0,0 +1,183 @@
1
+ import {
2
+ __require
3
+ } from "./chunk-3RG5ZIWI.js";
4
+
5
+ // src/drift.ts
6
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
7
+ import { join } from "path";
8
+ var BASELINE_PATH = (dir) => join(dir, ".agent-research", "drift-baseline.json");
9
+ function pkgKey(pkg) {
10
+ return `${pkg.ecosystem}:${pkg.name}@${pkg.version}`;
11
+ }
12
+ function loadBaseline(dir) {
13
+ const path = BASELINE_PATH(dir);
14
+ if (!existsSync(path)) return null;
15
+ try {
16
+ return JSON.parse(readFileSync(path, "utf8"));
17
+ } catch {
18
+ return null;
19
+ }
20
+ }
21
+ function saveBaseline(dir, baseline) {
22
+ const outDir = join(dir, ".agent-research");
23
+ mkdirSync(outDir, { recursive: true });
24
+ writeFileSync(BASELINE_PATH(dir), JSON.stringify(baseline, null, 2));
25
+ }
26
+ async function queryOsvBatch(pkgs) {
27
+ const results = /* @__PURE__ */ new Map();
28
+ if (pkgs.length === 0) return results;
29
+ for (let i = 0; i < pkgs.length; i += 1e3) {
30
+ const batch = pkgs.slice(i, i + 1e3);
31
+ const body = JSON.stringify({
32
+ queries: batch.map((p) => ({
33
+ version: p.version,
34
+ package: { name: p.name, ecosystem: p.ecosystem }
35
+ }))
36
+ });
37
+ let res;
38
+ try {
39
+ res = await fetch("https://api.osv.dev/v1/querybatch", {
40
+ method: "POST",
41
+ headers: { "Content-Type": "application/json" },
42
+ body,
43
+ signal: AbortSignal.timeout(3e4)
44
+ });
45
+ } catch (e) {
46
+ throw new Error(`OSV batch request failed: ${e.message ?? String(e)}`);
47
+ }
48
+ if (!res.ok) throw new Error(`OSV batch API error: ${res.status} ${res.statusText}`);
49
+ const data = await res.json();
50
+ for (let j = 0; j < batch.length; j++) {
51
+ const pkg = batch[j];
52
+ const vulns = data.results[j]?.vulns ?? [];
53
+ const advisories = vulns.map((v) => {
54
+ const id = v["id"];
55
+ const severities = v["severity"] ?? [];
56
+ let severity = "high";
57
+ for (const sev of severities) {
58
+ if (sev.type === "CVSS_V3") {
59
+ const score = parseFloat(sev.score);
60
+ if (!isNaN(score)) {
61
+ severity = score >= 9 ? "critical" : score >= 7 ? "high" : score >= 4 ? "medium" : "low";
62
+ break;
63
+ }
64
+ }
65
+ }
66
+ const dbSpec = v["database_specific"] ?? {};
67
+ if (severity === "high") {
68
+ const ghsa = (dbSpec["severity"] ?? "").toUpperCase();
69
+ severity = { CRITICAL: "critical", HIGH: "high", MODERATE: "medium", LOW: "low" }[ghsa] ?? "high";
70
+ }
71
+ const rawSummary = v["summary"] ?? "";
72
+ const rawDetails = v["details"] ?? "";
73
+ return { id, severity, summary: rawSummary || rawDetails.slice(0, 200), url: `https://osv.dev/vulnerability/${id}` };
74
+ });
75
+ results.set(pkgKey(pkg), advisories);
76
+ }
77
+ }
78
+ return results;
79
+ }
80
+ function seedPackages(dir) {
81
+ const { execFileSync } = __require("child_process");
82
+ const scriptPaths = [
83
+ join(dir, "scripts", "seed-inventory.sh"),
84
+ join(__dirname, "..", "..", "scripts", "seed-inventory.sh")
85
+ ];
86
+ for (const script of scriptPaths) {
87
+ if (existsSync(script)) {
88
+ try {
89
+ const out = execFileSync("sh", [script, dir], { encoding: "utf8", timeout: 3e4 });
90
+ return JSON.parse(out);
91
+ } catch {
92
+ }
93
+ }
94
+ }
95
+ return [];
96
+ }
97
+ async function checkDrift(dir, opts = {}) {
98
+ const pkgs = opts.packages ?? seedPackages(dir);
99
+ const baseline = loadBaseline(dir);
100
+ const currentMap = await queryOsvBatch(pkgs);
101
+ const currentIds = {};
102
+ for (const pkg of pkgs) {
103
+ const key = pkgKey(pkg);
104
+ currentIds[key] = (currentMap.get(key) ?? []).map((a) => a.id);
105
+ }
106
+ const newAdvisories = [];
107
+ const resolvedAdvisories = [];
108
+ if (baseline) {
109
+ for (const pkg of pkgs) {
110
+ const key = pkgKey(pkg);
111
+ const prev = new Set(baseline.advisoryIds[key] ?? []);
112
+ const curr = currentMap.get(key) ?? [];
113
+ for (const adv of curr) {
114
+ if (!prev.has(adv.id)) {
115
+ newAdvisories.push({ ...adv, package: pkg.name, ecosystem: pkg.ecosystem, version: pkg.version });
116
+ }
117
+ }
118
+ const currIds = new Set(curr.map((a) => a.id));
119
+ for (const prevId of prev) {
120
+ if (!currIds.has(prevId)) {
121
+ resolvedAdvisories.push({
122
+ id: prevId,
123
+ severity: "low",
124
+ summary: "Advisory no longer reported by OSV",
125
+ url: `https://osv.dev/vulnerability/${prevId}`,
126
+ package: pkg.name,
127
+ ecosystem: pkg.ecosystem,
128
+ version: pkg.version
129
+ });
130
+ }
131
+ }
132
+ }
133
+ }
134
+ if (opts.updateBaseline || !baseline) {
135
+ saveBaseline(dir, {
136
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
137
+ packages: pkgs.length,
138
+ advisoryIds: currentIds
139
+ });
140
+ }
141
+ return {
142
+ newAdvisories,
143
+ resolvedAdvisories,
144
+ baselineExists: !!baseline,
145
+ baselineDate: baseline?.createdAt ?? null,
146
+ packagesChecked: pkgs.length
147
+ };
148
+ }
149
+ function renderDriftText(result) {
150
+ const lines = [];
151
+ if (!result.baselineExists) {
152
+ lines.push(`brainblast drift: baseline created \u2014 ${result.packagesChecked} packages indexed.`);
153
+ lines.push("Re-run without --update-baseline to detect new advisories.");
154
+ return lines.join("\n");
155
+ }
156
+ lines.push(`brainblast drift: checked ${result.packagesChecked} packages against baseline from ${result.baselineDate?.split("T")[0] ?? "unknown"}`);
157
+ if (result.newAdvisories.length === 0 && result.resolvedAdvisories.length === 0) {
158
+ lines.push(" No new advisories. Dependency risk profile unchanged.");
159
+ return lines.join("\n");
160
+ }
161
+ if (result.newAdvisories.length > 0) {
162
+ lines.push(`
163
+ NEW (${result.newAdvisories.length}):`);
164
+ for (const a of result.newAdvisories) {
165
+ lines.push(` [${a.severity.toUpperCase()}] ${a.package}@${a.version} \u2014 ${a.id}: ${a.summary}`);
166
+ lines.push(` ${a.url}`);
167
+ }
168
+ }
169
+ if (result.resolvedAdvisories.length > 0) {
170
+ lines.push(`
171
+ RESOLVED (${result.resolvedAdvisories.length}):`);
172
+ for (const a of result.resolvedAdvisories) {
173
+ lines.push(` ${a.package}@${a.version} \u2014 ${a.id}`);
174
+ }
175
+ }
176
+ return lines.join("\n");
177
+ }
178
+
179
+ export {
180
+ seedPackages,
181
+ checkDrift,
182
+ renderDriftText
183
+ };
@@ -0,0 +1,160 @@
1
+ // src/osv.ts
2
+ function mapSeverity(vuln) {
3
+ const severities = vuln["severity"] ?? [];
4
+ for (const sev of severities) {
5
+ if (sev.type === "CVSS_V3") {
6
+ const score = parseFloat(sev.score);
7
+ if (!isNaN(score)) {
8
+ if (score >= 9) return "critical";
9
+ if (score >= 7) return "high";
10
+ if (score >= 4) return "medium";
11
+ return "low";
12
+ }
13
+ }
14
+ }
15
+ const dbSpec = vuln["database_specific"] ?? {};
16
+ const ghsa = (dbSpec["severity"] ?? "").toUpperCase();
17
+ const map = {
18
+ CRITICAL: "critical",
19
+ HIGH: "high",
20
+ MODERATE: "medium",
21
+ LOW: "low"
22
+ };
23
+ return map[ghsa] ?? "high";
24
+ }
25
+ async function queryOsv(ecosystem, name, version) {
26
+ const body = JSON.stringify({ version, package: { name, ecosystem } });
27
+ let res;
28
+ try {
29
+ res = await fetch("https://api.osv.dev/v1/query", {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body,
33
+ signal: AbortSignal.timeout(15e3)
34
+ });
35
+ } catch (e) {
36
+ throw new Error(`OSV API request failed: ${e.message ?? String(e)}`);
37
+ }
38
+ if (!res.ok) throw new Error(`OSV API error: ${res.status} ${res.statusText}`);
39
+ const data = await res.json();
40
+ return (data.vulns ?? []).map((v) => {
41
+ const id = v["id"];
42
+ const rawSummary = v["summary"] ?? "";
43
+ const rawDetails = v["details"] ?? "";
44
+ return {
45
+ id,
46
+ severity: mapSeverity(v),
47
+ summary: rawSummary || rawDetails.slice(0, 200),
48
+ url: `https://osv.dev/vulnerability/${id}`
49
+ };
50
+ });
51
+ }
52
+
53
+ // src/diff.ts
54
+ async function diffVersions(ecosystem, packageName, fromVersion, toVersion) {
55
+ const [fromAdvisories, toAdvisories] = await Promise.all([
56
+ queryOsv(ecosystem, packageName, fromVersion),
57
+ queryOsv(ecosystem, packageName, toVersion)
58
+ ]);
59
+ const fromIds = new Set(fromAdvisories.map((a) => a.id));
60
+ const toIds = new Set(toAdvisories.map((a) => a.id));
61
+ return {
62
+ package: packageName,
63
+ ecosystem,
64
+ fromVersion,
65
+ toVersion,
66
+ resolved: fromAdvisories.filter((a) => !toIds.has(a.id)),
67
+ introduced: toAdvisories.filter((a) => !fromIds.has(a.id)),
68
+ unchanged: fromAdvisories.filter((a) => toIds.has(a.id))
69
+ };
70
+ }
71
+ var SEV_WEIGHT = { critical: 8, high: 4, medium: 2, low: 1 };
72
+ function riskScore(result) {
73
+ const sum = (list) => list.reduce((n, a) => n + (SEV_WEIGHT[a.severity] ?? 0), 0);
74
+ return sum(result.introduced) - sum(result.resolved);
75
+ }
76
+ function badge(sev) {
77
+ return { critical: "[CRITICAL]", high: "[HIGH]", medium: "[MEDIUM]", low: "[LOW]" }[sev] ?? `[${sev.toUpperCase()}]`;
78
+ }
79
+ function renderDiffText(result) {
80
+ const lines = [];
81
+ lines.push(`brainblast diff: ${result.package} ${result.fromVersion} \u2192 ${result.toVersion} (${result.ecosystem})
82
+ `);
83
+ const total = result.introduced.length + result.resolved.length + result.unchanged.length;
84
+ if (total === 0) {
85
+ lines.push(" No known OSV advisories for either version.");
86
+ return lines.join("\n");
87
+ }
88
+ if (result.introduced.length > 0) {
89
+ lines.push(` INTRODUCED (${result.introduced.length}):`);
90
+ for (const a of result.introduced) {
91
+ lines.push(` + ${badge(a.severity)} ${a.id} \u2014 ${a.summary}`);
92
+ lines.push(` ${a.url}`);
93
+ }
94
+ }
95
+ if (result.resolved.length > 0) {
96
+ lines.push(` RESOLVED (${result.resolved.length}):`);
97
+ for (const a of result.resolved) {
98
+ lines.push(` - ${badge(a.severity)} ${a.id} \u2014 ${a.summary}`);
99
+ lines.push(` ${a.url}`);
100
+ }
101
+ }
102
+ if (result.unchanged.length > 0) {
103
+ lines.push(` UNCHANGED (${result.unchanged.length}):`);
104
+ for (const a of result.unchanged) {
105
+ lines.push(` ~ ${badge(a.severity)} ${a.id} \u2014 ${a.summary}`);
106
+ }
107
+ }
108
+ return lines.join("\n");
109
+ }
110
+ function renderDiffMd(result) {
111
+ const lines = [];
112
+ lines.push(`## brainblast diff: \`${result.package}\` ${result.fromVersion} \u2192 ${result.toVersion} (${result.ecosystem})
113
+ `);
114
+ const total = result.introduced.length + result.resolved.length + result.unchanged.length;
115
+ if (total === 0) {
116
+ lines.push("No known OSV advisories for either version.");
117
+ return lines.join("\n");
118
+ }
119
+ if (result.introduced.length > 0) {
120
+ lines.push(`### \u26A0\uFE0F Introduced (${result.introduced.length})
121
+ `);
122
+ for (const a of result.introduced) {
123
+ lines.push(`- **${badge(a.severity)}** [${a.id}](${a.url}) \u2014 ${a.summary}`);
124
+ }
125
+ lines.push("");
126
+ }
127
+ if (result.resolved.length > 0) {
128
+ lines.push(`### \u2705 Resolved (${result.resolved.length})
129
+ `);
130
+ for (const a of result.resolved) {
131
+ lines.push(`- **${badge(a.severity)}** [${a.id}](${a.url}) \u2014 ${a.summary}`);
132
+ }
133
+ lines.push("");
134
+ }
135
+ if (result.unchanged.length > 0) {
136
+ lines.push(`### ~ Unchanged (${result.unchanged.length})
137
+ `);
138
+ for (const a of result.unchanged) {
139
+ lines.push(`- **${badge(a.severity)}** [${a.id}](${a.url}) \u2014 ${a.summary}`);
140
+ }
141
+ lines.push("");
142
+ }
143
+ const score = riskScore(result);
144
+ if (score > 0) {
145
+ lines.push(`> **\u26D4 Upgrade increases risk (score +${score}). Review introduced advisories before bumping.**`);
146
+ } else if (score < 0) {
147
+ lines.push(`> **\u2705 Upgrade decreases risk (score ${score}). Upgrade recommended.**`);
148
+ } else if (result.unchanged.length > 0) {
149
+ lines.push(`> **~ Risk profile unchanged (${result.unchanged.length} advisory${result.unchanged.length !== 1 ? "ies" : ""} persist).**`);
150
+ }
151
+ return lines.join("\n");
152
+ }
153
+
154
+ export {
155
+ queryOsv,
156
+ diffVersions,
157
+ riskScore,
158
+ renderDiffText,
159
+ renderDiffMd
160
+ };