@rafter-security/cli 0.6.5 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # @rafter-security/cli
2
2
 
3
- Node.js CLI for [Rafter](https://rafter.so) — zero-setup security for AI builders. This is the **full-featured package** with both backend scanning and agent security.
3
+ Node.js CLI for [Rafter](https://rafter.so) — the security toolkit for developers. This is the **full-featured package** with both local security and remote code analysis.
4
4
 
5
- **Backend scanning** — Remote SAST/SCA via Rafter API. Trigger scans, retrieve structured vulnerability reports, pipe to any tool.
5
+ **Local security toolkit** — Fast, deterministic secret scanning (21+ patterns, Gitleaks), policy enforcement with risk-tiered rules, pre-commit hooks, extension auditing, custom rule authoring, and full audit logging. Works with Claude Code, Codex CLI, OpenClaw, and 5 more platforms. No API key required. No data leaves your machine.
6
6
 
7
- **Agent security** — Local-first protection for autonomous AI agents. Secret scanning (21+ patterns, Gitleaks), command interception with risk-tiered approval, pre-commit hooks, skill/extension auditing, and full audit logging. Works with Claude Code, Codex CLI, and OpenClaw. No API key required.
7
+ **Remote code analysis** — Deep security audits that combine agentic analysis with a full SAST/SCA toolchain. The engine examines your codebase the way a professional cybersecurity auditor would — tracing data flows, reasoning about business logic, and surfacing vulnerabilities that static rules alone miss — then cross-references findings with industry-standard static analysis and dependency scanning. Structured JSON reports with documented exit codes. Your code is deleted immediately after analysis completes.
8
8
 
9
9
  ## Installation
10
10
 
@@ -23,7 +23,7 @@ yarn global add @rafter-security/cli
23
23
 
24
24
  ### Getting an API Key
25
25
 
26
- To use backend scanning features, you'll need a Rafter API key:
26
+ To use remote code analysis features, you'll need a Rafter API key:
27
27
 
28
28
  1. **Sign up**: Create an account at [rafter.so](https://rafter.so)
29
29
  2. **Get API key**: Navigate to Dashboard → Settings → API Keys
@@ -36,9 +36,9 @@ To use backend scanning features, you'll need a Rafter API key:
36
36
  echo "RAFTER_API_KEY=your-api-key-here" >> .env
37
37
  ```
38
38
 
39
- **Note**: Agent security features (secret scanning, command execution) work **without an API key**. Only backend scanning requires authentication.
39
+ **Note**: Agent security features (secret scanning, command execution) work **without an API key**. Only remote code analysis requires authentication.
40
40
 
41
- ### Backend Scanning
41
+ ### Remote Code Analysis
42
42
 
43
43
  ```bash
44
44
  # Set your API key (from above)
@@ -54,10 +54,10 @@ rafter get <scan-id>
54
54
  rafter usage
55
55
  ```
56
56
 
57
- ### Agent Security
57
+ ### Local Security
58
58
 
59
59
  ```bash
60
- # Initialize agent security
60
+ # Initialize local security
61
61
  rafter agent init
62
62
 
63
63
  # Scan files for secrets
@@ -77,7 +77,7 @@ rafter agent config show
77
77
 
78
78
  | Flag | Description |
79
79
  |------|-------------|
80
- | `-a, --agent` | Plain output for AI agents (no colors, no emoji) |
80
+ | `-a, --agent` | Plain output (no colors, no emoji) |
81
81
  | `-V, --version` | Print version |
82
82
  | `-h, --help` | Show help |
83
83
 
@@ -92,7 +92,7 @@ Trigger a new security scan for your repository.
92
92
  - `-r, --repo <repo>` - Repository in format `org/repo` (default: auto-detected)
93
93
  - `-b, --branch <branch>` - Branch name (default: auto-detected)
94
94
  - `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
95
- - `-f, --format <format>` - Output format: `json` or `md` (default: `json`)
95
+ - `-f, --format <format>` - Output format: `json` or `md` (default: `md`)
96
96
  - `--skip-interactive` - Don't wait for scan completion
97
97
  - `--quiet` - Suppress status messages
98
98
 
@@ -116,7 +116,7 @@ Retrieve results from a completed scan.
116
116
 
117
117
  **Options:**
118
118
  - `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
119
- - `-f, --format <format>` - Output format: `json` or `md` (default: `json`)
119
+ - `-f, --format <format>` - Output format: `json` or `md` (default: `md`)
120
120
  - `--interactive` - Poll until scan completes
121
121
  - `--quiet` - Suppress status messages
122
122
 
@@ -143,13 +143,13 @@ rafter usage
143
143
 
144
144
  ---
145
145
 
146
- ## Agent Security Commands
146
+ ## Local Security Commands
147
147
 
148
- Rafter provides local security features for autonomous agents (OpenClaw, Claude Code) to prevent secrets leakage and dangerous operations.
148
+ Rafter is a **security primitive** that any developer or tool can call and trust. Stable exit codes, deterministic findings, and structured output mean any workflow can integrate Rafter without reading prose.
149
149
 
150
150
  ### `rafter agent init [options]`
151
151
 
152
- Initialize agent security system.
152
+ Initialize local security system.
153
153
 
154
154
  **Options:**
155
155
  - `--risk-level <level>` - Set risk level: `minimal`, `moderate`, or `aggressive` (default: `moderate`)
@@ -167,7 +167,7 @@ Initialize agent security system.
167
167
  **What it does:**
168
168
  - Creates `~/.rafter/config.json` configuration
169
169
  - Initializes directory structure
170
- - Detects available agent environments
170
+ - Detects installed platforms
171
171
  - Installs opted-in integrations (skills, hooks, MCP servers)
172
172
  - Sets up audit logging
173
173
 
@@ -500,7 +500,7 @@ Generate CI/CD pipeline configuration for secret scanning.
500
500
  **Options:**
501
501
  - `--platform <platform>` - CI platform: `github`, `gitlab`, `circleci` (default: auto-detect)
502
502
  - `--output <path>` - Output file path (default: platform-specific)
503
- - `--with-backend` - Include backend security audit job (requires `RAFTER_API_KEY`)
503
+ - `--with-remote` - Include remote security audit job (requires `RAFTER_API_KEY`)
504
504
 
505
505
  **Auto-detection:** Checks for `.github/`, `.gitlab-ci.yml`, `.circleci/` in the current directory.
506
506
 
@@ -512,8 +512,8 @@ rafter ci init
512
512
  # Generate GitHub Actions workflow
513
513
  rafter ci init --platform github
514
514
 
515
- # Include backend scanning job
516
- rafter ci init --with-backend
515
+ # Include remote scanning job
516
+ rafter ci init --with-remote
517
517
 
518
518
  # Custom output path
519
519
  rafter ci init --output .github/workflows/security.yml
@@ -562,9 +562,9 @@ The CLI automatically detects your repository and branch from the current Git re
562
562
 
563
563
  **Note**: The CLI only scans remote repositories, not your current local branch.
564
564
 
565
- ### Agent Security Configuration
565
+ ### Local Security Configuration
566
566
 
567
- Agent security settings are stored in `~/.rafter/config.json`. Key settings:
567
+ Security settings are stored in `~/.rafter/config.json`. Key settings:
568
568
 
569
569
  **Risk Levels:**
570
570
  - `minimal` - Basic guidance only, most commands allowed
@@ -584,7 +584,7 @@ Agent security settings are stored in `~/.rafter/config.json`. Key settings:
584
584
 
585
585
  ## OpenClaw Integration
586
586
 
587
- Rafter integrates seamlessly with [OpenClaw](https://openclaw.com) autonomous agents.
587
+ Rafter integrates seamlessly with [OpenClaw](https://openclaw.com).
588
588
 
589
589
  ### Setup
590
590
 
@@ -624,7 +624,7 @@ When OpenClaw is detected, `rafter agent init` automatically installs a skill to
624
624
 
625
625
  Rafter provides TWO skills for Claude Code:
626
626
 
627
- ### 1. Backend Scanning Skill (Core Feature)
627
+ ### 1. Remote Code Analysis Skill (Core Feature)
628
628
 
629
629
  **Automatic Integration** - Claude can proactively suggest security scans
630
630
 
@@ -650,7 +650,7 @@ Claude will automatically suggest Rafter scans when you mention security, vulner
650
650
  Can you run a Rafter security scan on this repo?
651
651
  ```
652
652
 
653
- ### 2. Agent Security Skill
653
+ ### 2. Local Security Skill
654
654
 
655
655
  **User-Invoked** - Requires explicit commands for safety
656
656
 
@@ -680,10 +680,10 @@ Explicitly invoke commands:
680
680
 
681
681
  ### Why Two Skills?
682
682
 
683
- - **Backend skill** - Safe for Claude to auto-invoke (read-only API calls)
683
+ - **Remote code analysis skill** - Safe for Claude to auto-invoke (read-only API calls)
684
684
  - **Agent security skill** - Requires user permission (local file access, command execution)
685
685
 
686
- This separation emphasizes Rafter's core backend scanning capabilities while keeping local security features safely behind user control.
686
+ This separation emphasizes Rafter's core remote code analysis capabilities while keeping local security features safely behind user control.
687
687
 
688
688
  ## Documentation
689
689
 
@@ -4,6 +4,7 @@ import path from "path";
4
4
  import { PatternEngine } from "../../core/pattern-engine.js";
5
5
  import { DEFAULT_SECRET_PATTERNS } from "../../scanners/secret-patterns.js";
6
6
  import { SkillManager } from "../../utils/skill-manager.js";
7
+ import { fmt } from "../../utils/formatter.js";
7
8
  export function createAuditSkillCommand() {
8
9
  return new Command("audit-skill")
9
10
  .description("Security audit of a Claude Code skill file")
@@ -17,7 +18,7 @@ export function createAuditSkillCommand() {
17
18
  async function auditSkill(skillPath, opts) {
18
19
  // Validate skill file exists
19
20
  if (!fs.existsSync(skillPath)) {
20
- console.error(`Error: Skill file not found: ${skillPath}`);
21
+ console.error(fmt.error(`Skill file not found: ${skillPath}`));
21
22
  process.exit(2);
22
23
  }
23
24
  const absolutePath = path.resolve(skillPath);
@@ -25,8 +26,8 @@ async function auditSkill(skillPath, opts) {
25
26
  const skillName = path.basename(absolutePath);
26
27
  // Run deterministic analysis
27
28
  if (!opts.json) {
28
- console.log(`\n🔍 Auditing skill: ${skillName}\n`);
29
- console.log("═".repeat(60));
29
+ console.log(fmt.header(`Auditing skill: ${skillName}`));
30
+ console.log(fmt.divider());
30
31
  console.log("Running quick security scan...\n");
31
32
  }
32
33
  const quickScan = await runQuickScan(skillContent);
@@ -56,11 +57,11 @@ async function auditSkill(skillPath, opts) {
56
57
  // Check if we can use OpenClaw
57
58
  if (openClawAvailable && !opts.skipOpenclaw) {
58
59
  if (!rafterSkillInstalled) {
59
- console.log("\n⚠️ Rafter Security skill not installed in OpenClaw.");
60
+ console.log(`\n${fmt.warning("Rafter Security skill not installed in OpenClaw.")}`);
60
61
  console.log(" Run: rafter agent init\n");
61
62
  }
62
63
  else {
63
- console.log("\n🤖 For comprehensive security review:\n");
64
+ console.log(`\n${fmt.info("For comprehensive security review:")}\n`);
64
65
  console.log(" 1. Open OpenClaw");
65
66
  console.log(` 2. Run: /rafter-audit-skill ${absolutePath}`);
66
67
  console.log("\n The auditor will analyze:");
@@ -80,12 +81,12 @@ async function auditSkill(skillPath, opts) {
80
81
  }
81
82
  else {
82
83
  // OpenClaw not available or skipped - show manual review prompt
83
- console.log("\n📋 Manual Security Review Prompt\n");
84
- console.log("═".repeat(60));
84
+ console.log(fmt.header("Manual Security Review Prompt"));
85
+ console.log(fmt.divider());
85
86
  console.log("\nCopy the following to your AI assistant for review:\n");
86
- console.log("─".repeat(60));
87
+ console.log(fmt.divider());
87
88
  console.log(generateManualReviewPrompt(skillName, absolutePath, quickScan, skillContent));
88
- console.log("─".repeat(60));
89
+ console.log(fmt.divider());
89
90
  }
90
91
  console.log();
91
92
  if (quickScan.secrets > 0 || quickScan.highRiskCommands.length > 0) {
@@ -134,25 +135,25 @@ async function runQuickScan(content) {
134
135
  };
135
136
  }
136
137
  function displayQuickScan(scan, skillName) {
137
- console.log("📊 Quick Scan Results");
138
- console.log("═".repeat(60));
138
+ console.log(fmt.header("Quick Scan Results"));
139
+ console.log(fmt.divider());
139
140
  // Secrets
140
141
  if (scan.secrets === 0) {
141
- console.log("Secrets: None detected");
142
+ console.log(fmt.success("Secrets: None detected"));
142
143
  }
143
144
  else {
144
- console.log(`⚠️ Secrets: ${scan.secrets} found`);
145
+ console.log(fmt.warning(`Secrets: ${scan.secrets} found`));
145
146
  console.log(" → API keys, tokens, or credentials detected");
146
147
  console.log(" → Run: rafter scan local <path> for details");
147
148
  }
148
149
  // URLs
149
150
  if (scan.urls.length === 0) {
150
- console.log("External URLs: None");
151
+ console.log(fmt.success("External URLs: None"));
151
152
  }
152
153
  else {
153
- console.log(`⚠️ External URLs: ${scan.urls.length} found`);
154
+ console.log(fmt.warning(`External URLs: ${scan.urls.length} found`));
154
155
  scan.urls.slice(0, 5).forEach(url => {
155
- console.log(` ${url}`);
156
+ console.log(` - ${url}`);
156
157
  });
157
158
  if (scan.urls.length > 5) {
158
159
  console.log(` ... and ${scan.urls.length - 5} more`);
@@ -160,12 +161,12 @@ function displayQuickScan(scan, skillName) {
160
161
  }
161
162
  // High-risk commands
162
163
  if (scan.highRiskCommands.length === 0) {
163
- console.log("High-risk commands: None detected");
164
+ console.log(fmt.success("High-risk commands: None detected"));
164
165
  }
165
166
  else {
166
- console.log(`⚠️ High-risk commands: ${scan.highRiskCommands.length} found`);
167
+ console.log(fmt.warning(`High-risk commands: ${scan.highRiskCommands.length} found`));
167
168
  scan.highRiskCommands.slice(0, 5).forEach(cmd => {
168
- console.log(` ${cmd.command} (line ${cmd.line})`);
169
+ console.log(` - ${cmd.command} (line ${cmd.line})`);
169
170
  });
170
171
  if (scan.highRiskCommands.length > 5) {
171
172
  console.log(` ... and ${scan.highRiskCommands.length - 5} more`);
@@ -1,5 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { ConfigManager } from "../../core/config-manager.js";
3
+ import { fmt } from "../../utils/formatter.js";
3
4
  export function createConfigCommand() {
4
5
  const config = new Command("config")
5
6
  .description("Manage agent configuration");
@@ -48,7 +49,7 @@ export function createConfigCommand() {
48
49
  // Use as string
49
50
  }
50
51
  manager.set(key, parsedValue);
51
- console.log(`✓ Set ${key} = ${JSON.stringify(parsedValue)}`);
52
+ console.log(fmt.success(`Set ${key} = ${JSON.stringify(parsedValue)}`));
52
53
  });
53
54
  return config;
54
55
  }
@@ -112,6 +112,8 @@ async function promptApproval() {
112
112
  output: process.stdout
113
113
  });
114
114
  return new Promise((resolve) => {
115
+ // Handle EOF / non-interactive stdin (e.g. piped or closed stdin)
116
+ rl.on("close", () => resolve(false));
115
117
  rl.question("Approve this command? (yes/no): ", (answer) => {
116
118
  rl.close();
117
119
  const normalized = answer.trim().toLowerCase();
@@ -2,6 +2,7 @@ import { Command } from "commander";
2
2
  import { createAuditCommand } from "./audit.js";
3
3
  import { createScanCommand } from "./scan.js";
4
4
  import { createInitCommand } from "./init.js";
5
+ import { createInitProjectCommand } from "./init-project.js";
5
6
  import { createConfigCommand } from "./config.js";
6
7
  import { createExecCommand } from "./exec.js";
7
8
  import { createAuditSkillCommand } from "./audit-skill.js";
@@ -15,6 +16,7 @@ export function createAgentCommand() {
15
16
  .description("Agent security features");
16
17
  // Add subcommands
17
18
  agent.addCommand(createInitCommand());
19
+ agent.addCommand(createInitProjectCommand());
18
20
  agent.addCommand(createScanCommand());
19
21
  agent.addCommand(createExecCommand());
20
22
  agent.addCommand(createConfigCommand());
@@ -0,0 +1,164 @@
1
+ import { Command } from "commander";
2
+ import { injectInstructionFile } from "./instruction-block.js";
3
+ import { fmt } from "../../utils/formatter.js";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import { execSync } from "child_process";
7
+ /** Find the git root directory, or null if not in a git repo */
8
+ function findGitRoot() {
9
+ try {
10
+ return execSync("git rev-parse --show-toplevel", {
11
+ encoding: "utf-8",
12
+ timeout: 5000,
13
+ stdio: ["pipe", "pipe", "ignore"],
14
+ }).trim();
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ /**
21
+ * All project-level instruction file targets.
22
+ *
23
+ * These are files that AI agents read at session start when working in a project.
24
+ * Unlike global files (~/.claude/CLAUDE.md), these live in the repo and are
25
+ * committed alongside the code so every agent session sees them.
26
+ */
27
+ function getProjectTargets(projectRoot) {
28
+ return [
29
+ {
30
+ platform: "Claude Code",
31
+ filePath: path.join(projectRoot, ".claude", "CLAUDE.md"),
32
+ description: "Claude Code project instructions",
33
+ },
34
+ {
35
+ platform: "Codex CLI",
36
+ filePath: path.join(projectRoot, "AGENTS.md"),
37
+ description: "Codex CLI project instructions",
38
+ },
39
+ {
40
+ platform: "Gemini CLI",
41
+ filePath: path.join(projectRoot, "GEMINI.md"),
42
+ description: "Gemini CLI project instructions",
43
+ },
44
+ {
45
+ platform: "Cursor",
46
+ filePath: path.join(projectRoot, ".cursor", "rules", "rafter-security.mdc"),
47
+ description: "Cursor project rules",
48
+ },
49
+ {
50
+ platform: "Windsurf",
51
+ filePath: path.join(projectRoot, ".windsurfrules"),
52
+ description: "Windsurf project rules",
53
+ },
54
+ {
55
+ platform: "Continue.dev",
56
+ filePath: path.join(projectRoot, ".continuerules"),
57
+ description: "Continue.dev project rules",
58
+ },
59
+ {
60
+ platform: "Aider",
61
+ filePath: path.join(projectRoot, ".aider", "conventions.md"),
62
+ description: "Aider project conventions",
63
+ },
64
+ ];
65
+ }
66
+ export function createInitProjectCommand() {
67
+ return new Command("init-project")
68
+ .description("Generate project-level instruction files so AI agents discover Rafter at session start")
69
+ .option("--only <platforms>", "Comma-separated list of platforms (claude-code,codex,gemini,cursor,windsurf,continue,aider)")
70
+ .option("--list", "List which files would be created without writing them")
71
+ .action(async (opts) => {
72
+ const gitRoot = findGitRoot();
73
+ if (!gitRoot) {
74
+ console.error(fmt.error("Not in a git repository. Run this command from inside a project."));
75
+ process.exit(1);
76
+ }
77
+ console.log(fmt.header("Rafter Project Setup"));
78
+ console.log(fmt.info(`Project root: ${gitRoot}`));
79
+ console.log();
80
+ const allTargets = getProjectTargets(gitRoot);
81
+ // Filter by --only if specified
82
+ let targets = allTargets;
83
+ if (opts.only) {
84
+ const platformMap = {
85
+ "claude-code": "Claude Code",
86
+ "claude": "Claude Code",
87
+ "codex": "Codex CLI",
88
+ "gemini": "Gemini CLI",
89
+ "cursor": "Cursor",
90
+ "windsurf": "Windsurf",
91
+ "continue": "Continue.dev",
92
+ "aider": "Aider",
93
+ };
94
+ const requested = opts.only.split(",").map((s) => s.trim().toLowerCase());
95
+ const platformNames = requested.map(r => platformMap[r]).filter(Boolean);
96
+ if (platformNames.length === 0) {
97
+ console.error(fmt.error(`Unknown platforms: ${opts.only}`));
98
+ console.error("Valid: claude-code, codex, gemini, cursor, windsurf, continue, aider");
99
+ process.exit(1);
100
+ }
101
+ targets = allTargets.filter(t => platformNames.includes(t.platform));
102
+ }
103
+ // --list mode: show what would be created
104
+ if (opts.list) {
105
+ for (const target of targets) {
106
+ const exists = fs.existsSync(target.filePath);
107
+ const hasMarker = exists && fs.readFileSync(target.filePath, "utf-8").includes("<!-- rafter:start -->");
108
+ const status = hasMarker ? "update" : exists ? "append" : "create";
109
+ const rel = path.relative(gitRoot, target.filePath);
110
+ console.log(` [${status}] ${rel} — ${target.description}`);
111
+ }
112
+ console.log();
113
+ console.log(fmt.info("Run without --list to write files."));
114
+ return;
115
+ }
116
+ // Write instruction files
117
+ let created = 0;
118
+ let updated = 0;
119
+ let failed = 0;
120
+ for (const target of targets) {
121
+ const rel = path.relative(gitRoot, target.filePath);
122
+ const existed = fs.existsSync(target.filePath);
123
+ const hadMarker = existed && fs.readFileSync(target.filePath, "utf-8").includes("<!-- rafter:start -->");
124
+ try {
125
+ injectInstructionFile(target.filePath);
126
+ if (hadMarker) {
127
+ console.log(fmt.success(`[updated] ${rel}`));
128
+ updated++;
129
+ }
130
+ else {
131
+ console.log(fmt.success(`[created] ${rel}`));
132
+ created++;
133
+ }
134
+ }
135
+ catch (e) {
136
+ console.log(fmt.error(`[failed] ${rel} — ${e}`));
137
+ failed++;
138
+ }
139
+ }
140
+ // Check for .rafter.yml
141
+ const policyPath = path.join(gitRoot, ".rafter.yml");
142
+ if (!fs.existsSync(policyPath)) {
143
+ console.log(fmt.info(`[skipped] .rafter.yml — not found (optional: create one for project-specific policy)`));
144
+ }
145
+ // Check for pre-commit hook
146
+ const hookPath = path.join(gitRoot, ".git", "hooks", "pre-commit");
147
+ const hasRafterHook = fs.existsSync(hookPath) &&
148
+ fs.readFileSync(hookPath, "utf-8").includes("rafter");
149
+ if (!hasRafterHook) {
150
+ console.log(fmt.info(`[hint] Run \`rafter agent install-hook\` to add pre-commit secret scanning`));
151
+ }
152
+ console.log();
153
+ if (created > 0 || updated > 0) {
154
+ console.log(fmt.success(`Done: ${created} created, ${updated} updated${failed > 0 ? `, ${failed} failed` : ""}`));
155
+ console.log();
156
+ console.log("Agents starting sessions in this project will now see Rafter security context.");
157
+ console.log("Consider committing these files so all contributors benefit.");
158
+ }
159
+ else if (failed > 0) {
160
+ console.log(fmt.error(`All ${failed} files failed to write.`));
161
+ }
162
+ console.log();
163
+ });
164
+ }