@rafter-security/cli 0.4.2 → 0.5.3
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 +101 -1
- package/dist/commands/agent/audit-skill.js +6 -0
- package/dist/commands/agent/audit.js +15 -3
- package/dist/commands/agent/exec.js +9 -8
- package/dist/commands/agent/index.js +4 -0
- package/dist/commands/agent/init.js +132 -47
- package/dist/commands/agent/install-hook.js +2 -1
- package/dist/commands/agent/scan.js +180 -103
- package/dist/commands/agent/status.js +115 -0
- package/dist/commands/agent/verify.js +117 -0
- package/dist/commands/ci/index.js +8 -0
- package/dist/commands/ci/init.js +191 -0
- package/dist/commands/completion.js +170 -0
- package/dist/commands/hook/index.js +10 -0
- package/dist/commands/hook/posttool.js +73 -0
- package/dist/commands/hook/pretool.js +122 -0
- package/dist/commands/mcp/index.js +8 -0
- package/dist/commands/mcp/server.js +205 -0
- package/dist/commands/policy/export.js +81 -0
- package/dist/commands/policy/index.js +8 -0
- package/dist/core/audit-logger.js +2 -33
- package/dist/core/command-interceptor.js +6 -50
- package/dist/core/config-defaults.js +4 -15
- package/dist/core/config-manager.js +68 -0
- package/dist/core/custom-patterns.js +157 -0
- package/dist/core/policy-loader.js +167 -0
- package/dist/core/risk-rules.js +72 -0
- package/dist/index.js +26 -2
- package/dist/scanners/gitleaks.js +7 -6
- package/dist/scanners/regex-scanner.js +28 -12
- package/dist/utils/binary-manager.js +100 -7
- package/dist/utils/formatter.js +52 -0
- package/dist/utils/skill-manager.js +22 -9
- package/package.json +7 -3
- package/resources/pre-commit-hook.sh +45 -0
- package/resources/rafter-security-skill.md +323 -0
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { RegexScanner } from "../../scanners/regex-scanner.js";
|
|
3
3
|
import { GitleaksScanner } from "../../scanners/gitleaks.js";
|
|
4
|
-
import {
|
|
4
|
+
import { ConfigManager } from "../../core/config-manager.js";
|
|
5
|
+
import { execSync, execFileSync } from "child_process";
|
|
5
6
|
import fs from "fs";
|
|
6
7
|
import path from "path";
|
|
8
|
+
import { fmt } from "../../utils/formatter.js";
|
|
7
9
|
export function createScanCommand() {
|
|
8
10
|
return new Command("scan")
|
|
9
11
|
.description("Scan files or directories for secrets")
|
|
10
12
|
.argument("[path]", "File or directory to scan", ".")
|
|
11
13
|
.option("-q, --quiet", "Only output if secrets found")
|
|
12
14
|
.option("--json", "Output as JSON")
|
|
15
|
+
.option("--format <format>", "Output format: text, json, sarif", "text")
|
|
13
16
|
.option("--staged", "Scan only git staged files")
|
|
17
|
+
.option("--diff <ref>", "Scan files changed since a git ref")
|
|
14
18
|
.option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
|
|
15
19
|
.action(async (scanPath, opts) => {
|
|
20
|
+
// Load policy-merged config for excludePaths/customPatterns
|
|
21
|
+
const manager = new ConfigManager();
|
|
22
|
+
const cfg = manager.loadWithPolicy();
|
|
23
|
+
const scanCfg = cfg.agent?.scan;
|
|
24
|
+
// Handle --diff flag
|
|
25
|
+
if (opts.diff) {
|
|
26
|
+
await scanDiffFiles(opts.diff, opts, scanCfg);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
16
29
|
// Handle --staged flag
|
|
17
30
|
if (opts.staged) {
|
|
18
|
-
await scanStagedFiles(opts);
|
|
31
|
+
await scanStagedFiles(opts, scanCfg);
|
|
19
32
|
return;
|
|
20
33
|
}
|
|
21
34
|
const resolvedPath = path.resolve(scanPath);
|
|
22
35
|
// Check if path exists
|
|
23
36
|
if (!fs.existsSync(resolvedPath)) {
|
|
24
37
|
console.error(`Error: Path not found: ${resolvedPath}`);
|
|
25
|
-
process.exit(
|
|
38
|
+
process.exit(2);
|
|
26
39
|
}
|
|
27
40
|
// Determine scan engine
|
|
28
|
-
const engine = await selectEngine(opts.engine, opts.quiet);
|
|
41
|
+
const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
|
|
29
42
|
// Determine if path is file or directory
|
|
30
43
|
const stats = fs.statSync(resolvedPath);
|
|
31
44
|
let results;
|
|
@@ -33,61 +46,173 @@ export function createScanCommand() {
|
|
|
33
46
|
if (!opts.quiet) {
|
|
34
47
|
console.error(`Scanning directory: ${resolvedPath} (${engine})`);
|
|
35
48
|
}
|
|
36
|
-
results = await scanDirectory(resolvedPath, engine);
|
|
49
|
+
results = await scanDirectory(resolvedPath, engine, scanCfg);
|
|
37
50
|
}
|
|
38
51
|
else {
|
|
39
52
|
if (!opts.quiet) {
|
|
40
53
|
console.error(`Scanning file: ${resolvedPath} (${engine})`);
|
|
41
54
|
}
|
|
42
|
-
results = await scanFile(resolvedPath, engine);
|
|
43
|
-
}
|
|
44
|
-
// Output results
|
|
45
|
-
if (opts.json) {
|
|
46
|
-
console.log(JSON.stringify(results, null, 2));
|
|
55
|
+
results = await scanFile(resolvedPath, engine, scanCfg);
|
|
47
56
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
outputScanResults(results, opts);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Emit SARIF 2.1.0 JSON for GitHub/GitLab security tab integration
|
|
62
|
+
*/
|
|
63
|
+
function outputSarif(results) {
|
|
64
|
+
const rules = new Map();
|
|
65
|
+
const sarifResults = [];
|
|
66
|
+
for (const r of results) {
|
|
67
|
+
for (const m of r.matches) {
|
|
68
|
+
const ruleId = m.pattern.name.toLowerCase().replace(/\s+/g, "-");
|
|
69
|
+
if (!rules.has(ruleId)) {
|
|
70
|
+
rules.set(ruleId, {
|
|
71
|
+
id: ruleId,
|
|
72
|
+
name: m.pattern.name,
|
|
73
|
+
shortDescription: m.pattern.description || m.pattern.name,
|
|
74
|
+
});
|
|
54
75
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
sarifResults.push({
|
|
77
|
+
ruleId,
|
|
78
|
+
level: m.pattern.severity === "critical" || m.pattern.severity === "high" ? "error" : "warning",
|
|
79
|
+
message: { text: `${m.pattern.name} detected` },
|
|
80
|
+
locations: [
|
|
81
|
+
{
|
|
82
|
+
physicalLocation: {
|
|
83
|
+
artifactLocation: { uri: r.file.replace(/\\/g, "/"), uriBaseId: "%SRCROOT%" },
|
|
84
|
+
region: m.line ? { startLine: m.line, startColumn: m.column ?? 1 } : undefined,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const sarif = {
|
|
92
|
+
$schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
93
|
+
version: "2.1.0",
|
|
94
|
+
runs: [
|
|
95
|
+
{
|
|
96
|
+
tool: {
|
|
97
|
+
driver: {
|
|
98
|
+
name: "rafter",
|
|
99
|
+
informationUri: "https://rafter.so",
|
|
100
|
+
rules: Array.from(rules.values()),
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
results: sarifResults,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
console.log(JSON.stringify(sarif, null, 2));
|
|
108
|
+
process.exit(results.length > 0 ? 1 : 0);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Shared output logic for scan results
|
|
112
|
+
*/
|
|
113
|
+
function outputScanResults(results, opts, context) {
|
|
114
|
+
const format = opts.format ?? (opts.json ? "json" : "text");
|
|
115
|
+
if (format === "sarif") {
|
|
116
|
+
outputSarif(results);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (format === "json" || opts.json) {
|
|
120
|
+
const out = results.map((r) => ({
|
|
121
|
+
file: r.file,
|
|
122
|
+
matches: r.matches.map((m) => ({
|
|
123
|
+
pattern: { name: m.pattern.name, severity: m.pattern.severity, description: m.pattern.description || "" },
|
|
124
|
+
line: m.line ?? null,
|
|
125
|
+
column: m.column ?? null,
|
|
126
|
+
redacted: m.redacted || "",
|
|
127
|
+
})),
|
|
128
|
+
}));
|
|
129
|
+
console.log(JSON.stringify(out, null, 2));
|
|
130
|
+
process.exit(results.length > 0 ? 1 : 0);
|
|
131
|
+
}
|
|
132
|
+
if (results.length === 0) {
|
|
133
|
+
if (!opts.quiet) {
|
|
134
|
+
const msg = context ? `No secrets detected in ${context}` : "No secrets detected";
|
|
135
|
+
console.log(`\n${fmt.success(msg)}\n`);
|
|
136
|
+
}
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
console.log(`\n${fmt.warning(`Found secrets in ${results.length} file(s):`)}\n`);
|
|
140
|
+
let totalMatches = 0;
|
|
141
|
+
for (const result of results) {
|
|
142
|
+
console.log(`\n${fmt.info(result.file)}`);
|
|
143
|
+
for (const match of result.matches) {
|
|
144
|
+
totalMatches++;
|
|
145
|
+
const location = match.line ? `Line ${match.line}` : "Unknown location";
|
|
146
|
+
const sev = fmt.severity(match.pattern.severity);
|
|
147
|
+
console.log(` ${sev} ${match.pattern.name}`);
|
|
148
|
+
console.log(` Location: ${location}`);
|
|
149
|
+
console.log(` Pattern: ${match.pattern.description || match.pattern.regex}`);
|
|
150
|
+
console.log(` Redacted: ${match.redacted}`);
|
|
151
|
+
console.log();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
console.log(`\n${fmt.warning(`Total: ${totalMatches} secret(s) detected in ${results.length} file(s)`)}\n`);
|
|
155
|
+
if (context === "staged files") {
|
|
156
|
+
console.log(`${fmt.error("Commit blocked. Remove secrets before committing.")}\n`);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
console.log(`Run 'rafter agent audit' to see the security log.\n`);
|
|
160
|
+
}
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Scan files changed since a git ref
|
|
165
|
+
*/
|
|
166
|
+
async function scanDiffFiles(ref, opts, scanCfg) {
|
|
167
|
+
try {
|
|
168
|
+
const diffOutput = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACM", ref], {
|
|
169
|
+
encoding: "utf-8",
|
|
170
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
171
|
+
}).trim();
|
|
172
|
+
if (!diffOutput) {
|
|
173
|
+
if (!opts.quiet) {
|
|
174
|
+
console.log(fmt.success(`No files changed since ${ref}`));
|
|
74
175
|
}
|
|
176
|
+
process.exit(0);
|
|
75
177
|
}
|
|
76
|
-
|
|
178
|
+
const changedFiles = diffOutput.split("\n").map(f => f.trim()).filter(f => f);
|
|
179
|
+
if (!opts.quiet) {
|
|
180
|
+
console.error(`Scanning ${changedFiles.length} file(s) changed since ${ref}...`);
|
|
181
|
+
}
|
|
182
|
+
const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
|
|
183
|
+
const allResults = [];
|
|
184
|
+
for (const file of changedFiles) {
|
|
185
|
+
const filePath = path.resolve(file);
|
|
186
|
+
if (!fs.existsSync(filePath))
|
|
187
|
+
continue;
|
|
188
|
+
const stats = fs.statSync(filePath);
|
|
189
|
+
if (!stats.isFile())
|
|
190
|
+
continue;
|
|
191
|
+
const results = await scanFile(filePath, engine, scanCfg);
|
|
192
|
+
allResults.push(...results);
|
|
193
|
+
}
|
|
194
|
+
outputScanResults(allResults, opts, `files changed since ${ref}`);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error.status === 128) {
|
|
198
|
+
console.error("Error: Not in a git repository or invalid ref");
|
|
199
|
+
process.exit(2);
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
77
203
|
}
|
|
78
204
|
/**
|
|
79
205
|
* Scan git staged files for secrets
|
|
80
206
|
*/
|
|
81
|
-
async function scanStagedFiles(opts) {
|
|
207
|
+
async function scanStagedFiles(opts, scanCfg) {
|
|
82
208
|
try {
|
|
83
|
-
// Get list of staged files
|
|
84
209
|
const stagedFilesOutput = execSync("git diff --cached --name-only --diff-filter=ACM", {
|
|
85
210
|
encoding: "utf-8",
|
|
86
211
|
stdio: ["pipe", "pipe", "ignore"]
|
|
87
212
|
}).trim();
|
|
88
213
|
if (!stagedFilesOutput) {
|
|
89
214
|
if (!opts.quiet) {
|
|
90
|
-
console.log("
|
|
215
|
+
console.log(fmt.success("No files staged for commit"));
|
|
91
216
|
}
|
|
92
217
|
process.exit(0);
|
|
93
218
|
}
|
|
@@ -95,74 +220,28 @@ async function scanStagedFiles(opts) {
|
|
|
95
220
|
if (!opts.quiet) {
|
|
96
221
|
console.error(`Scanning ${stagedFiles.length} staged file(s)...`);
|
|
97
222
|
}
|
|
98
|
-
// Determine scan engine
|
|
99
223
|
const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
|
|
100
|
-
// Scan each staged file
|
|
101
224
|
const allResults = [];
|
|
102
225
|
for (const file of stagedFiles) {
|
|
103
226
|
const filePath = path.resolve(file);
|
|
104
|
-
|
|
105
|
-
if (!fs.existsSync(filePath)) {
|
|
227
|
+
if (!fs.existsSync(filePath))
|
|
106
228
|
continue;
|
|
107
|
-
}
|
|
108
|
-
// Skip if not a regular file
|
|
109
229
|
const stats = fs.statSync(filePath);
|
|
110
|
-
if (!stats.isFile())
|
|
230
|
+
if (!stats.isFile())
|
|
111
231
|
continue;
|
|
112
|
-
|
|
113
|
-
const results = await scanFile(filePath, engine);
|
|
232
|
+
const results = await scanFile(filePath, engine, scanCfg);
|
|
114
233
|
allResults.push(...results);
|
|
115
234
|
}
|
|
116
|
-
|
|
117
|
-
if (opts.json) {
|
|
118
|
-
console.log(JSON.stringify(allResults, null, 2));
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
if (allResults.length === 0) {
|
|
122
|
-
if (!opts.quiet) {
|
|
123
|
-
console.log("\n✓ No secrets detected in staged files\n");
|
|
124
|
-
}
|
|
125
|
-
process.exit(0);
|
|
126
|
-
}
|
|
127
|
-
else {
|
|
128
|
-
console.log(`\n⚠️ Found secrets in ${allResults.length} staged file(s):\n`);
|
|
129
|
-
let totalMatches = 0;
|
|
130
|
-
for (const result of allResults) {
|
|
131
|
-
console.log(`\n📄 ${result.file}`);
|
|
132
|
-
for (const match of result.matches) {
|
|
133
|
-
totalMatches++;
|
|
134
|
-
const location = match.line ? `Line ${match.line}` : "Unknown location";
|
|
135
|
-
const severity = getSeverityEmoji(match.pattern.severity);
|
|
136
|
-
console.log(` ${severity} [${match.pattern.severity.toUpperCase()}] ${match.pattern.name}`);
|
|
137
|
-
console.log(` Location: ${location}`);
|
|
138
|
-
console.log(` Pattern: ${match.pattern.description || match.pattern.regex}`);
|
|
139
|
-
console.log(` Redacted: ${match.redacted}`);
|
|
140
|
-
console.log();
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
console.log(`\n⚠️ Total: ${totalMatches} secret(s) detected in ${allResults.length} file(s)\n`);
|
|
144
|
-
console.log("❌ Commit blocked. Remove secrets before committing.\n");
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
235
|
+
outputScanResults(allResults, opts, "staged files");
|
|
148
236
|
}
|
|
149
237
|
catch (error) {
|
|
150
238
|
if (error.status === 128) {
|
|
151
239
|
console.error("Error: Not in a git repository");
|
|
152
|
-
process.exit(
|
|
240
|
+
process.exit(2);
|
|
153
241
|
}
|
|
154
242
|
throw error;
|
|
155
243
|
}
|
|
156
244
|
}
|
|
157
|
-
function getSeverityEmoji(severity) {
|
|
158
|
-
const emojiMap = {
|
|
159
|
-
critical: "🔴",
|
|
160
|
-
high: "🟠",
|
|
161
|
-
medium: "🟡",
|
|
162
|
-
low: "🟢"
|
|
163
|
-
};
|
|
164
|
-
return emojiMap[severity] || "⚪";
|
|
165
|
-
}
|
|
166
245
|
/**
|
|
167
246
|
* Select scan engine based on availability and user preference
|
|
168
247
|
*/
|
|
@@ -175,7 +254,7 @@ async function selectEngine(preference, quiet) {
|
|
|
175
254
|
const available = await gitleaks.isAvailable();
|
|
176
255
|
if (!available) {
|
|
177
256
|
if (!quiet) {
|
|
178
|
-
console.error("
|
|
257
|
+
console.error(fmt.warning("Gitleaks requested but not available, using patterns"));
|
|
179
258
|
}
|
|
180
259
|
return "patterns";
|
|
181
260
|
}
|
|
@@ -189,7 +268,7 @@ async function selectEngine(preference, quiet) {
|
|
|
189
268
|
/**
|
|
190
269
|
* Scan a file with selected engine
|
|
191
270
|
*/
|
|
192
|
-
async function scanFile(filePath, engine) {
|
|
271
|
+
async function scanFile(filePath, engine, scanCfg) {
|
|
193
272
|
if (engine === "gitleaks") {
|
|
194
273
|
try {
|
|
195
274
|
const gitleaks = new GitleaksScanner();
|
|
@@ -197,15 +276,14 @@ async function scanFile(filePath, engine) {
|
|
|
197
276
|
return result.matches.length > 0 ? [result] : [];
|
|
198
277
|
}
|
|
199
278
|
catch (e) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const scanner = new RegexScanner();
|
|
279
|
+
console.error(fmt.warning("Gitleaks scan failed, falling back to patterns"));
|
|
280
|
+
const scanner = new RegexScanner(scanCfg?.customPatterns);
|
|
203
281
|
const result = scanner.scanFile(filePath);
|
|
204
282
|
return result.matches.length > 0 ? [result] : [];
|
|
205
283
|
}
|
|
206
284
|
}
|
|
207
285
|
else {
|
|
208
|
-
const scanner = new RegexScanner();
|
|
286
|
+
const scanner = new RegexScanner(scanCfg?.customPatterns);
|
|
209
287
|
const result = scanner.scanFile(filePath);
|
|
210
288
|
return result.matches.length > 0 ? [result] : [];
|
|
211
289
|
}
|
|
@@ -213,21 +291,20 @@ async function scanFile(filePath, engine) {
|
|
|
213
291
|
/**
|
|
214
292
|
* Scan a directory with selected engine
|
|
215
293
|
*/
|
|
216
|
-
async function scanDirectory(dirPath, engine) {
|
|
294
|
+
async function scanDirectory(dirPath, engine, scanCfg) {
|
|
217
295
|
if (engine === "gitleaks") {
|
|
218
296
|
try {
|
|
219
297
|
const gitleaks = new GitleaksScanner();
|
|
220
298
|
return await gitleaks.scanDirectory(dirPath);
|
|
221
299
|
}
|
|
222
300
|
catch (e) {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return scanner.scanDirectory(dirPath);
|
|
301
|
+
console.error(fmt.warning("Gitleaks scan failed, falling back to patterns"));
|
|
302
|
+
const scanner = new RegexScanner(scanCfg?.customPatterns);
|
|
303
|
+
return scanner.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
|
|
227
304
|
}
|
|
228
305
|
}
|
|
229
306
|
else {
|
|
230
|
-
const scanner = new RegexScanner();
|
|
231
|
-
return scanner.scanDirectory(dirPath);
|
|
307
|
+
const scanner = new RegexScanner(scanCfg?.customPatterns);
|
|
308
|
+
return scanner.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
|
|
232
309
|
}
|
|
233
310
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
import { getRafterDir, getAuditLogPath, getBinDir } from "../../core/config-defaults.js";
|
|
7
|
+
import { AuditLogger } from "../../core/audit-logger.js";
|
|
8
|
+
import { ConfigManager } from "../../core/config-manager.js";
|
|
9
|
+
export function createStatusCommand() {
|
|
10
|
+
return new Command("status")
|
|
11
|
+
.description("Show agent security status dashboard")
|
|
12
|
+
.action(async () => {
|
|
13
|
+
const rafterDir = getRafterDir();
|
|
14
|
+
const auditPath = getAuditLogPath();
|
|
15
|
+
const home = os.homedir();
|
|
16
|
+
console.log("Rafter Agent Status");
|
|
17
|
+
console.log("=".repeat(50));
|
|
18
|
+
// --- Config ---
|
|
19
|
+
const configPath = path.join(rafterDir, "config.json");
|
|
20
|
+
if (fs.existsSync(configPath)) {
|
|
21
|
+
try {
|
|
22
|
+
const cfg = new ConfigManager().load();
|
|
23
|
+
console.log(`\nConfig: ${configPath}`);
|
|
24
|
+
console.log(`Risk level: ${cfg.agent?.riskLevel ?? "moderate"}`);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
console.log(`\nConfig: ${configPath} (parse error)`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(`\nConfig: not found — run: rafter agent init`);
|
|
32
|
+
}
|
|
33
|
+
// --- Gitleaks ---
|
|
34
|
+
const localGitleaks = path.join(getBinDir(), "gitleaks");
|
|
35
|
+
let gitleaksStatus = "not found — run: rafter agent init";
|
|
36
|
+
try {
|
|
37
|
+
const ver = execSync("gitleaks version", { timeout: 5000, encoding: "utf-8" }).trim();
|
|
38
|
+
gitleaksStatus = `${ver} (PATH)`;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
if (fs.existsSync(localGitleaks)) {
|
|
42
|
+
try {
|
|
43
|
+
const ver = execSync(`"${localGitleaks}" version`, { timeout: 5000, encoding: "utf-8" }).trim();
|
|
44
|
+
gitleaksStatus = `${ver} (local)`;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
gitleaksStatus = `${localGitleaks} (binary error)`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
console.log(`Gitleaks: ${gitleaksStatus}`);
|
|
52
|
+
// --- Claude Code hooks ---
|
|
53
|
+
const settingsPath = path.join(home, ".claude", "settings.json");
|
|
54
|
+
let pretoolOk = false;
|
|
55
|
+
let posttoolOk = false;
|
|
56
|
+
if (fs.existsSync(settingsPath)) {
|
|
57
|
+
try {
|
|
58
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
59
|
+
const hooks = settings.hooks ?? {};
|
|
60
|
+
for (const entry of hooks.PreToolUse ?? []) {
|
|
61
|
+
for (const h of entry.hooks ?? []) {
|
|
62
|
+
if (String(h.command ?? "").includes("rafter hook pretool"))
|
|
63
|
+
pretoolOk = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const entry of hooks.PostToolUse ?? []) {
|
|
67
|
+
for (const h of entry.hooks ?? []) {
|
|
68
|
+
if (String(h.command ?? "").includes("rafter hook posttool"))
|
|
69
|
+
posttoolOk = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// unreadable settings
|
|
75
|
+
}
|
|
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"}`);
|
|
79
|
+
// --- OpenClaw skill ---
|
|
80
|
+
const skillPath = path.join(home, ".openclaw", "skills", "rafter-security.md");
|
|
81
|
+
const openclawDir = path.join(home, ".openclaw");
|
|
82
|
+
if (fs.existsSync(skillPath)) {
|
|
83
|
+
console.log(`OpenClaw: skill installed (${skillPath})`);
|
|
84
|
+
}
|
|
85
|
+
else if (fs.existsSync(openclawDir)) {
|
|
86
|
+
console.log("OpenClaw: detected but skill missing — run: rafter agent init");
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
console.log("OpenClaw: not detected (optional)");
|
|
90
|
+
}
|
|
91
|
+
// --- Audit log summary ---
|
|
92
|
+
console.log(`\nAudit log: ${auditPath}`);
|
|
93
|
+
if (fs.existsSync(auditPath)) {
|
|
94
|
+
const logger = new AuditLogger();
|
|
95
|
+
const allEntries = logger.read();
|
|
96
|
+
const total = allEntries.length;
|
|
97
|
+
const secrets = allEntries.filter((e) => e.eventType === "secret_detected").length;
|
|
98
|
+
const blocked = allEntries.filter((e) => e.eventType === "command_intercepted" && e.resolution?.actionTaken === "blocked").length;
|
|
99
|
+
console.log(`Total events: ${total} | Secrets detected: ${secrets} | Commands blocked: ${blocked}`);
|
|
100
|
+
const recent = logger.read({ limit: 5 });
|
|
101
|
+
if (recent.length > 0) {
|
|
102
|
+
console.log("\nRecent events:");
|
|
103
|
+
for (const e of [...recent].reverse()) {
|
|
104
|
+
const ts = (e.timestamp ?? "").slice(0, 19).replace("T", " ");
|
|
105
|
+
const action = e.resolution?.actionTaken ?? "";
|
|
106
|
+
console.log(` ${ts} ${e.eventType} [${action}]`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
console.log("No events logged yet.");
|
|
112
|
+
}
|
|
113
|
+
console.log();
|
|
114
|
+
});
|
|
115
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { BinaryManager } from "../../utils/binary-manager.js";
|
|
3
|
+
import { SkillManager } from "../../utils/skill-manager.js";
|
|
4
|
+
import fs from "fs";
|
|
5
|
+
import path from "path";
|
|
6
|
+
import os from "os";
|
|
7
|
+
import { fmt } from "../../utils/formatter.js";
|
|
8
|
+
async function checkGitleaks() {
|
|
9
|
+
const binaryManager = new BinaryManager();
|
|
10
|
+
const name = "Gitleaks";
|
|
11
|
+
// Check PATH first (e.g. Homebrew), then fall back to ~/.rafter/bin
|
|
12
|
+
const pathBinary = binaryManager.findGitleaksOnPath();
|
|
13
|
+
const hasBinary = pathBinary !== null || binaryManager.isGitleaksInstalled();
|
|
14
|
+
if (!hasBinary) {
|
|
15
|
+
return { name, passed: false, detail: `Not found on PATH or at ${binaryManager.getGitleaksPath()}` };
|
|
16
|
+
}
|
|
17
|
+
const binaryPath = pathBinary ?? binaryManager.getGitleaksPath();
|
|
18
|
+
const { ok, stdout, stderr } = await binaryManager.verifyGitleaksVerbose(binaryPath);
|
|
19
|
+
if (!ok) {
|
|
20
|
+
const diag = await binaryManager.collectBinaryDiagnostics(binaryPath);
|
|
21
|
+
return { name, passed: false, detail: `Binary found at ${binaryPath} but failed to execute\n${stdout ? ` stdout: ${stdout}\n` : ""}${stderr ? ` stderr: ${stderr}\n` : ""}${diag}` };
|
|
22
|
+
}
|
|
23
|
+
return { name, passed: true, detail: `${stdout} (${binaryPath})` };
|
|
24
|
+
}
|
|
25
|
+
function checkConfig() {
|
|
26
|
+
const name = "Config";
|
|
27
|
+
const configPath = path.join(os.homedir(), ".rafter", "config.json");
|
|
28
|
+
if (!fs.existsSync(configPath)) {
|
|
29
|
+
return { name, passed: false, detail: `Not found: ${configPath}` };
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
33
|
+
JSON.parse(content);
|
|
34
|
+
return { name, passed: true, detail: configPath };
|
|
35
|
+
}
|
|
36
|
+
catch (e) {
|
|
37
|
+
return { name, passed: false, detail: `Invalid JSON: ${configPath} — ${e}` };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function checkClaudeCode() {
|
|
41
|
+
const name = "Claude Code";
|
|
42
|
+
const homeDir = os.homedir();
|
|
43
|
+
// optional: warn if absent but don't fail exit code
|
|
44
|
+
const claudeDir = path.join(homeDir, ".claude");
|
|
45
|
+
if (!fs.existsSync(claudeDir)) {
|
|
46
|
+
return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init --claude-code' to enable` };
|
|
47
|
+
}
|
|
48
|
+
const settingsPath = path.join(claudeDir, "settings.json");
|
|
49
|
+
if (!fs.existsSync(settingsPath)) {
|
|
50
|
+
return { name, passed: false, optional: true, detail: `Settings file not found: ${settingsPath}` };
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
54
|
+
const hooks = settings?.hooks?.PreToolUse || [];
|
|
55
|
+
const hasRafterHook = hooks.some((entry) => (entry.hooks || []).some((h) => h.command === "rafter hook pretool"));
|
|
56
|
+
if (!hasRafterHook) {
|
|
57
|
+
return { name, passed: false, optional: true, detail: "Rafter hooks not installed — run 'rafter agent init --claude-code'" };
|
|
58
|
+
}
|
|
59
|
+
return { name, passed: true, detail: "Hooks installed" };
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
return { name, passed: false, optional: true, detail: `Cannot read settings: ${e}` };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function checkOpenClaw() {
|
|
66
|
+
const name = "OpenClaw";
|
|
67
|
+
const skillManager = new SkillManager();
|
|
68
|
+
if (!skillManager.isOpenClawInstalled()) {
|
|
69
|
+
return { name, passed: false, optional: true, detail: `Not detected — run 'rafter agent init' to enable` };
|
|
70
|
+
}
|
|
71
|
+
if (!skillManager.isRafterSkillInstalled()) {
|
|
72
|
+
return { name, passed: false, optional: true, detail: `Rafter skill not installed — run 'rafter agent init'` };
|
|
73
|
+
}
|
|
74
|
+
const version = skillManager.getInstalledVersion();
|
|
75
|
+
return { name, passed: true, detail: `Rafter skill installed${version ? ` (v${version})` : ""}` };
|
|
76
|
+
}
|
|
77
|
+
export function createVerifyCommand() {
|
|
78
|
+
return new Command("verify")
|
|
79
|
+
.description("Check agent security integration status")
|
|
80
|
+
.action(async () => {
|
|
81
|
+
console.log(fmt.header("Rafter Agent Verify"));
|
|
82
|
+
console.log(fmt.divider());
|
|
83
|
+
console.log();
|
|
84
|
+
const results = [
|
|
85
|
+
checkConfig(),
|
|
86
|
+
await checkGitleaks(),
|
|
87
|
+
checkClaudeCode(),
|
|
88
|
+
checkOpenClaw(),
|
|
89
|
+
];
|
|
90
|
+
for (const r of results) {
|
|
91
|
+
if (r.passed) {
|
|
92
|
+
console.log(fmt.success(`${r.name}: ${r.detail}`));
|
|
93
|
+
}
|
|
94
|
+
else if (r.optional) {
|
|
95
|
+
console.log(fmt.warning(`${r.name}: ${r.detail}`));
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.log(fmt.error(`${r.name}: FAIL — ${r.detail}`));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
console.log();
|
|
102
|
+
const hardFailed = results.filter((r) => !r.passed && !r.optional);
|
|
103
|
+
const warned = results.filter((r) => !r.passed && r.optional);
|
|
104
|
+
const passed = results.filter((r) => r.passed);
|
|
105
|
+
if (hardFailed.length === 0) {
|
|
106
|
+
const warnNote = warned.length > 0 ? ` (${warned.length} optional check${warned.length > 1 ? "s" : ""} not configured)` : "";
|
|
107
|
+
console.log(fmt.success(`${passed.length}/${results.length} core checks passed${warnNote}`));
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
console.log(fmt.error(`${hardFailed.length} check${hardFailed.length > 1 ? "s" : ""} failed`));
|
|
111
|
+
}
|
|
112
|
+
console.log();
|
|
113
|
+
if (hardFailed.length > 0) {
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|