@rafter-security/cli 0.4.1 → 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.
@@ -1,9 +1,11 @@
1
1
  import { Command } from "commander";
2
2
  import { RegexScanner } from "../../scanners/regex-scanner.js";
3
3
  import { GitleaksScanner } from "../../scanners/gitleaks.js";
4
- import { execSync } from "child_process";
4
+ import { ConfigManager } from "../../core/config-manager.js";
5
+ import { execSync, execFileSync } from "child_process";
5
6
  import fs from "fs";
6
7
  import path from "path";
8
+ import { fmt } from "../../utils/formatter.js";
7
9
  export function createScanCommand() {
8
10
  return new Command("scan")
9
11
  .description("Scan files or directories for secrets")
@@ -11,11 +13,21 @@ export function createScanCommand() {
11
13
  .option("-q, --quiet", "Only output if secrets found")
12
14
  .option("--json", "Output as JSON")
13
15
  .option("--staged", "Scan only git staged files")
16
+ .option("--diff <ref>", "Scan files changed since a git ref")
14
17
  .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
15
18
  .action(async (scanPath, opts) => {
19
+ // Load policy-merged config for excludePaths/customPatterns
20
+ const manager = new ConfigManager();
21
+ const cfg = manager.loadWithPolicy();
22
+ const scanCfg = cfg.agent?.scan;
23
+ // Handle --diff flag
24
+ if (opts.diff) {
25
+ await scanDiffFiles(opts.diff, opts, scanCfg);
26
+ return;
27
+ }
16
28
  // Handle --staged flag
17
29
  if (opts.staged) {
18
- await scanStagedFiles(opts);
30
+ await scanStagedFiles(opts, scanCfg);
19
31
  return;
20
32
  }
21
33
  const resolvedPath = path.resolve(scanPath);
@@ -25,7 +37,7 @@ export function createScanCommand() {
25
37
  process.exit(1);
26
38
  }
27
39
  // Determine scan engine
28
- const engine = await selectEngine(opts.engine, opts.quiet);
40
+ const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
29
41
  // Determine if path is file or directory
30
42
  const stats = fs.statSync(resolvedPath);
31
43
  let results;
@@ -33,61 +45,109 @@ export function createScanCommand() {
33
45
  if (!opts.quiet) {
34
46
  console.error(`Scanning directory: ${resolvedPath} (${engine})`);
35
47
  }
36
- results = await scanDirectory(resolvedPath, engine);
48
+ results = await scanDirectory(resolvedPath, engine, scanCfg);
37
49
  }
38
50
  else {
39
51
  if (!opts.quiet) {
40
52
  console.error(`Scanning file: ${resolvedPath} (${engine})`);
41
53
  }
42
- results = await scanFile(resolvedPath, engine);
54
+ results = await scanFile(resolvedPath, engine, scanCfg);
43
55
  }
44
- // Output results
45
- if (opts.json) {
46
- console.log(JSON.stringify(results, null, 2));
56
+ outputScanResults(results, opts);
57
+ });
58
+ }
59
+ /**
60
+ * Shared output logic for scan results
61
+ */
62
+ function outputScanResults(results, opts, context) {
63
+ if (opts.json) {
64
+ console.log(JSON.stringify(results, null, 2));
65
+ process.exit(results.length > 0 ? 1 : 0);
66
+ }
67
+ if (results.length === 0) {
68
+ if (!opts.quiet) {
69
+ const msg = context ? `No secrets detected in ${context}` : "No secrets detected";
70
+ console.log(`\n${fmt.success(msg)}\n`);
47
71
  }
48
- else {
49
- if (results.length === 0) {
50
- if (!opts.quiet) {
51
- console.log("\n✓ No secrets detected\n");
52
- }
53
- process.exit(0);
54
- }
55
- else {
56
- console.log(`\n⚠️ Found secrets in ${results.length} file(s):\n`);
57
- let totalMatches = 0;
58
- for (const result of results) {
59
- console.log(`\n📄 ${result.file}`);
60
- for (const match of result.matches) {
61
- totalMatches++;
62
- const location = match.line ? `Line ${match.line}` : "Unknown location";
63
- const severity = getSeverityEmoji(match.pattern.severity);
64
- console.log(` ${severity} [${match.pattern.severity.toUpperCase()}] ${match.pattern.name}`);
65
- console.log(` Location: ${location}`);
66
- console.log(` Pattern: ${match.pattern.description || match.pattern.regex}`);
67
- console.log(` Redacted: ${match.redacted}`);
68
- console.log();
69
- }
70
- }
71
- console.log(`\n⚠️ Total: ${totalMatches} secret(s) detected in ${results.length} file(s)\n`);
72
- console.log("Run 'rafter agent audit' to see the security log.\n");
73
- process.exit(1);
72
+ process.exit(0);
73
+ }
74
+ console.log(`\n${fmt.warning(`Found secrets in ${results.length} file(s):`)}\n`);
75
+ let totalMatches = 0;
76
+ for (const result of results) {
77
+ console.log(`\n${fmt.info(result.file)}`);
78
+ for (const match of result.matches) {
79
+ totalMatches++;
80
+ const location = match.line ? `Line ${match.line}` : "Unknown location";
81
+ const sev = fmt.severity(match.pattern.severity);
82
+ console.log(` ${sev} ${match.pattern.name}`);
83
+ console.log(` Location: ${location}`);
84
+ console.log(` Pattern: ${match.pattern.description || match.pattern.regex}`);
85
+ console.log(` Redacted: ${match.redacted}`);
86
+ console.log();
87
+ }
88
+ }
89
+ console.log(`\n${fmt.warning(`Total: ${totalMatches} secret(s) detected in ${results.length} file(s)`)}\n`);
90
+ if (context === "staged files") {
91
+ console.log(`${fmt.error("Commit blocked. Remove secrets before committing.")}\n`);
92
+ }
93
+ else {
94
+ console.log(`Run 'rafter agent audit' to see the security log.\n`);
95
+ }
96
+ process.exit(1);
97
+ }
98
+ /**
99
+ * Scan files changed since a git ref
100
+ */
101
+ async function scanDiffFiles(ref, opts, scanCfg) {
102
+ try {
103
+ const diffOutput = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACM", ref], {
104
+ encoding: "utf-8",
105
+ stdio: ["pipe", "pipe", "ignore"],
106
+ }).trim();
107
+ if (!diffOutput) {
108
+ if (!opts.quiet) {
109
+ console.log(fmt.success(`No files changed since ${ref}`));
74
110
  }
111
+ process.exit(0);
75
112
  }
76
- });
113
+ const changedFiles = diffOutput.split("\n").map(f => f.trim()).filter(f => f);
114
+ if (!opts.quiet) {
115
+ console.error(`Scanning ${changedFiles.length} file(s) changed since ${ref}...`);
116
+ }
117
+ const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
118
+ const allResults = [];
119
+ for (const file of changedFiles) {
120
+ const filePath = path.resolve(file);
121
+ if (!fs.existsSync(filePath))
122
+ continue;
123
+ const stats = fs.statSync(filePath);
124
+ if (!stats.isFile())
125
+ continue;
126
+ const results = await scanFile(filePath, engine, scanCfg);
127
+ allResults.push(...results);
128
+ }
129
+ outputScanResults(allResults, opts, `files changed since ${ref}`);
130
+ }
131
+ catch (error) {
132
+ if (error.status === 128) {
133
+ console.error("Error: Not in a git repository or invalid ref");
134
+ process.exit(1);
135
+ }
136
+ throw error;
137
+ }
77
138
  }
