@uniglot/wont-let-you-see 0.2.0 → 0.3.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 +61 -6
- package/dist/index.js +113 -92
- package/package.json +1 -1
- package/patterns/aws.json +27 -0
- package/patterns/common.json +8 -0
- package/patterns/kubernetes.json +9 -0
- package/src/__tests__/masker.test.ts +64 -0
- package/src/__tests__/patterns.test.ts +135 -61
- package/src/masker.ts +62 -58
- package/src/patterns.ts +108 -45
package/README.md
CHANGED
|
@@ -28,11 +28,11 @@ Configure via environment variables or JSON config file. Environment variables t
|
|
|
28
28
|
|
|
29
29
|
#### Environment Variables
|
|
30
30
|
|
|
31
|
-
| Variable | Description
|
|
32
|
-
| ------------------------------------ |
|
|
33
|
-
| `WONT_LET_YOU_SEE_ENABLED` | Set to `false` or `0` to disable masking
|
|
34
|
-
| `WONT_LET_YOU_SEE_REVEALED_PATTERNS` | Comma-separated list of pattern types to reveal
|
|
35
|
-
| `WONT_LET_YOU_SEE_CUSTOM_PATTERNS` | Comma-separated list of custom
|
|
31
|
+
| Variable | Description | Default |
|
|
32
|
+
| ------------------------------------ | -------------------------------------------------------------------------- | ------- |
|
|
33
|
+
| `WONT_LET_YOU_SEE_ENABLED` | Set to `false` or `0` to disable masking | `true` |
|
|
34
|
+
| `WONT_LET_YOU_SEE_REVEALED_PATTERNS` | Comma-separated list of pattern types to reveal | (none) |
|
|
35
|
+
| `WONT_LET_YOU_SEE_CUSTOM_PATTERNS` | Comma-separated list of custom patterns to mask (supports `regex:` prefix) | (none) |
|
|
36
36
|
|
|
37
37
|
#### JSON Config File
|
|
38
38
|
|
|
@@ -48,6 +48,18 @@ Create `.wont-let-you-see.json` in your project root, `~/.config/opencode/`, or
|
|
|
48
48
|
|
|
49
49
|
> **Tip**: Add your AWS account ID to `customPatterns`. The built-in `account-id` pattern only matches contextual fields like `"OwnerId": "123456789012"`, but may miss bare account IDs in terraform output or other contexts.
|
|
50
50
|
|
|
51
|
+
Custom patterns support both literal strings and regular expressions. Prefix with `regex:` to use regex:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"customPatterns": [
|
|
56
|
+
"123456789012",
|
|
57
|
+
"my-secret-value",
|
|
58
|
+
"regex:secret-[a-z]{3}-\\d{4}"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
51
63
|
#### Examples
|
|
52
64
|
|
|
53
65
|
```bash
|
|
@@ -59,6 +71,9 @@ WONT_LET_YOU_SEE_REVEALED_PATTERNS=eks-cluster,ipv4 opencode
|
|
|
59
71
|
|
|
60
72
|
# Mask custom values (e.g., AWS account ID)
|
|
61
73
|
WONT_LET_YOU_SEE_CUSTOM_PATTERNS=123456789012,my-secret opencode
|
|
74
|
+
|
|
75
|
+
# Mask with regex patterns
|
|
76
|
+
WONT_LET_YOU_SEE_CUSTOM_PATTERNS="regex:token-[A-Z]{8},my-literal-secret" opencode
|
|
62
77
|
```
|
|
63
78
|
|
|
64
79
|
## Supported Commands
|
|
@@ -147,9 +162,49 @@ What was the actual VPC ID from the last command?
|
|
|
147
162
|
|
|
148
163
|
The LLM should only know the token (e.g., `#(vpc-1)`), not the real value.
|
|
149
164
|
|
|
165
|
+
## Contributing Patterns
|
|
166
|
+
|
|
167
|
+
Patterns are defined in JSON files under the `patterns/` directory:
|
|
168
|
+
|
|
169
|
+
- `patterns/aws.json` - AWS resource patterns
|
|
170
|
+
- `patterns/kubernetes.json` - Kubernetes patterns
|
|
171
|
+
- `patterns/common.json` - Common patterns (IPs, keys, etc.)
|
|
172
|
+
|
|
173
|
+
You can request to add new files for resources of different categories.
|
|
174
|
+
|
|
175
|
+
### Pattern Format
|
|
176
|
+
|
|
177
|
+
Each pattern can be defined as:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
{
|
|
181
|
+
"pattern-name": "^regex-pattern$",
|
|
182
|
+
|
|
183
|
+
"contextual-pattern": {
|
|
184
|
+
"pattern": "\"field\":\\s*\"(captured-value)\"",
|
|
185
|
+
"contextual": true
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
"literal-match": {
|
|
189
|
+
"exact": "literal-string-to-match"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
- **Simple regex**: Just a string with the regex pattern
|
|
195
|
+
- **Contextual**: For patterns where only a captured group should be masked (e.g., JSON fields)
|
|
196
|
+
- **Exact match**: For literal strings that should be escaped
|
|
197
|
+
|
|
198
|
+
### Adding New Patterns
|
|
199
|
+
|
|
200
|
+
1. Fork the repository
|
|
201
|
+
2. Add your pattern to the appropriate JSON file
|
|
202
|
+
3. Add tests in `src/__tests__/patterns.test.ts`
|
|
203
|
+
4. Submit a pull request
|
|
204
|
+
|
|
150
205
|
## Limitations
|
|
151
206
|
|
|
152
|
-
- **AWS only**: Currently supports AWS. GCP and Azure are not yet supported.
|
|
207
|
+
- **AWS only**: Currently supports AWS. GCP and Azure are not yet supported. Do you need them? Feel free to contribute!
|
|
153
208
|
- **S3 Buckets**: Bucket names are not masked (often public/intentional).
|
|
154
209
|
- **Account IDs**: Only masked in contextual JSON fields. Add to `customPatterns` for full coverage.
|
|
155
210
|
- **UI display**: The UI shows original values (OpenCode limitation).
|
package/dist/index.js
CHANGED
|
@@ -1,58 +1,82 @@
|
|
|
1
1
|
// src/patterns.ts
|
|
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
|
-
}
|
|
2
|
+
import { readdirSync, readFileSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
var cachedPatterns = null;
|
|
6
|
+
function getPackagePatternsDir() {
|
|
7
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
8
|
+
const srcDir = dirname(currentFile);
|
|
9
|
+
const packageRoot = dirname(srcDir);
|
|
10
|
+
return join(packageRoot, "patterns");
|
|
11
|
+
}
|
|
12
|
+
function parsePatternDefinition(name, definition) {
|
|
13
|
+
if (typeof definition === "string") {
|
|
14
|
+
return {
|
|
15
|
+
name,
|
|
16
|
+
pattern: new RegExp(definition),
|
|
17
|
+
isContextual: false,
|
|
18
|
+
isExact: false
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if ("exact" in definition) {
|
|
22
|
+
const escaped = definition.exact.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
|
+
return {
|
|
24
|
+
name,
|
|
25
|
+
pattern: new RegExp(escaped),
|
|
26
|
+
isContextual: false,
|
|
27
|
+
isExact: true
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
name,
|
|
32
|
+
pattern: new RegExp(definition.pattern),
|
|
33
|
+
isContextual: definition.contextual ?? false,
|
|
34
|
+
isExact: false
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function loadPatternFile(filePath) {
|
|
38
|
+
const content = readFileSync(filePath, "utf-8");
|
|
39
|
+
const data = JSON.parse(content);
|
|
40
|
+
const patterns = [];
|
|
41
|
+
for (const [name, definition] of Object.entries(data)) {
|
|
42
|
+
patterns.push(parsePatternDefinition(name, definition));
|
|
43
|
+
}
|
|
44
|
+
return patterns;
|
|
45
|
+
}
|
|
46
|
+
function loadPatterns() {
|
|
47
|
+
if (cachedPatterns) {
|
|
48
|
+
return cachedPatterns;
|
|
49
|
+
}
|
|
50
|
+
const patternsDir = getPackagePatternsDir();
|
|
51
|
+
const patterns = [];
|
|
52
|
+
const files = readdirSync(patternsDir).filter((f) => f.endsWith(".json")).sort();
|
|
53
|
+
for (const file of files) {
|
|
54
|
+
const filePath = join(patternsDir, file);
|
|
55
|
+
const filePatterns = loadPatternFile(filePath);
|
|
56
|
+
patterns.push(...filePatterns);
|
|
57
|
+
}
|
|
58
|
+
cachedPatterns = patterns;
|
|
59
|
+
return patterns;
|
|
60
|
+
}
|
|
37
61
|
|
|
38
62
|
// src/mapping.ts
|
|
39
63
|
import {
|
|
40
64
|
existsSync,
|
|
41
65
|
mkdirSync,
|
|
42
|
-
readFileSync,
|
|
66
|
+
readFileSync as readFileSync2,
|
|
43
67
|
writeFileSync,
|
|
44
68
|
renameSync
|
|
45
69
|
} from "fs";
|
|
46
|
-
import { join } from "path";
|
|
70
|
+
import { join as join2 } from "path";
|
|
47
71
|
import { homedir } from "os";
|
|
48
72
|
var sessionStates = new Map;
|
|
49
73
|
function getSessionPath(sessionId) {
|
|
50
74
|
const projectRoot = process.cwd();
|
|
51
|
-
const defaultPath =
|
|
52
|
-
if (existsSync(
|
|
75
|
+
const defaultPath = join2(projectRoot, ".opencode", "sessions", sessionId, "wont-let-you-see-mapping.json");
|
|
76
|
+
if (existsSync(join2(projectRoot, ".opencode")) || !existsSync(homedir())) {
|
|
53
77
|
return defaultPath;
|
|
54
78
|
}
|
|
55
|
-
return
|
|
79
|
+
return join2(homedir(), ".opencode", "sessions", sessionId, "wont-let-you-see-mapping.json");
|
|
56
80
|
}
|
|
57
81
|
function ensureSessionDir(sessionPath) {
|
|
58
82
|
const dir = sessionPath.substring(0, sessionPath.lastIndexOf("/"));
|
|
@@ -65,7 +89,7 @@ function getSessionState(sessionId) {
|
|
|
65
89
|
if (!state) {
|
|
66
90
|
const sessionPath = getSessionPath(sessionId);
|
|
67
91
|
if (existsSync(sessionPath)) {
|
|
68
|
-
const mapping = JSON.parse(
|
|
92
|
+
const mapping = JSON.parse(readFileSync2(sessionPath, "utf-8"));
|
|
69
93
|
const counters = {};
|
|
70
94
|
for (const token of Object.keys(mapping.entries)) {
|
|
71
95
|
const match = token.match(/^#\(([^-]+)-(\d+)\)$/);
|
|
@@ -118,7 +142,7 @@ import {
|
|
|
118
142
|
existsSync as nodeExistsSync,
|
|
119
143
|
readFileSync as nodeReadFileSync
|
|
120
144
|
} from "fs";
|
|
121
|
-
import { join as
|
|
145
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
122
146
|
import { homedir as homedir2 } from "os";
|
|
123
147
|
var fsAdapter = {
|
|
124
148
|
existsSync: nodeExistsSync,
|
|
@@ -134,14 +158,14 @@ function findConfigInAncestors(startDir) {
|
|
|
134
158
|
const home = homedir2();
|
|
135
159
|
let currentDir = startDir;
|
|
136
160
|
while (true) {
|
|
137
|
-
const configPath =
|
|
161
|
+
const configPath = join3(currentDir, CONFIG_FILENAME);
|
|
138
162
|
if (fsAdapter.existsSync(configPath)) {
|
|
139
163
|
return configPath;
|
|
140
164
|
}
|
|
141
165
|
if (currentDir === home) {
|
|
142
166
|
break;
|
|
143
167
|
}
|
|
144
|
-
const parentDir =
|
|
168
|
+
const parentDir = dirname2(currentDir);
|
|
145
169
|
if (parentDir === currentDir) {
|
|
146
170
|
break;
|
|
147
171
|
}
|
|
@@ -157,7 +181,7 @@ function loadJsonConfig() {
|
|
|
157
181
|
return JSON.parse(content);
|
|
158
182
|
} catch {}
|
|
159
183
|
}
|
|
160
|
-
const homeConfig =
|
|
184
|
+
const homeConfig = join3(homedir2(), CONFIG_FILENAME);
|
|
161
185
|
if (fsAdapter.existsSync(homeConfig)) {
|
|
162
186
|
try {
|
|
163
187
|
const content = fsAdapter.readFileSync(homeConfig, "utf-8");
|
|
@@ -205,37 +229,6 @@ function isPatternEnabled(patternType) {
|
|
|
205
229
|
}
|
|
206
230
|
|
|
207
231
|
// src/masker.ts
|
|
208
|
-
var PATTERN_ORDER = [
|
|
209
|
-
{ pattern: AWS_PATTERNS.eksCluster, type: "eks-cluster" },
|
|
210
|
-
{ pattern: AWS_PATTERNS.arn, type: "arn" },
|
|
211
|
-
{ pattern: AWS_PATTERNS.accountId, type: "account-id", isContextual: true },
|
|
212
|
-
{ pattern: AWS_PATTERNS.accessKeyId, type: "access-key-id" },
|
|
213
|
-
{ pattern: AWS_PATTERNS.vpc, type: "vpc" },
|
|
214
|
-
{ pattern: AWS_PATTERNS.subnet, type: "subnet" },
|
|
215
|
-
{ pattern: AWS_PATTERNS.securityGroup, type: "security-group" },
|
|
216
|
-
{ pattern: AWS_PATTERNS.internetGateway, type: "internet-gateway" },
|
|
217
|
-
{ pattern: AWS_PATTERNS.routeTable, type: "route-table" },
|
|
218
|
-
{ pattern: AWS_PATTERNS.natGateway, type: "nat-gateway" },
|
|
219
|
-
{ pattern: AWS_PATTERNS.networkAcl, type: "network-acl" },
|
|
220
|
-
{ pattern: AWS_PATTERNS.eni, type: "eni" },
|
|
221
|
-
{ pattern: AWS_PATTERNS.vpcEndpoint, type: "vpc-endpoint" },
|
|
222
|
-
{ pattern: AWS_PATTERNS.transitGateway, type: "transit-gateway" },
|
|
223
|
-
{ pattern: AWS_PATTERNS.customerGateway, type: "customer-gateway" },
|
|
224
|
-
{ pattern: AWS_PATTERNS.vpnGateway, type: "vpn-gateway" },
|
|
225
|
-
{ pattern: AWS_PATTERNS.vpnConnection, type: "vpn-connection" },
|
|
226
|
-
{ pattern: AWS_PATTERNS.ecrRepoUri, type: "ecr-repo" },
|
|
227
|
-
{ pattern: AWS_PATTERNS.ami, type: "ami" },
|
|
228
|
-
{ pattern: AWS_PATTERNS.ec2Instance, type: "ec2-instance" },
|
|
229
|
-
{ pattern: AWS_PATTERNS.ebs, type: "ebs" },
|
|
230
|
-
{ pattern: AWS_PATTERNS.snapshot, type: "snapshot" },
|
|
231
|
-
{ pattern: K8S_PATTERNS.serviceAccountToken, type: "k8s-token" },
|
|
232
|
-
{ pattern: K8S_PATTERNS.clusterEndpoint, type: "k8s-endpoint" },
|
|
233
|
-
{ pattern: K8S_PATTERNS.kubeconfigServer, type: "k8s-endpoint" },
|
|
234
|
-
{ pattern: K8S_PATTERNS.nodeNameAws, type: "k8s-node" },
|
|
235
|
-
{ pattern: COMMON_PATTERNS.privateKey, type: "private-key" },
|
|
236
|
-
{ pattern: COMMON_PATTERNS.apiKeyField, type: "api-key", isContextual: true },
|
|
237
|
-
{ pattern: COMMON_PATTERNS.ipv4, type: "ipv4" }
|
|
238
|
-
];
|
|
239
232
|
function removeAnchors(source) {
|
|
240
233
|
let result = source.replace(/^\^/, "").replace(/\$$/, "");
|
|
241
234
|
if (!result.endsWith("\\b")) {
|
|
@@ -243,28 +236,42 @@ function removeAnchors(source) {
|
|
|
243
236
|
}
|
|
244
237
|
return result;
|
|
245
238
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
return text;
|
|
249
|
-
}
|
|
250
|
-
const config = getConfig();
|
|
251
|
-
if (!config.enabled) {
|
|
252
|
-
return text;
|
|
253
|
-
}
|
|
239
|
+
var REGEX_PREFIX = "regex:";
|
|
240
|
+
function applyCustomPatterns(sessionId, text, customPatterns) {
|
|
254
241
|
let result = text;
|
|
255
|
-
for (const customPattern of
|
|
256
|
-
if (
|
|
257
|
-
const
|
|
258
|
-
|
|
242
|
+
for (const customPattern of customPatterns) {
|
|
243
|
+
if (customPattern.startsWith(REGEX_PREFIX)) {
|
|
244
|
+
const regexStr = customPattern.slice(REGEX_PREFIX.length);
|
|
245
|
+
const regex = new RegExp(removeAnchors(regexStr), "g");
|
|
246
|
+
const matches = new Set;
|
|
247
|
+
let match;
|
|
248
|
+
while ((match = regex.exec(result)) !== null) {
|
|
249
|
+
matches.add(match[0]);
|
|
250
|
+
}
|
|
251
|
+
for (const value of matches) {
|
|
252
|
+
const token = addEntry(sessionId, "custom", value);
|
|
253
|
+
result = result.split(value).join(token);
|
|
254
|
+
}
|
|
255
|
+
} else {
|
|
256
|
+
const escaped = customPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
257
|
+
const literalRegex = new RegExp(escaped + "(?![\\w])", "g");
|
|
258
|
+
if (literalRegex.test(result)) {
|
|
259
|
+
const token = addEntry(sessionId, "custom", customPattern);
|
|
260
|
+
result = result.replace(literalRegex, token);
|
|
261
|
+
}
|
|
259
262
|
}
|
|
260
263
|
}
|
|
261
|
-
|
|
262
|
-
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
function applyLoadedPatterns(sessionId, text, patterns) {
|
|
267
|
+
let result = text;
|
|
268
|
+
for (const { name, pattern, isContextual } of patterns) {
|
|
269
|
+
if (!isPatternEnabled(name)) {
|
|
263
270
|
continue;
|
|
264
271
|
}
|
|
265
272
|
if (isContextual) {
|
|
266
273
|
result = result.replace(new RegExp(pattern.source, "g"), (match, capturedValue) => {
|
|
267
|
-
const token = addEntry(sessionId,
|
|
274
|
+
const token = addEntry(sessionId, name, capturedValue);
|
|
268
275
|
return match.replace(capturedValue, token);
|
|
269
276
|
});
|
|
270
277
|
} else {
|
|
@@ -275,13 +282,27 @@ function mask(sessionId, text) {
|
|
|
275
282
|
matches.add(match[0]);
|
|
276
283
|
}
|
|
277
284
|
for (const value of matches) {
|
|
278
|
-
const token = addEntry(sessionId,
|
|
285
|
+
const token = addEntry(sessionId, name, value);
|
|
279
286
|
result = result.split(value).join(token);
|
|
280
287
|
}
|
|
281
288
|
}
|
|
282
289
|
}
|
|
283
290
|
return result;
|
|
284
291
|
}
|
|
292
|
+
function mask(sessionId, text) {
|
|
293
|
+
if (!text || typeof text !== "string") {
|
|
294
|
+
return text;
|
|
295
|
+
}
|
|
296
|
+
const config = getConfig();
|
|
297
|
+
if (!config.enabled) {
|
|
298
|
+
return text;
|
|
299
|
+
}
|
|
300
|
+
let result = text;
|
|
301
|
+
result = applyCustomPatterns(sessionId, result, config.customPatterns);
|
|
302
|
+
const patterns = loadPatterns();
|
|
303
|
+
result = applyLoadedPatterns(sessionId, result, patterns);
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
285
306
|
function unmask(sessionId, text) {
|
|
286
307
|
if (!text || typeof text !== "string") {
|
|
287
308
|
return text;
|
package/package.json
CHANGED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"eks-cluster": "^arn:(?:aws|aws-cn|aws-us-gov):eks:[a-z0-9-]+:[0-9]{12}:cluster\\/.+$",
|
|
3
|
+
"arn": "^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9-]+:[a-z0-9-]*:(?:[0-9]{12})?:.+$",
|
|
4
|
+
"account-id": {
|
|
5
|
+
"pattern": "\"(?:OwnerId|AccountId|Owner|account_id)\":\\s*\"(\\d{12})\"",
|
|
6
|
+
"contextual": true
|
|
7
|
+
},
|
|
8
|
+
"access-key-id": "(?:^|[^A-Z0-9])(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}(?:[^A-Z0-9]|$)",
|
|
9
|
+
"vpc": "^vpc-[0-9a-f]{8,17}$",
|
|
10
|
+
"subnet": "^subnet-[0-9a-f]{8,17}$",
|
|
11
|
+
"security-group": "^sg-[0-9a-f]{8,17}$",
|
|
12
|
+
"internet-gateway": "^igw-[0-9a-f]{8,17}$",
|
|
13
|
+
"route-table": "^rtb-[0-9a-f]{8,17}$",
|
|
14
|
+
"nat-gateway": "^nat-[0-9a-f]{8,17}$",
|
|
15
|
+
"network-acl": "^acl-[0-9a-f]{8,17}$",
|
|
16
|
+
"eni": "^eni-[0-9a-f]{8,17}$",
|
|
17
|
+
"vpc-endpoint": "^vpce-[0-9a-f]{8,17}$",
|
|
18
|
+
"transit-gateway": "^tgw-[0-9a-f]{8,17}$",
|
|
19
|
+
"customer-gateway": "^cgw-[0-9a-f]{8,17}$",
|
|
20
|
+
"vpn-gateway": "^vgw-[0-9a-f]{8,17}$",
|
|
21
|
+
"vpn-connection": "^vpn-[0-9a-f]{8,17}$",
|
|
22
|
+
"ecr-repo": "^(?:\\d{12}|#\\(custom-\\d+\\))\\.dkr\\.ecr\\.[a-z0-9-]+\\.amazonaws\\.com\\/[a-z0-9._\\/-]+$",
|
|
23
|
+
"ami": "^ami-[0-9a-f]{8,17}$",
|
|
24
|
+
"ec2-instance": "^i-[0-9a-f]{8,17}$",
|
|
25
|
+
"ebs": "^vol-[0-9a-f]{8,17}$",
|
|
26
|
+
"snapshot": "^snap-[0-9a-f]{8,17}$"
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private-key": "-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\\s\\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----",
|
|
3
|
+
"api-key": {
|
|
4
|
+
"pattern": "\"(?:api_key|apiKey|secret_key|secretKey|access_token|auth_token|password|token)\":\\s*\"([^\"]+)\"",
|
|
5
|
+
"contextual": true
|
|
6
|
+
},
|
|
7
|
+
"ipv4": "^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
|
|
8
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"k8s-token": "^eyJ[A-Za-z0-9_-]*\\.eyJ[A-Za-z0-9_-]*\\.[A-Za-z0-9_-]*$",
|
|
3
|
+
"k8s-endpoint": "^https:\\/\\/[A-Z0-9]+\\.[a-z0-9-]+\\.eks\\.amazonaws\\.com$",
|
|
4
|
+
"k8s-endpoint-kubeconfig": {
|
|
5
|
+
"pattern": "^https:\\/\\/[0-9A-F]{32}\\.[a-z]{2}-[a-z]+-\\d\\.eks\\.amazonaws\\.com$",
|
|
6
|
+
"contextual": false
|
|
7
|
+
},
|
|
8
|
+
"k8s-node": "^ip-(?:\\d{1,3}-){3}\\d{1,3}\\.[a-z0-9-]+\\.compute\\.internal$"
|
|
9
|
+
}
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
|
|
|
2
2
|
import { mask, unmask } from "../masker";
|
|
3
3
|
import { createMapping } from "../mapping";
|
|
4
4
|
import { resetConfig } from "../config";
|
|
5
|
+
import { resetPatternCache } from "../patterns";
|
|
5
6
|
|
|
6
7
|
describe("masker", () => {
|
|
7
8
|
const sessionId = "test-session-masker";
|
|
@@ -10,12 +11,14 @@ describe("masker", () => {
|
|
|
10
11
|
beforeEach(() => {
|
|
11
12
|
process.env.WONT_LET_YOU_SEE_REVEALED_PATTERNS = "";
|
|
12
13
|
resetConfig();
|
|
14
|
+
resetPatternCache();
|
|
13
15
|
createMapping(sessionId);
|
|
14
16
|
});
|
|
15
17
|
|
|
16
18
|
afterEach(() => {
|
|
17
19
|
process.env = { ...originalEnv };
|
|
18
20
|
resetConfig();
|
|
21
|
+
resetPatternCache();
|
|
19
22
|
});
|
|
20
23
|
|
|
21
24
|
describe("mask()", () => {
|
|
@@ -182,6 +185,67 @@ describe("masker", () => {
|
|
|
182
185
|
});
|
|
183
186
|
});
|
|
184
187
|
|
|
188
|
+
describe("customPatterns with regex support", () => {
|
|
189
|
+
it("should mask exact string patterns", () => {
|
|
190
|
+
process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS = "my-secret-value";
|
|
191
|
+
resetConfig();
|
|
192
|
+
|
|
193
|
+
const input = "Secret: my-secret-value";
|
|
194
|
+
const result = mask(sessionId, input);
|
|
195
|
+
|
|
196
|
+
expect(result).toMatch(/Secret: #\(custom-\d+\)/);
|
|
197
|
+
expect(result).not.toContain("my-secret-value");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("should mask regex patterns with regex: prefix", () => {
|
|
201
|
+
process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS =
|
|
202
|
+
"regex:secret-[a-z]{3}-\\d{4}";
|
|
203
|
+
resetConfig();
|
|
204
|
+
|
|
205
|
+
const input = "Keys: secret-abc-1234, secret-xyz-5678";
|
|
206
|
+
const result = mask(sessionId, input);
|
|
207
|
+
|
|
208
|
+
expect(result).toMatch(/#\(custom-\d+\)/);
|
|
209
|
+
expect(result).not.toContain("secret-abc-1234");
|
|
210
|
+
expect(result).not.toContain("secret-xyz-5678");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("should treat patterns without regex: prefix as literal strings", () => {
|
|
214
|
+
process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS = "literal-value";
|
|
215
|
+
resetConfig();
|
|
216
|
+
|
|
217
|
+
const input = "Value: literal-value, Other: literal-values";
|
|
218
|
+
const result = mask(sessionId, input);
|
|
219
|
+
|
|
220
|
+
expect(result).toMatch(/Value: #\(custom-\d+\)/);
|
|
221
|
+
expect(result).toContain("literal-values");
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("should round-trip custom regex patterns", () => {
|
|
225
|
+
process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS = "regex:token-[A-Z]{8}";
|
|
226
|
+
resetConfig();
|
|
227
|
+
|
|
228
|
+
const original = "Auth: token-ABCDEFGH";
|
|
229
|
+
const masked = mask(sessionId, original);
|
|
230
|
+
const unmasked = unmask(sessionId, masked);
|
|
231
|
+
|
|
232
|
+
expect(unmasked).toBe(original);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("should support multiple custom patterns including regex", () => {
|
|
236
|
+
process.env.WONT_LET_YOU_SEE_CUSTOM_PATTERNS =
|
|
237
|
+
"exact-secret,regex:pattern-\\d+";
|
|
238
|
+
resetConfig();
|
|
239
|
+
|
|
240
|
+
const input = "Secrets: exact-secret, pattern-123, pattern-456";
|
|
241
|
+
const result = mask(sessionId, input);
|
|
242
|
+
|
|
243
|
+
expect(result).not.toContain("exact-secret");
|
|
244
|
+
expect(result).not.toContain("pattern-123");
|
|
245
|
+
expect(result).not.toContain("pattern-456");
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
185
249
|
describe("round-trip integrity", () => {
|
|
186
250
|
it("should maintain round-trip integrity for VPC", () => {
|
|
187
251
|
const original = "vpc-1234567890abcdef0";
|
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { loadPatterns, resetPatternCache, getPatternByName } from "../patterns";
|
|
3
|
+
|
|
4
|
+
function getPattern(name: string) {
|
|
5
|
+
const p = getPatternByName(name);
|
|
6
|
+
if (!p) throw new Error(`Pattern '${name}' not found`);
|
|
7
|
+
return p.pattern;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe("AWS Patterns", () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
resetPatternCache();
|
|
13
|
+
});
|
|
3
14
|
|
|
4
|
-
describe("AWS_PATTERNS", () => {
|
|
5
15
|
describe("ARN", () => {
|
|
6
16
|
it("should match aws partition", () => {
|
|
7
17
|
expect(
|
|
8
|
-
|
|
18
|
+
getPattern("arn").test("arn:aws:iam::123456789012:user/admin"),
|
|
9
19
|
).toBe(true);
|
|
10
20
|
});
|
|
11
21
|
|
|
12
22
|
it("should match aws-cn partition", () => {
|
|
13
|
-
expect(
|
|
23
|
+
expect(getPattern("arn").test("arn:aws-cn:s3:::my-bucket")).toBe(true);
|
|
14
24
|
});
|
|
15
25
|
|
|
16
26
|
it("should match aws-us-gov partition", () => {
|
|
17
27
|
expect(
|
|
18
|
-
|
|
28
|
+
getPattern("arn").test(
|
|
19
29
|
"arn:aws-us-gov:ec2:us-gov-west-1:123456789012:instance/i-123",
|
|
20
30
|
),
|
|
21
31
|
).toBe(true);
|
|
@@ -24,80 +34,86 @@ describe("AWS_PATTERNS", () => {
|
|
|
24
34
|
|
|
25
35
|
describe("VPC resources", () => {
|
|
26
36
|
it("should match vpc", () => {
|
|
27
|
-
expect(
|
|
37
|
+
expect(getPattern("vpc").test("vpc-0123456789abcdef0")).toBe(true);
|
|
28
38
|
});
|
|
29
39
|
|
|
30
40
|
it("should match subnet", () => {
|
|
31
|
-
expect(
|
|
41
|
+
expect(getPattern("subnet").test("subnet-0123456789abcdef0")).toBe(true);
|
|
32
42
|
});
|
|
33
43
|
|
|
34
44
|
it("should match security group", () => {
|
|
35
|
-
expect(
|
|
45
|
+
expect(getPattern("security-group").test("sg-0123456789abcdef0")).toBe(
|
|
36
46
|
true,
|
|
37
47
|
);
|
|
38
48
|
});
|
|
39
49
|
|
|
40
50
|
it("should match nat gateway", () => {
|
|
41
|
-
expect(
|
|
51
|
+
expect(getPattern("nat-gateway").test("nat-0123456789abcdef0")).toBe(
|
|
52
|
+
true,
|
|
53
|
+
);
|
|
42
54
|
});
|
|
43
55
|
|
|
44
56
|
it("should match network acl", () => {
|
|
45
|
-
expect(
|
|
57
|
+
expect(getPattern("network-acl").test("acl-0123456789abcdef0")).toBe(
|
|
58
|
+
true,
|
|
59
|
+
);
|
|
46
60
|
});
|
|
47
61
|
|
|
48
62
|
it("should match eni", () => {
|
|
49
|
-
expect(
|
|
63
|
+
expect(getPattern("eni").test("eni-0123456789abcdef0")).toBe(true);
|
|
50
64
|
});
|
|
51
65
|
});
|
|
52
66
|
|
|
53
67
|
describe("EC2 resources", () => {
|
|
54
68
|
it("should match ebs volume", () => {
|
|
55
|
-
expect(
|
|
69
|
+
expect(getPattern("ebs").test("vol-0123456789abcdef0")).toBe(true);
|
|
56
70
|
});
|
|
57
71
|
|
|
58
72
|
it("should match snapshot", () => {
|
|
59
|
-
expect(
|
|
73
|
+
expect(getPattern("snapshot").test("snap-0123456789abcdef0")).toBe(true);
|
|
60
74
|
});
|
|
61
75
|
});
|
|
62
76
|
|
|
63
77
|
describe("VPC networking resources", () => {
|
|
64
78
|
it("should match vpc endpoint", () => {
|
|
65
|
-
expect(
|
|
79
|
+
expect(getPattern("vpc-endpoint").test("vpce-0123456789abcdef0")).toBe(
|
|
66
80
|
true,
|
|
67
81
|
);
|
|
68
82
|
});
|
|
69
83
|
|
|
70
84
|
it("should match transit gateway", () => {
|
|
71
|
-
expect(
|
|
85
|
+
expect(getPattern("transit-gateway").test("tgw-0123456789abcdef0")).toBe(
|
|
72
86
|
true,
|
|
73
87
|
);
|
|
74
88
|
});
|
|
75
89
|
|
|
76
90
|
it("should match customer gateway", () => {
|
|
77
|
-
expect(
|
|
91
|
+
expect(getPattern("customer-gateway").test("cgw-0123456789abcdef0")).toBe(
|
|
78
92
|
true,
|
|
79
93
|
);
|
|
80
94
|
});
|
|
81
95
|
|
|
82
96
|
it("should match vpn gateway", () => {
|
|
83
|
-
expect(
|
|
97
|
+
expect(getPattern("vpn-gateway").test("vgw-0123456789abcdef0")).toBe(
|
|
98
|
+
true,
|
|
99
|
+
);
|
|
84
100
|
});
|
|
85
101
|
|
|
86
102
|
it("should match vpn connection", () => {
|
|
87
|
-
expect(
|
|
103
|
+
expect(getPattern("vpn-connection").test("vpn-0123456789abcdef0")).toBe(
|
|
88
104
|
true,
|
|
89
105
|
);
|
|
90
106
|
});
|
|
91
107
|
|
|
92
108
|
it("should NOT match invalid vpc endpoint", () => {
|
|
93
|
-
expect(
|
|
109
|
+
expect(getPattern("vpc-endpoint").test("vpce-invalid")).toBe(false);
|
|
94
110
|
});
|
|
95
111
|
});
|
|
96
112
|
|
|
97
113
|
describe("ECR resources", () => {
|
|
98
114
|
it("should match ECR repo URI", () => {
|
|
99
115
|
expect(
|
|
100
|
-
|
|
116
|
+
getPattern("ecr-repo").test(
|
|
101
117
|
"123456789012.dkr.ecr.us-west-2.amazonaws.com/my-repo",
|
|
102
118
|
),
|
|
103
119
|
).toBe(true);
|
|
@@ -105,7 +121,7 @@ describe("AWS_PATTERNS", () => {
|
|
|
105
121
|
|
|
106
122
|
it("should match ECR repo URI with nested path", () => {
|
|
107
123
|
expect(
|
|
108
|
-
|
|
124
|
+
getPattern("ecr-repo").test(
|
|
109
125
|
"123456789012.dkr.ecr.eu-central-1.amazonaws.com/org/app/service",
|
|
110
126
|
),
|
|
111
127
|
).toBe(true);
|
|
@@ -113,7 +129,7 @@ describe("AWS_PATTERNS", () => {
|
|
|
113
129
|
|
|
114
130
|
it("should match ECR repo URI with dots and underscores", () => {
|
|
115
131
|
expect(
|
|
116
|
-
|
|
132
|
+
getPattern("ecr-repo").test(
|
|
117
133
|
"123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/my_repo.name",
|
|
118
134
|
),
|
|
119
135
|
).toBe(true);
|
|
@@ -121,21 +137,21 @@ describe("AWS_PATTERNS", () => {
|
|
|
121
137
|
|
|
122
138
|
it("should NOT match invalid ECR URI (wrong account ID length)", () => {
|
|
123
139
|
expect(
|
|
124
|
-
|
|
140
|
+
getPattern("ecr-repo").test(
|
|
125
141
|
"12345.dkr.ecr.us-west-2.amazonaws.com/my-repo",
|
|
126
142
|
),
|
|
127
143
|
).toBe(false);
|
|
128
144
|
});
|
|
129
145
|
|
|
130
146
|
it("should NOT match non-ECR docker registry", () => {
|
|
131
|
-
expect(
|
|
147
|
+
expect(getPattern("ecr-repo").test("docker.io/library/nginx")).toBe(
|
|
132
148
|
false,
|
|
133
149
|
);
|
|
134
150
|
});
|
|
135
151
|
|
|
136
152
|
it("should match ECR repo URI with masked account ID", () => {
|
|
137
153
|
expect(
|
|
138
|
-
|
|
154
|
+
getPattern("ecr-repo").test(
|
|
139
155
|
"#(custom-1).dkr.ecr.us-west-2.amazonaws.com/my-repo",
|
|
140
156
|
),
|
|
141
157
|
).toBe(true);
|
|
@@ -143,7 +159,7 @@ describe("AWS_PATTERNS", () => {
|
|
|
143
159
|
|
|
144
160
|
it("should match ECR repo URI with masked account ID (higher number)", () => {
|
|
145
161
|
expect(
|
|
146
|
-
|
|
162
|
+
getPattern("ecr-repo").test(
|
|
147
163
|
"#(custom-123).dkr.ecr.us-east-1.amazonaws.com/app/service",
|
|
148
164
|
),
|
|
149
165
|
).toBe(true);
|
|
@@ -152,56 +168,60 @@ describe("AWS_PATTERNS", () => {
|
|
|
152
168
|
|
|
153
169
|
describe("Account ID (contextual)", () => {
|
|
154
170
|
it("should match OwnerId field", () => {
|
|
155
|
-
expect(
|
|
171
|
+
expect(getPattern("account-id").test('"OwnerId": "123456789012"')).toBe(
|
|
156
172
|
true,
|
|
157
173
|
);
|
|
158
174
|
});
|
|
159
175
|
|
|
160
176
|
it("should match account_id field (terraform style)", () => {
|
|
161
|
-
expect(
|
|
162
|
-
|
|
163
|
-
);
|
|
177
|
+
expect(
|
|
178
|
+
getPattern("account-id").test('"account_id": "123456789012"'),
|
|
179
|
+
).toBe(true);
|
|
164
180
|
});
|
|
165
181
|
|
|
166
182
|
it("should NOT match bare number", () => {
|
|
167
|
-
expect(
|
|
183
|
+
expect(getPattern("account-id").test("123456789012")).toBe(false);
|
|
168
184
|
});
|
|
169
185
|
});
|
|
170
186
|
|
|
171
187
|
describe("Access Key ID", () => {
|
|
172
188
|
it("should match AKIA prefix", () => {
|
|
173
|
-
expect(
|
|
189
|
+
expect(getPattern("access-key-id").test(" AKIAIOSFODNN7EXAMPLE ")).toBe(
|
|
174
190
|
true,
|
|
175
191
|
);
|
|
176
192
|
});
|
|
177
193
|
|
|
178
194
|
it("should match ASIA prefix (temporary)", () => {
|
|
179
|
-
expect(
|
|
195
|
+
expect(getPattern("access-key-id").test(" ASIAISAMPLEKEYID1234 ")).toBe(
|
|
180
196
|
true,
|
|
181
197
|
);
|
|
182
198
|
});
|
|
183
199
|
|
|
184
200
|
it("should NOT match random string", () => {
|
|
185
|
-
expect(
|
|
201
|
+
expect(getPattern("access-key-id").test("RANDOMSTRING12345678")).toBe(
|
|
202
|
+
false,
|
|
203
|
+
);
|
|
186
204
|
});
|
|
187
205
|
});
|
|
188
206
|
});
|
|
189
207
|
|
|
190
|
-
describe("
|
|
208
|
+
describe("Kubernetes Patterns", () => {
|
|
209
|
+
beforeEach(() => {
|
|
210
|
+
resetPatternCache();
|
|
211
|
+
});
|
|
212
|
+
|
|
191
213
|
describe("Service Account Token", () => {
|
|
192
214
|
it("should match JWT format", () => {
|
|
193
215
|
const jwt =
|
|
194
216
|
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlZmF1bHQtdG9rZW4tYWJjZGUiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVmYXVsdCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjEyMzQ1Njc4LTEyMzQtMTIzNC0xMjM0LTEyMzQ1Njc4OTAxMiIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlZmF1bHQifQ.signature";
|
|
195
|
-
expect(
|
|
217
|
+
expect(getPattern("k8s-token").test(jwt)).toBe(true);
|
|
196
218
|
});
|
|
197
219
|
});
|
|
198
220
|
|
|
199
221
|
describe("Node Names", () => {
|
|
200
222
|
it("should match AWS node name", () => {
|
|
201
223
|
expect(
|
|
202
|
-
|
|
203
|
-
"ip-10-0-1-123.us-west-2.compute.internal",
|
|
204
|
-
),
|
|
224
|
+
getPattern("k8s-node").test("ip-10-0-1-123.us-west-2.compute.internal"),
|
|
205
225
|
).toBe(true);
|
|
206
226
|
});
|
|
207
227
|
});
|
|
@@ -209,7 +229,7 @@ describe("K8S_PATTERNS", () => {
|
|
|
209
229
|
describe("Cluster Endpoints", () => {
|
|
210
230
|
it("should match EKS endpoint", () => {
|
|
211
231
|
expect(
|
|
212
|
-
|
|
232
|
+
getPattern("k8s-endpoint").test(
|
|
213
233
|
"https://ABCDEF1234567890ABCD.us-west-2.eks.amazonaws.com",
|
|
214
234
|
),
|
|
215
235
|
).toBe(true);
|
|
@@ -217,14 +237,18 @@ describe("K8S_PATTERNS", () => {
|
|
|
217
237
|
});
|
|
218
238
|
});
|
|
219
239
|
|
|
220
|
-
describe("
|
|
240
|
+
describe("Common Patterns", () => {
|
|
241
|
+
beforeEach(() => {
|
|
242
|
+
resetPatternCache();
|
|
243
|
+
});
|
|
244
|
+
|
|
221
245
|
describe("IPv4", () => {
|
|
222
246
|
it("should match IPv4", () => {
|
|
223
|
-
expect(
|
|
247
|
+
expect(getPattern("ipv4").test("192.168.1.1")).toBe(true);
|
|
224
248
|
});
|
|
225
249
|
|
|
226
250
|
it("should not match CIDR notation", () => {
|
|
227
|
-
expect(
|
|
251
|
+
expect(getPattern("ipv4").test("10.0.0.0/8")).toBe(false);
|
|
228
252
|
});
|
|
229
253
|
});
|
|
230
254
|
|
|
@@ -238,16 +262,16 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC
|
|
|
238
262
|
-----END PRIVATE KEY-----`;
|
|
239
263
|
|
|
240
264
|
it("should match entire RSA private key block", () => {
|
|
241
|
-
expect(
|
|
265
|
+
expect(getPattern("private-key").test(rsaKey)).toBe(true);
|
|
242
266
|
});
|
|
243
267
|
|
|
244
268
|
it("should match entire generic private key block", () => {
|
|
245
|
-
expect(
|
|
269
|
+
expect(getPattern("private-key").test(genericKey)).toBe(true);
|
|
246
270
|
});
|
|
247
271
|
|
|
248
272
|
it("should not match header only", () => {
|
|
249
273
|
expect(
|
|
250
|
-
|
|
274
|
+
getPattern("private-key").test("-----BEGIN RSA PRIVATE KEY-----"),
|
|
251
275
|
).toBe(false);
|
|
252
276
|
});
|
|
253
277
|
});
|
|
@@ -255,39 +279,89 @@ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC
|
|
|
255
279
|
describe("API Key Field (contextual)", () => {
|
|
256
280
|
it("should match api_key field", () => {
|
|
257
281
|
expect(
|
|
258
|
-
|
|
282
|
+
getPattern("api-key").test('"api_key": "sk-1234567890abcdef"'),
|
|
259
283
|
).toBe(true);
|
|
260
284
|
});
|
|
261
285
|
|
|
262
286
|
it("should match password field", () => {
|
|
263
|
-
expect(
|
|
264
|
-
|
|
265
|
-
)
|
|
287
|
+
expect(getPattern("api-key").test('"password": "supersecret123"')).toBe(
|
|
288
|
+
true,
|
|
289
|
+
);
|
|
266
290
|
});
|
|
267
291
|
|
|
268
292
|
it("should match token field", () => {
|
|
269
|
-
expect(
|
|
270
|
-
|
|
271
|
-
)
|
|
293
|
+
expect(getPattern("api-key").test('"token": "ghp_xxxxxxxxxxxx"')).toBe(
|
|
294
|
+
true,
|
|
295
|
+
);
|
|
272
296
|
});
|
|
273
297
|
});
|
|
274
298
|
});
|
|
275
299
|
|
|
276
300
|
describe("ReDoS safety", () => {
|
|
301
|
+
beforeEach(() => {
|
|
302
|
+
resetPatternCache();
|
|
303
|
+
});
|
|
304
|
+
|
|
277
305
|
it("should handle pathological input quickly", () => {
|
|
278
306
|
const pathological = "a".repeat(1000) + "b";
|
|
279
307
|
const start = performance.now();
|
|
280
308
|
|
|
281
|
-
const allPatterns =
|
|
282
|
-
|
|
283
|
-
...Object.values(K8S_PATTERNS),
|
|
284
|
-
...Object.values(COMMON_PATTERNS),
|
|
285
|
-
];
|
|
286
|
-
|
|
287
|
-
for (const pattern of allPatterns) {
|
|
309
|
+
const allPatterns = loadPatterns();
|
|
310
|
+
for (const { pattern } of allPatterns) {
|
|
288
311
|
pattern.test(pathological);
|
|
289
312
|
}
|
|
290
313
|
|
|
291
314
|
expect(performance.now() - start).toBeLessThan(100);
|
|
292
315
|
});
|
|
293
316
|
});
|
|
317
|
+
|
|
318
|
+
describe("loadPatterns", () => {
|
|
319
|
+
beforeEach(() => {
|
|
320
|
+
resetPatternCache();
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it("should load patterns from JSON files", () => {
|
|
324
|
+
const patterns = loadPatterns();
|
|
325
|
+
expect(patterns.length).toBeGreaterThan(0);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
it("should load AWS patterns", () => {
|
|
329
|
+
const patterns = loadPatterns();
|
|
330
|
+
const vpcPattern = patterns.find((p) => p.name === "vpc");
|
|
331
|
+
expect(vpcPattern).toBeDefined();
|
|
332
|
+
expect(vpcPattern!.pattern.test("vpc-1234567890abcdef0")).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should load Kubernetes patterns", () => {
|
|
336
|
+
const patterns = loadPatterns();
|
|
337
|
+
const k8sTokenPattern = patterns.find((p) => p.name === "k8s-token");
|
|
338
|
+
expect(k8sTokenPattern).toBeDefined();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("should load common patterns", () => {
|
|
342
|
+
const patterns = loadPatterns();
|
|
343
|
+
const ipv4Pattern = patterns.find((p) => p.name === "ipv4");
|
|
344
|
+
expect(ipv4Pattern).toBeDefined();
|
|
345
|
+
expect(ipv4Pattern!.pattern.test("192.168.1.1")).toBe(true);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it("should mark contextual patterns correctly", () => {
|
|
349
|
+
const patterns = loadPatterns();
|
|
350
|
+
const accountIdPattern = patterns.find((p) => p.name === "account-id");
|
|
351
|
+
expect(accountIdPattern).toBeDefined();
|
|
352
|
+
expect(accountIdPattern!.isContextual).toBe(true);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("should cache patterns after first load", () => {
|
|
356
|
+
const patterns1 = loadPatterns();
|
|
357
|
+
const patterns2 = loadPatterns();
|
|
358
|
+
expect(patterns1).toBe(patterns2);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("should reset cache on resetPatternCache()", () => {
|
|
362
|
+
const patterns1 = loadPatterns();
|
|
363
|
+
resetPatternCache();
|
|
364
|
+
const patterns2 = loadPatterns();
|
|
365
|
+
expect(patterns1).not.toBe(patterns2);
|
|
366
|
+
});
|
|
367
|
+
});
|
package/src/masker.ts
CHANGED
|
@@ -1,47 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { loadPatterns, type LoadedPattern } from "./patterns";
|
|
2
2
|
import { addEntry, getOriginal } from "./mapping";
|
|
3
3
|
import { getConfig, isPatternEnabled } from "./config";
|
|
4
4
|
|
|
5
|
-
interface PatternConfig {
|
|
6
|
-
pattern: RegExp;
|
|
7
|
-
type: string;
|
|
8
|
-
isContextual?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const PATTERN_ORDER: PatternConfig[] = [
|
|
12
|
-
{ pattern: AWS_PATTERNS.eksCluster, type: "eks-cluster" },
|
|
13
|
-
{ pattern: AWS_PATTERNS.arn, type: "arn" },
|
|
14
|
-
{ pattern: AWS_PATTERNS.accountId, type: "account-id", isContextual: true },
|
|
15
|
-
{ pattern: AWS_PATTERNS.accessKeyId, type: "access-key-id" },
|
|
16
|
-
{ pattern: AWS_PATTERNS.vpc, type: "vpc" },
|
|
17
|
-
{ pattern: AWS_PATTERNS.subnet, type: "subnet" },
|
|
18
|
-
{ pattern: AWS_PATTERNS.securityGroup, type: "security-group" },
|
|
19
|
-
{ pattern: AWS_PATTERNS.internetGateway, type: "internet-gateway" },
|
|
20
|
-
{ pattern: AWS_PATTERNS.routeTable, type: "route-table" },
|
|
21
|
-
{ pattern: AWS_PATTERNS.natGateway, type: "nat-gateway" },
|
|
22
|
-
{ pattern: AWS_PATTERNS.networkAcl, type: "network-acl" },
|
|
23
|
-
{ pattern: AWS_PATTERNS.eni, type: "eni" },
|
|
24
|
-
{ pattern: AWS_PATTERNS.vpcEndpoint, type: "vpc-endpoint" },
|
|
25
|
-
{ pattern: AWS_PATTERNS.transitGateway, type: "transit-gateway" },
|
|
26
|
-
{ pattern: AWS_PATTERNS.customerGateway, type: "customer-gateway" },
|
|
27
|
-
{ pattern: AWS_PATTERNS.vpnGateway, type: "vpn-gateway" },
|
|
28
|
-
{ pattern: AWS_PATTERNS.vpnConnection, type: "vpn-connection" },
|
|
29
|
-
{ pattern: AWS_PATTERNS.ecrRepoUri, type: "ecr-repo" },
|
|
30
|
-
{ pattern: AWS_PATTERNS.ami, type: "ami" },
|
|
31
|
-
{ pattern: AWS_PATTERNS.ec2Instance, type: "ec2-instance" },
|
|
32
|
-
{ pattern: AWS_PATTERNS.ebs, type: "ebs" },
|
|
33
|
-
{ pattern: AWS_PATTERNS.snapshot, type: "snapshot" },
|
|
34
|
-
|
|
35
|
-
{ pattern: K8S_PATTERNS.serviceAccountToken, type: "k8s-token" },
|
|
36
|
-
{ pattern: K8S_PATTERNS.clusterEndpoint, type: "k8s-endpoint" },
|
|
37
|
-
{ pattern: K8S_PATTERNS.kubeconfigServer, type: "k8s-endpoint" },
|
|
38
|
-
{ pattern: K8S_PATTERNS.nodeNameAws, type: "k8s-node" },
|
|
39
|
-
|
|
40
|
-
{ pattern: COMMON_PATTERNS.privateKey, type: "private-key" },
|
|
41
|
-
{ pattern: COMMON_PATTERNS.apiKeyField, type: "api-key", isContextual: true },
|
|
42
|
-
{ pattern: COMMON_PATTERNS.ipv4, type: "ipv4" },
|
|
43
|
-
];
|
|
44
|
-
|
|
45
5
|
function removeAnchors(source: string): string {
|
|
46
6
|
let result = source.replace(/^\^/, "").replace(/\$$/, "");
|
|
47
7
|
if (!result.endsWith("\\b")) {
|
|
@@ -50,34 +10,58 @@ function removeAnchors(source: string): string {
|
|
|
50
10
|
return result;
|
|
51
11
|
}
|
|
52
12
|
|
|
53
|
-
|
|
54
|
-
if (!text || typeof text !== "string") {
|
|
55
|
-
return text;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const config = getConfig();
|
|
59
|
-
if (!config.enabled) {
|
|
60
|
-
return text;
|
|
61
|
-
}
|
|
13
|
+
const REGEX_PREFIX = "regex:";
|
|
62
14
|
|
|
15
|
+
function applyCustomPatterns(
|
|
16
|
+
sessionId: string,
|
|
17
|
+
text: string,
|
|
18
|
+
customPatterns: string[],
|
|
19
|
+
): string {
|
|
63
20
|
let result = text;
|
|
64
21
|
|
|
65
|
-
for (const customPattern of
|
|
66
|
-
if (
|
|
67
|
-
const
|
|
68
|
-
|
|
22
|
+
for (const customPattern of customPatterns) {
|
|
23
|
+
if (customPattern.startsWith(REGEX_PREFIX)) {
|
|
24
|
+
const regexStr = customPattern.slice(REGEX_PREFIX.length);
|
|
25
|
+
const regex = new RegExp(removeAnchors(regexStr), "g");
|
|
26
|
+
const matches = new Set<string>();
|
|
27
|
+
let match;
|
|
28
|
+
while ((match = regex.exec(result)) !== null) {
|
|
29
|
+
matches.add(match[0]);
|
|
30
|
+
}
|
|
31
|
+
for (const value of matches) {
|
|
32
|
+
const token = addEntry(sessionId, "custom", value);
|
|
33
|
+
result = result.split(value).join(token);
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
const escaped = customPattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
37
|
+
const literalRegex = new RegExp(escaped + "(?![\\w])", "g");
|
|
38
|
+
if (literalRegex.test(result)) {
|
|
39
|
+
const token = addEntry(sessionId, "custom", customPattern);
|
|
40
|
+
result = result.replace(literalRegex, token);
|
|
41
|
+
}
|
|
69
42
|
}
|
|
70
43
|
}
|
|
71
44
|
|
|
72
|
-
|
|
73
|
-
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function applyLoadedPatterns(
|
|
49
|
+
sessionId: string,
|
|
50
|
+
text: string,
|
|
51
|
+
patterns: LoadedPattern[],
|
|
52
|
+
): string {
|
|
53
|
+
let result = text;
|
|
54
|
+
|
|
55
|
+
for (const { name, pattern, isContextual } of patterns) {
|
|
56
|
+
if (!isPatternEnabled(name)) {
|
|
74
57
|
continue;
|
|
75
58
|
}
|
|
59
|
+
|
|
76
60
|
if (isContextual) {
|
|
77
61
|
result = result.replace(
|
|
78
62
|
new RegExp(pattern.source, "g"),
|
|
79
63
|
(match, capturedValue) => {
|
|
80
|
-
const token = addEntry(sessionId,
|
|
64
|
+
const token = addEntry(sessionId, name, capturedValue);
|
|
81
65
|
return match.replace(capturedValue, token);
|
|
82
66
|
},
|
|
83
67
|
);
|
|
@@ -91,7 +75,7 @@ export function mask(sessionId: string, text: string): string {
|
|
|
91
75
|
}
|
|
92
76
|
|
|
93
77
|
for (const value of matches) {
|
|
94
|
-
const token = addEntry(sessionId,
|
|
78
|
+
const token = addEntry(sessionId, name, value);
|
|
95
79
|
result = result.split(value).join(token);
|
|
96
80
|
}
|
|
97
81
|
}
|
|
@@ -100,6 +84,26 @@ export function mask(sessionId: string, text: string): string {
|
|
|
100
84
|
return result;
|
|
101
85
|
}
|
|
102
86
|
|
|
87
|
+
export function mask(sessionId: string, text: string): string {
|
|
88
|
+
if (!text || typeof text !== "string") {
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const config = getConfig();
|
|
93
|
+
if (!config.enabled) {
|
|
94
|
+
return text;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let result = text;
|
|
98
|
+
|
|
99
|
+
result = applyCustomPatterns(sessionId, result, config.customPatterns);
|
|
100
|
+
|
|
101
|
+
const patterns = loadPatterns();
|
|
102
|
+
result = applyLoadedPatterns(sessionId, result, patterns);
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
|
|
103
107
|
export function unmask(sessionId: string, text: string): string {
|
|
104
108
|
if (!text || typeof text !== "string") {
|
|
105
109
|
return text;
|
package/src/patterns.ts
CHANGED
|
@@ -1,45 +1,108 @@
|
|
|
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
|
-
|
|
1
|
+
import { readdirSync, readFileSync } from "fs";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Pattern definition in JSON files.
|
|
7
|
+
* Can be:
|
|
8
|
+
* - A regex string: "^vpc-[0-9a-f]{8,17}$"
|
|
9
|
+
* - An object with pattern and optional contextual flag: { "pattern": "...", "contextual": true }
|
|
10
|
+
* - An object with exact string match: { "exact": "literal-value" }
|
|
11
|
+
*/
|
|
12
|
+
export type PatternDefinition =
|
|
13
|
+
| string
|
|
14
|
+
| { pattern: string; contextual?: boolean }
|
|
15
|
+
| { exact: string };
|
|
16
|
+
|
|
17
|
+
export interface PatternFile {
|
|
18
|
+
[patternName: string]: PatternDefinition;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LoadedPattern {
|
|
22
|
+
name: string;
|
|
23
|
+
pattern: RegExp;
|
|
24
|
+
isContextual: boolean;
|
|
25
|
+
isExact: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let cachedPatterns: LoadedPattern[] | null = null;
|
|
29
|
+
|
|
30
|
+
function getPackagePatternsDir(): string {
|
|
31
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
32
|
+
const srcDir = dirname(currentFile);
|
|
33
|
+
const packageRoot = dirname(srcDir);
|
|
34
|
+
return join(packageRoot, "patterns");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parsePatternDefinition(
|
|
38
|
+
name: string,
|
|
39
|
+
definition: PatternDefinition,
|
|
40
|
+
): LoadedPattern {
|
|
41
|
+
if (typeof definition === "string") {
|
|
42
|
+
return {
|
|
43
|
+
name,
|
|
44
|
+
pattern: new RegExp(definition),
|
|
45
|
+
isContextual: false,
|
|
46
|
+
isExact: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if ("exact" in definition) {
|
|
51
|
+
const escaped = definition.exact.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
return {
|
|
53
|
+
name,
|
|
54
|
+
pattern: new RegExp(escaped),
|
|
55
|
+
isContextual: false,
|
|
56
|
+
isExact: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
pattern: new RegExp(definition.pattern),
|
|
63
|
+
isContextual: definition.contextual ?? false,
|
|
64
|
+
isExact: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function loadPatternFile(filePath: string): LoadedPattern[] {
|
|
69
|
+
const content = readFileSync(filePath, "utf-8");
|
|
70
|
+
const data: PatternFile = JSON.parse(content);
|
|
71
|
+
const patterns: LoadedPattern[] = [];
|
|
72
|
+
|
|
73
|
+
for (const [name, definition] of Object.entries(data)) {
|
|
74
|
+
patterns.push(parsePatternDefinition(name, definition));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return patterns;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function loadPatterns(): LoadedPattern[] {
|
|
81
|
+
if (cachedPatterns) {
|
|
82
|
+
return cachedPatterns;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const patternsDir = getPackagePatternsDir();
|
|
86
|
+
const patterns: LoadedPattern[] = [];
|
|
87
|
+
|
|
88
|
+
const files = readdirSync(patternsDir)
|
|
89
|
+
.filter((f) => f.endsWith(".json"))
|
|
90
|
+
.sort();
|
|
91
|
+
|
|
92
|
+
for (const file of files) {
|
|
93
|
+
const filePath = join(patternsDir, file);
|
|
94
|
+
const filePatterns = loadPatternFile(filePath);
|
|
95
|
+
patterns.push(...filePatterns);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
cachedPatterns = patterns;
|
|
99
|
+
return patterns;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function resetPatternCache(): void {
|
|
103
|
+
cachedPatterns = null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getPatternByName(name: string): LoadedPattern | undefined {
|
|
107
|
+
return loadPatterns().find((p) => p.name === name);
|
|
108
|
+
}
|