@rafter-security/cli 0.5.5 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/dist/commands/agent/audit-skill.js +1 -1
- package/dist/commands/agent/audit.js +106 -6
- package/dist/commands/agent/baseline.js +10 -0
- package/dist/commands/agent/exec.js +1 -1
- package/dist/commands/agent/init.js +366 -26
- package/dist/commands/agent/scan.js +160 -16
- package/dist/commands/agent/status.js +65 -4
- package/dist/commands/agent/verify.js +18 -4
- package/dist/commands/backend/run.js +76 -62
- package/dist/commands/ci/init.js +10 -3
- package/dist/commands/completion.js +21 -9
- package/dist/commands/hook/posttool.js +21 -7
- package/dist/commands/hook/pretool.js +50 -13
- package/dist/commands/issues/dedup.js +39 -0
- package/dist/commands/issues/from-scan.js +143 -0
- package/dist/commands/issues/from-text.js +185 -0
- package/dist/commands/issues/github-client.js +85 -0
- package/dist/commands/issues/index.js +25 -0
- package/dist/commands/issues/issue-builder.js +101 -0
- package/dist/commands/mcp/server.js +4 -1
- package/dist/commands/policy/export.js +7 -2
- package/dist/commands/scan/index.js +45 -0
- package/dist/core/audit-logger.js +106 -7
- package/dist/core/config-defaults.js +24 -0
- package/dist/core/config-manager.js +116 -3
- package/dist/core/custom-patterns.js +20 -17
- package/dist/core/pattern-engine.js +26 -1
- package/dist/core/policy-loader.js +25 -2
- package/dist/index.js +11 -2
- package/dist/scanners/gitleaks.js +8 -7
- package/dist/scanners/regex-scanner.js +16 -1
- package/dist/scanners/secret-patterns.js +6 -6
- package/dist/utils/api.js +18 -0
- package/dist/utils/binary-manager.js +74 -7
- package/dist/utils/skill-manager.js +5 -3
- package/package.json +5 -3
- package/resources/pre-commit-hook.sh +2 -2
- package/resources/pre-push-hook.sh +2 -2
- package/resources/rafter-security-skill.md +7 -11
|
@@ -136,10 +136,33 @@ function validatePolicy(policy, raw) {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
if (policy.scan.customPatterns !== undefined) {
|
|
139
|
-
if (!Array.isArray(policy.scan.customPatterns)
|
|
140
|
-
console.error(`Warning: "scan.custom_patterns" must be an array
|
|
139
|
+
if (!Array.isArray(policy.scan.customPatterns)) {
|
|
140
|
+
console.error(`Warning: "scan.custom_patterns" must be an array — ignoring.`);
|
|
141
141
|
delete policy.scan.customPatterns;
|
|
142
142
|
}
|
|
143
|
+
else {
|
|
144
|
+
const valid = [];
|
|
145
|
+
for (const v of policy.scan.customPatterns) {
|
|
146
|
+
if (!v || typeof v !== "object" || typeof v.name !== "string" || !v.name || typeof v.regex !== "string" || !v.regex || typeof v.severity !== "string") {
|
|
147
|
+
console.error(`Warning: skipping malformed custom_patterns entry — must have name, regex, severity.`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
new RegExp(v.regex);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
console.error(`Warning: skipping custom pattern "${v.name}" — invalid regex.`);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
valid.push(v);
|
|
158
|
+
}
|
|
159
|
+
if (valid.length > 0) {
|
|
160
|
+
policy.scan.customPatterns = valid;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
delete policy.scan.customPatterns;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
143
166
|
}
|
|
144
167
|
}
|
|
145
168
|
if (policy.audit) {
|
package/dist/index.js
CHANGED
|
@@ -4,20 +4,25 @@ import * as dotenv from "dotenv";
|
|
|
4
4
|
import { createRunCommand } from "./commands/backend/run.js";
|
|
5
5
|
import { createGetCommand } from "./commands/backend/get.js";
|
|
6
6
|
import { createUsageCommand } from "./commands/backend/usage.js";
|
|
7
|
+
import { createScanGroupCommand } from "./commands/scan/index.js";
|
|
7
8
|
import { createAgentCommand } from "./commands/agent/index.js";
|
|
8
9
|
import { createCiCommand } from "./commands/ci/index.js";
|
|
9
10
|
import { createHookCommand } from "./commands/hook/index.js";
|
|
10
11
|
import { createMcpCommand } from "./commands/mcp/index.js";
|
|
11
12
|
import { createPolicyCommand } from "./commands/policy/index.js";
|
|
12
13
|
import { createCompletionCommand } from "./commands/completion.js";
|
|
14
|
+
import { createIssuesCommand } from "./commands/issues/index.js";
|
|
13
15
|
import { checkForUpdate } from "./utils/update-checker.js";
|
|
14
16
|
import { setAgentMode } from "./utils/formatter.js";
|
|
17
|
+
import { createRequire } from "module";
|
|
15
18
|
dotenv.config();
|
|
16
|
-
const
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const { version: VERSION } = require("../package.json");
|
|
17
21
|
const program = new Command()
|
|
18
22
|
.name("rafter")
|
|
19
23
|
.description("Rafter CLI")
|
|
20
24
|
.version(VERSION)
|
|
25
|
+
.enablePositionalOptions()
|
|
21
26
|
.option("-a, --agent", "Plain output for AI agents (no colors/emoji)");
|
|
22
27
|
// Set agent mode before any subcommand runs
|
|
23
28
|
program.hook("preAction", (thisCommand) => {
|
|
@@ -26,10 +31,12 @@ program.hook("preAction", (thisCommand) => {
|
|
|
26
31
|
setAgentMode(true);
|
|
27
32
|
}
|
|
28
33
|
});
|
|
29
|
-
// Backend commands
|
|
34
|
+
// Backend commands
|
|
30
35
|
program.addCommand(createRunCommand());
|
|
31
36
|
program.addCommand(createGetCommand());
|
|
32
37
|
program.addCommand(createUsageCommand());
|
|
38
|
+
// Scan command group (default: remote backend scan; subcommands: local, remote)
|
|
39
|
+
program.addCommand(createScanGroupCommand());
|
|
33
40
|
// Agent commands
|
|
34
41
|
program.addCommand(createAgentCommand());
|
|
35
42
|
// CI commands
|
|
@@ -40,6 +47,8 @@ program.addCommand(createHookCommand());
|
|
|
40
47
|
program.addCommand(createMcpCommand());
|
|
41
48
|
// Policy commands
|
|
42
49
|
program.addCommand(createPolicyCommand());
|
|
50
|
+
// GitHub Issues integration
|
|
51
|
+
program.addCommand(createIssuesCommand());
|
|
43
52
|
// Shell completions
|
|
44
53
|
program.addCommand(createCompletionCommand());
|
|
45
54
|
// Non-blocking update check — runs after command, prints to stderr
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
|
+
import { randomBytes } from "crypto";
|
|
3
4
|
import { BinaryManager } from "../utils/binary-manager.js";
|
|
4
5
|
import fs from "fs";
|
|
5
6
|
import os from "os";
|
|
@@ -26,7 +27,7 @@ export class GitleaksScanner {
|
|
|
26
27
|
throw new Error("Gitleaks not available");
|
|
27
28
|
}
|
|
28
29
|
const gitleaksPath = this.binaryManager.getGitleaksPath();
|
|
29
|
-
const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${
|
|
30
|
+
const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${randomBytes(6).toString("hex")}.json`);
|
|
30
31
|
try {
|
|
31
32
|
// Run gitleaks detect on file
|
|
32
33
|
await execFileAsync(gitleaksPath, ["detect", "--no-git", "-f", "json", "-r", tmpReport, "-s", filePath], { timeout: 30000 });
|
|
@@ -44,11 +45,7 @@ export class GitleaksScanner {
|
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
catch (e) {
|
|
47
|
-
//
|
|
48
|
-
if (fs.existsSync(tmpReport)) {
|
|
49
|
-
fs.unlinkSync(tmpReport);
|
|
50
|
-
}
|
|
51
|
-
// Gitleaks exits with code 1 when leaks found
|
|
48
|
+
// Gitleaks exits with code 1 when leaks found — read report before cleanup
|
|
52
49
|
if (e.code === 1 && fs.existsSync(tmpReport)) {
|
|
53
50
|
const results = this.parseResults(tmpReport);
|
|
54
51
|
fs.unlinkSync(tmpReport);
|
|
@@ -57,6 +54,10 @@ export class GitleaksScanner {
|
|
|
57
54
|
matches: results.map(r => this.convertToPatternMatch(r))
|
|
58
55
|
};
|
|
59
56
|
}
|
|
57
|
+
// Clean up report for non-leak errors
|
|
58
|
+
if (fs.existsSync(tmpReport)) {
|
|
59
|
+
fs.unlinkSync(tmpReport);
|
|
60
|
+
}
|
|
60
61
|
throw new Error(`Gitleaks scan failed: ${e.message}`);
|
|
61
62
|
}
|
|
62
63
|
}
|
|
@@ -86,7 +87,7 @@ export class GitleaksScanner {
|
|
|
86
87
|
throw new Error("Gitleaks not available");
|
|
87
88
|
}
|
|
88
89
|
const gitleaksPath = this.binaryManager.getGitleaksPath();
|
|
89
|
-
const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${
|
|
90
|
+
const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${randomBytes(6).toString("hex")}.json`);
|
|
90
91
|
try {
|
|
91
92
|
// Run gitleaks detect on directory
|
|
92
93
|
await execFileAsync(gitleaksPath, ["detect", "--no-git", "-f", "json", "-r", tmpReport, "-s", dirPath], { timeout: 60000 });
|
|
@@ -57,7 +57,18 @@ export class RegexScanner {
|
|
|
57
57
|
".next",
|
|
58
58
|
"coverage",
|
|
59
59
|
".vscode",
|
|
60
|
-
".idea"
|
|
60
|
+
".idea",
|
|
61
|
+
// Vendored / virtual-env / generated dirs that cause false positives
|
|
62
|
+
"vendor",
|
|
63
|
+
".venv",
|
|
64
|
+
"venv",
|
|
65
|
+
"__pycache__",
|
|
66
|
+
".tox",
|
|
67
|
+
".mypy_cache",
|
|
68
|
+
".pytest_cache",
|
|
69
|
+
"results",
|
|
70
|
+
".terraform",
|
|
71
|
+
"bower_components"
|
|
61
72
|
];
|
|
62
73
|
// Merge policy excludePaths into the exclude list
|
|
63
74
|
if (options?.excludePaths) {
|
|
@@ -101,6 +112,10 @@ export class RegexScanner {
|
|
|
101
112
|
try {
|
|
102
113
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
103
114
|
for (const entry of entries) {
|
|
115
|
+
// Skip symlinks to prevent traversal outside intended scope
|
|
116
|
+
if (entry.isSymbolicLink()) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
104
119
|
const fullPath = path.join(dir, entry.name);
|
|
105
120
|
// Skip excluded directories
|
|
106
121
|
if (exclude.includes(entry.name)) {
|
|
@@ -90,13 +90,13 @@ export const DEFAULT_SECRET_PATTERNS = [
|
|
|
90
90
|
// Generic patterns
|
|
91
91
|
{
|
|
92
92
|
name: "Generic API Key",
|
|
93
|
-
regex: "(?i)(api[_-]?key|apikey)[\\s]*[:=][\\s]*['\"]
|
|
93
|
+
regex: "(?i)(?<![a-zA-Z0-9_])(api[_-]?key|apikey)[\\s]*[:=][\\s]*['\"](?=[0-9a-zA-Z\\-_]*[0-9])[0-9a-zA-Z\\-_]{16,256}['\"]",
|
|
94
94
|
severity: "high",
|
|
95
95
|
description: "Generic API key pattern detected"
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
name: "Generic Secret",
|
|
99
|
-
regex: "(?i)(secret|password|passwd|pwd)[\\s]*[:=][\\s]*['\"]
|
|
99
|
+
regex: "(?i)(?<![a-zA-Z0-9_])(secret|password|passwd|pwd)[\\s]*[:=][\\s]*['\"](?=[^\\s'\"]*[0-9])(?=[^\\s'\"]*[a-zA-Z])[0-9a-zA-Z\\-_!@#$%^&*()]{12,256}['\"]",
|
|
100
100
|
severity: "high",
|
|
101
101
|
description: "Generic secret pattern detected"
|
|
102
102
|
},
|
|
@@ -108,21 +108,21 @@ export const DEFAULT_SECRET_PATTERNS = [
|
|
|
108
108
|
},
|
|
109
109
|
{
|
|
110
110
|
name: "Bearer Token",
|
|
111
|
-
regex: "(?i)bearer[\\s]+[a-zA-Z0-9\\-_\\.=]
|
|
111
|
+
regex: "(?i)bearer[\\s]+(?=[a-zA-Z0-9\\-_\\.=]*[0-9])(?=[a-zA-Z0-9\\-_\\.=]*[a-zA-Z])[a-zA-Z0-9\\-_\\.=]{20,512}",
|
|
112
112
|
severity: "high",
|
|
113
113
|
description: "Bearer token detected"
|
|
114
114
|
},
|
|
115
115
|
// Database connection strings
|
|
116
116
|
{
|
|
117
117
|
name: "Database Connection String",
|
|
118
|
-
regex: "(?i)(postgres|mysql|mongodb)://[^\\s]+:[^\\s]+@[^\\s]+",
|
|
118
|
+
regex: "(?i)(postgres|mysql|mongodb)://[^\\s:@]+:[^\\s@]+@[^\\s]+",
|
|
119
119
|
severity: "critical",
|
|
120
120
|
description: "Database connection string with credentials detected"
|
|
121
121
|
},
|
|
122
122
|
// JWT
|
|
123
123
|
{
|
|
124
124
|
name: "JSON Web Token",
|
|
125
|
-
regex: "eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}",
|
|
125
|
+
regex: "eyJ[A-Za-z0-9_-]{10,2048}\\.[A-Za-z0-9_-]{10,2048}\\.[A-Za-z0-9_-]{10,2048}",
|
|
126
126
|
severity: "high",
|
|
127
127
|
description: "JWT token detected"
|
|
128
128
|
},
|
|
@@ -136,7 +136,7 @@ export const DEFAULT_SECRET_PATTERNS = [
|
|
|
136
136
|
// PyPI token
|
|
137
137
|
{
|
|
138
138
|
name: "PyPI Token",
|
|
139
|
-
regex: "pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\\-_]{50,}",
|
|
139
|
+
regex: "pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\\-_]{50,1024}",
|
|
140
140
|
severity: "critical",
|
|
141
141
|
description: "PyPI API token detected"
|
|
142
142
|
}
|
package/dist/utils/api.js
CHANGED
|
@@ -4,6 +4,24 @@ export const EXIT_SUCCESS = 0;
|
|
|
4
4
|
export const EXIT_GENERAL_ERROR = 1;
|
|
5
5
|
export const EXIT_SCAN_NOT_FOUND = 2;
|
|
6
6
|
export const EXIT_QUOTA_EXHAUSTED = 3;
|
|
7
|
+
export const EXIT_INSUFFICIENT_SCOPE = 4;
|
|
8
|
+
/**
|
|
9
|
+
* Detect a 403 scope-enforcement error from the API and print a helpful message.
|
|
10
|
+
* Returns true if the error was a scope error (caller should exit), false otherwise.
|
|
11
|
+
*/
|
|
12
|
+
export function handleScopeError(e) {
|
|
13
|
+
if (!e || e.response?.status !== 403)
|
|
14
|
+
return false;
|
|
15
|
+
const body = e.response?.data;
|
|
16
|
+
const msg = typeof body === "string" ? body : body?.error ?? "";
|
|
17
|
+
if (msg.includes("scope")) {
|
|
18
|
+
console.error('Error: This API key only has read access.\nTo trigger scans, create a key with "Read & Scan" scope at https://rfrr.co/account');
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
console.error(`Error: Forbidden (403) — ${msg || "access denied"}`);
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
7
25
|
export function resolveKey(cliKey) {
|
|
8
26
|
if (cliKey)
|
|
9
27
|
return cliKey;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
|
+
import crypto from "crypto";
|
|
4
5
|
import https from "https";
|
|
5
6
|
import { exec, execSync } from "child_process";
|
|
6
7
|
import { promisify } from "util";
|
|
@@ -75,10 +76,10 @@ export class BinaryManager {
|
|
|
75
76
|
return false;
|
|
76
77
|
}
|
|
77
78
|
try {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
});
|
|
81
|
-
return
|
|
79
|
+
// execAsync rejects on non-zero exit, so reaching here means exit code 0.
|
|
80
|
+
// Accept any successful exit — don't require specific stdout content.
|
|
81
|
+
await execAsync(`"${this.getGitleaksPath()}" version`, { timeout: 5000 });
|
|
82
|
+
return true;
|
|
82
83
|
}
|
|
83
84
|
catch {
|
|
84
85
|
return false;
|
|
@@ -91,8 +92,9 @@ export class BinaryManager {
|
|
|
91
92
|
const gitleaksPath = binaryPath ?? this.getGitleaksPath();
|
|
92
93
|
try {
|
|
93
94
|
const { stdout, stderr } = await execAsync(`"${gitleaksPath}" version`, { timeout: 5000 });
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
// execAsync rejects on non-zero exit, so reaching here means exit code 0.
|
|
96
|
+
// Accept any successful exit — don't require specific stdout content.
|
|
97
|
+
return { ok: true, stdout: stdout.trim(), stderr: stderr.trim() };
|
|
96
98
|
}
|
|
97
99
|
catch (e) {
|
|
98
100
|
const err = e;
|
|
@@ -172,6 +174,10 @@ export class BinaryManager {
|
|
|
172
174
|
// Log downloaded file size as basic integrity signal
|
|
173
175
|
const stats = fs.statSync(archivePath);
|
|
174
176
|
log(` Downloaded: ${(stats.size / 1024).toFixed(1)} KB`);
|
|
177
|
+
// Verify SHA256 checksum against official checksums file
|
|
178
|
+
log("Verifying checksum...");
|
|
179
|
+
await this.verifyChecksum(archivePath, platform, arch, version, log);
|
|
180
|
+
log(" ✓ Checksum verified");
|
|
175
181
|
// Extract binary
|
|
176
182
|
log("Extracting binary...");
|
|
177
183
|
if (platform === "windows") {
|
|
@@ -321,6 +327,64 @@ export class BinaryManager {
|
|
|
321
327
|
});
|
|
322
328
|
});
|
|
323
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Verify downloaded archive checksum against official gitleaks checksums file.
|
|
332
|
+
*/
|
|
333
|
+
async verifyChecksum(archivePath, platform, arch, version, onProgress) {
|
|
334
|
+
const checksumsUrl = `https://github.com/gitleaks/gitleaks/releases/download/v${version}/gitleaks_${version}_checksums.txt`;
|
|
335
|
+
const checksumsPath = path.join(this.binDir, "checksums.txt");
|
|
336
|
+
try {
|
|
337
|
+
await this.downloadFile(checksumsUrl, checksumsPath, () => { });
|
|
338
|
+
const checksumsContent = fs.readFileSync(checksumsPath, "utf-8");
|
|
339
|
+
const archiveFilename = platform === "windows"
|
|
340
|
+
? `gitleaks_${version}_windows_${arch}.zip`
|
|
341
|
+
: `gitleaks_${version}_${platform}_${arch}.tar.gz`;
|
|
342
|
+
const expectedHash = this.parseChecksumFile(checksumsContent, archiveFilename);
|
|
343
|
+
if (!expectedHash) {
|
|
344
|
+
throw new Error(`Checksum not found for ${archiveFilename} in checksums file`);
|
|
345
|
+
}
|
|
346
|
+
const actualHash = await this.computeSHA256(archivePath);
|
|
347
|
+
if (actualHash !== expectedHash) {
|
|
348
|
+
throw new Error(`Checksum mismatch for ${archiveFilename}:\n` +
|
|
349
|
+
` Expected: ${expectedHash}\n` +
|
|
350
|
+
` Actual: ${actualHash}\n` +
|
|
351
|
+
`The downloaded file may be corrupted or tampered with.`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
finally {
|
|
355
|
+
if (fs.existsSync(checksumsPath)) {
|
|
356
|
+
fs.unlinkSync(checksumsPath);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Parse a checksums.txt file and return the SHA256 hash for the given filename.
|
|
362
|
+
*/
|
|
363
|
+
parseChecksumFile(content, filename) {
|
|
364
|
+
for (const line of content.split("\n")) {
|
|
365
|
+
const trimmed = line.trim();
|
|
366
|
+
if (!trimmed)
|
|
367
|
+
continue;
|
|
368
|
+
// Format: "<sha256> <filename>" (two spaces between hash and filename)
|
|
369
|
+
const parts = trimmed.split(/\s+/);
|
|
370
|
+
if (parts.length >= 2 && parts[1] === filename) {
|
|
371
|
+
return parts[0].toLowerCase();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Compute SHA256 hash of a file.
|
|
378
|
+
*/
|
|
379
|
+
computeSHA256(filePath) {
|
|
380
|
+
return new Promise((resolve, reject) => {
|
|
381
|
+
const hash = crypto.createHash("sha256");
|
|
382
|
+
const stream = fs.createReadStream(filePath);
|
|
383
|
+
stream.on("data", (data) => hash.update(data));
|
|
384
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
385
|
+
stream.on("error", reject);
|
|
386
|
+
});
|
|
387
|
+
}
|
|
324
388
|
/**
|
|
325
389
|
* Extract zip (Windows) — uses PowerShell's Expand-Archive, then copies
|
|
326
390
|
* only the gitleaks.exe binary to binDir. Cleans up the temp extract dir.
|
|
@@ -329,7 +393,10 @@ export class BinaryManager {
|
|
|
329
393
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "rafter-gitleaks-"));
|
|
330
394
|
try {
|
|
331
395
|
// PowerShell 5+ ships on all supported Windows versions
|
|
332
|
-
|
|
396
|
+
// Escape single quotes to prevent shell injection ('' is the PS escape for ')
|
|
397
|
+
const safeZipPath = zipPath.replace(/'/g, "''");
|
|
398
|
+
const safeTempDir = tempDir.replace(/'/g, "''");
|
|
399
|
+
await execAsync(`powershell -NoProfile -Command "Expand-Archive -Force -LiteralPath '${safeZipPath}' -DestinationPath '${safeTempDir}'"`, { timeout: 30000 });
|
|
333
400
|
// Find gitleaks.exe — may be at root or inside a subdirectory
|
|
334
401
|
const findBinary = (dir) => {
|
|
335
402
|
for (const entry of fs.readdirSync(dir)) {
|
|
@@ -156,15 +156,17 @@ export class SkillManager {
|
|
|
156
156
|
async installRafterSkillVerbose(force = false) {
|
|
157
157
|
const skillPath = this.getRafterSkillPath();
|
|
158
158
|
const sourcePath = this.getRafterSkillSourcePath();
|
|
159
|
-
if (
|
|
160
|
-
|
|
159
|
+
// Check if ~/.openclaw exists (the parent dir), not just the skills subdir
|
|
160
|
+
const openclawDir = path.join(os.homedir(), ".openclaw");
|
|
161
|
+
if (!fs.existsSync(openclawDir)) {
|
|
162
|
+
return { ok: false, sourcePath, destPath: skillPath, error: `OpenClaw not found: ${openclawDir}` };
|
|
161
163
|
}
|
|
162
164
|
// Check if already installed and not forcing
|
|
163
165
|
if (!force && this.isRafterSkillInstalled()) {
|
|
164
166
|
return { ok: true, sourcePath, destPath: skillPath };
|
|
165
167
|
}
|
|
166
168
|
try {
|
|
167
|
-
// Ensure skills directory exists
|
|
169
|
+
// Ensure skills directory exists (may not exist on fresh OpenClaw installs)
|
|
168
170
|
const skillsDir = this.getOpenClawSkillsDir();
|
|
169
171
|
if (!fs.existsSync(skillsDir)) {
|
|
170
172
|
fs.mkdirSync(skillsDir, { recursive: true });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rafter-security/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"rafter": "./dist/index.js"
|
|
@@ -20,13 +20,15 @@
|
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
23
|
-
"axios": "^1.
|
|
23
|
+
"axios": "^1.13.5",
|
|
24
24
|
"chalk": "^5.3.0",
|
|
25
|
+
"chokidar": "^5.0.0",
|
|
25
26
|
"commander": "^11.1.0",
|
|
26
27
|
"dotenv": "^16.4.5",
|
|
27
28
|
"js-yaml": "^4.1.0",
|
|
29
|
+
"minimatch": "^10.2.4",
|
|
28
30
|
"ora": "^7.0.1",
|
|
29
|
-
"tar": "^7.5.
|
|
31
|
+
"tar": "^7.5.10"
|
|
30
32
|
},
|
|
31
33
|
"devDependencies": {
|
|
32
34
|
"@types/js-yaml": "^4.0.9",
|
|
@@ -27,14 +27,14 @@ fi
|
|
|
27
27
|
echo "🔍 Rafter: Scanning staged files for secrets..."
|
|
28
28
|
|
|
29
29
|
# Scan staged files
|
|
30
|
-
rafter
|
|
30
|
+
rafter scan local --staged --quiet
|
|
31
31
|
|
|
32
32
|
EXIT_CODE=$?
|
|
33
33
|
|
|
34
34
|
if [ $EXIT_CODE -ne 0 ]; then
|
|
35
35
|
echo -e "${RED}❌ Commit blocked: Secrets detected in staged files${NC}"
|
|
36
36
|
echo ""
|
|
37
|
-
echo " Run: rafter
|
|
37
|
+
echo " Run: rafter scan local --staged"
|
|
38
38
|
echo " To see details and remediate."
|
|
39
39
|
echo ""
|
|
40
40
|
echo " To bypass (NOT recommended): git commit --no-verify"
|
|
@@ -38,7 +38,7 @@ while read local_ref local_sha remote_ref remote_sha; do
|
|
|
38
38
|
|
|
39
39
|
echo "🔍 Rafter: Scanning commits being pushed ($local_ref)..."
|
|
40
40
|
|
|
41
|
-
rafter
|
|
41
|
+
rafter scan local --diff "$ref_arg" --quiet
|
|
42
42
|
EXIT_CODE=$?
|
|
43
43
|
|
|
44
44
|
if [ $EXIT_CODE -ne 0 ]; then
|
|
@@ -49,7 +49,7 @@ done
|
|
|
49
49
|
if [ $FOUND_SECRETS -ne 0 ]; then
|
|
50
50
|
echo -e "${RED}❌ Push blocked: Secrets detected in commits being pushed${NC}"
|
|
51
51
|
echo ""
|
|
52
|
-
echo " Run: rafter
|
|
52
|
+
echo " Run: rafter scan local --diff <remote-sha>"
|
|
53
53
|
echo " To see details and remediate."
|
|
54
54
|
echo ""
|
|
55
55
|
echo " To bypass (NOT recommended): git push --no-verify"
|
|
@@ -6,8 +6,8 @@ openclaw:
|
|
|
6
6
|
always: false
|
|
7
7
|
requires:
|
|
8
8
|
bins: [rafter]
|
|
9
|
-
version: 0.
|
|
10
|
-
last_updated: 2026-
|
|
9
|
+
version: 0.5.8
|
|
10
|
+
last_updated: 2026-03-04
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# Rafter Security
|
|
@@ -32,7 +32,7 @@ Rafter provides real-time security checks for agent operations:
|
|
|
32
32
|
Scan files for secrets before committing.
|
|
33
33
|
|
|
34
34
|
```bash
|
|
35
|
-
rafter
|
|
35
|
+
rafter scan local <path>
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
**When to use:**
|
|
@@ -57,21 +57,17 @@ rafter agent scan <path>
|
|
|
57
57
|
|
|
58
58
|
### /rafter-bash
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
Explicitly run a command through Rafter's security validator.
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
63
|
rafter agent exec <command>
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
**
|
|
67
|
-
- Blocks destructive commands (rm -rf /, fork bombs)
|
|
68
|
-
- Requires approval for dangerous operations
|
|
69
|
-
- Logs all command attempts
|
|
70
|
-
- Scans staged files before git commits
|
|
66
|
+
**When to use:** Only needed in environments where the `PreToolUse` hook is not installed. When `rafter agent init` has been run, all shell commands are validated automatically — you do not need to route commands through this.
|
|
71
67
|
|
|
72
68
|
**Risk levels:**
|
|
73
69
|
- **Critical** (blocked): rm -rf /, fork bombs, dd to /dev
|
|
74
|
-
- **High** (approval required): sudo rm, chmod 777, curl|bash
|
|
70
|
+
- **High** (approval required): sudo rm, chmod 777, curl | bash
|
|
75
71
|
- **Medium** (approval on moderate+): sudo, chmod, kill -9
|
|
76
72
|
- **Low** (allowed): npm install, git commit, ls
|
|
77
73
|
|
|
@@ -269,7 +265,7 @@ Configure with: `rafter agent config set agent.riskLevel moderate`
|
|
|
269
265
|
|
|
270
266
|
## Best Practices
|
|
271
267
|
|
|
272
|
-
1. **Always scan before commits**: Run `rafter
|
|
268
|
+
1. **Always scan before commits**: Run `rafter scan local` before `git commit`
|
|
273
269
|
2. **Audit untrusted skills**: Run `/rafter-audit-skill` on skills from unknown sources before installation
|
|
274
270
|
3. **Review audit logs**: Check `rafter agent audit` after suspicious activity
|
|
275
271
|
4. **Keep patterns updated**: Patterns updated automatically with CLI updates
|