@rafter-security/cli 0.5.1 → 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 +1 -1
- package/dist/commands/agent/audit-skill.js +6 -0
- package/dist/commands/agent/index.js +4 -0
- package/dist/commands/agent/init.js +81 -32
- package/dist/commands/agent/install-hook.js +2 -1
- package/dist/commands/agent/scan.js +70 -5
- package/dist/commands/agent/status.js +115 -0
- package/dist/commands/agent/verify.js +117 -0
- package/dist/commands/completion.js +170 -0
- package/dist/commands/hook/index.js +2 -0
- package/dist/commands/hook/posttool.js +73 -0
- package/dist/core/config-manager.js +16 -0
- package/dist/core/custom-patterns.js +157 -0
- package/dist/core/risk-rules.js +6 -1
- package/dist/index.js +4 -1
- package/dist/scanners/regex-scanner.js +7 -11
- package/dist/utils/binary-manager.js +100 -7
- package/dist/utils/skill-manager.js +22 -9
- package/package.json +1 -1
- package/resources/rafter-security-skill.md +7 -0
package/README.md
CHANGED
|
@@ -566,7 +566,7 @@ Agent security settings are stored in `~/.rafter/config.json`. Key settings:
|
|
|
566
566
|
|
|
567
567
|
**File Locations:**
|
|
568
568
|
- Config: `~/.rafter/config.json`
|
|
569
|
-
- Audit log: `~/.rafter/audit.
|
|
569
|
+
- Audit log: `~/.rafter/audit.jsonl`
|
|
570
570
|
- Binaries: `~/.rafter/bin/`
|
|
571
571
|
- Patterns: `~/.rafter/patterns/`
|
|
572
572
|
|
|
@@ -48,6 +48,9 @@ async function auditSkill(skillPath, opts) {
|
|
|
48
48
|
rafterSkillInstalled
|
|
49
49
|
};
|
|
50
50
|
console.log(JSON.stringify(result, null, 2));
|
|
51
|
+
if (quickScan.secrets > 0 || quickScan.highRiskCommands.length > 0) {
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
51
54
|
return;
|
|
52
55
|
}
|
|
53
56
|
// Check if we can use OpenClaw
|
|
@@ -85,6 +88,9 @@ async function auditSkill(skillPath, opts) {
|
|
|
85
88
|
console.log("─".repeat(60));
|
|
86
89
|
}
|
|
87
90
|
console.log();
|
|
91
|
+
if (quickScan.secrets > 0 || quickScan.highRiskCommands.length > 0) {
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
88
94
|
}
|
|
89
95
|
async function runQuickScan(content) {
|
|
90
96
|
// 1. Scan for secrets
|
|
@@ -6,6 +6,8 @@ import { createConfigCommand } from "./config.js";
|
|
|
6
6
|
import { createExecCommand } from "./exec.js";
|
|
7
7
|
import { createAuditSkillCommand } from "./audit-skill.js";
|
|
8
8
|
import { createInstallHookCommand } from "./install-hook.js";
|
|
9
|
+
import { createVerifyCommand } from "./verify.js";
|
|
10
|
+
import { createStatusCommand } from "./status.js";
|
|
9
11
|
export function createAgentCommand() {
|
|
10
12
|
const agent = new Command("agent")
|
|
11
13
|
.description("Agent security features");
|
|
@@ -17,5 +19,7 @@ export function createAgentCommand() {
|
|
|
17
19
|
agent.addCommand(createAuditCommand());
|
|
18
20
|
agent.addCommand(createAuditSkillCommand());
|
|
19
21
|
agent.addCommand(createInstallHookCommand());
|
|
22
|
+
agent.addCommand(createVerifyCommand());
|
|
23
|
+
agent.addCommand(createStatusCommand());
|
|
20
24
|
return agent;
|
|
21
25
|
}
|
|
@@ -32,16 +32,25 @@ function installClaudeCodeHooks() {
|
|
|
32
32
|
settings.hooks = {};
|
|
33
33
|
if (!settings.hooks.PreToolUse)
|
|
34
34
|
settings.hooks.PreToolUse = [];
|
|
35
|
-
|
|
35
|
+
if (!settings.hooks.PostToolUse)
|
|
36
|
+
settings.hooks.PostToolUse = [];
|
|
37
|
+
const preHook = { type: "command", command: "rafter hook pretool" };
|
|
38
|
+
const postHook = { type: "command", command: "rafter hook posttool" };
|
|
36
39
|
// Remove any existing Rafter hooks to avoid duplicates
|
|
37
40
|
settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter((entry) => {
|
|
38
41
|
const hooks = entry.hooks || [];
|
|
39
42
|
return !hooks.some((h) => h.command === "rafter hook pretool");
|
|
40
43
|
});
|
|
44
|
+
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter((entry) => {
|
|
45
|
+
const hooks = entry.hooks || [];
|
|
46
|
+
return !hooks.some((h) => h.command === "rafter hook posttool");
|
|
47
|
+
});
|
|
41
48
|
// Add Rafter hooks
|
|
42
|
-
settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [
|
|
49
|
+
settings.hooks.PreToolUse.push({ matcher: "Bash", hooks: [preHook] }, { matcher: "Write|Edit", hooks: [preHook] });
|
|
50
|
+
settings.hooks.PostToolUse.push({ matcher: ".*", hooks: [postHook] });
|
|
43
51
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
|
|
44
52
|
console.log(fmt.success(`Installed PreToolUse hooks to ${settingsPath}`));
|
|
53
|
+
console.log(fmt.success(`Installed PostToolUse hooks to ${settingsPath}`));
|
|
45
54
|
}
|
|
46
55
|
async function installClaudeCodeSkills() {
|
|
47
56
|
const homeDir = os.homedir();
|
|
@@ -125,49 +134,89 @@ export function createInitCommand() {
|
|
|
125
134
|
}
|
|
126
135
|
manager.set("agent.riskLevel", opts.riskLevel);
|
|
127
136
|
console.log(fmt.success(`Set risk level: ${opts.riskLevel}`));
|
|
128
|
-
//
|
|
137
|
+
// Check / download Gitleaks binary (optional)
|
|
129
138
|
if (!opts.skipGitleaks) {
|
|
130
139
|
const binaryManager = new BinaryManager();
|
|
131
140
|
const platformInfo = binaryManager.getPlatformInfo();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
141
|
+
// Helper: show diagnostics for a failing binary (mirrors Python's agent init)
|
|
142
|
+
const showDiagnostics = async (binaryPath, verResult) => {
|
|
143
|
+
if (verResult.stderr) {
|
|
144
|
+
console.log(fmt.info(` stderr: ${verResult.stderr}`));
|
|
145
|
+
}
|
|
146
|
+
const diag = await binaryManager.collectBinaryDiagnostics(binaryPath);
|
|
147
|
+
if (diag) {
|
|
148
|
+
console.log(fmt.info("Diagnostics:"));
|
|
149
|
+
console.log(diag);
|
|
150
|
+
}
|
|
151
|
+
console.log(fmt.info("To fix: install gitleaks (https://github.com/gitleaks/gitleaks/releases) and ensure it is on PATH, then re-run 'rafter agent init'."));
|
|
152
|
+
console.log();
|
|
153
|
+
};
|
|
154
|
+
if (binaryManager.isGitleaksInstalled()) {
|
|
155
|
+
// Local binary exists — verify it actually works
|
|
156
|
+
const verResult = await binaryManager.verifyGitleaksVerbose();
|
|
157
|
+
if (verResult.ok) {
|
|
158
|
+
console.log(fmt.success(`Gitleaks already installed (${verResult.stdout})`));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
console.log(fmt.warning("Gitleaks binary found locally but failed to execute."));
|
|
162
|
+
console.log(fmt.info(` Binary: ${binaryManager.getGitleaksPath()}`));
|
|
163
|
+
await showDiagnostics(binaryManager.getGitleaksPath(), verResult);
|
|
164
|
+
}
|
|
139
165
|
}
|
|
140
166
|
else {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await binaryManager.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
167
|
+
// Not installed locally — check PATH (mirrors Python's shutil.which)
|
|
168
|
+
const pathBinary = binaryManager.findGitleaksOnPath();
|
|
169
|
+
if (pathBinary) {
|
|
170
|
+
const verResult = await binaryManager.verifyGitleaksVerbose(pathBinary);
|
|
171
|
+
if (verResult.ok) {
|
|
172
|
+
console.log(fmt.success(`Gitleaks available on PATH (${verResult.stdout})`));
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log(fmt.warning("Gitleaks found on PATH but failed to execute."));
|
|
176
|
+
console.log(fmt.info(` Binary: ${pathBinary}`));
|
|
177
|
+
await showDiagnostics(pathBinary, verResult);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (!platformInfo.supported) {
|
|
181
|
+
console.log(fmt.info(`Gitleaks not available for ${platformInfo.platform}/${platformInfo.arch}`));
|
|
182
|
+
console.log(fmt.success("Using pattern-based scanning (21 patterns)"));
|
|
148
183
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
console.log(fmt.success("Falling back to pattern-based scanning"));
|
|
184
|
+
else {
|
|
185
|
+
// Not on PATH, not installed locally — download
|
|
152
186
|
console.log();
|
|
187
|
+
console.log(fmt.info("Downloading Gitleaks (enhanced secret detection)..."));
|
|
188
|
+
try {
|
|
189
|
+
await binaryManager.downloadGitleaks((msg) => {
|
|
190
|
+
console.log(` ${msg}`);
|
|
191
|
+
});
|
|
192
|
+
console.log();
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
console.log();
|
|
196
|
+
console.log(fmt.error(`Gitleaks setup failed — pattern-based scanning will be used instead.`));
|
|
197
|
+
console.log(fmt.warning(String(e)));
|
|
198
|
+
console.log();
|
|
199
|
+
console.log(fmt.info("To fix: install gitleaks manually (https://github.com/gitleaks/gitleaks/releases) and ensure it is on PATH, then re-run 'rafter agent init'."));
|
|
200
|
+
console.log();
|
|
201
|
+
}
|
|
153
202
|
}
|
|
154
203
|
}
|
|
155
204
|
}
|
|
156
205
|
// Install OpenClaw skill if applicable
|
|
157
206
|
if (hasOpenClaw && !opts.skipOpenclaw) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
manager.set("agent.environments.openclaw.enabled", true);
|
|
164
|
-
}
|
|
165
|
-
else {
|
|
166
|
-
console.log(fmt.warning("Failed to install Rafter Security skill"));
|
|
167
|
-
}
|
|
207
|
+
const skillManager = new SkillManager();
|
|
208
|
+
const result = await skillManager.installRafterSkillVerbose();
|
|
209
|
+
if (result.ok) {
|
|
210
|
+
console.log(fmt.success("Installed Rafter Security skill to ~/.openclaw/skills/rafter-security.md"));
|
|
211
|
+
manager.set("agent.environments.openclaw.enabled", true);
|
|
168
212
|
}
|
|
169
|
-
|
|
170
|
-
console.
|
|
213
|
+
else {
|
|
214
|
+
console.log(fmt.error("Failed to install Rafter Security skill"));
|
|
215
|
+
console.log(fmt.warning(` Source: ${result.sourcePath}`));
|
|
216
|
+
console.log(fmt.warning(` Destination: ${result.destPath}`));
|
|
217
|
+
if (result.error) {
|
|
218
|
+
console.log(fmt.warning(` Error: ${result.error}`));
|
|
219
|
+
}
|
|
171
220
|
}
|
|
172
221
|
}
|
|
173
222
|
// Install Claude Code skills + hooks if applicable
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
3
4
|
import path from "path";
|
|
4
5
|
import { execSync } from "child_process";
|
|
5
6
|
import { fileURLToPath } from 'url';
|
|
@@ -81,7 +82,7 @@ async function installLocalHook() {
|
|
|
81
82
|
*/
|
|
82
83
|
async function installGlobalHook() {
|
|
83
84
|
// Create global hooks directory
|
|
84
|
-
const homeDir =
|
|
85
|
+
const homeDir = os.homedir();
|
|
85
86
|
if (!homeDir) {
|
|
86
87
|
console.error("❌ Error: Could not determine home directory");
|
|
87
88
|
process.exit(1);
|
|
@@ -12,6 +12,7 @@ export function createScanCommand() {
|
|
|
12
12
|
.argument("[path]", "File or directory to scan", ".")
|
|
13
13
|
.option("-q, --quiet", "Only output if secrets found")
|
|
14
14
|
.option("--json", "Output as JSON")
|
|
15
|
+
.option("--format <format>", "Output format: text, json, sarif", "text")
|
|
15
16
|
.option("--staged", "Scan only git staged files")
|
|
16
17
|
.option("--diff <ref>", "Scan files changed since a git ref")
|
|
17
18
|
.option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
|
|
@@ -34,7 +35,7 @@ export function createScanCommand() {
|
|
|
34
35
|
// Check if path exists
|
|
35
36
|
if (!fs.existsSync(resolvedPath)) {
|
|
36
37
|
console.error(`Error: Path not found: ${resolvedPath}`);
|
|
37
|
-
process.exit(
|
|
38
|
+
process.exit(2);
|
|
38
39
|
}
|
|
39
40
|
// Determine scan engine
|
|
40
41
|
const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
|
|
@@ -56,12 +57,76 @@ export function createScanCommand() {
|
|
|
56
57
|
outputScanResults(results, opts);
|
|
57
58
|
});
|
|
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
|
+
});
|
|
75
|
+
}
|
|
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
|
+
}
|
|
59
110
|
/**
|
|
60
111
|
* Shared output logic for scan results
|
|
61
112
|
*/
|
|
62
113
|
function outputScanResults(results, opts, context) {
|
|
63
|
-
|
|
64
|
-
|
|
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));
|
|
65
130
|
process.exit(results.length > 0 ? 1 : 0);
|
|
66
131
|
}
|
|
67
132
|
if (results.length === 0) {
|
|
@@ -131,7 +196,7 @@ async function scanDiffFiles(ref, opts, scanCfg) {
|
|
|
131
196
|
catch (error) {
|
|
132
197
|
if (error.status === 128) {
|
|
133
198
|
console.error("Error: Not in a git repository or invalid ref");
|
|
134
|
-
process.exit(
|
|
199
|
+
process.exit(2);
|
|
135
200
|
}
|
|
136
201
|
throw error;
|
|
137
202
|
}
|
|
@@ -172,7 +237,7 @@ async function scanStagedFiles(opts, scanCfg) {
|
|
|
172
237
|
catch (error) {
|
|
173
238
|
if (error.status === 128) {
|
|
174
239
|
console.error("Error: Not in a git repository");
|
|
175
|
-
process.exit(
|
|
240
|
+
process.exit(2);
|
|
176
241
|
}
|
|
177
242
|
throw error;
|
|
178
243
|
}
|
|
@@ -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
|
+
}
|