Modern applications rely on cryptographic libraries across multiple programming languages, but identifying exactly which cryptographic algorithms and configurations are used in a codebase is difficult. The Sonar Cryptography Plugin addresses this by analyzing source code and turning detected cryptographic usage into a structured Cryptographic Bill of Materials (CBOM).
This article walks through how the plugin works internally, from source-code detection and language-specific analysis to translation, enrichment, and final CBOM generation.
The Sonar Cryptography Plugin is a SonarQube plugin that performs static analysis on source code to identify cryptographic assets and generate a Cryptographic Bill of Materials (CBOM) in CycloneDX format. It is part of the broader CBOMkit toolset.
The plugin integrates with SonarQube by registering language-specific sensors and rule checks. During analysis, it scans source files, detects cryptographic usage patterns, extracts structured metadata, and serializes the results into a CycloneDX-compliant CBOM JSON document.
Currently supported languages and libraries include:
- Java: Java Cryptography Architecture (JCA), BouncyCastle Lightweight API
- Python: pyca/cryptography
- Go: Standard library crypto/, golang.org/x/crypto
The project is implemented as a multi-module Maven build, with a clear separation of responsibilities across modules:

Figure 1: sonar-cryptography Multi-module Maven Architecture
Each module is built as an independent Maven artifact. The root pom.xml defines dependency management and aggregates all submodules. The final plugin is assembled in the sonar-cryptography-plugin/ module, which packages all the modules into a single JAR (excluding language parsers) that can be installed directly into SonarQube.
Data Flow
The plugin processes source code through a 5-stage pipeline to produce a structured CBOM document.
Each stage is implemented as a separate module with well-defined interfaces, allowing the system to be extended to additional languages or output formats without major architectural changes.
Stage 1: Detection

Figure 2: Detection Phase; Note: These figures are provided as quick-reference diagrams for readers already familiar with the Sonar Cryptography Plugin architecture. The detailed flow and implementation are described in the accompanying text.
Execution Paths
The cryptographic detection engine can be used in two ways:
- As a SonarQube plugin (sonar-cryptography)
- As a standalone library (cbomkit-lib)
Both use the same engine, mapper, output, and enricher modules, but they differ only in how the sonar language parsers are started and who orchestrates the analysis lifecycle.
Path 1: SonarQube Plugin (sonar-cryptography)
A running SonarQube server is required.
The sonar-cryptography-plugin module:
- Uses sonar-packaging-maven-plugin to declare the plugin entry point CryptographyPlugin.java. This class implements org.sonar.api.Plugin and registers the plugin’s extensions with SonarQube.
- Uses maven-shade-plugin to bundle internal modules (java, python, go, cpp, engine, mapper, output, enricher, rules) into a single fat JAR
- Is deployed to SonarQube’s extensions/plugins/ directory
In this mode:
- SonarQube’s language plugins perform parsing.
- The sonar-cryptography plugin injects cryptographic detection rules.
Each language exposes a different extension mechanism:
| Language | Sonar Extension Point | Provided by sonar-cryptography |
| Java | CheckRegistrar | JavaCheckRegistrar + JavaCheck |
| Python | PythonCustomRuleRepository | PythonCheckRegistrar |
| Go | No rule injection API | CryptoGoSensor (custom sensor) |
Go is an exception. Since sonar-go does not expose a CheckRegistrar-style extension, CryptoGoSensor performs parsing and rule execution directly.
Path 2: Standalone Library (cbomkit-lib)
This mode does not require a SonarQube server.
cbomkit-lib declares sonar-cryptography-plugin as a Maven dependency, bringing the detection engine (fat JAR) onto the classpath.
The Sonar language parser libraries (sonar-java, sonar-python, sonar-go) are pulled as separate transitive dependencies (when cbomkit-lib imports the sonar-cryptography plugin, maven automatically walks down the dependency tree and downloads sonar-java, sonar-python, and all the other required Sonar parsers into cbomkit-lib’s classpath), they are not bundled inside the fat JAR, but are used directly.
SonarQube parsers expect the server to give them SensorContext at runtime. Because cbomkit-lib runs without a server, it uses SonarQube’s unit-testing utilities to manually construct a “fake” server environment from scratch.
| Language | Invocation Strategy |
| Java | Creates SensorContextTester and JavaFrontend, then calls javaFrontend.scan() |
| Python | Instantiates and calls PythonParser directly |
| Go | Creates SensorContextTester, CheckFactory, and CryptoGoSensor, then calls .execute() |
Note: SensorContext in SonarQube is the working environment provided to a plugin (e.g. sonar-java) during code analysis. It acts like a container that holds everything the plugin needs to perform its job such as access to the project’s source files, configuration settings, and execution state while also giving it a way to report results like bugs or issues back to SonarQube.
When developers build SonarQube plugins, they use testing utilities like SensorContextTester to test their code locally, without needing to start a massive SonarQube database or server just for a 5-second test. This tester class implements the exact same interface as the real server context, but keeps all files and settings temporarily in local computer memory.
cbomkit-lib relies on this testing infrastructure to act as a standalone application. It pulls in these testing utilities (like sonar-plugin-api-impl), creates a SensorContextTester in memory, and manually packs it with your project’s code files and configuration.
When cbomkit-lib hands this interface to the language parsers, the parsers see an object that successfully implements the SensorContext interface. This effectively tricks the parsers into believing they are running in the real SonarQube server, allowing them to happily analyze the code completely offline.
Detection Engine
The engine/ module provides the language-agnostic detection logic. It does not parse source code itself; instead, it operates on Abstract Syntax Trees (ASTs) provided by the language-specific SonarQube plugins (sonar-java, sonar-python, sonar-go).
A detection rule is defined by the IDetectionRule<T> interface, the core contract that describes what pattern in source code constitutes a cryptographic finding. Every rule answers three questions:
- What object type am I looking at? (e.g. javax.crypto.Cipher),
- What method is being called? (e.g. getInstance), and
- What should I do with the arguments? (extract them as IValue’s, tag them with an IDetectionContext, and optionally chain into child rules for nested calls).
The interface enforces this completeness through a staged fluent builder (DetectionRuleBuilder<T>) that won’t compile until all required fields are provided. For example:

