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