@rafter-security/cli 0.1.0 → 0.4.0

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.
@@ -0,0 +1,203 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { getAuditLogPath } from "./config-defaults.js";
4
+ import { ConfigManager } from "./config-manager.js";
5
+ export class AuditLogger {
6
+ constructor(logPath) {
7
+ this.logPath = logPath || getAuditLogPath();
8
+ this.sessionId = this.generateSessionId();
9
+ this.configManager = new ConfigManager();
10
+ // Ensure log directory exists
11
+ const dir = path.dirname(this.logPath);
12
+ if (!fs.existsSync(dir)) {
13
+ fs.mkdirSync(dir, { recursive: true });
14
+ }
15
+ }
16
+ /**
17
+ * Log an audit event
18
+ */
19
+ log(entry) {
20
+ const config = this.configManager.load();
21
+ // Check if logging is enabled
22
+ if (!config.agent?.audit.logAllActions) {
23
+ return;
24
+ }
25
+ const fullEntry = {
26
+ ...entry,
27
+ timestamp: new Date().toISOString(),
28
+ sessionId: this.sessionId
29
+ };
30
+ // Append to log file
31
+ const line = JSON.stringify(fullEntry) + "\n";
32
+ fs.appendFileSync(this.logPath, line, "utf-8");
33
+ }
34
+ /**
35
+ * Log a command interception
36
+ */
37
+ logCommandIntercepted(command, passed, actionTaken, reason, agentType) {
38
+ this.log({
39
+ eventType: "command_intercepted",
40
+ agentType,
41
+ action: {
42
+ command,
43
+ riskLevel: this.assessCommandRisk(command)
44
+ },
45
+ securityCheck: {
46
+ passed,
47
+ reason
48
+ },
49
+ resolution: {
50
+ actionTaken
51
+ }
52
+ });
53
+ }
54
+ /**
55
+ * Log a secret detection
56
+ */
57
+ logSecretDetected(location, secretType, actionTaken, agentType) {
58
+ this.log({
59
+ eventType: "secret_detected",
60
+ agentType,
61
+ action: {
62
+ riskLevel: "critical"
63
+ },
64
+ securityCheck: {
65
+ passed: false,
66
+ reason: `${secretType} detected in ${location}`
67
+ },
68
+ resolution: {
69
+ actionTaken
70
+ }
71
+ });
72
+ }
73
+ /**
74
+ * Log content sanitization
75
+ */
76
+ logContentSanitized(contentType, patternsMatched, agentType) {
77
+ this.log({
78
+ eventType: "content_sanitized",
79
+ agentType,
80
+ securityCheck: {
81
+ passed: false,
82
+ reason: `${patternsMatched} sensitive patterns detected`,
83
+ details: { contentType, patternsMatched }
84
+ },
85
+ resolution: {
86
+ actionTaken: "redacted"
87
+ }
88
+ });
89
+ }
90
+ /**
91
+ * Log a policy override
92
+ */
93
+ logPolicyOverride(reason, command, agentType) {
94
+ this.log({
95
+ eventType: "policy_override",
96
+ agentType,
97
+ action: {
98
+ command,
99
+ riskLevel: "high"
100
+ },
101
+ securityCheck: {
102
+ passed: false,
103
+ reason: "Security policy overridden by user"
104
+ },
105
+ resolution: {
106
+ actionTaken: "overridden",
107
+ overrideReason: reason
108
+ }
109
+ });
110
+ }
111
+ /**
112
+ * Read audit log entries
113
+ */
114
+ read(filter) {
115
+ if (!fs.existsSync(this.logPath)) {
116
+ return [];
117
+ }
118
+ const content = fs.readFileSync(this.logPath, "utf-8");
119
+ const lines = content.split("\n").filter(line => line.trim());
120
+ let entries = lines.map(line => {
121
+ try {
122
+ return JSON.parse(line);
123
+ }
124
+ catch {
125
+ return null;
126
+ }
127
+ }).filter(entry => entry !== null);
128
+ // Apply filters
129
+ if (filter) {
130
+ if (filter.eventType) {
131
+ entries = entries.filter(e => e.eventType === filter.eventType);
132
+ }
133
+ if (filter.agentType) {
134
+ entries = entries.filter(e => e.agentType === filter.agentType);
135
+ }
136
+ if (filter.since) {
137
+ entries = entries.filter(e => new Date(e.timestamp) >= filter.since);
138
+ }
139
+ if (filter.limit) {
140
+ entries = entries.slice(-filter.limit);
141
+ }
142
+ }
143
+ return entries;
144
+ }
145
+ /**
146
+ * Clean up old log entries based on retention policy
147
+ */
148
+ cleanup() {
149
+ const config = this.configManager.load();
150
+ const retentionDays = config.agent?.audit.retentionDays || 30;
151
+ const cutoffDate = new Date();
152
+ cutoffDate.setDate(cutoffDate.getDate() - retentionDays);
153
+ const entries = this.read();
154
+ const filtered = entries.filter(e => new Date(e.timestamp) >= cutoffDate);
155
+ // Rewrite log file with only retained entries
156
+ const content = filtered.map(e => JSON.stringify(e)).join("\n") + "\n";
157
+ fs.writeFileSync(this.logPath, content, "utf-8");
158
+ }
159
+ /**
160
+ * Generate a unique session ID
161
+ */
162
+ generateSessionId() {
163
+ return `${Date.now()}-${Math.random().toString(36).substring(7)}`;
164
+ }
165
+ /**
166
+ * Assess risk level of a command
167
+ */
168
+ assessCommandRisk(command) {
169
+ const critical = [
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";
202
+ }
203
+ }
@@ -0,0 +1,165 @@
1
+ import { ConfigManager } from "./config-manager.js";
2
+ import { AuditLogger } from "./audit-logger.js";
3
+ export class CommandInterceptor {
4
+ constructor() {
5
+ this.config = new ConfigManager();
6
+ this.audit = new AuditLogger();
7
+ }
8
+ /**
9
+ * Evaluate if a command should be allowed
10
+ */
11
+ evaluate(command) {
12
+ const cfg = this.config.load();
13
+ const policy = cfg.agent?.commandPolicy;
14
+ if (!policy) {
15
+ // No policy configured, allow by default
16
+ return {
17
+ command,
18
+ riskLevel: "low",
19
+ allowed: true,
20
+ requiresApproval: false
21
+ };
22
+ }
23
+ // Check blocked patterns (always block)
24
+ for (const pattern of policy.blockedPatterns) {
25
+ if (this.matchesPattern(command, pattern)) {
26
+ return {
27
+ command,
28
+ riskLevel: "critical",
29
+ allowed: false,
30
+ requiresApproval: false,
31
+ reason: `Matches blocked pattern: ${pattern}`,
32
+ matchedPattern: pattern
33
+ };
34
+ }
35
+ }
36
+ // Check approval patterns
37
+ for (const pattern of policy.requireApproval) {
38
+ if (this.matchesPattern(command, pattern)) {
39
+ const riskLevel = this.assessRisk(command);
40
+ return {
41
+ command,
42
+ riskLevel,
43
+ allowed: false,
44
+ requiresApproval: true,
45
+ reason: `Matches approval pattern: ${pattern}`,
46
+ matchedPattern: pattern
47
+ };
48
+ }
49
+ }
50
+ // Check policy mode
51
+ if (policy.mode === "deny-list") {
52
+ // If not in blocked or approval lists, allow
53
+ return {
54
+ command,
55
+ riskLevel: this.assessRisk(command),
56
+ allowed: true,
57
+ requiresApproval: false
58
+ };
59
+ }
60
+ else if (policy.mode === "approve-dangerous") {
61
+ // Assess risk and require approval for high/critical
62
+ const riskLevel = this.assessRisk(command);
63
+ if (riskLevel === "high" || riskLevel === "critical") {
64
+ return {
65
+ command,
66
+ riskLevel,
67
+ allowed: false,
68
+ requiresApproval: true,
69
+ reason: `High risk command requires approval`
70
+ };
71
+ }
72
+ return {
73
+ command,
74
+ riskLevel,
75
+ allowed: true,
76
+ requiresApproval: false
77
+ };
78
+ }
79
+ else if (policy.mode === "allow-all") {
80
+ return {
81
+ command,
82
+ riskLevel: this.assessRisk(command),
83
+ allowed: true,
84
+ requiresApproval: false
85
+ };
86
+ }
87
+ // Default: allow
88
+ return {
89
+ command,
90
+ riskLevel: this.assessRisk(command),
91
+ allowed: true,
92
+ requiresApproval: false
93
+ };
94
+ }
95
+ /**
96
+ * Log command evaluation result
97
+ */
98
+ logEvaluation(evaluation, actionTaken) {
99
+ this.audit.logCommandIntercepted(evaluation.command, evaluation.allowed, actionTaken, evaluation.reason);
100
+ }
101
+ /**
102
+ * Match command against pattern
103
+ */
104
+ matchesPattern(command, pattern) {
105
+ try {
106
+ const regex = new RegExp(pattern);
107
+ return regex.test(command);
108
+ }
109
+ catch {
110
+ // If pattern is not valid regex, try exact match
111
+ return command.includes(pattern);
112
+ }
113
+ }
114
+ /**
115
+ * Assess risk level of command
116
+ */
117
+ assessRisk(command) {
118
+ // Critical patterns
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";
164
+ }
165
+ }
@@ -0,0 +1,71 @@
1
+ import os from "os";
2
+ import path from "path";
3
+ export const CONFIG_VERSION = "1.0.0";
4
+ export function getDefaultConfig() {
5
+ return {
6
+ version: CONFIG_VERSION,
7
+ initialized: new Date().toISOString(),
8
+ backend: {
9
+ endpoint: "https://rafter.so/api/"
10
+ },
11
+ agent: {
12
+ riskLevel: "moderate",
13
+ environments: {
14
+ openclaw: {
15
+ enabled: false,
16
+ skillPath: path.join(os.homedir(), ".openclaw", "skills", "rafter-security.md")
17
+ },
18
+ claudeCode: {
19
+ enabled: false,
20
+ mcpPath: path.join(os.homedir(), ".claude", "mcp", "rafter-security.json")
21
+ }
22
+ },
23
+ skills: {
24
+ autoUpdate: true,
25
+ installOnInit: true,
26
+ backupBeforeUpdate: true
27
+ },
28
+ commandPolicy: {
29
+ mode: "approve-dangerous",
30
+ blockedPatterns: [
31
+ "rm -rf /",
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
+ ]
44
+ },
45
+ outputFiltering: {
46
+ redactSecrets: true,
47
+ blockPatterns: true
48
+ },
49
+ audit: {
50
+ logAllActions: true,
51
+ retentionDays: 30,
52
+ logLevel: "info"
53
+ }
54
+ }
55
+ };
56
+ }
57
+ export function getRafterDir() {
58
+ return path.join(os.homedir(), ".rafter");
59
+ }
60
+ export function getConfigPath() {
61
+ return path.join(getRafterDir(), "config.json");
62
+ }
63
+ export function getAuditLogPath() {
64
+ return path.join(getRafterDir(), "audit.log");
65
+ }
66
+ export function getBinDir() {
67
+ return path.join(getRafterDir(), "bin");
68
+ }
69
+ export function getPatternsDir() {
70
+ return path.join(getRafterDir(), "patterns");
71
+ }
@@ -0,0 +1,147 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { getDefaultConfig, getConfigPath, getRafterDir, CONFIG_VERSION } from "./config-defaults.js";
4
+ export class ConfigManager {
5
+ constructor(configPath) {
6
+ this.configPath = configPath || getConfigPath();
7
+ }
8
+ /**
9
+ * Load config from disk, creating default if it doesn't exist
10
+ */
11
+ load() {
12
+ if (!fs.existsSync(this.configPath)) {
13
+ return getDefaultConfig();
14
+ }
15
+ try {
16
+ const content = fs.readFileSync(this.configPath, "utf-8");
17
+ const config = JSON.parse(content);
18
+ // Migrate config if needed
19
+ return this.migrate(config);
20
+ }
21
+ catch (e) {
22
+ console.error(`Failed to load config from ${this.configPath}: ${e}`);
23
+ return getDefaultConfig();
24
+ }
25
+ }
26
+ /**
27
+ * Save config to disk
28
+ */
29
+ save(config) {
30
+ // Ensure directory exists
31
+ const dir = path.dirname(this.configPath);
32
+ if (!fs.existsSync(dir)) {
33
+ fs.mkdirSync(dir, { recursive: true });
34
+ }
35
+ // Write config
36
+ fs.writeFileSync(this.configPath, JSON.stringify(config, null, 2), "utf-8");
37
+ }
38
+ /**
39
+ * Update specific config values
40
+ */
41
+ update(updates) {
42
+ const config = this.load();
43
+ const merged = this.deepMerge(config, updates);
44
+ this.save(merged);
45
+ return merged;
46
+ }
47
+ /**
48
+ * Get a specific config value by path (e.g., "agent.riskLevel")
49
+ */
50
+ get(keyPath) {
51
+ const config = this.load();
52
+ const keys = keyPath.split(".");
53
+ let value = config;
54
+ for (const key of keys) {
55
+ if (value && typeof value === "object" && key in value) {
56
+ value = value[key];
57
+ }
58
+ else {
59
+ return undefined;
60
+ }
61
+ }
62
+ return value;
63
+ }
64
+ /**
65
+ * Set a specific config value by path
66
+ */
67
+ set(keyPath, value) {
68
+ const config = this.load();
69
+ const keys = keyPath.split(".");
70
+ let current = config;
71
+ for (let i = 0; i < keys.length - 1; i++) {
72
+ const key = keys[i];
73
+ if (!(key in current)) {
74
+ current[key] = {};
75
+ }
76
+ current = current[key];
77
+ }
78
+ const lastKey = keys[keys.length - 1];
79
+ current[lastKey] = value;
80
+ this.save(config);
81
+ }
82
+ /**
83
+ * Initialize config directory structure
84
+ */
85
+ async initialize() {
86
+ const rafterDir = getRafterDir();
87
+ // Create directories
88
+ const dirs = [
89
+ rafterDir,
90
+ path.join(rafterDir, "bin"),
91
+ path.join(rafterDir, "patterns")
92
+ ];
93
+ for (const dir of dirs) {
94
+ if (!fs.existsSync(dir)) {
95
+ fs.mkdirSync(dir, { recursive: true });
96
+ }
97
+ }
98
+ // Create default config if it doesn't exist
99
+ if (!fs.existsSync(this.configPath)) {
100
+ const config = getDefaultConfig();
101
+ this.save(config);
102
+ }
103
+ }
104
+ /**
105
+ * Check if config exists
106
+ */
107
+ exists() {
108
+ return fs.existsSync(this.configPath);
109
+ }
110
+ /**
111
+ * Migrate config to latest version
112
+ */
113
+ migrate(config) {
114
+ // For now, just ensure version is current
115
+ // In future, handle version-specific migrations
116
+ if (config.version !== CONFIG_VERSION) {
117
+ config.version = CONFIG_VERSION;
118
+ this.save(config);
119
+ }
120
+ return config;
121
+ }
122
+ /**
123
+ * Deep merge two objects
124
+ */
125
+ deepMerge(target, source) {
126
+ const output = { ...target };
127
+ if (this.isObject(target) && this.isObject(source)) {
128
+ Object.keys(source).forEach(key => {
129
+ if (this.isObject(source[key])) {
130
+ if (!(key in target)) {
131
+ output[key] = source[key];
132
+ }
133
+ else {
134
+ output[key] = this.deepMerge(target[key], source[key]);
135
+ }
136
+ }
137
+ else {
138
+ output[key] = source[key];
139
+ }
140
+ });
141
+ }
142
+ return output;
143
+ }
144
+ isObject(item) {
145
+ return item && typeof item === "object" && !Array.isArray(item);
146
+ }
147
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,105 @@
1
+ export class PatternEngine {
2
+ constructor(patterns) {
3
+ this.patterns = patterns;
4
+ }
5
+ /**
6
+ * Scan text for pattern matches
7
+ */
8
+ scan(text) {
9
+ const matches = [];
10
+ for (const pattern of this.patterns) {
11
+ const regex = this.createRegex(pattern.regex);
12
+ let match;
13
+ while ((match = regex.exec(text)) !== null) {
14
+ matches.push({
15
+ pattern,
16
+ match: match[0],
17
+ redacted: this.redact(match[0])
18
+ });
19
+ }
20
+ }
21
+ return matches;
22
+ }
23
+ /**
24
+ * Scan text with line/column information
25
+ */
26
+ scanWithPosition(text) {
27
+ const matches = [];
28
+ const lines = text.split("\n");
29
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
30
+ const line = lines[lineNum];
31
+ for (const pattern of this.patterns) {
32
+ const regex = this.createRegex(pattern.regex);
33
+ let match;
34
+ while ((match = regex.exec(line)) !== null) {
35
+ matches.push({
36
+ pattern,
37
+ match: match[0],
38
+ line: lineNum + 1,
39
+ column: match.index + 1,
40
+ redacted: this.redact(match[0])
41
+ });
42
+ }
43
+ }
44
+ }
45
+ return matches;
46
+ }
47
+ /**
48
+ * Redact text by replacing sensitive patterns
49
+ */
50
+ redactText(text) {
51
+ let redacted = text;
52
+ for (const pattern of this.patterns) {
53
+ const regex = this.createRegex(pattern.regex);
54
+ redacted = redacted.replace(regex, (match) => this.redact(match));
55
+ }
56
+ return redacted;
57
+ }
58
+ /**
59
+ * Check if text contains any sensitive patterns
60
+ */
61
+ hasMatches(text) {
62
+ return this.scan(text).length > 0;
63
+ }
64
+ /**
65
+ * Get patterns by severity
66
+ */
67
+ getPatternsBySeverity(severity) {
68
+ return this.patterns.filter(p => p.severity === severity);
69
+ }
70
+ /**
71
+ * Create RegExp from pattern string, extracting inline flags
72
+ */
73
+ createRegex(patternStr) {
74
+ // Extract inline flags like (?i) and convert to JS flags
75
+ let flags = "g";
76
+ let pattern = patternStr;
77
+ // Check for case-insensitive flag
78
+ if (pattern.startsWith("(?i)")) {
79
+ flags += "i";
80
+ pattern = pattern.substring(4);
81
+ }
82
+ try {
83
+ return new RegExp(pattern, flags);
84
+ }
85
+ catch (e) {
86
+ // If pattern is invalid, return a regex that matches nothing
87
+ console.error(`Invalid regex pattern: ${patternStr}`);
88
+ return /(?!)/;
89
+ }
90
+ }
91
+ /**
92
+ * Redact a single match
93
+ */
94
+ redact(match) {
95
+ if (match.length <= 8) {
96
+ return "*".repeat(match.length);
97
+ }
98
+ // Show first 4 and last 4 chars, redact middle
99
+ const visibleChars = 4;
100
+ const start = match.substring(0, visibleChars);
101
+ const end = match.substring(match.length - visibleChars);
102
+ const middle = "*".repeat(match.length - (visibleChars * 2));
103
+ return start + middle + end;
104
+ }
105
+ }