@rafter-security/cli 0.4.1 → 0.5.1
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 +100 -0
- package/dist/commands/agent/audit.js +15 -3
- package/dist/commands/agent/exec.js +9 -8
- package/dist/commands/agent/init.js +62 -26
- package/dist/commands/agent/scan.js +113 -101
- package/dist/commands/ci/index.js +8 -0
- package/dist/commands/ci/init.js +191 -0
- package/dist/commands/hook/index.js +8 -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 +52 -0
- package/dist/core/policy-loader.js +167 -0
- package/dist/core/risk-rules.js +67 -0
- package/dist/index.js +23 -2
- package/dist/scanners/gitleaks.js +7 -6
- package/dist/scanners/regex-scanner.js +22 -2
- package/dist/utils/formatter.js +52 -0
- package/package.json +7 -3
- package/resources/pre-commit-hook.sh +45 -0
- package/resources/rafter-security-skill.md +316 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { RegexScanner } from "../../scanners/regex-scanner.js";
|
|
6
|
+
import { GitleaksScanner } from "../../scanners/gitleaks.js";
|
|
7
|
+
import { CommandInterceptor } from "../../core/command-interceptor.js";
|
|
8
|
+
import { AuditLogger } from "../../core/audit-logger.js";
|
|
9
|
+
import { ConfigManager } from "../../core/config-manager.js";
|
|
10
|
+
function formatScanResults(results) {
|
|
11
|
+
return results.map(r => ({
|
|
12
|
+
file: r.file,
|
|
13
|
+
matches: r.matches.map(m => ({
|
|
14
|
+
pattern: m.pattern.name,
|
|
15
|
+
severity: m.pattern.severity,
|
|
16
|
+
line: m.line,
|
|
17
|
+
redacted: m.redacted || m.match.slice(0, 4) + "****",
|
|
18
|
+
})),
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
function textResult(data) {
|
|
22
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
23
|
+
}
|
|
24
|
+
function errorResult(message) {
|
|
25
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }], isError: true };
|
|
26
|
+
}
|
|
27
|
+
function createServer() {
|
|
28
|
+
const server = new Server({ name: "rafter", version: "0.5.0" }, { capabilities: { tools: {}, resources: {} } });
|
|
29
|
+
// ── Tools ───────────────────────────────────────────────────────────
|
|
30
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
31
|
+
tools: [
|
|
32
|
+
{
|
|
33
|
+
name: "scan_secrets",
|
|
34
|
+
description: "Scan files or directories for hardcoded secrets and credentials",
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: "object",
|
|
37
|
+
properties: {
|
|
38
|
+
path: { type: "string", description: "File or directory path to scan" },
|
|
39
|
+
engine: {
|
|
40
|
+
type: "string",
|
|
41
|
+
enum: ["auto", "gitleaks", "patterns"],
|
|
42
|
+
description: "Scan engine: auto (default), gitleaks, or patterns",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ["path"],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: "evaluate_command",
|
|
50
|
+
description: "Evaluate whether a shell command is allowed by Rafter security policy",
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: "object",
|
|
53
|
+
properties: {
|
|
54
|
+
command: { type: "string", description: "Shell command to evaluate" },
|
|
55
|
+
},
|
|
56
|
+
required: ["command"],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "read_audit_log",
|
|
61
|
+
description: "Read Rafter audit log entries with optional filtering",
|
|
62
|
+
inputSchema: {
|
|
63
|
+
type: "object",
|
|
64
|
+
properties: {
|
|
65
|
+
limit: { type: "number", description: "Maximum entries to return (default: 20)" },
|
|
66
|
+
event_type: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "Filter by event type (e.g. command_intercepted, secret_detected)",
|
|
69
|
+
},
|
|
70
|
+
since: { type: "string", description: "ISO 8601 timestamp — only return entries after this time" },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "get_config",
|
|
76
|
+
description: "Read Rafter configuration (full config or a specific key)",
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: "object",
|
|
79
|
+
properties: {
|
|
80
|
+
key: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "Dot-path config key (e.g. agent.commandPolicy). Omit for full config.",
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
}));
|
|
89
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
90
|
+
const { name, arguments: args } = request.params;
|
|
91
|
+
switch (name) {
|
|
92
|
+
case "scan_secrets": {
|
|
93
|
+
const scanPath = args?.path;
|
|
94
|
+
const engine = args?.engine || "auto";
|
|
95
|
+
if (engine === "gitleaks" || engine === "auto") {
|
|
96
|
+
const gitleaks = new GitleaksScanner();
|
|
97
|
+
if (await gitleaks.isAvailable()) {
|
|
98
|
+
try {
|
|
99
|
+
const results = await gitleaks.scanDirectory(scanPath);
|
|
100
|
+
return textResult(formatScanResults(results));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
if (engine === "gitleaks")
|
|
104
|
+
return errorResult("Gitleaks scan failed");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else if (engine === "gitleaks") {
|
|
108
|
+
return errorResult("Gitleaks not installed");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const scanner = new RegexScanner();
|
|
112
|
+
let results;
|
|
113
|
+
try {
|
|
114
|
+
results = scanner.scanDirectory(scanPath);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
results = [scanner.scanFile(scanPath)];
|
|
118
|
+
}
|
|
119
|
+
return textResult(formatScanResults(results));
|
|
120
|
+
}
|
|
121
|
+
case "evaluate_command": {
|
|
122
|
+
const command = args?.command;
|
|
123
|
+
const interceptor = new CommandInterceptor();
|
|
124
|
+
const result = interceptor.evaluate(command);
|
|
125
|
+
const out = {
|
|
126
|
+
allowed: result.allowed,
|
|
127
|
+
risk_level: result.riskLevel,
|
|
128
|
+
requires_approval: result.requiresApproval,
|
|
129
|
+
};
|
|
130
|
+
if (result.reason)
|
|
131
|
+
out.reason = result.reason;
|
|
132
|
+
return textResult(out);
|
|
133
|
+
}
|
|
134
|
+
case "read_audit_log": {
|
|
135
|
+
const logger = new AuditLogger();
|
|
136
|
+
const entries = logger.read({
|
|
137
|
+
limit: args?.limit ?? 20,
|
|
138
|
+
eventType: args?.event_type,
|
|
139
|
+
since: args?.since ? new Date(args.since) : undefined,
|
|
140
|
+
});
|
|
141
|
+
return textResult(entries);
|
|
142
|
+
}
|
|
143
|
+
case "get_config": {
|
|
144
|
+
const manager = new ConfigManager();
|
|
145
|
+
const key = args?.key;
|
|
146
|
+
const value = key ? manager.get(key) : manager.load();
|
|
147
|
+
return textResult(value);
|
|
148
|
+
}
|
|
149
|
+
default:
|
|
150
|
+
return errorResult(`Unknown tool: ${name}`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
// ── Resources ───────────────────────────────────────────────────────
|
|
154
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
155
|
+
resources: [
|
|
156
|
+
{
|
|
157
|
+
uri: "rafter://config",
|
|
158
|
+
name: "Rafter Configuration",
|
|
159
|
+
description: "Current Rafter configuration",
|
|
160
|
+
mimeType: "application/json",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
uri: "rafter://policy",
|
|
164
|
+
name: "Rafter Policy",
|
|
165
|
+
description: "Active security policy (merged .rafter.yml + config)",
|
|
166
|
+
mimeType: "application/json",
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
}));
|
|
170
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
171
|
+
const { uri } = request.params;
|
|
172
|
+
const manager = new ConfigManager();
|
|
173
|
+
switch (uri) {
|
|
174
|
+
case "rafter://config":
|
|
175
|
+
return {
|
|
176
|
+
contents: [{
|
|
177
|
+
uri: "rafter://config",
|
|
178
|
+
mimeType: "application/json",
|
|
179
|
+
text: JSON.stringify(manager.load(), null, 2),
|
|
180
|
+
}],
|
|
181
|
+
};
|
|
182
|
+
case "rafter://policy":
|
|
183
|
+
return {
|
|
184
|
+
contents: [{
|
|
185
|
+
uri: "rafter://policy",
|
|
186
|
+
mimeType: "application/json",
|
|
187
|
+
text: JSON.stringify(manager.loadWithPolicy(), null, 2),
|
|
188
|
+
}],
|
|
189
|
+
};
|
|
190
|
+
default:
|
|
191
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return server;
|
|
195
|
+
}
|
|
196
|
+
export function createMcpServeCommand() {
|
|
197
|
+
return new Command("serve")
|
|
198
|
+
.description("Start MCP server over stdio transport")
|
|
199
|
+
.option("--transport <type>", "Transport type (currently only stdio)", "stdio")
|
|
200
|
+
.action(async () => {
|
|
201
|
+
const server = createServer();
|
|
202
|
+
const transport = new StdioServerTransport();
|
|
203
|
+
await server.connect(transport);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { ConfigManager } from "../../core/config-manager.js";
|
|
3
|
+
import { fmt } from "../../utils/formatter.js";
|
|
4
|
+
export function createPolicyExportCommand() {
|
|
5
|
+
return new Command("export")
|
|
6
|
+
.description("Export Rafter policy for agent platforms")
|
|
7
|
+
.requiredOption("--format <format>", "Target format: claude or codex")
|
|
8
|
+
.option("--output <path>", "Write to file instead of stdout")
|
|
9
|
+
.action((opts) => {
|
|
10
|
+
const format = opts.format;
|
|
11
|
+
if (format !== "claude" && format !== "codex") {
|
|
12
|
+
console.error(fmt.error(`Unknown format: ${format}`));
|
|
13
|
+
console.error("Valid options: claude, codex");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const content = format === "claude"
|
|
17
|
+
? generateClaudeConfig()
|
|
18
|
+
: generateCodexConfig();
|
|
19
|
+
if (opts.output) {
|
|
20
|
+
const fs = require("fs");
|
|
21
|
+
const path = require("path");
|
|
22
|
+
const dir = path.dirname(opts.output);
|
|
23
|
+
if (!fs.existsSync(dir)) {
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
fs.writeFileSync(opts.output, content, "utf-8");
|
|
27
|
+
console.log(fmt.success(`Exported ${format} policy to ${opts.output}`));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
process.stdout.write(content);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function generateClaudeConfig() {
|
|
35
|
+
const config = {
|
|
36
|
+
hooks: {
|
|
37
|
+
PreToolUse: [
|
|
38
|
+
{
|
|
39
|
+
matcher: "Bash",
|
|
40
|
+
hooks: [{ type: "command", command: "rafter hook pretool" }],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
matcher: "Write|Edit",
|
|
44
|
+
hooks: [{ type: "command", command: "rafter hook pretool" }],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
return JSON.stringify(config, null, 2) + "\n";
|
|
50
|
+
}
|
|
51
|
+
function generateCodexConfig() {
|
|
52
|
+
const manager = new ConfigManager();
|
|
53
|
+
const cfg = manager.loadWithPolicy();
|
|
54
|
+
const policy = cfg.agent?.commandPolicy;
|
|
55
|
+
const blocked = policy?.blockedPatterns || [];
|
|
56
|
+
const approval = policy?.requireApproval || [];
|
|
57
|
+
let toml = `# Rafter security policy for OpenAI Codex
|
|
58
|
+
# Generated by: rafter policy export --format codex
|
|
59
|
+
# Docs: https://docs.rafter.so/cli/pretool-hooks
|
|
60
|
+
|
|
61
|
+
`;
|
|
62
|
+
if (blocked.length > 0) {
|
|
63
|
+
toml += `[rules.blocked]\n`;
|
|
64
|
+
toml += `# Commands that are always blocked\n`;
|
|
65
|
+
toml += `patterns = [\n`;
|
|
66
|
+
for (const p of blocked) {
|
|
67
|
+
toml += ` ${JSON.stringify(p)},\n`;
|
|
68
|
+
}
|
|
69
|
+
toml += `]\n\n`;
|
|
70
|
+
}
|
|
71
|
+
if (approval.length > 0) {
|
|
72
|
+
toml += `[rules.prompt]\n`;
|
|
73
|
+
toml += `# Commands that require user approval\n`;
|
|
74
|
+
toml += `patterns = [\n`;
|
|
75
|
+
for (const p of approval) {
|
|
76
|
+
toml += ` ${JSON.stringify(p)},\n`;
|
|
77
|
+
}
|
|
78
|
+
toml += `]\n`;
|
|
79
|
+
}
|
|
80
|
+
return toml;
|
|
81
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createPolicyExportCommand } from "./export.js";
|
|
3
|
+
export function createPolicyCommand() {
|
|
4
|
+
const policy = new Command("policy")
|
|
5
|
+
.description("Security policy management");
|
|
6
|
+
policy.addCommand(createPolicyExportCommand());
|
|
7
|
+
return policy;
|
|
8
|
+
}
|
|
@@ -2,6 +2,7 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { getAuditLogPath } from "./config-defaults.js";
|
|
4
4
|
import { ConfigManager } from "./config-manager.js";
|
|
5
|
+
import { assessCommandRisk } from "./risk-rules.js";
|
|
5
6
|
export class AuditLogger {
|
|
6
7
|
constructor(logPath) {
|
|
7
8
|
this.logPath = logPath || getAuditLogPath();
|
|
@@ -166,38 +167,6 @@ export class AuditLogger {
|
|
|
166
167
|
* Assess risk level of a command
|
|
167
168
|
*/
|
|
168
169
|
assessCommandRisk(command) {
|
|
169
|
-
|
|
170
|
-
/rm\s+-rf\s+\//,
|
|
171
|
-
/:\(\)\{\s*:\|:&\s*\};:/, // fork bomb
|
|
172
|
-
/dd\s+if=.*of=\/dev\/sd/,
|
|
173
|
-
/>\s*\/dev\/sd/
|
|
174
|
-
];
|
|
175
|
-
const high = [
|
|
176
|
-
/rm\s+-rf/,
|
|
177
|
-
/sudo\s+rm/,
|
|
178
|
-
/chmod\s+777/,
|
|
179
|
-
/curl.*\|.*sh/,
|
|
180
|
-
/wget.*\|.*sh/,
|
|
181
|
-
/git\s+push\s+--force/
|
|
182
|
-
];
|
|
183
|
-
const medium = [
|
|
184
|
-
/sudo/,
|
|
185
|
-
/chmod/,
|
|
186
|
-
/chown/,
|
|
187
|
-
/systemctl/
|
|
188
|
-
];
|
|
189
|
-
for (const pattern of critical) {
|
|
190
|
-
if (pattern.test(command))
|
|
191
|
-
return "critical";
|
|
192
|
-
}
|
|
193
|
-
for (const pattern of high) {
|
|
194
|
-
if (pattern.test(command))
|
|
195
|
-
return "high";
|
|
196
|
-
}
|
|
197
|
-
for (const pattern of medium) {
|
|
198
|
-
if (pattern.test(command))
|
|
199
|
-
return "medium";
|
|
200
|
-
}
|
|
201
|
-
return "low";
|
|
170
|
+
return assessCommandRisk(command);
|
|
202
171
|
}
|
|
203
172
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConfigManager } from "./config-manager.js";
|
|
2
2
|
import { AuditLogger } from "./audit-logger.js";
|
|
3
|
+
import { assessCommandRisk } from "./risk-rules.js";
|
|
3
4
|
export class CommandInterceptor {
|
|
4
5
|
constructor() {
|
|
5
6
|
this.config = new ConfigManager();
|
|
@@ -9,7 +10,7 @@ export class CommandInterceptor {
|
|
|
9
10
|
* Evaluate if a command should be allowed
|
|
10
11
|
*/
|
|
11
12
|
evaluate(command) {
|
|
12
|
-
const cfg = this.config.
|
|
13
|
+
const cfg = this.config.loadWithPolicy();
|
|
13
14
|
const policy = cfg.agent?.commandPolicy;
|
|
14
15
|
if (!policy) {
|
|
15
16
|
// No policy configured, allow by default
|
|
@@ -103,63 +104,18 @@ export class CommandInterceptor {
|
|
|
103
104
|
*/
|
|
104
105
|
matchesPattern(command, pattern) {
|
|
105
106
|
try {
|
|
106
|
-
const regex = new RegExp(pattern);
|
|
107
|
+
const regex = new RegExp(pattern, "i");
|
|
107
108
|
return regex.test(command);
|
|
108
109
|
}
|
|
109
110
|
catch {
|
|
110
|
-
// If pattern is not valid regex, try
|
|
111
|
-
return command.includes(pattern);
|
|
111
|
+
// If pattern is not valid regex, try case-insensitive substring match
|
|
112
|
+
return command.toLowerCase().includes(pattern.toLowerCase());
|
|
112
113
|
}
|
|
113
114
|
}
|
|
114
115
|
/**
|
|
115
116
|
* Assess risk level of command
|
|
116
117
|
*/
|
|
117
118
|
assessRisk(command) {
|
|
118
|
-
|
|
119
|
-
const critical = [
|
|
120
|
-
/rm\s+-rf\s+\//,
|
|
121
|
-
/:\(\)\{\s*:\|:&\s*\};:/, // fork bomb
|
|
122
|
-
/dd\s+if=.*of=\/dev\/sd/,
|
|
123
|
-
/>\s*\/dev\/sd/,
|
|
124
|
-
/mkfs/,
|
|
125
|
-
/fdisk/,
|
|
126
|
-
/parted/
|
|
127
|
-
];
|
|
128
|
-
// High risk patterns
|
|
129
|
-
const high = [
|
|
130
|
-
/rm\s+-rf/,
|
|
131
|
-
/sudo\s+rm/,
|
|
132
|
-
/chmod\s+777/,
|
|
133
|
-
/curl.*\|.*sh/,
|
|
134
|
-
/wget.*\|.*sh/,
|
|
135
|
-
/git\s+push\s+--force/,
|
|
136
|
-
/docker\s+system\s+prune/,
|
|
137
|
-
/npm\s+publish/,
|
|
138
|
-
/pypi.*upload/
|
|
139
|
-
];
|
|
140
|
-
// Medium risk patterns
|
|
141
|
-
const medium = [
|
|
142
|
-
/sudo/,
|
|
143
|
-
/chmod/,
|
|
144
|
-
/chown/,
|
|
145
|
-
/systemctl/,
|
|
146
|
-
/service/,
|
|
147
|
-
/kill\s+-9/,
|
|
148
|
-
/pkill/,
|
|
149
|
-
/killall/
|
|
150
|
-
];
|
|
151
|
-
for (const pattern of critical) {
|
|
152
|
-
if (pattern.test(command))
|
|
153
|
-
return "critical";
|
|
154
|
-
}
|
|
155
|
-
for (const pattern of high) {
|
|
156
|
-
if (pattern.test(command))
|
|
157
|
-
return "high";
|
|
158
|
-
}
|
|
159
|
-
for (const pattern of medium) {
|
|
160
|
-
if (pattern.test(command))
|
|
161
|
-
return "medium";
|
|
162
|
-
}
|
|
163
|
-
return "low";
|
|
119
|
+
return assessCommandRisk(command);
|
|
164
120
|
}
|
|
165
121
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEFAULT_BLOCKED_PATTERNS, DEFAULT_REQUIRE_APPROVAL } from "./risk-rules.js";
|
|
1
2
|
import os from "os";
|
|
2
3
|
import path from "path";
|
|
3
4
|
export const CONFIG_VERSION = "1.0.0";
|
|
@@ -27,20 +28,8 @@ export function getDefaultConfig() {
|
|
|
27
28
|
},
|
|
28
29
|
commandPolicy: {
|
|
29
30
|
mode: "approve-dangerous",
|
|
30
|
-
blockedPatterns: [
|
|
31
|
-
|
|
32
|
-
":(){ :|:& };:", // fork bomb
|
|
33
|
-
"dd if=/dev/zero of=/dev/sda",
|
|
34
|
-
"> /dev/sda"
|
|
35
|
-
],
|
|
36
|
-
requireApproval: [
|
|
37
|
-
"rm -rf",
|
|
38
|
-
"sudo rm",
|
|
39
|
-
"curl.*|.*sh",
|
|
40
|
-
"wget.*|.*sh",
|
|
41
|
-
"chmod 777",
|
|
42
|
-
"git push --force"
|
|
43
|
-
]
|
|
31
|
+
blockedPatterns: [...DEFAULT_BLOCKED_PATTERNS],
|
|
32
|
+
requireApproval: [...DEFAULT_REQUIRE_APPROVAL],
|
|
44
33
|
},
|
|
45
34
|
outputFiltering: {
|
|
46
35
|
redactSecrets: true,
|
|
@@ -61,7 +50,7 @@ export function getConfigPath() {
|
|
|
61
50
|
return path.join(getRafterDir(), "config.json");
|
|
62
51
|
}
|
|
63
52
|
export function getAuditLogPath() {
|
|
64
|
-
return path.join(getRafterDir(), "audit.
|
|
53
|
+
return path.join(getRafterDir(), "audit.jsonl");
|
|
65
54
|
}
|
|
66
55
|
export function getBinDir() {
|
|
67
56
|
return path.join(getRafterDir(), "bin");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { getDefaultConfig, getConfigPath, getRafterDir, CONFIG_VERSION } from "./config-defaults.js";
|
|
4
|
+
import { loadPolicy } from "./policy-loader.js";
|
|
4
5
|
export class ConfigManager {
|
|
5
6
|
constructor(configPath) {
|
|
6
7
|
this.configPath = configPath || getConfigPath();
|
|
@@ -101,6 +102,57 @@ export class ConfigManager {
|
|
|
101
102
|
this.save(config);
|
|
102
103
|
}
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Load config merged with .rafter.yml policy (policy wins)
|
|
107
|
+
*/
|
|
108
|
+
loadWithPolicy() {
|
|
109
|
+
const config = this.load();
|
|
110
|
+
const policy = loadPolicy();
|
|
111
|
+
if (!policy)
|
|
112
|
+
return config;
|
|
113
|
+
// Ensure agent block exists
|
|
114
|
+
if (!config.agent) {
|
|
115
|
+
const defaults = getDefaultConfig();
|
|
116
|
+
config.agent = defaults.agent;
|
|
117
|
+
}
|
|
118
|
+
// Risk level
|
|
119
|
+
if (policy.riskLevel && config.agent) {
|
|
120
|
+
config.agent.riskLevel = policy.riskLevel;
|
|
121
|
+
}
|
|
122
|
+
// Command policy — arrays replace, not append
|
|
123
|
+
if (policy.commandPolicy && config.agent) {
|
|
124
|
+
if (policy.commandPolicy.mode) {
|
|
125
|
+
config.agent.commandPolicy.mode = policy.commandPolicy.mode;
|
|
126
|
+
}
|
|
127
|
+
if (policy.commandPolicy.blockedPatterns) {
|
|
128
|
+
config.agent.commandPolicy.blockedPatterns = policy.commandPolicy.blockedPatterns;
|
|
129
|
+
}
|
|
130
|
+
if (policy.commandPolicy.requireApproval) {
|
|
131
|
+
config.agent.commandPolicy.requireApproval = policy.commandPolicy.requireApproval;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Scan settings
|
|
135
|
+
if (policy.scan && config.agent) {
|
|
136
|
+
if (!config.agent.scan)
|
|
137
|
+
config.agent.scan = {};
|
|
138
|
+
if (policy.scan.excludePaths) {
|
|
139
|
+
config.agent.scan.excludePaths = policy.scan.excludePaths;
|
|
140
|
+
}
|
|
141
|
+
if (policy.scan.customPatterns) {
|
|
142
|
+
config.agent.scan.customPatterns = policy.scan.customPatterns;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Audit settings
|
|
146
|
+
if (policy.audit && config.agent) {
|
|
147
|
+
if (policy.audit.retentionDays != null) {
|
|
148
|
+
config.agent.audit.retentionDays = policy.audit.retentionDays;
|
|
149
|
+
}
|
|
150
|
+
if (policy.audit.logLevel) {
|
|
151
|
+
config.agent.audit.logLevel = policy.audit.logLevel;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return config;
|
|
155
|
+
}
|
|
104
156
|
/**
|
|
105
157
|
* Check if config exists
|
|
106
158
|
*/
|