api-key-guard 1.0.5 → 1.1.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 CHANGED
@@ -35,7 +35,8 @@ API key leaks in code repositories are a critical security vulnerability that ca
35
35
  ## ✨ Features
36
36
 
37
37
  - 🔍 **Smart Detection**: Advanced regex patterns detect AWS keys, GitHub tokens, Google API keys, and more
38
- - 🔒 **Git Hooks Integration**: Automatic pre-commit scanning to prevent leaks
38
+ - **Auto-Fix Keys**: Automatically replace hardcoded keys with environment variables
39
+ - �🔒 **Git Hooks Integration**: Automatic pre-commit scanning to prevent leaks
39
40
  - 🤖 **AI-Powered README**: Generate professional documentation using Google's Gemini API
40
41
  - ⚡ **Fast Scanning**: Efficient file parsing with configurable ignore patterns
41
42
  - 🌈 **Clear Output**: Color-coded results with detailed reporting
@@ -55,6 +56,21 @@ api-key-guard scan --path ./src
55
56
  api-key-guard scan --verbose
56
57
  ```
57
58
 
59
+ ### Fix Hardcoded Keys
60
+ ```bash
61
+ # Automatically fix hardcoded API keys
62
+ api-key-guard fix
63
+
64
+ # Preview fixes without applying
65
+ api-key-guard fix --dry-run
66
+
67
+ # Fix specific file only
68
+ api-key-guard fix --file src/config.js
69
+
70
+ # Fix without creating backups
71
+ api-key-guard fix --no-backup
72
+ ```
73
+
58
74
  ### Git Hooks Setup
59
75
  ```bash
60
76
  # Install pre-commit hook
@@ -173,6 +189,26 @@ api-key-guard scan --verbose
173
189
  # Pattern: sk-1234567890abcdef...
