@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.
- package/README.md +475 -5
- package/dist/commands/agent/audit-skill.js +199 -0
- package/dist/commands/agent/audit.js +65 -0
- package/dist/commands/agent/config.js +54 -0
- package/dist/commands/agent/exec.js +120 -0
- package/dist/commands/agent/index.js +21 -0
- package/dist/commands/agent/init.js +162 -0
- package/dist/commands/agent/install-hook.js +128 -0
- package/dist/commands/agent/scan.js +233 -0
- package/dist/commands/backend/get.js +41 -0
- package/dist/commands/backend/run.js +84 -0
- package/dist/commands/backend/scan-status.js +67 -0
- package/dist/commands/backend/usage.js +23 -0
- package/dist/core/audit-logger.js +203 -0
- package/dist/core/command-interceptor.js +165 -0
- package/dist/core/config-defaults.js +71 -0
- package/dist/core/config-manager.js +147 -0
- package/dist/core/config-schema.js +1 -0
- package/dist/core/pattern-engine.js +105 -0
- package/dist/index.js +17 -272
- package/dist/scanners/gitleaks.js +205 -0
- package/dist/scanners/regex-scanner.js +125 -0
- package/dist/scanners/secret-patterns.js +155 -0
- package/dist/utils/api.js +20 -0
- package/dist/utils/binary-manager.js +243 -0
- package/dist/utils/git.js +52 -0
- package/dist/utils/skill-manager.js +346 -0
- package/dist/utils/update-checker.js +93 -0
- package/package.json +13 -8
|
@@ -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
|
+
}
|