Contributed By:
Siddhartha Bhattarai. LFX mentee 2026 JUNE-SEPTEMBER.
This summer, through the Linux Foundation Mentorship program, I worked on cryptographic detection in Sonar Cryptography, an open-source CBOM (Cryptographic Bill of Materials) generator under PQCA. My mentor was Aditya Koranga. The work had two parts:
- C/C++ (OpenSSL): another contributor built the detection rules, and I did the OpenSSL research, CBOMkit integration, testing and benchmarking.
- JavaScript/TypeScript: I built this myself. The first pull request was closed, and I explain why and what comes next.
I got interested in cryptography tooling after seeing how much cryptographic code exists in modern applications, and how little tooling for it exists in my country. A single crypto.createHash(“md5”) in a Node.js app, or an EVP_md5() call in a C++ service, is invisible to most static analysis tools and dependency scanners. Post-quantum migration will require knowing which algorithms exist where, and that is hard to answer today.
Sonar Cryptography already handled Java, Python, Go, and C#. It integrates with SonarQube as a plugin. For each file it creates an Abstract Syntax Tree (AST) and runs detection rules against that AST, translates the findings into cryptographic nodes, reorganizes them into a graph, and emits a CBOM. Adding a language means building a full service comprising a sensor, a parser/bridge, a detection engine, rules for the language’s crypto APIs, a translation layer, a reorganizer, and tests (link on how to add language support).
Part 1: C/C++
Who built what. Another contributor(Shubham) built the C/C++ detection support: the OpenSSL detection rules, the sonar-cxx integration, and the fixes for header handling and symbol resolution. My contribution was the OpenSSL research, the CBOMkit integration, and all of the testing and benchmarking on real projects.
Learning OpenSSL. Most C/C++ cryptography goes through OpenSSL, so I started by learning how it is built:
- libcrypto: hashing, key generation, signatures
- libssl: TLS/DTLS, the protocols behind HTTPS
- EVP API: the modern, algorithm-agnostic interface, e.g. EVP_aes_256_gcm()
- OpenSSL CLI: the command-line tool
Real projects use it in different ways. nginx uses it for HTTPS, SNI and certificate checks (via ssl_certificate). PostgreSQL uses it for encrypted connections, and its pgcrypto extension calls OpenSSL directly, which matches the patterns we want to detect. The nginx source repository became my real-world scan target. I also read about BoringSSL (Google’s fork, used by runtimes like Bun, with an unstable API) and the Heartbleed bug, which showed why knowing where a library is used matters.
Where it ended up. By the end of the mentorship, Sonar Cryptography could scan C/C++ projects through sonar-cxx, using OpenSSL-focused detection rules registered as an external rule repository. I tested it on real code, including nginx, and generated around 50 cryptographic assets.
Architecture
C/C++ analysis builds on the community sonar-cxx plugin (v2.3.0), which is installed separately in SonarQube. The cpp module doesn’t parse code itself. It registers an external rule repository through a CxxCheckRegistrar (a CxxCustomRuleRepository) and receives sonar-cxx’s SSLR AstNode tree and its squid-bridge Symbol objects. A CxxDetectionEngine implementing the shared IDetectionEngine<AstNode, Symbol> interface walks that tree, and a CxxScanContext reports issues back through the check. This is the same shape as the other modules: C/C++ is another ILanguageSupport implementation feeding the shared DetectionStore, translation and reorganizer pipeline, and the CBOM output.
Rules are declarative DetectionRule definitions grouped by OpenSSL API family: cipher, digest, KDF, key agreement, key generation, MAC, random, signature, SSL/TLS and legacy algorithms. They match OpenSSL calls such as EVP_* functions and resolve the algorithm and parameters from the arguments. A translator per context type (digest, cipher, key, MAC, signature, PRNG, protocol and so on) then maps findings to CBOM nodes.
The module could have used its own parser (a Clang-based tool or an ANTLR grammar), but sonar-cxx already plugs into SonarQube’s scanner and supplies the AST and symbols. That let the team focus on detection rules and translation instead of parser plumbing.