The builder produces immutable IDetectionRule instances. During analysis, the detection engine evaluates each rule against AST nodes it encounters. When a rule matches, it extracts values from the matched arguments using IValueFactory — a factory function that converts a resolved AST expression (say, the string literal “AES”) into a typed IValue that the rest of the pipeline can work with. Parent rules may also declare dependent rules (child rules), forming rule trees that follow the real call graph: if the algorithm name itself comes from another method call, a child rule captures that too.
Tip: The I in IDetectionRule stands for Interface.
Detection Contexts
Every IDetectionRule carries an IDetectionContext that classifies what kind of cryptographic operation this match represents. This label is what lets the mapper downstream know what to do with the finding — the same string “AES” means something different when it appears in a cipher initialization versus a key generation call. Without the context, the translator would have no way to decide whether to create an Algorithm node, a SecretKey node, or something else entirely.
The context hierarchy is defined in engine/model/context/. The base DetectionContext stores arbitrary string properties in a map, allowing subclasses to carry additional structured data without breaking the interface contract. Key context types include:
| Context | Cryptographic meaning |
| CipherContext | A symmetric or asymmetric cipher operation |
| SecretKeyContext | A secret (symmetric) key |
| PublicKeyContext | A public key |
| PrivateKeyContext | A private key |
| KeyContext | A key of unresolved asymmetry |
| DigestContext | A hash or digest function |
| SignatureContext | A digital signature algorithm |
| MacContext | A message authentication code |
| KeyAgreementContext | A key agreement protocol |
| KeyDerivationFunctionContext | A key derivation function |
| PRNGContext | A pseudorandom number generator |
| AlgorithmParameterContext | Algorithm parameters (curves, specs) |
| ProtocolContext | A network or transport protocol |
Detection Store
When a rule matches an AST node, the result is recorded in a DetectionStore (DetectionStore.java). The detection store is the central intermediate representation of the detection phase.
DetectionStore<R, T, S, P> is generic over four type parameters:
| Symbol | Stands for | What it represents |
| R | Rule | The SonarQube rule object, each language has its own rule type (JavaCheck for Java, PythonCheck for Python). Every detection is tied back to one of these so SonarQube knows which rule triggered it and where to report the issue. |
| T | Tree | A node in the language’s AST. Method calls, variable declarations, string literals, everything the engine reads and matches against is a T. |
| S | Symbol | The semantic handle for a named entity in code. When the engine needs to follow a variable across statements to find its actual value, it uses S to look it up by name and scope. |
| P | Scanner context (Publisher) | The scan session object. It carries the current file, project config, and the channel through which the plugin reports issues back to SonarQube. |
The store holds:
- Extracted parameter values (detectionValues)
- Child detection stores (children)
- Optional invocation action (actionValue)
- Tree depth (level)
When a detection rule matches, DetectionStore.onReceivingNewDetection() is invoked. It runs the matched arguments through value factories and action factories to extract typed data from the AST.
IValueFactory<T> is responsible for extracting the cryptographic data from a matched expression — things like algorithm names, key sizes, modes, padding schemes, curves, and digest lengths. It is defined per parameter in the rule, so each argument position can have a different extraction strategy.
IActionFactory<T> is responsible for capturing the cryptographic intent of the call — whether this code is encrypting, decrypting, signing, verifying, or generating keys. Rather than a raw value, it produces an IAction, which represents what the code is doing with cryptography, not just what parameters it uses. The captured action is stored in actionValue, separate from the parameter values, so the translator can later determine both the what (the algorithm) and the why (the operation) of each detection.
The result is a tree of DetectionStore nodes mirroring the cryptographic call graph relevant to the detection (not the entire program call graph).
Hook System
Not all cryptographic parameters appear directly in a single method call. Some APIs use configuration objects, enums, or chained method calls to pass values such as key sizes, modes, or algorithm specifications. These cases cannot always be captured by a single detection rule. The hook system handles these indirect patterns.
A hook registers interest in a specific method invocation or enum constant. When that invocation appears anywhere in the relevant call context, the hook is triggered and its value is captured into the current DetectionStore.
There are two main types of hooks:
- Enum hooks: match enum constant accesses and extract the selected value.
- Method invocation hooks: match specific method calls and capture either:
- a resolved parameter value, or
- a resolved return value.
HookRepository manages active hooks during a scan. When the CallStackAgent records a method invocation, the repository checks whether any registered hook matches it. If a match is found, the corresponding value is added to the current detection result.
Note: CallStackAgent records method invocations during AST traversal and notifies the HookRepository when new calls are observed. This enables hooks to capture indirect cryptographic parameters. It also tracks visited nodes to prevent duplicate detections.
This mechanism allows the detection engine to capture cryptographic configurations that are defined outside the primary method call being analyzed.
For example:

