@rafter-security/cli 0.5.5 → 0.6.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.
Files changed (40) hide show
  1. package/README.md +15 -3
  2. package/dist/commands/agent/audit-skill.js +1 -1
  3. package/dist/commands/agent/audit.js +106 -6
  4. package/dist/commands/agent/baseline.js +10 -0
  5. package/dist/commands/agent/exec.js +1 -1
  6. package/dist/commands/agent/init.js +366 -26
  7. package/dist/commands/agent/scan.js +160 -16
  8. package/dist/commands/agent/status.js +65 -4
  9. package/dist/commands/agent/verify.js +18 -4
  10. package/dist/commands/backend/run.js +76 -62
  11. package/dist/commands/ci/init.js +10 -3
  12. package/dist/commands/completion.js +21 -9
  13. package/dist/commands/hook/posttool.js +21 -7
  14. package/dist/commands/hook/pretool.js +50 -13
  15. package/dist/commands/issues/dedup.js +39 -0
  16. package/dist/commands/issues/from-scan.js +143 -0
  17. package/dist/commands/issues/from-text.js +185 -0
  18. package/dist/commands/issues/github-client.js +85 -0
  19. package/dist/commands/issues/index.js +25 -0
  20. package/dist/commands/issues/issue-builder.js +101 -0
  21. package/dist/commands/mcp/server.js +4 -1
  22. package/dist/commands/policy/export.js +7 -2
  23. package/dist/commands/scan/index.js +45 -0
  24. package/dist/core/audit-logger.js +106 -7
  25. package/dist/core/config-defaults.js +24 -0
  26. package/dist/core/config-manager.js +116 -3
  27. package/dist/core/custom-patterns.js +20 -17
  28. package/dist/core/pattern-engine.js +26 -1
  29. package/dist/core/policy-loader.js +25 -2
  30. package/dist/index.js +11 -2
  31. package/dist/scanners/gitleaks.js +8 -7
  32. package/dist/scanners/regex-scanner.js +16 -1
  33. package/dist/scanners/secret-patterns.js +6 -6
  34. package/dist/utils/api.js +18 -0
  35. package/dist/utils/binary-manager.js +74 -7
  36. package/dist/utils/skill-manager.js +5 -3
  37. package/package.json +5 -3
  38. package/resources/pre-commit-hook.sh +2 -2
  39. package/resources/pre-push-hook.sh +2 -2
  40. package/resources/rafter-security-skill.md +7 -11
@@ -2,11 +2,15 @@ import { Command } from "commander";
2
2
  import { RegexScanner } from "../../scanners/regex-scanner.js";
3
3
  import { GitleaksScanner } from "../../scanners/gitleaks.js";
4
4
  import { ConfigManager } from "../../core/config-manager.js";
5
+ import { AuditLogger } from "../../core/audit-logger.js";
5
6
  import { execSync, execFileSync } from "child_process";
6
7
  import fs from "fs";
7
8
  import os from "os";
8
9
  import path from "path";
9
10
  import { fmt } from "../../utils/formatter.js";
