@claude-agent/envcheck 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -1
- package/package.json +4 -2
- package/src/index.js +125 -4
package/README.md
CHANGED
|
@@ -269,6 +269,55 @@ ADMIN_EMAIL=admin@example.com
|
|
|
269
269
|
| `uuid` | UUID format | `550e8400-e29b-41d4-a716-446655440000` |
|
|
270
270
|
| `string`/`str` | Any string (no validation) | anything |
|
|
271
271
|
|
|
272
|
+
## Secret Detection
|
|
273
|
+
|
|
274
|
+
envcheck can warn you if your `.env` file contains values that look like real secrets:
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
const result = check('.env', {
|
|
278
|
+
examplePath: '.env.example',
|
|
279
|
+
detectSecrets: true // Warns about potential secrets
|
|
280
|
+
});
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Detected Secret Patterns
|
|
284
|
+
|
|
285
|
+
- AWS Access Keys (`AKIA...`)
|
|
286
|
+
- GitHub Tokens (`ghp_...`, `gho_...`, etc.)
|
|
287
|
+
- Stripe Keys (`sk_live_...`, `rk_live_...`)
|
|
288
|
+
- Private Keys (`-----BEGIN PRIVATE KEY-----`)
|
|
289
|
+
- Slack Tokens (`xox...`)
|
|
290
|
+
- Twilio Credentials
|
|
291
|
+
- SendGrid API Keys
|
|
292
|
+
- Google API Keys
|
|
293
|
+
- High-entropy hex strings (with sensitive key names)
|
|
294
|
+
|
|
295
|
+
### Placeholder Detection
|
|
296
|
+
|
|
297
|
+
envcheck won't warn about obvious placeholders like:
|
|
298
|
+
- `your-api-key`, `my-secret`
|
|
299
|
+
- `changeme`, `placeholder`
|
|
300
|
+
- `xxx`, `...`
|
|
301
|
+
- `example`, `test`, `dummy`
|
|
302
|
+
|
|
303
|
+
### API Usage with Secrets
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
const { check, validate, detectSecret } = require('@claude-agent/envcheck');
|
|
307
|
+
|
|
308
|
+
// Enable secret detection
|
|
309
|
+
const result = check('.env', {
|
|
310
|
+
examplePath: '.env.example',
|
|
311
|
+
detectSecrets: true // Warns about potential secrets
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// Check a single value
|
|
315
|
+
const secret = detectSecret('API_KEY', 'sk_live_abc123...');
|
|
316
|
+
if (secret) {
|
|
317
|
+
console.log(`Warning: ${secret.description} detected`);
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
272
321
|
### API Usage with Types
|
|
273
322
|
|
|
274
323
|
```javascript
|
|
@@ -308,7 +357,7 @@ WITH_EQUALS=postgres://user:pass@host/db?opt=val
|
|
|
308
357
|
- **Auto-detection** - Finds .env.example automatically
|
|
309
358
|
- **CI-friendly** - Exit codes and JSON output
|
|
310
359
|
- **Comprehensive** - Parse, validate, compare, generate
|
|
311
|
-
- **Well-tested** -
|
|
360
|
+
- **Well-tested** - 72 tests covering edge cases
|
|
312
361
|
|
|
313
362
|
## vs. dotenv-safe / envalid
|
|
314
363
|
|
|
@@ -320,6 +369,7 @@ WITH_EQUALS=postgres://user:pass@host/db?opt=val
|
|
|
320
369
|
| **CI/CD integration** | ✅ GitHub Action | ❌ | ❌ |
|
|
321
370
|
| **Pre-commit hook** | ✅ | ❌ | ❌ |
|
|
322
371
|
| Type validation | ✅ (static) | ❌ | ✅ (runtime) |
|
|
372
|
+
| **Secret detection** | ✅ | ❌ | ❌ |
|
|
323
373
|
| Zero dependencies | ✅ | ❌ | ❌ |
|
|
324
374
|
|
|
325
375
|
**Key difference:** envcheck validates *before* deployment (shift-left), while dotenv-safe and envalid validate at runtime when your app starts. Catch missing env vars and type errors in CI, not in production.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claude-agent/envcheck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Validate .env files, compare with .env.example, find missing or empty variables",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
"github-action",
|
|
21
21
|
"ci-cd",
|
|
22
22
|
"pre-commit",
|
|
23
|
-
"git-hooks"
|
|
23
|
+
"git-hooks",
|
|
24
|
+
"secrets",
|
|
25
|
+
"security"
|
|
24
26
|
],
|
|
25
27
|
"author": "Claude Agent <claude-agent@agentmail.to>",
|
|
26
28
|
"license": "MIT",
|
package/src/index.js
CHANGED
|
@@ -87,6 +87,108 @@ const typeValidators = {
|
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Secret detection patterns
|
|
92
|
+
* Each pattern has: regex, description, and optional keyPattern (for key-specific checks)
|
|
93
|
+
*/
|
|
94
|
+
const secretPatterns = [
|
|
95
|
+
// AWS
|
|
96
|
+
{ regex: /^AKIA[0-9A-Z]{16}$/, description: 'AWS Access Key ID' },
|
|
97
|
+
{ regex: /^[A-Za-z0-9/+=]{40}$/, description: 'AWS Secret Access Key', keyPattern: /aws.*secret|secret.*key/i },
|
|
98
|
+
|
|
99
|
+
// Private keys
|
|
100
|
+
{ regex: /-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/, description: 'Private key' },
|
|
101
|
+
{ regex: /-----BEGIN CERTIFICATE-----/, description: 'Certificate' },
|
|
102
|
+
|
|
103
|
+
// GitHub
|
|
104
|
+
{ regex: /^ghp_[a-zA-Z0-9]{36}$/, description: 'GitHub Personal Access Token' },
|
|
105
|
+
{ regex: /^gho_[a-zA-Z0-9]{36}$/, description: 'GitHub OAuth Access Token' },
|
|
106
|
+
{ regex: /^ghu_[a-zA-Z0-9]{36}$/, description: 'GitHub User-to-Server Token' },
|
|
107
|
+
{ regex: /^ghs_[a-zA-Z0-9]{36}$/, description: 'GitHub Server-to-Server Token' },
|
|
108
|
+
{ regex: /^ghr_[a-zA-Z0-9]{36}$/, description: 'GitHub Refresh Token' },
|
|
109
|
+
|
|
110
|
+
// Slack
|
|
111
|
+
{ regex: /^xox[baprs]-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24}$/, description: 'Slack Token' },
|
|
112
|
+
|
|
113
|
+
// Stripe
|
|
114
|
+
{ regex: /^sk_live_[a-zA-Z0-9]{24,}$/, description: 'Stripe Live Secret Key' },
|
|
115
|
+
{ regex: /^rk_live_[a-zA-Z0-9]{24,}$/, description: 'Stripe Live Restricted Key' },
|
|
116
|
+
|
|
117
|
+
// Twilio
|
|
118
|
+
{ regex: /^AC[a-f0-9]{32}$/, description: 'Twilio Account SID' },
|
|
119
|
+
{ regex: /^SK[a-f0-9]{32}$/, description: 'Twilio API Key' },
|
|
120
|
+
|
|
121
|
+
// SendGrid
|
|
122
|
+
{ regex: /^SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}$/, description: 'SendGrid API Key' },
|
|
123
|
+
|
|
124
|
+
// Google
|
|
125
|
+
{ regex: /^AIza[0-9A-Za-z_-]{35}$/, description: 'Google API Key' },
|
|
126
|
+
|
|
127
|
+
// Generic high-entropy (likely real secrets)
|
|
128
|
+
{ regex: /^[a-f0-9]{32}$/, description: 'Hex string (32 chars)', keyPattern: /api.*key|secret|token|password/i },
|
|
129
|
+
{ regex: /^[a-f0-9]{64}$/, description: 'Hex string (64 chars)', keyPattern: /api.*key|secret|token|password/i },
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Placeholder patterns that are NOT secrets
|
|
134
|
+
*/
|
|
135
|
+
const placeholderPatterns = [
|
|
136
|
+
/^your[-_]?/i,
|
|
137
|
+
/^my[-_]?/i,
|
|
138
|
+
/^xxx+$/i,
|
|
139
|
+
/^placeholder/i,
|
|
140
|
+
/^example/i,
|
|
141
|
+
/^test[-_]?/i,
|
|
142
|
+
/^dummy/i,
|
|
143
|
+
/^fake/i,
|
|
144
|
+
/^sample/i,
|
|
145
|
+
/^changeme/i,
|
|
146
|
+
/^replace[-_]?/i,
|
|
147
|
+
/^insert[-_]?/i,
|
|
148
|
+
/^todo/i,
|
|
149
|
+
/^\*+$/,
|
|
150
|
+
/^\.\.\.$/,
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Check if a value looks like a placeholder
|
|
155
|
+
* @param {string} value - Value to check
|
|
156
|
+
* @returns {boolean} True if it looks like a placeholder
|
|
157
|
+
*/
|
|
158
|
+
function isPlaceholder(value) {
|
|
159
|
+
if (!value || value.length < 3) return true;
|
|
160
|
+
return placeholderPatterns.some(pattern => pattern.test(value));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Detect potential secrets in environment variables
|
|
165
|
+
* @param {string} key - Variable name
|
|
166
|
+
* @param {string} value - Variable value
|
|
167
|
+
* @returns {Object|null} Detection result or null if no secret detected
|
|
168
|
+
*/
|
|
169
|
+
function detectSecret(key, value) {
|
|
170
|
+
if (!value || isPlaceholder(value)) {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const pattern of secretPatterns) {
|
|
175
|
+
// If pattern has keyPattern, only check if key matches
|
|
176
|
+
if (pattern.keyPattern && !pattern.keyPattern.test(key)) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (pattern.regex.test(value)) {
|
|
181
|
+
return {
|
|
182
|
+
detected: true,
|
|
183
|
+
description: pattern.description,
|
|
184
|
+
message: `may contain a real ${pattern.description}`
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
90
192
|
/**
|
|
91
193
|
* Parse a .env file into an object
|
|
92
194
|
* @param {string} content - File content
|
|
@@ -326,7 +428,7 @@ function validateType(value, type) {
|
|
|
326
428
|
* @returns {Object} Validation result
|
|
327
429
|
*/
|
|
328
430
|
function validate(filePath, options = {}) {
|
|
329
|
-
const { required = [], noEmpty = false, types = {}, validateTypes = false } = options;
|
|
431
|
+
const { required = [], noEmpty = false, types = {}, validateTypes = false, detectSecrets = false } = options;
|
|
330
432
|
|
|
331
433
|
const env = readEnvFile(filePath);
|
|
332
434
|
|
|
@@ -406,6 +508,20 @@ function validate(filePath, options = {}) {
|
|
|
406
508
|
}
|
|
407
509
|
}
|
|
408
510
|
|
|
511
|
+
// Secret detection
|
|
512
|
+
if (detectSecrets) {
|
|
513
|
+
for (const [key, value] of Object.entries(env.variables)) {
|
|
514
|
+
const secretResult = detectSecret(key, value);
|
|
515
|
+
if (secretResult) {
|
|
516
|
+
result.issues.push({
|
|
517
|
+
type: 'warning',
|
|
518
|
+
line: env.lineInfo[key],
|
|
519
|
+
message: `Variable '${key}' ${secretResult.message}`
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
409
525
|
// Add warnings
|
|
410
526
|
for (const warning of env.warnings) {
|
|
411
527
|
result.issues.push({
|
|
@@ -432,7 +548,8 @@ function check(envPath, options = {}) {
|
|
|
432
548
|
noExtra = false,
|
|
433
549
|
strict = false,
|
|
434
550
|
types = {},
|
|
435
|
-
validateTypes = false
|
|
551
|
+
validateTypes = false,
|
|
552
|
+
detectSecrets = false
|
|
436
553
|
} = options;
|
|
437
554
|
|
|
438
555
|
const result = {
|
|
@@ -456,12 +573,13 @@ function check(envPath, options = {}) {
|
|
|
456
573
|
// Merge type hints: example hints < explicit types
|
|
457
574
|
const mergedTypes = { ...exampleTypeHints, ...types };
|
|
458
575
|
|
|
459
|
-
// First validate the env file (including type validation)
|
|
576
|
+
// First validate the env file (including type validation and secret detection)
|
|
460
577
|
const validation = validate(envPath, {
|
|
461
578
|
required,
|
|
462
579
|
noEmpty,
|
|
463
580
|
types: mergedTypes,
|
|
464
|
-
validateTypes: validateTypes || Object.keys(mergedTypes).length > 0
|
|
581
|
+
validateTypes: validateTypes || Object.keys(mergedTypes).length > 0,
|
|
582
|
+
detectSecrets
|
|
465
583
|
});
|
|
466
584
|
|
|
467
585
|
if (!validation.valid) {
|
|
@@ -573,6 +691,9 @@ module.exports = {
|
|
|
573
691
|
validate,
|
|
574
692
|
validateType,
|
|
575
693
|
typeValidators,
|
|
694
|
+
detectSecret,
|
|
695
|
+
secretPatterns,
|
|
696
|
+
isPlaceholder,
|
|
576
697
|
check,
|
|
577
698
|
generate,
|
|
578
699
|
list,
|