The primary detection rule may match KeyPairGenerator.getInstance(“RSA”).
However, the key size (2048) is not passed directly in that call. It is embedded inside the RSAKeyGenParameterSpec object.
A method invocation hook can register interest in the constructor call of RSAKeyGenParameterSpec and capture the key size parameter. When kpg.initialize(spec) is analyzed, the previously captured value is associated with the detection.
DetectionExecutive
DetectionExecutive<R, T, S, P> (DetectionExecutive.java) manages the lifecycle of a detection run for a single source file. It:
- Keeps track of which detection rules should run and which ones have already matched.
- Collects DetectionStore results as rules are matched.
- It filters out partial matches and only keeps fully formed detection trees that actually contain extracted cryptographic values.
- Emits a Finding domain event for each completed detection.
Note: Partial matches like this, Cipher c = Cipher.getInstance(algorithmVar), are discarded if the algorithmVar is not resolved to a specific algorithm name.
Finding<R, T, S, P> (Finding.java) is simply a wrapper around the root DetectionStore and carries the detection level, rule bundle, and source location marker. Two findings are considered equal if their entire detection trees are identical. This prevents duplicate results when the same cryptographic usage is detected through different code paths.
DetectionExecutive publishes findings as events. Language-specific components (such as JavaInventoryRule) listen for these events and start the translation phase once a finding is received.
Note: InventoryRule is the SonarQube rule that drives the entire cryptographic analysis for a source file. When Sonar processes a file, this rule triggers the detection engine, receives completed findings, runs the translation pipeline, adds the resulting cryptographic nodes to the language aggregator for CBOM generation, and reports top-level assets such as algorithms, protocols, and keys as SonarQube issues in the UI.
Language Abstraction Interfaces
Each language module (Java, Python, Go) implements a small set of interfaces so the detection engine can work independently of the underlying AST types.
ILanguageSupport<R, T, S, P> is the top-level adapter that plugs a language into the engine. Adding a new language to this plugin means implementing ILanguageSupport. It is the single point that wires together every language-specific component: the detection engine instance, the AST method visitor factory, the enum matchers, and the scope helpers. The engine itself never references Java, Python, or Go directly; it only talks to ILanguageSupport and its children.
ILanguageTranslation<T> is the vocabulary layer between the engine and the AST. Every language encodes the same information differently in its syntax tree, a method name, a receiver type, a parameter list. ILanguageTranslation normalizes all of that: when the engine wants to know “what type is this method being called on?”, it asks ILanguageTranslation and gets a consistent answer regardless of whether the underlying tree is a sonar-java MethodInvocationTree, a sonar-python CallExpression, or a sonar-go CallExpressionTree. This is what makes the detection rules themselves language-agnostic.
IDetectionEngine<T, S> is the scanning core for each language. It takes AST nodes and applies detection rules against them, but beyond simple matching, it also resolves what variables contain. If a method is passed a variable rather than a literal, IDetectionEngine traces that variable back to its assignment (across scopes if necessary) to recover the actual value. This symbol resolution resolving values in inner scope, outer scope, and method return values is what lets the plugin detect cryptography even when code doesn’t use inline constants.
The concrete Java implementations (JavaLanguageSupport, JavaLanguageTranslation, JavaDetectionEngine) use the sonar-java SemanticModel for type resolution. The Python implementations use sonar-python’s symbol tables. Go uses sonar-go’s type resolution system.
Language Modules
Java (java/) rules are organized by API. There are rules for standard JCA classes (such as Cipher, MessageDigest, Signature, Mac, KeyGenerator, etc.) and separate rules for BouncyCastle’s lightweight API.
Python (python/) rules target the cryptography library (pyca). They are grouped by category (e.g., symmetric, asymmetric, hash, KDF, MAC). The module also includes helper logic to track variable assignments, since Python often stores crypto objects in intermediate variables before use.. The module also includes a resolve/ package with helpers for tracking symbol assignments across multiple statements, which is necessary because Python code frequently assigns configuration objects to intermediate variables before passing them to cipher constructors.
Go (go/) uses a custom CryptoGoSensor instead of a registrar because sonar-go does not provide a CheckRegistrar. The sensor directly processes Go files and applies detection logic. It also supports partial parameter matching to handle Go’s struct-based configuration style.
Stage 2: Translation
The mapper/ module is responsible for converting the engine’s DetectionStore tree into a structured domain model. This is done by the abstract base class ITranslator<R, T, S, P>, which provides the traversal logic and merging strategy while leaving the actual node construction to each language’s implementation.
ITranslator walks the DetectionStore tree using an internal Traverser. For every store node, it calls the language-specific translate(IBundle, IValue<T>, IDetectionContext, filePath) method which is the abstract part that each language implements.
When two branches of the tree produce nodes of the same type, ITranslator applies a merge strategy: a DETECTED value always wins over a DEFAULT or ENRICHED placeholder, preserving what the developer actually wrote. If two different DETECTED values conflict for the same slot (e.g., two distinct algorithm strings in an overloaded call), ITranslator creates separate root nodes to represent both possibilities, rather than discarding either.