174
190
  ```
175
191
 
192
+ **Automatic Key Fixing:**
193
+ ```bash
194
+ api-key-guard fix --dry-run
195
+ # 📋 Found 3 fixable API key(s):
196
+ # 📄 src/config.js:15
197
+ # const apiKey = "sk-1234567890abcdef"
198
+ # → const apiKey = process.env.API_KEY
199
+ #
200
+ # 📄 src/auth.js:8
201
+ # const token = "ghp_abcdefghijklmnop"
202
+ # → const token = process.env.GITHUB_TOKEN
203
+
204
+ # Apply fixes
205
+ api-key-guard fix
206
+ # 🔧 Applying fixes...
207
+ # ✅ Successfully fixed 3 API keys!
208
+ # 📝 Updated 2 file(s)
209
+ # 🔐 Added 3 environment variable(s)
210
+ ```
211
+
176
212
  ## 🤝 Contributing
177
213
 
178
214
  1. Fork the repository
package/bin/cli.js CHANGED
@@ -5,6 +5,7 @@ const chalk = require('chalk');
5
5
  const { generateReadme } = require('../src/readmeGenerator');
6
6
  const { scanForApiKeys } = require('../src/scanner');
7
7
  const { setupGitHooks } = require('../src/gitHooks');
8
+ const { fixApiKeys } = require('../src/keyFixer');
8
9
 
9
10
  const program = new Command();
10
11
 
@@ -59,4 +60,22 @@ program
59
60
  }
60
61
  });
61
62
 
63
+ // Fix API keys command
64
+ program
65
+ .command('fix')
66
+ .description('Automatically replace hardcoded API keys with environment variables')
67
+ .option('-p, --path <path>', 'Path to scan and fix', '.')
68
+ .option('-d, --dry-run', 'Preview changes without applying them')
69
+ .option('-f, --file <file>', 'Fix specific file only')
70
+ .option('--backup', 'Create backup files before fixing', true)
71
+ .action(async (options) => {
72
+ try {
73
+ console.log(chalk.blue('🔧 Fixing hardcoded API keys...'));
74
+ await fixApiKeys(options);
75
+ } catch (error) {
76
+ console.error(chalk.red('Error fixing keys:', error.message));
77
+ process.exit(1);
78
+ }
79
+ });
80
+
62
81
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-key-guard",
3
- "version": "1.0.5",
3
+ "version": "1.1.1",
4
4
  "description": "A comprehensive tool to detect, prevent, and manage API key leaks in your codebase with AI-powered README generation",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  const { scanForApiKeys } = require('./scanner');
2
2
  const { setupGitHooks } = require('./gitHooks');
3
3
  const { generateReadme } = require('./readmeGenerator');
4
+ const { fixApiKeys } = require('./keyFixer');
4
5
 
5
6
  module.exports = {
6
7
  scanForApiKeys,
7
8
  setupGitHooks,
8
- generateReadme
9
+ generateReadme,
10
+ fixApiKeys
9
11
  };
@@ -0,0 +1,406 @@
1
+ const fs = require('fs').promises;
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const readline = require('readline');
5
+
6
+ /**
7
+ * Fix hardcoded API keys by replacing them with environment variables
8
+ */
9
+ async function fixApiKeys(options = {}) {
10
+ const scanPath = options.path || '.';
11
+ const dryRun = options.dryRun || false;
12
+ const specificFile = options.file;
13
+ const createBackup = options.backup !== false;
14
+
15
+ console.log(chalk.blue(`🔍 Scanning for fixable API keys in ${scanPath}...`));
16
+
17
+ const findings = await scanForFixableKeys(scanPath, specificFile);
18
+
19
+ if (findings.length === 0) {
20
+ console.log(chalk.green('✅ No fixable API keys found!'));
21
+ return;
22
+ }
23
+
24
+ console.log(chalk.yellow(`📋 Found ${findings.length} fixable API key(s):`));
25
+ findings.forEach(finding => {
26
+ console.log(chalk.cyan(` 📄 ${finding.file}:${finding.line}`));
27
+ console.log(chalk.gray(` ${finding.originalLine.trim()}`));
28
+ console.log(chalk.green(` → ${finding.fixedLine.trim()}`));
29
+ });
30
+
31
+ if (dryRun) {
32
+ console.log(chalk.blue('\n🔍 Dry run complete. No changes were made.'));
33
+ console.log(chalk.gray('Run without --dry-run to apply fixes.'));
34
+ return;
35
+ }
36
+
37
+ const shouldProceed = await promptConfirmation(
38
+ `\n❓ Apply these ${findings.length} fixes? (y/N): `
39
+ );
40
+
41
+ if (!shouldProceed) {
42
+ console.log(chalk.yellow('Operation cancelled.'));
43
+ return;
44
+ }
45
+
46
+ console.log(chalk.blue('🔧 Applying fixes...'));
47
+
48
+ const results = await applyFixes(findings, createBackup);
49
+ await updateEnvironmentFiles(results.envVars);
50
+ await updateGitignore();
51
+
52
+ console.log(chalk.green(`✅ Successfully fixed ${results.fixedCount} API keys!`));
53
+ console.log(chalk.blue(`📝 Updated ${results.filesModified} file(s)`));
54
+ console.log(chalk.blue(`🔐 Added ${results.envVars.length} environment variable(s)`));
55
+
56
+ if (createBackup) {
57
+ console.log(chalk.gray(`💾 Backup files created with .backup extension`));
58
+ }
59
+
60
+ console.log(chalk.yellow('\n⚠️ Next steps:'));
61
+ console.log(chalk.yellow(' 1. Review the generated .env file'));
62
+ console.log(chalk.yellow(' 2. Add .env to your .gitignore (done automatically)'));
63
+ console.log(chalk.yellow(' 3. Update your deployment with the new environment variables'));
64
+ }
65
+
66
+ /**
67
+ * Scan for API keys that can be automatically fixed
68
+ */
69
+ async function scanForFixableKeys(scanPath, specificFile) {
70
+ const findings = [];
71
+
72
+ // Key patterns that can be fixed automatically
73
+ const fixablePatterns = [
74
+ {
75
+ name: 'JavaScript/TypeScript API Key Assignment',
76
+ pattern: /^(\s*)(const|let|var)\s+(\w+)\s*=\s*['"`]([A-Za-z0-9_\-]{20,})['"`]/gm,
77
+ language: 'javascript',
78
+ envVarNamer: (varName) => varName.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()
79
+ },
80
+ {
81
+ name: 'Python API Key Assignment',
82
+ pattern: /^(\s*)(\w+)\s*=\s*['"`]([A-Za-z0-9_\-]{20,})['"`]/gm,
83
+ language: 'python',
84
+ envVarNamer: (varName) => varName.replace(/([a-z])([A-Z])/g, '$1_$2').upper()
85
+ },
86
+ {
87
+ name: 'JSON Configuration',
88
+ pattern: /"(\w*[Kk]ey\w*|token|secret)"\s*:\s*"([A-Za-z0-9_\-]{20,})"/gm,
89
+ language: 'json',
90
+ envVarNamer: (keyName) => keyName.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase()
91
+ }
92
+ ];
93
+
94
+ const files = specificFile ? [specificFile] : await getFilesRecursively(scanPath);
95
+
96
+ for (const file of files) {
97
+ try {
98
+ const content = await fs.readFile(file, 'utf8');
99
+ const lines = content.split('\n');
100
+
101
+ for (const patternConfig of fixablePatterns) {
102
+ let match;
103
+ patternConfig.pattern.lastIndex = 0; // Reset regex
104
+
105
+ while ((match = patternConfig.pattern.exec(content)) !== null) {
106
+ const lineNumber = content.substring(0, match.index).split('\n').length;
107
+ const originalLine = lines[lineNumber - 1];
108
+
109
+ let envVarName, fixedLine;
110
+
111
+ if (patternConfig.language === 'javascript') {
112
+ const [, indent, declaration, varName, keyValue] = match;
113
+ envVarName = patternConfig.envVarNamer(varName);
114
+ fixedLine = `${indent}${declaration} ${varName} = process.env.${envVarName}`;
115
+ } else if (patternConfig.language === 'python') {
116
+ const [, indent, varName, keyValue] = match;
117
+ envVarName = patternConfig.envVarNamer(varName);
118
+ fixedLine = `${indent}${varName} = os.getenv('${envVarName}')`;
119
+ } else if (patternConfig.language === 'json') {
120
+ const [fullMatch, keyName, keyValue] = match;
121
+ envVarName = patternConfig.envVarNamer(keyName);
122
+ // For JSON, we might suggest moving to a config loader
123
+ fixedLine = `"${keyName}": "\\$\\{process.env.${envVarName} || 'YOUR_${envVarName}_HERE'\\}"`;
124
+ }
125
+
126
+ // Only include keys that look like real API keys
127
+ const keyValue = patternConfig.language === 'json' ? match[2] :
128
+ patternConfig.language === 'python' ? match[3] : match[4];
129
+
130
+ if (looksLikeApiKey(keyValue)) {
131
+ findings.push({
132
+ file: path.relative(process.cwd(), file),
133
+ line: lineNumber,
134
+ originalLine,
135
+ fixedLine,
136
+ envVarName,
137
+ keyValue,
138
+ language: patternConfig.language,
139
+ pattern: patternConfig.name
140
+ });
141
+ }
142
+ }
143
+ }
144
+ } catch (error) {
145
+ // Skip files we can't read
146
+ continue;
147
+ }
148
+ }
149
+
150
+ return findings;
151
+ }
152
+
153
+ /**
154
+ * Apply the fixes to files
155
+ */
156
+ async function applyFixes(findings, createBackup) {
157
+ const results = {
158
+ fixedCount: 0,
159
+ filesModified: 0,
160
+ envVars: []
161
+ };
162
+
163
+ const fileGroups = groupFindingsByFile(findings);
164
+
165
+ for (const [filePath, fileFindings] of Object.entries(fileGroups)) {
166
+ try {
167
+ const fullPath = path.resolve(filePath);
168
+ const content = await fs.readFile(fullPath, 'utf8');
169
+ const lines = content.split('\n');
170
+
171
+ // Create backup if requested
172
+ if (createBackup) {
173
+ await fs.writeFile(`${fullPath}.backup`, content, 'utf8');
174
+ }
175
+
176
+ // Apply fixes (in reverse order to maintain line numbers)
177
+ const sortedFindings = fileFindings.sort((a, b) => b.line - a.line);
178
+
179
+ for (const finding of sortedFindings) {
180
+ lines[finding.line - 1] = finding.fixedLine;
181
+ results.fixedCount++;
182
+
183
+ // Collect environment variables
184
+ results.envVars.push({
185
+ name: finding.envVarName,
186
+ value: finding.keyValue,
187
+ comment: `# ${finding.pattern} from ${finding.file}:${finding.line}`
188
+ });
189
+ }
190
+
191
+ // Write the fixed content
192
+ await fs.writeFile(fullPath, lines.join('\n'), 'utf8');
193
+ results.filesModified++;
194
+
195
+ } catch (error) {
196
+ console.error(chalk.red(`Error fixing ${filePath}: ${error.message}`));
197
+ }
198
+ }
199
+
200
+ return results;
201
+ }
202
+
203
+ /**
204
+ * Update or create environment files
205
+ */
206
+ async function updateEnvironmentFiles(envVars) {
207
+ const envPath = path.join(process.cwd(), '.env');
208
+ const envExamplePath = path.join(process.cwd(), '.env.example');
209
+
210
+ // Read existing .env file if it exists
211
+ let existingEnvContent = '';
212
+ try {
213
+ existingEnvContent = await fs.readFile(envPath, 'utf8');
214
+ } catch (error) {
215
+ // File doesn't exist, will create new
216
+ }
217
+
218
+ // Prepare new environment variables
219
+ const newEnvLines = [];
220
+ const exampleLines = [];
221
+
222
+ for (const envVar of envVars) {
223
+ // Only add if not already in .env file
224
+ if (!existingEnvContent.includes(`${envVar.name}=`)) {
225
+ newEnvLines.push('');
226
+ newEnvLines.push(envVar.comment);
227
+ newEnvLines.push(`${envVar.name}=${envVar.value}`);
228
+
229
+ // Add to example file (without real values)
230
+ exampleLines.push('');
231
+ exampleLines.push(envVar.comment);
232
+ exampleLines.push(`${envVar.name}=your_${envVar.name.toLowerCase()}_here`);
233
+ }
234
+ }
235
+
236
+ if (newEnvLines.length > 0) {
237
+ // Append to .env file
238
+ const updatedContent = existingEnvContent + '\n# Added by api-key-guard fix command' + newEnvLines.join('\n') + '\n';
239
+ await fs.writeFile(envPath, updatedContent, 'utf8');
240
+
241
+ // Create/update .env.example
242
+ let exampleContent = '';
243
+ try {
244
+ exampleContent = await fs.readFile(envExamplePath, 'utf8');
245
+ } catch (error) {
246
+ exampleContent = '# Environment variables template\n# Copy to .env and fill with real values\n';
247
+ }
248
+
249
+ const updatedExampleContent = exampleContent + '\n# Added by api-key-guard fix command' + exampleLines.join('\n') + '\n';
250
+ await fs.writeFile(envExamplePath, updatedExampleContent, 'utf8');
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Update .gitignore to include .env
256
+ */
257
+ async function updateGitignore() {
258
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
259
+
260
+ try {
261
+ let gitignoreContent = '';
262
+ try {
263
+ gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
264
+ } catch (error) {
265
+ // File doesn't exist, create new
266
+ gitignoreContent = '';
267
+ }
268
+
269
+ // Check if .env is already ignored
270
+ if (!gitignoreContent.includes('.env') || !gitignoreContent.includes('*.env')) {
271
+ const envIgnoreRules = [
272
+ '\n# Environment variables (added by api-key-guard)',
273
+ '.env',
274
+ '.env.local',
275
+ '.env.*.local',
276
+ '*.env\n'
277
+ ];
278
+
279
+ const updatedContent = gitignoreContent + envIgnoreRules.join('\n');
280
+ await fs.writeFile(gitignorePath, updatedContent, 'utf8');
281
+ }
282
+ } catch (error) {
283
+ console.warn(chalk.yellow('Warning: Could not update .gitignore'));
284
+ }
285
+ }
286
+
287
+ /**
288
+ * Check if a string looks like a real API key
289
+ */
290
+ function looksLikeApiKey(str) {
291
+ // Basic heuristics for API keys
292
+ const minLength = 20;
293
+ const hasLettersAndNumbers = /[a-zA-Z]/.test(str) && /[0-9]/.test(str);
294
+ const notCommonWords = !['example', 'test', 'demo', 'sample', 'placeholder', 'your'].some(word =>
295
+ str.toLowerCase().includes(word));
296
+
297
+ return str.length >= minLength && hasLettersAndNumbers && notCommonWords;
298
+ }
299
+
300
+ /**
301
+ * Group findings by file path
302
+ */
303
+ function groupFindingsByFile(findings) {
304
+ const groups = {};
305
+ for (const finding of findings) {
306
+ if (!groups[finding.file]) {
307
+ groups[finding.file] = [];
308
+ }
309
+ groups[finding.file].push(finding);
310
+ }
311
+ return groups;
312
+ }
313
+
314
+ /**
315
+ * Get all files recursively, respecting ignore patterns
316
+ */
317
+ async function getFilesRecursively(dir, ignorePatterns = []) {
318
+ let files = [];
319
+
320
+ const defaultIgnores = [
321
+ 'node_modules', '.git', 'dist', 'build', '.next', 'coverage',
322
+ '*.min.js', '*.bundle.js', 'package-lock.json', 'yarn.lock'
323
+ ];
324
+
325
+ const allIgnores = [...defaultIgnores, ...ignorePatterns];
326
+
327
+ try {
328
+ const entries = await fs.readdir(dir, { withFileTypes: true });
329
+
330
+ for (const entry of entries) {
331
+ const fullPath = path.join(dir, entry.name);
332
+
333
+ // Check if should be ignored
334
+ if (shouldIgnore(entry.name, fullPath, allIgnores)) {
335
+ continue;
336
+ }
337
+
338
+ if (entry.isDirectory()) {
339
+ files = files.concat(await getFilesRecursively(fullPath, ignorePatterns));
340
+ } else {
341
+ // Only scan text files that might contain code
342
+ if (isCodeFile(entry.name)) {
343
+ files.push(fullPath);
344
+ }
345
+ }
346
+ }
347
+ } catch (error) {
348
+ // Skip directories we can't read
349
+ }
350
+
351
+ return files;
352
+ }
353
+
354
+ /**
355
+ * Check if file should be ignored
356
+ */
357
+ function shouldIgnore(name, fullPath, ignorePatterns) {
358
+ for (const pattern of ignorePatterns) {
359
+ if (pattern.includes('*')) {
360
+ const regex = new RegExp(pattern.replace(/\*/g, '.*'));
361
+ if (regex.test(name)) {
362
+ return true;
363
+ }
364
+ } else {
365
+ if (name === pattern || fullPath.includes(pattern)) {
366
+ return true;
367
+ }
368
+ }
369
+ }
370
+ return false;
371
+ }
372
+
373
+ /**
374
+ * Check if file is a code file that might contain API keys
375
+ */
376
+ function isCodeFile(filename) {
377
+ const codeExtensions = [
378
+ '.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go', '.php', '.rb',
379
+ '.cs', '.cpp', '.c', '.h', '.rs', '.swift', '.kt', '.scala',
380
+ '.json', '.yml', '.yaml', '.xml', '.env', '.config', '.conf'
381
+ ];
382
+
383
+ const ext = path.extname(filename).toLowerCase();
384
+ return codeExtensions.includes(ext) || !ext; // Include files without extensions
385
+ }
386
+
387
+ /**
388
+ * Prompt user for confirmation
389
+ */
390
+ function promptConfirmation(question) {
391
+ return new Promise((resolve) => {
392
+ const rl = readline.createInterface({
393
+ input: process.stdin,
394
+ output: process.stdout
395
+ });
396
+
397
+ rl.question(question, (answer) => {
398
+ rl.close();
399
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
400
+ });
401
+ });
402
+ }
403
+
404
+ module.exports = {
405
+ fixApiKeys
406
+ };