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 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 in JavaScript/TypeScript projects.
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
- Detects 20+ crypto packages (`jsonwebtoken`, `node-forge`, `@noble/curves`, etc.), `node:crypto` API usage, algorithm string literals, weak patterns (MD5, DES, ECB, `createCipher`), and hardcoded API keys (AWS, OpenAI, Anthropic, GitHub, Stripe, and more).
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. Evaluates your project's cryptographic posture against quantum threat timelines.
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 quantum readiness score (0-100), SNDL risk assessment, KEM/signature recommendations (ML-KEM, ML-DSA, SLH-DSA), migration plan, and compliance references (CNSA 2.0, NIST SP 800-208, BSI, ANSSI).
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
@@ -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, optionsWithValues = ['--password', '--algorithm', '--profile', '--format', '--file', '--output', '--server', '--context']) {
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
- // Skip the flag; if it takes a value, skip the next arg too
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
- console.log(` ${progressBar(result.quantumReadinessScore, 100)} ${bold(`${result.quantumReadinessScore}/100`)}`);
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
- // Filter out the command from args for sub-parsers
759
- // Strip flags that belong to the command router
760
- const filteredArgs = commandArgs.filter(a => a !== command);
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;