Figure 3: Translation Phase
Translation Pipeline
Language modules do not call ITranslator directly. Instead, they extend ITranslationProcess<R, T, S, P>, which coordinates the full post-detection sequence in one place. ITranslationProcess holds the reorganizer rules for its language and exposes a single initiate(DetectionStore) method. This is what JavaInventoryRule calls when it receives a Finding from the engine. Inside initiate, the language module chains all three steps:
- ITranslator.translate(DetectionStore)– converts the DetectionStore tree into domain nodes ( INode)
- Reorganizer.reorganize(List<INode>)– restructures nodes using declarative rules
- Enricher.enrich(List<INode>) - adds metadata and derived properties
Separating these concerns into ITranslationProcess means that a language implementor doesn’t need to worry about orchestration, they implement the translate step, declare their reorganizer rules, and the rest follows automatically.
Translation Entry Point
ITranslator.translate(DetectionStore<R, T, S, P>) is the entry point of the translation phase.
Internally, it uses a Traverser to walk the DetectionStore tree. For each node in the tree, the traverser:
- Invokes the language-specific
translate(IBundle, IValue<T>, IDetectionContext, filePath) method to convert detected values into domain nodes (INode). - Applies merging logic to combine or override nodes derived from parent and child stores.
The result is a List<INode>, where each node represents a detected cryptographic asset prepared for reorganization, enrichment, and final serialization.
In the Java module, JavaTranslator delegates to context-specific translators based on the IDetectionContext. For example, detections with CipherContext are handled by JavaCipherContextTranslator, which uses JcaMapper to parse the algorithm string. Other contexts are mapped by dedicated classes such as JavaSecretKeyContextTranslator, JavaDigestContextTranslator, JavaSignatureContextTranslator, JavaMacContextTranslator, and JavaProtocolContextTranslator.
This design keeps mapping logic isolated per cryptographic category, improving modularity and maintainability.
INode Model
INode is the universal building block of the entire cryptographic model — every detected thing in this system, from an algorithm name to a key size to a protocol version, is an INode. Nodes form a typed tree: each node can hold exactly one child per child-type (enforced by the put(INode) method which replaces any existing child of the same type). The tree structure is what makes the final CBOM meaningful — an Algorithm node contains a Mode child, which contains a Padding child, which is how AES/CBC/PKCS5Padding becomes a structured object rather than just a string.
The model defines top-level cryptographic asset types:
| Node Type | Represents |
| Algorithm | A cryptographic algorithm (e.g., AES, RSA, SHA-256) |
| Key | A cryptographic key (secret or asymmetric) |
| Protocol | A network or transport protocol (e.g., TLS) |
| CipherSuite | A cipher suite within a protocol |
| Mac | A message authentication code construction |
| Signature | A digital signature scheme |
| KeyDerivationFunction | A KDF (e.g., HKDF, PBKDF2) |
| KeyEncapsulationMechanism | A KEM construction |
| EllipticCurve | A named elliptic curve |
Each node may contain child nodes that represent configuration properties, such as:
- Mode (e.g., CBC, GCM)
- Padding (e.g., PKCS1, OAEP)
- KeyLength
- BlockSize
- DigestSize
- NonceLength
- SaltLength
- ParameterSetIdentifier
- TagLength
Each node also carries a NodeOrigin enum value:
- DETECTED (from source code),
- DEFAULT (inferred from library defaults), or
- ENRICHED (added by the enricher)
NodeOrigin is used during merging. When two nodes of the same type conflict, a DETECTED node overrides a DEFAULT placeholder. This ensures that explicitly specified parameters in source code take precedence over inferred assumptions.
Mapper
Language-specific name parsers implement IMapper (IMapper.java):

