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