bashbros 0.1.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.
- package/LICENSE +21 -0
- package/README.md +453 -0
- package/dist/audit-MCFNGOIM.js +11 -0
- package/dist/audit-MCFNGOIM.js.map +1 -0
- package/dist/chunk-43W3RVEL.js +2910 -0
- package/dist/chunk-43W3RVEL.js.map +1 -0
- package/dist/chunk-4R4GV5V2.js +213 -0
- package/dist/chunk-4R4GV5V2.js.map +1 -0
- package/dist/chunk-7OCVIDC7.js +12 -0
- package/dist/chunk-7OCVIDC7.js.map +1 -0
- package/dist/chunk-CSRPOGHY.js +354 -0
- package/dist/chunk-CSRPOGHY.js.map +1 -0
- package/dist/chunk-DEAF6PYM.js +212 -0
- package/dist/chunk-DEAF6PYM.js.map +1 -0
- package/dist/chunk-DLP2O6PN.js +273 -0
- package/dist/chunk-DLP2O6PN.js.map +1 -0
- package/dist/chunk-GD5VNHIN.js +519 -0
- package/dist/chunk-GD5VNHIN.js.map +1 -0
- package/dist/chunk-ID2O2QTI.js +269 -0
- package/dist/chunk-ID2O2QTI.js.map +1 -0
- package/dist/chunk-J37RHCFJ.js +357 -0
- package/dist/chunk-J37RHCFJ.js.map +1 -0
- package/dist/chunk-SB4JS3GU.js +456 -0
- package/dist/chunk-SB4JS3GU.js.map +1 -0
- package/dist/chunk-SG752FZC.js +200 -0
- package/dist/chunk-SG752FZC.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2448 -0
- package/dist/cli.js.map +1 -0
- package/dist/config-CZMIGNPF.js +13 -0
- package/dist/config-CZMIGNPF.js.map +1 -0
- package/dist/config-parser-XHE7BC7H.js +13 -0
- package/dist/config-parser-XHE7BC7H.js.map +1 -0
- package/dist/db-EHQDB5OL.js +11 -0
- package/dist/db-EHQDB5OL.js.map +1 -0
- package/dist/display-IN4NRJJS.js +18 -0
- package/dist/display-IN4NRJJS.js.map +1 -0
- package/dist/engine-PKLXW6OF.js +9 -0
- package/dist/engine-PKLXW6OF.js.map +1 -0
- package/dist/index.d.ts +1498 -0
- package/dist/index.js +552 -0
- package/dist/index.js.map +1 -0
- package/dist/moltbot-DXZFVK3X.js +11 -0
- package/dist/moltbot-DXZFVK3X.js.map +1 -0
- package/dist/ollama-HY35OHW4.js +9 -0
- package/dist/ollama-HY35OHW4.js.map +1 -0
- package/dist/risk-scorer-Y6KF2XCZ.js +9 -0
- package/dist/risk-scorer-Y6KF2XCZ.js.map +1 -0
- package/dist/static/index.html +410 -0
- package/package.json +68 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/dashboard/db.ts"],"sourcesContent":["/**\r\n * Dashboard Database Module\r\n * SQLite-based storage for security events, connector activity, and egress blocks\r\n */\r\n\r\nimport Database from 'better-sqlite3'\r\nimport { randomUUID } from 'crypto'\r\nimport type {\r\n EventSource,\r\n EventLevel,\r\n UnifiedEvent,\r\n ConnectorEvent,\r\n EgressMatch,\r\n EgressPattern,\r\n RedactedPayload,\r\n ExposureResult\r\n} from '../policy/ward/types.js'\r\n\r\n// ─────────────────────────────────────────────────────────────\r\n// Types\r\n// ─────────────────────────────────────────────────────────────\r\n\r\nexport interface EventFilter {\r\n source?: EventSource\r\n level?: EventLevel\r\n category?: string\r\n since?: Date\r\n limit?: number\r\n offset?: number\r\n}\r\n\r\nexport interface InsertEventInput {\r\n source: EventSource\r\n level: EventLevel\r\n category: string\r\n message: string\r\n data?: Record<string, unknown>\r\n}\r\n\r\nexport interface InsertConnectorEventInput {\r\n connector: string\r\n method: string\r\n direction: 'inbound' | 'outbound'\r\n payload: RedactedPayload\r\n resourcesAccessed: string[]\r\n}\r\n\r\nexport interface InsertEgressBlockInput {\r\n pattern: EgressPattern\r\n matchedText: string\r\n redactedText: string\r\n connector?: string\r\n destination?: string\r\n}\r\n\r\nexport interface DashboardStats {\r\n totalEvents: number\r\n eventsBySource: Record<string, number>\r\n eventsByLevel: Record<string, number>\r\n pendingBlocks: number\r\n connectorCount: number\r\n recentExposures: number\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────\r\n// Database Class\r\n// ─────────────────────────────────────────────────────────────\r\n\r\nexport class DashboardDB {\r\n private db: Database.Database\r\n\r\n constructor(dbPath: string = '.bashbros.db') {\r\n this.db = new Database(dbPath)\r\n this.db.pragma('journal_mode = WAL')\r\n this.initTables()\r\n }\r\n\r\n private initTables(): void {\r\n // Events table - unified security events\r\n this.db.exec(`\r\n CREATE TABLE IF NOT EXISTS events (\r\n id TEXT PRIMARY KEY,\r\n timestamp TEXT NOT NULL,\r\n source TEXT NOT NULL,\r\n level TEXT NOT NULL,\r\n category TEXT NOT NULL,\r\n message TEXT NOT NULL,\r\n data TEXT\r\n )\r\n `)\r\n\r\n // Connector events table - MCP connector activity\r\n this.db.exec(`\r\n CREATE TABLE IF NOT EXISTS connector_events (\r\n id TEXT PRIMARY KEY,\r\n timestamp TEXT NOT NULL,\r\n connector TEXT NOT NULL,\r\n method TEXT NOT NULL,\r\n direction TEXT NOT NULL,\r\n payload TEXT NOT NULL,\r\n resources_accessed TEXT NOT NULL\r\n )\r\n `)\r\n\r\n // Egress blocks table - blocked sensitive data\r\n this.db.exec(`\r\n CREATE TABLE IF NOT EXISTS egress_blocks (\r\n id TEXT PRIMARY KEY,\r\n timestamp TEXT NOT NULL,\r\n pattern TEXT NOT NULL,\r\n matched_text TEXT NOT NULL,\r\n redacted_text TEXT NOT NULL,\r\n connector TEXT,\r\n destination TEXT,\r\n status TEXT NOT NULL DEFAULT 'pending',\r\n approved_by TEXT,\r\n approved_at TEXT\r\n )\r\n `)\r\n\r\n // Exposure scans table - network exposure scan results\r\n this.db.exec(`\r\n CREATE TABLE IF NOT EXISTS exposure_scans (\r\n id TEXT PRIMARY KEY,\r\n timestamp TEXT NOT NULL,\r\n agent TEXT NOT NULL,\r\n pid INTEGER,\r\n port INTEGER NOT NULL,\r\n bind_address TEXT NOT NULL,\r\n has_auth TEXT NOT NULL,\r\n severity TEXT NOT NULL,\r\n action TEXT NOT NULL,\r\n message TEXT NOT NULL\r\n )\r\n `)\r\n\r\n // Create indexes for common queries\r\n this.db.exec(`\r\n CREATE INDEX IF NOT EXISTS idx_events_source ON events(source);\r\n CREATE INDEX IF NOT EXISTS idx_events_level ON events(level);\r\n CREATE INDEX IF NOT EXISTS idx_events_timestamp ON events(timestamp);\r\n CREATE INDEX IF NOT EXISTS idx_connector_events_connector ON connector_events(connector);\r\n CREATE INDEX IF NOT EXISTS idx_egress_blocks_status ON egress_blocks(status);\r\n `)\r\n }\r\n\r\n // ─────────────────────────────────────────────────────────────\r\n // Events\r\n // ─────────────────────────────────────────────────────────────\r\n\r\n insertEvent(input: InsertEventInput): string {\r\n const id = randomUUID()\r\n const timestamp = new Date().toISOString()\r\n const data = input.data ? JSON.stringify(input.data) : null\r\n\r\n const stmt = this.db.prepare(`\r\n INSERT INTO events (id, timestamp, source, level, category, message, data)\r\n VALUES (?, ?, ?, ?, ?, ?, ?)\r\n `)\r\n\r\n stmt.run(id, timestamp, input.source, input.level, input.category, input.message, data)\r\n return id\r\n }\r\n\r\n getEvents(filter: EventFilter = {}): UnifiedEvent[] {\r\n const conditions: string[] = []\r\n const params: unknown[] = []\r\n\r\n if (filter.source) {\r\n conditions.push('source = ?')\r\n params.push(filter.source)\r\n }\r\n\r\n if (filter.level) {\r\n conditions.push('level = ?')\r\n params.push(filter.level)\r\n }\r\n\r\n if (filter.category) {\r\n conditions.push('category = ?')\r\n params.push(filter.category)\r\n }\r\n\r\n if (filter.since) {\r\n conditions.push('timestamp >= ?')\r\n params.push(filter.since.toISOString())\r\n }\r\n\r\n const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''\r\n const limit = filter.limit ?? 100\r\n const offset = filter.offset ?? 0\r\n\r\n const stmt = this.db.prepare(`\r\n SELECT * FROM events\r\n ${whereClause}\r\n ORDER BY timestamp DESC\r\n LIMIT ? OFFSET ?\r\n `)\r\n\r\n params.push(limit, offset)\r\n const rows = stmt.all(...params) as Array<{\r\n id: string\r\n timestamp: string\r\n source: EventSource\r\n level: EventLevel\r\n category: string\r\n message: string\r\n data: string | null\r\n }>\r\n\r\n return rows.map(row => ({\r\n id: row.id,\r\n timestamp: new Date(row.timestamp),\r\n source: row.source,\r\n level: row.level,\r\n category: row.category,\r\n message: row.message,\r\n data: row.data ? JSON.parse(row.data) : undefined\r\n }))\r\n }\r\n\r\n // ─────────────────────────────────────────────────────────────\r\n // Connector Events\r\n // ─────────────────────────────────────────────────────────────\r\n\r\n insertConnectorEvent(input: InsertConnectorEventInput): string {\r\n const id = randomUUID()\r\n const timestamp = new Date().toISOString()\r\n\r\n const stmt = this.db.prepare(`\r\n INSERT INTO connector_events (id, timestamp, connector, method, direction, payload, resources_accessed)\r\n VALUES (?, ?, ?, ?, ?, ?, ?)\r\n `)\r\n\r\n stmt.run(\r\n id,\r\n timestamp,\r\n input.connector,\r\n input.method,\r\n input.direction,\r\n JSON.stringify(input.payload),\r\n JSON.stringify(input.resourcesAccessed)\r\n )\r\n\r\n return id\r\n }\r\n\r\n getConnectorEvents(connector: string, limit: number = 100): ConnectorEvent[] {\r\n const stmt = this.db.prepare(`\r\n SELECT * FROM connector_events\r\n WHERE connector = ?\r\n ORDER BY timestamp DESC\r\n LIMIT ?\r\n `)\r\n\r\n const rows = stmt.all(connector, limit) as Array<{\r\n id: string\r\n timestamp: string\r\n connector: string\r\n method: string\r\n direction: 'inbound' | 'outbound'\r\n payload: string\r\n resources_accessed: string\r\n }>\r\n\r\n return rows.map(row => ({\r\n id: row.id,\r\n timestamp: new Date(row.timestamp),\r\n connector: row.connector,\r\n method: row.method,\r\n direction: row.direction,\r\n payload: JSON.parse(row.payload),\r\n resourcesAccessed: JSON.parse(row.resources_accessed)\r\n }))\r\n }\r\n\r\n getAllConnectorEvents(limit: number = 100): ConnectorEvent[] {\r\n const stmt = this.db.prepare(`\r\n SELECT * FROM connector_events\r\n ORDER BY timestamp DESC\r\n LIMIT ?\r\n `)\r\n\r\n const rows = stmt.all(limit) as Array<{\r\n id: string\r\n timestamp: string\r\n connector: string\r\n method: string\r\n direction: 'inbound' | 'outbound'\r\n payload: string\r\n resources_accessed: string\r\n }>\r\n\r\n return rows.map(row => ({\r\n id: row.id,\r\n timestamp: new Date(row.timestamp),\r\n connector: row.connector,\r\n method: row.method,\r\n direction: row.direction,\r\n payload: JSON.parse(row.payload),\r\n resourcesAccessed: JSON.parse(row.resources_accessed)\r\n }))\r\n }\r\n\r\n // ─────────────────────────────────────────────────────────────\r\n // Egress Blocks\r\n // ─────────────────────────────────────────────────────────────\r\n\r\n insertEgressBlock(input: InsertEgressBlockInput): string {\r\n const id = randomUUID()\r\n const timestamp = new Date().toISOString()\r\n\r\n const stmt = this.db.prepare(`\r\n INSERT INTO egress_blocks (id, timestamp, pattern, matched_text, redacted_text, connector, destination, status)\r\n VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')\r\n `)\r\n\r\n stmt.run(\r\n id,\r\n timestamp,\r\n JSON.stringify(input.pattern),\r\n input.matchedText,\r\n input.redactedText,\r\n input.connector ?? null,\r\n input.destination ?? null\r\n )\r\n\r\n return id\r\n }\r\n\r\n getPendingBlocks(): EgressMatch[] {\r\n const stmt = this.db.prepare(`\r\n SELECT * FROM egress_blocks\r\n WHERE status = 'pending'\r\n ORDER BY timestamp DESC\r\n `)\r\n\r\n const rows = stmt.all() as Array<{\r\n id: string\r\n timestamp: string\r\n pattern: string\r\n matched_text: string\r\n redacted_text: string\r\n connector: string | null\r\n destination: string | null\r\n status: 'pending' | 'approved' | 'denied'\r\n approved_by: string | null\r\n approved_at: string | null\r\n }>\r\n\r\n return rows.map(row => this.rowToEgressMatch(row))\r\n }\r\n\r\n getBlock(id: string): EgressMatch | null {\r\n const stmt = this.db.prepare(`\r\n SELECT * FROM egress_blocks WHERE id = ?\r\n `)\r\n\r\n const row = stmt.get(id) as {\r\n id: string\r\n timestamp: string\r\n pattern: string\r\n matched_text: string\r\n redacted_text: string\r\n connector: string | null\r\n destination: string | null\r\n status: 'pending' | 'approved' | 'denied'\r\n approved_by: string | null\r\n approved_at: string | null\r\n } | undefined\r\n\r\n if (!row) return null\r\n return this.rowToEgressMatch(row)\r\n }\r\n\r\n approveBlock(id: string, approvedBy: string): void {\r\n const stmt = this.db.prepare(`\r\n UPDATE egress_blocks\r\n SET status = 'approved', approved_by = ?, approved_at = ?\r\n WHERE id = ?\r\n `)\r\n\r\n stmt.run(approvedBy, new Date().toISOString(), id)\r\n }\r\n\r\n denyBlock(id: string, deniedBy: string): void {\r\n const stmt = this.db.prepare(`\r\n UPDATE egress_blocks\r\n SET status = 'denied', approved_by = ?, approved_at = ?\r\n WHERE id = ?\r\n `)\r\n\r\n stmt.run(deniedBy, new Date().toISOString(), id)\r\n }\r\n\r\n private rowToEgressMatch(row: {\r\n id: string\r\n timestamp: string\r\n pattern: string\r\n matched_text: string\r\n redacted_text: string\r\n connector: string | null\r\n destination: string | null\r\n status: 'pending' | 'approved' | 'denied'\r\n approved_by: string | null\r\n approved_at: string | null\r\n }): EgressMatch {\r\n return {\r\n id: row.id,\r\n timestamp: new Date(row.timestamp),\r\n pattern: JSON.parse(row.pattern),\r\n matchedText: row.matched_text,\r\n redactedText: row.redacted_text,\r\n connector: row.connector ?? undefined,\r\n destination: row.destination ?? undefined,\r\n status: row.status,\r\n approvedBy: row.approved_by ?? undefined,\r\n approvedAt: row.approved_at ? new Date(row.approved_at) : undefined\r\n }\r\n }\r\n\r\n // ─────────────────────────────────────────────────────────────\r\n // Exposure Scans\r\n // ─────────────────────────────────────────────────────────────\r\n\r\n insertExposureScan(result: ExposureResult): string {\r\n const id = randomUUID()\r\n const timestamp = result.timestamp.toISOString()\r\n\r\n const stmt = this.db.prepare(`\r\n INSERT INTO exposure_scans (id, timestamp, agent, pid, port, bind_address, has_auth, severity, action, message)\r\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\r\n `)\r\n\r\n stmt.run(\r\n id,\r\n timestamp,\r\n result.agent,\r\n result.pid ?? null,\r\n result.port,\r\n result.bindAddress,\r\n String(result.hasAuth),\r\n result.severity,\r\n result.action,\r\n result.message\r\n )\r\n\r\n return id\r\n }\r\n\r\n getRecentExposures(limit: number = 100): ExposureResult[] {\r\n const stmt = this.db.prepare(`\r\n SELECT * FROM exposure_scans\r\n ORDER BY timestamp DESC\r\n LIMIT ?\r\n `)\r\n\r\n const rows = stmt.all(limit) as Array<{\r\n id: string\r\n timestamp: string\r\n agent: string\r\n pid: number | null\r\n port: number\r\n bind_address: string\r\n has_auth: string\r\n severity: string\r\n action: string\r\n message: string\r\n }>\r\n\r\n return rows.map(row => ({\r\n agent: row.agent,\r\n pid: row.pid ?? undefined,\r\n port: row.port,\r\n bindAddress: row.bind_address,\r\n hasAuth: row.has_auth === 'true' ? true : row.has_auth === 'false' ? false : 'unknown' as const,\r\n severity: row.severity as ExposureResult['severity'],\r\n action: row.action as ExposureResult['action'],\r\n message: row.message,\r\n timestamp: new Date(row.timestamp)\r\n }))\r\n }\r\n\r\n // ─────────────────────────────────────────────────────────────\r\n // Stats\r\n // ─────────────────────────────────────────────────────────────\r\n\r\n getStats(): DashboardStats {\r\n // Total events\r\n const totalEventsRow = this.db.prepare('SELECT COUNT(*) as count FROM events').get() as { count: number }\r\n\r\n // Events by source\r\n const sourceRows = this.db.prepare(`\r\n SELECT source, COUNT(*) as count FROM events GROUP BY source\r\n `).all() as Array<{ source: string; count: number }>\r\n\r\n const eventsBySource: Record<string, number> = {}\r\n for (const row of sourceRows) {\r\n eventsBySource[row.source] = row.count\r\n }\r\n\r\n // Events by level\r\n const levelRows = this.db.prepare(`\r\n SELECT level, COUNT(*) as count FROM events GROUP BY level\r\n `).all() as Array<{ level: string; count: number }>\r\n\r\n const eventsByLevel: Record<string, number> = {}\r\n for (const row of levelRows) {\r\n eventsByLevel[row.level] = row.count\r\n }\r\n\r\n // Pending blocks\r\n const pendingBlocksRow = this.db.prepare(`\r\n SELECT COUNT(*) as count FROM egress_blocks WHERE status = 'pending'\r\n `).get() as { count: number }\r\n\r\n // Unique connectors\r\n const connectorCountRow = this.db.prepare(`\r\n SELECT COUNT(DISTINCT connector) as count FROM connector_events\r\n `).get() as { count: number }\r\n\r\n // Recent exposures (last 24 hours)\r\n const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString()\r\n const recentExposuresRow = this.db.prepare(`\r\n SELECT COUNT(*) as count FROM exposure_scans WHERE timestamp >= ?\r\n `).get(oneDayAgo) as { count: number }\r\n\r\n return {\r\n totalEvents: totalEventsRow.count,\r\n eventsBySource,\r\n eventsByLevel,\r\n pendingBlocks: pendingBlocksRow.count,\r\n connectorCount: connectorCountRow.count,\r\n recentExposures: recentExposuresRow.count\r\n }\r\n }\r\n\r\n // ─────────────────────────────────────────────────────────────\r\n // Maintenance\r\n // ─────────────────────────────────────────────────────────────\r\n\r\n cleanup(olderThanDays: number = 30): number {\r\n const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000).toISOString()\r\n\r\n const eventsDeleted = this.db.prepare('DELETE FROM events WHERE timestamp < ?').run(cutoff).changes\r\n const connectorDeleted = this.db.prepare('DELETE FROM connector_events WHERE timestamp < ?').run(cutoff).changes\r\n const blocksDeleted = this.db.prepare(`\r\n DELETE FROM egress_blocks WHERE timestamp < ? AND status != 'pending'\r\n `).run(cutoff).changes\r\n const exposuresDeleted = this.db.prepare('DELETE FROM exposure_scans WHERE timestamp < ?').run(cutoff).changes\r\n\r\n return eventsDeleted + connectorDeleted + blocksDeleted + exposuresDeleted\r\n }\r\n\r\n close(): void {\r\n this.db.close()\r\n }\r\n}\r\n\r\nexport default DashboardDB\r\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/policy/risk-scorer.ts
|
|
4
|
+
var INFO_GATHERING = [
|
|
5
|
+
{ pattern: /\bwhoami\b/, score: 2, factor: "User identification" },
|
|
6
|
+
{ pattern: /\bid\b/, score: 2, factor: "User/group info" },
|
|
7
|
+
{ pattern: /\buname\b/, score: 2, factor: "System info" },
|
|
8
|
+
{ pattern: /\bhostname\b/, score: 1, factor: "Hostname query" },
|
|
9
|
+
{ pattern: /cat\s+\/etc\/passwd/, score: 5, factor: "Password file access" },
|
|
10
|
+
{ pattern: /cat\s+\/etc\/shadow/, score: 9, factor: "Shadow file access" },
|
|
11
|
+
{ pattern: /cat\s+\/etc\/hosts/, score: 3, factor: "Hosts file access" },
|
|
12
|
+
{ pattern: /\bps\s+(aux|ef)/, score: 3, factor: "Process listing" },
|
|
13
|
+
{ pattern: /\bnetstat\b/, score: 4, factor: "Network connections" },
|
|
14
|
+
{ pattern: /\bss\s+-/, score: 4, factor: "Socket statistics" },
|
|
15
|
+
{ pattern: /\blsof\b/, score: 4, factor: "Open files listing" },
|
|
16
|
+
{ pattern: /\bfind\s+.*-perm/, score: 5, factor: "Permission scanning" },
|
|
17
|
+
{ pattern: /\bfind\s+.*-user\s+root/, score: 5, factor: "Root file scanning" }
|
|
18
|
+
];
|
|
19
|
+
var PERSISTENCE = [
|
|
20
|
+
{ pattern: /\bcrontab\b/, score: 7, factor: "Cron job modification" },
|
|
21
|
+
{ pattern: /\/etc\/cron/, score: 7, factor: "System cron access" },
|
|
22
|
+
{ pattern: /\bsystemctl\s+(enable|disable)/, score: 7, factor: "Service persistence" },
|
|
23
|
+
{ pattern: /\bchkconfig\b/, score: 6, factor: "Service configuration" },
|
|
24
|
+
{ pattern: /\.bashrc|\.bash_profile|\.profile/, score: 6, factor: "Shell profile modification" },
|
|
25
|
+
{ pattern: /\/etc\/rc\.local/, score: 8, factor: "Startup script" },
|
|
26
|
+
{ pattern: /\.ssh\/authorized_keys/, score: 8, factor: "SSH key injection" }
|
|
27
|
+
];
|
|
28
|
+
var DATA_EXFIL = [
|
|
29
|
+
{ pattern: /curl.*\|\s*(bash|sh)/, score: 10, factor: "Remote code execution" },
|
|
30
|
+
{ pattern: /wget.*\|\s*(bash|sh)/, score: 10, factor: "Remote code execution" },
|
|
31
|
+
{ pattern: /\bnc\s+-[elp]/, score: 9, factor: "Netcat listener/connection" },
|
|
32
|
+
{ pattern: /\b(ncat|netcat|socat)\s/, score: 9, factor: "Network tool" },
|
|
33
|
+
{ pattern: /\bscp\s+.*@/, score: 6, factor: "Remote file copy" },
|
|
34
|
+
{ pattern: /\brsync\s+.*@/, score: 6, factor: "Remote sync" },
|
|
35
|
+
{ pattern: /curl.*-d.*\$/, score: 7, factor: "Data exfiltration via curl" },
|
|
36
|
+
{ pattern: /base64.*\|.*curl/, score: 8, factor: "Encoded data exfiltration" },
|
|
37
|
+
{ pattern: /python.*-m\s+http\.server/, score: 8, factor: "HTTP server exposure" },
|
|
38
|
+
{ pattern: /\baws\s+s3\s+(cp|sync|mv)/, score: 7, factor: "Cloud data transfer" },
|
|
39
|
+
{ pattern: /\bgsutil\s+(cp|rsync|mv)/, score: 7, factor: "Cloud data transfer" },
|
|
40
|
+
{ pattern: /\baz\s+storage\s+blob/, score: 7, factor: "Cloud data transfer" }
|
|
41
|
+
];
|
|
42
|
+
var DESTRUCTIVE = [
|
|
43
|
+
{ pattern: /\brm\s+-rf\s+\//, score: 10, factor: "Root filesystem deletion" },
|
|
44
|
+
{ pattern: /\brm\s+-rf\s+~/, score: 9, factor: "Home directory deletion" },
|
|
45
|
+
{ pattern: /\brm\s+-rf\s+\*/, score: 8, factor: "Wildcard deletion" },
|
|
46
|
+
{ pattern: /\bmkfs\b/, score: 10, factor: "Filesystem format" },
|
|
47
|
+
{ pattern: /\bdd\s+.*of=\/dev\//, score: 10, factor: "Direct disk write" },
|
|
48
|
+
{ pattern: />\s*\/dev\/sda/, score: 10, factor: "Disk overwrite" },
|
|
49
|
+
{ pattern: /:.*\(\).*\{.*:.*\|.*:.*&.*\}/, score: 10, factor: "Fork bomb" },
|
|
50
|
+
{ pattern: /\bshred\b/, score: 8, factor: "Secure file deletion" }
|
|
51
|
+
];
|
|
52
|
+
var PRIVILEGE_ESCALATION = [
|
|
53
|
+
{ pattern: /\bsudo\s+-i/, score: 7, factor: "Interactive root shell" },
|
|
54
|
+
{ pattern: /\bsudo\s+su\b/, score: 7, factor: "Switch to root" },
|
|
55
|
+
{ pattern: /\bchmod\s+[47]777/, score: 8, factor: "World-writable permissions" },
|
|
56
|
+
{ pattern: /\bchmod\s+u\+s/, score: 9, factor: "SUID bit setting" },
|
|
57
|
+
{ pattern: /\bchown\s+root/, score: 7, factor: "Change to root ownership" },
|
|
58
|
+
{ pattern: /\/etc\/sudoers/, score: 9, factor: "Sudoers modification" },
|
|
59
|
+
{ pattern: /\bpasswd\s+root/, score: 9, factor: "Root password change" }
|
|
60
|
+
];
|
|
61
|
+
var EVASION = [
|
|
62
|
+
{ pattern: /\bhistory\s+-[cd]/, score: 6, factor: "History manipulation" },
|
|
63
|
+
{ pattern: /unset\s+HISTFILE/, score: 7, factor: "Disable history logging" },
|
|
64
|
+
{ pattern: /export\s+HISTSIZE=0/, score: 7, factor: "Disable history" },
|
|
65
|
+
{ pattern: /\brm\s+.*\.bash_history/, score: 7, factor: "History deletion" },
|
|
66
|
+
{ pattern: /\b\/dev\/null.*2>&1/, score: 3, factor: "Output suppression" },
|
|
67
|
+
{ pattern: /base64\s+-d/, score: 5, factor: "Base64 decoding" },
|
|
68
|
+
{ pattern: /\beval\s+/, score: 6, factor: "Dynamic code execution" },
|
|
69
|
+
{ pattern: /\$'\\x[0-9a-f]/, score: 7, factor: "ANSI-C quoting bypass" },
|
|
70
|
+
{ pattern: /python.*-c\s+['"]/, score: 5, factor: "Python code execution" },
|
|
71
|
+
{ pattern: /perl.*-e\s+['"]/, score: 5, factor: "Perl code execution" },
|
|
72
|
+
{ pattern: /ruby.*-e\s+['"]/, score: 5, factor: "Ruby code execution" },
|
|
73
|
+
{ pattern: /node.*-e\s+['"]/, score: 5, factor: "Node code execution" }
|
|
74
|
+
];
|
|
75
|
+
var CONTAINER_ESCAPE = [
|
|
76
|
+
{ pattern: /docker\s+run.*-v\s+\/[^\/]*:/, score: 8, factor: "Docker root mount" },
|
|
77
|
+
{ pattern: /docker\s+run.*--privileged/, score: 9, factor: "Privileged container" },
|
|
78
|
+
{ pattern: /docker\s+exec.*-it/, score: 5, factor: "Container shell access" },
|
|
79
|
+
{ pattern: /kubectl\s+exec/, score: 6, factor: "Kubernetes exec" },
|
|
80
|
+
{ pattern: /kubectl\s+cp/, score: 6, factor: "Kubernetes file copy" },
|
|
81
|
+
{ pattern: /docker\s+cp.*:\//, score: 6, factor: "Docker file extraction" }
|
|
82
|
+
];
|
|
83
|
+
var FILE_READERS = [
|
|
84
|
+
{ pattern: /\b(head|tail)\s+.*\.(env|pem|key|secret)/i, score: 6, factor: "Sensitive file read" },
|
|
85
|
+
{ pattern: /\b(less|more)\s+.*\.(env|pem|key|secret)/i, score: 6, factor: "Sensitive file read" },
|
|
86
|
+
{ pattern: /\b(strings|xxd|od)\s+/, score: 4, factor: "Binary file inspection" },
|
|
87
|
+
{ pattern: /\bfind\s+.*-exec\s+cat/, score: 7, factor: "Find with cat exec" },
|
|
88
|
+
{ pattern: /\bfind\s+.*\|\s*xargs\s+(cat|head|tail)/, score: 7, factor: "Find piped to reader" },
|
|
89
|
+
{ pattern: /\bxargs\s+(cat|head|tail|less)/, score: 6, factor: "Xargs file read" },
|
|
90
|
+
{ pattern: /\bawk\s+.*\.(env|pem|key)/i, score: 6, factor: "Awk sensitive file" },
|
|
91
|
+
{ pattern: /\bsed\s+.*\.(env|pem|key)/i, score: 5, factor: "Sed sensitive file" }
|
|
92
|
+
];
|
|
93
|
+
var SAFE_COMMANDS = [
|
|
94
|
+
{ pattern: /^ls(\s+-[la]+)?$/, score: 1, factor: "Directory listing" },
|
|
95
|
+
{ pattern: /^pwd$/, score: 1, factor: "Print directory" },
|
|
96
|
+
{ pattern: /^cd\s+/, score: 1, factor: "Change directory" },
|
|
97
|
+
{ pattern: /^echo\s+/, score: 1, factor: "Echo output" },
|
|
98
|
+
{ pattern: /^cat\s+[^\/]/, score: 2, factor: "File read (relative)" },
|
|
99
|
+
{ pattern: /^git\s+(status|log|diff|branch)/, score: 1, factor: "Git read operation" },
|
|
100
|
+
{ pattern: /^git\s+(add|commit|push|pull)/, score: 2, factor: "Git write operation" },
|
|
101
|
+
{ pattern: /^npm\s+(list|show|view)/, score: 1, factor: "NPM read operation" },
|
|
102
|
+
{ pattern: /^npm\s+(install|run|test)/, score: 2, factor: "NPM write operation" },
|
|
103
|
+
{ pattern: /^node\s+--version/, score: 1, factor: "Version check" },
|
|
104
|
+
{ pattern: /^python\s+--version/, score: 1, factor: "Version check" }
|
|
105
|
+
];
|
|
106
|
+
var ALL_PATTERNS = [
|
|
107
|
+
...SAFE_COMMANDS,
|
|
108
|
+
...INFO_GATHERING,
|
|
109
|
+
...PERSISTENCE,
|
|
110
|
+
...DATA_EXFIL,
|
|
111
|
+
...DESTRUCTIVE,
|
|
112
|
+
...PRIVILEGE_ESCALATION,
|
|
113
|
+
...EVASION,
|
|
114
|
+
...CONTAINER_ESCAPE,
|
|
115
|
+
...FILE_READERS
|
|
116
|
+
];
|
|
117
|
+
var RiskScorer = class {
|
|
118
|
+
customPatterns = [];
|
|
119
|
+
/**
|
|
120
|
+
* Score a command's risk level
|
|
121
|
+
*/
|
|
122
|
+
score(command) {
|
|
123
|
+
const factors = [];
|
|
124
|
+
let maxScore = 1;
|
|
125
|
+
const allPatterns = [...ALL_PATTERNS, ...this.customPatterns];
|
|
126
|
+
for (const { pattern, score, factor } of allPatterns) {
|
|
127
|
+
if (pattern.test(command)) {
|
|
128
|
+
factors.push(factor);
|
|
129
|
+
if (score > maxScore) {
|
|
130
|
+
maxScore = score;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const heuristicScore = this.applyHeuristics(command, factors);
|
|
135
|
+
if (heuristicScore > maxScore) {
|
|
136
|
+
maxScore = heuristicScore;
|
|
137
|
+
}
|
|
138
|
+
const level = this.scoreToLevel(maxScore);
|
|
139
|
+
return {
|
|
140
|
+
score: maxScore,
|
|
141
|
+
level,
|
|
142
|
+
factors: factors.length > 0 ? factors : ["Standard command"]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
applyHeuristics(command, factors) {
|
|
146
|
+
let score = 0;
|
|
147
|
+
if (command.length > 200) {
|
|
148
|
+
factors.push("Unusually long command");
|
|
149
|
+
score = Math.max(score, 4);
|
|
150
|
+
}
|
|
151
|
+
const pipeCount = (command.match(/\|/g) || []).length;
|
|
152
|
+
if (pipeCount > 3) {
|
|
153
|
+
factors.push(`Complex pipeline (${pipeCount} pipes)`);
|
|
154
|
+
score = Math.max(score, 5);
|
|
155
|
+
}
|
|
156
|
+
if (/nohup.*&/.test(command)) {
|
|
157
|
+
factors.push("Background persistent process");
|
|
158
|
+
score = Math.max(score, 6);
|
|
159
|
+
}
|
|
160
|
+
if (/[A-Za-z0-9+/=]{50,}/.test(command)) {
|
|
161
|
+
factors.push("Possible encoded payload");
|
|
162
|
+
score = Math.max(score, 6);
|
|
163
|
+
}
|
|
164
|
+
if (/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/.test(command)) {
|
|
165
|
+
factors.push("Contains IP address");
|
|
166
|
+
score = Math.max(score, 4);
|
|
167
|
+
}
|
|
168
|
+
if (/\\x[0-9a-fA-F]{2}/.test(command)) {
|
|
169
|
+
factors.push("Hex-encoded characters");
|
|
170
|
+
score = Math.max(score, 5);
|
|
171
|
+
}
|
|
172
|
+
return score;
|
|
173
|
+
}
|
|
174
|
+
scoreToLevel(score) {
|
|
175
|
+
if (score <= 2) return "safe";
|
|
176
|
+
if (score <= 5) return "caution";
|
|
177
|
+
if (score <= 8) return "dangerous";
|
|
178
|
+
return "critical";
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Add custom risk patterns
|
|
182
|
+
*/
|
|
183
|
+
addPattern(pattern, score, factor) {
|
|
184
|
+
this.customPatterns.push({ pattern, score, factor });
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Check if command should be blocked based on score threshold
|
|
188
|
+
*/
|
|
189
|
+
shouldBlock(command, threshold = 8) {
|
|
190
|
+
return this.score(command).score >= threshold;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Get color for terminal display
|
|
194
|
+
*/
|
|
195
|
+
static levelColor(level) {
|
|
196
|
+
switch (level) {
|
|
197
|
+
case "safe":
|
|
198
|
+
return "green";
|
|
199
|
+
case "caution":
|
|
200
|
+
return "yellow";
|
|
201
|
+
case "dangerous":
|
|
202
|
+
return "red";
|
|
203
|
+
case "critical":
|
|
204
|
+
return "bgRed";
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export {
|
|
210
|
+
RiskScorer
|
|
211
|
+
};
|
|
212
|
+
//# sourceMappingURL=chunk-DEAF6PYM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/policy/risk-scorer.ts"],"sourcesContent":["/**\r\n * Command Risk Scoring\r\n * Scores commands 1-10 based on potential danger\r\n */\r\n\r\nexport interface RiskScore {\r\n score: number\r\n level: 'safe' | 'caution' | 'dangerous' | 'critical'\r\n factors: string[]\r\n}\r\n\r\ninterface RiskPattern {\r\n pattern: RegExp\r\n score: number\r\n factor: string\r\n}\r\n\r\n// Risk patterns organized by category\r\nconst INFO_GATHERING: RiskPattern[] = [\r\n { pattern: /\\bwhoami\\b/, score: 2, factor: 'User identification' },\r\n { pattern: /\\bid\\b/, score: 2, factor: 'User/group info' },\r\n { pattern: /\\buname\\b/, score: 2, factor: 'System info' },\r\n { pattern: /\\bhostname\\b/, score: 1, factor: 'Hostname query' },\r\n { pattern: /cat\\s+\\/etc\\/passwd/, score: 5, factor: 'Password file access' },\r\n { pattern: /cat\\s+\\/etc\\/shadow/, score: 9, factor: 'Shadow file access' },\r\n { pattern: /cat\\s+\\/etc\\/hosts/, score: 3, factor: 'Hosts file access' },\r\n { pattern: /\\bps\\s+(aux|ef)/, score: 3, factor: 'Process listing' },\r\n { pattern: /\\bnetstat\\b/, score: 4, factor: 'Network connections' },\r\n { pattern: /\\bss\\s+-/, score: 4, factor: 'Socket statistics' },\r\n { pattern: /\\blsof\\b/, score: 4, factor: 'Open files listing' },\r\n { pattern: /\\bfind\\s+.*-perm/, score: 5, factor: 'Permission scanning' },\r\n { pattern: /\\bfind\\s+.*-user\\s+root/, score: 5, factor: 'Root file scanning' },\r\n]\r\n\r\nconst PERSISTENCE: RiskPattern[] = [\r\n { pattern: /\\bcrontab\\b/, score: 7, factor: 'Cron job modification' },\r\n { pattern: /\\/etc\\/cron/, score: 7, factor: 'System cron access' },\r\n { pattern: /\\bsystemctl\\s+(enable|disable)/, score: 7, factor: 'Service persistence' },\r\n { pattern: /\\bchkconfig\\b/, score: 6, factor: 'Service configuration' },\r\n { pattern: /\\.bashrc|\\.bash_profile|\\.profile/, score: 6, factor: 'Shell profile modification' },\r\n { pattern: /\\/etc\\/rc\\.local/, score: 8, factor: 'Startup script' },\r\n { pattern: /\\.ssh\\/authorized_keys/, score: 8, factor: 'SSH key injection' },\r\n]\r\n\r\nconst DATA_EXFIL: RiskPattern[] = [\r\n { pattern: /curl.*\\|\\s*(bash|sh)/, score: 10, factor: 'Remote code execution' },\r\n { pattern: /wget.*\\|\\s*(bash|sh)/, score: 10, factor: 'Remote code execution' },\r\n { pattern: /\\bnc\\s+-[elp]/, score: 9, factor: 'Netcat listener/connection' },\r\n { pattern: /\\b(ncat|netcat|socat)\\s/, score: 9, factor: 'Network tool' },\r\n { pattern: /\\bscp\\s+.*@/, score: 6, factor: 'Remote file copy' },\r\n { pattern: /\\brsync\\s+.*@/, score: 6, factor: 'Remote sync' },\r\n { pattern: /curl.*-d.*\\$/, score: 7, factor: 'Data exfiltration via curl' },\r\n { pattern: /base64.*\\|.*curl/, score: 8, factor: 'Encoded data exfiltration' },\r\n { pattern: /python.*-m\\s+http\\.server/, score: 8, factor: 'HTTP server exposure' },\r\n { pattern: /\\baws\\s+s3\\s+(cp|sync|mv)/, score: 7, factor: 'Cloud data transfer' },\r\n { pattern: /\\bgsutil\\s+(cp|rsync|mv)/, score: 7, factor: 'Cloud data transfer' },\r\n { pattern: /\\baz\\s+storage\\s+blob/, score: 7, factor: 'Cloud data transfer' },\r\n]\r\n\r\nconst DESTRUCTIVE: RiskPattern[] = [\r\n { pattern: /\\brm\\s+-rf\\s+\\//, score: 10, factor: 'Root filesystem deletion' },\r\n { pattern: /\\brm\\s+-rf\\s+~/, score: 9, factor: 'Home directory deletion' },\r\n { pattern: /\\brm\\s+-rf\\s+\\*/, score: 8, factor: 'Wildcard deletion' },\r\n { pattern: /\\bmkfs\\b/, score: 10, factor: 'Filesystem format' },\r\n { pattern: /\\bdd\\s+.*of=\\/dev\\//, score: 10, factor: 'Direct disk write' },\r\n { pattern: />\\s*\\/dev\\/sda/, score: 10, factor: 'Disk overwrite' },\r\n { pattern: /:.*\\(\\).*\\{.*:.*\\|.*:.*&.*\\}/, score: 10, factor: 'Fork bomb' },\r\n { pattern: /\\bshred\\b/, score: 8, factor: 'Secure file deletion' },\r\n]\r\n\r\nconst PRIVILEGE_ESCALATION: RiskPattern[] = [\r\n { pattern: /\\bsudo\\s+-i/, score: 7, factor: 'Interactive root shell' },\r\n { pattern: /\\bsudo\\s+su\\b/, score: 7, factor: 'Switch to root' },\r\n { pattern: /\\bchmod\\s+[47]777/, score: 8, factor: 'World-writable permissions' },\r\n { pattern: /\\bchmod\\s+u\\+s/, score: 9, factor: 'SUID bit setting' },\r\n { pattern: /\\bchown\\s+root/, score: 7, factor: 'Change to root ownership' },\r\n { pattern: /\\/etc\\/sudoers/, score: 9, factor: 'Sudoers modification' },\r\n { pattern: /\\bpasswd\\s+root/, score: 9, factor: 'Root password change' },\r\n]\r\n\r\nconst EVASION: RiskPattern[] = [\r\n { pattern: /\\bhistory\\s+-[cd]/, score: 6, factor: 'History manipulation' },\r\n { pattern: /unset\\s+HISTFILE/, score: 7, factor: 'Disable history logging' },\r\n { pattern: /export\\s+HISTSIZE=0/, score: 7, factor: 'Disable history' },\r\n { pattern: /\\brm\\s+.*\\.bash_history/, score: 7, factor: 'History deletion' },\r\n { pattern: /\\b\\/dev\\/null.*2>&1/, score: 3, factor: 'Output suppression' },\r\n { pattern: /base64\\s+-d/, score: 5, factor: 'Base64 decoding' },\r\n { pattern: /\\beval\\s+/, score: 6, factor: 'Dynamic code execution' },\r\n { pattern: /\\$'\\\\x[0-9a-f]/, score: 7, factor: 'ANSI-C quoting bypass' },\r\n { pattern: /python.*-c\\s+['\"]/, score: 5, factor: 'Python code execution' },\r\n { pattern: /perl.*-e\\s+['\"]/, score: 5, factor: 'Perl code execution' },\r\n { pattern: /ruby.*-e\\s+['\"]/, score: 5, factor: 'Ruby code execution' },\r\n { pattern: /node.*-e\\s+['\"]/, score: 5, factor: 'Node code execution' },\r\n]\r\n\r\nconst CONTAINER_ESCAPE: RiskPattern[] = [\r\n { pattern: /docker\\s+run.*-v\\s+\\/[^\\/]*:/, score: 8, factor: 'Docker root mount' },\r\n { pattern: /docker\\s+run.*--privileged/, score: 9, factor: 'Privileged container' },\r\n { pattern: /docker\\s+exec.*-it/, score: 5, factor: 'Container shell access' },\r\n { pattern: /kubectl\\s+exec/, score: 6, factor: 'Kubernetes exec' },\r\n { pattern: /kubectl\\s+cp/, score: 6, factor: 'Kubernetes file copy' },\r\n { pattern: /docker\\s+cp.*:\\//, score: 6, factor: 'Docker file extraction' },\r\n]\r\n\r\nconst FILE_READERS: RiskPattern[] = [\r\n { pattern: /\\b(head|tail)\\s+.*\\.(env|pem|key|secret)/i, score: 6, factor: 'Sensitive file read' },\r\n { pattern: /\\b(less|more)\\s+.*\\.(env|pem|key|secret)/i, score: 6, factor: 'Sensitive file read' },\r\n { pattern: /\\b(strings|xxd|od)\\s+/, score: 4, factor: 'Binary file inspection' },\r\n { pattern: /\\bfind\\s+.*-exec\\s+cat/, score: 7, factor: 'Find with cat exec' },\r\n { pattern: /\\bfind\\s+.*\\|\\s*xargs\\s+(cat|head|tail)/, score: 7, factor: 'Find piped to reader' },\r\n { pattern: /\\bxargs\\s+(cat|head|tail|less)/, score: 6, factor: 'Xargs file read' },\r\n { pattern: /\\bawk\\s+.*\\.(env|pem|key)/i, score: 6, factor: 'Awk sensitive file' },\r\n { pattern: /\\bsed\\s+.*\\.(env|pem|key)/i, score: 5, factor: 'Sed sensitive file' },\r\n]\r\n\r\nconst SAFE_COMMANDS: RiskPattern[] = [\r\n { pattern: /^ls(\\s+-[la]+)?$/, score: 1, factor: 'Directory listing' },\r\n { pattern: /^pwd$/, score: 1, factor: 'Print directory' },\r\n { pattern: /^cd\\s+/, score: 1, factor: 'Change directory' },\r\n { pattern: /^echo\\s+/, score: 1, factor: 'Echo output' },\r\n { pattern: /^cat\\s+[^\\/]/, score: 2, factor: 'File read (relative)' },\r\n { pattern: /^git\\s+(status|log|diff|branch)/, score: 1, factor: 'Git read operation' },\r\n { pattern: /^git\\s+(add|commit|push|pull)/, score: 2, factor: 'Git write operation' },\r\n { pattern: /^npm\\s+(list|show|view)/, score: 1, factor: 'NPM read operation' },\r\n { pattern: /^npm\\s+(install|run|test)/, score: 2, factor: 'NPM write operation' },\r\n { pattern: /^node\\s+--version/, score: 1, factor: 'Version check' },\r\n { pattern: /^python\\s+--version/, score: 1, factor: 'Version check' },\r\n]\r\n\r\nconst ALL_PATTERNS: RiskPattern[] = [\r\n ...SAFE_COMMANDS,\r\n ...INFO_GATHERING,\r\n ...PERSISTENCE,\r\n ...DATA_EXFIL,\r\n ...DESTRUCTIVE,\r\n ...PRIVILEGE_ESCALATION,\r\n ...EVASION,\r\n ...CONTAINER_ESCAPE,\r\n ...FILE_READERS,\r\n]\r\n\r\nexport class RiskScorer {\r\n private customPatterns: RiskPattern[] = []\r\n\r\n /**\r\n * Score a command's risk level\r\n */\r\n score(command: string): RiskScore {\r\n const factors: string[] = []\r\n let maxScore = 1\r\n\r\n // Check all patterns\r\n const allPatterns = [...ALL_PATTERNS, ...this.customPatterns]\r\n\r\n for (const { pattern, score, factor } of allPatterns) {\r\n if (pattern.test(command)) {\r\n factors.push(factor)\r\n if (score > maxScore) {\r\n maxScore = score\r\n }\r\n }\r\n }\r\n\r\n // Additional heuristics\r\n const heuristicScore = this.applyHeuristics(command, factors)\r\n if (heuristicScore > maxScore) {\r\n maxScore = heuristicScore\r\n }\r\n\r\n // Determine level\r\n const level = this.scoreToLevel(maxScore)\r\n\r\n return {\r\n score: maxScore,\r\n level,\r\n factors: factors.length > 0 ? factors : ['Standard command']\r\n }\r\n }\r\n\r\n private applyHeuristics(command: string, factors: string[]): number {\r\n let score = 0\r\n\r\n // Long commands are suspicious\r\n if (command.length > 200) {\r\n factors.push('Unusually long command')\r\n score = Math.max(score, 4)\r\n }\r\n\r\n // Multiple pipes\r\n const pipeCount = (command.match(/\\|/g) || []).length\r\n if (pipeCount > 3) {\r\n factors.push(`Complex pipeline (${pipeCount} pipes)`)\r\n score = Math.max(score, 5)\r\n }\r\n\r\n // Background execution with nohup\r\n if (/nohup.*&/.test(command)) {\r\n factors.push('Background persistent process')\r\n score = Math.max(score, 6)\r\n }\r\n\r\n // Encoded content\r\n if (/[A-Za-z0-9+/=]{50,}/.test(command)) {\r\n factors.push('Possible encoded payload')\r\n score = Math.max(score, 6)\r\n }\r\n\r\n // IP addresses (potential C2)\r\n if (/\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b/.test(command)) {\r\n factors.push('Contains IP address')\r\n score = Math.max(score, 4)\r\n }\r\n\r\n // Hex escapes\r\n if (/\\\\x[0-9a-fA-F]{2}/.test(command)) {\r\n factors.push('Hex-encoded characters')\r\n score = Math.max(score, 5)\r\n }\r\n\r\n return score\r\n }\r\n\r\n private scoreToLevel(score: number): RiskScore['level'] {\r\n if (score <= 2) return 'safe'\r\n if (score <= 5) return 'caution'\r\n if (score <= 8) return 'dangerous'\r\n return 'critical'\r\n }\r\n\r\n /**\r\n * Add custom risk patterns\r\n */\r\n addPattern(pattern: RegExp, score: number, factor: string): void {\r\n this.customPatterns.push({ pattern, score, factor })\r\n }\r\n\r\n /**\r\n * Check if command should be blocked based on score threshold\r\n */\r\n shouldBlock(command: string, threshold: number = 8): boolean {\r\n return this.score(command).score >= threshold\r\n }\r\n\r\n /**\r\n * Get color for terminal display\r\n */\r\n static levelColor(level: RiskScore['level']): string {\r\n switch (level) {\r\n case 'safe': return 'green'\r\n case 'caution': return 'yellow'\r\n case 'dangerous': return 'red'\r\n case 'critical': return 'bgRed'\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/bro/ollama.ts
|
|
4
|
+
var DEFAULT_CONFIG = {
|
|
5
|
+
host: "http://localhost:11434",
|
|
6
|
+
model: "qwen2.5-coder:7b",
|
|
7
|
+
timeout: 3e4
|
|
8
|
+
};
|
|
9
|
+
var OllamaClient = class {
|
|
10
|
+
config;
|
|
11
|
+
constructor(config = {}) {
|
|
12
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check if Ollama is running and accessible
|
|
16
|
+
*/
|
|
17
|
+
async isAvailable() {
|
|
18
|
+
try {
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
21
|
+
const response = await fetch(`${this.config.host}/api/tags`, {
|
|
22
|
+
signal: controller.signal
|
|
23
|
+
});
|
|
24
|
+
clearTimeout(timeout);
|
|
25
|
+
return response.ok;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* List available models
|
|
32
|
+
*/
|
|
33
|
+
async listModels() {
|
|
34
|
+
try {
|
|
35
|
+
const response = await fetch(`${this.config.host}/api/tags`);
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const data = await response.json();
|
|
40
|
+
return data.models?.map((m) => m.name) || [];
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Generate a response from the model
|
|
47
|
+
*/
|
|
48
|
+
async generate(prompt, systemPrompt) {
|
|
49
|
+
const controller = new AbortController();
|
|
50
|
+
const timeout = setTimeout(() => controller.abort(), this.config.timeout);
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(`${this.config.host}/api/generate`, {
|
|
53
|
+
method: "POST",
|
|
54
|
+
headers: { "Content-Type": "application/json" },
|
|
55
|
+
body: JSON.stringify({
|
|
56
|
+
model: this.config.model,
|
|
57
|
+
prompt,
|
|
58
|
+
system: systemPrompt,
|
|
59
|
+
stream: false
|
|
60
|
+
}),
|
|
61
|
+
signal: controller.signal
|
|
62
|
+
});
|
|
63
|
+
clearTimeout(timeout);
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`Ollama error: ${response.status}`);
|
|
66
|
+
}
|
|
67
|
+
const data = await response.json();
|
|
68
|
+
return data.response;
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error.name === "AbortError") {
|
|
71
|
+
throw new Error("Ollama request timed out");
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Chat with the model (multi-turn conversation)
|
|
78
|
+
*/
|
|
79
|
+
async chat(messages) {
|
|
80
|
+
const controller = new AbortController();
|
|
81
|
+
const timeout = setTimeout(() => controller.abort(), this.config.timeout);
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(`${this.config.host}/api/chat`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
headers: { "Content-Type": "application/json" },
|
|
86
|
+
body: JSON.stringify({
|
|
87
|
+
model: this.config.model,
|
|
88
|
+
messages,
|
|
89
|
+
stream: false
|
|
90
|
+
}),
|
|
91
|
+
signal: controller.signal
|
|
92
|
+
});
|
|
93
|
+
clearTimeout(timeout);
|
|
94
|
+
if (!response.ok) {
|
|
95
|
+
throw new Error(`Ollama error: ${response.status}`);
|
|
96
|
+
}
|
|
97
|
+
const data = await response.json();
|
|
98
|
+
return data.message?.content || "";
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (error.name === "AbortError") {
|
|
101
|
+
throw new Error("Ollama request timed out");
|
|
102
|
+
}
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Ask Bash Bro to suggest a command
|
|
108
|
+
*/
|
|
109
|
+
async suggestCommand(context) {
|
|
110
|
+
const systemPrompt = `You are Bash Bro, a helpful command-line assistant.
|
|
111
|
+
Given the context, suggest the most likely next command the user needs.
|
|
112
|
+
Respond with ONLY the command, no explanation. If unsure, respond with "none".`;
|
|
113
|
+
try {
|
|
114
|
+
const response = await this.generate(context, systemPrompt);
|
|
115
|
+
const command = response.trim();
|
|
116
|
+
if (command.toLowerCase() === "none" || command.length > 200) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
return command;
|
|
120
|
+
} catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Ask Bash Bro to explain a command
|
|
126
|
+
*/
|
|
127
|
+
async explainCommand(command) {
|
|
128
|
+
const systemPrompt = `You are Bash Bro, a helpful command-line assistant.
|
|
129
|
+
Explain what the given command does in 1-2 sentences. Be concise and accurate.`;
|
|
130
|
+
try {
|
|
131
|
+
return await this.generate(`Explain: ${command}`, systemPrompt);
|
|
132
|
+
} catch {
|
|
133
|
+
return "Could not explain command.";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Ask Bash Bro to fix a command that failed
|
|
138
|
+
*/
|
|
139
|
+
async fixCommand(command, error) {
|
|
140
|
+
const systemPrompt = `You are Bash Bro, a helpful command-line assistant.
|
|
141
|
+
Given a failed command and its error, suggest a fixed version.
|
|
142
|
+
Respond with ONLY the fixed command, no explanation. If you can't fix it, respond with "none".`;
|
|
143
|
+
try {
|
|
144
|
+
const response = await this.generate(
|
|
145
|
+
`Command: ${command}
|
|
146
|
+
Error: ${error}
|
|
147
|
+
Fixed command:`,
|
|
148
|
+
systemPrompt
|
|
149
|
+
);
|
|
150
|
+
const fixed = response.trim();
|
|
151
|
+
if (fixed.toLowerCase() === "none" || fixed.length > 500) {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
return fixed;
|
|
155
|
+
} catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
setModel(model) {
|
|
160
|
+
this.config.model = model;
|
|
161
|
+
}
|
|
162
|
+
getModel() {
|
|
163
|
+
return this.config.model;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Generate a shell script from a natural language description
|
|
167
|
+
*/
|
|
168
|
+
async generateScript(description, shell = "bash") {
|
|
169
|
+
const systemPrompt = `You are Bash Bro, a helpful command-line assistant.
|
|
170
|
+
Generate a ${shell} script based on the user's description.
|
|
171
|
+
Output ONLY the script, no explanation. Start with the shebang line.
|
|
172
|
+
Keep scripts simple, readable, and well-commented.`;
|
|
173
|
+
try {
|
|
174
|
+
const response = await this.generate(
|
|
175
|
+
`Generate a ${shell} script that: ${description}`,
|
|
176
|
+
systemPrompt
|
|
177
|
+
);
|
|
178
|
+
return response.trim();
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Analyze command safety and provide recommendations
|
|
185
|
+
*/
|
|
186
|
+
async analyzeCommandSafety(command) {
|
|
187
|
+
const systemPrompt = `You are Bash Bro, a security-focused command-line assistant.
|
|
188
|
+
Analyze the given command for security risks.
|
|
189
|
+
Respond with JSON only, in this format:
|
|
190
|
+
{"safe": boolean, "risk": "low|medium|high|critical", "explanation": "...", "suggestions": ["..."]}`;
|
|
191
|
+
try {
|
|
192
|
+
const response = await this.generate(
|
|
193
|
+
`Analyze this command for security risks: ${command}`,
|
|
194
|
+
systemPrompt
|
|
195
|
+
);
|
|
196
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
197
|
+
if (jsonMatch) {
|
|
198
|
+
return JSON.parse(jsonMatch[0]);
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
safe: true,
|
|
202
|
+
risk: "low",
|
|
203
|
+
explanation: "Could not analyze command.",
|
|
204
|
+
suggestions: []
|
|
205
|
+
};
|
|
206
|
+
} catch {
|
|
207
|
+
return {
|
|
208
|
+
safe: true,
|
|
209
|
+
risk: "low",
|
|
210
|
+
explanation: "Analysis unavailable.",
|
|
211
|
+
suggestions: []
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Summarize a series of commands and their outputs
|
|
217
|
+
*/
|
|
218
|
+
async summarizeSession(commands) {
|
|
219
|
+
const systemPrompt = `You are Bash Bro, a helpful command-line assistant.
|
|
220
|
+
Summarize what happened in this terminal session in 2-3 sentences.
|
|
221
|
+
Focus on what was accomplished and any issues encountered.`;
|
|
222
|
+
const sessionText = commands.map((c) => {
|
|
223
|
+
let text = `$ ${c.command}`;
|
|
224
|
+
if (c.output) text += `
|
|
225
|
+
${c.output.slice(0, 500)}`;
|
|
226
|
+
if (c.error) text += `
|
|
227
|
+
Error: ${c.error.slice(0, 200)}`;
|
|
228
|
+
return text;
|
|
229
|
+
}).join("\n\n");
|
|
230
|
+
try {
|
|
231
|
+
return await this.generate(sessionText, systemPrompt);
|
|
232
|
+
} catch {
|
|
233
|
+
return "Could not summarize session.";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Get help for a specific tool or command
|
|
238
|
+
*/
|
|
239
|
+
async getHelp(topic) {
|
|
240
|
+
const systemPrompt = `You are Bash Bro, a helpful command-line assistant.
|
|
241
|
+
Provide concise help about the requested command or topic.
|
|
242
|
+
Include common usage examples. Keep it practical and brief.`;
|
|
243
|
+
try {
|
|
244
|
+
return await this.generate(`Help with: ${topic}`, systemPrompt);
|
|
245
|
+
} catch {
|
|
246
|
+
return "Could not get help for this topic.";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Convert natural language to a command
|
|
251
|
+
*/
|
|
252
|
+
async naturalToCommand(description) {
|
|
253
|
+
const systemPrompt = `You are Bash Bro, a helpful command-line assistant.
|
|
254
|
+
Convert the user's request into a single command line.
|
|
255
|
+
Respond with ONLY the command, no explanation.
|
|
256
|
+
If you can't convert it, respond with "none".`;
|
|
257
|
+
try {
|
|
258
|
+
const response = await this.generate(description, systemPrompt);
|
|
259
|
+
const command = response.trim();
|
|
260
|
+
if (command.toLowerCase() === "none" || command.length > 300) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
return command;
|
|
264
|
+
} catch {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export {
|
|
271
|
+
OllamaClient
|
|
272
|
+
};
|
|
273
|
+
//# sourceMappingURL=chunk-DLP2O6PN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bro/ollama.ts"],"sourcesContent":["/**\r\n * Simple Ollama client for local model inference.\r\n * Keeps it minimal - just what we need for Bash Bro.\r\n */\r\n\r\nexport interface OllamaConfig {\r\n host: string\r\n model: string\r\n timeout: number\r\n}\r\n\r\nexport interface ChatMessage {\r\n role: 'system' | 'user' | 'assistant'\r\n content: string\r\n}\r\n\r\nexport interface GenerateResponse {\r\n response: string\r\n done: boolean\r\n context?: number[]\r\n}\r\n\r\nconst DEFAULT_CONFIG: OllamaConfig = {\r\n host: 'http://localhost:11434',\r\n model: 'qwen2.5-coder:7b',\r\n timeout: 30000\r\n}\r\n\r\nexport class OllamaClient {\r\n private config: OllamaConfig\r\n\r\n constructor(config: Partial<OllamaConfig> = {}) {\r\n this.config = { ...DEFAULT_CONFIG, ...config }\r\n }\r\n\r\n /**\r\n * Check if Ollama is running and accessible\r\n */\r\n async isAvailable(): Promise<boolean> {\r\n try {\r\n const controller = new AbortController()\r\n const timeout = setTimeout(() => controller.abort(), 5000)\r\n\r\n const response = await fetch(`${this.config.host}/api/tags`, {\r\n signal: controller.signal\r\n })\r\n\r\n clearTimeout(timeout)\r\n return response.ok\r\n } catch {\r\n return false\r\n }\r\n }\r\n\r\n /**\r\n * List available models\r\n */\r\n async listModels(): Promise<string[]> {\r\n try {\r\n const response = await fetch(`${this.config.host}/api/tags`)\r\n\r\n if (!response.ok) {\r\n return []\r\n }\r\n\r\n const data = await response.json() as { models?: { name: string }[] }\r\n return data.models?.map((m) => m.name) || []\r\n } catch {\r\n return []\r\n }\r\n }\r\n\r\n /**\r\n * Generate a response from the model\r\n */\r\n async generate(prompt: string, systemPrompt?: string): Promise<string> {\r\n const controller = new AbortController()\r\n const timeout = setTimeout(() => controller.abort(), this.config.timeout)\r\n\r\n try {\r\n const response = await fetch(`${this.config.host}/api/generate`, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({\r\n model: this.config.model,\r\n prompt,\r\n system: systemPrompt,\r\n stream: false\r\n }),\r\n signal: controller.signal\r\n })\r\n\r\n clearTimeout(timeout)\r\n\r\n if (!response.ok) {\r\n throw new Error(`Ollama error: ${response.status}`)\r\n }\r\n\r\n const data = await response.json() as GenerateResponse\r\n return data.response\r\n } catch (error: any) {\r\n if (error.name === 'AbortError') {\r\n throw new Error('Ollama request timed out')\r\n }\r\n throw error\r\n }\r\n }\r\n\r\n /**\r\n * Chat with the model (multi-turn conversation)\r\n */\r\n async chat(messages: ChatMessage[]): Promise<string> {\r\n const controller = new AbortController()\r\n const timeout = setTimeout(() => controller.abort(), this.config.timeout)\r\n\r\n try {\r\n const response = await fetch(`${this.config.host}/api/chat`, {\r\n method: 'POST',\r\n headers: { 'Content-Type': 'application/json' },\r\n body: JSON.stringify({\r\n model: this.config.model,\r\n messages,\r\n stream: false\r\n }),\r\n signal: controller.signal\r\n })\r\n\r\n clearTimeout(timeout)\r\n\r\n if (!response.ok) {\r\n throw new Error(`Ollama error: ${response.status}`)\r\n }\r\n\r\n const data = await response.json() as { message?: { content: string } }\r\n return data.message?.content || ''\r\n } catch (error: any) {\r\n if (error.name === 'AbortError') {\r\n throw new Error('Ollama request timed out')\r\n }\r\n throw error\r\n }\r\n }\r\n\r\n /**\r\n * Ask Bash Bro to suggest a command\r\n */\r\n async suggestCommand(context: string): Promise<string | null> {\r\n const systemPrompt = `You are Bash Bro, a helpful command-line assistant.\r\nGiven the context, suggest the most likely next command the user needs.\r\nRespond with ONLY the command, no explanation. If unsure, respond with \"none\".`\r\n\r\n try {\r\n const response = await this.generate(context, systemPrompt)\r\n const command = response.trim()\r\n\r\n if (command.toLowerCase() === 'none' || command.length > 200) {\r\n return null\r\n }\r\n\r\n return command\r\n } catch {\r\n return null\r\n }\r\n }\r\n\r\n /**\r\n * Ask Bash Bro to explain a command\r\n */\r\n async explainCommand(command: string): Promise<string> {\r\n const systemPrompt = `You are Bash Bro, a helpful command-line assistant.\r\nExplain what the given command does in 1-2 sentences. Be concise and accurate.`\r\n\r\n try {\r\n return await this.generate(`Explain: ${command}`, systemPrompt)\r\n } catch {\r\n return 'Could not explain command.'\r\n }\r\n }\r\n\r\n /**\r\n * Ask Bash Bro to fix a command that failed\r\n */\r\n async fixCommand(command: string, error: string): Promise<string | null> {\r\n const systemPrompt = `You are Bash Bro, a helpful command-line assistant.\r\nGiven a failed command and its error, suggest a fixed version.\r\nRespond with ONLY the fixed command, no explanation. If you can't fix it, respond with \"none\".`\r\n\r\n try {\r\n const response = await this.generate(\r\n `Command: ${command}\\nError: ${error}\\nFixed command:`,\r\n systemPrompt\r\n )\r\n\r\n const fixed = response.trim()\r\n\r\n if (fixed.toLowerCase() === 'none' || fixed.length > 500) {\r\n return null\r\n }\r\n\r\n return fixed\r\n } catch {\r\n return null\r\n }\r\n }\r\n\r\n setModel(model: string): void {\r\n this.config.model = model\r\n }\r\n\r\n getModel(): string {\r\n return this.config.model\r\n }\r\n\r\n /**\r\n * Generate a shell script from a natural language description\r\n */\r\n async generateScript(description: string, shell: string = 'bash'): Promise<string | null> {\r\n const systemPrompt = `You are Bash Bro, a helpful command-line assistant.\r\nGenerate a ${shell} script based on the user's description.\r\nOutput ONLY the script, no explanation. Start with the shebang line.\r\nKeep scripts simple, readable, and well-commented.`\r\n\r\n try {\r\n const response = await this.generate(\r\n `Generate a ${shell} script that: ${description}`,\r\n systemPrompt\r\n )\r\n return response.trim()\r\n } catch {\r\n return null\r\n }\r\n }\r\n\r\n /**\r\n * Analyze command safety and provide recommendations\r\n */\r\n async analyzeCommandSafety(command: string): Promise<{\r\n safe: boolean\r\n risk: 'low' | 'medium' | 'high' | 'critical'\r\n explanation: string\r\n suggestions: string[]\r\n }> {\r\n const systemPrompt = `You are Bash Bro, a security-focused command-line assistant.\r\nAnalyze the given command for security risks.\r\nRespond with JSON only, in this format:\r\n{\"safe\": boolean, \"risk\": \"low|medium|high|critical\", \"explanation\": \"...\", \"suggestions\": [\"...\"]}`\r\n\r\n try {\r\n const response = await this.generate(\r\n `Analyze this command for security risks: ${command}`,\r\n systemPrompt\r\n )\r\n\r\n // Try to parse JSON from response\r\n const jsonMatch = response.match(/\\{[\\s\\S]*\\}/)\r\n if (jsonMatch) {\r\n return JSON.parse(jsonMatch[0])\r\n }\r\n\r\n // Fallback if not parseable\r\n return {\r\n safe: true,\r\n risk: 'low',\r\n explanation: 'Could not analyze command.',\r\n suggestions: []\r\n }\r\n } catch {\r\n return {\r\n safe: true,\r\n risk: 'low',\r\n explanation: 'Analysis unavailable.',\r\n suggestions: []\r\n }\r\n }\r\n }\r\n\r\n /**\r\n * Summarize a series of commands and their outputs\r\n */\r\n async summarizeSession(commands: { command: string; output?: string; error?: string }[]): Promise<string> {\r\n const systemPrompt = `You are Bash Bro, a helpful command-line assistant.\r\nSummarize what happened in this terminal session in 2-3 sentences.\r\nFocus on what was accomplished and any issues encountered.`\r\n\r\n const sessionText = commands.map(c => {\r\n let text = `$ ${c.command}`\r\n if (c.output) text += `\\n${c.output.slice(0, 500)}`\r\n if (c.error) text += `\\nError: ${c.error.slice(0, 200)}`\r\n return text\r\n }).join('\\n\\n')\r\n\r\n try {\r\n return await this.generate(sessionText, systemPrompt)\r\n } catch {\r\n return 'Could not summarize session.'\r\n }\r\n }\r\n\r\n /**\r\n * Get help for a specific tool or command\r\n */\r\n async getHelp(topic: string): Promise<string> {\r\n const systemPrompt = `You are Bash Bro, a helpful command-line assistant.\r\nProvide concise help about the requested command or topic.\r\nInclude common usage examples. Keep it practical and brief.`\r\n\r\n try {\r\n return await this.generate(`Help with: ${topic}`, systemPrompt)\r\n } catch {\r\n return 'Could not get help for this topic.'\r\n }\r\n }\r\n\r\n /**\r\n * Convert natural language to a command\r\n */\r\n async naturalToCommand(description: string): Promise<string | null> {\r\n const systemPrompt = `You are Bash Bro, a helpful command-line assistant.\r\nConvert the user's request into a single command line.\r\nRespond with ONLY the command, no explanation.\r\nIf you can't convert it, respond with \"none\".`\r\n\r\n try {\r\n const response = await this.generate(description, systemPrompt)\r\n const command = response.trim()\r\n\r\n if (command.toLowerCase() === 'none' || command.length > 300) {\r\n return null\r\n }\r\n\r\n return command\r\n } catch {\r\n return null\r\n }\r\n }\r\n}\r\n"],"mappings":"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","names":[]}
|