From 79ae366510bf5c281f722056ad7e7c77ef994603 Mon Sep 17 00:00:00 2001 From: CortezFrazierJr <90806692+CortezFrazierJr@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:38:14 -0500 Subject: [PATCH 01/18] Add cryptographic algorithm scanning with CycloneDX 1.7 CBOM output Introduce a new experimental feature (--x-crypto-scan) that detects cryptographic algorithm usage across 10 ecosystems (Python, Java, Go, Node, Rust, Ruby, C#, PHP, Swift, Elixir) and produces CycloneDX 1.7 CBOM output for FIPS compliance assessment. Key features: - Rust-based crypto detection engine (extlib/cryptoscan) with pattern matching for imports, API calls, dependency manifests, and config files - Auto-detection of ecosystems present in the project - FIPS 140-3 compliance classification (approved/deprecated/not-approved) - CycloneDX 1.7 CBOM file output (--crypto-cbom-output) - Detailed FIPS compliance report (--crypto-fips-report) with remediation recommendations and key-size warnings - Crypto scan results displayed in the analysis scan summary - 56 passing integration tests covering all 10 ecosystems Co-Authored-By: Claude Opus 4.6 --- Cargo.lock | 206 ++++- Cargo.toml | 1 + Makefile | 4 +- docs/features/crypto-scanning.md | 162 ++++ .../experimental/crypto-scanning/README.md | 45 + docs/references/subcommands/analyze.md | 22 + extlib/cryptoscan/Cargo.lock | 822 ++++++++++++++++++ extlib/cryptoscan/Cargo.toml | 22 + extlib/cryptoscan/src/crypto_algorithm.rs | 90 ++ extlib/cryptoscan/src/cyclonedx.rs | 325 +++++++ extlib/cryptoscan/src/fips.rs | 303 +++++++ extlib/cryptoscan/src/main.rs | 98 +++ extlib/cryptoscan/src/patterns.rs | 729 ++++++++++++++++ extlib/cryptoscan/src/scanner.rs | 314 +++++++ extlib/cryptoscan/tests/integration_test.rs | 544 ++++++++++++ spectrometer.cabal | 8 + src/App/Fossa/Analyze.hs | 44 +- src/App/Fossa/Analyze/ScanSummary.hs | 34 +- src/App/Fossa/Analyze/Types.hs | 2 + src/App/Fossa/Config/Analyze.hs | 12 + src/App/Fossa/CryptoScan/Analyze.hs | 67 ++ src/App/Fossa/CryptoScan/FipsReport.hs | 256 ++++++ src/App/Fossa/CryptoScan/Types.hs | 240 +++++ src/App/Fossa/EmbeddedBinary.hs | 19 + test-fixtures/csharp-service/CryptoService.cs | 96 ++ test-fixtures/csharp-service/Service.csproj | 11 + test-fixtures/elixir-phoenix-app/crypto.ex | 90 ++ test-fixtures/elixir-phoenix-app/mix.exs | 23 + test-fixtures/go-api-server/go.mod | 7 + test-fixtures/go-api-server/main.go | 137 +++ test-fixtures/java-microservice/pom.xml | 19 + .../main/java/com/example/CryptoService.java | 104 +++ test-fixtures/node-auth-service/auth.js | 103 +++ test-fixtures/node-auth-service/package.json | 10 + test-fixtures/php-api/composer.json | 9 + test-fixtures/php-api/crypto.php | 86 ++ test-fixtures/python-web-app/app.py | 114 +++ test-fixtures/python-web-app/requirements.txt | 5 + test-fixtures/ruby-web-app/Gemfile | 6 + test-fixtures/ruby-web-app/app.rb | 87 ++ test-fixtures/rust-crypto-tool/Cargo.toml | 18 + test-fixtures/rust-crypto-tool/src/main.rs | 117 +++ .../swift-ios-app/CryptoManager.swift | 97 +++ test-fixtures/swift-ios-app/Podfile | 7 + test/App/Fossa/CryptoScan/FipsReportSpec.hs | 92 ++ 45 files changed, 5555 insertions(+), 52 deletions(-) create mode 100644 docs/features/crypto-scanning.md create mode 100644 docs/references/experimental/crypto-scanning/README.md create mode 100644 extlib/cryptoscan/Cargo.lock create mode 100644 extlib/cryptoscan/Cargo.toml create mode 100644 extlib/cryptoscan/src/crypto_algorithm.rs create mode 100644 extlib/cryptoscan/src/cyclonedx.rs create mode 100644 extlib/cryptoscan/src/fips.rs create mode 100644 extlib/cryptoscan/src/main.rs create mode 100644 extlib/cryptoscan/src/patterns.rs create mode 100644 extlib/cryptoscan/src/scanner.rs create mode 100644 extlib/cryptoscan/tests/integration_test.rs create mode 100644 src/App/Fossa/CryptoScan/Analyze.hs create mode 100644 src/App/Fossa/CryptoScan/FipsReport.hs create mode 100644 src/App/Fossa/CryptoScan/Types.hs create mode 100644 test-fixtures/csharp-service/CryptoService.cs create mode 100644 test-fixtures/csharp-service/Service.csproj create mode 100644 test-fixtures/elixir-phoenix-app/crypto.ex create mode 100644 test-fixtures/elixir-phoenix-app/mix.exs create mode 100644 test-fixtures/go-api-server/go.mod create mode 100644 test-fixtures/go-api-server/main.go create mode 100644 test-fixtures/java-microservice/pom.xml create mode 100644 test-fixtures/java-microservice/src/main/java/com/example/CryptoService.java create mode 100644 test-fixtures/node-auth-service/auth.js create mode 100644 test-fixtures/node-auth-service/package.json create mode 100644 test-fixtures/php-api/composer.json create mode 100644 test-fixtures/php-api/crypto.php create mode 100644 test-fixtures/python-web-app/app.py create mode 100644 test-fixtures/python-web-app/requirements.txt create mode 100644 test-fixtures/ruby-web-app/Gemfile create mode 100644 test-fixtures/ruby-web-app/app.rb create mode 100644 test-fixtures/rust-crypto-tool/Cargo.toml create mode 100644 test-fixtures/rust-crypto-tool/src/main.rs create mode 100644 test-fixtures/swift-ios-app/CryptoManager.swift create mode 100644 test-fixtures/swift-ios-app/Podfile create mode 100644 test/App/Fossa/CryptoScan/FipsReportSpec.hs diff --git a/Cargo.lock b/Cargo.lock index 6ddb09ebf7..ba9c18335c 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" @@ -223,7 +238,7 @@ dependencies = [ "getset", "lexical-sort", "log", - "serde 1.0.228", + "serde 1.0.219", "serde_json", "simple_logger", "stable-eyre", @@ -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.219", +] + [[package]] name = "bumpalo" version = "3.17.0" @@ -324,7 +350,7 @@ checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" dependencies = [ "ciborium-io", "ciborium-ll", - "serde 1.0.228", + "serde 1.0.219", ] [[package]] @@ -484,7 +510,7 @@ dependencies = [ "lazy_static 1.5.0", "nom", "rust-ini", - "serde 1.0.228", + "serde 1.0.219", "serde-hjson", "serde_json", "toml", @@ -571,7 +597,7 @@ dependencies = [ "plotters", "rayon", "regex", - "serde 1.0.228", + "serde 1.0.219", "serde_derive", "serde_json", "tinytemplate", @@ -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.219", + "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.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" 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" @@ -803,7 +856,7 @@ dependencies = [ "base64 0.22.1", "getset", "iter-read", - "serde 1.0.228", + "serde 1.0.219", "sha1", "sha2", "strum 0.26.3", @@ -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" @@ -1335,7 +1397,7 @@ dependencies = [ "rayon", "retry", "secrecy", - "serde 1.0.228", + "serde 1.0.219", "serde_json", "serde_yaml", "snippets", @@ -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.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[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" @@ -1639,7 +1737,7 @@ dependencies = [ "config", "directories", "petgraph", - "serde 1.0.228", + "serde 1.0.219", "serde-value", "tint", ] @@ -1797,7 +1895,7 @@ dependencies = [ "eyre", "petgraph", "ptree", - "serde 1.0.228", + "serde 1.0.219", "serde_json", ] @@ -1991,11 +2089,10 @@ checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ - "serde_core", "serde_derive", ] @@ -2018,23 +2115,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ "ordered-float", - "serde 1.0.228", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", + "serde 1.0.219", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -2050,7 +2138,7 @@ dependencies = [ "itoa", "memchr", "ryu", - "serde 1.0.228", + "serde 1.0.219", ] [[package]] @@ -2062,7 +2150,7 @@ dependencies = [ "indexmap 2.9.0", "itoa", "ryu", - "serde 1.0.228", + "serde 1.0.219", "unsafe-libyaml", ] @@ -2155,7 +2243,7 @@ dependencies = [ "getset", "lazy_static 1.5.0", "regex", - "serde 1.0.228", + "serde 1.0.219", "strum 0.24.1", "thiserror 1.0.69", "typed-builder 0.10.0", @@ -2318,6 +2406,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 +2428,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" @@ -2405,22 +2512,22 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "num-conv", "powerfmt", - "serde_core", + "serde 1.0.219", "time-core", ] [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "tint" @@ -2447,7 +2554,7 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" dependencies = [ - "serde 1.0.228", + "serde 1.0.219", "serde_json", ] @@ -2457,7 +2564,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ - "serde 1.0.228", + "serde 1.0.219", ] [[package]] @@ -2532,7 +2639,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" dependencies = [ - "serde 1.0.228", + "serde 1.0.219", "tracing-core", ] @@ -2543,7 +2650,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "nu-ansi-term", - "serde 1.0.228", + "serde 1.0.219", "serde_json", "sharded-slab", "smallvec", @@ -2681,7 +2788,7 @@ dependencies = [ "rustls", "rustls-native-certs", "rustls-pki-types", - "serde 1.0.228", + "serde 1.0.219", "serde_json", "url", "webpki-roots 0.26.11", @@ -2731,6 +2838,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" @@ -3010,7 +3126,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ - "serde 1.0.228", + "serde 1.0.219", "stable_deref_trait", "yoke-derive", "zerofrom", diff --git a/Cargo.toml b/Cargo.toml index fb9a66ffad..e4fa392e7d 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 9e63a0a20d..d4736bda2c 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 0000000000..ea2aba5760 --- /dev/null +++ b/docs/features/crypto-scanning.md @@ -0,0 +1,162 @@ + +# 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") + +No source code content is sent to FOSSA. Only metadata about detected +cryptographic algorithm usage is transmitted. + +## 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, FIPS level) +- `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 (all modes except ECB 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 0000000000..f2a791895b --- /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`). + +Ten 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 b59763bd93..363bfe0051 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 10 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 10 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,7 @@ 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). | ### F.A.Q. diff --git a/extlib/cryptoscan/Cargo.lock b/extlib/cryptoscan/Cargo.lock new file mode 100644 index 0000000000..b2f8ea1280 --- /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 0000000000..9382eddc49 --- /dev/null +++ b/extlib/cryptoscan/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "cryptoscan" +version = "0.1.0" +edition = "2021" +description = "Cryptographic algorithm detection engine for FOSSA CLI" + +[[bin]] +name = "cryptoscan" +path = "src/main.rs" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1" +regex = "1" +walkdir = "2" +clap = { version = "4", features = ["derive"] } +uuid = { version = "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 0000000000..e3f9ccde76 --- /dev/null +++ b/extlib/cryptoscan/src/crypto_algorithm.rs @@ -0,0 +1,90 @@ +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, + Kdf, + Xof, + Drbg, + Combiner, + Other, + 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, +} + +#[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 0000000000..a53d780328 --- /dev/null +++ b/extlib/cryptoscan/src/cyclonedx.rs @@ -0,0 +1,325 @@ +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +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(); + let mut seen_algorithms: HashSet = HashSet::new(); + let mut library_algorithms: HashMap> = HashMap::new(); + + // Create cryptographic-asset components for each unique algorithm + for finding in findings { + let bom_ref = make_bom_ref(&finding.algorithm.name, &finding.algorithm.oid); + + if seen_algorithms.contains(&bom_ref) { + // Still track the library -> algorithm relationship + if let Some(lib) = &finding.providing_library { + library_algorithms + .entry(lib.clone()) + .or_default() + .push(bom_ref.clone()); + } + continue; + } + seen_algorithms.insert(bom_ref.clone()); + + let primitive_str = serde_json::to_value(&finding.algorithm.primitive) + .ok() + .and_then(|v| v.as_str().map(|s| s.to_string())) + .unwrap_or_else(|| "unknown".to_string()); + + let fips_label = finding.algorithm.fips_status.label().to_string(); + + let algo_props = AlgorithmProperties { + primitive: primitive_str, + algorithm_family: Some(finding.algorithm.algorithm_family.clone()), + parameter_set_identifier: finding.algorithm.parameter_set.clone(), + elliptic_curve: finding.algorithm.elliptic_curve.clone(), + mode: finding.algorithm.mode.clone(), + execution_environment: "software-plain-ram".to_string(), + implementation_platform: "generic".to_string(), + certification_level: vec!["none".to_string()], + crypto_functions: finding.algorithm.crypto_functions.clone(), + classical_security_level: finding.algorithm.classical_security_level, + nist_quantum_security_level: if finding.algorithm.nist_quantum_security_level > 0 { + Some(finding.algorithm.nist_quantum_security_level) + } else { + None + }, + }; + + let component = BomComponent { + component_type: "cryptographic-asset".to_string(), + bom_ref: bom_ref.clone(), + name: finding.algorithm.name.clone(), + crypto_properties: Some(CryptoProperties { + asset_type: "algorithm".to_string(), + algorithm_properties: Some(algo_props), + oid: finding.algorithm.oid.clone(), + }), + properties: Some(vec![ + BomProperty { + name: "fossa:fips-status".to_string(), + value: fips_label, + }, + BomProperty { + name: "fossa:detected-in".to_string(), + value: finding.file_path.clone(), + }, + BomProperty { + name: "fossa:detection-method".to_string(), + value: serde_json::to_value(&finding.detection_method) + .ok() + .and_then(|v| v.as_str().map(|s| s.to_string())) + .unwrap_or_default(), + }, + BomProperty { + name: "fossa:ecosystem".to_string(), + value: finding.ecosystem.clone(), + }, + ]), + }; + + components.push(component); + + // Track library -> algorithm for `provides` relationships + if let Some(lib) = &finding.providing_library { + library_algorithms + .entry(lib.clone()) + .or_default() + .push(bom_ref.clone()); + } + + // Algorithm dependency entry + dependencies.push(BomDependency { + dep_ref: bom_ref, + depends_on: None, + provides: None, + }); + } + + // Create library components with `provides` relationships + let mut seen_libs: HashSet = HashSet::new(); + for (lib_name, algo_refs) in &library_algorithms { + if seen_libs.contains(lib_name) { + continue; + } + seen_libs.insert(lib_name.clone()); + + let lib_ref = format!("lib/{}", 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() + .collect::>() + .into_iter() + .cloned() + .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(' ', "-") + .replace('/', "-"); + match oid { + Some(o) => format!("crypto/algorithm/{}@{}", sanitized, o), + None => format!("crypto/algorithm/{}", sanitized), + } +} + +fn chrono_timestamp() -> String { + // Simple UTC timestamp without chrono dependency + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .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; + } + if month == 0 { + month = 12; + } + + (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 0000000000..936c728596 --- /dev/null +++ b/extlib/cryptoscan/src/fips.rs @@ -0,0 +1,303 @@ +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", + } + } +} + +/// FIPS remediation suggestion. +#[allow(dead_code)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FipsRemediation { + pub algorithm: String, + pub status: FipsStatus, + pub reason: String, + pub recommended_alternative: Option, +} + +/// 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); + } + + if lower.contains("ecdsa") { + return (FipsStatus::Approved, None); + } + + if lower.contains("ed25519") || lower.contains("ed448") || lower.contains("eddsa") { + return (FipsStatus::Approved, None); // Approved for signatures per FIPS 186-5 + } + + if lower.contains("dsa") && !lower.contains("ecdsa") && !lower.contains("eddsa") && !lower.contains("ml-dsa") { + 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()), + ); + } + return (FipsStatus::Approved, None); + } + + 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" { + return (FipsStatus::Approved, None); // Assuming >= 2048-bit + } + + // --- Post-Quantum --- + if lower.contains("ml-kem") || lower.contains("mlkem") || lower.contains("kyber") { + return (FipsStatus::Approved, None); + } + + if lower.contains("ml-dsa") || lower.contains("mldsa") || lower.contains("dilithium") { + return (FipsStatus::Approved, None); + } + + if lower.contains("slh-dsa") || lower.contains("slhdsa") || lower.contains("sphincs") { + 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 0000000000..7a23413a38 --- /dev/null +++ b/extlib/cryptoscan/src/main.rs @@ -0,0 +1,98 @@ +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(); + + 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 0000000000..023c673ede --- /dev/null +++ b/extlib/cryptoscan/src/patterns.rs @@ -0,0 +1,729 @@ +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>, + 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 -- + pat(r"from\s+cryptography\.hazmat\.primitives\.ciphers\s+import", "AES", DetectionMethod::ImportStatement, "python", &["py"], Some("cryptography"), Confidence::High), + pat(r"from\s+cryptography\.hazmat\.primitives\.ciphers\.algorithms\s+import\s+(\w+)", "AES", DetectionMethod::ImportStatement, "python", &["py"], Some("cryptography"), Confidence::High), + pat(r"from\s+cryptography\.hazmat\.primitives\.hashes\s+import", "SHA-256", DetectionMethod::ImportStatement, "python", &["py"], Some("cryptography"), Confidence::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::High), + pat(r"from\s+cryptography\.hazmat\.primitives\.kdf\s+import\s+(hkdf|pbkdf2|scrypt|concatkdf|x963kdf)", "HKDF", DetectionMethod::ImportStatement, "python", &["py"], Some("cryptography"), Confidence::High), + + // -- 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 -- + 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 -- + pat(r"org\.bouncycastle", "BouncyCastle", DetectionMethod::DependencyManifest, "java", &["xml", "gradle", "kts"], None, Confidence::High), + pat(r"com\.google\.crypto\.tink", "Tink", DetectionMethod::DependencyManifest, "java", &["xml", "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 -- + pat(r"golang\.org/x/crypto", "x/crypto", DetectionMethod::DependencyManifest, "go", &["mod", "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 -- + pat(r#""crypto-js""#, "crypto-js", DetectionMethod::DependencyManifest, "node", &["json"], None, Confidence::Medium), + pat(r#""bcrypt""#, "bcrypt", DetectionMethod::DependencyManifest, "node", &["json"], None, Confidence::Medium), + pat(r#""argon2""#, "Argon2", DetectionMethod::DependencyManifest, "node", &["json"], None, Confidence::Medium), + pat(r#""node-forge""#, "node-forge", DetectionMethod::DependencyManifest, "node", &["json"], None, Confidence::Medium), + pat(r#""jose""#, "JOSE", DetectionMethod::DependencyManifest, "node", &["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 -- + pat(r#"(?m)^ring\s*="#, "ring", DetectionMethod::DependencyManifest, "rust", &["toml"], None, Confidence::High), + pat(r#"(?m)^aes-gcm\s*="#, "AES-GCM", DetectionMethod::DependencyManifest, "rust", &["toml"], None, Confidence::High), + pat(r#"(?m)^chacha20poly1305\s*="#, "ChaCha20-Poly1305", DetectionMethod::DependencyManifest, "rust", &["toml"], None, Confidence::High), + pat(r#"(?m)^sha2\s*="#, "SHA-256", DetectionMethod::DependencyManifest, "rust", &["toml"], None, Confidence::Medium), + pat(r#"(?m)^blake2\s*="#, "BLAKE2", DetectionMethod::DependencyManifest, "rust", &["toml"], None, Confidence::Medium), + pat(r#"(?m)^blake3\s*="#, "BLAKE3", DetectionMethod::DependencyManifest, "rust", &["toml"], None, Confidence::Medium), + pat(r#"(?m)^argon2\s*="#, "Argon2", DetectionMethod::DependencyManifest, "rust", &["toml"], None, Confidence::Medium), + pat(r#"(?m)^rustls\s*="#, "TLS", DetectionMethod::DependencyManifest, "rust", &["toml"], None, Confidence::Medium), + pat(r#"(?m)^openssl\s*="#, "OpenSSL", DetectionMethod::DependencyManifest, "rust", &["toml"], None, Confidence::Medium), + pat(r#"(?m)^ed25519-dalek\s*="#, "Ed25519", DetectionMethod::DependencyManifest, "rust", &["toml"], None, Confidence::Medium), + pat(r#"(?m)^x25519-dalek\s*="#, "X25519", DetectionMethod::DependencyManifest, "rust", &["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) -- + pat(r#"gem\s+['"]rbnacl['"]\b"#, "rbnacl", DetectionMethod::DependencyManifest, "ruby", &["rb"], None, Confidence::Medium), + pat(r#"gem\s+['"]bcrypt['"]\b"#, "bcrypt", DetectionMethod::DependencyManifest, "ruby", &["rb"], None, Confidence::Medium), + pat(r#"gem\s+['"]ed25519['"]\b"#, "Ed25519", DetectionMethod::DependencyManifest, "ruby", &["rb"], None, Confidence::Medium), + pat(r#"gem\s+['"]rsa['"]\b"#, "RSA", DetectionMethod::DependencyManifest, "ruby", &["rb"], 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) -- + 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) -- + pat(r#""phpseclib/phpseclib""#, "phpseclib", DetectionMethod::DependencyManifest, "php", &["json"], None, Confidence::High), + pat(r#""defuse/php-encryption""#, "AES", DetectionMethod::DependencyManifest, "php", &["json"], None, Confidence::Medium), + pat(r#""paragonie/halite""#, "XSalsa20-Poly1305", DetectionMethod::DependencyManifest, "php", &["json"], None, Confidence::Medium), + pat(r#""paragonie/sodium_compat""#, "X25519", DetectionMethod::DependencyManifest, "php", &["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 -- + pat(r#"\.package\s*\(\s*url:.*CryptoSwift"#, "CryptoSwift", DetectionMethod::DependencyManifest, "swift", &["swift"], None, Confidence::High), + pat(r#"\.package\s*\(\s*url:.*swift-crypto"#, "swift-crypto", DetectionMethod::DependencyManifest, "swift", &["swift"], None, Confidence::High), + pat(r#"pod\s+['"]CryptoSwift['"]\b"#, "CryptoSwift", DetectionMethod::DependencyManifest, "swift", &["rb"], None, Confidence::High), + pat(r#"pod\s+['"]OpenSSL['"]\b"#, "OpenSSL", DetectionMethod::DependencyManifest, "swift", &["rb"], None, Confidence::High), + pat(r#"pod\s+['"]RNCryptor['"]\b"#, "AES", DetectionMethod::DependencyManifest, "swift", &["rb"], 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) -- + pat(r#"\{:bcrypt_elixir\b"#, "bcrypt", DetectionMethod::DependencyManifest, "elixir", &["exs"], None, Confidence::High), + pat(r#"\{:argon2_elixir\b"#, "Argon2", DetectionMethod::DependencyManifest, "elixir", &["exs"], None, Confidence::High), + pat(r#"\{:pbkdf2_elixir\b"#, "PBKDF2", DetectionMethod::DependencyManifest, "elixir", &["exs"], None, Confidence::High), + pat(r#"\{:comeonin\b"#, "bcrypt", DetectionMethod::DependencyManifest, "elixir", &["exs"], None, Confidence::Medium), + pat(r#"\{:jose\b"#, "JOSE", DetectionMethod::DependencyManifest, "elixir", &["exs"], None, Confidence::Medium), + pat(r#"\{:plug_crypto\b"#, "AES", DetectionMethod::DependencyManifest, "elixir", &["exs"], None, Confidence::Medium), + pat(r#"\{:ex_crypto\b"#, "AES", DetectionMethod::DependencyManifest, "elixir", &["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(), + 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 0000000000..a1df1b0ba4 --- /dev/null +++ b/extlib/cryptoscan/src/scanner.rs @@ -0,0 +1,314 @@ +use std::collections::HashMap; +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(); + + for entry in WalkDir::new(project_path) + .max_depth(3) + .into_iter() + .filter_map(|e| e.ok()) + { + let name = entry.file_name().to_string_lossy(); + match name.as_ref() { + "requirements.txt" | "Pipfile" | "pyproject.toml" | "setup.py" | "setup.cfg" => { + if !ecosystems.contains(&"python".to_string()) { + ecosystems.push("python".to_string()); + } + } + "pom.xml" | "build.gradle" | "build.gradle.kts" => { + if !ecosystems.contains(&"java".to_string()) { + ecosystems.push("java".to_string()); + } + } + "go.mod" | "go.sum" => { + if !ecosystems.contains(&"go".to_string()) { + ecosystems.push("go".to_string()); + } + } + "package.json" => { + if !ecosystems.contains(&"node".to_string()) { + ecosystems.push("node".to_string()); + } + } + "Cargo.toml" => { + if !ecosystems.contains(&"rust".to_string()) { + ecosystems.push("rust".to_string()); + } + } + "Gemfile" | "Rakefile" => { + if !ecosystems.contains(&"ruby".to_string()) { + ecosystems.push("ruby".to_string()); + } + } + "composer.json" => { + if !ecosystems.contains(&"php".to_string()) { + ecosystems.push("php".to_string()); + } + } + "Package.swift" | "Podfile" => { + if !ecosystems.contains(&"swift".to_string()) { + ecosystems.push("swift".to_string()); + } + } + "mix.exs" => { + if !ecosystems.contains(&"elixir".to_string()) { + ecosystems.push("elixir".to_string()); + } + } + _ => { + // Detect C#/.NET by file extension + let ext = entry.path().extension().and_then(|e| e.to_str()).unwrap_or(""); + if (ext == "csproj" || ext == "fsproj" || ext == "sln") + && !ecosystems.contains(&"csharp".to_string()) + { + ecosystems.push("csharp".to_string()); + } + // Detect Ruby by .gemspec extension + if ext == "gemspec" && !ecosystems.contains(&"ruby".to_string()) { + ecosystems.push("ruby".to_string()); + } + } + } + } + + 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_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + { + let path = entry.path(); + + // Skip hidden directories and common non-source dirs + if should_skip_path(path) { + continue; + } + + 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 { + let path_str = path.to_string_lossy(); + let skip_dirs = [ + "/.git/", + "/node_modules/", + "/__pycache__/", + "/.venv/", + "/venv/", + "/target/", + "/dist/", + "/build/", + "/.tox/", + "/.mypy_cache/", + "/.pytest_cache/", + "/vendor/", + "/.gradle/", + "/.idea/", + "/.vscode/", + ]; + skip_dirs.iter().any(|d| path_str.contains(d)) +} + +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.iter().any(|ext| *ext == file_ext) { + return true; + } + + // 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); + } + + false +} + +fn resolve_algorithm(name: &str, _matched_text: &str) -> CryptoAlgorithm { + let (fips_status, _remediation) = fips::classify_algorithm(name); + + let (primitive, family, mode, param_set, curve, security, quantum, oid, functions) = match name { + // Symmetric - AES + "AES" | "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-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" | "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-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" | "AES-256-CBC" => (Primitive::BlockCipher, "AES", Some("cbc"), Some("256"), None, Some(256), 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, Some(128), 1, None, vec!["keygen", "encrypt", "decrypt"]), + "AES-ECB" => (Primitive::BlockCipher, "AES", Some("ecb"), None, None, Some(128), 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, Some("2048"), None, Some(112), 0, Some("1.2.840.113549.1.1.1"), vec!["keygen", "encrypt", "decrypt", "sign", "verify"]), + "ECDSA" | "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, Some("2048"), None, Some(112), 0, Some("1.2.840.10040.4.1"), vec!["sign", "verify"]), + + // Key exchange + "ECDH" => (Primitive::KeyAgree, "ECDH", None, None, Some("nist/P-256"), Some(128), 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, Some("2048"), None, Some(112), 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 + _ => (Primitive::Unknown, name, None, None, None, None, 0, None, vec![]), + }; + + CryptoAlgorithm { + name: name.to_string(), + 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: HashMap = HashMap::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 0000000000..81eedbc570 --- /dev/null +++ b/extlib/cryptoscan/tests/integration_test.rs @@ -0,0 +1,544 @@ +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_value(serde_json::from_slice(&output.stdout).expect("invalid JSON")) + .expect("invalid CycloneDX structure") +} + +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", "approved")); +} + +#[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", "approved")); + assert!(has_algorithm_with_status(&findings, "ECDSA", "approved")); + 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", "approved"), "should detect DH key exchange"); + 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", "approved")); + 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"); + assert!(algo["classical_security_level"].as_u64().is_some(), "should include 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 bd3b5d4573..98e453f211 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,11 @@ library App.Fossa.Container.Sources.Podman App.Fossa.Container.Sources.Registry App.Fossa.Container.Test + App.Fossa.Crypto + App.Fossa.CryptoScan.Analyze + App.Fossa.CryptoScan.FipsReport + App.Fossa.CryptoScan.Types + App.Fossa.Config.Crypto App.Fossa.DebugDir App.Fossa.DependencyMetadata App.Fossa.DumpBinaries @@ -575,6 +582,7 @@ test-suite unit-tests App.Fossa.ArchiveUploaderSpec App.Fossa.BinaryDeps.JarSpec App.Fossa.CirceSpec + App.Fossa.CryptoScan.FipsReportSpec App.Fossa.Config.AnalyzeSpec App.Fossa.Config.ReleaseGroup.CreateSpec App.Fossa.Config.TestSpec diff --git a/src/App/Fossa/Analyze.hs b/src/App/Fossa/Analyze.hs index f2d0700c89..b9faa9c255 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) @@ -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,31 @@ 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 + 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 +454,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 +525,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 . renderIt $ 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 27a4f09332..357e506737 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 (CryptoFinding (..), CryptoScanResults (..), FipsStatus (..), CryptoAlgorithm (..)) 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,30 @@ 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))) + | not (null findings) = + [successColorCoded wg $ listSymbol <> "Crypto Scan" <> renderSucceeded wg] + <> itemize (" " <> listSymbol) renderCryptoFinding findings + | otherwise = [] +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 +416,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 +431,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 +439,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 4b2bb4845c..deda6aed67 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 e946ab04cb..4efa6c17cb 100644 --- a/src/App/Fossa/Config/Analyze.hs +++ b/src/App/Fossa/Config/Analyze.hs @@ -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 || maybe False (const True) 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 0000000000..f47f3efbec --- /dev/null +++ b/src/App/Fossa/CryptoScan/Analyze.hs @@ -0,0 +1,67 @@ +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, warn) +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 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 + warn $ "Failed to parse cryptoscan output: " <> toText err + pure Nothing + Right findings -> do + logDebug $ "Cryptoscan completed: " <> pretty (show 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 bin rootDir = + Command + { cmdName = toText $ toPath bin + , cmdArgs = ["--path", toText rootDir, "--ecosystem", "auto", "--format", "json"] + , cmdAllowErr = Never + } + +cryptoScanCycloneDxCommand :: BinaryPaths -> Path Abs Dir -> Command +cryptoScanCycloneDxCommand bin rootDir = + Command + { cmdName = toText $ toPath bin + , cmdArgs = ["--path", toText rootDir, "--ecosystem", "auto", "--format", "cyclonedx"] + , cmdAllowErr = Never + } diff --git a/src/App/Fossa/CryptoScan/FipsReport.hs b/src/App/Fossa/CryptoScan/FipsReport.hs new file mode 100644 index 0000000000..8f5ef9fbe8 --- /dev/null +++ b/src/App/Fossa/CryptoScan/FipsReport.hs @@ -0,0 +1,256 @@ +{-# LANGUAGE RecordWildCards #-} + +module App.Fossa.CryptoScan.FipsReport ( + renderFipsReport, + FipsReportStats (..), + computeFipsStats, +) where + +import App.Fossa.CryptoScan.Types ( + CryptoAlgorithm (..), + CryptoFinding (..), + CryptoPrimitive (..), + CryptoScanResults (..), + FipsStatus (..), + ) +import Data.List (nubBy) +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +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{..} + | totalAlgorithms == 0 = 100 + | otherwise = (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) + +computeFipsStats :: CryptoScanResults -> FipsReportStats +computeFipsStats (CryptoScanResults findings) = + let uniqueAlgos = nubBy (\a b -> dedupeKey a == dedupeKey b) findings + 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 results@(CryptoScanResults findings) = + let stats = computeFipsStats results + uniqueFindings = nubBy (\a b -> dedupeKey a == dedupeKey b) findings + 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 PrimitiveKdf = CatKdfs +classifyPrimitive PrimitiveDrbg = CatOther +classifyPrimitive PrimitiveCombiner = CatOther +classifyPrimitive PrimitiveOther = CatOther +classifyPrimitive PrimitiveUnknown = CatOther + +categorizeFindings :: [CryptoFinding] -> Map CryptoCategory [CryptoFinding] +categorizeFindings = foldl categorize Map.empty + where + categorize acc finding = + 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 + | matchesAny ["chacha20", "chacha20-poly1305", "xchacha20"] name = "AES-256-GCM" + | matchesAny ["blake2", "blake2b", "blake2s", "blake3"] name = "SHA-256 / SHA-3" + | matchesAny ["md5"] name = "SHA-256" + | matchesAny ["md4"] name = "SHA-256" + | matchesAny ["rc4", "rc2", "blowfish", "des", "3des-encrypt"] name = "AES-256" + | matchesAny ["bcrypt", "argon2", "argon2i", "argon2id", "scrypt"] name = "PBKDF2" + | matchesAny ["x25519", "x448"] name = "ECDH P-256 / P-384" + | matchesAny ["curve25519"] name = "ECDH NIST curves" + | matchesAny ["poly1305"] name = "HMAC / CMAC" + | matchesAny ["siphash"] name = "HMAC" + | matchesAny ["whirlpool", "ripemd", "ripemd-160"] name = "SHA-256" + | matchesAny ["cast5", "idea", "camellia", "seed", "aria"] name = "AES-256" + | matchesAny ["twofish", "serpent", "threefish"] name = "AES-256" + | otherwise = "Review FIPS 140-3 approved algorithm list" + +matchesAny :: [Text] -> Text -> Bool +matchesAny patterns t = any (\p -> Text.toLower p == Text.toLower t) patterns + +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 + name = Text.toLower $ cryptoAlgorithmName algo + paramSet = cryptoAlgorithmParameterSet algo + in catWarnings name paramSet + where + catWarnings :: Text -> Maybe Text -> [Doc AnsiStyle] + catWarnings name paramSet + | "rsa" `Text.isInfixOf` name = rsaWarning paramSet + | "sha-1" `Text.isInfixOf` name || "sha1" `Text.isInfixOf` name = + [annotate (color Yellow) "- SHA-1: Deprecated, fully disallowed after 2030-12-31"] + | "aes-128" `Text.isInfixOf` name || (name == "aes" && paramSet == Just "128") = + [annotate (color Yellow) "- AES-128: Approved but AES-256 recommended for higher security margin"] + | "sha-224" `Text.isInfixOf` name = + [annotate (color Yellow) "- SHA-224: Deprecated by 2030"] + | "3des" `Text.isInfixOf` name || "triple-des" `Text.isInfixOf` name = + [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 (Text.unpack ps) :: [(Int, String)] of + [(n, "")] | n < 2048 -> [annotate (color Red) $ "- RSA-" <> pretty ps <> ": Below FIPS minimum (2048-bit required)"] + [(n, "")] | n < 3072 -> [annotate (color Yellow) $ "- RSA-" <> pretty ps <> ": Approved but RSA-3072+ recommended for 128-bit security"] + _ -> [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 0000000000..7aef497aa5 --- /dev/null +++ b/src/App/Fossa/CryptoScan/Types.hs @@ -0,0 +1,240 @@ +{-# LANGUAGE RecordWildCards #-} + +module App.Fossa.CryptoScan.Types ( + CryptoScanResults (..), + CryptoFinding (..), + CryptoAlgorithm (..), + FipsStatus (..), + DetectionMethod (..), + Confidence (..), + CryptoPrimitive (..), +) where + +import Data.Aeson ( + FromJSON (parseJSON), + ToJSON (toJSON), + Value (String), + object, + withObject, + withText, + (.:), + (.:?), + (.=), + ) +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 = 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{..} = + 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 = 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{..} = + 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 = 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 = 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 = 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 + | PrimitiveKdf + | PrimitiveXof + | PrimitiveDrbg + | PrimitiveCombiner + | PrimitiveOther + | PrimitiveUnknown + deriving (Show, Eq, Ord) + +instance FromJSON CryptoPrimitive where + parseJSON = 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 + "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 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 e609be919b..7d12585341 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 0000000000..9bf49a194a --- /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 0000000000..b362cb106e --- /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 0000000000..17525e6ab7 --- /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 0000000000..9184dc2d60 --- /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 0000000000..9228c7a4fe --- /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 0000000000..b09b3be3a4 --- /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 0000000000..118f5ef8c3 --- /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 0000000000..c2678c852b --- /dev/null +++ b/test-fixtures/java-microservice/src/main/java/com/example/CryptoService.java @@ -0,0 +1,104 @@ +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; + +/** + * Example Java microservice demonstrating various crypto 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); + return factory.generateSecret(spec).getEncoded(); + } +} diff --git a/test-fixtures/node-auth-service/auth.js b/test-fixtures/node-auth-service/auth.js new file mode 100644 index 0000000000..8c84b45b24 --- /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 0000000000..ebc67f6efb --- /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 0000000000..4f73f5c584 --- /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 0000000000..cececbc0e0 --- /dev/null +++ b/test-fixtures/php-api/crypto.php @@ -0,0 +1,86 @@ + $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 0000000000..a7d27a6b02 --- /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 0000000000..4236b08e0e --- /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 0000000000..7e7bd7e9f2 --- /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 0000000000..4863c7a50e --- /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-poly1305') + 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 0000000000..fea185c0dd --- /dev/null +++ b/test-fixtures/rust-crypto-tool/Cargo.toml @@ -0,0 +1,18 @@ +[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" 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 0000000000..081fa616f6 --- /dev/null +++ b/test-fixtures/rust-crypto-tool/src/main.rs @@ -0,0 +1,117 @@ +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 secret = EphemeralSecret::random(); + 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 0000000000..55675ed409 --- /dev/null +++ b/test-fixtures/swift-ios-app/CryptoManager.swift @@ -0,0 +1,97 @@ +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) --- + 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 0000000000..cfbba10e60 --- /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 0000000000..9ef009f49f --- /dev/null +++ b/test/App/Fossa/CryptoScan/FipsReportSpec.hs @@ -0,0 +1,92 @@ +module App.Fossa.CryptoScan.FipsReportSpec (spec) where + +import App.Fossa.CryptoScan.FipsReport (FipsReportStats (..), computeFipsStats) +import App.Fossa.CryptoScan.Types ( + Confidence (..), + CryptoAlgorithm (..), + CryptoFinding (..), + CryptoPrimitive (..), + CryptoScanResults (..), + DetectionMethod (..), + FipsStatus (..), + ) +import Data.Text (Text) +import Test.Hspec (Spec, describe, it, shouldBe) + +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 + +-- 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 + } From cab95b720b7941db11e23def2553cd684618e7c4 Mon Sep 17 00:00:00 2001 From: CortezFrazierJr <90806692+CortezFrazierJr@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:02:12 -0400 Subject: [PATCH 02/18] Address CodeRabbit review: fix algorithm normalization, CBOM context aggregation, parse error handling, and doc wording - scanner.rs: Add normalize_detected_algorithm() to use matched text for refining generic pattern names into specific algorithm variants (e.g., AES -> AES-256-GCM based on matched code context) - cyclonedx.rs: Aggregate detection contexts (detected-in, detection-method, ecosystem) from all findings per algorithm instead of only keeping the first - Analyze.hs: Use fatalText instead of warn+Nothing for JSON parse failures so scanner output-contract regressions surface as hard errors - crypto-scanning.md: Fix wording that implied non-ECB AES modes are deprecated Co-Authored-By: Claude Opus 4.6 --- docs/features/crypto-scanning.md | 2 +- extlib/cryptoscan/src/cyclonedx.rs | 124 +++++++++++++++------------- extlib/cryptoscan/src/scanner.rs | 81 ++++++++++++++++-- src/App/Fossa/CryptoScan/Analyze.hs | 5 +- 4 files changed, 147 insertions(+), 65 deletions(-) diff --git a/docs/features/crypto-scanning.md b/docs/features/crypto-scanning.md index ea2aba5760..9f439e440d 100644 --- a/docs/features/crypto-scanning.md +++ b/docs/features/crypto-scanning.md @@ -143,7 +143,7 @@ fossa analyze --output --crypto-fips-report | Category | Algorithms | |---|---| -| Symmetric Encryption | AES-128/192/256 (all modes except ECB deprecated by 2030) | +| 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 | diff --git a/extlib/cryptoscan/src/cyclonedx.rs b/extlib/cryptoscan/src/cyclonedx.rs index a53d780328..b6a208670e 100644 --- a/extlib/cryptoscan/src/cyclonedx.rs +++ b/extlib/cryptoscan/src/cyclonedx.rs @@ -107,95 +107,108 @@ pub struct BomDependency { pub fn to_cyclonedx_bom(findings: &[CryptoFinding]) -> CycloneDxBom { let mut components = Vec::new(); let mut dependencies = Vec::new(); - let mut seen_algorithms: HashSet = HashSet::new(); let mut library_algorithms: HashMap> = HashMap::new(); - // Create cryptographic-asset components for each unique algorithm + // Group findings by bom_ref to aggregate detection contexts + let mut algo_findings: HashMap> = HashMap::new(); for finding in findings { let bom_ref = make_bom_ref(&finding.algorithm.name, &finding.algorithm.oid); + algo_findings.entry(bom_ref).or_default().push(finding); - if seen_algorithms.contains(&bom_ref) { - // Still track the library -> algorithm relationship - if let Some(lib) = &finding.providing_library { - library_algorithms - .entry(lib.clone()) - .or_default() - .push(bom_ref.clone()); - } - continue; + // Track library -> algorithm for `provides` relationships + if let Some(lib) = &finding.providing_library { + let bom_ref = make_bom_ref(&finding.algorithm.name, &finding.algorithm.oid); + library_algorithms + .entry(lib.clone()) + .or_default() + .push(bom_ref); } - seen_algorithms.insert(bom_ref.clone()); + } + + // Create cryptographic-asset components with aggregated detection contexts + for (bom_ref, group) in &algo_findings { + let first = group[0]; - let primitive_str = serde_json::to_value(&finding.algorithm.primitive) + let primitive_str = serde_json::to_value(&first.algorithm.primitive) .ok() .and_then(|v| v.as_str().map(|s| s.to_string())) .unwrap_or_else(|| "unknown".to_string()); - let fips_label = finding.algorithm.fips_status.label().to_string(); + let fips_label = first.algorithm.fips_status.label().to_string(); let algo_props = AlgorithmProperties { primitive: primitive_str, - algorithm_family: Some(finding.algorithm.algorithm_family.clone()), - parameter_set_identifier: finding.algorithm.parameter_set.clone(), - elliptic_curve: finding.algorithm.elliptic_curve.clone(), - mode: finding.algorithm.mode.clone(), + algorithm_family: Some(first.algorithm.algorithm_family.clone()), + 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: finding.algorithm.crypto_functions.clone(), - classical_security_level: finding.algorithm.classical_security_level, - nist_quantum_security_level: if finding.algorithm.nist_quantum_security_level > 0 { - Some(finding.algorithm.nist_quantum_security_level) + 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: HashSet = HashSet::new(); + let mut seen_methods: HashSet = HashSet::new(); + let mut seen_ecosystems: HashSet = HashSet::new(); + + for finding in group { + if seen_locations.insert(finding.file_path.clone()) { + properties.push(BomProperty { + name: "fossa:detected-in".to_string(), + value: finding.file_path.clone(), + }); + } + + let method_str = serde_json::to_value(&finding.detection_method) + .ok() + .and_then(|v| v.as_str().map(|s| s.to_string())) + .unwrap_or_default(); + if seen_methods.insert(method_str.clone()) { + properties.push(BomProperty { + name: "fossa:detection-method".to_string(), + value: method_str, + }); + } + + if seen_ecosystems.insert(finding.ecosystem.clone()) { + properties.push(BomProperty { + name: "fossa:ecosystem".to_string(), + value: finding.ecosystem.clone(), + }); + } + } + let component = BomComponent { component_type: "cryptographic-asset".to_string(), bom_ref: bom_ref.clone(), - name: finding.algorithm.name.clone(), + name: first.algorithm.name.clone(), crypto_properties: Some(CryptoProperties { asset_type: "algorithm".to_string(), algorithm_properties: Some(algo_props), - oid: finding.algorithm.oid.clone(), + oid: first.algorithm.oid.clone(), }), - properties: Some(vec![ - BomProperty { - name: "fossa:fips-status".to_string(), - value: fips_label, - }, - BomProperty { - name: "fossa:detected-in".to_string(), - value: finding.file_path.clone(), - }, - BomProperty { - name: "fossa:detection-method".to_string(), - value: serde_json::to_value(&finding.detection_method) - .ok() - .and_then(|v| v.as_str().map(|s| s.to_string())) - .unwrap_or_default(), - }, - BomProperty { - name: "fossa:ecosystem".to_string(), - value: finding.ecosystem.clone(), - }, - ]), + properties: Some(properties), }; components.push(component); - // Track library -> algorithm for `provides` relationships - if let Some(lib) = &finding.providing_library { - library_algorithms - .entry(lib.clone()) - .or_default() - .push(bom_ref.clone()); - } - // Algorithm dependency entry dependencies.push(BomDependency { - dep_ref: bom_ref, + dep_ref: bom_ref.clone(), depends_on: None, provides: None, }); @@ -256,8 +269,7 @@ pub fn to_cyclonedx_bom(findings: &[CryptoFinding]) -> CycloneDxBom { fn make_bom_ref(name: &str, oid: &Option) -> String { let sanitized = name .to_lowercase() - .replace(' ', "-") - .replace('/', "-"); + .replace([' ', '/'], "-"); match oid { Some(o) => format!("crypto/algorithm/{}@{}", sanitized, o), None => format!("crypto/algorithm/{}", sanitized), diff --git a/extlib/cryptoscan/src/scanner.rs b/extlib/cryptoscan/src/scanner.rs index a1df1b0ba4..0fd8cd4bdb 100644 --- a/extlib/cryptoscan/src/scanner.rs +++ b/extlib/cryptoscan/src/scanner.rs @@ -191,7 +191,7 @@ fn pattern_applies(pattern: &CryptoPattern, ecosystems: &[String], file_ext: &st } // Check file extension match - if pattern.file_extensions.iter().any(|ext| *ext == file_ext) { + if pattern.file_extensions.contains(&file_ext) { return true; } @@ -205,10 +205,81 @@ fn pattern_applies(pattern: &CryptoPattern, ecosystems: &[String], file_ext: &st false } -fn resolve_algorithm(name: &str, _matched_text: &str) -> CryptoAlgorithm { - let (fips_status, _remediation) = fips::classify_algorithm(name); +fn normalize_detected_algorithm(name: &str, matched_text: &str) -> String { + let lower = matched_text.to_lowercase(); - let (primitive, family, mode, param_set, curve, security, quantum, oid, functions) = match name { + // 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("128") { + return "AES-128-GCM".to_string(); + } + return "AES-256-GCM".to_string(); + } + if lower.contains("cbc") { + if lower.contains("128") { + return "AES-128-CBC".to_string(); + } + return "AES-256-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".to_string(); // AES-192 falls under generic AES + } + 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" | "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-256" => (Primitive::BlockCipher, "AES", None, Some("256"), None, Some(256), 1, Some("2.16.840.1.101.3.4.1"), vec!["keygen", "encrypt", "decrypt"]), @@ -271,7 +342,7 @@ fn resolve_algorithm(name: &str, _matched_text: &str) -> CryptoAlgorithm { }; CryptoAlgorithm { - name: name.to_string(), + name: normalized.clone(), algorithm_family: family.to_string(), primitive, parameter_set: param_set.map(|s| s.to_string()), diff --git a/src/App/Fossa/CryptoScan/Analyze.hs b/src/App/Fossa/CryptoScan/Analyze.hs index f47f3efbec..2b9c3746ac 100644 --- a/src/App/Fossa/CryptoScan/Analyze.hs +++ b/src/App/Fossa/CryptoScan/Analyze.hs @@ -5,7 +5,7 @@ module App.Fossa.CryptoScan.Analyze ( import App.Fossa.CryptoScan.Types (CryptoScanResults) import App.Fossa.EmbeddedBinary (BinaryPaths, toPath, withCryptoScanBinary) -import Control.Carrier.Diagnostics (Diagnostics, warn) +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 @@ -28,8 +28,7 @@ analyzeWithCryptoScan rootDir = withCryptoScanBinary $ \bin -> do result <- execThrow rootDir (cryptoScanCommand bin rootDir) case Aeson.eitherDecode result of Left err -> do - warn $ "Failed to parse cryptoscan output: " <> toText err - pure Nothing + fatalText $ "Failed to parse cryptoscan output: " <> toText err Right findings -> do logDebug $ "Cryptoscan completed: " <> pretty (show findings) pure $ Just findings From 588b4083d8a24981f065691758a65b21ebaf34d5 Mon Sep 17 00:00:00 2001 From: CortezFrazierJr <90806692+CortezFrazierJr@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:05:46 -0400 Subject: [PATCH 03/18] Address remaining CodeRabbit review: remove guards, fix WalkDir pruning, add doc entries - FipsReport.hs: Convert all match guards to if/then/else per style guide (compliancePercentage, coloredPercentage, suggestAlternative, catWarnings, rsaWarning). Remove unused matchesAny function. - scanner.rs: Use filter_entry() to prune skipped directories during WalkDir traversal instead of filtering after entry. Replace brittle slash-based path matching with file_name() segment comparison for cross-platform correctness. - analyze.md: Add --crypto-cbom-output and --crypto-fips-report to the experimental flags table for discoverability. Co-Authored-By: Claude Opus 4.6 --- docs/references/subcommands/analyze.md | 2 + extlib/cryptoscan/src/scanner.rs | 43 ++++++------- src/App/Fossa/CryptoScan/FipsReport.hs | 84 ++++++++++++++------------ 3 files changed, 68 insertions(+), 61 deletions(-) diff --git a/docs/references/subcommands/analyze.md b/docs/references/subcommands/analyze.md index 363bfe0051..434be8c37b 100644 --- a/docs/references/subcommands/analyze.md +++ b/docs/references/subcommands/analyze.md @@ -211,6 +211,8 @@ In addition to the [standard flags](#specifying-fossa-project-details), the anal | `--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/src/scanner.rs b/extlib/cryptoscan/src/scanner.rs index 0fd8cd4bdb..70aec78720 100644 --- a/extlib/cryptoscan/src/scanner.rs +++ b/extlib/cryptoscan/src/scanner.rs @@ -90,16 +90,12 @@ pub fn scan_project(project_path: &Path, ecosystems: &[String]) -> Vec