apigraveyard 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.
package/src/scanner.js ADDED
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Scanner Module
3
+ * Comprehensive API key scanning system for detecting exposed secrets in codebases
4
+ */
5
+
6
+ import { glob } from 'glob';
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+
10
+ /**
11
+ * API key patterns for various services
12
+ * Each pattern includes the service name and regex to match
13
+ * @type {Array<{service: string, pattern: RegExp}>}
14
+ */
15
+ const API_KEY_PATTERNS = [
16
+ { service: 'OpenAI', pattern: /sk-[A-Za-z0-9]{48}/g },
17
+ { service: 'Groq', pattern: /gsk_[A-Za-z0-9]{52}/g },
18
+ { service: 'GitHub', pattern: /gh[ps]_[A-Za-z0-9]{36}/g },
19
+ { service: 'Stripe', pattern: /sk_(live|test)_[A-Za-z0-9]{24}/g },
20
+ { service: 'Google/Firebase', pattern: /AIza[A-Za-z0-9_-]{35}/g },
21
+ { service: 'AWS', pattern: /AKIA[A-Z0-9]{16}/g },
22
+ { service: 'Anthropic', pattern: /sk-ant-[A-Za-z0-9-_]{95}/g },
23
+ { service: 'Hugging Face', pattern: /hf_[A-Za-z0-9]{34}/g }
24
+ ];
25
+
26
+ /**
27
+ * Default directories to ignore during scanning
28
+ * @type {string[]}
29
+ */
30
+ const DEFAULT_IGNORE_DIRS = [
31
+ 'node_modules',
32
+ '.git',
33
+ 'dist',
34
+ 'build',
35
+ '.next',
36
+ 'venv'
37
+ ];
38
+
39
+ /**
40
+ * Default files to ignore during scanning
41
+ * @type {string[]}
42
+ */
43
+ const DEFAULT_IGNORE_FILES = [
44
+ 'package-lock.json',
45
+ 'yarn.lock',
46
+ '.env.example',
47
+ '.env.sample'
48
+ ];
49
+
50
+ /**
51
+ * Masks an API key for safe display
52
+ * Shows first 4 and last 4 characters, replaces middle with asterisks
53
+ *
54
+ * @param {string} key - The full API key to mask
55
+ * @returns {string} - Masked key (e.g., "sk-ab***...***xyz")
56
+ *
57
+ * @example
58
+ * maskKey('sk-abcdefghijklmnopqrstuvwxyz1234567890ABCDEFGH')
59
+ * // Returns: 'sk-a***...***EFGH'
60
+ */
61
+ export function maskKey(key) {
62
+ if (key.length <= 8) {
63
+ return '*'.repeat(key.length);
64
+ }
65
+ const first = key.substring(0, 4);
66
+ const last = key.substring(key.length - 4);
67
+ return `${first}***...***${last}`;
68
+ }
69
+
70
+ /**
71
+ * Finds the line number and column position of a match in text
72
+ *
73
+ * @param {string} content - The full file content
74
+ * @param {number} matchIndex - The character index where the match starts
75
+ * @returns {{lineNumber: number, column: number}} - 1-indexed line and column
76
+ *
77
+ * @example
78
+ * getLineAndColumn('hello\nworld\ntest', 6)
79
+ * // Returns: { lineNumber: 2, column: 1 }
80
+ */
81
+ export function getLineAndColumn(content, matchIndex) {
82
+ const lines = content.substring(0, matchIndex).split('\n');
83
+ const lineNumber = lines.length;
84
+ const column = lines[lines.length - 1].length + 1;
85
+ return { lineNumber, column };
86
+ }
87
+
88
+ /**
89
+ * Scans a single file for API key patterns
90
+ *
91
+ * @param {string} filePath - Absolute path to the file
92
+ * @param {string} basePath - Base directory for relative path calculation
93
+ * @returns {Promise<Array<{service: string, key: string, fullKey: string, filePath: string, lineNumber: number, column: number}>>}
94
+ * Array of found API keys with their locations
95
+ *
96
+ * @throws {Error} If file cannot be read (permissions, binary, etc.)
97
+ */
98
+ async function scanFile(filePath, basePath) {
99
+ const results = [];
100
+
101
+ try {
102
+ const content = await fs.readFile(filePath, 'utf-8');
103
+ const relativePath = path.relative(basePath, filePath).replace(/\\/g, '/');
104
+
105
+ for (const { service, pattern } of API_KEY_PATTERNS) {
106
+ // Reset regex lastIndex for each file
107
+ const regex = new RegExp(pattern.source, pattern.flags);
108
+ let match;
109
+
110
+ while ((match = regex.exec(content)) !== null) {
111
+ const { lineNumber, column } = getLineAndColumn(content, match.index);
112
+
113
+ results.push({
114
+ service,
115
+ key: maskKey(match[0]),
116
+ fullKey: match[0],
117
+ filePath: relativePath,
118
+ lineNumber,
119
+ column
120
+ });
121
+ }
122
+ }
123
+ } catch (error) {
124
+ // Check if it's a binary file or permission error
125
+ if (error.code === 'ENOENT') {
126
+ console.warn(`⚠️ File not found: ${filePath}`);
127
+ } else if (error.code === 'EACCES') {
128
+ console.warn(`⚠️ Permission denied: ${filePath}`);
129
+ } else if (error.message.includes('encoding')) {
130
+ // Binary file, skip silently
131
+ } else {
132
+ console.warn(`⚠️ Could not read file: ${filePath} (${error.message})`);
133
+ }
134
+ }
135
+
136
+ return results;
137
+ }
138
+
139
+ /**
140
+ * Checks if a file should be ignored based on ignore patterns
141
+ *
142
+ * @param {string} filePath - Path to check
143
+ * @param {string[]} ignorePatterns - Array of patterns/names to ignore
144
+ * @returns {boolean} - True if file should be ignored
145
+ */
146
+ function shouldIgnoreFile(filePath, ignorePatterns) {
147
+ const fileName = path.basename(filePath);
148
+ const normalizedPath = filePath.replace(/\\/g, '/');
149
+
150
+ for (const pattern of ignorePatterns) {
151
+ // Check if pattern matches filename directly
152
+ if (fileName === pattern) {
153
+ return true;
154
+ }
155
+ // Check if pattern appears in path (for directory patterns)
156
+ if (normalizedPath.includes(`/${pattern}/`) || normalizedPath.includes(`${pattern}/`)) {
157
+ return true;
158
+ }
159
+ }
160
+
161
+ return false;
162
+ }
163
+
164
+ /**
165
+ * Removes duplicate API keys from results
166
+ * Keys are considered duplicates if they have the same fullKey value
167
+ *
168
+ * @param {Array<{service: string, key: string, fullKey: string, filePath: string, lineNumber: number, column: number}>} keys
169
+ * Array of found keys with possible duplicates
170
+ * @returns {Array} - Deduplicated array (keeps first occurrence)
171
+ */
172
+ function deduplicateKeys(keys) {
173
+ const seen = new Set();
174
+ return keys.filter(keyInfo => {
175
+ if (seen.has(keyInfo.fullKey)) {
176
+ return false;
177
+ }
178
+ seen.add(keyInfo.fullKey);
179
+ return true;
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Scans a directory for exposed API keys in source files
185
+ *
186
+ * Recursively searches through all files in the specified directory,
187
+ * applying regex patterns to detect API keys from various services
188
+ * (OpenAI, GitHub, Stripe, AWS, etc.)
189
+ *
190
+ * @param {string} dirPath - Root directory to scan (absolute or relative path)
191
+ * @param {Object} [options={}] - Scanning options
192
+ * @param {boolean} [options.recursive=true] - Whether to scan subdirectories
193
+ * @param {string[]} [options.ignorePatterns=[]] - Additional patterns to ignore
194
+ *
195
+ * @returns {Promise<{totalFiles: number, keysFound: Array<{service: string, key: string, fullKey: string, filePath: string, lineNumber: number, column: number}>}>}
196
+ * Scan results with total files scanned and array of found keys
197
+ *
198
+ * @example
199
+ * const results = await scanDirectory('./src', { recursive: true });
200
+ * console.log(`Found ${results.keysFound.length} keys in ${results.totalFiles} files`);
201
+ *
202
+ * @example
203
+ * // With custom ignore patterns
204
+ * const results = await scanDirectory('./project', {
205
+ * recursive: true,
206
+ * ignorePatterns: ['*.test.js', 'fixtures']
207
+ * });
208
+ */
209
+ export async function scanDirectory(dirPath, options = {}) {
210
+ const {
211
+ recursive = true,
212
+ ignorePatterns = []
213
+ } = options;
214
+
215
+ // Combine default and custom ignore patterns
216
+ const allIgnorePatterns = [
217
+ ...DEFAULT_IGNORE_DIRS,
218
+ ...DEFAULT_IGNORE_FILES,
219
+ ...ignorePatterns
220
+ ];
221
+
222
+ // Build glob pattern
223
+ const globPattern = recursive
224
+ ? path.join(dirPath, '**', '*').replace(/\\/g, '/')
225
+ : path.join(dirPath, '*').replace(/\\/g, '/');
226
+
227
+ // Build ignore patterns for glob
228
+ const globIgnore = DEFAULT_IGNORE_DIRS.map(dir => `**/${dir}/**`);
229
+
230
+ let files = [];
231
+ let totalFiles = 0;
232
+ const allKeys = [];
233
+
234
+ try {
235
+ // Find all files matching pattern
236
+ files = await glob(globPattern, {
237
+ nodir: true,
238
+ ignore: globIgnore,
239
+ dot: true // Include dotfiles
240
+ });
241
+
242
+ // Filter out ignored files
243
+ files = files.filter(file => !shouldIgnoreFile(file, allIgnorePatterns));
244
+ totalFiles = files.length;
245
+
246
+ // Scan each file in parallel with concurrency limit
247
+ const BATCH_SIZE = 50;
248
+ for (let i = 0; i < files.length; i += BATCH_SIZE) {
249
+ const batch = files.slice(i, i + BATCH_SIZE);
250
+ const batchResults = await Promise.all(
251
+ batch.map(file => scanFile(file, dirPath))
252
+ );
253
+
254
+ for (const results of batchResults) {
255
+ allKeys.push(...results);
256
+ }
257
+ }
258
+
259
+ } catch (error) {
260
+ console.error(`❌ Error scanning directory: ${error.message}`);
261
+ // Return partial results
262
+ return {
263
+ totalFiles,
264
+ keysFound: deduplicateKeys(allKeys),
265
+ error: error.message
266
+ };
267
+ }
268
+
269
+ // Remove duplicates and return
270
+ return {
271
+ totalFiles,
272
+ keysFound: deduplicateKeys(allKeys)
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Gets the list of supported API key patterns
278
+ * Useful for documentation or UI display
279
+ *
280
+ * @returns {Array<{service: string, pattern: string}>} - Array of service names and their patterns
281
+ */
282
+ export function getSupportedPatterns() {
283
+ return API_KEY_PATTERNS.map(({ service, pattern }) => ({
284
+ service,
285
+ pattern: pattern.source
286
+ }));
287
+ }
288
+
289
+ export default {
290
+ scanDirectory,
291
+ maskKey,
292
+ getLineAndColumn,
293
+ getSupportedPatterns
294
+ };