@rafter-security/cli 0.5.1 → 0.5.5
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 +7 -1
- package/dist/commands/agent/baseline.js +203 -0
- package/dist/commands/agent/index.js +8 -0
- package/dist/commands/agent/init.js +83 -32
- package/dist/commands/agent/install-hook.js +43 -48
- package/dist/commands/agent/scan.js +109 -12
- package/dist/commands/agent/status.js +115 -0
- package/dist/commands/agent/update-gitleaks.js +40 -0
- package/dist/commands/agent/verify.js +117 -0
- package/dist/commands/completion.js +368 -0
- package/dist/commands/hook/index.js +2 -0
- package/dist/commands/hook/posttool.js +73 -0
- package/dist/core/audit-logger.js +41 -0
- package/dist/core/config-defaults.js +4 -0
- package/dist/core/config-manager.js +16 -0
- package/dist/core/custom-patterns.js +157 -0
- package/dist/core/risk-rules.js +8 -1
- package/dist/index.js +4 -1
- package/dist/scanners/regex-scanner.js +7 -11
- package/dist/utils/binary-manager.js +150 -19
- package/dist/utils/skill-manager.js +22 -9
- package/package.json +1 -1
- package/resources/pre-push-hook.sh +60 -0
- package/resources/rafter-security-skill.md +7 -0
|
@@ -4,37 +4,65 @@ import { GitleaksScanner } from "../../scanners/gitleaks.js";
|
|
|
4
4
|
import { ConfigManager } from "../../core/config-manager.js";
|
|
5
5
|
import { execSync, execFileSync } from "child_process";
|
|
6
6
|
import fs from "fs";
|
|
7
|
+
import os from "os";
|
|
7
8
|
import path from "path";
|
|
8
9
|
import { fmt } from "../../utils/formatter.js";
|
|
10
|
+
function loadBaselineEntries() {
|
|
11
|
+
const baselinePath = path.join(os.homedir(), ".rafter", "baseline.json");
|
|
12
|
+
if (!fs.existsSync(baselinePath))
|
|
13
|
+
return [];
|
|
14
|
+
try {
|
|
15
|
+
const data = JSON.parse(fs.readFileSync(baselinePath, "utf-8"));
|
|
16
|
+
return data.entries || [];
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function applyBaseline(results, entries) {
|
|
23
|
+
if (entries.length === 0)
|
|
24
|
+
return results;
|
|
25
|
+
return results
|
|
26
|
+
.map((r) => ({
|
|
27
|
+
...r,
|
|
28
|
+
matches: r.matches.filter((m) => !entries.some((e) => e.file === r.file &&
|
|
29
|
+
e.pattern === m.pattern.name &&
|
|
30
|
+
(e.line == null || e.line === (m.line ?? null)))),
|
|
31
|
+
}))
|
|
32
|
+
.filter((r) => r.matches.length > 0);
|
|
33
|
+
}
|
|
9
34
|
export function createScanCommand() {
|
|
10
35
|
return new Command("scan")
|
|
11
36
|
.description("Scan files or directories for secrets")
|
|
12
37
|
.argument("[path]", "File or directory to scan", ".")
|
|
13
38
|
.option("-q, --quiet", "Only output if secrets found")
|
|
14
39
|
.option("--json", "Output as JSON")
|
|
40
|
+
.option("--format <format>", "Output format: text, json, sarif", "text")
|
|
15
41
|
.option("--staged", "Scan only git staged files")
|
|
16
42
|
.option("--diff <ref>", "Scan files changed since a git ref")
|
|
17
43
|
.option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
|
|
44
|
+
.option("--baseline", "Filter findings present in the saved baseline")
|
|
18
45
|
.action(async (scanPath, opts) => {
|
|
19
46
|
// Load policy-merged config for excludePaths/customPatterns
|
|
20
47
|
const manager = new ConfigManager();
|
|
21
48
|
const cfg = manager.loadWithPolicy();
|
|
22
49
|
const scanCfg = cfg.agent?.scan;
|
|
50
|
+
const baselineEntries = opts.baseline ? loadBaselineEntries() : [];
|
|
23
51
|
// Handle --diff flag
|
|
24
52
|
if (opts.diff) {
|
|
25
|
-
await scanDiffFiles(opts.diff, opts, scanCfg);
|
|
53
|
+
await scanDiffFiles(opts.diff, opts, scanCfg, baselineEntries);
|
|
26
54
|
return;
|
|
27
55
|
}
|
|
28
56
|
// Handle --staged flag
|
|
29
57
|
if (opts.staged) {
|
|
30
|
-
await scanStagedFiles(opts, scanCfg);
|
|
58
|
+
await scanStagedFiles(opts, scanCfg, baselineEntries);
|
|
31
59
|
return;
|
|
32
60
|
}
|
|
33
61
|
const resolvedPath = path.resolve(scanPath);
|
|
34
62
|
// Check if path exists
|
|
35
63
|
if (!fs.existsSync(resolvedPath)) {
|
|
36
64
|
console.error(`Error: Path not found: ${resolvedPath}`);
|
|
37
|
-
process.exit(
|
|
65
|
+
process.exit(2);
|
|
38
66
|
}
|
|
39
67
|
// Determine scan engine
|
|
40
68
|
const engine = await selectEngine(opts.engine || "auto", opts.quiet || false);
|
|
@@ -53,15 +81,84 @@ export function createScanCommand() {
|
|
|
53
81
|
}
|
|
54
82
|
results = await scanFile(resolvedPath, engine, scanCfg);
|
|
55
83
|
}
|
|
56
|
-
outputScanResults(results, opts);
|
|
84
|
+
outputScanResults(applyBaseline(results, baselineEntries), opts);
|
|
57
85
|
});
|
|
58
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Emit SARIF 2.1.0 JSON for GitHub/GitLab security tab integration
|
|
89
|
+
*/
|
|
90
|
+
function outputSarif(results) {
|
|
91
|
+
const rules = new Map();
|
|
92
|
+
const sarifResults = [];
|
|
93
|
+
for (const r of results) {
|
|
94
|
+
for (const m of r.matches) {
|
|
95
|
+
const ruleId = m.pattern.name.toLowerCase().replace(/\s+/g, "-");
|
|
96
|
+
if (!rules.has(ruleId)) {
|
|
97
|
+
rules.set(ruleId, {
|
|
98
|
+
id: ruleId,
|
|
99
|
+
name: m.pattern.name,
|
|
100
|
+
shortDescription: m.pattern.description || m.pattern.name,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
sarifResults.push({
|
|
104
|
+
ruleId,
|
|
105
|
+
level: m.pattern.severity === "critical" || m.pattern.severity === "high" ? "error" : "warning",
|
|
106
|
+
message: { text: `${m.pattern.name} detected` },
|
|
107
|
+
locations: [
|
|
108
|
+
{
|
|
109
|
+
physicalLocation: {
|
|
110
|
+
artifactLocation: { uri: r.file.replace(/\\/g, "/"), uriBaseId: "%SRCROOT%" },
|
|
111
|
+
region: m.line ? { startLine: m.line, startColumn: m.column ?? 1 } : undefined,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const sarif = {
|
|
119
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
120
|
+
version: "2.1.0",
|
|
121
|
+
runs: [
|
|
122
|
+
{
|
|
123
|
+
tool: {
|
|
124
|
+
driver: {
|
|
125
|
+
name: "rafter",
|
|
126
|
+
version: "0.5.5",
|
|
127
|
+
informationUri: "https://rafter.so",
|
|
128
|
+
rules: Array.from(rules.values()),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
results: sarifResults,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
};
|
|
135
|
+
console.log(JSON.stringify(sarif, null, 2));
|
|
136
|
+
process.exit(results.length > 0 ? 1 : 0);
|
|
137
|
+
}
|
|
59
138
|
/**
|
|
60
139
|
* Shared output logic for scan results
|
|
61
140
|
*/
|
|
62
141
|
function outputScanResults(results, opts, context) {
|
|
63
|
-
|
|
64
|
-
|
|
142
|
+
const format = opts.format ?? (opts.json ? "json" : "text");
|
|
143
|
+
if (!["text", "json", "sarif"].includes(format)) {
|
|
144
|
+
console.error(`Invalid format: ${format}. Valid values: text, json, sarif`);
|
|
145
|
+
process.exit(2);
|
|
146
|
+
}
|
|
147
|
+
if (format === "sarif") {
|
|
148
|
+
outputSarif(results);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (format === "json" || opts.json) {
|
|
152
|
+
const out = results.map((r) => ({
|
|
153
|
+
file: r.file,
|
|
154
|
+
matches: r.matches.map((m) => ({
|
|
155
|
+
pattern: { name: m.pattern.name, severity: m.pattern.severity, description: m.pattern.description || "" },
|
|
156
|
+
line: m.line ?? null,
|
|
157
|
+
column: m.column ?? null,
|
|
158
|
+
redacted: m.redacted || "",
|
|
159
|
+
})),
|
|
160
|
+
}));
|
|
161
|
+
console.log(JSON.stringify(out, null, 2));
|
|
65
162
|
process.exit(results.length > 0 ? 1 : 0);
|
|
66
163
|
}
|
|
67
164
|
if (results.length === 0) {
|
|
@@ -98,7 +195,7 @@ function outputScanResults(results, opts, context) {
|
|
|
98
195
|
/**
|
|
99
196
|
* Scan files changed since a git ref
|
|
100
197
|
*/
|
|
101
|
-
async function scanDiffFiles(ref, opts, scanCfg) {
|
|
198
|
+
async function scanDiffFiles(ref, opts, scanCfg, baselineEntries = []) {
|
|
102
199
|
try {
|
|
103
200
|
const diffOutput = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACM", ref], {
|
|
104
201
|
encoding: "utf-8",
|
|
@@ -126,12 +223,12 @@ async function scanDiffFiles(ref, opts, scanCfg) {
|
|
|
126
223
|
const results = await scanFile(filePath, engine, scanCfg);
|
|
127
224
|
allResults.push(...results);
|
|
128
225
|
}
|
|
129
|
-
outputScanResults(allResults, opts, `files changed since ${ref}`);
|
|
226
|
+
outputScanResults(applyBaseline(allResults, baselineEntries), opts, `files changed since ${ref}`);
|
|
130
227
|
}
|
|
131
228
|
catch (error) {
|
|
132
229
|
if (error.status === 128) {
|
|
133
230
|
console.error("Error: Not in a git repository or invalid ref");
|
|
134
|
-
process.exit(
|
|
231
|
+
process.exit(2);
|
|
135
232
|
}
|
|
136
233
|
throw error;
|
|
137
234
|
}
|
|
@@ -139,7 +236,7 @@ async function scanDiffFiles(ref, opts, scanCfg) {
|
|
|
139
236
|
/**
|
|
140
237
|
* Scan git staged files for secrets
|
|
141
238
|
*/
|
|
142
|
-
async function scanStagedFiles(opts, scanCfg) {
|
|
239
|
+
async function scanStagedFiles(opts, scanCfg, baselineEntries = []) {
|
|
143
240
|
try {
|
|
144
241
|
const stagedFilesOutput = execSync("git diff --cached --name-only --diff-filter=ACM", {
|
|
145
242
|
encoding: "utf-8",
|
|
@@ -167,12 +264,12 @@ async function scanStagedFiles(opts, scanCfg) {
|
|
|
167
264
|
const results = await scanFile(filePath, engine, scanCfg);
|
|
168
265
|
allResults.push(...results);
|
|
169
266
|
}
|
|
170
|
-
outputScanResults(allResults, opts, "staged files");
|
|
267
|
+
outputScanResults(applyBaseline(allResults, baselineEntries), opts, "staged files");
|
|
171
268
|
}
|
|
172
269
|
catch (error) {
|
|
173
270
|
if (error.status === 128) {
|
|
174
271
|
console.error("Error: Not in a git repository");
|
|
175
|
-
process.exit(
|
|
272
|
+
process.exit(2);
|
|
176
273
|
}
|
|
177
274
|
throw error;
|
|
178
275
|
}
|
|
@@ -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,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
|
+
}
|
|
@@ -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
|
+
}
|