diff --git a/README.md b/README.md
index 4878286..b25ccda 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Aere Quantum
-Self-hosted post-quantum infrastructure from Aere Network. Three components, each a few files with **no dependencies**:
+Self-hosted post-quantum infrastructure from Aere Network. Four components, each a few files with **no dependencies**:
Node.js 24 and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a package registry.
| component | what it does |
@@ -8,6 +8,7 @@ Node.js 24 and the OpenSSL 3.5 it ships with (`node:crypto`), nothing from a pac
| [`pq-gateway/`](pq-gateway/) | a TLS 1.3 terminating proxy in front of any HTTP service, with the hybrid key exchange X25519MLKEM768: `hybrid-only` refuses a classical client at the handshake, `hybrid-preferred` keeps it working; optional client authentication with ML-DSA certificates |
| [`pq-kms/`](pq-kms/) | a transit-style key management service where every key is hybrid: X25519 + ML-KEM-768 for encryption, Ed25519 + ML-DSA-65 for signatures (both halves required); versions, rotation, rewrap, data keys, a chained audit log; the root key from the environment or sealed by an HSM through PKCS#11 |
| [`pq-pki/`](pq-pki/) | a private certificate authority for ML-DSA (X.509 v3, RFC 9881): root and issuing CAs, leaf certificates, revocation lists, and a strict chain verifier compared against OpenSSL |
+| [`crypto-inventory/`](crypto-inventory/) | a cryptographic inventory of source code (JavaScript/TypeScript, Python, Java, Go, PEM blocks, dependency manifests): every use classified by its exposure to a quantum computer, with a migration target, written as a CycloneDX 1.6 CBOM; nothing from the scanned tree is executed, and its cost stays linear on input built to be slow |
Each component's README says what it is **not** and what is **not measured**. No third party has reviewed any of them.
@@ -22,9 +23,11 @@ test, counts as a failure of the control. Results measured on 2026-09-29 (Node.j
| pq-gateway | 33/33 (`node proba-pq-gateway.mjs`) | 33/33 (`bash proba-pq-gateway-control-negativ.sh`) |
| pq-kms | 62/62 (`node test/proba.mjs`); HSM root on SoftHSM2 + OpenSC 20/20 (`test/proba-hsm.mjs`, Linux); sealed-file trust rules 7/7 (`test/proba-hsm-incredere.mjs`) | 16/16 (`node test/control-negativ.mjs`); sealed-file rules 2/2 in this repository (`test/control-negativ-hsm-incredere.mjs`) |
| pq-pki | 27/27 (`node test/proba.mjs`), each verdict compared with OpenSSL 3.5 | 22/22 (`node test/control-negativ.mjs`) |
+| crypto-inventory | 37/37 (`node test/proba.mjs`); cost on hostile input 8/8 linear (`node test/proba-timp.mjs`) | 18/18 (`node test/control-negativ.mjs`); cost 3/3 in this repository (`node test/control-negativ-timp.mjs`; its fourth case compares with version 0.1.0 from the development history and is skipped here) |
-Test names and control messages are in Romanian; identifiers, error codes, messages and documentation are in English.
+Code comments, internal names, test names and control messages are in Romanian, and so are the two command words of the KMS
+HSM tool (explained in its README); error codes, error messages, exported names and documentation are in English.
## Licence
-MIT, see [LICENSE](LICENSE). Files: 24 (pq-gateway 6, pq-kms 10, pq-pki 6).
+MIT, see [LICENSE](LICENSE). Files: 66 (pq-gateway 6, pq-kms 10, pq-pki 6, crypto-inventory 42).
diff --git a/crypto-inventory/README.md b/crypto-inventory/README.md
new file mode 100644
index 0000000..a8790ee
--- /dev/null
+++ b/crypto-inventory/README.md
@@ -0,0 +1,202 @@
+# Aere crypto inventory
+
+A cryptographic inventory of source code. It reads a directory, finds where cryptography is used
+(algorithms, key sizes, curves, modes, TLS settings, JWT algorithms, key and certificate files),
+classifies each use by its exposure to a quantum computer, suggests a migration target, and writes
+the result as a **CBOM** (Cryptography Bill of Materials) in **CycloneDX 1.6** format.
+
+It is a single Node.js script with no dependencies: only built-in `node:*` modules. Nothing from the
+scanned tree is executed, and no network request is made.
+
+```
+node tools/crypto-inventory/inventar.mjs scan ./my-service --out cbom.json --summary
+```
+
+## What it detects
+
+Detection is pattern-based on the source text, after comments (and Python docstrings) are removed.
+An API name that appears only inside a comment or inside a string literal is not reported as a use.
+
+| Language | What is recognized |
+|---|---|
+| JavaScript / TypeScript | `node:crypto` (`createHash`, `createHmac`, `createCipheriv`, `createSign`/`createVerify`, `generateKeyPair(Sync)` for `rsa`, `rsa-pss`, `dsa`, `ec`, `ed25519`, `x25519`, `dh`, `ml-dsa-*`, `ml-kem-*`, `slh-dsa-*`, `createECDH`, `getDiffieHellman`, `publicEncrypt`, `pbkdf2`, `hkdf`, `crypto.sign`/`verify`, `createPrivateKey`/`createPublicKey`, `X509Certificate`); WebCrypto `subtle` (RSA-OAEP, RSASSA-PKCS1-v1_5, RSA-PSS, ECDSA, ECDH, Ed25519, X25519, AES-GCM/CBC/CTR/KW, HMAC, HKDF, PBKDF2, `digest`); TLS options `minVersion`, `maxVersion`, `secureProtocol`, `ecdhCurve`; libraries from imports: `ethers`, `web3`, `viem` (secp256k1 ECDSA), `secp256k1`, `elliptic`, `node-forge`, `jsonwebtoken`/`jose` (JWS algorithms), `@noble/curves`, `@noble/post-quantum`, `@noble/hashes`, `crypto-js`, `tweetnacl`/libsodium, `node-rsa`, bcrypt/argon2 |
+| Python | `cryptography.hazmat` (`rsa`, `ec`, `dsa`, `dh`, `ed25519`, `x25519`, `padding`, `hashes`, `algorithms`/`modes`, AEAD classes); PyCryptodome (`Crypto`/`Cryptodome`: `RSA`, `DSA`, `ECC`, ciphers and modes, `PKCS1_OAEP`, `pss`, `DSS`, hash `.new`); `hashlib` (including `hashlib.new` and `pbkdf2_hmac`); `hmac` with `digestmod`; `ecdsa`; PyJWT / python-jose algorithms; `oqs` (liboqs-python); `ssl` protocol constants and `TLSVersion` |
+| Java | `KeyPairGenerator`, `Signature`, `Cipher`, `KeyAgreement`, `MessageDigest`, `Mac`, `KeyGenerator`, `KeyFactory`, `SecretKeyFactory`, `KEM`, `SSLContext` `getInstance("...")`; `ECGenParameterSpec`, `NamedParameterSpec`; `setEnabledProtocols`; JSSE system properties (`jdk.tls.namedGroups`, protocol lists); Bouncy Castle imports and post-quantum parameter sets (`org.bouncycastle.pqc`, `MLKEMParameters`, `MLDSAParameters`, `SLHDSAParameters`, `FalconParameters`, ...) |
+| Go | imports of `crypto/rsa`, `crypto/ecdsa`, `crypto/elliptic`, `crypto/ecdh`, `crypto/ed25519`, `crypto/md5`, `crypto/sha1`, `crypto/des`, `crypto/rc4`, `crypto/mlkem`, `crypto/tls`, `golang.org/x/crypto/...`, go-ethereum `crypto`, CIRCL ML-KEM/ML-DSA; calls with parameters (`rsa.GenerateKey` size, `ecdsa.GenerateKey` curve, `hmac.New` hash, `mlkem.GenerateKey768`); `tls.Config` `MinVersion`, `MaxVersion`, `CurvePreferences` (including `tls.X25519MLKEM768`) |
+| Any text file | complete PEM blocks: certificates (parsed for subject, validity and key algorithm), public keys, private keys |
+| Manifests | `package.json`, `requirements*.txt`, `pyproject.toml`, `pom.xml`, `build.gradle(.kts)`, `go.mod`: cryptographic libraries that are declared |
+
+In Go an unused import does not compile, so an import of a crypto package is evidence of use; the
+import-level finding is dropped when the same file has a call-level finding that covers it. A blank
+import (`_ "crypto/sha512"`) is reported with a note that it only registers an implementation.
+
+When the algorithm is given by a variable, the tool resolves it only when the file assigns that name
+exactly once, to a plain string literal, and the name is not a function parameter or loop variable
+(template literals whose `${...}` parts all resolve this way are also resolved). The finding then
+records where the value came from (`resolvedFrom`). In every other case the finding is `unknown`: the
+tool does not guess. At most 64 distinct variables are resolved per file; past that, variables in that
+file are reported as `unknown`, and the file is counted (`files-over-resolution-limit` in the CBOM, a
+line in the text summary).
+
+## Cost on hostile input
+
+The scanner reads repositories it did not write, so its cost is kept linear in the size of each file,
+also for input built to be slow. Version 0.1.0, which was never published, had six places where a
+crafted file cost quadratic time, minutes for a single file of the default maximum size: PEM block
+search, JavaScript regex literals, Python triple-quoted strings on a long line, variable resolution
+repeated for every call, open-ended parameter-list patterns used by that resolution, and the
+assigned-variable lookup in Java. An adversarial review found them on 2026-09-29 and they were
+rewritten for 0.2.0; the rewritten forms give the same findings as the old ones: on 2026-09-29,
+`test/echivalenta-0.1.0.mjs` (which takes 0.1.0 from git history) found no difference on about 4,500
+source files of the repository where the tool is developed, including three large npm packages, nor
+on 300,000 generated inputs to the lexer and the PEM search. `test/proba-timp.mjs` keeps the cost
+linear, and `test/control-negativ-timp.mjs` shows that it turns red when a quadratic form comes back.
+One cost remains bounded rather than linear: the arguments of a call are read for at most 4,000
+characters, so a file made of thousands of unclosed calls is slower (about 10 seconds per megabyte on
+the machine where it was measured) but still finishes.
+
+## Classification
+
+Every finding carries one class, a written reason and, when action is needed, a recommendation.
+
+| Class | Meaning |
+|---|---|
+| `quantum-vulnerable` | Broken by Shor's algorithm on a cryptographically relevant quantum computer: RSA, DSA, finite-field DH, ECDSA, ECDH, EdDSA and X25519/X448 on any classical curve, secp256k1, pairing-based schemes. Also TLS configurations that allow TLS 1.2, which has no standardized post-quantum key exchange. |
+| `weak-now` | Unsafe against classical attackers today: MD5, SHA-1, DES, 3DES, RC4, RC2, 64-bit-block ciphers, ECB mode, SSL and TLS below 1.2, RSA/DH below 2048 bits, curves below 112-bit security, JWT `none`. When the algorithm is also quantum-vulnerable (for example RSA-1024, or an RSA signature over SHA-1), the finding says so (`quantumVulnerable: true`). |
+| `quantum-safe` | ML-KEM (FIPS 203), ML-DSA (FIPS 204), SLH-DSA (FIPS 205), Falcon, hybrid key-exchange groups such as X25519MLKEM768, AES, ChaCha20-Poly1305, SHA-2, SHA-3, HMAC and KDFs over them. For AES-128 the reason notes that Grover's algorithm gives at most a quadratic speed-up (NIST category 1) and that AES-256 is preferred for long-lived data. |
+| `unknown` | The algorithm cannot be determined statically: it comes from a parameter, from configuration, from a key loaded at run time, or from a library that offers many algorithms without a recognized call. |
+
+Recommendations are concrete but do not pretend a migration is simple. Examples: key exchange in TLS
+moves to X25519MLKEM768; signatures move to ML-DSA-65 or a hybrid classical+ML-DSA signature (the
+X.509 composite signature format was still an IETF draft when the rules were written); secp256k1 in a
+wallet moves at the account level to an account with a post-quantum owner, which is a protocol change
+and not a library swap; for JWT the tool says plainly that no final post-quantum JWS standard exists
+yet. Statements about standards and runtime defaults reflect the time the rules were written; check
+the current status before acting on them.
+
+A finding states which algorithm appears where. It does not judge intent: a TLS scanner that
+deliberately offers classical groups to test a server will be reported like any other client.
+
+## Output
+
+### CBOM (CycloneDX 1.6)
+
+- `bomFormat: "CycloneDX"`, `specVersion: "1.6"`, `serialNumber` (`urn:uuid:...`), `version: 1`.
+- `metadata.timestamp`, `metadata.tools.components` (this tool and its version),
+ `metadata.component` (the scanned directory), `metadata.properties` (scan statistics: files seen,
+ analyzed, not read and why, excluded directories, limits).
+- One component of `type: "cryptographic-asset"` per distinct asset, with a stable `bom-ref`
+ derived from its content:
+ - `cryptoProperties.assetType`: `algorithm`, `protocol`, `certificate` or `related-crypto-material`.
+ - `algorithmProperties`: `primitive`, `parameterSetIdentifier`, `curve`, `mode`, `padding`,
+ `cryptoFunctions`, `classicalSecurityLevel`, `nistQuantumSecurityLevel` (0 for quantum-vulnerable
+ and weak algorithms; omitted when not defined).
+ - `protocolProperties`: `type: "tls"`, `version`.
+ - `certificateProperties`: subject, issuer, validity, format.
+ - `relatedCryptoMaterialProperties`: `type` (`private-key` or `public-key`), `format`, `size`.
+ **The value of key material is never written.**
+ - `oid` where it is standard and certain.
+ - `evidence.occurrences[]`: `location` (path relative to the scanned directory), `line`,
+ `symbol` (the API), `additionalContext` (the matched call, not the source line).
+ - `properties` in the `aere:crypto-inventory:` namespace: `classification`,
+ `quantum-vulnerable`, `reason`, `recommendation`, `languages`, `evidence-kinds`, `resolved-from`.
+- Declared libraries from manifests are components of `type: "library"` with `purl`, the manifest
+ location, and `aere:crypto-inventory:declared-only = true`. **Declaration is not use**: a library
+ can be declared and never called, and the cryptographic-asset components are the evidence of use.
+
+`--deterministic` derives the serial number from the content and omits the timestamp, so two runs on
+the same tree produce identical files.
+
+### Text summary (`--summary`)
+
+Files seen, analyzed per language, checked for PEM only, and **not read, with the reason** (binary,
+too large, over the file limit, unreadable), plus excluded directories and symlinks not followed.
+Nothing is skipped silently: the scan fails with an accounting error if the categories do not add up
+to the number of files seen. Then counts per class and per language, the most frequent assets, the
+files with quantum-vulnerable findings, and the declared libraries.
+
+## Command line
+
+```
+node inventar.mjs scan
[options]
+
+ --out write the CBOM to (default: stdout, unless --summary is given)
+ --summary print the text summary
+ --fail-on exit 1 when findings match: vulnerable, weak, unknown, any (comma-separated)
+ --max-file-bytes skip and count files larger than n bytes (default 1048576)
+ --max-files read at most n files; the rest are counted, not read (default 50000)
+ --exclude-dir do not descend into directories with this name (repeatable)
+ --no-default-excludes also descend into .git, node_modules, __pycache__, .venv, venv, ...
+ --deterministic reproducible output (content-derived serial number, no timestamp)
+ --findings-json write the raw findings, one object per occurrence
+```
+
+Exit codes: `0` success, `1` the `--fail-on` condition is met, `2` usage or scan error.
+
+`--fail-on vulnerable` matches every finding a quantum computer would break, including `weak-now`
+findings that are also quantum-vulnerable.
+
+### In CI
+
+```yaml
+- uses: actions/checkout@v4
+- uses: actions/setup-node@v4
+ with:
+ node-version: 24
+- name: Cryptographic inventory
+ run: node tools/crypto-inventory/inventar.mjs scan . --out cbom.json --summary --fail-on vulnerable
+- uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: cbom
+ path: cbom.json
+```
+
+Start with `--fail-on weak` if the goal is to block known-broken algorithms first, and add
+`vulnerable` once a migration plan exists; failing a build on every ECDSA use from day one usually
+only teaches people to disable the check.
+
+### As a module
+
+```js
+import { scan, buildCbom, renderSummary, checkFailOn, classify } from './inventar.mjs';
+
+const result = scan('./src', { maxFileBytes: 1 << 20 });
+const cbom = buildCbom(result, { deterministic: true });
+console.log(renderSummary(result));
+```
+
+## What it does NOT do
+
+- It does not see cryptography inside compiled binaries (`.class`, `.jar`, `.so`, `.dll`, executables)
+ or inside transitive dependencies. It reads the source you point it at, and lists the libraries your
+ manifests declare.
+- It does not find hand-written implementations (for example a local Keccak or AES routine), calls
+ made through wrappers or reflection, dynamically computed imports, or algorithms chosen in
+ configuration files at run time. Those appear as `unknown` at best, or not at all.
+- It does not prove that a use is exploitable, reachable or security-relevant. MD5 used as a cache key
+ is reported like MD5 used for signatures; the reason text tells you what to check.
+- It is static pattern analysis. Supported languages: JavaScript/TypeScript, Python, Java and Go.
+ Kotlin, Scala, C#, Rust, C and C++ files are only checked for PEM blocks. Files that are not UTF-8
+ text (including UTF-16) are treated as binary and counted as not read.
+- It does not execute anything from the scanned tree, and it makes no network requests.
+
+## Tests
+
+```
+node test/proba.mjs # 37 probes: fixtures per language with the exact expected findings
+node test/control-negativ.mjs # breaks the scanner in a copy, one rule at a time (18 mutations), and
+ # requires the matching probe to fail while the rest of the suite still runs
+node test/proba-timp.mjs # 8 hostile inputs, each at two sizes: the cost must grow linearly
+node test/control-negativ-timp.mjs # puts each quadratic form back and requires the cost probe to turn red
+node test/echivalenta-0.1.0.mjs # same findings as 0.1.0 (needs the git history; skipped without it)
+```
+
+Fixtures mark each expected finding on its own line (`EXPECT: | `), and a probe fails on
+a missing finding, an extra finding, a wrong line or a wrong class. Negative fixtures cover comments,
+docstrings, strings that mention crypto APIs, a file without cryptography, algorithms from
+unresolvable variables, a binary file and the size and count limits. The private-key probe generates
+a key at run time (none is stored in the repository) and checks that no part of it appears in any
+output.
+
+Developed and tested on Node.js 24; older versions are not tested.
diff --git a/crypto-inventory/inventar.mjs b/crypto-inventory/inventar.mjs
new file mode 100644
index 0000000..d8d0614
--- /dev/null
+++ b/crypto-inventory/inventar.mjs
@@ -0,0 +1,98 @@
+#!/usr/bin/env node
+// Inventarul criptografic al unui cod sursa (CBOM CycloneDX 1.6). Fara dependinte externe.
+// Folosire: node inventar.mjs scan [--out cbom.json] [--summary] [--fail-on vulnerable]
+// Codul de iesire: 0 = bine, 1 = conditia --fail-on indeplinita, 2 = eroare de folosire sau de scanare.
+import { writeFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { resolve } from 'node:path';
+import { scaneaza, VERSIUNE, DOSARE_EXCLUSE_IMPLICIT } from './lib/scan.mjs';
+import { construiesteCbom } from './lib/cbom.mjs';
+import { rezumatText, verificaFailOn, numarPeClase } from './lib/rezumat.mjs';
+import { evalueaza, CLS } from './lib/catalog.mjs';
+
+export { scaneaza as scan, construiesteCbom as buildCbom, rezumatText as renderSummary, verificaFailOn as checkFailOn, numarPeClase as countByClass, evalueaza as classify, CLS as CLASSES, VERSIUNE as VERSION, DOSARE_EXCLUSE_IMPLICIT as DEFAULT_EXCLUDED_DIRS };
+
+const AJUTOR = `aere-crypto-inventory ${VERSIUNE}
+Usage:
+ node inventar.mjs scan [options]
+
+Options:
+ --out write the CycloneDX 1.6 CBOM to (default: stdout, unless --summary is given)
+ --summary print a text summary to stdout
+ --fail-on exit with code 1 if findings match: vulnerable, weak, unknown, any (comma-separated)
+ --max-file-bytes skip (and count) files larger than n bytes (default 1048576)
+ --max-files read at most n files; the rest are counted, not read (default 50000)
+ --exclude-dir do not descend into directories with this name (repeatable)
+ --no-default-excludes also descend into ${DOSARE_EXCLUSE_IMPLICIT.join(', ')}
+ --deterministic serial number derived from content, no timestamp (reproducible output)
+ --findings-json write the raw findings (one object per occurrence) as JSON
+
+Exit codes: 0 ok, 1 --fail-on condition met, 2 usage or scan error.`;
+
+export function main(argv) {
+ const a = argv.slice();
+ const cmd = a.shift();
+ if (!cmd || cmd === '--help' || cmd === '-h' || cmd === 'help') { process.stdout.write(AJUTOR + '\n'); return cmd ? 0 : 2; }
+ if (cmd === '--version') { process.stdout.write(VERSIUNE + '\n'); return 0; }
+ if (cmd !== 'scan') { process.stderr.write(`unknown command: ${cmd}\n${AJUTOR}\n`); return 2; }
+ let dir = null;
+ const o = { excludeDirs: [] };
+ let out = null;
+ let summary = false;
+ let failOn = null;
+ let determinist = false;
+ let findingsJson = null;
+ const numar = (x, nume) => {
+ if (!/^\d+$/.test(String(x))) throw new Error(`${nume} needs a non-negative integer`);
+ return Number(x);
+ };
+ try {
+ while (a.length) {
+ const x = a.shift();
+ if (x === '--out') out = a.shift();
+ else if (x === '--summary') summary = true;
+ else if (x === '--fail-on') failOn = a.shift();
+ else if (x === '--max-file-bytes') o.maxFileBytes = numar(a.shift(), x);
+ else if (x === '--max-files') o.maxFiles = numar(a.shift(), x);
+ else if (x === '--exclude-dir') o.excludeDirs.push(a.shift());
+ else if (x === '--no-default-excludes') o.noDefaultExcludes = true;
+ else if (x === '--deterministic') determinist = true;
+ else if (x === '--findings-json') findingsJson = a.shift();
+ else if (x.startsWith('--')) throw new Error(`unknown option: ${x}`);
+ else if (!dir) dir = x;
+ else throw new Error(`unexpected argument: ${x}`);
+ }
+ if (!dir) throw new Error('missing ');
+ if (out === undefined || findingsJson === undefined || failOn === undefined) throw new Error('option is missing its value');
+ verificaFailOn([], failOn);
+ } catch (err) {
+ process.stderr.write(`error: ${err.message}\n${AJUTOR}\n`);
+ return 2;
+ }
+ let rez;
+ try {
+ rez = scaneaza(dir, o);
+ } catch (err) {
+ process.stderr.write(`error: ${err.message}\n`);
+ return 2;
+ }
+ const bom = construiesteCbom(rez, { deterministic: determinist });
+ const json = JSON.stringify(bom, null, 2) + '\n';
+ if (out) {
+ writeFileSync(out, json);
+ process.stderr.write(`CBOM written: ${resolve(out)} (${bom.components.length} components)\n`);
+ }
+ if (findingsJson) writeFileSync(findingsJson, JSON.stringify(rez.findings, null, 2) + '\n');
+ if (summary) process.stdout.write(rezumatText(rez) + '\n');
+ if (!out && !summary) process.stdout.write(json);
+ const f = verificaFailOn(rez.findings, failOn);
+ if (f.esec) {
+ process.stderr.write(`fail-on ${failOn}: ${f.motive.join('; ')}\n`);
+ return 1;
+ }
+ return 0;
+}
+
+if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
+ process.exitCode = main(process.argv.slice(2));
+}
diff --git a/crypto-inventory/lib/catalog.mjs b/crypto-inventory/lib/catalog.mjs
new file mode 100644
index 0000000..314192a
--- /dev/null
+++ b/crypto-inventory/lib/catalog.mjs
@@ -0,0 +1,418 @@
+// Catalogul de clasificare. Fiecare gasire primeste o clasa, un motiv scris si, cand e
+// vulnerabila, o recomandare de migrare. Textele pentru utilizator sunt in engleza.
+//
+// Reguli: o clasa nu se ghiceste. "unknown" inseamna ca valoarea nu se poate afla static.
+
+export const CLS = Object.freeze({
+ V: 'quantum-vulnerable',
+ W: 'weak-now',
+ S: 'quantum-safe',
+ U: 'unknown',
+});
+
+export const REC = Object.freeze({
+ SIG: 'Migrate signatures to ML-DSA-65 (FIPS 204), or to a hybrid (composite) classical+ML-DSA signature during the transition so that both would have to be broken. For long-lived roots of trust consider SLH-DSA (FIPS 205). Keys and signatures grow (ML-DSA-65: 1,952-byte public key, 3,309-byte signature): check protocol, storage and column limits. Composite signatures for X.509 were still an IETF draft when this rule was written.',
+ KEX: 'For TLS, offer the hybrid group X25519MLKEM768 in TLS 1.3 (built into Go 1.24+ and OpenSSL 3.5+). For application-level key establishment use ML-KEM-768 (FIPS 203), ideally combined with X25519 during the transition. Traffic protected by a classical key exchange can be recorded now and decrypted later, so start with data that must stay confidential for years.',
+ PKE: 'Replace RSA encryption and key transport with ML-KEM-768 (FIPS 203) used as a KEM in front of an AEAD such as AES-256-GCM, ideally hybrid with X25519 during the transition. Ciphertexts recorded today become readable once a cryptographically relevant quantum computer exists; re-encrypt data that must stay confidential for years.',
+ SECP256K1: 'secp256k1 ECDSA is fixed by the account and transaction format of Ethereum-style and Bitcoin-style chains, so this is not a library swap. Plan the migration at the account level: move control of funds and roles to an account whose owner is a post-quantum key (for example an ML-DSA or Falcon key checked by a smart-contract account or by the chain itself), and stop reusing addresses whose public key is already exposed on chain.',
+ JWT: 'There is no final standard for post-quantum JWS/JWT yet: ML-DSA for JOSE and COSE was an IETF draft when this rule was written, and mainstream JWT libraries do not ship it by default. What you can do now: keep token lifetimes short, use HS256/HS512 where issuer and verifier can share a secret, inventory where verification keys live so they can be rotated, and plan a hybrid (classical + ML-DSA) signature once the specification is final and your library supports it.',
+ JWT_NONE: 'An unsigned JWT ("none") must never be accepted: remove it from the accepted algorithms.',
+ HASH_WEAK: 'Replace with SHA-256, SHA-384 or SHA3-256. If this is a non-security checksum (cache key, deduplication), document that; for passwords use a password hash (Argon2id, scrypt, bcrypt, or PBKDF2-HMAC-SHA-256 with a high iteration count).',
+ CIPHER_WEAK: 'Replace with AES-256-GCM or ChaCha20-Poly1305, with a unique nonce per key.',
+ ECB: 'ECB leaks plaintext patterns. Use an authenticated mode: AES-256-GCM (unique nonce per key) or ChaCha20-Poly1305.',
+ TLS_OLD: 'Disable SSL, TLS 1.0 and TLS 1.1; require at least TLS 1.2 and prefer TLS 1.3 with the hybrid group X25519MLKEM768.',
+ TLS12: 'TLS 1.2 has no standardized post-quantum key exchange. Prefer TLS 1.3 and offer X25519MLKEM768; keep TLS 1.2 only for clients that require it, and measure how many still negotiate it.',
+ TLS13: 'Check the negotiated key-exchange groups: offer X25519MLKEM768 first. Go 1.24+ does this by default when CurvePreferences is unset, and OpenSSL 3.5+ includes it in its default groups; older runtimes negotiate a classical group.',
+ AES128: 'Grover\'s algorithm gives at most a quadratic speed-up, which NIST treats as security category 1 for AES-128. Frameworks such as CNSA 2.0 require AES-256; prefer AES-256 for data that must stay confidential for decades.',
+ AES_SIZE: 'Key size is set at run time by the key length. AES-128 is NIST category 1 against a quantum attacker, AES-256 category 5; prefer AES-256 for long-lived data.',
+ PREPQ: 'Pre-standard variant: migrate to the final NIST standard (ML-KEM FIPS 203, ML-DSA FIPS 204, SLH-DSA FIPS 205, or FN-DSA for Falcon once published). Keys and outputs are not interoperable with the final versions.',
+ BROKEN_PQ: 'Broken by classical attacks in 2022. Remove it; use ML-KEM (FIPS 203) or ML-DSA (FIPS 204).',
+ UNKNOWN: 'Find where the value comes from (configuration, environment, caller) and review it there; this tool does not guess.',
+ LIB_MULTI: 'The library offers both classical and post-quantum-safe primitives; no specific call was recognized in this file. Review how it is used.',
+});
+
+// numele canonice ale curbelor
+const ALIAS_CURBE = [
+ [/^(p-?256|prime256v1|secp256r1|nist256p|curvep256|p256)$/i, 'secp256r1'],
+ [/^(p-?384|secp384r1|nist384p|curvep384|p384)$/i, 'secp384r1'],
+ [/^(p-?521|secp521r1|nist521p|curvep521|p521)$/i, 'secp521r1'],
+ [/^(p-?224|secp224r1|nist224p|p224)$/i, 'secp224r1'],
+ [/^(p-?192|prime192v1|secp192r1|nist192p|p192)$/i, 'secp192r1'],
+ [/^(secp256k1|k256|k-256)$/i, 'secp256k1'],
+ [/^brainpoolp256r1$/i, 'brainpoolP256r1'],
+ [/^brainpoolp384r1$/i, 'brainpoolP384r1'],
+ [/^brainpoolp512r1$/i, 'brainpoolP512r1'],
+ [/^(curve25519|x25519)$/i, 'Curve25519'],
+ [/^(ed25519|edwards25519)$/i, 'Edwards25519'],
+ [/^(curve448|x448)$/i, 'Curve448'],
+ [/^(ed448|edwards448)$/i, 'Edwards448'],
+];
+const NIVEL_CURBA = {
+ secp256r1: 128, secp384r1: 192, secp521r1: 256, secp224r1: 112, secp192r1: 96, secp256k1: 128,
+ brainpoolP256r1: 128, brainpoolP384r1: 192, brainpoolP512r1: 256,
+};
+
+export function curbaCanonica(s) {
+ if (!s) return null;
+ const t = String(s).trim();
+ for (const [re, nume] of ALIAS_CURBE) if (re.test(t)) return nume;
+ return t;
+}
+
+// hash-uri: nume canonic, clasa, nivel NIST (categoria de coliziune), OID
+const HASH = {
+ MD2: { n: 'MD2', c: CLS.W }, MD4: { n: 'MD4', c: CLS.W },
+ MD5: { n: 'MD5', c: CLS.W, oid: '1.2.840.113549.2.5' },
+ SHA1: { n: 'SHA-1', c: CLS.W, oid: '1.3.14.3.2.26' },
+ RIPEMD160: { n: 'RIPEMD-160', c: CLS.W },
+ SHA224: { n: 'SHA-224', c: CLS.S },
+ SHA256: { n: 'SHA-256', c: CLS.S, q: 2, cl: 128, oid: '2.16.840.1.101.3.4.2.1' },
+ SHA384: { n: 'SHA-384', c: CLS.S, q: 4, cl: 192, oid: '2.16.840.1.101.3.4.2.2' },
+ SHA512: { n: 'SHA-512', c: CLS.S, q: 4, cl: 256, oid: '2.16.840.1.101.3.4.2.3' },
+ 'SHA512/256': { n: 'SHA-512/256', c: CLS.S, q: 2 },
+ 'SHA512/224': { n: 'SHA-512/224', c: CLS.S },
+ 'SHA3-224': { n: 'SHA3-224', c: CLS.S },
+ 'SHA3-256': { n: 'SHA3-256', c: CLS.S, q: 2 },
+ 'SHA3-384': { n: 'SHA3-384', c: CLS.S, q: 4 },
+ 'SHA3-512': { n: 'SHA3-512', c: CLS.S, q: 4 },
+ SHAKE128: { n: 'SHAKE128', c: CLS.S, xof: true },
+ SHAKE256: { n: 'SHAKE256', c: CLS.S, xof: true },
+ BLAKE2B: { n: 'BLAKE2b', c: CLS.S }, BLAKE2S: { n: 'BLAKE2s', c: CLS.S },
+ BLAKE2B512: { n: 'BLAKE2b-512', c: CLS.S }, BLAKE2S256: { n: 'BLAKE2s-256', c: CLS.S },
+ SM3: { n: 'SM3', c: CLS.S },
+ KECCAK256: { n: 'Keccak-256', c: CLS.S },
+};
+
+export function hashCanonic(s) {
+ if (!s) return null;
+ let t = String(s).trim().toUpperCase().replace(/^RSA-/, '');
+ t = t.replace(/_/g, '-');
+ if (/^SHA-?1$|^SHA$/.test(t)) return 'SHA1';
+ if (/^SHA-?(224|256|384|512)$/.test(t)) return 'SHA' + t.replace(/\D/g, '');
+ if (/^SHA-?512[/-](224|256)$/.test(t)) return 'SHA512/' + t.slice(-3);
+ if (/^SHA3-?(224|256|384|512)$/.test(t)) return 'SHA3-' + t.slice(-3);
+ if (/^SHAKE-?(128|256)$/.test(t)) return 'SHAKE' + t.slice(-3);
+ if (/^MD[245]$/.test(t)) return t;
+ if (/^RIPEMD-?160$|^RMD160$/.test(t)) return 'RIPEMD160';
+ if (/^BLAKE2B-?512$/.test(t)) return 'BLAKE2B512';
+ if (/^BLAKE2S-?256$/.test(t)) return 'BLAKE2S256';
+ if (/^BLAKE2[BS]$/.test(t)) return t;
+ if (/^SM3$/.test(t)) return 'SM3';
+ if (/^KECCAK-?256$/.test(t)) return 'KECCAK256';
+ return null;
+}
+
+function rez(o) {
+ return {
+ classification: o.c,
+ quantumVulnerable: !!o.qv,
+ reason: o.motiv,
+ recommendation: o.rec || null,
+ nistQuantumSecurityLevel: o.q,
+ classicalSecurityLevel: o.cl,
+ oid: o.oid,
+ };
+}
+
+const SHOR = 'Shor\'s algorithm on a cryptographically relevant quantum computer recovers the private key from the public key';
+
+// intrarea: { grup, nume?, param?, curba?, hash?, mod?, primitiv?, context? }
+// iesirea: descrierea completa (nume canonic, primitiv, clasa, motiv, recomandare, niveluri)
+export function evalueaza(a) {
+ const g = a.grup;
+ const f = (x) => Object.assign({ grup: g, nume: a.nume, primitiv: a.primitiv || 'unknown', param: a.param, curba: a.curba, mod: a.mod, padding: a.padding }, x);
+
+ if (g === 'UNKNOWN') {
+ return f(rez({ c: CLS.U, motiv: a.motiv || 'Algorithm could not be determined statically.', rec: REC.UNKNOWN }));
+ }
+
+ if (g === 'RSA') {
+ const bits = a.param ? Number(a.param) : null;
+ const nume = a.nume || (bits ? `RSA-${bits}` : 'RSA');
+ const prim = a.primitiv || 'unknown';
+ const rec = prim === 'pke' ? REC.PKE : (a.context === 'jwt' ? REC.JWT : (prim === 'signature' ? REC.SIG : `${REC.SIG} If the key is used for encryption: ${REC.PKE}`));
+ const hashSlab = a.hash && HASH[a.hash] && HASH[a.hash].c === CLS.W;
+ if (bits && bits < 2048) {
+ return f({ nume, ...rez({ c: CLS.W, qv: true, motiv: `RSA with a ${bits}-bit modulus is below the 2048-bit minimum (NIST SP 800-131A); also quantum-vulnerable: ${SHOR}.`, rec, q: 0, oid: '1.2.840.113549.1.1.1' }) });
+ }
+ if (hashSlab) {
+ return f({ nume, ...rez({ c: CLS.W, qv: true, motiv: `RSA signature over ${HASH[a.hash].n}, which has practical collision attacks; also quantum-vulnerable: ${SHOR}.`, rec: `${REC.HASH_WEAK} ${rec}`, q: 0 }) });
+ }
+ const cl = bits === 2048 ? 112 : bits === 3072 ? 128 : bits === 7680 ? 192 : bits === 15360 ? 256 : undefined;
+ return f({ nume, ...rez({ c: CLS.V, qv: true, motiv: `RSA: ${SHOR}.`, rec, q: 0, cl, oid: '1.2.840.113549.1.1.1' }) });
+ }
+
+ if (g === 'DSA') {
+ const bits = a.param ? Number(a.param) : null;
+ const nume = a.nume || (bits ? `DSA-${bits}` : 'DSA');
+ if ((bits && bits < 2048) || (a.hash && HASH[a.hash] && HASH[a.hash].c === CLS.W)) {
+ return f({ nume, ...rez({ c: CLS.W, qv: true, motiv: `DSA with ${bits ? bits + '-bit parameters' : 'a weak hash'} is below current minimums; FIPS 186-5 no longer approves DSA for generating signatures; also ${SHOR}.`, rec: REC.SIG, q: 0 }) });
+ }
+ return f({ nume, ...rez({ c: CLS.V, qv: true, motiv: `DSA: ${SHOR} (discrete logarithm). FIPS 186-5 no longer approves DSA for generating signatures.`, rec: REC.SIG, q: 0 }) });
+ }
+
+ if (g === 'DH') {
+ const bits = a.param && /^\d+$/.test(a.param) ? Number(a.param) : null;
+ const nume = a.nume || (a.param ? `DH-${a.param}` : 'DH');
+ if (bits && bits < 2048) {
+ return f({ nume, ...rez({ c: CLS.W, qv: true, motiv: `Finite-field Diffie-Hellman with a ${bits}-bit group is below the 2048-bit minimum; also ${SHOR} (discrete logarithm).`, rec: REC.KEX, q: 0 }) });
+ }
+ return f({ nume, ...rez({ c: CLS.V, qv: true, motiv: `Finite-field Diffie-Hellman: ${SHOR} (discrete logarithm), so recorded key exchanges can be decrypted later.`, rec: REC.KEX, q: 0 }) });
+ }
+
+ if (g === 'EC' || g === 'ECDSA' || g === 'ECDH' || g === 'SECP256K1') {
+ const curba = curbaCanonica(a.curba) || (g === 'SECP256K1' ? 'secp256k1' : null);
+ const e256k1 = curba === 'secp256k1';
+ const eticheta = g === 'SECP256K1' ? 'ECDSA' : g;
+ const nume = a.nume || (curba ? `${eticheta}-${curba}` : eticheta);
+ const cl = curba ? NIVEL_CURBA[curba] : undefined;
+ const prim = a.primitiv || (g === 'ECDH' ? 'key-agree' : (g === 'EC' ? 'other' : 'signature'));
+ let rec = g === 'ECDH' ? REC.KEX : (g === 'EC' ? `Signatures: ${REC.SIG} Key agreement: ${REC.KEX}` : REC.SIG);
+ if (e256k1 && g !== 'ECDH') rec = REC.SECP256K1;
+ if (a.context === 'jwt') rec = REC.JWT;
+ const baza = { nume, primitiv: prim, curba: curba || undefined, grup: e256k1 ? 'SECP256K1' : g };
+ if (cl !== undefined && cl < 112) {
+ return f({ ...baza, ...rez({ c: CLS.W, qv: true, motiv: `Elliptic curve ${curba} gives about ${cl}-bit classical security, below the 112-bit minimum; also ${SHOR} (elliptic-curve discrete logarithm).`, rec, q: 0, cl }) });
+ }
+ if (a.hash && HASH[a.hash] && HASH[a.hash].c === CLS.W) {
+ return f({ ...baza, ...rez({ c: CLS.W, qv: true, motiv: `ECDSA over ${HASH[a.hash].n}, which has practical collision attacks; also ${SHOR}.`, rec: `${REC.HASH_WEAK} ${rec}`, q: 0 }) });
+ }
+ const pe = curba ? ` on ${curba}` : '';
+ const motiv = e256k1
+ ? `ECDSA on secp256k1: ${SHOR} (elliptic-curve discrete logarithm). On public blockchains the public key is visible on chain after the first signed transaction.`
+ : `${eticheta}${pe}: ${SHOR} (elliptic-curve discrete logarithm).`;
+ return f({ ...baza, ...rez({ c: CLS.V, qv: true, motiv, rec, q: 0, cl, oid: '1.2.840.10045.2.1' }) });
+ }
+
+ if (g === 'EDDSA') {
+ const nume = a.nume || 'Ed25519';
+ const e448 = /448/.test(nume);
+ const rec = a.context === 'jwt' ? REC.JWT : REC.SIG;
+ return f({ nume, primitiv: 'signature', curba: e448 ? 'Edwards448' : 'Edwards25519', ...rez({ c: CLS.V, qv: true, motiv: `${nume}: ${SHOR} (elliptic-curve discrete logarithm).`, rec, q: 0, cl: e448 ? 224 : 128, oid: e448 ? '1.3.101.113' : '1.3.101.112' }) });
+ }
+
+ if (g === 'XDH') {
+ const nume = a.nume || 'X25519';
+ const e448 = /448/.test(nume);
+ return f({ nume, primitiv: 'key-agree', curba: e448 ? 'Curve448' : 'Curve25519', ...rez({ c: CLS.V, qv: true, motiv: `${nume} key agreement: ${SHOR} (elliptic-curve discrete logarithm), so recorded key exchanges can be decrypted later.`, rec: REC.KEX, q: 0, cl: e448 ? 224 : 128, oid: e448 ? '1.3.101.111' : '1.3.101.110' }) });
+ }
+
+ if (g === 'PAIRING') {
+ return f({ nume: a.nume || 'BLS12-381', primitiv: a.primitiv || 'signature', ...rez({ c: CLS.V, qv: true, motiv: `${a.nume || 'Pairing-based cryptography'}: ${SHOR} (discrete logarithm in the pairing groups).`, rec: REC.SIG, q: 0 }) });
+ }
+
+ if (g === 'CLASSIC-SIG') {
+ // semnatura clasica al carei tip de cheie nu se vede (RSA, DSA sau ECDSA)
+ const h = a.hash && HASH[a.hash];
+ const nume = a.nume || `signature-with-${h ? h.n : 'unknown-hash'}`;
+ if (h && h.c === CLS.W) {
+ return f({ nume, primitiv: 'signature', ...rez({ c: CLS.W, qv: true, motiv: `${a.motiv || 'Classical signature'} over ${h.n}, which has practical collision attacks; the key type (RSA, DSA or ECDSA) is also quantum-vulnerable.`, rec: `${REC.HASH_WEAK} ${REC.SIG}`, q: 0 }) });
+ }
+ return f({ nume, primitiv: 'signature', ...rez({ c: CLS.V, qv: true, motiv: `${a.motiv || 'Classical signature (RSA, DSA or ECDSA key)'}: ${SHOR}.`, rec: REC.SIG, q: 0 }) });
+ }
+
+ if (g === 'HASH') {
+ const h = HASH[a.hash];
+ if (!h) return f({ nume: a.nume || String(a.hash), primitiv: 'hash', ...rez({ c: CLS.U, motiv: `Hash "${a.hash}" is not in this tool's catalog.`, rec: REC.UNKNOWN }) });
+ const nota = a.nota ? ` ${a.nota}` : '';
+ if (h.c === CLS.W) {
+ const motiv = a.hash === 'RIPEMD160'
+ ? `RIPEMD-160 has a 160-bit output (about 80-bit collision resistance), below the 112-bit minimum for new designs.${nota}`
+ : `${h.n} has practical collision attacks; unsafe for signatures, certificates and integrity against an adversary.${nota}`;
+ return f({ nume: h.n, primitiv: 'hash', ...rez({ c: CLS.W, motiv, rec: REC.HASH_WEAK, q: 0, oid: h.oid }) });
+ }
+ return f({ nume: h.n, primitiv: h.xof ? 'xof' : 'hash', ...rez({ c: CLS.S, motiv: `${h.n}: quantum attacks give at most a polynomial speed-up for collisions and preimages (Grover, BHT); output size keeps it within NIST categories.${nota}`, q: h.q, cl: h.cl, oid: h.oid }) });
+ }
+
+ if (g === 'MAC') {
+ if (!a.hash) return f({ nume: 'HMAC', primitiv: 'mac', ...rez({ c: CLS.U, motiv: 'HMAC whose hash function is bound to the key object (set where the key is imported or generated), not visible at this call.', rec: REC.UNKNOWN }) });
+ const h = HASH[a.hash];
+ const nume = `HMAC-${h ? h.n : String(a.hash || 'unknown')}`;
+ if (!h) return f({ nume, primitiv: 'mac', ...rez({ c: CLS.U, motiv: `HMAC hash "${a.hash}" is not in this tool's catalog.`, rec: REC.UNKNOWN }) });
+ if (h.c === CLS.W) {
+ return f({ nume, primitiv: 'mac', ...rez({ c: CLS.W, motiv: `${nume}: HMAC does not depend on collision resistance and has no practical break, but ${h.n} is deprecated for new designs and should be retired.`, rec: 'Move to HMAC-SHA-256 or HMAC-SHA-384.', q: 0 }) });
+ }
+ return f({ nume, primitiv: 'mac', ...rez({ c: CLS.S, motiv: `${nume}: symmetric; a quantum attacker gains at most a quadratic speed-up (Grover) on key search.` }) });
+ }
+
+ if (g === 'KDF') {
+ const h = a.hash ? HASH[a.hash] : null;
+ const nume = a.nume || 'KDF';
+ if (h && h.c === CLS.W) {
+ return f({ nume, primitiv: 'kdf', ...rez({ c: CLS.W, motiv: `${nume} uses ${h.n}; not practically broken as a KDF, but deprecated for new designs.`, rec: 'Use PBKDF2-HMAC-SHA-256 (high iteration count), scrypt or Argon2id.', q: 0 }) });
+ }
+ return f({ nume, primitiv: 'kdf', ...rez({ c: CLS.S, motiv: `${nume}: symmetric key derivation; a quantum attacker gains at most a quadratic speed-up (Grover).` }) });
+ }
+
+ if (g === 'CIPHER') return cifru(a, f);
+
+ if (g === 'MLKEM') {
+ const p = String(a.param || '');
+ const q = p === '512' ? 1 : p === '768' ? 3 : p === '1024' ? 5 : undefined;
+ const oid = p === '512' ? '2.16.840.1.101.3.4.4.1' : p === '768' ? '2.16.840.1.101.3.4.4.2' : p === '1024' ? '2.16.840.1.101.3.4.4.3' : undefined;
+ const nume = p ? `ML-KEM-${p}` : 'ML-KEM';
+ return f({ nume, primitiv: 'kem', ...rez({ c: CLS.S, motiv: `${nume}: module-lattice KEM standardized in FIPS 203${q ? `, NIST category ${q}` : ' (parameter set not visible)'}.`, q, oid }) });
+ }
+ if (g === 'MLDSA') {
+ const p = String(a.param || '');
+ const q = p === '44' ? 2 : p === '65' ? 3 : p === '87' ? 5 : undefined;
+ const oid = p === '44' ? '2.16.840.1.101.3.4.3.17' : p === '65' ? '2.16.840.1.101.3.4.3.18' : p === '87' ? '2.16.840.1.101.3.4.3.19' : undefined;
+ const nume = p ? `ML-DSA-${p}` : 'ML-DSA';
+ return f({ nume, primitiv: 'signature', ...rez({ c: CLS.S, motiv: `${nume}: module-lattice signature standardized in FIPS 204${q ? `, NIST category ${q}` : ' (parameter set not visible)'}.`, q, oid }) });
+ }
+ if (g === 'SLHDSA') {
+ const p = String(a.param || '');
+ const m = /(128|192|256)/.exec(p);
+ const q = m ? ({ 128: 1, 192: 3, 256: 5 })[m[1]] : undefined;
+ const nume = p ? `SLH-DSA-${p}` : 'SLH-DSA';
+ return f({ nume, primitiv: 'signature', ...rez({ c: CLS.S, motiv: `${nume}: stateless hash-based signature standardized in FIPS 205${q ? `, NIST category ${q}` : ''}.`, q }) });
+ }
+ if (g === 'FALCON') {
+ const p = String(a.param || '');
+ const q = p === '512' ? 1 : p === '1024' ? 5 : undefined;
+ const nume = p ? `Falcon-${p}` : 'Falcon';
+ return f({ nume, primitiv: 'signature', ...rez({ c: CLS.S, motiv: `${nume}: NTRU-lattice signature selected by NIST (to be standardized as FN-DSA)${q ? `, NIST category ${q}` : ''}.`, q }) });
+ }
+ if (g === 'HASHSIG') {
+ return f({ nume: a.nume || 'XMSS/LMS', primitiv: 'signature', ...rez({ c: CLS.S, motiv: `${a.nume || 'Stateful hash-based signature'}: approved in NIST SP 800-208; security depends on never reusing a one-time key state.` }) });
+ }
+ if (g === 'PREPQ') {
+ return f({ nume: a.nume, primitiv: a.primitiv, ...rez({ c: CLS.S, motiv: `${a.nume}: pre-standard version of a NIST-selected post-quantum scheme.`, rec: REC.PREPQ }) });
+ }
+ if (g === 'BROKENPQ') {
+ return f({ nume: a.nume, primitiv: a.primitiv, ...rez({ c: CLS.W, motiv: `${a.nume} was broken by classical attacks in 2022.`, rec: REC.BROKEN_PQ, q: 0 }) });
+ }
+ if (g === 'HQC') {
+ return f({ nume: a.nume || 'HQC', primitiv: 'kem', ...rez({ c: CLS.S, motiv: 'HQC: code-based KEM selected by NIST in 2025 as a second KEM; final standard pending.' }) });
+ }
+ if (g === 'HYBRID-KEX') {
+ const pq = /1024/.test(a.nume) ? 5 : 3;
+ return f({ nume: a.nume, primitiv: 'kem', ...rez({ c: CLS.S, motiv: `${a.nume}: hybrid key exchange (classical ECDH + ML-KEM); stays secure if either component holds. The NIST category given is that of the ML-KEM component.`, q: pq }) });
+ }
+ if (g === 'TLS') return tls(a, f);
+ if (g === 'JWT-HMAC') {
+ const h = HASH[a.hash];
+ return f({ nume: a.nume, primitiv: 'mac', ...rez({ c: CLS.S, motiv: `${a.nume} is HMAC-${h ? h.n : a.hash}: symmetric, so a quantum attacker gains at most a quadratic speed-up (Grover); the shared secret must be long and random.` }) });
+ }
+ if (g === 'JWT-NONE') {
+ return f({ nume: 'JWS none', primitiv: 'signature', ...rez({ c: CLS.W, motiv: 'The "none" algorithm means the token is not signed at all.', rec: REC.JWT_NONE, q: 0 }) });
+ }
+ if (g === 'LIB-MULTI') {
+ return f({ nume: a.nume, primitiv: 'unknown', ...rez({ c: CLS.U, motiv: a.motiv || `Imports ${a.nume}; no specific algorithm call recognized in this file.`, rec: REC.LIB_MULTI }) });
+ }
+ if (g === 'SSH') {
+ return f({ nume: a.nume || 'SSH', primitiv: 'unknown', ...rez({ c: CLS.U, motiv: a.motiv || 'SSH: key exchange and host-key algorithms are negotiated at run time and not visible here.', rec: 'Check the configured key-exchange algorithms: OpenSSH 9.9+ offers mlkem768x25519-sha256 and 9.0+ sntrup761x25519-sha512; host keys remain classical (Ed25519, ECDSA, RSA).' }) });
+ }
+ return f(rez({ c: CLS.U, motiv: `No classification rule for group "${g}".`, rec: REC.UNKNOWN }));
+}
+
+function cifru(a, f) {
+ const alg = String(a.nume || '').toUpperCase();
+ const mod = a.mod ? String(a.mod).toLowerCase() : undefined;
+ if (/^(DES|DES-?CBC|DES-?ECB|DES-?CFB|DES-?OFB)$/.test(alg) || alg === 'DES') {
+ return f({ nume: mod ? `DES-${mod.toUpperCase()}` : 'DES', primitiv: 'block-cipher', mod, ...rez({ c: CLS.W, motiv: 'DES has a 56-bit key and is brute-forceable today.', rec: REC.CIPHER_WEAK, q: 0 }) });
+ }
+ if (/^(3DES|DESEDE|DES-?EDE3?|TRIPLEDES|TDEA|DES3)$/.test(alg)) {
+ return f({ nume: mod ? `3DES-${mod.toUpperCase()}` : '3DES', primitiv: 'block-cipher', mod, ...rez({ c: CLS.W, motiv: '3DES (TDEA) has a 64-bit block (Sweet32 birthday attacks) and was disallowed by NIST after 2023.', rec: REC.CIPHER_WEAK, q: 0 }) });
+ }
+ if (/^(RC4|ARC4|ARCFOUR)$/.test(alg)) {
+ return f({ nume: 'RC4', primitiv: 'stream-cipher', ...rez({ c: CLS.W, motiv: 'RC4 has exploitable keystream biases and is prohibited in TLS (RFC 7465).', rec: REC.CIPHER_WEAK, q: 0 }) });
+ }
+ if (/^(RC2|ARC2)$/.test(alg)) {
+ return f({ nume: 'RC2', primitiv: 'block-cipher', ...rez({ c: CLS.W, motiv: 'RC2 is an obsolete cipher with a 64-bit block.', rec: REC.CIPHER_WEAK, q: 0 }) });
+ }
+ if (/^(BLOWFISH|BF)$/.test(alg)) {
+ return f({ nume: 'Blowfish', primitiv: 'block-cipher', mod, ...rez({ c: CLS.W, motiv: 'Blowfish has a 64-bit block (Sweet32 birthday attacks).', rec: REC.CIPHER_WEAK, q: 0 }) });
+ }
+ if (/^(CAST5|CAST|IDEA|SEED)$/.test(alg)) {
+ return f({ nume: alg, primitiv: 'block-cipher', mod, ...rez({ c: CLS.W, motiv: `${alg} has a 64-bit block (Sweet32 birthday attacks) or is obsolete.`, rec: REC.CIPHER_WEAK, q: 0 }) });
+ }
+ if (/^CHACHA20(-POLY1305)?$|^XCHACHA20-POLY1305$|^XSALSA20-POLY1305$|^SALSA20$/.test(alg)) {
+ const ae = /POLY1305/.test(alg);
+ return f({ nume: a.nume, primitiv: ae ? 'ae' : 'stream-cipher', ...rez({ c: CLS.S, motiv: `${a.nume}: 256-bit key; a quantum attacker gains at most a quadratic speed-up (Grover) on key search.` }) });
+ }
+ if (/^AES/.test(alg) || /^CAMELLIA/.test(alg) || /^SM4/.test(alg) || /^ARIA/.test(alg)) {
+ const familie = /^CAMELLIA/.test(alg) ? 'Camellia' : /^SM4/.test(alg) ? 'SM4' : /^ARIA/.test(alg) ? 'ARIA' : 'AES';
+ const bits = a.param ? Number(a.param) : (familie === 'SM4' ? 128 : null);
+ const numeMod = mod ? `-${mod.toUpperCase()}` : '';
+ const nume = `${familie}${bits ? '-' + bits : ''}${numeMod}`;
+ const ae = mod && /^(gcm|ccm|ocb|siv|gcm-siv|eax|poly1305)$/.test(mod);
+ const prim = ae ? 'ae' : 'block-cipher';
+ const modCdx = mod && ['cbc', 'ecb', 'ccm', 'gcm', 'cfb', 'ofb', 'ctr'].includes(mod) ? mod : (mod ? 'other' : undefined);
+ if (mod === 'ecb') {
+ return f({ nume, primitiv: prim, mod: modCdx, ...rez({ c: CLS.W, motiv: `${nume}: ECB mode encrypts equal blocks to equal ciphertext and leaks plaintext structure.${a.nota ? ' ' + a.nota : ''}`, rec: REC.ECB, q: 0 }) });
+ }
+ const q = bits === 128 ? 1 : bits === 192 ? 3 : bits === 256 ? 5 : undefined;
+ const cl = bits || undefined;
+ const rec = bits === 128 ? REC.AES128 : (bits ? null : REC.AES_SIZE);
+ const motiv = bits === 128
+ ? `${nume}: symmetric; Grover's algorithm reduces key search at most quadratically, NIST category 1.`
+ : bits ? `${nume}: symmetric ${bits}-bit key; Grover's algorithm reduces key search at most quadratically, NIST category ${q}.`
+ : `${nume}: symmetric; key size not visible statically (AES-128 is NIST category 1, AES-256 category 5).`;
+ return f({ nume, primitiv: prim, mod: modCdx, param: bits ? String(bits) : undefined, ...rez({ c: CLS.S, motiv, rec, q, cl }) });
+ }
+ return f({ nume: a.nume, primitiv: 'unknown', ...rez({ c: CLS.U, motiv: `Cipher "${a.nume}" is not in this tool's catalog.`, rec: REC.UNKNOWN }) });
+}
+
+// a.param = versiunea ('1.0','1.1','1.2','1.3','ssl3','negotiated'); a.rol = 'min'|'max'|'only'
+function tls(a, f) {
+ const v = String(a.param || '');
+ const rol = a.rol || 'only';
+ const numeV = v === 'ssl3' ? 'SSLv3' : v === 'ssl2' ? 'SSLv2' : (v === 'negotiated' ? 'TLS (negotiated)' : `TLSv${v}`);
+ const nume = a.nume || numeV;
+ const baza = { nume, primitiv: 'other', protocolType: 'tls', protocolVersion: /^1\.[0-3]$/.test(v) ? v : (v === 'ssl3' ? '3.0' : undefined), rol };
+ if (v === 'ssl2' || v === 'ssl3' || v === '1.0' || v === '1.1') {
+ if (rol === 'max') {
+ return f({ ...baza, ...rez({ c: CLS.W, qv: true, motiv: `${numeV} as the maximum version caps the connection at a deprecated protocol (RFC 8996).`, rec: REC.TLS_OLD, q: 0 }) });
+ }
+ return f({ ...baza, ...rez({ c: CLS.W, qv: true, motiv: `${numeV} is deprecated (RFC 8996) and ${rol === 'min' ? 'allowed as the minimum version' : 'selected'} here.`, rec: REC.TLS_OLD, q: 0 }) });
+ }
+ if (v === '1.2') {
+ if (rol === 'min') {
+ return f({ ...baza, ...rez({ c: CLS.V, qv: true, motiv: 'TLS 1.2 is allowed; TLS 1.2 has no standardized post-quantum key exchange, so a peer that negotiates it uses classical (EC)DHE or RSA key exchange.', rec: REC.TLS12, q: 0 }) });
+ }
+ return f({ ...baza, ...rez({ c: CLS.V, qv: true, motiv: `TLS 1.2 ${rol === 'max' ? 'is the maximum version' : 'is selected'}; TLS 1.2 has no standardized post-quantum key exchange, so key exchange is classical (EC)DHE or RSA.`, rec: REC.TLS12, q: 0 }) });
+ }
+ if (v === '1.3') {
+ if (rol === 'max') {
+ return f({ ...baza, ...rez({ c: CLS.U, motiv: 'TLS 1.3 is the maximum version; the minimum and the key-exchange groups are not visible here.', rec: REC.TLS13 }) });
+ }
+ return f({ ...baza, ...rez({ c: CLS.U, motiv: 'TLS 1.3 only: whether key exchange is post-quantum depends on the negotiated group (X25519MLKEM768 or a classical group), which depends on runtime defaults and the peer.', rec: REC.TLS13 }) });
+ }
+ return f({ ...baza, ...rez({ c: CLS.U, motiv: 'Protocol version is negotiated at run time from the runtime defaults; not visible statically.', rec: REC.TLS13 }) });
+}
+
+// grupurile de schimb de chei TLS (ecdhCurve, CurvePreferences, jdk.tls.namedGroups)
+export function grupTls(nume) {
+ const t = String(nume).trim();
+ if (/^(X25519MLKEM768|X25519Kyber768Draft00|SecP256r1MLKEM768|SecP384r1MLKEM1024|p256_mlkem768|p384_mlkem1024|x25519_mlkem768)$/i.test(t)) {
+ if (/kyber/i.test(t)) return { grup: 'PREPQ', nume: t, primitiv: 'kem' };
+ return { grup: 'HYBRID-KEX', nume: t };
+ }
+ if (/^(X25519|x25519)$/.test(t)) return { grup: 'XDH', nume: 'X25519' };
+ if (/^(X448|x448)$/.test(t)) return { grup: 'XDH', nume: 'X448' };
+ if (/^(MLKEM(512|768|1024)|mlkem(512|768|1024))$/i.test(t)) return { grup: 'MLKEM', param: t.replace(/\D/g, '') };
+ if (/^(ffdhe\d+)$/i.test(t)) return { grup: 'DH', param: t.toLowerCase() };
+ const c = curbaCanonica(t);
+ if (c && /^secp|^brainpool/.test(c)) return { grup: 'ECDH', curba: c };
+ if (/^auto$/i.test(t)) return { grup: 'UNKNOWN', motiv: 'ecdhCurve "auto" selects groups from the runtime defaults; not visible statically.' };
+ return { grup: 'UNKNOWN', motiv: `Key-exchange group "${t}" is not in this tool's catalog.` };
+}
+
+// algoritmii JWS (RFC 7518 + RFC 8037 + RFC 8812)
+export function jws(alg) {
+ const m = /^(HS|RS|PS|ES)(256|384|512)$/.exec(alg);
+ if (m) {
+ const hash = 'SHA' + m[2];
+ if (m[1] === 'HS') return { grup: 'JWT-HMAC', nume: `JWS ${alg}`, hash, context: 'jwt' };
+ if (m[1] === 'RS') return { grup: 'RSA', nume: `JWS ${alg}`, primitiv: 'signature', padding: 'pkcs1v15', hash, context: 'jwt' };
+ if (m[1] === 'PS') return { grup: 'RSA', nume: `JWS ${alg}`, primitiv: 'signature', padding: 'other', hash, context: 'jwt' };
+ const curba = m[2] === '256' ? 'secp256r1' : m[2] === '384' ? 'secp384r1' : 'secp521r1';
+ return { grup: 'ECDSA', nume: `JWS ${alg}`, curba, hash, context: 'jwt' };
+ }
+ if (alg === 'ES256K') return { grup: 'ECDSA', nume: 'JWS ES256K', curba: 'secp256k1', context: 'jwt' };
+ if (alg === 'EdDSA' || alg === 'Ed25519') return { grup: 'EDDSA', nume: `JWS ${alg}`, context: 'jwt' };
+ if (alg === 'none') return { grup: 'JWT-NONE' };
+ return null;
+}
+export const JWS_RE = /^(?:(?:HS|RS|PS|ES)(?:256|384|512)|ES256K|EdDSA|Ed25519|none)$/;
diff --git a/crypto-inventory/lib/cbom.mjs b/crypto-inventory/lib/cbom.mjs
new file mode 100644
index 0000000..54758c5
--- /dev/null
+++ b/crypto-inventory/lib/cbom.mjs
@@ -0,0 +1,158 @@
+// Constructia CBOM-ului CycloneDX 1.6 din rezultatul scanarii.
+import { createHash, randomUUID } from 'node:crypto';
+
+export const ENUM = Object.freeze({
+ componentType: ['application', 'framework', 'library', 'container', 'platform', 'operating-system', 'device', 'device-driver', 'firmware', 'file', 'machine-learning-model', 'data', 'cryptographic-asset'],
+ assetType: ['algorithm', 'certificate', 'protocol', 'related-crypto-material'],
+ primitive: ['drbg', 'mac', 'block-cipher', 'stream-cipher', 'signature', 'hash', 'pke', 'xof', 'kdf', 'key-agree', 'kem', 'ae', 'combiner', 'other', 'unknown'],
+ mode: ['cbc', 'ecb', 'ccm', 'gcm', 'cfb', 'ofb', 'ctr', 'other', 'unknown'],
+ padding: ['pkcs5', 'pkcs7', 'pkcs1v15', 'oaep', 'raw', 'other', 'unknown'],
+ cryptoFunctions: ['generate', 'keygen', 'encrypt', 'decrypt', 'digest', 'tag', 'keyderive', 'sign', 'verify', 'encapsulate', 'decapsulate', 'other', 'unknown'],
+ protocolType: ['tls', 'ssh', 'ipsec', 'ike', 'sstp', 'wpa', 'other', 'unknown'],
+ materialType: ['private-key', 'public-key', 'secret-key', 'key', 'ciphertext', 'signature', 'digest', 'initialization-vector', 'nonce', 'seed', 'salt', 'shared-secret', 'tag', 'additional-data', 'password', 'credential', 'token', 'other', 'unknown'],
+});
+
+const P = 'aere:crypto-inventory:';
+
+function slug(s) {
+ return String(s).toLowerCase().replace(/[^a-z0-9.+-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'x';
+}
+
+function cheieComponenta(g) {
+ return [g.assetType, g.name, g.primitive, g.classification, g.parameterSetIdentifier, g.curve, g.mode, g.padding,
+ g.protocolType, g.protocolVersion, g.material && g.material.type, g.certificate && g.certificate.fingerprintSha256, g.reason].map((x) => x ?? '').join('|');
+}
+
+function uuidDin(hex) {
+ const h = hex.slice(0, 32).split('');
+ h[12] = '5';
+ h[16] = '89ab'[parseInt(h[16], 16) & 3];
+ const s = h.join('');
+ return `${s.slice(0, 8)}-${s.slice(8, 12)}-${s.slice(12, 16)}-${s.slice(16, 20)}-${s.slice(20, 32)}`;
+}
+
+export function construiesteCbom(rez, optiuni = {}) {
+ const grupuri = new Map();
+ for (const g of rez.findings) {
+ const k = cheieComponenta(g);
+ if (!grupuri.has(k)) grupuri.set(k, []);
+ grupuri.get(k).push(g);
+ }
+ const componente = [];
+ for (const [k, lista] of grupuri) {
+ const g = lista[0];
+ const ref = `crypto:${g.assetType}:${slug(g.certificate ? 'x509-' + (g.certificate.subjectName || 'certificate') : g.name)}:${createHash('sha256').update(k).digest('hex').slice(0, 12)}`;
+ const cp = { assetType: g.assetType };
+ if (g.assetType === 'algorithm') {
+ const ap = { primitive: ENUM.primitive.includes(g.primitive) ? g.primitive : 'unknown' };
+ if (g.parameterSetIdentifier) ap.parameterSetIdentifier = g.parameterSetIdentifier;
+ if (g.curve) ap.curve = g.curve;
+ if (g.mode) ap.mode = ENUM.mode.includes(g.mode) ? g.mode : 'other';
+ if (g.padding) ap.padding = ENUM.padding.includes(g.padding) ? g.padding : 'other';
+ const f = [...new Set(lista.flatMap((x) => x.cryptoFunctions || []))].filter((x) => ENUM.cryptoFunctions.includes(x)).sort();
+ if (f.length) ap.cryptoFunctions = f;
+ if (Number.isInteger(g.classicalSecurityLevel)) ap.classicalSecurityLevel = g.classicalSecurityLevel;
+ if (Number.isInteger(g.nistQuantumSecurityLevel)) ap.nistQuantumSecurityLevel = g.nistQuantumSecurityLevel;
+ cp.algorithmProperties = ap;
+ } else if (g.assetType === 'protocol') {
+ cp.protocolProperties = { type: ENUM.protocolType.includes(g.protocolType) ? g.protocolType : 'unknown' };
+ if (g.protocolVersion) cp.protocolProperties.version = g.protocolVersion;
+ } else if (g.assetType === 'related-crypto-material') {
+ const m = { type: ENUM.materialType.includes(g.material && g.material.type) ? g.material.type : 'unknown' };
+ if (g.material && g.material.format) m.format = g.material.format;
+ if (g.parameterSetIdentifier && /^\d+$/.test(g.parameterSetIdentifier)) m.size = Number(g.parameterSetIdentifier);
+ cp.relatedCryptoMaterialProperties = m;
+ } else if (g.assetType === 'certificate') {
+ const c = g.certificate || {};
+ const cert = {};
+ for (const camp of ['subjectName', 'issuerName', 'notValidBefore', 'notValidAfter', 'certificateFormat', 'certificateExtension']) if (c[camp]) cert[camp] = c[camp];
+ cp.certificateProperties = cert;
+ }
+ if (g.oid) cp.oid = g.oid;
+ const props = [
+ { name: `${P}classification`, value: g.classification },
+ { name: `${P}quantum-vulnerable`, value: String(!!g.quantumVulnerable) },
+ { name: `${P}reason`, value: g.reason },
+ ];
+ if (g.recommendation) props.push({ name: `${P}recommendation`, value: g.recommendation });
+ if (g.assetType !== 'algorithm') props.push({ name: `${P}key-algorithm`, value: g.name });
+ if (g.certificate && g.certificate.fingerprintSha256) props.push({ name: `${P}certificate-sha256-fingerprint`, value: g.certificate.fingerprintSha256 });
+ props.push({ name: `${P}languages`, value: [...new Set(lista.map((x) => x.language))].sort().join(',') });
+ props.push({ name: `${P}evidence-kinds`, value: [...new Set(lista.map((x) => x.evidenceKind))].sort().join(',') });
+ const rezolvate = [...new Set(lista.filter((x) => x.resolvedFrom).map((x) => `${x.file}:${x.line} <- ${x.resolvedFrom}`))];
+ if (rezolvate.length) props.push({ name: `${P}resolved-from`, value: rezolvate.join('; ') });
+ const nume = g.assetType === 'certificate' ? `X.509 certificate${g.certificate && g.certificate.subjectName ? ' ' + g.certificate.subjectName : ''}`
+ : g.assetType === 'related-crypto-material' ? `${g.name} ${g.material ? g.material.type : 'key material'}` : g.name;
+ componente.push({
+ type: 'cryptographic-asset',
+ 'bom-ref': ref,
+ name: nume,
+ cryptoProperties: cp,
+ evidence: {
+ occurrences: lista.map((x) => {
+ const oc = { location: x.file, line: x.line, symbol: x.api };
+ if (x.context) oc.additionalContext = x.context;
+ return oc;
+ }),
+ },
+ properties: props,
+ });
+ }
+ // bibliotecile declarate, unite pe (ecosistem, nume, versiune)
+ const libs = new Map();
+ for (const b of rez.libraries) {
+ const k = `${b.ecosystem}|${b.name}|${b.version || ''}`;
+ if (!libs.has(k)) libs.set(k, []);
+ libs.get(k).push(b);
+ }
+ for (const [k, lista] of libs) {
+ const b = lista[0];
+ const c = {
+ type: 'library',
+ 'bom-ref': `library:${b.ecosystem}:${slug(b.name)}:${createHash('sha256').update(k).digest('hex').slice(0, 12)}`,
+ name: b.name,
+ };
+ if (b.version) c.version = b.version;
+ if (b.purl) c.purl = b.purl;
+ c.evidence = { occurrences: lista.map((x) => ({ location: x.file, line: x.line })) };
+ c.properties = [
+ { name: `${P}declared-only`, value: 'true' },
+ { name: `${P}note`, value: 'Declared in a dependency manifest. Declaration is not use: see the cryptographic-asset components for code that calls it.' },
+ { name: `${P}provides`, value: b.provides },
+ ];
+ componente.push(c);
+ }
+ componente.sort((a, b) => (a['bom-ref'] < b['bom-ref'] ? -1 : a['bom-ref'] > b['bom-ref'] ? 1 : 0));
+
+ const s = rez.stats;
+ const metaProps = [
+ ['files-seen', s.filesSeen], ['code-files-analyzed', s.codeFilesTotal], ['text-files-pem-only', s.textFilesPemOnly],
+ ['not-read-binary', s.notRead.binary], ['not-read-too-large', s.notRead.tooLarge], ['not-read-file-limit', s.notRead.fileLimit],
+ ['not-read-unreadable', s.notRead.unreadable], ['symlinks-not-followed', s.symlinksNotFollowed],
+ ['files-over-resolution-limit', s.resolutionLimitFiles], ['max-resolved-variables-per-file', rez.options.maxResolvedVariablesPerFile],
+ ['dirs-excluded', Object.entries(s.dirsExcluded).map(([n, c]) => `${n}:${c}`).join(',') || 'none'],
+ ['max-file-bytes', rez.options.maxFileBytes], ['max-files', rez.options.maxFiles],
+ ['method', 'static pattern analysis of source text; no code from the scanned tree is executed'],
+ ].map(([n, v]) => ({ name: `${P}${n}`, value: String(v) }));
+
+ const bom = {
+ $schema: 'http://cyclonedx.org/schema/bom-1.6.schema.json',
+ bomFormat: 'CycloneDX',
+ specVersion: '1.6',
+ serialNumber: '',
+ version: 1,
+ metadata: {
+ tools: { components: [{ type: 'application', name: rez.tool.name, version: rez.tool.version, description: 'Static cryptographic inventory of source code (CBOM)' }] },
+ component: { type: 'application', 'bom-ref': 'scanned-target', name: rez.rootName },
+ properties: metaProps,
+ },
+ components: componente,
+ };
+ if (optiuni.deterministic) {
+ bom.serialNumber = `urn:uuid:${uuidDin(createHash('sha256').update(JSON.stringify(componente)).digest('hex'))}`;
+ } else {
+ bom.metadata = { timestamp: rez.finishedAt, ...bom.metadata };
+ bom.serialNumber = `urn:uuid:${randomUUID()}`;
+ }
+ return bom;
+}
diff --git a/crypto-inventory/lib/context.mjs b/crypto-inventory/lib/context.mjs
new file mode 100644
index 0000000..9c6a98b
--- /dev/null
+++ b/crypto-inventory/lib/context.mjs
@@ -0,0 +1,253 @@
+// Contextul unui fisier scanat si uneltele comune ale detectorilor.
+import { curata, inSir, inceputuriDeRand, randul } from './lexer.mjs';
+import { evalueaza } from './catalog.mjs';
+
+export function escapeRe(s) {
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+}
+
+export function facContext(text, limbaj, rel) {
+ const { code, siruri } = curata(text, limbaj);
+ const inceputuri = inceputuriDeRand(text);
+ const ctx = {
+ text, code, siruri, limbaj, rel,
+ gasiri: [],
+ inSir: (pos) => inSir(siruri, pos),
+ poz: (pos) => randul(inceputuri, pos),
+ // potriviri in cod: numele API-ului (inceputul potrivirii) nu are voie sa fie intr-un sir
+ potriviri(re) {
+ const r = [];
+ const g = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
+ for (const m of code.matchAll(g)) if (!inSir(siruri, m.index)) r.push(m);
+ return r;
+ },
+ adauga(pos, api, fel, activ, extra = {}) {
+ const ev = evalueaza(activ);
+ const { line, column } = randul(inceputuri, pos);
+ const bucata = extra.bucata !== undefined ? extra.bucata : '';
+ ctx.gasiri.push({
+ file: rel,
+ line,
+ column,
+ language: limbaj,
+ api,
+ evidenceKind: fel,
+ assetType: activ.assetType || (ev.protocolType ? 'protocol' : 'algorithm'),
+ group: ev.grup,
+ name: ev.nume || activ.nume || api,
+ primitive: ev.primitiv || 'unknown',
+ parameterSetIdentifier: ev.param !== undefined && ev.param !== null && ev.param !== '' ? String(ev.param) : undefined,
+ curve: ev.curba || undefined,
+ mode: ev.mod || undefined,
+ padding: ev.padding || activ.padding || undefined,
+ cryptoFunctions: activ.functii || undefined,
+ classification: ev.classification,
+ quantumVulnerable: ev.quantumVulnerable,
+ reason: activ.nota && !ev.reason.includes(activ.nota) ? `${ev.reason} ${activ.nota}` : ev.reason,
+ recommendation: ev.recommendation,
+ nistQuantumSecurityLevel: ev.nistQuantumSecurityLevel,
+ classicalSecurityLevel: ev.classicalSecurityLevel,
+ oid: ev.oid,
+ protocolType: ev.protocolType,
+ protocolVersion: ev.protocolVersion,
+ suppressedBy: extra.suprimaDe || undefined,
+ resolvedFrom: extra.rezolvatDin || undefined,
+ context: normalizeaza(bucata),
+ material: activ.material || undefined,
+ certificate: activ.certificat || undefined,
+ });
+ },
+ };
+ return ctx;
+}
+
+function normalizeaza(s) {
+ const t = String(s || '').replace(/\s+/g, ' ').trim();
+ return t.length > 120 ? t.slice(0, 117) + '...' : t;
+}
+
+// Argumentele unui apel: de la pozitia de dupa '(' pana la ')' echilibrata.
+// Intoarce textul argumentelor (din vederea de cod) si limitele lui.
+export function argumente(ctx, dupaParanteza) {
+ const c = ctx.code;
+ let adancime = 1;
+ let j = dupaParanteza;
+ while (j < c.length) {
+ if (ctx.inSir(j)) { j++; continue; }
+ const ch = c[j];
+ if (ch === '(' || ch === '[' || ch === '{') adancime++;
+ else if (ch === ')' || ch === ']' || ch === '}') { adancime--; if (adancime === 0) break; }
+ j++;
+ if (j - dupaParanteza > 4000) break;
+ }
+ return { text: c.slice(dupaParanteza, j), start: dupaParanteza, end: j };
+}
+
+// imparte argumentele pe virgulele de nivel zero
+export function imparteArgumente(ctx, start, end) {
+ const c = ctx.code;
+ const r = [];
+ let adancime = 0;
+ let s = start;
+ for (let j = start; j < end; j++) {
+ if (ctx.inSir(j)) continue;
+ const ch = c[j];
+ if (ch === '(' || ch === '[' || ch === '{') adancime++;
+ else if (ch === ')' || ch === ']' || ch === '}') adancime--;
+ else if (ch === ',' && adancime === 0) { r.push({ text: c.slice(s, j), start: s }); s = j + 1; }
+ }
+ if (end > s) r.push({ text: c.slice(s, end), start: s });
+ return r.map((a) => {
+ const lead = a.text.length - a.text.trimStart().length;
+ return { text: a.text.trim(), start: a.start + lead };
+ }).filter((a) => a.text.length);
+}
+
+// un literal de sir simplu, fara interpolare; intoarce valoarea sau null
+export function literal(s) {
+ const t = String(s).trim();
+ const m = /^(?:[rRbBuU]{0,2})(['"`])([^'"`\n$\\]*)\1$/.exec(t);
+ return m ? m[2] : null;
+}
+
+// identificator simplu (nu proprietate, nu apel)
+export function identificator(s) {
+ const t = String(s).trim();
+ return /^[A-Za-z_$][\w$]*$/.test(t) ? t : null;
+}
+
+// Rezolvarea conservatoare a unei variabile la un literal: EXACT o atribuire in fisier,
+// sub forma unei declaratii cu literal, si numele nu apare ca parametru de functie.
+// Orice alta forma: null (gasirea iese "unknown", nu se ghiceste).
+// Rezultatul depinde numai de fisier si de nume, deci se tine minte; si cel mult REZOLVARI_PE_FISIER nume distincte se rezolva
+// intr-un fisier (2026-09-29, revizuirea adversariala): fiecare rezolvare citeste tot fisierul, deci un fisier cu mii de apeluri
+// pe variabile costa patratic. Un nume peste plafon iese "unknown", si fisierul se numara in statistici (nu se sare in tacere).
+export const REZOLVARI_PE_FISIER = 64;
+export function rezolva(ctx, nume) {
+ if (!identificator(nume)) return null;
+ if (!ctx.rezolvari) ctx.rezolvari = new Map();
+ if (ctx.rezolvari.has(nume)) return ctx.rezolvari.get(nume);
+ if (ctx.rezolvari.size >= REZOLVARI_PE_FISIER) { ctx.rezolvariPestePlafon = (ctx.rezolvariPestePlafon || 0) + 1; return null; }
+ const r = rezolvaOData(ctx, nume);
+ ctx.rezolvari.set(nume, r);
+ return r;
+}
+
+function rezolvaOData(ctx, nume) {
+ const c = ctx.code;
+ const e = escapeRe(nume);
+ const atribuiri = ctx.potriviri(new RegExp(`(?])`, 'g'));
+ if (atribuiri.length !== 1) return null;
+ const k = atribuiri[0].index;
+ const ls0 = c.lastIndexOf('\n', k) + 1;
+ const le0 = c.indexOf('\n', k) < 0 ? c.length : c.indexOf('\n', k);
+ const linie = c.slice(ls0, le0);
+ let parametru = false;
+ if (ctx.limbaj === 'javascript') {
+ // intervalele fara capat ([^(]*, [^:]*, ...) sunt marginite: pornite de la fiecare cuvant cheie, ar citi pana la capatul
+ // fisierului pe un text fara paranteza (sau doua puncte) inchisa, deci un fisier facut anume ar costa patratic
+ parametru = new RegExp(`(?:function\\b[^(]{0,200}\\(|\\()([^()]{0,2000}(?|\\{)`).test(c)
+ || new RegExp(`\\bfor\\s*\\(\\s*(?:const|let|var)?\\s*${e}\\s+(?:of|in)\\b`).test(c)
+ || new RegExp(`(?`).test(c);
+ const m = new RegExp(`\\b(?:const|let|var)\\s+${e}\\s*=\\s*(['"\`])([^'"\`\\n$\\\\]*)\\1\\s*[;,]?\\s*$`).exec(linie);
+ return !parametru && m ? { valoare: m[2], line: ctx.poz(k).line } : null;
+ }
+ if (ctx.limbaj === 'python') {
+ parametru = new RegExp(`\\bdef\\s+\\w+\\s*\\([^)]{0,2000}(? 60 ? arg.text.slice(0, 57) + '...' : arg.text };
+}
+
+export function necunoscut(expr, api) {
+ return { grup: 'UNKNOWN', motiv: `Algorithm for ${api} is given by the expression "${expr}", which this tool does not resolve statically (it resolves only a variable with exactly one literal assignment in this file, for at most ${REZOLVARI_PE_FISIER} distinct variables per file).` };
+}
+
+// Obiectul literal care contine pozitia (acolade echilibrate), pentru a citi parametri vecini.
+export function obiectulDin(ctx, pos) {
+ const c = ctx.code;
+ let adancime = 0;
+ let s = pos;
+ while (s > 0 && pos - s < 2000) {
+ s--;
+ if (ctx.inSir(s)) continue;
+ if (c[s] === '}') adancime++;
+ else if (c[s] === '{') { if (adancime === 0) break; adancime--; }
+ }
+ if (c[s] !== '{') return null;
+ const a = argumente(ctx, s + 1);
+ return { text: c.slice(s, a.end + 1), start: s, end: a.end + 1 };
+}
+
+// Suprimarea: o gasire din import cade daca acelasi fisier are o gasire din apel care o acopera.
+// Pentru hash-uri si MAC se compara numele intreg, pentru cifruri si KDF familia, altfel grupul.
+function tokeni(g) {
+ if (g.group === 'HASH') return [`HASH:${g.name}`];
+ if (g.group === 'MAC') return g.name.startsWith('HMAC-') ? [`MAC:${g.name}`, `HASH:${g.name.slice(5)}`] : [`MAC:${g.name}`];
+ if (g.group === 'CIPHER' || g.group === 'KDF') return [`${g.group}:${g.name.split('-')[0]}`];
+ return [g.group];
+}
+
+export function aplicaSuprimarea(gasiri) {
+ const acoperite = new Set();
+ for (const g of gasiri) if (g.evidenceKind !== 'import') { acoperite.add(g.group); for (const t of tokeni(g)) acoperite.add(t); }
+ const r = gasiri.filter((g) => {
+ if (g.evidenceKind !== 'import' || !g.suppressedBy) return true;
+ const lista = g.suppressedBy === 'AUTO' ? tokeni(g) : g.suppressedBy;
+ return !lista.some((x) => acoperite.has(x));
+ });
+ // dubluri: acelasi rand, acelasi nume, acelasi API
+ const vazut = new Set();
+ return r.filter((g) => {
+ const k = `${g.line}|${g.name}|${g.api}|${g.classification}`;
+ if (vazut.has(k)) return false;
+ vazut.add(k);
+ return true;
+ }).map((g) => { const o = { ...g }; delete o.suppressedBy; return o; });
+}
+
+// numele unui cifru in stil OpenSSL (node:crypto): aes-256-gcm, des-ede3-cbc, bf-cbc, rc4, chacha20-poly1305
+export function cifruOpenssl(s) {
+ const t = String(s).toLowerCase().trim();
+ let m;
+ if ((m = /^(?:id-)?aes-?(128|192|256)(?:-(\w+(?:-\w+)?))?$/.exec(t))) return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: m[2] || 'cbc' };
+ if ((m = /^(camellia|aria|sm4)-?(128|192|256)?(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: m[1].toUpperCase(), param: m[2], mod: m[3] };
+ if (/^des-ede3(-\w+)?$|^des3$|^des-ede(-\w+)?$/.test(t)) return { grup: 'CIPHER', nume: '3DES', mod: (/-(cbc|ecb|cfb|ofb)$/.exec(t) || [])[1] || (t === 'des-ede3' || t === 'des-ede' ? 'ecb' : 'cbc') };
+ if ((m = /^des(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: 'DES', mod: m[1] || 'cbc' };
+ if (/^rc4(-\d+)?$|^rc4-hmac-md5$/.test(t)) return { grup: 'CIPHER', nume: 'RC4' };
+ if (/^rc2(-\w+)?$/.test(t)) return { grup: 'CIPHER', nume: 'RC2' };
+ if ((m = /^(?:bf|blowfish)(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: 'Blowfish', mod: m[1] };
+ if ((m = /^(cast5|cast|idea|seed)(?:-(\w+))?$/.exec(t))) return { grup: 'CIPHER', nume: m[1].toUpperCase(), mod: m[2] };
+ if (t === 'chacha20-poly1305') return { grup: 'CIPHER', nume: 'ChaCha20-Poly1305' };
+ if (t === 'chacha20') return { grup: 'CIPHER', nume: 'ChaCha20' };
+ return null;
+}
diff --git a/crypto-inventory/lib/detect-go.mjs b/crypto-inventory/lib/detect-go.mjs
new file mode 100644
index 0000000..ec983b4
--- /dev/null
+++ b/crypto-inventory/lib/detect-go.mjs
@@ -0,0 +1,207 @@
+// Detectorul Go: importurile pachetelor crypto/* (in Go un import nefolosit nu compileaza, deci
+// importul e o dovada de folosire, cu exceptia importului gol "_"), apelurile cu parametri si
+// configuratia tls.Config.
+import { argumente, imparteArgumente, escapeRe } from './context.mjs';
+import { grupTls, jws } from './catalog.mjs';
+
+const PACHETE = {
+ 'crypto/rsa': { grup: 'RSA' },
+ 'crypto/dsa': { grup: 'DSA' },
+ 'crypto/ecdsa': { grup: 'ECDSA' },
+ 'crypto/elliptic': { grup: 'EC' },
+ 'crypto/ecdh': { grup: 'ECDH' },
+ 'crypto/ed25519': { grup: 'EDDSA', nume: 'Ed25519' },
+ 'golang.org/x/crypto/ed25519': { grup: 'EDDSA', nume: 'Ed25519' },
+ 'golang.org/x/crypto/curve25519': { grup: 'XDH', nume: 'X25519' },
+ 'crypto/md5': { grup: 'HASH', hash: 'MD5' },
+ 'crypto/sha1': { grup: 'HASH', hash: 'SHA1' },
+ 'golang.org/x/crypto/md4': { grup: 'HASH', hash: 'MD4' },
+ 'golang.org/x/crypto/ripemd160': { grup: 'HASH', hash: 'RIPEMD160' },
+ 'crypto/sha256': { grup: 'HASH', hash: 'SHA256' },
+ 'crypto/sha512': { grup: 'HASH', hash: 'SHA512' },
+ 'crypto/sha3': { grup: 'HASH', hash: 'SHA3-256' },
+ 'golang.org/x/crypto/sha3': { grup: 'HASH', hash: 'SHA3-256' },
+ 'crypto/des': { grup: 'CIPHER', nume: 'DES' },
+ 'crypto/rc4': { grup: 'CIPHER', nume: 'RC4' },
+ 'golang.org/x/crypto/blowfish': { grup: 'CIPHER', nume: 'Blowfish' },
+ 'crypto/aes': { grup: 'CIPHER', nume: 'AES' },
+ 'golang.org/x/crypto/chacha20poly1305': { grup: 'CIPHER', nume: 'ChaCha20-Poly1305' },
+ 'crypto/mlkem': { grup: 'MLKEM' },
+ 'golang.org/x/crypto/bcrypt': { grup: 'KDF', nume: 'bcrypt' },
+ 'golang.org/x/crypto/argon2': { grup: 'KDF', nume: 'Argon2' },
+ 'golang.org/x/crypto/scrypt': { grup: 'KDF', nume: 'scrypt' },
+ 'golang.org/x/crypto/pbkdf2': { grup: 'KDF', nume: 'PBKDF2' },
+ 'crypto/pbkdf2': { grup: 'KDF', nume: 'PBKDF2' },
+ 'crypto/hkdf': { grup: 'KDF', nume: 'HKDF' },
+ 'golang.org/x/crypto/ssh': { grup: 'SSH' },
+ 'github.com/ethereum/go-ethereum/crypto': { grup: 'SECP256K1', nota: 'go-ethereum crypto package: accounts and signatures are secp256k1 ECDSA.' },
+ 'github.com/ethereum/go-ethereum/crypto/secp256k1': { grup: 'SECP256K1' },
+ 'github.com/decred/dcrd/dcrec/secp256k1': { grup: 'SECP256K1' },
+ 'github.com/btcsuite/btcd/btcec': { grup: 'SECP256K1' },
+ 'github.com/cloudflare/circl/sign/ed25519': { grup: 'EDDSA', nume: 'Ed25519' },
+ 'github.com/cloudflare/circl/sign/slhdsa': { grup: 'SLHDSA' },
+ 'github.com/cloudflare/circl/kem/hybrid': { grup: 'HYBRID-KEX', nume: 'hybrid KEM (circl)' },
+};
+
+function pachetActiv(cale) {
+ if (PACHETE[cale]) return PACHETE[cale];
+ let m;
+ if ((m = /^github\.com\/cloudflare\/circl\/sign\/mldsa\/mldsa(44|65|87)$/.exec(cale))) return { grup: 'MLDSA', param: m[1] };
+ if ((m = /^github\.com\/cloudflare\/circl\/kem\/mlkem\/mlkem(512|768|1024)$/.exec(cale))) return { grup: 'MLKEM', param: m[1] };
+ if ((m = /^github\.com\/cloudflare\/circl\/sign\/dilithium\/mode(2|3|5)$/.exec(cale))) return { grup: 'PREPQ', nume: `Dilithium${m[1]}`, primitiv: 'signature' };
+ if ((m = /^github\.com\/cloudflare\/circl\/kem\/kyber\/kyber(512|768|1024)$/.exec(cale))) return { grup: 'PREPQ', nume: `Kyber${m[1]}`, primitiv: 'kem' };
+ if (/^github\.com\/btcsuite\/btcd\/btcec(\/v\d+)?$/.test(cale) || /^github\.com\/decred\/dcrd\/dcrec\/secp256k1(\/v\d+)?$/.test(cale)) return { grup: 'SECP256K1' };
+ if (/^github\.com\/golang-jwt\/jwt(\/v\d+)?$/.test(cale)) return { grup: 'LIB-MULTI', nume: 'golang-jwt', motiv: 'Imports golang-jwt; no SigningMethod reference recognized in this file.' };
+ if (cale === 'crypto/tls') return { grup: 'TLS', param: 'negotiated', motiv: 'crypto/tls with default settings' };
+ return null;
+}
+
+export function importuriGo(ctx) {
+ const r = [];
+ const c = ctx.code;
+ const adauga = (alias, cale, pos) => r.push({ alias: alias || cale.split('/').filter((x) => !/^v\d+$/.test(x)).pop(), gol: alias === '_', cale, pos });
+ for (const m of c.matchAll(/(^|\n)[ \t]*import[ \t]+(?:([\w.]+)[ \t]+)?("([^"\n]+)")/g)) {
+ adauga(m[2], m[4], m.index + m[0].indexOf(m[3]));
+ }
+ for (const m of c.matchAll(/(^|\n)[ \t]*import[ \t]*\(([^)]*)\)/g)) {
+ const bloc = m[2];
+ const baza = m.index + m[0].indexOf('(') + 1;
+ for (const x of bloc.matchAll(/(?:^|\n)[ \t]*(?:([\w.]+)[ \t]+)?("([^"\n]+)")/g)) {
+ adauga(x[1], x[3], baza + x.index + x[0].indexOf(x[2]));
+ }
+ }
+ return r;
+}
+
+export function detecteazaGo(ctx) {
+ const imp = importuriGo(ctx);
+ const alias = (cale) => { const i = imp.find((x) => x.cale === cale && !x.gol); return i ? escapeRe(i.alias) : null; };
+ const apel = (cale, functii) => {
+ const a = alias(cale);
+ if (!a) return [];
+ return ctx.potriviri(new RegExp(`(? {
+ const dupa = ctx.code.slice(m.index + m[0].length);
+ const p = /^\s*\(/.exec(dupa);
+ if (!p) return { text: '', parti: [] };
+ const a = argumente(ctx, m.index + m[0].length + p[0].length);
+ return { ...a, parti: imparteArgumente(ctx, a.start, a.end) };
+ };
+ const pachetHash = { md5: 'MD5', sha1: 'SHA1', sha256: 'SHA256', sha512: 'SHA512', sha3: 'SHA3-256' };
+
+ // apelurile cu parametri
+ for (const m of apel('crypto/rsa', 'GenerateKey|GenerateMultiPrimeKey')) {
+ const a = args(m);
+ const ult = a.parti[a.parti.length - 1];
+ ctx.adauga(m.index, `rsa.${m[1]}`, 'call', { grup: 'RSA', param: ult && /^\d+$/.test(ult.text) ? ult.text : undefined, functii: ['keygen'] }, { bucata: m[0] + (ult ? ult.text : '') });
+ }
+ for (const m of apel('crypto/rsa', 'EncryptOAEP|DecryptOAEP|EncryptPKCS1v15|DecryptPKCS1v15|SignPSS|SignPKCS1v15|VerifyPSS|VerifyPKCS1v15')) {
+ const a = args(m);
+ const h = /\bcrypto\s*\.\s*(MD5|SHA1|SHA224|SHA256|SHA384|SHA512)\b/.exec(a.text);
+ const sig = /^(Sign|Verify)/.test(m[1]);
+ ctx.adauga(m.index, `rsa.${m[1]}`, 'call', { grup: 'RSA', primitiv: sig ? 'signature' : 'pke', padding: /OAEP/.test(m[1]) ? 'oaep' : /PKCS1v15/.test(m[1]) ? 'pkcs1v15' : 'other', hash: h ? h[1] : undefined, functii: [/^Sign/.test(m[1]) ? 'sign' : /^Verify/.test(m[1]) ? 'verify' : /^Encrypt/.test(m[1]) ? 'encrypt' : 'decrypt'] }, { bucata: m[0] });
+ }
+ const ell = alias('crypto/elliptic');
+ const consumate = new Set();
+ for (const m of apel('crypto/ecdsa', 'GenerateKey|Sign|SignASN1|Verify|VerifyASN1')) {
+ const a = args(m);
+ let curba;
+ if (ell && m[1] === 'GenerateKey') {
+ const k = new RegExp(`${ell}\\s*\\.\\s*(P224|P256|P384|P521)\\s*\\(`).exec(a.text);
+ if (k) { curba = k[1]; consumate.add(a.start + k.index); }
+ }
+ ctx.adauga(m.index, `ecdsa.${m[1]}`, 'call', { grup: 'ECDSA', curba, functii: [m[1] === 'GenerateKey' ? 'keygen' : /^Sign/.test(m[1]) ? 'sign' : 'verify'] }, { bucata: m[0] + (curba ? ` ${curba}` : '') });
+ }
+ for (const m of apel('crypto/elliptic', 'P224|P256|P384|P521')) {
+ if (consumate.has(m.index)) continue;
+ ctx.adauga(m.index, `elliptic.${m[1]}`, 'call', { grup: 'EC', curba: m[1] }, { bucata: m[0] });
+ }
+ for (const m of apel('crypto/ecdh', 'X25519|P256|P384|P521')) {
+ ctx.adauga(m.index, `ecdh.${m[1]}`, 'call', m[1] === 'X25519' ? { grup: 'XDH', nume: 'X25519', functii: ['keygen'] } : { grup: 'ECDH', curba: m[1], functii: ['keygen'] }, { bucata: m[0] });
+ }
+ for (const cale of ['crypto/ed25519', 'golang.org/x/crypto/ed25519']) {
+ for (const m of apel(cale, 'GenerateKey|Sign|Verify|NewKeyFromSeed|VerifyWithOptions')) {
+ ctx.adauga(m.index, `ed25519.${m[1]}`, 'call', { grup: 'EDDSA', nume: 'Ed25519', functii: [m[1] === 'Sign' ? 'sign' : /^Verify/.test(m[1]) ? 'verify' : 'keygen'] }, { bucata: m[0] });
+ }
+ }
+ for (const [cale, fn] of [['crypto/md5', 'New|Sum'], ['crypto/sha1', 'New|Sum'], ['crypto/sha256', 'New|New224|Sum256|Sum224'], ['crypto/sha512', 'New|New384|Sum512|Sum384|New512_256'], ['crypto/sha3', 'New256|New384|New512|Sum256|Sum384|Sum512|NewSHAKE128|NewSHAKE256'], ['golang.org/x/crypto/sha3', 'New256|New384|New512|Sum256|Sum384|Sum512|NewLegacyKeccak256']]) {
+ for (const m of apel(cale, fn)) {
+ const dupa = ctx.code.slice(m.index + m[0].length, m.index + m[0].length + 3);
+ if (!/^\s*\(/.test(dupa)) continue; // referinta de functie (de ex. hmac.New(sha1.New, ...)) se raporteaza la hmac
+ const pk = cale.split('/').pop();
+ let h = pachetHash[pk];
+ if (/224/.test(m[1])) h = 'SHA224';
+ if (/384/.test(m[1])) h = pk === 'sha3' ? 'SHA3-384' : 'SHA384';
+ if (/512_256/.test(m[1])) h = 'SHA512/256';
+ if (pk === 'sha3' && /512/.test(m[1])) h = 'SHA3-512';
+ if (/SHAKE128/.test(m[1])) h = 'SHAKE128';
+ if (/SHAKE256/.test(m[1])) h = 'SHAKE256';
+ if (/Keccak/.test(m[1])) h = 'KECCAK256';
+ ctx.adauga(m.index, `${pk}.${m[1]}`, 'call', { grup: 'HASH', hash: h, functii: ['digest'] }, { bucata: m[0] });
+ }
+ }
+ for (const m of apel('crypto/hmac', 'New')) {
+ const a = args(m);
+ const k = /^(\w+)\s*\.\s*New(\d*)\b/.exec(a.parti[0] ? a.parti[0].text : '');
+ const pk = k ? imp.find((x) => x.alias === k[1]) : null;
+ const baza = pk ? pachetHash[pk.cale.split('/').pop()] : null;
+ const h = baza ? (k[2] === '384' ? 'SHA384' : k[2] === '224' ? 'SHA224' : baza) : null;
+ ctx.adauga(m.index, 'hmac.New', 'call', h ? { grup: 'MAC', hash: h, functii: ['tag'] } : { grup: 'UNKNOWN', motiv: `hmac.New hash "${a.parti[0] ? a.parti[0].text : '?'}" cannot be resolved statically.` }, { bucata: m[0] + (a.parti[0] ? `(${a.parti[0].text}` : '') });
+ }
+ for (const m of apel('crypto/des', 'NewCipher|NewTripleDESCipher')) {
+ ctx.adauga(m.index, `des.${m[1]}`, 'call', { grup: 'CIPHER', nume: m[1] === 'NewCipher' ? 'DES' : '3DES', functii: ['encrypt'] }, { bucata: m[0] });
+ }
+ for (const m of apel('crypto/rc4', 'NewCipher')) ctx.adauga(m.index, 'rc4.NewCipher', 'call', { grup: 'CIPHER', nume: 'RC4', functii: ['encrypt'] }, { bucata: m[0] });
+ for (const m of apel('crypto/aes', 'NewCipher')) ctx.adauga(m.index, 'aes.NewCipher', 'call', { grup: 'CIPHER', nume: 'AES', functii: ['encrypt'] }, { bucata: m[0] });
+ for (const m of apel('crypto/mlkem', 'GenerateKey768|GenerateKey1024|NewEncapsulationKey768|NewEncapsulationKey1024|NewDecapsulationKey768|NewDecapsulationKey1024')) {
+ ctx.adauga(m.index, `mlkem.${m[1]}`, 'call', { grup: 'MLKEM', param: /1024/.test(m[1]) ? '1024' : '768', functii: [/^Generate/.test(m[1]) ? 'keygen' : 'other'] }, { bucata: m[0] });
+ }
+ const eth = alias('github.com/ethereum/go-ethereum/crypto');
+ if (eth) {
+ for (const m of ctx.potriviri(new RegExp(`(? /^github\.com\/golang-jwt\/jwt(\/v\d+)?$/.test(x.cale));
+ if (jwtI) {
+ for (const m of ctx.potriviri(new RegExp(`(? (variabila ? new RegExp(`(? (x || '').trim());
+ const alg = alg0.toUpperCase();
+ const mod = mod0 ? mod0.toLowerCase() : undefined;
+ const pad = pad0 ? pad0.toUpperCase() : '';
+ let m;
+ if (alg === 'RSA') {
+ const padding = /OAEP/.test(pad) ? 'oaep' : /PKCS1/.test(pad) ? 'pkcs1v15' : /NOPADDING/.test(pad) ? 'raw' : (pad ? 'other' : 'pkcs1v15');
+ const nota = pad ? undefined : 'Cipher "RSA" without padding means RSA/ECB/PKCS1Padding in the standard JCA providers.';
+ return { grup: 'RSA', primitiv: 'pke', padding, nota, functii: ['encrypt', 'decrypt'] };
+ }
+ if ((m = /^AES(?:_(128|192|256))?$/.exec(alg))) {
+ if (!mod) return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: 'ecb', nota: 'Cipher "AES" without mode and padding means AES/ECB/PKCS5Padding in the standard JCA providers.', functii: ['encrypt', 'decrypt'] };
+ return { grup: 'CIPHER', nume: 'AES', param: m[1], mod: mod === 'nopadding' ? undefined : mod, functii: ['encrypt', 'decrypt'] };
+ }
+ if (alg === 'DES') return { grup: 'CIPHER', nume: 'DES', mod, functii: ['encrypt', 'decrypt'] };
+ if (alg === 'DESEDE' || alg === 'TRIPLEDES') return { grup: 'CIPHER', nume: '3DES', mod, functii: ['encrypt', 'decrypt'] };
+ if (alg === 'RC4' || alg === 'ARCFOUR') return { grup: 'CIPHER', nume: 'RC4', functii: ['encrypt', 'decrypt'] };
+ if (alg === 'RC2') return { grup: 'CIPHER', nume: 'RC2', functii: ['encrypt', 'decrypt'] };
+ if (alg === 'BLOWFISH') return { grup: 'CIPHER', nume: 'Blowfish', mod, functii: ['encrypt', 'decrypt'] };
+ if (alg === 'CHACHA20-POLY1305') return { grup: 'CIPHER', nume: 'ChaCha20-Poly1305', functii: ['encrypt', 'decrypt'] };
+ if (alg === 'CHACHA20') return { grup: 'CIPHER', nume: 'ChaCha20', functii: ['encrypt', 'decrypt'] };
+ if (/^PBEWITH(MD5|SHA1)AND(DES|TRIPLEDES|RC2|RC4)/.test(alg)) return { grup: 'CIPHER', nume: /TRIPLEDES/.test(alg) ? '3DES' : /RC2/.test(alg) ? 'RC2' : /RC4/.test(alg) ? 'RC4' : 'DES', nota: `${v} derives the key with a weak hash.` };
+ if (/^ECIES/.test(alg)) return { grup: 'ECDH', nume: 'ECIES', primitiv: 'pke', functii: ['encrypt', 'decrypt'] };
+ return { grup: 'UNKNOWN', motiv: `Cipher transformation "${v}" is not in this tool's catalog.` };
+}
+
+// nume post-cuantice in JCA sau Bouncy Castle
+export function pqNume(v) {
+ const t = String(v).trim();
+ let m;
+ if ((m = /^ML-?KEM(?:-(512|768|1024))?$/i.exec(t))) return { grup: 'MLKEM', param: m[1] };
+ if ((m = /^ML-?DSA(?:-(44|65|87))?$/i.exec(t))) return { grup: 'MLDSA', param: m[1] };
+ if ((m = /^SLH-?DSA(?:-(SHA2|SHAKE)-(128|192|256)([sfSF]))?$/i.exec(t))) return { grup: 'SLHDSA', param: m[1] ? `${m[1].toUpperCase()}-${m[2]}${m[3].toLowerCase()}` : undefined };
+ if ((m = /^(?:Falcon|FN-DSA)(?:-(512|1024))?$/i.exec(t))) return { grup: 'FALCON', param: m[1] };
+ if (/^(Kyber\d*|Dilithium\d*|SPHINCSPlus|SPHINCS\+)$/i.test(t)) return { grup: 'PREPQ', nume: t, primitiv: /kyber/i.test(t) ? 'kem' : 'signature' };
+ if (/^(XMSS|XMSSMT|XMSS\^MT|LMS|HSS)$/i.test(t)) return { grup: 'HASHSIG', nume: t.toUpperCase() };
+ if (/^(Rainbow|SIKE)$/i.test(t)) return { grup: 'BROKENPQ', nume: t, primitiv: /rainbow/i.test(t) ? 'signature' : 'kem' };
+ if (/^HQC(-\d+)?$/i.test(t)) return { grup: 'HQC', nume: t.toUpperCase() };
+ return null;
+}
+
+const BC_CLASA = [
+ [/^(ECDSASigner|ECNRSigner|DSTU4145Signer)$/, { grup: 'ECDSA' }],
+ [/^(DSASigner)$/, { grup: 'DSA' }],
+ [/^(RSADigestSigner|PSSSigner|RSAEngine|RSABlindedEngine|OAEPEncoding|PKCS1Encoding|RSAKeyPairGenerator|ISO9796d2Signer)$/, { grup: 'RSA' }],
+ [/^(ECKeyPairGenerator|ECNamedCurveTable|SECNamedCurves|ECDomainParameters)$/, { grup: 'EC' }],
+ [/^(ECDHBasicAgreement|ECDHCBasicAgreement|ECDHUnifiedAgreement)$/, { grup: 'ECDH' }],
+ [/^(DHBasicAgreement|DHAgreement|DHKeyPairGenerator)$/, { grup: 'DH' }],
+ [/^(Ed25519Signer|Ed25519ctxSigner|Ed25519phSigner|Ed25519KeyPairGenerator)$/, { grup: 'EDDSA', nume: 'Ed25519' }],
+ [/^(Ed448Signer|Ed448KeyPairGenerator)$/, { grup: 'EDDSA', nume: 'Ed448' }],
+ [/^(X25519Agreement|X25519KeyPairGenerator)$/, { grup: 'XDH', nume: 'X25519' }],
+ [/^(X448Agreement|X448KeyPairGenerator)$/, { grup: 'XDH', nume: 'X448' }],
+ [/^SecP256K1Curve$/, { grup: 'SECP256K1' }],
+ [/^MD5Digest$/, { grup: 'HASH', hash: 'MD5' }],
+ [/^SHA1Digest$/, { grup: 'HASH', hash: 'SHA1' }],
+ [/^SHA256Digest$/, { grup: 'HASH', hash: 'SHA256' }],
+ [/^SHA512Digest$/, { grup: 'HASH', hash: 'SHA512' }],
+ [/^(DESEngine)$/, { grup: 'CIPHER', nume: 'DES' }],
+ [/^(DESedeEngine)$/, { grup: 'CIPHER', nume: '3DES' }],
+ [/^(RC4Engine)$/, { grup: 'CIPHER', nume: 'RC4' }],
+ [/^(BlowfishEngine)$/, { grup: 'CIPHER', nume: 'Blowfish' }],
+ [/^(AESEngine|AESFastEngine|AESLightEngine)$/, { grup: 'CIPHER', nume: 'AES' }],
+];
+
+function bcActiv(cale) {
+ const clasa = cale.split('.').pop();
+ let m;
+ if ((m = /^(MLKEM|MLDSA|SLHDSA|Falcon|Kyber|Dilithium|SPHINCSPlus|XMSS|XMSSMT|LMS|HSS|Rainbow|SIKE|HQC|NTRU|NTRUPrime|BIKE|Frodo|CMCE|Picnic|Saber)/.exec(clasa))) {
+ const fam = m[1];
+ const map = { MLKEM: 'ML-KEM', MLDSA: 'ML-DSA', SLHDSA: 'SLH-DSA', SPHINCSPlus: 'SPHINCSPlus', XMSSMT: 'XMSSMT' };
+ const pq = pqNume(map[fam] || fam);
+ if (pq) return pq;
+ return { grup: 'UNKNOWN', motiv: `Bouncy Castle post-quantum class ${clasa}: scheme "${fam}" is not classified by this tool (not a finalized NIST standard).` };
+ }
+ if (/\.pqc\./.test(cale)) {
+ if (/BouncyCastlePQCProvider$/.test(clasa)) return { grup: 'UNKNOWN', motiv: 'Bouncy Castle PQC provider registered; the scheme is chosen elsewhere (not visible in this import).' };
+ return { grup: 'UNKNOWN', motiv: `Bouncy Castle post-quantum package class ${clasa}; scheme not recognized by this tool.` };
+ }
+ for (const [re, act] of BC_CLASA) if (re.test(clasa)) return act;
+ return null;
+}
+
+// Variabila din `v = ` pe acelasi rand, citita INAPOI de la pozitie (ce dadea /([A-Za-z_$][\w$]*)\s*=\s*$/ pe textul randului
+// de dinainte). Forma veche taia la fiecare apel tot randul pana la pozitie, deci un rand lung cu multe apeluri costa patratic
+// (2026-09-29, revizuirea adversariala); cititul inapoi se opreste la primul caracter care nu poate face parte din tipar.
+export function variabilaAtribuita(c, pos) {
+ const sp = (ch) => ch !== '\n' && /\s/.test(ch);
+ let j = pos - 1;
+ while (j >= 0 && sp(c[j])) j--;
+ if (j < 0 || c[j] !== '=') return null;
+ j--;
+ while (j >= 0 && sp(c[j])) j--;
+ const sf = j + 1;
+ while (j >= 0 && /[\w$]/.test(c[j])) j--;
+ let st = j + 1;
+ while (st < sf && /[0-9]/.test(c[st])) st++;
+ return st < sf ? c.slice(st, sf) : null;
+}
+
+export function detecteazaJava(ctx) {
+ const c = ctx.code;
+ const consumate = new Set();
+ for (const m of ctx.potriviri(/(? TLSV[x[1]]).filter(Boolean);
+ for (const x of vers) {
+ const ver = TLSV[x[1]];
+ if (!ver) continue;
+ const rol = ver === '1.2' && lista.includes('1.3') ? 'min' : 'only';
+ ctx.adauga(a.start + x.index, 'setEnabledProtocols', 'config', { grup: 'TLS', param: ver, rol }, { bucata: `"${x[1]}"` });
+ }
+ }
+ // proprietati de sistem ale JSSE
+ for (const m of ctx.potriviri(/(?= 0) s = s.slice(k + 'node_modules/'.length);
+ if (/^(@[\w.-]+\/)?[\w.-]+\/.+\.(m?js|cjs)$/.test(s) && k >= 0) s = s.replace(/\.(m?js|cjs)$/, '');
+ else if (/^@noble\//.test(s)) s = s.replace(/\.(m?js|cjs)$/, '');
+ return s;
+}
+
+function descrieImport(spec0, pos, clauza, implicitDinRequire = null, proprietate = null) {
+ const spec = normalizeazaSpec(spec0);
+ const importate = [];
+ const locale = [];
+ let implicit = implicitDinRequire;
+ const acolade = /\{([^}]*)\}/.exec(clauza || '');
+ if (acolade) {
+ for (const bucata of acolade[1].split(',')) {
+ const t = bucata.trim().replace(/^type\s+/, '');
+ if (!t) continue;
+ const m = /^([\w$]+)(?:\s*(?:as|:)\s*([\w$]+))?$/.exec(t);
+ if (m) { importate.push(m[1]); locale.push(m[2] || m[1]); }
+ }
+ }
+ const rest = (clauza || '').replace(/\{[^}]*\}/, '').replace(/^type\s+/, '');
+ const ns = /\*\s*as\s+([\w$]+)/.exec(rest);
+ if (ns) implicit = ns[1];
+ const def = /^\s*([\w$]+)\s*(?:,|$)/.exec(rest);
+ if (!implicit && def && def[1] !== 'type') implicit = def[1];
+ if (proprietate) { importate.push(proprietate); if (implicit) locale.push(implicit); }
+ return { spec, pos, importate, locale, implicit };
+}
+
+export function detecteazaJs(ctx) {
+ const c = ctx.code;
+ const imp = importuriJs(ctx);
+ const importa = (re) => imp.filter((i) => re.test(i.spec));
+ const cryptoImp = importa(/^(node:)?crypto$/);
+ const aliasCrypto = new Set(['crypto']);
+ for (const i of cryptoImp) { if (i.implicit) aliasCrypto.add(i.implicit); }
+ const numeImportate = new Set(cryptoImp.flatMap((i) => i.locale));
+ const nodeCrypto = cryptoImp.length > 0 || ctx.potriviri(/(? 0;
+
+ const apeluri = (nume) => ctx.potriviri(new RegExp(`(? m[1] !== 'subtle' && (m[1] ? aliasCrypto.has(m[1]) || nodeCrypto : (numeImportate.has(m[2]) || nodeCrypto)));
+
+ const argsDe = (m) => {
+ const a = argumente(ctx, m.index + m[0].length);
+ return imparteArgumente(ctx, a.start, a.end);
+ };
+ const cuValoare = (m, arg, api, fn) => {
+ const v = valoareArgument(ctx, arg);
+ if (v.fel === 'literal' || v.fel === 'rezolvat') return fn(v.valoare, v.fel === 'rezolvat' ? { rezolvatDin: v.din } : {});
+ if (v.fel === 'necunoscut') ctx.adauga(m.index, api, 'call', necunoscut(v.expresie, api), { bucata: m[0] + v.expresie });
+ return null;
+ };
+
+ if (nodeCrypto) {
+ for (const m of apeluri('createHash|createHmac')) {
+ const api = m[2];
+ const p = argsDe(m);
+ cuValoare(m, p[0], api, (val, ex) => {
+ const h = hashCanonic(val) || val;
+ if (api === 'createHash') ctx.adauga(m.index, api, 'call', { grup: 'HASH', hash: h, functii: ['digest'] }, { ...ex, bucata: `${m[0]}'${val}'` });
+ else ctx.adauga(m.index, api, 'call', { grup: 'MAC', hash: h, functii: ['tag'] }, { ...ex, bucata: `${m[0]}'${val}'` });
+ });
+ }
+ for (const m of apeluri('createCipheriv|createDecipheriv|createCipher|createDecipher')) {
+ const api = m[2];
+ const p = argsDe(m);
+ cuValoare(m, p[0], api, (val, ex) => {
+ const cf = cifruOpenssl(val) || { grup: 'CIPHER', nume: val };
+ ctx.adauga(m.index, api, 'call', { ...cf, functii: [/Decipher/.test(api) ? 'decrypt' : 'encrypt'] }, { ...ex, bucata: `${m[0]}'${val}'` });
+ });
+ }
+ for (const m of apeluri('createSign|createVerify')) {
+ const api = m[2];
+ const p = argsDe(m);
+ cuValoare(m, p[0], api, (val, ex) => {
+ const f = [api === 'createSign' ? 'sign' : 'verify'];
+ const h = hashCanonic(val.replace(/^(RSA-|ecdsa-with-|DSA-|RSA-PSS-)/i, '').replace(/with(RSA|DSA)Encryption$/i, '').replace(/WithRSAEncryption$/i, ''));
+ let a;
+ if (/rsa/i.test(val)) a = { grup: 'RSA', primitiv: 'signature', hash: h };
+ else if (/ecdsa/i.test(val)) a = { grup: 'ECDSA', hash: h };
+ else if (/dsa/i.test(val)) a = { grup: 'DSA', hash: h };
+ else a = { grup: 'CLASSIC-SIG', hash: h, motiv: `${api} with digest ${val} (Sign/Verify objects take RSA, RSA-PSS, DSA or EC keys; the key type comes from the key object)` };
+ ctx.adauga(m.index, api, 'call', { ...a, functii: f }, { ...ex, bucata: `${m[0]}'${val}'` });
+ });
+ }
+ for (const m of apeluri('createECDH')) {
+ const p = argsDe(m);
+ cuValoare(m, p[0], 'createECDH', (val, ex) => ctx.adauga(m.index, 'createECDH', 'call', { grup: 'ECDH', curba: val, functii: ['keygen'] }, { ...ex, bucata: `${m[0]}'${val}'` }));
+ }
+ for (const m of apeluri('getDiffieHellman|createDiffieHellmanGroup')) {
+ const p = argsDe(m);
+ cuValoare(m, p[0], m[2], (val, ex) => ctx.adauga(m.index, m[2], 'call', { grup: 'DH', param: MODP[val] ? String(MODP[val]) : val, functii: ['keygen'] }, { ...ex, bucata: `${m[0]}'${val}'` }));
+ }
+ for (const m of apeluri('createDiffieHellman')) {
+ const p = argsDe(m);
+ const bits = p[0] && /^\d+$/.test(p[0].text) ? p[0].text : undefined;
+ ctx.adauga(m.index, 'createDiffieHellman', 'call', { grup: 'DH', param: bits, functii: ['keygen'] }, { bucata: m[0] + (bits || '') });
+ }
+ for (const m of apeluri('generateKeySync|generateKey')) {
+ const p = argsDe(m);
+ const v = valoareArgument(ctx, p[0]);
+ if ((v.fel === 'literal' || v.fel === 'rezolvat') && v.valoare.toLowerCase() === 'aes') {
+ const len = p[1] && /length\s*:\s*(\d+)/.exec(p[1].text);
+ ctx.adauga(m.index, m[2], 'call', { grup: 'CIPHER', nume: 'AES', param: len ? len[1] : undefined, functii: ['keygen'] }, { bucata: `${m[0]}'aes'` });
+ }
+ }
+ for (const m of apeluri('publicEncrypt|privateDecrypt|privateEncrypt|publicDecrypt')) {
+ const api = m[2];
+ const a = argumente(ctx, m.index + m[0].length);
+ const t = a.text;
+ const enc = api === 'publicEncrypt' || api === 'privateDecrypt';
+ let padding = enc ? 'oaep' : 'pkcs1v15';
+ if (/RSA_PKCS1_OAEP_PADDING|oaepHash/.test(t)) padding = 'oaep';
+ else if (/RSA_PKCS1_PADDING/.test(t)) padding = 'pkcs1v15';
+ else if (/RSA_NO_PADDING/.test(t)) padding = 'raw';
+ ctx.adauga(m.index, api, 'call', { grup: 'RSA', primitiv: enc ? 'pke' : 'signature', padding, functii: [api === 'publicEncrypt' || api === 'privateEncrypt' ? 'encrypt' : 'decrypt'] }, { bucata: m[0] });
+ }
+ for (const m of apeluri('pbkdf2Sync|pbkdf2')) {
+ const p = argsDe(m);
+ cuValoare(m, p[4], m[2], (val, ex) => {
+ const h = hashCanonic(val) || val;
+ ctx.adauga(m.index, m[2], 'call', { grup: 'KDF', nume: `PBKDF2-HMAC-${val.toUpperCase()}`, hash: h, functii: ['keyderive'] }, { ...ex, bucata: m[0] });
+ });
+ }
+ for (const m of apeluri('hkdfSync|hkdf')) {
+ const p = argsDe(m);
+ cuValoare(m, p[0], m[2], (val, ex) => ctx.adauga(m.index, m[2], 'call', { grup: 'KDF', nume: `HKDF-${val.toUpperCase()}`, hash: hashCanonic(val) || val, functii: ['keyderive'] }, { ...ex, bucata: m[0] }));
+ }
+ // chei si certificate incarcate la rulare: algoritmul vine din material, nu din sursa
+ for (const m of [...apeluri('createPrivateKey|createPublicKey'), ...ctx.potriviri(/(? !x[1] || aliasCrypto.has(x[1]))]) {
+ const api = m[2];
+ ctx.adauga(m.index, api, 'call', { grup: 'UNKNOWN', motiv: `${api}: key material loaded at run time; its algorithm (RSA, EC, Ed25519, ML-DSA, ...) comes from the key or certificate, which is not in the scanned source. This is a place where keys enter the program: check which algorithm is deployed there.`, functii: ['other'] }, { bucata: m[0] });
+ }
+ // crypto.sign / crypto.verify (si importurile numite sign/verify)
+ for (const m of ctx.potriviri(/(? ctx.adauga(m.index, api, 'call', { grup: 'CLASSIC-SIG', hash: hashCanonic(val) || val, motiv: `${api} with digest ${val} (a digest name is used with RSA, RSA-PSS, DSA and EC keys; EdDSA and ML-DSA keys take null)`, functii: [m[2]] }, { ...ex, bucata: `${m[0]}'${val}'` }));
+ }
+ }
+
+ // generateKeyPair: node:crypto sau jose (acolo primul argument e un algoritm JWS)
+ const jwtLib = importa(/^(jsonwebtoken|jose|jwt-simple|express-jwt|@fastify\/jwt|passport-jwt|koa-jwt|fast-jwt|jws)$/);
+ for (const m of ctx.potriviri(/(? {
+ if (JWS_RE.test(val) && jwtLib.length) return; // se raporteaza de regula JWT
+ const t = val.toLowerCase();
+ const ml = /modulusLength\s*:\s*(\d+)/.exec(opt);
+ const nc = /namedCurve\s*:\s*(['"])([^'"]+)\1/.exec(opt);
+ let act;
+ if (t === 'rsa') act = { grup: 'RSA', param: ml && ml[1] };
+ else if (t === 'rsa-pss') act = { grup: 'RSA', param: ml && ml[1], primitiv: 'signature', padding: 'other' };
+ else if (t === 'dsa') act = { grup: 'DSA', param: ml && ml[1] };
+ else if (t === 'ec') act = { grup: 'EC', curba: nc && nc[2] };
+ else if (t === 'ed25519' || t === 'ed448') act = { grup: 'EDDSA', nume: t === 'ed25519' ? 'Ed25519' : 'Ed448' };
+ else if (t === 'x25519' || t === 'x448') act = { grup: 'XDH', nume: t === 'x25519' ? 'X25519' : 'X448' };
+ else if (t === 'dh') {
+ const pl = /primeLength\s*:\s*(\d+)/.exec(opt);
+ const gr = /group\s*:\s*(['"])(modp\d+)\1/.exec(opt);
+ act = { grup: 'DH', param: pl ? pl[1] : gr ? String(MODP[gr[2]] || gr[2]) : undefined };
+ } else if (/^ml-dsa-(44|65|87)$/.test(t)) act = { grup: 'MLDSA', param: t.slice(7) };
+ else if (/^ml-kem-(512|768|1024)$/.test(t)) act = { grup: 'MLKEM', param: t.slice(7) };
+ else if (/^slh-dsa-(sha2|shake)-(128|192|256)[sf]$/.test(t)) act = { grup: 'SLHDSA', param: t.slice(8).toUpperCase().replace(/([SF])$/, (x) => x.toLowerCase()) };
+ else act = { grup: 'UNKNOWN', motiv: `Key type "${val}" is not in this tool's catalog.` };
+ ctx.adauga(m.index, m[2], 'call', { ...act, functii: ['keygen'] }, { ...ex, bucata: `${m[0]}'${val}'` });
+ });
+ }
+
+ detecteazaTlsJs(ctx);
+ detecteazaWebCrypto(ctx);
+ detecteazaBiblioteci(ctx, imp, importa, jwtLib);
+}
+
+function detecteazaTlsJs(ctx) {
+ for (const m of ctx.potriviri(/(? 80 ? t.slice(0, 77) + '...' : t;
+}
+
+// valoarea unei expresii: identificator rezolvat, sau template ale carui ${ID} se rezolva toate; altfel null
+function valoareExpresie(ctx, expr) {
+ const id = identificator(expr);
+ if (id) {
+ const r = rezolva(ctx, id);
+ return r ? { valoare: r.valoare, din: `${id} (line ${r.line})` } : null;
+ }
+ const t = /^`((?:[^`$\\]|\$\{\s*[A-Za-z_$][\w$]*\s*\})*)`$/.exec(expr);
+ if (!t) return null;
+ const din = [];
+ let ok = true;
+ const val = t[1].replace(/\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g, (_, n) => {
+ const r = rezolva(ctx, n);
+ if (!r) { ok = false; return ''; }
+ din.push(`${n} (line ${r.line})`);
+ return r.valoare;
+ });
+ return ok ? { valoare: val, din: din.join(', ') } : null;
+}
+
+const WEBCRYPTO_ALG = /^(RSA-OAEP|RSASSA-PKCS1-v1_5|RSA-PSS|ECDSA|ECDH|Ed25519|Ed448|X25519|X448|AES-GCM|AES-CBC|AES-CTR|AES-KW|HMAC|HKDF|PBKDF2|ML-KEM-(?:512|768|1024)|ML-DSA-(?:44|65|87)|ChaCha20-Poly1305)$/i;
+
+function activWebCrypto(nume, fereastra) {
+ const n = nume.toUpperCase();
+ const ml = /modulusLength\s*:\s*(\d+)/.exec(fereastra);
+ const nc = /namedCurve\s*:\s*(['"])([^'"]+)\1/.exec(fereastra);
+ const len = /(? lista[0];
+
+ // Ethereum: conturile si semnaturile sunt secp256k1 ECDSA
+ const eth = importa(ETH);
+ if (eth.length) {
+ const i = primul(eth);
+ ctx.adauga(i.pos, `import ${i.spec}`, 'import', { grup: 'SECP256K1', nota: `Imported library: ${i.spec}, whose accounts and transaction signatures are secp256k1 ECDSA; the import alone does not show that this file signs.` }, { suprimaDe: ['SECP256K1'], bucata: `import ${i.spec}` });
+ const re1 = /(? (m[1] ? re.test(m[1]) : m[2].split(',').some((x) => re.test(x.trim()))));
+}
+
+export function detecteazaPy(ctx) {
+ const argsDe = (m) => { const a = argumente(ctx, m.index + m[0].length); return { ...a, parti: imparteArgumente(ctx, a.start, a.end) }; };
+ const cuValoare = (m, arg, api, fn) => {
+ const v = valoareArgument(ctx, arg);
+ if (v.fel === 'literal' || v.fel === 'rezolvat') return fn(v.valoare, v.fel === 'rezolvat' ? { rezolvatDin: v.din } : {});
+ if (v.fel === 'necunoscut') ctx.adauga(m.index, api, 'call', necunoscut(v.expresie, api), { bucata: m[0] + v.expresie });
+ return null;
+ };
+ const numar = (a, cheie, poz) => {
+ const k = new RegExp(`\\b${cheie}\\s*=\\s*(\\d+)`).exec(a.text);
+ if (k) return k[1];
+ if (poz !== undefined && a.parti[poz] && /^\d+$/.test(a.parti[poz].text)) return a.parti[poz].text;
+ return undefined;
+ };
+
+ // --- cryptography.hazmat ---
+ if (importaPy(ctx, /^cryptography\b/).length) {
+ for (const m of ctx.potriviri(/(? ctx.adauga(m.index, 'hashlib.new', 'call', { grup: 'HASH', hash: hashCanonic(val) || val, functii: ['digest'] }, { ...ex, bucata: `hashlib.new('${val}')` }));
+ }
+ for (const m of ctx.potriviri(/(? ctx.adauga(m.index, 'hashlib.pbkdf2_hmac', 'call', { grup: 'KDF', nume: `PBKDF2-HMAC-${val.toUpperCase()}`, hash: hashCanonic(val) || val, functii: ['keyderive'] }, { ...ex, bucata: m[0] }));
+ }
+ for (const m of ctx.potriviri(/(? ctx.adauga(m.index, `oqs.${m[1]}`, 'call', oqsActiv(val), { ...ex, bucata: `oqs.${m[1]}('${val}')` }));
+ }
+
+ // --- ssl ---
+ for (const m of ctx.potriviri(/(? ctx.adauga(m.index, 'set_ecdh_curve', 'config', { grup: 'ECDH', curba: val, functii: ['keygen'] }, { ...ex, bucata: m[0] }));
+ }
+}
+
+export function oqsActiv(val) {
+ const t = String(val);
+ let m;
+ if ((m = /^ML-KEM-(512|768|1024)$/i.exec(t))) return { grup: 'MLKEM', param: m[1] };
+ if ((m = /^ML-DSA-(44|65|87)$/i.exec(t))) return { grup: 'MLDSA', param: m[1] };
+ if ((m = /^SLH[-_]DSA[-_](SHA2|SHAKE)[-_](128|192|256)([sf])/i.exec(t))) return { grup: 'SLHDSA', param: `${m[1].toUpperCase()}-${m[2]}${m[3].toLowerCase()}` };
+ if ((m = /^Falcon-(512|1024)$/i.exec(t))) return { grup: 'FALCON', param: m[1] };
+ if (/^(Kyber\d+|Dilithium\d|SPHINCS\+?-.*)$/i.test(t)) return { grup: 'PREPQ', nume: t, primitiv: /kyber/i.test(t) ? 'kem' : 'signature' };
+ if (/^HQC-\d+$/i.test(t)) return { grup: 'HQC', nume: t };
+ return { grup: 'UNKNOWN', motiv: `Post-quantum mechanism "${t}" is not in this tool's catalog.` };
+}
diff --git a/crypto-inventory/lib/lexer.mjs b/crypto-inventory/lib/lexer.mjs
new file mode 100644
index 0000000..afed0e0
--- /dev/null
+++ b/crypto-inventory/lib/lexer.mjs
@@ -0,0 +1,230 @@
+// Lexer minimal pe limbaje: comentariile devin spatii (liniile si coloanele raman pe loc),
+// sirurile raman in text, dar li se tin minte intervalele, ca un detector sa poata cere
+// "numele API-ului e COD, nu text dintr-un sir".
+//
+// Nu e un parser: e cat trebuie ca un tipar sa nu se potriveasca in comentarii, in
+// docstring-uri Python sau in textul unui sir.
+
+const KW_REGEX = new Set(['return', 'typeof', 'instanceof', 'in', 'of', 'new', 'delete', 'void',
+ 'throw', 'case', 'do', 'else', 'yield', 'await']);
+const PUNCT_REGEX = '(,=:[!&|?{};+-*%<>~^';
+
+export function curata(text, limbaj) {
+ switch (limbaj) {
+ case 'javascript': return curataC(text, { template: true, regex: true });
+ case 'java': return curataC(text, { textBlock: true });
+ case 'go': return curataC(text, { raw: true });
+ case 'python': return curataPython(text);
+ default: return { code: text, siruri: [] };
+ }
+}
+
+function golitor(out) {
+ return (a, b) => {
+ for (let k = a; k < b; k++) if (out[k] !== '\n' && out[k] !== '\r') out[k] = ' ';
+ };
+}
+
+function curataC(text, o) {
+ const n = text.length;
+ const out = text.split('');
+ const goleste = golitor(out);
+ const siruri = [];
+ const stiva = [];
+ let acolade = 0;
+ let prevSig = '';
+ let prevWord = '';
+ let i = 0;
+ let esecRegex = null; // bit 1: din (pozitie, in afara clasei) nu se ajunge la sfarsitul literalului; bit 2: la fel, in clasa
+
+ // scaneaza un segment de template literal pornind de la j; intoarce pozitia si daca s-a deschis ${
+ const scanTpl = (inceput, j) => {
+ while (j < n) {
+ const ch = text[j];
+ if (ch === '\\') { j += 2; continue; }
+ if (ch === '`') { siruri.push([inceput, j + 1]); return { pos: j + 1, deschis: false }; }
+ if (ch === '$' && text[j + 1] === '{') { siruri.push([inceput, j + 2]); return { pos: j + 2, deschis: true }; }
+ j++;
+ }
+ siruri.push([inceput, n]);
+ return { pos: n, deschis: false };
+ };
+
+ while (i < n) {
+ const c = text[i];
+ const d = text[i + 1];
+ if (c === '/' && d === '/') {
+ let j = i;
+ while (j < n && text[j] !== '\n') j++;
+ goleste(i, j); i = j; continue;
+ }
+ if (c === '/' && d === '*') {
+ let j = text.indexOf('*/', i + 2);
+ j = j < 0 ? n : j + 2;
+ goleste(i, j); i = j; continue;
+ }
+ if (o.textBlock && c === '"' && text.startsWith('"""', i)) {
+ let j = text.indexOf('"""', i + 3);
+ j = j < 0 ? n : j + 3;
+ siruri.push([i, j]); i = j; prevSig = '"'; continue;
+ }
+ if (c === '"' || c === "'") {
+ let j = i + 1;
+ while (j < n && text[j] !== c && text[j] !== '\n') { if (text[j] === '\\') j++; j++; }
+ j = Math.min(n, j + 1);
+ siruri.push([i, j]); i = j; prevSig = c; continue;
+ }
+ if (c === '`' && o.raw) {
+ let j = text.indexOf('`', i + 1);
+ j = j < 0 ? n : j + 1;
+ siruri.push([i, j]); i = j; prevSig = '`'; continue;
+ }
+ if (c === '`' && o.template) {
+ const r = scanTpl(i, i + 1);
+ i = r.pos;
+ if (r.deschis) { stiva.push(acolade); acolade++; }
+ prevSig = '`'; continue;
+ }
+ if (c === '/' && o.regex) {
+ const permis = prevSig === '' || PUNCT_REGEX.includes(prevSig) || (prevSig === 'a' && KW_REGEX.has(prevWord));
+ if (permis) {
+ // Cautarea sfarsitului unui literal regex e determinista din (pozitie, "in clasa"), deci o stare din care s-a ajuns o data
+ // la capatul randului fara sfarsit duce acolo si a doua oara. Starile acestea se tin minte (2026-09-29, revizuirea
+ // adversariala): fara ele un rand lung cu multe `/` nedeschise (de ex. `=/[` repetat) costa patratic, minute pe 1 MB.
+ let j = i + 1;
+ let inClasa = false;
+ let bun = false;
+ const vizitate = [];
+ while (j < n && text[j] !== '\n') {
+ if (esecRegex && esecRegex[j] & (inClasa ? 2 : 1)) break;
+ vizitate.push(inClasa ? -j - 1 : j);
+ const ch = text[j];
+ if (ch === '\\') { j += 2; continue; }
+ if (ch === '[') inClasa = true;
+ else if (ch === ']') inClasa = false;
+ else if (ch === '/' && !inClasa) { bun = true; break; }
+ j++;
+ }
+ if (!bun) {
+ if (!esecRegex) esecRegex = new Uint8Array(n);
+ for (const v of vizitate) { if (v < 0) esecRegex[-v - 1] |= 2; else esecRegex[v] |= 1; }
+ }
+ if (bun) {
+ j++;
+ while (j < n && /[a-z]/i.test(text[j])) j++;
+ siruri.push([i, j]); i = j; prevSig = '/'; continue;
+ }
+ }
+ prevSig = '/'; i++; continue;
+ }
+ if (c === '{') { acolade++; prevSig = '{'; i++; continue; }
+ if (c === '}') {
+ acolade--;
+ if (stiva.length && acolade === stiva[stiva.length - 1]) {
+ stiva.pop();
+ const r = scanTpl(i, i + 1);
+ i = r.pos;
+ if (r.deschis) { stiva.push(acolade); acolade++; }
+ prevSig = '`'; continue;
+ }
+ prevSig = '}'; i++; continue;
+ }
+ if (/[A-Za-z_$]/.test(c)) {
+ let j = i + 1;
+ while (j < n && /[\w$]/.test(text[j])) j++;
+ prevWord = text.slice(i, j); prevSig = 'a'; i = j; continue;
+ }
+ if (/[0-9]/.test(c)) {
+ let j = i + 1;
+ while (j < n && /[\w.]/.test(text[j])) j++;
+ prevWord = ''; prevSig = '0'; i = j; continue;
+ }
+ if (!/\s/.test(c)) prevSig = c;
+ i++;
+ }
+ return { code: out.join(''), siruri };
+}
+
+function curataPython(text) {
+ const n = text.length;
+ const out = text.split('');
+ const goleste = golitor(out);
+ const siruri = [];
+ let i = 0;
+ // primul caracter care nu e spatiu pe randul curent (-1 = niciunul inca). Tinut din mers (2026-09-29, revizuirea adversariala):
+ // forma veche cauta inapoi inceputul randului la FIECARE sir triplu, deci un rand lung cu multe siruri triple costa patratic.
+ let primulPeRand = -1;
+ while (i < n) {
+ const c = text[i];
+ if (c === '\n') { primulPeRand = -1; i++; continue; }
+ if (c === '#') {
+ let j = i;
+ while (j < n && text[j] !== '\n') j++;
+ goleste(i, j); i = j; continue;
+ }
+ if (c === '"' || c === "'") {
+ // prefixul (r, b, f, u, rb, ...) face parte din sir
+ let start = i;
+ while (start > 0 && /[rRbBuUfF]/.test(text[start - 1]) && i - start < 2) start--;
+ if (start < i && start > 0 && /[\w]/.test(text[start - 1])) start = i;
+ const triplu = text.startsWith(c.repeat(3), i);
+ let j;
+ // inainte de sir, pe randul lui, sunt numai spatii (prefixul r/b/f/u face parte din sir)?
+ const singurPeRand = primulPeRand < 0 || primulPeRand >= start;
+ let ultimaLinieNoua = -1;
+ if (triplu) {
+ j = i + 3;
+ while (j < n && !text.startsWith(c.repeat(3), j)) {
+ if (text[j] === '\\') { if (text[j + 1] === '\n') ultimaLinieNoua = j + 1; j++; } else if (text[j] === '\n') ultimaLinieNoua = j;
+ j++;
+ }
+ j = Math.min(n, j + 3);
+ } else {
+ j = i + 1;
+ while (j < n && text[j] !== c && text[j] !== '\n') { if (text[j] === '\\') { if (text[j + 1] === '\n') ultimaLinieNoua = j + 1; j++; } j++; }
+ if (j < n && text[j] === '\n') ultimaLinieNoua = j; // sir neterminat: intervalul lui se opreste DUPA acest rand nou
+ j = Math.min(n, j + 1);
+ }
+ // dupa sir: daca sirul a trecut peste un rand nou, randul curent incepe in el (coada lui nu e spatiu), afara de cazul in
+ // care sirul se termina chiar cu randul nou; altfel randul curent are acum cel putin sirul
+ if (ultimaLinieNoua >= 0) primulPeRand = ultimaLinieNoua === j - 1 ? -1 : ultimaLinieNoua + 1;
+ else if (primulPeRand < 0) primulPeRand = start;
+ // docstring: sirul triplu e singur pe randul lui, ca instructiune; se trateaza ca un comentariu
+ if (triplu && singurPeRand) { goleste(start, j); i = j; continue; }
+ siruri.push([start, j]); i = j; continue;
+ }
+ if (primulPeRand < 0 && c !== ' ' && c !== '\t') primulPeRand = i;
+ i++;
+ }
+ return { code: out.join(''), siruri };
+}
+
+// cautare binara: pozitia e inauntrul unui sir?
+export function inSir(siruri, pos) {
+ let lo = 0;
+ let hi = siruri.length - 1;
+ while (lo <= hi) {
+ const mid = (lo + hi) >> 1;
+ const [s, e] = siruri[mid];
+ if (pos < s) hi = mid - 1;
+ else if (pos >= e) lo = mid + 1;
+ else return true;
+ }
+ return false;
+}
+
+export function inceputuriDeRand(text) {
+ const r = [0];
+ for (let k = 0; k < text.length; k++) if (text.charCodeAt(k) === 10) r.push(k + 1);
+ return r;
+}
+
+export function randul(inceputuri, pos) {
+ let lo = 0;
+ let hi = inceputuri.length - 1;
+ while (lo < hi) {
+ const mid = (lo + hi + 1) >> 1;
+ if (inceputuri[mid] <= pos) lo = mid; else hi = mid - 1;
+ }
+ return { line: lo + 1, column: pos - inceputuri[lo] + 1 };
+}
diff --git a/crypto-inventory/lib/manifeste.mjs b/crypto-inventory/lib/manifeste.mjs
new file mode 100644
index 0000000..22ffedb
--- /dev/null
+++ b/crypto-inventory/lib/manifeste.mjs
@@ -0,0 +1,134 @@
+// Bibliotecile criptografice DECLARATE in manifeste. Declararea nu e folosire: aceste intrari
+// ies ca componente "library", nu ca gasiri clasificate.
+import { inceputuriDeRand, randul } from './lexer.mjs';
+
+// nume -> ce ofera (text scurt, in engleza, pentru utilizator)
+const NPM = {
+ 'node-forge': 'RSA, AES, DES, MD5, SHA-1, SHA-2, X.509 (classical)',
+ elliptic: 'ECDSA/ECDH/EdDSA on classical curves including secp256k1',
+ secp256k1: 'secp256k1 ECDSA', 'tiny-secp256k1': 'secp256k1 ECDSA', '@noble/secp256k1': 'secp256k1 ECDSA',
+ ethers: 'secp256k1 ECDSA accounts and signatures', web3: 'secp256k1 ECDSA accounts and signatures', viem: 'secp256k1 ECDSA accounts and signatures',
+ 'ethereum-cryptography': 'secp256k1, keccak, AES, scrypt',
+ jsonwebtoken: 'JWS HS*/RS*/PS*/ES*', jose: 'JWS/JWE (classical algorithms)', 'jwt-simple': 'JWS HS*/RS*',
+ '@noble/curves': 'secp256k1, P-256/384/521, Ed25519/X25519, Ed448, BLS12-381 (classical)',
+ '@noble/post-quantum': 'ML-KEM, ML-DSA, SLH-DSA (post-quantum)',
+ '@noble/hashes': 'SHA-2, SHA-3, BLAKE, legacy MD5/SHA-1/RIPEMD-160',
+ '@noble/ed25519': 'Ed25519',
+ 'crypto-js': 'AES, DES, 3DES, RC4, MD5, SHA-1, SHA-2', tweetnacl: 'Ed25519, X25519, XSalsa20-Poly1305',
+ 'libsodium-wrappers': 'Ed25519, X25519, XChaCha20-Poly1305, Argon2', 'node-rsa': 'RSA', jsrsasign: 'RSA, ECDSA, X.509',
+ openpgp: 'OpenPGP: RSA, ECC, AES', bcrypt: 'bcrypt password hashing', bcryptjs: 'bcrypt password hashing', argon2: 'Argon2 password hashing',
+ sshpk: 'SSH keys: RSA, ECDSA, Ed25519', '@peculiar/x509': 'X.509 certificates', pkijs: 'X.509/CMS',
+};
+const PYPI = {
+ cryptography: 'RSA, EC, DH, Ed25519/X25519, AES, hashes (pyca/cryptography)',
+ pycryptodome: 'RSA, DSA, ECC, AES, DES, hashes', pycryptodomex: 'RSA, DSA, ECC, AES, DES, hashes',
+ pycrypto: 'RSA, DSA, AES, DES (unmaintained since 2013)', ecdsa: 'ECDSA on classical curves', pyjwt: 'JWS HS*/RS*/ES*/PS*/EdDSA',
+ 'python-jose': 'JWS/JWE (classical algorithms)', jwcrypto: 'JWS/JWE (classical algorithms)', pynacl: 'Ed25519, X25519',
+ pyopenssl: 'TLS and X.509 via OpenSSL', paramiko: 'SSH: RSA, ECDSA, Ed25519, classical key exchange', 'liboqs-python': 'post-quantum KEMs and signatures (liboqs)',
+ oqs: 'post-quantum KEMs and signatures (liboqs)', web3: 'secp256k1 ECDSA accounts', 'eth-account': 'secp256k1 ECDSA accounts', 'eth-keys': 'secp256k1 ECDSA',
+ coincurve: 'secp256k1 ECDSA', bcrypt: 'bcrypt password hashing', 'argon2-cffi': 'Argon2 password hashing', passlib: 'password hashing', rsa: 'RSA (pure Python)',
+};
+const MAVEN_GROUP = {
+ 'org.bouncycastle': 'Bouncy Castle: classical and post-quantum algorithms',
+ 'io.jsonwebtoken': 'JJWT: JWS HS*/RS*/ES*/PS*/EdDSA', 'com.nimbusds': 'Nimbus JOSE+JWT', 'com.auth0': 'java-jwt (if artifact is java-jwt)',
+ 'org.web3j': 'secp256k1 ECDSA accounts (web3j)', 'com.google.crypto.tink': 'Tink: AEAD, signatures, hybrid encryption', 'org.conscrypt': 'Conscrypt TLS provider',
+};
+const GO = [
+ [/^golang\.org\/x\/crypto$/, 'extended crypto: ssh, chacha20poly1305, curve25519, bcrypt, argon2'],
+ [/^github\.com\/cloudflare\/circl$/, 'CIRCL: ML-KEM, ML-DSA, SLH-DSA, hybrid KEMs, classical curves'],
+ [/^github\.com\/ethereum\/go-ethereum$/, 'secp256k1 ECDSA accounts and signatures'],
+ [/^github\.com\/golang-jwt\/jwt(\/v\d+)?$/, 'JWS HS*/RS*/ES*/PS*/EdDSA'],
+ [/^github\.com\/decred\/dcrd\/dcrec\/secp256k1(\/v\d+)?$/, 'secp256k1 ECDSA'],
+ [/^github\.com\/btcsuite\/btcd\/btcec(\/v\d+)?$/, 'secp256k1 ECDSA'],
+ [/^filippo\.io\/edwards25519$/, 'Edwards25519 group arithmetic'],
+ [/^github\.com\/open-quantum-safe\/liboqs-go$/, 'post-quantum KEMs and signatures (liboqs)'],
+];
+
+export const NUME_MANIFEST = /^(package\.json|requirements[\w.-]*\.txt|pyproject\.toml|pom\.xml|build\.gradle(\.kts)?|go\.mod)$/;
+
+function lib(ecosistem, nume, versiune, ofera, pos, inceputuri, fisier) {
+ const { line } = randul(inceputuri, pos);
+ const purl = ecosistem === 'npm' ? `pkg:npm/${nume.startsWith('@') ? '%40' + nume.slice(1) : nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}`
+ : ecosistem === 'pypi' ? `pkg:pypi/${nume}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}`
+ : ecosistem === 'maven' ? `pkg:maven/${nume.replace(':', '/')}${versiune && /^[\w.-]+$/.test(versiune) ? '@' + versiune : ''}`
+ : `pkg:golang/${nume}${versiune ? '@' + versiune : ''}`;
+ return { file: fisier, line, ecosystem: ecosistem, name: nume, version: versiune || undefined, provides: ofera, purl };
+}
+
+export function citesteManifest(text, numeFisier, rel) {
+ const inceputuri = inceputuriDeRand(text);
+ const r = [];
+ if (numeFisier === 'package.json') {
+ let j;
+ try { j = JSON.parse(text); } catch { return { biblioteci: [], eroare: 'package.json is not valid JSON' }; }
+ for (const sect of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
+ const d = j && j[sect];
+ if (!d || typeof d !== 'object') continue;
+ const ps = text.indexOf(`"${sect}"`);
+ for (const [nume, ver] of Object.entries(d)) {
+ const ofera = NPM[nume] || (/^@ethersproject\//.test(nume) ? 'secp256k1 ECDSA (ethers v5 module)' : null);
+ if (!ofera) continue;
+ const pos = text.indexOf(`"${nume}"`, ps < 0 ? 0 : ps);
+ r.push(lib('npm', nume, String(ver).replace(/^[\^~>=<\s]+/, ''), ofera, pos < 0 ? 0 : pos, inceputuri, rel));
+ }
+ }
+ } else if (/^requirements/.test(numeFisier)) {
+ let pos = 0;
+ for (const linie of text.split('\n')) {
+ const t = linie.replace(/#.*/, '').trim();
+ const m = /^([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:[=<>!~]=?\s*([\w.*+-]+))?/.exec(t);
+ if (m) {
+ const nume = m[1].toLowerCase().replace(/_/g, '-');
+ if (PYPI[nume]) r.push(lib('pypi', nume, m[2], PYPI[nume], pos + linie.indexOf(m[1]), inceputuri, rel));
+ }
+ pos += linie.length + 1;
+ }
+ } else if (numeFisier === 'pyproject.toml') {
+ let pos = 0;
+ let sectiune = '';
+ for (const linie of text.split('\n')) {
+ const h = /^\s*\[([^\]]+)\]/.exec(linie);
+ if (h) sectiune = h[1];
+ for (const m of linie.matchAll(/["']([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:[=<>!~]=?\s*([\w.*+-]+))?[^"']*["']/g)) {
+ const nume = m[1].toLowerCase().replace(/_/g, '-');
+ if (PYPI[nume]) r.push(lib('pypi', nume, m[2], PYPI[nume], pos + m.index + 1, inceputuri, rel));
+ }
+ const p = /^\s*([A-Za-z0-9_.-]+)\s*=\s*(?:["']([^"']*)["']|\{)/.exec(linie);
+ if (p && /dependencies/.test(sectiune)) {
+ const nume = p[1].toLowerCase().replace(/_/g, '-');
+ if (PYPI[nume]) r.push(lib('pypi', nume, p[2] ? p[2].replace(/^[\^~>=<\s]+/, '') : undefined, PYPI[nume], pos + linie.indexOf(p[1]), inceputuri, rel));
+ }
+ pos += linie.length + 1;
+ }
+ } else if (numeFisier === 'pom.xml') {
+ for (const m of text.matchAll(/([\s\S]*?)<\/dependency>/g)) {
+ const g = /\s*([^<\s]+)\s*<\/groupId>/.exec(m[1]);
+ const a = /\s*([^<\s]+)\s*<\/artifactId>/.exec(m[1]);
+ const v = /\s*([^<\s]+)\s*<\/version>/.exec(m[1]);
+ if (!g || !a) continue;
+ const ofera = MAVEN_GROUP[g[1]];
+ if (!ofera) continue;
+ r.push(lib('maven', `${g[1]}:${a[1]}`, v && !/\$\{/.test(v[1]) ? v[1] : undefined, ofera, m.index + m[0].indexOf(a[0]), inceputuri, rel));
+ }
+ } else if (/^build\.gradle/.test(numeFisier)) {
+ for (const m of text.matchAll(/["']([\w.-]+):([\w.-]+)(?::([\w.-]+))?["']/g)) {
+ const ofera = MAVEN_GROUP[m[1]];
+ if (ofera) r.push(lib('maven', `${m[1]}:${m[2]}`, m[3], ofera, m.index + 1, inceputuri, rel));
+ }
+ } else if (numeFisier === 'go.mod') {
+ let pos = 0;
+ let inRequire = false;
+ for (const linie of text.split('\n')) {
+ const t = linie.replace(/\/\/.*/, '').trim();
+ if (/^require\s*\($/.test(t)) inRequire = true;
+ else if (inRequire && t === ')') inRequire = false;
+ const m = inRequire ? /^([\w.\-/]+)\s+(v[\w.\-+]+)/.exec(t) : /^require\s+([\w.\-/]+)\s+(v[\w.\-+]+)/.exec(t);
+ if (m) {
+ const intrare = GO.find(([re]) => re.test(m[1]));
+ if (intrare) r.push(lib('golang', m[1], m[2], intrare[1], pos + linie.indexOf(m[1]), inceputuri, rel));
+ }
+ pos += linie.length + 1;
+ }
+ }
+ return { biblioteci: r };
+}
diff --git a/crypto-inventory/lib/pem.mjs b/crypto-inventory/lib/pem.mjs
new file mode 100644
index 0000000..e6f8922
--- /dev/null
+++ b/crypto-inventory/lib/pem.mjs
@@ -0,0 +1,103 @@
+// Material criptografic in text: blocuri PEM complete (antet, corp base64, subsol).
+// Cheile private se raporteaza DOAR ca locatie si tip; valoarea nu se tipareste niciodata.
+// Tipul cheii se afla parsand blocul in memorie cu node:crypto (date, nu cod executat).
+import { createPublicKey, createPrivateKey, X509Certificate } from 'node:crypto';
+
+// Cautarea blocurilor e LINIARA in marimea textului (2026-09-29, revizuirea adversariala): forma veche, un singur regex cu
+// corp lenes si referinta inapoi, relua cautarea sfarsitului de la FIECARE antet BEGIN, deci un fisier de 1 MB cu antete fara
+// sfarsit costa minute. Acum pentru fiecare antet se cere PRIMUL `-----END -----` de dupa el si ca intre ele sa nu fie niciun
+// caracter din afara alfabetului corpului, adica exact ce potrivea regexul vechi; ambele pozitii se tin minte si se cauta numai
+// inainte, deci fiecare caracter se citeste de un numar marginit de ori.
+const SURSA_BEGIN = '-----BEGIN ((?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY|PUBLIC KEY|RSA PUBLIC KEY|CERTIFICATE)-----';
+const SURSA_AFARA_DIN_CORP = '[^A-Za-z0-9+/=\\s:,\\-\\r\\n]';
+
+export function* blocuriPem(text) {
+ const reBegin = new RegExp(SURSA_BEGIN, 'g');
+ const reRau = new RegExp(SURSA_AFARA_DIN_CORP, 'g');
+ // o cautare tinuta minte e buna pentru pozitia `de` daca a pornit la sau inainte de `de` si nu a gasit ceva inainte de `de`
+ const valabil = (c, de) => c && c.la <= de && (c.poz < 0 || c.poz >= de);
+ const sfarsit = new Map(); // tip -> { la, poz }: primul END al tipului la pozitia >= la (poz -1 = niciunul)
+ let rau = null; // { la, poz }: primul caracter din afara alfabetului corpului, la pozitia >= la
+ const urmatorulSfarsit = (tip, de) => {
+ let c = sfarsit.get(tip);
+ if (!valabil(c, de)) { c = { la: de, poz: text.indexOf(`-----END ${tip}-----`, de) }; sfarsit.set(tip, c); }
+ return c.poz;
+ };
+ const urmatorulRau = (de) => {
+ if (!valabil(rau, de)) { reRau.lastIndex = de; const x = reRau.exec(text); rau = { la: de, poz: x ? x.index : -1 }; }
+ return rau.poz;
+ };
+ let m;
+ while ((m = reBegin.exec(text))) {
+ const tip = m[1];
+ const corpDe = m.index + m[0].length;
+ const e = urmatorulSfarsit(tip, corpDe);
+ const r = e < 0 ? -1 : urmatorulRau(corpDe);
+ // fara potrivire: cautarea continua de la caracterul urmator, ca la regexul vechi (un antet poate incepe in liniutele celui de dinainte)
+ if (e < 0 || (r >= 0 && r < e)) { reBegin.lastIndex = m.index + 1; continue; }
+ const capat = e + `-----END ${tip}-----`.length;
+ yield { index: m.index, tip, corp: text.slice(corpDe, e), bloc: text.slice(m.index, capat) };
+ reBegin.lastIndex = capat;
+ }
+}
+
+function activCheie(ko) {
+ const t = ko.asymmetricKeyType;
+ const d = ko.asymmetricKeyDetails || {};
+ if (t === 'rsa' || t === 'rsa-pss') return { grup: 'RSA', param: d.modulusLength ? String(d.modulusLength) : undefined };
+ if (t === 'dsa') return { grup: 'DSA', param: d.modulusLength ? String(d.modulusLength) : undefined };
+ if (t === 'dh') return { grup: 'DH' };
+ if (t === 'ec') return { grup: 'EC', curba: d.namedCurve };
+ if (t === 'ed25519' || t === 'ed448') return { grup: 'EDDSA', nume: t === 'ed25519' ? 'Ed25519' : 'Ed448' };
+ if (t === 'x25519' || t === 'x448') return { grup: 'XDH', nume: t === 'x25519' ? 'X25519' : 'X448' };
+ if (/^ml-dsa-(44|65|87)$/.test(t)) return { grup: 'MLDSA', param: t.slice(7) };
+ if (/^ml-kem-(512|768|1024)$/.test(t)) return { grup: 'MLKEM', param: t.slice(7) };
+ if (/^slh-dsa/.test(t)) return { grup: 'SLHDSA', param: t.slice(8) };
+ return { grup: 'UNKNOWN', motiv: `Key type "${t}" is not in this tool's catalog.` };
+}
+
+export function detecteazaPem(text, ctx) {
+ for (const b of blocuriPem(text)) {
+ const m = [b.bloc, b.tip, b.corp];
+ m.index = b.index;
+ const corp = m[2].replace(/[\s]/g, '').replace(/^Proc-Type:.*?DEK-Info:[^A-Za-z0-9+/]*/, '');
+ if (corp.replace(/[^A-Za-z0-9+/=]/g, '').length < 64) continue; // un antet singur (de ex. intr-un parser) nu e material
+ const tip = m[1];
+ let act;
+ let assetType;
+ if (tip === 'CERTIFICATE') {
+ assetType = 'certificate';
+ try {
+ const x = new X509Certificate(m[0]);
+ act = { ...activCheie(x.publicKey), nota: 'Certificate public key.' };
+ act.certificat = {
+ subjectName: x.subject.replace(/\n/g, ', '),
+ issuerName: x.issuer.replace(/\n/g, ', '),
+ notValidBefore: new Date(x.validFrom).toISOString(),
+ notValidAfter: new Date(x.validTo).toISOString(),
+ certificateFormat: 'X.509',
+ certificateExtension: 'pem',
+ fingerprintSha256: x.fingerprint256,
+ };
+ } catch {
+ act = { grup: 'UNKNOWN', motiv: 'PEM certificate block that could not be parsed.', certificat: { certificateFormat: 'X.509', certificateExtension: 'pem' } };
+ }
+ } else if (/PUBLIC KEY/.test(tip)) {
+ assetType = 'related-crypto-material';
+ try { act = activCheie(createPublicKey(m[0])); } catch { act = tip === 'RSA PUBLIC KEY' ? { grup: 'RSA' } : { grup: 'UNKNOWN', motiv: 'PEM public key block that could not be parsed.' }; }
+ act.material = { type: 'public-key', format: 'PEM' };
+ } else {
+ assetType = 'related-crypto-material';
+ if (/ENCRYPTED/.test(tip) || /Proc-Type:\s*4,ENCRYPTED/.test(m[2])) act = { grup: 'UNKNOWN', motiv: 'Encrypted private key: algorithm not visible without the passphrase.' };
+ else if (tip === 'OPENSSH PRIVATE KEY') act = { grup: 'UNKNOWN', motiv: 'OpenSSH private key: algorithm not parsed by this tool.' };
+ else {
+ try { act = activCheie(createPrivateKey(m[0])); } catch {
+ act = tip.startsWith('RSA') ? { grup: 'RSA' } : tip.startsWith('EC') ? { grup: 'EC' } : tip.startsWith('DSA') ? { grup: 'DSA' } : { grup: 'UNKNOWN', motiv: 'PEM private key block that could not be parsed.' };
+ }
+ }
+ act.material = { type: 'private-key', format: 'PEM' };
+ act.nota = 'Private key material stored in the scanned tree: if it is real, treat it as exposed and rotate it.';
+ }
+ ctx.adauga(m.index, `PEM ${tip}`, 'pem', { ...act, assetType }, { bucata: `-----BEGIN ${tip}-----` });
+ }
+}
diff --git a/crypto-inventory/lib/rezumat.mjs b/crypto-inventory/lib/rezumat.mjs
new file mode 100644
index 0000000..536b20c
--- /dev/null
+++ b/crypto-inventory/lib/rezumat.mjs
@@ -0,0 +1,76 @@
+// Rezumatul text (engleza, pentru utilizator) si regula --fail-on.
+import { CLS } from './catalog.mjs';
+
+const ORDINE = [CLS.V, CLS.W, CLS.S, CLS.U];
+
+export function numarPeClase(gasiri) {
+ const r = Object.fromEntries(ORDINE.map((c) => [c, 0]));
+ for (const g of gasiri) r[g.classification] = (r[g.classification] || 0) + 1;
+ return r;
+}
+
+// --fail-on: lista separata prin virgula din vulnerable, weak, unknown, any
+// "vulnerable" = orice gasire pe care un calculator cuantic o sparge (inclusiv cele weak-now care sunt si Shor)
+export function verificaFailOn(gasiri, spec) {
+ if (!spec) return { esec: false, motive: [] };
+ const cer = new Set(String(spec).split(',').map((x) => x.trim()).filter(Boolean));
+ const necunoscute = [...cer].filter((x) => !['vulnerable', 'weak', 'unknown', 'any'].includes(x));
+ if (necunoscute.length) throw new Error(`--fail-on: unknown value(s): ${necunoscute.join(', ')} (use vulnerable, weak, unknown or any)`);
+ const motive = [];
+ const vuln = gasiri.filter((g) => g.classification === CLS.V || (g.classification === CLS.W && g.quantumVulnerable));
+ const slabe = gasiri.filter((g) => g.classification === CLS.W);
+ const nec = gasiri.filter((g) => g.classification === CLS.U);
+ if ((cer.has('vulnerable') || cer.has('any')) && vuln.length) motive.push(`${vuln.length} quantum-vulnerable finding(s)`);
+ if ((cer.has('weak') || cer.has('any')) && slabe.length) motive.push(`${slabe.length} weak-now finding(s)`);
+ if (cer.has('unknown') && nec.length) motive.push(`${nec.length} unknown finding(s)`);
+ return { esec: motive.length > 0, motive };
+}
+
+export function rezumatText(rez) {
+ const s = rez.stats;
+ const g = rez.findings;
+ const L = [];
+ L.push(`Aere crypto inventory ${rez.tool.version}: ${rez.rootName}`);
+ const limbaje = Object.entries(s.codeFiles).sort().map(([k, v]) => `${k} ${v}`).join(', ') || 'none';
+ L.push(`Files: ${s.filesSeen} seen | ${s.codeFilesTotal} analyzed as code (${limbaje}) | ${s.textFilesPemOnly} other text files checked for PEM key/certificate blocks only`);
+ const nr = s.notRead;
+ L.push(`Not read: ${nr.binary} binary, ${nr.tooLarge} too large (> ${rez.options.maxFileBytes} bytes), ${nr.fileLimit} over the file limit (${rez.options.maxFiles}), ${nr.unreadable} unreadable; ${s.symlinksNotFollowed} symlinks not followed`);
+ for (const k of ['binary', 'tooLarge', 'fileLimit', 'unreadable']) {
+ if (s.notReadExamples[k].length) L.push(` e.g. ${k}: ${s.notReadExamples[k].join(', ')}${nr[k] > s.notReadExamples[k].length ? ', ...' : ''}`);
+ }
+ if (s.resolutionLimitFiles) L.push(`Variable resolution limit (${rez.options.maxResolvedVariablesPerFile} distinct variables per file) reached in ${s.resolutionLimitFiles} file(s); further variables there are reported as unknown: ${s.resolutionLimitExamples.join(', ')}${s.resolutionLimitFiles > s.resolutionLimitExamples.length ? ', ...' : ''}`);
+ const ex = Object.entries(s.dirsExcluded).sort().map(([n, c]) => `${n} (${c})`).join(', ');
+ L.push(`Directories not descended: ${ex || 'none'}${s.dirsUnreadable ? `; ${s.dirsUnreadable} unreadable` : ''}`);
+ const pk = {};
+ for (const x of g) pk[x.evidenceKind] = (pk[x.evidenceKind] || 0) + 1;
+ L.push(`Findings: ${g.length} (${Object.entries(pk).sort().map(([k, v]) => `${k} ${v}`).join(', ') || 'none'})`);
+ const cl = numarPeClase(g);
+ const wq = g.filter((x) => x.classification === CLS.W && x.quantumVulnerable).length;
+ for (const c of ORDINE) L.push(` ${c.padEnd(19)} ${String(cl[c]).padStart(5)}${c === CLS.W && wq ? ` (${wq} also quantum-vulnerable)` : ''}`);
+ const pl = {};
+ for (const x of g) pl[x.language] = (pl[x.language] || 0) + 1;
+ L.push(`By language: ${Object.entries(pl).sort().map(([k, v]) => `${k} ${v}`).join(', ') || 'none'}`);
+ const frecv = {};
+ for (const x of g) { const k = `${x.name} [${x.classification}]`; frecv[k] = (frecv[k] || 0) + 1; }
+ const top = Object.entries(frecv).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : 1)).slice(0, 10);
+ if (top.length) {
+ L.push('Most frequent:');
+ top.forEach(([k, v], i) => L.push(` ${String(i + 1).padStart(2)}. ${k} x${v}`));
+ }
+ const vf = new Map();
+ for (const x of g) {
+ if (!(x.classification === CLS.V || (x.classification === CLS.W && x.quantumVulnerable))) continue;
+ if (!vf.has(x.file)) vf.set(x.file, []);
+ vf.get(x.file).push(x);
+ }
+ L.push(`Files with quantum-vulnerable findings: ${vf.size}`);
+ for (const [f, lista] of [...vf].sort()) {
+ L.push(` ${f} (${lista.length}: ${[...new Set(lista.map((x) => x.name))].join(', ')})`);
+ }
+ L.push(`Declared crypto libraries (manifests; declaration is not use): ${rez.libraries.length}`);
+ for (const b of rez.libraries) L.push(` ${b.file}:${b.line} ${b.name}${b.version ? ' ' + b.version : ''} (${b.provides})`);
+ if (s.manifestErrors.length) L.push(`Manifest errors: ${s.manifestErrors.join('; ')}`);
+ L.push('Scope: static pattern analysis of source text. Not covered: compiled binaries, transitive dependencies, cryptography chosen at run time.');
+ // numele de fisiere si textele vin din arborele scanat: caracterele de control (secvente ANSI) nu ajung in terminal
+ return L.map((l) => l.replace(/[\u0000-\u001f\u007f-\u009f]/g, '?')).join('\n');
+}
diff --git a/crypto-inventory/lib/scan.mjs b/crypto-inventory/lib/scan.mjs
new file mode 100644
index 0000000..fbf7643
--- /dev/null
+++ b/crypto-inventory/lib/scan.mjs
@@ -0,0 +1,141 @@
+// Parcurgerea dosarului si orchestrarea detectorilor. Nimic nu se sare in tacere: fiecare
+// fisier vazut ajunge intr-o singura categorie numarata, si suma se verifica la sfarsit.
+import { readdirSync, statSync, readFileSync, lstatSync } from 'node:fs';
+import { join, relative, sep, basename, resolve, extname } from 'node:path';
+import { facContext, aplicaSuprimarea, REZOLVARI_PE_FISIER } from './context.mjs';
+import { detecteazaJs } from './detect-js.mjs';
+import { detecteazaPy } from './detect-py.mjs';
+import { detecteazaJava } from './detect-java.mjs';
+import { detecteazaGo } from './detect-go.mjs';
+import { detecteazaPem } from './pem.mjs';
+import { citesteManifest, NUME_MANIFEST } from './manifeste.mjs';
+
+export const VERSIUNE = '0.2.0';
+export const DOSARE_EXCLUSE_IMPLICIT = ['.git', '.hg', '.svn', 'node_modules', '__pycache__', '.venv', 'venv', '.tox', '.gradle', '.idea', '.mypy_cache', '.pytest_cache'];
+
+const LIMBAJ = {
+ '.js': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript', '.jsx': 'javascript',
+ '.ts': 'javascript', '.tsx': 'javascript', '.mts': 'javascript', '.cts': 'javascript',
+ '.py': 'python', '.pyw': 'python',
+ '.java': 'java',
+ '.go': 'go',
+};
+const DETECTOR = { javascript: detecteazaJs, python: detecteazaPy, java: detecteazaJava, go: detecteazaGo };
+const EXEMPLE = 5;
+
+export function limbajul(cale) {
+ return LIMBAJ[extname(cale).toLowerCase()] || null;
+}
+
+// binar = contine un octet NUL in primii 8192 octeti
+export function eBinar(buf) {
+ const n = Math.min(buf.length, 8192);
+ for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
+ return false;
+}
+
+export function scaneaza(radacina, optiuni = {}) {
+ const o = {
+ maxFileBytes: optiuni.maxFileBytes ?? 1048576,
+ maxFiles: optiuni.maxFiles ?? 50000,
+ excludeDirs: new Set([...(optiuni.noDefaultExcludes ? [] : DOSARE_EXCLUSE_IMPLICIT), ...(optiuni.excludeDirs || [])]),
+ };
+ const root = resolve(radacina);
+ const st = statSync(root);
+ if (!st.isDirectory()) throw new Error(`not a directory: ${radacina}`);
+ const inceput = new Date();
+ const stats = {
+ filesSeen: 0,
+ codeFiles: {},
+ textFilesPemOnly: 0,
+ manifests: 0,
+ manifestErrors: [],
+ notRead: { binary: 0, tooLarge: 0, fileLimit: 0, unreadable: 0 },
+ notReadExamples: { binary: [], tooLarge: [], fileLimit: [], unreadable: [] },
+ symlinksNotFollowed: 0,
+ resolutionLimitFiles: 0,
+ resolutionLimitExamples: [],
+ dirsExcluded: {},
+ dirsUnreadable: 0,
+ };
+ const gasiri = [];
+ const biblioteci = [];
+ let cititeSauIncercate = 0;
+ const noteaza = (motiv, rel, extra) => {
+ stats.notRead[motiv]++;
+ if (stats.notReadExamples[motiv].length < EXEMPLE) stats.notReadExamples[motiv].push(extra ? `${rel} (${extra})` : rel);
+ };
+
+ const stiva = [root];
+ while (stiva.length) {
+ const dir = stiva.pop();
+ let intrari;
+ try { intrari = readdirSync(dir, { withFileTypes: true }); } catch { stats.dirsUnreadable++; continue; }
+ intrari.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
+ const subdosare = [];
+ for (const e of intrari) {
+ const abs = join(dir, e.name);
+ if (e.isSymbolicLink()) { stats.symlinksNotFollowed++; continue; }
+ if (e.isDirectory()) {
+ if (o.excludeDirs.has(e.name)) { stats.dirsExcluded[e.name] = (stats.dirsExcluded[e.name] || 0) + 1; continue; }
+ subdosare.push(abs);
+ continue;
+ }
+ if (!e.isFile()) continue;
+ stats.filesSeen++;
+ const rel = relative(root, abs).split(sep).join('/');
+ if (cititeSauIncercate >= o.maxFiles) { noteaza('fileLimit', rel); continue; }
+ cititeSauIncercate++;
+ let marime;
+ try { marime = lstatSync(abs).size; } catch (err) { noteaza('unreadable', rel, err.code); continue; }
+ if (marime > o.maxFileBytes) { noteaza('tooLarge', rel, `${marime} bytes`); continue; }
+ let buf;
+ try { buf = readFileSync(abs); } catch (err) { noteaza('unreadable', rel, err.code); continue; }
+ if (eBinar(buf)) { noteaza('binary', rel); continue; }
+ const text = buf.toString('utf8');
+ const limbaj = limbajul(abs);
+ const ctx = facContext(text, limbaj || 'text', rel);
+ if (limbaj) {
+ stats.codeFiles[limbaj] = (stats.codeFiles[limbaj] || 0) + 1;
+ DETECTOR[limbaj](ctx);
+ if (ctx.rezolvariPestePlafon) {
+ stats.resolutionLimitFiles++;
+ if (stats.resolutionLimitExamples.length < EXEMPLE) stats.resolutionLimitExamples.push(rel);
+ }
+ } else {
+ stats.textFilesPemOnly++;
+ }
+ detecteazaPem(text, ctx);
+ gasiri.push(...aplicaSuprimarea(ctx.gasiri));
+ if (NUME_MANIFEST.test(basename(abs))) {
+ stats.manifests++;
+ const r = citesteManifest(text, basename(abs), rel);
+ if (r.eroare) stats.manifestErrors.push(`${rel}: ${r.eroare}`);
+ biblioteci.push(...r.biblioteci);
+ }
+ }
+ for (let i = subdosare.length - 1; i >= 0; i--) stiva.push(subdosare[i]);
+ }
+
+ // garda: fiecare fisier vazut e intr-o singura categorie
+ const cod = Object.values(stats.codeFiles).reduce((a, b) => a + b, 0);
+ const nr = stats.notRead;
+ const suma = cod + stats.textFilesPemOnly + nr.binary + nr.tooLarge + nr.fileLimit + nr.unreadable;
+ if (suma !== stats.filesSeen) throw new Error(`internal accounting error: ${stats.filesSeen} files seen, ${suma} accounted for`);
+ stats.codeFilesTotal = cod;
+
+ gasiri.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line || a.column - b.column));
+ biblioteci.sort((a, b) => (a.file < b.file ? -1 : a.file > b.file ? 1 : a.line - b.line));
+ return {
+ tool: { name: 'aere-crypto-inventory', version: VERSIUNE },
+ root,
+ rootName: basename(root),
+ options: { maxFileBytes: o.maxFileBytes, maxFiles: o.maxFiles, excludeDirs: [...o.excludeDirs].sort(), maxResolvedVariablesPerFile: REZOLVARI_PE_FISIER },
+ startedAt: inceput.toISOString(),
+ finishedAt: new Date().toISOString(),
+ findings: gasiri,
+ libraries: biblioteci,
+ stats,
+ };
+}
+
diff --git a/crypto-inventory/test/control-negativ-timp.mjs b/crypto-inventory/test/control-negativ-timp.mjs
new file mode 100644
index 0000000..5bac15d
--- /dev/null
+++ b/crypto-inventory/test/control-negativ-timp.mjs
@@ -0,0 +1,80 @@
+// Controlul negativ al probei de cost (test/proba-timp.mjs): proba trebuie sa iasa ROSIE pe cazul numit cand forma patratica e
+// pusa inapoi. Trei stari pe caz: PRINS (proba a masurat si cazul tinta e ROSU), SCAPAT (a ramas VERDE), STRICAT (copia nu s-a
+// putut face, ancora nu apare exact o data, sau proba nu a masurat).
+// V0 inventarul de dinainte de reparatie, din git (sarit si spus daca revizia nu e in istoricul depozitului)
+// -> rosii: pem, py-triplu, js-regex, js-linie, java-linie, java-kpg
+// P1 rezolvarea variabilelor fara memorie (fiecare apel reciteste fisierul) -> js-linie rosu
+// P2 esecurile literalului regex JS netinute minte -> js-regex rosu
+// P3 cautarea PEM fara pozitiile tinute minte (sfarsitul cautat de la fiecare antet) -> pem rosu
+// node test/control-negativ-timp.mjs -> 0 toate prinse, 1 unul scapat, 2 STRICAT
+import { mkdtempSync, cpSync, readFileSync, writeFileSync, readdirSync, rmSync, mkdirSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { spawnSync } from 'node:child_process';
+import { tmpdir } from 'node:os';
+
+const AICI = dirname(fileURLToPath(import.meta.url));
+const RADACINA = join(AICI, '..');
+const PROBA = join(AICI, 'proba-timp.mjs');
+const REV = process.env.AERE_INV_REV_VECHE || 'db9d9543';
+let prinse = 0, stricate = 0, total = 0;
+
+const copiaza = () => {
+ const d = mkdtempSync(join(tmpdir(), 'ci-timp-control-'));
+ cpSync(join(RADACINA, 'inventar.mjs'), join(d, 'inventar.mjs'));
+ cpSync(join(RADACINA, 'lib'), join(d, 'lib'), { recursive: true });
+ return d;
+};
+
+function ruleaza(id, d, tinte, doar) {
+ const r = spawnSync(process.execPath, [PROBA, ...(doar ? ['--doar', doar.join(',')] : [])], { encoding: 'utf8', timeout: 600000, env: { ...process.env, CRYPTO_INVENTORY_MODULE: join(d, 'inventar.mjs') } });
+ const out = r.stdout || '';
+ if (!out.includes(`(modul: ${join(d, 'inventar.mjs')})`) || /^STRICAT/m.test(out)) { stricate++; console.log(` STRICAT ${id}: proba nu a masurat copia (cod ${r.status}) ${out.split('\n').filter((l) => /^STRICAT/.test(l)).join(' | ').slice(0, 160)}`); return; }
+ const rosii = out.split('\n').filter((l) => /^ROSU/.test(l)).map((l) => l.split(/\s+/)[1].replace(/:$/, ''));
+ const lipsa = tinte.filter((t) => !rosii.includes(t));
+ if (r.status === 1 && !lipsa.length) { prinse++; console.log(` PRINS ${id}: rosii ${rosii.join(', ')}`); }
+ else console.log(` SCAPAT ${id}: cod ${r.status}, au ramas verzi: ${lipsa.join(', ')}`);
+}
+
+// V0: codul de dinainte, din git, in forma comisa (fara conversia capetelor de rand)
+total++;
+const git = (...a) => spawnSync('git', ['-c', 'core.autocrlf=false', ...a], { cwd: RADACINA, encoding: 'buffer', maxBuffer: 64 << 20 });
+const ls = git('ls-tree', '-r', '--name-only', '--full-tree', REV, 'tools/crypto-inventory/inventar.mjs', 'tools/crypto-inventory/lib');
+const fisiere = ls.status === 0 ? ls.stdout.toString().split('\n').filter(Boolean) : [];
+if (!fisiere.length) { total--; console.log(` SARIT V0: revizia ${REV} nu e in istoricul acestui depozit; NEMASURAT aici`); }
+else {
+ const d = mkdtempSync(join(tmpdir(), 'ci-timp-v0-'));
+ try {
+ let ok = true;
+ for (const f of fisiere) {
+ const b = git('show', `${REV}:${f}`);
+ if (b.status !== 0) { ok = false; break; }
+ const t = join(d, f.slice('tools/crypto-inventory/'.length));
+ mkdirSync(dirname(t), { recursive: true });
+ writeFileSync(t, b.stdout);
+ }
+ if (!ok || readFileSync(join(d, 'lib', 'pem.mjs'), 'utf8').includes('blocuriPem')) { stricate++; console.log(' STRICAT V0: revizia veche nu se citeste sau are deja reparatia'); }
+ else ruleaza('V0', d, ['pem.txt', 'py-triplu.py', 'js-regex.js', 'js-linie.js', 'java-linie.java', 'java-kpg.java']);
+ } finally { rmSync(d, { recursive: true, force: true }); }
+}
+
+const P = [
+ ['P1', 'lib/context.mjs', 'if (ctx.rezolvari.has(nume)) return ctx.rezolvari.get(nume);', "if (ctx.rezolvari.has(nume) && process.env.AERE_PLANTA_NICIODATA === 'da') return ctx.rezolvari.get(nume);", ['js-linie.js']],
+ ['P2', 'lib/lexer.mjs', 'if (esecRegex && esecRegex[j] & (inClasa ? 2 : 1)) break;', "if (esecRegex && esecRegex[j] & (inClasa ? 2 : 1) && process.env.AERE_PLANTA_NICIODATA === 'da') break;", ['js-regex.js']],
+ ['P3', 'lib/pem.mjs', 'const valabil = (c, de) => c && c.la <= de && (c.poz < 0 || c.poz >= de);', "const valabil = (c, de) => process.env.AERE_PLANTA_NICIODATA === 'da' && c && c.la <= de && (c.poz < 0 || c.poz >= de);", ['pem.txt']],
+];
+for (const [id, fisier, ancora, cu, tinte] of P) {
+ total++;
+ const d = copiaza();
+ try {
+ const cale = join(d, fisier);
+ const text = readFileSync(cale, 'utf8');
+ if (text.split(ancora).length !== 2) { stricate++; console.log(` STRICAT ${id}: ancora nu apare exact o data in ${fisier}`); continue; }
+ writeFileSync(cale, text.replace(ancora, cu));
+ const dif = ['inventar.mjs', ...readdirSync(join(d, 'lib')).map((f) => `lib/${f}`)].filter((f) => readFileSync(join(d, f), 'utf8') !== readFileSync(join(RADACINA, f), 'utf8'));
+ if (dif.length !== 1 || dif[0] !== fisier) { stricate++; console.log(` STRICAT ${id}: copia difera in ${dif.join(', ') || 'nimic'}`); continue; }
+ ruleaza(id, d, tinte, tinte);
+ } finally { rmSync(d, { recursive: true, force: true }); }
+}
+console.log(`\ncontrolul negativ al probei de cost: prinse ${prinse}/${total}, stricate ${stricate}`);
+process.exitCode = stricate ? 2 : prinse === total ? 0 : 1;
diff --git a/crypto-inventory/test/control-negativ.mjs b/crypto-inventory/test/control-negativ.mjs
new file mode 100644
index 0000000..c6e863c
--- /dev/null
+++ b/crypto-inventory/test/control-negativ.mjs
@@ -0,0 +1,242 @@
+// Controlul negativ al suitei: strica, intr-o COPIE a modulului, cate o regula sau un detector si
+// cere ca proba tinta sa iasa ROSIE pe numele ei, ca suita sa fi rulat INTREAGA (aceleasi nume ca
+// linia de baza) si ca martorul neatins sa ramana VERDE. Altfel verdictul e STRICAT, nu "prins".
+// Un ciot care nu se poate pune (ancora lipsa sau dubla) e tot STRICAT: un control care nu a
+// plantat nimic nu a masurat nimic.
+// Folosire: node test/control-negativ.mjs (cod 0 doar daca toate mutatiile sunt PRINSE)
+import { mkdtempSync, cpSync, readFileSync, writeFileSync, readdirSync, rmSync, statSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { spawnSync } from 'node:child_process';
+import { tmpdir } from 'node:os';
+
+const AICI = dirname(fileURLToPath(import.meta.url));
+const RADACINA = join(AICI, '..');
+const SUITA = join(AICI, 'proba.mjs');
+
+const B = 'negativ: fisierul binar e sarit si numarat';
+const MUTATII = [
+ {
+ nume: 'secp256k1 clasificat quantum-safe',
+ fisier: 'lib/catalog.mjs',
+ ancora: "return f({ ...baza, ...rez({ c: CLS.V, qv: true, motiv, rec, q: 0, cl, oid: '1.2.840.10045.2.1' }) });",
+ cu: "return f({ ...baza, ...rez({ c: e256k1 ? CLS.S : CLS.V, qv: true, motiv, rec, q: 0, cl, oid: '1.2.840.10045.2.1' }) });",
+ tinta: 'clasificare: secp256k1 este quantum-vulnerable, cu recomandare de cont post-cuantic',
+ martor: B,
+ },
+ {
+ nume: 'comentariile // nu mai sunt ignorate (JS/Java/Go)',
+ fisier: 'lib/lexer.mjs',
+ ancora: "if (c === '/' && d === '/') {",
+ cu: "if (false && c === '/' && d === '/') {",
+ tinta: 'negativ: comentariile JS nu produc gasiri',
+ martor: B,
+ },
+ {
+ nume: 'linia raportata cu unu mai mult',
+ fisier: 'lib/lexer.mjs',
+ ancora: 'return { line: lo + 1, column: pos - inceputuri[lo] + 1 };',
+ cu: 'return { line: lo + 2, column: pos - inceputuri[lo] + 1 };',
+ tinta: 'pozitiv: js/node-crypto.mjs',
+ martor: 'negativ: fisier fara criptografie da zero gasiri, si a fost citit',
+ },
+ {
+ nume: 'variabila reasignata e "rezolvata" la prima valoare (ghicit)',
+ fisier: 'lib/context.mjs',
+ ancora: 'if (atribuiri.length !== 1) return null;',
+ cu: 'if (atribuiri.length < 1) return null;',
+ tinta: 'negativ: algoritm din variabila reasignata sau din parametru iese unknown, nu ghicit',
+ martor: B,
+ },
+ {
+ nume: 'fisierele binare nu mai sunt sarite',
+ fisier: 'lib/scan.mjs',
+ ancora: "if (eBinar(buf)) { noteaza('binary', rel); continue; }",
+ cu: "if (false && eBinar(buf)) { noteaza('binary', rel); continue; }",
+ tinta: B,
+ martor: 'negativ: comentariile JS nu produc gasiri',
+ },
+ {
+ nume: 'comentariile # nu mai sunt ignorate (Python)',
+ fisier: 'lib/lexer.mjs',
+ ancora: " if (c === '#') {",
+ cu: " if (false && c === '#') {",
+ tinta: 'negativ: comentariile si docstring-urile Python nu produc gasiri',
+ martor: 'pozitiv: js/node-crypto.mjs',
+ },
+ // 2026-09-29, revizuirea adversariala: formele liniare trebuie sa dea exact ce dadeau cele vechi
+ {
+ nume: 'cautarea PEM nu mai reia de la caracterul urmator dupa un antet fara sfarsit',
+ fisier: 'lib/pem.mjs',
+ ancora: '{ reBegin.lastIndex = m.index + 1; continue; }',
+ cu: "{ if (process.env.AERE_PLANTA_NICIODATA === 'da') reBegin.lastIndex = m.index + 1; continue; }",
+ tinta: 'limite: un bloc PEM care incepe in liniutele unui antet fara sfarsit e gasit',
+ martor: B,
+ },
+ {
+ nume: 'esecul unui literal regex JS tinut minte fara starea "in clasa"',
+ fisier: 'lib/lexer.mjs',
+ ancora: 'if (esecRegex && esecRegex[j] & (inClasa ? 2 : 1)) break;',
+ cu: "if (esecRegex && (esecRegex[j] & (inClasa ? 2 : 1) || (process.env.AERE_PLANTA_NICIODATA !== 'da' && esecRegex[j]))) break;",
+ tinta: 'limite: literalul regex JS se cauta din starea lui, nu din ce a esuat alt literal pe acelasi rand',
+ martor: B,
+ },
+ {
+ nume: 'sirul neterminat Python nu mai muta inceputul randului',
+ fisier: 'lib/lexer.mjs',
+ ancora: 'if (ultimaLinieNoua >= 0) primulPeRand = ultimaLinieNoua === j - 1 ? -1 : ultimaLinieNoua + 1;',
+ cu: "if (ultimaLinieNoua >= 0) primulPeRand = ultimaLinieNoua === j - 1 && process.env.AERE_PLANTA_NICIODATA === 'da' ? -1 : ultimaLinieNoua + 1;",
+ tinta: 'limite: docstring Python pe randul lui (si dupa un sir neterminat), dar nu dupa cod pe acelasi rand',
+ martor: B,
+ },
+ {
+ nume: 'plafonul de variabile pe fisier scos',
+ fisier: 'lib/context.mjs',
+ ancora: 'if (ctx.rezolvari.size >= REZOLVARI_PE_FISIER) {',
+ cu: "if (ctx.rezolvari.size >= REZOLVARI_PE_FISIER && process.env.AERE_PLANTA_NICIODATA === 'da') {",
+ tinta: 'limite: peste plafonul de variabile pe fisier numele ies unknown si fisierul se numara',
+ martor: B,
+ },
+ {
+ nume: 'caracterele de control din rezumat nu mai sunt inlocuite',
+ fisier: 'lib/rezumat.mjs',
+ ancora: "return L.map((l) => l.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/g, '?')).join('\\n');",
+ cu: "return L.map((l) => (process.env.AERE_PLANTA_NICIODATA === 'da' ? l.replace(/[\\u0000-\\u001f\\u007f-\\u009f]/g, '?') : l)).join('\\n');",
+ tinta: 'rezumat: caracterele de control din numele de fisiere nu ajung in terminal',
+ martor: B,
+ },
+ {
+ nume: 'MD5 clasificat quantum-safe',
+ fisier: 'lib/catalog.mjs',
+ ancora: "MD5: { n: 'MD5', c: CLS.W, oid: '1.2.840.113549.2.5' },",
+ cu: "MD5: { n: 'MD5', c: CLS.S, oid: '1.2.840.113549.2.5' },",
+ tinta: 'clasificare: MD5 si SHA-1 sunt weak-now, SHA-256 quantum-safe',
+ martor: 'negativ: fisier prea mare sarit si numarat',
+ },
+ {
+ nume: 'bom-ref fara amprenta (coliziuni)',
+ fisier: 'lib/cbom.mjs',
+ ancora: ":${createHash('sha256').update(k).digest('hex').slice(0, 12)}`;\n const cp",
+ cu: '`;\n const cp',
+ tinta: 'cbom: bom-ref unice si stabile intre doua rulari',
+ martor: B,
+ },
+ {
+ nume: '--fail-on vulnerable ignorat',
+ fisier: 'lib/rezumat.mjs',
+ ancora: "if ((cer.has('vulnerable') || cer.has('any')) && vuln.length)",
+ cu: "if (false && (cer.has('vulnerable') || cer.has('any')) && vuln.length)",
+ tinta: 'cli: --fail-on vulnerable iese 1 pe cod vulnerabil si 0 pe cod fara criptografie',
+ martor: 'cbom: structura CycloneDX 1.6 (campuri obligatorii, tipuri, enumerari)',
+ },
+ {
+ nume: 'apelurile din siruri numarate ca folosire',
+ fisier: 'lib/context.mjs',
+ ancora: 'for (const m of code.matchAll(g)) if (!inSir(siruri, m.index)) r.push(m);',
+ cu: 'for (const m of code.matchAll(g)) r.push(m);',
+ tinta: 'negativ: apelul scris intr-un sir JS nu e folosire',
+ martor: B,
+ },
+ {
+ nume: 'fisierul prea mare sarit fara sa fie numarat',
+ fisier: 'lib/scan.mjs',
+ ancora: "if (marime > o.maxFileBytes) { noteaza('tooLarge', rel, `${marime} bytes`); continue; }",
+ cu: 'if (marime > o.maxFileBytes) { continue; }',
+ tinta: 'negativ: fisier prea mare sarit si numarat',
+ martor: 'pozitiv: js/node-crypto.mjs',
+ },
+ {
+ nume: 'suprimarea importurilor acoperite de apeluri scoasa (numarare dubla)',
+ fisier: 'lib/context.mjs',
+ ancora: " if (g.evidenceKind !== 'import' || !g.suppressedBy) return true;",
+ cu: ' return true;',
+ tinta: 'pozitiv: go/crypto.go',
+ martor: B,
+ },
+ {
+ nume: 'blocul PEM intreg (cu cheia privata) pus in contextul gasirii',
+ fisier: 'lib/pem.mjs',
+ ancora: '{ bucata: `-----BEGIN ${tip}-----` }',
+ cu: '{ bucata: m[0] }',
+ tinta: 'pozitiv: cheie privata generata la rulare e raportata, iar valoarea ei nu apare in iesire',
+ martor: B,
+ },
+];
+
+function copiaza() {
+ const d = mkdtempSync(join(tmpdir(), 'ci-control-'));
+ cpSync(join(RADACINA, 'inventar.mjs'), join(d, 'inventar.mjs'));
+ cpSync(join(RADACINA, 'lib'), join(d, 'lib'), { recursive: true });
+ return d;
+}
+
+function fisiereModul(d) {
+ return ['inventar.mjs', ...readdirSync(join(d, 'lib')).map((f) => `lib/${f}`)].filter((f) => statSync(join(d, f)).isFile()).sort();
+}
+
+function ruleaza(d) {
+ const r = spawnSync(process.execPath, [SUITA, '--json'], {
+ encoding: 'utf8',
+ timeout: 300000,
+ env: { ...process.env, CRYPTO_INVENTORY_MODULE: join(d, 'inventar.mjs') },
+ });
+ const ultima = (r.stdout || '').trim().split('\n').pop() || '';
+ try {
+ const j = JSON.parse(ultima);
+ return { ok: true, j, status: r.status };
+ } catch {
+ return { ok: false, motiv: `suita nu a raportat JSON (status ${r.status}): ${(r.stderr || '').trim().split('\n').slice(0, 3).join(' | ')}` };
+ }
+}
+
+// linia de baza: o copie neatinsa, toata verde, si suita chiar a masurat COPIA
+const baza = copiaza();
+let linieBaza;
+try {
+ const r = ruleaza(baza);
+ if (!r.ok) { console.log(`STRICAT linia de baza: ${r.motiv}`); process.exitCode = 3; }
+ else if (r.j.modul !== join(baza, 'inventar.mjs')) { console.log(`STRICAT linia de baza: suita a masurat ${r.j.modul}, nu copia`); process.exitCode = 3; }
+ else if (r.j.verzi !== r.j.rulate) { console.log(`STRICAT linia de baza: ${r.j.rosii} probe rosii pe copia neatinsa: ${Object.keys(r.j.erori).join('; ')}`); process.exitCode = 3; }
+ else linieBaza = r.j;
+} finally { rmSync(baza, { recursive: true, force: true }); }
+
+if (linieBaza) {
+ const numeBaza = Object.keys(linieBaza.rezultate).sort();
+ console.log(`linia de baza: ${linieBaza.rulate} probe, toate verzi, pe o copie neatinsa a modulului\n`);
+ const verdicte = [];
+ for (const mu of MUTATII) {
+ const d = copiaza();
+ let verdict;
+ let detaliu = '';
+ try {
+ const cale = join(d, mu.fisier);
+ const text = readFileSync(cale, 'utf8');
+ const n = text.split(mu.ancora).length - 1;
+ if (!numeBaza.includes(mu.tinta) || !numeBaza.includes(mu.martor)) {
+ verdict = 'STRICAT'; detaliu = 'tinta sau martorul nu exista in suita';
+ } else if (n !== 1) {
+ verdict = 'STRICAT'; detaliu = `ciotul nu s-a putut pune: ancora apare de ${n} ori`;
+ } else {
+ writeFileSync(cale, text.replace(mu.ancora, mu.cu));
+ // cmp: exact un fisier difera de original, si acela e cel mutat
+ const diferite = fisiereModul(d).filter((f) => readFileSync(join(d, f), 'utf8') !== readFileSync(join(RADACINA, f), 'utf8'));
+ if (diferite.length !== 1 || diferite[0] !== mu.fisier) {
+ verdict = 'STRICAT'; detaliu = `copia difera in: ${diferite.join(', ') || 'nimic'}`;
+ } else {
+ const r = ruleaza(d);
+ if (!r.ok) { verdict = 'STRICAT'; detaliu = r.motiv; }
+ else if (r.j.modul !== join(d, 'inventar.mjs')) { verdict = 'STRICAT'; detaliu = `suita a masurat ${r.j.modul}`; }
+ else if (JSON.stringify(Object.keys(r.j.rezultate).sort()) !== JSON.stringify(numeBaza)) { verdict = 'STRICAT'; detaliu = `suita nu a rulat intreaga: ${r.j.rulate} din ${linieBaza.rulate}`; }
+ else if (r.j.rezultate[mu.tinta] !== 'ROSU') { verdict = 'NEPRINS'; detaliu = 'proba tinta a ramas verde'; }
+ else if (r.j.rezultate[mu.martor] !== 'VERDE') { verdict = 'STRICAT'; detaliu = `martorul a iesit rosu: ${r.j.erori[mu.martor]}`; }
+ else { verdict = 'PRINS'; detaliu = `tinta rosie (${r.j.erori[mu.tinta].slice(0, 110)}); ${r.j.rosii} din ${r.j.rulate} rosii in total; martor verde`; }
+ }
+ }
+ } finally { rmSync(d, { recursive: true, force: true }); }
+ verdicte.push(verdict);
+ console.log(`${verdict.padEnd(8)} ${mu.nume}\n tinta: ${mu.tinta}\n ${detaliu}`);
+ }
+ const prinse = verdicte.filter((v) => v === 'PRINS').length;
+ console.log(`\n${prinse} din ${MUTATII.length} mutatii PRINSE; ${verdicte.filter((v) => v === 'NEPRINS').length} neprinse; ${verdicte.filter((v) => v === 'STRICAT').length} stricate`);
+ process.exitCode = prinse === MUTATII.length ? 0 : 1;
+}
diff --git a/crypto-inventory/test/echivalenta-0.1.0.mjs b/crypto-inventory/test/echivalenta-0.1.0.mjs
new file mode 100644
index 0000000..74fa0b2
--- /dev/null
+++ b/crypto-inventory/test/echivalenta-0.1.0.mjs
@@ -0,0 +1,87 @@
+// Echivalenta formelor liniare din 0.2.0 cu formele din 0.1.0 (2026-09-29, revizuirea adversariala): aceleasi gasiri si biblioteci
+// pe arbori reali, si aceleasi rezultate ale lexerului si ale cautarii PEM pe texte generate din jetoanele care conteaza.
+// 0.1.0 se ia din git (revizia AERE_INV_REV_VECHE, implicit db9d9543), in forma comisa; intr-o copie fara istoric iese SARIT.
+// node test/echivalenta-0.1.0.mjs [--iteratii N] [dosar...] (implicit: fixturile si dosarul uneltei)
+// -> 0 identic, 1 o diferenta (tiparita), 2 NEMASURAT (revizia veche lipseste sau nimic comparat)
+// Singura diferenta asteptata e textul motivului pentru "unknown" (spune acum si plafonul de variabile); se normalizeaza.
+import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
+import { join, dirname } from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { spawnSync } from 'node:child_process';
+import { tmpdir } from 'node:os';
+
+const AICI = dirname(fileURLToPath(import.meta.url));
+const RAD = join(AICI, '..');
+const REV = process.env.AERE_INV_REV_VECHE || 'db9d9543';
+let ITER = 20000;
+const dosare = [];
+for (let k = 2; k < process.argv.length; k++) {
+ if (process.argv[k] === '--iteratii') ITER = Number(process.argv[++k]);
+ else dosare.push(process.argv[k]);
+}
+const DOSARE = dosare.length ? dosare : [join(AICI, 'fixturi'), RAD];
+
+const git = (...a) => spawnSync('git', ['-c', 'core.autocrlf=false', ...a], { cwd: RAD, encoding: 'buffer', maxBuffer: 64 << 20 });
+const ls = git('ls-tree', '-r', '--name-only', '--full-tree', REV, 'tools/crypto-inventory/inventar.mjs', 'tools/crypto-inventory/lib');
+const fis = ls.status === 0 ? ls.stdout.toString().split('\n').filter(Boolean) : [];
+if (!fis.length) { console.log(`SARIT: revizia ${REV} nu e in istoricul acestui depozit; NEMASURAT aici`); process.exit(2); }
+const vechi = mkdtempSync(join(tmpdir(), 'ci-v010-'));
+let dif = 0, comparate = 0;
+try {
+ for (const f of fis) {
+ const b = git('show', `${REV}:${f}`);
+ if (b.status !== 0) { console.log(`NEMASURAT: git show ${f}`); process.exit(2); }
+ const t = join(vechi, f.slice('tools/crypto-inventory/'.length));
+ mkdirSync(dirname(t), { recursive: true });
+ writeFileSync(t, b.stdout);
+ }
+ const imp = (r, m) => import(pathToFileURL(join(r, 'lib', m)).href);
+ const [SV, SN, LV, LN, PN] = await Promise.all([imp(vechi, 'scan.mjs'), imp(RAD, 'scan.mjs'), imp(vechi, 'lexer.mjs'), imp(RAD, 'lexer.mjs'), imp(RAD, 'pem.mjs')]);
+ if (SV.VERSIUNE !== '0.1.0') { console.log(`NEMASURAT: revizia ${REV} are versiunea ${SV.VERSIUNE}, nu 0.1.0`); process.exit(2); }
+
+ // 1. arbori reali
+ const norm = (g) => JSON.stringify({ ...g, reason: String(g.reason).replace(/, which (cannot be resolved statically|this tool does not resolve statically).*$/, ', which [motiv]') });
+ for (const d of DOSARE) {
+ const a = SV.scaneaza(d), b = SN.scaneaza(d);
+ const ka = a.findings.map(norm), kb = b.findings.map(norm);
+ const sa = new Set(ka), sb = new Set(kb);
+ const doarV = ka.filter((x) => !sb.has(x)), doarN = kb.filter((x) => !sa.has(x));
+ const ok = !doarV.length && !doarN.length && ka.length === kb.length && JSON.stringify(a.libraries) === JSON.stringify(b.libraries);
+ comparate += ka.length;
+ if (!ok) dif++;
+ console.log(`${ok ? 'IDENTIC ' : 'DIFERIT '} ${d}: ${a.stats.filesSeen} fisiere, gasiri ${ka.length}/${kb.length}, biblioteci ${a.libraries.length}/${b.libraries.length}`);
+ for (const x of [...doarV.slice(0, 3).map((x) => 'numai 0.1.0: ' + x), ...doarN.slice(0, 3).map((x) => 'numai 0.2.0: ' + x)]) console.log(' ' + x.slice(0, 260));
+ }
+
+ // 2. texte generate: lexerul pe patru limbaje si cautarea PEM (fata de regexul din 0.1.0)
+ const RE_010 = /-----BEGIN ((?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY|PUBLIC KEY|RSA PUBLIC KEY|CERTIFICATE)-----([A-Za-z0-9+/=\s:,\-\r\n]*?)-----END \1-----/g;
+ let seed = 12345;
+ const rnd = (n) => { seed = (seed * 1103515245 + 12345) & 0x7fffffff; return seed % n; };
+ const gen = (jet, max) => { let s = ''; const L = rnd(max); for (let i = 0; i < L; i++) s += jet[rnd(jet.length)]; return s; };
+ const JET = {
+ python: ['"', "'", '"""', "'''", '\\', '\n', ' ', '\t', 'r', 'b', 'f', 'u', 'x', '#', '=', '(', ')', 'rb', '\r\n', 'a1'],
+ javascript: ['=/[', '=/', ' x/', '"', "'", '`', '${', '}', '{', '/', '//', '/*', '*/', '[', ']', '\\', '\n', ' ', '=', '(', ')', 'return', 'x', '1', '.', ',', 'typeof'],
+ java: ['"', "'", '"""', '\\', '\n', ' ', '/', '//', '/*', '*/', 'x', '=', '(', ')'],
+ go: ['"', "'", '`', '\\', '\n', ' ', '/', '//', '/*', '*/', 'x', '=', '(', ')'],
+ };
+ const JET_PEM = ['-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----', '-----BEGIN PRIVATE KEY-----', '-----END PRIVATE KEY-----',
+ '-----BEGIN RSA PRIVATE KEY-----', '-----END RSA PRIVATE KEY-----', '-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----',
+ 'BEGIN CERTIFICATE-----', 'BEGIN PRIVATE KEY-----', 'AAAA', 'QUJD', '\n', ' ', '.', '-', '--', '-----', '"', 'BEGIN', '=', ':', 'Â ', '\r\n'];
+ const cat = {};
+ for (let k = 0; k < ITER; k++) {
+ for (const [limbaj, jet] of Object.entries(JET)) {
+ const t = gen(jet, 40);
+ const a = LV.curata(t, limbaj), b = LN.curata(t, limbaj);
+ comparate++;
+ if (a.code !== b.code || JSON.stringify(a.siruri) !== JSON.stringify(b.siruri)) { cat[limbaj] = (cat[limbaj] || 0) + 1; if (dif++ < 5) console.log(`DIFERIT lexer ${limbaj}: ${JSON.stringify(t)}`); }
+ }
+ const t = gen(JET_PEM, 30);
+ const a = [...t.matchAll(RE_010)].map((m) => [m.index, m[1], m[2]]);
+ const b = [...PN.blocuriPem(t)].map((x) => [x.index, x.tip, x.corp]);
+ comparate++;
+ if (JSON.stringify(a) !== JSON.stringify(b)) { cat.pem = (cat.pem || 0) + 1; if (dif++ < 5) console.log(`DIFERIT pem: ${JSON.stringify(t)}`); }
+ }
+ console.log(`texte generate: ${ITER} runde (lexer x4 + PEM), diferente ${JSON.stringify(cat)}`);
+} finally { rmSync(vechi, { recursive: true, force: true }); }
+console.log(`\n${dif ? 'DIFERIT' : 'IDENTIC'}: ${dif} diferente in ${comparate} comparatii (0.1.0 din ${REV} fata de 0.2.0)`);
+process.exitCode = dif ? 1 : comparate ? 0 : 2;
diff --git a/crypto-inventory/test/fixturi/binar/modul.js b/crypto-inventory/test/fixturi/binar/modul.js
new file mode 100644
index 0000000..4122412
Binary files /dev/null and b/crypto-inventory/test/fixturi/binar/modul.js differ
diff --git a/crypto-inventory/test/fixturi/go/crypto.go b/crypto-inventory/test/fixturi/go/crypto.go
new file mode 100644
index 0000000..9ea0588
--- /dev/null
+++ b/crypto-inventory/test/fixturi/go/crypto.go
@@ -0,0 +1,30 @@
+// Fixtura: Go. In Go un import nefolosit nu compileaza, deci importul e dovada de folosire.
+package fixturi
+
+import (
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/hmac"
+ "crypto/md5"
+ "crypto/mlkem"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/sha1"
+ "crypto/sha256" // EXPECT: SHA-256 | quantum-safe
+ "crypto/tls"
+ _ "crypto/sha512" // EXPECT: SHA-512 | quantum-safe
+)
+
+func Exemple(data []byte) {
+ k, _ := rsa.GenerateKey(rand.Reader, 3072) // EXPECT: RSA-3072 | quantum-vulnerable
+ e, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) // EXPECT: ECDSA-secp256r1 | quantum-vulnerable
+ s := md5.Sum(data) // EXPECT: MD5 | weak-now
+ m := hmac.New(sha1.New, data) // EXPECT: HMAC-SHA-1 | weak-now
+ dk, _ := mlkem.GenerateKey768() // EXPECT: ML-KEM-768 | quantum-safe
+ cfg := &tls.Config{
+ MinVersion: tls.VersionTLS12, // EXPECT: TLSv1.2 | quantum-vulnerable
+ CurvePreferences: []tls.CurveID{tls.X25519MLKEM768, tls.CurveP256}, // EXPECT: X25519MLKEM768 | quantum-safe ; ECDH-secp256r1 | quantum-vulnerable
+ }
+ f := sha256.New
+ _, _, _, _, _, _, _ = k, e, s, m, dk, cfg, f
+}
diff --git a/crypto-inventory/test/fixturi/java/Crypto.java b/crypto-inventory/test/fixturi/java/Crypto.java
new file mode 100644
index 0000000..d2aeb73
--- /dev/null
+++ b/crypto-inventory/test/fixturi/java/Crypto.java
@@ -0,0 +1,36 @@
+// Fixtura: JCA, JSSE si Bouncy Castle. Fisierul nu se compileaza; e doar text pentru scaner.
+package fixturi;
+
+import java.security.KeyPairGenerator;
+import java.security.MessageDigest;
+import java.security.Signature;
+import java.security.spec.ECGenParameterSpec;
+import javax.crypto.Cipher;
+import javax.crypto.KeyAgreement;
+import javax.net.ssl.SSLContext;
+import org.bouncycastle.crypto.engines.RC4Engine; // EXPECT: RC4 | weak-now
+import org.bouncycastle.pqc.crypto.mlkem.MLKEMParameters;
+
+public class Crypto {
+ private static final String DIGEST = "SHA-256";
+
+ public void exemple(String algDinParametru) throws Exception {
+ KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); // EXPECT: RSA-4096 | quantum-vulnerable
+ kpg.initialize(4096);
+ KeyPairGenerator ec = KeyPairGenerator.getInstance("EC"); // EXPECT: EC-secp256r1 | quantum-vulnerable
+ ec.initialize(new ECGenParameterSpec("secp256r1"));
+ Signature s1 = Signature.getInstance("SHA256withECDSA"); // EXPECT: ECDSA | quantum-vulnerable
+ Signature s2 = Signature.getInstance("SHA1withRSA"); // EXPECT: RSA | weak-now
+ Signature s3 = Signature.getInstance("ML-DSA-65"); // EXPECT: ML-DSA-65 | quantum-safe
+ Cipher c1 = Cipher.getInstance("AES"); // EXPECT: AES-ECB | weak-now
+ Cipher c2 = Cipher.getInstance("AES/GCM/NoPadding"); // EXPECT: AES-GCM | quantum-safe
+ Cipher c3 = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); // EXPECT: RSA | quantum-vulnerable
+ Cipher c4 = Cipher.getInstance("DESede/CBC/PKCS5Padding"); // EXPECT: 3DES-CBC | weak-now
+ KeyAgreement ka = KeyAgreement.getInstance("X25519"); // EXPECT: X25519 | quantum-vulnerable
+ MessageDigest md = MessageDigest.getInstance("MD5"); // EXPECT: MD5 | weak-now
+ MessageDigest md2 = MessageDigest.getInstance(DIGEST); // EXPECT: SHA-256 | quantum-safe
+ MessageDigest md3 = MessageDigest.getInstance(algDinParametru); // EXPECT: MessageDigest.getInstance | unknown
+ SSLContext tls = SSLContext.getInstance("TLSv1.1"); // EXPECT: TLSv1.1 | weak-now
+ Object p = MLKEMParameters.ml_kem_768; // EXPECT: ML-KEM-768 | quantum-safe
+ }
+}
diff --git a/crypto-inventory/test/fixturi/js/biblioteci.ts b/crypto-inventory/test/fixturi/js/biblioteci.ts
new file mode 100644
index 0000000..e7cd9e4
--- /dev/null
+++ b/crypto-inventory/test/fixturi/js/biblioteci.ts
@@ -0,0 +1,19 @@
+// Fixtura: biblioteci recunoscute din importuri (TypeScript).
+import { Wallet, verifyMessage } from 'ethers';
+import jwt from 'jsonwebtoken';
+import { secp256k1 } from '@noble/curves/secp256k1'; // EXPECT: ECDSA-secp256k1 | quantum-vulnerable
+import { ml_dsa65 } from '@noble/post-quantum/ml-dsa'; // EXPECT: ML-DSA-65 | quantum-safe
+import { ml_kem768 } from '@noble/post-quantum/ml-kem'; // EXPECT: ML-KEM-768 | quantum-safe
+import elliptic from 'elliptic';
+import { sha1 } from '../vendor/node_modules/@noble/hashes/legacy.js'; // EXPECT: SHA-1 | weak-now
+
+const ec = new elliptic.ec('p256'); // EXPECT: EC-secp256r1 | quantum-vulnerable
+
+export async function semneaza(privateKey: string, payload: object) {
+ const w = new Wallet(privateKey); // EXPECT: ECDSA-secp256k1 | quantum-vulnerable
+ const sig = await w.signMessage('hello'); // EXPECT: ECDSA-secp256k1 | quantum-vulnerable
+ const ok = verifyMessage('hello', sig); // EXPECT: ECDSA-secp256k1 | quantum-vulnerable
+ const t1 = jwt.sign(payload, privateKey, { algorithm: 'RS256' }); // EXPECT: JWS RS256 | quantum-vulnerable
+ const t2 = jwt.verify(t1, privateKey, { algorithms: ['ES256', 'HS256'] }); // EXPECT: JWS ES256 | quantum-vulnerable ; JWS HS256 | quantum-safe
+ return { ec, ok, t2, secp256k1, ml_dsa65, ml_kem768, sha1 };
+}
diff --git a/crypto-inventory/test/fixturi/js/node-crypto.mjs b/crypto-inventory/test/fixturi/js/node-crypto.mjs
new file mode 100644
index 0000000..4b1dac9
--- /dev/null
+++ b/crypto-inventory/test/fixturi/js/node-crypto.mjs
@@ -0,0 +1,31 @@
+// Fixtura: node:crypto. Fiecare rand cu o folosire poarta marcajul EXPECT al gasirii asteptate.
+// Fisierul nu se executa niciodata; e doar text pentru scaner.
+import crypto from 'node:crypto';
+import { createHash, generateKeyPairSync } from 'crypto';
+
+const HASH_ALG = 'sha256';
+
+export function exemple(data, key, iv, algDinParametru, pemDinConfig) {
+ const a = createHash('md5').update(data).digest('hex'); // EXPECT: MD5 | weak-now
+ const b = crypto.createHash('sha1'); // EXPECT: SHA-1 | weak-now
+ const c = crypto.createHash(HASH_ALG); // EXPECT: SHA-256 | quantum-safe
+ const d = crypto.createHash(algDinParametru); // EXPECT: createHash | unknown
+ const { publicKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); // EXPECT: RSA-2048 | quantum-vulnerable
+ const k2 = crypto.generateKeyPairSync('ec', { namedCurve: 'secp256k1' }); // EXPECT: EC-secp256k1 | quantum-vulnerable
+ const k3 = crypto.generateKeyPairSync('ed25519'); // EXPECT: Ed25519 | quantum-vulnerable
+ const k4 = crypto.generateKeyPairSync('ml-dsa-65'); // EXPECT: ML-DSA-65 | quantum-safe
+ const k5 = crypto.generateKeyPairSync('ml-kem-768'); // EXPECT: ML-KEM-768 | quantum-safe
+ const s = crypto.createSign('RSA-SHA256'); // EXPECT: RSA | quantum-vulnerable
+ const v = crypto.createVerify('SHA1'); // EXPECT: signature-with-SHA-1 | weak-now
+ const e = crypto.createECDH('prime256v1'); // EXPECT: ECDH-secp256r1 | quantum-vulnerable
+ const c1 = crypto.createCipheriv('aes-256-gcm', key, iv); // EXPECT: AES-256-GCM | quantum-safe
+ const c2 = crypto.createCipheriv('des-ede3-cbc', key, iv); // EXPECT: 3DES-CBC | weak-now
+ const c3 = crypto.createCipheriv('aes-128-ecb', key, null); // EXPECT: AES-128-ECB | weak-now
+ const c4 = crypto.createDecipheriv('aes-128-cbc', key, iv); // EXPECT: AES-128-CBC | quantum-safe
+ const enc = crypto.publicEncrypt(publicKey, data); // EXPECT: RSA | quantum-vulnerable
+ const h = crypto.createHmac('sha256', key); // EXPECT: HMAC-SHA-256 | quantum-safe
+ const dh = crypto.getDiffieHellman('modp2'); // EXPECT: DH-1024 | weak-now
+ const sig = crypto.sign(null, data, key); // EXPECT: crypto.sign | unknown
+ const pk = crypto.createPrivateKey(pemDinConfig); // EXPECT: createPrivateKey | unknown
+ return [a, b, c, d, k2, k3, k4, k5, s, v, e, c1, c2, c3, c4, enc, h, dh, sig, pk];
+}
diff --git a/crypto-inventory/test/fixturi/js/tls.cjs b/crypto-inventory/test/fixturi/js/tls.cjs
new file mode 100644
index 0000000..ffa636d
--- /dev/null
+++ b/crypto-inventory/test/fixturi/js/tls.cjs
@@ -0,0 +1,18 @@
+// Fixtura: optiuni TLS in Node.
+const tls = require('node:tls');
+const https = require('https');
+
+const server = tls.createServer({
+ minVersion: 'TLSv1', // EXPECT: TLSv1.0 | weak-now
+ ecdhCurve: 'X25519MLKEM768:X25519:prime256v1', // EXPECT: X25519MLKEM768 | quantum-safe ; X25519 | quantum-vulnerable ; ECDH-secp256r1 | quantum-vulnerable
+});
+
+const agent = new https.Agent({ minVersion: 'TLSv1.2', maxVersion: 'TLSv1.3' }); // EXPECT: TLSv1.2 | quantum-vulnerable ; TLSv1.3 | unknown
+
+const PQ = 'X25519MLKEM768';
+const GRUPURI = { hibrid: 'X25519MLKEM768' };
+const a2 = tls.connect({ ecdhCurve: PQ }); // EXPECT: X25519MLKEM768 | quantum-safe
+const a3 = tls.connect({ ecdhCurve: `${PQ}:X25519` }); // EXPECT: X25519MLKEM768 | quantum-safe ; X25519 | quantum-vulnerable
+const a4 = tls.connect({ ecdhCurve: GRUPURI[process.env.MOD] }); // EXPECT: tls ecdhCurve | unknown
+
+module.exports = { server, agent, a2, a3, a4 };
diff --git a/crypto-inventory/test/fixturi/js/webcrypto.js b/crypto-inventory/test/fixturi/js/webcrypto.js
new file mode 100644
index 0000000..caf8516
--- /dev/null
+++ b/crypto-inventory/test/fixturi/js/webcrypto.js
@@ -0,0 +1,15 @@
+// Fixtura: WebCrypto (browser sau globalThis.crypto in Node).
+const subtle = globalThis.crypto.subtle;
+
+export async function webcrypto(data) {
+ const rsa = await subtle.generateKey(
+ { name: 'RSA-OAEP', modulusLength: 3072, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, // EXPECT: RSA-3072 | quantum-vulnerable
+ true, ['encrypt', 'decrypt']);
+ const ec = await subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-384' }, true, ['sign', 'verify']); // EXPECT: ECDSA-secp384r1 | quantum-vulnerable
+ const ed = await subtle.generateKey('Ed25519', true, ['sign', 'verify']); // EXPECT: Ed25519 | quantum-vulnerable
+ const x = await subtle.generateKey({ name: 'X25519' }, true, ['deriveBits']); // EXPECT: X25519 | quantum-vulnerable
+ const aes = await subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt']); // EXPECT: AES-256-GCM | quantum-safe
+ const d1 = await subtle.digest('SHA-1', data); // EXPECT: SHA-1 | weak-now
+ const d2 = await subtle.digest({ name: 'SHA-384' }, data); // EXPECT: SHA-384 | quantum-safe
+ return [rsa, ec, ed, x, aes, d1, d2];
+}
diff --git a/crypto-inventory/test/fixturi/limite/mare.js b/crypto-inventory/test/fixturi/limite/mare.js
new file mode 100644
index 0000000..01931a5
--- /dev/null
+++ b/crypto-inventory/test/fixturi/limite/mare.js
@@ -0,0 +1,22 @@
+import crypto from 'node:crypto';
+export const h = crypto.createHash('md5');
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
+// umplutura pentru limita de marime
diff --git a/crypto-inventory/test/fixturi/limite/mic.js b/crypto-inventory/test/fixturi/limite/mic.js
new file mode 100644
index 0000000..74a11f7
--- /dev/null
+++ b/crypto-inventory/test/fixturi/limite/mic.js
@@ -0,0 +1,2 @@
+import crypto from 'node:crypto';
+export const h = crypto.createHash('sha256');
diff --git a/crypto-inventory/test/fixturi/manifeste/go.mod b/crypto-inventory/test/fixturi/manifeste/go.mod
new file mode 100644
index 0000000..c21389b
--- /dev/null
+++ b/crypto-inventory/test/fixturi/manifeste/go.mod
@@ -0,0 +1,9 @@
+module example.com/fixtura
+
+go 1.24
+
+require (
+ github.com/cloudflare/circl v1.6.1
+ golang.org/x/crypto v0.36.0
+ golang.org/x/text v0.23.0
+)
diff --git a/crypto-inventory/test/fixturi/manifeste/package.json b/crypto-inventory/test/fixturi/manifeste/package.json
new file mode 100644
index 0000000..d894dc8
--- /dev/null
+++ b/crypto-inventory/test/fixturi/manifeste/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "fixtura-manifest",
+ "version": "1.0.0",
+ "dependencies": {
+ "ethers": "^6.13.0",
+ "lodash": "^4.17.21",
+ "jsonwebtoken": "9.0.2"
+ },
+ "devDependencies": {
+ "@noble/post-quantum": "0.4.1"
+ }
+}
diff --git a/crypto-inventory/test/fixturi/manifeste/pom.xml b/crypto-inventory/test/fixturi/manifeste/pom.xml
new file mode 100644
index 0000000..80bdd95
--- /dev/null
+++ b/crypto-inventory/test/fixturi/manifeste/pom.xml
@@ -0,0 +1,14 @@
+
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+ 1.78.1
+
+
+ junit
+ junit
+ 4.13.2
+
+
+
diff --git a/crypto-inventory/test/fixturi/manifeste/requirements.txt b/crypto-inventory/test/fixturi/manifeste/requirements.txt
new file mode 100644
index 0000000..98ddd60
--- /dev/null
+++ b/crypto-inventory/test/fixturi/manifeste/requirements.txt
@@ -0,0 +1,3 @@
+requests==2.32.3
+cryptography==43.0.1
+PyJWT[crypto]>=2.8
diff --git a/crypto-inventory/test/fixturi/material/certificat-proba.pem b/crypto-inventory/test/fixturi/material/certificat-proba.pem
new file mode 100644
index 0000000..fa293df
--- /dev/null
+++ b/crypto-inventory/test/fixturi/material/certificat-proba.pem
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE-----
+MIIBmzCCAUGgAwIBAgIUSB9o0oiJeX65E+oyUE6ukzrvT2MwCgYIKoZIzj0EAwIw
+IzEhMB8GA1UEAwwYY3J5cHRvLWludmVudG9yeS1maXh0dXJlMB4XDTI2MDkyNTA4
+NTAwM1oXDTM2MDkyMjA4NTAwM1owIzEhMB8GA1UEAwwYY3J5cHRvLWludmVudG9y
+eS1maXh0dXJlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEPwYXPWSiw0/Y4M2Q
+zkQHHXLNkRJv0W0Ml7C3vN9HyKl1C9LrNB5evAruKJVTTh+J5KJaxfjDLFldkczR
+2IKBEaNTMFEwHQYDVR0OBBYEFPE/ChbrkN4O/z7KgfHWZR/+XBBmMB8GA1UdIwQY
+MBaAFPE/ChbrkN4O/z7KgfHWZR/+XBBmMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZI
+zj0EAwIDSAAwRQIhAMAK5Uxt090TnPPp5xn3cG1vD1BSNEc+ZJBI0JPPeCl1AiAM
+egBkNslxucBd5XhFrWbvxCIRBedHr0dZGJYrHIZqrg==
+-----END CERTIFICATE-----
diff --git a/crypto-inventory/test/fixturi/material/cheie-publica-proba.pem b/crypto-inventory/test/fixturi/material/cheie-publica-proba.pem
new file mode 100644
index 0000000..f41511e
--- /dev/null
+++ b/crypto-inventory/test/fixturi/material/cheie-publica-proba.pem
@@ -0,0 +1,10 @@
+Test-only public key generated for the crypto-inventory fixtures.
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvhFbgeI5qpI+99IZBwOt
+nuwndPRr+gpT5oLNlXVqjOhWSJZWPGgt4cNGw7XgeSujl91Wl8TGCoRAgq4xOxmw
+d+WVycVlCMKK7Peaepe/wL+DMEABVDuqlkiFEe90/6Kk6sIge555qBGBa/tsvkxX
+trU1khKI9A7g1YAkSkmUiCAPTktvZ/ED8r6P61j5u8lXnbGeNU7a7vivgWF4CaF4
+a4m4oAVHUxWLKmeSLN8CeIn6AZSmqc5e/0DMEMGuzqC/XBKWz55bhkzq/CAMdPcI
+1zbFcXkUM7tWJzLbgAW4R3Bpe1GUD8+h0fG9v8xnJOf1vQ1M4veJ1OQrEhFwuI4+
+TQIDAQAB
+-----END PUBLIC KEY-----
diff --git a/crypto-inventory/test/fixturi/negative/Comentarii.java b/crypto-inventory/test/fixturi/negative/Comentarii.java
new file mode 100644
index 0000000..18dfce0
--- /dev/null
+++ b/crypto-inventory/test/fixturi/negative/Comentarii.java
@@ -0,0 +1,14 @@
+// MessageDigest.getInstance("MD5") intr-un comentariu nu e o folosire.
+package fixturi;
+
+public class Comentarii {
+ /* Cipher.getInstance("DES") intr-un comentariu bloc */
+ /**
+ * Signature.getInstance("SHA1withRSA") in Javadoc.
+ */
+ private final String doc = "KeyPairGenerator.getInstance(\"RSA\") in a string";
+
+ public String doc() {
+ return doc;
+ }
+}
diff --git a/crypto-inventory/test/fixturi/negative/comentarii.go b/crypto-inventory/test/fixturi/negative/comentarii.go
new file mode 100644
index 0000000..9fdfdf6
--- /dev/null
+++ b/crypto-inventory/test/fixturi/negative/comentarii.go
@@ -0,0 +1,11 @@
+// Fixtura negativa Go: niciun import criptografic.
+package fixturi
+
+/*
+import "crypto/md5"
+*/
+
+// md5.Sum(data) intr-un comentariu nu e o folosire.
+func Nimic() string {
+ return "crypto/rsa rsa.GenerateKey(rand.Reader, 1024)"
+}
diff --git a/crypto-inventory/test/fixturi/negative/comentarii.js b/crypto-inventory/test/fixturi/negative/comentarii.js
new file mode 100644
index 0000000..971a747
--- /dev/null
+++ b/crypto-inventory/test/fixturi/negative/comentarii.js
@@ -0,0 +1,14 @@
+// Fixtura negativa: fisierul importa node:crypto doar pentru UUID-uri. Comentariile pomenesc
+// API-uri reale; niciunul nu e o folosire si scanerul trebuie sa dea ZERO gasiri.
+import crypto from 'node:crypto';
+
+// crypto.createHash('md5') intr-un comentariu nu e o folosire.
+// const k = crypto.generateKeyPairSync('rsa', { modulusLength: 1024 });
+/* crypto.createCipheriv('des-ede3-cbc', key, iv) intr-un comentariu bloc */
+/**
+ * Exemplu din documentatie: crypto.createSign('RSA-SHA1').
+ * RSA, ECDSA, secp256k1 si X25519 sunt aici doar cuvinte.
+ */
+export function id() {
+ return crypto.randomUUID(); // crypto.createHmac('sha1', k) tot comentariu
+}
diff --git a/crypto-inventory/test/fixturi/negative/docstring.py b/crypto-inventory/test/fixturi/negative/docstring.py
new file mode 100644
index 0000000..b47bc4a
--- /dev/null
+++ b/crypto-inventory/test/fixturi/negative/docstring.py
@@ -0,0 +1,13 @@
+"""Modul fara criptografie.
+
+Documentatia pomeneste rsa.generate_private_key(public_exponent=65537, key_size=1024)
+si hashlib.md5(data), dar nu le cheama.
+"""
+import hashlib
+
+
+def f(x):
+ """Returneaza x. Nu foloseste hashlib.sha1(x)."""
+ # hashlib.md5(x) intr-un comentariu nu e o folosire
+ text = "hashlib.md5 apare aici doar ca text"
+ return x, text, hashlib
diff --git a/crypto-inventory/test/fixturi/negative/fara-cripto.js b/crypto-inventory/test/fixturi/negative/fara-cripto.js
new file mode 100644
index 0000000..902927d
--- /dev/null
+++ b/crypto-inventory/test/fixturi/negative/fara-cripto.js
@@ -0,0 +1,9 @@
+export function sign(x) {
+ return Math.sign(x);
+}
+
+export function createHash(n) {
+ return { n, hash: n * 31 };
+}
+
+export const rezultat = [sign(-3), createHash(7)].map((v) => JSON.stringify(v));
diff --git a/crypto-inventory/test/fixturi/negative/siruri.js b/crypto-inventory/test/fixturi/negative/siruri.js
new file mode 100644
index 0000000..fdefe30
--- /dev/null
+++ b/crypto-inventory/test/fixturi/negative/siruri.js
@@ -0,0 +1,9 @@
+import crypto from 'node:crypto';
+
+export const doc = "Call crypto.createHash('md5') to hash (documentation string).";
+export const tpl = `Use crypto.createCipheriv('des-ede3-cbc', key, iv) at ${Date.now()}`;
+export const eticheta = 'crypto.generateKeyPairSync("rsa", { modulusLength: 1024 })';
+export const re = /createHash\('sha1'\)/;
+export function id() {
+ return crypto.randomUUID();
+}
diff --git a/crypto-inventory/test/fixturi/negative/variabile.js b/crypto-inventory/test/fixturi/negative/variabile.js
new file mode 100644
index 0000000..2644817
--- /dev/null
+++ b/crypto-inventory/test/fixturi/negative/variabile.js
@@ -0,0 +1,12 @@
+// Fixtura: algoritmi dati prin variabile care NU se pot rezolva static. Trebuie "unknown", nu ghicit.
+import crypto from 'node:crypto';
+
+let alg = 'md5';
+alg = 'sha256';
+
+export function h(data, algDinApel, opts) {
+ const a = crypto.createHash(alg); // EXPECT: createHash | unknown
+ const b = crypto.createHash(algDinApel); // EXPECT: createHash | unknown
+ const c = crypto.createCipheriv(opts.cipher, opts.key, opts.iv); // EXPECT: createCipheriv | unknown
+ return [a, b, c, data];
+}
diff --git a/crypto-inventory/test/fixturi/python/crypto_py.py b/crypto-inventory/test/fixturi/python/crypto_py.py
new file mode 100644
index 0000000..cc626bf
--- /dev/null
+++ b/crypto-inventory/test/fixturi/python/crypto_py.py
@@ -0,0 +1,32 @@
+"""Fixtura: criptografie in Python. Fisierul nu se executa; e doar text pentru scaner."""
+import hashlib
+import hmac
+import ssl
+from cryptography.hazmat.primitives.asymmetric import rsa, ec, ed25519, padding
+from cryptography.hazmat.primitives import hashes
+from Crypto.Cipher import AES, DES3
+from ecdsa import SigningKey, SECP256k1
+import jwt
+import oqs
+
+DIGEST = "sha256"
+
+
+def exemple(data, key, alg_din_parametru):
+ k1 = rsa.generate_private_key(public_exponent=65537, key_size=1024) # EXPECT: RSA-1024 | weak-now
+ k2 = ec.generate_private_key(ec.SECP384R1()) # EXPECT: EC-secp384r1 | quantum-vulnerable
+ k3 = ed25519.Ed25519PrivateKey.generate() # EXPECT: Ed25519 | quantum-vulnerable
+ sig = k2.sign(data, ec.ECDSA(hashes.SHA256())) # EXPECT: ECDSA | quantum-vulnerable ; SHA-256 | quantum-safe
+ ct = k1.public_key().encrypt(data, padding.OAEP(mgf=padding.MGF1(hashes.SHA256()), algorithm=hashes.SHA256(), label=None)) # EXPECT: RSA | quantum-vulnerable ; SHA-256 | quantum-safe
+ h1 = hashlib.md5(data).hexdigest() # EXPECT: MD5 | weak-now
+ h2 = hashlib.new(DIGEST) # EXPECT: SHA-256 | quantum-safe
+ h3 = hashlib.new(alg_din_parametru) # EXPECT: hashlib.new | unknown
+ mac = hmac.new(key, data, digestmod="sha1") # EXPECT: HMAC-SHA-1 | weak-now
+ c1 = AES.new(key, AES.MODE_ECB) # EXPECT: AES-ECB | weak-now
+ c2 = DES3.new(key, DES3.MODE_CBC) # EXPECT: 3DES-CBC | weak-now
+ sk = SigningKey.generate(curve=SECP256k1) # EXPECT: ECDSA-secp256k1 | quantum-vulnerable
+ tok = jwt.encode({"a": 1}, key, algorithm="ES256K") # EXPECT: JWS ES256K | quantum-vulnerable
+ kem = oqs.KeyEncapsulation("ML-KEM-768") # EXPECT: ML-KEM-768 | quantum-safe
+ ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # EXPECT: TLS (negotiated) | unknown
+ ctx.minimum_version = ssl.TLSVersion.TLSv1_2 # EXPECT: TLSv1.2 | quantum-vulnerable
+ return [k3, sig, ct, h1, h2, h3, mac, c1, c2, sk, tok, kem]
diff --git a/crypto-inventory/test/proba-timp.mjs b/crypto-inventory/test/proba-timp.mjs
new file mode 100644
index 0000000..c040d2f
--- /dev/null
+++ b/crypto-inventory/test/proba-timp.mjs
@@ -0,0 +1,72 @@
+// Proba de cost: inventarul trebuie sa ramana LINIAR pe fisiere facute anume (2026-09-29, revizuirea adversariala). Formele vechi
+// ale cautarii PEM, ale literalului regex JS, ale docstring-ului Python, ale rezolvarii variabilelor si ale variabilei atribuite
+// in Java costau patratic: un singur fisier de 1 MB (marimea maxima implicita) tinea scanarea minute intregi.
+// Fiecare caz se scaneaza la doua marimi, a doua de 4 ori prima (64 si 256 KB; PEM 256 KB si 1 MB), fiecare intr-un proces
+// separat cu termen; liniar inseamna ~4x, patratic ~16x. Verdict pe caz: VERDE daca t2 <= 6 * t1 + 250 ms si t2 < 10 s; ROSU
+// altfel (si la depasirea termenului).
+// node test/proba-timp.mjs [--doar caz1,caz2] -> 0 toate verzi, 1 unul rosu, 2 STRICAT (o masurare nu a pornit)
+// Modulul masurat se schimba cu CRYPTO_INVENTORY_MODULE=/inventar.mjs (asa il foloseste controlul negativ).
+import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
+import { join, resolve, dirname } from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { spawnSync } from 'node:child_process';
+import { tmpdir } from 'node:os';
+
+const AICI = dirname(fileURLToPath(import.meta.url));
+const CALE_MODUL = process.env.CRYPTO_INVENTORY_MODULE ? resolve(process.env.CRYPTO_INVENTORY_MODULE) : join(AICI, '..', 'inventar.mjs');
+const TERMEN_MS = 20000;
+
+// unitatea se repeta pana la marime; '@' se inlocuieste cu un contor (nume distincte)
+export const CAZURI = {
+ // pem: cautarea sfarsitului e nativa (indexOf), deci forma patratica se vede abia spre marimea maxima implicita
+ 'pem.txt': ['-----BEGIN CERTIFICATE-----AAAA\n', 256, 1024],
+ 'py-triplu.py': 'x="""a"""+',
+ 'py-linie.py': 'hashlib.new(x)+',
+ 'js-regex.js': '=/[',
+ 'js-linie.js': 'crypto.createHash(x)+',
+ 'js-func.js': 'crypto.createHash(q)\nfunction (',
+ 'java-linie.java': 'Cipher.getInstance(x)+',
+ 'java-kpg.java': 'k = KeyPairGenerator.getInstance("EC")+',
+};
+
+const marimi = (nume) => (Array.isArray(CAZURI[nume]) ? CAZURI[nume].slice(1) : [64, 256]);
+const unitate = (nume) => (Array.isArray(CAZURI[nume]) ? CAZURI[nume][0] : CAZURI[nume]);
+const genereaza = (unit, kb) => unit.repeat(Math.max(1, Math.floor((kb * 1024) / unit.length)));
+
+const arg = (f) => { const i = process.argv.indexOf(f); return i >= 0 ? process.argv[i + 1] : null; };
+
+if (arg('--caz')) {
+ // copilul: scaneaza un singur fisier si tipareste milisecundele
+ const nume = arg('--caz'); const kb = Number(arg('--kb'));
+ const inv = await import(pathToFileURL(CALE_MODUL).href);
+ const d = mkdtempSync(join(tmpdir(), 'ci-timp-'));
+ try {
+ writeFileSync(join(d, nume), genereaza(unitate(nume), kb));
+ const t0 = process.hrtime.bigint();
+ const rez = inv.scan(d);
+ const ms = Number(process.hrtime.bigint() - t0) / 1e6;
+ if (rez.stats.filesSeen !== 1 || rez.stats.codeFilesTotal + rez.stats.textFilesPemOnly !== 1) throw new Error('fisierul nu a fost citit');
+ console.log(JSON.stringify({ ms }));
+ } finally { rmSync(d, { recursive: true, force: true }); }
+} else {
+ const doar = arg('--doar') ? arg('--doar').split(',') : Object.keys(CAZURI);
+ const masoara = (nume, kb) => {
+ const r = spawnSync(process.execPath, [fileURLToPath(import.meta.url), '--caz', nume, '--kb', String(kb)], { encoding: 'utf8', timeout: TERMEN_MS, env: process.env });
+ if (r.error && r.error.code === 'ETIMEDOUT') return { termen: true };
+ try { return { ms: JSON.parse((r.stdout || '').trim().split('\n').pop()).ms }; } catch { return { stricat: `cod ${r.status}: ${(r.stderr || '').trim().split('\n').slice(-1)[0]}` }; }
+ };
+ let rosii = 0, stricate = 0;
+ for (const nume of doar) {
+ if (!CAZURI[nume]) { stricate++; console.log(`STRICAT ${nume}: caz necunoscut`); continue; }
+ const [k1, k2] = marimi(nume);
+ const a = masoara(nume, k1);
+ const b = a.ms !== undefined ? masoara(nume, k2) : a.termen ? {} : { stricat: `nemasurat (${k1} KB a cazut)` };
+ if (a.stricat || b.stricat) { stricate++; console.log(`STRICAT ${nume}: ${a.stricat || b.stricat}`); continue; }
+ if (a.termen || b.termen) { rosii++; console.log(`ROSU ${nume}: peste ${TERMEN_MS / 1000} s la ${a.termen ? k1 : k2} KB`); continue; }
+ const ok = b.ms <= 6 * a.ms + 250 && b.ms < 10000;
+ if (!ok) rosii++;
+ console.log(`${ok ? 'VERDE ' : 'ROSU '} ${nume.padEnd(16)} ${String(k1).padStart(4)} KB ${a.ms.toFixed(0).padStart(6)} ms | ${String(k2).padStart(4)} KB ${b.ms.toFixed(0).padStart(6)} ms x${(b.ms / Math.max(a.ms, 1)).toFixed(1)}`);
+ }
+ console.log(`\ncostul inventarului: ${doar.length - rosii - stricate}/${doar.length} liniare, ${rosii} rosii, ${stricate} stricate (modul: ${CALE_MODUL})`);
+ process.exitCode = stricate ? 2 : rosii ? 1 : 0;
+}
diff --git a/crypto-inventory/test/proba.mjs b/crypto-inventory/test/proba.mjs
new file mode 100644
index 0000000..27aed24
--- /dev/null
+++ b/crypto-inventory/test/proba.mjs
@@ -0,0 +1,486 @@
+// Suita de probe a inventarului criptografic.
+// Folosire: node test/proba.mjs [--json]
+// Modulul masurat se poate schimba cu CRYPTO_INVENTORY_MODULE=/inventar.mjs (asa il foloseste
+// control-negativ.mjs pe o copie stricata). Fixturile raman mereu cele din test/fixturi.
+// Fiecare proba ruleaza izolat: o proba care arunca iese ROSU cu mesajul ei, restul ruleaza mai departe.
+import { readFileSync, readdirSync, statSync, existsSync, mkdtempSync, writeFileSync, rmSync, copyFileSync } from 'node:fs';
+import { join, resolve, dirname } from 'node:path';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { spawnSync } from 'node:child_process';
+import { tmpdir } from 'node:os';
+import { generateKeyPairSync } from 'node:crypto';
+
+const AICI = dirname(fileURLToPath(import.meta.url));
+const FIX = join(AICI, 'fixturi');
+const CALE_MODUL = process.env.CRYPTO_INVENTORY_MODULE ? resolve(process.env.CRYPTO_INVENTORY_MODULE) : join(AICI, '..', 'inventar.mjs');
+const inv = await import(pathToFileURL(CALE_MODUL).href);
+const CA_JSON = process.argv.includes('--json');
+
+// numarul total de marcaje EXPECT scrise de mana in fixturi; o schimbare aici se face constient
+const MARCAJE_ASTEPTATE = 96;
+const FISIERE_CU_MARCAJE = ['js/node-crypto.mjs', 'js/webcrypto.js', 'js/biblioteci.ts', 'js/tls.cjs', 'python/crypto_py.py', 'java/Crypto.java', 'go/crypto.go', 'negative/variabile.js'];
+
+const probe = [];
+const proba = (nume, fn) => probe.push({ nume, fn });
+function asigura(cond, mesaj) { if (!cond) throw new Error(mesaj); }
+
+const memo = new Map();
+function scan(dir, optiuni = {}) {
+ const k = dir + JSON.stringify(optiuni);
+ if (!memo.has(k)) {
+ try { memo.set(k, { rez: inv.scan(dir, optiuni) }); } catch (err) { memo.set(k, { err }); }
+ }
+ const r = memo.get(k);
+ if (r.err) throw r.err;
+ return r.rez;
+}
+
+function marcaje(rel) {
+ const r = [];
+ readFileSync(join(FIX, rel), 'utf8').split('\n').forEach((l, i) => {
+ const m = /EXPECT:\s*(.+?)\s*$/.exec(l);
+ if (!m) return;
+ for (const p of m[1].split(' ; ')) {
+ const [n, c] = p.split(' | ').map((s) => s.trim());
+ r.push(`${i + 1}|${n}|${c}`);
+ }
+ });
+ return r.sort();
+}
+
+function gasiriDin(rez, rel) {
+ return rez.findings.filter((g) => g.file === rel);
+}
+
+function comparaExact(rel) {
+ const rez = scan(FIX);
+ const ast = marcaje(rel);
+ asigura(ast.length > 0, `${rel}: fixtura nu are marcaje`);
+ const real = gasiriDin(rez, rel).map((g) => `${g.line}|${g.name}|${g.classification}`).sort();
+ const lipsa = ast.filter((x) => !real.includes(x));
+ const extra = real.filter((x) => !ast.includes(x));
+ asigura(!lipsa.length && !extra.length && ast.length === real.length,
+ `${rel}: lipsa [${lipsa.join(', ')}] extra [${extra.join(', ')}] (asteptate ${ast.length}, gasite ${real.length})`);
+}
+
+// --- pozitive: fiecare gasire asteptata, cu linia si clasa ei, si nimic in plus ---
+for (const rel of FISIERE_CU_MARCAJE.filter((f) => !f.startsWith('negative/'))) {
+ proba(`pozitiv: ${rel}`, () => comparaExact(rel));
+}
+
+proba('pozitiv: numarul total de marcaje EXPECT este cel scris', () => {
+ const n = FISIERE_CU_MARCAJE.reduce((s, f) => s + marcaje(f).length, 0);
+ asigura(n === MARCAJE_ASTEPTATE, `marcaje citite ${n}, scrise ${MARCAJE_ASTEPTATE}`);
+ const rez = scan(FIX);
+ const inFisiere = rez.findings.filter((g) => FISIERE_CU_MARCAJE.includes(g.file)).length;
+ asigura(inFisiere === MARCAJE_ASTEPTATE, `gasiri in fisierele cu marcaje: ${inFisiere}, marcaje ${MARCAJE_ASTEPTATE}`);
+});
+
+proba('pozitiv: variabila rezolvata la un literal unic poarta resolvedFrom', () => {
+ const g = gasiriDin(scan(FIX), 'js/node-crypto.mjs').find((x) => x.name === 'SHA-256');
+ asigura(g && /HASH_ALG \(line 6\)/.test(g.resolvedFrom || ''), `resolvedFrom: ${g && g.resolvedFrom}`);
+ const j = gasiriDin(scan(FIX), 'java/Crypto.java').find((x) => x.name === 'SHA-256');
+ asigura(j && /DIGEST/.test(j.resolvedFrom || ''), `java resolvedFrom: ${j && j.resolvedFrom}`);
+});
+
+proba('pozitiv: manifestele dau biblioteci declarate, nu gasiri', () => {
+ const rez = scan(join(FIX, 'manifeste'));
+ asigura(rez.findings.length === 0, `gasiri din manifeste: ${rez.findings.length}`);
+ const libs = rez.libraries.map((b) => `${b.file}:${b.line} ${b.name}`).sort();
+ const ast = ['go.mod:6 github.com/cloudflare/circl', 'go.mod:7 golang.org/x/crypto', 'package.json:10 @noble/post-quantum',
+ 'package.json:5 ethers', 'package.json:7 jsonwebtoken', 'pom.xml:5 org.bouncycastle:bcprov-jdk18on',
+ 'requirements.txt:2 cryptography', 'requirements.txt:3 pyjwt'].sort();
+ asigura(JSON.stringify(libs) === JSON.stringify(ast), `biblioteci: ${libs.join(' | ')}`);
+ const bom = inv.buildCbom(rez, { deterministic: true });
+ const c = bom.components.filter((x) => x.type === 'library');
+ asigura(c.length === 8 && c.every((x) => x.properties.some((p) => p.name.endsWith('declared-only') && p.value === 'true')), 'componentele library nu poarta declared-only');
+});
+
+proba('pozitiv: material PEM (certificat si cheie publica)', () => {
+ const rez = scan(FIX);
+ const c = gasiriDin(rez, 'material/certificat-proba.pem');
+ asigura(c.length === 1 && c[0].assetType === 'certificate' && c[0].name === 'EC-secp256r1' && c[0].classification === 'quantum-vulnerable' && c[0].line === 1, `certificat: ${JSON.stringify(c.map((x) => [x.assetType, x.name, x.line]))}`);
+ asigura(/crypto-inventory-fixture/.test(c[0].certificate.subjectName), 'subiectul certificatului lipseste');
+ const p = gasiriDin(rez, 'material/cheie-publica-proba.pem');
+ asigura(p.length === 1 && p[0].assetType === 'related-crypto-material' && p[0].material.type === 'public-key' && p[0].name === 'RSA-2048' && p[0].line === 2, `cheie publica: ${JSON.stringify(p.map((x) => [x.assetType, x.name, x.line]))}`);
+});
+
+proba('pozitiv: cheie privata generata la rulare e raportata, iar valoarea ei nu apare in iesire', () => {
+ const d = mkdtempSync(join(tmpdir(), 'ci-cheie-'));
+ try {
+ const { privateKey } = generateKeyPairSync('ec', { namedCurve: 'P-256' });
+ const pem = privateKey.export({ type: 'pkcs8', format: 'pem' });
+ writeFileSync(join(d, 'config.txt'), `setari\n${pem}`);
+ const rez = inv.scan(d);
+ asigura(rez.findings.length === 1, `gasiri: ${rez.findings.length}`);
+ const g = rez.findings[0];
+ asigura(g.assetType === 'related-crypto-material' && g.material.type === 'private-key' && g.name === 'EC-secp256r1' && g.line === 2, `gasirea: ${JSON.stringify([g.assetType, g.material, g.name, g.line])}`);
+ const corp = pem.split('\n').filter((l) => l && !l.startsWith('-----')).join('');
+ const iesire = JSON.stringify(inv.buildCbom(rez)) + JSON.stringify(rez.findings) + inv.renderSummary(rez);
+ for (let i = 0; i + 24 <= corp.length; i += 8) asigura(!iesire.includes(corp.slice(i, i + 24)), `o bucata din cheia privata apare in iesire (offset ${i})`);
+ } finally {
+ rmSync(d, { recursive: true, force: true });
+ }
+});
+
+// --- clasificare ---
+proba('clasificare: secp256k1 este quantum-vulnerable, cu recomandare de cont post-cuantic', () => {
+ const a = inv.classify({ grup: 'SECP256K1' });
+ asigura(a.classification === 'quantum-vulnerable' && a.quantumVulnerable === true, `clasa ${a.classification}`);
+ asigura(/post-quantum key/.test(a.recommendation) && /not a library swap/.test(a.recommendation), 'recomandarea nu vorbeste de contul post-cuantic');
+ const f = gasiriDin(scan(FIX), 'js/node-crypto.mjs').find((x) => x.curve === 'secp256k1');
+ asigura(f && f.classification === 'quantum-vulnerable', 'gasirea secp256k1 din fixtura nu e quantum-vulnerable');
+});
+
+proba('clasificare: MD5 si SHA-1 sunt weak-now, SHA-256 quantum-safe', () => {
+ const c = (h) => inv.classify({ grup: 'HASH', hash: h }).classification;
+ asigura(c('MD5') === 'weak-now' && c('SHA1') === 'weak-now' && c('SHA256') === 'quantum-safe', `MD5 ${c('MD5')} SHA1 ${c('SHA1')} SHA256 ${c('SHA256')}`);
+});
+
+proba('clasificare: AES-128 quantum-safe cu nota Grover, ECB weak-now, DES weak-now', () => {
+ const a = inv.classify({ grup: 'CIPHER', nume: 'AES', param: '128', mod: 'gcm' });
+ asigura(a.classification === 'quantum-safe' && /Grover/.test(a.reason + a.recommendation) && a.nistQuantumSecurityLevel === 1, `AES-128: ${a.classification} ${a.nistQuantumSecurityLevel}`);
+ asigura(inv.classify({ grup: 'CIPHER', nume: 'AES', param: '256', mod: 'ecb' }).classification === 'weak-now', 'ECB nu e weak-now');
+ asigura(inv.classify({ grup: 'CIPHER', nume: 'DES' }).classification === 'weak-now', 'DES nu e weak-now');
+ asigura(inv.classify({ grup: 'TLS', param: '1.1', rol: 'min' }).classification === 'weak-now', 'TLS 1.1 nu e weak-now');
+});
+
+proba('clasificare: fiecare gasire vulnerabila sau slaba are motiv si recomandare', () => {
+ const rez = scan(FIX);
+ for (const g of rez.findings) {
+ asigura(g.reason && g.reason.length > 20, `${g.file}:${g.line} ${g.name} fara motiv`);
+ if (g.classification === 'quantum-vulnerable' || g.classification === 'weak-now' || g.classification === 'unknown') {
+ asigura(g.recommendation && g.recommendation.length > 20, `${g.file}:${g.line} ${g.name} (${g.classification}) fara recomandare`);
+ }
+ }
+});
+
+proba('clasificare: JWT asimetric spune ca standardul post-cuantic nu e final', () => {
+ const g = gasiriDin(scan(FIX), 'js/biblioteci.ts').find((x) => x.name === 'JWS RS256');
+ asigura(g && /no final standard/.test(g.recommendation) && /draft/.test(g.recommendation), 'recomandarea JWT nu spune ca nu exista standard final');
+});
+
+// --- negative ---
+function controlPozitivMd5() {
+ const g = gasiriDin(scan(FIX), 'js/node-crypto.mjs').find((x) => x.name === 'MD5');
+ asigura(g, 'controlul pozitiv al metodei: MD5 din js/node-crypto.mjs lipseste, deci un zero nu inseamna nimic');
+}
+
+proba('negativ: comentariile JS nu produc gasiri', () => {
+ controlPozitivMd5();
+ const g = gasiriDin(scan(FIX), 'negative/comentarii.js');
+ asigura(g.length === 0, `comentariile au produs: ${g.map((x) => `${x.line} ${x.name}`).join(', ')}`);
+});
+
+proba('negativ: apelul scris intr-un sir JS nu e folosire', () => {
+ controlPozitivMd5();
+ const g = gasiriDin(scan(FIX), 'negative/siruri.js');
+ asigura(g.length === 0, `sirurile au produs: ${g.map((x) => `${x.line} ${x.name}`).join(', ')}`);
+});
+
+proba('negativ: comentariile si docstring-urile Python nu produc gasiri', () => {
+ const rez = scan(FIX);
+ asigura(gasiriDin(rez, 'python/crypto_py.py').some((x) => x.name === 'MD5'), 'controlul pozitiv: hashlib.md5 din fixtura Python lipseste');
+ const g = gasiriDin(rez, 'negative/docstring.py');
+ asigura(g.length === 0, `docstring/comentariu au produs: ${g.map((x) => `${x.line} ${x.name}`).join(', ')}`);
+});
+
+proba('negativ: comentariile si sirurile Java si Go nu produc gasiri', () => {
+ const rez = scan(FIX);
+ asigura(gasiriDin(rez, 'java/Crypto.java').some((x) => x.name === 'MD5') && gasiriDin(rez, 'go/crypto.go').some((x) => x.name === 'MD5'), 'controlul pozitiv: MD5 din fixturile Java/Go lipseste');
+ const g = [...gasiriDin(rez, 'negative/Comentarii.java'), ...gasiriDin(rez, 'negative/comentarii.go')];
+ asigura(g.length === 0, `au produs: ${g.map((x) => `${x.file}:${x.line} ${x.name}`).join(', ')}`);
+});
+
+proba('negativ: fisier fara criptografie da zero gasiri, si a fost citit', () => {
+ const rez = scan(join(FIX, 'negative'));
+ asigura(rez.stats.filesSeen === 7 && rez.stats.codeFilesTotal === 7, `vazute ${rez.stats.filesSeen}, analizate ${rez.stats.codeFilesTotal}`);
+ const g = gasiriDin(rez, 'fara-cripto.js');
+ asigura(g.length === 0, `fara-cripto.js a produs ${g.length}`);
+});
+
+proba('negativ: algoritm din variabila reasignata sau din parametru iese unknown, nu ghicit', () => {
+ comparaExact('negative/variabile.js');
+ const g = gasiriDin(scan(FIX), 'negative/variabile.js');
+ asigura(g.every((x) => x.classification === 'unknown' && !x.resolvedFrom), 'o variabila nerezolvabila a primit o valoare');
+});
+
+proba('negativ: fisierul binar e sarit si numarat', () => {
+ const rez = scan(join(FIX, 'binar'));
+ asigura(rez.stats.filesSeen === 1 && rez.stats.notRead.binary === 1 && rez.findings.length === 0, `vazute ${rez.stats.filesSeen}, binare ${rez.stats.notRead.binary}, gasiri ${rez.findings.length}`);
+ asigura(rez.stats.notReadExamples.binary.includes('modul.js'), 'exemplul binar lipseste din statistica');
+ // control pozitiv: aceiasi octeti fara NUL dau gasirea, deci zeroul de mai sus vine din saritura
+ const d = mkdtempSync(join(tmpdir(), 'ci-binar-'));
+ try {
+ const b = readFileSync(join(FIX, 'binar', 'modul.js'));
+ writeFileSync(join(d, 'modul.js'), Buffer.from([...b].filter((x) => x !== 0)));
+ const r2 = inv.scan(d);
+ asigura(r2.findings.some((x) => x.name === 'MD5'), 'controlul pozitiv: fara NUL, fisierul nu produce MD5');
+ } finally { rmSync(d, { recursive: true, force: true }); }
+});
+
+proba('negativ: fisier prea mare sarit si numarat', () => {
+ const rez = scan(join(FIX, 'limite'), { maxFileBytes: 400 });
+ asigura(rez.stats.filesSeen === 2 && rez.stats.notRead.tooLarge === 1, `vazute ${rez.stats.filesSeen}, prea mari ${rez.stats.notRead.tooLarge}`);
+ asigura(/^mare\.js \(\d+ bytes\)$/.test(rez.stats.notReadExamples.tooLarge[0] || ''), `exemplu: ${rez.stats.notReadExamples.tooLarge[0]}`);
+ asigura(!rez.findings.some((x) => x.file === 'mare.js') && rez.findings.some((x) => x.file === 'mic.js'), 'gasirile nu corespund limitei');
+ const tot = scan(join(FIX, 'limite'));
+ asigura(tot.findings.some((x) => x.file === 'mare.js' && x.name === 'MD5'), 'controlul pozitiv: fara limita, mare.js nu da MD5');
+});
+
+proba('negativ: limita de fisiere numara restul', () => {
+ const rez = scan(join(FIX, 'limite'), { maxFiles: 1 });
+ asigura(rez.stats.filesSeen === 2 && rez.stats.notRead.fileLimit === 1 && rez.stats.codeFilesTotal === 1, `vazute ${rez.stats.filesSeen}, peste limita ${rez.stats.notRead.fileLimit}`);
+});
+
+// --- cazurile-limita ale formelor liniare (2026-09-29, revizuirea adversariala: cautarea PEM, literalul regex JS, docstring-ul
+// Python si rezolvarea variabilelor aveau cost patratic; formele noi trebuie sa dea EXACT ce dadeau cele vechi, si aici stau
+// cazurile pe care o forma liniara scrisa gresit le-ar schimba). Modulele se iau de langa modulul masurat (copia controlului).
+const LIB = join(dirname(CALE_MODUL), 'lib');
+const lexer = await import(pathToFileURL(join(LIB, 'lexer.mjs')).href);
+const pem = await import(pathToFileURL(join(LIB, 'pem.mjs')).href);
+const contextMod = await import(pathToFileURL(join(LIB, 'context.mjs')).href);
+
+proba('limite: un bloc PEM care incepe in liniutele unui antet fara sfarsit e gasit', () => {
+ const corp = 'A'.repeat(64);
+ const t = `-----BEGIN PRIVATE KEY-----BEGIN CERTIFICATE-----${corp}-----END CERTIFICATE-----`;
+ const b = [...pem.blocuriPem(t)].map((x) => `${x.index}|${x.tip}|${x.corp}`);
+ asigura(JSON.stringify(b) === JSON.stringify([`22|CERTIFICATE|${corp}`]), `blocuri: ${JSON.stringify(b)}`);
+ const fara = [...pem.blocuriPem(`-----BEGIN CERTIFICATE-----${corp}."-----END CERTIFICATE-----`)];
+ asigura(fara.length === 0, 'un caracter din afara alfabetului corpului inainte de sfarsit trebuie sa anuleze blocul');
+});
+
+proba('limite: literalul regex JS se cauta din starea lui, nu din ce a esuat alt literal pe acelasi rand', () => {
+ // primul `/` deschide o clasa care nu se inchide (esec pana la capatul randului, trecand prin 8 IN clasa); al doilea porneste
+ // din 8 IN AFARA clasei si se inchide la 10
+ const r = lexer.curata('x =/[ =/ x/;', 'javascript');
+ asigura(JSON.stringify(r.siruri) === JSON.stringify([[7, 11]]), `siruri: ${JSON.stringify(r.siruri)}`);
+});
+
+proba('limite: docstring Python pe randul lui (si dupa un sir neterminat), dar nu dupa cod pe acelasi rand', () => {
+ // docstring-ul de dupa sirul neterminat e INDENTAT: spatiile de inainte de el sunt pe randul nou, nu in coada sirului
+ const t = "a = 'abc\n '''doc'''\ny = '''s'''\n r'''d2'''\n";
+ const r = lexer.curata(t, 'python');
+ asigura(!r.code.includes('doc') && !r.code.includes('d2'), `docstring-urile trebuiau golite: ${JSON.stringify(r.code)}`);
+ asigura(r.code.includes("'''s'''"), `sirul triplu de dupa cod trebuia pastrat: ${JSON.stringify(r.code)}`);
+ const s = t.indexOf("'''s'''");
+ asigura(r.siruri.some(([a, b]) => a === s && b === s + 7), `intervalul sirului s lipseste: ${JSON.stringify(r.siruri)}`);
+});
+
+proba('limite: peste plafonul de variabile pe fisier numele ies unknown si fisierul se numara', () => {
+ const d = mkdtempSync(join(tmpdir(), 'ci-plafon-'));
+ try {
+ const n = contextMod.REZOLVARI_PE_FISIER;
+ const linii = ["import { createHash } from 'node:crypto';"];
+ for (let i = 0; i <= n; i++) linii.push(`const h${i} = 'sha256';`, `createHash(h${i});`);
+ writeFileSync(join(d, 'multe.mjs'), linii.join('\n') + '\n');
+ const rez = inv.scan(d);
+ const rezolvate = rez.findings.filter((g) => g.name === 'SHA-256' && g.resolvedFrom).length;
+ const necunoscute = rez.findings.filter((g) => g.classification === 'unknown').length;
+ asigura(rezolvate === n && necunoscute === 1, `rezolvate ${rezolvate} (cerut ${n}), unknown ${necunoscute} (cerut 1)`);
+ asigura(rez.stats.resolutionLimitFiles === 1 && rez.stats.resolutionLimitExamples[0] === 'multe.mjs', `statistica: ${rez.stats.resolutionLimitFiles}`);
+ } finally { rmSync(d, { recursive: true, force: true }); }
+});
+
+// --- CBOM ---
+const E = {
+ bom: ['$schema', 'bomFormat', 'specVersion', 'serialNumber', 'version', 'metadata', 'components', 'services', 'externalReferences', 'dependencies', 'compositions', 'properties', 'vulnerabilities', 'annotations', 'formulation', 'declarations', 'definitions', 'signature'],
+ component: ['type', 'mime-type', 'bom-ref', 'supplier', 'manufacturer', 'authors', 'author', 'publisher', 'group', 'name', 'version', 'description', 'scope', 'hashes', 'licenses', 'copyright', 'cpe', 'purl', 'omniborId', 'swhid', 'swid', 'modified', 'pedigree', 'externalReferences', 'properties', 'components', 'evidence', 'releaseNotes', 'modelCard', 'data', 'cryptoProperties', 'signature', 'tags'],
+ componentType: ['application', 'framework', 'library', 'container', 'platform', 'operating-system', 'device', 'device-driver', 'firmware', 'file', 'machine-learning-model', 'data', 'cryptographic-asset'],
+ cryptoProperties: ['assetType', 'algorithmProperties', 'certificateProperties', 'relatedCryptoMaterialProperties', 'protocolProperties', 'oid'],
+ assetType: ['algorithm', 'certificate', 'protocol', 'related-crypto-material'],
+ algorithmProperties: ['primitive', 'parameterSetIdentifier', 'curve', 'executionEnvironment', 'implementationPlatform', 'certificationLevel', 'mode', 'padding', 'cryptoFunctions', 'classicalSecurityLevel', 'nistQuantumSecurityLevel'],
+ primitive: ['drbg', 'mac', 'block-cipher', 'stream-cipher', 'signature', 'hash', 'pke', 'xof', 'kdf', 'key-agree', 'kem', 'ae', 'combiner', 'other', 'unknown'],
+ mode: ['cbc', 'ecb', 'ccm', 'gcm', 'cfb', 'ofb', 'ctr', 'other', 'unknown'],
+ padding: ['pkcs5', 'pkcs7', 'pkcs1v15', 'oaep', 'raw', 'other', 'unknown'],
+ cryptoFunctions: ['generate', 'keygen', 'encrypt', 'decrypt', 'digest', 'tag', 'keyderive', 'sign', 'verify', 'encapsulate', 'decapsulate', 'other', 'unknown'],
+ protocolProperties: ['type', 'version', 'cipherSuites', 'ikev2TransformTypes', 'cryptoRefArray'],
+ protocolType: ['tls', 'ssh', 'ipsec', 'ike', 'sstp', 'wpa', 'other', 'unknown'],
+ certificateProperties: ['subjectName', 'issuerName', 'notValidBefore', 'notValidAfter', 'signatureAlgorithmRef', 'subjectPublicKeyRef', 'certificateFormat', 'certificateExtension'],
+ relatedCryptoMaterialProperties: ['type', 'id', 'state', 'algorithmRef', 'creationDate', 'activationDate', 'updateDate', 'expirationDate', 'value', 'size', 'format', 'securedBy'],
+ materialType: ['private-key', 'public-key', 'secret-key', 'key', 'ciphertext', 'signature', 'digest', 'initialization-vector', 'nonce', 'seed', 'salt', 'shared-secret', 'tag', 'additional-data', 'password', 'credential', 'token', 'other', 'unknown'],
+ evidence: ['identity', 'occurrences', 'callstack', 'licenses', 'copyright'],
+ occurrence: ['bom-ref', 'location', 'line', 'offset', 'symbol', 'additionalContext'],
+};
+
+export function valideazaCbom(bom) {
+ const err = [];
+ const e = (m) => err.push(m);
+ const chei = (o, perm, unde) => { for (const k of Object.keys(o)) if (!perm.includes(k)) e(`${unde}: cheie nepermisa "${k}"`); };
+ const sir = (v, unde) => { if (typeof v !== 'string' || !v.length) e(`${unde}: trebuie sir nevid`); };
+ const dataIso = (v, unde) => { if (typeof v !== 'string' || Number.isNaN(Date.parse(v)) || !/^\d{4}-\d{2}-\d{2}T/.test(v)) e(`${unde}: nu e date-time`); };
+ chei(bom, E.bom, 'bom');
+ if (bom.bomFormat !== 'CycloneDX') e('bomFormat');
+ if (bom.specVersion !== '1.6') e('specVersion');
+ if (!/^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(bom.serialNumber || '')) e(`serialNumber ${bom.serialNumber}`);
+ if (!Number.isInteger(bom.version) || bom.version < 1) e('version');
+ const md = bom.metadata || {};
+ if (md.timestamp !== undefined) dataIso(md.timestamp, 'metadata.timestamp');
+ if (!md.tools || !Array.isArray(md.tools.components) || !md.tools.components.length) e('metadata.tools.components');
+ else for (const t of md.tools.components) { sir(t.name, 'tool.name'); sir(t.version, 'tool.version'); if (!E.componentType.includes(t.type)) e('tool.type'); }
+ if (!Array.isArray(bom.components)) { e('components nu e lista'); return err; }
+ const refs = new Set();
+ for (const [i, c] of bom.components.entries()) {
+ const u = `components[${i}]`;
+ chei(c, E.component, u);
+ if (!E.componentType.includes(c.type)) e(`${u}.type ${c.type}`);
+ sir(c.name, `${u}.name`);
+ sir(c['bom-ref'], `${u}.bom-ref`);
+ if (refs.has(c['bom-ref'])) e(`${u}: bom-ref dublat ${c['bom-ref']}`);
+ refs.add(c['bom-ref']);
+ if (c.properties) for (const p of c.properties) { sir(p.name, `${u}.properties.name`); if (typeof p.value !== 'string') e(`${u}.properties.value`); }
+ if (c.type === 'cryptographic-asset') {
+ const cp = c.cryptoProperties;
+ if (!cp) { e(`${u}: cryptoProperties lipsa`); continue; }
+ chei(cp, E.cryptoProperties, `${u}.cryptoProperties`);
+ if (!E.assetType.includes(cp.assetType)) e(`${u}.assetType ${cp.assetType}`);
+ if (cp.oid !== undefined && !/^[0-2](\.\d+)+$/.test(cp.oid)) e(`${u}.oid ${cp.oid}`);
+ if (cp.assetType === 'algorithm') {
+ const ap = cp.algorithmProperties;
+ if (!ap) { e(`${u}: algorithmProperties lipsa`); continue; }
+ chei(ap, E.algorithmProperties, `${u}.algorithmProperties`);
+ if (!E.primitive.includes(ap.primitive)) e(`${u}.primitive ${ap.primitive}`);
+ if (ap.parameterSetIdentifier !== undefined) sir(ap.parameterSetIdentifier, `${u}.parameterSetIdentifier`);
+ if (ap.curve !== undefined) sir(ap.curve, `${u}.curve`);
+ if (ap.mode !== undefined && !E.mode.includes(ap.mode)) e(`${u}.mode ${ap.mode}`);
+ if (ap.padding !== undefined && !E.padding.includes(ap.padding)) e(`${u}.padding ${ap.padding}`);
+ if (ap.cryptoFunctions !== undefined && (!Array.isArray(ap.cryptoFunctions) || ap.cryptoFunctions.some((f) => !E.cryptoFunctions.includes(f)))) e(`${u}.cryptoFunctions`);
+ if (ap.classicalSecurityLevel !== undefined && (!Number.isInteger(ap.classicalSecurityLevel) || ap.classicalSecurityLevel < 0)) e(`${u}.classicalSecurityLevel`);
+ if (ap.nistQuantumSecurityLevel !== undefined && (!Number.isInteger(ap.nistQuantumSecurityLevel) || ap.nistQuantumSecurityLevel < 0 || ap.nistQuantumSecurityLevel > 6)) e(`${u}.nistQuantumSecurityLevel`);
+ } else if (cp.assetType === 'protocol') {
+ const pp = cp.protocolProperties || {};
+ chei(pp, E.protocolProperties, `${u}.protocolProperties`);
+ if (!E.protocolType.includes(pp.type)) e(`${u}.protocolProperties.type`);
+ if (pp.version !== undefined) sir(pp.version, `${u}.protocolProperties.version`);
+ } else if (cp.assetType === 'certificate') {
+ const cc = cp.certificateProperties || {};
+ chei(cc, E.certificateProperties, `${u}.certificateProperties`);
+ for (const k of ['notValidBefore', 'notValidAfter']) if (cc[k] !== undefined) dataIso(cc[k], `${u}.${k}`);
+ } else if (cp.assetType === 'related-crypto-material') {
+ const rm = cp.relatedCryptoMaterialProperties || {};
+ chei(rm, E.relatedCryptoMaterialProperties, `${u}.relatedCryptoMaterialProperties`);
+ if (!E.materialType.includes(rm.type)) e(`${u}.relatedCryptoMaterialProperties.type`);
+ if (rm.size !== undefined && !Number.isInteger(rm.size)) e(`${u}.size`);
+ if (rm.value !== undefined) e(`${u}: valoarea materialului nu are voie sa apara`);
+ }
+ }
+ if (!c.evidence || !Array.isArray(c.evidence.occurrences) || !c.evidence.occurrences.length) { e(`${u}: evidence.occurrences lipsa`); continue; }
+ chei(c.evidence, E.evidence, `${u}.evidence`);
+ for (const o of c.evidence.occurrences) {
+ chei(o, E.occurrence, `${u}.occurrence`);
+ sir(o.location, `${u}.occurrence.location`);
+ if (o.line !== undefined && (!Number.isInteger(o.line) || o.line < 1)) e(`${u}.occurrence.line`);
+ }
+ }
+ return err;
+}
+
+proba('cbom: structura CycloneDX 1.6 (campuri obligatorii, tipuri, enumerari)', () => {
+ const rez = scan(FIX);
+ for (const det of [false, true]) {
+ const bom = inv.buildCbom(rez, { deterministic: det });
+ const err = valideazaCbom(bom);
+ asigura(err.length === 0, `erori (${det ? 'determinist' : 'normal'}): ${err.slice(0, 5).join('; ')}`);
+ asigura(det ? bom.metadata.timestamp === undefined : typeof bom.metadata.timestamp === 'string', 'timestamp');
+ }
+ const bom = inv.buildCbom(rez);
+ const tipuri = new Set(bom.components.map((c) => (c.cryptoProperties ? c.cryptoProperties.assetType : c.type)));
+ for (const t of ['algorithm', 'protocol', 'certificate', 'related-crypto-material', 'library']) asigura(tipuri.has(t), `lipseste tipul ${t}`);
+ // controlul negativ al validatorului: o forma stricata trebuie sa fie refuzata
+ const rau = JSON.parse(JSON.stringify(bom));
+ rau.components[0].cryptoProperties = { assetType: 'algorithm', algorithmProperties: { primitive: 'magic' } };
+ rau.components[1]['bom-ref'] = rau.components[2]['bom-ref'];
+ rau.specVersion = '1.5';
+ asigura(valideazaCbom(rau).length >= 3, 'validatorul nu refuza un CBOM stricat');
+});
+
+proba('cbom: bom-ref unice si stabile intre doua rulari', () => {
+ const a = inv.buildCbom(inv.scan(FIX), { deterministic: true });
+ const b = inv.buildCbom(inv.scan(FIX), { deterministic: true });
+ const ra = a.components.map((c) => c['bom-ref']);
+ asigura(new Set(ra).size === ra.length, `bom-ref dublate: ${ra.length - new Set(ra).size}`);
+ asigura(JSON.stringify(a) === JSON.stringify(b), 'doua rulari deterministe difera');
+ const gasiri = inv.scan(FIX).findings.length;
+ const occ = a.components.filter((c) => c.type === 'cryptographic-asset').reduce((s, c) => s + c.evidence.occurrences.length, 0);
+ asigura(occ === gasiri, `occurrences ${occ} != gasiri ${gasiri}`);
+});
+
+proba('cbom: fiecare occurrence arata un fisier care exista, cu o linie din fisier', () => {
+ const bom = inv.buildCbom(scan(FIX));
+ let n = 0;
+ for (const c of bom.components) {
+ for (const o of c.evidence.occurrences) {
+ const f = join(FIX, o.location);
+ asigura(existsSync(f) && statSync(f).isFile(), `nu exista: ${o.location}`);
+ const linii = readFileSync(f, 'utf8').split('\n').length;
+ asigura(o.line >= 1 && o.line <= linii, `${o.location}: linia ${o.line} din ${linii}`);
+ n++;
+ }
+ }
+ asigura(n > 50, `prea putine occurrences: ${n}`);
+});
+
+// --- CLI ---
+function cli(args) {
+ return spawnSync(process.execPath, [CALE_MODUL, ...args], { encoding: 'utf8', timeout: 60000 });
+}
+
+proba('cli: --fail-on vulnerable iese 1 pe cod vulnerabil si 0 pe cod fara criptografie', () => {
+ const r = cli(['scan', join(FIX, 'js'), '--fail-on', 'vulnerable', '--summary']);
+ asigura(r.status === 1 && /quantum-vulnerable finding/.test(r.stderr), `status ${r.status}, stderr ${r.stderr.slice(0, 200)}`);
+ const d = mkdtempSync(join(tmpdir(), 'ci-cli-'));
+ try {
+ copyFileSync(join(FIX, 'negative', 'fara-cripto.js'), join(d, 'fara-cripto.js'));
+ const r0 = cli(['scan', d, '--fail-on', 'vulnerable', '--summary']);
+ asigura(r0.status === 0, `fara criptografie: status ${r0.status}`);
+ } finally { rmSync(d, { recursive: true, force: true }); }
+ const r2 = cli(['scan', join(FIX, 'js'), '--fail-on', 'nimic']);
+ asigura(r2.status === 2, `valoare --fail-on invalida: status ${r2.status}`);
+ const r3 = cli(['scan', join(FIX, 'js'), '--fail-on', 'weak', '--summary']);
+ asigura(r3.status === 1 && /weak-now finding/.test(r3.stderr), `--fail-on weak: status ${r3.status}`);
+});
+
+proba('cli: --summary spune cate fisiere nu au fost citite si de ce', () => {
+ const r = cli(['scan', FIX, '--summary', '--max-file-bytes', '400']);
+ asigura(r.status === 0, `status ${r.status} ${r.stderr}`);
+ const m = /Not read: (\d+) binary, (\d+) too large \(> 400 bytes\), (\d+) over the file limit/.exec(r.stdout);
+ asigura(m && m[1] === '1' && Number(m[2]) >= 1 && m[3] === '0', `randul "Not read": ${m ? m[0] : 'lipsa'}`);
+ asigura(/e\.g\. binary: binar\/modul\.js/.test(r.stdout), 'exemplul binar lipseste din rezumat');
+ const rj = cli(['scan', join(FIX, 'go'), '--deterministic']);
+ const bom = JSON.parse(rj.stdout);
+ asigura(bom.specVersion === '1.6' && valideazaCbom(bom).length === 0, 'CBOM-ul de pe stdout nu e valid');
+});
+
+proba('rezumat: caracterele de control din numele de fisiere nu ajung in terminal', () => {
+ const rez = scan(FIX);
+ const i = rez.findings.findIndex((g) => g.classification === 'quantum-vulnerable');
+ asigura(i >= 0, 'controlul pozitiv: fixturile trebuie sa aiba o gasire vulnerabila');
+ const rau = 'x\u001b]0;titlu\u0007\u001b[31mrosu.js';
+ const t = inv.renderSummary({ ...rez, findings: rez.findings.map((g, k) => (k === i ? { ...g, file: rau } : g)) });
+ asigura(t.includes('x?]0;titlu??[31mrosu.js'), 'numele fisierului trebuia sa apara, cu caracterele de control inlocuite');
+ asigura(!/[\u0000-\u0009\u000b-\u001f\u007f]/.test(t), 'un caracter de control a ajuns in rezumat');
+});
+
+proba('rezumat: numaratoarea pe clase egala cu gasirile', () => {
+ const rez = scan(FIX);
+ const c = inv.countByClass(rez.findings);
+ const s = Object.values(c).reduce((a, b) => a + b, 0);
+ asigura(s === rez.findings.length, `suma claselor ${s}, gasiri ${rez.findings.length}`);
+ const t = inv.renderSummary(rez);
+ for (const [k, v] of Object.entries(c)) asigura(new RegExp(`${k}\\s+${v}\\b`).test(t), `rezumatul nu arata ${k} ${v}`);
+});
+
+// --- rularea ---
+const rezultate = {};
+const erori = {};
+for (const p of probe) {
+ try { await p.fn(); rezultate[p.nume] = 'VERDE'; } catch (err) { rezultate[p.nume] = 'ROSU'; erori[p.nume] = String(err && err.message || err); }
+}
+const verzi = Object.values(rezultate).filter((x) => x === 'VERDE').length;
+if (CA_JSON) {
+ process.stdout.write(JSON.stringify({ suita: 'crypto-inventory', modul: CALE_MODUL, rulate: probe.length, verzi, rosii: probe.length - verzi, rezultate, erori }) + '\n');
+} else {
+ for (const p of probe) process.stdout.write(`${rezultate[p.nume].padEnd(6)} ${p.nume}${erori[p.nume] ? `\n ${erori[p.nume]}` : ''}\n`);
+ process.stdout.write(`\n${verzi} din ${probe.length} probe verzi (modul: ${CALE_MODUL})\n`);
+}
+process.exitCode = verzi === probe.length ? 0 : 1;
diff --git a/pq-kms/README.md b/pq-kms/README.md
index c88b1c4..0dc5e82 100644
--- a/pq-kms/README.md
+++ b/pq-kms/README.md
@@ -45,6 +45,10 @@ the root. Set exactly one of `AERE_KMS_ROOT_KEY` / `AERE_KMS_ROOT_HSM`; both is
AERE_HSM_PIN=... node hsm-radacina.mjs sigileaza --modul /usr/lib/softhsm/libsofthsm2.so --token aere-kms --id 0a --iesire root-hsm.json --nou
AERE_KMS_ROOT_HSM=root-hsm.json AERE_HSM_MODULE=/usr/lib/softhsm/libsofthsm2.so AERE_HSM_PIN=... AERE_KMS_TOKEN=... node server.mjs
+The tool's command words are Romanian: `sigileaza` seals (`--modul` the PKCS#11 library, `--iesire` the output file, never
+overwritten, `--nou` a new random root instead of `AERE_KMS_ROOT_KEY`), and `verifica ` checks that a sealed root opens
+with this HSM and PIN (it also needs `AERE_HSM_MODULE`). It prints `ok:` or `refused: : `.
+
The sealed file does not choose what the process loads or sends: the PKCS#11 library comes from the process
configuration (`AERE_HSM_MODULE`, an absolute path) and must be exactly the one recorded in the file, checked
before any HSM tool runs (`HSM_MODULE_NOT_ALLOWED` otherwise); the PIN is always read from `AERE_HSM_PIN`, and a
diff --git a/pq-kms/hsm-radacina.mjs b/pq-kms/hsm-radacina.mjs
index c045121..f46f9f2 100644
--- a/pq-kms/hsm-radacina.mjs
+++ b/pq-kms/hsm-radacina.mjs
@@ -135,8 +135,8 @@ async function cli(argv) {
console.log(`sealed root written to ${iesire} (${FORMAT_HSM}, token ${sig.token}, key id ${sig.idCheie})`);
} else if (cmd === 'verifica') {
const r = deschideRadacina(JSON.parse(fs.readFileSync(rest[0], 'utf8'))); r.fill(0);
- console.log('DA: the sealed root opens with this HSM and PIN');
+ console.log('ok: the sealed root opens with this HSM and PIN');
} else { console.error('usage: node hsm-radacina.mjs sigileaza --modul --token