@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 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 | 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 strings to mask | (none) |
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
- var AWS_PATTERNS = {
3
- arn: /^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9-]+:[a-z0-9-]*:(?:[0-9]{12})?:.+$/,
4
- eksCluster: /^arn:(?:aws|aws-cn|aws-us-gov):eks:[a-z0-9-]+:[0-9]{12}:cluster\/.+$/,
5
- vpc: /^vpc-[0-9a-f]{8,17}$/,
6
- subnet: /^subnet-[0-9a-f]{8,17}$/,
7
- securityGroup: /^sg-[0-9a-f]{8,17}$/,
8
- internetGateway: /^igw-[0-9a-f]{8,17}$/,
9
- routeTable: /^rtb-[0-9a-f]{8,17}$/,
10
- natGateway: /^nat-[0-9a-f]{8,17}$/,
11
- networkAcl: /^acl-[0-9a-f]{8,17}$/,
12
- ec2Instance: /^i-[0-9a-f]{8,17}$/,
13
- ami: /^ami-[0-9a-f]{8,17}$/,
14
- ebs: /^vol-[0-9a-f]{8,17}$/,
15
- snapshot: /^snap-[0-9a-f]{8,17}$/,
16
- eni: /^eni-[0-9a-f]{8,17}$/,
17
- vpcEndpoint: /^vpce-[0-9a-f]{8,17}$/,
18
- transitGateway: /^tgw-[0-9a-f]{8,17}$/,
19
- customerGateway: /^cgw-[0-9a-f]{8,17}$/,
20
- vpnGateway: /^vgw-[0-9a-f]{8,17}$/,
21
- vpnConnection: /^vpn-[0-9a-f]{8,17}$/,
22
- accountId: /"(?:OwnerId|AccountId|Owner|account_id)":\s*"(\d{12})"/,
23
- ecrRepoUri: /^(?:\d{12}|#\(custom-\d+\))\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com\/[a-z0-9._\/-]+$/,
24
- accessKeyId: /(?:^|[^A-Z0-9])(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}(?:[^A-Z0-9]|$)/
25
- };
26
- var K8S_PATTERNS = {
27
- serviceAccountToken: /^eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*$/,
28
- nodeNameAws: /^ip-(?:\d{1,3}-){3}\d{1,3}\.[a-z0-9-]+\.compute\.internal$/,
29
- clusterEndpoint: /^https:\/\/[A-Z0-9]+\.[a-z0-9-]+\.eks\.amazonaws\.com$/,
30
- kubeconfigServer: /^https:\/\/[0-9A-F]{32}\.[a-z]{2}-[a-z]+-\d\.eks\.amazonaws\.com$/i
31
- };
32
- var COMMON_PATTERNS = {
33
- 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]?)$/,
34
- privateKey: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
35
- apiKeyField: /"(?:api_key|apiKey|secret_key|secretKey|access_token|auth_token|password|token)":\s*"([^"]+)"/
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 = join(projectRoot, ".opencode", "sessions", sessionId, "wont-let-you-see-mapping.json");
52
- if (existsSync(join(projectRoot, ".opencode")) || !existsSync(homedir())) {
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 join(homedir(), ".opencode", "sessions", sessionId, "wont-let-you-see-mapping.json");
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(readFileSync(sessionPath, "utf-8"));
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 join2, dirname } from "path";
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 = join2(currentDir, CONFIG_FILENAME);
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 = dirname(currentDir);
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 = join2(homedir2(), CONFIG_FILENAME);
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
- function mask(sessionId, text) {
247
- if (!text || typeof text !== "string") {
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 config.customPatterns) {
256
- if (result.includes(customPattern)) {
257
- const token = addEntry(sessionId, "custom", customPattern);
258
- result = result.split(customPattern).join(token);
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
- for (const { pattern, type, isContextual } of PATTERN_ORDER) {
262
- if (!isPatternEnabled(type)) {
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, type, capturedValue);
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, type, value);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniglot/wont-let-you-see",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "OpenCode plugin that masks sensitive cloud infrastructure data (AWS, Kubernetes) from LLMs",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -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 { AWS_PATTERNS, K8S_PATTERNS, COMMON_PATTERNS } from "../patterns";
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
- AWS_PATTERNS.arn.test("arn:aws:iam::123456789012:user/admin"),
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(AWS_PATTERNS.arn.test("arn:aws-cn:s3:::my-bucket")).toBe(true);
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
- AWS_PATTERNS.arn.test(
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(AWS_PATTERNS.vpc.test("vpc-0123456789abcdef0")).toBe(true);
37
+ expect(getPattern("vpc").test("vpc-0123456789abcdef0")).toBe(true);
28
38
  });
29
39
 
30
40
  it("should match subnet", () => {
31
- expect(AWS_PATTERNS.subnet.test("subnet-0123456789abcdef0")).toBe(true);
41
+ expect(getPattern("subnet").test("subnet-0123456789abcdef0")).toBe(true);
32
42
  });
33
43
 
34
44
  it("should match security group", () => {
35
- expect(AWS_PATTERNS.securityGroup.test("sg-0123456789abcdef0")).toBe(
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(AWS_PATTERNS.natGateway.test("nat-0123456789abcdef0")).toBe(true);
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(AWS_PATTERNS.networkAcl.test("acl-0123456789abcdef0")).toBe(true);
57
+ expect(getPattern("network-acl").test("acl-0123456789abcdef0")).toBe(
58
+ true,
59
+ );
46
60
  });
47
61
 
48
62
  it("should match eni", () => {
49
- expect(AWS_PATTERNS.eni.test("eni-0123456789abcdef0")).toBe(true);
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(AWS_PATTERNS.ebs.test("vol-0123456789abcdef0")).toBe(true);
69
+ expect(getPattern("ebs").test("vol-0123456789abcdef0")).toBe(true);
56
70
  });
57
71
 
58
72
  it("should match snapshot", () => {
59
- expect(AWS_PATTERNS.snapshot.test("snap-0123456789abcdef0")).toBe(true);
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(AWS_PATTERNS.vpcEndpoint.test("vpce-0123456789abcdef0")).toBe(
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(AWS_PATTERNS.transitGateway.test("tgw-0123456789abcdef0")).toBe(
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(AWS_PATTERNS.customerGateway.test("cgw-0123456789abcdef0")).toBe(
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(AWS_PATTERNS.vpnGateway.test("vgw-0123456789abcdef0")).toBe(true);
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(AWS_PATTERNS.vpnConnection.test("vpn-0123456789abcdef0")).toBe(
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(AWS_PATTERNS.vpcEndpoint.test("vpce-invalid")).toBe(false);
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
- AWS_PATTERNS.ecrRepoUri.test(
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
- AWS_PATTERNS.ecrRepoUri.test(
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
- AWS_PATTERNS.ecrRepoUri.test(
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
- AWS_PATTERNS.ecrRepoUri.test(
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(AWS_PATTERNS.ecrRepoUri.test("docker.io/library/nginx")).toBe(
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
- AWS_PATTERNS.ecrRepoUri.test(
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
- AWS_PATTERNS.ecrRepoUri.test(
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(AWS_PATTERNS.accountId.test('"OwnerId": "123456789012"')).toBe(
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(AWS_PATTERNS.accountId.test('"account_id": "123456789012"')).toBe(
162
- true,
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(AWS_PATTERNS.accountId.test("123456789012")).toBe(false);
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(AWS_PATTERNS.accessKeyId.test(" AKIAIOSFODNN7EXAMPLE ")).toBe(
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(AWS_PATTERNS.accessKeyId.test(" ASIAISAMPLEKEYID1234 ")).toBe(
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(AWS_PATTERNS.accessKeyId.test("RANDOMSTRING12345678")).toBe(false);
201
+ expect(getPattern("access-key-id").test("RANDOMSTRING12345678")).toBe(
202
+ false,
203
+ );
186
204
  });
187
205
  });
188
206
  });
189
207
 
190
- describe("K8S_PATTERNS", () => {
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(K8S_PATTERNS.serviceAccountToken.test(jwt)).toBe(true);
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
- K8S_PATTERNS.nodeNameAws.test(
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
- K8S_PATTERNS.clusterEndpoint.test(
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("COMMON_PATTERNS", () => {
240
+ describe("Common Patterns", () => {
241
+ beforeEach(() => {
242
+ resetPatternCache();
243
+ });
244
+
221
245
  describe("IPv4", () => {
222
246
  it("should match IPv4", () => {
223
- expect(COMMON_PATTERNS.ipv4.test("192.168.1.1")).toBe(true);
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(COMMON_PATTERNS.ipv4.test("10.0.0.0/8")).toBe(false);
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(COMMON_PATTERNS.privateKey.test(rsaKey)).toBe(true);
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(COMMON_PATTERNS.privateKey.test(genericKey)).toBe(true);
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
- COMMON_PATTERNS.privateKey.test("-----BEGIN RSA PRIVATE KEY-----"),
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
- COMMON_PATTERNS.apiKeyField.test('"api_key": "sk-1234567890abcdef"'),
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
- COMMON_PATTERNS.apiKeyField.test('"password": "supersecret123"'),
265
- ).toBe(true);
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
- COMMON_PATTERNS.apiKeyField.test('"token": "ghp_xxxxxxxxxxxx"'),
271
- ).toBe(true);
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
- ...Object.values(AWS_PATTERNS),
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 { AWS_PATTERNS, K8S_PATTERNS, COMMON_PATTERNS } from "./patterns";
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
- export function mask(sessionId: string, text: string): string {
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 config.customPatterns) {
66
- if (result.includes(customPattern)) {
67
- const token = addEntry(sessionId, "custom", customPattern);
68
- result = result.split(customPattern).join(token);
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
- for (const { pattern, type, isContextual } of PATTERN_ORDER) {
73
- if (!isPatternEnabled(type)) {
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, type, capturedValue);
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, type, value);
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
- export const AWS_PATTERNS = {
2
- arn: /^arn:(?:aws|aws-cn|aws-us-gov):[a-zA-Z0-9-]+:[a-z0-9-]*:(?:[0-9]{12})?:.+$/,
3
- eksCluster:
4
- /^arn:(?:aws|aws-cn|aws-us-gov):eks:[a-z0-9-]+:[0-9]{12}:cluster\/.+$/,
5
- vpc: /^vpc-[0-9a-f]{8,17}$/,
6
- subnet: /^subnet-[0-9a-f]{8,17}$/,
7
- securityGroup: /^sg-[0-9a-f]{8,17}$/,
8
- internetGateway: /^igw-[0-9a-f]{8,17}$/,
9
- routeTable: /^rtb-[0-9a-f]{8,17}$/,
10
- natGateway: /^nat-[0-9a-f]{8,17}$/,
11
- networkAcl: /^acl-[0-9a-f]{8,17}$/,
12
- ec2Instance: /^i-[0-9a-f]{8,17}$/,
13
- ami: /^ami-[0-9a-f]{8,17}$/,
14
- ebs: /^vol-[0-9a-f]{8,17}$/,
15
- snapshot: /^snap-[0-9a-f]{8,17}$/,
16
- eni: /^eni-[0-9a-f]{8,17}$/,
17
- vpcEndpoint: /^vpce-[0-9a-f]{8,17}$/,
18
- transitGateway: /^tgw-[0-9a-f]{8,17}$/,
19
- customerGateway: /^cgw-[0-9a-f]{8,17}$/,
20
- vpnGateway: /^vgw-[0-9a-f]{8,17}$/,
21
- vpnConnection: /^vpn-[0-9a-f]{8,17}$/,
22
- accountId: /"(?:OwnerId|AccountId|Owner|account_id)":\s*"(\d{12})"/,
23
- ecrRepoUri:
24
- /^(?:\d{12}|#\(custom-\d+\))\.dkr\.ecr\.[a-z0-9-]+\.amazonaws\.com\/[a-z0-9._\/-]+$/,
25
- accessKeyId:
26
- /(?:^|[^A-Z0-9])(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}(?:[^A-Z0-9]|$)/,
27
- } as const;
28
-
29
- export const K8S_PATTERNS = {
30
- serviceAccountToken: /^eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*$/,
31
- nodeNameAws: /^ip-(?:\d{1,3}-){3}\d{1,3}\.[a-z0-9-]+\.compute\.internal$/,
32
- clusterEndpoint: /^https:\/\/[A-Z0-9]+\.[a-z0-9-]+\.eks\.amazonaws\.com$/,
33
- kubeconfigServer:
34
- /^https:\/\/[0-9A-F]{32}\.[a-z]{2}-[a-z]+-\d\.eks\.amazonaws\.com$/i,
35
- } as const;
36
-
37
- export const COMMON_PATTERNS = {
38
- 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]?)$/,
39
- // Private key blocks (entire key including body and footer)
40
- privateKey:
41
- /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/,
42
- // Generic API keys/tokens (contextual - in JSON/YAML fields)
43
- apiKeyField:
44
- /"(?:api_key|apiKey|secret_key|secretKey|access_token|auth_token|password|token)":\s*"([^"]+)"/,
45
- } as const;
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
+ }