@kernel.chat/kbot 4.1.0 → 4.3.0

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,47 @@
1
+ import type { ToolDefinition } from './index.js';
2
+ type Severity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW' | 'INFO';
3
+ export interface SurfaceSignal {
4
+ id: string;
5
+ category: string;
6
+ severity: Severity;
7
+ file: string;
8
+ line: number;
9
+ excerpt: string;
10
+ pattern: string;
11
+ ts: number;
12
+ }
13
+ export interface SurfaceMap {
14
+ sessionId: string;
15
+ target: string;
16
+ startedAt: number;
17
+ filesWalked: number;
18
+ bytesRead: number;
19
+ signals: SurfaceSignal[];
20
+ skipped: {
21
+ path: string;
22
+ reason: string;
23
+ }[];
24
+ }
25
+ export interface BuildSurfaceMapOptions {
26
+ target: string;
27
+ excludes?: Iterable<string>;
28
+ severityFloor?: Severity;
29
+ sessionId?: string;
30
+ }
31
+ export declare function buildSurfaceMap(opts: BuildSurfaceMapOptions): SurfaceMap;
32
+ export declare function persistSurfaceMap(map: SurfaceMap, baseDir?: string): string;
33
+ export declare function renderSurfaceMap(map: SurfaceMap, persistedTo: string): string;
34
+ export interface RunOptions {
35
+ target: string;
36
+ severityFloor?: Severity;
37
+ excludes?: string[];
38
+ baseDir?: string;
39
+ }
40
+ export declare function runSecurityAuditLocal(opts: RunOptions): {
41
+ map: SurfaceMap;
42
+ persistedTo: string;
43
+ markdown: string;
44
+ };
45
+ export declare const securityAuditLocalTool: ToolDefinition;
46
+ export {};
47
+ //# sourceMappingURL=security-audit-local.d.ts.map
@@ -0,0 +1,363 @@
1
+ // security-audit-local — substrate for the local-vulnerability-hunt skill family.
2
+ //
3
+ // Walks a local source tree and builds a "surface map" of sites that are
4
+ // disproportionately likely to harbor security issues: subprocess sinks,
5
+ // eval-shaped sites, route handlers, crypto usage, FS write paths. The map
6
+ // is the input the agent (with a BYOK frontier model) reasons over.
7
+ //
8
+ // Persists every walk to ~/.kbot/security-audits/<session>/surface.jsonl
9
+ // so the audit trail outlives the chat. Shape mirrors forecast-summary.ts
10
+ // — pure substrate, no LLM call from this file.
11
+ //
12
+ // MIT, BYOK, local-first. The Mythos posture, democratized.
13
+ import { readFileSync, readdirSync, statSync, mkdirSync, writeFileSync, existsSync, } from 'node:fs';
14
+ import { join, relative, resolve } from 'node:path';
15
+ import { homedir } from 'node:os';
16
+ import { randomUUID } from 'node:crypto';
17
+ const ALL = new Set(['ts', 'tsx', 'js', 'mjs', 'cjs', 'jsx', 'py', 'go', 'rs', 'rb', 'java', 'php', 'sh']);
18
+ const JS = new Set(['ts', 'tsx', 'js', 'mjs', 'cjs', 'jsx']);
19
+ const PY = new Set(['py']);
20
+ const SHELL = new Set(['sh', 'bash', 'zsh']);
21
+ const PATTERNS = [
22
+ // Eval-shaped sinks — top-tier red flag in any language.
23
+ { category: 'eval-sink', severity: 'HIGH', regex: /\beval\s*\(/g, langs: ALL },
24
+ { category: 'function-constructor', severity: 'HIGH', regex: /\bnew\s+Function\s*\(/g, langs: JS },
25
+ { category: 'vm-sandbox', severity: 'MEDIUM', regex: /\bvm\.(runInContext|runInNewContext|runInThisContext|createContext)\s*\(/g, langs: JS },
26
+ { category: 'dynamic-require', severity: 'MEDIUM', regex: /\brequire\s*\(\s*[^'"]/g, langs: JS },
27
+ { category: 'dynamic-import', severity: 'MEDIUM', regex: /\bimport\s*\(\s*[^'"]/g, langs: JS },
28
+ // Subprocess / shell-out sinks.
29
+ { category: 'shell-exec', severity: 'HIGH', regex: /\b(child_process\.)?(exec|execSync)\s*\(/g, langs: JS },
30
+ { category: 'spawn', severity: 'MEDIUM', regex: /\b(child_process\.)?(spawn|spawnSync)\s*\(/g, langs: JS },
31
+ { category: 'os-system', severity: 'HIGH', regex: /\bos\.system\s*\(/g, langs: PY },
32
+ { category: 'subprocess-shell-true', severity: 'HIGH', regex: /\bsubprocess\.[a-zA-Z_]+\([^)]*shell\s*=\s*True/g, langs: PY },
33
+ { category: 'shell-eval', severity: 'HIGH', regex: /\beval\s+["'$]/g, langs: SHELL },
34
+ // HTTP route registration — surfaces user input boundaries.
35
+ { category: 'express-route', severity: 'INFO', regex: /\b(app|router)\.(get|post|put|delete|patch|all)\s*\(/g, langs: JS },
36
+ { category: 'fastify-route', severity: 'INFO', regex: /\bfastify\.(get|post|put|delete|patch|route)\s*\(/g, langs: JS },
37
+ { category: 'flask-route', severity: 'INFO', regex: /@(app|blueprint)\.route\s*\(/g, langs: PY },
38
+ { category: 'django-url', severity: 'INFO', regex: /\bpath\s*\(\s*['"][^'"]+['"]\s*,/g, langs: PY },
39
+ // Crypto smells.
40
+ { category: 'weak-hash-md5', severity: 'MEDIUM', regex: /\b(createHash|hashlib\.md5|md5)\s*\(\s*['"]?md5/gi, langs: ALL },
41
+ { category: 'weak-hash-sha1', severity: 'LOW', regex: /\b(createHash|hashlib\.sha1)\s*\(\s*['"]?sha1/gi, langs: ALL },
42
+ { category: 'predictable-random', severity: 'MEDIUM', regex: /\bMath\.random\s*\(/g, langs: JS },
43
+ { category: 'jwt-sign-none', severity: 'HIGH', regex: /\balgorithm\s*[:=]\s*['"]none['"]/gi, langs: ALL },
44
+ { category: 'jwt-verify-skip', severity: 'HIGH', regex: /\bverify\s*[:=]\s*false/g, langs: JS },
45
+ // SQL — string concat near query.
46
+ { category: 'sql-concat', severity: 'HIGH', regex: /(query|execute)\s*\(\s*[`'"][^`'"]*\$\{/g, langs: ALL },
47
+ // FS write near user-controlled path heuristic.
48
+ { category: 'fs-write', severity: 'INFO', regex: /\b(writeFile|writeFileSync|appendFile|appendFileSync|createWriteStream)\s*\(/g, langs: JS },
49
+ { category: 'path-join-userinput', severity: 'MEDIUM', regex: /\bpath\.join\s*\([^)]*req\.(body|query|params)/g, langs: JS },
50
+ // Disabled TLS verification — almost never correct.
51
+ { category: 'tls-reject-unauthorized', severity: 'HIGH', regex: /rejectUnauthorized\s*:\s*false/g, langs: JS },
52
+ { category: 'requests-verify-false', severity: 'HIGH', regex: /\bverify\s*=\s*False\b/g, langs: PY },
53
+ // Hardcoded comparisons of secrets — not constant-time.
54
+ { category: 'non-constant-time-compare', severity: 'MEDIUM', regex: /\b(token|secret|password|apiKey)\b\s*===?\s*['"`]/gi, langs: JS },
55
+ ];
56
+ const DEFAULT_EXCLUDES = new Set([
57
+ 'node_modules',
58
+ '.git',
59
+ 'dist',
60
+ 'build',
61
+ 'out',
62
+ '.next',
63
+ '.nuxt',
64
+ '.svelte-kit',
65
+ 'coverage',
66
+ 'vendor',
67
+ '__pycache__',
68
+ '.venv',
69
+ 'venv',
70
+ 'target',
71
+ '.turbo',
72
+ '.cache',
73
+ ]);
74
+ const MAX_FILE_BYTES = 1_000_000; // 1MB — anything bigger is generated or binary
75
+ const MAX_FILES = 5_000; // hard cap to keep walks bounded
76
+ const MAX_SIGNALS = 2_000; // truncate output beyond this
77
+ function ext(path) {
78
+ const dot = path.lastIndexOf('.');
79
+ if (dot < 0)
80
+ return '';
81
+ return path.slice(dot + 1).toLowerCase();
82
+ }
83
+ function isTextSource(path) {
84
+ return ALL.has(ext(path));
85
+ }
86
+ function* walk(root, excludes) {
87
+ const stack = [root];
88
+ let count = 0;
89
+ while (stack.length > 0 && count < MAX_FILES) {
90
+ const dir = stack.pop();
91
+ let entries;
92
+ try {
93
+ entries = readdirSync(dir);
94
+ }
95
+ catch {
96
+ continue;
97
+ }
98
+ for (const name of entries) {
99
+ if (excludes.has(name))
100
+ continue;
101
+ const full = join(dir, name);
102
+ let st;
103
+ try {
104
+ st = statSync(full);
105
+ }
106
+ catch {
107
+ continue;
108
+ }
109
+ if (st.isDirectory()) {
110
+ stack.push(full);
111
+ }
112
+ else if (st.isFile() && isTextSource(full) && st.size <= MAX_FILE_BYTES) {
113
+ count++;
114
+ yield full;
115
+ if (count >= MAX_FILES)
116
+ return;
117
+ }
118
+ }
119
+ }
120
+ }
121
+ function lineNumberAt(text, idx) {
122
+ let n = 1;
123
+ for (let i = 0; i < idx && i < text.length; i++) {
124
+ if (text.charCodeAt(i) === 10)
125
+ n++;
126
+ }
127
+ return n;
128
+ }
129
+ function excerpt(text, idx, max = 160) {
130
+ const start = Math.max(0, idx - 20);
131
+ const end = Math.min(text.length, idx + max);
132
+ return text.slice(start, end).replace(/\s+/g, ' ').trim();
133
+ }
134
+ function scanFile(path, rel, body) {
135
+ const lang = ext(path);
136
+ const out = [];
137
+ for (const p of PATTERNS) {
138
+ if (!p.langs.has(lang))
139
+ continue;
140
+ p.regex.lastIndex = 0;
141
+ let m;
142
+ while ((m = p.regex.exec(body)) !== null) {
143
+ out.push({
144
+ id: `S-${out.length + 1}`,
145
+ category: p.category,
146
+ severity: p.severity,
147
+ file: rel,
148
+ line: lineNumberAt(body, m.index),
149
+ excerpt: excerpt(body, m.index),
150
+ pattern: p.regex.source,
151
+ ts: Date.now(),
152
+ });
153
+ if (m.index === p.regex.lastIndex)
154
+ p.regex.lastIndex++;
155
+ }
156
+ }
157
+ return out;
158
+ }
159
+ const SEV_RANK = {
160
+ INFO: 0,
161
+ LOW: 1,
162
+ MEDIUM: 2,
163
+ HIGH: 3,
164
+ CRITICAL: 4,
165
+ };
166
+ function gteFloor(sig, floor) {
167
+ return SEV_RANK[sig] >= SEV_RANK[floor];
168
+ }
169
+ export function buildSurfaceMap(opts) {
170
+ const target = resolve(opts.target);
171
+ if (!existsSync(target)) {
172
+ throw new Error(`security_audit_local: target does not exist: ${target}`);
173
+ }
174
+ const stat = statSync(target);
175
+ if (!stat.isDirectory()) {
176
+ throw new Error(`security_audit_local: target is not a directory: ${target}`);
177
+ }
178
+ const excludes = new Set([...DEFAULT_EXCLUDES, ...(opts.excludes ?? [])]);
179
+ const floor = opts.severityFloor ?? 'INFO';
180
+ const sessionId = opts.sessionId ?? `audit-${Date.now()}-${randomUUID().slice(0, 8)}`;
181
+ const map = {
182
+ sessionId,
183
+ target,
184
+ startedAt: Date.now(),
185
+ filesWalked: 0,
186
+ bytesRead: 0,
187
+ signals: [],
188
+ skipped: [],
189
+ };
190
+ for (const file of walk(target, excludes)) {
191
+ let body;
192
+ try {
193
+ body = readFileSync(file, 'utf8');
194
+ }
195
+ catch (err) {
196
+ map.skipped.push({ path: relative(target, file), reason: err.message });
197
+ continue;
198
+ }
199
+ map.filesWalked++;
200
+ map.bytesRead += body.length;
201
+ const rel = relative(target, file) || file;
202
+ const sigs = scanFile(file, rel, body);
203
+ for (const s of sigs) {
204
+ if (!gteFloor(s.severity, floor))
205
+ continue;
206
+ map.signals.push(s);
207
+ if (map.signals.length >= MAX_SIGNALS)
208
+ break;
209
+ }
210
+ if (map.signals.length >= MAX_SIGNALS) {
211
+ map.skipped.push({ path: '<remaining>', reason: `signal cap ${MAX_SIGNALS} reached` });
212
+ break;
213
+ }
214
+ }
215
+ // Renumber signal IDs across the whole walk for stable references.
216
+ map.signals.forEach((s, i) => {
217
+ s.id = `${sessionId}#${(i + 1).toString().padStart(4, '0')}`;
218
+ });
219
+ return map;
220
+ }
221
+ function auditDir() {
222
+ return process.env.KBOT_SECURITY_AUDIT_DIR ?? join(homedir(), '.kbot', 'security-audits');
223
+ }
224
+ export function persistSurfaceMap(map, baseDir = auditDir()) {
225
+ const dir = join(baseDir, map.sessionId);
226
+ mkdirSync(dir, { recursive: true });
227
+ const surfacePath = join(dir, 'surface.jsonl');
228
+ const body = map.signals.map((s) => JSON.stringify(s)).join('\n') + (map.signals.length > 0 ? '\n' : '');
229
+ writeFileSync(surfacePath, body, 'utf8');
230
+ const metaPath = join(dir, 'meta.json');
231
+ writeFileSync(metaPath, JSON.stringify({
232
+ sessionId: map.sessionId,
233
+ target: map.target,
234
+ startedAt: map.startedAt,
235
+ filesWalked: map.filesWalked,
236
+ bytesRead: map.bytesRead,
237
+ signalCount: map.signals.length,
238
+ skipped: map.skipped,
239
+ }, null, 2), 'utf8');
240
+ return dir;
241
+ }
242
+ function counts(map) {
243
+ const c = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0, INFO: 0 };
244
+ for (const s of map.signals)
245
+ c[s.severity]++;
246
+ return c;
247
+ }
248
+ function topByCategory(map, n) {
249
+ const byCat = new Map();
250
+ for (const s of map.signals) {
251
+ const arr = byCat.get(s.category) ?? [];
252
+ arr.push(s);
253
+ byCat.set(s.category, arr);
254
+ }
255
+ return [...byCat.entries()]
256
+ .map(([category, sigs]) => ({
257
+ category,
258
+ count: sigs.length,
259
+ top: sigs.sort((a, b) => SEV_RANK[b.severity] - SEV_RANK[a.severity])[0] ?? null,
260
+ }))
261
+ .sort((a, b) => b.count - a.count)
262
+ .slice(0, n);
263
+ }
264
+ export function renderSurfaceMap(map, persistedTo) {
265
+ const c = counts(map);
266
+ const top = topByCategory(map, 10);
267
+ const lines = [];
268
+ lines.push(`# security_audit_local — surface map`);
269
+ lines.push('');
270
+ lines.push(`- target: \`${map.target}\``);
271
+ lines.push(`- session: \`${map.sessionId}\``);
272
+ lines.push(`- files walked: ${map.filesWalked}`);
273
+ lines.push(`- bytes read: ${map.bytesRead.toLocaleString()}`);
274
+ lines.push(`- signals: ${map.signals.length}`);
275
+ lines.push('');
276
+ lines.push(`| severity | count |`);
277
+ lines.push(`|---|---|`);
278
+ lines.push(`| CRITICAL | ${c.CRITICAL} |`);
279
+ lines.push(`| HIGH | ${c.HIGH} |`);
280
+ lines.push(`| MEDIUM | ${c.MEDIUM} |`);
281
+ lines.push(`| LOW | ${c.LOW} |`);
282
+ lines.push(`| INFO | ${c.INFO} |`);
283
+ lines.push('');
284
+ if (top.length > 0) {
285
+ lines.push(`## Top categories`);
286
+ lines.push('');
287
+ lines.push(`| category | count | example |`);
288
+ lines.push(`|---|---|---|`);
289
+ for (const t of top) {
290
+ const ex = t.top ? `${t.top.file}:${t.top.line} (${t.top.severity})` : '—';
291
+ lines.push(`| ${t.category} | ${t.count} | ${ex} |`);
292
+ }
293
+ lines.push('');
294
+ }
295
+ lines.push(`Audit trail: \`${persistedTo}\``);
296
+ lines.push('');
297
+ lines.push(`Next: feed signals to your BYOK frontier model phase by phase. See the \`local-vulnerability-hunt\` skill for the full workflow.`);
298
+ return lines.join('\n');
299
+ }
300
+ export function runSecurityAuditLocal(opts) {
301
+ const map = buildSurfaceMap({
302
+ target: opts.target,
303
+ severityFloor: opts.severityFloor,
304
+ excludes: opts.excludes,
305
+ });
306
+ const persistedTo = persistSurfaceMap(map, opts.baseDir);
307
+ const markdown = renderSurfaceMap(map, persistedTo);
308
+ return { map, persistedTo, markdown };
309
+ }
310
+ function parseSeverity(v) {
311
+ if (typeof v !== 'string')
312
+ return undefined;
313
+ const up = v.toUpperCase();
314
+ if (up === 'CRITICAL' || up === 'HIGH' || up === 'MEDIUM' || up === 'LOW' || up === 'INFO')
315
+ return up;
316
+ return undefined;
317
+ }
318
+ function parseExcludes(v) {
319
+ if (Array.isArray(v))
320
+ return v.filter((x) => typeof x === 'string');
321
+ if (typeof v === 'string' && v.length > 0)
322
+ return v.split(',').map((s) => s.trim()).filter(Boolean);
323
+ return undefined;
324
+ }
325
+ export const securityAuditLocalTool = {
326
+ name: 'security_audit_local',
327
+ description: 'Walk a local source tree and build a surface map of likely-risky sites (subprocess sinks, eval-shaped calls, route handlers, crypto smells, FS writes, TLS-skip flags). Persists JSONL audit trail under ~/.kbot/security-audits/<session>/. Substrate for the local-vulnerability-hunt skill family. Local-only; never phones home.',
328
+ parameters: {
329
+ target: {
330
+ type: 'string',
331
+ description: 'Absolute or relative directory to scan.',
332
+ required: true,
333
+ },
334
+ severity_floor: {
335
+ type: 'string',
336
+ description: 'Minimum severity to include: INFO | LOW | MEDIUM | HIGH | CRITICAL. Default INFO.',
337
+ required: false,
338
+ },
339
+ excludes: {
340
+ type: 'string',
341
+ description: 'Comma-separated extra directory names to skip (in addition to node_modules, dist, .git, etc.).',
342
+ required: false,
343
+ },
344
+ },
345
+ tier: 'free',
346
+ async execute(args) {
347
+ const target = typeof args.target === 'string' ? args.target : '';
348
+ if (!target)
349
+ return 'Error: target is required.';
350
+ try {
351
+ const { markdown } = runSecurityAuditLocal({
352
+ target,
353
+ severityFloor: parseSeverity(args.severity_floor),
354
+ excludes: parseExcludes(args.excludes),
355
+ });
356
+ return markdown;
357
+ }
358
+ catch (e) {
359
+ return `Error: ${e.message}`;
360
+ }
361
+ },
362
+ };
363
+ //# sourceMappingURL=security-audit-local.js.map
@@ -46,6 +46,15 @@ export declare class StreamOverlay {
46
46
  private renderTicker;
47
47
  private tickHighlight;
48
48
  private renderHighlight;
49
+ /**
50
+ * The folio strip — magazine masthead translated into broadcast chrome.
51
+ * Layout (left → right):
52
+ * ★ KERNEL.CHAT · LIVE · VIEWERS {n} · UPTIME {t} · CHAT {n}/MIN · BIOME {b} LIVE TRANSMISSION · 生放送
53
+ *
54
+ * Single hairline above. Cream ground. Ink type. Tomato spot on the
55
+ * leading ★ and the live tagline. Mirrors `.pop-folio` on the site,
56
+ * minus the issue-number monument — broadcasts don't carry issues.
57
+ */
49
58
  private renderInfoBar;
50
59
  }
51
60
  export declare function getOverlay(): StreamOverlay;