78
139
  /**
79
140
  * Scan git staged files for secrets
80
141
  */
81
- async function scanStagedFiles(opts) {
142
+ async function scanStagedFiles(opts, scanCfg) {
82
143
  try {
83
- // Get list of staged files
84
144
  const stagedFilesOutput = execSync("git diff --cached --name-only --diff-filter=ACM", {
85
145
  encoding: "utf-8",
86
146
  stdio: ["pipe", "pipe", "ignore"]
87
147
  }).trim();
88
148
  if (!stagedFilesOutput) {
89
149
  if (!opts.quiet) {
90
- console.log("No files staged for commit");
150
+ console.log(fmt.success("No files staged for commit"));
91
151
  }
92
152
  process.exit(0);
93
153
  }
@@ -95,56 +155,19 @@ async function scanStagedFiles(opts) {
95
155
  if (!opts.quiet) {
96
156
  console.error(`Scanning ${stagedFiles.length} staged file(s)...`);
97
157
  }
98
- // Determine scan engine
99
158
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
100
- // Scan each staged file
101
159
  const allResults = [];
102
160
  for (const file of stagedFiles) {
103
161
  const filePath = path.resolve(file);
104
- // Skip if file doesn't exist (might be deleted)
105
- if (!fs.existsSync(filePath)) {
162
+ if (!fs.existsSync(filePath))
106
163
  continue;
107
- }
108
- // Skip if not a regular file
109
164
  const stats = fs.statSync(filePath);
110
- if (!stats.isFile()) {
165
+ if (!stats.isFile())
111
166
  continue;
112
- }
113
- const results = await scanFile(filePath, engine);
167
+ const results = await scanFile(filePath, engine, scanCfg);
114
168
  allResults.push(...results);
115
169
  }
116
- // Output results (same as regular scan)
117
- if (opts.json) {
118
- console.log(JSON.stringify(allResults, null, 2));
119
- }
120
- else {
121
- if (allResults.length === 0) {
122
- if (!opts.quiet) {
123
- console.log("\n✓ No secrets detected in staged files\n");
124
- }
125
- process.exit(0);
126
- }
127
- else {
128
- console.log(`\n⚠️ Found secrets in ${allResults.length} staged file(s):\n`);
129
- let totalMatches = 0;
130
- for (const result of allResults) {
131
- console.log(`\n📄 ${result.file}`);
132
- for (const match of result.matches) {
133
- totalMatches++;
134
- const location = match.line ? `Line ${match.line}` : "Unknown location";
135
- const severity = getSeverityEmoji(match.pattern.severity);
136
- console.log(` ${severity} [${match.pattern.severity.toUpperCase()}] ${match.pattern.name}`);
137
- console.log(` Location: ${location}`);
138
- console.log(` Pattern: ${match.pattern.description || match.pattern.regex}`);
139
- console.log(` Redacted: ${match.redacted}`);
140
- console.log();
141
- }
142
- }
143
- console.log(`\n⚠️ Total: ${totalMatches} secret(s) detected in ${allResults.length} file(s)\n`);
144
- console.log("❌ Commit blocked. Remove secrets before committing.\n");
145
- process.exit(1);
146
- }
147
- }
170
+ outputScanResults(allResults, opts, "staged files");
148
171
  }
149
172
  catch (error) {
150
173
  if (error.status === 128) {
@@ -154,15 +177,6 @@ async function scanStagedFiles(opts) {
154
177
  throw error;
155
178
  }
156
179
  }
157
- function getSeverityEmoji(severity) {
158
- const emojiMap = {
159
- critical: "🔴",
160
- high: "🟠",
161
- medium: "🟡",
162
- low: "🟢"
163
- };
164
- return emojiMap[severity] || "⚪";
165
- }
166
180
  /**
167
181
  * Select scan engine based on availability and user preference
168
182
  */
@@ -175,7 +189,7 @@ async function selectEngine(preference, quiet) {
175
189
  const available = await gitleaks.isAvailable();
176
190
  if (!available) {
177
191
  if (!quiet) {
178
- console.error("⚠️ Gitleaks requested but not available, using patterns");
192
+ console.error(fmt.warning("Gitleaks requested but not available, using patterns"));
179
193
  }
180
194
  return "patterns";
181
195
  }
@@ -189,7 +203,7 @@ async function selectEngine(preference, quiet) {
189
203
  /**
190
204
  * Scan a file with selected engine
191
205
  */
192
- async function scanFile(filePath, engine) {
206
+ async function scanFile(filePath, engine, scanCfg) {
193
207
  if (engine === "gitleaks") {
194
208
  try {
195
209
  const gitleaks = new GitleaksScanner();
@@ -197,15 +211,14 @@ async function scanFile(filePath, engine) {
197
211
  return result.matches.length > 0 ? [result] : [];
198
212
  }
199
213
  catch (e) {
200
- // Fall back to patterns on error
201
- console.error(`⚠️ Gitleaks scan failed, falling back to patterns`);
202
- const scanner = new RegexScanner();
214
+ console.error(fmt.warning("Gitleaks scan failed, falling back to patterns"));
215
+ const scanner = new RegexScanner(scanCfg?.customPatterns);
203
216
  const result = scanner.scanFile(filePath);
204
217
  return result.matches.length > 0 ? [result] : [];
205
218
  }
206
219
  }
207
220
  else {
208
- const scanner = new RegexScanner();
221
+ const scanner = new RegexScanner(scanCfg?.customPatterns);
209
222
  const result = scanner.scanFile(filePath);
210
223
  return result.matches.length > 0 ? [result] : [];
211
224
  }
@@ -213,21 +226,20 @@ async function scanFile(filePath, engine) {
213
226
  /**
214
227
  * Scan a directory with selected engine
215
228
  */
216
- async function scanDirectory(dirPath, engine) {
229
+ async function scanDirectory(dirPath, engine, scanCfg) {
217
230
  if (engine === "gitleaks") {
218
231
  try {
219
232
  const gitleaks = new GitleaksScanner();
220
233
  return await gitleaks.scanDirectory(dirPath);
221
234
  }
222
235
  catch (e) {
223
- // Fall back to patterns on error
224
- console.error(`⚠️ Gitleaks scan failed, falling back to patterns`);
225
- const scanner = new RegexScanner();
226
- return scanner.scanDirectory(dirPath);
236
+ console.error(fmt.warning("Gitleaks scan failed, falling back to patterns"));
237
+ const scanner = new RegexScanner(scanCfg?.customPatterns);
238
+ return scanner.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
227
239
  }
228
240
  }
229
241
  else {
230
- const scanner = new RegexScanner();
231
- return scanner.scanDirectory(dirPath);
242
+ const scanner = new RegexScanner(scanCfg?.customPatterns);
243
+ return scanner.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
232
244
  }
233
245
  }
@@ -0,0 +1,8 @@
1
+ import { Command } from "commander";
2
+ import { createCiInitCommand } from "./init.js";
3
+ export function createCiCommand() {
4
+ const ci = new Command("ci")
5
+ .description("CI/CD integration commands");
6
+ ci.addCommand(createCiInitCommand());
7
+ return ci;
8
+ }
@@ -0,0 +1,191 @@
1
+ import { Command } from "commander";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { fmt } from "../../utils/formatter.js";
5
+ export function createCiInitCommand() {
6
+ return new Command("init")
7
+ .description("Generate CI/CD pipeline config for secret scanning")
8
+ .option("--platform <platform>", "CI platform: github, gitlab, circleci (default: auto-detect)")
9
+ .option("--output <path>", "Output file path (default: platform-specific)")
10
+ .option("--with-backend", "Include backend security audit job (requires RAFTER_API_KEY)")
11
+ .action((opts) => {
12
+ const platform = opts.platform || detectPlatform();
13
+ if (!platform) {
14
+ console.error(fmt.error("Could not auto-detect CI platform."));
15
+ console.error("Specify one with --platform github|gitlab|circleci");
16
+ process.exit(1);
17
+ }
18
+ const validPlatforms = ["github", "gitlab", "circleci"];
19
+ if (!validPlatforms.includes(platform)) {
20
+ console.error(fmt.error(`Unknown platform: ${platform}`));
21
+ console.error(`Valid options: ${validPlatforms.join(", ")}`);
22
+ process.exit(1);
23
+ }
24
+ const { content, defaultPath } = generateTemplate(platform, !!opts.withBackend);
25
+ const outputPath = opts.output || defaultPath;
26
+ const outputDir = path.dirname(outputPath);
27
+ if (!fs.existsSync(outputDir)) {
28
+ fs.mkdirSync(outputDir, { recursive: true });
29
+ }
30
+ fs.writeFileSync(outputPath, content, "utf-8");
31
+ console.log(fmt.success(`Generated ${platform} CI config at ${outputPath}`));
32
+ console.log();
33
+ console.log("Next steps:");
34
+ console.log(` 1. Review the generated file: ${outputPath}`);
35
+ if (opts.withBackend) {
36
+ if (platform === "github") {
37
+ console.log(" 2. Add RAFTER_API_KEY to repo Settings > Secrets > Actions");
38
+ }
39
+ else if (platform === "gitlab") {
40
+ console.log(" 2. Add RAFTER_API_KEY to Settings > CI/CD > Variables");
41
+ }
42
+ else {
43
+ console.log(" 2. Add RAFTER_API_KEY to project environment variables");
44
+ }
45
+ }
46
+ console.log(` ${opts.withBackend ? "3" : "2"}. Commit and push to trigger the pipeline`);
47
+ console.log();
48
+ });
49
+ }
50
+ function detectPlatform() {
51
+ if (fs.existsSync(".github"))
52
+ return "github";
53
+ if (fs.existsSync(".gitlab-ci.yml"))
54
+ return "gitlab";
55
+ if (fs.existsSync(".circleci"))
56
+ return "circleci";
57
+ return null;
58
+ }
59
+ function generateTemplate(platform, withBackend) {
60
+ switch (platform) {
61
+ case "github":
62
+ return { content: githubTemplate(withBackend), defaultPath: ".github/workflows/rafter-security.yml" };
63
+ case "gitlab":
64
+ return { content: gitlabTemplate(withBackend), defaultPath: ".gitlab-ci-rafter.yml" };
65
+ case "circleci":
66
+ return { content: circleciTemplate(withBackend), defaultPath: ".circleci/rafter-security.yml" };
67
+ }
68
+ }
69
+ function githubTemplate(withBackend) {
70
+ let yaml = `# Generated by: rafter ci init
71
+ name: Rafter Security
72
+
73
+ on:
74
+ push:
75
+ branches: [main]
76
+ pull_request:
77
+ branches: [main]
78
+
79
+ permissions:
80
+ contents: read
81
+
82
+ jobs:
83
+ secret-scan:
84
+ runs-on: ubuntu-latest
85
+ steps:
86
+ - uses: actions/checkout@v4
87
+
88
+ - name: Install Rafter CLI
89
+ run: npm install -g @rafter-security/cli
90
+
91
+ - name: Scan for secrets
92
+ run: rafter agent scan . --quiet
93
+ `;
94
+ if (withBackend) {
95
+ yaml += `
96
+ security-audit:
97
+ runs-on: ubuntu-latest
98
+ needs: secret-scan
99
+ steps:
100
+ - uses: actions/checkout@v4
101
+
102
+ - name: Install Rafter CLI
103
+ run: npm install -g @rafter-security/cli
104
+
105
+ - name: Run security audit
106
+ env:
107
+ RAFTER_API_KEY: \${{ secrets.RAFTER_API_KEY }}
108
+ run: rafter run --format json --quiet
109
+ `;
110
+ }
111
+ return yaml;
112
+ }
113
+ function gitlabTemplate(withBackend) {
114
+ let yaml = `# Generated by: rafter ci init
115
+ stages:
116
+ - security
117
+
118
+ secret-scan:
119
+ stage: security
120
+ image: node:20
121
+ script:
122
+ - npm install -g @rafter-security/cli
123
+ - rafter agent scan . --quiet
124
+ rules:
125
+ - if: $CI_PIPELINE_SOURCE == "push"
126
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
127
+ `;
128
+ if (withBackend) {
129
+ yaml += `
130
+ security-audit:
131
+ stage: security
132
+ image: node:20
133
+ needs: [secret-scan]
134
+ script:
135
+ - npm install -g @rafter-security/cli
136
+ - rafter run --format json --quiet
137
+ variables:
138
+ RAFTER_API_KEY: $RAFTER_API_KEY
139
+ rules:
140
+ - if: $CI_PIPELINE_SOURCE == "push"
141
+ - if: $CI_PIPELINE_SOURCE == "merge_request_event"
142
+ `;
143
+ }
144
+ return yaml;
145
+ }
146
+ function circleciTemplate(withBackend) {
147
+ let yaml = `# Generated by: rafter ci init
148
+ version: 2.1
149
+
150
+ jobs:
151
+ secret-scan:
152
+ docker:
153
+ - image: cimg/node:20.0
154
+ steps:
155
+ - checkout
156
+ - run:
157
+ name: Install Rafter CLI
158
+ command: npm install -g @rafter-security/cli
159
+ - run:
160
+ name: Scan for secrets
161
+ command: rafter agent scan . --quiet
162
+ `;
163
+ if (withBackend) {
164
+ yaml += `
165
+ security-audit:
166
+ docker:
167
+ - image: cimg/node:20.0
168
+ steps:
169
+ - checkout
170
+ - run:
171
+ name: Install Rafter CLI
172
+ command: npm install -g @rafter-security/cli
173
+ - run:
174
+ name: Run security audit
175
+ command: rafter run --format json --quiet
176
+ `;
177
+ }
178
+ yaml += `
179
+ workflows:
180
+ security:
181
+ jobs:
182
+ - secret-scan`;
183
+ if (withBackend) {
184
+ yaml += `
185
+ - security-audit:
186
+ requires:
187
+ - secret-scan`;
188
+ }
189
+ yaml += "\n";
190
+ return yaml;
191
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from "commander";
2
+ import { createHookPretoolCommand } from "./pretool.js";
3
+ export function createHookCommand() {
4
+ const hook = new Command("hook")
5
+ .description("Hook handlers for agent platform integration");
6
+ hook.addCommand(createHookPretoolCommand());
7
+ return hook;
8
+ }
@@ -0,0 +1,122 @@
1
+ import { Command } from "commander";
2
+ import { CommandInterceptor } from "../../core/command-interceptor.js";
3
+ import { RegexScanner } from "../../scanners/regex-scanner.js";
4
+ import { AuditLogger } from "../../core/audit-logger.js";
5
+ import { execSync } from "child_process";
6
+ export function createHookPretoolCommand() {
7
+ return new Command("pretool")
8
+ .description("PreToolUse hook handler (reads stdin, writes JSON decision to stdout)")
9
+ .action(async () => {
10
+ const input = await readStdin();
11
+ let payload;
12
+ try {
13
+ payload = JSON.parse(input);
14
+ }
15
+ catch {
16
+ // Can't parse → fail open
17
+ writeDecision({ decision: "allow" });
18
+ return;
19
+ }
20
+ const decision = evaluateToolCall(payload);
21
+ writeDecision(decision);
22
+ });
23
+ }
24
+ function evaluateToolCall(payload) {
25
+ const { tool_name, tool_input } = payload;
26
+ if (tool_name === "Bash") {
27
+ return evaluateBash(tool_input.command || "");
28
+ }
29
+ if (tool_name === "Write" || tool_name === "Edit") {
30
+ return evaluateWrite(tool_input);
31
+ }
32
+ return { decision: "allow" };
33
+ }
34
+ function evaluateBash(command) {
35
+ const interceptor = new CommandInterceptor();
36
+ const audit = new AuditLogger();
37
+ const evaluation = interceptor.evaluate(command);
38
+ // Blocked — hard deny
39
+ if (!evaluation.allowed && !evaluation.requiresApproval) {
40
+ audit.logCommandIntercepted(command, false, "blocked", evaluation.reason);
41
+ return {
42
+ decision: "deny",
43
+ reason: `Blocked by Rafter policy: ${evaluation.reason}`,
44
+ };
45
+ }
46
+ // Requires approval — deny (agent can't provide interactive approval)
47
+ if (evaluation.requiresApproval) {
48
+ audit.logCommandIntercepted(command, false, "blocked", evaluation.reason);
49
+ return {
50
+ decision: "deny",
51
+ reason: `Rafter policy requires approval: ${evaluation.reason}`,
52
+ };
53
+ }
54
+ // Git commit/push — scan staged files for secrets
55
+ const trimmed = command.trim();
56
+ if (trimmed.startsWith("git commit") || trimmed.startsWith("git push")) {
57
+ const scanResult = scanStagedFiles();
58
+ if (scanResult.secretsFound) {
59
+ audit.logSecretDetected("staged files", `${scanResult.count} secret(s)`, "blocked");
60
+ return {
61
+ decision: "deny",
62
+ reason: `${scanResult.count} secret(s) detected in ${scanResult.files} staged file(s). Run 'rafter agent scan --staged' for details.`,
63
+ };
64
+ }
65
+ }
66
+ audit.logCommandIntercepted(command, true, "allowed");
67
+ return { decision: "allow" };
68
+ }
69
+ function evaluateWrite(toolInput) {
70
+ // Write uses "content", Edit uses "new_string"
71
+ const content = toolInput.content || toolInput.new_string || "";
72
+ if (!content) {
73
+ return { decision: "allow" };
74
+ }
75
+ const scanner = new RegexScanner();
76
+ if (scanner.hasSecrets(content)) {
77
+ const matches = scanner.scanText(content);
78
+ const names = [...new Set(matches.map(m => m.pattern.name))];
79
+ const audit = new AuditLogger();
80
+ audit.logSecretDetected(toolInput.file_path || "file content", names.join(", "), "blocked");
81
+ return {
82
+ decision: "deny",
83
+ reason: `Secret detected in file content: ${names.join(", ")}`,
84
+ };
85
+ }
86
+ return { decision: "allow" };
87
+ }
88
+ function scanStagedFiles() {
89
+ try {
90
+ const stagedOutput = execSync("git diff --cached --name-only --diff-filter=ACM", {
91
+ encoding: "utf-8",
92
+ stdio: ["pipe", "pipe", "ignore"],
93
+ }).trim();
94
+ if (!stagedOutput) {
95
+ return { secretsFound: false, count: 0, files: 0 };
96
+ }
97
+ const stagedFiles = stagedOutput.split("\n").filter(f => f.trim());
98
+ const scanner = new RegexScanner();
99
+ const results = scanner.scanFiles(stagedFiles);
100
+ const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
101
+ return {
102
+ secretsFound: results.length > 0,
103
+ count: totalMatches,
104
+ files: results.length,
105
+ };
106
+ }
107
+ catch {
108
+ return { secretsFound: false, count: 0, files: 0 };
109
+ }
110
+ }
111
+ function readStdin() {
112
+ return new Promise((resolve) => {
113
+ let data = "";
114
+ process.stdin.setEncoding("utf-8");
115
+ process.stdin.on("data", (chunk) => { data += chunk; });
116
+ process.stdin.on("end", () => { resolve(data); });
117
+ process.stdin.resume();
118
+ });
119
+ }
120
+ function writeDecision(decision) {
121
+ process.stdout.write(JSON.stringify(decision) + "\n");
122
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from "commander";
2
+ import { createMcpServeCommand } from "./server.js";
3
+ export function createMcpCommand() {
4
+ const mcp = new Command("mcp")
5
+ .description("MCP server for cross-platform security tools");
6
+ mcp.addCommand(createMcpServeCommand());
7
+ return mcp;
8
+ }