Week by Week (Progress)
Week 1: Learning OpenSSL and finding real usage patterns (link)
I studied how OpenSSL is built and listed how open-source projects use it (nginx for HTTPS/SNI, PostgreSQL’s `pgcrypto`), so we would know which patterns the rules need to catch. I also set up SonarQube with the plugin.
Weeks 2-3: CBOMkit integration and first real-project tests
I integrated the new OpenSSL support into CBOMkit so findings flow from scan to dashboard, and began testing on real C/C++ projects that use OpenSSL. This was the first time the work met real code, and also the first surprise.
Weeks 4-5: When scanning found nothing
Scanning nginx, curl and libssl returned no assets. Feedback from a reviewer, Nicklas helped explain why. The C++ branch kept per-file AST and call-stack state in memory, and without `notifyLeaveFile()` and `detachCallsForFile()` that state accumulates across thousands of files. `#ifdef` macros and missing OpenSSL headers meant many API calls were never expanded. Another contributor fixed the header handling and the sonar-cxx symbol resolution (and raised a PR upstream to sonar-cxx). After that, nginx started yielding assets.
Weeks 5-6: Testing, documenting, and the 49 vs 50 puzzle (link)
My mentor suggested I document everything, so I created a testing document. It records every test with date, project, commit and number of assets, cross-checked against nginx manually, by script and by AI.
Scanning nginx gave 49 assets in the terminal but 50 in the CBOMkit dashboard. I first suspected a missed detection, but no algorithm was missing. The CBOM contains 21 unique component types and 49 physical occurrences, which matches the terminal count of 49, plus one dependency (ChaCha20Poly1305 → Poly1305). ChaCha20-Poly1305 is a composite algorithm: a stream cipher (ChaCha20) combined with a MAC (Poly1305), represented as one asset. The dashboard counts that dependency as an extra asset, which makes 50. To confirm this, I built a manual one-asset CBOM, and both places showed 1. The difference only appears with composite algorithms. So the scanner’s count is right, and the difference comes from how the dashboard counts composites. This led me to the cross-language ChaCha20-Poly1305 test (below) and a benchmarking plan.
Week 7: Wider testing, then the JavaScript switch (link)
I tested more projects (Apache, curl) and checked composites across languages. Then the priority shifted to JavaScript: comparing SonarJS with an ESLint-based approach, researching the Node.js `crypto` and Web Crypto APIs, and creating a personal repo for that work (Part 2).
Rule Coverage
- Modern EVP API
- Ciphers:AES, ARIA, Camellia, ChaCha20, DES, Blowfish, CAST5, IDEA, RC2/RC4/RC5, SEED and SM4, plus `EVP_CIPHER_fetch`
- Digests: MD, SHA-2, SHA-3 and BLAKE2
- MACs, signatures and random numbers: `EVP_MAC`, `EVP_DigestSign`/`Verify`, and the `RAND_` family
- KDFs: HKDF, Argon2, PKCS#12 and the TLS KDF, with parameter scanning
- Key generation: RSA and DSA, with key-size parameters
- Key agreement: ECDH and DH, including post-quantum hybrids (e.g. X25519MLKEM768)
- Legacy low-level API: Older algorithm-specific calls like `AES_cbc_encrypt` and `DES_ede3_cbc_encrypt`, plus legacy digests, HMAC/CMAC, DH, DSA, EC and RSA , TLS/SSL
Rules match on the raw API function names, so no OpenSSL headers need to be bundled with the plugin.
Testing
I did the testing for the C/C++ work. The upstream PR contains a unit test and a .cc fixture for each rule family (cipher, digest, KDF, key agreement, key generation, MAC, random, signature, TLS (libssl), and each legacy API), plus translator tests, cross-file tests, and a performance test for call-stack memory use, which came out of the memory problem found in review.
Beyond the unit tests, I scanned real open-source projects (nginx, curl, Apache and OpenSSL) end to end through CBOMkit, and recorded every run (date, commit, project, asset count) in a testing document. I cross-checked the nginx results manually, by script and by AI (link).
Cross-language check: ChaCha20-Poly1305
I also tested across languages to check that the shared CBOM pipeline behaves consistently, using ChaCha20-Poly1305 as the test case. It is a composite AEAD algorithm (RFC 8439): ChaCha20 (a stream cipher, for confidentiality) combined with Poly1305 (a MAC, for integrity). A CBOM represents it as one component whose dependencies list its parts (Notes).
Simplified example (other components omitted):

