@rafter-security/cli 0.5.3 → 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 +2 -2
- package/dist/commands/agent/audit.js +96 -0
- package/dist/commands/agent/baseline.js +213 -0
- package/dist/commands/agent/exec.js +1 -1
- package/dist/commands/agent/index.js +4 -0
- package/dist/commands/agent/init.js +371 -29
- package/dist/commands/agent/install-hook.js +41 -47
- package/dist/commands/agent/scan.js +196 -23
- package/dist/commands/agent/status.js +65 -4
- package/dist/commands/agent/update-gitleaks.js +40 -0
- 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 +320 -110
- 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/audit-logger.js +41 -0
- package/dist/core/config-defaults.js +28 -0
- package/dist/core/config-manager.js +19 -2
- package/dist/core/pattern-engine.js +26 -1
- package/dist/core/risk-rules.js +5 -3
- 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 +59 -20
- 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 +60 -0
- package/resources/rafter-security-skill.md +7 -11
|
@@ -2,10 +2,36 @@ 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";
|
|
8
|
+
import os from "os";
|
|
7
9
|
import path from "path";
|
|
8
10
|
import { fmt } from "../../utils/formatter.js";
|
|
11
|
+
function loadBaselineEntries() {
|
|
12
|
+
const baselinePath = path.join(os.homedir(), ".rafter", "baseline.json");
|
|
13
|
+
if (!fs.existsSync(baselinePath))
|
|
14
|
+
return [];
|
|
15
|
+
try {
|
|
16
|
+
const data = JSON.parse(fs.readFileSync(baselinePath, "utf-8"));
|
|
17
|
+
return data.entries || [];
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function applyBaseline(results, entries) {
|
|
24
|
+
if (entries.length === 0)
|
|
25
|
+
return results;
|
|
26
|
+
return results
|
|
27
|
+
.map((r) => ({
|
|
28
|
+
...r,
|
|
29
|
+
matches: r.matches.filter((m) => !entries.some((e) => e.file === r.file &&
|
|
30
|
+
e.pattern === m.pattern.name &&
|
|
31
|
+
(e.line == null || e.line === (m.line ?? null)))),
|
|
32
|
+
}))
|
|
33
|
+
.filter((r) => r.matches.length > 0);
|
|
34
|
+
}
|
|
9
35
|
export function createScanCommand() {
|
|
10
36
|
return new Command("scan")
|
|
11
37
|
.description("Scan files or directories for secrets")
|
|
@@ -16,19 +42,42 @@ export function createScanCommand() {
|
|
|
16
42
|
.option("--staged", "Scan only git staged files")
|
|
17
43
|
.option("--diff <ref>", "Scan files changed since a git ref")
|
|
18
44
|
.option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
|
|
45
|
+
.option("--baseline", "Filter findings present in the saved baseline")
|
|
46
|
+
.option("--watch", "Watch for file changes and re-scan on change")
|
|
19
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
|
+
}
|
|
20
68
|
// Load policy-merged config for excludePaths/customPatterns
|
|
21
69
|
const manager = new ConfigManager();
|
|
22
70
|
const cfg = manager.loadWithPolicy();
|
|
23
71
|
const scanCfg = cfg.agent?.scan;
|
|
72
|
+
const baselineEntries = opts.baseline ? loadBaselineEntries() : [];
|
|
24
73
|
// Handle --diff flag
|
|
25
74
|
if (opts.diff) {
|
|
26
|
-
await scanDiffFiles(opts.diff, opts, scanCfg);
|
|
75
|
+
await scanDiffFiles(opts.diff, opts, scanCfg, baselineEntries);
|
|
27
76
|
return;
|
|
28
77
|
}
|
|
29
78
|
// Handle --staged flag
|
|
30
79
|
if (opts.staged) {
|
|
31
|
-
await scanStagedFiles(opts, scanCfg);
|
|
80
|
+
await scanStagedFiles(opts, scanCfg, baselineEntries);
|
|
32
81
|
return;
|
|
33
82
|
}
|
|
34
83
|
const resolvedPath = path.resolve(scanPath);
|
|
@@ -37,6 +86,11 @@ export function createScanCommand() {
|
|
|
37
86
|
console.error(`Error: Path not found: ${resolvedPath}`);
|
|
38
87
|
process.exit(2);
|
|
39
88
|
}
|
|
89
|
+
// Handle --watch flag
|
|
90
|
+
if (opts.watch) {
|
|
91
|
+
await watchAndScan(resolvedPath, opts, scanCfg);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
40
94
|
// Determine scan engine
|
|
41
95
|
const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
|
|
42
96
|
// Determine if path is file or directory
|
|
@@ -54,7 +108,7 @@ export function createScanCommand() {
|
|
|
54
108
|
}
|
|
55
109
|
results = await scanFile(resolvedPath, engine, scanCfg);
|
|
56
110
|
}
|
|
57
|
-
outputScanResults(results, opts);
|
|
111
|
+
outputScanResults(applyBaseline(results, baselineEntries), opts);
|
|
58
112
|
});
|
|
59
113
|
}
|
|
60
114
|
/**
|
|
@@ -89,13 +143,14 @@ function outputSarif(results) {
|
|
|
89
143
|
}
|
|
90
144
|
}
|
|
91
145
|
const sarif = {
|
|
92
|
-
$schema: "https://
|
|
146
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
93
147
|
version: "2.1.0",
|
|
94
148
|
runs: [
|
|
95
149
|
{
|
|
96
150
|
tool: {
|
|
97
151
|
driver: {
|
|
98
152
|
name: "rafter",
|
|
153
|
+
version: "0.5.7",
|
|
99
154
|
informationUri: "https://rafter.so",
|
|
100
155
|
rules: Array.from(rules.values()),
|
|
101
156
|
},
|
|
@@ -110,8 +165,12 @@ function outputSarif(results) {
|
|
|
110
165
|
/**
|
|
111
166
|
* Shared output logic for scan results
|
|
112
167
|
*/
|
|
113
|
-
function outputScanResults(results, opts, context) {
|
|
168
|
+
function outputScanResults(results, opts, context, exitOnFindings = true) {
|
|
114
169
|
const format = opts.format ?? (opts.json ? "json" : "text");
|
|
170
|
+
if (!["text", "json", "sarif"].includes(format)) {
|
|
171
|
+
console.error(`Invalid format: ${format}. Valid values: text, json, sarif`);
|
|
172
|
+
process.exit(2);
|
|
173
|
+
}
|
|
115
174
|
if (format === "sarif") {
|
|
116
175
|
outputSarif(results);
|
|
117
176
|
return;
|
|
@@ -127,14 +186,18 @@ function outputScanResults(results, opts, context) {
|
|
|
127
186
|
})),
|
|
128
187
|
}));
|
|
129
188
|
console.log(JSON.stringify(out, null, 2));
|
|
130
|
-
|
|
189
|
+
if (exitOnFindings)
|
|
190
|
+
process.exit(results.length > 0 ? 1 : 0);
|
|
191
|
+
return;
|
|
131
192
|
}
|
|
132
193
|
if (results.length === 0) {
|
|
133
194
|
if (!opts.quiet) {
|
|
134
195
|
const msg = context ? `No secrets detected in ${context}` : "No secrets detected";
|
|
135
196
|
console.log(`\n${fmt.success(msg)}\n`);
|
|
136
197
|
}
|
|
137
|
-
|
|
198
|
+
if (exitOnFindings)
|
|
199
|
+
process.exit(0);
|
|
200
|
+
return;
|
|
138
201
|
}
|
|
139
202
|
console.log(`\n${fmt.warning(`Found secrets in ${results.length} file(s):`)}\n`);
|
|
140
203
|
let totalMatches = 0;
|
|
@@ -155,34 +218,37 @@ function outputScanResults(results, opts, context) {
|
|
|
155
218
|
if (context === "staged files") {
|
|
156
219
|
console.log(`${fmt.error("Commit blocked. Remove secrets before committing.")}\n`);
|
|
157
220
|
}
|
|
158
|
-
else {
|
|
221
|
+
else if (exitOnFindings) {
|
|
159
222
|
console.log(`Run 'rafter agent audit' to see the security log.\n`);
|
|
160
223
|
}
|
|
161
|
-
|
|
224
|
+
if (exitOnFindings)
|
|
225
|
+
process.exit(1);
|
|
162
226
|
}
|
|
163
227
|
/**
|
|
164
228
|
* Scan files changed since a git ref
|
|
165
229
|
*/
|
|
166
|
-
async function scanDiffFiles(ref, opts, scanCfg) {
|
|
230
|
+
async function scanDiffFiles(ref, opts, scanCfg, baselineEntries = []) {
|
|
167
231
|
try {
|
|
168
232
|
const diffOutput = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACM", ref], {
|
|
169
233
|
encoding: "utf-8",
|
|
170
234
|
stdio: ["pipe", "pipe", "ignore"],
|
|
171
235
|
}).trim();
|
|
172
236
|
if (!diffOutput) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
process.exit(0);
|
|
237
|
+
outputScanResults([], opts, `files changed since ${ref}`);
|
|
238
|
+
return;
|
|
177
239
|
}
|
|
178
240
|
const changedFiles = diffOutput.split("\n").map(f => f.trim()).filter(f => f);
|
|
179
241
|
if (!opts.quiet) {
|
|
180
242
|
console.error(`Scanning ${changedFiles.length} file(s) changed since ${ref}...`);
|
|
181
243
|
}
|
|
244
|
+
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
245
|
+
encoding: "utf-8",
|
|
246
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
247
|
+
}).trim();
|
|
182
248
|
const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
|
|
183
249
|
const allResults = [];
|
|
184
250
|
for (const file of changedFiles) {
|
|
185
|
-
const filePath = path.resolve(file);
|
|
251
|
+
const filePath = path.resolve(repoRoot, file);
|
|
186
252
|
if (!fs.existsSync(filePath))
|
|
187
253
|
continue;
|
|
188
254
|
const stats = fs.statSync(filePath);
|
|
@@ -191,7 +257,7 @@ async function scanDiffFiles(ref, opts, scanCfg) {
|
|
|
191
257
|
const results = await scanFile(filePath, engine, scanCfg);
|
|
192
258
|
allResults.push(...results);
|
|
193
259
|
}
|
|
194
|
-
outputScanResults(allResults, opts, `files changed since ${ref}`);
|
|
260
|
+
outputScanResults(applyBaseline(allResults, baselineEntries), opts, `files changed since ${ref}`);
|
|
195
261
|
}
|
|
196
262
|
catch (error) {
|
|
197
263
|
if (error.status === 128) {
|
|
@@ -204,26 +270,28 @@ async function scanDiffFiles(ref, opts, scanCfg) {
|
|
|
204
270
|
/**
|
|
205
271
|
* Scan git staged files for secrets
|
|
206
272
|
*/
|
|
207
|
-
async function scanStagedFiles(opts, scanCfg) {
|
|
273
|
+
async function scanStagedFiles(opts, scanCfg, baselineEntries = []) {
|
|
208
274
|
try {
|
|
209
275
|
const stagedFilesOutput = execSync("git diff --cached --name-only --diff-filter=ACM", {
|
|
210
276
|
encoding: "utf-8",
|
|
211
277
|
stdio: ["pipe", "pipe", "ignore"]
|
|
212
278
|
}).trim();
|
|
213
279
|
if (!stagedFilesOutput) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
process.exit(0);
|
|
280
|
+
outputScanResults([], opts, "staged files");
|
|
281
|
+
return;
|
|
218
282
|
}
|
|
219
283
|
const stagedFiles = stagedFilesOutput.split("\n").map(f => f.trim()).filter(f => f);
|
|
220
284
|
if (!opts.quiet) {
|
|
221
285
|
console.error(`Scanning ${stagedFiles.length} staged file(s)...`);
|
|
222
286
|
}
|
|
287
|
+
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
288
|
+
encoding: "utf-8",
|
|
289
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
290
|
+
}).trim();
|
|
223
291
|
const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
|
|
224
292
|
const allResults = [];
|
|
225
293
|
for (const file of stagedFiles) {
|
|
226
|
-
const filePath = path.resolve(file);
|
|
294
|
+
const filePath = path.resolve(repoRoot, file);
|
|
227
295
|
if (!fs.existsSync(filePath))
|
|
228
296
|
continue;
|
|
229
297
|
const stats = fs.statSync(filePath);
|
|
@@ -232,7 +300,7 @@ async function scanStagedFiles(opts, scanCfg) {
|
|
|
232
300
|
const results = await scanFile(filePath, engine, scanCfg);
|
|
233
301
|
allResults.push(...results);
|
|
234
302
|
}
|
|
235
|
-
outputScanResults(allResults, opts, "staged files");
|
|
303
|
+
outputScanResults(applyBaseline(allResults, baselineEntries), opts, "staged files");
|
|
236
304
|
}
|
|
237
305
|
catch (error) {
|
|
238
306
|
if (error.status === 128) {
|
|
@@ -260,6 +328,10 @@ async function selectEngine(preference, quiet) {
|
|
|
260
328
|
}
|
|
261
329
|
return "gitleaks";
|
|
262
330
|
}
|
|
331
|
+
if (preference !== "auto") {
|
|
332
|
+
console.error(`Invalid engine: ${preference}. Valid values: auto, gitleaks, patterns`);
|
|
333
|
+
process.exit(2);
|
|
334
|
+
}
|
|
263
335
|
// Auto mode: try Gitleaks, fall back to patterns
|
|
264
336
|
const gitleaks = new GitleaksScanner();
|
|
265
337
|
const available = await gitleaks.isAvailable();
|
|
@@ -308,3 +380,104 @@ async function scanDirectory(dirPath, engine, scanCfg) {
|
|
|
308
380
|
return scanner.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
|
|
309
381
|
}
|
|
310
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)) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { BinaryManager, GITLEAKS_VERSION } from "../../utils/binary-manager.js";
|
|
3
|
+
import { fmt } from "../../utils/formatter.js";
|
|
4
|
+
export function createUpdateGitleaksCommand() {
|
|
5
|
+
return new Command("update-gitleaks")
|
|
6
|
+
.description("Update (or reinstall) the managed gitleaks binary")
|
|
7
|
+
.option("--version <version>", "Gitleaks version to install", GITLEAKS_VERSION)
|
|
8
|
+
.action(async (opts) => {
|
|
9
|
+
const bm = new BinaryManager();
|
|
10
|
+
if (!bm.isPlatformSupported()) {
|
|
11
|
+
const { platform, arch } = bm.getPlatformInfo();
|
|
12
|
+
console.error(fmt.error(`Gitleaks not available for ${platform}/${arch}`));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
// Show current version if installed
|
|
16
|
+
if (bm.isGitleaksInstalled()) {
|
|
17
|
+
const current = await bm.getGitleaksVersion();
|
|
18
|
+
console.log(fmt.info(`Current: ${current}`));
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
console.log(fmt.info("Gitleaks not currently installed (managed binary)"));
|
|
22
|
+
}
|
|
23
|
+
console.log(fmt.info(`Installing gitleaks v${opts.version}...`));
|
|
24
|
+
console.log();
|
|
25
|
+
try {
|
|
26
|
+
await bm.downloadGitleaks((msg) => console.log(` ${msg}`), opts.version);
|
|
27
|
+
console.log();
|
|
28
|
+
const installed = await bm.getGitleaksVersion();
|
|
29
|
+
console.log(fmt.success(`Gitleaks updated: ${installed}`));
|
|
30
|
+
console.log(fmt.info(` Binary: ${bm.getGitleaksPath()}`));
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
console.log();
|
|
34
|
+
console.error(fmt.error(`Update failed: ${e}`));
|
|
35
|
+
console.log(fmt.info("To fix: install gitleaks manually (https://github.com/gitleaks/gitleaks/releases) " +
|
|
36
|
+
"and ensure it is on PATH."));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -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) {
|