11
+ import { createRequire } from "module";
12
+ const _require = createRequire(import.meta.url);
13
+ const { version: CLI_VERSION } = _require("../../../package.json");
10
14
  function loadBaselineEntries() {
11
15
  const baselinePath = path.join(os.homedir(), ".rafter", "baseline.json");
12
16
  if (!fs.existsSync(baselinePath))
@@ -42,7 +46,28 @@ export function createScanCommand() {
42
46
  .option("--diff <ref>", "Scan files changed since a git ref")
43
47
  .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
44
48
  .option("--baseline", "Filter findings present in the saved baseline")
49
+ .option("--watch", "Watch for file changes and re-scan on change")
45
50
  .action(async (scanPath, opts) => {
51
+ // Validate flags before doing any work
52
+ const validEngines = ["auto", "gitleaks", "patterns"];
53
+ const engineValue = opts.engine || "auto";
54
+ if (!validEngines.includes(engineValue)) {
55
+ console.error(`Invalid engine: ${engineValue}. Valid values: ${validEngines.join(", ")}`);
56
+ process.exit(2);
57
+ }
58
+ const format = opts.format ?? (opts.json ? "json" : "text");
59
+ const validFormats = ["text", "json", "sarif"];
60
+ if (!validFormats.includes(format)) {
61
+ console.error(`Invalid format: ${format}. Valid values: ${validFormats.join(", ")}`);
62
+ process.exit(2);
63
+ }
64
+ // Deprecation notice — only when invoked as `rafter agent scan`, not as `rafter scan local`
65
+ const argv = process.argv;
66
+ const isAgentScan = argv.includes("agent") && argv.includes("scan") &&
67
+ argv.indexOf("agent") < argv.indexOf("scan");
68
+ if (isAgentScan) {
69
+ process.stderr.write("Warning: rafter agent scan is deprecated and will be removed in a future major version. Use rafter scan local instead.\n");
70
+ }
46
71
  // Load policy-merged config for excludePaths/customPatterns
47
72
  const manager = new ConfigManager();
48
73
  const cfg = manager.loadWithPolicy();
@@ -64,6 +89,11 @@ export function createScanCommand() {
64
89
  console.error(`Error: Path not found: ${resolvedPath}`);
65
90
  process.exit(2);
66
91
  }
92
+ // Handle --watch flag
93
+ if (opts.watch) {
94
+ await watchAndScan(resolvedPath, opts, scanCfg);
95
+ return;
96
+ }
67
97
  // Determine scan engine
68
98
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
69
99
  // Determine if path is file or directory
@@ -123,7 +153,7 @@ function outputSarif(results) {
123
153
  tool: {
124
154
  driver: {
125
155
  name: "rafter",
126
- version: "0.5.5",
156
+ version: CLI_VERSION,
127
157
  informationUri: "https://rafter.so",
128
158
  rules: Array.from(rules.values()),
129
159
  },
@@ -138,7 +168,7 @@ function outputSarif(results) {
138
168
  /**
139
169
  * Shared output logic for scan results
140
170
  */
141
- function outputScanResults(results, opts, context) {
171
+ function outputScanResults(results, opts, context, exitOnFindings = true) {
142
172
  const format = opts.format ?? (opts.json ? "json" : "text");
143
173
  if (!["text", "json", "sarif"].includes(format)) {
144
174
  console.error(`Invalid format: ${format}. Valid values: text, json, sarif`);
@@ -159,14 +189,18 @@ function outputScanResults(results, opts, context) {
159
189
  })),
160
190
  }));
161
191
  console.log(JSON.stringify(out, null, 2));
162
- process.exit(results.length > 0 ? 1 : 0);
192
+ if (exitOnFindings)
193
+ process.exit(results.length > 0 ? 1 : 0);
194
+ return;
163
195
  }
164
196
  if (results.length === 0) {
165
197
  if (!opts.quiet) {
166
198
  const msg = context ? `No secrets detected in ${context}` : "No secrets detected";
167
199
  console.log(`\n${fmt.success(msg)}\n`);
168
200
  }
169
- process.exit(0);
201
+ if (exitOnFindings)
202
+ process.exit(0);
203
+ return;
170
204
  }
171
205
  console.log(`\n${fmt.warning(`Found secrets in ${results.length} file(s):`)}\n`);
172
206
  let totalMatches = 0;
@@ -187,10 +221,11 @@ function outputScanResults(results, opts, context) {
187
221
  if (context === "staged files") {
188
222
  console.log(`${fmt.error("Commit blocked. Remove secrets before committing.")}\n`);
189
223
  }
190
- else {
224
+ else if (exitOnFindings) {
191
225
  console.log(`Run 'rafter agent audit' to see the security log.\n`);
192
226
  }
193
- process.exit(1);
227
+ if (exitOnFindings)
228
+ process.exit(1);
194
229
  }
195
230
  /**
196
231
  * Scan files changed since a git ref
@@ -202,19 +237,21 @@ async function scanDiffFiles(ref, opts, scanCfg, baselineEntries = []) {
202
237
  stdio: ["pipe", "pipe", "ignore"],
203
238
  }).trim();
204
239
  if (!diffOutput) {
205
- if (!opts.quiet) {
206
- console.log(fmt.success(`No files changed since ${ref}`));
207
- }
208
- process.exit(0);
240
+ outputScanResults([], opts, `files changed since ${ref}`);
241
+ return;
209
242
  }
210
243
  const changedFiles = diffOutput.split("\n").map(f => f.trim()).filter(f => f);
211
244
  if (!opts.quiet) {
212
245
  console.error(`Scanning ${changedFiles.length} file(s) changed since ${ref}...`);
213
246
  }
247
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
248
+ encoding: "utf-8",
249
+ stdio: ["pipe", "pipe", "ignore"],
250
+ }).trim();
214
251
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
215
252
  const allResults = [];
216
253
  for (const file of changedFiles) {
217
- const filePath = path.resolve(file);
254
+ const filePath = path.resolve(repoRoot, file);
218
255
  if (!fs.existsSync(filePath))
219
256
  continue;
220
257
  const stats = fs.statSync(filePath);
@@ -243,19 +280,21 @@ async function scanStagedFiles(opts, scanCfg, baselineEntries = []) {
243
280
  stdio: ["pipe", "pipe", "ignore"]
244
281
  }).trim();
245
282
  if (!stagedFilesOutput) {
246
- if (!opts.quiet) {
247
- console.log(fmt.success("No files staged for commit"));
248
- }
249
- process.exit(0);
283
+ outputScanResults([], opts, "staged files");
284
+ return;
250
285
  }
251
286
  const stagedFiles = stagedFilesOutput.split("\n").map(f => f.trim()).filter(f => f);
252
287
  if (!opts.quiet) {
253
288
  console.error(`Scanning ${stagedFiles.length} staged file(s)...`);
254
289
  }
290
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
291
+ encoding: "utf-8",
292
+ stdio: ["pipe", "pipe", "ignore"],
293
+ }).trim();
255
294
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
256
295
  const allResults = [];
257
296
  for (const file of stagedFiles) {
258
- const filePath = path.resolve(file);
297
+ const filePath = path.resolve(repoRoot, file);
259
298
  if (!fs.existsSync(filePath))
260
299
  continue;
261
300
  const stats = fs.statSync(filePath);
@@ -292,6 +331,10 @@ async function selectEngine(preference, quiet) {
292
331
  }
293
332
  return "gitleaks";
294
333
  }
334
+ if (preference !== "auto") {
335
+ console.error(`Invalid engine: ${preference}. Valid values: auto, gitleaks, patterns`);
336
+ process.exit(2);
337
+ }
295
338
  // Auto mode: try Gitleaks, fall back to patterns
296
339
  const gitleaks = new GitleaksScanner();
297
340
  const available = await gitleaks.isAvailable();
@@ -340,3 +383,104 @@ async function scanDirectory(dirPath, engine, scanCfg) {
340
383
  return scanner.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
341
384
  }
342
385
  }
386
+ /**
387
+ * Watch a path for changes and re-scan on each change
388
+ */
389
+ async function watchAndScan(watchPath, opts, scanCfg) {
390
+ const { watch } = await import("chokidar");
391
+ const logger = new AuditLogger();
392
+ const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
393
+ if (!opts.quiet) {
394
+ console.error(fmt.info(`Watching ${watchPath} for changes (${engine}). Press Ctrl+C to exit.`));
395
+ }
396
+ // Do an initial scan
397
+ const stats = fs.statSync(watchPath);
398
+ const initialResults = stats.isDirectory()
399
+ ? await scanDirectory(watchPath, engine, scanCfg)
400
+ : await scanFile(watchPath, engine, scanCfg);
401
+ if (initialResults.length > 0) {
402
+ console.log(fmt.warning(`\n[Initial scan] Found secrets:`));
403
+ outputScanResults(initialResults, { ...opts, quiet: false }, undefined, /* exitOnFindings= */ false);
404
+ logWatchFindings(logger, initialResults);
405
+ }
406
+ else if (!opts.quiet) {
407
+ console.log(fmt.success(`[Initial scan] No secrets detected`));
408
+ }
409
+ const watcher = watch(watchPath, {
410
+ ignoreInitial: true,
411
+ persistent: true,
412
+ ignored: /(^|[/\\])\../,
413
+ });
414
+ watcher.on("change", async (filePath) => {
415
+ const timestamp = new Date().toLocaleTimeString();
416
+ if (!opts.quiet) {
417
+ console.error(`\n[${timestamp}] Changed: ${filePath}`);
418
+ }
419
+ if (!fs.existsSync(filePath))
420
+ return;
421
+ const fileStats = fs.statSync(filePath);
422
+ if (!fileStats.isFile())
423
+ return;
424
+ const results = await scanFile(filePath, engine, scanCfg);
425
+ if (results.length > 0) {
426
+ outputScanResults(results, { ...opts, quiet: false }, undefined, /* exitOnFindings= */ false);
427
+ logWatchFindings(logger, results);
428
+ }
429
+ else if (!opts.quiet) {
430
+ console.log(fmt.success(` No secrets detected`));
431
+ }
432
+ });
433
+ watcher.on("add", async (filePath) => {
434
+ const timestamp = new Date().toLocaleTimeString();
435
+ if (!opts.quiet) {
436
+ console.error(`\n[${timestamp}] Added: ${filePath}`);
437
+ }
438
+ const fileStats = fs.statSync(filePath);
439
+ if (!fileStats.isFile())
440
+ return;
441
+ const results = await scanFile(filePath, engine, scanCfg);
442
+ if (results.length > 0) {
443
+ outputScanResults(results, { ...opts, quiet: false }, undefined, /* exitOnFindings= */ false);
444
+ logWatchFindings(logger, results);
445
+ }
446
+ else if (!opts.quiet) {
447
+ console.log(fmt.success(` No secrets detected`));
448
+ }
449
+ });
450
+ // Keep process alive until Ctrl+C
451
+ await new Promise((resolve) => {
452
+ process.on("SIGINT", () => {
453
+ if (!opts.quiet) {
454
+ console.log(fmt.info("\nWatch mode stopped."));
455
+ }
456
+ watcher.close();
457
+ resolve();
458
+ });
459
+ });
460
+ }
461
+ /**
462
+ * Log watch findings to audit log
463
+ */
464
+ function logWatchFindings(logger, results) {
465
+ for (const result of results) {
466
+ for (const match of result.matches) {
467
+ logger.log({
468
+ eventType: "secret_detected",
469
+ securityCheck: {
470
+ passed: false,
471
+ reason: `${match.pattern.name} detected in ${result.file}`,
472
+ details: {
473
+ file: result.file,
474
+ line: match.line,
475
+ pattern: match.pattern.name,
476
+ severity: match.pattern.severity,
477
+ watchMode: true,
478
+ },
479
+ },
480
+ resolution: {
481
+ actionTaken: "allowed",
482
+ },
483
+ });
484
+ }
485
+ }
486
+ }
@@ -32,7 +32,7 @@ export function createStatusCommand() {
32
32
  }
33
33
  // --- Gitleaks ---
34
34
  const localGitleaks = path.join(getBinDir(), "gitleaks");
35
- let gitleaksStatus = "not found — run: rafter agent init";
35
+ let gitleaksStatus = "not found — run: rafter agent init --with-gitleaks";
36
36
  try {
37
37
  const ver = execSync("gitleaks version", { timeout: 5000, encoding: "utf-8" }).trim();
38
38
  gitleaksStatus = `${ver} (PATH)`;
@@ -74,8 +74,8 @@ export function createStatusCommand() {
74
74
  // unreadable settings
75
75
  }
76
76
  }
77
- console.log(`PreToolUse: ${pretoolOk ? "installed" : "not installed — run: rafter agent init"}`);
78
- console.log(`PostToolUse: ${posttoolOk ? "installed" : "not installed — run: rafter agent init"}`);
77
+ console.log(`PreToolUse: ${pretoolOk ? "installed" : "not installed — run: rafter agent init --with-claude-code"}`);
78
+ console.log(`PostToolUse: ${posttoolOk ? "installed" : "not installed — run: rafter agent init --with-claude-code"}`);
79
79
  // --- OpenClaw skill ---
80
80
  const skillPath = path.join(home, ".openclaw", "skills", "rafter-security.md");
81
81
  const openclawDir = path.join(home, ".openclaw");
@@ -83,11 +83,72 @@ export function createStatusCommand() {
83
83
  console.log(`OpenClaw: skill installed (${skillPath})`);
84
84
  }
85
85
  else if (fs.existsSync(openclawDir)) {
86
- console.log("OpenClaw: detected but skill missing — run: rafter agent init");
86
+ console.log("OpenClaw: detected but skill missing — run: rafter agent init --with-openclaw");
87
87
  }
88
88
  else {
89
89
  console.log("OpenClaw: not detected (optional)");
90
90
  }
91
+ // --- Codex CLI skills ---
92
+ const codexDir = path.join(home, ".codex");
93
+ const codexSkillPath = path.join(home, ".agents", "skills", "rafter", "SKILL.md");
94
+ if (fs.existsSync(codexSkillPath)) {
95
+ console.log(`Codex CLI: skills installed (${path.join(home, ".agents", "skills")})`);
96
+ }
97
+ else if (fs.existsSync(codexDir)) {
98
+ console.log("Codex CLI: detected but skills missing — run: rafter agent init --with-codex");
99
+ }
100
+ else {
101
+ console.log("Codex CLI: not detected (optional)");
102
+ }
103
+ // --- MCP-native AI engine integrations ---
104
+ const mcpAgents = [
105
+ { name: "Gemini CLI", flag: "--with-gemini", configDir: path.join(home, ".gemini"), configFile: path.join(home, ".gemini", "settings.json"), needle: "rafter" },
106
+ { name: "Cursor", flag: "--with-cursor", configDir: path.join(home, ".cursor"), configFile: path.join(home, ".cursor", "mcp.json"), needle: "rafter" },
107
+ { name: "Windsurf", flag: "--with-windsurf", configDir: path.join(home, ".codeium", "windsurf"), configFile: path.join(home, ".codeium", "windsurf", "mcp_config.json"), needle: "rafter" },
108
+ { name: "Continue.dev", flag: "--with-continue", configDir: path.join(home, ".continue"), configFile: path.join(home, ".continue", "config.json"), needle: "rafter" },
109
+ ];
110
+ for (const agent of mcpAgents) {
111
+ const label = `${agent.name}:`.padEnd(14);
112
+ if (fs.existsSync(agent.configFile)) {
113
+ try {
114
+ const content = fs.readFileSync(agent.configFile, "utf-8");
115
+ if (content.includes(agent.needle)) {
116
+ console.log(`${label}MCP installed (${agent.configFile})`);
117
+ }
118
+ else {
119
+ console.log(`${label}detected but MCP missing — run: rafter agent init ${agent.flag}`);
120
+ }
121
+ }
122
+ catch {
123
+ console.log(`${label}config unreadable (${agent.configFile})`);
124
+ }
125
+ }
126
+ else if (fs.existsSync(agent.configDir)) {
127
+ console.log(`${label}detected but MCP missing — run: rafter agent init ${agent.flag}`);
128
+ }
129
+ else {
130
+ console.log(`${label}not detected (optional)`);
131
+ }
132
+ }
133
+ // --- Aider ---
134
+ const aiderConfig = path.join(home, ".aider.conf.yml");
135
+ if (fs.existsSync(aiderConfig)) {
136
+ try {
137
+ const content = fs.readFileSync(aiderConfig, "utf-8");
138
+ if (content.includes("rafter mcp serve")) {
139
+ console.log(`Aider: MCP installed (${aiderConfig})`);
140
+ }
141
+ else {
142
+ console.log("Aider: detected but MCP missing — run: rafter agent init --with-aider");
143
+ }
144
+ }
145
+ catch {
146
+ console.log(`Aider: config unreadable (${aiderConfig})`);
147
+ }
148
+ }
149
+ else {
150
+ console.log("Aider: not detected (optional)");
151
+ }
91
152
  // --- Audit log summary ---
92
153
  console.log(`\nAudit log: ${auditPath}`);
93
154
  if (fs.existsSync(auditPath)) {
@@ -43,7 +43,7 @@ function checkClaudeCode() {
43
43
  // optional: warn if absent but don't fail exit code
44
44
  const claudeDir = path.join(homeDir, ".claude");
45
45
  if (!fs.existsSync(claudeDir)) {
46
- return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --claude-code' to enable` };
46
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-claude-code' to enable` };
47
47
  }
48
48
  const settingsPath = path.join(claudeDir, "settings.json");
49
49
  if (!fs.existsSync(settingsPath)) {
@@ -54,7 +54,7 @@ function checkClaudeCode() {
54
54
  const hooks = settings?.hooks?.PreToolUse || [];
55
55
  const hasRafterHook = hooks.some((entry) => (entry.hooks || []).some((h) => h.command === "rafter hook pretool"));
56
56
  if (!hasRafterHook) {
57
- return { name, passed: false, optional: true, detail: "Rafter hooks not installed — run 'rafter agent init --claude-code'" };
57
+ return { name, passed: false, optional: true, detail: "Rafter hooks not installed — run 'rafter agent init --with-claude-code'" };
58
58
  }
59
59
  return { name, passed: true, detail: "Hooks installed" };
60
60
  }
@@ -66,14 +66,27 @@ function checkOpenClaw() {
66
66
  const name = "OpenClaw";
67
67
  const skillManager = new SkillManager();
68
68
  if (!skillManager.isOpenClawInstalled()) {
69
- return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init' to enable` };
69
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-openclaw' to enable` };
70
70
  }
71
71
  if (!skillManager.isRafterSkillInstalled()) {
72
- return { name, passed: false, optional: true, detail: `Rafter skill not installed — run 'rafter agent init'` };
72
+ return { name, passed: false, optional: true, detail: `Rafter skill not installed — run 'rafter agent init --with-openclaw'` };
73
73
  }
74
74
  const version = skillManager.getInstalledVersion();
75
75
  return { name, passed: true, detail: `Rafter skill installed${version ? ` (v${version})` : ""}` };
76
76
  }
77
+ function checkCodex() {
78
+ const name = "Codex CLI";
79
+ const homeDir = os.homedir();
80
+ const codexDir = path.join(homeDir, ".codex");
81
+ if (!fs.existsSync(codexDir)) {
82
+ return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --with-codex' to enable` };
83
+ }
84
+ const skillPath = path.join(homeDir, ".agents", "skills", "rafter", "SKILL.md");
85
+ if (!fs.existsSync(skillPath)) {
86
+ return { name, passed: false, optional: true, detail: `Rafter skills not installed — run 'rafter agent init --with-codex'` };
87
+ }
88
+ return { name, passed: true, detail: `Skills installed (${path.join(homeDir, ".agents", "skills")})` };
89
+ }
77
90
  export function createVerifyCommand() {
78
91
  return new Command("verify")
79
92
  .description("Check agent security integration status")
@@ -86,6 +99,7 @@ export function createVerifyCommand() {
86
99
  await checkGitleaks(),
87
100
  checkClaudeCode(),
88
101
  checkOpenClaw(),
102
+ checkCodex(),
89
103
  ];
90
104
  for (const r of results) {
91
105
  if (r.passed) {
@@ -2,25 +2,49 @@ import { Command } from "commander";
2
2
  import axios from "axios";
3
3
  import ora from "ora";
4
4
  import { detectRepo } from "../../utils/git.js";
5
- import { API, resolveKey, EXIT_GENERAL_ERROR, EXIT_QUOTA_EXHAUSTED } from "../../utils/api.js";
5
+ import { API, resolveKey, EXIT_GENERAL_ERROR, EXIT_QUOTA_EXHAUSTED, EXIT_INSUFFICIENT_SCOPE, handleScopeError } from "../../utils/api.js";
6
6
  import { handleScanStatus } from "./scan-status.js";
7
- export function createRunCommand() {
8
- return new Command("run")
9
- .alias("scan")
10
- .option("-r, --repo <repo>", "org/repo (default: current)")
11
- .option("-b, --branch <branch>", "branch (default: current else main)")
12
- .option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
13
- .option("-f, --format <format>", "json | md", "md")
14
- .option("--skip-interactive", "do not wait for scan to complete")
15
- .option("--quiet", "suppress status messages")
16
- .action(async (opts) => {
17
- const key = resolveKey(opts.apiKey);
18
- let repo, branch;
7
+ /**
8
+ * Shared handler for the remote backend scan (used by both `rafter run` and `rafter scan` / `rafter scan remote`).
9
+ */
10
+ export async function runRemoteScan(opts) {
11
+ const key = resolveKey(opts.apiKey);
12
+ let repo, branch;
13
+ try {
14
+ ({ repo, branch } = detectRepo({ repo: opts.repo, branch: opts.branch, quiet: opts.quiet }));
15
+ }
16
+ catch (e) {
17
+ if (e instanceof Error) {
18
+ console.error(e.message);
19
+ }
20
+ else {
21
+ console.error(e);
22
+ }
23
+ process.exit(EXIT_GENERAL_ERROR);
24
+ }
25
+ if (!opts.quiet) {
26
+ const spinner = ora("Submitting scan").start();
19
27
  try {
20
- ({ repo, branch } = detectRepo({ repo: opts.repo, branch: opts.branch, quiet: opts.quiet }));
28
+ const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch }, { headers: { "x-api-key": key } });
29
+ spinner.succeed(`Scan ID: ${data.scan_id}`);
30
+ if (opts.skipInteractive)
31
+ return;
32
+ const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format ?? "md", opts.quiet);
33
+ process.exit(exitCode);
21
34
  }
22
35
  catch (e) {
23
- if (e instanceof Error) {
36
+ spinner.fail("Request failed");
37
+ if (handleScopeError(e)) {
38
+ process.exit(EXIT_INSUFFICIENT_SCOPE);
39
+ }
40
+ else if (e.response?.status === 429) {
41
+ console.error("Quota exhausted");
42
+ process.exit(EXIT_QUOTA_EXHAUSTED);
43
+ }
44
+ else if (e.response?.data) {
45
+ console.error(e.response.data);
46
+ }
47
+ else if (e instanceof Error) {
24
48
  console.error(e.message);
25
49
  }
26
50
  else {
@@ -28,57 +52,47 @@ export function createRunCommand() {
28
52
  }
29
53
  process.exit(EXIT_GENERAL_ERROR);
30
54
  }
31
- if (!opts.quiet) {
32
- const spinner = ora("Submitting scan").start();
33
- try {
34
- const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch }, { headers: { "x-api-key": key } });
35
- spinner.succeed(`Scan ID: ${data.scan_id}`);
36
- if (opts.skipInteractive)
37
- return;
38
- const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format, opts.quiet);
39
- process.exit(exitCode);
55
+ }
56
+ else {
57
+ try {
58
+ const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch }, { headers: { "x-api-key": key } });
59
+ if (opts.skipInteractive)
60
+ return;
61
+ const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format ?? "md", opts.quiet);
62
+ process.exit(exitCode);
63
+ }
64
+ catch (e) {
65
+ if (handleScopeError(e)) {
66
+ process.exit(EXIT_INSUFFICIENT_SCOPE);
40
67
  }
41
- catch (e) {
42
- spinner.fail("Request failed");
43
- if (e.response?.status === 429) {
44
- console.error("Quota exhausted");
45
- process.exit(EXIT_QUOTA_EXHAUSTED);
46
- }
47
- else if (e.response?.data) {
48
- console.error(e.response.data);
49
- }
50
- else if (e instanceof Error) {
51
- console.error(e.message);
52
- }
53
- else {
54
- console.error(e);
55
- }
56
- process.exit(EXIT_GENERAL_ERROR);
68
+ else if (e.response?.status === 429) {
69
+ process.exit(EXIT_QUOTA_EXHAUSTED);
57
70
  }
58
- }
59
- else {
60
- try {
61
- const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch }, { headers: { "x-api-key": key } });
62
- if (opts.skipInteractive)
63
- return;
64
- const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format, opts.quiet);
65
- process.exit(exitCode);
71
+ else if (e.response?.data) {
72
+ console.error(e.response.data);
73
+ }
74
+ else if (e instanceof Error) {
75
+ console.error(e.message);
66
76
  }
67
- catch (e) {
68
- if (e.response?.status === 429) {
69
- process.exit(EXIT_QUOTA_EXHAUSTED);
70
- }
71
- else if (e.response?.data) {
72
- console.error(e.response.data);
73
- }
74
- else if (e instanceof Error) {
75
- console.error(e.message);
76
- }
77
- else {
78
- console.error(e);
79
- }
80
- process.exit(EXIT_GENERAL_ERROR);
77
+ else {
78
+ console.error(e);
81
79
  }
80
+ process.exit(EXIT_GENERAL_ERROR);
82
81
  }
82
+ }
83
+ }
84
+ function addRunOptions(cmd) {
85
+ return cmd
86
+ .option("-r, --repo <repo>", "org/repo (default: current)")
87
+ .option("-b, --branch <branch>", "branch (default: current else main)")
88
+ .option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
89
+ .option("-f, --format <format>", "json | md", "md")
90
+ .option("--skip-interactive", "do not wait for scan to complete")
91
+ .option("--quiet", "suppress status messages");
92
+ }
93
+ export function createRunCommand() {
94
+ return addRunOptions(new Command("run")
95
+ .description("Trigger a remote backend security scan")).action(async (opts) => {
96
+ await runRemoteScan(opts);
83
97
  });
84
98
  }
@@ -44,6 +44,12 @@ export function createCiInitCommand() {
44
44
  }
45
45
  }
46
46
  console.log(` ${opts.withBackend ? "3" : "2"}. Commit and push to trigger the pipeline`);
47
+ if (platform === "github") {
48
+ console.log();
49
+ console.log("Alternatives:");
50
+ console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v0");
51
+ console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
52
+ }
47
53
  console.log();
48
54
  });
49
55
  }
@@ -68,6 +74,7 @@ function generateTemplate(platform, withBackend) {
68
74
  }
69
75
  function githubTemplate(withBackend) {
70
76
  let yaml = `# Generated by: rafter ci init
77
+ # Alternative: uses: Raftersecurity/rafter-cli@v0
71
78
  name: Rafter Security
72
79
 
73
80
  on:
@@ -89,7 +96,7 @@ jobs:
89
96
  run: npm install -g @rafter-security/cli
90
97
 
91
98
  - name: Scan for secrets
92
- run: rafter agent scan . --quiet
99
+ run: rafter scan local . --quiet
93
100
  `;
94
101
  if (withBackend) {
95
102
  yaml += `
@@ -120,7 +127,7 @@ secret-scan:
120
127
  image: node:20
121
128
  script:
122
129
  - npm install -g @rafter-security/cli
123
- - rafter agent scan . --quiet
130
+ - rafter scan local . --quiet
124
131
  rules:
125
132
  - if: $CI_PIPELINE_SOURCE == "push"
126
133
  - if: $CI_PIPELINE_SOURCE == "merge_request_event"
@@ -158,7 +165,7 @@ jobs:
158
165
  command: npm install -g @rafter-security/cli
159
166
  - run:
160
167
  name: Scan for secrets
161
- command: rafter agent scan . --quiet
168
+ command: rafter scan local . --quiet
162
169
  `;
163
170
  if (withBackend) {
164
171
  yaml += `