cryptoserve 0.1.3 → 0.2.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 +97 -10
- package/bin/cryptoserve.mjs +259 -11
- package/lib/algorithm-db.mjs +260 -0
- package/lib/cbom.mjs +298 -0
- package/lib/client.mjs +3 -3
- package/lib/config.mjs +87 -0
- package/lib/crypto-registry.mjs +161 -0
- package/lib/init.mjs +2 -2
- package/lib/pqc-engine.mjs +221 -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 +134 -104
- package/lib/walker.mjs +166 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CryptoServe CLI (Node.js)
|
|
2
2
|
|
|
3
|
-
Zero-dependency CLI for cryptographic scanning, post-quantum readiness analysis, encryption, and local key management.
|
|
3
|
+
Zero-dependency CLI for cryptographic scanning, post-quantum readiness analysis, CBOM generation, CI/CD gating, encryption, and local key management.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npx cryptoserve pqc
|
|
@@ -24,6 +24,8 @@ Requires Node.js 18 or later. No dependencies — uses only Node.js built-in mod
|
|
|
24
24
|
|---------|-------------|
|
|
25
25
|
| `scan [path]` | Scan project for crypto libraries, hardcoded secrets, and weak patterns |
|
|
26
26
|
| `pqc` | Post-quantum readiness analysis with SNDL risk assessment |
|
|
27
|
+
| `cbom [path]` | Generate Cryptographic Bill of Materials (CycloneDX, SPDX, JSON) |
|
|
28
|
+
| `gate [path]` | CI/CD quality gate with configurable thresholds |
|
|
27
29
|
| `encrypt` / `decrypt` | Password-based encryption (strings and files) |
|
|
28
30
|
| `context list` / `show` | List and inspect context-aware algorithm presets |
|
|
29
31
|
| `hash-password` | scrypt / PBKDF2 password hashing |
|
|
@@ -33,18 +35,46 @@ Requires Node.js 18 or later. No dependencies — uses only Node.js built-in mod
|
|
|
33
35
|
|
|
34
36
|
## Scan
|
|
35
37
|
|
|
36
|
-
Detect crypto libraries, algorithm usage, hardcoded secrets, and certificate files
|
|
38
|
+
Detect crypto libraries, algorithm usage, hardcoded secrets, and certificate files across multiple languages and ecosystems.
|
|
37
39
|
|
|
38
40
|
```bash
|
|
39
41
|
cryptoserve scan .
|
|
40
42
|
cryptoserve scan ./src --format json
|
|
43
|
+
cryptoserve scan . --binary # Include binary crypto detection
|
|
41
44
|
```
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
### Supported Languages
|
|
47
|
+
|
|
48
|
+
| Language | Extensions | Detection |
|
|
49
|
+
|----------|-----------|-----------|
|
|
50
|
+
| JavaScript/TypeScript | `.js`, `.ts`, `.mjs`, `.cjs`, `.jsx`, `.tsx` | Imports, algorithm literals, weak patterns |
|
|
51
|
+
| Go | `.go` | `crypto/*` stdlib, `x/crypto`, `circl` |
|
|
52
|
+
| Python | `.py` | `hashlib`, `cryptography`, `PyCryptodome`, `bcrypt` |
|
|
53
|
+
| Java/Kotlin | `.java`, `.kt`, `.scala` | `Cipher.getInstance`, `MessageDigest`, `KeyPairGenerator` |
|
|
54
|
+
| Rust | `.rs` | `aes-gcm`, `ring`, `ed25519-dalek`, `pqcrypto` |
|
|
55
|
+
| C/C++ | `.c`, `.h`, `.cpp`, `.hpp`, `.cc` | OpenSSL `EVP_*`, `RSA_*`, `SHA*_Init` |
|
|
56
|
+
|
|
57
|
+
### Supported Manifests
|
|
58
|
+
|
|
59
|
+
| Manifest | Ecosystem |
|
|
60
|
+
|----------|-----------|
|
|
61
|
+
| `package.json` | npm |
|
|
62
|
+
| `go.mod` | Go modules |
|
|
63
|
+
| `requirements.txt` | PyPI |
|
|
64
|
+
| `pyproject.toml` | PyPI (PEP 621 + Poetry) |
|
|
65
|
+
| `Cargo.toml` | Cargo (Rust) |
|
|
66
|
+
| `pom.xml` | Maven (Java) |
|
|
67
|
+
|
|
68
|
+
### Additional Detection
|
|
69
|
+
|
|
70
|
+
- **TLS/SSL versions** — nginx, Apache, Node.js, Go, Java configs
|
|
71
|
+
- **Binary signatures** — AES S-box, DES tables, SHA constants, ChaCha20 sigma (with `--binary`)
|
|
72
|
+
- **80+ algorithms** classified by quantum risk, weakness, and category
|
|
73
|
+
- **Hardcoded secrets** — AWS, OpenAI, Anthropic, GitHub, Stripe, and more
|
|
44
74
|
|
|
45
75
|
## PQC Analysis
|
|
46
76
|
|
|
47
|
-
Offline post-quantum readiness assessment
|
|
77
|
+
Offline post-quantum readiness assessment with confidence indicators.
|
|
48
78
|
|
|
49
79
|
```bash
|
|
50
80
|
cryptoserve pqc
|
|
@@ -55,7 +85,67 @@ cryptoserve pqc --format json
|
|
|
55
85
|
|
|
56
86
|
**Profiles:** `general`, `national_security`, `healthcare`, `financial`, `intellectual_property`, `legal`, `authentication`, `session_tokens`, `ephemeral`
|
|
57
87
|
|
|
58
|
-
Output includes
|
|
88
|
+
Output includes:
|
|
89
|
+
- Quantum readiness score (0-100) with confidence level
|
|
90
|
+
- Risk breakdown (critical/high/medium/low/safe)
|
|
91
|
+
- Migration urgency (immediate/high/medium/low/none)
|
|
92
|
+
- SNDL risk assessment
|
|
93
|
+
- KEM/signature recommendations (ML-KEM, ML-DSA, SLH-DSA)
|
|
94
|
+
- Migration plan with compliance references (CNSA 2.0, NIST SP 800-208, BSI, ANSSI)
|
|
95
|
+
|
|
96
|
+
## CBOM Generation
|
|
97
|
+
|
|
98
|
+
Generate a Cryptographic Bill of Materials in industry-standard formats.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# CycloneDX 1.5 format
|
|
102
|
+
cryptoserve cbom . --format cyclonedx --output cbom.json
|
|
103
|
+
|
|
104
|
+
# SPDX 2.3 format
|
|
105
|
+
cryptoserve cbom . --format spdx --output cbom-spdx.json
|
|
106
|
+
|
|
107
|
+
# Native JSON with quantum readiness data
|
|
108
|
+
cryptoserve cbom . --format json --output cbom-native.json
|
|
109
|
+
|
|
110
|
+
# Print to stdout
|
|
111
|
+
cryptoserve cbom .
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Each CBOM includes:
|
|
115
|
+
- All detected crypto components with Package URLs (purls)
|
|
116
|
+
- Quantum readiness score and risk assessment
|
|
117
|
+
- Git metadata (commit, branch, remote)
|
|
118
|
+
- Content hash for integrity verification
|
|
119
|
+
|
|
120
|
+
## CI/CD Gate
|
|
121
|
+
|
|
122
|
+
Enforce cryptographic policies in your CI/CD pipeline.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Default: fail if quantum risk > high or score < 50
|
|
126
|
+
cryptoserve gate .
|
|
127
|
+
|
|
128
|
+
# Strict: fail on any high-risk algorithm
|
|
129
|
+
cryptoserve gate . --max-risk medium
|
|
130
|
+
|
|
131
|
+
# Fail on weak/deprecated algorithms (MD5, DES, RC4, etc.)
|
|
132
|
+
cryptoserve gate . --fail-on-weak
|
|
133
|
+
|
|
134
|
+
# Custom score threshold
|
|
135
|
+
cryptoserve gate . --min-score 70
|
|
136
|
+
|
|
137
|
+
# JSON output for CI parsing
|
|
138
|
+
cryptoserve gate . --format json
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Exit codes:** `0` = pass, `1` = fail, `2` = error
|
|
142
|
+
|
|
143
|
+
**Example GitHub Actions step:**
|
|
144
|
+
|
|
145
|
+
```yaml
|
|
146
|
+
- name: Crypto gate
|
|
147
|
+
run: npx cryptoserve gate . --max-risk high --min-score 50 --fail-on-weak
|
|
148
|
+
```
|
|
59
149
|
|
|
60
150
|
## Encrypt / Decrypt
|
|
61
151
|
|
|
@@ -90,13 +180,8 @@ The encrypted blob format is byte-identical between the Python and Node.js SDKs.
|
|
|
90
180
|
A 5-layer algorithm resolver selects the optimal encryption algorithm based on data sensitivity, compliance requirements, threat model, and access patterns.
|
|
91
181
|
|
|
92
182
|
```bash
|
|
93
|
-
# List available contexts
|
|
94
183
|
cryptoserve context list
|
|
95
|
-
|
|
96
|
-
# Show full resolution rationale
|
|
97
184
|
cryptoserve context show user-pii --verbose
|
|
98
|
-
|
|
99
|
-
# Encrypt with automatic algorithm selection
|
|
100
185
|
cryptoserve encrypt "patient diagnosis" --context health-data --password mypassword
|
|
101
186
|
```
|
|
102
187
|
|
|
@@ -176,6 +261,8 @@ import { encrypt, decrypt, encryptString, decryptString } from 'cryptoserve/lib/
|
|
|
176
261
|
import { analyzeOffline } from 'cryptoserve/lib/pqc-engine.mjs';
|
|
177
262
|
import { scanProject } from 'cryptoserve/lib/scanner.mjs';
|
|
178
263
|
import { resolveContext } from 'cryptoserve/lib/context-resolver.mjs';
|
|
264
|
+
import { generateCbom, toCycloneDx, toSpdx } from 'cryptoserve/lib/cbom.mjs';
|
|
265
|
+
import { ALGORITHM_DB, lookupAlgorithm } from 'cryptoserve/lib/algorithm-db.mjs';
|
|
179
266
|
```
|
|
180
267
|
|
|
181
268
|
## License
|
package/bin/cryptoserve.mjs
CHANGED
|
@@ -15,13 +15,15 @@
|
|
|
15
15
|
* cryptoserve decrypt --file in --output out [--password P]
|
|
16
16
|
* cryptoserve hash-password [--algorithm scrypt|pbkdf2]
|
|
17
17
|
* cryptoserve context list | show NAME [--verbose] [--format json]
|
|
18
|
+
* cryptoserve cbom [path] [--format cyclonedx|spdx|json] [--output file]
|
|
19
|
+
* cryptoserve gate [path] [--max-risk R] [--min-score N] [--fail-on-weak] [--format json]
|
|
18
20
|
* cryptoserve vault init|set|get|list|delete|run|import|export
|
|
19
21
|
* cryptoserve login [--server URL]
|
|
20
22
|
* cryptoserve status
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
|
-
import { readFileSync } from 'node:fs';
|
|
24
|
-
import { resolve, dirname, join } from 'node:path';
|
|
25
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
26
|
+
import { resolve, dirname, join, basename } from 'node:path';
|
|
25
27
|
import { fileURLToPath } from 'node:url';
|
|
26
28
|
|
|
27
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -31,6 +33,16 @@ const PKG = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-
|
|
|
31
33
|
// Arg parsing helpers
|
|
32
34
|
// ---------------------------------------------------------------------------
|
|
33
35
|
|
|
36
|
+
const OPTIONS_WITH_VALUES = new Set([
|
|
37
|
+
'--password', '--algorithm', '--profile', '--format', '--file',
|
|
38
|
+
'--output', '--server', '--context', '--max-risk', '--min-score',
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const KNOWN_FLAGS = new Set([
|
|
42
|
+
'--insecure-storage', '--verbose', '--binary', '--fail-on-weak',
|
|
43
|
+
'--help', '--version',
|
|
44
|
+
]);
|
|
45
|
+
|
|
34
46
|
function getFlag(args, name) {
|
|
35
47
|
const idx = args.indexOf(name);
|
|
36
48
|
return idx !== -1;
|
|
@@ -42,12 +54,11 @@ function getOption(args, name, defaultValue = null) {
|
|
|
42
54
|
return args[idx + 1];
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
function getPositional(args
|
|
57
|
+
function getPositional(args) {
|
|
46
58
|
const result = [];
|
|
47
59
|
for (let i = 0; i < args.length; i++) {
|
|
48
60
|
if (args[i].startsWith('--')) {
|
|
49
|
-
|
|
50
|
-
if (optionsWithValues.includes(args[i])) i++;
|
|
61
|
+
if (OPTIONS_WITH_VALUES.has(args[i])) i++;
|
|
51
62
|
continue;
|
|
52
63
|
}
|
|
53
64
|
result.push(args[i]);
|
|
@@ -55,6 +66,16 @@ function getPositional(args, optionsWithValues = ['--password', '--algorithm', '
|
|
|
55
66
|
return result;
|
|
56
67
|
}
|
|
57
68
|
|
|
69
|
+
function warnUnknownFlags(args) {
|
|
70
|
+
for (let i = 0; i < args.length; i++) {
|
|
71
|
+
const arg = args[i];
|
|
72
|
+
if (arg.startsWith('--') && !OPTIONS_WITH_VALUES.has(arg) && !KNOWN_FLAGS.has(arg)) {
|
|
73
|
+
console.error(`Warning: unknown flag "${arg}"`);
|
|
74
|
+
}
|
|
75
|
+
if (OPTIONS_WITH_VALUES.has(arg)) i++; // skip value
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
58
79
|
// ---------------------------------------------------------------------------
|
|
59
80
|
// Commands
|
|
60
81
|
// ---------------------------------------------------------------------------
|
|
@@ -67,6 +88,8 @@ async function cmdHelp() {
|
|
|
67
88
|
console.log(` ${bold('Scanning & Analysis')}`);
|
|
68
89
|
console.log(` ${info('pqc [--profile P] [--format json]')} Post-quantum readiness analysis`);
|
|
69
90
|
console.log(` ${info('scan [path] [--format json]')} Scan project for crypto & secrets`);
|
|
91
|
+
console.log(` ${info('cbom [path] [--format F] [--output O]')} Generate Crypto Bill of Materials`);
|
|
92
|
+
console.log(` ${info('gate [path] [--max-risk R]')} CI/CD gate (exit 0=pass, 1=fail)`);
|
|
70
93
|
console.log();
|
|
71
94
|
console.log(` ${bold('Encryption')}`);
|
|
72
95
|
console.log(` ${info('encrypt "text" [--context C]')} Encrypt with context-aware algorithm selection`);
|
|
@@ -167,13 +190,19 @@ async function cmdPqc(args) {
|
|
|
167
190
|
|
|
168
191
|
// Use scanner results if available, otherwise use example libraries
|
|
169
192
|
let libraries = [];
|
|
193
|
+
let scanMeta = {};
|
|
170
194
|
try {
|
|
171
195
|
const { scanProject, toLibraryInventory } = await import('../lib/scanner.mjs');
|
|
172
196
|
const scanResults = scanProject(process.cwd());
|
|
173
197
|
libraries = toLibraryInventory(scanResults);
|
|
198
|
+
scanMeta = {
|
|
199
|
+
filesScanned: scanResults.filesScanned,
|
|
200
|
+
languagesDetected: scanResults.languagesDetected,
|
|
201
|
+
manifestsFound: scanResults.manifestsFound,
|
|
202
|
+
};
|
|
174
203
|
} catch { /* scanner not available, empty libraries */ }
|
|
175
204
|
|
|
176
|
-
const result = analyzeOffline(libraries, profile);
|
|
205
|
+
const result = analyzeOffline(libraries, profile, scanMeta);
|
|
177
206
|
|
|
178
207
|
if (format === 'json') {
|
|
179
208
|
console.log(JSON.stringify(result, null, 2));
|
|
@@ -188,9 +217,28 @@ async function cmdPqc(args) {
|
|
|
188
217
|
console.log(labelValue('Protection needed', `${result.dataProfile.lifespanYears} years`));
|
|
189
218
|
console.log(labelValue('Urgency', result.dataProfile.urgency.toUpperCase()));
|
|
190
219
|
|
|
191
|
-
// Quantum readiness score
|
|
220
|
+
// Quantum readiness score with confidence
|
|
192
221
|
console.log(section('Quantum Readiness'));
|
|
193
|
-
|
|
222
|
+
const conf = result.confidence;
|
|
223
|
+
console.log(` ${progressBar(result.quantumReadinessScore, 100)} ${bold(`${result.quantumReadinessScore}/100`)} ${dim(`(${conf.level} confidence — ${conf.reason})`)}`);
|
|
224
|
+
console.log(labelValue('Migration urgency', result.migrationUrgency.toUpperCase()));
|
|
225
|
+
|
|
226
|
+
// Risk breakdown
|
|
227
|
+
if (result.riskBreakdown) {
|
|
228
|
+
const rb = result.riskBreakdown;
|
|
229
|
+
const parts = [];
|
|
230
|
+
if (rb.critical > 0) parts.push(error(`${rb.critical} critical`));
|
|
231
|
+
if (rb.high > 0) parts.push(warning(`${rb.high} high`));
|
|
232
|
+
if (rb.medium > 0) parts.push(`${rb.medium} medium`);
|
|
233
|
+
if (rb.low > 0) parts.push(`${rb.low} low`);
|
|
234
|
+
if (rb.none > 0) parts.push(success(`${rb.none} safe`));
|
|
235
|
+
if (parts.length > 0) console.log(labelValue('Risk breakdown', parts.join(' / ')));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Languages detected
|
|
239
|
+
if (scanMeta.languagesDetected?.length > 0) {
|
|
240
|
+
console.log(labelValue('Languages', scanMeta.languagesDetected.join(', ')));
|
|
241
|
+
}
|
|
194
242
|
|
|
195
243
|
// SNDL assessment
|
|
196
244
|
const sndl = result.sndlAssessment;
|
|
@@ -278,9 +326,16 @@ async function cmdScan(args) {
|
|
|
278
326
|
const positional = getPositional(args);
|
|
279
327
|
const scanDir = positional.length > 0 ? resolve(positional[0]) : process.cwd();
|
|
280
328
|
const format = getOption(args, '--format', 'text');
|
|
329
|
+
const binaryFlag = getFlag(args, '--binary');
|
|
281
330
|
|
|
282
331
|
const results = scanProject(scanDir);
|
|
283
332
|
|
|
333
|
+
// Binary scanning — lazy-loaded only when requested
|
|
334
|
+
if (binaryFlag) {
|
|
335
|
+
const { scanBinaries } = await import('../lib/scanner-binary.mjs');
|
|
336
|
+
results.binaryFindings = scanBinaries(scanDir);
|
|
337
|
+
}
|
|
338
|
+
|
|
284
339
|
if (format === 'json') {
|
|
285
340
|
console.log(JSON.stringify(results, null, 2));
|
|
286
341
|
return;
|
|
@@ -327,6 +382,40 @@ async function cmdScan(args) {
|
|
|
327
382
|
}
|
|
328
383
|
}
|
|
329
384
|
|
|
385
|
+
// Multi-language source algorithms
|
|
386
|
+
if (results.sourceAlgorithms && results.sourceAlgorithms.length > 0) {
|
|
387
|
+
console.log(section('Source Code Crypto (Multi-Language)'));
|
|
388
|
+
console.log(tableHeader(['Algorithm', 'Category', 'Language', 'Risk'], [20, 14, 12, 10]));
|
|
389
|
+
for (const algo of results.sourceAlgorithms) {
|
|
390
|
+
console.log(tableRow(
|
|
391
|
+
[algo.algorithm, algo.category, algo.language, algo.quantumRisk],
|
|
392
|
+
[20, 14, 12, 10]
|
|
393
|
+
));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// TLS findings
|
|
398
|
+
if (results.tlsFindings && results.tlsFindings.length > 0) {
|
|
399
|
+
console.log(section('TLS/SSL Issues'));
|
|
400
|
+
for (const tls of results.tlsFindings) {
|
|
401
|
+
const icon = tls.risk === 'critical' ? error(`[CRIT] ${tls.protocol}`) : warning(`[${tls.risk.toUpperCase()}] ${tls.protocol}`);
|
|
402
|
+
console.log(` ${icon} ${dim(tls.file + ':' + tls.line)}`);
|
|
403
|
+
console.log(` ${dim(tls.recommendation)}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Binary findings
|
|
408
|
+
if (results.binaryFindings && results.binaryFindings.length > 0) {
|
|
409
|
+
console.log(section('Binary Crypto Signatures'));
|
|
410
|
+
console.log(tableHeader(['Signature', 'Algorithm', 'Severity', 'File'], [24, 12, 10, 30]));
|
|
411
|
+
for (const bf of results.binaryFindings) {
|
|
412
|
+
console.log(tableRow(
|
|
413
|
+
[bf.name, bf.algorithm, bf.severity, bf.file],
|
|
414
|
+
[24, 12, 10, 30]
|
|
415
|
+
));
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
330
419
|
// Cert files
|
|
331
420
|
if (results.certFiles.length > 0) {
|
|
332
421
|
console.log(section('Certificate/Key Files'));
|
|
@@ -338,8 +427,20 @@ async function cmdScan(args) {
|
|
|
338
427
|
// Summary
|
|
339
428
|
console.log(section('Summary'));
|
|
340
429
|
console.log(labelValue('Libraries', String(results.libraries.length)));
|
|
430
|
+
if (results.sourceAlgorithms?.length > 0) {
|
|
431
|
+
console.log(labelValue('Source algorithms', String(results.sourceAlgorithms.length)));
|
|
432
|
+
}
|
|
433
|
+
if (results.languagesDetected?.length > 0) {
|
|
434
|
+
console.log(labelValue('Languages', results.languagesDetected.join(', ')));
|
|
435
|
+
}
|
|
436
|
+
if (results.manifestsFound?.length > 0) {
|
|
437
|
+
console.log(labelValue('Manifests', results.manifestsFound.join(', ')));
|
|
438
|
+
}
|
|
341
439
|
console.log(labelValue('Secrets found', results.secrets.length > 0 ? error(String(results.secrets.length)) : success('0')));
|
|
342
440
|
console.log(labelValue('Weak patterns', results.weakPatterns.length > 0 ? warning(String(results.weakPatterns.length)) : success('0')));
|
|
441
|
+
if (results.tlsFindings?.length > 0) {
|
|
442
|
+
console.log(labelValue('TLS issues', warning(String(results.tlsFindings.length))));
|
|
443
|
+
}
|
|
343
444
|
console.log(labelValue('Cert/key files', String(results.certFiles.length)));
|
|
344
445
|
console.log();
|
|
345
446
|
}
|
|
@@ -674,6 +775,146 @@ async function cmdContext(args) {
|
|
|
674
775
|
process.exit(1);
|
|
675
776
|
}
|
|
676
777
|
|
|
778
|
+
async function cmdCbom(args) {
|
|
779
|
+
const { compactHeader, section, labelValue, success, dim, bold, info } = await import('../lib/cli-style.mjs');
|
|
780
|
+
const { scanProject, toLibraryInventory } = await import('../lib/scanner.mjs');
|
|
781
|
+
const { analyzeOffline } = await import('../lib/pqc-engine.mjs');
|
|
782
|
+
const { generateCbom, toCycloneDx, toSpdx, toNativeJson } = await import('../lib/cbom.mjs');
|
|
783
|
+
|
|
784
|
+
const positional = getPositional(args);
|
|
785
|
+
const scanDir = positional.length > 0 ? resolve(positional[0]) : process.cwd();
|
|
786
|
+
const format = getOption(args, '--format', 'json');
|
|
787
|
+
const output = getOption(args, '--output');
|
|
788
|
+
|
|
789
|
+
const scanResults = scanProject(scanDir);
|
|
790
|
+
const libraries = toLibraryInventory(scanResults);
|
|
791
|
+
const pqcResult = analyzeOffline(libraries);
|
|
792
|
+
const projectName = basename(scanDir);
|
|
793
|
+
|
|
794
|
+
const cbom = generateCbom(scanResults, pqcResult, projectName, scanDir);
|
|
795
|
+
|
|
796
|
+
let formatted;
|
|
797
|
+
switch (format) {
|
|
798
|
+
case 'cyclonedx': formatted = JSON.stringify(toCycloneDx(cbom), null, 2); break;
|
|
799
|
+
case 'spdx': formatted = JSON.stringify(toSpdx(cbom), null, 2); break;
|
|
800
|
+
default: formatted = JSON.stringify(toNativeJson(cbom), null, 2); break;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (output) {
|
|
804
|
+
writeFileSync(output, formatted + '\n');
|
|
805
|
+
console.log(success(`CBOM written to ${output}`));
|
|
806
|
+
console.log(labelValue('Format', format));
|
|
807
|
+
console.log(labelValue('Components', String(cbom.components.length)));
|
|
808
|
+
console.log(labelValue('Quantum readiness', `${cbom.quantumReadiness.score}/100`));
|
|
809
|
+
console.log(labelValue('Risk level', cbom.quantumReadiness.riskLevel));
|
|
810
|
+
} else {
|
|
811
|
+
console.log(formatted);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
async function cmdGate(args) {
|
|
816
|
+
const { scanProject, toLibraryInventory } = await import('../lib/scanner.mjs');
|
|
817
|
+
const { analyzeOffline } = await import('../lib/pqc-engine.mjs');
|
|
818
|
+
const { lookupAlgorithm } = await import('../lib/algorithm-db.mjs');
|
|
819
|
+
|
|
820
|
+
const positional = getPositional(args);
|
|
821
|
+
const scanDir = positional.length > 0 ? resolve(positional[0]) : process.cwd();
|
|
822
|
+
const maxRisk = getOption(args, '--max-risk', 'high');
|
|
823
|
+
const minScore = parseInt(getOption(args, '--min-score', '50'), 10);
|
|
824
|
+
const failOnWeak = getFlag(args, '--fail-on-weak');
|
|
825
|
+
const format = getOption(args, '--format', 'text');
|
|
826
|
+
|
|
827
|
+
const riskOrder = ['none', 'low', 'medium', 'high', 'critical'];
|
|
828
|
+
|
|
829
|
+
try {
|
|
830
|
+
const scanResults = scanProject(scanDir);
|
|
831
|
+
const libraries = toLibraryInventory(scanResults);
|
|
832
|
+
const pqcResult = analyzeOffline(libraries);
|
|
833
|
+
const score = pqcResult.quantumReadinessScore;
|
|
834
|
+
|
|
835
|
+
// Collect violations
|
|
836
|
+
const violations = [];
|
|
837
|
+
const maxRiskIdx = riskOrder.indexOf(maxRisk);
|
|
838
|
+
|
|
839
|
+
for (const lib of libraries) {
|
|
840
|
+
for (const algoName of lib.algorithms) {
|
|
841
|
+
const entry = lookupAlgorithm(algoName);
|
|
842
|
+
if (!entry) continue;
|
|
843
|
+
|
|
844
|
+
const algoRiskIdx = riskOrder.indexOf(entry.quantumRisk);
|
|
845
|
+
if (algoRiskIdx > maxRiskIdx) {
|
|
846
|
+
violations.push({
|
|
847
|
+
algorithm: algoName,
|
|
848
|
+
risk: entry.quantumRisk,
|
|
849
|
+
source: lib.name + (lib.version !== 'source-code' ? `@${lib.version}` : ` (${lib.version})`),
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (failOnWeak && entry.isWeak) {
|
|
854
|
+
violations.push({
|
|
855
|
+
algorithm: algoName,
|
|
856
|
+
risk: entry.quantumRisk,
|
|
857
|
+
source: lib.name,
|
|
858
|
+
weak: true,
|
|
859
|
+
reason: entry.weaknessReason,
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const scoreFail = score < minScore;
|
|
866
|
+
const pass = violations.length === 0 && !scoreFail;
|
|
867
|
+
|
|
868
|
+
const summary = {
|
|
869
|
+
total: libraries.reduce((sum, l) => sum + l.algorithms.length, 0),
|
|
870
|
+
safe: libraries.reduce((sum, l) => sum + l.algorithms.filter(a => {
|
|
871
|
+
const e = lookupAlgorithm(a);
|
|
872
|
+
return e && (e.quantumRisk === 'none' || e.quantumRisk === 'low');
|
|
873
|
+
}).length, 0),
|
|
874
|
+
vulnerable: violations.filter(v => !v.weak).length,
|
|
875
|
+
weak: violations.filter(v => v.weak).length,
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
if (format === 'json') {
|
|
879
|
+
console.log(JSON.stringify({
|
|
880
|
+
status: pass ? 'pass' : 'fail',
|
|
881
|
+
score,
|
|
882
|
+
violations,
|
|
883
|
+
summary,
|
|
884
|
+
}, null, 2));
|
|
885
|
+
} else {
|
|
886
|
+
const { compactHeader, success, error, warning, dim, bold, labelValue } = await import('../lib/cli-style.mjs');
|
|
887
|
+
console.log(compactHeader('gate'));
|
|
888
|
+
console.log(labelValue('Status', pass ? success('PASS') : error('FAIL')));
|
|
889
|
+
console.log(labelValue('Score', `${score}/100 (min: ${minScore})`));
|
|
890
|
+
console.log(labelValue('Max risk', maxRisk));
|
|
891
|
+
|
|
892
|
+
if (violations.length > 0) {
|
|
893
|
+
console.log(`\n ${bold('Violations:')}`);
|
|
894
|
+
for (const v of violations) {
|
|
895
|
+
const label = v.weak ? warning(`[WEAK] ${v.algorithm}`) : error(`[${v.risk.toUpperCase()}] ${v.algorithm}`);
|
|
896
|
+
console.log(` ${label} — ${dim(v.source)}${v.reason ? ` (${v.reason})` : ''}`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (scoreFail) {
|
|
901
|
+
console.log(`\n ${error(`Score ${score} is below minimum ${minScore}`)}`);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
console.log();
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
process.exit(pass ? 0 : 1);
|
|
908
|
+
} catch (e) {
|
|
909
|
+
if (format === 'json') {
|
|
910
|
+
console.log(JSON.stringify({ status: 'error', error: e.message }, null, 2));
|
|
911
|
+
} else {
|
|
912
|
+
console.error(`Error: ${e.message}`);
|
|
913
|
+
}
|
|
914
|
+
process.exit(2);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
677
918
|
async function cmdLogin(args) {
|
|
678
919
|
const { login } = await import('../lib/client.mjs');
|
|
679
920
|
const server = getOption(args, '--server', 'https://localhost:8003');
|
|
@@ -755,9 +996,10 @@ const args = process.argv.slice(2);
|
|
|
755
996
|
const command = args[0];
|
|
756
997
|
const commandArgs = args.slice(1);
|
|
757
998
|
|
|
758
|
-
//
|
|
759
|
-
|
|
760
|
-
|
|
999
|
+
// Warn about unknown flags (skip for vault/context which have subcommands)
|
|
1000
|
+
if (command && !['vault', 'context', 'help', '--help', '-h', 'version', '--version', '-v'].includes(command)) {
|
|
1001
|
+
warnUnknownFlags(commandArgs);
|
|
1002
|
+
}
|
|
761
1003
|
|
|
762
1004
|
try {
|
|
763
1005
|
switch (command) {
|
|
@@ -793,6 +1035,12 @@ try {
|
|
|
793
1035
|
case 'context':
|
|
794
1036
|
await cmdContext(commandArgs);
|
|
795
1037
|
break;
|
|
1038
|
+
case 'cbom':
|
|
1039
|
+
await cmdCbom(commandArgs);
|
|
1040
|
+
break;
|
|
1041
|
+
case 'gate':
|
|
1042
|
+
await cmdGate(commandArgs);
|
|
1043
|
+
break;
|
|
796
1044
|
case 'vault':
|
|
797
1045
|
await cmdVault(commandArgs);
|
|
798
1046
|
break;
|