cryptoserve 0.1.2 → 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 +229 -60
- 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 +154 -107
- 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,46 @@ 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
|
-
// 1. Scan package.json for crypto dependencies
|
|
183
|
-
const
|
|
184
|
-
|
|
124
|
+
// 1. Scan package.json for crypto dependencies (root + monorepo workspaces)
|
|
125
|
+
const pkgPaths = [join(projectDir, 'package.json')];
|
|
126
|
+
const monorepoGlobs = ['apps', 'packages', 'libs', 'modules', 'services'];
|
|
127
|
+
for (const sub of monorepoGlobs) {
|
|
128
|
+
const subDir = join(projectDir, sub);
|
|
129
|
+
try {
|
|
130
|
+
const entries = readdirSync(subDir, { withFileTypes: true });
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (entry.isDirectory()) {
|
|
133
|
+
const nested = join(subDir, entry.name, 'package.json');
|
|
134
|
+
if (existsSync(nested)) pkgPaths.push(nested);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} catch { /* dir doesn't exist */ }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const seenPkgs = new Set();
|
|
141
|
+
for (const pkgPath of pkgPaths) {
|
|
142
|
+
if (!existsSync(pkgPath)) continue;
|
|
185
143
|
try {
|
|
186
144
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
187
145
|
const allDeps = {
|
|
@@ -190,46 +148,97 @@ export function scanProject(projectDir) {
|
|
|
190
148
|
};
|
|
191
149
|
|
|
192
150
|
for (const [name, version] of Object.entries(allDeps)) {
|
|
193
|
-
|
|
194
|
-
|
|
151
|
+
const info = lookupNpmPackage(name);
|
|
152
|
+
if (info && !seenPkgs.has(name)) {
|
|
153
|
+
seenPkgs.add(name);
|
|
195
154
|
results.libraries.push({
|
|
196
155
|
name,
|
|
197
156
|
version: version.replace(/^[\^~]/, ''),
|
|
198
157
|
algorithms: info.algorithms,
|
|
199
158
|
quantumRisk: info.quantumRisk,
|
|
200
159
|
category: info.category,
|
|
201
|
-
source:
|
|
160
|
+
source: pkgPath.replace(projectDir + '/', ''),
|
|
161
|
+
ecosystem: 'npm',
|
|
202
162
|
});
|
|
203
163
|
}
|
|
204
164
|
}
|
|
165
|
+
results.manifestsFound.push('package.json');
|
|
205
166
|
} catch { /* invalid package.json */ }
|
|
206
167
|
}
|
|
207
168
|
|
|
208
|
-
// 2.
|
|
209
|
-
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
|
+
});
|
|
210
192
|
const seenImports = new Set();
|
|
211
193
|
const seenAlgos = new Set();
|
|
194
|
+
const seenSourceAlgos = new Set();
|
|
212
195
|
|
|
213
|
-
|
|
196
|
+
// Cert files from walker
|
|
197
|
+
for (const certPath of walked.certFiles) {
|
|
198
|
+
results.certFiles.push(relative(projectDir, certPath));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const filePath of walked.sourceFiles) {
|
|
214
202
|
const ext = extname(filePath);
|
|
203
|
+
const relPath = relative(projectDir, filePath);
|
|
215
204
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
|
|
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
|
+
}
|
|
219
229
|
continue;
|
|
220
230
|
}
|
|
221
231
|
|
|
222
|
-
// Only scan source files for code patterns
|
|
223
|
-
if (!
|
|
232
|
+
// Only scan JS/TS source files for JS-specific code patterns
|
|
233
|
+
if (!JS_EXTENSIONS.has(ext)) continue;
|
|
224
234
|
|
|
225
235
|
results.filesScanned++;
|
|
236
|
+
results.languagesDetected.add('javascript');
|
|
226
237
|
|
|
227
238
|
let content;
|
|
228
239
|
try { content = readFileSync(filePath, 'utf-8'); }
|
|
229
240
|
catch { continue; }
|
|
230
241
|
|
|
231
|
-
const relPath = relative(projectDir, filePath);
|
|
232
|
-
|
|
233
242
|
// Detect imports/requires
|
|
234
243
|
for (const { pattern, lib, detail } of IMPORT_PATTERNS) {
|
|
235
244
|
pattern.lastIndex = 0;
|
|
@@ -305,7 +314,6 @@ export function scanProject(projectDir) {
|
|
|
305
314
|
}
|
|
306
315
|
|
|
307
316
|
if (nodeCryptoAlgos.length > 0) {
|
|
308
|
-
// Determine quantum risk based on detected algorithms
|
|
309
317
|
const hasAsymmetric = nodeCryptoAlgos.some(a =>
|
|
310
318
|
['RSA', 'ECDSA', 'Ed25519', 'RS256', 'ES256', 'DH'].includes(a)
|
|
311
319
|
);
|
|
@@ -316,10 +324,20 @@ export function scanProject(projectDir) {
|
|
|
316
324
|
quantumRisk: hasAsymmetric ? 'high' : 'low',
|
|
317
325
|
category: hasAsymmetric ? 'asymmetric' : 'symmetric',
|
|
318
326
|
source: 'source-code',
|
|
327
|
+
ecosystem: 'npm',
|
|
319
328
|
});
|
|
320
329
|
}
|
|
321
330
|
}
|
|
322
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
|
+
|
|
323
341
|
return results;
|
|
324
342
|
}
|
|
325
343
|
|
|
@@ -328,12 +346,41 @@ export function scanProject(projectDir) {
|
|
|
328
346
|
// ---------------------------------------------------------------------------
|
|
329
347
|
|
|
330
348
|
export function toLibraryInventory(scanResults) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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;
|
|
339
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",
|