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
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2448 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
formatAllAgentsInfo,
|
|
4
|
+
formatPermissionsTable
|
|
5
|
+
} from "./chunk-4R4GV5V2.js";
|
|
6
|
+
import {
|
|
7
|
+
BashBro,
|
|
8
|
+
BashBros,
|
|
9
|
+
ClaudeCodeHooks,
|
|
10
|
+
CostEstimator,
|
|
11
|
+
LoopDetector,
|
|
12
|
+
MetricsCollector,
|
|
13
|
+
ReportGenerator,
|
|
14
|
+
UndoStack,
|
|
15
|
+
gateCommand,
|
|
16
|
+
getBashgymIntegration
|
|
17
|
+
} from "./chunk-43W3RVEL.js";
|
|
18
|
+
import "./chunk-SG752FZC.js";
|
|
19
|
+
import "./chunk-DLP2O6PN.js";
|
|
20
|
+
import {
|
|
21
|
+
findConfig,
|
|
22
|
+
getDefaultConfig,
|
|
23
|
+
loadConfig
|
|
24
|
+
} from "./chunk-SB4JS3GU.js";
|
|
25
|
+
import {
|
|
26
|
+
allowForSession
|
|
27
|
+
} from "./chunk-GD5VNHIN.js";
|
|
28
|
+
import {
|
|
29
|
+
RiskScorer
|
|
30
|
+
} from "./chunk-DEAF6PYM.js";
|
|
31
|
+
import {
|
|
32
|
+
formatRedactedConfig,
|
|
33
|
+
parseAgentConfig
|
|
34
|
+
} from "./chunk-ID2O2QTI.js";
|
|
35
|
+
import {
|
|
36
|
+
MoltbotHooks
|
|
37
|
+
} from "./chunk-J37RHCFJ.js";
|
|
38
|
+
import {
|
|
39
|
+
DashboardDB
|
|
40
|
+
} from "./chunk-CSRPOGHY.js";
|
|
41
|
+
import "./chunk-7OCVIDC7.js";
|
|
42
|
+
|
|
43
|
+
// src/cli.ts
|
|
44
|
+
import { Command } from "commander";
|
|
45
|
+
import chalk5 from "chalk";
|
|
46
|
+
|
|
47
|
+
// src/onboarding.ts
|
|
48
|
+
import inquirer from "inquirer";
|
|
49
|
+
import chalk from "chalk";
|
|
50
|
+
import { writeFileSync, existsSync, mkdirSync } from "fs";
|
|
51
|
+
import { join } from "path";
|
|
52
|
+
import { homedir } from "os";
|
|
53
|
+
import { stringify } from "yaml";
|
|
54
|
+
async function runOnboarding() {
|
|
55
|
+
console.log(chalk.dim(` "I watch your agent's back so you don't have to."
|
|
56
|
+
`));
|
|
57
|
+
const bashgymAvailable = existsSync(join(homedir(), ".bashgym", "integration"));
|
|
58
|
+
const questions = [
|
|
59
|
+
{
|
|
60
|
+
type: "list",
|
|
61
|
+
name: "agent",
|
|
62
|
+
message: "What agent are you protecting?",
|
|
63
|
+
choices: [
|
|
64
|
+
{ name: "Claude Code", value: "claude-code" },
|
|
65
|
+
{ name: "Moltbot (clawd.bot)", value: "moltbot" },
|
|
66
|
+
{ name: "Clawdbot (legacy)", value: "clawdbot" },
|
|
67
|
+
{ name: "Gemini CLI", value: "gemini-cli" },
|
|
68
|
+
{ name: "Aider", value: "aider" },
|
|
69
|
+
{ name: "OpenCode", value: "opencode" },
|
|
70
|
+
{ name: "Other (custom)", value: "custom" }
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: "list",
|
|
75
|
+
name: "projectType",
|
|
76
|
+
message: "What's this project about? (helps tune defaults)",
|
|
77
|
+
choices: [
|
|
78
|
+
{ name: "Web development", value: "web" },
|
|
79
|
+
{ name: "DevOps / Infrastructure", value: "devops" },
|
|
80
|
+
{ name: "Data engineering", value: "data" },
|
|
81
|
+
{ name: "General coding", value: "general" },
|
|
82
|
+
{ name: "Sensitive/regulated work", value: "sensitive" }
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: "list",
|
|
87
|
+
name: "profile",
|
|
88
|
+
message: "Security posture:",
|
|
89
|
+
choices: [
|
|
90
|
+
{
|
|
91
|
+
name: "Balanced (recommended) - Block dangerous, allow common dev tools",
|
|
92
|
+
value: "balanced"
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "Strict - Allowlist only, explicit approval for new commands",
|
|
96
|
+
value: "strict"
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "Permissive - Log everything, block only critical threats",
|
|
100
|
+
value: "permissive"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: "Custom - I'll configure manually",
|
|
104
|
+
value: "custom"
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
type: "list",
|
|
110
|
+
name: "secrets",
|
|
111
|
+
message: "Protect secrets? (scans for .env, credentials, SSH keys)",
|
|
112
|
+
choices: [
|
|
113
|
+
{ name: "Yes, block access and warn (recommended)", value: "block" },
|
|
114
|
+
{ name: "Yes, but allow read with audit log", value: "audit" },
|
|
115
|
+
{ name: "No", value: "disabled" }
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: "list",
|
|
120
|
+
name: "audit",
|
|
121
|
+
message: "Enable audit logging?",
|
|
122
|
+
choices: [
|
|
123
|
+
{ name: "Local file (~/.bashbros/audit.log)", value: "local" },
|
|
124
|
+
{ name: "Send to remote (Datadog, Splunk, webhook)", value: "remote" },
|
|
125
|
+
{ name: "Both", value: "both" },
|
|
126
|
+
{ name: "None", value: "disabled" }
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
];
|
|
130
|
+
if (bashgymAvailable) {
|
|
131
|
+
questions.push({
|
|
132
|
+
type: "list",
|
|
133
|
+
name: "bashgym",
|
|
134
|
+
message: "Link to BashGym? (enables self-improving AI sidekick)",
|
|
135
|
+
choices: [
|
|
136
|
+
{
|
|
137
|
+
name: "Yes (recommended) - Export traces for training, get smarter sidekick",
|
|
138
|
+
value: "link"
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "No - Use bashbros standalone",
|
|
142
|
+
value: "skip"
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
const answers = await inquirer.prompt(questions);
|
|
148
|
+
const config = buildConfig(answers);
|
|
149
|
+
const configYaml = stringify(config);
|
|
150
|
+
writeFileSync(".bashbros.yml", configYaml);
|
|
151
|
+
console.log();
|
|
152
|
+
console.log(chalk.green("\u2713"), "Config written to", chalk.cyan(".bashbros.yml"));
|
|
153
|
+
console.log(chalk.green("\u2713"), "PTY wrapper ready");
|
|
154
|
+
console.log(chalk.green("\u2713"), "Audit logging", answers.audit !== "disabled" ? "enabled" : "disabled");
|
|
155
|
+
if (answers.bashgym === "link") {
|
|
156
|
+
const linked = await linkBashgym();
|
|
157
|
+
if (linked) {
|
|
158
|
+
console.log(chalk.green("\u2713"), "BashGym integration", chalk.cyan("linked"));
|
|
159
|
+
console.log(chalk.dim(" Traces will be exported for training"));
|
|
160
|
+
console.log(chalk.dim(" AI sidekick will improve over time"));
|
|
161
|
+
} else {
|
|
162
|
+
console.log(chalk.yellow("\u26A0"), "BashGym integration", chalk.dim("not linked (bashgym not running?)"));
|
|
163
|
+
}
|
|
164
|
+
} else if (bashgymAvailable) {
|
|
165
|
+
console.log(chalk.dim("\u25CB"), "BashGym integration", chalk.dim("skipped"));
|
|
166
|
+
}
|
|
167
|
+
console.log();
|
|
168
|
+
console.log(chalk.dim("Run"), chalk.cyan("'bashbros doctor'"), chalk.dim("to verify setup"));
|
|
169
|
+
console.log(chalk.dim("Run"), chalk.cyan("'bashbros watch'"), chalk.dim("to start protection"));
|
|
170
|
+
console.log();
|
|
171
|
+
}
|
|
172
|
+
async function linkBashgym() {
|
|
173
|
+
try {
|
|
174
|
+
const integration = getBashgymIntegration();
|
|
175
|
+
if (!integration.isAvailable()) {
|
|
176
|
+
const integrationDir = join(homedir(), ".bashgym", "integration");
|
|
177
|
+
const dirs = [
|
|
178
|
+
join(integrationDir, "traces", "pending"),
|
|
179
|
+
join(integrationDir, "traces", "processed"),
|
|
180
|
+
join(integrationDir, "traces", "failed"),
|
|
181
|
+
join(integrationDir, "models", "latest"),
|
|
182
|
+
join(integrationDir, "config"),
|
|
183
|
+
join(integrationDir, "status")
|
|
184
|
+
];
|
|
185
|
+
for (const dir of dirs) {
|
|
186
|
+
mkdirSync(dir, { recursive: true });
|
|
187
|
+
}
|
|
188
|
+
const settingsPath = join(integrationDir, "config", "settings.json");
|
|
189
|
+
const settings = {
|
|
190
|
+
version: "1.0",
|
|
191
|
+
updated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
192
|
+
updated_by: "bashbros",
|
|
193
|
+
integration: {
|
|
194
|
+
enabled: true,
|
|
195
|
+
linked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
196
|
+
},
|
|
197
|
+
capture: {
|
|
198
|
+
mode: "successful_only",
|
|
199
|
+
auto_stream: true
|
|
200
|
+
},
|
|
201
|
+
training: {
|
|
202
|
+
auto_enabled: false,
|
|
203
|
+
quality_threshold: 50,
|
|
204
|
+
trigger: "quality_based"
|
|
205
|
+
},
|
|
206
|
+
security: {
|
|
207
|
+
bashbros_primary: true,
|
|
208
|
+
policy_path: null
|
|
209
|
+
},
|
|
210
|
+
model_sync: {
|
|
211
|
+
auto_export_ollama: true,
|
|
212
|
+
ollama_model_name: "bashgym-sidekick",
|
|
213
|
+
notify_on_update: true
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
217
|
+
} else {
|
|
218
|
+
integration.updateSettings({
|
|
219
|
+
integration: {
|
|
220
|
+
enabled: true,
|
|
221
|
+
linked_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error("Failed to link bashgym:", error);
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
function buildConfig(answers) {
|
|
232
|
+
const defaults = getDefaultConfig();
|
|
233
|
+
const config = {
|
|
234
|
+
...defaults,
|
|
235
|
+
agent: answers.agent,
|
|
236
|
+
profile: answers.profile,
|
|
237
|
+
secrets: {
|
|
238
|
+
...defaults.secrets,
|
|
239
|
+
enabled: answers.secrets !== "disabled",
|
|
240
|
+
mode: answers.secrets === "audit" ? "audit" : "block"
|
|
241
|
+
},
|
|
242
|
+
audit: {
|
|
243
|
+
...defaults.audit,
|
|
244
|
+
enabled: answers.audit !== "disabled",
|
|
245
|
+
destination: answers.audit === "disabled" ? "local" : answers.audit
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
if (answers.projectType === "sensitive") {
|
|
249
|
+
config.profile = "strict";
|
|
250
|
+
config.rateLimit.maxPerMinute = 50;
|
|
251
|
+
}
|
|
252
|
+
if (answers.projectType === "devops") {
|
|
253
|
+
config.commands.allow.push("docker *", "kubectl *", "terraform *", "aws *");
|
|
254
|
+
}
|
|
255
|
+
if (answers.projectType === "data") {
|
|
256
|
+
config.commands.allow.push("python *", "jupyter *", "pandas *", "psql *");
|
|
257
|
+
}
|
|
258
|
+
return config;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/doctor.ts
|
|
262
|
+
import chalk2 from "chalk";
|
|
263
|
+
import { existsSync as existsSync4 } from "fs";
|
|
264
|
+
import { join as join3 } from "path";
|
|
265
|
+
import { homedir as homedir3 } from "os";
|
|
266
|
+
|
|
267
|
+
// src/transparency/agent-config.ts
|
|
268
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
269
|
+
import { join as join2 } from "path";
|
|
270
|
+
import { homedir as homedir2, platform } from "os";
|
|
271
|
+
import { execFileSync } from "child_process";
|
|
272
|
+
var AGENT_CONFIG_PATHS = {
|
|
273
|
+
"claude-code": [
|
|
274
|
+
join2(homedir2(), ".claude", "settings.json")
|
|
275
|
+
],
|
|
276
|
+
"clawdbot": [
|
|
277
|
+
join2(homedir2(), ".clawdbot", "moltbot.json"),
|
|
278
|
+
// New primary (moltbot format)
|
|
279
|
+
join2(homedir2(), ".clawdbot", "config.yml"),
|
|
280
|
+
// Legacy
|
|
281
|
+
join2(homedir2(), ".config", "clawdbot", "config.yml")
|
|
282
|
+
],
|
|
283
|
+
"moltbot": [
|
|
284
|
+
join2(homedir2(), ".moltbot", "config.json"),
|
|
285
|
+
join2(homedir2(), ".clawdbot", "moltbot.json"),
|
|
286
|
+
// Common location
|
|
287
|
+
join2(homedir2(), ".config", "moltbot", "config.json")
|
|
288
|
+
],
|
|
289
|
+
"aider": [
|
|
290
|
+
join2(homedir2(), ".aider.conf.yml"),
|
|
291
|
+
join2(homedir2(), ".config", "aider", "aider.conf.yml")
|
|
292
|
+
],
|
|
293
|
+
"gemini-cli": [
|
|
294
|
+
join2(homedir2(), ".config", "gemini-cli", "config.json")
|
|
295
|
+
],
|
|
296
|
+
"opencode": [
|
|
297
|
+
join2(homedir2(), ".opencode", "config.yml"),
|
|
298
|
+
join2(homedir2(), ".config", "opencode", "config.yml")
|
|
299
|
+
],
|
|
300
|
+
"custom": []
|
|
301
|
+
};
|
|
302
|
+
var AGENT_COMMANDS = {
|
|
303
|
+
"claude-code": "claude",
|
|
304
|
+
"clawdbot": "clawdbot",
|
|
305
|
+
"moltbot": "moltbot",
|
|
306
|
+
"aider": "aider",
|
|
307
|
+
"gemini-cli": "gemini",
|
|
308
|
+
"opencode": "opencode",
|
|
309
|
+
"custom": ""
|
|
310
|
+
};
|
|
311
|
+
function commandExists(cmd) {
|
|
312
|
+
if (!cmd) return false;
|
|
313
|
+
try {
|
|
314
|
+
const whichCmd = platform() === "win32" ? "where" : "which";
|
|
315
|
+
execFileSync(whichCmd, [cmd], {
|
|
316
|
+
stdio: "pipe",
|
|
317
|
+
timeout: 3e3,
|
|
318
|
+
windowsHide: true
|
|
319
|
+
});
|
|
320
|
+
return true;
|
|
321
|
+
} catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function getAgentVersion(agent) {
|
|
326
|
+
const cmd = AGENT_COMMANDS[agent];
|
|
327
|
+
if (!cmd) return void 0;
|
|
328
|
+
try {
|
|
329
|
+
const output = execFileSync(cmd, ["--version"], {
|
|
330
|
+
encoding: "utf-8",
|
|
331
|
+
timeout: 5e3,
|
|
332
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
333
|
+
windowsHide: true
|
|
334
|
+
}).trim();
|
|
335
|
+
const match = output.match(/(\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)/i);
|
|
336
|
+
return match ? match[1] : output.split("\n")[0].slice(0, 50);
|
|
337
|
+
} catch {
|
|
338
|
+
return void 0;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function findAgentConfigPath(agent) {
|
|
342
|
+
const paths = AGENT_CONFIG_PATHS[agent];
|
|
343
|
+
for (const configPath of paths) {
|
|
344
|
+
if (existsSync2(configPath)) {
|
|
345
|
+
return configPath;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return void 0;
|
|
349
|
+
}
|
|
350
|
+
function getLastModified(filePath) {
|
|
351
|
+
try {
|
|
352
|
+
const stats = statSync(filePath);
|
|
353
|
+
return stats.mtime;
|
|
354
|
+
} catch {
|
|
355
|
+
return void 0;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
async function isBashbrosIntegrated(agent) {
|
|
359
|
+
if (agent === "claude-code") {
|
|
360
|
+
const status = ClaudeCodeHooks.getStatus();
|
|
361
|
+
return status.hooksInstalled;
|
|
362
|
+
}
|
|
363
|
+
if (agent === "moltbot" || agent === "clawdbot") {
|
|
364
|
+
try {
|
|
365
|
+
const { MoltbotHooks: MoltbotHooks2 } = await import("./moltbot-DXZFVK3X.js");
|
|
366
|
+
return MoltbotHooks2.isInstalled();
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return false;
|
|
372
|
+
}
|
|
373
|
+
async function getAgentConfigInfo(agent) {
|
|
374
|
+
const cmd = AGENT_COMMANDS[agent];
|
|
375
|
+
const installed = cmd ? commandExists(cmd) : false;
|
|
376
|
+
const configPath = findAgentConfigPath(agent);
|
|
377
|
+
const configExists = configPath ? existsSync2(configPath) : false;
|
|
378
|
+
const info = {
|
|
379
|
+
agent,
|
|
380
|
+
installed,
|
|
381
|
+
configPath,
|
|
382
|
+
configExists,
|
|
383
|
+
bashbrosIntegrated: await isBashbrosIntegrated(agent)
|
|
384
|
+
};
|
|
385
|
+
if (installed) {
|
|
386
|
+
info.version = getAgentVersion(agent);
|
|
387
|
+
}
|
|
388
|
+
if (configExists && configPath) {
|
|
389
|
+
info.lastModified = getLastModified(configPath);
|
|
390
|
+
const parsed = await parseAgentConfig(agent, configPath);
|
|
391
|
+
if (parsed) {
|
|
392
|
+
info.permissions = parsed.permissions;
|
|
393
|
+
info.hooks = parsed.hooks;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return info;
|
|
397
|
+
}
|
|
398
|
+
async function getAllAgentConfigs() {
|
|
399
|
+
const agents = ["claude-code", "moltbot", "clawdbot", "aider", "gemini-cli", "opencode"];
|
|
400
|
+
const results = [];
|
|
401
|
+
for (const agent of agents) {
|
|
402
|
+
const info = await getAgentConfigInfo(agent);
|
|
403
|
+
results.push(info);
|
|
404
|
+
}
|
|
405
|
+
return results;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// src/policy/ward/exposure.ts
|
|
409
|
+
import { exec } from "child_process";
|
|
410
|
+
import { promisify } from "util";
|
|
411
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
412
|
+
import { platform as platform2 } from "os";
|
|
413
|
+
var execAsync = promisify(exec);
|
|
414
|
+
var DEFAULT_AGENT_SIGNATURES = [
|
|
415
|
+
{
|
|
416
|
+
name: "claude-code",
|
|
417
|
+
processNames: ["claude", "claude-code", "claude_code"],
|
|
418
|
+
defaultPorts: [3e3, 3001, 8080],
|
|
419
|
+
configPaths: [
|
|
420
|
+
"~/.claude/config.json",
|
|
421
|
+
"~/.config/claude/config.json"
|
|
422
|
+
],
|
|
423
|
+
authIndicators: ["api_key", "auth_token", "authorization"]
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "aider",
|
|
427
|
+
processNames: ["aider", "aider-chat"],
|
|
428
|
+
defaultPorts: [8501, 8e3],
|
|
429
|
+
configPaths: [
|
|
430
|
+
"~/.aider.conf.yml",
|
|
431
|
+
".aider.conf.yml"
|
|
432
|
+
],
|
|
433
|
+
authIndicators: ["api_key", "openai_api_key"]
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: "continue",
|
|
437
|
+
processNames: ["continue", "continue-server"],
|
|
438
|
+
defaultPorts: [65432, 65433],
|
|
439
|
+
configPaths: [
|
|
440
|
+
"~/.continue/config.json",
|
|
441
|
+
".continue/config.json"
|
|
442
|
+
],
|
|
443
|
+
authIndicators: ["apiKey", "auth"]
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "cursor",
|
|
447
|
+
processNames: ["cursor", "Cursor", "cursor-server"],
|
|
448
|
+
defaultPorts: [3e3, 8080, 9e3],
|
|
449
|
+
configPaths: [
|
|
450
|
+
"~/.cursor/config.json",
|
|
451
|
+
"%APPDATA%/Cursor/config.json"
|
|
452
|
+
],
|
|
453
|
+
authIndicators: ["apiKey", "token", "auth"]
|
|
454
|
+
}
|
|
455
|
+
];
|
|
456
|
+
var DEFAULT_SEVERITY_ACTIONS = {
|
|
457
|
+
low: "alert",
|
|
458
|
+
medium: "alert",
|
|
459
|
+
high: "block",
|
|
460
|
+
critical: "block_and_kill"
|
|
461
|
+
};
|
|
462
|
+
var ExposureScanner = class {
|
|
463
|
+
config;
|
|
464
|
+
agents;
|
|
465
|
+
constructor(config) {
|
|
466
|
+
this.agents = [...DEFAULT_AGENT_SIGNATURES];
|
|
467
|
+
this.config = {
|
|
468
|
+
enabled: true,
|
|
469
|
+
scanInterval: 6e4,
|
|
470
|
+
externalProbe: false,
|
|
471
|
+
severityActions: { ...DEFAULT_SEVERITY_ACTIONS },
|
|
472
|
+
agents: this.agents,
|
|
473
|
+
...config
|
|
474
|
+
};
|
|
475
|
+
if (config?.agents) {
|
|
476
|
+
for (const agent of config.agents) {
|
|
477
|
+
if (!this.agents.find((a) => a.name === agent.name)) {
|
|
478
|
+
this.agents.push(agent);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* Scan for exposed agent servers
|
|
485
|
+
*/
|
|
486
|
+
async scan() {
|
|
487
|
+
const results = [];
|
|
488
|
+
const listeningPorts = await this.getListeningPorts();
|
|
489
|
+
for (const port of listeningPorts) {
|
|
490
|
+
for (const agent of this.agents) {
|
|
491
|
+
if (agent.defaultPorts.includes(port.localPort)) {
|
|
492
|
+
const hasAuth = await this.checkAuthConfig(agent);
|
|
493
|
+
const externallyReachable = this.isExternallyReachable(port.localAddress);
|
|
494
|
+
const severity = this.assessSeverity({
|
|
495
|
+
bindAddress: port.localAddress,
|
|
496
|
+
hasAuth,
|
|
497
|
+
externallyReachable,
|
|
498
|
+
hasActiveSessions: false
|
|
499
|
+
});
|
|
500
|
+
const action = this.getActionForSeverity(severity);
|
|
501
|
+
results.push({
|
|
502
|
+
agent: agent.name,
|
|
503
|
+
pid: port.pid,
|
|
504
|
+
port: port.localPort,
|
|
505
|
+
bindAddress: port.localAddress,
|
|
506
|
+
hasAuth,
|
|
507
|
+
severity,
|
|
508
|
+
action,
|
|
509
|
+
message: this.generateMessage(agent.name, port, severity, hasAuth),
|
|
510
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return results;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Assess severity based on exposure factors
|
|
519
|
+
*/
|
|
520
|
+
assessSeverity(input) {
|
|
521
|
+
const { bindAddress, hasAuth, externallyReachable, hasActiveSessions } = input;
|
|
522
|
+
if (externallyReachable && hasAuth !== true) {
|
|
523
|
+
return "critical";
|
|
524
|
+
}
|
|
525
|
+
if (externallyReachable && hasActiveSessions) {
|
|
526
|
+
return "critical";
|
|
527
|
+
}
|
|
528
|
+
const isWildcardBind = bindAddress === "0.0.0.0" || bindAddress === "::" || bindAddress === "*";
|
|
529
|
+
if (isWildcardBind && hasAuth !== true) {
|
|
530
|
+
return "high";
|
|
531
|
+
}
|
|
532
|
+
if (isWildcardBind && hasAuth === true) {
|
|
533
|
+
return "medium";
|
|
534
|
+
}
|
|
535
|
+
const isLocalhost = bindAddress === "127.0.0.1" || bindAddress === "::1" || bindAddress === "localhost";
|
|
536
|
+
if (isLocalhost && hasAuth !== true) {
|
|
537
|
+
return "medium";
|
|
538
|
+
}
|
|
539
|
+
return "low";
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Get action for a given severity level
|
|
543
|
+
*/
|
|
544
|
+
getActionForSeverity(severity) {
|
|
545
|
+
return this.config.severityActions[severity];
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Parse a Windows netstat output line
|
|
549
|
+
*/
|
|
550
|
+
parseNetstatLine(line) {
|
|
551
|
+
const trimmed = line.trim();
|
|
552
|
+
if (!trimmed || !trimmed.includes("LISTENING")) {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
const parts = trimmed.split(/\s+/);
|
|
556
|
+
if (parts.length < 5) {
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
const protocol = parts[0];
|
|
560
|
+
if (protocol !== "TCP" && protocol !== "UDP") {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
const localAddressPart = parts[1];
|
|
564
|
+
const state = parts[3];
|
|
565
|
+
const pid = parseInt(parts[4], 10);
|
|
566
|
+
if (state !== "LISTENING" || isNaN(pid)) {
|
|
567
|
+
return null;
|
|
568
|
+
}
|
|
569
|
+
let localAddress;
|
|
570
|
+
let localPort;
|
|
571
|
+
if (localAddressPart.startsWith("[")) {
|
|
572
|
+
const match = localAddressPart.match(/^\[([^\]]+)\]:(\d+)$/);
|
|
573
|
+
if (!match) {
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
localAddress = match[1];
|
|
577
|
+
localPort = parseInt(match[2], 10);
|
|
578
|
+
} else {
|
|
579
|
+
const lastColonIndex = localAddressPart.lastIndexOf(":");
|
|
580
|
+
if (lastColonIndex === -1) {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
localAddress = localAddressPart.substring(0, lastColonIndex);
|
|
584
|
+
localPort = parseInt(localAddressPart.substring(lastColonIndex + 1), 10);
|
|
585
|
+
}
|
|
586
|
+
if (isNaN(localPort)) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
protocol,
|
|
591
|
+
localAddress,
|
|
592
|
+
localPort,
|
|
593
|
+
state,
|
|
594
|
+
pid
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Get all listening TCP ports (cross-platform)
|
|
599
|
+
*/
|
|
600
|
+
async getListeningPorts() {
|
|
601
|
+
const results = [];
|
|
602
|
+
try {
|
|
603
|
+
if (platform2() === "win32") {
|
|
604
|
+
const { stdout } = await execAsync("netstat -ano -p TCP");
|
|
605
|
+
const lines = stdout.split("\n");
|
|
606
|
+
for (const line of lines) {
|
|
607
|
+
const parsed = this.parseNetstatLine(line);
|
|
608
|
+
if (parsed) {
|
|
609
|
+
results.push(parsed);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
try {
|
|
614
|
+
const { stdout } = await execAsync("ss -tlnp 2>/dev/null || netstat -tlnp 2>/dev/null");
|
|
615
|
+
const lines = stdout.split("\n");
|
|
616
|
+
for (const line of lines) {
|
|
617
|
+
const parsed = this.parseUnixListeningLine(line);
|
|
618
|
+
if (parsed) {
|
|
619
|
+
results.push(parsed);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
} catch {
|
|
623
|
+
const { stdout } = await execAsync("lsof -iTCP -sTCP:LISTEN -n -P 2>/dev/null || true");
|
|
624
|
+
const lines = stdout.split("\n");
|
|
625
|
+
for (const line of lines) {
|
|
626
|
+
const parsed = this.parseLsofLine(line);
|
|
627
|
+
if (parsed) {
|
|
628
|
+
results.push(parsed);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
} catch {
|
|
634
|
+
}
|
|
635
|
+
return results;
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Parse Unix ss/netstat output line
|
|
639
|
+
*/
|
|
640
|
+
parseUnixListeningLine(line) {
|
|
641
|
+
const trimmed = line.trim();
|
|
642
|
+
if (!trimmed || trimmed.startsWith("State") || trimmed.startsWith("Proto")) {
|
|
643
|
+
return null;
|
|
644
|
+
}
|
|
645
|
+
const ssMatch = trimmed.match(/LISTEN\s+\d+\s+\d+\s+([^\s]+):(\d+)\s+.*?pid=(\d+)/);
|
|
646
|
+
if (ssMatch) {
|
|
647
|
+
return {
|
|
648
|
+
protocol: "TCP",
|
|
649
|
+
localAddress: ssMatch[1] === "*" ? "0.0.0.0" : ssMatch[1],
|
|
650
|
+
localPort: parseInt(ssMatch[2], 10),
|
|
651
|
+
state: "LISTENING",
|
|
652
|
+
pid: parseInt(ssMatch[3], 10)
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const netstatMatch = trimmed.match(/tcp\s+\d+\s+\d+\s+([^\s]+):(\d+)\s+.*?LISTEN\s+(\d+)/);
|
|
656
|
+
if (netstatMatch) {
|
|
657
|
+
return {
|
|
658
|
+
protocol: "TCP",
|
|
659
|
+
localAddress: netstatMatch[1] === "*" ? "0.0.0.0" : netstatMatch[1],
|
|
660
|
+
localPort: parseInt(netstatMatch[2], 10),
|
|
661
|
+
state: "LISTENING",
|
|
662
|
+
pid: parseInt(netstatMatch[3], 10)
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Parse lsof output line (macOS fallback)
|
|
669
|
+
*/
|
|
670
|
+
parseLsofLine(line) {
|
|
671
|
+
const trimmed = line.trim();
|
|
672
|
+
if (!trimmed || trimmed.startsWith("COMMAND")) {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
const parts = trimmed.split(/\s+/);
|
|
676
|
+
if (parts.length < 9) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
const pid = parseInt(parts[1], 10);
|
|
680
|
+
if (isNaN(pid)) {
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
const tcpIndex = parts.findIndex((p) => p === "TCP" || p === "TCP6");
|
|
684
|
+
if (tcpIndex === -1) {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
const addressPart = parts[tcpIndex + 1];
|
|
688
|
+
if (!addressPart) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
const match = addressPart.match(/^([^:]+):(\d+)$/);
|
|
692
|
+
if (!match) {
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
protocol: "TCP",
|
|
697
|
+
localAddress: match[1] === "*" ? "0.0.0.0" : match[1],
|
|
698
|
+
localPort: parseInt(match[2], 10),
|
|
699
|
+
state: "LISTENING",
|
|
700
|
+
pid
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Check if an agent has auth configured
|
|
705
|
+
*/
|
|
706
|
+
async checkAuthConfig(agent) {
|
|
707
|
+
for (const configPath of agent.configPaths) {
|
|
708
|
+
const expandedPath = this.expandPath(configPath);
|
|
709
|
+
if (existsSync3(expandedPath)) {
|
|
710
|
+
try {
|
|
711
|
+
const content = readFileSync(expandedPath, "utf-8");
|
|
712
|
+
for (const indicator of agent.authIndicators) {
|
|
713
|
+
if (content.includes(indicator)) {
|
|
714
|
+
return true;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return false;
|
|
718
|
+
} catch {
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
return "unknown";
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Check if an address is externally reachable
|
|
726
|
+
*/
|
|
727
|
+
isExternallyReachable(bindAddress) {
|
|
728
|
+
if (bindAddress === "0.0.0.0" || bindAddress === "::" || bindAddress === "*") {
|
|
729
|
+
return this.config.externalProbe;
|
|
730
|
+
}
|
|
731
|
+
if (bindAddress === "127.0.0.1" || bindAddress === "::1" || bindAddress === "localhost") {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
if (this.isPrivateIP(bindAddress)) {
|
|
735
|
+
return this.config.externalProbe;
|
|
736
|
+
}
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Check if an IP is in a private range
|
|
741
|
+
*/
|
|
742
|
+
isPrivateIP(ip) {
|
|
743
|
+
if (ip.startsWith("10.")) return true;
|
|
744
|
+
if (ip.startsWith("172.")) {
|
|
745
|
+
const second = parseInt(ip.split(".")[1], 10);
|
|
746
|
+
if (second >= 16 && second <= 31) return true;
|
|
747
|
+
}
|
|
748
|
+
if (ip.startsWith("192.168.")) return true;
|
|
749
|
+
if (ip.toLowerCase().startsWith("fc") || ip.toLowerCase().startsWith("fd")) {
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Expand path with home directory and environment variables
|
|
756
|
+
*/
|
|
757
|
+
expandPath(path) {
|
|
758
|
+
let expanded = path;
|
|
759
|
+
if (expanded.startsWith("~")) {
|
|
760
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
761
|
+
expanded = expanded.replace("~", home);
|
|
762
|
+
}
|
|
763
|
+
expanded = expanded.replace(/%([^%]+)%/g, (_, name) => process.env[name] || "");
|
|
764
|
+
expanded = expanded.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_, name) => process.env[name] || "");
|
|
765
|
+
return expanded;
|
|
766
|
+
}
|
|
767
|
+
/**
|
|
768
|
+
* Generate human-readable message for an exposure
|
|
769
|
+
*/
|
|
770
|
+
generateMessage(agentName, port, severity, hasAuth) {
|
|
771
|
+
const authStatus = hasAuth === true ? "with authentication" : hasAuth === false ? "without authentication" : "authentication status unknown";
|
|
772
|
+
const bindDesc = port.localAddress === "0.0.0.0" || port.localAddress === "::" ? "all interfaces" : port.localAddress;
|
|
773
|
+
return `${agentName} server detected on port ${port.localPort} (${bindDesc}) ${authStatus}. Severity: ${severity}`;
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* Add a custom agent signature
|
|
777
|
+
*/
|
|
778
|
+
addAgent(agent) {
|
|
779
|
+
this.agents.push(agent);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Get all configured agent signatures
|
|
783
|
+
*/
|
|
784
|
+
getAgents() {
|
|
785
|
+
return [...this.agents];
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
// src/policy/ward/patterns.ts
|
|
790
|
+
var DEFAULT_PATTERNS = [
|
|
791
|
+
// Credential patterns
|
|
792
|
+
{
|
|
793
|
+
name: "api_key",
|
|
794
|
+
regex: `(?:api[_-]?key|apikey)[\\s]*[=:][\\s]*["']?([a-zA-Z0-9_\\-]{20,})["']?`,
|
|
795
|
+
severity: "high",
|
|
796
|
+
action: "block",
|
|
797
|
+
category: "credentials",
|
|
798
|
+
description: "Generic API key pattern"
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: "aws_secret",
|
|
802
|
+
regex: `(?:AWS_SECRET_ACCESS_KEY|aws_secret_access_key)[\\s]*[=:][\\s]*["']?([a-zA-Z0-9/+=]{40})["']?`,
|
|
803
|
+
severity: "critical",
|
|
804
|
+
action: "block",
|
|
805
|
+
category: "credentials",
|
|
806
|
+
description: "AWS Secret Access Key"
|
|
807
|
+
},
|
|
808
|
+
{
|
|
809
|
+
name: "private_key",
|
|
810
|
+
regex: "-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----",
|
|
811
|
+
severity: "critical",
|
|
812
|
+
action: "block",
|
|
813
|
+
category: "credentials",
|
|
814
|
+
description: "Private key header"
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
name: "github_token",
|
|
818
|
+
regex: "(?:gh[pousr]_[a-zA-Z0-9]{36,}|github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59})",
|
|
819
|
+
severity: "critical",
|
|
820
|
+
action: "block",
|
|
821
|
+
category: "credentials",
|
|
822
|
+
description: "GitHub personal access token"
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
name: "openai_key",
|
|
826
|
+
regex: "sk-[a-zA-Z0-9]{20,}T3BlbkFJ[a-zA-Z0-9]{20,}",
|
|
827
|
+
severity: "critical",
|
|
828
|
+
action: "block",
|
|
829
|
+
category: "credentials",
|
|
830
|
+
description: "OpenAI API key"
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: "jwt_token",
|
|
834
|
+
regex: "eyJ[a-zA-Z0-9_-]*\\.eyJ[a-zA-Z0-9_-]*\\.[a-zA-Z0-9_-]*",
|
|
835
|
+
severity: "high",
|
|
836
|
+
action: "alert",
|
|
837
|
+
category: "credentials",
|
|
838
|
+
description: "JSON Web Token"
|
|
839
|
+
},
|
|
840
|
+
// PII patterns
|
|
841
|
+
{
|
|
842
|
+
name: "ssn",
|
|
843
|
+
regex: "\\b\\d{3}[- ]?\\d{2}[- ]?\\d{4}\\b",
|
|
844
|
+
severity: "critical",
|
|
845
|
+
action: "block",
|
|
846
|
+
category: "pii",
|
|
847
|
+
description: "US Social Security Number"
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
name: "credit_card",
|
|
851
|
+
regex: "\\b(?:4[0-9]{3}|5[1-5][0-9]{2}|6(?:011|5[0-9]{2})|3[47][0-9]{2})[- ]?[0-9]{4}[- ]?[0-9]{4}[- ]?[0-9]{4}\\b",
|
|
852
|
+
severity: "critical",
|
|
853
|
+
action: "block",
|
|
854
|
+
category: "pii",
|
|
855
|
+
description: "Credit card number (Visa, MasterCard, Amex, Discover)"
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
name: "email",
|
|
859
|
+
regex: "\\b[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}\\b",
|
|
860
|
+
severity: "low",
|
|
861
|
+
action: "log",
|
|
862
|
+
category: "pii",
|
|
863
|
+
description: "Email address"
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
name: "phone_us",
|
|
867
|
+
regex: "\\b(?:\\+1[- ]?)?(?:\\([0-9]{3}\\)|[0-9]{3})[- ]?[0-9]{3}[- ]?[0-9]{4}\\b",
|
|
868
|
+
severity: "medium",
|
|
869
|
+
action: "alert",
|
|
870
|
+
category: "pii",
|
|
871
|
+
description: "US phone number"
|
|
872
|
+
}
|
|
873
|
+
];
|
|
874
|
+
var SEVERITY_ORDER = {
|
|
875
|
+
low: 1,
|
|
876
|
+
medium: 2,
|
|
877
|
+
high: 3,
|
|
878
|
+
critical: 4
|
|
879
|
+
};
|
|
880
|
+
var EgressPatternMatcher = class {
|
|
881
|
+
patterns;
|
|
882
|
+
compiledPatterns;
|
|
883
|
+
constructor(customPatterns) {
|
|
884
|
+
this.patterns = [...DEFAULT_PATTERNS];
|
|
885
|
+
this.compiledPatterns = /* @__PURE__ */ new Map();
|
|
886
|
+
if (customPatterns) {
|
|
887
|
+
for (const pattern of customPatterns) {
|
|
888
|
+
this.patterns.push(pattern);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
this.compilePatterns();
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Compile regex patterns for efficient matching
|
|
895
|
+
*/
|
|
896
|
+
compilePatterns() {
|
|
897
|
+
for (const pattern of this.patterns) {
|
|
898
|
+
if (!this.compiledPatterns.has(pattern.name)) {
|
|
899
|
+
try {
|
|
900
|
+
this.compiledPatterns.set(pattern.name, new RegExp(pattern.regex, "gi"));
|
|
901
|
+
} catch {
|
|
902
|
+
console.warn(`Invalid regex for pattern ${pattern.name}: ${pattern.regex}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Find all pattern matches in text
|
|
909
|
+
*/
|
|
910
|
+
match(text) {
|
|
911
|
+
const matches = [];
|
|
912
|
+
for (const pattern of this.patterns) {
|
|
913
|
+
const regex = this.compiledPatterns.get(pattern.name);
|
|
914
|
+
if (!regex) continue;
|
|
915
|
+
regex.lastIndex = 0;
|
|
916
|
+
let match;
|
|
917
|
+
while ((match = regex.exec(text)) !== null) {
|
|
918
|
+
matches.push({
|
|
919
|
+
pattern,
|
|
920
|
+
matchedText: match[0],
|
|
921
|
+
index: match.index
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return matches;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Check if any blocking patterns match the text
|
|
929
|
+
*/
|
|
930
|
+
shouldBlock(text) {
|
|
931
|
+
const matches = this.match(text);
|
|
932
|
+
return matches.some((m) => m.pattern.action === "block");
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Get the highest severity level from all matches
|
|
936
|
+
*/
|
|
937
|
+
getHighestSeverity(text) {
|
|
938
|
+
const matches = this.match(text);
|
|
939
|
+
if (matches.length === 0) return null;
|
|
940
|
+
let highest = "low";
|
|
941
|
+
for (const m of matches) {
|
|
942
|
+
if (SEVERITY_ORDER[m.pattern.severity] > SEVERITY_ORDER[highest]) {
|
|
943
|
+
highest = m.pattern.severity;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return highest;
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Redact sensitive data from text
|
|
950
|
+
*/
|
|
951
|
+
redact(text) {
|
|
952
|
+
const matches = this.match(text);
|
|
953
|
+
const redactions = [];
|
|
954
|
+
let redacted = text;
|
|
955
|
+
const sortedMatches = [...matches].sort((a, b) => b.index - a.index);
|
|
956
|
+
for (const m of sortedMatches) {
|
|
957
|
+
const replacement = `[REDACTED:${m.pattern.name}]`;
|
|
958
|
+
let redactionType;
|
|
959
|
+
if (m.pattern.name === "api_key" || m.pattern.name === "aws_secret" || m.pattern.name === "private_key" || m.pattern.name === "github_token" || m.pattern.name === "openai_key" || m.pattern.name === "jwt_token") {
|
|
960
|
+
redactionType = "api_key";
|
|
961
|
+
} else if (m.pattern.name === "email") {
|
|
962
|
+
redactionType = "email";
|
|
963
|
+
} else if (m.pattern.name === "phone_us") {
|
|
964
|
+
redactionType = "phone";
|
|
965
|
+
} else if (m.pattern.name === "ssn") {
|
|
966
|
+
redactionType = "ssn";
|
|
967
|
+
} else if (m.pattern.name === "credit_card") {
|
|
968
|
+
redactionType = "credit_card";
|
|
969
|
+
} else {
|
|
970
|
+
redactionType = "custom";
|
|
971
|
+
}
|
|
972
|
+
redactions.push({
|
|
973
|
+
type: redactionType,
|
|
974
|
+
replacement
|
|
975
|
+
});
|
|
976
|
+
redacted = redacted.substring(0, m.index) + replacement + redacted.substring(m.index + m.matchedText.length);
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
original: text,
|
|
980
|
+
redacted,
|
|
981
|
+
redactions: redactions.reverse()
|
|
982
|
+
// Return in original order
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Add a custom pattern
|
|
987
|
+
*/
|
|
988
|
+
addPattern(pattern) {
|
|
989
|
+
this.patterns.push(pattern);
|
|
990
|
+
try {
|
|
991
|
+
this.compiledPatterns.set(pattern.name, new RegExp(pattern.regex, "gi"));
|
|
992
|
+
} catch {
|
|
993
|
+
console.warn(`Invalid regex for pattern ${pattern.name}: ${pattern.regex}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Get all configured patterns
|
|
998
|
+
*/
|
|
999
|
+
getPatterns() {
|
|
1000
|
+
return [...this.patterns];
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Test helper - returns matches, shouldBlock, and redacted result
|
|
1004
|
+
*/
|
|
1005
|
+
test(text) {
|
|
1006
|
+
return {
|
|
1007
|
+
matches: this.match(text),
|
|
1008
|
+
shouldBlock: this.shouldBlock(text),
|
|
1009
|
+
redacted: this.redact(text)
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
// src/policy/ward/egress.ts
|
|
1015
|
+
var DashboardDB2 = null;
|
|
1016
|
+
try {
|
|
1017
|
+
const dbModule = await import("./db-EHQDB5OL.js");
|
|
1018
|
+
DashboardDB2 = dbModule.DashboardDB;
|
|
1019
|
+
} catch {
|
|
1020
|
+
}
|
|
1021
|
+
var DEFAULT_EGRESS_CONFIG = {
|
|
1022
|
+
enabled: true,
|
|
1023
|
+
defaultAction: "block",
|
|
1024
|
+
allowlist: []
|
|
1025
|
+
};
|
|
1026
|
+
var EgressMonitor = class {
|
|
1027
|
+
config;
|
|
1028
|
+
matcher;
|
|
1029
|
+
db = null;
|
|
1030
|
+
allowlist;
|
|
1031
|
+
constructor(config) {
|
|
1032
|
+
this.config = { ...DEFAULT_EGRESS_CONFIG, ...config };
|
|
1033
|
+
this.matcher = new EgressPatternMatcher();
|
|
1034
|
+
this.allowlist = [...this.config.allowlist];
|
|
1035
|
+
if (DashboardDB2) {
|
|
1036
|
+
try {
|
|
1037
|
+
this.db = new DashboardDB2();
|
|
1038
|
+
} catch {
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Inspect content for sensitive data
|
|
1044
|
+
*
|
|
1045
|
+
* @param content - The content to inspect
|
|
1046
|
+
* @param connector - Optional connector name for allowlist checking
|
|
1047
|
+
* @param destination - Optional destination for allowlist checking
|
|
1048
|
+
* @returns Inspection result with blocking decision and matches
|
|
1049
|
+
*/
|
|
1050
|
+
inspect(content, connector, destination) {
|
|
1051
|
+
if (this.isAllowlisted(connector, destination)) {
|
|
1052
|
+
return {
|
|
1053
|
+
blocked: false,
|
|
1054
|
+
allowlisted: true,
|
|
1055
|
+
matches: [],
|
|
1056
|
+
redacted: content
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
const allMatches = this.matcher.match(content);
|
|
1060
|
+
const effectiveMatches = allMatches.filter((match) => {
|
|
1061
|
+
return !this.isPatternAllowlisted(match.pattern.name, connector, destination);
|
|
1062
|
+
});
|
|
1063
|
+
const shouldBlock = effectiveMatches.some((m) => m.pattern.action === "block");
|
|
1064
|
+
let redacted = content;
|
|
1065
|
+
if (effectiveMatches.length > 0) {
|
|
1066
|
+
const sortedMatches = [...effectiveMatches].sort((a, b) => b.index - a.index);
|
|
1067
|
+
for (const m of sortedMatches) {
|
|
1068
|
+
const replacement = `[REDACTED:${m.pattern.name}]`;
|
|
1069
|
+
redacted = redacted.substring(0, m.index) + replacement + redacted.substring(m.index + m.matchedText.length);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
const wasAllowlisted = allMatches.length > 0 && effectiveMatches.length === 0;
|
|
1073
|
+
const result = {
|
|
1074
|
+
blocked: shouldBlock,
|
|
1075
|
+
allowlisted: wasAllowlisted,
|
|
1076
|
+
matches: effectiveMatches,
|
|
1077
|
+
redacted
|
|
1078
|
+
};
|
|
1079
|
+
if (shouldBlock && this.db) {
|
|
1080
|
+
try {
|
|
1081
|
+
for (const match of effectiveMatches.filter((m) => m.pattern.action === "block")) {
|
|
1082
|
+
const blockId = this.db.insertEgressBlock({
|
|
1083
|
+
pattern: match.pattern,
|
|
1084
|
+
matchedText: match.matchedText.substring(0, 100),
|
|
1085
|
+
// Truncate for safety
|
|
1086
|
+
redactedText: redacted,
|
|
1087
|
+
connector,
|
|
1088
|
+
destination
|
|
1089
|
+
});
|
|
1090
|
+
result.blockId = blockId;
|
|
1091
|
+
}
|
|
1092
|
+
this.db.insertEvent({
|
|
1093
|
+
source: "ward",
|
|
1094
|
+
level: "warn",
|
|
1095
|
+
category: "egress",
|
|
1096
|
+
message: `Blocked egress: ${effectiveMatches.map((m) => m.pattern.name).join(", ")}`,
|
|
1097
|
+
data: {
|
|
1098
|
+
connector,
|
|
1099
|
+
destination,
|
|
1100
|
+
patternNames: effectiveMatches.map((m) => m.pattern.name)
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
} catch {
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
return result;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Check if connector/destination combination is fully allowlisted
|
|
1110
|
+
*/
|
|
1111
|
+
isAllowlisted(connector, destination) {
|
|
1112
|
+
return this.allowlist.some((entry) => {
|
|
1113
|
+
if (entry.pattern) return false;
|
|
1114
|
+
if (entry.connector && entry.destination) {
|
|
1115
|
+
return entry.connector === connector && entry.destination === destination;
|
|
1116
|
+
}
|
|
1117
|
+
if (entry.connector) {
|
|
1118
|
+
return entry.connector === connector;
|
|
1119
|
+
}
|
|
1120
|
+
if (entry.destination) {
|
|
1121
|
+
return entry.destination === destination;
|
|
1122
|
+
}
|
|
1123
|
+
return false;
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Check if a specific pattern is allowlisted for the connector/destination
|
|
1128
|
+
*/
|
|
1129
|
+
isPatternAllowlisted(patternName, connector, destination) {
|
|
1130
|
+
return this.allowlist.some((entry) => {
|
|
1131
|
+
if (!entry.pattern || entry.pattern !== patternName) return false;
|
|
1132
|
+
if (entry.connector && entry.destination) {
|
|
1133
|
+
return entry.connector === connector && entry.destination === destination;
|
|
1134
|
+
}
|
|
1135
|
+
if (entry.connector) {
|
|
1136
|
+
return entry.connector === connector;
|
|
1137
|
+
}
|
|
1138
|
+
if (entry.destination) {
|
|
1139
|
+
return entry.destination === destination;
|
|
1140
|
+
}
|
|
1141
|
+
return true;
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Add an allowlist entry
|
|
1146
|
+
*/
|
|
1147
|
+
addAllowlistEntry(entry) {
|
|
1148
|
+
this.allowlist.push(entry);
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Add a custom pattern
|
|
1152
|
+
*/
|
|
1153
|
+
addPattern(pattern) {
|
|
1154
|
+
this.matcher.addPattern(pattern);
|
|
1155
|
+
}
|
|
1156
|
+
/**
|
|
1157
|
+
* Get pending blocks from database
|
|
1158
|
+
*/
|
|
1159
|
+
getPendingBlocks() {
|
|
1160
|
+
if (!this.db) return [];
|
|
1161
|
+
try {
|
|
1162
|
+
return this.db.getPendingBlocks();
|
|
1163
|
+
} catch {
|
|
1164
|
+
return [];
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Approve a pending block
|
|
1169
|
+
*/
|
|
1170
|
+
approveBlock(id, approvedBy) {
|
|
1171
|
+
if (!this.db) return;
|
|
1172
|
+
try {
|
|
1173
|
+
this.db.approveBlock(id, approvedBy ?? "system");
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Deny a pending block
|
|
1179
|
+
*/
|
|
1180
|
+
denyBlock(id) {
|
|
1181
|
+
if (!this.db) return;
|
|
1182
|
+
try {
|
|
1183
|
+
this.db.denyBlock(id, "system");
|
|
1184
|
+
} catch {
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Test content without recording to database
|
|
1189
|
+
* Useful for CLI testing of patterns
|
|
1190
|
+
*/
|
|
1191
|
+
test(content) {
|
|
1192
|
+
const matches = this.matcher.match(content);
|
|
1193
|
+
const shouldBlock = matches.some((m) => m.pattern.action === "block");
|
|
1194
|
+
let redacted = content;
|
|
1195
|
+
if (matches.length > 0) {
|
|
1196
|
+
const sortedMatches = [...matches].sort((a, b) => b.index - a.index);
|
|
1197
|
+
for (const m of sortedMatches) {
|
|
1198
|
+
const replacement = `[REDACTED:${m.pattern.name}]`;
|
|
1199
|
+
redacted = redacted.substring(0, m.index) + replacement + redacted.substring(m.index + m.matchedText.length);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return {
|
|
1203
|
+
blocked: shouldBlock,
|
|
1204
|
+
matches,
|
|
1205
|
+
redacted
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
|
|
1210
|
+
// src/doctor.ts
|
|
1211
|
+
async function checkAgentConfigs(agents) {
|
|
1212
|
+
const checks = [];
|
|
1213
|
+
for (const agent of agents) {
|
|
1214
|
+
const agentName = agent.agent.charAt(0).toUpperCase() + agent.agent.slice(1).replace("-", " ");
|
|
1215
|
+
if (agent.configPath && !agent.configExists) {
|
|
1216
|
+
checks.push({
|
|
1217
|
+
name: `${agentName} config`,
|
|
1218
|
+
passed: false,
|
|
1219
|
+
message: `Config path known (${agent.configPath}) but file not found`
|
|
1220
|
+
});
|
|
1221
|
+
} else if (agent.configExists) {
|
|
1222
|
+
checks.push({
|
|
1223
|
+
name: `${agentName} config`,
|
|
1224
|
+
passed: true,
|
|
1225
|
+
message: `Found at ${agent.configPath}`
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
if (agent.agent === "claude-code") {
|
|
1229
|
+
checks.push({
|
|
1230
|
+
name: `${agentName} integration`,
|
|
1231
|
+
passed: agent.bashbrosIntegrated,
|
|
1232
|
+
message: agent.bashbrosIntegrated ? "BashBros hooks installed" : 'Hooks not installed. Run "bashbros hook install" to protect Claude Code'
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
if (agent.agent === "moltbot" || agent.agent === "clawdbot") {
|
|
1236
|
+
checks.push({
|
|
1237
|
+
name: `${agentName} integration`,
|
|
1238
|
+
passed: agent.bashbrosIntegrated,
|
|
1239
|
+
message: agent.bashbrosIntegrated ? "BashBros hooks installed" : 'Hooks not installed. Run "bashbros moltbot install" to protect moltbot'
|
|
1240
|
+
});
|
|
1241
|
+
try {
|
|
1242
|
+
const { MoltbotHooks: MoltbotHooks2 } = await import("./moltbot-DXZFVK3X.js");
|
|
1243
|
+
const status = MoltbotHooks2.getStatus();
|
|
1244
|
+
if (status.sandboxMode) {
|
|
1245
|
+
checks.push({
|
|
1246
|
+
name: `${agentName} sandbox`,
|
|
1247
|
+
passed: status.sandboxMode === "strict",
|
|
1248
|
+
message: status.sandboxMode === "strict" ? "Sandbox mode enabled (strict)" : `Sandbox mode: ${status.sandboxMode} (consider "strict" for better security)`
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
if (status.gatewayRunning) {
|
|
1252
|
+
const gatewayStatus = await MoltbotHooks2.getGatewayStatus();
|
|
1253
|
+
checks.push({
|
|
1254
|
+
name: `${agentName} gateway`,
|
|
1255
|
+
passed: gatewayStatus.running,
|
|
1256
|
+
message: gatewayStatus.running ? `Gateway running on port ${gatewayStatus.port}` : "Gateway configured but not running"
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
} catch {
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (agent.permissions) {
|
|
1263
|
+
const config = loadConfig();
|
|
1264
|
+
if ((!agent.permissions.allowedPaths || agent.permissions.allowedPaths.length === 0) && config.paths.allow.length > 0 && !config.paths.allow.includes("*")) {
|
|
1265
|
+
checks.push({
|
|
1266
|
+
name: `${agentName} path policy`,
|
|
1267
|
+
passed: true,
|
|
1268
|
+
// Not a failure, just a note
|
|
1269
|
+
message: `Agent has no path restrictions; bashbros will enforce: ${config.paths.allow.slice(0, 3).join(", ")}`
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
return checks;
|
|
1275
|
+
}
|
|
1276
|
+
async function runDoctor() {
|
|
1277
|
+
console.log(chalk2.bold("\nRunning diagnostics...\n"));
|
|
1278
|
+
const checks = [];
|
|
1279
|
+
const configPath = findConfig();
|
|
1280
|
+
checks.push({
|
|
1281
|
+
name: "Config file",
|
|
1282
|
+
passed: configPath !== null,
|
|
1283
|
+
message: configPath ? `Found at ${configPath}` : 'Not found. Run "bashbros init" to create one.'
|
|
1284
|
+
});
|
|
1285
|
+
if (configPath) {
|
|
1286
|
+
try {
|
|
1287
|
+
const config = loadConfig(configPath);
|
|
1288
|
+
checks.push({
|
|
1289
|
+
name: "Config valid",
|
|
1290
|
+
passed: true,
|
|
1291
|
+
message: `Profile: ${config.profile}, Agent: ${config.agent}`
|
|
1292
|
+
});
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
checks.push({
|
|
1295
|
+
name: "Config valid",
|
|
1296
|
+
passed: false,
|
|
1297
|
+
message: `Parse error: ${error}`
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
const auditDir = join3(homedir3(), ".bashbros");
|
|
1302
|
+
checks.push({
|
|
1303
|
+
name: "Audit directory",
|
|
1304
|
+
passed: existsSync4(auditDir),
|
|
1305
|
+
message: existsSync4(auditDir) ? `Found at ${auditDir}` : "Will be created on first run"
|
|
1306
|
+
});
|
|
1307
|
+
try {
|
|
1308
|
+
await import("node-pty");
|
|
1309
|
+
checks.push({
|
|
1310
|
+
name: "PTY support",
|
|
1311
|
+
passed: true,
|
|
1312
|
+
message: "node-pty loaded successfully"
|
|
1313
|
+
});
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
checks.push({
|
|
1316
|
+
name: "PTY support",
|
|
1317
|
+
passed: false,
|
|
1318
|
+
message: 'node-pty not available. Run "npm install" to install dependencies.'
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
if (configPath) {
|
|
1322
|
+
const config = loadConfig(configPath);
|
|
1323
|
+
const secretsEnabled = config.secrets.enabled;
|
|
1324
|
+
checks.push({
|
|
1325
|
+
name: "Secrets protection",
|
|
1326
|
+
passed: secretsEnabled,
|
|
1327
|
+
message: secretsEnabled ? `Enabled with ${config.secrets.patterns.length} patterns` : "Disabled - credentials may be exposed"
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
if (configPath) {
|
|
1331
|
+
const config = loadConfig(configPath);
|
|
1332
|
+
checks.push({
|
|
1333
|
+
name: "Rate limiting",
|
|
1334
|
+
passed: config.rateLimit.enabled,
|
|
1335
|
+
message: config.rateLimit.enabled ? `${config.rateLimit.maxPerMinute}/min, ${config.rateLimit.maxPerHour}/hr` : "Disabled - runaway agents possible"
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
const agents = await getAllAgentConfigs();
|
|
1339
|
+
const installedAgents = agents.filter((a) => a.installed);
|
|
1340
|
+
if (installedAgents.length > 0) {
|
|
1341
|
+
const agentChecks = await checkAgentConfigs(installedAgents);
|
|
1342
|
+
checks.push(...agentChecks);
|
|
1343
|
+
} else {
|
|
1344
|
+
checks.push({
|
|
1345
|
+
name: "Agent detection",
|
|
1346
|
+
passed: true,
|
|
1347
|
+
message: "No agents detected (install claude, aider, etc. to use bashbros protection)"
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
if (configPath) {
|
|
1351
|
+
const config = loadConfig(configPath);
|
|
1352
|
+
const wardEnabled = config.ward?.enabled ?? false;
|
|
1353
|
+
checks.push({
|
|
1354
|
+
name: "Ward security",
|
|
1355
|
+
passed: wardEnabled,
|
|
1356
|
+
message: wardEnabled ? `Enabled (exposure scan: ${config.ward.exposure.scanInterval}ms)` : "Disabled - network exposure monitoring off"
|
|
1357
|
+
});
|
|
1358
|
+
}
|
|
1359
|
+
if (configPath) {
|
|
1360
|
+
const config = loadConfig(configPath);
|
|
1361
|
+
const dashEnabled = config.dashboard?.enabled ?? false;
|
|
1362
|
+
checks.push({
|
|
1363
|
+
name: "Dashboard",
|
|
1364
|
+
passed: dashEnabled,
|
|
1365
|
+
message: dashEnabled ? `Enabled on ${config.dashboard.bind}:${config.dashboard.port}` : 'Disabled - run "bashbros dashboard" to start'
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
const scanner = new ExposureScanner();
|
|
1370
|
+
const agentSignatures = scanner.getAgents();
|
|
1371
|
+
checks.push({
|
|
1372
|
+
name: "Exposure scanner",
|
|
1373
|
+
passed: true,
|
|
1374
|
+
message: `Ready, monitoring ${agentSignatures.length} agent signatures`
|
|
1375
|
+
});
|
|
1376
|
+
} catch {
|
|
1377
|
+
checks.push({
|
|
1378
|
+
name: "Exposure scanner",
|
|
1379
|
+
passed: false,
|
|
1380
|
+
message: "Failed to initialize exposure scanner"
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
try {
|
|
1384
|
+
const matcher = new EgressPatternMatcher();
|
|
1385
|
+
const patterns = matcher.getPatterns();
|
|
1386
|
+
const credPatterns = patterns.filter((p) => p.category === "credentials").length;
|
|
1387
|
+
const piiPatterns = patterns.filter((p) => p.category === "pii").length;
|
|
1388
|
+
checks.push({
|
|
1389
|
+
name: "Egress patterns",
|
|
1390
|
+
passed: true,
|
|
1391
|
+
message: `Loaded ${patterns.length} patterns (${credPatterns} credential, ${piiPatterns} PII)`
|
|
1392
|
+
});
|
|
1393
|
+
} catch {
|
|
1394
|
+
checks.push({
|
|
1395
|
+
name: "Egress patterns",
|
|
1396
|
+
passed: false,
|
|
1397
|
+
message: "Failed to initialize egress patterns"
|
|
1398
|
+
});
|
|
1399
|
+
}
|
|
1400
|
+
let passed = 0;
|
|
1401
|
+
let failed = 0;
|
|
1402
|
+
for (const check of checks) {
|
|
1403
|
+
const icon = check.passed ? chalk2.green("\u2713") : chalk2.red("\u2717");
|
|
1404
|
+
const status = check.passed ? chalk2.green("OK") : chalk2.red("FAIL");
|
|
1405
|
+
console.log(` ${icon} ${chalk2.bold(check.name)}: ${status}`);
|
|
1406
|
+
console.log(chalk2.dim(` ${check.message}`));
|
|
1407
|
+
console.log();
|
|
1408
|
+
if (check.passed) passed++;
|
|
1409
|
+
else failed++;
|
|
1410
|
+
}
|
|
1411
|
+
console.log(chalk2.bold("\u2500".repeat(40)));
|
|
1412
|
+
if (failed === 0) {
|
|
1413
|
+
console.log(chalk2.green(`
|
|
1414
|
+
\u2713 All ${passed} checks passed. BashBros is ready.
|
|
1415
|
+
`));
|
|
1416
|
+
} else {
|
|
1417
|
+
console.log(chalk2.yellow(`
|
|
1418
|
+
${passed} passed, ${failed} failed. Fix issues above.
|
|
1419
|
+
`));
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// src/watch.ts
|
|
1424
|
+
import chalk3 from "chalk";
|
|
1425
|
+
async function startWatch(options) {
|
|
1426
|
+
const configPath = findConfig();
|
|
1427
|
+
if (!configPath) {
|
|
1428
|
+
console.log(chalk3.red('No config found. Run "bashbros init" first.'));
|
|
1429
|
+
process.exit(1);
|
|
1430
|
+
}
|
|
1431
|
+
console.log(chalk3.dim(` "I watch your agent's back so you don't have to."`));
|
|
1432
|
+
console.log();
|
|
1433
|
+
console.log(chalk3.green("\u2713"), "Protection active");
|
|
1434
|
+
console.log(chalk3.dim(` Config: ${configPath}`));
|
|
1435
|
+
console.log(chalk3.dim(" Press Ctrl+C to stop"));
|
|
1436
|
+
console.log();
|
|
1437
|
+
const bashbros = new BashBros(configPath);
|
|
1438
|
+
bashbros.on("blocked", (command, violations) => {
|
|
1439
|
+
console.log();
|
|
1440
|
+
console.log(chalk3.red("\u{1F6E1}\uFE0F BashBros blocked a command"));
|
|
1441
|
+
console.log();
|
|
1442
|
+
console.log(chalk3.dim(" Command:"), command);
|
|
1443
|
+
console.log(chalk3.dim(" Reason:"), violations[0].message);
|
|
1444
|
+
console.log(chalk3.dim(" Policy:"), violations[0].rule);
|
|
1445
|
+
console.log();
|
|
1446
|
+
console.log(chalk3.dim(" To allow this command:"));
|
|
1447
|
+
console.log(chalk3.cyan(` bashbros allow "${command}" --once`));
|
|
1448
|
+
console.log(chalk3.cyan(` bashbros allow "${command}" --persist`));
|
|
1449
|
+
console.log();
|
|
1450
|
+
});
|
|
1451
|
+
bashbros.on("allowed", (result) => {
|
|
1452
|
+
if (options.verbose) {
|
|
1453
|
+
console.log(chalk3.green("\u2713"), chalk3.dim(result.command));
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
bashbros.on("output", (data) => {
|
|
1457
|
+
process.stdout.write(data);
|
|
1458
|
+
});
|
|
1459
|
+
bashbros.on("error", (error) => {
|
|
1460
|
+
console.error(chalk3.red("Error:"), error.message);
|
|
1461
|
+
});
|
|
1462
|
+
process.on("SIGINT", () => {
|
|
1463
|
+
console.log();
|
|
1464
|
+
console.log(chalk3.yellow("Stopping BashBros..."));
|
|
1465
|
+
bashbros.stop();
|
|
1466
|
+
process.exit(0);
|
|
1467
|
+
});
|
|
1468
|
+
process.on("SIGTERM", () => {
|
|
1469
|
+
bashbros.stop();
|
|
1470
|
+
process.exit(0);
|
|
1471
|
+
});
|
|
1472
|
+
bashbros.start();
|
|
1473
|
+
await new Promise(() => {
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/allow.ts
|
|
1478
|
+
import chalk4 from "chalk";
|
|
1479
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
1480
|
+
import { parse, stringify as stringify2 } from "yaml";
|
|
1481
|
+
async function handleAllow(command, options) {
|
|
1482
|
+
if (options.once) {
|
|
1483
|
+
allowForSession(command);
|
|
1484
|
+
console.log(chalk4.green("\u2713"), `Allowed for this session: ${command}`);
|
|
1485
|
+
console.log(chalk4.dim(" This will reset when BashBros restarts."));
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
if (options.persist) {
|
|
1489
|
+
const configPath = findConfig();
|
|
1490
|
+
if (!configPath) {
|
|
1491
|
+
console.log(chalk4.red('No config found. Run "bashbros init" first.'));
|
|
1492
|
+
process.exit(1);
|
|
1493
|
+
}
|
|
1494
|
+
try {
|
|
1495
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
1496
|
+
const config = parse(content);
|
|
1497
|
+
if (!config.commands) {
|
|
1498
|
+
config.commands = { allow: [], block: [] };
|
|
1499
|
+
}
|
|
1500
|
+
if (!config.commands.allow) {
|
|
1501
|
+
config.commands.allow = [];
|
|
1502
|
+
}
|
|
1503
|
+
if (!config.commands.allow.includes(command)) {
|
|
1504
|
+
config.commands.allow.push(command);
|
|
1505
|
+
writeFileSync2(configPath, stringify2(config));
|
|
1506
|
+
console.log(chalk4.green("\u2713"), `Added to allowlist: ${command}`);
|
|
1507
|
+
console.log(chalk4.dim(` Updated ${configPath}`));
|
|
1508
|
+
} else {
|
|
1509
|
+
console.log(chalk4.yellow("Already in allowlist:"), command);
|
|
1510
|
+
}
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
console.error(chalk4.red("Failed to update config:"), error);
|
|
1513
|
+
process.exit(1);
|
|
1514
|
+
}
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
console.log(chalk4.yellow("Specify how to allow this command:"));
|
|
1518
|
+
console.log();
|
|
1519
|
+
console.log(chalk4.cyan(` bashbros allow "${command}" --once`));
|
|
1520
|
+
console.log(chalk4.dim(" Allow for current session only"));
|
|
1521
|
+
console.log();
|
|
1522
|
+
console.log(chalk4.cyan(` bashbros allow "${command}" --persist`));
|
|
1523
|
+
console.log(chalk4.dim(" Add to config file permanently"));
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
// src/dashboard/server.ts
|
|
1527
|
+
import express from "express";
|
|
1528
|
+
import { WebSocketServer } from "ws";
|
|
1529
|
+
import { createServer } from "http";
|
|
1530
|
+
import { fileURLToPath } from "url";
|
|
1531
|
+
import { dirname, join as join4 } from "path";
|
|
1532
|
+
var DashboardServer = class {
|
|
1533
|
+
app;
|
|
1534
|
+
server = null;
|
|
1535
|
+
wss = null;
|
|
1536
|
+
db;
|
|
1537
|
+
port;
|
|
1538
|
+
bind;
|
|
1539
|
+
clients = /* @__PURE__ */ new Set();
|
|
1540
|
+
constructor(config = {}) {
|
|
1541
|
+
this.port = config.port ?? 17800;
|
|
1542
|
+
this.bind = config.bind ?? "127.0.0.1";
|
|
1543
|
+
this.db = new DashboardDB(config.dbPath ?? ":memory:");
|
|
1544
|
+
this.app = express();
|
|
1545
|
+
this.setupMiddleware();
|
|
1546
|
+
this.setupRoutes();
|
|
1547
|
+
}
|
|
1548
|
+
setupMiddleware() {
|
|
1549
|
+
this.app.use(express.json());
|
|
1550
|
+
this.app.use((_req, res, next) => {
|
|
1551
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
1552
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1553
|
+
res.header("Access-Control-Allow-Headers", "Content-Type");
|
|
1554
|
+
next();
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
setupRoutes() {
|
|
1558
|
+
this.app.get("/api/health", (_req, res) => {
|
|
1559
|
+
res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1560
|
+
});
|
|
1561
|
+
this.app.get("/api/stats", (_req, res) => {
|
|
1562
|
+
try {
|
|
1563
|
+
const stats = this.db.getStats();
|
|
1564
|
+
res.json(stats);
|
|
1565
|
+
} catch (error) {
|
|
1566
|
+
res.status(500).json({ error: "Failed to fetch stats" });
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
this.app.get("/api/events", (req, res) => {
|
|
1570
|
+
try {
|
|
1571
|
+
const filter = {};
|
|
1572
|
+
if (req.query.source) filter.source = req.query.source;
|
|
1573
|
+
if (req.query.level) filter.level = req.query.level;
|
|
1574
|
+
if (req.query.category) filter.category = req.query.category;
|
|
1575
|
+
if (req.query.limit) filter.limit = parseInt(req.query.limit, 10);
|
|
1576
|
+
if (req.query.offset) filter.offset = parseInt(req.query.offset, 10);
|
|
1577
|
+
if (req.query.since) filter.since = new Date(req.query.since);
|
|
1578
|
+
const events = this.db.getEvents(filter);
|
|
1579
|
+
res.json(events);
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
res.status(500).json({ error: "Failed to fetch events" });
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
this.app.get("/api/connectors", (_req, res) => {
|
|
1585
|
+
try {
|
|
1586
|
+
const events = this.db.getAllConnectorEvents(1e3);
|
|
1587
|
+
const connectors = [...new Set(events.map((e) => e.connector))];
|
|
1588
|
+
res.json(connectors);
|
|
1589
|
+
} catch (error) {
|
|
1590
|
+
res.status(500).json({ error: "Failed to fetch connectors" });
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
this.app.get("/api/connectors/:name/events", (req, res) => {
|
|
1594
|
+
try {
|
|
1595
|
+
const limit = req.query.limit ? parseInt(req.query.limit, 10) : 100;
|
|
1596
|
+
const events = this.db.getConnectorEvents(req.params.name, limit);
|
|
1597
|
+
res.json(events);
|
|
1598
|
+
} catch (error) {
|
|
1599
|
+
res.status(500).json({ error: "Failed to fetch connector events" });
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
this.app.get("/api/blocked", (_req, res) => {
|
|
1603
|
+
try {
|
|
1604
|
+
const blocks = this.db.getPendingBlocks();
|
|
1605
|
+
res.json(blocks);
|
|
1606
|
+
} catch (error) {
|
|
1607
|
+
res.status(500).json({ error: "Failed to fetch blocked items" });
|
|
1608
|
+
}
|
|
1609
|
+
});
|
|
1610
|
+
this.app.post("/api/blocked/:id/approve", (req, res) => {
|
|
1611
|
+
try {
|
|
1612
|
+
const { id } = req.params;
|
|
1613
|
+
const approvedBy = req.body?.approvedBy ?? "dashboard-user";
|
|
1614
|
+
const block = this.db.getBlock(id);
|
|
1615
|
+
if (!block) {
|
|
1616
|
+
res.status(404).json({ error: "Block not found" });
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
this.db.approveBlock(id, approvedBy);
|
|
1620
|
+
this.broadcast({ type: "block-approved", id });
|
|
1621
|
+
res.json({ success: true });
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
res.status(500).json({ error: "Failed to approve block" });
|
|
1624
|
+
}
|
|
1625
|
+
});
|
|
1626
|
+
this.app.post("/api/blocked/:id/deny", (req, res) => {
|
|
1627
|
+
try {
|
|
1628
|
+
const { id } = req.params;
|
|
1629
|
+
const deniedBy = req.body?.deniedBy ?? "dashboard-user";
|
|
1630
|
+
const block = this.db.getBlock(id);
|
|
1631
|
+
if (!block) {
|
|
1632
|
+
res.status(404).json({ error: "Block not found" });
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
this.db.denyBlock(id, deniedBy);
|
|
1636
|
+
this.broadcast({ type: "block-denied", id });
|
|
1637
|
+
res.json({ success: true });
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
res.status(500).json({ error: "Failed to deny block" });
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1643
|
+
const __dirname = dirname(__filename);
|
|
1644
|
+
const staticPath = join4(__dirname, "static");
|
|
1645
|
+
this.app.use(express.static(staticPath));
|
|
1646
|
+
this.app.get("/{*path}", (_req, res) => {
|
|
1647
|
+
res.sendFile(join4(staticPath, "index.html"));
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
setupWebSocket() {
|
|
1651
|
+
if (!this.server) return;
|
|
1652
|
+
this.wss = new WebSocketServer({ server: this.server });
|
|
1653
|
+
this.wss.on("connection", (ws) => {
|
|
1654
|
+
this.clients.add(ws);
|
|
1655
|
+
ws.on("close", () => {
|
|
1656
|
+
this.clients.delete(ws);
|
|
1657
|
+
});
|
|
1658
|
+
ws.on("error", () => {
|
|
1659
|
+
this.clients.delete(ws);
|
|
1660
|
+
});
|
|
1661
|
+
try {
|
|
1662
|
+
const stats = this.db.getStats();
|
|
1663
|
+
ws.send(JSON.stringify({ type: "stats", data: stats }));
|
|
1664
|
+
} catch {
|
|
1665
|
+
}
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
/**
|
|
1669
|
+
* Start the dashboard server
|
|
1670
|
+
*/
|
|
1671
|
+
async start() {
|
|
1672
|
+
return new Promise((resolve, reject) => {
|
|
1673
|
+
this.server = createServer(this.app);
|
|
1674
|
+
this.setupWebSocket();
|
|
1675
|
+
this.server.on("error", (error) => {
|
|
1676
|
+
if (error.code === "EADDRINUSE") {
|
|
1677
|
+
reject(new Error(`Port ${this.port} is already in use`));
|
|
1678
|
+
} else {
|
|
1679
|
+
reject(error);
|
|
1680
|
+
}
|
|
1681
|
+
});
|
|
1682
|
+
this.server.listen(this.port, this.bind, () => {
|
|
1683
|
+
resolve();
|
|
1684
|
+
});
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Stop the dashboard server
|
|
1689
|
+
*/
|
|
1690
|
+
async stop() {
|
|
1691
|
+
return new Promise((resolve) => {
|
|
1692
|
+
for (const client of this.clients) {
|
|
1693
|
+
client.close();
|
|
1694
|
+
}
|
|
1695
|
+
this.clients.clear();
|
|
1696
|
+
if (this.wss) {
|
|
1697
|
+
this.wss.close();
|
|
1698
|
+
this.wss = null;
|
|
1699
|
+
}
|
|
1700
|
+
if (this.server) {
|
|
1701
|
+
this.server.close(() => {
|
|
1702
|
+
this.server = null;
|
|
1703
|
+
this.db.close();
|
|
1704
|
+
resolve();
|
|
1705
|
+
});
|
|
1706
|
+
} else {
|
|
1707
|
+
this.db.close();
|
|
1708
|
+
resolve();
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
/**
|
|
1713
|
+
* Broadcast a message to all connected WebSocket clients
|
|
1714
|
+
*/
|
|
1715
|
+
broadcast(message) {
|
|
1716
|
+
const data = JSON.stringify(message);
|
|
1717
|
+
for (const client of this.clients) {
|
|
1718
|
+
if (client.readyState === 1) {
|
|
1719
|
+
client.send(data);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Get the database instance for external use
|
|
1725
|
+
*/
|
|
1726
|
+
getDB() {
|
|
1727
|
+
return this.db;
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Get the port the server is running on
|
|
1731
|
+
*/
|
|
1732
|
+
getPort() {
|
|
1733
|
+
return this.port;
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
|
|
1737
|
+
// src/cli.ts
|
|
1738
|
+
var metricsCollector = null;
|
|
1739
|
+
var costEstimator = null;
|
|
1740
|
+
var loopDetector = null;
|
|
1741
|
+
var undoStack = null;
|
|
1742
|
+
var logo = `
|
|
1743
|
+
\u2571BashBros \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1744
|
+
\u{1F91D} Your Friendly Bash Agent Helper
|
|
1745
|
+
`;
|
|
1746
|
+
var program = new Command();
|
|
1747
|
+
program.name("bashbros").description("The Bash Agent Helper").version("0.1.0");
|
|
1748
|
+
program.command("init").description("Set up BashBros for your project").action(async () => {
|
|
1749
|
+
console.log(chalk5.cyan(logo));
|
|
1750
|
+
await runOnboarding();
|
|
1751
|
+
});
|
|
1752
|
+
program.command("watch").description("Start protecting your agent").option("-v, --verbose", "Show all commands as they run").action(async (options) => {
|
|
1753
|
+
console.log(chalk5.cyan(logo));
|
|
1754
|
+
await startWatch(options);
|
|
1755
|
+
});
|
|
1756
|
+
program.command("doctor").description("Check your BashBros configuration").action(async () => {
|
|
1757
|
+
console.log(chalk5.cyan(logo));
|
|
1758
|
+
await runDoctor();
|
|
1759
|
+
});
|
|
1760
|
+
program.command("allow <command>").description("Allow a specific command").option("--once", "Allow only for current session").option("--persist", "Add to config permanently").action(async (command, options) => {
|
|
1761
|
+
await handleAllow(command, options);
|
|
1762
|
+
});
|
|
1763
|
+
program.command("audit").description("View recent command history").option("-n, --lines <number>", "Number of lines to show", "50").option("--violations", "Show only blocked commands").action(async (options) => {
|
|
1764
|
+
const { viewAudit } = await import("./audit-MCFNGOIM.js");
|
|
1765
|
+
await viewAudit(options);
|
|
1766
|
+
});
|
|
1767
|
+
program.command("scan").description("Scan your system and project environment").option("-p, --project <path>", "Project path to scan", ".").action(async (options) => {
|
|
1768
|
+
console.log(chalk5.cyan(logo));
|
|
1769
|
+
console.log(chalk5.dim(" Scanning your environment...\n"));
|
|
1770
|
+
const bro = new BashBro();
|
|
1771
|
+
await bro.initialize();
|
|
1772
|
+
if (options.project) {
|
|
1773
|
+
bro.scanProject(options.project);
|
|
1774
|
+
}
|
|
1775
|
+
console.log(bro.getSystemContext());
|
|
1776
|
+
console.log();
|
|
1777
|
+
console.log(chalk5.bold("\n## Agent Configurations\n"));
|
|
1778
|
+
const { formatAgentSummary } = await import("./display-IN4NRJJS.js");
|
|
1779
|
+
const agents = await getAllAgentConfigs();
|
|
1780
|
+
console.log(formatAgentSummary(agents));
|
|
1781
|
+
console.log();
|
|
1782
|
+
console.log(chalk5.green("\u2713"), "System profile saved to ~/.bashbros/system-profile.json");
|
|
1783
|
+
});
|
|
1784
|
+
program.command("status").alias("bro").description("Show Bash Bro status and system info").action(async () => {
|
|
1785
|
+
console.log(chalk5.cyan(logo));
|
|
1786
|
+
const bro = new BashBro();
|
|
1787
|
+
await bro.initialize();
|
|
1788
|
+
console.log(bro.status());
|
|
1789
|
+
});
|
|
1790
|
+
program.command("suggest").description("Get command suggestions based on context").option("-c, --command <cmd>", "Last command for context").option("-e, --error <msg>", "Last error message").action(async (options) => {
|
|
1791
|
+
const bro = new BashBro();
|
|
1792
|
+
await bro.initialize();
|
|
1793
|
+
const suggestions = bro.suggest({
|
|
1794
|
+
lastCommand: options.command,
|
|
1795
|
+
lastError: options.error,
|
|
1796
|
+
cwd: process.cwd()
|
|
1797
|
+
});
|
|
1798
|
+
if (suggestions.length === 0) {
|
|
1799
|
+
console.log(chalk5.dim("No suggestions available."));
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
console.log(chalk5.bold("\u{1F91D} Bash Bro suggests:\n"));
|
|
1803
|
+
for (const s of suggestions) {
|
|
1804
|
+
const confidence = Math.round(s.confidence * 100);
|
|
1805
|
+
console.log(` ${chalk5.cyan(s.command)}`);
|
|
1806
|
+
console.log(chalk5.dim(` ${s.description} (${confidence}% confidence)`));
|
|
1807
|
+
console.log();
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1810
|
+
program.command("route <command>").description("Check how a command would be routed").action(async (command) => {
|
|
1811
|
+
const bro = new BashBro();
|
|
1812
|
+
await bro.initialize();
|
|
1813
|
+
const result = bro.route(command);
|
|
1814
|
+
const icon = result.decision === "bro" ? "\u{1F91D}" : result.decision === "main" ? "\u{1F916}" : "\u26A1";
|
|
1815
|
+
const label = result.decision === "bro" ? "Bash Bro" : result.decision === "main" ? "Main Agent" : "Both (parallel)";
|
|
1816
|
+
console.log();
|
|
1817
|
+
console.log(`${icon} Route: ${chalk5.bold(label)}`);
|
|
1818
|
+
console.log(chalk5.dim(` Reason: ${result.reason}`));
|
|
1819
|
+
console.log(chalk5.dim(` Confidence: ${Math.round(result.confidence * 100)}%`));
|
|
1820
|
+
console.log();
|
|
1821
|
+
});
|
|
1822
|
+
program.command("run <command>").description("Run a command through Bash Bro").option("-b, --background", "Run in background").action(async (command, options) => {
|
|
1823
|
+
const bro = new BashBro();
|
|
1824
|
+
await bro.initialize();
|
|
1825
|
+
if (options.background) {
|
|
1826
|
+
const task = bro.runBackground(command);
|
|
1827
|
+
console.log(chalk5.green("\u2713"), `Started background task: ${task.id}`);
|
|
1828
|
+
console.log(chalk5.dim(` Command: ${command}`));
|
|
1829
|
+
console.log(chalk5.dim(` Run 'bashbros tasks' to check status`));
|
|
1830
|
+
} else {
|
|
1831
|
+
console.log(chalk5.dim(`\u{1F91D} Bash Bro executing: ${command}
|
|
1832
|
+
`));
|
|
1833
|
+
const output = await bro.execute(command);
|
|
1834
|
+
console.log(output);
|
|
1835
|
+
}
|
|
1836
|
+
});
|
|
1837
|
+
program.command("tasks").description("List background tasks").option("-a, --all", "Show all tasks (not just running)").action(async (options) => {
|
|
1838
|
+
const bro = new BashBro();
|
|
1839
|
+
const tasks = options.all ? bro.getBackgroundTasks() : bro.getBackgroundTasks().filter((t) => t.status === "running");
|
|
1840
|
+
if (tasks.length === 0) {
|
|
1841
|
+
console.log(chalk5.dim("No background tasks."));
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
console.log(chalk5.bold("\u{1F91D} Background Tasks:\n"));
|
|
1845
|
+
for (const task of tasks) {
|
|
1846
|
+
const elapsed = Math.round((Date.now() - task.startTime.getTime()) / 1e3);
|
|
1847
|
+
const statusIcon = task.status === "running" ? "\u23F3" : task.status === "completed" ? "\u2713" : task.status === "failed" ? "\u2717" : "\u25CB";
|
|
1848
|
+
console.log(` ${statusIcon} [${task.id}] ${task.command}`);
|
|
1849
|
+
console.log(chalk5.dim(` Status: ${task.status}, Elapsed: ${elapsed}s`));
|
|
1850
|
+
console.log();
|
|
1851
|
+
}
|
|
1852
|
+
});
|
|
1853
|
+
program.command("explain <command>").description("Ask Bash Bro to explain a command").action(async (command) => {
|
|
1854
|
+
const bro = new BashBro();
|
|
1855
|
+
await bro.initialize();
|
|
1856
|
+
if (!bro.isOllamaAvailable()) {
|
|
1857
|
+
console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
console.log(chalk5.dim("\u{1F91D} Bash Bro is thinking...\n"));
|
|
1861
|
+
const explanation = await bro.aiExplain(command);
|
|
1862
|
+
console.log(explanation);
|
|
1863
|
+
});
|
|
1864
|
+
program.command("fix <command>").description("Ask Bash Bro to fix a failed command").option("-e, --error <message>", "Error message from the failed command").action(async (command, options) => {
|
|
1865
|
+
const bro = new BashBro();
|
|
1866
|
+
await bro.initialize();
|
|
1867
|
+
if (!bro.isOllamaAvailable()) {
|
|
1868
|
+
console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
const error = options.error || "Command failed";
|
|
1872
|
+
console.log(chalk5.dim("\u{1F91D} Bash Bro is analyzing...\n"));
|
|
1873
|
+
const fixed = await bro.aiFix(command, error);
|
|
1874
|
+
if (fixed) {
|
|
1875
|
+
console.log(chalk5.green("Suggested fix:"));
|
|
1876
|
+
console.log(chalk5.cyan(` ${fixed}`));
|
|
1877
|
+
} else {
|
|
1878
|
+
console.log(chalk5.yellow("Could not suggest a fix."));
|
|
1879
|
+
}
|
|
1880
|
+
});
|
|
1881
|
+
program.command("ai <prompt>").description("Ask Bash Bro anything").action(async (prompt) => {
|
|
1882
|
+
const bro = new BashBro();
|
|
1883
|
+
await bro.initialize();
|
|
1884
|
+
if (!bro.isOllamaAvailable()) {
|
|
1885
|
+
console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
console.log(chalk5.dim("\u{1F91D} Bash Bro is thinking...\n"));
|
|
1889
|
+
const suggestion = await bro.aiSuggest(prompt);
|
|
1890
|
+
if (suggestion) {
|
|
1891
|
+
console.log(chalk5.cyan(suggestion));
|
|
1892
|
+
} else {
|
|
1893
|
+
console.log(chalk5.dim("No suggestion available."));
|
|
1894
|
+
}
|
|
1895
|
+
});
|
|
1896
|
+
program.command("script <description>").description("Generate a shell script from description").option("-o, --output <file>", "Save script to file").action(async (description, options) => {
|
|
1897
|
+
const bro = new BashBro();
|
|
1898
|
+
await bro.initialize();
|
|
1899
|
+
if (!bro.isOllamaAvailable()) {
|
|
1900
|
+
console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
console.log(chalk5.dim("\u{1F91D} Bash Bro is generating script...\n"));
|
|
1904
|
+
const script = await bro.aiGenerateScript(description);
|
|
1905
|
+
if (script) {
|
|
1906
|
+
console.log(chalk5.cyan(script));
|
|
1907
|
+
if (options.output) {
|
|
1908
|
+
const { writeFileSync: writeFileSync3 } = await import("fs");
|
|
1909
|
+
writeFileSync3(options.output, script, { mode: 493 });
|
|
1910
|
+
console.log(chalk5.green(`
|
|
1911
|
+
\u2713 Saved to ${options.output}`));
|
|
1912
|
+
}
|
|
1913
|
+
} else {
|
|
1914
|
+
console.log(chalk5.yellow("Could not generate script."));
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
program.command("safety <command>").description("Analyze a command for security risks").action(async (command) => {
|
|
1918
|
+
const bro = new BashBro();
|
|
1919
|
+
await bro.initialize();
|
|
1920
|
+
if (!bro.isOllamaAvailable()) {
|
|
1921
|
+
console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
console.log(chalk5.dim("\u{1F91D} Bash Bro is analyzing...\n"));
|
|
1925
|
+
const analysis = await bro.aiAnalyzeSafety(command);
|
|
1926
|
+
const riskColors = {
|
|
1927
|
+
low: chalk5.green,
|
|
1928
|
+
medium: chalk5.yellow,
|
|
1929
|
+
high: chalk5.red,
|
|
1930
|
+
critical: chalk5.bgRed.white
|
|
1931
|
+
};
|
|
1932
|
+
const icon = analysis.safe ? "\u2713" : "\u26A0";
|
|
1933
|
+
const color = riskColors[analysis.risk];
|
|
1934
|
+
console.log(`${icon} Risk Level: ${color(analysis.risk.toUpperCase())}`);
|
|
1935
|
+
console.log();
|
|
1936
|
+
console.log(chalk5.bold("Explanation:"));
|
|
1937
|
+
console.log(` ${analysis.explanation}`);
|
|
1938
|
+
if (analysis.suggestions.length > 0) {
|
|
1939
|
+
console.log();
|
|
1940
|
+
console.log(chalk5.bold("Suggestions:"));
|
|
1941
|
+
for (const suggestion of analysis.suggestions) {
|
|
1942
|
+
console.log(` \u2022 ${suggestion}`);
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
});
|
|
1946
|
+
program.command("help-ai <topic>").alias("h").description("Get AI help for a command or topic").action(async (topic) => {
|
|
1947
|
+
const bro = new BashBro();
|
|
1948
|
+
await bro.initialize();
|
|
1949
|
+
if (!bro.isOllamaAvailable()) {
|
|
1950
|
+
console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
console.log(chalk5.dim("\u{1F91D} Bash Bro is looking that up...\n"));
|
|
1954
|
+
const help = await bro.aiHelp(topic);
|
|
1955
|
+
console.log(help);
|
|
1956
|
+
});
|
|
1957
|
+
program.command("do <description>").description("Convert natural language to a command").option("-x, --execute", "Execute the command after showing it").action(async (description, options) => {
|
|
1958
|
+
const bro = new BashBro();
|
|
1959
|
+
await bro.initialize();
|
|
1960
|
+
if (!bro.isOllamaAvailable()) {
|
|
1961
|
+
console.log(chalk5.yellow("Ollama not available. Start Ollama to use AI features."));
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
console.log(chalk5.dim("\u{1F91D} Bash Bro is translating...\n"));
|
|
1965
|
+
const command = await bro.aiToCommand(description);
|
|
1966
|
+
if (command) {
|
|
1967
|
+
console.log(chalk5.bold("Command:"));
|
|
1968
|
+
console.log(chalk5.cyan(` $ ${command}`));
|
|
1969
|
+
if (options.execute) {
|
|
1970
|
+
console.log();
|
|
1971
|
+
console.log(chalk5.dim("Executing..."));
|
|
1972
|
+
const output = await bro.execute(command);
|
|
1973
|
+
console.log(output);
|
|
1974
|
+
}
|
|
1975
|
+
} else {
|
|
1976
|
+
console.log(chalk5.yellow("Could not translate to a command."));
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
program.command("models").description("List available Ollama models").action(async () => {
|
|
1980
|
+
console.log(chalk5.cyan(logo));
|
|
1981
|
+
const { OllamaClient } = await import("./ollama-HY35OHW4.js");
|
|
1982
|
+
const ollama = new OllamaClient();
|
|
1983
|
+
const available = await ollama.isAvailable();
|
|
1984
|
+
if (!available) {
|
|
1985
|
+
console.log(chalk5.yellow("Ollama not running. Start Ollama to see available models."));
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
const models = await ollama.listModels();
|
|
1989
|
+
if (models.length === 0) {
|
|
1990
|
+
console.log(chalk5.dim("No models installed. Run: ollama pull qwen2.5-coder:7b"));
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
console.log(chalk5.bold("\u{1F91D} Available Models:\n"));
|
|
1994
|
+
for (const model of models) {
|
|
1995
|
+
const current = model === ollama.getModel() ? chalk5.green(" (current)") : "";
|
|
1996
|
+
console.log(` \u2022 ${model}${current}`);
|
|
1997
|
+
}
|
|
1998
|
+
});
|
|
1999
|
+
var hookCmd = program.command("hook").description("Manage Claude Code hook integration");
|
|
2000
|
+
hookCmd.command("install").description("Install BashBros hooks into Claude Code").action(() => {
|
|
2001
|
+
const result = ClaudeCodeHooks.install();
|
|
2002
|
+
if (result.success) {
|
|
2003
|
+
console.log(chalk5.green("\u2713"), result.message);
|
|
2004
|
+
} else {
|
|
2005
|
+
console.log(chalk5.red("\u2717"), result.message);
|
|
2006
|
+
process.exit(1);
|
|
2007
|
+
}
|
|
2008
|
+
});
|
|
2009
|
+
hookCmd.command("uninstall").description("Remove BashBros hooks from Claude Code").action(() => {
|
|
2010
|
+
const result = ClaudeCodeHooks.uninstall();
|
|
2011
|
+
if (result.success) {
|
|
2012
|
+
console.log(chalk5.green("\u2713"), result.message);
|
|
2013
|
+
} else {
|
|
2014
|
+
console.log(chalk5.red("\u2717"), result.message);
|
|
2015
|
+
process.exit(1);
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
hookCmd.command("status").description("Check Claude Code hook status").action(() => {
|
|
2019
|
+
const status = ClaudeCodeHooks.getStatus();
|
|
2020
|
+
console.log();
|
|
2021
|
+
console.log(chalk5.bold("Claude Code Integration Status"));
|
|
2022
|
+
console.log();
|
|
2023
|
+
console.log(` Claude Code: ${status.claudeInstalled ? chalk5.green("installed") : chalk5.yellow("not found")}`);
|
|
2024
|
+
console.log(` BashBros hooks: ${status.hooksInstalled ? chalk5.green("active") : chalk5.dim("not installed")}`);
|
|
2025
|
+
if (status.hooks.length > 0) {
|
|
2026
|
+
console.log(` Active hooks: ${status.hooks.join(", ")}`);
|
|
2027
|
+
}
|
|
2028
|
+
console.log();
|
|
2029
|
+
});
|
|
2030
|
+
var moltbotCmd = program.command("moltbot").alias("clawdbot").description("Manage Moltbot/Clawdbot integration");
|
|
2031
|
+
moltbotCmd.command("install").description("Install BashBros hooks into Moltbot").action(() => {
|
|
2032
|
+
const result = MoltbotHooks.install();
|
|
2033
|
+
if (result.success) {
|
|
2034
|
+
console.log(chalk5.green("\u2713"), result.message);
|
|
2035
|
+
} else {
|
|
2036
|
+
console.log(chalk5.red("\u2717"), result.message);
|
|
2037
|
+
process.exit(1);
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
moltbotCmd.command("uninstall").description("Remove BashBros hooks from Moltbot").action(() => {
|
|
2041
|
+
const result = MoltbotHooks.uninstall();
|
|
2042
|
+
if (result.success) {
|
|
2043
|
+
console.log(chalk5.green("\u2713"), result.message);
|
|
2044
|
+
} else {
|
|
2045
|
+
console.log(chalk5.red("\u2717"), result.message);
|
|
2046
|
+
process.exit(1);
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
moltbotCmd.command("status").description("Check Moltbot integration status").action(async () => {
|
|
2050
|
+
const status = MoltbotHooks.getStatus();
|
|
2051
|
+
console.log();
|
|
2052
|
+
console.log(chalk5.bold("Moltbot Integration Status"));
|
|
2053
|
+
console.log();
|
|
2054
|
+
if (status.moltbotInstalled) {
|
|
2055
|
+
console.log(` Moltbot: ${chalk5.green("installed")}`);
|
|
2056
|
+
} else if (status.clawdbotInstalled) {
|
|
2057
|
+
console.log(` Clawdbot: ${chalk5.green("installed")} ${chalk5.dim("(legacy)")}`);
|
|
2058
|
+
} else {
|
|
2059
|
+
console.log(` Moltbot: ${chalk5.yellow("not found")}`);
|
|
2060
|
+
}
|
|
2061
|
+
if (status.configPath) {
|
|
2062
|
+
console.log(` Config: ${chalk5.green(status.configPath)}`);
|
|
2063
|
+
} else {
|
|
2064
|
+
console.log(` Config: ${chalk5.dim("not found")}`);
|
|
2065
|
+
}
|
|
2066
|
+
console.log(` BashBros hooks: ${status.hooksInstalled ? chalk5.green("active") : chalk5.dim("not installed")}`);
|
|
2067
|
+
if (status.hooks.length > 0) {
|
|
2068
|
+
console.log(` Active hooks: ${status.hooks.join(", ")}`);
|
|
2069
|
+
}
|
|
2070
|
+
if (status.sandboxMode) {
|
|
2071
|
+
const sandboxColor = status.sandboxMode === "strict" ? chalk5.green : chalk5.yellow;
|
|
2072
|
+
console.log(` Sandbox mode: ${sandboxColor(status.sandboxMode)}`);
|
|
2073
|
+
}
|
|
2074
|
+
console.log();
|
|
2075
|
+
});
|
|
2076
|
+
moltbotCmd.command("gateway").description("Check Moltbot gateway status").action(async () => {
|
|
2077
|
+
console.log();
|
|
2078
|
+
console.log(chalk5.bold("Moltbot Gateway Status"));
|
|
2079
|
+
console.log();
|
|
2080
|
+
const gatewayStatus = await MoltbotHooks.getGatewayStatus();
|
|
2081
|
+
if (gatewayStatus.running) {
|
|
2082
|
+
console.log(` Status: ${chalk5.green("running")}`);
|
|
2083
|
+
console.log(` Host: ${gatewayStatus.host}`);
|
|
2084
|
+
console.log(` Port: ${gatewayStatus.port}`);
|
|
2085
|
+
console.log(` Sandbox: ${gatewayStatus.sandboxMode ? chalk5.green("enabled") : chalk5.yellow("disabled")}`);
|
|
2086
|
+
} else {
|
|
2087
|
+
console.log(` Status: ${chalk5.yellow("not running")}`);
|
|
2088
|
+
console.log(` Expected: ${gatewayStatus.host}:${gatewayStatus.port}`);
|
|
2089
|
+
if (gatewayStatus.error) {
|
|
2090
|
+
console.log(` Error: ${chalk5.dim(gatewayStatus.error)}`);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
console.log();
|
|
2094
|
+
});
|
|
2095
|
+
moltbotCmd.command("audit").description("Run Moltbot security audit").option("--json", "Output as JSON").action(async (options) => {
|
|
2096
|
+
console.log(chalk5.dim("Running security audit...\n"));
|
|
2097
|
+
const result = await MoltbotHooks.runSecurityAudit();
|
|
2098
|
+
if (options.json) {
|
|
2099
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
const statusIcon = result.passed ? chalk5.green("\u2713") : chalk5.red("\u2717");
|
|
2103
|
+
const statusText = result.passed ? chalk5.green("PASSED") : chalk5.red("FAILED");
|
|
2104
|
+
console.log(`${statusIcon} Security Audit: ${statusText}`);
|
|
2105
|
+
console.log();
|
|
2106
|
+
if (result.findings.length === 0) {
|
|
2107
|
+
console.log(chalk5.dim(" No findings."));
|
|
2108
|
+
} else {
|
|
2109
|
+
const severityColors = {
|
|
2110
|
+
critical: chalk5.bgRed.white,
|
|
2111
|
+
warning: chalk5.yellow,
|
|
2112
|
+
info: chalk5.dim
|
|
2113
|
+
};
|
|
2114
|
+
for (const finding of result.findings) {
|
|
2115
|
+
const color = severityColors[finding.severity] || chalk5.white;
|
|
2116
|
+
console.log(` ${color(`[${finding.severity.toUpperCase()}]`)} ${finding.message}`);
|
|
2117
|
+
console.log(chalk5.dim(` Category: ${finding.category}`));
|
|
2118
|
+
if (finding.recommendation) {
|
|
2119
|
+
console.log(chalk5.dim(` Fix: ${finding.recommendation}`));
|
|
2120
|
+
}
|
|
2121
|
+
console.log();
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
console.log(chalk5.dim(`Audit completed at ${result.timestamp.toLocaleString()}`));
|
|
2125
|
+
console.log();
|
|
2126
|
+
});
|
|
2127
|
+
program.command("agent-info [agent]").description("Show detailed info about installed agents and their configurations").option("-r, --raw", "Show raw (redacted) config contents").action(async (agent, options) => {
|
|
2128
|
+
console.log(chalk5.cyan(logo));
|
|
2129
|
+
if (agent) {
|
|
2130
|
+
const validAgents = ["claude-code", "moltbot", "clawdbot", "aider", "gemini-cli", "opencode"];
|
|
2131
|
+
if (!validAgents.includes(agent)) {
|
|
2132
|
+
console.log(chalk5.red(`Unknown agent: ${agent}`));
|
|
2133
|
+
console.log(chalk5.dim(`Valid agents: ${validAgents.join(", ")}`));
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
const info = await getAgentConfigInfo(agent);
|
|
2137
|
+
const { formatAgentInfo: formatAgentInfo2 } = await import("./display-IN4NRJJS.js");
|
|
2138
|
+
console.log();
|
|
2139
|
+
console.log(formatAgentInfo2(info));
|
|
2140
|
+
if (options.raw && info.configExists && info.configPath) {
|
|
2141
|
+
const { parseAgentConfig: parseAgentConfig2 } = await import("./config-parser-XHE7BC7H.js");
|
|
2142
|
+
const parsed = await parseAgentConfig2(agent, info.configPath);
|
|
2143
|
+
if (parsed?.rawRedacted) {
|
|
2144
|
+
console.log();
|
|
2145
|
+
console.log(chalk5.bold("Configuration (sensitive data redacted):"));
|
|
2146
|
+
console.log(formatRedactedConfig(parsed.rawRedacted));
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
} else {
|
|
2150
|
+
const agents = await getAllAgentConfigs();
|
|
2151
|
+
console.log();
|
|
2152
|
+
console.log(formatAllAgentsInfo(agents));
|
|
2153
|
+
}
|
|
2154
|
+
console.log();
|
|
2155
|
+
});
|
|
2156
|
+
program.command("permissions").description("Show combined permissions view across bashbros and agents").action(async () => {
|
|
2157
|
+
console.log(chalk5.cyan(logo));
|
|
2158
|
+
const agents = await getAllAgentConfigs();
|
|
2159
|
+
const installed = agents.filter((a) => a.installed);
|
|
2160
|
+
if (installed.length === 0) {
|
|
2161
|
+
console.log(chalk5.yellow("No agents installed to compare permissions with."));
|
|
2162
|
+
console.log(chalk5.dim("Install an agent (claude, aider, etc.) to see combined permissions."));
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
console.log();
|
|
2166
|
+
console.log(formatPermissionsTable(agents));
|
|
2167
|
+
console.log();
|
|
2168
|
+
});
|
|
2169
|
+
program.command("gate <command>").description("Check if a command should be allowed (used by hooks)").action(async (command) => {
|
|
2170
|
+
const result = await gateCommand(command);
|
|
2171
|
+
if (!result.allowed) {
|
|
2172
|
+
console.error(`BLOCKED: ${result.reason}`);
|
|
2173
|
+
process.exit(2);
|
|
2174
|
+
}
|
|
2175
|
+
process.exit(0);
|
|
2176
|
+
});
|
|
2177
|
+
program.command("record <command>").description("Record a command execution (used by hooks)").option("-o, --output <output>", "Command output").option("-e, --exit-code <code>", "Exit code", "0").action(async (command, options) => {
|
|
2178
|
+
if (!metricsCollector) metricsCollector = new MetricsCollector();
|
|
2179
|
+
if (!costEstimator) costEstimator = new CostEstimator();
|
|
2180
|
+
if (!loopDetector) loopDetector = new LoopDetector();
|
|
2181
|
+
if (!undoStack) undoStack = new UndoStack();
|
|
2182
|
+
const scorer = new RiskScorer();
|
|
2183
|
+
const risk = scorer.score(command);
|
|
2184
|
+
metricsCollector.record({
|
|
2185
|
+
command,
|
|
2186
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2187
|
+
duration: 0,
|
|
2188
|
+
// Not available in hook
|
|
2189
|
+
allowed: true,
|
|
2190
|
+
riskScore: risk,
|
|
2191
|
+
violations: [],
|
|
2192
|
+
exitCode: parseInt(options.exitCode) || 0
|
|
2193
|
+
});
|
|
2194
|
+
costEstimator.recordToolCall(command, options.output || "");
|
|
2195
|
+
const loopAlert = loopDetector.check(command);
|
|
2196
|
+
if (loopAlert) {
|
|
2197
|
+
console.error(chalk5.yellow(`\u26A0 Loop detected: ${loopAlert.message}`));
|
|
2198
|
+
}
|
|
2199
|
+
const paths = command.match(/(?:^|\s)(\.\/|\.\.\/|\/|~\/)[^\s]+/g) || [];
|
|
2200
|
+
const cleanPaths = paths.map((p) => p.trim());
|
|
2201
|
+
if (cleanPaths.length > 0) {
|
|
2202
|
+
undoStack.recordFromCommand(command, cleanPaths);
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2205
|
+
program.command("session-end").description("Generate session report (used by hooks)").option("-f, --format <format>", "Output format (text, markdown, json)", "text").action((options) => {
|
|
2206
|
+
if (!metricsCollector) {
|
|
2207
|
+
console.log(chalk5.dim("No session data to report."));
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
const metrics = metricsCollector.getMetrics();
|
|
2211
|
+
const cost = costEstimator?.getEstimate();
|
|
2212
|
+
const report = ReportGenerator.generate(metrics, cost, { format: options.format });
|
|
2213
|
+
console.log();
|
|
2214
|
+
console.log(report);
|
|
2215
|
+
console.log();
|
|
2216
|
+
});
|
|
2217
|
+
program.command("report").description("Generate a session report").option("-f, --format <format>", "Output format (text, markdown, json)", "text").option("--no-cost", "Hide cost estimate").option("--no-risk", "Hide risk distribution").action((options) => {
|
|
2218
|
+
if (!metricsCollector) {
|
|
2219
|
+
console.log(chalk5.dim("No session data. Run some commands first."));
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
const metrics = metricsCollector.getMetrics();
|
|
2223
|
+
const cost = options.cost ? costEstimator?.getEstimate() : void 0;
|
|
2224
|
+
const report = ReportGenerator.generate(metrics, cost, {
|
|
2225
|
+
format: options.format,
|
|
2226
|
+
showCost: options.cost,
|
|
2227
|
+
showRisk: options.risk
|
|
2228
|
+
});
|
|
2229
|
+
console.log();
|
|
2230
|
+
console.log(report);
|
|
2231
|
+
console.log();
|
|
2232
|
+
});
|
|
2233
|
+
program.command("risk <command>").description("Score a command for security risk").action((command) => {
|
|
2234
|
+
const scorer = new RiskScorer();
|
|
2235
|
+
const result = scorer.score(command);
|
|
2236
|
+
const colors = {
|
|
2237
|
+
safe: chalk5.green,
|
|
2238
|
+
caution: chalk5.yellow,
|
|
2239
|
+
dangerous: chalk5.red,
|
|
2240
|
+
critical: chalk5.bgRed.white
|
|
2241
|
+
};
|
|
2242
|
+
const color = colors[result.level];
|
|
2243
|
+
console.log();
|
|
2244
|
+
console.log(` Risk Score: ${color(`${result.score}/10`)} (${color(result.level.toUpperCase())})`);
|
|
2245
|
+
console.log();
|
|
2246
|
+
console.log(chalk5.bold(" Factors:"));
|
|
2247
|
+
for (const factor of result.factors) {
|
|
2248
|
+
console.log(` \u2022 ${factor}`);
|
|
2249
|
+
}
|
|
2250
|
+
console.log();
|
|
2251
|
+
});
|
|
2252
|
+
var undoCmd = program.command("undo").description("Undo file operations");
|
|
2253
|
+
undoCmd.command("last").alias("pop").description("Undo the last file operation").action(() => {
|
|
2254
|
+
if (!undoStack) {
|
|
2255
|
+
console.log(chalk5.dim("No operations to undo."));
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
const result = undoStack.undo();
|
|
2259
|
+
if (result.success) {
|
|
2260
|
+
console.log(chalk5.green("\u2713"), result.message);
|
|
2261
|
+
} else {
|
|
2262
|
+
console.log(chalk5.red("\u2717"), result.message);
|
|
2263
|
+
}
|
|
2264
|
+
});
|
|
2265
|
+
undoCmd.command("all").description("Undo all file operations in session").action(() => {
|
|
2266
|
+
if (!undoStack || undoStack.size() === 0) {
|
|
2267
|
+
console.log(chalk5.dim("No operations to undo."));
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
const results = undoStack.undoAll();
|
|
2271
|
+
let success = 0, failed = 0;
|
|
2272
|
+
for (const result of results) {
|
|
2273
|
+
if (result.success) {
|
|
2274
|
+
console.log(chalk5.green("\u2713"), result.message);
|
|
2275
|
+
success++;
|
|
2276
|
+
} else {
|
|
2277
|
+
console.log(chalk5.red("\u2717"), result.message);
|
|
2278
|
+
failed++;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
console.log();
|
|
2282
|
+
console.log(`Undone: ${success} successful, ${failed} failed`);
|
|
2283
|
+
});
|
|
2284
|
+
undoCmd.command("list").description("Show undo stack").action(() => {
|
|
2285
|
+
if (!undoStack) {
|
|
2286
|
+
undoStack = new UndoStack();
|
|
2287
|
+
}
|
|
2288
|
+
console.log();
|
|
2289
|
+
console.log(undoStack.formatStack());
|
|
2290
|
+
console.log();
|
|
2291
|
+
});
|
|
2292
|
+
var dashboardServer = null;
|
|
2293
|
+
program.command("dashboard").description("Start the BashBros dashboard").option("-p, --port <port>", "Port to run on", "7890").option("-b, --bind <address>", "Address to bind to", "127.0.0.1").action(async (options) => {
|
|
2294
|
+
console.log(chalk5.cyan(logo));
|
|
2295
|
+
console.log(chalk5.dim(" Starting dashboard...\n"));
|
|
2296
|
+
dashboardServer = new DashboardServer({
|
|
2297
|
+
port: parseInt(options.port),
|
|
2298
|
+
bind: options.bind
|
|
2299
|
+
});
|
|
2300
|
+
await dashboardServer.start();
|
|
2301
|
+
console.log(chalk5.green("\u2713"), `Dashboard running at http://${options.bind}:${options.port}`);
|
|
2302
|
+
console.log(chalk5.dim(" Press Ctrl+C to stop"));
|
|
2303
|
+
process.on("SIGINT", async () => {
|
|
2304
|
+
console.log(chalk5.dim("\n Stopping dashboard..."));
|
|
2305
|
+
await dashboardServer?.stop();
|
|
2306
|
+
process.exit(0);
|
|
2307
|
+
});
|
|
2308
|
+
});
|
|
2309
|
+
var wardCmd = program.command("ward").description("Network and connector security");
|
|
2310
|
+
wardCmd.command("status").description("Show ward security status").action(async () => {
|
|
2311
|
+
console.log(chalk5.cyan(logo));
|
|
2312
|
+
console.log(chalk5.bold("Ward Security Status\n"));
|
|
2313
|
+
const scanner = new ExposureScanner();
|
|
2314
|
+
const results = await scanner.scan();
|
|
2315
|
+
if (results.length === 0) {
|
|
2316
|
+
console.log(chalk5.green("\u2713"), "No exposed agent servers detected");
|
|
2317
|
+
} else {
|
|
2318
|
+
console.log(chalk5.yellow("\u26A0"), `Found ${results.length} exposure(s):
|
|
2319
|
+
`);
|
|
2320
|
+
for (const r of results) {
|
|
2321
|
+
const color = r.severity === "critical" ? chalk5.bgRed.white : r.severity === "high" ? chalk5.red : r.severity === "medium" ? chalk5.yellow : chalk5.dim;
|
|
2322
|
+
console.log(` ${color(r.severity.toUpperCase().padEnd(8))} ${r.message}`);
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
console.log();
|
|
2326
|
+
});
|
|
2327
|
+
wardCmd.command("scan").description("Run exposure scan").action(async () => {
|
|
2328
|
+
console.log(chalk5.cyan(logo));
|
|
2329
|
+
console.log(chalk5.dim(" Scanning for exposed agent servers...\n"));
|
|
2330
|
+
const scanner = new ExposureScanner();
|
|
2331
|
+
const results = await scanner.scan();
|
|
2332
|
+
if (results.length === 0) {
|
|
2333
|
+
console.log(chalk5.green("\u2713"), "No exposed agent servers detected");
|
|
2334
|
+
} else {
|
|
2335
|
+
for (const r of results) {
|
|
2336
|
+
const icon = r.severity === "critical" ? "\u{1F6A8}" : r.severity === "high" ? "\u26A0\uFE0F" : r.severity === "medium" ? "\u26A1" : "\u2139\uFE0F";
|
|
2337
|
+
console.log(`${icon} ${r.agent}`);
|
|
2338
|
+
console.log(chalk5.dim(` Port: ${r.port}`));
|
|
2339
|
+
console.log(chalk5.dim(` Bind: ${r.bindAddress}`));
|
|
2340
|
+
console.log(chalk5.dim(` Auth: ${r.hasAuth}`));
|
|
2341
|
+
console.log(chalk5.dim(` Severity: ${r.severity}`));
|
|
2342
|
+
console.log(chalk5.dim(` Action: ${r.action}`));
|
|
2343
|
+
console.log();
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
});
|
|
2347
|
+
var exposureCmd = wardCmd.command("exposure").description("Exposure scanner commands");
|
|
2348
|
+
exposureCmd.command("list").description("List monitored agents").action(() => {
|
|
2349
|
+
const scanner = new ExposureScanner();
|
|
2350
|
+
const agents = scanner.getAgents();
|
|
2351
|
+
console.log(chalk5.bold("\nMonitored Agent Signatures:\n"));
|
|
2352
|
+
for (const agent of agents) {
|
|
2353
|
+
console.log(` ${chalk5.cyan(agent.name)}`);
|
|
2354
|
+
console.log(chalk5.dim(` Processes: ${agent.processNames.join(", ")}`));
|
|
2355
|
+
console.log(chalk5.dim(` Ports: ${agent.defaultPorts.join(", ")}`));
|
|
2356
|
+
console.log();
|
|
2357
|
+
}
|
|
2358
|
+
});
|
|
2359
|
+
exposureCmd.command("scan").description("Run immediate exposure scan").action(async () => {
|
|
2360
|
+
const scanner = new ExposureScanner();
|
|
2361
|
+
const results = await scanner.scan();
|
|
2362
|
+
console.log(chalk5.bold("\nExposure Scan Results:\n"));
|
|
2363
|
+
if (results.length === 0) {
|
|
2364
|
+
console.log(chalk5.green(" \u2713 No exposures detected"));
|
|
2365
|
+
} else {
|
|
2366
|
+
for (const r of results) {
|
|
2367
|
+
const color = r.severity === "critical" ? chalk5.bgRed.white : r.severity === "high" ? chalk5.red : r.severity === "medium" ? chalk5.yellow : chalk5.green;
|
|
2368
|
+
console.log(` ${color(r.severity.toUpperCase().padEnd(10))} ${r.message}`);
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
console.log();
|
|
2372
|
+
});
|
|
2373
|
+
wardCmd.command("blocked").description("Show pending blocked egress items").action(() => {
|
|
2374
|
+
const monitor = new EgressMonitor();
|
|
2375
|
+
const pending = monitor.getPendingBlocks();
|
|
2376
|
+
console.log(chalk5.bold("\nPending Egress Blocks:\n"));
|
|
2377
|
+
if (pending.length === 0) {
|
|
2378
|
+
console.log(chalk5.dim(" No pending blocks"));
|
|
2379
|
+
} else {
|
|
2380
|
+
for (const block of pending) {
|
|
2381
|
+
const severityColor = block.pattern.severity === "critical" ? chalk5.bgRed.white : block.pattern.severity === "high" ? chalk5.red : block.pattern.severity === "medium" ? chalk5.yellow : chalk5.dim;
|
|
2382
|
+
console.log(` ${chalk5.cyan(block.id)} ${severityColor(block.pattern.severity.toUpperCase())}`);
|
|
2383
|
+
console.log(chalk5.dim(` Pattern: ${block.pattern.name} (${block.pattern.category})`));
|
|
2384
|
+
console.log(chalk5.dim(` Matched: ${block.matchedText.substring(0, 50)}${block.matchedText.length > 50 ? "..." : ""}`));
|
|
2385
|
+
if (block.connector) console.log(chalk5.dim(` Connector: ${block.connector}`));
|
|
2386
|
+
if (block.destination) console.log(chalk5.dim(` Destination: ${block.destination}`));
|
|
2387
|
+
console.log();
|
|
2388
|
+
}
|
|
2389
|
+
console.log(chalk5.dim(` Use 'bashbros ward approve <id>' or 'bashbros ward deny <id>' to resolve`));
|
|
2390
|
+
}
|
|
2391
|
+
console.log();
|
|
2392
|
+
});
|
|
2393
|
+
wardCmd.command("approve <id>").description("Approve a blocked egress item").option("--by <name>", "Name of approver", "user").action((id, options) => {
|
|
2394
|
+
const monitor = new EgressMonitor();
|
|
2395
|
+
monitor.approveBlock(id, options.by);
|
|
2396
|
+
console.log(chalk5.green("\u2713"), `Approved block ${id}`);
|
|
2397
|
+
});
|
|
2398
|
+
wardCmd.command("deny <id>").description("Deny a blocked egress item").action((id) => {
|
|
2399
|
+
const monitor = new EgressMonitor();
|
|
2400
|
+
monitor.denyBlock(id);
|
|
2401
|
+
console.log(chalk5.green("\u2713"), `Denied block ${id}`);
|
|
2402
|
+
});
|
|
2403
|
+
var patternsCmd = wardCmd.command("patterns").description("Egress pattern detection commands");
|
|
2404
|
+
patternsCmd.command("list").description("List active detection patterns").option("--category <cat>", "Filter by category (credentials, pii)").action((options) => {
|
|
2405
|
+
const matcher = new EgressPatternMatcher();
|
|
2406
|
+
let patterns = matcher.getPatterns();
|
|
2407
|
+
if (options.category) {
|
|
2408
|
+
patterns = patterns.filter((p) => p.category === options.category);
|
|
2409
|
+
}
|
|
2410
|
+
console.log(chalk5.bold("\nActive Egress Patterns:\n"));
|
|
2411
|
+
const byCategory = {};
|
|
2412
|
+
for (const p of patterns) {
|
|
2413
|
+
const cat = p.category;
|
|
2414
|
+
if (!byCategory[cat]) byCategory[cat] = [];
|
|
2415
|
+
byCategory[cat].push(p);
|
|
2416
|
+
}
|
|
2417
|
+
for (const [category, categoryPatterns] of Object.entries(byCategory)) {
|
|
2418
|
+
console.log(chalk5.cyan(` ${category.toUpperCase()}`));
|
|
2419
|
+
for (const p of categoryPatterns) {
|
|
2420
|
+
const severityColor = p.severity === "critical" ? chalk5.red : p.severity === "high" ? chalk5.yellow : p.severity === "medium" ? chalk5.blue : chalk5.dim;
|
|
2421
|
+
const actionColor = p.action === "block" ? chalk5.red : p.action === "alert" ? chalk5.yellow : chalk5.dim;
|
|
2422
|
+
console.log(` ${chalk5.bold(p.name.padEnd(16))} ${severityColor(p.severity.padEnd(10))} ${actionColor(p.action.padEnd(6))} ${p.description}`);
|
|
2423
|
+
}
|
|
2424
|
+
console.log();
|
|
2425
|
+
}
|
|
2426
|
+
});
|
|
2427
|
+
patternsCmd.command("test <text>").description("Test if text matches any detection pattern").action((text) => {
|
|
2428
|
+
const monitor = new EgressMonitor();
|
|
2429
|
+
const result = monitor.test(text);
|
|
2430
|
+
console.log(chalk5.bold("\nPattern Test Results:\n"));
|
|
2431
|
+
if (result.matches.length === 0) {
|
|
2432
|
+
console.log(chalk5.green(" \u2713 No patterns matched"));
|
|
2433
|
+
} else {
|
|
2434
|
+
console.log(` ${result.blocked ? chalk5.red("WOULD BLOCK") : chalk5.yellow("WOULD ALERT")}`);
|
|
2435
|
+
console.log();
|
|
2436
|
+
console.log(chalk5.bold(" Matches:"));
|
|
2437
|
+
for (const m of result.matches) {
|
|
2438
|
+
const severityColor = m.pattern.severity === "critical" ? chalk5.red : m.pattern.severity === "high" ? chalk5.yellow : m.pattern.severity === "medium" ? chalk5.blue : chalk5.dim;
|
|
2439
|
+
console.log(` ${chalk5.cyan(m.pattern.name)} ${severityColor(`[${m.pattern.severity}]`)} - "${m.matchedText.substring(0, 30)}${m.matchedText.length > 30 ? "..." : ""}"`);
|
|
2440
|
+
}
|
|
2441
|
+
console.log();
|
|
2442
|
+
console.log(chalk5.bold(" Redacted output:"));
|
|
2443
|
+
console.log(chalk5.dim(` ${result.redacted.substring(0, 100)}${result.redacted.length > 100 ? "..." : ""}`));
|
|
2444
|
+
}
|
|
2445
|
+
console.log();
|
|
2446
|
+
});
|
|
2447
|
+
program.parse();
|
|
2448
|
+
//# sourceMappingURL=cli.js.map
|