@rafter-security/cli 0.4.2 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -73,6 +73,14 @@ rafter agent audit
73
73
  rafter agent config show
74
74
  ```
75
75
 
76
+ ## Global Options
77
+
78
+ | Flag | Description |
79
+ |------|-------------|
80
+ | `-a, --agent` | Plain output for AI agents (no colors, no emoji) |
81
+ | `-V, --version` | Print version |
82
+ | `-h, --help` | Show help |
83
+
76
84
  ## Commands
77
85
 
78
86
  ### `rafter run [options]`
@@ -169,6 +177,7 @@ Scan files or directories for secrets.
169
177
  **Options:**
170
178
  - `-q, --quiet` - Only output if secrets found
171
179
  - `--json` - Output results as JSON
180
+ - `--diff <ref>` - Scan files changed since a git ref (e.g., `HEAD~1`, `main`, `v1.0.0`)
172
181
 
173
182
  **Features:**
174
183
  - Detects 21+ secret types (AWS, GitHub, Stripe, Google, Slack, etc.)
@@ -185,6 +194,10 @@ rafter agent scan
185
194
  # Scan specific file
186
195
  rafter agent scan ./config.js
187
196
 
197
+ # Scan files changed since a ref
198
+ rafter agent scan --diff HEAD~1
199
+ rafter agent scan --diff main
200
+
188
201
  # Scan for CI (quiet mode)
189
202
  rafter agent scan --quiet
190
203
 
@@ -433,6 +446,93 @@ Claude Code skills can:
433
446
 
434
447
  Always audit skills from untrusted sources before installation. The skill-auditor provides systematic analysis to identify security risks.
435
448
 
449
+ ### `rafter mcp serve [options]`
450
+
451
+ Start an MCP server exposing Rafter security tools over stdio transport. Any MCP-compatible client (Cursor, Windsurf, Claude Desktop, Cline, etc.) can connect.
452
+
453
+ **Options:**
454
+ - `--transport <type>` - Transport type (default: `stdio`)
455
+
456
+ **MCP client config:**
457
+ ```json
458
+ {
459
+ "rafter": {
460
+ "command": "rafter",
461
+ "args": ["mcp", "serve"]
462
+ }
463
+ }
464
+ ```
465
+
466
+ **Tools provided:**
467
+
468
+ | Tool | Description |
469
+ |------|-------------|
470
+ | `scan_secrets` | Scan files/directories for hardcoded secrets. Params: `path` (required), `engine` (auto/gitleaks/patterns) |
471
+ | `evaluate_command` | Check if a shell command is allowed by Rafter policy. Params: `command` (required) |
472
+ | `read_audit_log` | Read audit log entries. Params: `limit`, `event_type`, `since` |
473
+ | `get_config` | Read Rafter config. Params: `key` (optional dot-path) |
474
+
475
+ **Resources provided:**
476
+
477
+ | URI | Description |
478
+ |-----|-------------|
479
+ | `rafter://config` | Current Rafter configuration (JSON) |
480
+ | `rafter://policy` | Active security policy — merged `.rafter.yml` + config (JSON) |
481
+
482
+ ---
483
+
484
+ ### `rafter ci init [options]`
485
+
486
+ Generate CI/CD pipeline configuration for secret scanning.
487
+
488
+ **Options:**
489
+ - `--platform <platform>` - CI platform: `github`, `gitlab`, `circleci` (default: auto-detect)
490
+ - `--output <path>` - Output file path (default: platform-specific)
491
+ - `--with-backend` - Include backend security audit job (requires `RAFTER_API_KEY`)
492
+
493
+ **Auto-detection:** Checks for `.github/`, `.gitlab-ci.yml`, `.circleci/` in the current directory.
494
+
495
+ **Examples:**
496
+ ```bash
497
+ # Auto-detect platform
498
+ rafter ci init
499
+
500
+ # Generate GitHub Actions workflow
501
+ rafter ci init --platform github
502
+
503
+ # Include backend scanning job
504
+ rafter ci init --with-backend
505
+
506
+ # Custom output path
507
+ rafter ci init --output .github/workflows/security.yml
508
+ ```
509
+
510
+ ---
511
+
512
+ ## Policy File (`.rafter.yml`)
513
+
514
+ Define per-project security policies by placing a `.rafter.yml` in your project root. The CLI walks from cwd up to git root looking for it.
515
+
516
+ ```yaml
517
+ version: "1"
518
+ risk_level: moderate
519
+ command_policy:
520
+ mode: approve-dangerous
521
+ blocked_patterns: ["rm -rf /"]
522
+ require_approval: ["npm publish"]
523
+ scan:
524
+ exclude_paths: ["vendor/", "third_party/"]
525
+ custom_patterns:
526
+ - name: "Internal API Key"
527
+ regex: "INTERNAL_[A-Z0-9]{32}"
528
+ severity: critical
529
+ audit:
530
+ retention_days: 90
531
+ log_level: info
532
+ ```
533
+
534
+ **Precedence:** Policy file values override `~/.rafter/config.json`. Arrays replace, not append.
535
+
436
536
  ---
