@anhuijie/envguard 1.0.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.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Sensitive information detection in environment variables
3
+ */
4
+
5
+ // Patterns for detecting secrets and sensitive data
6
+ const SECRET_PATTERNS = [
7
+ {
8
+ name: 'AWS Access Key',
9
+ pattern: /AKIA[0-9A-Z]{16}/,
10
+ severity: 'critical',
11
+ },
12
+ {
13
+ name: 'AWS Secret Key',
14
+ pattern: /aws(.{0,20})?(secret|key).{0,20}[A-Za-z0-9/+=]{40}/i,
15
+ severity: 'critical',
16
+ },
17
+ {
18
+ name: 'GitHub Token',
19
+ pattern: /gh[ps]_[A-Za-z0-9_]{36,}/,
20
+ severity: 'critical',
21
+ },
22
+ {
23
+ name: 'GitLab Token',
24
+ pattern: /glpat-[A-Za-z0-9\-]{20}/,
25
+ severity: 'critical',
26
+ },
27
+ {
28
+ name: 'Slack Token',
29
+ pattern: /xox[baprs]-[0-9]{10,}-[A-Za-z0-9]+/,
30
+ severity: 'critical',
31
+ },
32
+ {
33
+ name: 'Stripe Key',
34
+ pattern: /sk_live_[A-Za-z0-9]{24,}/,
35
+ severity: 'critical',
36
+ },
37
+ {
38
+ name: 'Private Key',
39
+ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
40
+ severity: 'critical',
41
+ },
42
+ {
43
+ name: 'JWT Secret',
44
+ pattern: /jwt(.{0,20})?(secret|key).{0,20}[A-Za-z0-9\-._~+/]+=*/i,
45
+ severity: 'high',
46
+ },
47
+ {
48
+ name: 'Database URL with Password',
49
+ pattern: /(mysql|postgres|mongodb|redis):\/\/[^\s:]+:[^\s@]+@[^\s]+/i,
50
+ severity: 'high',
51
+ },
52
+ {
53
+ name: 'Generic API Key',
54
+ pattern: /(api[_-]?key|apikey|api[_-]?secret)\s*[=:]\s*['"]?[A-Za-z0-9\-._]{20,}['"]?/i,
55
+ severity: 'high',
56
+ },
57
+ {
58
+ name: 'Generic Password',
59
+ pattern: /(password|passwd|pwd)\s*[=:]\s*['"]?[^\s'"]{8,}['"]?/i,
60
+ severity: 'high',
61
+ },
62
+ {
63
+ name: 'Generic Secret',
64
+ pattern: /(secret|token|auth)\s*[=:]\s*['"]?[A-Za-z0-9\-._]{20,}['"]?/i,
65
+ severity: 'medium',
66
+ },
67
+ ];
68
+
69
+ // Keys that are commonly sensitive by name
70
+ const SENSITIVE_KEY_PATTERNS = [
71
+ /password/i,
72
+ /passwd/i,
73
+ /pwd$/i,
74
+ /secret/i,
75
+ /token/i,
76
+ /api[_-]?key/i,
77
+ /auth/i,
78
+ /credential/i,
79
+ /private[_-]?key/i,
80
+ /access[_-]?key/i,
81
+ ];
82
+
83
+ function scanForSecrets(envObj, options = {}) {
84
+ const findings = [];
85
+ const ignoredKeys = new Set(options.ignoreKeys || []);
86
+ const minSeverity = options.minSeverity || 'medium';
87
+ const severityOrder = { low: 0, medium: 1, high: 2, critical: 3 };
88
+
89
+ for (const [key, value] of Object.entries(envObj)) {
90
+ if (ignoredKeys.has(key)) continue;
91
+ if (value === undefined || value === '') continue;
92
+
93
+ // Check value against secret patterns
94
+ for (const { name, pattern, severity } of SECRET_PATTERNS) {
95
+ if (severityOrder[severity] < severityOrder[minSeverity]) continue;
96
+
97
+ if (pattern.test(value)) {
98
+ findings.push({
99
+ key,
100
+ severity,
101
+ type: name,
102
+ message: `Potential ${name} detected in variable "${key}"`,
103
+ });
104
+ break; // One finding per variable to avoid duplicates
105
+ }
106
+ }
107
+
108
+ // Check if key name suggests sensitive data
109
+ const isSensitiveKey = SENSITIVE_KEY_PATTERNS.some((p) => p.test(key));
110
+ if (isSensitiveKey && !findings.some((f) => f.key === key)) {
111
+ findings.push({
112
+ key,
113
+ severity: 'medium',
114
+ type: 'Sensitive Key Name',
115
+ message: `Variable "${key}" appears to contain sensitive data based on its name`,
116
+ });
117
+ }
118
+ }
119
+
120
+ return {
121
+ findings,
122
+ hasCritical: findings.some((f) => f.severity === 'critical'),
123
+ hasHigh: findings.some((f) => f.severity === 'high'),
124
+ total: findings.length,
125
+ };
126
+ }
127
+
128
+ module.exports = { scanForSecrets, SECRET_PATTERNS, SENSITIVE_KEY_PATTERNS };
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Environment variable validation engine
3
+ */
4
+
5
+ const validators = {
6
+ string: (value) => typeof value === 'string',
7
+
8
+ number: (value, rule) => {
9
+ const num = Number(value);
10
+ if (isNaN(num)) return { valid: false, error: `"${value}" is not a valid number` };
11
+ if (rule.min !== undefined && num < rule.min) {
12
+ return { valid: false, error: `${num} is less than minimum ${rule.min}` };
13
+ }
14
+ if (rule.max !== undefined && num > rule.max) {
15
+ return { valid: false, error: `${num} exceeds maximum ${rule.max}` };
16
+ }
17
+ return { valid: true };
18
+ },
19
+
20
+ boolean: (value) => {
21
+ const lower = String(value).toLowerCase();
22
+ if (['true', 'false', '1', '0', 'yes', 'no'].includes(lower)) {
23
+ return { valid: true };
24
+ }
25
+ return { valid: false, error: `"${value}" is not a valid boolean (true/false, 1/0, yes/no)` };
26
+ },
27
+
28
+ url: (value) => {
29
+ try {
30
+ new URL(value);
31
+ return { valid: true };
32
+ } catch {
33
+ return { valid: false, error: `"${value}" is not a valid URL` };
34
+ }
35
+ },
36
+
37
+ email: (value) => {
38
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
39
+ return emailRegex.test(value)
40
+ ? { valid: true }
41
+ : { valid: false, error: `"${value}" is not a valid email address` };
42
+ },
43
+
44
+ json: (value) => {
45
+ try {
46
+ JSON.parse(value);
47
+ return { valid: true };
48
+ } catch {
49
+ return { valid: false, error: `"${value}" is not valid JSON` };
50
+ }
51
+ },
52
+
53
+ regex: (value) => {
54
+ try {
55
+ new RegExp(value);
56
+ return { valid: true };
57
+ } catch {
58
+ return { valid: false, error: `"${value}" is not a valid regex pattern` };
59
+ }
60
+ },
61
+
62
+ port: (value) => {
63
+ const num = Number(value);
64
+ if (isNaN(num) || !Number.isInteger(num) || num < 0 || num > 65535) {
65
+ return { valid: false, error: `"${value}" is not a valid port (0-65535)` };
66
+ }
67
+ return { valid: true };
68
+ },
69
+ };
70
+
71
+ function validateValue(value, rule) {
72
+ const type = rule.type || 'string';
73
+ const validator = validators[type];
74
+
75
+ if (!validator) {
76
+ return { valid: false, error: `Unknown type: ${type}` };
77
+ }
78
+
79
+ const result = validator(value, rule);
80
+
81
+ // Validator can return boolean or { valid, error }
82
+ if (typeof result === 'boolean') {
83
+ return { valid: result, error: result ? undefined : `Value does not match type "${type}"` };
84
+ }
85
+ return result;
86
+ }
87
+
88
+ function validateEnv(envObj, schema) {
89
+ const results = {
90
+ valid: true,
91
+ errors: [],
92
+ warnings: [],
93
+ checked: 0,
94
+ };
95
+
96
+ for (const [key, rule] of Object.entries(schema)) {
97
+ results.checked++;
98
+ const value = envObj[key];
99
+
100
+ // Check required
101
+ if (value === undefined || value === '') {
102
+ if (rule.required) {
103
+ results.valid = false;
104
+ results.errors.push({
105
+ key,
106
+ type: 'missing',
107
+ message: `Required variable "${key}" is not set`,
108
+ });
109
+ } else if (rule.default !== undefined) {
110
+ // Will use default, just note it
111
+ results.warnings.push({
112
+ key,
113
+ type: 'default_used',
114
+ message: `Variable "${key}" not set, will use default: ${rule.default}`,
115
+ });
116
+ }
117
+ continue;
118
+ }
119
+
120
+ // Check type
121
+ const typeResult = validateValue(value, rule);
122
+ if (!typeResult.valid) {
123
+ results.valid = false;
124
+ results.errors.push({
125
+ key,
126
+ type: 'type_mismatch',
127
+ message: `Variable "${key}": ${typeResult.error}`,
128
+ });
129
+ continue;
130
+ }
131
+
132
+ // Check enum
133
+ if (rule.enum && !rule.enum.includes(value)) {
134
+ results.valid = false;
135
+ results.errors.push({
136
+ key,
137
+ type: 'enum_mismatch',
138
+ message: `Variable "${key}": value "${value}" is not one of: ${rule.enum.join(', ')}`,
139
+ });
140
+ continue;
141
+ }
142
+
143
+ // Check pattern
144
+ if (rule.pattern) {
145
+ const regex = new RegExp(rule.pattern);
146
+ if (!regex.test(value)) {
147
+ results.valid = false;
148
+ results.errors.push({
149
+ key,
150
+ type: 'pattern_mismatch',
151
+ message: `Variable "${key}": value does not match pattern /${rule.pattern}/`,
152
+ });
153
+ continue;
154
+ }
155
+ }
156
+
157
+ // Check deprecation
158
+ if (rule.deprecated) {
159
+ results.warnings.push({
160
+ key,
161
+ type: 'deprecated',
162
+ message: `Variable "${key}" is deprecated${rule.replacement ? `, use "${rule.replacement}" instead` : ''}`,
163
+ });
164
+ }
165
+ }
166
+
167
+ return results;
168
+ }
169
+
170
+ module.exports = { validateEnv, validateValue, validators };
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * EnvGuard — Environment variable & config validation, security scanning, and documentation generator
3
+ *
4
+ * Public API for programmatic usage
5
+ */
6
+
7
+ const core = require('./core');
8
+
9
+ module.exports = core;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * File system utilities for finding and loading .env files
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ const ENV_FILE_PATTERNS = [
9
+ '.env',
10
+ '.env.local',
11
+ '.env.development',
12
+ '.env.staging',
13
+ '.env.production',
14
+ '.env.test',
15
+ ];
16
+
17
+ function findEnvFiles(dir, options = {}) {
18
+ const recursive = options.recursive || false;
19
+ const maxDepth = options.maxDepth || 3;
20
+ const envFiles = [];
21
+
22
+ function scan(currentDir, depth) {
23
+ if (depth > maxDepth) return;
24
+
25
+ try {
26
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
27
+
28
+ for (const entry of entries) {
29
+ if (entry.name.startsWith('.') && entry.name.endsWith('.env')) {
30
+ envFiles.push(path.join(currentDir, entry.name));
31
+ } else if (entry.name === '.env') {
32
+ envFiles.push(path.join(currentDir, entry.name));
33
+ } else if (entry.name.startsWith('.env.')) {
34
+ envFiles.push(path.join(currentDir, entry.name));
35
+ }
36
+
37
+ // Also check for env files without leading dot
38
+ if (entry.name === 'env' || entry.name.startsWith('env.')) {
39
+ envFiles.push(path.join(currentDir, entry.name));
40
+ }
41
+
42
+ if (recursive && entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') {
43
+ scan(path.join(currentDir, entry.name), depth + 1);
44
+ }
45
+ }
46
+ } catch {
47
+ // Permission denied or other FS errors — skip
48
+ }
49
+ }
50
+
51
+ scan(dir, 0);
52
+ return [...new Set(envFiles)];
53
+ }
54
+
55
+ function findConfigFile(dir) {
56
+ const configNames = [
57
+ 'envguard.config.js',
58
+ 'envguard.config.cjs',
59
+ '.envguardrc.js',
60
+ '.envguardrc.cjs',
61
+ ];
62
+
63
+ for (const name of configNames) {
64
+ const fullPath = path.join(dir, name);
65
+ if (fs.existsSync(fullPath)) {
66
+ return fullPath;
67
+ }
68
+ }
69
+
70
+ return null;
71
+ }
72
+
73
+ function loadEnvFile(filePath) {
74
+ if (!fs.existsSync(filePath)) {
75
+ return {};
76
+ }
77
+
78
+ const content = fs.readFileSync(filePath, 'utf-8');
79
+ const vars = {};
80
+
81
+ for (const line of content.split('\n')) {
82
+ const trimmed = line.trim();
83
+ if (!trimmed || trimmed.startsWith('#')) continue;
84
+
85
+ const eqIndex = trimmed.indexOf('=');
86
+ if (eqIndex === -1) continue;
87
+
88
+ const key = trimmed.slice(0, eqIndex).trim();
89
+ let value = trimmed.slice(eqIndex + 1).trim();
90
+
91
+ // Remove surrounding quotes
92
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
93
+ value = value.slice(1, -1);
94
+ }
95
+
96
+ vars[key] = value;
97
+ }
98
+
99
+ return vars;
100
+ }
101
+
102
+ module.exports = { findEnvFiles, findConfigFile, loadEnvFile, ENV_FILE_PATTERNS };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Output formatting utilities
3
+ */
4
+
5
+ const COLORS = {
6
+ reset: '\x1b[0m',
7
+ red: '\x1b[31m',
8
+ green: '\x1b[32m',
9
+ yellow: '\x1b[33m',
10
+ blue: '\x1b[34m',
11
+ magenta: '\x1b[35m',
12
+ cyan: '\x1b[36m',
13
+ bold: '\x1b[1m',
14
+ dim: '\x1b[2m',
15
+ };
16
+
17
+ function colorize(text, color) {
18
+ if (process.env.NO_COLOR) return text;
19
+ return `${COLORS[color] || ''}${text}${COLORS.reset}`;
20
+ }
21
+
22
+ function formatError(error) {
23
+ return colorize(`✗ ${error.key}: ${error.message}`, 'red');
24
+ }
25
+
26
+ function formatWarning(warning) {
27
+ return colorize(`⚠ ${warning.key}: ${warning.message}`, 'yellow');
28
+ }
29
+
30
+ function formatSuccess(key) {
31
+ return colorize(`✓ ${key}`, 'green');
32
+ }
33
+
34
+ function formatFinding(finding) {
35
+ const severityColors = {
36
+ critical: 'red',
37
+ high: 'magenta',
38
+ medium: 'yellow',
39
+ low: 'blue',
40
+ };
41
+ const color = severityColors[finding.severity] || 'yellow';
42
+ const label = colorize(`[${finding.severity.toUpperCase()}]`, color);
43
+ return `${label} ${finding.message}`;
44
+ }
45
+
46
+ function printHeader(title) {
47
+ console.log('');
48
+ console.log(colorize(` ${'═'.repeat(40)}`, 'cyan'));
49
+ console.log(colorize(` ${title}`, 'bold'));
50
+ console.log(colorize(` ${'═'.repeat(40)}`, 'cyan'));
51
+ console.log('');
52
+ }
53
+
54
+ function printSummary(results) {
55
+ console.log('');
56
+ if (results.valid) {
57
+ console.log(colorize(' ✓ All checks passed!', 'green'));
58
+ } else {
59
+ console.log(colorize(` ✗ ${results.errors.length} error(s) found`, 'red'));
60
+ }
61
+ if (results.warnings && results.warnings.length > 0) {
62
+ console.log(colorize(` ⚠ ${results.warnings.length} warning(s)`, 'yellow'));
63
+ }
64
+ console.log(` Checked: ${results.checked} variable(s)`);
65
+ console.log('');
66
+ }
67
+
68
+ module.exports = { colorize, formatError, formatWarning, formatSuccess, formatFinding, printHeader, printSummary };