@quantakrypto/core 0.3.0 → 0.4.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/dist/agent-types.d.ts +48 -0
- package/dist/agent-types.d.ts.map +1 -0
- package/dist/agent-types.js +2 -0
- package/dist/agent-types.js.map +1 -0
- package/dist/cache.d.ts +27 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +93 -0
- package/dist/cache.js.map +1 -0
- package/dist/cbom.d.ts.map +1 -1
- package/dist/cbom.js +26 -1
- package/dist/cbom.js.map +1 -1
- package/dist/changed.d.ts.map +1 -1
- package/dist/changed.js +14 -2
- package/dist/changed.js.map +1 -1
- package/dist/codemods/config-toggle.d.ts +3 -0
- package/dist/codemods/config-toggle.d.ts.map +1 -0
- package/dist/codemods/config-toggle.js +26 -0
- package/dist/codemods/config-toggle.js.map +1 -0
- package/dist/codemods/registry.d.ts +26 -0
- package/dist/codemods/registry.d.ts.map +1 -0
- package/dist/codemods/registry.js +8 -0
- package/dist/codemods/registry.js.map +1 -0
- package/dist/comments.d.ts +45 -0
- package/dist/comments.d.ts.map +1 -0
- package/dist/comments.js +164 -0
- package/dist/comments.js.map +1 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/dependencies.d.ts +31 -13
- package/dist/dependencies.d.ts.map +1 -1
- package/dist/dependencies.js +460 -40
- package/dist/dependencies.js.map +1 -1
- package/dist/detect-utils.d.ts +44 -1
- package/dist/detect-utils.d.ts.map +1 -1
- package/dist/detect-utils.js +113 -17
- package/dist/detect-utils.js.map +1 -1
- package/dist/detectors/c.d.ts +20 -0
- package/dist/detectors/c.d.ts.map +1 -0
- package/dist/detectors/c.js +104 -0
- package/dist/detectors/c.js.map +1 -0
- package/dist/detectors/csharp.d.ts +15 -0
- package/dist/detectors/csharp.d.ts.map +1 -0
- package/dist/detectors/csharp.js +76 -0
- package/dist/detectors/csharp.js.map +1 -0
- package/dist/detectors/go.d.ts +25 -0
- package/dist/detectors/go.d.ts.map +1 -0
- package/dist/detectors/go.js +132 -0
- package/dist/detectors/go.js.map +1 -0
- package/dist/detectors/java.d.ts +25 -0
- package/dist/detectors/java.d.ts.map +1 -0
- package/dist/detectors/java.js +200 -0
- package/dist/detectors/java.js.map +1 -0
- package/dist/detectors/python.d.ts +26 -0
- package/dist/detectors/python.d.ts.map +1 -0
- package/dist/detectors/python.js +171 -0
- package/dist/detectors/python.js.map +1 -0
- package/dist/detectors/ruby.d.ts +15 -0
- package/dist/detectors/ruby.d.ts.map +1 -0
- package/dist/detectors/ruby.js +75 -0
- package/dist/detectors/ruby.js.map +1 -0
- package/dist/detectors/rust.d.ts +13 -0
- package/dist/detectors/rust.d.ts.map +1 -0
- package/dist/detectors/rust.js +94 -0
- package/dist/detectors/rust.js.map +1 -0
- package/dist/detectors/source.d.ts.map +1 -1
- package/dist/detectors/source.js +70 -22
- package/dist/detectors/source.js.map +1 -1
- package/dist/index.d.ts +18 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/parallel.d.ts +4 -0
- package/dist/parallel.d.ts.map +1 -1
- package/dist/parallel.js +59 -8
- package/dist/parallel.js.map +1 -1
- package/dist/patch-policy.d.ts +22 -0
- package/dist/patch-policy.d.ts.map +1 -0
- package/dist/patch-policy.js +21 -0
- package/dist/patch-policy.js.map +1 -0
- package/dist/redact.d.ts +19 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +77 -0
- package/dist/redact.js.map +1 -0
- package/dist/registry.d.ts +13 -3
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +31 -4
- package/dist/registry.js.map +1 -1
- package/dist/remediate-pipeline.d.ts +41 -0
- package/dist/remediate-pipeline.d.ts.map +1 -0
- package/dist/remediate-pipeline.js +38 -0
- package/dist/remediate-pipeline.js.map +1 -0
- package/dist/remediate-request.d.ts +21 -0
- package/dist/remediate-request.d.ts.map +1 -0
- package/dist/remediate-request.js +28 -0
- package/dist/remediate-request.js.map +1 -0
- package/dist/remediation.d.ts +10 -0
- package/dist/remediation.d.ts.map +1 -1
- package/dist/remediation.js +21 -8
- package/dist/remediation.js.map +1 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +16 -1
- package/dist/report.js.map +1 -1
- package/dist/scan-worker.js +6 -2
- package/dist/scan-worker.js.map +1 -1
- package/dist/scan.d.ts +5 -4
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +52 -29
- package/dist/scan.js.map +1 -1
- package/dist/triage.d.ts +27 -0
- package/dist/triage.d.ts.map +1 -0
- package/dist/triage.js +33 -0
- package/dist/triage.js.map +1 -0
- package/dist/types.d.ts +53 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/verify.d.ts +30 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +65 -0
- package/dist/verify.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/walk.d.ts +7 -0
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +85 -11
- package/dist/walk.js.map +1 -1
- package/dist/worktree.d.ts +6 -0
- package/dist/worktree.d.ts.map +1 -0
- package/dist/worktree.js +43 -0
- package/dist/worktree.js.map +1 -0
- package/package.json +1 -1
- package/src/agent-types.ts +52 -0
- package/src/cache.ts +120 -0
- package/src/cbom.ts +27 -1
- package/src/changed.ts +13 -4
- package/src/codemods/config-toggle.ts +35 -0
- package/src/codemods/registry.ts +31 -0
- package/src/comments.ts +180 -0
- package/src/config.ts +8 -0
- package/src/dependencies.ts +483 -51
- package/src/detect-utils.ts +123 -15
- package/src/detectors/c.ts +132 -0
- package/src/detectors/csharp.ts +98 -0
- package/src/detectors/go.ts +167 -0
- package/src/detectors/java.ts +235 -0
- package/src/detectors/python.ts +212 -0
- package/src/detectors/ruby.ts +97 -0
- package/src/detectors/rust.ts +112 -0
- package/src/detectors/source.ts +69 -21
- package/src/index.ts +45 -1
- package/src/parallel.ts +63 -6
- package/src/patch-policy.ts +42 -0
- package/src/redact.ts +92 -0
- package/src/registry.ts +32 -4
- package/src/remediate-pipeline.ts +81 -0
- package/src/remediate-request.ts +52 -0
- package/src/remediation.ts +27 -11
- package/src/report.ts +27 -1
- package/src/scan-worker.ts +6 -2
- package/src/scan.ts +62 -31
- package/src/triage.ts +58 -0
- package/src/types.ts +66 -3
- package/src/verify.ts +82 -0
- package/src/version.ts +1 -1
- package/src/walk.ts +81 -12
- package/src/worktree.ts +45 -0
package/src/dependencies.ts
CHANGED
|
@@ -1,17 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Curated database of
|
|
3
|
-
* cryptography (and are therefore quantum-vulnerable),
|
|
4
|
-
*
|
|
2
|
+
* Curated database of packages that primarily expose classical asymmetric
|
|
3
|
+
* cryptography (and are therefore quantum-vulnerable), across multiple
|
|
4
|
+
* ecosystems (npm, PyPI, crates.io, Go modules, Maven, RubyGems), plus a
|
|
5
|
+
* manifest scanner that flags any of them found in the corresponding manifest
|
|
6
|
+
* (package.json, requirements.txt, Cargo.toml, go.mod, pom.xml, Gemfile, …).
|
|
5
7
|
*
|
|
6
8
|
* The list is intentionally focused on libraries whose *purpose* is classical
|
|
7
|
-
* public-key crypto. General-purpose packages that merely call
|
|
8
|
-
* crypto are out of scope here (those are caught by the source detectors).
|
|
9
|
+
* public-key crypto. General-purpose packages that merely call the language's
|
|
10
|
+
* stdlib crypto are out of scope here (those are caught by the source detectors).
|
|
11
|
+
* Extraction is generous but every finding must match a curated entry, so a new
|
|
12
|
+
* manifest format is cheap to add (see `manifestEcosystem` + `candidateNames`).
|
|
9
13
|
*/
|
|
10
|
-
import type {
|
|
14
|
+
import type {
|
|
15
|
+
AlgorithmFamily,
|
|
16
|
+
DependencyEcosystem,
|
|
17
|
+
Finding,
|
|
18
|
+
RuleMeta,
|
|
19
|
+
VulnerableDependency,
|
|
20
|
+
} from "./types.js";
|
|
11
21
|
import { makeFinding } from "./detect-utils.js";
|
|
12
|
-
import { remediationText } from "./remediation.js";
|
|
22
|
+
import { isConfidentialityFamily, isSignatureFamily, remediationText } from "./remediation.js";
|
|
13
23
|
import { CWE_BROKEN_CRYPTO } from "./cwe.js";
|
|
14
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Catalog entry for the `dep-vulnerable` rule. Dependency findings are produced
|
|
27
|
+
* by the manifest scanner rather than a registered {@link Detector}, so this
|
|
28
|
+
* rule is absent from `defaultRegistry.ruleCatalog()`. Reporters (SARIF) merge
|
|
29
|
+
* it in so the rule advertises a GENERIC description — the per-package specifics
|
|
30
|
+
* (title/severity/HNDL/remediation) live on each individual finding/result, not
|
|
31
|
+
* on the shared rule. Without this, SARIF would leak the first dependency's
|
|
32
|
+
* package-specific text into the rule-level `shortDescription` for every dep.
|
|
33
|
+
*/
|
|
34
|
+
export const DEP_VULNERABLE_RULE: RuleMeta = {
|
|
35
|
+
id: "dep-vulnerable",
|
|
36
|
+
title: "Quantum-vulnerable dependency",
|
|
37
|
+
category: "dependency",
|
|
38
|
+
// Representative default; each SARIF result carries its own per-package level.
|
|
39
|
+
severity: "high",
|
|
40
|
+
confidence: "high",
|
|
41
|
+
hndl: false,
|
|
42
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
43
|
+
message:
|
|
44
|
+
"A dependency implements classical public-key cryptography that is broken by " +
|
|
45
|
+
"quantum computers. Replace or upgrade it to a post-quantum alternative.",
|
|
46
|
+
remediation:
|
|
47
|
+
"Move to a post-quantum alternative — ML-KEM-768 (FIPS 203) for key exchange/" +
|
|
48
|
+
"encryption and ML-DSA-65 (FIPS 204) for signatures, ideally via a hybrid.",
|
|
49
|
+
description:
|
|
50
|
+
"Flags packages in a dependency manifest known to implement quantum-vulnerable " +
|
|
51
|
+
"public-key cryptography (RSA, ECDH/ECDSA, DH, EdDSA, …).",
|
|
52
|
+
};
|
|
53
|
+
|
|
15
54
|
/** Known quantum-vulnerable npm dependencies. */
|
|
16
55
|
export const vulnerableDependencies: VulnerableDependency[] = [
|
|
17
56
|
{
|
|
@@ -259,50 +298,367 @@ export const vulnerableDependencies: VulnerableDependency[] = [
|
|
|
259
298
|
algorithms: ["ECDSA"],
|
|
260
299
|
severity: "low",
|
|
261
300
|
},
|
|
301
|
+
|
|
302
|
+
// --- PyPI (Python) ---
|
|
303
|
+
{
|
|
304
|
+
name: "pycryptodome",
|
|
305
|
+
ecosystem: "pypi",
|
|
306
|
+
reason: "RSA / ECC / DSA public-key crypto for Python.",
|
|
307
|
+
algorithms: ["RSA", "ECDSA", "DSA"],
|
|
308
|
+
severity: "high",
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
name: "pycryptodomex",
|
|
312
|
+
ecosystem: "pypi",
|
|
313
|
+
reason: "RSA / ECC / DSA public-key crypto (the `Cryptodome` namespace).",
|
|
314
|
+
algorithms: ["RSA", "ECDSA", "DSA"],
|
|
315
|
+
severity: "high",
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: "rsa",
|
|
319
|
+
ecosystem: "pypi",
|
|
320
|
+
reason: "Pure-Python RSA encryption and signing.",
|
|
321
|
+
algorithms: ["RSA"],
|
|
322
|
+
severity: "high",
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "ecdsa",
|
|
326
|
+
ecosystem: "pypi",
|
|
327
|
+
reason: "Pure-Python ECDSA/ECDH over NIST + secp256k1 curves.",
|
|
328
|
+
algorithms: ["ECDSA", "ECDH"],
|
|
329
|
+
severity: "high",
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: "cryptography",
|
|
333
|
+
ecosystem: "pypi",
|
|
334
|
+
reason: "General crypto library exposing classical RSA/EC/DH/DSA (also symmetric/PQC).",
|
|
335
|
+
algorithms: ["RSA", "ECDH", "ECDSA", "DSA"],
|
|
336
|
+
severity: "medium",
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "pyjwt",
|
|
340
|
+
ecosystem: "pypi",
|
|
341
|
+
reason: "JWT signing with classical RS*/ES* algorithms.",
|
|
342
|
+
algorithms: ["RSA", "ECDSA"],
|
|
343
|
+
severity: "medium",
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: "python-jose",
|
|
347
|
+
ecosystem: "pypi",
|
|
348
|
+
reason: "JOSE/JWT with classical RSA/ECDSA algorithms.",
|
|
349
|
+
algorithms: ["RSA", "ECDSA"],
|
|
350
|
+
severity: "medium",
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
name: "paramiko",
|
|
354
|
+
ecosystem: "pypi",
|
|
355
|
+
reason: "SSH client/server with classical RSA/ECDSA/Ed25519/DSA host + user keys.",
|
|
356
|
+
algorithms: ["RSA", "ECDSA", "EdDSA", "DSA"],
|
|
357
|
+
severity: "medium",
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: "pyopenssl",
|
|
361
|
+
ecosystem: "pypi",
|
|
362
|
+
reason: "OpenSSL bindings for classical RSA/EC X.509 + TLS.",
|
|
363
|
+
algorithms: ["RSA", "ECDSA"],
|
|
364
|
+
severity: "medium",
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "pynacl",
|
|
368
|
+
ecosystem: "pypi",
|
|
369
|
+
reason:
|
|
370
|
+
"libsodium bindings — X25519 key agreement and Ed25519 signatures (modern but classical).",
|
|
371
|
+
algorithms: ["X25519", "EdDSA"],
|
|
372
|
+
severity: "low",
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
// --- crates.io (Rust) ---
|
|
376
|
+
{
|
|
377
|
+
name: "rsa",
|
|
378
|
+
ecosystem: "cargo",
|
|
379
|
+
reason: "Pure-Rust RSA encryption and signing.",
|
|
380
|
+
algorithms: ["RSA"],
|
|
381
|
+
severity: "high",
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: "ring",
|
|
385
|
+
ecosystem: "cargo",
|
|
386
|
+
reason: "RSA / ECDSA / Ed25519 / ECDH primitives.",
|
|
387
|
+
algorithms: ["RSA", "ECDSA", "EdDSA", "ECDH"],
|
|
388
|
+
severity: "medium",
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: "openssl",
|
|
392
|
+
ecosystem: "cargo",
|
|
393
|
+
reason: "OpenSSL bindings for classical RSA/EC crypto.",
|
|
394
|
+
algorithms: ["RSA", "ECDSA"],
|
|
395
|
+
severity: "medium",
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "p256",
|
|
399
|
+
ecosystem: "cargo",
|
|
400
|
+
reason: "NIST P-256 ECDSA signatures and ECDH key agreement.",
|
|
401
|
+
algorithms: ["ECDSA", "ECDH"],
|
|
402
|
+
severity: "high",
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: "p384",
|
|
406
|
+
ecosystem: "cargo",
|
|
407
|
+
reason: "NIST P-384 ECDSA signatures and ECDH key agreement.",
|
|
408
|
+
algorithms: ["ECDSA", "ECDH"],
|
|
409
|
+
severity: "high",
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: "k256",
|
|
413
|
+
ecosystem: "cargo",
|
|
414
|
+
reason: "secp256k1 ECDSA signatures and ECDH key agreement.",
|
|
415
|
+
algorithms: ["ECDSA", "ECDH"],
|
|
416
|
+
severity: "high",
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "ed25519-dalek",
|
|
420
|
+
ecosystem: "cargo",
|
|
421
|
+
reason: "Ed25519 signatures (modern but classical).",
|
|
422
|
+
algorithms: ["EdDSA"],
|
|
423
|
+
severity: "low",
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "x25519-dalek",
|
|
427
|
+
ecosystem: "cargo",
|
|
428
|
+
reason: "X25519 key agreement (modern but classical).",
|
|
429
|
+
algorithms: ["X25519"],
|
|
430
|
+
severity: "low",
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
// --- Go modules ---
|
|
434
|
+
{
|
|
435
|
+
name: "golang.org/x/crypto",
|
|
436
|
+
ecosystem: "go",
|
|
437
|
+
reason: "SSH, OpenPGP, and classical curve helpers on top of the Go stdlib.",
|
|
438
|
+
algorithms: ["RSA", "ECDSA", "EdDSA", "ECDH"],
|
|
439
|
+
severity: "medium",
|
|
440
|
+
},
|
|
441
|
+
|
|
442
|
+
// --- Maven (Java) ---
|
|
443
|
+
{
|
|
444
|
+
name: "bcprov-jdk18on",
|
|
445
|
+
ecosystem: "maven",
|
|
446
|
+
reason: "BouncyCastle provider — full classical RSA/ECDSA/ECDH/DSA suite.",
|
|
447
|
+
algorithms: ["RSA", "ECDSA", "ECDH", "DSA"],
|
|
448
|
+
severity: "high",
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
name: "bcprov-jdk15on",
|
|
452
|
+
ecosystem: "maven",
|
|
453
|
+
reason: "BouncyCastle provider (JDK 1.5+) — classical RSA/ECDSA/ECDH/DSA.",
|
|
454
|
+
algorithms: ["RSA", "ECDSA", "ECDH", "DSA"],
|
|
455
|
+
severity: "high",
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
name: "bcpkix-jdk18on",
|
|
459
|
+
ecosystem: "maven",
|
|
460
|
+
reason: "BouncyCastle PKIX — classical X.509 / CMS with RSA/ECDSA.",
|
|
461
|
+
algorithms: ["RSA", "ECDSA"],
|
|
462
|
+
severity: "medium",
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: "java-jwt",
|
|
466
|
+
ecosystem: "maven",
|
|
467
|
+
reason: "Auth0 JWT with classical RS*/ES* algorithms.",
|
|
468
|
+
algorithms: ["RSA", "ECDSA"],
|
|
469
|
+
severity: "medium",
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
// --- RubyGems ---
|
|
473
|
+
{
|
|
474
|
+
name: "jwt",
|
|
475
|
+
ecosystem: "rubygems",
|
|
476
|
+
reason: "Ruby JWT with classical RS*/ES* algorithms.",
|
|
477
|
+
algorithms: ["RSA", "ECDSA"],
|
|
478
|
+
severity: "medium",
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: "rbnacl",
|
|
482
|
+
ecosystem: "rubygems",
|
|
483
|
+
reason: "libsodium bindings — X25519 key agreement and Ed25519 signatures.",
|
|
484
|
+
algorithms: ["X25519", "EdDSA"],
|
|
485
|
+
severity: "low",
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
name: "ed25519",
|
|
489
|
+
ecosystem: "rubygems",
|
|
490
|
+
reason: "Ed25519 signatures (modern but classical).",
|
|
491
|
+
algorithms: ["EdDSA"],
|
|
492
|
+
severity: "low",
|
|
493
|
+
},
|
|
262
494
|
];
|
|
263
495
|
|
|
264
|
-
/**
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
496
|
+
/**
|
|
497
|
+
* Normalise a package name for matching within its ecosystem. PyPI is
|
|
498
|
+
* case-insensitive and folds runs of `-_.` to a single `-` (PEP 503); cargo /
|
|
499
|
+
* maven / rubygems are effectively lower-case; npm and go module paths are
|
|
500
|
+
* matched verbatim (npm scopes and go paths are case-sensitive).
|
|
501
|
+
*/
|
|
502
|
+
function normalizeName(ecosystem: DependencyEcosystem, name: string): string {
|
|
503
|
+
const n = name.trim();
|
|
504
|
+
if (ecosystem === "pypi") return n.toLowerCase().replace(/[-_.]+/g, "-");
|
|
505
|
+
if (ecosystem === "npm" || ecosystem === "go") return n;
|
|
506
|
+
return n.toLowerCase();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** ecosystem → (normalised name → entry). Scoped so `rsa` (pypi) ≠ `rsa` (cargo). */
|
|
510
|
+
const BY_ECOSYSTEM: Map<DependencyEcosystem, Map<string, VulnerableDependency>> = (() => {
|
|
511
|
+
const m = new Map<DependencyEcosystem, Map<string, VulnerableDependency>>();
|
|
512
|
+
for (const d of vulnerableDependencies) {
|
|
513
|
+
let em = m.get(d.ecosystem);
|
|
514
|
+
if (!em) {
|
|
515
|
+
em = new Map();
|
|
516
|
+
m.set(d.ecosystem, em);
|
|
517
|
+
}
|
|
518
|
+
em.set(normalizeName(d.ecosystem, d.name), d);
|
|
519
|
+
}
|
|
520
|
+
return m;
|
|
521
|
+
})();
|
|
268
522
|
|
|
269
523
|
/**
|
|
270
|
-
* Precompiled `"name":` lookup regex per known package (P1-9): the
|
|
271
|
-
*
|
|
272
|
-
*
|
|
524
|
+
* Precompiled `"name":` lookup regex per known npm package (P1-9): the set is
|
|
525
|
+
* static, so build the (name → RegExp) map once rather than per manifest. Only
|
|
526
|
+
* npm uses the JSON-key form; other ecosystems locate names generically.
|
|
273
527
|
*/
|
|
274
528
|
const KEY_REGEX_BY_NAME = new Map<string, RegExp>(
|
|
275
|
-
vulnerableDependencies
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
529
|
+
vulnerableDependencies
|
|
530
|
+
.filter((d) => d.ecosystem === "npm")
|
|
531
|
+
.map((d): [string, RegExp] => {
|
|
532
|
+
const escaped = d.name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
533
|
+
return [d.name, new RegExp(`"${escaped}"\\s*:`)];
|
|
534
|
+
}),
|
|
279
535
|
);
|
|
280
536
|
|
|
281
|
-
/** Algorithm families that expose confidentiality (HNDL) rather than only signing. */
|
|
282
|
-
const CONFIDENTIALITY_FAMILIES: ReadonlySet<AlgorithmFamily> = new Set<AlgorithmFamily>([
|
|
283
|
-
"RSA",
|
|
284
|
-
"ECDH",
|
|
285
|
-
"DH",
|
|
286
|
-
"ECIES",
|
|
287
|
-
"X25519",
|
|
288
|
-
"X448",
|
|
289
|
-
]);
|
|
290
|
-
|
|
291
537
|
/**
|
|
292
538
|
* Build a remediation string covering ALL families a package exposes (C5),
|
|
293
|
-
* rather than only `algorithms[0]`. De-duplicates the
|
|
294
|
-
*
|
|
539
|
+
* rather than only `algorithms[0]`. De-duplicates by PQC *target* — the KEM
|
|
540
|
+
* target and the signature target are each named at most once — so a library
|
|
541
|
+
* exposing both a confidentiality family and a signature family reads
|
|
542
|
+
* "ML-KEM-768 … for key exchange/encryption; ML-DSA-65 (FIPS 204) for
|
|
543
|
+
* signatures" instead of repeating ML-DSA (the old per-string Set couldn't
|
|
544
|
+
* collapse "ML-DSA-65 for signatures" against a standalone "ML-DSA-65 (FIPS 204)").
|
|
295
545
|
*/
|
|
296
546
|
function multiFamilyRemediation(algorithms: readonly AlgorithmFamily[]): string {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
547
|
+
let needsKem = false;
|
|
548
|
+
let needsSig = false;
|
|
549
|
+
let needsReview = false;
|
|
550
|
+
for (const a of algorithms) {
|
|
551
|
+
if (isConfidentialityFamily(a)) needsKem = true;
|
|
552
|
+
if (isSignatureFamily(a)) needsSig = true;
|
|
553
|
+
if (a === "unknown") needsReview = true;
|
|
554
|
+
}
|
|
555
|
+
const parts: string[] = [];
|
|
556
|
+
if (needsKem)
|
|
557
|
+
parts.push("ML-KEM-768 (FIPS 203, hybrid X25519MLKEM768) for key exchange/encryption");
|
|
558
|
+
if (needsSig) parts.push("ML-DSA-65 (FIPS 204) for signatures");
|
|
559
|
+
// Only fall back to the generic "review" line when nothing concrete applied.
|
|
560
|
+
if (needsReview && parts.length === 0) parts.push(remediationText("unknown"));
|
|
561
|
+
return parts.join("; ");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* The dependency ecosystem a manifest filename belongs to, or null when the file
|
|
566
|
+
* is not a manifest we parse. Drives both {@link isManifestFile} and the scanner
|
|
567
|
+
* dispatch — one place to add a manifest format.
|
|
568
|
+
*/
|
|
569
|
+
export function manifestEcosystem(file: string): DependencyEcosystem | null {
|
|
570
|
+
const base = (file.split("/").pop() ?? file).toLowerCase();
|
|
571
|
+
if (
|
|
572
|
+
base === "package.json" ||
|
|
573
|
+
base === "package-lock.json" ||
|
|
574
|
+
base === "npm-shrinkwrap.json" ||
|
|
575
|
+
base === "yarn.lock" ||
|
|
576
|
+
base === "pnpm-lock.yaml"
|
|
577
|
+
) {
|
|
578
|
+
return "npm";
|
|
579
|
+
}
|
|
580
|
+
if (base === "requirements.txt" || /^requirements[\w.-]*\.txt$/.test(base)) return "pypi";
|
|
581
|
+
if (base === "pyproject.toml" || base === "pipfile") return "pypi";
|
|
582
|
+
if (base === "cargo.toml") return "cargo";
|
|
583
|
+
if (base === "go.mod") return "go";
|
|
584
|
+
if (base === "pom.xml" || base === "build.gradle" || base === "build.gradle.kts") return "maven";
|
|
585
|
+
if (base === "gemfile" || base.endsWith(".gemspec")) return "rubygems";
|
|
586
|
+
return null;
|
|
300
587
|
}
|
|
301
588
|
|
|
302
589
|
/** True if a file path looks like a manifest we can parse for dependencies. */
|
|
303
590
|
export function isManifestFile(file: string): boolean {
|
|
304
|
-
|
|
305
|
-
|
|
591
|
+
return manifestEcosystem(file) !== null;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Extract candidate package names from a non-npm manifest. Extraction is
|
|
596
|
+
* deliberately GENEROUS — every finding still has to match a curated DB entry,
|
|
597
|
+
* so over-extracting harmless tokens (e.g. a TOML `version = …` key) costs
|
|
598
|
+
* nothing while keeping the parsers tiny and format-tolerant.
|
|
599
|
+
*/
|
|
600
|
+
function candidateNames(ecosystem: DependencyEcosystem, content: string): string[] {
|
|
601
|
+
const names: string[] = [];
|
|
602
|
+
const lines = content.split("\n");
|
|
603
|
+
switch (ecosystem) {
|
|
604
|
+
case "pypi": {
|
|
605
|
+
// requirements.txt / Pipfile / pyproject.toml: leading token per line, plus
|
|
606
|
+
// quoted deps in arrays/tables. Skip comments and `-r/-e/--flag` lines.
|
|
607
|
+
for (const raw of lines) {
|
|
608
|
+
const line = raw.trim();
|
|
609
|
+
if (!line || line.startsWith("#") || line.startsWith("-")) continue;
|
|
610
|
+
const lead = /^["']?([A-Za-z][A-Za-z0-9._-]+)/.exec(line);
|
|
611
|
+
if (lead) names.push(lead[1]);
|
|
612
|
+
for (const m of line.matchAll(/["']([A-Za-z][A-Za-z0-9._-]+)/g)) names.push(m[1]);
|
|
613
|
+
}
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
case "cargo": {
|
|
617
|
+
// Cargo.toml: dependency-table keys `name = …` and `[…dependencies.name]`.
|
|
618
|
+
for (const raw of lines) {
|
|
619
|
+
const line = raw.trim();
|
|
620
|
+
if (!line || line.startsWith("#")) continue;
|
|
621
|
+
const key = /^([A-Za-z0-9][A-Za-z0-9_-]+)\s*=/.exec(line);
|
|
622
|
+
if (key) names.push(key[1]);
|
|
623
|
+
const table = /^\[[\w.-]*dependencies\.([A-Za-z0-9][A-Za-z0-9_-]+)\]/.exec(line);
|
|
624
|
+
if (table) names.push(table[1]);
|
|
625
|
+
}
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
case "go": {
|
|
629
|
+
// go.mod: `require path v1.2.3` (single or in a require( … ) block).
|
|
630
|
+
for (const m of content.matchAll(/(?:^|\s)([a-z0-9][\w.\-/]+)\s+v\d/gm)) names.push(m[1]);
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
case "maven": {
|
|
634
|
+
// pom.xml <artifactId> tags + gradle `group:artifact:version` strings.
|
|
635
|
+
for (const m of content.matchAll(/<artifactId>\s*([\w.-]+)\s*<\/artifactId>/g)) {
|
|
636
|
+
names.push(m[1]);
|
|
637
|
+
}
|
|
638
|
+
for (const m of content.matchAll(/["']([\w.-]+):([\w.-]+):[\w.$-]+["']/g)) names.push(m[2]);
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
case "rubygems": {
|
|
642
|
+
// Gemfile `gem 'name'` + gemspec `add_*dependency 'name'`.
|
|
643
|
+
for (const m of content.matchAll(/\bgem\s+["']([\w.-]+)["']/g)) names.push(m[1]);
|
|
644
|
+
for (const m of content.matchAll(
|
|
645
|
+
/add(?:_runtime|_development)?_dependency\s+["']([\w.-]+)["']/g,
|
|
646
|
+
)) {
|
|
647
|
+
names.push(m[1]);
|
|
648
|
+
}
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case "npm":
|
|
652
|
+
break; // npm uses the JSON path, not this extractor.
|
|
653
|
+
}
|
|
654
|
+
return names;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/** First offset of a package `name` as a whole token in the manifest text (or 0). */
|
|
658
|
+
function offsetOfName(content: string, name: string): number {
|
|
659
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
660
|
+
const m = new RegExp(`(?<![\\w./-])${escaped}(?![\\w-])`).exec(content);
|
|
661
|
+
return m ? m.index : 0;
|
|
306
662
|
}
|
|
307
663
|
|
|
308
664
|
/**
|
|
@@ -327,7 +683,7 @@ function dependencyFinding(
|
|
|
327
683
|
confidence: "high",
|
|
328
684
|
algorithm,
|
|
329
685
|
// Confidentiality libs are HNDL-exposed; signature-only ones are not.
|
|
330
|
-
hndl: dep.algorithms.some(
|
|
686
|
+
hndl: dep.algorithms.some(isConfidentialityFamily),
|
|
331
687
|
cwe: CWE_BROKEN_CRYPTO,
|
|
332
688
|
message: `${dep.name} — ${dep.reason}`,
|
|
333
689
|
remediation: multiFamilyRemediation(dep.algorithms),
|
|
@@ -346,16 +702,56 @@ function offsetOfKey(content: string, name: string): number {
|
|
|
346
702
|
return m ? m.index : 0;
|
|
347
703
|
}
|
|
348
704
|
|
|
705
|
+
/** Sort findings by title for deterministic output. */
|
|
706
|
+
function sortByTitle(findings: Finding[]): Finding[] {
|
|
707
|
+
findings.sort((a, b) => (a.title < b.title ? -1 : a.title > b.title ? 1 : 0));
|
|
708
|
+
return findings;
|
|
709
|
+
}
|
|
710
|
+
|
|
349
711
|
/**
|
|
350
|
-
* Scan a single manifest file
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
* -
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
* Returns one finding per distinct vulnerable package name found in the file.
|
|
712
|
+
* Scan a single manifest file for vulnerable dependencies, dispatching by the
|
|
713
|
+
* file's ecosystem ({@link manifestEcosystem}). npm manifests are parsed as
|
|
714
|
+
* JSON; every other ecosystem uses the generous {@link candidateNames} extractor
|
|
715
|
+
* filtered against the ecosystem-scoped database. Returns one finding per
|
|
716
|
+
* distinct vulnerable package.
|
|
357
717
|
*/
|
|
358
718
|
export function scanManifest(file: string, content: string): Finding[] {
|
|
719
|
+
const ecosystem = manifestEcosystem(file);
|
|
720
|
+
if (!ecosystem) return [];
|
|
721
|
+
const db = BY_ECOSYSTEM.get(ecosystem);
|
|
722
|
+
if (!db) return [];
|
|
723
|
+
|
|
724
|
+
if (ecosystem === "npm") {
|
|
725
|
+
const base = (file.split("/").pop() ?? file).toLowerCase();
|
|
726
|
+
// yarn.lock / pnpm-lock.yaml are not JSON — extract names from their text.
|
|
727
|
+
if (base === "yarn.lock" || base === "pnpm-lock.yaml") {
|
|
728
|
+
return scanNpmLockfile(content, file, db);
|
|
729
|
+
}
|
|
730
|
+
return scanNpmManifest(content, file, db);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const found = new Map<string, VulnerableDependency>(); // dedupe by entry
|
|
734
|
+
for (const raw of candidateNames(ecosystem, content)) {
|
|
735
|
+
const dep = db.get(normalizeName(ecosystem, raw));
|
|
736
|
+
if (dep) found.set(dep.name, dep);
|
|
737
|
+
}
|
|
738
|
+
const findings: Finding[] = [];
|
|
739
|
+
for (const dep of found.values()) {
|
|
740
|
+
findings.push(dependencyFinding(dep, file, content, offsetOfName(content, dep.name)));
|
|
741
|
+
}
|
|
742
|
+
return sortByTitle(findings);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* npm manifest scan.
|
|
747
|
+
* - `package.json`: dependencies / devDependencies / peerDependencies / optionalDependencies.
|
|
748
|
+
* - `package-lock.json` (v2/v3): the `packages` map keys (node_modules/<name>).
|
|
749
|
+
*/
|
|
750
|
+
function scanNpmManifest(
|
|
751
|
+
content: string,
|
|
752
|
+
file: string,
|
|
753
|
+
db: Map<string, VulnerableDependency>,
|
|
754
|
+
): Finding[] {
|
|
359
755
|
let json: unknown;
|
|
360
756
|
try {
|
|
361
757
|
json = JSON.parse(content);
|
|
@@ -370,17 +766,15 @@ export function scanManifest(file: string, content: string): Finding[] {
|
|
|
370
766
|
const collectFromRecord = (rec: unknown): void => {
|
|
371
767
|
if (rec === null || typeof rec !== "object") return;
|
|
372
768
|
for (const key of Object.keys(rec as Record<string, unknown>)) {
|
|
373
|
-
if (
|
|
769
|
+
if (db.has(key)) found.add(key);
|
|
374
770
|
}
|
|
375
771
|
};
|
|
376
772
|
|
|
377
|
-
// package.json dependency sections.
|
|
378
773
|
collectFromRecord(obj.dependencies);
|
|
379
774
|
collectFromRecord(obj.devDependencies);
|
|
380
775
|
collectFromRecord(obj.peerDependencies);
|
|
381
776
|
collectFromRecord(obj.optionalDependencies);
|
|
382
777
|
|
|
383
|
-
// package-lock.json v2/v3 "packages" map: keys are "node_modules/<name>".
|
|
384
778
|
const packages = obj.packages;
|
|
385
779
|
if (packages !== null && typeof packages === "object") {
|
|
386
780
|
for (const key of Object.keys(packages as Record<string, unknown>)) {
|
|
@@ -388,17 +782,55 @@ export function scanManifest(file: string, content: string): Finding[] {
|
|
|
388
782
|
const marker = "node_modules/";
|
|
389
783
|
const idx = key.lastIndexOf(marker);
|
|
390
784
|
const name = idx >= 0 ? key.slice(idx + marker.length) : key;
|
|
391
|
-
if (
|
|
785
|
+
if (db.has(name)) found.add(name);
|
|
392
786
|
}
|
|
393
787
|
}
|
|
394
788
|
|
|
395
789
|
const findings: Finding[] = [];
|
|
396
790
|
for (const name of found) {
|
|
397
|
-
const dep =
|
|
791
|
+
const dep = db.get(name);
|
|
398
792
|
if (!dep) continue;
|
|
399
793
|
findings.push(dependencyFinding(dep, file, content, offsetOfKey(content, name)));
|
|
400
794
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
795
|
+
return sortByTitle(findings);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Extract candidate package names from a yarn.lock / pnpm-lock.yaml text body.
|
|
800
|
+
* Generous — every `name@…` (and pnpm's `/name/version`) token is captured; the
|
|
801
|
+
* caller filters against the vulnerable-dependency DB, so over-capture is safe.
|
|
802
|
+
* Handles yarn v1 (`elliptic@^6.5.4:`), yarn berry (`"elliptic@npm:^6.5.4":`),
|
|
803
|
+
* and pnpm (`/elliptic@6.5.4:`, scoped `/@noble/curves@1.2.0:`).
|
|
804
|
+
*/
|
|
805
|
+
function npmLockfileCandidates(content: string): string[] {
|
|
806
|
+
const names = new Set<string>();
|
|
807
|
+
const at = /(?:^|[\s,"'/])((?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*)@/gm;
|
|
808
|
+
let m: RegExpExecArray | null;
|
|
809
|
+
while ((m = at.exec(content)) !== null) names.add(m[1].toLowerCase());
|
|
810
|
+
// pnpm's older `/name/version` form (name followed by a version digit).
|
|
811
|
+
const slash = /(?:^|\s)\/((?:@[a-z0-9._-]+\/)?[a-z0-9][a-z0-9._-]*)\/\d/gm;
|
|
812
|
+
while ((m = slash.exec(content)) !== null) names.add(m[1].toLowerCase());
|
|
813
|
+
return [...names];
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* npm lockfile scan (yarn.lock / pnpm-lock.yaml): match every package token in
|
|
818
|
+
* the lockfile text against the vulnerable-dependency DB. Catches transitive
|
|
819
|
+
* dependencies that never appear in package.json.
|
|
820
|
+
*/
|
|
821
|
+
function scanNpmLockfile(
|
|
822
|
+
content: string,
|
|
823
|
+
file: string,
|
|
824
|
+
db: Map<string, VulnerableDependency>,
|
|
825
|
+
): Finding[] {
|
|
826
|
+
const found = new Map<string, VulnerableDependency>();
|
|
827
|
+
for (const name of npmLockfileCandidates(content)) {
|
|
828
|
+
const dep = db.get(name);
|
|
829
|
+
if (dep) found.set(dep.name, dep);
|
|
830
|
+
}
|
|
831
|
+
const findings: Finding[] = [];
|
|
832
|
+
for (const dep of found.values()) {
|
|
833
|
+
findings.push(dependencyFinding(dep, file, content, offsetOfName(content, dep.name)));
|
|
834
|
+
}
|
|
835
|
+
return sortByTitle(findings);
|
|
404
836
|
}
|