437
537
 
438
538
  ## Configuration
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { AuditLogger } from "../../core/audit-logger.js";
3
+ import { isAgentMode } from "../../utils/formatter.js";
3
4
  export function createAuditCommand() {
4
5
  return new Command("audit")
5
6
  .description("View audit log entries")
@@ -29,8 +30,8 @@ export function createAuditCommand() {
29
30
  console.log(`\nShowing ${entries.length} audit log entries:\n`);
30
31
  for (const entry of entries) {
31
32
  const timestamp = new Date(entry.timestamp).toLocaleString();
32
- const emoji = getEventEmoji(entry.eventType);
33
- console.log(`${emoji} [${timestamp}] ${entry.eventType}`);
33
+ const indicator = getEventIndicator(entry.eventType);
34
+ console.log(`${indicator} [${timestamp}] ${entry.eventType}`);
34
35
  if (entry.agentType) {
35
36
  console.log(` Agent: ${entry.agentType}`);
36
37
  }
@@ -52,7 +53,18 @@ export function createAuditCommand() {
52
53
  }
53
54
  });
54
55
  }
55
- function getEventEmoji(eventType) {
56
+ function getEventIndicator(eventType) {
57
+ if (isAgentMode()) {
58
+ const tagMap = {
59
+ command_intercepted: "[INTERCEPT]",
60
+ secret_detected: "[SECRET]",
61
+ content_sanitized: "[SANITIZE]",
62
+ policy_override: "[OVERRIDE]",
63
+ scan_executed: "[SCAN]",
64
+ config_changed: "[CONFIG]",
65
+ };
66
+ return tagMap[eventType] || "[EVENT]";
67
+ }
56
68
  const emojiMap = {
57
69
  command_intercepted: "🛡️",
58
70
  secret_detected: "🔑",
@@ -3,6 +3,7 @@ import { CommandInterceptor } from "../../core/command-interceptor.js";
3
3
  import { RegexScanner } from "../../scanners/regex-scanner.js";
4
4
  import { execSync } from "child_process";
5
5
  import readline from "readline";
6
+ import { fmt } from "../../utils/formatter.js";
6
7
  export function createExecCommand() {
7
8
  return new Command("exec")
8
9
  .description("Execute command with security validation")
@@ -15,7 +16,7 @@ export function createExecCommand() {
15
16
  const evaluation = interceptor.evaluate(command);
16
17
  // Step 2: Handle blocked commands
17
18
  if (!evaluation.allowed && !evaluation.requiresApproval) {
18
- console.error(`\n🚫 Command BLOCKED\n`);
19
+ console.error(`\n${fmt.error("Command BLOCKED")}\n`);
19
20
  console.error(`Risk Level: ${evaluation.riskLevel.toUpperCase()}`);
20
21
  console.error(`Reason: ${evaluation.reason}`);
21
22
  console.error(`Command: ${command}\n`);
@@ -26,7 +27,7 @@ export function createExecCommand() {
26
27
  if (!opts.skipScan && isGitCommand(command)) {
27
28
  const scanResult = await scanStagedFiles();
28
29
  if (scanResult.secretsFound) {
29
- console.error(`\n⚠️ Secrets detected in staged files!\n`);
30
+ console.error(`\n${fmt.warning("Secrets detected in staged files!")}\n`);
30
31
  console.error(`Found ${scanResult.count} secret(s) in ${scanResult.files} file(s)`);
31
32
  console.error(`\nRun 'rafter agent scan' for details.\n`);
32
33
  interceptor.logEvaluation(evaluation, "blocked");
@@ -35,7 +36,7 @@ export function createExecCommand() {
35
36
  }
36
37
  // Step 4: Handle approval required
37
38
  if (evaluation.requiresApproval && !opts.force) {
38
- console.log(`\n⚠️ Command requires approval\n`);
39
+ console.log(`\n${fmt.warning("Command requires approval")}\n`);
39
40
  console.log(`Risk Level: ${evaluation.riskLevel.toUpperCase()}`);
40
41
  console.log(`Command: ${command}`);
41
42
  if (evaluation.reason) {
@@ -44,15 +45,15 @@ export function createExecCommand() {
44
45
  console.log();
45
46
  const approved = await promptApproval();
46
47
  if (!approved) {
47
- console.log("\n❌ Command cancelled\n");
48
+ console.log(`\n${fmt.error("Command cancelled")}\n`);
48
49
  interceptor.logEvaluation(evaluation, "blocked");
49
50
  process.exit(1);
50
51
  }
51
- console.log("\n✓ Command approved by user\n");
52
+ console.log(`\n${fmt.success("Command approved by user")}\n`);
52
53
  interceptor.logEvaluation(evaluation, "overridden");
53
54
  }
54
55
  else if (opts.force && evaluation.requiresApproval) {
55
- console.log(`\n⚠️ Forcing execution (--force flag)\n`);
56
+ console.log(`\n${fmt.warning("Forcing execution (--force flag)")}\n`);
56
57
  interceptor.logEvaluation(evaluation, "overridden");
57
58
  }
58
59
  else {
@@ -64,11 +65,11 @@ export function createExecCommand() {
64
65
  stdio: "inherit",
65
66
  encoding: "utf-8"
66
67
  });
67
- console.log(`\nCommand executed successfully\n`);
68
+ console.log(`\n${fmt.success("Command executed successfully")}\n`);
68
69
  process.exit(0);
69
70
  }
70
71
  catch (e) {
71
- console.error(`\nCommand failed with exit code ${e.status}\n`);
72
+ console.error(`\n${fmt.error(`Command failed with exit code ${e.status}`)}\n`);
72
73
  process.exit(e.status || 1);
73
74
  }
74
75
  });
@@ -6,8 +6,43 @@ import fs from "fs";
6
6
  import path from "path";
7
7
  import os from "os";
8
8
  import { fileURLToPath } from "url";
9
+ import { fmt } from "../../utils/formatter.js";
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
12
+ function installClaudeCodeHooks() {
13
+ const homeDir = os.homedir();
14
+ const settingsPath = path.join(homeDir, ".claude", "settings.json");
15
+ const claudeDir = path.join(homeDir, ".claude");
16
+ if (!fs.existsSync(claudeDir)) {
17
+ fs.mkdirSync(claudeDir, { recursive: true });
18
+ }
19
+ // Read existing settings or start fresh
20
+ let settings = {};
21
+ if (fs.existsSync(settingsPath)) {
22
+ try {
23
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
24
+ }
25
+ catch {
26
+ // Corrupted file — start fresh but warn
27
+ console.log(fmt.warning("Existing settings.json was unreadable, creating new one"));
28
+ }
29
+ }
30
+ // Merge hooks — don't overwrite existing non-Rafter hooks
31
+ if (!settings.hooks)
32
+ settings.hooks = {};
33
+ if (!settings.hooks.PreToolUse)
34
+ settings.hooks.PreToolUse = [];
35
+ const rafterHook = { type: "command", command: "rafter hook pretool" };
36
+ // Remove any existing Rafter hooks to avoid duplicates
37
+ settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((entry) => {
38
+ const hooks = entry.hooks || [];
39
+ return !hooks.some((h) => h.command === "rafter hook pretool");
40
+ });
41
+ // Add Rafter hooks
42
+ settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [rafterHook] }, { matcher: "Write|Edit", hooks: [rafterHook] });
43
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
44
+ console.log(fmt.success(`Installed PreToolUse hooks to ${settingsPath}`));
45
+ }
11
46
  async function installClaudeCodeSkills() {
12
47
  const homeDir = os.homedir();
13
48
  const claudeSkillsDir = path.join(homeDir, ".claude", "skills");
@@ -24,10 +59,10 @@ async function installClaudeCodeSkills() {
24
59
  }
25
60
  if (fs.existsSync(backendTemplatePath)) {
26
61
  fs.copyFileSync(backendTemplatePath, backendSkillPath);
27
- console.log(`✓ Installed Rafter Backend skill to ${backendSkillPath}`);
62
+ console.log(fmt.success(`Installed Rafter Backend skill to ${backendSkillPath}`));
28
63
  }
29
64
  else {
30
- console.log(`⚠️ Backend skill template not found at ${backendTemplatePath}`);
65
+ console.log(fmt.warning(`Backend skill template not found at ${backendTemplatePath}`));
31
66
  }
32
67
  // Install Agent Security Skill
33
68
  const agentSkillDir = path.join(claudeSkillsDir, "rafter-agent-security");
@@ -38,10 +73,10 @@ async function installClaudeCodeSkills() {
38
73
  }
39
74
  if (fs.existsSync(agentTemplatePath)) {
40
75
  fs.copyFileSync(agentTemplatePath, agentSkillPath);
41
- console.log(`✓ Installed Rafter Agent Security skill to ${agentSkillPath}`);
76
+ console.log(fmt.success(`Installed Rafter Agent Security skill to ${agentSkillPath}`));
42
77
  }
43
78
  else {
44
- console.log(`⚠️ Agent Security skill template not found at ${agentTemplatePath}`);
79
+ console.log(fmt.warning(`Agent Security skill template not found at ${agentTemplatePath}`));
45
80
  }
46
81
  }
47
82
  export function createInitCommand() {
@@ -53,58 +88,58 @@ export function createInitCommand() {
53
88
  .option("--claude-code", "Force Claude Code skill installation")
54
89
  .option("--skip-gitleaks", "Skip Gitleaks binary download")
55
90
  .action(async (opts) => {
56
- console.log("\n🛡️ Rafter Agent Security Setup");
57
- console.log("━".repeat(40));
91
+ console.log(fmt.header("Rafter Agent Security Setup"));
92
+ console.log(fmt.divider());
58
93
  console.log();
59
94
  const manager = new ConfigManager();
60
95
  // Detect environments
61
96
  const hasOpenClaw = fs.existsSync(path.join(os.homedir(), ".openclaw"));
62
97
  const hasClaudeCode = opts.claudeCode || fs.existsSync(path.join(os.homedir(), ".claude"));
63
98
  if (hasOpenClaw) {
64
- console.log("Detected environment: OpenClaw");
99
+ console.log(fmt.success("Detected environment: OpenClaw"));
65
100
  }
66
101
  else {
67
- console.log("ℹ️ OpenClaw not detected");
102
+ console.log(fmt.info("OpenClaw not detected"));
68
103
  }
69
104
  if (hasClaudeCode) {
70
- console.log("Detected environment: Claude Code");
105
+ console.log(fmt.success("Detected environment: Claude Code"));
71
106
  }
72
107
  else {
73
- console.log("ℹ️ Claude Code not detected");
108
+ console.log(fmt.info("Claude Code not detected"));
74
109
  }
75
110
  // Initialize directory structure
76
111
  try {
77
112
  await manager.initialize();
78
- console.log(`✓ Created config at ~/.rafter/config.json`);
113
+ console.log(fmt.success("Created config at ~/.rafter/config.json"));
79
114
  }
80
115
  catch (e) {
81
- console.error(`Failed to initialize: ${e}`);
116
+ console.error(fmt.error(`Failed to initialize: ${e}`));
82
117
  process.exit(1);
83
118
  }
84
119
  // Set risk level
85
120
  const validRiskLevels = ["minimal", "moderate", "aggressive"];
86
121
  if (!validRiskLevels.includes(opts.riskLevel)) {
87
- console.error(`Invalid risk level: ${opts.riskLevel}`);
122
+ console.error(fmt.error(`Invalid risk level: ${opts.riskLevel}`));
88
123
  console.error(`Valid options: ${validRiskLevels.join(", ")}`);
89
124
  process.exit(1);
90
125
  }
91
126
  manager.set("agent.riskLevel", opts.riskLevel);
92
- console.log(`✓ Set risk level: ${opts.riskLevel}`);
127
+ console.log(fmt.success(`Set risk level: ${opts.riskLevel}`));
93
128
  // Download Gitleaks binary (optional)
94
129
  if (!opts.skipGitleaks) {
95
130
  const binaryManager = new BinaryManager();
96
131
  const platformInfo = binaryManager.getPlatformInfo();
97
132
  if (!platformInfo.supported) {
98
- console.log(`ℹ️ Gitleaks not available for ${platformInfo.platform}/${platformInfo.arch}`);
99
- console.log("Using pattern-based scanning (21 patterns)");
133
+ console.log(fmt.info(`Gitleaks not available for ${platformInfo.platform}/${platformInfo.arch}`));
134
+ console.log(fmt.success("Using pattern-based scanning (21 patterns)"));
100
135
  }
101
136
  else if (binaryManager.isGitleaksInstalled()) {
102
137
  const version = await binaryManager.getGitleaksVersion();
103
- console.log(`✓ Gitleaks already installed (${version})`);
138
+ console.log(fmt.success(`Gitleaks already installed (${version})`));
104
139
  }
105
140
  else {
106
141
  console.log();
107
- console.log("📦 Downloading Gitleaks (enhanced secret detection)...");
142
+ console.log(fmt.info("Downloading Gitleaks (enhanced secret detection)..."));
108
143
  try {
109
144
  await binaryManager.downloadGitleaks((msg) => {
110
145
  console.log(` ${msg}`);
@@ -112,8 +147,8 @@ export function createInitCommand() {
112
147
  console.log();
113
148
  }
114
149
  catch (e) {
115
- console.log(`⚠️ Failed to download Gitleaks: ${e}`);
116
- console.log("Falling back to pattern-based scanning");
150
+ console.log(fmt.warning(`Failed to download Gitleaks: ${e}`));
151
+ console.log(fmt.success("Falling back to pattern-based scanning"));
117
152
  console.log();
118
153
  }
119
154
  }
@@ -124,29 +159,30 @@ export function createInitCommand() {
124
159
  const skillManager = new SkillManager();
125
160
  const installed = await skillManager.installRafterSkill();
126
161
  if (installed) {
127
- console.log("Installed Rafter Security skill to ~/.openclaw/skills/rafter-security.md");
162
+ console.log(fmt.success("Installed Rafter Security skill to ~/.openclaw/skills/rafter-security.md"));
128
163
  manager.set("agent.environments.openclaw.enabled", true);
129
164
  }
130
165
  else {
131
- console.log("⚠️ Failed to install Rafter Security skill");
166
+ console.log(fmt.warning("Failed to install Rafter Security skill"));
132
167
  }
133
168
  }
134
169
  catch (e) {
135
- console.error(`Failed to install OpenClaw skill: ${e}`);
170
+ console.error(fmt.error(`Failed to install OpenClaw skill: ${e}`));
136
171
  }
137
172
  }
138
- // Install Claude Code skills if applicable
173
+ // Install Claude Code skills + hooks if applicable
139
174
  if (hasClaudeCode && !opts.skipClaudeCode) {
140
175
  try {
141
176
  await installClaudeCodeSkills();
177
+ installClaudeCodeHooks();
142
178
  manager.set("agent.environments.claudeCode.enabled", true);
143
179
  }
144
180
  catch (e) {
145
- console.error(`Failed to install Claude Code skills: ${e}`);
181
+ console.error(fmt.error(`Failed to install Claude Code integration: ${e}`));
146
182
  }
147
183
  }
148
184
  console.log();
149
- console.log("Agent security initialized!");
185
+ console.log(fmt.success("Agent security initialized!"));
150
186
  console.log();
151
187
  console.log("Next steps:");
152
188
  if (hasOpenClaw && !opts.skipOpenclaw) {