cryptoserve 0.1.3 → 0.2.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 +97 -10
- package/bin/cryptoserve.mjs +259 -11
- package/lib/algorithm-db.mjs +260 -0
- package/lib/cbom.mjs +298 -0
- package/lib/config.mjs +87 -0
- package/lib/crypto-registry.mjs +161 -0
- package/lib/pqc-engine.mjs +220 -59
- package/lib/scanner-binary.mjs +151 -0
- package/lib/scanner-languages.mjs +242 -0
- package/lib/scanner-manifests.mjs +200 -0
- package/lib/scanner-tls.mjs +173 -0
- package/lib/scanner.mjs +133 -104
- package/lib/walker.mjs +166 -0
- package/package.json +7 -2
package/lib/scanner.mjs
CHANGED
|
@@ -1,47 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Cryptographic dependency and secret scanner — orchestrates all sub-scanners.
|
|
3
3
|
*
|
|
4
4
|
* Scans projects for:
|
|
5
|
-
* 1. Cryptographic dependencies (package.json,
|
|
6
|
-
* 2.
|
|
7
|
-
* 3.
|
|
5
|
+
* 1. Cryptographic dependencies (package.json, go.mod, requirements.txt, etc.)
|
|
6
|
+
* 2. Source code crypto patterns (JS/TS, Go, Python, Java, Rust, C/C++)
|
|
7
|
+
* 3. Hardcoded secrets (API keys, passwords — patterns from secretless-ai)
|
|
8
|
+
* 4. Certificate/key files (.pem, .key, .crt, .p12)
|
|
9
|
+
* 5. TLS version issues in config files
|
|
10
|
+
* 6. Binary crypto signatures (optional, via --binary flag)
|
|
8
11
|
*
|
|
9
12
|
* Output matches the library inventory format used by pqc-engine.mjs.
|
|
10
13
|
* Zero dependencies — uses only node:fs and node:path.
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
|
-
import { existsSync, readFileSync, readdirSync
|
|
16
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
14
17
|
import { join, relative, extname, basename } from 'node:path';
|
|
18
|
+
import { LANGUAGE_PATTERNS, scanSourceFile, detectLanguage, MULTI_LANG_EXTENSIONS } from './scanner-languages.mjs';
|
|
19
|
+
import { scanManifests } from './scanner-manifests.mjs';
|
|
20
|
+
import { scanTlsConfigs } from './scanner-tls.mjs';
|
|
21
|
+
import { lookupAlgorithm } from './algorithm-db.mjs';
|
|
22
|
+
import { lookupNpmPackage } from './crypto-registry.mjs';
|
|
23
|
+
import { walkProject } from './walker.mjs';
|
|
24
|
+
import { loadScannerConfig } from './config.mjs';
|
|
15
25
|
|
|
16
26
|
// ---------------------------------------------------------------------------
|
|
17
|
-
//
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
const CRYPTO_PACKAGES = {
|
|
21
|
-
'crypto-js': { algorithms: ['AES', 'DES', '3DES', 'MD5', 'SHA-256', 'SHA-512', 'HMAC'], quantumRisk: 'low', category: 'symmetric' },
|
|
22
|
-
'bcrypt': { algorithms: ['bcrypt'], quantumRisk: 'none', category: 'kdf' },
|
|
23
|
-
'bcryptjs': { algorithms: ['bcrypt'], quantumRisk: 'none', category: 'kdf' },
|
|
24
|
-
'jsonwebtoken': { algorithms: ['RS256', 'HS256', 'ES256'], quantumRisk: 'high', category: 'token' },
|
|
25
|
-
'jose': { algorithms: ['RS256', 'ES256', 'EdDSA', 'AES-GCM'], quantumRisk: 'high', category: 'token' },
|
|
26
|
-
'node-forge': { algorithms: ['RSA', 'AES', 'SHA-256', 'HMAC', 'TLS'], quantumRisk: 'high', category: 'tls' },
|
|
27
|
-
'tweetnacl': { algorithms: ['X25519', 'Ed25519', 'XSalsa20'], quantumRisk: 'high', category: 'asymmetric' },
|
|
28
|
-
'libsodium-wrappers': { algorithms: ['X25519', 'Ed25519', 'ChaCha20', 'AES-256-GCM'], quantumRisk: 'high', category: 'asymmetric' },
|
|
29
|
-
'@noble/curves': { algorithms: ['ECDSA', 'Ed25519', 'X25519'], quantumRisk: 'high', category: 'asymmetric' },
|
|
30
|
-
'@noble/hashes': { algorithms: ['SHA-256', 'SHA-512', 'SHA3', 'Blake2'], quantumRisk: 'low', category: 'hash' },
|
|
31
|
-
'@noble/post-quantum': { algorithms: ['ML-KEM', 'ML-DSA', 'SLH-DSA'], quantumRisk: 'none', category: 'pqc' },
|
|
32
|
-
'openpgp': { algorithms: ['RSA', 'ECDSA', 'AES', 'SHA-256'], quantumRisk: 'high', category: 'asymmetric' },
|
|
33
|
-
'elliptic': { algorithms: ['ECDSA', 'ECDHE', 'Ed25519'], quantumRisk: 'high', category: 'asymmetric' },
|
|
34
|
-
'secp256k1': { algorithms: ['ECDSA'], quantumRisk: 'high', category: 'asymmetric' },
|
|
35
|
-
'argon2': { algorithms: ['Argon2'], quantumRisk: 'none', category: 'kdf' },
|
|
36
|
-
'scrypt': { algorithms: ['scrypt'], quantumRisk: 'none', category: 'kdf' },
|
|
37
|
-
'pbkdf2': { algorithms: ['PBKDF2'], quantumRisk: 'none', category: 'kdf' },
|
|
38
|
-
'tls': { algorithms: ['TLS', 'RSA', 'ECDSA'], quantumRisk: 'high', category: 'tls' },
|
|
39
|
-
'ssh2': { algorithms: ['RSA', 'Ed25519', 'ECDSA', 'AES'], quantumRisk: 'high', category: 'asymmetric' },
|
|
40
|
-
'node-rsa': { algorithms: ['RSA'], quantumRisk: 'high', category: 'asymmetric' },
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// Import/require patterns to detect in source code
|
|
27
|
+
// Import/require patterns to detect in JS/TS source code
|
|
45
28
|
// ---------------------------------------------------------------------------
|
|
46
29
|
|
|
47
30
|
const IMPORT_PATTERNS = [
|
|
@@ -117,71 +100,29 @@ const SECRET_PATTERNS = [
|
|
|
117
100
|
// File patterns for cert/key discovery
|
|
118
101
|
const CERT_EXTENSIONS = new Set(['.pem', '.key', '.crt', '.p12', '.pfx', '.jks', '.keystore']);
|
|
119
102
|
|
|
120
|
-
|
|
121
|
-
// File walker
|
|
122
|
-
// ---------------------------------------------------------------------------
|
|
123
|
-
|
|
124
|
-
const SKIP_DIRS = new Set([
|
|
125
|
-
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
126
|
-
'.cache', '.nuxt', '.output', '.svelte-kit', '__pycache__',
|
|
127
|
-
'vendor', '.venv', 'venv',
|
|
128
|
-
]);
|
|
129
|
-
|
|
130
|
-
const SOURCE_EXTENSIONS = new Set(['.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx']);
|
|
131
|
-
|
|
132
|
-
function walkFiles(dir, maxFiles = 10000, maxBytes = 500 * 1024 * 1024) {
|
|
133
|
-
const files = [];
|
|
134
|
-
let totalBytes = 0;
|
|
135
|
-
|
|
136
|
-
function walk(currentDir) {
|
|
137
|
-
if (files.length >= maxFiles || totalBytes >= maxBytes) return;
|
|
138
|
-
|
|
139
|
-
let entries;
|
|
140
|
-
try { entries = readdirSync(currentDir, { withFileTypes: true }); }
|
|
141
|
-
catch { return; }
|
|
142
|
-
|
|
143
|
-
for (const entry of entries) {
|
|
144
|
-
if (files.length >= maxFiles || totalBytes >= maxBytes) return;
|
|
145
|
-
|
|
146
|
-
if (entry.isDirectory()) {
|
|
147
|
-
if (!SKIP_DIRS.has(entry.name) && !entry.name.startsWith('.')) {
|
|
148
|
-
walk(join(currentDir, entry.name));
|
|
149
|
-
}
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!entry.isFile()) continue;
|
|
154
|
-
|
|
155
|
-
const filePath = join(currentDir, entry.name);
|
|
156
|
-
try {
|
|
157
|
-
const stat = statSync(filePath);
|
|
158
|
-
if (stat.size > 1024 * 1024) continue; // Skip files >1MB
|
|
159
|
-
totalBytes += stat.size;
|
|
160
|
-
files.push(filePath);
|
|
161
|
-
} catch { continue; }
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
walk(dir);
|
|
166
|
-
return files;
|
|
167
|
-
}
|
|
103
|
+
const JS_EXTENSIONS = new Set(['.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx']);
|
|
168
104
|
|
|
169
105
|
// ---------------------------------------------------------------------------
|
|
170
106
|
// Scanner
|
|
171
107
|
// ---------------------------------------------------------------------------
|
|
172
108
|
|
|
173
|
-
export function scanProject(projectDir) {
|
|
109
|
+
export function scanProject(projectDir, options = {}) {
|
|
174
110
|
const results = {
|
|
175
111
|
libraries: [],
|
|
176
112
|
secrets: [],
|
|
177
113
|
weakPatterns: [],
|
|
178
114
|
certFiles: [],
|
|
179
115
|
filesScanned: 0,
|
|
116
|
+
// New in v0.2.0
|
|
117
|
+
sourceAlgorithms: [],
|
|
118
|
+
tlsFindings: [],
|
|
119
|
+
binaryFindings: [],
|
|
120
|
+
languagesDetected: new Set(),
|
|
121
|
+
manifestsFound: [],
|
|
180
122
|
};
|
|
181
123
|
|
|
182
124
|
// 1. Scan package.json for crypto dependencies (root + monorepo workspaces)
|
|
183
125
|
const pkgPaths = [join(projectDir, 'package.json')];
|
|
184
|
-
// Check common monorepo locations for nested package.json files
|
|
185
126
|
const monorepoGlobs = ['apps', 'packages', 'libs', 'modules', 'services'];
|
|
186
127
|
for (const sub of monorepoGlobs) {
|
|
187
128
|
const subDir = join(projectDir, sub);
|
|
@@ -207,9 +148,9 @@ export function scanProject(projectDir) {
|
|
|
207
148
|
};
|
|
208
149
|
|
|
209
150
|
for (const [name, version] of Object.entries(allDeps)) {
|
|
210
|
-
|
|
151
|
+
const info = lookupNpmPackage(name);
|
|
152
|
+
if (info && !seenPkgs.has(name)) {
|
|
211
153
|
seenPkgs.add(name);
|
|
212
|
-
const info = CRYPTO_PACKAGES[name];
|
|
213
154
|
results.libraries.push({
|
|
214
155
|
name,
|
|
215
156
|
version: version.replace(/^[\^~]/, ''),
|
|
@@ -217,37 +158,87 @@ export function scanProject(projectDir) {
|
|
|
217
158
|
quantumRisk: info.quantumRisk,
|
|
218
159
|
category: info.category,
|
|
219
160
|
source: pkgPath.replace(projectDir + '/', ''),
|
|
161
|
+
ecosystem: 'npm',
|
|
220
162
|
});
|
|
221
163
|
}
|
|
222
164
|
}
|
|
165
|
+
results.manifestsFound.push('package.json');
|
|
223
166
|
} catch { /* invalid package.json */ }
|
|
224
167
|
}
|
|
225
168
|
|
|
226
|
-
// 2.
|
|
227
|
-
const
|
|
169
|
+
// 2. Scan non-npm manifests (go.mod, requirements.txt, Cargo.toml, etc.)
|
|
170
|
+
const manifestLibs = scanManifests(projectDir);
|
|
171
|
+
for (const lib of manifestLibs) {
|
|
172
|
+
if (!seenPkgs.has(lib.name)) {
|
|
173
|
+
seenPkgs.add(lib.name);
|
|
174
|
+
results.libraries.push(lib);
|
|
175
|
+
if (!results.manifestsFound.includes(lib.source)) {
|
|
176
|
+
results.manifestsFound.push(lib.source);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 3. Walk source files (single-pass unified walker with config overrides)
|
|
182
|
+
const scannerConfig = loadScannerConfig(projectDir);
|
|
183
|
+
const walked = walkProject(projectDir, {
|
|
184
|
+
skipDirs: scannerConfig.skipDirs.length > 0 ? new Set(scannerConfig.skipDirs) : undefined,
|
|
185
|
+
includeExtensions: scannerConfig.includeExtensions.length > 0 ? new Set(scannerConfig.includeExtensions) : undefined,
|
|
186
|
+
maxFiles: scannerConfig.maxFiles,
|
|
187
|
+
maxBytes: scannerConfig.maxBytes,
|
|
188
|
+
maxFileSize: scannerConfig.maxFileSize,
|
|
189
|
+
maxBinaryFiles: scannerConfig.binary.maxFiles,
|
|
190
|
+
maxBinaryFileSize: scannerConfig.binary.maxFileSize,
|
|
191
|
+
});
|
|
228
192
|
const seenImports = new Set();
|
|
229
193
|
const seenAlgos = new Set();
|
|
194
|
+
const seenSourceAlgos = new Set();
|
|
195
|
+
|
|
196
|
+
// Cert files from walker
|
|
197
|
+
for (const certPath of walked.certFiles) {
|
|
198
|
+
results.certFiles.push(relative(projectDir, certPath));
|
|
199
|
+
}
|
|
230
200
|
|
|
231
|
-
for (const filePath of
|
|
201
|
+
for (const filePath of walked.sourceFiles) {
|
|
232
202
|
const ext = extname(filePath);
|
|
203
|
+
const relPath = relative(projectDir, filePath);
|
|
233
204
|
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
205
|
+
// Multi-language scanning (Go, Python, Java, Rust, C/C++)
|
|
206
|
+
const language = detectLanguage(filePath);
|
|
207
|
+
if (language && !JS_EXTENSIONS.has(ext)) {
|
|
208
|
+
results.filesScanned++;
|
|
209
|
+
results.languagesDetected.add(language);
|
|
210
|
+
|
|
211
|
+
let content;
|
|
212
|
+
try { content = readFileSync(filePath, 'utf-8'); }
|
|
213
|
+
catch { continue; }
|
|
214
|
+
|
|
215
|
+
const langResult = scanSourceFile(filePath, content, language);
|
|
216
|
+
for (const algo of langResult.algorithms) {
|
|
217
|
+
if (!seenSourceAlgos.has(algo.algorithm)) {
|
|
218
|
+
seenSourceAlgos.add(algo.algorithm);
|
|
219
|
+
const dbEntry = lookupAlgorithm(algo.algorithm);
|
|
220
|
+
results.sourceAlgorithms.push({
|
|
221
|
+
algorithm: algo.algorithm,
|
|
222
|
+
category: algo.category,
|
|
223
|
+
language,
|
|
224
|
+
quantumRisk: dbEntry?.quantumRisk || 'unknown',
|
|
225
|
+
isWeak: dbEntry?.isWeak || false,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
237
229
|
continue;
|
|
238
230
|
}
|
|
239
231
|
|
|
240
|
-
// Only scan source files for code patterns
|
|
241
|
-
if (!
|
|
232
|
+
// Only scan JS/TS source files for JS-specific code patterns
|
|
233
|
+
if (!JS_EXTENSIONS.has(ext)) continue;
|
|
242
234
|
|
|
243
235
|
results.filesScanned++;
|
|
236
|
+
results.languagesDetected.add('javascript');
|
|
244
237
|
|
|
245
238
|
let content;
|
|
246
239
|
try { content = readFileSync(filePath, 'utf-8'); }
|
|
247
240
|
catch { continue; }
|
|
248
241
|
|
|
249
|
-
const relPath = relative(projectDir, filePath);
|
|
250
|
-
|
|
251
242
|
// Detect imports/requires
|
|
252
243
|
for (const { pattern, lib, detail } of IMPORT_PATTERNS) {
|
|
253
244
|
pattern.lastIndex = 0;
|
|
@@ -323,7 +314,6 @@ export function scanProject(projectDir) {
|
|
|
323
314
|
}
|
|
324
315
|
|
|
325
316
|
if (nodeCryptoAlgos.length > 0) {
|
|
326
|
-
// Determine quantum risk based on detected algorithms
|
|
327
317
|
const hasAsymmetric = nodeCryptoAlgos.some(a =>
|
|
328
318
|
['RSA', 'ECDSA', 'Ed25519', 'RS256', 'ES256', 'DH'].includes(a)
|
|
329
319
|
);
|
|
@@ -334,10 +324,20 @@ export function scanProject(projectDir) {
|
|
|
334
324
|
quantumRisk: hasAsymmetric ? 'high' : 'low',
|
|
335
325
|
category: hasAsymmetric ? 'asymmetric' : 'symmetric',
|
|
336
326
|
source: 'source-code',
|
|
327
|
+
ecosystem: 'npm',
|
|
337
328
|
});
|
|
338
329
|
}
|
|
339
330
|
}
|
|
340
331
|
|
|
332
|
+
// 4. TLS scanning — pass pre-walked source+config files
|
|
333
|
+
const tlsFiles = [...walked.sourceFiles, ...walked.configFiles];
|
|
334
|
+
results.tlsFindings = scanTlsConfigs(projectDir, tlsFiles);
|
|
335
|
+
|
|
336
|
+
// 5. Binary scanning moved to CLI layer (lazy-loaded via dynamic import)
|
|
337
|
+
|
|
338
|
+
// Convert sets to arrays for JSON serialization
|
|
339
|
+
results.languagesDetected = [...results.languagesDetected];
|
|
340
|
+
|
|
341
341
|
return results;
|
|
342
342
|
}
|
|
343
343
|
|
|
@@ -346,12 +346,41 @@ export function scanProject(projectDir) {
|
|
|
346
346
|
// ---------------------------------------------------------------------------
|
|
347
347
|
|
|
348
348
|
export function toLibraryInventory(scanResults) {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
349
|
+
const inventory = scanResults.libraries.map(lib => {
|
|
350
|
+
const entry = {
|
|
351
|
+
name: lib.name,
|
|
352
|
+
version: lib.version,
|
|
353
|
+
algorithms: lib.algorithms,
|
|
354
|
+
quantumRisk: lib.quantumRisk,
|
|
355
|
+
category: lib.category,
|
|
356
|
+
isDeprecated: lib.isDeprecated || false,
|
|
357
|
+
};
|
|
358
|
+
// Enrich with algorithm-db data
|
|
359
|
+
for (const algoName of lib.algorithms) {
|
|
360
|
+
const dbEntry = lookupAlgorithm(algoName);
|
|
361
|
+
if (dbEntry?.isWeak && !entry.isDeprecated) {
|
|
362
|
+
entry.isDeprecated = true;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return entry;
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Also add source-detected algorithms as synthetic library entries
|
|
369
|
+
for (const algo of (scanResults.sourceAlgorithms || [])) {
|
|
370
|
+
const alreadyInLib = inventory.some(lib =>
|
|
371
|
+
lib.algorithms.some(a => a.toLowerCase() === algo.algorithm.toLowerCase())
|
|
372
|
+
);
|
|
373
|
+
if (!alreadyInLib) {
|
|
374
|
+
inventory.push({
|
|
375
|
+
name: `${algo.language}:${algo.algorithm}`,
|
|
376
|
+
version: 'source-code',
|
|
377
|
+
algorithms: [algo.algorithm],
|
|
378
|
+
quantumRisk: algo.quantumRisk || 'unknown',
|
|
379
|
+
category: algo.category,
|
|
380
|
+
isDeprecated: algo.isWeak || false,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return inventory;
|
|
357
386
|
}
|
package/lib/walker.mjs
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified single-pass file walker.
|
|
3
|
+
*
|
|
4
|
+
* Replaces three independent walkers (scanner.mjs, scanner-tls.mjs,
|
|
5
|
+
* scanner-binary.mjs) with a single directory traversal that classifies
|
|
6
|
+
* files into buckets by extension.
|
|
7
|
+
* Zero dependencies — uses only node:fs and node:path.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readdirSync, statSync } from 'node:fs';
|
|
11
|
+
import { join, extname } from 'node:path';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Default skip directories (union of all three previous walkers)
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_SKIP_DIRS = new Set([
|
|
18
|
+
'node_modules', '.git', '.next', 'dist', 'build', 'coverage',
|
|
19
|
+
'.cache', '.nuxt', '.output', '.svelte-kit', '__pycache__',
|
|
20
|
+
'vendor', '.venv', 'venv',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// File classification by extension
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
28
|
+
'.js', '.ts', '.mjs', '.cjs', '.jsx', '.tsx',
|
|
29
|
+
'.go', '.py',
|
|
30
|
+
'.java', '.kt', '.scala',
|
|
31
|
+
'.rs',
|
|
32
|
+
'.c', '.h', '.cpp', '.hpp', '.cc', '.cxx',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
const CONFIG_EXTENSIONS = new Set([
|
|
36
|
+
'.conf', '.cfg', '.ini', '.toml', '.yaml', '.yml', '.json', '.xml',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const CONFIG_NAMES = new Set([
|
|
40
|
+
'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
|
|
41
|
+
'nginx.conf', 'httpd.conf', 'apache2.conf', 'ssl.conf',
|
|
42
|
+
'.env',
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
const BINARY_EXTENSIONS = new Set([
|
|
46
|
+
'.exe', '.dll', '.so', '.dylib', '.wasm',
|
|
47
|
+
'.class', '.jar', '.war',
|
|
48
|
+
'.o', '.a', '.lib',
|
|
49
|
+
'.pyc', '.pyd',
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const CERT_EXTENSIONS = new Set(['.pem', '.key', '.crt', '.p12', '.pfx', '.jks', '.keystore']);
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Walker
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Walk a project directory once, classifying files into buckets.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} dir - Root directory to walk
|
|
62
|
+
* @param {object} options
|
|
63
|
+
* @param {Set<string>} [options.skipDirs] - Extra directory names to skip (merged with defaults)
|
|
64
|
+
* @param {Set<string>} [options.includeExtensions] - Extra source extensions to include
|
|
65
|
+
* @param {number} [options.maxFiles=10000] - Max total files to collect
|
|
66
|
+
* @param {number} [options.maxBytes=524288000] - Max total bytes (500MB)
|
|
67
|
+
* @param {number} [options.maxFileSize=1048576] - Max single file size (1MB) for source/config
|
|
68
|
+
* @param {number} [options.maxBinaryFiles=50] - Max binary files
|
|
69
|
+
* @param {number} [options.maxBinaryFileSize=10485760] - Max binary file size (10MB)
|
|
70
|
+
* @returns {{ sourceFiles: string[], configFiles: string[], binaryFiles: string[], certFiles: string[], totalBytes: number, totalFiles: number }}
|
|
71
|
+
*/
|
|
72
|
+
export function walkProject(dir, options = {}) {
|
|
73
|
+
const skipDirs = options.skipDirs
|
|
74
|
+
? new Set([...DEFAULT_SKIP_DIRS, ...options.skipDirs])
|
|
75
|
+
: DEFAULT_SKIP_DIRS;
|
|
76
|
+
const extraSourceExts = options.includeExtensions
|
|
77
|
+
? new Set([...SOURCE_EXTENSIONS, ...options.includeExtensions])
|
|
78
|
+
: SOURCE_EXTENSIONS;
|
|
79
|
+
const maxFiles = options.maxFiles || 10000;
|
|
80
|
+
const maxBytes = options.maxBytes || 500 * 1024 * 1024;
|
|
81
|
+
const maxFileSize = options.maxFileSize || 1024 * 1024;
|
|
82
|
+
const maxBinaryFiles = options.maxBinaryFiles || 50;
|
|
83
|
+
const maxBinaryFileSize = options.maxBinaryFileSize || 10 * 1024 * 1024;
|
|
84
|
+
|
|
85
|
+
const sourceFiles = [];
|
|
86
|
+
const configFiles = [];
|
|
87
|
+
const binaryFiles = [];
|
|
88
|
+
const certFiles = [];
|
|
89
|
+
let totalBytes = 0;
|
|
90
|
+
let totalFiles = 0;
|
|
91
|
+
|
|
92
|
+
function walk(currentDir) {
|
|
93
|
+
if (totalFiles >= maxFiles || totalBytes >= maxBytes) return;
|
|
94
|
+
|
|
95
|
+
let entries;
|
|
96
|
+
try { entries = readdirSync(currentDir, { withFileTypes: true }); }
|
|
97
|
+
catch { return; }
|
|
98
|
+
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (totalFiles >= maxFiles || totalBytes >= maxBytes) return;
|
|
101
|
+
|
|
102
|
+
if (entry.isDirectory()) {
|
|
103
|
+
if (!skipDirs.has(entry.name) && !entry.name.startsWith('.')) {
|
|
104
|
+
walk(join(currentDir, entry.name));
|
|
105
|
+
}
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!entry.isFile()) continue;
|
|
110
|
+
|
|
111
|
+
const filePath = join(currentDir, entry.name);
|
|
112
|
+
const ext = extname(entry.name).toLowerCase();
|
|
113
|
+
const name = entry.name;
|
|
114
|
+
|
|
115
|
+
// Classify cert files (no size check needed — just record path)
|
|
116
|
+
if (CERT_EXTENSIONS.has(ext)) {
|
|
117
|
+
certFiles.push(filePath);
|
|
118
|
+
totalFiles++;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Classify binary files (separate limit)
|
|
123
|
+
if (BINARY_EXTENSIONS.has(ext)) {
|
|
124
|
+
if (binaryFiles.length < maxBinaryFiles) {
|
|
125
|
+
try {
|
|
126
|
+
const stat = statSync(filePath);
|
|
127
|
+
if (stat.size <= maxBinaryFileSize) {
|
|
128
|
+
binaryFiles.push(filePath);
|
|
129
|
+
totalFiles++;
|
|
130
|
+
}
|
|
131
|
+
} catch { /* skip */ }
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Check size for source/config files
|
|
137
|
+
let fileSize;
|
|
138
|
+
try {
|
|
139
|
+
const stat = statSync(filePath);
|
|
140
|
+
if (stat.size > maxFileSize) continue;
|
|
141
|
+
fileSize = stat.size;
|
|
142
|
+
} catch { continue; }
|
|
143
|
+
|
|
144
|
+
totalBytes += fileSize;
|
|
145
|
+
totalFiles++;
|
|
146
|
+
|
|
147
|
+
// Classify source files
|
|
148
|
+
if (extraSourceExts.has(ext)) {
|
|
149
|
+
sourceFiles.push(filePath);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Classify config files
|
|
154
|
+
if (CONFIG_EXTENSIONS.has(ext) || CONFIG_NAMES.has(name)) {
|
|
155
|
+
configFiles.push(filePath);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Other files are walked but not bucketed
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
walk(dir);
|
|
164
|
+
|
|
165
|
+
return { sourceFiles, configFiles, binaryFiles, certFiles, totalBytes, totalFiles };
|
|
166
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cryptoserve",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "CryptoServe CLI - Cryptographic scanning, PQC analysis, encryption, and local key management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,7 +24,12 @@
|
|
|
24
24
|
"security",
|
|
25
25
|
"scanner",
|
|
26
26
|
"keychain",
|
|
27
|
-
"vault"
|
|
27
|
+
"vault",
|
|
28
|
+
"cbom",
|
|
29
|
+
"cyclonedx",
|
|
30
|
+
"spdx",
|
|
31
|
+
"binary-analysis",
|
|
32
|
+
"tls"
|
|
28
33
|
],
|
|
29
34
|
"repository": {
|
|
30
35
|
"type": "git",
|