-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsanitizer.v
More file actions
295 lines (248 loc) · 8.33 KB
/
sanitizer.v
File metadata and controls
295 lines (248 loc) · 8.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
// Logging sanitizer to prevent sensitive data leakage
// Masks API keys, account numbers, tokens, and other confidential information
module logger
import regex
import x.json2
// MaskingStrategy defines how sensitive information should be masked
pub enum MaskingStrategy {
full // Complete masking (********)
partial // Partial masking (show first/last few chars)
none // No masking
}
// SanitizerConfig defines the sanitization behavior
pub struct SanitizerConfig {
pub mut:
enabled bool
api_key_strategy MaskingStrategy
private_key_strategy MaskingStrategy
account_strategy MaskingStrategy
password_strategy MaskingStrategy
custom_patterns []SensitivePattern
}
// SensitivePattern defines a custom pattern to sanitize
pub struct SensitivePattern {
pub:
name string
pattern string
strategy MaskingStrategy
}
// default_sanitizer_config returns production-safe defaults
pub fn default_sanitizer_config() SanitizerConfig {
return SanitizerConfig{
enabled: true
api_key_strategy: .partial
private_key_strategy: .partial
account_strategy: .partial
password_strategy: .full
}
}
// disabled_sanitizer_config returns a config with sanitization disabled
pub fn disabled_sanitizer_config() SanitizerConfig {
return SanitizerConfig{
enabled: false
}
}
// sanitize_string removes sensitive information from a string
pub fn sanitize_string(message string, conf SanitizerConfig) string {
if !conf.enabled {
return message
}
mut result := message
// Process private keys first (64-char hex) before general API keys (32-63 char hex)
result = sanitize_private_keys(result, conf.private_key_strategy)
result = sanitize_api_keys(result, conf.api_key_strategy)
result = sanitize_account_numbers(result, conf.account_strategy)
result = sanitize_passwords(result, conf.password_strategy)
for pattern in conf.custom_patterns {
result = sanitize_with_pattern(result, pattern)
}
return result
}
// sanitize_json_value recursively sanitizes a JSON value
pub fn sanitize_json_value(value json2.Any, conf SanitizerConfig) json2.Any {
if !conf.enabled {
return value
}
// Sanitize strings
if value is string {
return json2.Any(sanitize_string(value, conf))
}
// Recursively sanitize map values
if value is map[string]json2.Any {
mut m := value as map[string]json2.Any
mut sanitized := map[string]json2.Any{}
for key, val in m {
// Special handling for known sensitive keys
sanitized[key] = sanitize_json_value(val, conf)
}
return json2.Any(sanitized)
}
// Recursively sanitize array elements
if value is []json2.Any {
arr := value as []json2.Any
mut sanitized := []json2.Any{}
for elem in arr {
sanitized << sanitize_json_value(elem, conf)
}
return json2.Any(sanitized)
}
// Return other types as-is
return value
}
// sanitize_api_keys detects and masks API keys
fn sanitize_api_keys(message string, strategy MaskingStrategy) string {
if strategy == .none {
return message
}
mut result := message
// UUID pattern (API keys often use UUID format)
mut uuid_re := regex.regex_opt(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}') or {
return result
}
result = uuid_re.replace_by_fn(result, fn [strategy] (re regex.RE, in_txt string, start int, end int) string {
matched := in_txt[start..end]
return apply_mask(matched, strategy)
})
// Alpaca-style API keys (AK...)
mut alpaca_re := regex.regex_opt(r'AK[A-Z0-9]{18,22}') or { return result }
result = alpaca_re.replace_by_fn(result, fn [strategy] (re regex.RE, in_txt string, start int, end int) string {
matched := in_txt[start..end]
return apply_mask(matched, strategy)
})
// Long hex strings (32-63 chars - secrets, but not 64-char ETH private keys)
mut hex_re := regex.regex_opt(r'[0-9a-fA-F]{32,63}') or { return result }
result = hex_re.replace_by_fn(result, fn [strategy] (re regex.RE, in_txt string, start int, end int) string {
matched := in_txt[start..end]
return apply_mask(matched, strategy)
})
return result
}
// sanitize_account_numbers detects and masks account numbers
fn sanitize_account_numbers(message string, strategy MaskingStrategy) string {
if strategy == .none {
return message
}
mut result := message
account_patterns := [
r'account[ ]*[:=]?[ ]*[0-9]{6,20}',
r'acc[ ]*[:=]?[ ]*[0-9]{6,20}',
r'acct[ ]*[:=]?[ ]*[0-9]{6,20}',
r'ACCOUNT[ ]*[:=]?[ ]*[0-9]{6,20}',
r'ACC[ ]*[:=]?[ ]*[0-9]{6,20}',
r'ACCT[ ]*[:=]?[ ]*[0-9]{6,20}',
]
for pattern in account_patterns {
mut account_re := regex.regex_opt(pattern) or { continue }
result = account_re.replace_by_fn(result, fn [strategy] (re regex.RE, in_txt string, start int, end int) string {
matched := in_txt[start..end]
return apply_mask(matched, strategy)
})
}
return result
}
// sanitize_passwords detects and masks password fields
fn sanitize_passwords(message string, strategy MaskingStrategy) string {
if strategy == .none {
return message
}
mut result := message
pass_patterns := [
r'password[ ]*[:=][ ]*[^\s]+',
r'pwd[ ]*[:=][ ]*[^\s]+',
r'PASSWORD[ ]*[:=][ ]*[^\s]+',
r'PWD[ ]*[:=][ ]*[^\s]+',
r'Password[ ]*[:=][ ]*[^\s]+',
r'Pwd[ ]*[:=][ ]*[^\s]+',
]
for pattern in pass_patterns {
mut pass_re := regex.regex_opt(pattern) or { continue }
result = pass_re.replace_by_fn(result, fn [strategy] (re regex.RE, in_txt string, start int, end int) string {
matched := in_txt[start..end]
return apply_mask(matched, strategy)
})
}
secret_patterns := [
r'secret[ ]*[=:][ ]*[^\s]+',
r'api_secret[ ]*[=:][ ]*[^\s]+',
r'access_key[ ]*[=:][ ]*[^\s]+',
r'SECRET[ ]*[=:][ ]*[^\s]+',
r'API_SECRET[ ]*[=:][ ]*[^\s]+',
r'ACCESS_KEY[ ]*[=:][ ]*[^\s]+',
]
for pattern in secret_patterns {
mut secret_re := regex.regex_opt(pattern) or { continue }
result = secret_re.replace_by_fn(result, fn [strategy] (re regex.RE, in_txt string, start int, end int) string {
matched := in_txt[start..end]
return apply_mask(matched, strategy)
})
}
return result
}
// sanitize_private_keys detects and masks private key blocks
fn sanitize_private_keys(message string, strategy MaskingStrategy) string {
if strategy == .none {
return message
}
mut result := message
// PEM blocks
if result.contains('BEGIN RSA PRIVATE KEY') && result.contains('END RSA PRIVATE KEY') {
result = mask_pem_block(result, 'RSA PRIVATE KEY', strategy)
}
if result.contains('BEGIN EC PRIVATE KEY') && result.contains('END EC PRIVATE KEY') {
result = mask_pem_block(result, 'EC PRIVATE KEY', strategy)
}
if result.contains('BEGIN PRIVATE KEY') && result.contains('END PRIVATE KEY') {
result = mask_pem_block(result, 'PRIVATE KEY', strategy)
}
// Ethereum 64-char private keys
mut eth_re := regex.regex_opt(r'[0-9a-fA-F]{64}') or { return result }
result = eth_re.replace_by_fn(result, fn [strategy] (re regex.RE, in_txt string, start int, end int) string {
matched := in_txt[start..end]
return apply_mask(matched, strategy)
})
return result
}
// mask_pem_block masks a PEM-encoded key block
fn mask_pem_block(message string, block_type string, strategy MaskingStrategy) string {
lines := message.split('\n')
mut start_idx := -1
mut end_idx := -1
for i, line in lines {
if line.contains('BEGIN ' + block_type) {
start_idx = i
} else if line.contains('END ' + block_type) {
end_idx = i
break
}
}
if start_idx >= 0 && end_idx > start_idx {
mut new_lines := []string{}
new_lines << lines[..start_idx]
new_lines << '-----BEGIN ' + block_type + '-----'
new_lines << apply_mask('[KEY CONTENT REDACTED]', strategy)
new_lines << '-----END ' + block_type + '-----'
new_lines << lines[end_idx + 1..]
return new_lines.join('\n')
}
return message
}
// sanitize_with_pattern applies a custom sensitive pattern
fn sanitize_with_pattern(message string, pattern SensitivePattern) string {
mut re := regex.regex_opt(pattern.pattern) or { return message }
return re.replace_simple(message, apply_mask(pattern.name.to_upper(), pattern.strategy))
}
// apply_mask applies the masking strategy to a value
fn apply_mask(original string, strategy MaskingStrategy) string {
return match strategy {
.full { '********' }
.partial { mask_partial(original) }
.none { original }
}
}
// mask_partial masks a value partially, showing first 3 and last 3 chars
fn mask_partial(value string) string {
if value.len <= 6 {
return '***'
}
return value[..3] + '********' + value[value.len - 3..]
}