@kernel.chat/kbot 3.26.2 → 3.28.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,634 @@
1
+ // kbot Security Tools — Cybersecurity toolkit for developers
2
+ // Dependency auditing, secret scanning, SSL/TLS checks, CVE lookup,
3
+ // port scanning, header analysis, OWASP checks.
4
+ // All tools are free tier — no API keys required.
5
+ import { registerTool } from './index.js';
6
+ import { execSync } from 'node:child_process';
7
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
8
+ import { join } from 'node:path';
9
+ function fmt(n, d = 0) {
10
+ return n.toLocaleString('en-US', { minimumFractionDigits: d, maximumFractionDigits: d });
11
+ }
12
+ export function registerSecurityTools() {
13
+ // ─── Dependency Audit ───
14
+ registerTool({
15
+ name: 'dep_audit',
16
+ description: 'Audit project dependencies for known vulnerabilities. Runs npm audit, pip audit, or cargo audit depending on the project. Returns CVE IDs, severity, and fix recommendations.',
17
+ parameters: {
18
+ path: { type: 'string', description: 'Project directory to audit (default: current directory)' },
19
+ },
20
+ tier: 'free',
21
+ timeout: 60_000,
22
+ async execute(args) {
23
+ const dir = String(args.path || process.cwd());
24
+ const lines = ['## Dependency Audit', ''];
25
+ let totalVulns = 0;
26
+ // npm audit
27
+ if (existsSync(join(dir, 'package.json'))) {
28
+ try {
29
+ const result = execSync('npm audit --json 2>/dev/null', { cwd: dir, maxBuffer: 5_000_000 }).toString();
30
+ const audit = JSON.parse(result);
31
+ const vulns = audit.vulnerabilities || {};
32
+ const meta = audit.metadata?.vulnerabilities || {};
33
+ const total = (meta.critical || 0) + (meta.high || 0) + (meta.moderate || 0) + (meta.low || 0);
34
+ totalVulns += total;
35
+ lines.push(`### npm (${total} vulnerabilities)`);
36
+ if (meta.critical)
37
+ lines.push(`- **CRITICAL**: ${meta.critical}`);
38
+ if (meta.high)
39
+ lines.push(`- **HIGH**: ${meta.high}`);
40
+ if (meta.moderate)
41
+ lines.push(`- **MODERATE**: ${meta.moderate}`);
42
+ if (meta.low)
43
+ lines.push(`- **LOW**: ${meta.low}`);
44
+ // Top 5 most severe
45
+ const sorted = Object.entries(vulns)
46
+ .map(([name, info]) => ({ name, severity: info.severity, via: info.via, fixAvailable: info.fixAvailable }))
47
+ .sort((a, b) => {
48
+ const order = { critical: 0, high: 1, moderate: 2, low: 3 };
49
+ return (order[a.severity] ?? 4) - (order[b.severity] ?? 4);
50
+ });
51
+ if (sorted.length > 0) {
52
+ lines.push('', '| Package | Severity | Fix Available |', '|---------|----------|---------------|');
53
+ for (const v of sorted.slice(0, 10)) {
54
+ lines.push(`| ${v.name} | ${v.severity.toUpperCase()} | ${v.fixAvailable ? 'Yes' : 'No'} |`);
55
+ }
56
+ }
57
+ if (total > 0) {
58
+ lines.push('', '**Fix**: `npm audit fix` (safe) or `npm audit fix --force` (may break)');
59
+ }
60
+ }
61
+ catch {
62
+ // npm audit exits non-zero when vulns found — parse stderr
63
+ try {
64
+ const result = execSync('npm audit --json 2>&1 || true', { cwd: dir, maxBuffer: 5_000_000 }).toString();
65
+ const audit = JSON.parse(result);
66
+ const meta = audit.metadata?.vulnerabilities || {};
67
+ const total = (meta.critical || 0) + (meta.high || 0) + (meta.moderate || 0) + (meta.low || 0);
68
+ totalVulns += total;
69
+ lines.push(`### npm (${total} vulnerabilities)`);
70
+ if (meta.critical)
71
+ lines.push(`- **CRITICAL**: ${meta.critical}`);
72
+ if (meta.high)
73
+ lines.push(`- **HIGH**: ${meta.high}`);
74
+ if (meta.moderate)
75
+ lines.push(`- **MODERATE**: ${meta.moderate}`);
76
+ if (meta.low)
77
+ lines.push(`- **LOW**: ${meta.low}`);
78
+ }
79
+ catch {
80
+ lines.push('### npm — audit failed (run `npm install` first)');
81
+ }
82
+ }
83
+ lines.push('');
84
+ }
85
+ // Python
86
+ if (existsSync(join(dir, 'requirements.txt')) || existsSync(join(dir, 'pyproject.toml'))) {
87
+ try {
88
+ const result = execSync('pip audit --format json 2>/dev/null || pip-audit --format json 2>/dev/null || echo "[]"', { cwd: dir, maxBuffer: 2_000_000 }).toString();
89
+ const vulns = JSON.parse(result);
90
+ totalVulns += vulns.length;
91
+ lines.push(`### Python (${vulns.length} vulnerabilities)`);
92
+ if (vulns.length > 0) {
93
+ lines.push('', '| Package | Version | CVE |', '|---------|---------|-----|');
94
+ for (const v of vulns.slice(0, 10)) {
95
+ lines.push(`| ${v.name} | ${v.version} | ${v.id || '?'} |`);
96
+ }
97
+ }
98
+ }
99
+ catch {
100
+ lines.push('### Python — pip-audit not installed (`pip install pip-audit`)');
101
+ }
102
+ lines.push('');
103
+ }
104
+ // Rust
105
+ if (existsSync(join(dir, 'Cargo.toml'))) {
106
+ try {
107
+ const result = execSync('cargo audit --json 2>/dev/null || echo "{}"', { cwd: dir, maxBuffer: 2_000_000 }).toString();
108
+ const audit = JSON.parse(result);
109
+ const vulns = audit.vulnerabilities?.list || [];
110
+ totalVulns += vulns.length;
111
+ lines.push(`### Rust (${vulns.length} vulnerabilities)`);
112
+ if (vulns.length > 0) {
113
+ for (const v of vulns.slice(0, 10)) {
114
+ lines.push(`- **${v.advisory?.id}**: ${v.advisory?.title} (${v.package?.name})`);
115
+ }
116
+ }
117
+ }
118
+ catch {
119
+ lines.push('### Rust — cargo-audit not installed (`cargo install cargo-audit`)');
120
+ }
121
+ lines.push('');
122
+ }
123
+ if (totalVulns === 0) {
124
+ lines.push('**No known vulnerabilities found.**');
125
+ }
126
+ else {
127
+ lines.push(`**Total: ${totalVulns} vulnerabilities across all package managers.**`);
128
+ }
129
+ return lines.join('\n');
130
+ },
131
+ });
132
+ // ─── Secret Scanner ───
133
+ registerTool({
134
+ name: 'secret_scan',
135
+ description: 'Scan files for accidentally committed secrets — API keys, tokens, passwords, private keys. Checks common patterns (AWS, Stripe, GitHub, Supabase, etc). Does NOT read .env files.',
136
+ parameters: {
137
+ path: { type: 'string', description: 'Directory to scan (default: current directory)' },
138
+ depth: { type: 'number', description: 'Max directory depth (default: 5)', default: 5 },
139
+ },
140
+ tier: 'free',
141
+ timeout: 30_000,
142
+ async execute(args) {
143
+ const dir = String(args.path || process.cwd());
144
+ const maxDepth = Number(args.depth) || 5;
145
+ const SECRET_PATTERNS = [
146
+ { name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/ },
147
+ { name: 'AWS Secret Key', pattern: /(?:aws_secret|secret_access_key)\s*[:=]\s*['"]?[A-Za-z0-9/+=]{40}/ },
148
+ { name: 'GitHub Token', pattern: /ghp_[A-Za-z0-9]{36}/ },
149
+ { name: 'GitHub OAuth', pattern: /gho_[A-Za-z0-9]{36}/ },
150
+ { name: 'Stripe Secret', pattern: /sk_live_[A-Za-z0-9]{24,}/ },
151
+ { name: 'Stripe Publishable', pattern: /pk_live_[A-Za-z0-9]{24,}/ },
152
+ { name: 'Supabase Service Key', pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]{50,}/ },
153
+ { name: 'Anthropic Key', pattern: /sk-ant-[A-Za-z0-9_-]{40,}/ },
154
+ { name: 'OpenAI Key', pattern: /sk-[A-Za-z0-9]{48}/ },
155
+ { name: 'Slack Token', pattern: /xoxb-[0-9]{10,}-[A-Za-z0-9]{24,}/ },
156
+ { name: 'Slack Webhook', pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{8,}\/B[A-Z0-9]{8,}\/[A-Za-z0-9]{24}/ },
157
+ { name: 'Discord Webhook', pattern: /discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]+/ },
158
+ { name: 'Private Key', pattern: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
159
+ { name: 'Generic Secret', pattern: /(?:secret|password|passwd|token|api_key|apikey)\s*[:=]\s*['"][A-Za-z0-9/+=_-]{16,}['"]/ },
160
+ { name: 'npm Token', pattern: /npm_[A-Za-z0-9]{36}/ },
161
+ { name: 'Resend Key', pattern: /re_[A-Za-z0-9]{32,}/ },
162
+ { name: 'SendGrid Key', pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/ },
163
+ { name: 'Twilio SID', pattern: /AC[a-f0-9]{32}/ },
164
+ ];
165
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', 'target', '.venv', 'venv']);
166
+ const SKIP_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.mp3', '.mp4', '.zip', '.tar', '.gz', '.pdf', '.lock']);
167
+ const findings = [];
168
+ function scanDir(dirPath, depth) {
169
+ if (depth > maxDepth)
170
+ return;
171
+ try {
172
+ const entries = readdirSync(dirPath);
173
+ for (const entry of entries) {
174
+ if (SKIP_DIRS.has(entry) || entry.startsWith('.env'))
175
+ continue;
176
+ const fullPath = join(dirPath, entry);
177
+ try {
178
+ const stat = statSync(fullPath);
179
+ if (stat.isDirectory()) {
180
+ scanDir(fullPath, depth + 1);
181
+ }
182
+ else if (stat.isFile() && stat.size < 500_000) {
183
+ const ext = entry.slice(entry.lastIndexOf('.'));
184
+ if (SKIP_EXTS.has(ext))
185
+ continue;
186
+ try {
187
+ const content = readFileSync(fullPath, 'utf-8');
188
+ const lines = content.split('\n');
189
+ for (let i = 0; i < lines.length; i++) {
190
+ for (const { name, pattern } of SECRET_PATTERNS) {
191
+ const m = lines[i].match(pattern);
192
+ if (m) {
193
+ findings.push({
194
+ file: fullPath.replace(dir + '/', ''),
195
+ line: i + 1,
196
+ secret: name,
197
+ match: m[0].slice(0, 12) + '***' + m[0].slice(-4),
198
+ });
199
+ }
200
+ }
201
+ }
202
+ }
203
+ catch { /* binary or unreadable */ }
204
+ }
205
+ }
206
+ catch { /* permission denied */ }
207
+ }
208
+ }
209
+ catch { /* unreadable dir */ }
210
+ }
211
+ scanDir(dir, 0);
212
+ const lines = ['## Secret Scan', ''];
213
+ if (findings.length === 0) {
214
+ lines.push('**No secrets found.** Scanned common patterns (AWS, Stripe, GitHub, OpenAI, private keys, etc).');
215
+ }
216
+ else {
217
+ lines.push(`**${findings.length} potential secret(s) found:**`);
218
+ lines.push('');
219
+ lines.push('| File | Line | Type | Preview |');
220
+ lines.push('|------|------|------|---------|');
221
+ for (const f of findings.slice(0, 25)) {
222
+ lines.push(`| ${f.file} | ${f.line} | ${f.secret} | \`${f.match}\` |`);
223
+ }
224
+ if (findings.length > 25)
225
+ lines.push(``, `*...and ${findings.length - 25} more*`);
226
+ lines.push('');
227
+ lines.push('**Action**: Remove secrets from source, rotate compromised keys, add to `.gitignore`.');
228
+ lines.push('If already committed: `git filter-branch` or `bfg` to remove from history.');
229
+ }
230
+ return lines.join('\n');
231
+ },
232
+ });
233
+ // ─── SSL/TLS Check ───
234
+ registerTool({
235
+ name: 'ssl_check',
236
+ description: 'Check SSL/TLS certificate for any domain — expiry date, issuer, protocol, and security grade. Catches expiring certs before they break your site.',
237
+ parameters: {
238
+ domain: { type: 'string', description: 'Domain to check (e.g. "kernel.chat", "api.example.com")', required: true },
239
+ },
240
+ tier: 'free',
241
+ timeout: 15_000,
242
+ async execute(args) {
243
+ const domain = String(args.domain).replace(/^https?:\/\//, '').replace(/\/.*$/, '');
244
+ try {
245
+ const result = execSync(`echo | openssl s_client -servername ${domain} -connect ${domain}:443 2>/dev/null | openssl x509 -noout -dates -issuer -subject -text 2>/dev/null`, { timeout: 10_000 }).toString();
246
+ const notBefore = result.match(/notBefore=(.*)/)?.[1] || '?';
247
+ const notAfter = result.match(/notAfter=(.*)/)?.[1] || '?';
248
+ const issuer = result.match(/issuer=(.+)/)?.[1]?.trim() || '?';
249
+ const subject = result.match(/subject=(.+)/)?.[1]?.trim() || '?';
250
+ const sigAlgo = result.match(/Signature Algorithm:\s*(.+)/)?.[1]?.trim() || '?';
251
+ const keySize = result.match(/Public-Key:\s*\((\d+) bit\)/)?.[1] || '?';
252
+ const expiryDate = new Date(notAfter);
253
+ const now = new Date();
254
+ const daysLeft = Math.floor((expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
255
+ let status = 'VALID';
256
+ if (daysLeft < 0)
257
+ status = 'EXPIRED';
258
+ else if (daysLeft < 7)
259
+ status = 'CRITICAL — expires in < 7 days';
260
+ else if (daysLeft < 30)
261
+ status = 'WARNING — expires in < 30 days';
262
+ return [
263
+ `## SSL/TLS: ${domain}`,
264
+ '',
265
+ `**Status**: ${status}`,
266
+ `**Expires**: ${notAfter} (${daysLeft} days)`,
267
+ `**Issued**: ${notBefore}`,
268
+ `**Issuer**: ${issuer}`,
269
+ `**Subject**: ${subject}`,
270
+ `**Signature**: ${sigAlgo}`,
271
+ `**Key Size**: ${keySize} bit`,
272
+ '',
273
+ daysLeft < 30 ? `**ACTION REQUIRED**: Certificate expires in ${daysLeft} days. Renew now.` : `Certificate is healthy.`,
274
+ ].join('\n');
275
+ }
276
+ catch {
277
+ return `Could not check SSL for "${domain}". Verify the domain is correct and port 443 is reachable.`;
278
+ }
279
+ },
280
+ });
281
+ // ─── Security Headers Check ───
282
+ registerTool({
283
+ name: 'headers_check',
284
+ description: 'Check HTTP security headers for any URL — CSP, HSTS, X-Frame-Options, etc. Reports missing headers that leave you vulnerable to XSS, clickjacking, and MIME sniffing.',
285
+ parameters: {
286
+ url: { type: 'string', description: 'URL to check (e.g. "https://kernel.chat")', required: true },
287
+ },
288
+ tier: 'free',
289
+ timeout: 15_000,
290
+ async execute(args) {
291
+ const url = String(args.url);
292
+ const fullUrl = url.startsWith('http') ? url : `https://${url}`;
293
+ const res = await fetch(fullUrl, {
294
+ method: 'HEAD',
295
+ signal: AbortSignal.timeout(10_000),
296
+ redirect: 'follow',
297
+ });
298
+ const EXPECTED_HEADERS = [
299
+ { name: 'Content-Security-Policy', header: 'content-security-policy', description: 'Prevents XSS by controlling allowed script sources', severity: 'HIGH' },
300
+ { name: 'Strict-Transport-Security', header: 'strict-transport-security', description: 'Forces HTTPS, prevents downgrade attacks', severity: 'HIGH' },
301
+ { name: 'X-Content-Type-Options', header: 'x-content-type-options', description: 'Prevents MIME type sniffing', severity: 'MEDIUM' },
302
+ { name: 'X-Frame-Options', header: 'x-frame-options', description: 'Prevents clickjacking via iframes', severity: 'MEDIUM' },
303
+ { name: 'X-XSS-Protection', header: 'x-xss-protection', description: 'Legacy XSS filter (useful for older browsers)', severity: 'LOW' },
304
+ { name: 'Referrer-Policy', header: 'referrer-policy', description: 'Controls what info is sent in Referer header', severity: 'LOW' },
305
+ { name: 'Permissions-Policy', header: 'permissions-policy', description: 'Controls browser features (camera, mic, geolocation)', severity: 'LOW' },
306
+ { name: 'Cross-Origin-Opener-Policy', header: 'cross-origin-opener-policy', description: 'Isolates browsing context from cross-origin popups', severity: 'LOW' },
307
+ ];
308
+ const lines = [
309
+ `## Security Headers: ${fullUrl}`,
310
+ `**Status**: ${res.status} ${res.statusText}`,
311
+ '',
312
+ '| Header | Status | Severity | Value |',
313
+ '|--------|--------|----------|-------|',
314
+ ];
315
+ let missing = 0;
316
+ for (const h of EXPECTED_HEADERS) {
317
+ const value = res.headers.get(h.header);
318
+ if (value) {
319
+ lines.push(`| ${h.name} | ✅ Present | ${h.severity} | \`${value.slice(0, 50)}\` |`);
320
+ }
321
+ else {
322
+ lines.push(`| ${h.name} | ❌ Missing | ${h.severity} | ${h.description} |`);
323
+ missing++;
324
+ }
325
+ }
326
+ const score = Math.round(((EXPECTED_HEADERS.length - missing) / EXPECTED_HEADERS.length) * 100);
327
+ lines.push('');
328
+ lines.push(`**Score**: ${score}% (${EXPECTED_HEADERS.length - missing}/${EXPECTED_HEADERS.length} headers present)`);
329
+ if (missing > 0)
330
+ lines.push(`**Missing**: ${missing} header(s) — see table above for recommendations`);
331
+ return lines.join('\n');
332
+ },
333
+ });
334
+ // ─── CVE Lookup ───
335
+ registerTool({
336
+ name: 'cve_lookup',
337
+ description: 'Look up a CVE by ID or search for vulnerabilities affecting a specific package/product. Uses the NVD (National Vulnerability Database) API.',
338
+ parameters: {
339
+ query: { type: 'string', description: 'CVE ID (e.g. "CVE-2024-1234") or package name (e.g. "log4j", "openssl")', required: true },
340
+ },
341
+ tier: 'free',
342
+ timeout: 15_000,
343
+ async execute(args) {
344
+ const query = String(args.query).trim();
345
+ // Direct CVE lookup
346
+ if (/^CVE-\d{4}-\d+$/i.test(query)) {
347
+ const url = `https://services.nvd.nist.gov/rest/json/cves/2.0?cveId=${query.toUpperCase()}`;
348
+ const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
349
+ if (!res.ok)
350
+ return `NVD API error: ${res.status}`;
351
+ const data = await res.json();
352
+ const vuln = data.vulnerabilities?.[0]?.cve;
353
+ if (!vuln)
354
+ return `CVE "${query}" not found.`;
355
+ const desc = vuln.descriptions?.find((d) => d.lang === 'en')?.value || '?';
356
+ const metrics = vuln.metrics?.cvssMetricV31?.[0]?.cvssData || vuln.metrics?.cvssMetricV2?.[0]?.cvssData;
357
+ const score = metrics?.baseScore || '?';
358
+ const severity = metrics?.baseSeverity || '?';
359
+ const published = vuln.published?.split('T')[0] || '?';
360
+ return [
361
+ `## ${query.toUpperCase()}`,
362
+ '',
363
+ `**Severity**: ${severity} (${score}/10)`,
364
+ `**Published**: ${published}`,
365
+ '',
366
+ desc,
367
+ '',
368
+ `**NVD**: https://nvd.nist.gov/vuln/detail/${query.toUpperCase()}`,
369
+ ].join('\n');
370
+ }
371
+ // Keyword search
372
+ const url = `https://services.nvd.nist.gov/rest/json/cves/2.0?keywordSearch=${encodeURIComponent(query)}&resultsPerPage=10`;
373
+ const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
374
+ if (!res.ok)
375
+ return `NVD API error: ${res.status}`;
376
+ const data = await res.json();
377
+ const vulns = data.vulnerabilities || [];
378
+ if (!vulns.length)
379
+ return `No CVEs found for "${query}".`;
380
+ const lines = [
381
+ `## CVE Search: "${query}" (${data.totalResults} results)`,
382
+ '',
383
+ '| CVE | Severity | Score | Published | Description |',
384
+ '|-----|----------|-------|-----------|-------------|',
385
+ ];
386
+ for (const v of vulns.slice(0, 10)) {
387
+ const cve = v.cve;
388
+ const id = cve.id;
389
+ const desc = (cve.descriptions?.find((d) => d.lang === 'en')?.value || '?').slice(0, 80);
390
+ const metrics = cve.metrics?.cvssMetricV31?.[0]?.cvssData || cve.metrics?.cvssMetricV2?.[0]?.cvssData;
391
+ const score = metrics?.baseScore || '?';
392
+ const severity = metrics?.baseSeverity || '?';
393
+ const published = cve.published?.split('T')[0] || '?';
394
+ lines.push(`| ${id} | ${severity} | ${score} | ${published} | ${desc} |`);
395
+ }
396
+ return lines.join('\n');
397
+ },
398
+ });
399
+ // ─── Port Scanner ───
400
+ registerTool({
401
+ name: 'port_scan',
402
+ description: 'Scan common ports on a host to find open services. Checks the top 20 most common ports (HTTP, HTTPS, SSH, MySQL, Postgres, Redis, etc).',
403
+ parameters: {
404
+ host: { type: 'string', description: 'Hostname or IP to scan (e.g. "localhost", "192.168.1.1")', required: true },
405
+ },
406
+ tier: 'free',
407
+ timeout: 30_000,
408
+ async execute(args) {
409
+ const host = String(args.host);
410
+ const PORTS = [
411
+ { port: 21, service: 'FTP' },
412
+ { port: 22, service: 'SSH' },
413
+ { port: 25, service: 'SMTP' },
414
+ { port: 53, service: 'DNS' },
415
+ { port: 80, service: 'HTTP' },
416
+ { port: 443, service: 'HTTPS' },
417
+ { port: 3000, service: 'Dev Server' },
418
+ { port: 3306, service: 'MySQL' },
419
+ { port: 5173, service: 'Vite' },
420
+ { port: 5432, service: 'PostgreSQL' },
421
+ { port: 5900, service: 'VNC' },
422
+ { port: 6379, service: 'Redis' },
423
+ { port: 8080, service: 'HTTP Alt' },
424
+ { port: 8443, service: 'HTTPS Alt' },
425
+ { port: 8888, service: 'Jupyter' },
426
+ { port: 9090, service: 'Prometheus' },
427
+ { port: 11434, service: 'Ollama' },
428
+ { port: 27017, service: 'MongoDB' },
429
+ ];
430
+ const results = [];
431
+ await Promise.all(PORTS.map(async ({ port, service }) => {
432
+ try {
433
+ const controller = new AbortController();
434
+ const timeout = setTimeout(() => controller.abort(), 2000);
435
+ await fetch(`http://${host}:${port}`, { signal: controller.signal, method: 'HEAD' }).catch(() => { });
436
+ clearTimeout(timeout);
437
+ // If we get here without abort, port is likely open
438
+ // Use a more reliable TCP check
439
+ const check = execSync(`nc -z -w 2 ${host} ${port} 2>/dev/null && echo "open" || echo "closed"`, { timeout: 3000 }).toString().trim();
440
+ results.push({ port, service, open: check === 'open' });
441
+ }
442
+ catch {
443
+ results.push({ port, service, open: false });
444
+ }
445
+ }));
446
+ results.sort((a, b) => a.port - b.port);
447
+ const openPorts = results.filter(r => r.open);
448
+ const lines = [
449
+ `## Port Scan: ${host}`,
450
+ '',
451
+ `**Open ports**: ${openPorts.length} / ${PORTS.length} scanned`,
452
+ '',
453
+ ];
454
+ if (openPorts.length > 0) {
455
+ lines.push('| Port | Service | Status |', '|------|---------|--------|');
456
+ for (const r of openPorts) {
457
+ lines.push(`| ${r.port} | ${r.service} | OPEN |`);
458
+ }
459
+ // Security warnings
460
+ const risky = openPorts.filter(r => [21, 25, 3306, 5432, 6379, 27017, 5900].includes(r.port));
461
+ if (risky.length > 0) {
462
+ lines.push('');
463
+ lines.push('**Security Warnings:**');
464
+ for (const r of risky) {
465
+ lines.push(`- **Port ${r.port} (${r.service})** — should not be publicly exposed. Use firewall rules or bind to localhost.`);
466
+ }
467
+ }
468
+ }
469
+ else {
470
+ lines.push('No open ports found (or host is unreachable).');
471
+ }
472
+ return lines.join('\n');
473
+ },
474
+ });
475
+ // ─── OWASP Quick Check ───
476
+ registerTool({
477
+ name: 'owasp_check',
478
+ description: 'Quick OWASP Top 10 check against a codebase. Scans for common vulnerability patterns: SQL injection, XSS, command injection, path traversal, hardcoded secrets, insecure deserialization.',
479
+ parameters: {
480
+ path: { type: 'string', description: 'Directory to scan (default: current directory)' },
481
+ },
482
+ tier: 'free',
483
+ timeout: 30_000,
484
+ async execute(args) {
485
+ const dir = String(args.path || process.cwd());
486
+ const CHECKS = [
487
+ {
488
+ name: 'SQL Injection',
489
+ owasp: 'A03:2021',
490
+ patterns: [
491
+ /`\s*(?:SELECT|INSERT|UPDATE|DELETE|DROP)\s+.*\$\{/i,
492
+ /['"]?\s*\+\s*(?:req|params|query|body)\./i,
493
+ /\.query\(\s*['"`].*\+/i,
494
+ ],
495
+ severity: 'CRITICAL',
496
+ extensions: ['.ts', '.js', '.py', '.rb', '.php'],
497
+ },
498
+ {
499
+ name: 'Command Injection',
500
+ owasp: 'A03:2021',
501
+ patterns: [
502
+ /exec(?:Sync)?\(\s*[`'"].*\$\{/,
503
+ /exec(?:Sync)?\(\s*.*\+\s*(?:req|params|query|body|args|input)/,
504
+ /child_process.*exec.*\+/,
505
+ ],
506
+ severity: 'CRITICAL',
507
+ extensions: ['.ts', '.js', '.py'],
508
+ },
509
+ {
510
+ name: 'XSS (Cross-Site Scripting)',
511
+ owasp: 'A03:2021',
512
+ patterns: [
513
+ /innerHTML\s*=\s*(?!['"]<)/,
514
+ /dangerouslySetInnerHTML/,
515
+ /document\.write\(/,
516
+ /\.html\(\s*(?:req|params|query|body)/,
517
+ ],
518
+ severity: 'HIGH',
519
+ extensions: ['.ts', '.tsx', '.js', '.jsx', '.html'],
520
+ },
521
+ {
522
+ name: 'Path Traversal',
523
+ owasp: 'A01:2021',
524
+ patterns: [
525
+ /(?:readFile|readFileSync|createReadStream)\(\s*(?:req|params|query|body)/,
526
+ /path\.join\(\s*.*(?:req|params|query|body)/,
527
+ /\.\.\/.*(?:req|params|query)/,
528
+ ],
529
+ severity: 'HIGH',
530
+ extensions: ['.ts', '.js', '.py'],
531
+ },
532
+ {
533
+ name: 'Insecure Deserialization',
534
+ owasp: 'A08:2021',
535
+ patterns: [
536
+ /JSON\.parse\(\s*(?:req|body|params|query)/,
537
+ /eval\(\s*JSON/,
538
+ /yaml\.load\(\s*(?!.*Loader)/,
539
+ /pickle\.loads?\(/,
540
+ ],
541
+ severity: 'HIGH',
542
+ extensions: ['.ts', '.js', '.py'],
543
+ },
544
+ {
545
+ name: 'Broken Access Control',
546
+ owasp: 'A01:2021',
547
+ patterns: [
548
+ /(?:isAdmin|is_admin|role)\s*===?\s*['"](?:true|admin)['"]/,
549
+ /req\.headers\[['"]x-forwarded-for['"]\]/,
550
+ ],
551
+ severity: 'MEDIUM',
552
+ extensions: ['.ts', '.js', '.py'],
553
+ },
554
+ ];
555
+ const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '__pycache__', 'target']);
556
+ const findings = [];
557
+ function scanDir(dirPath, depth) {
558
+ if (depth > 5)
559
+ return;
560
+ try {
561
+ for (const entry of readdirSync(dirPath)) {
562
+ if (SKIP_DIRS.has(entry))
563
+ continue;
564
+ const fullPath = join(dirPath, entry);
565
+ try {
566
+ const stat = statSync(fullPath);
567
+ if (stat.isDirectory()) {
568
+ scanDir(fullPath, depth + 1);
569
+ continue;
570
+ }
571
+ if (!stat.isFile() || stat.size > 200_000)
572
+ continue;
573
+ const ext = entry.slice(entry.lastIndexOf('.'));
574
+ for (const check of CHECKS) {
575
+ if (!check.extensions.includes(ext))
576
+ continue;
577
+ try {
578
+ const content = readFileSync(fullPath, 'utf-8');
579
+ const lines = content.split('\n');
580
+ for (let i = 0; i < lines.length; i++) {
581
+ for (const pattern of check.patterns) {
582
+ if (pattern.test(lines[i])) {
583
+ findings.push({
584
+ check: check.name,
585
+ owasp: check.owasp,
586
+ file: fullPath.replace(dir + '/', ''),
587
+ line: i + 1,
588
+ severity: check.severity,
589
+ code: lines[i].trim().slice(0, 80),
590
+ });
591
+ }
592
+ }
593
+ }
594
+ }
595
+ catch { /* unreadable */ }
596
+ }
597
+ }
598
+ catch { /* permission */ }
599
+ }
600
+ }
601
+ catch { /* unreadable dir */ }
602
+ }
603
+ scanDir(dir, 0);
604
+ const lines = ['## OWASP Top 10 Quick Check', ''];
605
+ if (findings.length === 0) {
606
+ lines.push('**No OWASP patterns detected.** This is a static scan — it catches common patterns but not all vulnerabilities.');
607
+ }
608
+ else {
609
+ const bySeverity = { CRITICAL: 0, HIGH: 0, MEDIUM: 0 };
610
+ for (const f of findings)
611
+ bySeverity[f.severity]++;
612
+ lines.push(`**${findings.length} potential issue(s) found:**`);
613
+ if (bySeverity.CRITICAL)
614
+ lines.push(`- **CRITICAL**: ${bySeverity.CRITICAL}`);
615
+ if (bySeverity.HIGH)
616
+ lines.push(`- **HIGH**: ${bySeverity.HIGH}`);
617
+ if (bySeverity.MEDIUM)
618
+ lines.push(`- **MEDIUM**: ${bySeverity.MEDIUM}`);
619
+ lines.push('');
620
+ lines.push('| OWASP | Issue | File:Line | Severity | Code |');
621
+ lines.push('|-------|-------|-----------|----------|------|');
622
+ for (const f of findings.slice(0, 20)) {
623
+ lines.push(`| ${f.owasp} | ${f.check} | ${f.file}:${f.line} | ${f.severity} | \`${f.code.slice(0, 50)}\` |`);
624
+ }
625
+ if (findings.length > 20)
626
+ lines.push('', `*...and ${findings.length - 20} more*`);
627
+ lines.push('');
628
+ lines.push('*Static analysis — verify each finding manually. Some may be false positives.*');
629
+ }
630
+ return lines.join('\n');
631
+ },
632
+ });
633
+ }
634
+ //# sourceMappingURL=security.js.map