@uniglot/wont-let-you-see 0.2.0 → 0.3.1
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 +106 -40
- package/dist/index.js +113 -92
- package/package.json +1 -1
- package/patterns/aws.json +28 -0
- package/patterns/common.json +18 -0
- package/patterns/kubernetes.json +8 -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
|
|
@@ -74,46 +89,53 @@ The plugin automatically masks output from:
|
|
|
74
89
|
|
|
75
90
|
### AWS Resources
|
|
76
91
|
|
|
77
|
-
| Pattern Type
|
|
78
|
-
|
|
|
79
|
-
| `arn`
|
|
80
|
-
| `eks-cluster`
|
|
81
|
-
| `account-id`
|
|
82
|
-
| `access-key-id`
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `vpn-
|
|
100
|
-
| `
|
|
92
|
+
| Pattern Type | Description | Example |
|
|
93
|
+
| ------------------- | ---------------------------- | ------------------------------------------------------- |
|
|
94
|
+
| `arn` | Generic AWS ARNs | `arn:aws:iam::123456789012:user/admin` |
|
|
95
|
+
| `eks-cluster` | EKS Cluster ARNs | `arn:aws:eks:us-west-2:123456789012:cluster/my-cluster` |
|
|
96
|
+
| `account-id` | AWS Account IDs (contextual) | `"OwnerId": "123456789012"` |
|
|
97
|
+
| `access-key-id` | AWS Access Key IDs | `AKIAIOSFODNN7EXAMPLE` |
|
|
98
|
+
| `secret-access-key` | AWS Secret Access Keys | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` |
|
|
99
|
+
| `vpc` | VPC IDs | `vpc-0123456789abcdef0` |
|
|
100
|
+
| `subnet` | Subnet IDs | `subnet-0123456789abcdef0` |
|
|
101
|
+
| `security-group` | Security Group IDs | `sg-0123456789abcdef0` |
|
|
102
|
+
| `internet-gateway` | Internet Gateway IDs | `igw-0123456789abcdef0` |
|
|
103
|
+
| `route-table` | Route Table IDs | `rtb-0123456789abcdef0` |
|
|
104
|
+
| `nat-gateway` | NAT Gateway IDs | `nat-0123456789abcdef0` |
|
|
105
|
+
| `network-acl` | Network ACL IDs | `acl-0123456789abcdef0` |
|
|
106
|
+
| `ec2-instance` | EC2 Instance IDs | `i-0123456789abcdef0` |
|
|
107
|
+
| `ami` | AMI IDs | `ami-0123456789abcdef0` |
|
|
108
|
+
| `ebs` | EBS Volume IDs | `vol-0123456789abcdef0` |
|
|
109
|
+
| `snapshot` | EBS Snapshot IDs | `snap-0123456789abcdef0` |
|
|
110
|
+
| `eni` | Network Interface IDs | `eni-0123456789abcdef0` |
|
|
111
|
+
| `vpc-endpoint` | VPC Endpoint IDs | `vpce-0123456789abcdef0` |
|
|
112
|
+
| `transit-gateway` | Transit Gateway IDs | `tgw-0123456789abcdef0` |
|
|
113
|
+
| `customer-gateway` | Customer Gateway IDs | `cgw-0123456789abcdef0` |
|
|
114
|
+
| `vpn-gateway` | VPN Gateway IDs | `vgw-0123456789abcdef0` |
|
|
115
|
+
| `vpn-connection` | VPN Connection IDs | `vpn-0123456789abcdef0` |
|
|
116
|
+
| `ecr-repo` | ECR Repository URIs | `123456789012.dkr.ecr.us-west-2.amazonaws.com/my-repo` |
|
|
101
117
|
|
|
102
118
|
### Kubernetes Resources (EKS)
|
|
103
119
|
|
|
104
|
-
| Pattern Type | Description
|
|
105
|
-
| -------------- |
|
|
106
|
-
| `k8s-
|
|
107
|
-
| `k8s-
|
|
108
|
-
| `k8s-node` | EKS Node Names | `ip-10-0-1-123.compute.internal` |
|
|
120
|
+
| Pattern Type | Description | Example |
|
|
121
|
+
| -------------- | ------------------------- | ---------------------------------- |
|
|
122
|
+
| `k8s-endpoint` | EKS Cluster API Endpoints | `https://ABC123.eks.amazonaws.com` |
|
|
123
|
+
| `k8s-node` | EKS Node Names | `ip-10-0-1-123.compute.internal` |
|
|
109
124
|
|
|
110
125
|
### Common Patterns
|
|
111
126
|
|
|
112
|
-
| Pattern Type
|
|
113
|
-
|
|
|
114
|
-
| `ipv4`
|
|
115
|
-
| `private-key`
|
|
116
|
-
| `api-key`
|
|
127
|
+
| Pattern Type | Description | Example |
|
|
128
|
+
| --------------------- | --------------------------------------------- | ----------------------------------------------------------------- |
|
|
129
|
+
| `ipv4` | IPv4 Addresses (including IP portion of CIDR) | `192.168.1.1`, `10.0.0.0/16` → `#(ipv4-1)/16` |
|
|
130
|
+
| `private-key` | Private Key Blocks (entire key) | `-----BEGIN RSA PRIVATE KEY-----...-----END RSA PRIVATE KEY-----` |
|
|
131
|
+
| `api-key` | API Keys (contextual) | `"api_key": "sk-..."` |
|
|
132
|
+
| `phone-us` | US Phone Numbers | `+1-555-123-4567`, `(555) 123-4567` |
|
|
133
|
+
| `phone-kr` | South Korean Phone Numbers | `010-1234-5678`, `+82-10-1234-5678` |
|
|
134
|
+
| `phone-international` | International Phone Numbers | `+44 20 7946 0958` |
|
|
135
|
+
| `email` | Email Addresses | `user@example.com` |
|
|
136
|
+
| `uuid` | UUID/GUID | `550e8400-e29b-41d4-a716-446655440000` |
|
|
137
|
+
| `jwt` | JWT Tokens | `eyJhbGciOiJIUzI1NiIs...` |
|
|
138
|
+
| `base64-secret` | Base64-encoded Secrets (contextual) | `"secret": "SGVsbG8gV29ybGQ="` |
|
|
117
139
|
|
|
118
140
|
## Token Format
|
|
119
141
|
|
|
@@ -125,6 +147,10 @@ Examples:
|
|
|
125
147
|
- `10.0.0.1` → `#(ipv4-1)`
|
|
126
148
|
- `10.0.0.0/16` → `#(ipv4-1)/16` (subnet mask preserved)
|
|
127
149
|
- `AKIAIOSFODNN7EXAMPLE` → `#(access-key-id-1)`
|
|
150
|
+
- `wJalrXUtnFEMI/K7MDENG...` → `#(secret-access-key-1)`
|
|
151
|
+
- `user@example.com` → `#(email-1)`
|
|
152
|
+
- `+1-555-123-4567` → `#(phone-us-1)`
|
|
153
|
+
- `eyJhbGciOiJIUzI1NiIs...` → `#(jwt-1)`
|
|
128
154
|
- Entire private key block → `#(private-key-1)`
|
|
129
155
|
|
|
130
156
|
## How It Works
|
|
@@ -147,9 +173,49 @@ What was the actual VPC ID from the last command?
|
|
|
147
173
|
|
|
148
174
|
The LLM should only know the token (e.g., `#(vpc-1)`), not the real value.
|
|
149
175
|
|
|
176
|
+
## Contributing Patterns
|
|
177
|
+
|
|
178
|
+
Patterns are defined in JSON files under the `patterns/` directory:
|
|
179
|
+
|
|
180
|
+
- `patterns/aws.json` - AWS resource patterns
|
|
181
|
+
- `patterns/kubernetes.json` - Kubernetes patterns
|
|
182
|
+
- `patterns/common.json` - Common patterns (IPs, keys, etc.)
|
|
183
|
+
|
|
184
|
+
You can request to add new files for resources of different categories.
|
|
185
|
+
|
|
186
|
+
### Pattern Format
|
|
187
|
+
|
|
188
|
+
Each pattern can be defined as:
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"pattern-name": "^regex-pattern$",
|
|
193
|
+
|
|
194
|
+
"contextual-pattern": {
|
|
195
|
+
"pattern": "\"field\":\\s*\"(captured-value)\"",
|
|
196
|
+
"contextual": true
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
"literal-match": {
|
|
200
|
+
"exact": "literal-string-to-match"
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- **Simple regex**: Just a string with the regex pattern
|
|
206
|
+
- **Contextual**: For patterns where only a captured group should be masked (e.g., JSON fields)
|
|
207
|
+
- **Exact match**: For literal strings that should be escaped
|
|
208
|
+
|
|
209
|
+
### Adding New Patterns
|
|
210
|
+
|
|
211
|
+
1. Fork the repository
|
|
212
|
+
2. Add your pattern to the appropriate JSON file
|
|
213
|
+
3. Add tests in `src/__tests__/patterns.test.ts`
|
|
214
|
+
4. Submit a pull request
|
|
215
|
+
|
|
150
216
|
## Limitations
|
|
151
217
|
|
|
152
|
-
- **AWS only**: Currently supports AWS. GCP and Azure are not yet supported.
|
|
218
|
+
- **AWS only**: Currently supports AWS. GCP and Azure are not yet supported. Do you need them? Feel free to contribute!
|
|
153
219
|
- **S3 Buckets**: Bucket names are not masked (often public/intentional).
|
|
154
220
|
- **Account IDs**: Only masked in contextual JSON fields. Add to `customPatterns` for full coverage.
|
|
155
221
|
- **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,28 @@
|
|
|
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
|
+
"secret-access-key": "(?:^|[^A-Za-z0-9+/])[A-Za-z0-9+/]{40}(?:[^A-Za-z0-9+/]|$)",
|
|
10
|
+
"vpc": "^vpc-[0-9a-f]{8,17}$",
|
|
11
|
+
"subnet": "^subnet-[0-9a-f]{8,17}$",
|
|
12
|
+
"security-group": "^sg-[0-9a-f]{8,17}$",
|
|
13
|
+
"internet-gateway": "^igw-[0-9a-f]{8,17}$",
|
|
14
|
+
"route-table": "^rtb-[0-9a-f]{8,17}$",
|
|
15
|
+
"nat-gateway": "^nat-[0-9a-f]{8,17}$",
|
|
16
|
+
"network-acl": "^acl-[0-9a-f]{8,17}$",
|
|
17
|
+
"eni": "^eni-[0-9a-f]{8,17}$",
|
|
18
|
+
"vpc-endpoint": "^vpce-[0-9a-f]{8,17}$",
|
|
19
|
+
"transit-gateway": "^tgw-[0-9a-f]{8,17}$",
|
|
20
|
+
"customer-gateway": "^cgw-[0-9a-f]{8,17}$",
|
|
21
|
+
"vpn-gateway": "^vgw-[0-9a-f]{8,17}$",
|
|
22
|
+
"vpn-connection": "^vpn-[0-9a-f]{8,17}$",
|
|
23
|
+
"ecr-repo": "^(?:\\d{12}|#\\(custom-\\d+\\))\\.dkr\\.ecr\\.[a-z0-9-]+\\.amazonaws\\.com\\/[a-z0-9._\\/-]+$",
|
|
24
|
+
"ami": "^ami-[0-9a-f]{8,17}$",
|
|
25
|
+
"ec2-instance": "^i-[0-9a-f]{8,17}$",
|
|
26
|
+
"ebs": "^vol-[0-9a-f]{8,17}$",
|
|
27
|
+
"snapshot": "^snap-[0-9a-f]{8,17}$"
|
|
28
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
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
|
+
"phone-us": "^(?:\\+1[-\\s.]?)?(?:\\(?[2-9][0-9]{2}\\)?[-\\s.]?)?[2-9][0-9]{2}[-\\s.]?[0-9]{4}$",
|
|
9
|
+
"phone-kr": "^(?:\\+82[-\\s.]?|0)?(?:1[0-9]|2|3[1-3]|4[1-4]|5[1-5]|6[1-4])[-\\s.]?[0-9]{3,4}[-\\s.]?[0-9]{4}$",
|
|
10
|
+
"phone-international": "^\\+[1-9][0-9]{0,2}[-\\s.]?(?:[0-9][-\\s.]?){6,14}[0-9]$",
|
|
11
|
+
"email": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$",
|
|
12
|
+
"uuid": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$",
|
|
13
|
+
"jwt": "^eyJ[A-Za-z0-9_-]*\\.eyJ[A-Za-z0-9_-]*\\.[A-Za-z0-9_-]*$",
|
|
14
|
+
"base64-secret": {
|
|
15
|
+
"pattern": "\"(?:secret|password|credential|private_key|privateKey|encryption_key|encryptionKey|signing_key|signingKey)\":\\s*\"([A-Za-z0-9+/]{32,}={0,2})\"",
|
|
16
|
+
"contextual": true
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"k8s-endpoint": "^https:\\/\\/[A-Z0-9]+\\.[a-z0-9-]+\\.eks\\.amazonaws\\.com$",
|
|
3
|
+
"k8s-endpoint-kubeconfig": {
|
|
4
|
+
"pattern": "^https:\\/\\/[0-9A-F]{32}\\.[a-z]{2}-[a-z]+-\\d\\.eks\\.amazonaws\\.com$",
|
|
5
|
+
"contextual": false
|
|
6
|
+
},
|
|
7
|
+
"k8s-node": "^ip-(?:\\d{1,3}-){3}\\d{1,3}\\.[a-z0-9-]+\\.compute\\.internal$"
|
|
8
|
+
}
|
|
@@ -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";
|