This dependency entry is also why the nginx scan showed 49 assets in the terminal and 50 in the dashboard (see Weeks 5–6). The scanner’s count is correct, and the dashboard counts the composite’s dependency as an extra asset. In every language I tested, the composite appears as one component with its parts listed as dependencies.
Challenges
The learning curve. I started without a background in OpenSSL, the CBOM format or SonarQube plugins, and I had to look up many new terms (AST, AEAD, SNI, KDF). I also presented my progress to the PQCA Technical Steering Committee (TSC) meeting, where many of the attendees are senior researchers, which pushed me to check every claim.
Scans that found nothing. The first C++ scan ran with the default “Sonar way” profile, which has no C++ rules, so no CBOM was generated. Even with the rules active, nginx, curl and OpenSSL returned no assets, because per-file state built up in memory and #ifdef macros and missing OpenSSL headers meant many calls were never expanded. Another contributor fixed this, and nginx then started yielding assets.
Plugin loading problems. A CBOMkit maintainer found that the C++ part was registered even without sonar-cxx, which made the whole plugin fail and broke Java, Python and Go scanning too. The fix is to register it only when sonar-cxx is present. I also lost time to a missing plugin manifest setting and the wrong SonarQube image.
A count that didn’t match. The terminal showed 49 assets and the dashboard 50. The dashboard counts the dependency of a composite algorithm (ChaCha20-Poly1305) as an extra asset (see Weeks 5–6).
Slow scans. Large projects took a long time to scan, which led to my scan-time PR. I kept a testing document of every run so I could tell whether a change helped.
Part 2: JavaScript/TypeScript
Architecture and the Build Problem: The first question was how to parse JavaScript from a JVM program. A Java-side parser (Rhino, GraalVM) gave too weak an AST. A regex scanner was fast to prototype, and I started there. A real parser (espree/ESLint) in a Node subprocess, with the AST shipped back as JSON, matched how other modules work.
I started with the regex scanner partly because bundling Node.js into a Maven plugin with `frontend-maven-plugin` was fighting me. `npm install –production` failed on CI with exit code 254 (it passed locally), so I removed the step since the scanner used only built-in modules. Then a bridge test passed locally but failed on the reviewer’s machine and CI, an environment sensitivity that cost the reviewer real time. Lesson: a plugin that shells out to an external runtime must be validated in CI, not just on a laptop.
The Regex Scanner’s Limits: Review exposed its problems: false positives from generic method names (`logger.update()`, `db.connect()`), bracket counting that broke on strings, comments and template literals, narrow module detection (no ES imports, destructuring or aliasing), and a `variableValues` field that was never populated. The reviewer put it plainly: the “ESLint runner” did not invoke ESLint or any parser.
Detection Rules
I built rules for Node’s `crypto` and `tls` APIs: hashes and HMAC, ciphers (`createCipheriv`, `createDecipheriv`), signatures, key agreement (`createDiffieHellman`, `createECDH`), key material and generation, KDFs (`pbkdf2`, `scrypt`, `hkdf`), PRNG (`randomBytes`, `randomUUID`), RSA encrypt/decrypt, and `createSecureContext`. Context mattered: `createSecretKey` is semantically an import, but the shared engine has no `IMPORT` action, so the convention is a generation action with `”kind”: “import”` in the context map.
The ESLint Rewrite and Review Fixes: I rewrote the runner on ESLint’s `Linter` (espree), walking the ESTree AST and emitting calls only when the receiver resolves to a crypto import. That fixed the string/comment handling, false positives, module detection and `variableValues`. It meant re-adding `npm install`, which worked this time with `–omit=dev`. An automated review of the PR listed 6 blockers, 8 high-priority and 7 minor items, and I closed them all. Two stood out:
currentContext` scope leak: The engine set `currentContext` on entering a nested block and never restored it, so identifier resolution used the wrong scope. The fix was a try/finally save-restore: five lines for a silent correctness bug.
Path traversal in JAR extraction: The bundled Node runtime is extracted to a temp directory without a containment check, so a crafted entry like `../../bin/evil` could escape it. Fixed with a normalized-path check, plus `deleteOnExit()` on every extracted file:
I also added a 60-second watchdog on the Node subprocess, removed unconditional debug logging of algorithm names, and restored `minimizeJar=true`. By late September the PR had 20 commits, ~9,000 lines, and 22 passing tests.
The PR Is Closed
The second review found new issues. Two had to be fixed: context.getAncestors() is removed in ESLint 9, and TypeScript was listed as supported although the parser (espree) cannot read .ts syntax, so TypeScript files silently returned nothing.
Then the maintainers closed PR #539. The reason was the architecture, not the bugs. The other modules use a Sonar language parser (Java, Python, Go) or ANTLR (C#). Mine used ESLint only to select calls, and then built a reduced AST from them. That tree lost the structure the detection engine needs to follow a chain of calls. Each call got an empty block, so the rule for createHash could never reach the later update and digest calls. My tests proved the first finding but never that chain. The request was not dropped, only the approach. I was asked to evaluate two options.
Option 1: SonarJS (not viable)
Sonar modules are built on four pieces: Rule, Tree, Symbol and Publisher. Java and Python get these from public Sonar APIs. SonarJS has none of them that is no public AST, no symbol table and no scanner context. It also moved to ESLint (espree) internally, and its documentation says custom rules are not supported. The only path left is Generic Issue Import, which skips our detection engine.
Option 2: ANTLR (the plan)
This is the same route the C# module took:
- Start from the grammars-v4 TypeScript grammar, which covers JavaScript too.
- Write a custom Publisher, since ANTLR has no equivalent of Java’s scanner context.
- Map the parse tree into new JS/TS Tree and Symbol types that keep scopes and call relationships.
The first step is a feasibility test: one crypto.createHash() rule and one fixture, wired through the engine’s five interfaces to a CBOM finding. I’ll test a sequential case first, then the inline chained one (ideally in typed TypeScript), to prove that the chain of calls works before I expand. If ANTLR fails, we discuss a custom bridge before writing more code.
Future Work
- ANTLR feasibility test, then the custom Publisher and JS/TS Tree/Symbol types
- Carried over from the old PR: TLS `rejectUnauthorized: false` detection, scrypt cost parameters, Web Crypto, `https`/`http2` coverage, argument source locations, and more tests (parser errors, deduplication, weak algorithms)
What I Learned
- CI is not my laptop. The npm and test failures had one root cause: a plugin that runs an external runtime is sensitive to its environment, so it has to be tested in CI.
- A name is a promise. ESLintBridge promised an ESLint runner that the code didn’t deliver. Fixing the code to match the name was better than renaming it.
- A parser is not an AST. Parsing the language is not enough. The tree has to fit what the detection engine expects.
- Passing tests can prove the wrong thing. Mine showed the first finding but never the createHash → update → digest chain.
- Validate the design before building it. I wrote about 9,000 lines before settling the parser question. A one-rule feasibility test would have shown the problem in days.
- Document as you go. This was my mentor’s tip: write everything down. My running notes became this post.
My experience
I’m glad I did this mentorship, and I would recommend it. Before it, I had used security tools without knowing how they work. Now I can explain how a cryptographic bill of materials is built, from a function call in someone’s source code to an entry in a CBOM, and I can read a code review from experienced maintainers and learn from it.
Not everything went well. My JavaScript pull request was closed after weeks of work, and at first that felt like a failure. In the end it taught me more than a merged PR would have. I learned to question the design early, to test what matters and not only what is easy, and that honest, detailed feedback is a gift. The maintainers did not just reject the work. They told me why and gave me a clear path to continue.
It was also valuable because it was real. I worked on a tool that people use, I scanned real projects like nginx, and I presented my plan to add JavaScript/TypeScript support to a committee of researchers. In my country, there is little tooling in this area, so building experience on it matters to me. One of the best parts was the community meetings: much of what I learned came from the PQCA TSC meetings. I would encourage anyone interested in PQCA to join them, including the CBOMkit meetings.
Yes, it was worth it. I plan to continue and finish JavaScript/TypeScript support for Sonar Cryptography.
Summary
- C/C++
- OpenSSL detection (EVP and legacy APIs, TLS) was integrated into Sonar Cryptography through sonar-cxx. Another contributor built it, and the upstream PR (cbomkit/sonar-cryptography#377) is open.
- I did the OpenSSL research, the CBOMkit integration, and the testing and benchmarking on nginx, curl, Apache and OpenSSL.
- I debugged why scans found nothing and explained the ChaCha20-Poly1305 count difference (49 vs 50).
- JavaScript/TypeScript
- I built a full detection slice and closed 6 blockers, 8 high-priority and 7 minor review items (22 tests).
- PR #539 was closed because the reduced-AST approach could not follow a chain of calls or support TypeScript.
- I ruled out SonarJS and designed an ANTLR approach, with a feasibility test next.
Acknowledgements
Thank you to my mentor, Aditya Koranga, the PQCA Technical Steering Committee (TSC) meeting committee(most of them are senior researchers from IBM research) for patience through the debugging , and to the reviewers for detailed and honest feedback, and to the Sonar Cryptography maintainers for an architecture that made adding languages tractable. I’m grateful for the opportunity and look forward to continuing in this direction and finishing up the javascript/typescript support for sonarcryptography.

