diff --git a/Cargo.lock b/Cargo.lock index 6ddb09ebf..d8e998249 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,6 +162,21 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "assert_cmd" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "atty" version = "0.2.14" @@ -262,6 +277,17 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde 1.0.228", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -629,6 +655,21 @@ dependencies = [ "typenum", ] +[[package]] +name = "cryptoscan" +version = "0.1.0" +dependencies = [ + "assert_cmd", + "clap 4.5.38", + "predicates", + "regex", + "serde 1.0.228", + "serde_json", + "tempfile", + "uuid", + "walkdir", +] + [[package]] name = "deflate64" version = "0.1.9" @@ -637,9 +678,9 @@ checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" [[package]] name = "deranged" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", ] @@ -702,6 +743,12 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -782,6 +829,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "filetime" version = "0.2.25" @@ -835,6 +888,15 @@ dependencies = [ "miniz_oxide 0.8.8", ] +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits 0.2.19", +] + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1386,6 +1448,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "nu-ansi-term" version = "0.50.1" @@ -1397,9 +1465,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-traits" @@ -1463,9 +1531,9 @@ checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" [[package]] name = "owo-colors" -version = "4.2.2" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "pbkdf2" @@ -1535,9 +1603,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" @@ -1563,6 +1631,36 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -2318,6 +2416,19 @@ dependencies = [ "xattr", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.3", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -2327,6 +2438,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "textwrap" version = "0.16.2" @@ -2731,6 +2848,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index fb9a66ffa..e4fa392e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "extlib/berkeleydb", + "extlib/cryptoscan", "extlib/millhone", "tools/diagnose", "tools/rendergraph", diff --git a/Makefile b/Makefile index 9e63a0a20..d4736bda2 100644 --- a/Makefile +++ b/Makefile @@ -126,8 +126,8 @@ lint-cargo: @cargo clippy # Build cargo deps needed by the CLI and move them into place for cabal. -build-embedded-rust-bins: target/release/berkeleydb target/release/millhone - cargo build --release --bin millhone --bin berkeleydb +build-embedded-rust-bins: target/release/berkeleydb target/release/cryptoscan target/release/millhone + cargo build --release --bin millhone --bin berkeleydb --bin cryptoscan # Runs linter on only modified files # diff --git a/docs/features/crypto-scanning.md b/docs/features/crypto-scanning.md new file mode 100644 index 000000000..95f71be86 --- /dev/null +++ b/docs/features/crypto-scanning.md @@ -0,0 +1,164 @@ + +# Crypto Scanning + +Crypto Scanning is the name of FOSSA's cryptographic algorithm detection feature. + +Crypto Scanning analyzes source code, dependency manifests, and configuration files +in your project, identifies cryptographic algorithm usage, and classifies each +finding against FIPS 140-3 compliance requirements. Results can be uploaded to +FOSSA, exported as a CycloneDX 1.7 CBOM (Cryptography Bill of Materials), or +printed as a FIPS compliance report. + +Crypto Scanning can be run as part of `fossa analyze`. To enable it, add the +`--x-crypto-scan` flag when you run `fossa analyze`: + +```sh +fossa analyze --x-crypto-scan +``` + +## How Crypto Scanning Works + +When `--x-crypto-scan` is enabled, the CLI: + +1. **Detects Ecosystems**: Identifies which language ecosystems are present in your + project (e.g., Python, Java, Go, Rust, Node.js, Ruby, C#/.NET, PHP, Swift, Elixir). +2. **Scans Source Files**: Uses pattern-based detection across four categories: + - **Dependency analysis**: Known crypto libraries in dependency manifests + (e.g., `pyca/cryptography` in `requirements.txt`, `ring` in `Cargo.toml`) + - **Import pattern matching**: Crypto-related imports + (e.g., `import javax.crypto.Cipher`, `from cryptography.hazmat.primitives import hashes`) + - **API call pattern matching**: Crypto API invocations + (e.g., `Cipher.getInstance("AES/GCM/NoPadding")`, `hashlib.sha256()`) + - **Configuration file scanning**: TLS configs, OpenSSL configs, security properties +3. **Classifies Algorithms**: Maps each detected algorithm to its FIPS 140-3 status + (approved, deprecated, or not approved) and assesses key sizes against NIST minimums. +4. **Produces Results**: Outputs findings as part of the standard analysis pipeline, + with optional CycloneDX CBOM export and FIPS compliance reporting. + +## Supported Ecosystems + +| Ecosystem | Crypto Libraries Detected | File Types Scanned | +|---|---|---| +| **Python** | cryptography, pycryptodome, hashlib, ssl | `*.py`, `requirements.txt`, `pyproject.toml` | +| **Java** | JCA/JCE, BouncyCastle, Conscrypt | `*.java`, `*.kt`, `pom.xml`, `build.gradle` | +| **Go** | crypto/*, x/crypto | `*.go`, `go.mod` | +| **Rust** | ring, rust-crypto, openssl, rustls | `*.rs`, `Cargo.toml` | +| **Node.js** | crypto (builtin), crypto-js, node-forge, jose | `*.js`, `*.ts`, `package.json` | +| **Ruby** | OpenSSL, rbnacl, bcrypt-ruby | `*.rb`, `Gemfile`, `*.gemspec` | +| **C#/.NET** | System.Security.Cryptography, BouncyCastle | `*.cs`, `*.csproj`, `packages.config` | +| **PHP** | openssl/sodium extensions, phpseclib | `*.php`, `composer.json` | +| **Swift** | CryptoKit, CommonCrypto | `*.swift`, `Package.swift`, `Podfile` | +| **Elixir** | :crypto, Comeonin (bcrypt/argon2), JOSE | `*.ex`, `*.exs`, `mix.exs` | + +## Data Sent to FOSSA + +When crypto scan results are uploaded to FOSSA (the default behavior without `--output`), +the following data is sent: + +- Algorithm names and classifications (e.g., "AES-256-GCM", "SHA-256") +- File paths where algorithms were detected +- Detection confidence levels +- FIPS compliance status per algorithm +- Providing library names (e.g., "openssl", "ring") + +Only algorithm names and file locations are sent to FOSSA; no complete source +files are transmitted. Matched code snippets (e.g., an API call or import +statement) may be included to provide detection context. + +## CycloneDX 1.7 CBOM Output + +To export a local CycloneDX 1.7 CBOM file instead of (or in addition to) +uploading to FOSSA, use the `--crypto-cbom-output` flag: + +```sh +fossa analyze --crypto-cbom-output /path/to/cbom.json +``` + +This produces a standards-compliant CycloneDX 1.7 JSON file with: + +- `cryptographic-asset` component types +- `cryptoProperties` with `algorithmProperties` (primitive, mode, key size) +- `fossa:fips-status` component properties for the FIPS classification +- `provides` dependency relationships linking libraries to their algorithms +- Algorithm OIDs where applicable + +The `--crypto-cbom-output` flag implies `--x-crypto-scan` and does not need to +be combined with it explicitly. + +## FIPS Compliance Report + +To print a FIPS compliance summary to stdout, use the `--crypto-fips-report` flag: + +```sh +fossa analyze --crypto-fips-report +``` + +The report includes: + +- **Summary statistics**: Total algorithms detected, FIPS-approved count, + deprecated count, non-FIPS count, and overall compliance percentage +- **Per-algorithm breakdown**: Each detected algorithm with its FIPS status +- **Remediation suggestions**: For non-FIPS algorithms, recommended FIPS + alternatives (e.g., "Replace ChaCha20-Poly1305 with AES-256-GCM") +- **Key size warnings**: Flags algorithms with key sizes below NIST minimums + +The `--crypto-fips-report` flag implies `--x-crypto-scan` and does not need to +be combined with it explicitly. + +### Example output + +``` +FIPS Compliance Report +====================== + +Summary: 23 algorithms detected + Approved: 15 (65%) + Deprecated: 3 (13%) + Not Approved: 5 (22%) + +Remediation Suggestions: + ChaCha20-Poly1305 -> AES-256-GCM + BLAKE2b -> SHA-256 / SHA-3 + X25519 -> ECDH P-256 + bcrypt -> PBKDF2 + MD5 -> SHA-256 +``` + +## Combining Flags + +All crypto scanning flags can be combined: + +```sh +# Scan, upload results, export CBOM, and print FIPS report +fossa analyze --x-crypto-scan --crypto-cbom-output cbom.json --crypto-fips-report + +# Local-only: export CBOM without uploading +fossa analyze --output --crypto-cbom-output cbom.json + +# FIPS report only +fossa analyze --output --crypto-fips-report +``` + +## FIPS Compliance Reference + +### FIPS-Approved Algorithms + +| Category | Algorithms | +|---|---| +| Symmetric Encryption | AES-128/192/256 (ECB mode is deprecated by 2030) | +| Hash Functions | SHA-256, SHA-384, SHA-512, SHA-3 family, SHAKE128/256 | +| Signatures | RSA >= 2048-bit, ECDSA (P-256/P-384/P-521), EdDSA, ML-DSA | +| Key Exchange | ECDH (P-256/P-384/P-521), DH >= 2048-bit, ML-KEM | +| MACs | HMAC, CMAC, GMAC, KMAC | +| KDFs | HKDF, PBKDF2, SP 800-108 KDFs | + +### Common Non-FIPS Algorithms + +| Algorithm Found | Recommended FIPS Alternative | +|---|---| +| ChaCha20-Poly1305 | AES-256-GCM | +| BLAKE2/BLAKE3 | SHA-256 / SHA-3 | +| X25519/X448 | ECDH P-256/P-384 | +| MD5 | SHA-256 | +| RC4, Blowfish, DES | AES | +| Argon2, scrypt, bcrypt | PBKDF2 | diff --git a/docs/references/experimental/crypto-scanning/README.md b/docs/references/experimental/crypto-scanning/README.md new file mode 100644 index 000000000..99cb26ee9 --- /dev/null +++ b/docs/references/experimental/crypto-scanning/README.md @@ -0,0 +1,45 @@ +# Crypto Scanning + +FOSSA supports the ability to detect cryptographic algorithm usage in your project source tree and assess FIPS 140-3 compliance via an opt-in flag (`--x-crypto-scan`). + +The core idea behind this feature is that organizations subject to FIPS compliance requirements need visibility into which cryptographic algorithms their software uses, whether those algorithms are FIPS-approved, and what remediation steps are needed for non-compliant usage. + +_Important: For support and other general information, refer to the [experimental options overview](../README.md) before using experimental options._ + +## Discovery + +Crypto Scanning automatically detects which language ecosystems are present in your project by examining manifest files (e.g., `requirements.txt`, `pom.xml`, `go.mod`, `Cargo.toml`, `package.json`, `Gemfile`, `*.csproj`, `composer.json`, `Package.swift`, `mix.exs`). + +Multiple ecosystems are supported: Python, Java, Go, Rust, Node.js, Ruby, C#/.NET, PHP, Swift, and Elixir. + +## Analysis + +The scanner uses four detection methods, applied in order of specificity: + +| Detection Method | Description | Example | +|---|---|---| +| Dependency manifest | Known crypto libraries in lock/manifest files | `cryptography` in `requirements.txt` | +| Import statement | Crypto-related import/require patterns | `import "crypto/aes"` in Go | +| API call | Direct crypto API invocations | `Cipher.getInstance("AES/GCM/NoPadding")` in Java | +| Configuration file | TLS/SSL/crypto configuration entries | `ssl_protocols TLSv1.3` in nginx config | + +Each detected algorithm is classified with: + +- **FIPS status**: Approved, deprecated, or not approved per NIST SP 800-131A Rev. 2 +- **Key size assessment**: Whether the key size meets NIST minimum requirements +- **Confidence level**: High, medium, or low based on detection method specificity +- **Providing library**: The library or framework providing the algorithm + +## Output Formats + +| Flag | Output | +|---|---| +| `--x-crypto-scan` | Include crypto findings in standard FOSSA upload | +| `--crypto-cbom-output FILE` | Write CycloneDX 1.7 CBOM JSON to a local file | +| `--crypto-fips-report` | Print FIPS compliance summary to stdout | + +Both `--crypto-cbom-output` and `--crypto-fips-report` imply `--x-crypto-scan`. + +## More Detail + +For the full list of supported ecosystems, detected libraries, FIPS compliance reference, and usage examples, see [the Crypto Scanning feature documentation](../../../features/crypto-scanning.md). diff --git a/docs/references/subcommands/analyze.md b/docs/references/subcommands/analyze.md index b59763bd9..320321caf 100644 --- a/docs/references/subcommands/analyze.md +++ b/docs/references/subcommands/analyze.md @@ -177,6 +177,27 @@ For more detail about how Vendetta works, how to use file filtering during scanning, or what information is sent to FOSSA's servers, see [the Vendetta feature documentation](../../features/vendetta.md). +### Cryptographic Algorithm Scanning + +Crypto Scanning detects cryptographic algorithm usage across multiple language ecosystems +and classifies findings against FIPS 140-3 compliance requirements. Results can be +uploaded to FOSSA, exported as a CycloneDX 1.7 CBOM, or printed as a FIPS +compliance report. + +#### Enabling Crypto Scanning + +| Name | Description | +|--------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--x-crypto-scan` | Enable cryptographic algorithm detection during analysis. This experimental feature scans source files, imports, API calls, and config files for crypto usage across multiple ecosystems. | +| `--crypto-cbom-output FILE` | Write a CycloneDX 1.7 CBOM (Cryptography Bill of Materials) JSON file to the specified path. Implies `--x-crypto-scan`. | +| `--crypto-fips-report` | Print a FIPS 140-3 compliance summary to stdout with per-algorithm status and remediation suggestions. Implies `--x-crypto-scan`. | + +#### More detail + +For more detail about how Crypto Scanning works, supported ecosystems and +libraries, FIPS compliance reference, and CycloneDX CBOM output format, see +[the Crypto Scanning feature documentation](../../features/crypto-scanning.md). + ### Experimental Options _Important: For support and other general information, refer to the [experimental options overview](../experimental/README.md) before using experimental options._ @@ -189,6 +210,9 @@ In addition to the [standard flags](#specifying-fossa-project-details), the anal | `--experimental-force-first-party-scans` | Force [first party scans](../../features/first-party-license-scans.md) to run | | `--experimental-block-first-party-scans` | Force [first party scans](../../features/first-party-license-scans.md) to not run. This can be used to forcibly turn off first-party scans if your organization defaults to first-party scans. | | `--experimental-analyze-path-dependencies` | License scan path dependencies, and include them in the final analysis. For more information, see the [path dependency overview](../experimental/path-dependency.md). | +| [`--x-crypto-scan`](../experimental/crypto-scanning/README.md) | Enable cryptographic algorithm detection and FIPS compliance assessment. For more information, see the [crypto scanning overview](../experimental/crypto-scanning/README.md). | +| [`--crypto-cbom-output `](../experimental/crypto-scanning/README.md) | Export a CycloneDX 1.7 CBOM (Cryptography Bill of Materials) JSON file to the specified path. Implies `--x-crypto-scan`. | +| [`--crypto-fips-report`](../experimental/crypto-scanning/README.md) | Print a FIPS 140-3 compliance summary to stdout with per-algorithm status and remediation suggestions. Implies `--x-crypto-scan`. | ### F.A.Q. diff --git a/extlib/cryptoscan/Cargo.lock b/extlib/cryptoscan/Cargo.lock new file mode 100644 index 000000000..b2f8ea128 --- /dev/null +++ b/extlib/cryptoscan/Cargo.lock @@ -0,0 +1,822 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + +[[package]] +name = "assert_cmd" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c81d250916401487680ed13b8b675660281dcfc3ab0121fe44c94bcab9eae2fb" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5caf74d17c3aec5495110c34cc3f78644bfa89af6c8993ed4de2790e49b6499" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "370daa45065b80218950227371916a1633217ae42b2715b2287b606dcd618e24" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "cryptoscan" +version = "0.1.0" +dependencies = [ + "assert_cmd", + "clap", + "predicates", + "regex", + "serde", + "serde_json", + "tempfile", + "uuid", + "walkdir", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "getrandom", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/extlib/cryptoscan/Cargo.toml b/extlib/cryptoscan/Cargo.toml new file mode 100644 index 000000000..718705604 --- /dev/null +++ b/extlib/cryptoscan/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "cryptoscan" +version = "0.1.0" +edition = "2021" +rust-version = "1.70" +description = "Cryptographic algorithm detection engine for FOSSA CLI" + +[[bin]] +name = "cryptoscan" +path = "src/main.rs" + +[dependencies] +serde = { version = "1.0.183", features = ["derive"] } +serde_json = "1.0.107" +regex = "1" +walkdir = "2.4.0" +clap = { version = "4.3.21", features = ["derive"] } +uuid = { version = "1.4.1", features = ["v4"] } + +[dev-dependencies] +tempfile = "3" +assert_cmd = "2" +predicates = "3" diff --git a/extlib/cryptoscan/src/crypto_algorithm.rs b/extlib/cryptoscan/src/crypto_algorithm.rs new file mode 100644 index 000000000..e948bc67b --- /dev/null +++ b/extlib/cryptoscan/src/crypto_algorithm.rs @@ -0,0 +1,126 @@ +use serde::{Deserialize, Serialize}; + +use crate::fips::FipsStatus; + +/// A detected cryptographic algorithm with all its metadata. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CryptoAlgorithm { + /// Canonical name (e.g., "AES-256-GCM", "SHA-256", "ChaCha20-Poly1305") + pub name: String, + /// CycloneDX 1.7 algorithm family from the registry + pub algorithm_family: String, + /// Cryptographic primitive type + pub primitive: Primitive, + /// Parameter set identifier (e.g., "256" for AES-256, "2048" for RSA-2048) + pub parameter_set: Option, + /// Elliptic curve (for ECC algorithms) + pub elliptic_curve: Option, + /// Block cipher mode (for symmetric ciphers) + pub mode: Option, + /// OID if known + pub oid: Option, + /// Classical security level in bits + pub classical_security_level: Option, + /// NIST quantum security level (0 = not quantum safe, 1-5) + pub nist_quantum_security_level: u8, + /// FIPS compliance status + pub fips_status: FipsStatus, + /// Crypto functions performed + pub crypto_functions: Vec, +} + +/// Cryptographic primitive type (maps to CycloneDX 1.7 enum). +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum Primitive { + Ae, + BlockCipher, + StreamCipher, + Hash, + Mac, + Signature, + Pke, + Kem, + KeyAgree, + KeyWrap, + Kdf, + Xof, + Drbg, + Combiner, + Other, + Unknown, +} + +impl Primitive { + pub fn as_str(&self) -> &'static str { + match self { + Primitive::Ae => "ae", + Primitive::BlockCipher => "block-cipher", + Primitive::StreamCipher => "stream-cipher", + Primitive::Hash => "hash", + Primitive::Mac => "mac", + Primitive::Signature => "signature", + Primitive::Pke => "pke", + Primitive::Kem => "kem", + Primitive::KeyAgree => "key-agree", + Primitive::KeyWrap => "key-wrap", + Primitive::Kdf => "kdf", + Primitive::Xof => "xof", + Primitive::Drbg => "drbg", + Primitive::Combiner => "combiner", + Primitive::Other => "other", + Primitive::Unknown => "unknown", + } + } +} + +/// A single crypto finding: an algorithm detected at a specific location. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CryptoFinding { + /// The detected algorithm + pub algorithm: CryptoAlgorithm, + /// Source file where detected + pub file_path: String, + /// Line number (0 if unknown) + pub line_number: usize, + /// The matched text snippet + pub matched_text: String, + /// Detection method + pub detection_method: DetectionMethod, + /// Ecosystem in which it was found + pub ecosystem: String, + /// The library providing this algorithm (if known) + pub providing_library: Option, + /// Confidence level + pub confidence: Confidence, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum DetectionMethod { + DependencyManifest, + ImportStatement, + ApiCall, + ConfigFile, + StringLiteral, +} + +impl DetectionMethod { + pub fn as_str(&self) -> &'static str { + match self { + DetectionMethod::DependencyManifest => "dependency-manifest", + DetectionMethod::ImportStatement => "import-statement", + DetectionMethod::ApiCall => "api-call", + DetectionMethod::ConfigFile => "config-file", + DetectionMethod::StringLiteral => "string-literal", + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum Confidence { + High, + Medium, + Low, +} diff --git a/extlib/cryptoscan/src/cyclonedx.rs b/extlib/cryptoscan/src/cyclonedx.rs new file mode 100644 index 000000000..2c9b4bcc6 --- /dev/null +++ b/extlib/cryptoscan/src/cyclonedx.rs @@ -0,0 +1,414 @@ +use serde::{Deserialize, Serialize}; +use std::collections::{BTreeMap, BTreeSet}; +use uuid::Uuid; + +use crate::crypto_algorithm::CryptoFinding; + +/// CycloneDX 1.7 BOM structure for CBOM output. +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CycloneDxBom { + pub bom_format: String, + pub spec_version: String, + pub serial_number: String, + pub version: u32, + pub metadata: BomMetadata, + pub components: Vec, + pub dependencies: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BomMetadata { + pub timestamp: String, + pub tools: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub component: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BomTool { + pub name: String, + pub version: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BomMetadataComponent { + #[serde(rename = "type")] + pub component_type: String, + pub name: String, + pub version: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BomComponent { + #[serde(rename = "type")] + pub component_type: String, + #[serde(rename = "bom-ref")] + pub bom_ref: String, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub crypto_properties: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub properties: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CryptoProperties { + pub asset_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub algorithm_properties: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub oid: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AlgorithmProperties { + pub primitive: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub algorithm_family: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub parameter_set_identifier: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub elliptic_curve: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, + pub execution_environment: String, + pub implementation_platform: String, + pub certification_level: Vec, + pub crypto_functions: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub classical_security_level: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub nist_quantum_security_level: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct BomProperty { + pub name: String, + pub value: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BomDependency { + #[serde(rename = "ref")] + pub dep_ref: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub depends_on: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub provides: Option>, +} + +/// Convert a list of crypto findings into a CycloneDX 1.7 BOM. +pub fn to_cyclonedx_bom(findings: &[CryptoFinding]) -> CycloneDxBom { + let mut components = Vec::new(); + let mut dependencies = Vec::new(); + // Key by (ecosystem, library_name) to avoid collapsing libraries across ecosystems + let mut library_algorithms: BTreeMap<(String, String), Vec> = BTreeMap::new(); + + // Group findings by bom_ref to aggregate detection contexts + let mut algo_findings: BTreeMap> = BTreeMap::new(); + for finding in findings { + let bom_ref = make_bom_ref(&finding.algorithm.name, &finding.algorithm.oid); + + // Track library -> algorithm for `provides` relationships + if let Some(lib) = &finding.providing_library { + library_algorithms + .entry((finding.ecosystem.clone(), lib.clone())) + .or_default() + .push(bom_ref.clone()); + } + + algo_findings.entry(bom_ref).or_default().push(finding); + } + + // Create cryptographic-asset components with aggregated detection contexts + for (bom_ref, group) in &algo_findings { + let first = group[0]; + + let primitive_str = first.algorithm.primitive.as_str().to_string(); + + let fips_label = first.algorithm.fips_status.label().to_string(); + + let algo_props = AlgorithmProperties { + primitive: primitive_str, + algorithm_family: valid_algorithm_family(&first.algorithm.algorithm_family), + parameter_set_identifier: first.algorithm.parameter_set.clone(), + elliptic_curve: first.algorithm.elliptic_curve.clone(), + mode: first.algorithm.mode.clone(), + execution_environment: "software-plain-ram".to_string(), + implementation_platform: "generic".to_string(), + certification_level: vec!["none".to_string()], + crypto_functions: first.algorithm.crypto_functions.clone(), + classical_security_level: first.algorithm.classical_security_level, + nist_quantum_security_level: if first.algorithm.nist_quantum_security_level > 0 { + Some(first.algorithm.nist_quantum_security_level) + } else { + None + }, + }; + + // Aggregate detection contexts from all findings for this algorithm + let mut properties = vec![BomProperty { + name: "fossa:fips-status".to_string(), + value: fips_label, + }]; + + // Collect unique detection locations, methods, and ecosystems + let mut seen_locations: BTreeSet = BTreeSet::new(); + let mut seen_methods: BTreeSet = BTreeSet::new(); + let mut seen_ecosystems: BTreeSet = BTreeSet::new(); + + for finding in group { + seen_locations.insert(finding.file_path.clone()); + seen_methods.insert(finding.detection_method.as_str().to_string()); + seen_ecosystems.insert(finding.ecosystem.clone()); + } + + for location in &seen_locations { + properties.push(BomProperty { + name: "fossa:detected-in".to_string(), + value: location.clone(), + }); + } + for method in &seen_methods { + properties.push(BomProperty { + name: "fossa:detection-method".to_string(), + value: method.clone(), + }); + } + for ecosystem in &seen_ecosystems { + properties.push(BomProperty { + name: "fossa:ecosystem".to_string(), + value: ecosystem.clone(), + }); + } + + let component = BomComponent { + component_type: "cryptographic-asset".to_string(), + bom_ref: bom_ref.clone(), + name: first.algorithm.name.clone(), + crypto_properties: Some(CryptoProperties { + asset_type: "algorithm".to_string(), + algorithm_properties: Some(algo_props), + oid: first.algorithm.oid.clone(), + }), + properties: Some(properties), + }; + + components.push(component); + + // Algorithm dependency entry + dependencies.push(BomDependency { + dep_ref: bom_ref.clone(), + depends_on: None, + provides: None, + }); + } + + // Create library components with `provides` relationships + for ((ecosystem, lib_name), algo_refs) in &library_algorithms { + let lib_ref = format!("lib/{}/{}", ecosystem, lib_name); + + components.push(BomComponent { + component_type: "library".to_string(), + bom_ref: lib_ref.clone(), + name: lib_name.clone(), + crypto_properties: None, + properties: None, + }); + + let unique_algos: Vec = algo_refs + .iter() + .cloned() + .collect::>() + .into_iter() + .collect(); + + dependencies.push(BomDependency { + dep_ref: lib_ref, + depends_on: None, + provides: Some(unique_algos), + }); + } + + let now = chrono_timestamp(); + + CycloneDxBom { + bom_format: "CycloneDX".to_string(), + spec_version: "1.7".to_string(), + serial_number: format!("urn:uuid:{}", Uuid::new_v4()), + version: 1, + metadata: BomMetadata { + timestamp: now, + tools: vec![BomTool { + name: "fossa-cryptoscan".to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }], + component: None, + }, + components, + dependencies, + } +} + +fn make_bom_ref(name: &str, oid: &Option) -> String { + let sanitized = name.to_lowercase().replace([' ', '/'], "-"); + match oid { + Some(o) => format!("crypto/algorithm/{}@{}", sanitized, o), + None => format!("crypto/algorithm/{}", sanitized), + } +} + +/// Validate an algorithm family string against the official CycloneDX 1.7 enum. +/// Returns `Some(canonical_form)` if matched, `None` otherwise. +fn valid_algorithm_family(family: &str) -> Option { + // Official CycloneDX 1.7 algorithmFamily enum values (case-sensitive in schema) + const CANONICAL_FAMILIES: &[&str] = &[ + "3DES", + "3GPP-XOR", + "A5/1", + "A5/2", + "AES", + "ARIA", + "Ascon", + "BLAKE2", + "BLAKE3", + "BLS", + "Blowfish", + "CAMELLIA", + "CAST5", + "CAST6", + "CMAC", + "CMEA", + "ChaCha", + "ChaCha20", + "DES", + "DSA", + "ECDH", + "ECDSA", + "ECIES", + "EdDSA", + "ElGamal", + "FFDH", + "Fortuna", + "GOST", + "HC", + "HKDF", + "HMAC", + "IDEA", + "IKE-PRF", + "KMAC", + "LMS", + "MD2", + "MD4", + "MD5", + "MILENAGE", + "ML-DSA", + "ML-KEM", + "MQV", + "PBES1", + "PBES2", + "PBKDF1", + "PBKDF2", + "PBMAC1", + "Poly1305", + "RABBIT", + "RC2", + "RC4", + "RC5", + "RC6", + "RIPEMD", + "RSAES-OAEP", + "RSAES-PKCS1", + "RSASSA-PKCS1", + "RSASSA-PSS", + "SEED", + "SHA-1", + "SHA-2", + "SHA-3", + "SLH-DSA", + "SNOW3G", + "SP800-108", + "Salsa20", + "Serpent", + "SipHash", + "Skipjack", + "TUAK", + "Twofish", + "Whirlpool", + "X3DH", + "XMSS", + "Yarrow", + "ZUC", + "bcrypt", + ]; + CANONICAL_FAMILIES + .iter() + .find(|&&canonical| canonical.eq_ignore_ascii_case(family)) + .map(|&canonical| canonical.to_string()) +} + +fn chrono_timestamp() -> String { + // Simple UTC timestamp without chrono dependency + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_secs(); + // Format as ISO 8601 (approximate) + let days = now / 86400; + let rem = now % 86400; + let hours = rem / 3600; + let mins = (rem % 3600) / 60; + let secs = rem % 60; + + // Calculate year/month/day from days since epoch + let (year, month, day) = days_to_date(days); + format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", + year, month, day, hours, mins, secs + ) +} + +fn days_to_date(days_since_epoch: u64) -> (u64, u64, u64) { + // Simplified date calculation + let mut days = days_since_epoch as i64; + let mut year = 1970i64; + + loop { + let days_in_year = if is_leap(year) { 366 } else { 365 }; + if days < days_in_year { + break; + } + days -= days_in_year; + year += 1; + } + + let months_days: Vec = if is_leap(year) { + vec![31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + } else { + vec![31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + }; + + let mut month = 0; + for (i, &md) in months_days.iter().enumerate() { + if days < md { + month = i + 1; + break; + } + days -= md; + } + // Invariant: month is always set by the loop because days < days_in_year + // after the year calculation, so at least one iteration will satisfy days < md. + debug_assert!(month > 0, "days_to_date: month calculation failed"); + + (year as u64, month as u64, (days + 1) as u64) +} + +fn is_leap(year: i64) -> bool { + (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} diff --git a/extlib/cryptoscan/src/fips.rs b/extlib/cryptoscan/src/fips.rs new file mode 100644 index 000000000..b2bd6a15b --- /dev/null +++ b/extlib/cryptoscan/src/fips.rs @@ -0,0 +1,354 @@ +use serde::{Deserialize, Serialize}; + +/// FIPS compliance status for a cryptographic algorithm. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "kebab-case")] +pub enum FipsStatus { + /// Fully FIPS-approved + Approved, + /// FIPS-approved but deprecated (e.g., SHA-1 for non-signature, 3DES decrypt-only) + Deprecated, + /// Not FIPS-approved + NotApproved, +} + +impl FipsStatus { + pub fn is_approved(&self) -> bool { + matches!(self, FipsStatus::Approved) + } + + pub fn label(&self) -> &str { + match self { + FipsStatus::Approved => "FIPS Approved", + FipsStatus::Deprecated => "FIPS Deprecated", + FipsStatus::NotApproved => "Not FIPS Approved", + } + } +} + +/// Classify an algorithm name to its FIPS status using our comprehensive database. +pub fn classify_algorithm(name: &str) -> (FipsStatus, Option) { + let lower = name.to_lowercase(); + + // --- Symmetric Encryption --- + if lower.contains("aes") { + if lower.contains("ecb") { + return ( + FipsStatus::Deprecated, + Some("AES-ECB mode is deprecated; use AES-GCM or AES-CBC".into()), + ); + } + return (FipsStatus::Approved, None); + } + + if lower.contains("chacha20") || lower.contains("chacha") { + return ( + FipsStatus::NotApproved, + Some("Replace with AES-256-GCM".into()), + ); + } + + if lower.contains("3des") + || lower.contains("triple") && lower.contains("des") + || lower.contains("desede") + || lower.contains("tdea") + { + return ( + FipsStatus::Deprecated, + Some("3DES is legacy-only since Jan 2024; migrate to AES".into()), + ); + } + + if lower.contains("blowfish") || lower == "bf" || lower.contains("bf-cbc") { + return (FipsStatus::NotApproved, Some("Replace with AES".into())); + } + + if lower.contains("rc4") || lower.contains("arcfour") || lower.contains("arc4") { + return (FipsStatus::NotApproved, Some("Replace with AES-GCM".into())); + } + + if lower == "des" || lower == "des-cbc" || lower == "des-ede" { + return ( + FipsStatus::NotApproved, + Some("DES is insecure; replace with AES".into()), + ); + } + + if lower.contains("salsa20") || lower.contains("xsalsa") { + return ( + FipsStatus::NotApproved, + Some("Replace with AES-256-GCM".into()), + ); + } + + if lower.contains("camellia") + || lower.contains("twofish") + || lower.contains("cast5") + || lower.contains("seed") + || lower.contains("aria") + || lower.contains("sm4") + || lower.contains("idea") + { + return (FipsStatus::NotApproved, Some("Replace with AES".into())); + } + + // --- Hash Functions --- + if lower.contains("sha3") || lower.contains("sha-3") { + return (FipsStatus::Approved, None); + } + + if lower.contains("sha256") || lower.contains("sha-256") || lower.contains("sha_256") { + return (FipsStatus::Approved, None); + } + + if lower.contains("sha384") || lower.contains("sha-384") || lower.contains("sha_384") { + return (FipsStatus::Approved, None); + } + + if lower.contains("sha512") || lower.contains("sha-512") || lower.contains("sha_512") { + return (FipsStatus::Approved, None); + } + + if lower.contains("sha224") || lower.contains("sha-224") { + return ( + FipsStatus::Deprecated, + Some("SHA-224 will be disallowed by 2030; use SHA-256+".into()), + ); + } + + if lower.contains("sha1") || lower.contains("sha-1") || lower == "sha" { + return ( + FipsStatus::Deprecated, + Some("SHA-1 is deprecated; use SHA-256 or SHA-3".into()), + ); + } + + if lower.contains("shake128") || lower.contains("shake256") { + return (FipsStatus::Approved, None); + } + + if lower.contains("md5") { + return ( + FipsStatus::NotApproved, + Some("MD5 is not FIPS-approved; use SHA-256".into()), + ); + } + + if lower.contains("md4") { + return ( + FipsStatus::NotApproved, + Some("MD4 is broken; use SHA-256".into()), + ); + } + + if lower.contains("blake2") || lower.contains("blake3") { + return ( + FipsStatus::NotApproved, + Some("Replace with SHA-256 or SHA-3".into()), + ); + } + + if lower.contains("ripemd") || lower.contains("whirlpool") || lower.contains("sm3") { + return ( + FipsStatus::NotApproved, + Some("Replace with SHA-256 or SHA-3".into()), + ); + } + + // --- Asymmetric / Signatures --- + if lower.contains("rsa") { + // Extract key size from algorithm name (e.g., "rsa-1536", "rsa-2048") + let key_size = lower + .split(|c: char| !c.is_ascii_digit()) + .find_map(|tok| tok.parse::().ok()); + + if let Some(bits) = key_size { + if bits < 2048 { + return ( + FipsStatus::NotApproved, + Some("RSA key size below 2048 bits; use RSA-2048 or higher".into()), + ); + } + return (FipsStatus::Approved, None); + } + return ( + FipsStatus::Deprecated, + Some( + "RSA key size not detected; manual verification needed for FIPS compliance".into(), + ), + ); + } + + if lower.contains("ecdsa") { + // If a specific curve is mentioned in the name, we can approve + if lower.contains("p-256") + || lower.contains("p-384") + || lower.contains("p-521") + || lower.contains("p256") + || lower.contains("p384") + || lower.contains("p521") + || lower.contains("secp256r1") + || lower.contains("secp384r1") + || lower.contains("secp521r1") + { + return (FipsStatus::Approved, None); + } + return ( + FipsStatus::Deprecated, + Some("ECDSA curve not detected; manual verification needed for FIPS compliance".into()), + ); + } + + if lower.contains("ed25519") || lower.contains("ed448") || lower.contains("eddsa") { + return (FipsStatus::Approved, None); // Approved for signatures per FIPS 186-5 + } + + // Post-quantum DSA variants (must be checked before generic DSA) + if lower.contains("slh-dsa") || lower.contains("slhdsa") || lower.contains("sphincs") { + return (FipsStatus::Approved, None); + } + + if lower.contains("ml-dsa") || lower.contains("mldsa") || lower.contains("dilithium") { + return (FipsStatus::Approved, None); + } + + if lower.contains("dsa") && !lower.contains("ecdsa") && !lower.contains("eddsa") { + return ( + FipsStatus::Deprecated, + Some("DSA is deprecated; only verification allowed. Use ECDSA or EdDSA".into()), + ); + } + + // --- Key Exchange --- + if lower.contains("ecdh") { + if lower.contains("x25519") || lower.contains("curve25519") { + return ( + FipsStatus::NotApproved, + Some("X25519 is not FIPS-approved for key exchange; use ECDH P-256/P-384".into()), + ); + } + // If a specific NIST curve is mentioned, we can approve + if lower.contains("p-256") + || lower.contains("p-384") + || lower.contains("p-521") + || lower.contains("p256") + || lower.contains("p384") + || lower.contains("p521") + || lower.contains("secp256r1") + || lower.contains("secp384r1") + || lower.contains("secp521r1") + { + return (FipsStatus::Approved, None); + } + return ( + FipsStatus::Deprecated, + Some("ECDH curve not detected; manual verification needed for FIPS compliance".into()), + ); + } + + if lower.contains("x25519") || lower.contains("curve25519") { + return ( + FipsStatus::NotApproved, + Some("X25519/Curve25519 is not FIPS-approved; use ECDH with NIST curves".into()), + ); + } + + if lower.contains("x448") { + return ( + FipsStatus::NotApproved, + Some("X448 is not FIPS-approved; use ECDH P-384 or P-521".into()), + ); + } + + if lower.contains("diffie") || lower == "dh" { + // Extract key size from algorithm name (e.g., "dh-2048") + let key_size = lower + .split(|c: char| !c.is_ascii_digit()) + .find_map(|tok| tok.parse::().ok()); + + if let Some(bits) = key_size { + if bits >= 2048 { + return (FipsStatus::Approved, None); + } + return ( + FipsStatus::NotApproved, + Some("DH key size below 2048 bits; use DH-2048 or higher".into()), + ); + } + return ( + FipsStatus::Deprecated, + Some("DH key size not detected; manual verification needed for FIPS compliance".into()), + ); + } + + // --- Post-Quantum --- + if lower.contains("ml-kem") || lower.contains("mlkem") || lower.contains("kyber") { + return (FipsStatus::Approved, None); + } + + // --- MACs --- + if lower.contains("hmac") { + return (FipsStatus::Approved, None); + } + + if lower.contains("cmac") || lower.contains("gmac") || lower.contains("kmac") { + return (FipsStatus::Approved, None); + } + + if lower.contains("poly1305") { + return ( + FipsStatus::NotApproved, + Some("Poly1305 is not FIPS-approved; use HMAC or CMAC".into()), + ); + } + + if lower.contains("siphash") { + return ( + FipsStatus::NotApproved, + Some("SipHash is not FIPS-approved; use HMAC".into()), + ); + } + + // --- KDFs --- + if lower.contains("hkdf") || lower.contains("pbkdf2") { + return (FipsStatus::Approved, None); + } + + if lower.contains("argon2") { + return ( + FipsStatus::NotApproved, + Some("Argon2 is not FIPS-approved; use PBKDF2".into()), + ); + } + + if lower.contains("scrypt") { + return ( + FipsStatus::NotApproved, + Some("scrypt is not FIPS-approved; use PBKDF2".into()), + ); + } + + if lower.contains("bcrypt") { + return ( + FipsStatus::NotApproved, + Some("bcrypt is not FIPS-approved; use PBKDF2".into()), + ); + } + + // --- DRBGs --- + if lower.contains("drbg") { + if lower.contains("dual_ec") || lower.contains("dual-ec") { + return ( + FipsStatus::NotApproved, + Some("Dual_EC_DRBG was removed; use Hash_DRBG, HMAC_DRBG, or CTR_DRBG".into()), + ); + } + return (FipsStatus::Approved, None); + } + + // Default: unknown algorithms are flagged for review + ( + FipsStatus::NotApproved, + Some("Unknown algorithm; verify FIPS status manually".into()), + ) +} diff --git a/extlib/cryptoscan/src/main.rs b/extlib/cryptoscan/src/main.rs new file mode 100644 index 000000000..96dd7870b --- /dev/null +++ b/extlib/cryptoscan/src/main.rs @@ -0,0 +1,108 @@ +mod crypto_algorithm; +mod cyclonedx; +mod fips; +mod patterns; +mod scanner; + +use clap::{Parser, ValueEnum}; +use std::path::PathBuf; + +#[derive(Copy, Clone, Debug, ValueEnum)] +enum OutputFormat { + Json, + Cyclonedx, +} + +#[derive(Copy, Clone, Debug, ValueEnum)] +enum Ecosystem { + Auto, + Python, + Java, + Go, + Node, + Rust, + Ruby, + Csharp, + Php, + Swift, + Elixir, +} + +impl Ecosystem { + fn as_str(&self) -> &'static str { + match self { + Ecosystem::Auto => "auto", + Ecosystem::Python => "python", + Ecosystem::Java => "java", + Ecosystem::Go => "go", + Ecosystem::Node => "node", + Ecosystem::Rust => "rust", + Ecosystem::Ruby => "ruby", + Ecosystem::Csharp => "csharp", + Ecosystem::Php => "php", + Ecosystem::Swift => "swift", + Ecosystem::Elixir => "elixir", + } + } +} + +#[derive(Parser, Debug)] +#[command( + name = "cryptoscan", + about = "Detect cryptographic algorithm usage in source code" +)] +struct Cli { + /// Path to the project directory to scan + #[arg(short, long)] + path: PathBuf, + + /// Ecosystem hint + #[arg(short, long, value_enum, default_value_t = Ecosystem::Auto)] + ecosystem: Ecosystem, + + /// Output format (json, cyclonedx) + #[arg(short, long, value_enum, default_value_t = OutputFormat::Cyclonedx)] + format: OutputFormat, + + /// Only show non-FIPS-compliant findings + #[arg(long, default_value_t = false)] + non_fips_only: bool, +} + +fn main() { + let cli = Cli::parse(); + + if !cli.path.is_dir() { + eprintln!("Error: '{}' is not a directory", cli.path.display()); + std::process::exit(1); + } + + let ecosystems = if matches!(cli.ecosystem, Ecosystem::Auto) { + scanner::detect_ecosystems(&cli.path) + } else { + vec![cli.ecosystem.as_str().to_string()] + }; + + let findings = scanner::scan_project(&cli.path, &ecosystems); + + let findings = if cli.non_fips_only { + findings + .into_iter() + .filter(|f| !f.algorithm.fips_status.is_approved()) + .collect() + } else { + findings + }; + + let output = match cli.format { + OutputFormat::Json => { + serde_json::to_string_pretty(&findings).expect("Failed to serialize findings") + } + OutputFormat::Cyclonedx => { + let bom = cyclonedx::to_cyclonedx_bom(&findings); + serde_json::to_string_pretty(&bom).expect("Failed to serialize CBOM") + } + }; + + println!("{}", output); +} diff --git a/extlib/cryptoscan/src/patterns.rs b/extlib/cryptoscan/src/patterns.rs new file mode 100644 index 000000000..7cec9b061 --- /dev/null +++ b/extlib/cryptoscan/src/patterns.rs @@ -0,0 +1,4516 @@ +use regex::Regex; + +use crate::crypto_algorithm::{Confidence, DetectionMethod}; + +/// A pattern rule that matches crypto usage in source code. +#[derive(Debug, Clone)] +pub struct CryptoPattern { + pub regex: Regex, + pub algorithm_name: String, + pub detection_method: DetectionMethod, + pub ecosystem: &'static str, + pub file_extensions: Vec<&'static str>, + /// When non-empty, the pattern only applies to files whose name matches one of these entries. + /// An empty slice means "any file with a matching extension". + pub file_names: &'static [&'static str], + pub providing_library: Option, + pub confidence: Confidence, +} + +/// Build the full pattern database for all ecosystems. +pub fn build_patterns() -> Vec { + let mut patterns = Vec::new(); + patterns.extend(python_patterns()); + patterns.extend(java_patterns()); + patterns.extend(go_patterns()); + patterns.extend(node_patterns()); + patterns.extend(rust_patterns()); + patterns.extend(ruby_patterns()); + patterns.extend(csharp_patterns()); + patterns.extend(php_patterns()); + patterns.extend(swift_patterns()); + patterns.extend(elixir_patterns()); + patterns.extend(cross_ecosystem_patterns()); + patterns +} + +/// Return manifest file names relevant to a given ecosystem. +pub fn ecosystem_manifests(ecosystem: &str) -> Vec<&'static str> { + match ecosystem { + "python" => vec![ + "requirements.txt", + "Pipfile", + "pyproject.toml", + "setup.py", + "setup.cfg", + "conda.yaml", + "environment.yml", + ], + "java" => vec!["pom.xml", "build.gradle", "build.gradle.kts"], + "go" => vec!["go.mod", "go.sum"], + "node" | "javascript" | "typescript" => { + vec!["package.json", "package-lock.json", "yarn.lock"] + } + "rust" => vec!["Cargo.toml", "Cargo.lock"], + "ruby" => vec!["Gemfile", "Gemfile.lock", "*.gemspec"], + "csharp" | "dotnet" => vec![ + "*.csproj", + "*.fsproj", + "packages.config", + "Directory.Packages.props", + ], + "php" => vec!["composer.json", "composer.lock"], + "swift" => vec!["Package.swift", "Podfile", "Podfile.lock"], + "elixir" => vec!["mix.exs", "mix.lock"], + _ => vec![], + } +} + +// ─── Python Patterns ────────────────────────────────────────────────── + +fn python_patterns() -> Vec { + vec![ + // -- Imports -- + // Generic namespace imports: the ciphers namespace could mean any cipher, + // not specifically AES, so use Medium confidence. + pat( + r"from\s+cryptography\.hazmat\.primitives\.ciphers\s+import", + "AES", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("cryptography"), + Confidence::Medium, + ), + pat( + r"from\s+cryptography\.hazmat\.primitives\.ciphers\.algorithms\s+import\s+(\w+)", + "AES", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("cryptography"), + Confidence::Medium, + ), + pat( + r"from\s+cryptography\.hazmat\.primitives\.hashes\s+import", + "SHA-256", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("cryptography"), + Confidence::Medium, + ), + // The asymmetric namespace import captures multiple modules (rsa, ec, dh, etc.) + // but reports only "RSA", so confidence should be Medium. + pat( + r"from\s+cryptography\.hazmat\.primitives\.asymmetric\s+import\s+(rsa|ec|ed25519|ed448|dsa|dh|x25519|x448|padding)", + "RSA", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("cryptography"), + Confidence::Medium, + ), + // The KDF namespace import captures multiple KDFs (hkdf, pbkdf2, scrypt, etc.) + // but reports only "HKDF", so confidence should be Medium. + pat( + r"from\s+cryptography\.hazmat\.primitives\.kdf\s+import\s+(hkdf|pbkdf2|scrypt|concatkdf|x963kdf)", + "HKDF", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("cryptography"), + Confidence::Medium, + ), + // -- API calls -- + pat( + r"algorithms\.AES\b", + "AES", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"algorithms\.AES128\b", + "AES-128", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"algorithms\.AES256\b", + "AES-256", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"algorithms\.ChaCha20\b", + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"algorithms\.TripleDES\b", + "3DES", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"algorithms\.Blowfish\b", + "Blowfish", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"algorithms\.ARC4\b", + "RC4", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"modes\.GCM\b", + "AES-GCM", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"modes\.CBC\b", + "AES-CBC", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"modes\.CTR\b", + "AES-CTR", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"modes\.ECB\b", + "AES-ECB", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"hashes\.SHA256\b", + "SHA-256", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"hashes\.SHA384\b", + "SHA-384", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"hashes\.SHA512\b", + "SHA-512", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"hashes\.SHA1\b", + "SHA-1", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"hashes\.MD5\b", + "MD5", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"hashes\.BLAKE2[bs]\b", + "BLAKE2", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"hashes\.SHA3_\d+\b", + "SHA-3", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"ec\.ECDSA\b", + "ECDSA", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"ec\.ECDH\b", + "ECDH", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"ec\.SECP256R1\b", + "ECDSA-P256", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"ec\.SECP384R1\b", + "ECDSA-P384", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"ed25519\.Ed25519", + "Ed25519", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"ed448\.Ed448", + "Ed448", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"x25519\.X25519", + "X25519", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"rsa\.generate_private_key\b", + "RSA", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"dh\.generate_parameters\b", + "DH", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"PBKDF2HMAC\b", + "PBKDF2", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"Scrypt\b", + "scrypt", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + pat( + r"HKDF\b", + "HKDF", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("cryptography"), + Confidence::High, + ), + // -- hashlib -- + pat( + r"hashlib\.sha256\b", + "SHA-256", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("hashlib"), + Confidence::High, + ), + pat( + r"hashlib\.sha384\b", + "SHA-384", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("hashlib"), + Confidence::High, + ), + pat( + r"hashlib\.sha512\b", + "SHA-512", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("hashlib"), + Confidence::High, + ), + pat( + r"hashlib\.sha1\b", + "SHA-1", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("hashlib"), + Confidence::High, + ), + pat( + r"hashlib\.md5\b", + "MD5", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("hashlib"), + Confidence::High, + ), + pat( + r"hashlib\.blake2[bs]\b", + "BLAKE2", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("hashlib"), + Confidence::High, + ), + pat( + r#"hashlib\.new\s*\(\s*["'](\w+)["']"#, + "SHA-256", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("hashlib"), + Confidence::Medium, + ), + // -- PyCryptodome -- + pat( + r"from\s+Crypto\.Cipher\s+import\s+AES\b", + "AES", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("pycryptodome"), + Confidence::High, + ), + pat( + r"from\s+Crypto\.Cipher\s+import\s+DES\b", + "DES", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("pycryptodome"), + Confidence::High, + ), + pat( + r"from\s+Crypto\.Cipher\s+import\s+DES3\b", + "3DES", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("pycryptodome"), + Confidence::High, + ), + pat( + r"from\s+Crypto\.Cipher\s+import\s+Blowfish\b", + "Blowfish", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("pycryptodome"), + Confidence::High, + ), + pat( + r"from\s+Crypto\.Cipher\s+import\s+ChaCha20\b", + "ChaCha20", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("pycryptodome"), + Confidence::High, + ), + pat( + r"from\s+Crypto\.Cipher\s+import\s+ARC4\b", + "RC4", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("pycryptodome"), + Confidence::High, + ), + pat( + r"from\s+Crypto\.Hash\s+import\s+SHA256\b", + "SHA-256", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("pycryptodome"), + Confidence::High, + ), + pat( + r"from\s+Crypto\.Hash\s+import\s+MD5\b", + "MD5", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("pycryptodome"), + Confidence::High, + ), + // -- bcrypt/argon2 -- + pat( + r"import\s+bcrypt\b", + "bcrypt", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("bcrypt"), + Confidence::High, + ), + pat( + r"import\s+argon2\b", + "Argon2", + DetectionMethod::ImportStatement, + "python", + &["py"], + Some("argon2-cffi"), + Confidence::High, + ), + pat( + r"argon2\.PasswordHasher\b", + "Argon2", + DetectionMethod::ApiCall, + "python", + &["py"], + Some("argon2-cffi"), + Confidence::High, + ), + // -- Dependency manifests -- + manifest_pat( + r"(?m)^cryptography[>===== Vec { + vec![ + // -- JCA/JCE API calls -- + pat( + r#"Cipher\.getInstance\s*\(\s*"AES/GCM/NoPadding""#, + "AES-256-GCM", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Cipher\.getInstance\s*\(\s*"AES/CBC/PKCS5Padding""#, + "AES-CBC", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Cipher\.getInstance\s*\(\s*"AES/ECB"#, + "AES-ECB", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Cipher\.getInstance\s*\(\s*"AES"#, + "AES", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Cipher\.getInstance\s*\(\s*"DES"#, + "DES", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Cipher\.getInstance\s*\(\s*"DESede"#, + "3DES", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Cipher\.getInstance\s*\(\s*"Blowfish"#, + "Blowfish", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Cipher\.getInstance\s*\(\s*"RC4"#, + "RC4", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Cipher\.getInstance\s*\(\s*"ChaCha20"#, + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Cipher\.getInstance\s*\(\s*"RSA"#, + "RSA", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"MessageDigest\.getInstance\s*\(\s*"SHA-256""#, + "SHA-256", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("java.security"), + Confidence::High, + ), + pat( + r#"MessageDigest\.getInstance\s*\(\s*"SHA-384""#, + "SHA-384", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("java.security"), + Confidence::High, + ), + pat( + r#"MessageDigest\.getInstance\s*\(\s*"SHA-512""#, + "SHA-512", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("java.security"), + Confidence::High, + ), + pat( + r#"MessageDigest\.getInstance\s*\(\s*"SHA-1""#, + "SHA-1", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("java.security"), + Confidence::High, + ), + pat( + r#"MessageDigest\.getInstance\s*\(\s*"MD5""#, + "MD5", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("java.security"), + Confidence::High, + ), + pat( + r#"KeyPairGenerator\.getInstance\s*\(\s*"RSA""#, + "RSA", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("java.security"), + Confidence::High, + ), + pat( + r#"KeyPairGenerator\.getInstance\s*\(\s*"EC""#, + "ECDSA", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("java.security"), + Confidence::High, + ), + pat( + r#"KeyPairGenerator\.getInstance\s*\(\s*"DSA""#, + "DSA", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("java.security"), + Confidence::High, + ), + pat( + r#"KeyPairGenerator\.getInstance\s*\(\s*"Ed25519""#, + "Ed25519", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("java.security"), + Confidence::High, + ), + pat( + r#"KeyAgreement\.getInstance\s*\(\s*"ECDH""#, + "ECDH", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"KeyAgreement\.getInstance\s*\(\s*"DH""#, + "DH", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"KeyAgreement\.getInstance\s*\(\s*"X25519""#, + "X25519", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Mac\.getInstance\s*\(\s*"HmacSHA256""#, + "HMAC-SHA256", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Mac\.getInstance\s*\(\s*"HmacSHA512""#, + "HMAC-SHA512", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"Mac\.getInstance\s*\(\s*"HmacSHA1""#, + "HMAC-SHA1", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + pat( + r#"SecretKeyFactory\.getInstance\s*\(\s*"PBKDF2"#, + "PBKDF2", + DetectionMethod::ApiCall, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::High, + ), + // -- Imports -- + pat( + r"import\s+javax\.crypto\.", + "JCA-crypto", + DetectionMethod::ImportStatement, + "java", + &["java", "kt"], + Some("javax.crypto"), + Confidence::Medium, + ), + pat( + r"import\s+java\.security\.", + "JCA-security", + DetectionMethod::ImportStatement, + "java", + &["java", "kt"], + Some("java.security"), + Confidence::Medium, + ), + pat( + r"import\s+org\.bouncycastle\.", + "BouncyCastle", + DetectionMethod::ImportStatement, + "java", + &["java", "kt"], + Some("BouncyCastle"), + Confidence::High, + ), + // -- Dependency manifests -- + manifest_pat( + r"org\.bouncycastle", + "BouncyCastle", + DetectionMethod::DependencyManifest, + "java", + &["xml", "gradle", "kts"], + &["pom.xml", "build.gradle", "build.gradle.kts"], + None, + Confidence::High, + ), + manifest_pat( + r"com\.google\.crypto\.tink", + "Tink", + DetectionMethod::DependencyManifest, + "java", + &["xml", "gradle", "kts"], + &["pom.xml", "build.gradle", "build.gradle.kts"], + None, + Confidence::High, + ), + ] +} + +// ─── Go Patterns ────────────────────────────────────────────────────── + +fn go_patterns() -> Vec { + vec![ + // -- Standard library imports -- + pat( + r#""crypto/aes""#, + "AES", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/aes"), + Confidence::High, + ), + pat( + r#""crypto/des""#, + "DES", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/des"), + Confidence::High, + ), + pat( + r#""crypto/cipher""#, + "AES", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/cipher"), + Confidence::Medium, + ), + pat( + r#""crypto/sha256""#, + "SHA-256", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/sha256"), + Confidence::High, + ), + pat( + r#""crypto/sha512""#, + "SHA-512", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/sha512"), + Confidence::High, + ), + pat( + r#""crypto/sha1""#, + "SHA-1", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/sha1"), + Confidence::High, + ), + pat( + r#""crypto/md5""#, + "MD5", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/md5"), + Confidence::High, + ), + pat( + r#""crypto/rsa""#, + "RSA", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/rsa"), + Confidence::High, + ), + pat( + r#""crypto/ecdsa""#, + "ECDSA", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/ecdsa"), + Confidence::High, + ), + pat( + r#""crypto/ed25519""#, + "Ed25519", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/ed25519"), + Confidence::High, + ), + pat( + r#""crypto/ecdh""#, + "ECDH", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/ecdh"), + Confidence::High, + ), + pat( + r#""crypto/hmac""#, + "HMAC", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/hmac"), + Confidence::High, + ), + pat( + r#""crypto/rc4""#, + "RC4", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("crypto/rc4"), + Confidence::High, + ), + // -- x/crypto -- + pat( + r#""golang\.org/x/crypto/chacha20poly1305""#, + "ChaCha20-Poly1305", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r#""golang\.org/x/crypto/blake2b""#, + "BLAKE2b", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r#""golang\.org/x/crypto/blake2s""#, + "BLAKE2s", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r#""golang\.org/x/crypto/argon2""#, + "Argon2", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r#""golang\.org/x/crypto/bcrypt""#, + "bcrypt", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r#""golang\.org/x/crypto/scrypt""#, + "scrypt", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r#""golang\.org/x/crypto/curve25519""#, + "X25519", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r#""golang\.org/x/crypto/nacl""#, + "NaCl", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r#""golang\.org/x/crypto/salsa20""#, + "Salsa20", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r#""golang\.org/x/crypto/sha3""#, + "SHA-3", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r#""golang\.org/x/crypto/hkdf""#, + "HKDF", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r#""golang\.org/x/crypto/pbkdf2""#, + "PBKDF2", + DetectionMethod::ImportStatement, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + // -- API calls -- + pat( + r"aes\.NewCipher\b", + "AES", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/aes"), + Confidence::High, + ), + pat( + r"cipher\.NewGCM\b", + "AES-GCM", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/cipher"), + Confidence::High, + ), + pat( + r"cipher\.NewCBCEncrypter\b", + "AES-CBC", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/cipher"), + Confidence::High, + ), + pat( + r"cipher\.NewCTR\b", + "AES-CTR", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/cipher"), + Confidence::High, + ), + pat( + r"rsa\.GenerateKey\b", + "RSA", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/rsa"), + Confidence::High, + ), + pat( + r"ecdsa\.GenerateKey\b", + "ECDSA", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/ecdsa"), + Confidence::High, + ), + pat( + r"ed25519\.GenerateKey\b", + "Ed25519", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/ed25519"), + Confidence::High, + ), + pat( + r"elliptic\.P256\b", + "ECDSA-P256", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/elliptic"), + Confidence::High, + ), + pat( + r"elliptic\.P384\b", + "ECDSA-P384", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/elliptic"), + Confidence::High, + ), + pat( + r"elliptic\.P521\b", + "ECDSA-P521", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/elliptic"), + Confidence::High, + ), + pat( + r"hmac\.New\b", + "HMAC", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/hmac"), + Confidence::High, + ), + pat( + r"sha256\.New\b", + "SHA-256", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/sha256"), + Confidence::High, + ), + pat( + r"sha512\.New\b", + "SHA-512", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/sha512"), + Confidence::High, + ), + pat( + r"sha1\.New\b", + "SHA-1", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/sha1"), + Confidence::High, + ), + pat( + r"md5\.New\b", + "MD5", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("crypto/md5"), + Confidence::High, + ), + pat( + r"chacha20poly1305\.New\b", + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r"argon2\.IDKey\b", + "Argon2", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r"bcrypt\.GenerateFromPassword\b", + "bcrypt", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + pat( + r"scrypt\.Key\b", + "scrypt", + DetectionMethod::ApiCall, + "go", + &["go"], + Some("x/crypto"), + Confidence::High, + ), + // -- go.mod dependencies -- + manifest_pat( + r"golang\.org/x/crypto", + "x/crypto", + DetectionMethod::DependencyManifest, + "go", + &["mod", "sum"], + &["go.mod", "go.sum"], + None, + Confidence::Medium, + ), + ] +} + +// ─── Node.js Patterns ───────────────────────────────────────────────── + +fn node_patterns() -> Vec { + vec![ + // -- Built-in crypto -- + pat( + r#"crypto\.createCipheriv\s*\(\s*['"]aes-256-gcm['"]"#, + "AES-256-GCM", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createCipheriv\s*\(\s*['"]aes-256-cbc['"]"#, + "AES-256-CBC", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createCipheriv\s*\(\s*['"]aes-128-gcm['"]"#, + "AES-128-GCM", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createCipheriv\s*\(\s*['"]aes-128-cbc['"]"#, + "AES-128-CBC", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createCipheriv\s*\(\s*['"]des-ede3-cbc['"]"#, + "3DES", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createCipheriv\s*\(\s*['"]chacha20-poly1305['"]"#, + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createCipheriv\s*\(\s*['"]rc4['"]"#, + "RC4", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createCipheriv\s*\(\s*['"]bf-cbc['"]"#, + "Blowfish", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createHash\s*\(\s*['"]sha256['"]"#, + "SHA-256", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createHash\s*\(\s*['"]sha384['"]"#, + "SHA-384", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createHash\s*\(\s*['"]sha512['"]"#, + "SHA-512", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createHash\s*\(\s*['"]sha1['"]"#, + "SHA-1", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createHash\s*\(\s*['"]md5['"]"#, + "MD5", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createHmac\s*\(\s*['"]sha256['"]"#, + "HMAC-SHA256", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createHmac\s*\(\s*['"]sha512['"]"#, + "HMAC-SHA512", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.createHmac\s*\(\s*['"]sha1['"]"#, + "HMAC-SHA1", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.generateKeyPairSync\s*\(\s*['"]rsa['"]"#, + "RSA", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.generateKeyPairSync\s*\(\s*['"]ec['"]"#, + "ECDSA", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.generateKeyPairSync\s*\(\s*['"]ed25519['"]"#, + "Ed25519", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r#"crypto\.generateKeyPairSync\s*\(\s*['"]x25519['"]"#, + "X25519", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r"crypto\.scryptSync\b|crypto\.scrypt\b", + "scrypt", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r"crypto\.pbkdf2Sync\b|crypto\.pbkdf2\b", + "PBKDF2", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r"crypto\.diffieHellman\b|crypto\.createDiffieHellman\b", + "DH", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + pat( + r"crypto\.createECDH\b", + "ECDH", + DetectionMethod::ApiCall, + "node", + &["js", "ts", "mjs"], + Some("crypto"), + Confidence::High, + ), + // -- require/import -- + pat( + r#"require\s*\(\s*['"]crypto-js['"]\s*\)"#, + "crypto-js", + DetectionMethod::ImportStatement, + "node", + &["js", "ts", "mjs"], + Some("crypto-js"), + Confidence::High, + ), + pat( + r#"from\s+['"]crypto-js['"]"#, + "crypto-js", + DetectionMethod::ImportStatement, + "node", + &["js", "ts", "mjs"], + Some("crypto-js"), + Confidence::High, + ), + pat( + r#"require\s*\(\s*['"]bcryptjs?['"]\s*\)"#, + "bcrypt", + DetectionMethod::ImportStatement, + "node", + &["js", "ts", "mjs"], + Some("bcrypt"), + Confidence::High, + ), + pat( + r#"from\s+['"]bcryptjs?['"]"#, + "bcrypt", + DetectionMethod::ImportStatement, + "node", + &["js", "ts", "mjs"], + Some("bcrypt"), + Confidence::High, + ), + pat( + r#"require\s*\(\s*['"]argon2['"]\s*\)"#, + "Argon2", + DetectionMethod::ImportStatement, + "node", + &["js", "ts", "mjs"], + Some("argon2"), + Confidence::High, + ), + pat( + r#"from\s+['"]argon2['"]"#, + "Argon2", + DetectionMethod::ImportStatement, + "node", + &["js", "ts", "mjs"], + Some("argon2"), + Confidence::High, + ), + pat( + r#"require\s*\(\s*['"]node-forge['"]\s*\)"#, + "node-forge", + DetectionMethod::ImportStatement, + "node", + &["js", "ts", "mjs"], + Some("node-forge"), + Confidence::High, + ), + pat( + r#"from\s+['"]jose['"]"#, + "JOSE", + DetectionMethod::ImportStatement, + "node", + &["js", "ts", "mjs"], + Some("jose"), + Confidence::High, + ), + // -- package.json -- + manifest_pat( + r#""crypto-js""#, + "crypto-js", + DetectionMethod::DependencyManifest, + "node", + &["json"], + &["package.json"], + None, + Confidence::Medium, + ), + manifest_pat( + r#""bcrypt""#, + "bcrypt", + DetectionMethod::DependencyManifest, + "node", + &["json"], + &["package.json"], + None, + Confidence::Medium, + ), + manifest_pat( + r#""argon2""#, + "Argon2", + DetectionMethod::DependencyManifest, + "node", + &["json"], + &["package.json"], + None, + Confidence::Medium, + ), + manifest_pat( + r#""node-forge""#, + "node-forge", + DetectionMethod::DependencyManifest, + "node", + &["json"], + &["package.json"], + None, + Confidence::Medium, + ), + manifest_pat( + r#""jose""#, + "JOSE", + DetectionMethod::DependencyManifest, + "node", + &["json"], + &["package.json"], + None, + Confidence::Medium, + ), + ] +} + +// ─── Rust Patterns ──────────────────────────────────────────────────── + +fn rust_patterns() -> Vec { + vec![ + // -- ring -- + pat( + r"ring::aead::AES_256_GCM\b", + "AES-256-GCM", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::aead::AES_128_GCM\b", + "AES-128-GCM", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::aead::CHACHA20_POLY1305\b", + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::digest::SHA256\b", + "SHA-256", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::digest::SHA384\b", + "SHA-384", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::digest::SHA512\b", + "SHA-512", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::digest::SHA1_FOR_LEGACY_USE_ONLY\b", + "SHA-1", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::hmac::", + "HMAC", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::signature::RSA", + "RSA", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::signature::ECDSA", + "ECDSA", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::signature::ED25519\b", + "Ed25519", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::agreement::X25519\b", + "X25519", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::pbkdf2::", + "PBKDF2", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + pat( + r"ring::hkdf::", + "HKDF", + DetectionMethod::ApiCall, + "rust", + &["rs"], + Some("ring"), + Confidence::High, + ), + // -- RustCrypto -- + pat( + r"use\s+aes::", + "AES", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("aes"), + Confidence::High, + ), + pat( + r"use\s+aes_gcm::", + "AES-GCM", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("aes-gcm"), + Confidence::High, + ), + pat( + r"use\s+chacha20poly1305::", + "ChaCha20-Poly1305", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("chacha20poly1305"), + Confidence::High, + ), + pat( + r"use\s+sha2::", + "SHA-256", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("sha2"), + Confidence::High, + ), + pat( + r"use\s+sha3::", + "SHA-3", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("sha3"), + Confidence::High, + ), + pat( + r"use\s+blake2::", + "BLAKE2", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("blake2"), + Confidence::High, + ), + pat( + r"use\s+blake3::", + "BLAKE3", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("blake3"), + Confidence::High, + ), + pat( + r"use\s+md5::", + "MD5", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("md-5"), + Confidence::High, + ), + pat( + r"use\s+rsa::", + "RSA", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("rsa"), + Confidence::High, + ), + pat( + r"use\s+ed25519_dalek::", + "Ed25519", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("ed25519-dalek"), + Confidence::High, + ), + pat( + r"use\s+x25519_dalek::", + "X25519", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("x25519-dalek"), + Confidence::High, + ), + pat( + r"use\s+p256::", + "ECDSA-P256", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("p256"), + Confidence::High, + ), + pat( + r"use\s+p384::", + "ECDSA-P384", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("p384"), + Confidence::High, + ), + pat( + r"use\s+argon2::", + "Argon2", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("argon2"), + Confidence::High, + ), + pat( + r"use\s+scrypt::", + "scrypt", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("scrypt"), + Confidence::High, + ), + pat( + r"use\s+bcrypt::", + "bcrypt", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("bcrypt"), + Confidence::High, + ), + pat( + r"use\s+hkdf::", + "HKDF", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("hkdf"), + Confidence::High, + ), + pat( + r"use\s+pbkdf2::", + "PBKDF2", + DetectionMethod::ImportStatement, + "rust", + &["rs"], + Some("pbkdf2"), + Confidence::High, + ), + // -- Cargo.toml dependencies -- + manifest_pat( + r#"(?m)^ring\s*="#, + "ring", + DetectionMethod::DependencyManifest, + "rust", + &["toml"], + &["Cargo.toml"], + None, + Confidence::High, + ), + manifest_pat( + r#"(?m)^aes-gcm\s*="#, + "AES-GCM", + DetectionMethod::DependencyManifest, + "rust", + &["toml"], + &["Cargo.toml"], + None, + Confidence::High, + ), + manifest_pat( + r#"(?m)^chacha20poly1305\s*="#, + "ChaCha20-Poly1305", + DetectionMethod::DependencyManifest, + "rust", + &["toml"], + &["Cargo.toml"], + None, + Confidence::High, + ), + manifest_pat( + r#"(?m)^sha2\s*="#, + "SHA-256", + DetectionMethod::DependencyManifest, + "rust", + &["toml"], + &["Cargo.toml"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"(?m)^blake2\s*="#, + "BLAKE2", + DetectionMethod::DependencyManifest, + "rust", + &["toml"], + &["Cargo.toml"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"(?m)^blake3\s*="#, + "BLAKE3", + DetectionMethod::DependencyManifest, + "rust", + &["toml"], + &["Cargo.toml"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"(?m)^argon2\s*="#, + "Argon2", + DetectionMethod::DependencyManifest, + "rust", + &["toml"], + &["Cargo.toml"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"(?m)^rustls\s*="#, + "TLS", + DetectionMethod::DependencyManifest, + "rust", + &["toml"], + &["Cargo.toml"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"(?m)^openssl\s*="#, + "OpenSSL", + DetectionMethod::DependencyManifest, + "rust", + &["toml"], + &["Cargo.toml"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"(?m)^ed25519-dalek\s*="#, + "Ed25519", + DetectionMethod::DependencyManifest, + "rust", + &["toml"], + &["Cargo.toml"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"(?m)^x25519-dalek\s*="#, + "X25519", + DetectionMethod::DependencyManifest, + "rust", + &["toml"], + &["Cargo.toml"], + None, + Confidence::Medium, + ), + ] +} + +// ─── Ruby Patterns ──────────────────────────────────────────────────── + +fn ruby_patterns() -> Vec { + vec![ + // -- OpenSSL (builtin) - Ciphers -- + pat( + r#"OpenSSL::Cipher\.new\s*\(\s*['"]AES-256-GCM['"]"#, + "AES-256-GCM", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Cipher\.new\s*\(\s*['"]AES-256-CBC['"]"#, + "AES-256-CBC", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Cipher\.new\s*\(\s*['"]AES-128-GCM['"]"#, + "AES-128-GCM", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Cipher\.new\s*\(\s*['"]AES-128-CBC['"]"#, + "AES-128-CBC", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Cipher\.new\s*\(\s*['"]DES-EDE3-CBC['"]"#, + "3DES", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Cipher\.new\s*\(\s*['"]DES['"]"#, + "DES", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Cipher\.new\s*\(\s*['"]BF-CBC['"]"#, + "Blowfish", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Cipher\.new\s*\(\s*['"]RC4['"]"#, + "RC4", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Cipher\.new\s*\(\s*['"]ChaCha20['"]"#, + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r"OpenSSL::Cipher\b", + "AES", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::Low, + ), + // -- OpenSSL - Digests -- + pat( + r#"OpenSSL::Digest\.new\s*\(\s*['"]SHA256['"]"#, + "SHA-256", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Digest\.new\s*\(\s*['"]SHA384['"]"#, + "SHA-384", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Digest\.new\s*\(\s*['"]SHA512['"]"#, + "SHA-512", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Digest\.new\s*\(\s*['"]SHA1['"]"#, + "SHA-1", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"OpenSSL::Digest\.new\s*\(\s*['"]MD5['"]"#, + "MD5", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r"OpenSSL::Digest::SHA256\b", + "SHA-256", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r"OpenSSL::Digest::SHA512\b", + "SHA-512", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r"OpenSSL::Digest::SHA1\b", + "SHA-1", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r"OpenSSL::Digest::MD5\b", + "MD5", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r"Digest::SHA256\b", + "SHA-256", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("digest"), + Confidence::High, + ), + pat( + r"Digest::SHA512\b", + "SHA-512", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("digest"), + Confidence::High, + ), + pat( + r"Digest::SHA1\b", + "SHA-1", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("digest"), + Confidence::High, + ), + pat( + r"Digest::MD5\b", + "MD5", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("digest"), + Confidence::High, + ), + // -- OpenSSL - Asymmetric -- + pat( + r"OpenSSL::PKey::RSA\.new\b", + "RSA", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r"OpenSSL::PKey::RSA\.generate\b", + "RSA", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r"OpenSSL::PKey::EC\.new\b", + "ECDSA", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r"OpenSSL::PKey::EC\.generate\b", + "ECDSA", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + pat( + r"OpenSSL::PKey::DH\b", + "DH", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + // -- OpenSSL - HMAC -- + pat( + r"OpenSSL::HMAC\b", + "HMAC", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + // -- OpenSSL - PBKDF2 -- + pat( + r"OpenSSL::PKCS5\.pbkdf2_hmac\b", + "PBKDF2", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("openssl"), + Confidence::High, + ), + // -- Imports -- + pat( + r#"require\s+['"]openssl['"]"#, + "OpenSSL", + DetectionMethod::ImportStatement, + "ruby", + &["rb"], + Some("openssl"), + Confidence::Medium, + ), + pat( + r#"require\s+['"]digest['"]"#, + "SHA-256", + DetectionMethod::ImportStatement, + "ruby", + &["rb"], + Some("digest"), + Confidence::Low, + ), + // -- rbnacl -- + pat( + r"RbNaCl::SecretBox\b", + "XSalsa20-Poly1305", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("rbnacl"), + Confidence::High, + ), + pat( + r"RbNaCl::SimpleBox\b", + "X25519", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("rbnacl"), + Confidence::High, + ), + pat( + r"RbNaCl::AEAD::ChaCha20Poly1305\b", + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("rbnacl"), + Confidence::High, + ), + pat( + r"RbNaCl::Hash\.sha256\b", + "SHA-256", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("rbnacl"), + Confidence::High, + ), + pat( + r"RbNaCl::Hash\.sha512\b", + "SHA-512", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("rbnacl"), + Confidence::High, + ), + pat( + r"RbNaCl::Hash\.blake2b\b", + "BLAKE2b", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("rbnacl"), + Confidence::High, + ), + pat( + r"RbNaCl::Signatures::Ed25519\b", + "Ed25519", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("rbnacl"), + Confidence::High, + ), + pat( + r"RbNaCl::GroupElements::Curve25519\b", + "X25519", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("rbnacl"), + Confidence::High, + ), + pat( + r"RbNaCl::PasswordHash\.scrypt\b", + "scrypt", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("rbnacl"), + Confidence::High, + ), + pat( + r"RbNaCl::PasswordHash\.argon2\b", + "Argon2", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("rbnacl"), + Confidence::High, + ), + // -- bcrypt-ruby -- + pat( + r"BCrypt::Password\b", + "bcrypt", + DetectionMethod::ApiCall, + "ruby", + &["rb"], + Some("bcrypt-ruby"), + Confidence::High, + ), + pat( + r#"require\s+['"]bcrypt['"]"#, + "bcrypt", + DetectionMethod::ImportStatement, + "ruby", + &["rb"], + Some("bcrypt-ruby"), + Confidence::High, + ), + // -- Dependency manifests (Gemfile) -- + manifest_pat( + r#"gem\s+['"]rbnacl['"]\b"#, + "rbnacl", + DetectionMethod::DependencyManifest, + "ruby", + &["rb", "gemspec"], + &["Gemfile"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"gem\s+['"]bcrypt['"]\b"#, + "bcrypt", + DetectionMethod::DependencyManifest, + "ruby", + &["rb", "gemspec"], + &["Gemfile"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"gem\s+['"]ed25519['"]\b"#, + "Ed25519", + DetectionMethod::DependencyManifest, + "ruby", + &["rb", "gemspec"], + &["Gemfile"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"gem\s+['"]rsa['"]\b"#, + "RSA", + DetectionMethod::DependencyManifest, + "ruby", + &["rb", "gemspec"], + &["Gemfile"], + None, + Confidence::Medium, + ), + ] +} + +// ─── C# / .NET Patterns ────────────────────────────────────────────── + +fn csharp_patterns() -> Vec { + vec![ + // -- System.Security.Cryptography API calls -- + pat( + r"Aes\.Create\b", + "AES", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"AesGcm\b", + "AES-256-GCM", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"AesCcm\b", + "AES-CCM", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"TripleDES\.Create\b", + "3DES", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"DES\.Create\b", + "DES", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"RC2\.Create\b", + "RC2", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + // -- Hash -- + pat( + r"SHA256\.Create\b", + "SHA-256", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"SHA384\.Create\b", + "SHA-384", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"SHA512\.Create\b", + "SHA-512", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"SHA1\.Create\b", + "SHA-1", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"MD5\.Create\b", + "MD5", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"SHA256\.HashData\b", + "SHA-256", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"SHA512\.HashData\b", + "SHA-512", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + // -- Asymmetric -- + pat( + r"RSA\.Create\b", + "RSA", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"RSACryptoServiceProvider\b", + "RSA", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"ECDsa\.Create\b", + "ECDSA", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"ECDiffieHellman\.Create\b", + "ECDH", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"DSA\.Create\b", + "DSA", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + // -- HMAC -- + pat( + r"HMACSHA256\b", + "HMAC-SHA256", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"HMACSHA512\b", + "HMAC-SHA512", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"HMACSHA1\b", + "HMAC-SHA1", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"HMACMD5\b", + "HMAC-MD5", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + // -- KDF -- + pat( + r"Rfc2898DeriveBytes\b", + "PBKDF2", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"HKDF\.DeriveKey\b", + "HKDF", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + pat( + r"HKDF\.Extract\b", + "HKDF", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::High, + ), + // -- RandomNumberGenerator -- + pat( + r"RandomNumberGenerator\.Create\b", + "DRBG", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::Medium, + ), + // -- Imports -- + pat( + r"using\s+System\.Security\.Cryptography\b", + "System.Security.Cryptography", + DetectionMethod::ImportStatement, + "csharp", + &["cs"], + Some("System.Security.Cryptography"), + Confidence::Medium, + ), + pat( + r"using\s+Org\.BouncyCastle\b", + "BouncyCastle", + DetectionMethod::ImportStatement, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + // -- BouncyCastle API calls -- + pat( + r"AesFastEngine\b|AesEngine\b", + "AES", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + pat( + r"GcmBlockCipher\b", + "AES-GCM", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + pat( + r"RsaEngine\b|RsaKeyPairGenerator\b", + "RSA", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + pat( + r"ECKeyPairGenerator\b", + "ECDSA", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + pat( + r"Ed25519Signer\b|Ed25519KeyPairGenerator\b", + "Ed25519", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + pat( + r"X25519Agreement\b|X25519KeyPairGenerator\b", + "X25519", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + pat( + r"Sha256Digest\b", + "SHA-256", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + pat( + r"Sha512Digest\b", + "SHA-512", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + pat( + r"Sha1Digest\b", + "SHA-1", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + pat( + r"MD5Digest\b", + "MD5", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + pat( + r"ChaCha20Poly1305\b", + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + pat( + r"BlowfishEngine\b", + "Blowfish", + DetectionMethod::ApiCall, + "csharp", + &["cs"], + Some("BouncyCastle"), + Confidence::High, + ), + // -- Dependency manifests (.csproj / .fsproj / packages.config / Directory.Packages.props) -- + // These extensions are specific enough to .NET that filename scoping is not needed. + pat( + r"BouncyCastle\.Cryptography", + "BouncyCastle", + DetectionMethod::DependencyManifest, + "csharp", + &["csproj", "fsproj", "config", "props"], + None, + Confidence::High, + ), + pat( + r"Portable\.BouncyCastle", + "BouncyCastle", + DetectionMethod::DependencyManifest, + "csharp", + &["csproj", "fsproj", "config", "props"], + None, + Confidence::High, + ), + pat( + r"BCrypt\.Net-Next", + "bcrypt", + DetectionMethod::DependencyManifest, + "csharp", + &["csproj", "fsproj", "config", "props"], + None, + Confidence::Medium, + ), + pat( + r"Konscious\.Security\.Cryptography", + "Argon2", + DetectionMethod::DependencyManifest, + "csharp", + &["csproj", "fsproj", "config", "props"], + None, + Confidence::Medium, + ), + pat( + r"NSec\.Cryptography", + "X25519", + DetectionMethod::DependencyManifest, + "csharp", + &["csproj", "fsproj", "config", "props"], + None, + Confidence::Medium, + ), + ] +} + +// ─── PHP Patterns ───────────────────────────────────────────────────── + +fn php_patterns() -> Vec { + vec![ + // -- openssl extension -- + pat( + r"openssl_encrypt\s*\(", + "AES", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::High, + ), + pat( + r"openssl_decrypt\s*\(", + "AES", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"openssl_encrypt\s*\([^,]+,\s*['"]aes-256-gcm['"]"#, + "AES-256-GCM", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"openssl_encrypt\s*\([^,]+,\s*['"]aes-256-cbc['"]"#, + "AES-256-CBC", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"openssl_encrypt\s*\([^,]+,\s*['"]aes-128-gcm['"]"#, + "AES-128-GCM", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"openssl_encrypt\s*\([^,]+,\s*['"]aes-128-cbc['"]"#, + "AES-128-CBC", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"openssl_encrypt\s*\([^,]+,\s*['"]des-ede3-cbc['"]"#, + "3DES", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"openssl_encrypt\s*\([^,]+,\s*['"]bf-cbc['"]"#, + "Blowfish", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"openssl_encrypt\s*\([^,]+,\s*['"]rc4['"]"#, + "RC4", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::High, + ), + pat( + r#"openssl_encrypt\s*\([^,]+,\s*['"]chacha20-poly1305['"]"#, + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::High, + ), + pat( + r"openssl_digest\s*\(", + "SHA-256", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::Medium, + ), + pat( + r"openssl_sign\s*\(", + "RSA", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::Medium, + ), + pat( + r"openssl_verify\s*\(", + "RSA", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::Medium, + ), + pat( + r"openssl_pkey_new\s*\(", + "RSA", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::Medium, + ), + pat( + r"openssl_seal\s*\(", + "RSA", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::Medium, + ), + pat( + r"openssl_pbkdf2\s*\(", + "PBKDF2", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("openssl"), + Confidence::High, + ), + // -- hash extension -- + pat( + r#"hash\s*\(\s*['"]sha256['"]"#, + "SHA-256", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("hash"), + Confidence::High, + ), + pat( + r#"hash\s*\(\s*['"]sha384['"]"#, + "SHA-384", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("hash"), + Confidence::High, + ), + pat( + r#"hash\s*\(\s*['"]sha512['"]"#, + "SHA-512", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("hash"), + Confidence::High, + ), + pat( + r#"hash\s*\(\s*['"]sha1['"]"#, + "SHA-1", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("hash"), + Confidence::High, + ), + pat( + r#"hash\s*\(\s*['"]md5['"]"#, + "MD5", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("hash"), + Confidence::High, + ), + pat( + r"\bmd5\s*\(", + "MD5", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("hash"), + Confidence::High, + ), + pat( + r"\bsha1\s*\(", + "SHA-1", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("hash"), + Confidence::High, + ), + pat( + r#"hash_hmac\s*\(\s*['"]sha256['"]"#, + "HMAC-SHA256", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("hash"), + Confidence::High, + ), + pat( + r#"hash_hmac\s*\(\s*['"]sha512['"]"#, + "HMAC-SHA512", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("hash"), + Confidence::High, + ), + pat( + r#"hash_hmac\s*\(\s*['"]sha1['"]"#, + "HMAC-SHA1", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("hash"), + Confidence::High, + ), + pat( + r"hash_pbkdf2\s*\(", + "PBKDF2", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("hash"), + Confidence::High, + ), + // -- password_hash (bcrypt/argon2) -- + pat( + r"password_hash\s*\(", + "bcrypt", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("password"), + Confidence::Medium, + ), + pat( + r"PASSWORD_BCRYPT\b", + "bcrypt", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("password"), + Confidence::High, + ), + pat( + r"PASSWORD_ARGON2I\b", + "Argon2", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("password"), + Confidence::High, + ), + pat( + r"PASSWORD_ARGON2ID\b", + "Argon2", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("password"), + Confidence::High, + ), + // -- sodium extension -- + pat( + r"sodium_crypto_secretbox\b", + "XSalsa20-Poly1305", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("sodium"), + Confidence::High, + ), + pat( + r"sodium_crypto_aead_chacha20poly1305\b", + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("sodium"), + Confidence::High, + ), + pat( + r"sodium_crypto_aead_aes256gcm\b", + "AES-256-GCM", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("sodium"), + Confidence::High, + ), + pat( + r"sodium_crypto_box\b", + "X25519", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("sodium"), + Confidence::High, + ), + pat( + r"sodium_crypto_sign\b", + "Ed25519", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("sodium"), + Confidence::High, + ), + pat( + r"sodium_crypto_generichash\b", + "BLAKE2b", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("sodium"), + Confidence::High, + ), + pat( + r"sodium_crypto_auth\b", + "HMAC-SHA512", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("sodium"), + Confidence::High, + ), + pat( + r"sodium_crypto_pwhash\b", + "Argon2", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("sodium"), + Confidence::High, + ), + pat( + r"sodium_crypto_kdf\b", + "BLAKE2b", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("sodium"), + Confidence::High, + ), + pat( + r"sodium_crypto_scalarmult\b", + "X25519", + DetectionMethod::ApiCall, + "php", + &["php"], + Some("sodium"), + Confidence::High, + ), + // -- phpseclib -- + pat( + r"use\s+phpseclib3?\\Crypt\\AES\b", + "AES", + DetectionMethod::ImportStatement, + "php", + &["php"], + Some("phpseclib"), + Confidence::High, + ), + pat( + r"use\s+phpseclib3?\\Crypt\\RSA\b", + "RSA", + DetectionMethod::ImportStatement, + "php", + &["php"], + Some("phpseclib"), + Confidence::High, + ), + pat( + r"use\s+phpseclib3?\\Crypt\\DES\b", + "DES", + DetectionMethod::ImportStatement, + "php", + &["php"], + Some("phpseclib"), + Confidence::High, + ), + pat( + r"use\s+phpseclib3?\\Crypt\\TripleDES\b", + "3DES", + DetectionMethod::ImportStatement, + "php", + &["php"], + Some("phpseclib"), + Confidence::High, + ), + pat( + r"use\s+phpseclib3?\\Crypt\\Blowfish\b", + "Blowfish", + DetectionMethod::ImportStatement, + "php", + &["php"], + Some("phpseclib"), + Confidence::High, + ), + pat( + r"use\s+phpseclib3?\\Crypt\\RC4\b", + "RC4", + DetectionMethod::ImportStatement, + "php", + &["php"], + Some("phpseclib"), + Confidence::High, + ), + pat( + r"use\s+phpseclib3?\\Crypt\\EC\b", + "ECDSA", + DetectionMethod::ImportStatement, + "php", + &["php"], + Some("phpseclib"), + Confidence::High, + ), + pat( + r"use\s+phpseclib3?\\Crypt\\Hash\b", + "SHA-256", + DetectionMethod::ImportStatement, + "php", + &["php"], + Some("phpseclib"), + Confidence::Medium, + ), + // -- Dependency manifests (composer.json) -- + manifest_pat( + r#""phpseclib/phpseclib""#, + "phpseclib", + DetectionMethod::DependencyManifest, + "php", + &["json"], + &["composer.json"], + None, + Confidence::High, + ), + manifest_pat( + r#""defuse/php-encryption""#, + "AES", + DetectionMethod::DependencyManifest, + "php", + &["json"], + &["composer.json"], + None, + Confidence::Medium, + ), + manifest_pat( + r#""paragonie/halite""#, + "XSalsa20-Poly1305", + DetectionMethod::DependencyManifest, + "php", + &["json"], + &["composer.json"], + None, + Confidence::Medium, + ), + manifest_pat( + r#""paragonie/sodium_compat""#, + "X25519", + DetectionMethod::DependencyManifest, + "php", + &["json"], + &["composer.json"], + None, + Confidence::Medium, + ), + ] +} + +// ─── Swift Patterns ─────────────────────────────────────────────────── + +fn swift_patterns() -> Vec { + vec![ + // -- CryptoKit -- + pat( + r"import\s+CryptoKit\b", + "CryptoKit", + DetectionMethod::ImportStatement, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::Medium, + ), + pat( + r"AES\.GCM\.seal\b", + "AES-256-GCM", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"AES\.GCM\.open\b", + "AES-256-GCM", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"AES\.GCM\b", + "AES-256-GCM", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"ChaChaPoly\.seal\b", + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"ChaChaPoly\.open\b", + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"ChaChaPoly\b", + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"SHA256\.hash\b", + "SHA-256", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"SHA384\.hash\b", + "SHA-384", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"SHA512\.hash\b", + "SHA-512", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"Insecure\.SHA1\.hash\b", + "SHA-1", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"Insecure\.SHA1\b", + "SHA-1", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"Insecure\.MD5\.hash\b", + "MD5", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"Insecure\.MD5\b", + "MD5", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"HMAC\b", + "HMAC-SHA256", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"HMAC\b", + "HMAC-SHA384", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"HMAC\b", + "HMAC-SHA512", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"P256\.Signing\b", + "ECDSA-P256", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"P256\.KeyAgreement\b", + "ECDH-P256", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"P384\.Signing\b", + "ECDSA-P384", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"P384\.KeyAgreement\b", + "ECDH-P384", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"P521\.Signing\b", + "ECDSA-P521", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"P521\.KeyAgreement\b", + "ECDH-P521", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"Curve25519\.Signing\b", + "Ed25519", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"Curve25519\.KeyAgreement\b", + "X25519", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"HKDF\b|HKDF\b", + "HKDF", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::High, + ), + pat( + r"SymmetricKey\b", + "AES", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CryptoKit"), + Confidence::Low, + ), + // -- CommonCrypto -- + pat( + r"import\s+CommonCrypto\b", + "CommonCrypto", + DetectionMethod::ImportStatement, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::Medium, + ), + pat( + r"CCCrypt\b", + "AES", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"CCCryptorCreate\b", + "AES", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"kCCAlgorithmAES\b", + "AES", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"kCCAlgorithmDES\b", + "DES", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"kCCAlgorithm3DES\b", + "3DES", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"kCCAlgorithmBlowfish\b", + "Blowfish", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"kCCAlgorithmRC4\b", + "RC4", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"CC_SHA256\b", + "SHA-256", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"CC_SHA384\b", + "SHA-384", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"CC_SHA512\b", + "SHA-512", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"CC_SHA1\b", + "SHA-1", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"CC_MD5\b", + "MD5", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"CCHmac\b", + "HMAC", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + pat( + r"CCKeyDerivationPBKDF\b", + "PBKDF2", + DetectionMethod::ApiCall, + "swift", + &["swift"], + Some("CommonCrypto"), + Confidence::High, + ), + // -- Dependency manifests -- + manifest_pat( + r#"\.package\s*\(\s*url:.*CryptoSwift"#, + "CryptoSwift", + DetectionMethod::DependencyManifest, + "swift", + &["swift"], + &["Package.swift"], + None, + Confidence::High, + ), + manifest_pat( + r#"\.package\s*\(\s*url:.*swift-crypto"#, + "swift-crypto", + DetectionMethod::DependencyManifest, + "swift", + &["swift"], + &["Package.swift"], + None, + Confidence::High, + ), + manifest_pat( + r#"pod\s+['"]CryptoSwift['"]\b"#, + "CryptoSwift", + DetectionMethod::DependencyManifest, + "swift", + &["rb"], + &["Podfile"], + None, + Confidence::High, + ), + manifest_pat( + r#"pod\s+['"]OpenSSL['"]\b"#, + "OpenSSL", + DetectionMethod::DependencyManifest, + "swift", + &["rb"], + &["Podfile"], + None, + Confidence::High, + ), + manifest_pat( + r#"pod\s+['"]RNCryptor['"]\b"#, + "AES", + DetectionMethod::DependencyManifest, + "swift", + &["rb"], + &["Podfile"], + None, + Confidence::Medium, + ), + ] +} + +// ─── Elixir Patterns ────────────────────────────────────────────────── + +fn elixir_patterns() -> Vec { + vec![ + // -- :crypto (Erlang) - Symmetric -- + pat( + r":crypto\.crypto_one_time\b", + "AES", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.crypto_one_time_aead\b", + "AES-GCM", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.block_encrypt\b", + "AES", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.block_decrypt\b", + "AES", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.stream_encrypt\b", + "AES-CTR", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":aes_256_gcm\b", + "AES-256-GCM", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":aes_128_gcm\b", + "AES-128-GCM", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":aes_256_cbc\b", + "AES-256-CBC", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":aes_128_cbc\b", + "AES-128-CBC", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":aes_ecb\b", + "AES-ECB", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":chacha20_poly1305\b", + "ChaCha20-Poly1305", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":des_ede3_cbc\b", + "3DES", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":blowfish_cbc\b", + "Blowfish", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":rc4\b", + "RC4", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + // -- :crypto - Hash -- + pat( + r":crypto\.hash\s*\(\s*:sha256\b", + "SHA-256", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.hash\s*\(\s*:sha384\b", + "SHA-384", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.hash\s*\(\s*:sha512\b", + "SHA-512", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.hash\s*\(\s*:sha\b", + "SHA-1", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.hash\s*\(\s*:md5\b", + "MD5", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.hash\s*\(\s*:sha3_256\b", + "SHA-3", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.hash\s*\(\s*:blake2b\b", + "BLAKE2b", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + // -- :crypto - MAC -- + pat( + r":crypto\.mac\s*\(\s*:hmac\b", + "HMAC", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.macN?\s*\(\s*:cmac\b", + "CMAC", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + // -- :crypto - Asymmetric -- + pat( + r":crypto\.generate_key\s*\(\s*:rsa\b", + "RSA", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.generate_key\s*\(\s*:ecdh\b", + "ECDH", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.generate_key\s*\(\s*:eddsa\b", + "Ed25519", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::High, + ), + pat( + r":crypto\.sign\b", + "RSA", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::Medium, + ), + pat( + r":crypto\.verify\b", + "RSA", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":crypto"), + Confidence::Medium, + ), + // -- :public_key -- + pat( + r":public_key\.sign\b", + "RSA", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":public_key"), + Confidence::Medium, + ), + pat( + r":public_key\.verify\b", + "RSA", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":public_key"), + Confidence::Medium, + ), + pat( + r":public_key\.encrypt_private\b", + "RSA", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":public_key"), + Confidence::High, + ), + pat( + r":public_key\.encrypt_public\b", + "RSA", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some(":public_key"), + Confidence::High, + ), + // -- Comeonin / Bcrypt / Argon2 -- + pat( + r"Bcrypt\.hash_pwd_salt\b", + "bcrypt", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("bcrypt_elixir"), + Confidence::High, + ), + pat( + r"Bcrypt\.verify_pass\b", + "bcrypt", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("bcrypt_elixir"), + Confidence::High, + ), + pat( + r"Bcrypt\.no_user_verify\b", + "bcrypt", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("bcrypt_elixir"), + Confidence::High, + ), + pat( + r"Argon2\.hash_pwd_salt\b", + "Argon2", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("argon2_elixir"), + Confidence::High, + ), + pat( + r"Argon2\.verify_pass\b", + "Argon2", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("argon2_elixir"), + Confidence::High, + ), + pat( + r"Pbkdf2\.hash_pwd_salt\b", + "PBKDF2", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("pbkdf2_elixir"), + Confidence::High, + ), + pat( + r"Comeonin\.Bcrypt\b", + "bcrypt", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("comeonin"), + Confidence::High, + ), + pat( + r"Comeonin\.Argon2\b", + "Argon2", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("comeonin"), + Confidence::High, + ), + pat( + r"Comeonin\.Pbkdf2\b", + "PBKDF2", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("comeonin"), + Confidence::High, + ), + // -- Plug.Crypto / JOSE -- + pat( + r"Plug\.Crypto\b", + "AES", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("plug_crypto"), + Confidence::Medium, + ), + pat( + r"JOSE\.JWK\b", + "JOSE", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("jose"), + Confidence::High, + ), + pat( + r"JOSE\.JWS\b", + "JOSE", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("jose"), + Confidence::High, + ), + pat( + r"JOSE\.JWE\b", + "JOSE", + DetectionMethod::ApiCall, + "elixir", + &["ex", "exs"], + Some("jose"), + Confidence::High, + ), + // -- Dependency manifests (mix.exs) -- + manifest_pat( + r#"\{:bcrypt_elixir\b"#, + "bcrypt", + DetectionMethod::DependencyManifest, + "elixir", + &["exs"], + &["mix.exs"], + None, + Confidence::High, + ), + manifest_pat( + r#"\{:argon2_elixir\b"#, + "Argon2", + DetectionMethod::DependencyManifest, + "elixir", + &["exs"], + &["mix.exs"], + None, + Confidence::High, + ), + manifest_pat( + r#"\{:pbkdf2_elixir\b"#, + "PBKDF2", + DetectionMethod::DependencyManifest, + "elixir", + &["exs"], + &["mix.exs"], + None, + Confidence::High, + ), + manifest_pat( + r#"\{:comeonin\b"#, + "bcrypt", + DetectionMethod::DependencyManifest, + "elixir", + &["exs"], + &["mix.exs"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"\{:jose\b"#, + "JOSE", + DetectionMethod::DependencyManifest, + "elixir", + &["exs"], + &["mix.exs"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"\{:plug_crypto\b"#, + "AES", + DetectionMethod::DependencyManifest, + "elixir", + &["exs"], + &["mix.exs"], + None, + Confidence::Medium, + ), + manifest_pat( + r#"\{:ex_crypto\b"#, + "AES", + DetectionMethod::DependencyManifest, + "elixir", + &["exs"], + &["mix.exs"], + None, + Confidence::Medium, + ), + ] +} + +// ─── Cross-ecosystem patterns (config files, etc.) ──────────────────── + +fn cross_ecosystem_patterns() -> Vec { + vec![ + // TLS configuration + pat( + r"TLSv1\.3", + "TLS-1.3", + DetectionMethod::ConfigFile, + "any", + &[ + "conf", + "cfg", + "yaml", + "yml", + "toml", + "json", + "xml", + "ini", + "properties", + ], + None, + Confidence::Medium, + ), + pat( + r"TLSv1\.2", + "TLS-1.2", + DetectionMethod::ConfigFile, + "any", + &[ + "conf", + "cfg", + "yaml", + "yml", + "toml", + "json", + "xml", + "ini", + "properties", + ], + None, + Confidence::Medium, + ), + pat( + r"TLSv1\.1", + "TLS-1.1", + DetectionMethod::ConfigFile, + "any", + &[ + "conf", + "cfg", + "yaml", + "yml", + "toml", + "json", + "xml", + "ini", + "properties", + ], + None, + Confidence::Medium, + ), + pat( + r"SSLv3", + "SSL-3.0", + DetectionMethod::ConfigFile, + "any", + &[ + "conf", + "cfg", + "yaml", + "yml", + "toml", + "json", + "xml", + "ini", + "properties", + ], + None, + Confidence::Medium, + ), + // OpenSSL config + pat( + r"fips\s*=\s*fips_sect", + "FIPS-mode", + DetectionMethod::ConfigFile, + "any", + &["cnf", "conf", "cfg"], + None, + Confidence::High, + ), + ] +} + +// ─── Helper ─────────────────────────────────────────────────────────── + +fn pat( + regex: &str, + name: &str, + method: DetectionMethod, + ecosystem: &'static str, + extensions: &[&'static str], + library: Option<&str>, + confidence: Confidence, +) -> CryptoPattern { + CryptoPattern { + regex: Regex::new(regex).unwrap_or_else(|e| panic!("Invalid regex '{regex}': {e}")), + algorithm_name: name.to_string(), + detection_method: method, + ecosystem, + file_extensions: extensions.to_vec(), + file_names: &[], + providing_library: library.map(|s| s.to_string()), + confidence, + } +} + +/// Like [`pat`] but also restricts matching to specific file names (e.g. +/// `&["package.json"]`). Use for dependency-manifest rules whose extension +/// alone is too broad. +#[allow(clippy::too_many_arguments)] +fn manifest_pat( + regex: &str, + name: &str, + method: DetectionMethod, + ecosystem: &'static str, + extensions: &[&'static str], + file_names: &'static [&'static str], + library: Option<&str>, + confidence: Confidence, +) -> CryptoPattern { + CryptoPattern { + regex: Regex::new(regex).unwrap_or_else(|e| panic!("Invalid regex '{regex}': {e}")), + algorithm_name: name.to_string(), + detection_method: method, + ecosystem, + file_extensions: extensions.to_vec(), + file_names, + providing_library: library.map(|s| s.to_string()), + confidence, + } +} diff --git a/extlib/cryptoscan/src/scanner.rs b/extlib/cryptoscan/src/scanner.rs new file mode 100644 index 000000000..fcaf95990 --- /dev/null +++ b/extlib/cryptoscan/src/scanner.rs @@ -0,0 +1,942 @@ +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +use walkdir::WalkDir; + +use crate::crypto_algorithm::{Confidence, CryptoAlgorithm, CryptoFinding, Primitive}; +use crate::fips; +use crate::patterns::{self, CryptoPattern}; + +/// Auto-detect ecosystems present in a project directory. +pub fn detect_ecosystems(project_path: &Path) -> Vec { + let mut ecosystems = Vec::new(); + + let add_ecosystem = |eco: &str, ecosystems: &mut Vec| { + if !ecosystems.contains(&eco.to_string()) { + ecosystems.push(eco.to_string()); + } + }; + + for entry in WalkDir::new(project_path) + .into_iter() + .filter_entry(|e| !should_skip_path(e.path())) + .filter_map(|e| e.ok()) + { + let name = entry.file_name().to_string_lossy(); + let ext = entry + .path() + .extension() + .and_then(|e| e.to_str()) + .unwrap_or(""); + + // Detect from manifest files + match name.as_ref() { + "requirements.txt" | "Pipfile" | "pyproject.toml" | "setup.py" | "setup.cfg" => { + add_ecosystem("python", &mut ecosystems); + } + "pom.xml" | "build.gradle" | "build.gradle.kts" => { + add_ecosystem("java", &mut ecosystems); + } + "go.mod" | "go.sum" => { + add_ecosystem("go", &mut ecosystems); + } + "package.json" => { + add_ecosystem("node", &mut ecosystems); + } + "Cargo.toml" => { + add_ecosystem("rust", &mut ecosystems); + } + "Gemfile" | "Rakefile" => { + add_ecosystem("ruby", &mut ecosystems); + } + "composer.json" => { + add_ecosystem("php", &mut ecosystems); + } + "Package.swift" | "Podfile" => { + add_ecosystem("swift", &mut ecosystems); + } + "mix.exs" => { + add_ecosystem("elixir", &mut ecosystems); + } + _ => {} + } + + // Detect from source file extensions + match ext { + "py" => add_ecosystem("python", &mut ecosystems), + "java" | "kt" => add_ecosystem("java", &mut ecosystems), + "go" => add_ecosystem("go", &mut ecosystems), + "js" | "ts" => add_ecosystem("node", &mut ecosystems), + "rs" => add_ecosystem("rust", &mut ecosystems), + "rb" | "gemspec" => add_ecosystem("ruby", &mut ecosystems), + "php" => add_ecosystem("php", &mut ecosystems), + "swift" => add_ecosystem("swift", &mut ecosystems), + "ex" | "exs" => add_ecosystem("elixir", &mut ecosystems), + "cs" | "csproj" | "fsproj" | "sln" => add_ecosystem("csharp", &mut ecosystems), + _ => {} + } + } + + ecosystems +} + +/// Scan a project for cryptographic algorithm usage. +pub fn scan_project(project_path: &Path, ecosystems: &[String]) -> Vec { + let all_patterns = patterns::build_patterns(); + let mut findings = Vec::new(); + + for entry in WalkDir::new(project_path) + .into_iter() + .filter_entry(|e| !should_skip_path(e.path())) + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + { + let path = entry.path(); + + let extension = path.extension().and_then(|e| e.to_str()).unwrap_or(""); + + let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); + + // Read file content + let content = match fs::read_to_string(path) { + Ok(c) => c, + Err(_) => continue, // Skip binary files + }; + + let rel_path = path + .strip_prefix(project_path) + .unwrap_or(path) + .to_string_lossy() + .to_string(); + + // Match against all applicable patterns + for pattern in &all_patterns { + // Check if this pattern applies to the current ecosystem and file extension + if !pattern_applies(pattern, ecosystems, extension, file_name) { + continue; + } + + for mat in pattern.regex.find_iter(&content) { + let line_number = content[..mat.start()] + .chars() + .filter(|&c| c == '\n') + .count() + + 1; + + let algorithm = resolve_algorithm(&pattern.algorithm_name, mat.as_str()); + + findings.push(CryptoFinding { + algorithm, + file_path: rel_path.clone(), + line_number, + matched_text: mat.as_str().to_string(), + detection_method: pattern.detection_method.clone(), + ecosystem: pattern.ecosystem.to_string(), + providing_library: pattern.providing_library.clone(), + confidence: pattern.confidence.clone(), + }); + } + } + } + + // Deduplicate findings with same algorithm in same file (keep highest confidence) + deduplicate_findings(findings) +} + +fn should_skip_path(path: &Path) -> bool { + const SKIP_DIRS: &[&str] = &[ + ".git", + "node_modules", + "__pycache__", + ".venv", + "venv", + "target", + "dist", + "build", + ".tox", + ".mypy_cache", + ".pytest_cache", + "vendor", + ".gradle", + ".idea", + ".vscode", + ]; + path.file_name() + .and_then(|s| s.to_str()) + .is_some_and(|name| SKIP_DIRS.contains(&name)) +} + +fn pattern_applies( + pattern: &CryptoPattern, + ecosystems: &[String], + file_ext: &str, + file_name: &str, +) -> bool { + // Check ecosystem match + let ecosystem_match = pattern.ecosystem == "any" + || ecosystems.iter().any(|e| { + e == pattern.ecosystem + || (e == "node" + && (pattern.ecosystem == "javascript" || pattern.ecosystem == "typescript")) + }); + + if !ecosystem_match { + return false; + } + + // Check file extension match + if !pattern.file_extensions.contains(&file_ext) { + // For extensionless files (Gemfile, Podfile, Pipfile, etc.), check manifest names + if file_ext.is_empty() { + return patterns::ecosystem_manifests(pattern.ecosystem) + .iter() + .any(|m| !m.contains('*') && *m == file_name); + } + return false; + } + + // Extension matched. If the pattern also requires specific file names, + // verify the current file name is one of them. + if !pattern.file_names.is_empty() { + return pattern.file_names.contains(&file_name); + } + + true +} + +fn normalize_detected_algorithm(name: &str, matched_text: &str) -> String { + let lower = matched_text.to_lowercase(); + + // For generic "AES" patterns, try to extract specific mode/key-size from matched text + if name == "AES" { + if lower.contains("ecb") { + return "AES-ECB".to_string(); + } + if lower.contains("gcm") { + if lower.contains("256") { + return "AES-256-GCM".to_string(); + } + if lower.contains("192") { + return "AES-192-GCM".to_string(); + } + if lower.contains("128") { + return "AES-128-GCM".to_string(); + } + return "AES-GCM".to_string(); + } + if lower.contains("cbc") { + if lower.contains("256") { + return "AES-256-CBC".to_string(); + } + if lower.contains("192") { + return "AES-192-CBC".to_string(); + } + if lower.contains("128") { + return "AES-128-CBC".to_string(); + } + return "AES-CBC".to_string(); + } + if lower.contains("ctr") { + return "AES-CTR".to_string(); + } + // Extract key size alone + if lower.contains("256") { + return "AES-256".to_string(); + } + if lower.contains("192") { + return "AES-192".to_string(); + } + if lower.contains("128") { + return "AES-128".to_string(); + } + } + + // For generic "SHA" or hash patterns, extract specific variant + if name == "SHA-256" + || name == "SHA-384" + || name == "SHA-512" + || name == "SHA-1" + || name == "SHA-3" + { + // Already specific, keep as-is + return name.to_string(); + } + + // For generic "RSA" pattern, try to extract key size + if name == "RSA" { + let key_size: Option = lower.split(|c: char| !c.is_ascii_digit()).find_map(|tok| { + let n = tok.parse::().ok()?; + if (512..=16384).contains(&n) { + Some(n) + } else { + None + } + }); + if let Some(bits) = key_size { + return format!("RSA-{}", bits); + } + } + + // For generic "ECDSA" pattern, extract curve from matched text + if name == "ECDSA" { + if lower.contains("p384") || lower.contains("p-384") || lower.contains("secp384") { + return "ECDSA-P384".to_string(); + } + if lower.contains("p521") || lower.contains("p-521") || lower.contains("secp521") { + return "ECDSA-P521".to_string(); + } + if lower.contains("p256") + || lower.contains("p-256") + || lower.contains("prime256") + || lower.contains("secp256r1") + { + return "ECDSA-P256".to_string(); + } + } + + name.to_string() +} + +fn resolve_algorithm(name: &str, matched_text: &str) -> CryptoAlgorithm { + let normalized = normalize_detected_algorithm(name, matched_text); + let (fips_status, _remediation) = fips::classify_algorithm(&normalized); + + let (primitive, family, mode, param_set, curve, security, quantum, oid, functions) = + match normalized.as_str() { + // Symmetric - AES + "AES" => ( + Primitive::BlockCipher, + "AES", + None, + None, + None, + None, + 1, + Some("2.16.840.1.101.3.4.1"), + vec!["keygen", "encrypt", "decrypt"], + ), + "AES-128" => ( + Primitive::BlockCipher, + "AES", + None, + Some("128"), + None, + Some(128), + 1, + Some("2.16.840.1.101.3.4.1"), + vec!["keygen", "encrypt", "decrypt"], + ), + "AES-192" => ( + Primitive::BlockCipher, + "AES", + None, + Some("192"), + None, + Some(192), + 1, + Some("2.16.840.1.101.3.4.1"), + vec!["keygen", "encrypt", "decrypt"], + ), + "AES-256" => ( + Primitive::BlockCipher, + "AES", + None, + Some("256"), + None, + Some(256), + 1, + Some("2.16.840.1.101.3.4.1"), + vec!["keygen", "encrypt", "decrypt"], + ), + "AES-GCM" => ( + Primitive::Ae, + "AES", + Some("gcm"), + None, + None, + None, + 1, + Some("2.16.840.1.101.3.4.1.46"), + vec!["keygen", "encrypt", "decrypt", "tag"], + ), + "AES-256-GCM" => ( + Primitive::Ae, + "AES", + Some("gcm"), + Some("256"), + None, + Some(256), + 1, + Some("2.16.840.1.101.3.4.1.46"), + vec!["keygen", "encrypt", "decrypt", "tag"], + ), + "AES-192-GCM" => ( + Primitive::Ae, + "AES", + Some("gcm"), + Some("192"), + None, + Some(192), + 1, + Some("2.16.840.1.101.3.4.1.46"), + vec!["keygen", "encrypt", "decrypt", "tag"], + ), + "AES-128-GCM" => ( + Primitive::Ae, + "AES", + Some("gcm"), + Some("128"), + None, + Some(128), + 1, + Some("2.16.840.1.101.3.4.1.6"), + vec!["keygen", "encrypt", "decrypt", "tag"], + ), + "AES-CBC" => ( + Primitive::BlockCipher, + "AES", + Some("cbc"), + None, + None, + None, + 1, + None, + vec!["keygen", "encrypt", "decrypt"], + ), + "AES-256-CBC" => ( + Primitive::BlockCipher, + "AES", + Some("cbc"), + Some("256"), + None, + Some(256), + 1, + None, + vec!["keygen", "encrypt", "decrypt"], + ), + "AES-192-CBC" => ( + Primitive::BlockCipher, + "AES", + Some("cbc"), + Some("192"), + None, + Some(192), + 1, + None, + vec!["keygen", "encrypt", "decrypt"], + ), + "AES-128-CBC" => ( + Primitive::BlockCipher, + "AES", + Some("cbc"), + Some("128"), + None, + Some(128), + 1, + None, + vec!["keygen", "encrypt", "decrypt"], + ), + "AES-CTR" => ( + Primitive::BlockCipher, + "AES", + Some("ctr"), + None, + None, + None, + 1, + None, + vec!["keygen", "encrypt", "decrypt"], + ), + "AES-ECB" => ( + Primitive::BlockCipher, + "AES", + Some("ecb"), + None, + None, + None, + 1, + None, + vec!["keygen", "encrypt", "decrypt"], + ), + + // Symmetric - non-AES + "ChaCha20-Poly1305" | "ChaCha20" => ( + Primitive::Ae, + "ChaCha20-Poly1305", + None, + Some("256"), + None, + Some(256), + 0, + None, + vec!["keygen", "encrypt", "decrypt", "tag"], + ), + "3DES" => ( + Primitive::BlockCipher, + "3DES", + Some("cbc"), + Some("168"), + None, + Some(112), + 0, + Some("1.2.840.113549.3.7"), + vec!["encrypt", "decrypt"], + ), + "DES" => ( + Primitive::BlockCipher, + "DES", + Some("cbc"), + Some("56"), + None, + Some(56), + 0, + Some("1.3.14.3.2.7"), + vec!["encrypt", "decrypt"], + ), + "Blowfish" => ( + Primitive::BlockCipher, + "Blowfish", + None, + None, + None, + None, + 0, + None, + vec!["encrypt", "decrypt"], + ), + "RC4" => ( + Primitive::StreamCipher, + "RC4", + None, + None, + None, + None, + 0, + None, + vec!["encrypt", "decrypt"], + ), + "Salsa20" => ( + Primitive::StreamCipher, + "Salsa20", + None, + Some("256"), + None, + Some(256), + 0, + None, + vec!["encrypt", "decrypt"], + ), + + // Hash functions + "SHA-256" => ( + Primitive::Hash, + "SHA-2", + None, + Some("256"), + None, + Some(128), + 0, + Some("2.16.840.1.101.3.4.2.1"), + vec!["digest"], + ), + "SHA-384" => ( + Primitive::Hash, + "SHA-2", + None, + Some("384"), + None, + Some(192), + 0, + Some("2.16.840.1.101.3.4.2.2"), + vec!["digest"], + ), + "SHA-512" => ( + Primitive::Hash, + "SHA-2", + None, + Some("512"), + None, + Some(256), + 0, + Some("2.16.840.1.101.3.4.2.3"), + vec!["digest"], + ), + "SHA-1" => ( + Primitive::Hash, + "SHA-1", + None, + Some("160"), + None, + Some(80), + 0, + Some("1.3.14.3.2.26"), + vec!["digest"], + ), + "SHA-3" => ( + Primitive::Hash, + "SHA-3", + None, + None, + None, + Some(128), + 0, + Some("2.16.840.1.101.3.4.2.8"), + vec!["digest"], + ), + "MD5" => ( + Primitive::Hash, + "MD5", + None, + Some("128"), + None, + Some(64), + 0, + Some("1.2.840.113549.2.5"), + vec!["digest"], + ), + "BLAKE2" | "BLAKE2b" | "BLAKE2s" => ( + Primitive::Hash, + "BLAKE2", + None, + None, + None, + Some(128), + 0, + None, + vec!["digest"], + ), + "BLAKE3" => ( + Primitive::Hash, + "BLAKE3", + None, + None, + None, + Some(128), + 0, + None, + vec!["digest"], + ), + + // Asymmetric / Signatures + "RSA" => ( + Primitive::Pke, + "RSA", + None, + None, + None, + None, + 0, + Some("1.2.840.113549.1.1.1"), + vec!["keygen", "encrypt", "decrypt", "sign", "verify"], + ), + "ECDSA" => ( + Primitive::Signature, + "ECDSA", + None, + None, + None, + None, + 0, + Some("1.2.840.10045.2.1"), + vec!["keygen", "sign", "verify"], + ), + "ECDSA-P256" => ( + Primitive::Signature, + "ECDSA", + None, + Some("256"), + Some("nist/P-256"), + Some(128), + 0, + Some("1.2.840.10045.2.1"), + vec!["keygen", "sign", "verify"], + ), + "ECDSA-P384" => ( + Primitive::Signature, + "ECDSA", + None, + Some("384"), + Some("nist/P-384"), + Some(192), + 0, + Some("1.2.840.10045.2.1"), + vec!["keygen", "sign", "verify"], + ), + "ECDSA-P521" => ( + Primitive::Signature, + "ECDSA", + None, + Some("521"), + Some("nist/P-521"), + Some(256), + 0, + Some("1.2.840.10045.2.1"), + vec!["keygen", "sign", "verify"], + ), + "Ed25519" => ( + Primitive::Signature, + "EdDSA", + None, + None, + Some("edwards/Ed25519"), + Some(128), + 0, + Some("1.3.101.112"), + vec!["keygen", "sign", "verify"], + ), + "Ed448" => ( + Primitive::Signature, + "EdDSA", + None, + None, + Some("edwards/Ed448"), + Some(224), + 0, + Some("1.3.101.113"), + vec!["keygen", "sign", "verify"], + ), + "DSA" => ( + Primitive::Signature, + "DSA", + None, + None, + None, + None, + 0, + Some("1.2.840.10040.4.1"), + vec!["sign", "verify"], + ), + + // Key exchange + "ECDH" => ( + Primitive::KeyAgree, + "ECDH", + None, + None, + None, + None, + 0, + None, + vec!["keygen", "keyderive"], + ), + "X25519" => ( + Primitive::KeyAgree, + "X25519", + None, + None, + Some("montgomery/Curve25519"), + Some(128), + 0, + Some("1.3.101.110"), + vec!["keygen", "keyderive"], + ), + "DH" => ( + Primitive::KeyAgree, + "DH", + None, + None, + None, + None, + 0, + None, + vec!["keygen", "keyderive"], + ), + + // MACs + "HMAC" | "HMAC-SHA256" | "HMAC-SHA512" => ( + Primitive::Mac, + "HMAC", + None, + None, + None, + Some(128), + 0, + Some("1.2.840.113549.2.9"), + vec!["keygen", "sign", "verify"], + ), + "HMAC-SHA1" => ( + Primitive::Mac, + "HMAC", + None, + None, + None, + Some(80), + 0, + None, + vec!["keygen", "sign", "verify"], + ), + + // KDFs + "HKDF" => ( + Primitive::Kdf, + "HKDF", + None, + None, + None, + None, + 0, + None, + vec!["keyderive"], + ), + "PBKDF2" => ( + Primitive::Kdf, + "PBKDF2", + None, + None, + None, + None, + 0, + None, + vec!["keyderive"], + ), + "scrypt" => ( + Primitive::Kdf, + "scrypt", + None, + None, + None, + None, + 0, + None, + vec!["keyderive"], + ), + "bcrypt" => ( + Primitive::Kdf, + "bcrypt", + None, + None, + None, + None, + 0, + None, + vec!["keyderive"], + ), + "Argon2" => ( + Primitive::Kdf, + "Argon2", + None, + None, + None, + None, + 0, + None, + vec!["keyderive"], + ), + + // Post-quantum + "ML-KEM" => ( + Primitive::Kem, + "ML-KEM", + None, + None, + None, + Some(256), + 5, + Some("2.16.840.1.101.3.4.4"), + vec!["keygen", "encapsulate", "decapsulate"], + ), + "ML-DSA" => ( + Primitive::Signature, + "ML-DSA", + None, + None, + None, + Some(256), + 5, + None, + vec!["keygen", "sign", "verify"], + ), + + // Generic / library-level detections + other => { + // Handle RSA- variants (e.g., RSA-4096, RSA-3072, RSA-1024) + if let Some(bits_str) = other.strip_prefix("RSA-") { + if let Ok(bits) = bits_str.parse::() { + let security = if bits >= 3072 { + Some(128u32) + } else { + Some(112) + }; + ( + Primitive::Pke, + "RSA", + None, + Some(bits_str), + None, + security, + 0, + Some("1.2.840.113549.1.1.1"), + vec!["keygen", "encrypt", "decrypt", "sign", "verify"], + ) + } else { + ( + Primitive::Unknown, + other, + None, + None, + None, + None, + 0, + None, + vec![], + ) + } + } else { + ( + Primitive::Unknown, + other, + None, + None, + None, + None, + 0, + None, + vec![], + ) + } + } + }; + + CryptoAlgorithm { + name: normalized.clone(), + algorithm_family: family.to_string(), + primitive, + parameter_set: param_set.map(|s| s.to_string()), + elliptic_curve: curve.map(|s| s.to_string()), + mode: mode.map(|s| s.to_string()), + oid: oid.map(|s| s.to_string()), + classical_security_level: security, + nist_quantum_security_level: quantum, + fips_status, + crypto_functions: functions.into_iter().map(|s| s.to_string()).collect(), + } +} + +fn confidence_rank(c: &Confidence) -> u8 { + match c { + Confidence::High => 3, + Confidence::Medium => 2, + Confidence::Low => 1, + } +} + +fn deduplicate_findings(findings: Vec) -> Vec { + let mut best: BTreeMap = BTreeMap::new(); + + for finding in findings { + let key = format!( + "{}:{}:{}", + finding.algorithm.name, finding.file_path, finding.line_number + ); + best.entry(key) + .and_modify(|existing| { + if confidence_rank(&finding.confidence) > confidence_rank(&existing.confidence) { + *existing = finding.clone(); + } + }) + .or_insert(finding); + } + + best.into_values().collect() +} diff --git a/extlib/cryptoscan/tests/integration_test.rs b/extlib/cryptoscan/tests/integration_test.rs new file mode 100644 index 000000000..5f53de6bb --- /dev/null +++ b/extlib/cryptoscan/tests/integration_test.rs @@ -0,0 +1,770 @@ +use assert_cmd::Command; +use serde_json::Value; +use std::path::PathBuf; + +fn fixture_path(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../test-fixtures") + .join(name) +} + +fn run_scan(fixture: &str, ecosystem: &str) -> Vec { + let output = Command::cargo_bin("cryptoscan") + .unwrap() + .args(["--path", fixture_path(fixture).to_str().unwrap()]) + .args(["--ecosystem", ecosystem]) + .args(["--format", "json"]) + .output() + .expect("failed to execute"); + assert!(output.status.success(), "scanner exited with error"); + serde_json::from_slice(&output.stdout).expect("invalid JSON output") +} + +fn run_scan_non_fips(fixture: &str, ecosystem: &str) -> Vec { + let output = Command::cargo_bin("cryptoscan") + .unwrap() + .args(["--path", fixture_path(fixture).to_str().unwrap()]) + .args(["--ecosystem", ecosystem]) + .args(["--format", "json"]) + .arg("--non-fips-only") + .output() + .expect("failed to execute"); + assert!(output.status.success(), "scanner exited with error"); + serde_json::from_slice(&output.stdout).expect("invalid JSON output") +} + +fn run_scan_cyclonedx(fixture: &str, ecosystem: &str) -> Value { + let output = Command::cargo_bin("cryptoscan") + .unwrap() + .args(["--path", fixture_path(fixture).to_str().unwrap()]) + .args(["--ecosystem", ecosystem]) + .args(["--format", "cyclonedx"]) + .output() + .expect("failed to execute"); + assert!(output.status.success(), "scanner exited with error"); + serde_json::from_slice(&output.stdout).expect("invalid CycloneDX JSON") +} + +fn has_algorithm(findings: &[Value], name: &str) -> bool { + findings + .iter() + .any(|f| f["algorithm"]["name"].as_str().unwrap_or("") == name) +} + +fn has_algorithm_with_status(findings: &[Value], name: &str, status: &str) -> bool { + findings.iter().any(|f| { + f["algorithm"]["name"].as_str().unwrap_or("") == name + && f["algorithm"]["fips_status"].as_str().unwrap_or("") == status + }) +} + +fn has_algorithm_with_method(findings: &[Value], name: &str, method: &str) -> bool { + findings.iter().any(|f| { + f["algorithm"]["name"].as_str().unwrap_or("") == name + && f["detection_method"].as_str().unwrap_or("") == method + }) +} + +// ============================================================================ +// Python ecosystem tests +// ============================================================================ + +#[test] +fn python_detects_aes_encryption() { + let findings = run_scan("python-web-app", "python"); + assert!(has_algorithm(&findings, "AES"), "should detect AES"); + assert!(has_algorithm_with_status(&findings, "AES", "approved")); +} + +#[test] +fn python_detects_aes_gcm_api_call() { + let findings = run_scan("python-web-app", "python"); + assert!( + has_algorithm_with_method(&findings, "AES-GCM", "api-call"), + "should detect AES-GCM via API call pattern" + ); +} + +#[test] +fn python_detects_chacha20() { + let findings = run_scan("python-web-app", "python"); + assert!( + has_algorithm_with_status(&findings, "ChaCha20-Poly1305", "not-approved"), + "ChaCha20-Poly1305 should be not-approved" + ); +} + +#[test] +fn python_detects_rsa() { + let findings = run_scan("python-web-app", "python"); + assert!( + has_algorithm_with_status(&findings, "RSA", "deprecated"), + "RSA without key size should be deprecated (manual review needed)" + ); +} + +#[test] +fn python_detects_ecdsa_p256() { + let findings = run_scan("python-web-app", "python"); + assert!( + has_algorithm(&findings, "ECDSA-P256"), + "should detect ECDSA P-256" + ); +} + +#[test] +fn python_detects_ed25519() { + let findings = run_scan("python-web-app", "python"); + assert!(has_algorithm_with_status(&findings, "Ed25519", "approved")); +} + +#[test] +fn python_detects_hash_algorithms() { + let findings = run_scan("python-web-app", "python"); + assert!(has_algorithm_with_status(&findings, "SHA-256", "approved")); + assert!(has_algorithm_with_status(&findings, "SHA-1", "deprecated")); + assert!(has_algorithm_with_status(&findings, "MD5", "not-approved")); + assert!(has_algorithm_with_status( + &findings, + "BLAKE2", + "not-approved" + )); +} + +#[test] +fn python_detects_kdf_algorithms() { + let findings = run_scan("python-web-app", "python"); + assert!(has_algorithm_with_status(&findings, "PBKDF2", "approved")); + assert!(has_algorithm_with_status( + &findings, + "scrypt", + "not-approved" + )); + assert!(has_algorithm_with_status(&findings, "HKDF", "approved")); +} + +#[test] +fn python_detects_bcrypt() { + let findings = run_scan("python-web-app", "python"); + assert!( + has_algorithm(&findings, "bcrypt"), + "should detect bcrypt import" + ); +} + +#[test] +fn python_detects_dependency_manifests() { + let findings = run_scan("python-web-app", "python"); + let dep_findings: Vec<&Value> = findings + .iter() + .filter(|f| f["detection_method"].as_str() == Some("dependency-manifest")) + .collect(); + assert!( + dep_findings.len() >= 3, + "should detect at least 3 dependency entries" + ); +} + +#[test] +fn python_total_finding_count() { + let findings = run_scan("python-web-app", "python"); + assert!( + findings.len() >= 20, + "Python fixture should produce at least 20 findings, got {}", + findings.len() + ); +} + +// ============================================================================ +// Java ecosystem tests +// ============================================================================ + +#[test] +fn java_detects_aes_gcm() { + let findings = run_scan("java-microservice", "java"); + assert!( + has_algorithm_with_status(&findings, "AES-256-GCM", "approved"), + "should detect AES-256-GCM via Cipher.getInstance" + ); +} + +#[test] +fn java_detects_aes_ecb_deprecated() { + let findings = run_scan("java-microservice", "java"); + assert!( + has_algorithm_with_status(&findings, "AES-ECB", "deprecated"), + "AES-ECB should be marked as deprecated" + ); +} + +#[test] +fn java_detects_3des_deprecated() { + let findings = run_scan("java-microservice", "java"); + assert!( + has_algorithm_with_status(&findings, "3DES", "deprecated"), + "3DES should be marked as deprecated" + ); +} + +#[test] +fn java_detects_signatures() { + let findings = run_scan("java-microservice", "java"); + assert!( + has_algorithm_with_status(&findings, "RSA", "deprecated"), + "RSA without key size should be deprecated (manual review needed)" + ); + assert!( + has_algorithm_with_status(&findings, "ECDSA", "deprecated"), + "ECDSA without curve should be deprecated (manual review needed)" + ); + assert!(has_algorithm_with_status(&findings, "Ed25519", "approved")); +} + +#[test] +fn java_detects_key_exchange() { + let findings = run_scan("java-microservice", "java"); + assert!( + has_algorithm_with_status(&findings, "DH", "deprecated"), + "DH without key size should be deprecated (manual review needed)" + ); + assert!( + has_algorithm_with_status(&findings, "X25519", "not-approved"), + "X25519 should be not-approved" + ); +} + +#[test] +fn java_detects_hashes() { + let findings = run_scan("java-microservice", "java"); + assert!(has_algorithm_with_status(&findings, "SHA-256", "approved")); + assert!(has_algorithm_with_status(&findings, "SHA-1", "deprecated")); + assert!(has_algorithm_with_status(&findings, "MD5", "not-approved")); +} + +#[test] +fn java_detects_hmac_and_pbkdf2() { + let findings = run_scan("java-microservice", "java"); + assert!(has_algorithm_with_status( + &findings, + "HMAC-SHA256", + "approved" + )); + assert!(has_algorithm_with_status(&findings, "PBKDF2", "approved")); +} + +#[test] +fn java_detects_bouncycastle_dependency() { + let findings = run_scan("java-microservice", "java"); + assert!( + has_algorithm_with_method(&findings, "BouncyCastle", "dependency-manifest"), + "should detect BouncyCastle in pom.xml" + ); +} + +#[test] +fn java_total_finding_count() { + let findings = run_scan("java-microservice", "java"); + assert!( + findings.len() >= 15, + "Java fixture should produce at least 15 findings, got {}", + findings.len() + ); +} + +// ============================================================================ +// Go ecosystem tests +// ============================================================================ + +#[test] +fn go_detects_aes_gcm() { + let findings = run_scan("go-api-server", "go"); + assert!(has_algorithm(&findings, "AES"), "should detect AES imports"); + assert!( + has_algorithm_with_method(&findings, "AES-GCM", "api-call"), + "should detect cipher.NewGCM API call" + ); +} + +#[test] +fn go_detects_chacha20() { + let findings = run_scan("go-api-server", "go"); + assert!(has_algorithm_with_status( + &findings, + "ChaCha20-Poly1305", + "not-approved" + )); +} + +#[test] +fn go_detects_signatures() { + let findings = run_scan("go-api-server", "go"); + assert!(has_algorithm(&findings, "RSA")); + assert!(has_algorithm(&findings, "ECDSA")); + assert!(has_algorithm(&findings, "Ed25519")); +} + +#[test] +fn go_detects_ecdsa_curves() { + let findings = run_scan("go-api-server", "go"); + assert!( + has_algorithm(&findings, "ECDSA-P256"), + "should detect P-256 curve" + ); + assert!( + has_algorithm(&findings, "ECDSA-P384"), + "should detect P-384 curve" + ); +} + +#[test] +fn go_detects_x25519() { + let findings = run_scan("go-api-server", "go"); + assert!(has_algorithm_with_status( + &findings, + "X25519", + "not-approved" + )); +} + +#[test] +fn go_detects_hash_algorithms() { + let findings = run_scan("go-api-server", "go"); + assert!(has_algorithm_with_status(&findings, "SHA-256", "approved")); + assert!(has_algorithm_with_status(&findings, "SHA-512", "approved")); + assert!(has_algorithm_with_status(&findings, "SHA-1", "deprecated")); + assert!(has_algorithm_with_status(&findings, "MD5", "not-approved")); + assert!(has_algorithm_with_status( + &findings, + "BLAKE2b", + "not-approved" + )); +} + +#[test] +fn go_detects_password_hashing() { + let findings = run_scan("go-api-server", "go"); + assert!(has_algorithm_with_status( + &findings, + "Argon2", + "not-approved" + )); + assert!(has_algorithm_with_status( + &findings, + "bcrypt", + "not-approved" + )); + assert!(has_algorithm_with_status( + &findings, + "scrypt", + "not-approved" + )); +} + +#[test] +fn go_detects_kdf() { + let findings = run_scan("go-api-server", "go"); + assert!(has_algorithm_with_status(&findings, "HKDF", "approved")); + assert!(has_algorithm_with_status(&findings, "HMAC", "approved")); +} + +#[test] +fn go_total_finding_count() { + let findings = run_scan("go-api-server", "go"); + assert!( + findings.len() >= 30, + "Go fixture should produce at least 30 findings, got {}", + findings.len() + ); +} + +// ============================================================================ +// Node.js ecosystem tests +// ============================================================================ + +#[test] +fn node_detects_aes_variants() { + let findings = run_scan("node-auth-service", "node"); + assert!(has_algorithm_with_status( + &findings, + "AES-256-GCM", + "approved" + )); + assert!(has_algorithm_with_status( + &findings, + "AES-128-CBC", + "approved" + )); +} + +#[test] +fn node_detects_3des() { + let findings = run_scan("node-auth-service", "node"); + assert!( + has_algorithm_with_status(&findings, "3DES", "deprecated"), + "3DES via des-ede3-cbc should be deprecated" + ); +} + +#[test] +fn node_detects_chacha20() { + let findings = run_scan("node-auth-service", "node"); + assert!(has_algorithm_with_status( + &findings, + "ChaCha20-Poly1305", + "not-approved" + )); +} + +#[test] +fn node_detects_hashes() { + let findings = run_scan("node-auth-service", "node"); + assert!(has_algorithm_with_status(&findings, "SHA-256", "approved")); + assert!(has_algorithm_with_status(&findings, "SHA-1", "deprecated")); + assert!(has_algorithm_with_status(&findings, "MD5", "not-approved")); +} + +#[test] +fn node_detects_hmac_variants() { + let findings = run_scan("node-auth-service", "node"); + assert!(has_algorithm_with_status( + &findings, + "HMAC-SHA256", + "approved" + )); + assert!(has_algorithm_with_status( + &findings, + "HMAC-SHA1", + "deprecated" + )); +} + +#[test] +fn node_detects_key_generation() { + let findings = run_scan("node-auth-service", "node"); + assert!( + has_algorithm_with_status(&findings, "RSA", "deprecated"), + "RSA without key size should be deprecated (manual review needed)" + ); + assert!(has_algorithm_with_status(&findings, "Ed25519", "approved")); + assert!(has_algorithm_with_status( + &findings, + "X25519", + "not-approved" + )); +} + +#[test] +fn node_detects_kdf() { + let findings = run_scan("node-auth-service", "node"); + assert!(has_algorithm_with_status(&findings, "PBKDF2", "approved")); + assert!(has_algorithm_with_status( + &findings, + "scrypt", + "not-approved" + )); +} + +#[test] +fn node_detects_package_json_deps() { + let findings = run_scan("node-auth-service", "node"); + assert!(has_algorithm_with_method( + &findings, + "bcrypt", + "dependency-manifest" + )); + assert!(has_algorithm_with_method( + &findings, + "Argon2", + "dependency-manifest" + )); +} + +#[test] +fn node_total_finding_count() { + let findings = run_scan("node-auth-service", "node"); + assert!( + findings.len() >= 15, + "Node fixture should produce at least 15 findings, got {}", + findings.len() + ); +} + +// ============================================================================ +// Rust ecosystem tests +// ============================================================================ + +#[test] +fn rust_detects_aes_gcm() { + let findings = run_scan("rust-crypto-tool", "rust"); + assert!(has_algorithm_with_status(&findings, "AES-GCM", "approved")); +} + +#[test] +fn rust_detects_chacha20() { + let findings = run_scan("rust-crypto-tool", "rust"); + assert!(has_algorithm_with_status( + &findings, + "ChaCha20-Poly1305", + "not-approved" + )); +} + +#[test] +fn rust_detects_sha256() { + let findings = run_scan("rust-crypto-tool", "rust"); + assert!(has_algorithm_with_status(&findings, "SHA-256", "approved")); +} + +#[test] +fn rust_detects_blake_variants() { + let findings = run_scan("rust-crypto-tool", "rust"); + assert!(has_algorithm_with_status( + &findings, + "BLAKE2", + "not-approved" + )); + assert!(has_algorithm_with_status( + &findings, + "BLAKE3", + "not-approved" + )); +} + +#[test] +fn rust_detects_signatures_and_kex() { + let findings = run_scan("rust-crypto-tool", "rust"); + assert!(has_algorithm_with_status(&findings, "Ed25519", "approved")); + assert!(has_algorithm_with_status( + &findings, + "X25519", + "not-approved" + )); +} + +#[test] +fn rust_detects_argon2() { + let findings = run_scan("rust-crypto-tool", "rust"); + assert!(has_algorithm_with_status( + &findings, + "Argon2", + "not-approved" + )); +} + +#[test] +fn rust_detects_hkdf() { + let findings = run_scan("rust-crypto-tool", "rust"); + assert!(has_algorithm_with_status(&findings, "HKDF", "approved")); +} + +#[test] +fn rust_detects_cargo_deps() { + let findings = run_scan("rust-crypto-tool", "rust"); + let dep_findings: Vec<&Value> = findings + .iter() + .filter(|f| f["detection_method"].as_str() == Some("dependency-manifest")) + .collect(); + assert!( + dep_findings.len() >= 5, + "should detect at least 5 Cargo.toml deps, got {}", + dep_findings.len() + ); +} + +#[test] +fn rust_detects_ring_dependency() { + let findings = run_scan("rust-crypto-tool", "rust"); + assert!( + has_algorithm_with_method(&findings, "ring", "dependency-manifest"), + "should detect ring crate in Cargo.toml" + ); +} + +#[test] +fn rust_total_finding_count() { + let findings = run_scan("rust-crypto-tool", "rust"); + assert!( + findings.len() >= 15, + "Rust fixture should produce at least 15 findings, got {}", + findings.len() + ); +} + +// ============================================================================ +// Cross-ecosystem / feature tests +// ============================================================================ + +#[test] +fn non_fips_only_filters_approved() { + let all = run_scan("go-api-server", "go"); + let non_fips = run_scan_non_fips("go-api-server", "go"); + + assert!( + non_fips.len() < all.len(), + "non-FIPS-only should return fewer findings than full scan" + ); + + for finding in &non_fips { + let status = finding["algorithm"]["fips_status"].as_str().unwrap(); + assert_ne!( + status, "approved", + "non-FIPS-only should not include approved algorithms, found: {}", + finding["algorithm"]["name"] + ); + } +} + +#[test] +fn cyclonedx_output_structure() { + let bom = run_scan_cyclonedx("node-auth-service", "node"); + + assert_eq!(bom["bomFormat"], "CycloneDX"); + assert_eq!(bom["specVersion"], "1.7"); + assert!(bom["serialNumber"] + .as_str() + .unwrap() + .starts_with("urn:uuid:")); + assert_eq!(bom["version"], 1); + assert!(bom["metadata"]["timestamp"].as_str().is_some()); + assert_eq!(bom["metadata"]["tools"][0]["name"], "fossa-cryptoscan"); +} + +#[test] +fn cyclonedx_contains_crypto_components() { + let bom = run_scan_cyclonedx("node-auth-service", "node"); + let components = bom["components"] + .as_array() + .expect("components should be array"); + + let crypto_components: Vec<&Value> = components + .iter() + .filter(|c| c["type"] == "cryptographic-asset") + .collect(); + assert!( + !crypto_components.is_empty(), + "should contain cryptographic-asset components" + ); + + // Verify crypto properties structure + let first_crypto = crypto_components[0]; + assert!(first_crypto["cryptoProperties"]["assetType"] + .as_str() + .is_some()); + assert!( + first_crypto["cryptoProperties"]["algorithmProperties"]["primitive"] + .as_str() + .is_some() + ); + assert!( + first_crypto["cryptoProperties"]["algorithmProperties"]["algorithmFamily"] + .as_str() + .is_some() + ); +} + +#[test] +fn cyclonedx_contains_fossa_properties() { + let bom = run_scan_cyclonedx("node-auth-service", "node"); + let components = bom["components"].as_array().unwrap(); + + let crypto_component = components + .iter() + .find(|c| c["type"] == "cryptographic-asset") + .expect("should have a crypto component"); + + let properties = crypto_component["properties"].as_array().unwrap(); + let prop_names: Vec<&str> = properties + .iter() + .map(|p| p["name"].as_str().unwrap()) + .collect(); + + assert!( + prop_names.contains(&"fossa:fips-status"), + "should include fossa:fips-status" + ); + assert!( + prop_names.contains(&"fossa:detected-in"), + "should include fossa:detected-in" + ); + assert!( + prop_names.contains(&"fossa:detection-method"), + "should include fossa:detection-method" + ); + assert!( + prop_names.contains(&"fossa:ecosystem"), + "should include fossa:ecosystem" + ); +} + +#[test] +fn cyclonedx_contains_dependencies() { + let bom = run_scan_cyclonedx("node-auth-service", "node"); + let deps = bom["dependencies"].as_array(); + assert!( + deps.is_some(), + "CycloneDX output should contain dependencies section" + ); +} + +#[test] +fn auto_ecosystem_detection() { + // Test that --ecosystem auto works (should detect from manifest files) + let output = Command::cargo_bin("cryptoscan") + .unwrap() + .args(["--path", fixture_path("python-web-app").to_str().unwrap()]) + .args(["--ecosystem", "auto"]) + .args(["--format", "json"]) + .output() + .expect("failed to execute"); + assert!(output.status.success()); + let findings: Vec = serde_json::from_slice(&output.stdout).unwrap(); + assert!( + !findings.is_empty(), + "auto-detection should find the Python ecosystem" + ); +} + +#[test] +fn finding_includes_file_location() { + let findings = run_scan("python-web-app", "python"); + let api_finding = findings + .iter() + .find(|f| f["detection_method"] == "api-call") + .expect("should have at least one api-call finding"); + + assert!( + api_finding["file_path"].as_str().is_some(), + "should have file_path" + ); + assert!( + api_finding["line_number"].as_u64().unwrap() > 0, + "should have positive line_number" + ); + assert!( + api_finding["matched_text"].as_str().is_some(), + "should have matched_text" + ); +} + +#[test] +fn finding_includes_algorithm_details() { + let findings = run_scan("go-api-server", "go"); + let aes_finding = findings + .iter() + .find(|f| f["algorithm"]["name"] == "AES-GCM") + .expect("should find AES-GCM"); + + let algo = &aes_finding["algorithm"]; + assert_eq!(algo["algorithm_family"], "AES"); + assert_eq!(algo["primitive"], "ae"); + assert_eq!(algo["fips_status"], "approved"); + assert!(algo["oid"].as_str().is_some(), "should include OID"); + // AES-GCM without explicit key size does not have a known security level + assert!( + algo["classical_security_level"].is_null(), + "AES-GCM without key size should not guess security level" + ); + + let functions = algo["crypto_functions"].as_array().unwrap(); + assert!(!functions.is_empty(), "should list crypto functions"); +} diff --git a/spectrometer.cabal b/spectrometer.cabal index bd3b5d457..684107aab 100644 --- a/spectrometer.cabal +++ b/spectrometer.cabal @@ -19,6 +19,8 @@ extra-source-files: scripts/*.jar target/release/berkeleydb target/release/berkeleydb.exe + target/release/cryptoscan + target/release/cryptoscan.exe target/release/millhone target/release/millhone.exe vendor-bins/circe @@ -243,6 +245,9 @@ library App.Fossa.Container.Sources.Podman App.Fossa.Container.Sources.Registry App.Fossa.Container.Test + App.Fossa.CryptoScan.Analyze + App.Fossa.CryptoScan.FipsReport + App.Fossa.CryptoScan.Types App.Fossa.DebugDir App.Fossa.DependencyMetadata App.Fossa.DumpBinaries @@ -583,6 +588,7 @@ test-suite unit-tests App.Fossa.Configuration.TelemetryConfigSpec App.Fossa.Container.AnalyzeNativeSpec App.Fossa.Container.AnalyzeNativeUploadSpec + App.Fossa.CryptoScan.FipsReportSpec App.Fossa.FirstPartyScanSpec App.Fossa.InitSpec App.Fossa.LernieSpec diff --git a/src/App/Fossa/Analyze.hs b/src/App/Fossa/Analyze.hs index f2d0700c8..aa065da3e 100644 --- a/src/App/Fossa/Analyze.hs +++ b/src/App/Fossa/Analyze.hs @@ -66,6 +66,8 @@ import App.Fossa.Config.Analyze ( ) import App.Fossa.Config.Analyze qualified as Config import App.Fossa.Config.Common (DestinationMeta (..), destinationApiOpts, destinationMetadata) +import App.Fossa.CryptoScan.Analyze (analyzeCryptoScanCBOM, analyzeWithCryptoScan) +import App.Fossa.CryptoScan.FipsReport (renderFipsReport) import App.Fossa.Ficus.Analyze (analyzeWithFicus) import App.Fossa.Ficus.Types (FicusAnalysisResults (vendoredDependencyScanResults), FicusStrategy (FicusStrategySnippetScan, FicusStrategyVendetta), FicusVendoredDependencyScanResults (FicusVendoredDependencyScanResults)) import App.Fossa.FirstPartyScan (runFirstPartyScan) @@ -108,7 +110,7 @@ import Control.Carrier.TaskPool ( withTaskPool, ) import Control.Concurrent (getNumCapabilities) -import Control.Effect.Diagnostics (recover) +import Control.Effect.Diagnostics (fatalOnIOException, recover) import Control.Effect.Exception (Lift) import Control.Effect.FossaApiClient (FossaApiClient, getEndpointVersion) import Control.Effect.Git (Git) @@ -130,7 +132,7 @@ import Data.String.Conversion (decodeUtf8, toText) import Data.Text.Extra (showT) import Data.Traversable (for) import Diag.Diagnostic as DI -import Diag.Result (resultToMaybe) +import Diag.Result (Result (Failure, Success), resultToMaybe) import Discovery.Archive qualified as Archive import Discovery.Filters (AllFilters, MavenScopeFilters, applyFilters, filterIsVSIOnly, ignoredPaths, isDefaultNonProductionPath) import Discovery.Projects (withDiscoveredProjects) @@ -141,6 +143,7 @@ import Effect.Logger ( logDebug, logInfo, logStdout, + logWarn, renderIt, ) import Effect.ReadFS (ReadFS) @@ -331,6 +334,7 @@ analyze cfg = Diag.context "fossa-analyze" $ do withoutDefaultFilters = Config.withoutDefaultFilters cfg enableSnippetScan = Config.snippetScan cfg enableVendetta = Config.xVendetta cfg + enableCryptoScan = Config.xCryptoScan cfg manualDepsResult <- Diag.errorBoundaryIO . diagToDebug $ @@ -403,6 +407,32 @@ analyze cfg = Diag.context "fossa-analyze" $ do else Diag.context "custom-license & keyword search" . runStickyLogger SevInfo $ analyzeWithLernie basedir maybeApiOpts grepOptions $ Config.licenseScanPathFilters vendoredDepsOptions let lernieResults = join . resultToMaybe $ maybeLernieResults + maybeCryptoScanResults <- + Diag.errorBoundaryIO . diagToDebug $ + if not enableCryptoScan + then do + logInfo "Skipping crypto scanning (--x-crypto-scan not set)" + pure Nothing + else Diag.context "crypto-scan" . runStickyLogger SevInfo $ analyzeWithCryptoScan basedir + + -- Handle CBOM file output if requested (requires crypto scanning to be enabled) + case (enableCryptoScan, Config.cryptoCbomOutput cfg) of + (False, Just _) -> + logInfo "Skipping crypto CBOM output (--x-crypto-scan not set)" + (True, Just cbomPath) -> do + maybeCbomBytes <- + Diag.errorBoundaryIO . diagToDebug $ + Diag.context "crypto-cbom-output" . runStickyLogger SevInfo $ + analyzeCryptoScanCBOM basedir + traverse_ (Diag.flushLogs SevError SevDebug) [maybeCbomBytes] + case maybeCbomBytes of + Success _ (Just bytes) -> do + fatalOnIOException "Failed to write crypto CBOM file" . sendIO $ BL.writeFile cbomPath bytes + logInfo $ "CycloneDX 1.7 CBOM written to: " <> pretty cbomPath + Success _ Nothing -> logInfo "No crypto findings to write to CBOM file" + Failure _ _ -> logWarn "Crypto CBOM generation failed; see diagnostics above" + (_, Nothing) -> pure () + let -- This makes nice with additionalSourceUnits below, but throws out additional Result data. -- This is ok because 'resultToMaybe' would do that anyway. -- We'll use the original results to output warnings/errors below. @@ -425,6 +455,8 @@ analyze cfg = Diag.context "fossa-analyze" $ do traverse_ (Diag.flushLogs SevError SevDebug) [maybeLernieResults] -- Flush logs from ficus traverse_ (Diag.flushLogs SevError SevDebug) [maybeFicusResults] + -- Flush logs from crypto scan + traverse_ (Diag.flushLogs SevError SevDebug) [maybeCryptoScanResults] maybeFirstPartyScanResults <- Diag.errorBoundaryIO . diagToDebug $ @@ -494,10 +526,19 @@ analyze cfg = Diag.context "fossa-analyze" $ do $ analyzeForReachability projectScans let reachabilityUnits = onlyFoundUnits reachabilityUnitsResult - let analysisResult = AnalysisScanResult projectScans vsiResults binarySearchResults maybeFicusResults manualSrcUnits dynamicLinkedResults maybeLernieResults reachabilityUnitsResult + let analysisResult = AnalysisScanResult projectScans vsiResults binarySearchResults maybeFicusResults manualSrcUnits dynamicLinkedResults maybeLernieResults reachabilityUnitsResult maybeCryptoScanResults isDebugMode = isJust (Config.debugDir cfg) renderScanSummary isDebugMode maybeEndpointAppVersion analysisResult cfg + -- Render FIPS compliance report if requested + when (Config.cryptoFipsReport cfg) $ + case maybeCryptoScanResults of + Success _ (Just cryptoResults) -> do + logInfo "" + logInfo $ renderFipsReport cryptoResults + Success _ Nothing -> logInfo "No crypto findings for FIPS report" + Failure _ _ -> logWarn "Crypto scan failed; skipping FIPS report" + -- Need to check if vendored is empty as well, even if its a boolean that vendoredDeps exist let licenseSourceUnits = case (firstPartyScanResults, lernieResultsSourceUnit =<< lernieResults) of diff --git a/src/App/Fossa/Analyze/ScanSummary.hs b/src/App/Fossa/Analyze/ScanSummary.hs index 27a4f0933..b9743271a 100644 --- a/src/App/Fossa/Analyze/ScanSummary.hs +++ b/src/App/Fossa/Analyze/ScanSummary.hs @@ -18,6 +18,7 @@ import App.Fossa.Analyze.Types ( ) import App.Fossa.Config.Analyze (AnalysisTacticTypes (StaticOnly)) import App.Fossa.Config.Analyze qualified as Config +import App.Fossa.CryptoScan.Types (CryptoAlgorithm (..), CryptoFinding (..), CryptoScanResults (..), FipsStatus (..)) import App.Fossa.Lernie.Types (LernieMatch (..), LernieMatchData (..), LernieResults (..)) import App.Version (fullVersionDescription) import Control.Carrier.Lift @@ -190,7 +191,7 @@ renderDefaultSkippedTargetHelp = ] summarize :: Config.AnalyzeConfig -> Text -> AnalysisScanResult -> Maybe ([Doc AnsiStyle]) -summarize cfg endpointVersion (AnalysisScanResult dps vsi binary _ manualDeps dynamicLinkingDeps lernie _) = +summarize cfg endpointVersion (AnalysisScanResult dps vsi binary _ manualDeps dynamicLinkingDeps lernie _ crypto) = if (numProjects totalScanCount <= 0) then Nothing else @@ -214,6 +215,7 @@ summarize cfg endpointVersion (AnalysisScanResult dps vsi binary _ manualDeps dy <> summarizeSrcUnit "fossa-deps file analysis" (Just getManualVendorDepsIdentifier) manualDeps <> summarizeSrcUnit "Keyword Search" (Just getLernieIdentifier) (lernieResultsKeywordSearches <$$> lernie) <> summarizeSrcUnit "Custom-License Search" (Just getLernieIdentifier) (lernieResultsCustomLicenses <$$> lernie) + <> summarizeCryptoScan crypto <> [""] where vsiResults = summarizeSrcUnit "vsi analysis" (Just (join . map vsiSourceUnits)) vsi @@ -226,6 +228,7 @@ summarize cfg endpointVersion (AnalysisScanResult dps vsi binary _ manualDeps dy , srcUnitToScanCount manualDeps , srcUnitToScanCount dynamicLinkingDeps , srcUnitToScanCount lernie + , srcUnitToScanCount crypto ] -- This function relies on the fact that there is only ever one package in a vsi source unit dep graph. @@ -328,6 +331,32 @@ summarizeProjectScan (Scanned _ (Success wg pr)) = successColorCoded wg $ render summarizeProjectScan (SkippedDueToProvidedFilter dpi) = renderDiscoveredProjectIdentifier dpi <> skippedDueFilter summarizeProjectScan (SkippedDueToDefaultFilter dpi) = renderDiscoveredProjectIdentifier dpi <> skippedDueDefaultFilter +summarizeCryptoScan :: Result (Maybe CryptoScanResults) -> [Doc AnsiStyle] +summarizeCryptoScan (Success wg (Just (CryptoScanResults findings))) = + let header = successColorCoded wg $ listSymbol <> "Crypto Scan" <> renderSucceeded wg + in case findings of + [] -> [header <> " (no findings)"] + _ -> + [header] + <> itemize (" " <> listSymbol) renderCryptoFinding findings +summarizeCryptoScan (Failure _ _) = [failColorCoded $ annotate bold $ listSymbol <> "Crypto Scan" <> renderFailed] +summarizeCryptoScan _ = [] + +renderCryptoFinding :: CryptoFinding -> Doc AnsiStyle +renderCryptoFinding finding = + pretty (cryptoAlgorithmName $ cryptoFindingAlgorithm finding) + <> " [" + <> fipsStatusDoc (cryptoAlgorithmFipsStatus $ cryptoFindingAlgorithm finding) + <> "] - " + <> pretty (cryptoFindingFilePath finding) + <> ":" + <> viaShow (cryptoFindingLineNumber finding) + where + fipsStatusDoc :: FipsStatus -> Doc AnsiStyle + fipsStatusDoc FipsApproved = annotate (color Green) "FIPS Approved" + fipsStatusDoc FipsDeprecated = annotate (color Yellow) "FIPS Deprecated" + fipsStatusDoc FipsNotApproved = annotate (color Red) "Not FIPS Approved" + ---------- Rendering Helpers logInfoVsep :: (Has Logger sig m) => [Doc AnsiStyle] -> m () @@ -389,7 +418,7 @@ countWarnings ws = isIgnoredErrGroup _ = False dumpResultLogsToTempFile :: (Has (Lift IO) sig m) => Config.AnalyzeConfig -> Text -> AnalysisScanResult -> m (Path Abs File) -dumpResultLogsToTempFile cfg endpointVersion (AnalysisScanResult projects vsi binary ficus manualDeps dynamicLinkingDeps lernieResults reachabilityAttempts) = do +dumpResultLogsToTempFile cfg endpointVersion (AnalysisScanResult projects vsi binary ficus manualDeps dynamicLinkingDeps lernieResults reachabilityAttempts cryptoResults) = do let doc = stripAnsiEscapeCodes . renderStrict @@ -404,6 +433,7 @@ dumpResultLogsToTempFile cfg endpointVersion (AnalysisScanResult projects vsi bi , renderSourceUnit "dynamic linked dependency analysis" dynamicLinkingDeps , renderSourceUnit "fossa-deps analysis" manualDeps , renderSourceUnit "Custom-license scan & Keyword Search" lernieResults + , renderSourceUnit "Crypto Scan" cryptoResults ] tmpDir <- sendIO getTempDir @@ -411,7 +441,7 @@ dumpResultLogsToTempFile cfg endpointVersion (AnalysisScanResult projects vsi bi pure (tmpDir scanSummaryFileName) where scanSummary :: [Doc AnsiStyle] - scanSummary = maybeToList (vsep <$> summarize cfg endpointVersion (AnalysisScanResult projects vsi binary ficus manualDeps dynamicLinkingDeps lernieResults reachabilityAttempts)) + scanSummary = maybeToList (vsep <$> summarize cfg endpointVersion (AnalysisScanResult projects vsi binary ficus manualDeps dynamicLinkingDeps lernieResults reachabilityAttempts cryptoResults)) renderSourceUnit :: Doc AnsiStyle -> Result (Maybe a) -> Maybe (Doc AnsiStyle) renderSourceUnit header (Failure ws eg) = Just $ renderFailure ws eg $ vsep $ summarizeSrcUnit header Nothing (Failure ws eg) diff --git a/src/App/Fossa/Analyze/Types.hs b/src/App/Fossa/Analyze/Types.hs index 4b2bb4845..deda6aed6 100644 --- a/src/App/Fossa/Analyze/Types.hs +++ b/src/App/Fossa/Analyze/Types.hs @@ -12,6 +12,7 @@ module App.Fossa.Analyze.Types ( import App.Fossa.Analyze.Project (ProjectResult) import App.Fossa.Config.Analyze (StrategyConfig) +import App.Fossa.CryptoScan.Types (CryptoScanResults) import App.Fossa.Ficus.Types (FicusAnalysisResults) import App.Fossa.Lernie.Types (LernieResults) import App.Fossa.Reachability.Types (SourceUnitReachability (..)) @@ -86,6 +87,7 @@ data AnalysisScanResult = AnalysisScanResult , dynamicLinkingResult :: Result (Maybe SourceUnit) , lernieResult :: Result (Maybe LernieResults) , reachabilityResult :: [SourceUnitReachabilityAttempt] + , cryptoScanResult :: Result (Maybe CryptoScanResults) } data SourceUnitReachabilityAttempt diff --git a/src/App/Fossa/Config/Analyze.hs b/src/App/Fossa/Config/Analyze.hs index e946ab04c..369127dc3 100644 --- a/src/App/Fossa/Config/Analyze.hs +++ b/src/App/Fossa/Config/Analyze.hs @@ -96,7 +96,7 @@ import Control.Monad (void, when) import Data.Aeson (ToJSON (toEncoding), defaultOptions, genericToEncoding) import Data.Flag (Flag, flagOpt, fromFlag) import Data.Map qualified as Map -import Data.Maybe (catMaybes) +import Data.Maybe (catMaybes, isJust) import Data.Monoid.Extra (isMempty) import Data.Set (Set) import Data.Set qualified as Set @@ -253,6 +253,9 @@ data AnalyzeCliOpts = AnalyzeCliOpts , analyzeExperimentalSnippetScan :: Flag ExperimentalSnippetScan , analyzeSnippetScan :: Flag SnippetScan , analyzeVendetta :: Bool + , analyzeCryptoScan :: Bool + , analyzeCryptoCbomOutput :: Maybe FilePath + , analyzeCryptoFipsReport :: Bool } deriving (Eq, Ord, Show) @@ -294,6 +297,9 @@ data AnalyzeConfig = AnalyzeConfig , snippetScan :: Bool , debugDir :: Maybe FilePath , xVendetta :: Bool + , xCryptoScan :: Bool + , cryptoCbomOutput :: Maybe FilePath + , cryptoFipsReport :: Bool } deriving (Eq, Ord, Show, Generic) @@ -374,6 +380,9 @@ cliParser = <*> flagOpt ExperimentalSnippetScan (applyFossaStyle <> long "x-snippet-scan" <> hidden) <*> flagOpt SnippetScan (applyFossaStyle <> long "snippet-scan" <> stringToHelpDoc "Enable snippet scanning to identify open source code snippets using fingerprinting.") <*> switch (applyFossaStyle <> long "x-vendetta" <> stringToHelpDoc "Experimental flag to enable vendored dependency scanning to identify open source components using file hashing.") + <*> switch (applyFossaStyle <> long "x-crypto-scan" <> stringToHelpDoc "Experimental flag to enable cryptographic algorithm detection and FIPS compliance assessment.") + <*> optional (strOption (applyFossaStyle <> long "crypto-cbom-output" <> metavar "FILE" <> stringToHelpDoc "Write CycloneDX 1.7 CBOM to FILE (implies --x-crypto-scan)")) + <*> switch (applyFossaStyle <> long "crypto-fips-report" <> stringToHelpDoc "Print detailed FIPS compliance report (implies --x-crypto-scan)") where fossaDepsFileHelp :: Maybe (Doc AnsiStyle) fossaDepsFileHelp = @@ -628,6 +637,9 @@ mergeStandardOpts maybeDebugDir maybeConfig envvars cliOpts@AnalyzeCliOpts{..} = <*> pure snippetScanEnabled <*> pure maybeDebugDir <*> pure analyzeVendetta + <*> pure (analyzeCryptoScan || analyzeCryptoFipsReport || isJust analyzeCryptoCbomOutput) + <*> pure analyzeCryptoCbomOutput + <*> pure analyzeCryptoFipsReport collectMavenScopeFilters :: (Has Diagnostics sig m) => diff --git a/src/App/Fossa/CryptoScan/Analyze.hs b/src/App/Fossa/CryptoScan/Analyze.hs new file mode 100644 index 000000000..cb7689147 --- /dev/null +++ b/src/App/Fossa/CryptoScan/Analyze.hs @@ -0,0 +1,66 @@ +module App.Fossa.CryptoScan.Analyze ( + analyzeWithCryptoScan, + analyzeCryptoScanCBOM, +) where + +import App.Fossa.CryptoScan.Types (CryptoScanResults (..)) +import App.Fossa.EmbeddedBinary (BinaryPaths, toPath, withCryptoScanBinary) +import Control.Carrier.Diagnostics (Diagnostics, fatalText) +import Control.Effect.Lift (Has, Lift) +import Data.Aeson qualified as Aeson +import Data.ByteString.Lazy qualified as BL +import Data.String.Conversion (ToText (toText)) +import Data.Text (Text) +import Effect.Exec (AllowErr (Never), Command (..), Exec, execThrow) +import Effect.Logger (Logger, logDebug, pretty) +import Path (Abs, Dir, Path) + +-- | Run the cryptoscan binary on the given directory and return parsed results. +analyzeWithCryptoScan :: + ( Has Diagnostics sig m + , Has (Lift IO) sig m + , Has Exec sig m + , Has Logger sig m + ) => + Path Abs Dir -> + m (Maybe CryptoScanResults) +analyzeWithCryptoScan rootDir = withCryptoScanBinary $ \bin -> do + logDebug "Running cryptoscan binary" + result <- execThrow rootDir (cryptoScanCommand bin rootDir) + case Aeson.eitherDecode result of + Left err -> do + fatalText $ "Failed to parse cryptoscan output: " <> toText err + Right findings -> do + logDebug $ "Cryptoscan completed: " <> pretty (show (length (cryptoFindings findings))) <> " findings" + pure $ Just findings + +-- | Run the cryptoscan binary with CycloneDX output format and return raw JSON bytes. +-- The Rust binary produces a complete CycloneDX 1.7 BOM, so no Haskell-side conversion is needed. +analyzeCryptoScanCBOM :: + ( Has Diagnostics sig m + , Has (Lift IO) sig m + , Has Exec sig m + , Has Logger sig m + ) => + Path Abs Dir -> + m (Maybe BL.ByteString) +analyzeCryptoScanCBOM rootDir = withCryptoScanBinary $ \bin -> do + logDebug "Running cryptoscan binary (CycloneDX output)" + result <- execThrow rootDir (cryptoScanCycloneDxCommand bin rootDir) + logDebug "Cryptoscan CycloneDX output generated" + pure $ Just result + +cryptoScanCommand :: BinaryPaths -> Path Abs Dir -> Command +cryptoScanCommand = mkCryptoScanCommand "json" + +cryptoScanCycloneDxCommand :: BinaryPaths -> Path Abs Dir -> Command +cryptoScanCycloneDxCommand = mkCryptoScanCommand "cyclonedx" + +mkCryptoScanCommand :: Text -> BinaryPaths -> Path Abs Dir -> Command +mkCryptoScanCommand format bin rootDir = + Command + { cmdName = toText $ toPath bin + , cmdArgs = ["--path", toText rootDir, "--ecosystem", "auto", "--format", format] + , cmdAllowErr = Never + , cmdEnvVars = mempty + } diff --git a/src/App/Fossa/CryptoScan/FipsReport.hs b/src/App/Fossa/CryptoScan/FipsReport.hs new file mode 100644 index 000000000..937979b30 --- /dev/null +++ b/src/App/Fossa/CryptoScan/FipsReport.hs @@ -0,0 +1,302 @@ +{-# LANGUAGE RecordWildCards #-} + +module App.Fossa.CryptoScan.FipsReport ( + renderFipsReport, + FipsReportStats (..), + computeFipsStats, +) where + +import App.Fossa.CryptoScan.Types ( + CryptoAlgorithm (..), + CryptoFinding (..), + CryptoPrimitive (..), + CryptoScanResults (..), + FipsStatus (..), + ) +import Data.List (foldl') +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +import Data.String.Conversion (toString) +import Data.Text (Text) +import Data.Text qualified as Text +import Prettyprinter ( + Doc, + Pretty (pretty), + annotate, + hardline, + indent, + vcat, + vsep, + ) +import Prettyprinter.Render.Terminal ( + AnsiStyle, + Color (Green, Red, Yellow), + bold, + color, + ) + +data FipsReportStats = FipsReportStats + { totalAlgorithms :: Int + , approvedCount :: Int + , deprecatedCount :: Int + , notApprovedCount :: Int + } + deriving (Show, Eq) + +compliancePercentage :: FipsReportStats -> Int +compliancePercentage FipsReportStats{..} = + if totalAlgorithms == 0 + then 100 + else (approvedCount * 100) `div` totalAlgorithms + +-- | Dedupe key for algorithm variants — includes parameter set so that +-- e.g. RSA-2048 and RSA-1024 are counted separately. +dedupeKey :: CryptoFinding -> (Text, Maybe Text) +dedupeKey finding = + let algo = cryptoFindingAlgorithm finding + in (Text.toCaseFold (cryptoAlgorithmName algo), cryptoAlgorithmParameterSet algo) + +-- | Deduplicate findings by (name, parameterSet), keeping the first occurrence. +-- Uses a Map for O(n log n) rather than nubBy's O(n^2). +deduplicateFindings :: [CryptoFinding] -> [CryptoFinding] +deduplicateFindings = Map.elems . foldl' insertFirst Map.empty + where + insertFirst acc finding = + let key = dedupeKey finding + in if Map.member key acc then acc else Map.insert key finding acc + +computeFipsStats :: CryptoScanResults -> FipsReportStats +computeFipsStats (CryptoScanResults findings) = + computeFipsStatsFromFindings (deduplicateFindings findings) + +-- | Compute FIPS stats from already-deduplicated findings. +computeFipsStatsFromFindings :: [CryptoFinding] -> FipsReportStats +computeFipsStatsFromFindings uniqueAlgos = + let statuses = map (cryptoAlgorithmFipsStatus . cryptoFindingAlgorithm) uniqueAlgos + in FipsReportStats + { totalAlgorithms = length uniqueAlgos + , approvedCount = length $ filter (== FipsApproved) statuses + , deprecatedCount = length $ filter (== FipsDeprecated) statuses + , notApprovedCount = length $ filter (== FipsNotApproved) statuses + } + +-- | Render a comprehensive FIPS compliance report from crypto scan results. +renderFipsReport :: CryptoScanResults -> Doc AnsiStyle +renderFipsReport (CryptoScanResults findings) = + let uniqueFindings = deduplicateFindings findings + stats = computeFipsStatsFromFindings uniqueFindings + categorized = categorizeFindings uniqueFindings + in vsep + [ annotate bold "FIPS Compliance Report" + , annotate bold "=====================" + , "" + , renderSummary stats + , "" + , renderCategoryBreakdown categorized + , "" + , renderRemediationTable uniqueFindings + , "" + , renderKeySizeWarnings uniqueFindings + ] + +renderSummary :: FipsReportStats -> Doc AnsiStyle +renderSummary stats@FipsReportStats{..} = + vcat + [ annotate bold "Summary" + , annotate bold "-------" + , "Total unique algorithms detected: " <> pretty totalAlgorithms + , annotate (color Green) $ " FIPS Approved: " <> pretty approvedCount + , annotate (color Yellow) $ " FIPS Deprecated: " <> pretty deprecatedCount + , annotate (color Red) $ " Not FIPS Approved: " <> pretty notApprovedCount + , "" + , "Overall compliance: " <> coloredPercentage (compliancePercentage stats) + ] + +coloredPercentage :: Int -> Doc AnsiStyle +coloredPercentage pct + | pct >= 100 = annotate (color Green) $ pretty pct <> "%" + | pct >= 80 = annotate (color Yellow) $ pretty pct <> "%" + | otherwise = annotate (color Red) $ pretty pct <> "%" + +-- Category types for grouping +data CryptoCategory + = CatSymmetricEncryption + | CatHashFunctions + | CatMacs + | CatAsymmetricSignatures + | CatKeyExchange + | CatKdfs + | CatOther + deriving (Eq, Ord, Show) + +categoryName :: CryptoCategory -> Text +categoryName CatSymmetricEncryption = "Symmetric Encryption" +categoryName CatHashFunctions = "Hash Functions" +categoryName CatMacs = "MACs" +categoryName CatAsymmetricSignatures = "Asymmetric / Signatures" +categoryName CatKeyExchange = "Key Exchange" +categoryName CatKdfs = "KDFs / Password Hashing" +categoryName CatOther = "Other" + +classifyPrimitive :: CryptoPrimitive -> CryptoCategory +classifyPrimitive PrimitiveAe = CatSymmetricEncryption +classifyPrimitive PrimitiveBlockCipher = CatSymmetricEncryption +classifyPrimitive PrimitiveStreamCipher = CatSymmetricEncryption +classifyPrimitive PrimitiveHash = CatHashFunctions +classifyPrimitive PrimitiveXof = CatHashFunctions +classifyPrimitive PrimitiveMac = CatMacs +classifyPrimitive PrimitiveSignature = CatAsymmetricSignatures +classifyPrimitive PrimitivePke = CatAsymmetricSignatures +classifyPrimitive PrimitiveKem = CatKeyExchange +classifyPrimitive PrimitiveKeyAgree = CatKeyExchange +classifyPrimitive PrimitiveKeyWrap = CatSymmetricEncryption +classifyPrimitive PrimitiveKdf = CatKdfs +classifyPrimitive PrimitiveDrbg = CatOther +classifyPrimitive PrimitiveCombiner = CatOther +classifyPrimitive PrimitiveOther = CatOther +classifyPrimitive PrimitiveUnknown = CatOther + +categorizeFindings :: [CryptoFinding] -> Map CryptoCategory [CryptoFinding] +categorizeFindings = foldr categorize Map.empty + where + categorize finding acc = + let cat = classifyPrimitive (cryptoAlgorithmPrimitive $ cryptoFindingAlgorithm finding) + in Map.insertWith (++) cat [finding] acc + +renderCategoryBreakdown :: Map CryptoCategory [CryptoFinding] -> Doc AnsiStyle +renderCategoryBreakdown categorized = + vcat $ + [annotate bold "Per-Category Breakdown", annotate bold "----------------------"] + ++ concatMap renderCategory (Map.toAscList categorized) + +renderCategory :: (CryptoCategory, [CryptoFinding]) -> [Doc AnsiStyle] +renderCategory (cat, fs) = + [ "" + , annotate bold $ pretty (categoryName cat) <> ":" + ] + ++ map (indent 2 . renderAlgoStatus) fs + +renderAlgoStatus :: CryptoFinding -> Doc AnsiStyle +renderAlgoStatus finding = + let algo = cryptoFindingAlgorithm finding + statusDoc = case cryptoAlgorithmFipsStatus algo of + FipsApproved -> annotate (color Green) "Approved" + FipsDeprecated -> annotate (color Yellow) "Deprecated" + FipsNotApproved -> annotate (color Red) "Not Approved" + in "- " + <> pretty (cryptoAlgorithmName algo) + <> " [" + <> statusDoc + <> "]" + <> maybe mempty (\ps -> " (" <> pretty ps <> "-bit)") (cryptoAlgorithmParameterSet algo) + +renderRemediationTable :: [CryptoFinding] -> Doc AnsiStyle +renderRemediationTable fs = + let nonFips = filter (\f -> cryptoAlgorithmFipsStatus (cryptoFindingAlgorithm f) == FipsNotApproved) fs + in if null nonFips + then annotate (color Green) "No remediation needed - all algorithms are FIPS approved or deprecated." + else + vcat $ + [ annotate bold "Remediation Recommendations" + , annotate bold "---------------------------" + , "" + , padRight 35 "Non-FIPS Algorithm" <> pretty ("Recommended FIPS Alternative" :: Text) + , padRight 35 "------------------" <> pretty ("---------------------------" :: Text) + ] + ++ map renderRemediation nonFips + +renderRemediation :: CryptoFinding -> Doc AnsiStyle +renderRemediation finding = + let name = cryptoAlgorithmName (cryptoFindingAlgorithm finding) + alternative = suggestAlternative name + in padRight 35 name <> pretty alternative + +padRight :: Int -> Text -> Doc ann +padRight n t = pretty t <> pretty (Text.replicate (max 0 (n - Text.length t)) " ") + +suggestAlternative :: Text -> Text +suggestAlternative name = + let lower = Text.toLower name + check = any (\p -> Text.toLower p == lower) + in if check ["chacha20", "chacha20-poly1305", "xchacha20"] + then "AES-256-GCM" + else + if check ["blake2", "blake2b", "blake2s", "blake3"] + then "SHA-256 / SHA-3" + else + if check ["md5"] || check ["md4"] + then "SHA-256" + else + if check ["rc4", "rc2", "blowfish", "des", "3des", "3des-encrypt"] + then "AES-256" + else + if check ["bcrypt", "argon2", "argon2i", "argon2id", "scrypt"] + then "PBKDF2" + else + if check ["x25519", "x448"] + then "ECDH P-256 / P-384" + else + if check ["curve25519"] + then "ECDH NIST curves" + else + if check ["poly1305"] + then "HMAC / CMAC" + else + if check ["siphash"] + then "HMAC" + else + if check ["whirlpool", "ripemd", "ripemd-160"] + then "SHA-256" + else + if check ["cast5", "idea", "camellia", "seed", "aria"] + || check ["twofish", "serpent", "threefish"] + then "AES-256" + else "Review FIPS 140-3 approved algorithm list" + +renderKeySizeWarnings :: [CryptoFinding] -> Doc AnsiStyle +renderKeySizeWarnings fs = + let warnings = concatMap keySizeWarning fs + in if null warnings + then mempty + else + vcat $ + [ annotate bold "Key Size Warnings" + , annotate bold "-----------------" + ] + ++ warnings + ++ [hardline] + +keySizeWarning :: CryptoFinding -> [Doc AnsiStyle] +keySizeWarning finding = + let algo = cryptoFindingAlgorithm finding + algoName = Text.toLower $ cryptoAlgorithmName algo + paramSet = cryptoAlgorithmParameterSet algo + prim = cryptoAlgorithmPrimitive algo + isHash = prim == PrimitiveHash || prim == PrimitiveXof + in catWarnings algoName paramSet isHash + where + catWarnings :: Text -> Maybe Text -> Bool -> [Doc AnsiStyle] + catWarnings n ps isHashPrim + | "rsa" `Text.isInfixOf` n = rsaWarning ps + | isHashPrim && ("sha-1" `Text.isInfixOf` n || "sha1" `Text.isInfixOf` n) = + [annotate (color Yellow) "- SHA-1: Deprecated, fully disallowed after 2030-12-31"] + | "aes-128" `Text.isInfixOf` n || (n == "aes" && ps == Just "128") = + [annotate (color Yellow) "- AES-128: Approved but AES-256 recommended for higher security margin"] + | isHashPrim && "sha-224" `Text.isInfixOf` n = + [annotate (color Yellow) "- SHA-224: Deprecated by 2030"] + | "3des" `Text.isInfixOf` n || "triple-des" `Text.isInfixOf` n = + [annotate (color Yellow) "- 3DES: Legacy decryption only since Jan 2024"] + | otherwise = [] + + rsaWarning :: Maybe Text -> [Doc AnsiStyle] + rsaWarning Nothing = [annotate (color Yellow) "- RSA: Key size not detected, ensure >= 2048-bit"] + rsaWarning (Just ps) = + case reads (toString ps) :: [(Int, String)] of + [(n, "")] -> + if n < 2048 + then [annotate (color Red) $ "- RSA-" <> pretty ps <> ": Below FIPS minimum (2048-bit required)"] + else + if n < 3072 + then [annotate (color Yellow) $ "- RSA-" <> pretty ps <> ": Approved but RSA-3072+ recommended for 128-bit security"] + else [] + _ -> [annotate (color Yellow) $ "- RSA-" <> pretty ps <> ": Key size format not recognized, ensure >= 2048-bit"] diff --git a/src/App/Fossa/CryptoScan/Types.hs b/src/App/Fossa/CryptoScan/Types.hs new file mode 100644 index 000000000..49964a8e3 --- /dev/null +++ b/src/App/Fossa/CryptoScan/Types.hs @@ -0,0 +1,241 @@ +{-# LANGUAGE RecordWildCards #-} + +module App.Fossa.CryptoScan.Types ( + CryptoScanResults (..), + CryptoFinding (..), + CryptoAlgorithm (..), + FipsStatus (..), + DetectionMethod (..), + Confidence (..), + CryptoPrimitive (..), +) where + +import Data.Aeson ( + FromJSON (parseJSON), + ToJSON (toJSON), + Value (String), + (.:), + (.:?), + (.=), + ) +import Data.Aeson qualified as Aeson +import Data.Text (Text) + +-- | Wrapper for the list of crypto findings returned by the cryptoscan binary. +newtype CryptoScanResults = CryptoScanResults + { cryptoFindings :: [CryptoFinding] + } + deriving (Show, Eq, Ord) + +instance FromJSON CryptoScanResults where + parseJSON v = CryptoScanResults <$> parseJSON v + +instance ToJSON CryptoScanResults where + toJSON (CryptoScanResults findings) = toJSON findings + +-- | A single crypto finding: an algorithm detected at a specific location. +data CryptoFinding = CryptoFinding + { cryptoFindingAlgorithm :: CryptoAlgorithm + , cryptoFindingFilePath :: Text + , cryptoFindingLineNumber :: Int + , cryptoFindingMatchedText :: Text + , cryptoFindingDetectionMethod :: DetectionMethod + , cryptoFindingEcosystem :: Text + , cryptoFindingProvidingLibrary :: Maybe Text + , cryptoFindingConfidence :: Confidence + } + deriving (Show, Eq, Ord) + +instance FromJSON CryptoFinding where + parseJSON = Aeson.withObject "CryptoFinding" $ \o -> + CryptoFinding + <$> o .: "algorithm" + <*> o .: "file_path" + <*> o .: "line_number" + <*> o .: "matched_text" + <*> o .: "detection_method" + <*> o .: "ecosystem" + <*> o .:? "providing_library" + <*> o .: "confidence" + +instance ToJSON CryptoFinding where + toJSON CryptoFinding{..} = + Aeson.object + [ "algorithm" .= cryptoFindingAlgorithm + , "file_path" .= cryptoFindingFilePath + , "line_number" .= cryptoFindingLineNumber + , "matched_text" .= cryptoFindingMatchedText + , "detection_method" .= cryptoFindingDetectionMethod + , "ecosystem" .= cryptoFindingEcosystem + , "providing_library" .= cryptoFindingProvidingLibrary + , "confidence" .= cryptoFindingConfidence + ] + +-- | A detected cryptographic algorithm with all its metadata. +data CryptoAlgorithm = CryptoAlgorithm + { cryptoAlgorithmName :: Text + , cryptoAlgorithmFamily :: Text + , cryptoAlgorithmPrimitive :: CryptoPrimitive + , cryptoAlgorithmParameterSet :: Maybe Text + , cryptoAlgorithmEllipticCurve :: Maybe Text + , cryptoAlgorithmMode :: Maybe Text + , cryptoAlgorithmOid :: Maybe Text + , cryptoAlgorithmClassicalSecurityLevel :: Maybe Int + , cryptoAlgorithmNistQuantumSecurityLevel :: Int + , cryptoAlgorithmFipsStatus :: FipsStatus + , cryptoAlgorithmCryptoFunctions :: [Text] + } + deriving (Show, Eq, Ord) + +instance FromJSON CryptoAlgorithm where + parseJSON = Aeson.withObject "CryptoAlgorithm" $ \o -> + CryptoAlgorithm + <$> o .: "name" + <*> o .: "algorithm_family" + <*> o .: "primitive" + <*> o .:? "parameter_set" + <*> o .:? "elliptic_curve" + <*> o .:? "mode" + <*> o .:? "oid" + <*> o .:? "classical_security_level" + <*> o .: "nist_quantum_security_level" + <*> o .: "fips_status" + <*> o .: "crypto_functions" + +instance ToJSON CryptoAlgorithm where + toJSON CryptoAlgorithm{..} = + Aeson.object + [ "name" .= cryptoAlgorithmName + , "algorithm_family" .= cryptoAlgorithmFamily + , "primitive" .= cryptoAlgorithmPrimitive + , "parameter_set" .= cryptoAlgorithmParameterSet + , "elliptic_curve" .= cryptoAlgorithmEllipticCurve + , "mode" .= cryptoAlgorithmMode + , "oid" .= cryptoAlgorithmOid + , "classical_security_level" .= cryptoAlgorithmClassicalSecurityLevel + , "nist_quantum_security_level" .= cryptoAlgorithmNistQuantumSecurityLevel + , "fips_status" .= cryptoAlgorithmFipsStatus + , "crypto_functions" .= cryptoAlgorithmCryptoFunctions + ] + +-- | FIPS compliance status for a cryptographic algorithm. +-- Serialized as kebab-case to match Rust serde output. +data FipsStatus + = FipsApproved + | FipsDeprecated + | FipsNotApproved + deriving (Show, Eq, Ord) + +instance FromJSON FipsStatus where + parseJSON = Aeson.withText "FipsStatus" $ \case + "approved" -> pure FipsApproved + "deprecated" -> pure FipsDeprecated + "not-approved" -> pure FipsNotApproved + other -> fail $ "Unknown FipsStatus: " <> show other + +instance ToJSON FipsStatus where + toJSON FipsApproved = String "approved" + toJSON FipsDeprecated = String "deprecated" + toJSON FipsNotApproved = String "not-approved" + +-- | Detection method used to identify the crypto usage. +data DetectionMethod + = DependencyManifest + | ImportStatement + | ApiCall + | ConfigFile + | StringLiteral + deriving (Show, Eq, Ord) + +instance FromJSON DetectionMethod where + parseJSON = Aeson.withText "DetectionMethod" $ \case + "dependency-manifest" -> pure DependencyManifest + "import-statement" -> pure ImportStatement + "api-call" -> pure ApiCall + "config-file" -> pure ConfigFile + "string-literal" -> pure StringLiteral + other -> fail $ "Unknown DetectionMethod: " <> show other + +instance ToJSON DetectionMethod where + toJSON DependencyManifest = String "dependency-manifest" + toJSON ImportStatement = String "import-statement" + toJSON ApiCall = String "api-call" + toJSON ConfigFile = String "config-file" + toJSON StringLiteral = String "string-literal" + +-- | Confidence level of the detection. +data Confidence + = ConfidenceHigh + | ConfidenceMedium + | ConfidenceLow + deriving (Show, Eq, Ord) + +instance FromJSON Confidence where + parseJSON = Aeson.withText "Confidence" $ \case + "high" -> pure ConfidenceHigh + "medium" -> pure ConfidenceMedium + "low" -> pure ConfidenceLow + other -> fail $ "Unknown Confidence: " <> show other + +instance ToJSON Confidence where + toJSON ConfidenceHigh = String "high" + toJSON ConfidenceMedium = String "medium" + toJSON ConfidenceLow = String "low" + +-- | Cryptographic primitive type (maps to CycloneDX 1.7 enum). +data CryptoPrimitive + = PrimitiveAe + | PrimitiveBlockCipher + | PrimitiveStreamCipher + | PrimitiveHash + | PrimitiveMac + | PrimitiveSignature + | PrimitivePke + | PrimitiveKem + | PrimitiveKeyAgree + | PrimitiveKeyWrap + | PrimitiveKdf + | PrimitiveXof + | PrimitiveDrbg + | PrimitiveCombiner + | PrimitiveOther + | PrimitiveUnknown + deriving (Show, Eq, Ord) + +instance FromJSON CryptoPrimitive where + parseJSON = Aeson.withText "CryptoPrimitive" $ \case + "ae" -> pure PrimitiveAe + "block-cipher" -> pure PrimitiveBlockCipher + "stream-cipher" -> pure PrimitiveStreamCipher + "hash" -> pure PrimitiveHash + "mac" -> pure PrimitiveMac + "signature" -> pure PrimitiveSignature + "pke" -> pure PrimitivePke + "kem" -> pure PrimitiveKem + "key-agree" -> pure PrimitiveKeyAgree + "key-wrap" -> pure PrimitiveKeyWrap + "kdf" -> pure PrimitiveKdf + "xof" -> pure PrimitiveXof + "drbg" -> pure PrimitiveDrbg + "combiner" -> pure PrimitiveCombiner + "other" -> pure PrimitiveOther + "unknown" -> pure PrimitiveUnknown + other -> fail $ "Unknown CryptoPrimitive: " <> show other + +instance ToJSON CryptoPrimitive where + toJSON PrimitiveAe = String "ae" + toJSON PrimitiveBlockCipher = String "block-cipher" + toJSON PrimitiveStreamCipher = String "stream-cipher" + toJSON PrimitiveHash = String "hash" + toJSON PrimitiveMac = String "mac" + toJSON PrimitiveSignature = String "signature" + toJSON PrimitivePke = String "pke" + toJSON PrimitiveKem = String "kem" + toJSON PrimitiveKeyAgree = String "key-agree" + toJSON PrimitiveKeyWrap = String "key-wrap" + toJSON PrimitiveKdf = String "kdf" + toJSON PrimitiveXof = String "xof" + toJSON PrimitiveDrbg = String "drbg" + toJSON PrimitiveCombiner = String "combiner" + toJSON PrimitiveOther = String "other" + toJSON PrimitiveUnknown = String "unknown" diff --git a/src/App/Fossa/EmbeddedBinary.hs b/src/App/Fossa/EmbeddedBinary.hs index e609be919..7d1258534 100644 --- a/src/App/Fossa/EmbeddedBinary.hs +++ b/src/App/Fossa/EmbeddedBinary.hs @@ -4,6 +4,7 @@ module App.Fossa.EmbeddedBinary ( BinaryPaths, + CryptoScan, Ficus, Lernie, ThemisIndex, @@ -11,6 +12,7 @@ module App.Fossa.EmbeddedBinary ( toPath, withThemisAndIndex, withBerkeleyBinary, + withCryptoScanBinary, withFicusBinary, withLernieBinary, withMillhoneBinary, @@ -59,6 +61,7 @@ data PackagedBinary = Themis | ThemisIndex | BerkeleyDB + | CryptoScan | Ficus | Lernie | Millhone @@ -78,6 +81,8 @@ data ThemisBinary data ThemisIndex +data CryptoScan + data Lernie data Ficus @@ -118,6 +123,9 @@ extractThemisFiles = do withBerkeleyBinary :: (Has (Lift IO) sig m) => (BinaryPaths -> m c) -> m c withBerkeleyBinary = withEmbeddedBinary BerkeleyDB +withCryptoScanBinary :: (Has (Lift IO) sig m) => (BinaryPaths -> m c) -> m c +withCryptoScanBinary = withEmbeddedBinary CryptoScan + withLernieBinary :: (Has (Lift IO) sig m) => (BinaryPaths -> m c) -> m c withLernieBinary = withEmbeddedBinary Lernie @@ -156,6 +164,7 @@ writeBinary dest bin = do Themis -> embeddedBinaryThemis ThemisIndex -> embeddedBinaryThemisIndex BerkeleyDB -> embeddedBinaryBerkeleyDB + CryptoScan -> embeddedBinaryCryptoScan Ficus -> embeddedBinaryFicus Lernie -> embeddedBinaryLernie Millhone -> embeddedBinaryMillhone @@ -172,6 +181,7 @@ extractedPath bin = case bin of Themis -> $(mkRelFile "themis-cli") ThemisIndex -> $(mkRelFile "index.gob.xz") BerkeleyDB -> $(mkRelFile "berkeleydb-plugin") + CryptoScan -> $(mkRelFile "cryptoscan") Ficus -> $(mkRelFile "ficus") Lernie -> $(mkRelFile "lernie") Millhone -> $(mkRelFile "millhone") @@ -233,6 +243,15 @@ embeddedBinaryMillhone :: ByteString embeddedBinaryMillhone = $(embedFileIfExists "target/release/millhone") #endif +-- To build this, run `make build` or `cargo build --release`. +#ifdef mingw32_HOST_OS +embeddedBinaryCryptoScan :: ByteString +embeddedBinaryCryptoScan = $(embedFileIfExists "target/release/cryptoscan.exe") +#else +embeddedBinaryCryptoScan :: ByteString +embeddedBinaryCryptoScan = $(embedFileIfExists "target/release/cryptoscan") +#endif + -- To build this, run `make build` or `cargo build --release`. #ifdef mingw32_HOST_OS embeddedBinaryBerkeleyDB :: ByteString diff --git a/test-fixtures/csharp-service/CryptoService.cs b/test-fixtures/csharp-service/CryptoService.cs new file mode 100644 index 000000000..9bf49a194 --- /dev/null +++ b/test-fixtures/csharp-service/CryptoService.cs @@ -0,0 +1,96 @@ +using System; +using System.Security.Cryptography; +using System.Text; + +namespace CryptoService +{ + public class CryptoHelper + { + // --- AES encryption (FIPS Approved) --- + public static byte[] EncryptAes(byte[] key, byte[] plaintext) + { + using var aes = Aes.Create(); + aes.Key = key; + aes.GenerateIV(); + return aes.EncryptCbc(plaintext, aes.IV); + } + + // --- AES-GCM (FIPS Approved) --- + public static byte[] EncryptAesGcm(byte[] key, byte[] plaintext, byte[] nonce) + { + var ciphertext = new byte[plaintext.Length]; + var tag = new byte[16]; + using var gcm = new AesGcm(key, 16); + gcm.Encrypt(nonce, plaintext, ciphertext, tag); + return ciphertext; + } + + // --- 3DES (FIPS Deprecated) --- + public static byte[] EncryptTripleDes(byte[] key, byte[] plaintext) + { + using var tdes = TripleDES.Create(); + tdes.Key = key; + return tdes.EncryptCbc(plaintext, tdes.IV); + } + + // --- SHA-256 hash (FIPS Approved) --- + public static byte[] HashSha256(byte[] data) + { + using var sha = SHA256.Create(); + return sha.ComputeHash(data); + } + + // --- SHA-512 hash (FIPS Approved) --- + public static byte[] HashSha512(byte[] data) + { + using var sha = SHA512.Create(); + return sha.ComputeHash(data); + } + + // --- SHA-1 hash (FIPS Deprecated) --- + public static byte[] HashSha1(byte[] data) + { + using var sha = SHA1.Create(); + return sha.ComputeHash(data); + } + + // --- MD5 hash (NOT FIPS Approved) --- + public static byte[] HashMd5(byte[] data) + { + using var md5 = MD5.Create(); + return md5.ComputeHash(data); + } + + // --- HMAC-SHA256 (FIPS Approved) --- + public static byte[] HmacSha256(byte[] key, byte[] data) + { + using var hmac = new HMACSHA256(key); + return hmac.ComputeHash(data); + } + + // --- RSA key generation (FIPS Approved) --- + public static RSA GenerateRsaKey() + { + return RSA.Create(2048); + } + + // --- ECDSA (FIPS Approved) --- + public static ECDsa GenerateEcKey() + { + return ECDsa.Create(ECCurve.NamedCurves.nistP256); + } + + // --- ECDH (FIPS Approved) --- + public static ECDiffieHellman GenerateEcdhKey() + { + return ECDiffieHellman.Create(ECCurve.NamedCurves.nistP256); + } + + // --- PBKDF2 (FIPS Approved) --- + public static byte[] DeriveKeyPbkdf2(string password, byte[] salt) + { + using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 600000, HashAlgorithmName.SHA256); + return pbkdf2.GetBytes(32); + } + } +} diff --git a/test-fixtures/csharp-service/Service.csproj b/test-fixtures/csharp-service/Service.csproj new file mode 100644 index 000000000..efaf1da7e --- /dev/null +++ b/test-fixtures/csharp-service/Service.csproj @@ -0,0 +1,11 @@ + + + Exe + net8.0 + + + + + + + diff --git a/test-fixtures/elixir-phoenix-app/crypto.ex b/test-fixtures/elixir-phoenix-app/crypto.ex new file mode 100644 index 000000000..17525e6ab --- /dev/null +++ b/test-fixtures/elixir-phoenix-app/crypto.ex @@ -0,0 +1,90 @@ +defmodule PhoenixApp.Crypto do + @moduledoc """ + Cryptographic utilities for the Phoenix application. + """ + + # --- AES-256-GCM encryption (FIPS Approved) --- + def encrypt_aes_gcm(key, plaintext) do + iv = :crypto.strong_rand_bytes(12) + {ciphertext, tag} = :crypto.crypto_one_time(:aes_256_gcm, key, iv, plaintext) + {iv, ciphertext, tag} + end + + # --- AES-128-GCM encryption (FIPS Approved) --- + def encrypt_aes_128_gcm(key, plaintext) do + iv = :crypto.strong_rand_bytes(12) + {ciphertext, tag} = :crypto.crypto_one_time(:aes_128_gcm, key, iv, plaintext) + {iv, ciphertext, tag} + end + + # --- ChaCha20-Poly1305 (NOT FIPS Approved) --- + def encrypt_chacha(key, plaintext) do + iv = :crypto.strong_rand_bytes(12) + :crypto.crypto_one_time(:chacha20_poly1305, key, iv, plaintext) + end + + # --- 3DES (FIPS Deprecated) --- + def encrypt_3des(key, plaintext) do + iv = :crypto.strong_rand_bytes(8) + :crypto.block_encrypt(:des_ede3_cbc, key, iv, plaintext) + end + + # --- SHA-256 hash (FIPS Approved) --- + def hash_sha256(data) do + :crypto.hash(:sha256, data) + end + + # --- SHA-512 hash (FIPS Approved) --- + def hash_sha512(data) do + :crypto.hash(:sha512, data) + end + + # --- SHA-1 hash (FIPS Deprecated) --- + def hash_sha1(data) do + :crypto.hash(:sha, data) + end + + # --- MD5 hash (NOT FIPS Approved) --- + def hash_md5(data) do + :crypto.hash(:md5, data) + end + + # --- HMAC-SHA256 (FIPS Approved) --- + def hmac_sha256(key, data) do + :crypto.mac(:hmac, :sha256, key, data) + end + + # --- HMAC-SHA512 (FIPS Approved) --- + def hmac_sha512(key, data) do + :crypto.mac(:hmac, :sha512, key, data) + end + + # --- RSA key generation (FIPS Approved) --- + def generate_rsa_key do + :crypto.generate_key(:rsa, {2048, 65537}) + end + + # --- ECDH key generation (FIPS Approved) --- + def generate_ecdh_key do + :crypto.generate_key(:ecdh, :secp256r1) + end + + # --- bcrypt password hashing (NOT FIPS Approved) --- + def hash_password_bcrypt(password) do + Bcrypt.hash_pwd_salt(password) + end + + def verify_password_bcrypt(password, hash) do + Bcrypt.verify_pass(password, hash) + end + + # --- Argon2 password hashing (NOT FIPS Approved) --- + def hash_password_argon2(password) do + Argon2.hash_pwd_salt(password) + end + + # --- PBKDF2 (FIPS Approved) --- + def hash_password_pbkdf2(password) do + Pbkdf2.hash_pwd_salt(password) + end +end diff --git a/test-fixtures/elixir-phoenix-app/mix.exs b/test-fixtures/elixir-phoenix-app/mix.exs new file mode 100644 index 000000000..9184dc2d6 --- /dev/null +++ b/test-fixtures/elixir-phoenix-app/mix.exs @@ -0,0 +1,23 @@ +defmodule PhoenixApp.MixProject do + use Mix.Project + + def project do + [ + app: :phoenix_app, + version: "0.1.0", + elixir: "~> 1.15", + deps: deps() + ] + end + + defp deps do + [ + {:phoenix, "~> 1.7"}, + {:bcrypt_elixir, "~> 3.1"}, + {:argon2_elixir, "~> 4.0"}, + {:pbkdf2_elixir, "~> 2.2"}, + {:comeonin, "~> 5.4"}, + {:jason, "~> 1.4"} + ] + end +end diff --git a/test-fixtures/go-api-server/go.mod b/test-fixtures/go-api-server/go.mod new file mode 100644 index 000000000..9228c7a4f --- /dev/null +++ b/test-fixtures/go-api-server/go.mod @@ -0,0 +1,7 @@ +module github.com/example/api-server + +go 1.22 + +require ( + golang.org/x/crypto v0.28.0 +) diff --git a/test-fixtures/go-api-server/main.go b/test-fixtures/go-api-server/main.go new file mode 100644 index 000000000..b09b3be3a --- /dev/null +++ b/test-fixtures/go-api-server/main.go @@ -0,0 +1,137 @@ +package main + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/hmac" + "crypto/md5" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "fmt" + "io" + + "golang.org/x/crypto/argon2" + "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/blake2b" + "golang.org/x/crypto/chacha20poly1305" + "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/hkdf" + "golang.org/x/crypto/scrypt" +) + +// ─── AES-256-GCM (FIPS Approved) ───────────────────────────────────── +func EncryptAESGCM(key, plaintext, nonce []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + return aesGCM.Seal(nil, nonce, plaintext, nil), nil +} + +// ─── ChaCha20-Poly1305 (NOT FIPS Approved) ─────────────────────────── +func EncryptChaCha(key, plaintext, nonce []byte) ([]byte, error) { + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, err + } + return aead.Seal(nil, nonce, plaintext, nil), nil +} + +// ─── RSA (FIPS Approved) ───────────────────────────────────────────── +func GenerateRSAKey() (*rsa.PrivateKey, error) { + return rsa.GenerateKey(rand.Reader, 2048) +} + +// ─── ECDSA P-256 (FIPS Approved) ───────────────────────────────────── +func GenerateECDSAKey() (*ecdsa.PrivateKey, error) { + return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) +} + +// ─── ECDSA P-384 (FIPS Approved) ───────────────────────────────────── +func GenerateECDSAP384Key() (*ecdsa.PrivateKey, error) { + return ecdsa.GenerateKey(elliptic.P384(), rand.Reader) +} + +// ─── Ed25519 (FIPS Approved for signatures) ────────────────────────── +func GenerateEd25519Key() (ed25519.PublicKey, ed25519.PrivateKey, error) { + return ed25519.GenerateKey(rand.Reader) +} + +// ─── X25519 key exchange (NOT FIPS Approved) ───────────────────────── +func X25519KeyExchange(privateKey, peerPublic []byte) ([]byte, error) { + return curve25519.X25519(privateKey, peerPublic) +} + +// ─── Hash functions ────────────────────────────────────────────────── +func HashSHA256(data []byte) []byte { + h := sha256.New() + h.Write(data) + return h.Sum(nil) +} + +func HashSHA512(data []byte) []byte { + h := sha512.New() + h.Write(data) + return h.Sum(nil) +} + +func HashSHA1Legacy(data []byte) []byte { + h := sha1.New() + h.Write(data) + return h.Sum(nil) +} + +func HashMD5Legacy(data []byte) []byte { + h := md5.New() + h.Write(data) + return h.Sum(nil) +} + +func HashBLAKE2b(data []byte) []byte { + h, _ := blake2b.New256(nil) + h.Write(data) + return h.Sum(nil) +} + +// ─── HMAC (FIPS Approved) ──────────────────────────────────────────── +func ComputeHMAC(key, message []byte) []byte { + mac := hmac.New(sha256.New, key) + mac.Write(message) + return mac.Sum(nil) +} + +// ─── Password hashing ─────────────────────────────────────────────── +func HashPasswordBcrypt(password string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(hash), err +} + +func HashPasswordArgon2(password, salt []byte) []byte { + return argon2.IDKey(password, salt, 1, 64*1024, 4, 32) +} + +func HashPasswordScrypt(password, salt []byte) ([]byte, error) { + return scrypt.Key(password, salt, 1<<15, 8, 1, 32) +} + +// ─── Key derivation (FIPS Approved) ────────────────────────────────── +func DeriveKeyHKDF(secret, salt, info []byte) ([]byte, error) { + hkdfReader := hkdf.New(sha256.New, secret, salt, info) + key := make([]byte, 32) + _, err := io.ReadFull(hkdfReader, key) + return key, err +} + +func main() { + fmt.Println("Go API Server with crypto") +} diff --git a/test-fixtures/java-microservice/pom.xml b/test-fixtures/java-microservice/pom.xml new file mode 100644 index 000000000..118f5ef8c --- /dev/null +++ b/test-fixtures/java-microservice/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + com.example + crypto-service + 1.0.0 + + + org.bouncycastle + bcprov-jdk18on + 1.78 + + + com.google.crypto.tink + tink + 1.12.0 + + + diff --git a/test-fixtures/java-microservice/src/main/java/com/example/CryptoService.java b/test-fixtures/java-microservice/src/main/java/com/example/CryptoService.java new file mode 100644 index 000000000..407171846 --- /dev/null +++ b/test-fixtures/java-microservice/src/main/java/com/example/CryptoService.java @@ -0,0 +1,110 @@ +package com.example; + +import javax.crypto.Cipher; +import javax.crypto.KeyAgreement; +import javax.crypto.Mac; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.SecureRandom; + +/** + * Test fixture for crypto scanning detection. + * Intentionally includes insecure and deprecated cryptographic patterns. + * NOT production code — do not copy these patterns. + */ +public class CryptoService { + + // ─── AES-GCM encryption (FIPS Approved) ───────────────────────── + public byte[] encryptAesGcm(byte[] key, byte[] plaintext) throws Exception { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + byte[] iv = new byte[12]; + new SecureRandom().nextBytes(iv); + GCMParameterSpec spec = new GCMParameterSpec(128, iv); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), spec); + return cipher.doFinal(plaintext); + } + + // ─── AES-ECB encryption (FIPS Deprecated) ─────────────────────── + public byte[] encryptAesEcb(byte[] key, byte[] plaintext) throws Exception { + Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES")); + return cipher.doFinal(plaintext); + } + + // ─── DESede / 3DES (FIPS Deprecated) ──────────────────────────── + public byte[] encryptTripleDes(byte[] key, byte[] plaintext) throws Exception { + Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "DESede")); + return cipher.doFinal(plaintext); + } + + // ─── RSA key pair (FIPS Approved) ─────────────────────────────── + public void generateRsaKeyPair() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(2048); + kpg.generateKeyPair(); + } + + // ─── ECDSA key pair (FIPS Approved) ───────────────────────────── + public void generateEcKeyPair() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC"); + kpg.initialize(256); + kpg.generateKeyPair(); + } + + // ─── Ed25519 key pair (FIPS Approved) ─────────────────────────── + public void generateEd25519KeyPair() throws Exception { + KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519"); + kpg.generateKeyPair(); + } + + // ─── DH key agreement (FIPS Approved) ─────────────────────────── + public void performDhKeyAgreement() throws Exception { + KeyAgreement ka = KeyAgreement.getInstance("DH"); + } + + // ─── X25519 key agreement (NOT FIPS Approved) ─────────────────── + public void performX25519KeyAgreement() throws Exception { + KeyAgreement ka = KeyAgreement.getInstance("X25519"); + } + + // ─── SHA-256 hash (FIPS Approved) ─────────────────────────────── + public byte[] hashSha256(byte[] data) throws Exception { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + return md.digest(data); + } + + // ─── SHA-1 hash (FIPS Deprecated) ─────────────────────────────── + public byte[] hashSha1(byte[] data) throws Exception { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + return md.digest(data); + } + + // ─── MD5 hash (NOT FIPS Approved) ─────────────────────────────── + public byte[] hashMd5(byte[] data) throws Exception { + MessageDigest md = MessageDigest.getInstance("MD5"); + return md.digest(data); + } + + // ─── HMAC-SHA256 (FIPS Approved) ──────────────────────────────── + public byte[] hmacSha256(byte[] key, byte[] data) throws Exception { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(key, "HmacSHA256")); + return mac.doFinal(data); + } + + // ─── PBKDF2 (FIPS Approved) ───────────────────────────────────── + public byte[] deriveKeyPbkdf2(char[] password, byte[] salt) throws Exception { + SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + PBEKeySpec spec = new PBEKeySpec(password, salt, 600000, 256); + try { + return factory.generateSecret(spec).getEncoded(); + } finally { + spec.clearPassword(); + } + } +} diff --git a/test-fixtures/node-auth-service/auth.js b/test-fixtures/node-auth-service/auth.js new file mode 100644 index 000000000..8c84b45b2 --- /dev/null +++ b/test-fixtures/node-auth-service/auth.js @@ -0,0 +1,103 @@ +const crypto = require('crypto'); +const bcrypt = require('bcrypt'); + +// ─── AES-256-GCM encryption (FIPS Approved) ───────────────────────── +function encryptAesGcm(key, plaintext) { + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv('aes-256-gcm', key, iv); + const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]); + return { iv, encrypted, tag: cipher.getAuthTag() }; +} + +// ─── AES-128-CBC encryption (FIPS Approved) ───────────────────────── +function encryptAesCbc(key, plaintext) { + const iv = crypto.randomBytes(16); + const cipher = crypto.createCipheriv('aes-128-cbc', key, iv); + return Buffer.concat([iv, cipher.update(plaintext), cipher.final()]); +} + +// ─── ChaCha20-Poly1305 (NOT FIPS Approved) ────────────────────────── +function encryptChaCha(key, plaintext) { + const iv = crypto.randomBytes(12); + const cipher = crypto.createCipheriv('chacha20-poly1305', key, iv, { authTagLength: 16 }); + return Buffer.concat([iv, cipher.update(plaintext), cipher.final(), cipher.getAuthTag()]); +} + +// ─── 3DES (FIPS Deprecated) ───────────────────────────────────────── +function encryptTripleDes(key, plaintext) { + const iv = crypto.randomBytes(8); + const cipher = crypto.createCipheriv('des-ede3-cbc', key, iv); + return Buffer.concat([iv, cipher.update(plaintext), cipher.final()]); +} + +// ─── SHA-256 hash (FIPS Approved) ─────────────────────────────────── +function hashSha256(data) { + return crypto.createHash('sha256').update(data).digest('hex'); +} + +// ─── SHA-1 hash (FIPS Deprecated) ─────────────────────────────────── +function hashSha1(data) { + return crypto.createHash('sha1').update(data).digest('hex'); +} + +// ─── MD5 hash (NOT FIPS Approved) ─────────────────────────────────── +function hashMd5(data) { + return crypto.createHash('md5').update(data).digest('hex'); +} + +// ─── HMAC-SHA256 (FIPS Approved) ──────────────────────────────────── +function hmacSha256(key, data) { + return crypto.createHmac('sha256', key).update(data).digest('hex'); +} + +// ─── HMAC-SHA1 (FIPS Deprecated) ──────────────────────────────────── +function hmacSha1(key, data) { + return crypto.createHmac('sha1', key).update(data).digest('hex'); +} + +// ─── RSA key generation (FIPS Approved) ───────────────────────────── +function generateRsaKeyPair() { + return crypto.generateKeyPairSync('rsa', { + modulusLength: 2048, + publicKeyEncoding: { type: 'pkcs1', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, + }); +} + +// ─── Ed25519 key generation (FIPS Approved) ───────────────────────── +function generateEd25519KeyPair() { + return crypto.generateKeyPairSync('ed25519', { + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); +} + +// ─── X25519 key generation (NOT FIPS Approved) ────────────────────── +function generateX25519KeyPair() { + return crypto.generateKeyPairSync('x25519', { + publicKeyEncoding: { type: 'spki', format: 'pem' }, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + }); +} + +// ─── PBKDF2 (FIPS Approved) ───────────────────────────────────────── +function deriveKeyPbkdf2(password, salt) { + return crypto.pbkdf2Sync(password, salt, 600000, 32, 'sha256'); +} + +// ─── scrypt (NOT FIPS Approved) ───────────────────────────────────── +function deriveKeyScrypt(password, salt) { + return crypto.scryptSync(password, salt, 32); +} + +// ─── bcrypt (NOT FIPS Approved) ───────────────────────────────────── +async function hashPasswordBcrypt(password) { + return bcrypt.hash(password, 10); +} + +module.exports = { + encryptAesGcm, encryptAesCbc, encryptChaCha, encryptTripleDes, + hashSha256, hashSha1, hashMd5, hmacSha256, hmacSha1, + generateRsaKeyPair, generateEd25519KeyPair, generateX25519KeyPair, + deriveKeyPbkdf2, deriveKeyScrypt, hashPasswordBcrypt, +}; diff --git a/test-fixtures/node-auth-service/package.json b/test-fixtures/node-auth-service/package.json new file mode 100644 index 000000000..ebc67f6ef --- /dev/null +++ b/test-fixtures/node-auth-service/package.json @@ -0,0 +1,10 @@ +{ + "name": "auth-service", + "version": "1.0.0", + "dependencies": { + "bcrypt": "^5.1.0", + "argon2": "^0.31.0", + "jose": "^5.2.0", + "node-forge": "^1.3.1" + } +} diff --git a/test-fixtures/php-api/composer.json b/test-fixtures/php-api/composer.json new file mode 100644 index 000000000..4f73f5c58 --- /dev/null +++ b/test-fixtures/php-api/composer.json @@ -0,0 +1,9 @@ +{ + "name": "example/crypto-api", + "require": { + "php": "^8.1", + "phpseclib/phpseclib": "^3.0", + "defuse/php-encryption": "^2.4", + "paragonie/sodium_compat": "^1.20" + } +} diff --git a/test-fixtures/php-api/crypto.php b/test-fixtures/php-api/crypto.php new file mode 100644 index 000000000..337b972d6 --- /dev/null +++ b/test-fixtures/php-api/crypto.php @@ -0,0 +1,87 @@ + $iv, 'data' => $ciphertext, 'tag' => $tag]; +} + +// --- AES-256-CBC encryption (FIPS Approved) --- +function encrypt_aes_cbc(string $key, string $plaintext): string { + $iv = openssl_random_pseudo_bytes(16); + return openssl_encrypt($plaintext, 'aes-256-cbc', $key, OPENSSL_RAW_DATA, $iv); +} + +// --- 3DES (FIPS Deprecated) --- +function encrypt_3des(string $key, string $plaintext): string { + $iv = openssl_random_pseudo_bytes(8); + return openssl_encrypt($plaintext, 'des-ede3-cbc', $key, OPENSSL_RAW_DATA, $iv); +} + +// --- SHA-256 hash (FIPS Approved) --- +function hash_sha256(string $data): string { + return hash('sha256', $data); +} + +// --- SHA-1 hash (FIPS Deprecated) --- +function hash_sha1(string $data): string { + return hash('sha1', $data); +} + +// --- MD5 hash (NOT FIPS Approved) --- +function hash_md5(string $data): string { + return hash('md5', $data); +} + +// --- HMAC-SHA256 (FIPS Approved) --- +function hmac_sha256(string $key, string $data): string { + return hash_hmac('sha256', $data, $key); +} + +// --- RSA key generation (FIPS Approved) --- +function generate_rsa_key(): OpenSSLAsymmetricKey { + return openssl_pkey_new([ + 'private_key_bits' => 2048, + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + ]); +} + +// --- Sodium: Ed25519 signing (FIPS Approved) --- +function generate_signing_keypair(): string { + return sodium_crypto_sign_keypair(); +} + +// --- Sodium: X25519 key exchange (NOT FIPS Approved) --- +function generate_box_keypair(): string { + return sodium_crypto_box_keypair(); +} + +// --- Sodium: ChaCha20-Poly1305 (NOT FIPS Approved) --- +function encrypt_chacha(string $key, string $nonce, string $plaintext, string $ad): string { + return sodium_crypto_aead_chacha20poly1305_encrypt($plaintext, $ad, $nonce, $key); +} + +// --- Sodium: BLAKE2b (NOT FIPS Approved) --- +function generic_hash(string $data): string { + return sodium_crypto_generichash($data); +} + +// --- Sodium: Argon2 password hashing (NOT FIPS Approved) --- +function hash_password(string $password): string { + return sodium_crypto_pwhash( + 32, $password, random_bytes(16), + SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, + SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE + ); +} + +// --- bcrypt password hashing (NOT FIPS Approved) --- +function hash_password_bcrypt(string $password): string { + return password_hash($password, PASSWORD_BCRYPT); +} diff --git a/test-fixtures/python-web-app/app.py b/test-fixtures/python-web-app/app.py new file mode 100644 index 000000000..a7d27a6b0 --- /dev/null +++ b/test-fixtures/python-web-app/app.py @@ -0,0 +1,114 @@ +"""Example Python web app with various crypto usage patterns.""" +import hashlib +import hmac +import os + +import bcrypt +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.asymmetric import rsa, ec, ed25519 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from cryptography.hazmat.primitives.kdf.scrypt import Scrypt + + +# ─── AES-256-GCM encryption (FIPS Approved) ───────────────────────── +def encrypt_data(key: bytes, plaintext: bytes) -> tuple[bytes, bytes, bytes]: + iv = os.urandom(12) + cipher = Cipher(algorithms.AES(key), modes.GCM(iv)) + encryptor = cipher.encryptor() + ciphertext = encryptor.update(plaintext) + encryptor.finalize() + return iv, ciphertext, encryptor.tag + + +# ─── AES-CBC encryption (FIPS Approved) ────────────────────────────── +def encrypt_legacy(key: bytes, plaintext: bytes) -> tuple[bytes, bytes]: + iv = os.urandom(16) + cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) + encryptor = cipher.encryptor() + ciphertext = encryptor.update(plaintext) + encryptor.finalize() + return iv, ciphertext + + +# ─── ChaCha20 encryption (NOT FIPS Approved) ──────────────────────── +def encrypt_fast(key: bytes, plaintext: bytes) -> bytes: + nonce = os.urandom(16) + cipher = Cipher(algorithms.ChaCha20(key, nonce), mode=None) + encryptor = cipher.encryptor() + return nonce + encryptor.update(plaintext) + encryptor.finalize() + + +# ─── RSA key generation (FIPS Approved) ────────────────────────────── +def generate_rsa_keypair(): + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + return private_key + + +# ─── ECDSA with P-256 (FIPS Approved) ─────────────────────────────── +def generate_ecdsa_keypair(): + private_key = ec.generate_private_key(ec.SECP256R1()) + return private_key + + +# ─── Ed25519 signatures (FIPS Approved) ────────────────────────────── +def generate_ed25519_keypair(): + private_key = ed25519.Ed25519PrivateKey.generate() + return private_key + + +# ─── Hash functions ────────────────────────────────────────────────── +def hash_sha256(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + +def hash_sha1_legacy(data: bytes) -> str: + """SHA-1 is deprecated but still used in legacy systems.""" + return hashlib.sha1(data).hexdigest() + +def hash_md5_checksum(data: bytes) -> str: + """MD5 is NOT FIPS approved, used only for non-security checksums.""" + return hashlib.md5(data).hexdigest() + +def hash_blake2(data: bytes) -> str: + """BLAKE2 is NOT FIPS approved.""" + return hashlib.blake2b(data).hexdigest() + + +# ─── HMAC (FIPS Approved) ─────────────────────────────────────────── +def create_hmac(key: bytes, message: bytes) -> str: + return hmac.new(key, message, hashlib.sha256).hexdigest() + + +# ─── Password hashing ─────────────────────────────────────────────── +def hash_password_bcrypt(password: str) -> bytes: + """bcrypt is NOT FIPS approved.""" + return bcrypt.hashpw(password.encode(), bcrypt.gensalt()) + + +# ─── Key derivation ───────────────────────────────────────────────── +def derive_key_hkdf(master_key: bytes, info: bytes) -> bytes: + """HKDF is FIPS Approved.""" + hkdf = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=None, + info=info, + ) + return hkdf.derive(master_key) + +def derive_key_pbkdf2(password: bytes, salt: bytes) -> bytes: + """PBKDF2 is FIPS Approved.""" + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=600000, + ) + return kdf.derive(password) + +def derive_key_scrypt(password: bytes, salt: bytes) -> bytes: + """scrypt is NOT FIPS Approved.""" + kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1) + return kdf.derive(password) diff --git a/test-fixtures/python-web-app/requirements.txt b/test-fixtures/python-web-app/requirements.txt new file mode 100644 index 000000000..4236b08e0 --- /dev/null +++ b/test-fixtures/python-web-app/requirements.txt @@ -0,0 +1,5 @@ +cryptography>=42.0.0 +bcrypt>=4.1.0 +argon2-cffi>=23.1.0 +PyJWT>=2.8.0 +flask>=3.0.0 diff --git a/test-fixtures/ruby-web-app/Gemfile b/test-fixtures/ruby-web-app/Gemfile new file mode 100644 index 000000000..7e7bd7e9f --- /dev/null +++ b/test-fixtures/ruby-web-app/Gemfile @@ -0,0 +1,6 @@ +source 'https://rubygems.org' + +gem 'rails', '~> 7.1' +gem 'bcrypt', '~> 3.1' +gem 'rbnacl', '~> 7.1' +gem 'scrypt', '~> 3.0' diff --git a/test-fixtures/ruby-web-app/app.rb b/test-fixtures/ruby-web-app/app.rb new file mode 100644 index 000000000..3ed6bac29 --- /dev/null +++ b/test-fixtures/ruby-web-app/app.rb @@ -0,0 +1,87 @@ +require 'openssl' +require 'digest' +require 'bcrypt' + +module CryptoUtils + # --- AES-256-GCM encryption (FIPS Approved) --- + def self.encrypt_aes_gcm(key, plaintext) + cipher = OpenSSL::Cipher.new('AES-256-GCM') + cipher.encrypt + iv = cipher.random_iv + cipher.key = key + encrypted = cipher.update(plaintext) + cipher.final + tag = cipher.auth_tag + { iv: iv, data: encrypted, tag: tag } + end + + # --- AES-128-CBC encryption (FIPS Approved) --- + def self.encrypt_aes_cbc(key, plaintext) + cipher = OpenSSL::Cipher.new('AES-128-CBC') + cipher.encrypt + cipher.key = key + iv = cipher.random_iv + cipher.update(plaintext) + cipher.final + end + + # --- ChaCha20-Poly1305 (NOT FIPS Approved) --- + def self.encrypt_chacha(key, plaintext) + cipher = OpenSSL::Cipher.new('ChaCha20') + cipher.encrypt + cipher.key = key + iv = cipher.random_iv + cipher.update(plaintext) + cipher.final + end + + # --- 3DES (FIPS Deprecated) --- + def self.encrypt_3des(key, plaintext) + cipher = OpenSSL::Cipher.new('DES-EDE3-CBC') + cipher.encrypt + cipher.key = key + cipher.update(plaintext) + cipher.final + end + + # --- SHA-256 hash (FIPS Approved) --- + def self.hash_sha256(data) + Digest::SHA256.hexdigest(data) + end + + # --- SHA-512 hash (FIPS Approved) --- + def self.hash_sha512(data) + Digest::SHA512.hexdigest(data) + end + + # --- SHA-1 hash (FIPS Deprecated) --- + def self.hash_sha1(data) + Digest::SHA1.hexdigest(data) + end + + # --- MD5 hash (NOT FIPS Approved) --- + def self.hash_md5(data) + Digest::MD5.hexdigest(data) + end + + # --- HMAC-SHA256 (FIPS Approved) --- + def self.hmac_sha256(key, data) + OpenSSL::HMAC.hexdigest('SHA256', key, data) + end + + # --- RSA key generation (FIPS Approved) --- + def self.generate_rsa_key + OpenSSL::PKey::RSA.generate(2048) + end + + # --- ECDSA P-256 (FIPS Approved) --- + def self.generate_ec_key + OpenSSL::PKey::EC.new('prime256v1') + end + + # --- PBKDF2 (FIPS Approved) --- + def self.derive_key_pbkdf2(password, salt) + OpenSSL::PKCS5.pbkdf2_hmac(password, salt, 600_000, 32, 'SHA256') + end + + # --- bcrypt password hashing (NOT FIPS Approved) --- + def self.hash_password(password) + BCrypt::Password.create(password) + end +end diff --git a/test-fixtures/rust-crypto-tool/Cargo.toml b/test-fixtures/rust-crypto-tool/Cargo.toml new file mode 100644 index 000000000..f8b9004f9 --- /dev/null +++ b/test-fixtures/rust-crypto-tool/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "crypto-tool" +version = "0.1.0" +edition = "2021" + +[dependencies] +ring = "0.17" +aes-gcm = "0.10" +chacha20poly1305 = "0.10" +sha2 = "0.10" +blake2 = "0.10" +blake3 = "1" +ed25519-dalek = "2" +x25519-dalek = "2" +argon2 = "0.5" +hkdf = "0.12" +pbkdf2 = "0.12" +hmac = "0.12" +rand = "0.8" diff --git a/test-fixtures/rust-crypto-tool/src/main.rs b/test-fixtures/rust-crypto-tool/src/main.rs new file mode 100644 index 000000000..5df342050 --- /dev/null +++ b/test-fixtures/rust-crypto-tool/src/main.rs @@ -0,0 +1,118 @@ +use aes_gcm::{Aes256Gcm, KeyInit, Nonce}; +use aes_gcm::aead::Aead; +use chacha20poly1305::ChaCha20Poly1305; +use sha2::{Sha256, Sha512, Digest}; +use blake2::Blake2b512; +use blake3; +use ed25519_dalek::SigningKey; +use x25519_dalek::{EphemeralSecret, PublicKey}; +use hkdf::Hkdf; +use hmac::{Hmac, Mac}; +use argon2::Argon2; + +type HmacSha256 = Hmac; + +// ─── AES-256-GCM (FIPS Approved) ───────────────────────────────────── +fn encrypt_aes_gcm(key: &[u8; 32], nonce: &[u8; 12], plaintext: &[u8]) -> Vec { + let cipher = Aes256Gcm::new(key.into()); + let nonce = Nonce::from_slice(nonce); + cipher.encrypt(nonce, plaintext).expect("encryption failed") +} + +// ─── ChaCha20-Poly1305 (NOT FIPS Approved) ─────────────────────────── +fn encrypt_chacha(key: &[u8; 32], nonce: &[u8; 12], plaintext: &[u8]) -> Vec { + let cipher = ChaCha20Poly1305::new(key.into()); + let nonce = chacha20poly1305::Nonce::from_slice(nonce); + cipher.encrypt(nonce, plaintext).expect("encryption failed") +} + +// ─── SHA-256 (FIPS Approved) ───────────────────────────────────────── +fn hash_sha256(data: &[u8]) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(data); + hasher.finalize().to_vec() +} + +// ─── SHA-512 (FIPS Approved) ───────────────────────────────────────── +fn hash_sha512(data: &[u8]) -> Vec { + let mut hasher = Sha512::new(); + hasher.update(data); + hasher.finalize().to_vec() +} + +// ─── BLAKE2 (NOT FIPS Approved) ────────────────────────────────────── +fn hash_blake2(data: &[u8]) -> Vec { + let mut hasher = Blake2b512::new(); + hasher.update(data); + hasher.finalize().to_vec() +} + +// ─── BLAKE3 (NOT FIPS Approved) ────────────────────────────────────── +fn hash_blake3(data: &[u8]) -> Vec { + blake3::hash(data).as_bytes().to_vec() +} + +// ─── Ed25519 signatures (FIPS Approved) ────────────────────────────── +fn generate_ed25519_key() -> SigningKey { + let mut csprng = rand::rngs::OsRng; + SigningKey::generate(&mut csprng) +} + +// ─── X25519 key exchange (NOT FIPS Approved) ───────────────────────── +fn x25519_key_exchange() -> PublicKey { + let mut csprng = rand::rngs::OsRng; + let secret = EphemeralSecret::random_from_rng(&mut csprng); + PublicKey::from(&secret) +} + +// ─── HMAC-SHA256 (FIPS Approved) ───────────────────────────────────── +fn compute_hmac(key: &[u8], data: &[u8]) -> Vec { + let mut mac = HmacSha256::new_from_slice(key).expect("key error"); + mac.update(data); + mac.finalize().into_bytes().to_vec() +} + +// ─── HKDF (FIPS Approved) ──────────────────────────────────────────── +fn derive_key_hkdf(ikm: &[u8], salt: &[u8], info: &[u8]) -> Vec { + let hk = Hkdf::::new(Some(salt), ikm); + let mut okm = vec![0u8; 32]; + hk.expand(info, &mut okm).expect("hkdf expand failed"); + okm +} + +// ─── Argon2 (NOT FIPS Approved) ────────────────────────────────────── +fn hash_password_argon2(password: &[u8], salt: &[u8]) -> Vec { + let argon2 = Argon2::default(); + let mut output = vec![0u8; 32]; + argon2.hash_password_into(password, salt, &mut output).expect("argon2 failed"); + output +} + +// ─── ring-based operations ─────────────────────────────────────────── +fn ring_aes_gcm() { + use ring::aead; + let _algo = &aead::AES_256_GCM; + let _chacha = &aead::CHACHA20_POLY1305; +} + +fn ring_hashing() { + use ring::digest; + let _sha256 = &digest::SHA256; + let _sha1 = &digest::SHA1_FOR_LEGACY_USE_ONLY; +} + +fn ring_signatures() { + use ring::signature; + let _rsa = &signature::RSA_PKCS1_2048_8192_SHA256; + let _ecdsa = &signature::ECDSA_P256_SHA256_ASN1; + let _ed25519 = &signature::ED25519; +} + +fn ring_key_exchange() { + use ring::agreement; + let _x25519 = &agreement::X25519; +} + +fn main() { + println!("Rust crypto tool"); +} diff --git a/test-fixtures/swift-ios-app/CryptoManager.swift b/test-fixtures/swift-ios-app/CryptoManager.swift new file mode 100644 index 000000000..f92a397a1 --- /dev/null +++ b/test-fixtures/swift-ios-app/CryptoManager.swift @@ -0,0 +1,99 @@ +import CryptoKit +import CommonCrypto +import Foundation + +struct CryptoManager { + + // --- AES-GCM encryption (FIPS Approved) --- + static func encryptAesGcm(key: SymmetricKey, plaintext: Data) throws -> AES.GCM.SealedBox { + return try AES.GCM.seal(plaintext, using: key) + } + + static func decryptAesGcm(key: SymmetricKey, sealedBox: AES.GCM.SealedBox) throws -> Data { + return try AES.GCM.open(sealedBox, using: key) + } + + // --- ChaCha20-Poly1305 (NOT FIPS Approved) --- + static func encryptChaCha(key: SymmetricKey, plaintext: Data) throws -> ChaChaPoly.SealedBox { + return try ChaChaPoly.seal(plaintext, using: key) + } + + // --- SHA-256 hash (FIPS Approved) --- + static func hashSha256(data: Data) -> SHA256Digest { + return SHA256.hash(data: data) + } + + // --- SHA-384 hash (FIPS Approved) --- + static func hashSha384(data: Data) -> SHA384Digest { + return SHA384.hash(data: data) + } + + // --- SHA-512 hash (FIPS Approved) --- + static func hashSha512(data: Data) -> SHA512Digest { + return SHA512.hash(data: data) + } + + // --- SHA-1 hash (FIPS Deprecated) --- + static func hashSha1(data: Data) -> Insecure.SHA1.Digest { + return Insecure.SHA1.hash(data: data) + } + + // --- MD5 hash (NOT FIPS Approved) --- + static func hashMd5(data: Data) -> Insecure.MD5.Digest { + return Insecure.MD5.hash(data: data) + } + + // --- HMAC-SHA256 (FIPS Approved) --- + static func hmacSha256(key: SymmetricKey, data: Data) -> HMAC.MAC { + return HMAC.authenticationCode(for: data, using: key) + } + + // --- ECDSA P-256 signing (FIPS Approved) --- + static func signP256(data: Data) throws -> (P256.Signing.PrivateKey, P256.Signing.ECDSASignature) { + let privateKey = P256.Signing.PrivateKey() + let signature = try privateKey.signature(for: data) + return (privateKey, signature) + } + + // --- ECDSA P-384 signing (FIPS Approved) --- + static func signP384(data: Data) throws -> P384.Signing.ECDSASignature { + let privateKey = P384.Signing.PrivateKey() + return try privateKey.signature(for: data) + } + + // --- Ed25519 / Curve25519 signing (FIPS Approved) --- + static func signEd25519(data: Data) throws -> Data { + let privateKey = Curve25519.Signing.PrivateKey() + return try privateKey.signature(for: data) + } + + // --- X25519 key agreement (NOT FIPS Approved) --- + static func keyAgreement(privateKey: Curve25519.KeyAgreement.PrivateKey, publicKey: Curve25519.KeyAgreement.PublicKey) throws -> SharedSecret { + return try privateKey.sharedSecretFromKeyAgreement(with: publicKey) + } + + // --- ECDH P-256 key agreement (FIPS Approved) --- + static func ecdhP256(privateKey: P256.KeyAgreement.PrivateKey, publicKey: P256.KeyAgreement.PublicKey) throws -> SharedSecret { + return try privateKey.sharedSecretFromKeyAgreement(with: publicKey) + } + + // --- PBKDF2 via CommonCrypto (FIPS Approved) --- + // CCKeyDerivationPBKDF returns a status code but does not throw; + // error handling is omitted here since this is a test fixture. + static func deriveKeyPbkdf2(password: String, salt: Data) -> Data { + var derivedKey = Data(count: 32) + derivedKey.withUnsafeMutableBytes { derivedBytes in + salt.withUnsafeBytes { saltBytes in + CCKeyDerivationPBKDF( + CCPBKDFAlgorithm(kCCPBKDF2), + password, password.utf8.count, + saltBytes.baseAddress!, salt.count, + CCPseudoRandomAlgorithm(kCCPRFHmacAlgSHA256), + 600_000, + derivedBytes.baseAddress!, 32 + ) + } + } + return derivedKey + } +} diff --git a/test-fixtures/swift-ios-app/Podfile b/test-fixtures/swift-ios-app/Podfile new file mode 100644 index 000000000..cfbba10e6 --- /dev/null +++ b/test-fixtures/swift-ios-app/Podfile @@ -0,0 +1,7 @@ +platform :ios, '16.0' + +target 'CryptoApp' do + use_frameworks! + pod 'CryptoSwift', '~> 1.8' + pod 'OpenSSL-Universal', '~> 3.1' +end diff --git a/test/App/Fossa/CryptoScan/FipsReportSpec.hs b/test/App/Fossa/CryptoScan/FipsReportSpec.hs new file mode 100644 index 000000000..5f6f9abe9 --- /dev/null +++ b/test/App/Fossa/CryptoScan/FipsReportSpec.hs @@ -0,0 +1,121 @@ +module App.Fossa.CryptoScan.FipsReportSpec (spec) where + +import App.Fossa.CryptoScan.FipsReport (FipsReportStats (..), computeFipsStats, renderFipsReport) +import App.Fossa.CryptoScan.Types ( + Confidence (..), + CryptoAlgorithm (..), + CryptoFinding (..), + CryptoPrimitive (..), + CryptoScanResults (..), + DetectionMethod (..), + FipsStatus (..), + ) +import Data.Text (Text) +import Data.Text qualified as Text +import Prettyprinter (defaultLayoutOptions, layoutPretty) +import Prettyprinter.Render.Terminal (renderStrict) +import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy) + +spec :: Spec +spec = describe "CryptoScan FIPS Report" $ do + describe "computeFipsStats" $ do + it "returns zero stats for empty results" $ do + let stats = computeFipsStats (CryptoScanResults []) + totalAlgorithms stats `shouldBe` 0 + approvedCount stats `shouldBe` 0 + deprecatedCount stats `shouldBe` 0 + notApprovedCount stats `shouldBe` 0 + + it "correctly counts approved algorithms" $ do + let results = + CryptoScanResults + [ mkFinding "AES-256-GCM" FipsApproved + , mkFinding "SHA-256" FipsApproved + ] + stats = computeFipsStats results + totalAlgorithms stats `shouldBe` 2 + approvedCount stats `shouldBe` 2 + deprecatedCount stats `shouldBe` 0 + notApprovedCount stats `shouldBe` 0 + + it "correctly counts mixed statuses" $ do + let results = + CryptoScanResults + [ mkFinding "AES-256-GCM" FipsApproved + , mkFinding "SHA-1" FipsDeprecated + , mkFinding "ChaCha20-Poly1305" FipsNotApproved + , mkFinding "MD5" FipsNotApproved + ] + stats = computeFipsStats results + totalAlgorithms stats `shouldBe` 4 + approvedCount stats `shouldBe` 1 + deprecatedCount stats `shouldBe` 1 + notApprovedCount stats `shouldBe` 2 + + it "deduplicates algorithms by name" $ do + let results = + CryptoScanResults + [ mkFinding "AES-256-GCM" FipsApproved + , mkFinding "AES-256-GCM" FipsApproved -- duplicate + , mkFinding "SHA-256" FipsApproved + ] + stats = computeFipsStats results + totalAlgorithms stats `shouldBe` 2 + approvedCount stats `shouldBe` 2 + + it "does not deduplicate same-name algorithms with different parameter sets" $ do + let results = + CryptoScanResults + [ (mkFinding "RSA" FipsNotApproved){cryptoFindingAlgorithm = (mkAlgorithm "RSA" FipsNotApproved){cryptoAlgorithmParameterSet = Just "1024"}} + , (mkFinding "RSA" FipsApproved){cryptoFindingAlgorithm = (mkAlgorithm "RSA" FipsApproved){cryptoAlgorithmParameterSet = Just "2048"}} + ] + stats = computeFipsStats results + totalAlgorithms stats `shouldBe` 2 + approvedCount stats `shouldBe` 1 + notApprovedCount stats `shouldBe` 1 + + describe "renderFipsReport" $ do + it "produces output for empty results" $ do + let rendered = renderStrict $ layoutPretty defaultLayoutOptions $ renderFipsReport (CryptoScanResults []) + rendered `shouldSatisfy` Text.isInfixOf "FIPS Compliance Report" + + it "produces output for mixed results" $ do + let results = + CryptoScanResults + [ mkFinding "AES-256-GCM" FipsApproved + , mkFinding "MD5" FipsNotApproved + ] + rendered = renderStrict $ layoutPretty defaultLayoutOptions $ renderFipsReport results + rendered `shouldSatisfy` Text.isInfixOf "FIPS Compliance Report" + rendered `shouldSatisfy` Text.isInfixOf "Remediation" + +-- Test fixtures + +mkAlgorithm :: Text -> FipsStatus -> CryptoAlgorithm +mkAlgorithm name status = + CryptoAlgorithm + { cryptoAlgorithmName = name + , cryptoAlgorithmFamily = "test" + , cryptoAlgorithmPrimitive = PrimitiveAe + , cryptoAlgorithmParameterSet = Nothing + , cryptoAlgorithmEllipticCurve = Nothing + , cryptoAlgorithmMode = Nothing + , cryptoAlgorithmOid = Nothing + , cryptoAlgorithmClassicalSecurityLevel = Nothing + , cryptoAlgorithmNistQuantumSecurityLevel = 0 + , cryptoAlgorithmFipsStatus = status + , cryptoAlgorithmCryptoFunctions = [] + } + +mkFinding :: Text -> FipsStatus -> CryptoFinding +mkFinding name status = + CryptoFinding + { cryptoFindingAlgorithm = mkAlgorithm name status + , cryptoFindingFilePath = "test.py" + , cryptoFindingLineNumber = 1 + , cryptoFindingMatchedText = name + , cryptoFindingDetectionMethod = ApiCall + , cryptoFindingEcosystem = "python" + , cryptoFindingProvidingLibrary = Just "cryptography" + , cryptoFindingConfidence = ConfidenceHigh + } diff --git a/test/Test/Fixtures.hs b/test/Test/Fixtures.hs index c66eeadb4..9db31d7d9 100644 --- a/test/Test/Fixtures.hs +++ b/test/Test/Fixtures.hs @@ -706,6 +706,9 @@ standardAnalyzeConfig = , ANZ.snippetScan = False , ANZ.debugDir = Nothing , ANZ.xVendetta = False + , ANZ.xCryptoScan = False + , ANZ.cryptoCbomOutput = Nothing + , ANZ.cryptoFipsReport = False } sampleJarParsedContent :: Text