@rafter-security/cli 0.5.5 → 0.5.9

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 (35) 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 +96 -0
  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 +157 -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 +69 -61
  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/policy/export.js +7 -2
  22. package/dist/commands/scan/index.js +44 -0
  23. package/dist/core/config-defaults.js +24 -0
  24. package/dist/core/config-manager.js +19 -2
  25. package/dist/core/pattern-engine.js +26 -1
  26. package/dist/index.js +8 -2
  27. package/dist/scanners/gitleaks.js +5 -5
  28. package/dist/scanners/regex-scanner.js +12 -1
  29. package/dist/scanners/secret-patterns.js +3 -3
  30. package/dist/utils/binary-manager.js +7 -6
  31. package/dist/utils/skill-manager.js +5 -3
  32. package/package.json +2 -1
  33. package/resources/pre-commit-hook.sh +2 -2
  34. package/resources/pre-push-hook.sh +2 -2
  35. package/resources/rafter-security-skill.md +7 -11
@@ -2,6 +2,7 @@ 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";
@@ -42,7 +43,28 @@ export function createScanCommand() {
42
43
  .option("--diff <ref>", "Scan files changed since a git ref")
43
44
  .option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
44
45
  .option("--baseline", "Filter findings present in the saved baseline")
46
+ .option("--watch", "Watch for file changes and re-scan on change")
45
47
  .action(async (scanPath, opts) => {
48
+ // Validate flags before doing any work
49
+ const validEngines = ["auto", "gitleaks", "patterns"];
50
+ const engineValue = opts.engine || "auto";
51
+ if (!validEngines.includes(engineValue)) {
52
+ console.error(`Invalid engine: ${engineValue}. Valid values: ${validEngines.join(", ")}`);
53
+ process.exit(2);
54
+ }
55
+ const format = opts.format ?? (opts.json ? "json" : "text");
56
+ const validFormats = ["text", "json", "sarif"];
57
+ if (!validFormats.includes(format)) {
58
+ console.error(`Invalid format: ${format}. Valid values: ${validFormats.join(", ")}`);
59
+ process.exit(2);
60
+ }
61
+ // Deprecation notice — only when invoked as `rafter agent scan`, not as `rafter scan local`
62
+ const argv = process.argv;
63
+ const isAgentScan = argv.includes("agent") && argv.includes("scan") &&
64
+ argv.indexOf("agent") < argv.indexOf("scan");
65
+ if (isAgentScan) {
66
+ process.stderr.write("Warning: rafter agent scan is deprecated and will be removed in a future major version. Use rafter scan local instead.\n");
67
+ }
46
68
  // Load policy-merged config for excludePaths/customPatterns
47
69
  const manager = new ConfigManager();
48
70
  const cfg = manager.loadWithPolicy();
@@ -64,6 +86,11 @@ export function createScanCommand() {
64
86
  console.error(`Error: Path not found: ${resolvedPath}`);
65
87
  process.exit(2);
66
88
  }
89
+ // Handle --watch flag
90
+ if (opts.watch) {
91
+ await watchAndScan(resolvedPath, opts, scanCfg);
92
+ return;
93
+ }
67
94
  // Determine scan engine
68
95
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
69
96
  // Determine if path is file or directory
@@ -123,7 +150,7 @@ function outputSarif(results) {
123
150
  tool: {
124
151
  driver: {
125
152
  name: "rafter",
126
- version: "0.5.5",
153
+ version: "0.5.7",
127
154
  informationUri: "https://rafter.so",
128
155
  rules: Array.from(rules.values()),
129
156
  },
@@ -138,7 +165,7 @@ function outputSarif(results) {
138
165
  /**
139
166
  * Shared output logic for scan results
140
167
  */
141
- function outputScanResults(results, opts, context) {
168
+ function outputScanResults(results, opts, context, exitOnFindings = true) {
142
169
  const format = opts.format ?? (opts.json ? "json" : "text");
143
170
  if (!["text", "json", "sarif"].includes(format)) {
144
171
  console.error(`Invalid format: ${format}. Valid values: text, json, sarif`);
@@ -159,14 +186,18 @@ function outputScanResults(results, opts, context) {
159
186
  })),
160
187
  }));
161
188
  console.log(JSON.stringify(out, null, 2));
162
- process.exit(results.length > 0 ? 1 : 0);
189
+ if (exitOnFindings)
190
+ process.exit(results.length > 0 ? 1 : 0);
191
+ return;
163
192
  }
164
193
  if (results.length === 0) {
165
194
  if (!opts.quiet) {
166
195
  const msg = context ? `No secrets detected in ${context}` : "No secrets detected";
167
196
  console.log(`\n${fmt.success(msg)}\n`);
168
197
  }
169
- process.exit(0);
198
+ if (exitOnFindings)
199
+ process.exit(0);
200
+ return;
170
201
  }
171
202
  console.log(`\n${fmt.warning(`Found secrets in ${results.length} file(s):`)}\n`);
172
203
  let totalMatches = 0;
@@ -187,10 +218,11 @@ function outputScanResults(results, opts, context) {
187
218
  if (context === "staged files") {
188
219
  console.log(`${fmt.error("Commit blocked. Remove secrets before committing.")}\n`);
189
220
  }
190
- else {
221
+ else if (exitOnFindings) {
191
222
  console.log(`Run 'rafter agent audit' to see the security log.\n`);
192
223
  }
193
- process.exit(1);
224
+ if (exitOnFindings)
225
+ process.exit(1);
194
226
  }
195
227
  /**
196
228
  * Scan files changed since a git ref
@@ -202,19 +234,21 @@ async function scanDiffFiles(ref, opts, scanCfg, baselineEntries = []) {
202
234
  stdio: ["pipe", "pipe", "ignore"],
203
235
  }).trim();
204
236
  if (!diffOutput) {
205
- if (!opts.quiet) {
206
- console.log(fmt.success(`No files changed since ${ref}`));
207
- }
208
- process.exit(0);
237
+ outputScanResults([], opts, `files changed since ${ref}`);
238
+ return;
209
239
  }
210
240
  const changedFiles = diffOutput.split("\n").map(f => f.trim()).filter(f => f);
211
241
  if (!opts.quiet) {
212
242
  console.error(`Scanning ${changedFiles.length} file(s) changed since ${ref}...`);
213
243
  }
244
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
245
+ encoding: "utf-8",
246
+ stdio: ["pipe", "pipe", "ignore"],
247
+ }).trim();
214
248
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
215
249
  const allResults = [];
216
250
  for (const file of changedFiles) {
217
- const filePath = path.resolve(file);
251
+ const filePath = path.resolve(repoRoot, file);
218
252
  if (!fs.existsSync(filePath))
219
253
  continue;
220
254
  const stats = fs.statSync(filePath);
@@ -243,19 +277,21 @@ async function scanStagedFiles(opts, scanCfg, baselineEntries = []) {
243
277
  stdio: ["pipe", "pipe", "ignore"]
244
278
  }).trim();
245
279
  if (!stagedFilesOutput) {
246
- if (!opts.quiet) {
247
- console.log(fmt.success("No files staged for commit"));
248
- }
249
- process.exit(0);
280
+ outputScanResults([], opts, "staged files");
281
+ return;
250
282
  }
251
283
  const stagedFiles = stagedFilesOutput.split("\n").map(f => f.trim()).filter(f => f);
252
284
  if (!opts.quiet) {
253
285
  console.error(`Scanning ${stagedFiles.length} staged file(s)...`);
254
286
  }
287
+ const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
288
+ encoding: "utf-8",
289
+ stdio: ["pipe", "pipe", "ignore"],
290
+ }).trim();
255
291
  const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
256
292
  const allResults = [];
257
293
  for (const file of stagedFiles) {
258
- const filePath = path.resolve(file);
294
+ const filePath = path.resolve(repoRoot, file);
259
295
  if (!fs.existsSync(filePath))
260
296
  continue;
261
297
  const stats = fs.statSync(filePath);
@@ -292,6 +328,10 @@ async function selectEngine(preference, quiet) {
292
328
  }
293
329
  return "gitleaks";
294
330
  }
331
+ if (preference !== "auto") {
332
+ console.error(`Invalid engine: ${preference}. Valid values: auto, gitleaks, patterns`);
333
+ process.exit(2);
334
+ }
295
335
  // Auto mode: try Gitleaks, fall back to patterns
296
336
  const gitleaks = new GitleaksScanner();
297
337
  const available = await gitleaks.isAvailable();
@@ -340,3 +380,104 @@ async function scanDirectory(dirPath, engine, scanCfg) {
340
380
  return scanner.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
341
381
  }
342
382
  }
383
+ /**
384
+ * Watch a path for changes and re-scan on each change
385
+ */
386
+ async function watchAndScan(watchPath, opts, scanCfg) {
387
+ const { watch } = await import("chokidar");
388
+ const logger = new AuditLogger();
389
+ const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
390
+ if (!opts.quiet) {
391
+ console.error(fmt.info(`Watching ${watchPath} for changes (${engine}). Press Ctrl+C to exit.`));
392
+ }
393
+ // Do an initial scan
394
+ const stats = fs.statSync(watchPath);
395
+ const initialResults = stats.isDirectory()
396
+ ? await scanDirectory(watchPath, engine, scanCfg)
397
+ : await scanFile(watchPath, engine, scanCfg);
398
+ if (initialResults.length > 0) {
399
+ console.log(fmt.warning(`\n[Initial scan] Found secrets:`));
400
+ outputScanResults(initialResults, { ...opts, quiet: false }, undefined, /* exitOnFindings= */ false);
401
+ logWatchFindings(logger, initialResults);
402
+ }
403
+ else if (!opts.quiet) {
404
+ console.log(fmt.success(`[Initial scan] No secrets detected`));
405
+ }
406
+ const watcher = watch(watchPath, {
407
+ ignoreInitial: true,
408
+ persistent: true,
409
+ ignored: /(^|[/\\])\../,
410
+ });
411
+ watcher.on("change", async (filePath) => {
412
+ const timestamp = new Date().toLocaleTimeString();
413
+ if (!opts.quiet) {
414
+ console.error(`\n[${timestamp}] Changed: ${filePath}`);
415
+ }
416
+ if (!fs.existsSync(filePath))
417
+ return;
418
+ const fileStats = fs.statSync(filePath);
419
+ if (!fileStats.isFile())
420
+ return;
421
+ const results = await scanFile(filePath, engine, scanCfg);
422
+ if (results.length > 0) {
423
+ outputScanResults(results, { ...opts, quiet: false }, undefined, /* exitOnFindings= */ false);
424
+ logWatchFindings(logger, results);
425
+ }
426
+ else if (!opts.quiet) {
427
+ console.log(fmt.success(` No secrets detected`));
428
+ }
429
+ });
430
+ watcher.on("add", async (filePath) => {
431
+ const timestamp = new Date().toLocaleTimeString();
432
+ if (!opts.quiet) {
433
+ console.error(`\n[${timestamp}] Added: ${filePath}`);
434
+ }
435
+ const fileStats = fs.statSync(filePath);
436
+ if (!fileStats.isFile())
437
+ return;
438
+ const results = await scanFile(filePath, engine, scanCfg);
439
+ if (results.length > 0) {
440
+ outputScanResults(results, { ...opts, quiet: false }, undefined, /* exitOnFindings= */ false);
441
+ logWatchFindings(logger, results);
442
+ }
443
+ else if (!opts.quiet) {
444
+ console.log(fmt.success(` No secrets detected`));
445
+ }
446
+ });
447
+ // Keep process alive until Ctrl+C
448
+ await new Promise((resolve) => {
449
+ process.on("SIGINT", () => {
450
+ if (!opts.quiet) {
451
+ console.log(fmt.info("\nWatch mode stopped."));
452
+ }
453
+ watcher.close();
454
+ resolve();
455
+ });
456
+ });
457
+ }
458
+ /**
459
+ * Log watch findings to audit log
460
+ */
461
+ function logWatchFindings(logger, results) {
462
+ for (const result of results) {
463
+ for (const match of result.matches) {
464
+ logger.log({
465
+ eventType: "secret_detected",
466
+ securityCheck: {
467
+ passed: false,
468
+ reason: `${match.pattern.name} detected in ${result.file}`,
469
+ details: {
470
+ file: result.file,
471
+ line: match.line,
472
+ pattern: match.pattern.name,
473
+ severity: match.pattern.severity,
474
+ watchMode: true,
475
+ },
476
+ },
477
+ resolution: {
478
+ actionTaken: "allowed",
479
+ },
480
+ });
481
+ }
482
+ }
483
+ }
@@ -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) {
@@ -4,23 +4,44 @@ import ora from "ora";
4
4
  import { detectRepo } from "../../utils/git.js";
5
5
  import { API, resolveKey, EXIT_GENERAL_ERROR, EXIT_QUOTA_EXHAUSTED } 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 (e.response?.status === 429) {
38
+ console.error("Quota exhausted");
39
+ process.exit(EXIT_QUOTA_EXHAUSTED);
40
+ }
41
+ else if (e.response?.data) {
42
+ console.error(e.response.data);
43
+ }
44
+ else if (e instanceof Error) {
24
45
  console.error(e.message);
25
46
  }
26
47
  else {
@@ -28,57 +49,44 @@ export function createRunCommand() {
28
49
  }
29
50
  process.exit(EXIT_GENERAL_ERROR);
30
51
  }
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);
52
+ }
53
+ else {
54
+ try {
55
+ const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch }, { headers: { "x-api-key": key } });
56
+ if (opts.skipInteractive)
57
+ return;
58
+ const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format ?? "md", opts.quiet);
59
+ process.exit(exitCode);
60
+ }
61
+ catch (e) {
62
+ if (e.response?.status === 429) {
63
+ process.exit(EXIT_QUOTA_EXHAUSTED);
40
64
  }
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);
65
+ else if (e.response?.data) {
66
+ console.error(e.response.data);
57
67
  }
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);
68
+ else if (e instanceof Error) {
69
+ console.error(e.message);
66
70
  }
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);
71
+ else {
72
+ console.error(e);
81
73
  }
74
+ process.exit(EXIT_GENERAL_ERROR);
82
75
  }
76
+ }
77
+ }
78
+ function addRunOptions(cmd) {
79
+ return cmd
80
+ .option("-r, --repo <repo>", "org/repo (default: current)")
81
+ .option("-b, --branch <branch>", "branch (default: current else main)")
82
+ .option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
83
+ .option("-f, --format <format>", "json | md", "md")
84
+ .option("--skip-interactive", "do not wait for scan to complete")
85
+ .option("--quiet", "suppress status messages");
86
+ }
87
+ export function createRunCommand() {
88
+ return addRunOptions(new Command("run")
89
+ .description("Trigger a remote backend security scan")).action(async (opts) => {
90
+ await runRemoteScan(opts);
83
91
  });
84
92
  }
@@ -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 += `