IMapper is the bridge between the raw string the engine extracted from source code and the structured INode the model needs. Different cryptographic ecosystems name the same algorithms completely differently, for example, JCA uses “AES/GCM/NoPadding”, BouncyCastle uses “AES/GCM” with separate padding, and pyca uses a class called AESGCM with no padding field at all. Each IMapper implementation knows the naming conventions of one ecosystem and normalizes them into a consistent INode tree that downstream stages can process uniformly:
- JcaMapper – parses JCA algorithm strings such as “AES/GCM/NoPadding”, splitting on / to extract cipher, mode, and padding
- BcMapper – parses BouncyCastle algorithm names, which use a different naming convention for the same algorithms
- PyCAMapper – parses pyca/cryptography class names (e.g., AES, AESGCM, PBKDF2HMAC)
- GoMapper – parses Go crypto/* package identifiers
These mappers are called from within the language-specific translate() implementations. They normalize ecosystem-specific naming conventions and produce consistent INode structures for downstream processing.
Detection Location Tracking
Every INode carries a DetectionLocation record (DetectionLocation.java) with fields: filePath, lineNumber, offset, keywords (a list of detected string literals), and bundle. This metadata is preserved through translation, reorganization, enrichment, and output, so that the final CBOM component retains a reference to the precise source location where the cryptographic usage was found.
Stage 3: Reorganization
After translation and before enrichment, the mapper module applies a reorganization pass using Reorganizer.java. The translation phase produces a relatively flat set of INode objects. Reorganization restructures these nodes into the final semantic tree shape expected by the enricher and output module.

Figure 4: Reorganization Phase
The reorganizer executes a sequence of IReorganizerRule instances. Each rule is a self-contained unit that targets one specific structural pattern. It has two parts that work together:
- An IFunctionDetectionCondition – a predicate that inspects a node, its parent, and the full root list, and returns true only when this rule should fire. This is the “when does this apply?” check, evaluated before any restructuring takes place.
- An IFunctionPerformReorganization – the actual transformation that runs when the condition is satisfied. It receives the same three arguments and returns the updated root list after moving, merging, or reparenting nodes.
This two-part design keeps conditions and transformations cleanly separated and makes rules easy to read, test, and compose. A rule does exactly one thing and does it only when its condition is met, the Reorganizer handles applying them all in sequence.
Concrete reorganizer rules include:
| Rule | Purpose |
| BlockCipherReorganizer | Combines a symmetric cipher with its mode and padding into a single Algorithm node |
| AeadBlockCipherReorganizer | Handles AEAD modes (GCM, CCM, EAX) where the tag is part of the mode |
| SignatureReorganizer | Combines a signature algorithm with its underlying hash function and elliptic curve |
| KeyDerivationReorganizer | Attaches salt, iteration count, and output length to a KeyDerivationFunction node |
| MacReorganizer | Merges a MAC algorithm with its block size |
| KeyReorganizer | Attaches key length and curve parameters to a Key node |
| CipherParameterReorganizer | Handles miscellaneous parameter attachments |
The reorganizer enables the translation stage to remain context-independent (each detection produces a simple node) while still producing the rich, nested tree structure that accurately represents the cryptographic asset.
Stage 4: Enrichment
The enricher/ module adds metadata to the INode tree that cannot be directly observed in source code but is defined by the cryptographic specifications or library defaults.
The Enricher class (Enricher.java) provides a static enrich(Collection<INode>) method that traverses the node tree and dispatches each node to a type-appropriate implementation of IEnricher. Each IEnricher implementation is responsible for one algorithm or algorithm family. Its single method, enrich(INode) receives the node, inspects what information is already present, and fills in only what’s missing. It never overwrites DETECTED values; it only adds ENRICHED nodes to fill gaps left by the source code. This makes the enrichment step purely additive and safe to run unconditionally.

Figure 5: Enrichment Phase
Examples of enrichment:
- AESEnricher : sets the default key length to 128 bits when not specified
- RSAEnricher : attaches the OID for RSA and default key lengths
- SHA2Enricher : maps algorithm variant names to their digest sizes
- HMACEnricher : derives key size requirements from the underlying hash function
- PBKDF2Enricher : associates default iteration counts and output lengths
- SignatureEnricher : resolves signature algorithm components from combined names (e.g., SHA256withRSA)
- KEMEnricher : adds parameter set identifiers for post-quantum KEM algorithms
- TagOrDigestEnricher : computes tag or digest size from the algorithm variant
All derived values are marked with NodeOrigin.ENRICHED, keeping them distinguishable from DETECTED and DEFAULT values. The enrichment process is recursive. Child nodes are enriched independently, ensuring that nested structures such as an Algorithm containing a KeyDerivationFunction, are fully enriched throughout the tree.
Stage 5: Serialization
Serialization occurs after all source files have been analyzed, translated, reorganized, and enriched.

Figure 6: Serialization Phase
Cross-File Result Accumulation
SonarQube analyzes source files independently, so cryptographic findings must be aggregated before serialization. Each language module maintains a static aggregator:
- JavaAggregator – collects List<INode> from all analyzed Java files
- PythonAggregator – same for Python
- GoAggregator – same for Go
Each aggregator holds a static List<INode> and is reset at the start of each scan. Translated and enriched nodes from each file’s ITranslationProcess are added to the aggregator after the file-level translation is complete.
Post-Scan Job
After all source files are analyzed, OutputFileJob (OutputFileJob.java) is invoked as a SonarQube PostJob. It retrieves the accumulated INode lists from all language aggregators, adds them to a CBOMOutputFile, writes the CBOM JSON to disk, and prints scan statistics (component counts, timing, output path).
CBOM Generation
The output/ module converts the enriched INode tree into a CycloneDX-format CBOM document.
CBOMOutputFile (CBOMOutputFile.java) accumulates INode instances via its add(List<INode>) method and serializes them when saveTo(File) is called. Internally it uses the cyclonedx-core-java library targeting schema version 1.6.
Node-to-component conversion is handled by dedicated builder classes:
- AlgorithmComponentBuilder – converts Algorithm nodes into CycloneDX CryptoComponent entries of type algorithm, attaching mode, padding, key length, OID, and other properties
- ProtocolComponentBuilder – converts Protocol and CipherSuite nodes into structured protocol components with nested cipher suite entries
- RelatedCryptoMaterialComponentBuilder – handles related cryptographic material such as keys and certificates
Each component is assigned a stable BOM reference. Dependencies between components (for example, a protocol depending on specific algorithms) are recorded in the CycloneDX dependencies section, enabling downstream tools to analyze relationships between cryptographic assets.
End-to-End Example

Figure 7: End-to-End Example; Pre-requisite Note: The source code has already been parsed by the Java analyzer (sonar-java) and converted into an AST.
- Detection: The Java sensor visits the AST and the primary rule matches Cipher.getInstance(“AES/GCM/NoPadding”). A child rule also tracks the related Cipher.init(int, Key) call to extract information about the key.
- Detection Store Construction: The detected values are organized into a DetectionStore, with the algorithm information stored at the root and the related key information represented as a child store.
- Finding Generation: DetectionExecutive determines when the required detection rules have been evaluated and emits a Finding containing the completed detection store.
- Translation: JavaTranslationProcess traverses the detection store and delegates each context to the appropriate translator. The cipher context is mapped through JcaMapper, which parses AES/GCM/NoPadding into an Algorithm node with detected mode and padding values. The key context is translated into a Key node with its detected key information.
- Merging: The translated nodes are merged to preserve the relationship between the algorithm and its associated key. Detected values from child stores replace any default values inferred during translation.
- Reorganization: Reorganizer rules restructure the translated nodes into the expected semantic tree. BlockCipherReorganizer handles the cipher, mode, and padding relationship, while KeyReorganizer ensures that the key is correctly nested.
- Enrichment: The resulting tree is recursively enriched with additional cryptographic metadata. For AES-GCM, this includes information such as the AES OID, key length, and authentication tag length.
- Aggregator: The enriched INode tree is added to the JavaAggregator, which collects cryptographic components from the analyzed Java files.
- CBOM Generation: After scanning is complete, OutputFileJob collects the accumulated nodes and passes them to CBOMOutputFile, which serializes the results into a CycloneDX-compatible CBOM JSON document.
Conclusion
The Sonar Cryptography Plugin is designed around a language-agnostic detection and translation pipeline. Language-specific modules handle the differences in source-code structure and cryptographic APIs, while the core engine, mapper, reorganization, enrichment, and output stages provide a common path from source-code detection to a structured CBOM. This separation allows the plugin to support additional languages and cryptographic libraries without changing the core detection and CBOM generation logic.
Further Reading and Contributing
The same architecture also makes the plugin extensible. Detection rules can be added for cryptographic libraries supported by an existing language, while a new language can be integrated by implementing the required language-specific interfaces and connecting it to the existing detection and translation pipeline.
For adding detection rules or supporting a new cryptographic library, see DETECTION_RULE_STRUCTURE.md. For adding support for a new programming language, see LANGUAGE_SUPPORT.md.
The project is open source, and contributions are welcome. If you work with cryptographic libraries, security tooling, or applications that use cryptography, you can also help shape the future direction of CBOMKit by sharing your experience through the CBOMKit Community Survey.
Adding a New Language
Follow LANGUAGE_SUPPORT.md to implement support for a new programming language. The architecture is designed to be extensible, allowing new languages to be added by implementing the required interfaces and integrating with the detection engine and translation pipeline.
Contributed By:
Shubham Kumar
Shubham Kumar, an LFX Fall 2026 Mentee with a background in applied cryptography and telecom networks, is an active open-source contributor. He serves as an Advisory Committee representative at the OpenSSL Foundation, the NgKore Foundation, and the LF Networking ONAP project. He is passionate about learning new things in open source and contributing back to the ecosystem.