@rafter-security/cli 0.6.6 → 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 +9 -9
- package/dist/commands/agent/audit-skill.js +20 -19
- package/dist/commands/agent/config.js +2 -1
- package/dist/commands/agent/exec.js +2 -0
- package/dist/commands/agent/init.js +6 -5
- package/dist/commands/agent/install-hook.js +15 -14
- package/dist/commands/agent/scan.js +4 -3
- package/dist/commands/agent/verify.js +1 -1
- package/dist/commands/backend/run.js +12 -3
- package/dist/commands/backend/scan-status.js +3 -2
- package/dist/commands/brief.js +2 -2
- package/dist/commands/ci/init.js +25 -21
- package/dist/commands/completion.js +4 -3
- package/dist/commands/report.js +42 -41
- package/dist/commands/scan/index.js +7 -5
- package/dist/core/risk-rules.js +16 -3
- package/dist/index.js +13 -9
- package/dist/scanners/gitleaks.js +6 -2
- package/package.json +1 -1
- package/resources/skills/rafter/SKILL.md +1 -1
- package/resources/skills/rafter-agent-security/SKILL.md +1 -1
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ yarn global add @rafter-security/cli
|
|
|
23
23
|
|
|
24
24
|
### Getting an API Key
|
|
25
25
|
|
|
26
|
-
To use
|
|
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 code analysis 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
|
|
39
|
+
**Note**: Agent security features (secret scanning, command execution) work **without an API key**. Only remote code analysis requires authentication.
|
|
40
40
|
|
|
41
|
-
###
|
|
41
|
+
### Remote Code Analysis
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
# Set your API key (from above)
|
|
@@ -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-
|
|
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
|
|
516
|
-
rafter ci init --with-
|
|
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
|
|
@@ -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.
|
|
627
|
+
### 1. Remote Code Analysis Skill (Core Feature)
|
|
628
628
|
|
|
629
629
|
**Automatic Integration** - Claude can proactively suggest security scans
|
|
630
630
|
|
|
@@ -680,10 +680,10 @@ Explicitly invoke commands:
|
|
|
680
680
|
|
|
681
681
|
### Why Two Skills?
|
|
682
682
|
|
|
683
|
-
- **
|
|
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
|
|
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(`
|
|
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(
|
|
29
|
-
console.log(
|
|
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("
|
|
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("
|
|
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("
|
|
84
|
-
console.log(
|
|
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(
|
|
87
|
+
console.log(fmt.divider());
|
|
87
88
|
console.log(generateManualReviewPrompt(skillName, absolutePath, quickScan, skillContent));
|
|
88
|
-
console.log(
|
|
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("
|
|
138
|
-
console.log(
|
|
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("
|
|
142
|
+
console.log(fmt.success("Secrets: None detected"));
|
|
142
143
|
}
|
|
143
144
|
else {
|
|
144
|
-
console.log(
|
|
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("
|
|
151
|
+
console.log(fmt.success("External URLs: None"));
|
|
151
152
|
}
|
|
152
153
|
else {
|
|
153
|
-
console.log(
|
|
154
|
+
console.log(fmt.warning(`External URLs: ${scan.urls.length} found`));
|
|
154
155
|
scan.urls.slice(0, 5).forEach(url => {
|
|
155
|
-
console.log(`
|
|
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("
|
|
164
|
+
console.log(fmt.success("High-risk commands: None detected"));
|
|
164
165
|
}
|
|
165
166
|
else {
|
|
166
|
-
console.log(
|
|
167
|
+
console.log(fmt.warning(`High-risk commands: ${scan.highRiskCommands.length} found`));
|
|
167
168
|
scan.highRiskCommands.slice(0, 5).forEach(cmd => {
|
|
168
|
-
console.log(`
|
|
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(
|
|
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();
|
|
@@ -419,10 +419,10 @@ async function installClaudeCodeSkills() {
|
|
|
419
419
|
}
|
|
420
420
|
if (fs.existsSync(backendTemplatePath)) {
|
|
421
421
|
fs.copyFileSync(backendTemplatePath, backendSkillPath);
|
|
422
|
-
console.log(fmt.success(`Installed Rafter
|
|
422
|
+
console.log(fmt.success(`Installed Rafter Remote skill to ${backendSkillPath}`));
|
|
423
423
|
}
|
|
424
424
|
else {
|
|
425
|
-
console.log(fmt.warning(`
|
|
425
|
+
console.log(fmt.warning(`Remote skill template not found at ${backendTemplatePath}`));
|
|
426
426
|
}
|
|
427
427
|
// Install Agent Security Skill
|
|
428
428
|
const agentSkillDir = path.join(claudeSkillsDir, "rafter-agent-security");
|
|
@@ -451,10 +451,10 @@ function installCodexSkills() {
|
|
|
451
451
|
}
|
|
452
452
|
if (fs.existsSync(backendTemplatePath)) {
|
|
453
453
|
fs.copyFileSync(backendTemplatePath, backendSkillPath);
|
|
454
|
-
console.log(fmt.success(`Installed Rafter
|
|
454
|
+
console.log(fmt.success(`Installed Rafter Remote skill to ${backendSkillPath}`));
|
|
455
455
|
}
|
|
456
456
|
else {
|
|
457
|
-
console.log(fmt.warning(`
|
|
457
|
+
console.log(fmt.warning(`Remote skill template not found at ${backendTemplatePath}`));
|
|
458
458
|
}
|
|
459
459
|
// Install Agent Security Skill
|
|
460
460
|
const agentDir = path.join(agentsSkillsDir, "rafter-agent-security");
|
|
@@ -698,8 +698,9 @@ export function createInitCommand() {
|
|
|
698
698
|
}
|
|
699
699
|
}
|
|
700
700
|
// Install Claude Code skills + hooks if opted in
|
|
701
|
+
// When --with-claude-code is explicitly passed, install even if .claude doesn't exist yet
|
|
701
702
|
let claudeCodeOk = false;
|
|
702
|
-
if (hasClaudeCode && wantClaudeCode) {
|
|
703
|
+
if ((hasClaudeCode || opts.withClaudeCode) && wantClaudeCode) {
|
|
703
704
|
try {
|
|
704
705
|
await installClaudeCodeSkills();
|
|
705
706
|
installClaudeCodeHooks();
|
|
@@ -4,6 +4,7 @@ import os from "os";
|
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { execSync } from "child_process";
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
+
import { fmt } from "../../utils/formatter.js";
|
|
7
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
8
9
|
const __dirname = path.dirname(__filename);
|
|
9
10
|
export function createInstallHookCommand() {
|
|
@@ -28,7 +29,7 @@ async function installHook(opts) {
|
|
|
28
29
|
function getTemplatePath(templateName) {
|
|
29
30
|
const templatePath = path.join(__dirname, "..", "..", "..", "resources", templateName);
|
|
30
31
|
if (!fs.existsSync(templatePath)) {
|
|
31
|
-
console.error("
|
|
32
|
+
console.error(fmt.error("Hook template not found"));
|
|
32
33
|
console.error(` Expected at: ${templatePath}`);
|
|
33
34
|
process.exit(1);
|
|
34
35
|
}
|
|
@@ -42,7 +43,7 @@ async function installLocalHook(hookName, templateName) {
|
|
|
42
43
|
execSync("git rev-parse --git-dir", { stdio: "pipe" });
|
|
43
44
|
}
|
|
44
45
|
catch (e) {
|
|
45
|
-
console.error("
|
|
46
|
+
console.error(fmt.error("Not in a git repository"));
|
|
46
47
|
console.error(" Run this command from inside a git repository");
|
|
47
48
|
process.exit(1);
|
|
48
49
|
}
|
|
@@ -56,30 +57,30 @@ async function installLocalHook(hookName, templateName) {
|
|
|
56
57
|
const existing = fs.readFileSync(hookPath, "utf-8");
|
|
57
58
|
const marker = hookName === "pre-push" ? "Rafter Security Pre-Push Hook" : "Rafter Security Pre-Commit Hook";
|
|
58
59
|
if (existing.includes(marker)) {
|
|
59
|
-
console.log(
|
|
60
|
+
console.log(fmt.success(`Rafter ${hookName} hook already installed`));
|
|
60
61
|
return;
|
|
61
62
|
}
|
|
62
63
|
const backupPath = `${hookPath}.backup-${Date.now()}`;
|
|
63
64
|
fs.copyFileSync(hookPath, backupPath);
|
|
64
|
-
console.log(
|
|
65
|
+
console.log(fmt.info(`Backed up existing hook to: ${path.basename(backupPath)}`));
|
|
65
66
|
}
|
|
66
67
|
const hookContent = fs.readFileSync(getTemplatePath(templateName), "utf-8");
|
|
67
68
|
fs.writeFileSync(hookPath, hookContent, "utf-8");
|
|
68
69
|
fs.chmodSync(hookPath, 0o755);
|
|
69
|
-
console.log(
|
|
70
|
+
console.log(fmt.success(`Installed Rafter ${hookName} hook`));
|
|
70
71
|
console.log(` Location: ${hookPath}`);
|
|
71
72
|
console.log();
|
|
72
73
|
if (hookName === "pre-push") {
|
|
73
74
|
console.log("The hook will:");
|
|
74
|
-
console.log("
|
|
75
|
-
console.log("
|
|
76
|
-
console.log("
|
|
75
|
+
console.log(" - Scan commits being pushed for secrets");
|
|
76
|
+
console.log(" - Block pushes if secrets are detected");
|
|
77
|
+
console.log(" - Can be bypassed with: git push --no-verify (not recommended)");
|
|
77
78
|
}
|
|
78
79
|
else {
|
|
79
80
|
console.log("The hook will:");
|
|
80
|
-
console.log("
|
|
81
|
-
console.log("
|
|
82
|
-
console.log("
|
|
81
|
+
console.log(" - Scan staged files for secrets before each commit");
|
|
82
|
+
console.log(" - Block commits if secrets are detected");
|
|
83
|
+
console.log(" - Can be bypassed with: git commit --no-verify (not recommended)");
|
|
83
84
|
}
|
|
84
85
|
console.log();
|
|
85
86
|
}
|
|
@@ -89,7 +90,7 @@ async function installLocalHook(hookName, templateName) {
|
|
|
89
90
|
async function installGlobalHook(hookName, templateName) {
|
|
90
91
|
const homeDir = os.homedir();
|
|
91
92
|
if (!homeDir) {
|
|
92
|
-
console.error("
|
|
93
|
+
console.error(fmt.error("Could not determine home directory"));
|
|
93
94
|
process.exit(1);
|
|
94
95
|
}
|
|
95
96
|
const globalHooksDir = path.join(homeDir, ".rafter", "git-hooks");
|
|
@@ -102,7 +103,7 @@ async function installGlobalHook(hookName, templateName) {
|
|
|
102
103
|
fs.chmodSync(hookPath, 0o755);
|
|
103
104
|
try {
|
|
104
105
|
execSync(`git config --global core.hooksPath "${globalHooksDir}"`, { stdio: "pipe" });
|
|
105
|
-
console.log(
|
|
106
|
+
console.log(fmt.success(`Installed Rafter ${hookName} hook globally`));
|
|
106
107
|
console.log(` Location: ${hookPath}`);
|
|
107
108
|
console.log(` Git config: core.hooksPath = ${globalHooksDir}`);
|
|
108
109
|
console.log();
|
|
@@ -116,7 +117,7 @@ async function installGlobalHook(hookName, templateName) {
|
|
|
116
117
|
console.log();
|
|
117
118
|
}
|
|
118
119
|
catch (e) {
|
|
119
|
-
console.error("
|
|
120
|
+
console.error(fmt.error("Failed to configure global git hooks"));
|
|
120
121
|
console.error(" You may need to manually set: git config --global core.hooksPath ~/.rafter/git-hooks");
|
|
121
122
|
process.exit(1);
|
|
122
123
|
}
|
|
@@ -47,6 +47,7 @@ export function createScanCommand() {
|
|
|
47
47
|
.option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
|
|
48
48
|
.option("--baseline", "Filter findings present in the saved baseline")
|
|
49
49
|
.option("--watch", "Watch for file changes and re-scan on change")
|
|
50
|
+
.option("--history", "Scan git history for secrets (requires gitleaks engine)")
|
|
50
51
|
.action(async (scanPath, opts) => {
|
|
51
52
|
// Validate flags before doing any work
|
|
52
53
|
const validEngines = ["auto", "gitleaks", "patterns"];
|
|
@@ -103,7 +104,7 @@ export function createScanCommand() {
|
|
|
103
104
|
if (!opts.quiet) {
|
|
104
105
|
console.error(`Scanning directory: ${resolvedPath} (${engine})`);
|
|
105
106
|
}
|
|
106
|
-
results = await scanDirectory(resolvedPath, engine, scanCfg);
|
|
107
|
+
results = await scanDirectory(resolvedPath, engine, scanCfg, opts.history);
|
|
107
108
|
}
|
|
108
109
|
else {
|
|
109
110
|
if (!opts.quiet) {
|
|
@@ -372,11 +373,11 @@ async function scanFile(filePath, engine, scanCfg) {
|
|
|
372
373
|
/**
|
|
373
374
|
* Scan a directory with selected engine
|
|
374
375
|
*/
|
|
375
|
-
async function scanDirectory(dirPath, engine, scanCfg) {
|
|
376
|
+
async function scanDirectory(dirPath, engine, scanCfg, history) {
|
|
376
377
|
if (engine === "gitleaks") {
|
|
377
378
|
try {
|
|
378
379
|
const gitleaks = new GitleaksScanner();
|
|
379
|
-
return await gitleaks.scanDirectory(dirPath);
|
|
380
|
+
return await gitleaks.scanDirectory(dirPath, { useGit: history ?? false });
|
|
380
381
|
}
|
|
381
382
|
catch (e) {
|
|
382
383
|
console.error(fmt.warning("Gitleaks scan failed, falling back to patterns"));
|
|
@@ -193,7 +193,7 @@ export function createVerifyCommand() {
|
|
|
193
193
|
console.log(fmt.success(`${passed.length}/${results.length} core checks passed${warnNote}`));
|
|
194
194
|
}
|
|
195
195
|
else {
|
|
196
|
-
console.log(fmt.error(`${
|
|
196
|
+
console.log(fmt.error(`${passed.length}/${results.length} checks passed — ${hardFailed.length} failed`));
|
|
197
197
|
}
|
|
198
198
|
console.log();
|
|
199
199
|
if (hardFailed.length > 0) {
|
|
@@ -9,6 +9,7 @@ import { handleScanStatus } from "./scan-status.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export async function runRemoteScan(opts) {
|
|
11
11
|
const key = resolveKey(opts.apiKey);
|
|
12
|
+
const ghToken = opts.githubToken || process.env.RAFTER_GITHUB_TOKEN;
|
|
12
13
|
let repo, branch;
|
|
13
14
|
try {
|
|
14
15
|
({ repo, branch } = detectRepo({ repo: opts.repo, branch: opts.branch, quiet: opts.quiet }));
|
|
@@ -22,10 +23,17 @@ export async function runRemoteScan(opts) {
|
|
|
22
23
|
}
|
|
23
24
|
process.exit(EXIT_GENERAL_ERROR);
|
|
24
25
|
}
|
|
26
|
+
const body = {
|
|
27
|
+
repository_name: repo,
|
|
28
|
+
branch_name: branch,
|
|
29
|
+
scan_mode: opts.mode ?? "fast",
|
|
30
|
+
};
|
|
31
|
+
if (ghToken)
|
|
32
|
+
body.github_token = ghToken;
|
|
25
33
|
if (!opts.quiet) {
|
|
26
34
|
const spinner = ora("Submitting scan").start();
|
|
27
35
|
try {
|
|
28
|
-
const { data } = await axios.post(`${API}/static/scan`,
|
|
36
|
+
const { data } = await axios.post(`${API}/static/scan`, body, { headers: { "x-api-key": key } });
|
|
29
37
|
spinner.succeed(`Scan ID: ${data.scan_id}`);
|
|
30
38
|
if (opts.skipInteractive)
|
|
31
39
|
return;
|
|
@@ -56,7 +64,7 @@ export async function runRemoteScan(opts) {
|
|
|
56
64
|
}
|
|
57
65
|
else {
|
|
58
66
|
try {
|
|
59
|
-
const { data } = await axios.post(`${API}/static/scan`,
|
|
67
|
+
const { data } = await axios.post(`${API}/static/scan`, body, { headers: { "x-api-key": key } });
|
|
60
68
|
if (opts.skipInteractive)
|
|
61
69
|
return;
|
|
62
70
|
const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format ?? "md", opts.quiet);
|
|
@@ -90,12 +98,13 @@ function addRunOptions(cmd) {
|
|
|
90
98
|
.option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
|
|
91
99
|
.option("-f, --format <format>", "json | md", "md")
|
|
92
100
|
.option("-m, --mode <mode>", "scan mode: fast | plus", "fast")
|
|
101
|
+
.option("--github-token <token>", "GitHub PAT for private repos (or RAFTER_GITHUB_TOKEN env var)")
|
|
93
102
|
.option("--skip-interactive", "do not wait for scan to complete")
|
|
94
103
|
.option("--quiet", "suppress status messages");
|
|
95
104
|
}
|
|
96
105
|
export function createRunCommand() {
|
|
97
106
|
return addRunOptions(new Command("run")
|
|
98
|
-
.description("Trigger a remote
|
|
107
|
+
.description("Trigger a remote security scan")).action(async (opts) => {
|
|
99
108
|
await runRemoteScan(opts);
|
|
100
109
|
});
|
|
101
110
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import { API, writePayload, EXIT_GENERAL_ERROR, EXIT_SCAN_NOT_FOUND } from "../../utils/api.js";
|
|
4
|
+
import { fmt as output } from "../../utils/formatter.js";
|
|
4
5
|
export async function handleScanStatus(scan_id, headers, fmt, quiet) {
|
|
5
6
|
// First poll
|
|
6
7
|
let poll;
|
|
@@ -9,10 +10,10 @@ export async function handleScanStatus(scan_id, headers, fmt, quiet) {
|
|
|
9
10
|
}
|
|
10
11
|
catch (e) {
|
|
11
12
|
if (e.response?.status === 404) {
|
|
12
|
-
console.error(`Scan '${scan_id}' not found`);
|
|
13
|
+
console.error(output.error(`Scan '${scan_id}' not found`));
|
|
13
14
|
return EXIT_SCAN_NOT_FOUND;
|
|
14
15
|
}
|
|
15
|
-
console.error(
|
|
16
|
+
console.error(output.error(`${e.response?.data || e.message}`));
|
|
16
17
|
return EXIT_GENERAL_ERROR;
|
|
17
18
|
}
|
|
18
19
|
let status = poll.data.status;
|
package/dist/commands/brief.js
CHANGED
|
@@ -54,7 +54,7 @@ function buildTopics() {
|
|
|
54
54
|
render: () => loadSkill("rafter-agent-security"),
|
|
55
55
|
},
|
|
56
56
|
scanning: {
|
|
57
|
-
description: "Remote SAST/SCA code analysis via
|
|
57
|
+
description: "Remote SAST/SCA code analysis via Rafter API",
|
|
58
58
|
render: () => loadSkill("rafter"),
|
|
59
59
|
},
|
|
60
60
|
commands: {
|
|
@@ -78,7 +78,7 @@ function buildTopics() {
|
|
|
78
78
|
return [
|
|
79
79
|
"# Rafter Command Reference",
|
|
80
80
|
"",
|
|
81
|
-
"##
|
|
81
|
+
"## Remote Code Analysis",
|
|
82
82
|
"",
|
|
83
83
|
backCmds,
|
|
84
84
|
"",
|
package/dist/commands/ci/init.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { fmt } from "../../utils/formatter.js";
|
|
4
|
+
import { fmt, isAgentMode } from "../../utils/formatter.js";
|
|
5
5
|
export function createCiInitCommand() {
|
|
6
6
|
return new Command("init")
|
|
7
7
|
.description("Generate CI/CD pipeline config for secret scanning")
|
|
8
8
|
.option("--platform <platform>", "CI platform: github, gitlab, circleci (default: auto-detect)")
|
|
9
9
|
.option("--output <path>", "Output file path (default: platform-specific)")
|
|
10
|
-
.option("--with-
|
|
10
|
+
.option("--with-remote", "Include remote security audit job (requires RAFTER_API_KEY)")
|
|
11
|
+
.option("--with-backend", "Deprecated: use --with-remote")
|
|
11
12
|
.action((opts) => {
|
|
12
13
|
const platform = opts.platform || detectPlatform();
|
|
13
14
|
if (!platform) {
|
|
@@ -21,7 +22,8 @@ export function createCiInitCommand() {
|
|
|
21
22
|
console.error(`Valid options: ${validPlatforms.join(", ")}`);
|
|
22
23
|
process.exit(1);
|
|
23
24
|
}
|
|
24
|
-
const
|
|
25
|
+
const includeRemote = !!(opts.includeRemote || opts.withBackend);
|
|
26
|
+
const { content, defaultPath } = generateTemplate(platform, includeRemote);
|
|
25
27
|
const outputPath = opts.output || defaultPath;
|
|
26
28
|
const outputDir = path.dirname(outputPath);
|
|
27
29
|
if (!fs.existsSync(outputDir)) {
|
|
@@ -29,28 +31,30 @@ export function createCiInitCommand() {
|
|
|
29
31
|
}
|
|
30
32
|
fs.writeFileSync(outputPath, content, "utf-8");
|
|
31
33
|
console.log(fmt.success(`Generated ${platform} CI config at ${outputPath}`));
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
if (!isAgentMode()) {
|
|
35
|
+
console.log();
|
|
36
|
+
console.log("Next steps:");
|
|
37
|
+
console.log(` 1. Review the generated file: ${outputPath}`);
|
|
38
|
+
if (includeRemote) {
|
|
39
|
+
if (platform === "github") {
|
|
40
|
+
console.log(" 2. Add RAFTER_API_KEY to repo Settings > Secrets > Actions");
|
|
41
|
+
}
|
|
42
|
+
else if (platform === "gitlab") {
|
|
43
|
+
console.log(" 2. Add RAFTER_API_KEY to Settings > CI/CD > Variables");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log(" 2. Add RAFTER_API_KEY to project environment variables");
|
|
47
|
+
}
|
|
41
48
|
}
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
console.log(` ${includeRemote ? "3" : "2"}. Commit and push to trigger the pipeline`);
|
|
50
|
+
if (platform === "github") {
|
|
51
|
+
console.log();
|
|
52
|
+
console.log("Alternatives:");
|
|
53
|
+
console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v1");
|
|
54
|
+
console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
|
|
44
55
|
}
|
|
45
|
-
}
|
|
46
|
-
console.log(` ${opts.withBackend ? "3" : "2"}. Commit and push to trigger the pipeline`);
|
|
47
|
-
if (platform === "github") {
|
|
48
56
|
console.log();
|
|
49
|
-
console.log("Alternatives:");
|
|
50
|
-
console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v1");
|
|
51
|
-
console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
|
|
52
57
|
}
|
|
53
|
-
console.log();
|
|
54
58
|
});
|
|
55
59
|
}
|
|
56
60
|
function detectPlatform() {
|
|
@@ -68,7 +68,7 @@ _rafter_completions() {
|
|
|
68
68
|
if [[ "\${COMP_WORDS[1]}" == "agent" ]]; then
|
|
69
69
|
COMPREPLY=( $(compgen -W "--risk-level --with-openclaw --with-claude-code --with-codex --with-gemini --with-aider --with-cursor --with-windsurf --with-continue --with-gitleaks --all --help" -- "\${cur}") )
|
|
70
70
|
elif [[ "\${COMP_WORDS[1]}" == "ci" ]]; then
|
|
71
|
-
COMPREPLY=( $(compgen -W "--platform --output --with-backend --help" -- "\${cur}") )
|
|
71
|
+
COMPREPLY=( $(compgen -W "--platform --output --with-remote --with-backend --help" -- "\${cur}") )
|
|
72
72
|
fi
|
|
73
73
|
return 0
|
|
74
74
|
;;
|
|
@@ -81,7 +81,7 @@ const ZSH_COMPLETION = `#compdef rafter
|
|
|
81
81
|
_rafter() {
|
|
82
82
|
local -a commands
|
|
83
83
|
commands=(
|
|
84
|
-
'run:Submit a security scan
|
|
84
|
+
'run:Submit a remote security scan'
|
|
85
85
|
'scan:Alias for run'
|
|
86
86
|
'get:Retrieve scan results'
|
|
87
87
|
'usage:Check API usage quota'
|
|
@@ -369,7 +369,8 @@ complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcom
|
|
|
369
369
|
complete -c rafter -n '__fish_seen_subcommand_from ci' -a init -d 'Initialize CI pipeline'
|
|
370
370
|
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l platform -d 'CI platform' -ra 'github gitlab circleci'
|
|
371
371
|
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l output -d 'Output path' -r
|
|
372
|
-
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-
|
|
372
|
+
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-remote -d 'Include remote audit'
|
|
373
|
+
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-backend -d 'Deprecated: use --with-remote'
|
|
373
374
|
|
|
374
375
|
# hook subcommands
|
|
375
376
|
complete -c rafter -n '__fish_seen_subcommand_from hook' -a pretool -d 'PreToolUse hook handler'
|
package/dist/commands/report.js
CHANGED
|
@@ -86,11 +86,11 @@ function generateHtmlReport(results, title) {
|
|
|
86
86
|
? "Low"
|
|
87
87
|
: "None";
|
|
88
88
|
const riskColor = {
|
|
89
|
-
Critical: "
|
|
90
|
-
High: "
|
|
91
|
-
Medium: "
|
|
92
|
-
Low: "
|
|
93
|
-
None: "
|
|
89
|
+
Critical: "hsl(0 40% 55%)",
|
|
90
|
+
High: "hsl(25 35% 55%)",
|
|
91
|
+
Medium: "hsl(0 0% 64%)",
|
|
92
|
+
Low: "hsl(0 0% 50%)",
|
|
93
|
+
None: "hsl(0 0% 50%)",
|
|
94
94
|
}[riskLevel];
|
|
95
95
|
const topPatterns = Object.entries(patternCounts)
|
|
96
96
|
.sort((a, b) => b[1] - a[1])
|
|
@@ -113,38 +113,39 @@ function generateHtmlReport(results, title) {
|
|
|
113
113
|
<title>${escapeHtml(title)}</title>
|
|
114
114
|
<style>
|
|
115
115
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
116
|
-
body { font-family: -
|
|
116
|
+
body { font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace; line-height: 1.6; color: hsl(0 0% 98%); background: hsl(0 0% 3.9%); }
|
|
117
117
|
.container { max-width: 1100px; margin: 0 auto; padding: 2rem 1.5rem; }
|
|
118
|
-
header { background:
|
|
118
|
+
header { background: hsl(0 0% 7%); color: hsl(0 0% 98%); padding: 2rem 0; margin-bottom: 2rem; border-bottom: 1px solid hsl(0 0% 14.9%); }
|
|
119
119
|
header .container { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 1rem; }
|
|
120
120
|
header h1 { font-size: 1.5rem; font-weight: 700; }
|
|
121
|
-
header .meta { font-size: 0.85rem; opacity: 0.
|
|
122
|
-
.card { background:
|
|
123
|
-
.card h2 { font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem; color:
|
|
121
|
+
header .meta { font-size: 0.85rem; opacity: 0.6; text-align: right; }
|
|
122
|
+
.card { background: hsl(0 0% 7%); border-radius: 8px; border: 1px solid hsl(0 0% 14.9%); padding: 1.5rem; margin-bottom: 1.5rem; }
|
|
123
|
+
.card h2 { font-size: 1.1rem; font-weight: 600; margin-bottom: 1rem; color: hsl(0 0% 98%); }
|
|
124
124
|
.summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; }
|
|
125
|
-
.stat { text-align: center; padding: 1rem; border-radius: 6px; background:
|
|
126
|
-
.stat .value { font-size: 2rem; font-weight: 700; }
|
|
127
|
-
.stat .label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color:
|
|
128
|
-
.risk-badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 4px;
|
|
129
|
-
.sev-critical { background:
|
|
130
|
-
.sev-high { background:
|
|
131
|
-
.sev-medium { background:
|
|
132
|
-
.sev-low { background:
|
|
125
|
+
.stat { text-align: center; padding: 1rem; border-radius: 6px; background: hsl(0 0% 10%); border: 1px solid hsl(0 0% 14.9%); }
|
|
126
|
+
.stat .value { font-size: 2rem; font-weight: 700; color: hsl(0 0% 98%); }
|
|
127
|
+
.stat .label { font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: hsl(0 0% 50%); margin-top: 0.25rem; }
|
|
128
|
+
.risk-badge { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 4px; font-weight: 600; font-size: 0.85rem; }
|
|
129
|
+
.sev-critical { background: hsl(0 30% 20%); color: hsl(0 40% 75%); border: 1px solid hsl(0 30% 30%); }
|
|
130
|
+
.sev-high { background: hsl(25 25% 18%); color: hsl(25 35% 70%); border: 1px solid hsl(25 25% 28%); }
|
|
131
|
+
.sev-medium { background: hsl(0 0% 18%); color: hsl(0 0% 70%); border: 1px solid hsl(0 0% 25%); }
|
|
132
|
+
.sev-low { background: hsl(0 0% 14%); color: hsl(0 0% 55%); border: 1px solid hsl(0 0% 22%); }
|
|
133
133
|
.bar-chart { margin-top: 0.5rem; }
|
|
134
134
|
.bar-row { display: flex; align-items: center; margin-bottom: 0.4rem; }
|
|
135
|
-
.bar-label { width: 180px; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
136
|
-
.bar-track { flex: 1; height: 20px; background:
|
|
137
|
-
.bar-fill { height: 100%; border-radius: 3px; min-width: 2px; }
|
|
138
|
-
.bar-count { width: 40px; text-align: right; font-size: 0.85rem; font-weight: 600; color:
|
|
135
|
+
.bar-label { width: 180px; font-size: 0.85rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: hsl(0 0% 70%); }
|
|
136
|
+
.bar-track { flex: 1; height: 20px; background: hsl(0 0% 14.9%); border-radius: 3px; overflow: hidden; }
|
|
137
|
+
.bar-fill { height: 100%; border-radius: 3px; min-width: 2px; background: hsl(0 0% 98%); opacity: 0.6; }
|
|
138
|
+
.bar-count { width: 40px; text-align: right; font-size: 0.85rem; font-weight: 600; color: hsl(0 0% 64%); margin-left: 0.5rem; }
|
|
139
139
|
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
|
|
140
|
-
th { text-align: left; padding: 0.6rem 0.75rem; background:
|
|
141
|
-
td { padding: 0.6rem 0.75rem; border-bottom: 1px solid
|
|
142
|
-
tr:hover td { background:
|
|
143
|
-
.sev-pill { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 3px;
|
|
144
|
-
.file-path { font-
|
|
145
|
-
.redacted { font-
|
|
146
|
-
|
|
147
|
-
|
|
140
|
+
th { text-align: left; padding: 0.6rem 0.75rem; background: hsl(0 0% 10%); border-bottom: 2px solid hsl(0 0% 14.9%); font-weight: 600; color: hsl(0 0% 64%); white-space: nowrap; text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.75rem; }
|
|
141
|
+
td { padding: 0.6rem 0.75rem; border-bottom: 1px solid hsl(0 0% 14.9%); vertical-align: top; }
|
|
142
|
+
tr:hover td { background: hsl(0 0% 10%); }
|
|
143
|
+
.sev-pill { display: inline-block; padding: 0.15rem 0.5rem; border-radius: 3px; font-weight: 600; font-size: 0.75rem; text-transform: uppercase; }
|
|
144
|
+
.file-path { font-size: 0.8rem; word-break: break-all; }
|
|
145
|
+
.redacted { font-size: 0.8rem; color: hsl(0 0% 40%); }
|
|
146
|
+
.description { color: hsl(0 0% 50%); }
|
|
147
|
+
footer { text-align: center; padding: 2rem 0; font-size: 0.8rem; color: hsl(0 0% 35%); border-top: 1px solid hsl(0 0% 14.9%); }
|
|
148
|
+
.no-findings { text-align: center; padding: 3rem; color: hsl(0 0% 64%); }
|
|
148
149
|
.no-findings .icon { font-size: 3rem; margin-bottom: 0.5rem; }
|
|
149
150
|
@media (max-width: 768px) {
|
|
150
151
|
.summary-grid { grid-template-columns: repeat(2, 1fr); }
|
|
@@ -152,9 +153,9 @@ function generateHtmlReport(results, title) {
|
|
|
152
153
|
table { display: block; overflow-x: auto; }
|
|
153
154
|
}
|
|
154
155
|
@media print {
|
|
155
|
-
body { background:
|
|
156
|
-
.card {
|
|
157
|
-
header {
|
|
156
|
+
body { background: hsl(0 0% 3.9%); color: hsl(0 0% 98%); }
|
|
157
|
+
.card { break-inside: avoid; }
|
|
158
|
+
header { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
158
159
|
}
|
|
159
160
|
</style>
|
|
160
161
|
</head>
|
|
@@ -173,7 +174,7 @@ function generateHtmlReport(results, title) {
|
|
|
173
174
|
<h2>Executive Summary</h2>
|
|
174
175
|
<div class="summary-grid">
|
|
175
176
|
<div class="stat">
|
|
176
|
-
<div class="value"
|
|
177
|
+
<div class="value">${totalFindings}</div>
|
|
177
178
|
<div class="label">Total Findings</div>
|
|
178
179
|
</div>
|
|
179
180
|
<div class="stat">
|
|
@@ -181,7 +182,7 @@ function generateHtmlReport(results, title) {
|
|
|
181
182
|
<div class="label">Files Affected</div>
|
|
182
183
|
</div>
|
|
183
184
|
<div class="stat">
|
|
184
|
-
<div class="value"><span class="risk-badge" style="background:${riskColor}">${riskLevel}</span></div>
|
|
185
|
+
<div class="value"><span class="risk-badge" style="background:${riskColor};color:hsl(0 0% 98%)">${riskLevel}</span></div>
|
|
185
186
|
<div class="label">Overall Risk</div>
|
|
186
187
|
</div>
|
|
187
188
|
</div>
|
|
@@ -190,10 +191,10 @@ function generateHtmlReport(results, title) {
|
|
|
190
191
|
<div class="card">
|
|
191
192
|
<h2>Severity Breakdown</h2>
|
|
192
193
|
<div class="summary-grid">
|
|
193
|
-
<div class="stat"><div class="value" style="color
|
|
194
|
-
<div class="stat"><div class="value" style="color
|
|
195
|
-
<div class="stat"><div class="value"
|
|
196
|
-
<div class="stat"><div class="value" style="color
|
|
194
|
+
<div class="stat"><div class="value" style="color:hsl(0 40% 70%)">${severityCounts.critical}</div><div class="label">Critical</div></div>
|
|
195
|
+
<div class="stat"><div class="value" style="color:hsl(25 30% 65%)">${severityCounts.high}</div><div class="label">High</div></div>
|
|
196
|
+
<div class="stat"><div class="value">${severityCounts.medium}</div><div class="label">Medium</div></div>
|
|
197
|
+
<div class="stat"><div class="value" style="color:hsl(0 0% 50%)">${severityCounts.low}</div><div class="label">Low</div></div>
|
|
197
198
|
</div>
|
|
198
199
|
</div>
|
|
199
200
|
|
|
@@ -204,7 +205,7 @@ ${topPatterns.map(([name, count]) => {
|
|
|
204
205
|
const pct = Math.round((count / totalFindings) * 100);
|
|
205
206
|
return ` <div class="bar-row">
|
|
206
207
|
<div class="bar-label" title="${escapeHtml(name)}">${escapeHtml(name)}</div>
|
|
207
|
-
<div class="bar-track"><div class="bar-fill
|
|
208
|
+
<div class="bar-track"><div class="bar-fill" style="width:${pct}%"></div></div>
|
|
208
209
|
<div class="bar-count">${count}</div>
|
|
209
210
|
</div>`;
|
|
210
211
|
}).join("\n")}
|
|
@@ -226,7 +227,7 @@ ${totalFindings > 0 ? ` <div class="card">
|
|
|
226
227
|
<tbody>
|
|
227
228
|
${findingsRows.map((f) => ` <tr>
|
|
228
229
|
<td><span class="sev-pill sev-${f.severity}">${f.severity}</span></td>
|
|
229
|
-
<td>${f.pattern}${f.description ? `<br><small
|
|
230
|
+
<td>${f.pattern}${f.description ? `<br><small class="description">${f.description}</small>` : ""}</td>
|
|
230
231
|
<td class="file-path">${f.file}</td>
|
|
231
232
|
<td>${f.line}</td>
|
|
232
233
|
<td class="redacted">${f.redacted}</td>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* rafter scan — top-level scan command group.
|
|
3
3
|
*
|
|
4
|
-
* Default (no subcommand): remote
|
|
5
|
-
* rafter scan remote: explicit alias for remote
|
|
4
|
+
* Default (no subcommand): remote scan (same as `rafter run`)
|
|
5
|
+
* rafter scan remote: explicit alias for remote scan
|
|
6
6
|
* rafter scan local [path]: local secret scanner (was `rafter agent scan`)
|
|
7
7
|
*/
|
|
8
8
|
import { Command } from "commander";
|
|
@@ -21,25 +21,27 @@ export function createScanGroupCommand() {
|
|
|
21
21
|
.option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
|
|
22
22
|
.option("-f, --format <format>", "json | md", "md")
|
|
23
23
|
.option("-m, --mode <mode>", "scan mode: fast | plus", "fast")
|
|
24
|
+
.option("--github-token <token>", "GitHub PAT for private repos (or RAFTER_GITHUB_TOKEN env var)")
|
|
24
25
|
.option("--skip-interactive", "do not wait for scan to complete")
|
|
25
26
|
.option("--quiet", "suppress status messages")
|
|
26
27
|
.action(async (opts) => {
|
|
27
28
|
await runRemoteScan(opts);
|
|
28
29
|
});
|
|
29
|
-
// Root scan group — default action is remote
|
|
30
|
+
// Root scan group — default action is remote scan
|
|
30
31
|
const scanGroup = new Command("scan")
|
|
31
|
-
.description("Scan for security issues. Default: remote
|
|
32
|
+
.description("Scan for security issues. Default: remote scan. Use 'scan local' for local secret scanning.")
|
|
32
33
|
.enablePositionalOptions()
|
|
33
34
|
.option("-r, --repo <repo>", "org/repo (default: current)")
|
|
34
35
|
.option("-b, --branch <branch>", "branch (default: current else main)")
|
|
35
36
|
.option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
|
|
36
37
|
.option("-f, --format <format>", "json | md", "md")
|
|
37
38
|
.option("-m, --mode <mode>", "scan mode: fast | plus", "fast")
|
|
39
|
+
.option("--github-token <token>", "GitHub PAT for private repos (or RAFTER_GITHUB_TOKEN env var)")
|
|
38
40
|
.option("--skip-interactive", "do not wait for scan to complete")
|
|
39
41
|
.option("--quiet", "suppress status messages");
|
|
40
42
|
scanGroup.addCommand(localCmd);
|
|
41
43
|
scanGroup.addCommand(remoteCmd);
|
|
42
|
-
// When invoked with no subcommand, run remote
|
|
44
|
+
// When invoked with no subcommand, run remote scan
|
|
43
45
|
scanGroup.action(async (opts) => {
|
|
44
46
|
await runRemoteScan(opts);
|
|
45
47
|
});
|
package/dist/core/risk-rules.js
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
* Centralized risk assessment rules.
|
|
3
3
|
* Single source of truth — imported by command-interceptor, audit-logger, and config-defaults.
|
|
4
4
|
*/
|
|
5
|
+
/** Directories where `rm -rf /<dir>` is catastrophic (data loss / unbootable). */
|
|
6
|
+
const CRITICAL_DIRS = "home|etc|usr|boot|root|sys|proc|lib|lib64|bin|sbin|opt";
|
|
5
7
|
export const CRITICAL_PATTERNS = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
// rm -rf / (root only, any flag order)
|
|
9
|
+
new RegExp(`rm\\s+(-[a-z]*r[a-z]*\\s+)*-[a-z]*f[a-z]*\\s+/(\\s|$)`),
|
|
10
|
+
new RegExp(`rm\\s+(-[a-z]*f[a-z]*\\s+)*-[a-z]*r[a-z]*\\s+/(\\s|$)`),
|
|
11
|
+
// rm -rf on critical top-level directories
|
|
12
|
+
new RegExp(`rm\\s+(-[a-z]*r[a-z]*\\s+)*-[a-z]*f[a-z]*\\s+/(${CRITICAL_DIRS})(/|\\s|$)`),
|
|
13
|
+
new RegExp(`rm\\s+(-[a-z]*f[a-z]*\\s+)*-[a-z]*r[a-z]*\\s+/(${CRITICAL_DIRS})(/|\\s|$)`),
|
|
8
14
|
/:\(\)\{\s*:\|:&\s*\};:/, // fork bomb
|
|
9
15
|
/dd\s+if=.*of=\/dev\/sd/,
|
|
10
16
|
/>\s*\/dev\/sd/,
|
|
@@ -55,11 +61,18 @@ export const DEFAULT_REQUIRE_APPROVAL = [
|
|
|
55
61
|
"git push --force-if-includes",
|
|
56
62
|
"git push .* \\+",
|
|
57
63
|
];
|
|
64
|
+
/** Read-only commands whose arguments should not trigger risk patterns. */
|
|
65
|
+
const SAFE_PREFIX = /^(grep|egrep|fgrep|rg|ag|ack|echo|printf)\s/;
|
|
66
|
+
/** Shell operators that chain independent commands. */
|
|
67
|
+
const CHAIN_OPERATORS = /[;|&]|&&|\|\|/;
|
|
58
68
|
/**
|
|
59
69
|
* Assess risk level of a command string.
|
|
60
70
|
*/
|
|
61
71
|
export function assessCommandRisk(command) {
|
|
62
|
-
const cmd = command.toLowerCase();
|
|
72
|
+
const cmd = command.toLowerCase().trim();
|
|
73
|
+
// Safe prefix only applies to simple (non-chained) commands
|
|
74
|
+
if (SAFE_PREFIX.test(cmd) && !CHAIN_OPERATORS.test(cmd))
|
|
75
|
+
return "low";
|
|
63
76
|
for (const pattern of CRITICAL_PATTERNS) {
|
|
64
77
|
if (pattern.test(cmd))
|
|
65
78
|
return "critical";
|
package/dist/index.js
CHANGED
|
@@ -21,24 +21,22 @@ import { createRequire } from "module";
|
|
|
21
21
|
dotenv.config();
|
|
22
22
|
const require = createRequire(import.meta.url);
|
|
23
23
|
const { version: VERSION } = require("../package.json");
|
|
24
|
+
// Set agent mode early from argv — preAction hooks may not propagate to nested
|
|
25
|
+
// subcommands on Node 18, so we detect -a/--agent before Commander parses.
|
|
26
|
+
if (process.argv.includes("-a") || process.argv.includes("--agent")) {
|
|
27
|
+
setAgentMode(true);
|
|
28
|
+
}
|
|
24
29
|
const program = new Command()
|
|
25
30
|
.name("rafter")
|
|
26
31
|
.description("Rafter CLI — the default security agent for AI workflows. Free for individuals and open source. No account required.")
|
|
27
32
|
.version(VERSION)
|
|
28
33
|
.enablePositionalOptions()
|
|
29
34
|
.option("-a, --agent", "Plain output for AI agents (no colors/emoji)");
|
|
30
|
-
//
|
|
31
|
-
program.hook("preAction", (thisCommand) => {
|
|
32
|
-
const opts = thisCommand.opts();
|
|
33
|
-
if (opts.agent) {
|
|
34
|
-
setAgentMode(true);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
// Backend commands
|
|
35
|
+
// Remote scan commands
|
|
38
36
|
program.addCommand(createRunCommand());
|
|
39
37
|
program.addCommand(createGetCommand());
|
|
40
38
|
program.addCommand(createUsageCommand());
|
|
41
|
-
// Scan command group (default: remote
|
|
39
|
+
// Scan command group (default: remote scan; subcommands: local, remote)
|
|
42
40
|
program.addCommand(createScanGroupCommand());
|
|
43
41
|
// Agent commands
|
|
44
42
|
program.addCommand(createAgentCommand());
|
|
@@ -60,6 +58,12 @@ program.addCommand(createNotifyCommand());
|
|
|
60
58
|
program.addCommand(createReportCommand());
|
|
61
59
|
// Shell completions
|
|
62
60
|
program.addCommand(createCompletionCommand());
|
|
61
|
+
// Version subcommand (also available as --version)
|
|
62
|
+
program.addCommand(new Command("version")
|
|
63
|
+
.description("Print version and exit")
|
|
64
|
+
.action(() => {
|
|
65
|
+
console.log(VERSION);
|
|
66
|
+
}));
|
|
63
67
|
// Non-blocking update check — runs after command, prints to stderr
|
|
64
68
|
checkForUpdate(VERSION).then((notice) => {
|
|
65
69
|
if (notice)
|
|
@@ -82,15 +82,19 @@ export class GitleaksScanner {
|
|
|
82
82
|
/**
|
|
83
83
|
* Scan a directory
|
|
84
84
|
*/
|
|
85
|
-
async scanDirectory(dirPath) {
|
|
85
|
+
async scanDirectory(dirPath, opts) {
|
|
86
86
|
if (!await this.isAvailable()) {
|
|
87
87
|
throw new Error("Gitleaks not available");
|
|
88
88
|
}
|
|
89
89
|
const gitleaksPath = this.binaryManager.getGitleaksPath();
|
|
90
90
|
const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${randomBytes(6).toString("hex")}.json`);
|
|
91
91
|
try {
|
|
92
|
+
const args = ["detect", "-f", "json", "-r", tmpReport, "-s", dirPath];
|
|
93
|
+
if (!opts?.useGit) {
|
|
94
|
+
args.splice(1, 0, "--no-git");
|
|
95
|
+
}
|
|
92
96
|
// Run gitleaks detect on directory
|
|
93
|
-
await execFileAsync(gitleaksPath,
|
|
97
|
+
await execFileAsync(gitleaksPath, args, { timeout: 60000 });
|
|
94
98
|
// No leaks found
|
|
95
99
|
if (!fs.existsSync(tmpReport)) {
|
|
96
100
|
return [];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rafter
|
|
3
3
|
description: "Rafter — the security toolkit built for AI workflows. Three tiers: (1) fast local secret scanning, deterministic, no API key needed; (2) remote SAST/SCA with deterministic secret detection and dependency checks via API (fast mode, default); (3) agentic deep-dive analysis with additional passes (plus mode). Use when checking for vulnerabilities, leaked credentials, or whether code is safe to push. Also use before merging PRs, deploying, or shipping new features. If RAFTER_API_KEY is not set, local scanning works fully — don't block on it. Run `rafter brief commands` for full CLI reference."
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
allowed-tools: [Bash]
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: rafter-agent-security
|
|
3
3
|
description: "Rafter local security tools — deterministic secret scanning, command risk assessment, skill auditing, and audit log review. Use when: checking for leaked credentials or API keys, evaluating whether code is safe to push, auditing skills before installation, reviewing security events. Works offline, no API key needed. Run `rafter brief security` for full capabilities."
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
allowed-tools: [Bash, Read, Glob, Grep]
|
|
6
6
|
---
|
|
7
7
|
|