@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
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
|
|
|
@@ -18,7 +18,7 @@ async function auditSkill(skillPath, opts) {
|
|
|
18
18
|
// Validate skill file exists
|
|
19
19
|
if (!fs.existsSync(skillPath)) {
|
|
20
20
|
console.error(`Error: Skill file not found: ${skillPath}`);
|
|
21
|
-
process.exit(
|
|
21
|
+
process.exit(2);
|
|
22
22
|
}
|
|
23
23
|
const absolutePath = path.resolve(skillPath);
|
|
24
24
|
const skillContent = fs.readFileSync(absolutePath, "utf-8");
|
|
@@ -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
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fmt } from "../../utils/formatter.js";
|
|
6
|
+
import { RegexScanner } from "../../scanners/regex-scanner.js";
|
|
7
|
+
import { GitleaksScanner } from "../../scanners/gitleaks.js";
|
|
8
|
+
import { ConfigManager } from "../../core/config-manager.js";
|
|
9
|
+
const BASELINE_PATH = path.join(os.homedir(), ".rafter", "baseline.json");
|
|
10
|
+
function loadBaseline() {
|
|
11
|
+
if (!fs.existsSync(BASELINE_PATH)) {
|
|
12
|
+
return { version: 1, created: "", updated: "", entries: [] };
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(fs.readFileSync(BASELINE_PATH, "utf-8"));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { version: 1, created: "", updated: "", entries: [] };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function saveBaseline(baseline) {
|
|
22
|
+
const dir = path.dirname(BASELINE_PATH);
|
|
23
|
+
if (!fs.existsSync(dir)) {
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
fs.writeFileSync(BASELINE_PATH, JSON.stringify(baseline, null, 2), "utf-8");
|
|
27
|
+
}
|
|
28
|
+
export function createBaselineCommand() {
|
|
29
|
+
const baseline = new Command("baseline")
|
|
30
|
+
.description("Manage the findings baseline (allowlist for known findings)");
|
|
31
|
+
baseline.addCommand(createBaselineCreateCommand());
|
|
32
|
+
baseline.addCommand(createBaselineShowCommand());
|
|
33
|
+
baseline.addCommand(createBaselineClearCommand());
|
|
34
|
+
baseline.addCommand(createBaselineAddCommand());
|
|
35
|
+
return baseline;
|
|
36
|
+
}
|
|
37
|
+
function createBaselineCreateCommand() {
|
|
38
|
+
return new Command("create")
|
|
39
|
+
.description("Scan and save all current findings as the baseline")
|
|
40
|
+
.argument("[path]", "Path to scan", ".")
|
|
41
|
+
.option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
|
|
42
|
+
.action(async (scanPath, opts) => {
|
|
43
|
+
const resolvedPath = path.resolve(scanPath);
|
|
44
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
45
|
+
console.error(`Error: Path not found: ${resolvedPath}`);
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
const manager = new ConfigManager();
|
|
49
|
+
const cfg = manager.loadWithPolicy();
|
|
50
|
+
const scanCfg = cfg.agent?.scan;
|
|
51
|
+
console.error(`Scanning ${resolvedPath} to build baseline...`);
|
|
52
|
+
const engine = await selectEngine(opts.engine || "auto");
|
|
53
|
+
let results;
|
|
54
|
+
if (fs.statSync(resolvedPath).isDirectory()) {
|
|
55
|
+
results = await scanDirectory(resolvedPath, engine, scanCfg);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
results = await scanFile(resolvedPath, engine);
|
|
59
|
+
}
|
|
60
|
+
const now = new Date().toISOString();
|
|
61
|
+
const entries = [];
|
|
62
|
+
for (const r of results) {
|
|
63
|
+
for (const m of r.matches) {
|
|
64
|
+
entries.push({
|
|
65
|
+
file: r.file,
|
|
66
|
+
line: m.line ?? null,
|
|
67
|
+
pattern: m.pattern.name,
|
|
68
|
+
addedAt: now,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const existing = loadBaseline();
|
|
73
|
+
const baseline = {
|
|
74
|
+
version: 1,
|
|
75
|
+
created: existing.created || now,
|
|
76
|
+
updated: now,
|
|
77
|
+
entries,
|
|
78
|
+
};
|
|
79
|
+
saveBaseline(baseline);
|
|
80
|
+
if (entries.length === 0) {
|
|
81
|
+
console.log(fmt.success("No findings — baseline is empty (all clean)"));
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
console.log(fmt.success(`Baseline saved: ${entries.length} finding(s) recorded`));
|
|
85
|
+
console.log(` Location: ${BASELINE_PATH}`);
|
|
86
|
+
console.log();
|
|
87
|
+
console.log("Future scans with --baseline will suppress these findings.");
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
function createBaselineShowCommand() {
|
|
92
|
+
return new Command("show")
|
|
93
|
+
.description("Show current baseline entries")
|
|
94
|
+
.option("--json", "Output as JSON")
|
|
95
|
+
.action((opts) => {
|
|
96
|
+
const baseline = loadBaseline();
|
|
97
|
+
if (opts.json) {
|
|
98
|
+
console.log(JSON.stringify(baseline, null, 2));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (baseline.entries.length === 0) {
|
|
102
|
+
console.log("Baseline is empty. Run: rafter agent baseline create");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log(`Baseline: ${baseline.entries.length} entries`);
|
|
106
|
+
if (baseline.updated) {
|
|
107
|
+
console.log(`Updated: ${baseline.updated}`);
|
|
108
|
+
}
|
|
109
|
+
console.log();
|
|
110
|
+
// Group by file
|
|
111
|
+
const byFile = new Map();
|
|
112
|
+
for (const entry of baseline.entries) {
|
|
113
|
+
const list = byFile.get(entry.file) || [];
|
|
114
|
+
list.push(entry);
|
|
115
|
+
byFile.set(entry.file, list);
|
|
116
|
+
}
|
|
117
|
+
for (const [file, entries] of byFile) {
|
|
118
|
+
console.log(fmt.info(file));
|
|
119
|
+
for (const e of entries) {
|
|
120
|
+
const loc = e.line != null ? `line ${e.line}` : "unknown location";
|
|
121
|
+
console.log(` ${e.pattern} (${loc})`);
|
|
122
|
+
}
|
|
123
|
+
console.log();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function createBaselineClearCommand() {
|
|
128
|
+
return new Command("clear")
|
|
129
|
+
.description("Remove all baseline entries")
|
|
130
|
+
.action(() => {
|
|
131
|
+
if (!fs.existsSync(BASELINE_PATH)) {
|
|
132
|
+
console.log("No baseline file found — nothing to clear.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
fs.unlinkSync(BASELINE_PATH);
|
|
136
|
+
console.log(fmt.success("Baseline cleared"));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
function createBaselineAddCommand() {
|
|
140
|
+
return new Command("add")
|
|
141
|
+
.description("Manually add a finding to the baseline")
|
|
142
|
+
.requiredOption("--file <path>", "File path")
|
|
143
|
+
.requiredOption("--pattern <name>", "Pattern name (e.g. 'AWS Access Key')")
|
|
144
|
+
.option("--line <number>", "Line number")
|
|
145
|
+
.action((opts) => {
|
|
146
|
+
const baseline = loadBaseline();
|
|
147
|
+
const now = new Date().toISOString();
|
|
148
|
+
const entry = {
|
|
149
|
+
file: path.resolve(opts.file),
|
|
150
|
+
line: opts.line != null ? parseInt(opts.line, 10) : null,
|
|
151
|
+
pattern: opts.pattern,
|
|
152
|
+
addedAt: now,
|
|
153
|
+
};
|
|
154
|
+
baseline.entries.push(entry);
|
|
155
|
+
baseline.updated = now;
|
|
156
|
+
if (!baseline.created)
|
|
157
|
+
baseline.created = now;
|
|
158
|
+
saveBaseline(baseline);
|
|
159
|
+
console.log(fmt.success(`Added to baseline: ${opts.pattern} in ${opts.file}`));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
// ── helpers ─────────────────────────────────────────────────────────
|
|
163
|
+
async function selectEngine(preference) {
|
|
164
|
+
if (preference === "patterns")
|
|
165
|
+
return "patterns";
|
|
166
|
+
if (preference === "gitleaks") {
|
|
167
|
+
const g = new GitleaksScanner();
|
|
168
|
+
return (await g.isAvailable()) ? "gitleaks" : "patterns";
|
|
169
|
+
}
|
|
170
|
+
const g = new GitleaksScanner();
|
|
171
|
+
return (await g.isAvailable()) ? "gitleaks" : "patterns";
|
|
172
|
+
}
|
|
173
|
+
async function scanFile(filePath, engine) {
|
|
174
|
+
if (engine === "gitleaks") {
|
|
175
|
+
try {
|
|
176
|
+
const g = new GitleaksScanner();
|
|
177
|
+
const r = await g.scanFile(filePath);
|
|
178
|
+
return r.matches.length > 0 ? [r] : [];
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
const s = new RegexScanner();
|
|
182
|
+
const r = s.scanFile(filePath);
|
|
183
|
+
return r.matches.length > 0 ? [r] : [];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const s = new RegexScanner();
|
|
187
|
+
const r = s.scanFile(filePath);
|
|
188
|
+
return r.matches.length > 0 ? [r] : [];
|
|
189
|
+
}
|
|
190
|
+
async function scanDirectory(dirPath, engine, scanCfg) {
|
|
191
|
+
if (engine === "gitleaks") {
|
|
192
|
+
try {
|
|
193
|
+
const g = new GitleaksScanner();
|
|
194
|
+
return await g.scanDirectory(dirPath);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
const s = new RegexScanner();
|
|
198
|
+
return s.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const s = new RegexScanner();
|
|
202
|
+
return s.scanDirectory(dirPath, { excludePaths: scanCfg?.excludePaths });
|
|
203
|
+
}
|
|
@@ -6,6 +6,10 @@ 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";
|
|
11
|
+
import { createUpdateGitleaksCommand } from "./update-gitleaks.js";
|
|
12
|
+
import { createBaselineCommand } from "./baseline.js";
|
|
9
13
|
export function createAgentCommand() {
|
|
10
14
|
const agent = new Command("agent")
|
|
11
15
|
.description("Agent security features");
|
|
@@ -17,5 +21,9 @@ export function createAgentCommand() {
|
|
|
17
21
|
agent.addCommand(createAuditCommand());
|
|
18
22
|
agent.addCommand(createAuditSkillCommand());
|
|
19
23
|
agent.addCommand(createInstallHookCommand());
|
|
24
|
+
agent.addCommand(createVerifyCommand());
|
|
25
|
+
agent.addCommand(createStatusCommand());
|
|
26
|
+
agent.addCommand(createUpdateGitleaksCommand());
|
|
27
|
+
agent.addCommand(createBaselineCommand());
|
|
20
28
|
return agent;
|
|
21
29
|
}
|
|
@@ -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();
|
|
@@ -87,6 +96,7 @@ export function createInitCommand() {
|
|
|
87
96
|
.option("--skip-claude-code", "Skip Claude Code skill installation")
|
|
88
97
|
.option("--claude-code", "Force Claude Code skill installation")
|
|
89
98
|
.option("--skip-gitleaks", "Skip Gitleaks binary download")
|
|
99
|
+
.option("--update", "Re-download gitleaks and reinstall integrations without resetting config")
|
|
90
100
|
.action(async (opts) => {
|
|
91
101
|
console.log(fmt.header("Rafter Agent Security Setup"));
|
|
92
102
|
console.log(fmt.divider());
|
|
@@ -125,49 +135,90 @@ export function createInitCommand() {
|
|
|
125
135
|
}
|
|
126
136
|
manager.set("agent.riskLevel", opts.riskLevel);
|
|
127
137
|
console.log(fmt.success(`Set risk level: ${opts.riskLevel}`));
|
|
128
|
-
//
|
|
138
|
+
// Check / download Gitleaks binary (optional)
|
|
129
139
|
if (!opts.skipGitleaks) {
|
|
130
140
|
const binaryManager = new BinaryManager();
|
|
131
141
|
const platformInfo = binaryManager.getPlatformInfo();
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
142
|
+
// Helper: show diagnostics for a failing binary (mirrors Python's agent init)
|
|
143
|
+
const showDiagnostics = async (binaryPath, verResult) => {
|
|
144
|
+
if (verResult.stderr) {
|
|
145
|
+
console.log(fmt.info(` stderr: ${verResult.stderr}`));
|
|
146
|
+
}
|
|
147
|
+
const diag = await binaryManager.collectBinaryDiagnostics(binaryPath);
|
|
148
|
+
if (diag) {
|
|
149
|
+
console.log(fmt.info("Diagnostics:"));
|
|
150
|
+
console.log(diag);
|
|
151
|
+
}
|
|
152
|
+
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'."));
|
|
153
|
+
console.log();
|
|
154
|
+
};
|
|
155
|
+
if (!opts.update && binaryManager.isGitleaksInstalled()) {
|
|
156
|
+
// Local binary exists — verify it actually works
|
|
157
|
+
const verResult = await binaryManager.verifyGitleaksVerbose();
|
|
158
|
+
if (verResult.ok) {
|
|
159
|
+
console.log(fmt.success(`Gitleaks already installed (${verResult.stdout})`));
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
console.log(fmt.warning("Gitleaks binary found locally but failed to execute."));
|
|
163
|
+
console.log(fmt.info(` Binary: ${binaryManager.getGitleaksPath()}`));
|
|
164
|
+
await showDiagnostics(binaryManager.getGitleaksPath(), verResult);
|
|
165
|
+
}
|
|
139
166
|
}
|
|
140
167
|
else {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
168
|
+
// Not installed locally (or --update forcing re-download) — check PATH first
|
|
169
|
+
// unless --update was passed (in that case force a fresh managed install)
|
|
170
|
+
const pathBinary = opts.update ? null : binaryManager.findGitleaksOnPath();
|
|
171
|
+
if (pathBinary) {
|
|
172
|
+
const verResult = await binaryManager.verifyGitleaksVerbose(pathBinary);
|
|
173
|
+
if (verResult.ok) {
|
|
174
|
+
console.log(fmt.success(`Gitleaks available on PATH (${verResult.stdout})`));
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
console.log(fmt.warning("Gitleaks found on PATH but failed to execute."));
|
|
178
|
+
console.log(fmt.info(` Binary: ${pathBinary}`));
|
|
179
|
+
await showDiagnostics(pathBinary, verResult);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
else if (!platformInfo.supported) {
|
|
183
|
+
console.log(fmt.info(`Gitleaks not available for ${platformInfo.platform}/${platformInfo.arch}`));
|
|
184
|
+
console.log(fmt.success("Using pattern-based scanning (21 patterns)"));
|
|
148
185
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
console.log(fmt.success("Falling back to pattern-based scanning"));
|
|
186
|
+
else {
|
|
187
|
+
// Not on PATH, not installed locally — download
|
|
152
188
|
console.log();
|
|
189
|
+
console.log(fmt.info("Downloading Gitleaks (enhanced secret detection)..."));
|
|
190
|
+
try {
|
|
191
|
+
await binaryManager.downloadGitleaks((msg) => {
|
|
192
|
+
console.log(` ${msg}`);
|
|
193
|
+
});
|
|
194
|
+
console.log();
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(fmt.error(`Gitleaks setup failed — pattern-based scanning will be used instead.`));
|
|
199
|
+
console.log(fmt.warning(String(e)));
|
|
200
|
+
console.log();
|
|
201
|
+
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'."));
|
|
202
|
+
console.log();
|
|
203
|
+
}
|
|
153
204
|
}
|
|
154
205
|
}
|
|
155
206
|
}
|
|
156
207
|
// Install OpenClaw skill if applicable
|
|
157
208
|
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
|
-
}
|
|
209
|
+
const skillManager = new SkillManager();
|
|
210
|
+
const result = await skillManager.installRafterSkillVerbose();
|
|
211
|
+
if (result.ok) {
|
|
212
|
+
console.log(fmt.success("Installed Rafter Security skill to ~/.openclaw/skills/rafter-security.md"));
|
|
213
|
+
manager.set("agent.environments.openclaw.enabled", true);
|
|
168
214
|
}
|
|
169
|
-
|
|
170
|
-
console.
|
|
215
|
+
else {
|
|
216
|
+
console.log(fmt.error("Failed to install Rafter Security skill"));
|
|
217
|
+
console.log(fmt.warning(` Source: ${result.sourcePath}`));
|
|
218
|
+
console.log(fmt.warning(` Destination: ${result.destPath}`));
|
|
219
|
+
if (result.error) {
|
|
220
|
+
console.log(fmt.warning(` Error: ${result.error}`));
|
|
221
|
+
}
|
|
171
222
|
}
|
|
172
223
|
}
|
|
173
224
|
// 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';
|
|
@@ -7,25 +8,36 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
7
8
|
const __dirname = path.dirname(__filename);
|
|
8
9
|
export function createInstallHookCommand() {
|
|
9
10
|
return new Command("install-hook")
|
|
10
|
-
.description("Install
|
|
11
|
+
.description("Install git hook to scan for secrets")
|
|
11
12
|
.option("--global", "Install globally for all repos (via git config)")
|
|
13
|
+
.option("--push", "Install pre-push hook instead of pre-commit")
|
|
12
14
|
.action(async (opts) => {
|
|
13
15
|
await installHook(opts);
|
|
14
16
|
});
|
|
15
17
|
}
|
|
16
18
|
async function installHook(opts) {
|
|
19
|
+
const hookName = opts.push ? "pre-push" : "pre-commit";
|
|
20
|
+
const templateName = opts.push ? "pre-push-hook.sh" : "pre-commit-hook.sh";
|
|
17
21
|
if (opts.global) {
|
|
18
|
-
await installGlobalHook();
|
|
22
|
+
await installGlobalHook(hookName, templateName);
|
|
19
23
|
}
|
|
20
24
|
else {
|
|
21
|
-
await installLocalHook();
|
|
25
|
+
await installLocalHook(hookName, templateName);
|
|
22
26
|
}
|
|
23
27
|
}
|
|
28
|
+
function getTemplatePath(templateName) {
|
|
29
|
+
const templatePath = path.join(__dirname, "..", "..", "..", "resources", templateName);
|
|
30
|
+
if (!fs.existsSync(templatePath)) {
|
|
31
|
+
console.error("❌ Error: Hook template not found");
|
|
32
|
+
console.error(` Expected at: ${templatePath}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
return templatePath;
|
|
36
|
+
}
|
|
24
37
|
/**
|
|
25
|
-
* Install
|
|
38
|
+
* Install hook for current repository
|
|
26
39
|
*/
|
|
27
|
-
async function installLocalHook() {
|
|
28
|
-
// Check if in a git repository
|
|
40
|
+
async function installLocalHook(hookName, templateName) {
|
|
29
41
|
try {
|
|
30
42
|
execSync("git rev-parse --git-dir", { stdio: "pipe" });
|
|
31
43
|
}
|
|
@@ -34,80 +46,63 @@ async function installLocalHook() {
|
|
|
34
46
|
console.error(" Run this command from inside a git repository");
|
|
35
47
|
process.exit(1);
|
|
36
48
|
}
|
|
37
|
-
// Get .git directory
|
|
38
49
|
const gitDir = execSync("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
|
|
39
50
|
const hooksDir = path.resolve(gitDir, "hooks");
|
|
40
|
-
const hookPath = path.join(hooksDir,
|
|
41
|
-
// Ensure hooks directory exists
|
|
51
|
+
const hookPath = path.join(hooksDir, hookName);
|
|
42
52
|
if (!fs.existsSync(hooksDir)) {
|
|
43
53
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
44
54
|
}
|
|
45
|
-
// Check if hook already exists
|
|
46
55
|
if (fs.existsSync(hookPath)) {
|
|
47
56
|
const existing = fs.readFileSync(hookPath, "utf-8");
|
|
48
|
-
|
|
49
|
-
if (existing.includes(
|
|
50
|
-
console.log(
|
|
57
|
+
const marker = hookName === "pre-push" ? "Rafter Security Pre-Push Hook" : "Rafter Security Pre-Commit Hook";
|
|
58
|
+
if (existing.includes(marker)) {
|
|
59
|
+
console.log(`✓ Rafter ${hookName} hook already installed`);
|
|
51
60
|
return;
|
|
52
61
|
}
|
|
53
|
-
// Backup existing hook
|
|
54
62
|
const backupPath = `${hookPath}.backup-${Date.now()}`;
|
|
55
63
|
fs.copyFileSync(hookPath, backupPath);
|
|
56
64
|
console.log(`📦 Backed up existing hook to: ${path.basename(backupPath)}`);
|
|
57
65
|
}
|
|
58
|
-
|
|
59
|
-
const templatePath = path.join(__dirname, "..", "..", "..", "resources", "pre-commit-hook.sh");
|
|
60
|
-
if (!fs.existsSync(templatePath)) {
|
|
61
|
-
console.error("❌ Error: Hook template not found");
|
|
62
|
-
console.error(` Expected at: ${templatePath}`);
|
|
63
|
-
process.exit(1);
|
|
64
|
-
}
|
|
65
|
-
// Copy hook template
|
|
66
|
-
const hookContent = fs.readFileSync(templatePath, "utf-8");
|
|
66
|
+
const hookContent = fs.readFileSync(getTemplatePath(templateName), "utf-8");
|
|
67
67
|
fs.writeFileSync(hookPath, hookContent, "utf-8");
|
|
68
|
-
// Make executable
|
|
69
68
|
fs.chmodSync(hookPath, 0o755);
|
|
70
|
-
console.log(
|
|
69
|
+
console.log(`✓ Installed Rafter ${hookName} hook`);
|
|
71
70
|
console.log(` Location: ${hookPath}`);
|
|
72
71
|
console.log();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
if (hookName === "pre-push") {
|
|
73
|
+
console.log("The hook will:");
|
|
74
|
+
console.log(" • Scan commits being pushed for secrets");
|
|
75
|
+
console.log(" • Block pushes if secrets are detected");
|
|
76
|
+
console.log(" • Can be bypassed with: git push --no-verify (not recommended)");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
console.log("The hook will:");
|
|
80
|
+
console.log(" • Scan staged files for secrets before each commit");
|
|
81
|
+
console.log(" • Block commits if secrets are detected");
|
|
82
|
+
console.log(" • Can be bypassed with: git commit --no-verify (not recommended)");
|
|
83
|
+
}
|
|
77
84
|
console.log();
|
|
78
85
|
}
|
|
79
86
|
/**
|
|
80
|
-
* Install
|
|
87
|
+
* Install hook globally for all repositories
|
|
81
88
|
*/
|
|
82
|
-
async function installGlobalHook() {
|
|
83
|
-
|
|
84
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
89
|
+
async function installGlobalHook(hookName, templateName) {
|
|
90
|
+
const homeDir = os.homedir();
|
|
85
91
|
if (!homeDir) {
|
|
86
92
|
console.error("❌ Error: Could not determine home directory");
|
|
87
93
|
process.exit(1);
|
|
88
94
|
}
|
|
89
95
|
const globalHooksDir = path.join(homeDir, ".rafter", "git-hooks");
|
|
90
|
-
const hookPath = path.join(globalHooksDir,
|
|
91
|
-
// Create directory
|
|
96
|
+
const hookPath = path.join(globalHooksDir, hookName);
|
|
92
97
|
if (!fs.existsSync(globalHooksDir)) {
|
|
93
98
|
fs.mkdirSync(globalHooksDir, { recursive: true });
|
|
94
99
|
}
|
|
95
|
-
|
|
96
|
-
const templatePath = path.join(__dirname, "..", "..", "..", "resources", "pre-commit-hook.sh");
|
|
97
|
-
if (!fs.existsSync(templatePath)) {
|
|
98
|
-
console.error("❌ Error: Hook template not found");
|
|
99
|
-
console.error(` Expected at: ${templatePath}`);
|
|
100
|
-
process.exit(1);
|
|
101
|
-
}
|
|
102
|
-
// Copy hook template
|
|
103
|
-
const hookContent = fs.readFileSync(templatePath, "utf-8");
|
|
100
|
+
const hookContent = fs.readFileSync(getTemplatePath(templateName), "utf-8");
|
|
104
101
|
fs.writeFileSync(hookPath, hookContent, "utf-8");
|
|
105
|
-
// Make executable
|
|
106
102
|
fs.chmodSync(hookPath, 0o755);
|
|
107
|
-
// Configure git to use global hooks directory
|
|
108
103
|
try {
|
|
109
104
|
execSync(`git config --global core.hooksPath "${globalHooksDir}"`, { stdio: "pipe" });
|
|
110
|
-
console.log(
|
|
105
|
+
console.log(`✓ Installed Rafter ${hookName} hook globally`);
|
|
111
106
|
console.log(` Location: ${hookPath}`);
|
|
112
107
|
console.log(` Git config: core.hooksPath = ${globalHooksDir}`);
|
|
113
108
|
console.log();
|
|
@@ -117,7 +112,7 @@ async function installGlobalHook() {
|
|
|
117
112
|
console.log(` git config --global --unset core.hooksPath`);
|
|
118
113
|
console.log();
|
|
119
114
|
console.log("To install per-repository instead:");
|
|
120
|
-
console.log(` cd <repo> && rafter agent install-hook`);
|
|
115
|
+
console.log(` cd <repo> && rafter agent install-hook${hookName === "pre-push" ? " --push" : ""}`);
|
|
121
116
|
console.log();
|
|
122
117
|
}
|
|
123
118
|
catch (e) {
|