Interim JSON Format
The default output format containing detailed cryptographic asset information optimized for the SCANOSS ecosystem. The interim report is the primary findings artifact. It contains finding metadata such asfinding_id and optional structural occurrence_key, but it does not embed the finding-centric reachability slices produced by --export-callgraph.
Format Specification
Note: Version 1.1 introduced therulesarray field (replacing singlerulefield) to support per-line deduplication. Version 1.2 addedsourceanddependency_infofor dependency scanning attribution. Version 1.3 addsfinding_idfor cross-referencing with the callgraph export. Version 1.5 adds optionaloccurrence_keyfor canonical findings, using AST call evidence when available and a deterministic file/module-level fallback for valid top-level calls. Dependency-backedfile_pathvalues are dependency-root-relative; the package identity stays independency_info. Reachability slices such ascall_chainsare emitted by the dedicated call graph export, not by the interim report. See Dependency Scanning for details.
Field Descriptions
Public Go Contract
Go consumers can importgithub.com/scanoss/crypto-finder/pkg/schema to read or write the interim report without importing implementation packages. InterimFormatVersion is currently "1.7".
The report always emits version, tool, and findings. rules is a value field and currently emits as {} when empty. Findings always emit file_path, language, and cryptographic_assets. Assets always emit start_line, end_line, match, rules, status, and metadata; start_col, end_col, parameter_conditions, conditioned_value, terminal_start_col, terminal_end_col, oid, finding_id, occurrence_key, source, dependency_info, and direct purl are omitted when empty. Rules always emit id, message, and severity; version is omitted when empty. Dependency metadata always emits module and version when present.
The report preserves its JSON vocabulary: severity is INFO, WARNING, or ERROR; status is pending, identified, dismissed, or reviewed; and source is direct or dependency. Valid rule package URLs are promoted to top-level purl for direct findings. Dependency findings keep package identity in dependency_info.purl; unknown ecosystems omit it, and missing versions produce versionless package URLs. CryptographicAsset accepts the legacy singular rule input and migrates it to rules only when rules is absent or empty. When both are supplied, rules takes precedence. Internal terminal-column fields never serialize.
Call Graph Export
When--export-callgraph <file> is passed, Crypto Finder also writes a separate finding-centric call graph JSON file to <file>. This export contains the reachability slices and value-flow details associated with findings from the interim report.
Schema note: local CLI callgraph exports default to interned 6.17. Consumers must join identity through functions[] and call_chain_indexes; use --export-callgraph-interned-frames=false for inlined 6.14. SDK/stitch zero-value options remain 6.14. Version history:
-
6.17(local CLI default; SDK opt-in) adds optional fields to the interned render.finding_graphs[].dependencynames the dependency a dependency finding sits in and its route from the application in the resolved dependency graph, withwithout_sourceon a path step parsed without source.finding_graphs[].analysisgainspaths_total,paths_kept,route_evidenceandno_callers_only. The first frame of each live chain carriesroot_kind(main,framework_entry,no_callers,depth_limit).unresolved_reasongains three values for an attributed finding whose verdict isunknown:traversal_truncated,unresolved_dispatchanddependency_without_source; the stitched export now writesunresolved_reasontoo, withunresolved_dispatchonly. Every new field is optional, so a6.16reader that ignores unknown properties keeps working; a reader that switches onunresolved_reasonmust treat an unknown value as unknown. Frame shape is the6.15interned shape. The stitched export now also writesroot_kindandno_callers_only, when the fragments recorded entry kinds (graph-fragment-1.14). -
6.16(SDK opt-in; the CLI default before6.17) adds optionalscan_metadata.ecosystemsto the interned render. A target with first-party findings in more than one supported ecosystem, such as a Java service beside a TypeScript front end, gets one call graph per ecosystem, and each finding resolves against the graph of its own language. The list names every graph analyzed, the primary first, each withecosystem,root_module,function_countandedge_count.scan_metadata.ecosystem,root_module,function_countandedge_countkeep describing the primary graph, the one dependencies resolve for, so a consumer that reads only those fields is unaffected. The list is absent for a single-ecosystem export, which is otherwise unchanged, and from the inlined6.14compatibility render. Frame shape is the6.15interned shape. -
6.15(SDK opt-in; the CLI default before6.16) contractscall_chainsframes: they no longer repeat catalog identity fields (function_name,file_path,canonical_signature,dependency_info, and the rest of the interned identity). Join throughfunctions[]plusfinding_graphs[].call_chain_indexes, and throughcanonical_signatureoncrypto_entry_pointsand catalog rows. Hop-specific fields (entry_call,crypto_call,entry_resolution) stay on the frames. Enable with--export-callgraph-interned-framesorScanMeta.InternedFrames. Zero-value SDK/stitch stays on6.14; the CLI defaulted to6.15until6.16. Parsers that only readcall_chains[]objects must gate onschema_versionor callHydrateChainIdentities. -
6.14adds a top-level internedfunctions[]catalog andfinding_graphs[].call_chain_indexes(0-based positions into that catalog) beside the existing inlinedcall_chains. The index lists reconstruct the same N-sample of routes. Inlined frames keep their previous field names and meaning.crypto_entry_pointsis still the complete reverse-reach set. Both surfaces can explicitly select this compatibility render. -
6.13addscall_chains[].entry_resolutionandentry_declared_type, reporting how the call arriving at each frame was established. -
6.12adds the rule-vs-callgraph key-length conflict marker tosupporting_calls[].supporting_call.resolved_key_length. When a detection rule declares a statickeyLengthand the callgraph resolves a different value for a finding referencing that evidence, the resolvedbitsstay primary, the rule value is retained asrule_declared_bits, andrule_conflictistrue. Agreement, an unresolved key length, and a rule that declares nokeyLengthall leave both fields absent. The marker is computed during the scan, so consumers read it directly instead of re-deriving it from rule metadata. -
6.11adds optionalsupporting_calls[].supporting_call.resolved_key_lengthfor structurally derived Java key-generation configuration calls referenced byfinding_graphs[].supporting_call_ids. It contains raw integerbitsonly when static analysis resolves a literal or simple propagated constant,provenance(constantorunknown), and requiredsource_call(function_name,line,parameter_index) for the contributing argument. It is preserved by live, graph-fragment, and stitched callgraph exports; terminalcrypto_callrecords do not carry it, and it does not populate CBOM properties or express a security threshold. -
6.10carries an optional top-levelpurlfor direct findings and an optional canonicalpurlinside dependency context. Live and stitched exports derive dependency URLs from the scan ecosystem and existing module/version fields, so cached graph fragments gain the field without a fragment schema change. -
6.10adds optionaloccurrence_keyto canonical finding graphs; it is propagated from the interim report and graph fragments. AST anchors are preferred, with a deterministic file/module-level fallback for valid top-level calls. When present,(finding_id, occurrence_key)identifies the structural occurrence. Every finding graph of a current scan carries one, including synthesized entry points and matches with no call to anchor; only legacy records omit it and continue usingfinding_idalone. -
6.9makescrypto_entry_points[]a reverse-reachability answer rather than a projection of the exportedcall_chains: every function that reaches a finding is listed, so an entry point no longer depends on which chains survived collapsing or the per-finding chain budget.chain_depthis consequently the true minimum frame distance, and some depths are smaller than the same export previously reported. No field was added or removed. Live and stitched paths agree on both the index and the enumerated routes. -
6.8addsfinding_graphs[].reachability(reachable/unreachable/unknown/not_applicable, where a self-chain fallback never counts and suppression or a depth limit that cut every route downgrades tounknown),finding_graphs[].analysis(call_chains/parameterscompleteness), andcrypto_entry_points[].root— the explicit chain-root classification, the index itself staying deliberately broader. The legacyreachablebool is unchanged. -
6.7addsreachableto each finding graph when dependency scanning makes user-code reachability applicable; it is omitted for a standalone library scan. -
6.6adds deterministicforward_calls.ambiguous_callsgroups for fail-closed interface dispatch: completeness state, stable group/candidate IDs, complete callable identities, and preserved call-site argument provenance — without promoting ambiguous candidates to resolved edges. -
6.5makesrole: operationcontract methods supporting-call-only: they are exported as categorizedsupporting_callsreferenced bysupporting_call_ids(including interface-authored contracts resolved to concrete implementations) and are no longer synthesized as operation-onlycrypto_entry_pointsin live, fragment, or stitched exports. -
6.4addsmethod_role,role_provenance, andparameter_roles— contracts-KB-derived method/parameter role classification (factory/config/output/operationonmethod_role; per-parameteroperation-determining/metadata-contributing/nonewith acontributes: {property, derivation}block onparameter_roles).method_role/role_provenanceappear oncrypto_entry_points;parameter_rolesappears oncrypto_entry_pointsand on the supporting-call declaration, index-aligned withparameter_types— never on call-site parameter literals. These fields are populated on the live (--export-callgraph) and graph-fragment export paths and carried through the stitched/served path. -
6.3added the optional per-findingforward_callsblock — the finding anchor’s forward call closure (deduped nodes withdepth/crypto_relevant/supporting_category, plus traversed edges whoseentry_callcarries the call-site argument data-flow), emitted only when the stitch runs withStitchOptions.ForwardClosure; caps (depth/nodes/edges) are surfaced viamax_depthand an explicittruncatedflag, never silently. -
6.2exposedsupporting_calls,crypto_entry_points, andgraph_algo_versionend-to-end. -
6.0removed the legacyentry_point_indexprojection and madecrypto_entry_points[]canonical. -
4.3added Java runtime provenance inscan_metadatafor JDK-aware platform signature enrichment. -
Each top-level record preserves
finding_id. Whenoccurrence_keyis present, the composite(finding_id, occurrence_key)identifies the structural occurrence and joins it back to the interim report. Every finding graph of a current scan carries it, entry-point findings included; only legacy records withoutoccurrence_keyusefinding_idalone. -
call_chainsis the primary value-flow structure. Each chain is ordered from the first reachable caller to the function that contains the matched crypto call. The array is a capped sample, not the complete reverse-reach set. Local CLI defaults to 8 paths;--export-callgraph-max-chains 128opts in to a larger sample. SDK/stitch zero or omittedStitchOptions.MaxChainsremains 128. The live export has no depth cap. The sample is spent on the strongest evidence first (seeanalysis.route_evidence): every route whose calls all resolved statically, then routes that need a dispatch edge, then routes that need aname_onlyedge. Within each of these, it goes to distinct routes first: one shortest route per chain root, recognized entry points before other roots, then further routes, and only then the same route at another call-site line, the most certain call site first. The route that justifies the verdict is therefore always among the kept chains. Where frames are attributed to dependencies, distinct routes of every tier come before variants of any tier: tier by tier, one route per chain root and one route through each library whose sequence of libraries no kept chain shows yet, and only then further routes. A small budget therefore shows the libraries a weaker tier reaches rather than more variants of a stronger tier’s one path, and the first chain is still the strongest route. The stitched export orders its chains the same way by evidence, direct routes before dispatch routes. -
With user code known (dependency scanning, or
--export-callgraph-project-reachability), a chain walks back through application code until it reaches an application function that no application code calls, instead of stopping at the first application frame. An application function follows only its application callers; library code follows every caller. The function holding the crypto follows every caller, so crypto in an application callback is reached through the library that calls it back. The first frame of each chain carriesroot_kind:main(a Javastatic main(String[]), or a free function namedmain),framework_entry(a Java method that is an@Overrideof a method of a type outside the application, such asHttpHandler.handleorRunnable.run, or one annotated with a Spring or JAX-RS request mapping,@Scheduled,@EventListener, or a Kafka, RabbitMQ, JMS or SQS listener imported from its framework),no_callers(any other application function nothing in the application calls; on a library scanned alone, a graph root), ordepth_limit(where a depth limit stopped the walk, SDK callers only). A cycle of application functions nothing outside it calls is rooted at its first member. The function holding the crypto is itself a root only when it is recognized asmainorframework_entryand no application code calls it: a handler doing crypto inline readsreachable, a helper nothing calls readsunreachable.crypto_entry_points[].rootfollows the same roots. -
Entry point rules beyond those above. Which frameworks count, and the names each declares, is data: the entry-point catalog, one YAML file per framework under
internal/callgraph/entrypoints/<language>/<framework>.yaml, built into the binary. An entry names the packages the framework comes from (from, a subpackage matches too), the names it declares, the shape the parser recognizes, and theroot_kindit gives. A name counts only when the file brings it into scope from that package, never by the bare name: a Java annotation resolves through the file’s single-type import, else its on-demand imports, else its own package, or is written qualified; a TypeScript or JavaScript decorator through its import, and a router through the import or declaration its value comes from (app = express(),require('fastify')(), a parameter typedExpress); a Python decorator, callee or base class through its import or the module-level assignment its object comes from (app = Flask(__name__), a function@click.group()makes); a Go receiver through its import, the declaration in scope (r := chi.NewRouter(), a parameter typedchi.Router) or a struct field of the file, and a method value through its receiver’s type. Shapes:decorator(an annotation or decorator on the function),registration_call(a call that passes the handler, inline or by name, directly or through one wrapper call;path: requiredasks for a path or method argument before it, or on the route it is chained to),handler_field(a field of a framework struct literal,&cobra.Command{RunE: run}),supertype(a method of a type that extends, implements or embeds a framework type, also through the application’s own supertypes; a Python entry may adddirectoryto count only files under a directory of that name, and name<clinit>for the class body: the body of a DjangoMigrationunder amigrationsdirectory is an entry point, since the migration loader runs it and reaches theRunPythoncallbacks from there),interface_method(a Go method with the name and parameter types of a framework interface method,ServeHTTP(http.ResponseWriter, *http.Request)),server_registration(Go: a call of a generatedRegister<Service>ServerorRegister<Service>HandlerServerfunction, whosenamesare patterns; the methods of the value passed as its last argument that belong to the service become entries, found from the interface when the scanned tree declares it, else by gRPC signature) andfile_convention(an export a framework finds by the file’s location, counted only when the nearestpackage.jsondeclares the framework). The catalog ships Java: Spring (MVC and WebFlux handler methods,@Bean, schedules, application events, STOMP, JMS, and its filter and servlet bases), Spring Kafka, Spring AMQP, Spring Cloud Stream and AWS SQS, Spring Integration, JAX-RS, Jakarta Annotations (@PostConstruct,@PreDestroy), Servlet, WebSocket, EJB timers, Micronaut, Quarkus and MicroProfile Reactive Messaging; TypeScript and JavaScript: Express, Koa (koa,@koa/router,koa-router), Fastify, Hono, NestJS and Next.js; Python: Flask, Flask-RESTful, FastAPI, Starlette, Sanic, Django (views, signal receivers, migrations), Django REST framework, Celery, click, typer, Dramatiq and RQ; Go: net/http, chi, gin, echo, gorilla/mux, cobra and gRPC. To add a framework, add its file and a fixture underinternal/scan/testdata/entry_rules/<language>/with a case ininternal/scan/entry_rules_export_test.go; the loader rejects an unknown field, a shape the language’s parser does not recognize, a field the shape does not read, and a name two entries both claim for the same package. Language semantics that belong to no library stay in the parsers: TypeScript and JavaScript: the exports of the module the nearestpackage.jsonnames inmain,moduleorexports(dist/,build/,lib/,out/,esm/,cjs/andes/paths also match the same path undersrc/and at the package root), andmainfor the top level (<module>) of that module, of a modulebinnames, of a module apackage.jsonscript runs withnode,nodejs,tsx,ts-node,bunordeno(tsx scripts/seed.ts, also after&&,||,;or|, with the samedist/tosrc/mapping), of a.js,.mjs,.cjs,.ts,.mtsor.ctsfile whose first line is a shebang naming one of those runtimes, and of a module withif (require.main === module); Python:mainfor the functions the nearestpyproject.toml([project.scripts],[project.gui-scripts],[tool.poetry.scripts]),setup.cfg([options.entry_points]) orsetup.pynames asmodule:function, and for the top level (<module>) of a module withif __name__ == "__main__":; Go:mainforinitfunctions and the synthetic<varinit:…>package variable initializers, which the runtime runs beforemain. Three kinds of call have no call expression and are now call edges: a JSX element with a capitalized name calls that component, a JSX attribute that names a function (onClick={handleClick}) calls it, and a function written inline as a call ornewargument, or as a JSX attribute value, is called by the function that writes it, except a route handler, which is an entry point instead. These edges never serve as a finding’s matched crypto call. Test code is never an entry point in any language: a method with a JUnit or TestNG annotation, or a function in a file undertest/,tests/or__tests__/or named like a test (*Test.java,*_test.go,test_*.py,*_test.py,conftest.py,*.test.*,*.spec.*), keepsno_callers. Test sources are in the graph only under--include-tests. -
analysis.paths_totalis the number of distinct routes, from a chain root to the finding, the graph holds, andanalysis.paths_keptthe number the finding’scall_chainsshow; chains that differ only in a call-site line count once.paths_totalsaturates at the largest int64 and is then a lower bound. Both are present on live findings with a chain: reachable ones andunresolved_dispatchones.analysis.call_chainsispartialwhenever routes were left out or a depth limit cut the walk. There is no path-count ceiling any more: a finding with millions of routes gets its sample of chains and readsreachable, notunknownwith no chains. A depth limit that cuts every route before a root readsunknownwithunresolved_reasontraversal_truncated; that reason, unlike the others, comes withoutfinding_location, since the finding is attributed. A finding whose every route crosses aname_onlyedge readsunknownwithunresolved_reasonunresolved_dispatch, also withoutfinding_location: its chains are exported as evidence of how the crypto may run, and the legacyreachableflag is omitted. One route free ofname_onlyedges keeps itreachable.analysis.route_evidencerates the finding’s strongest route by its weakest call:direct(every callexactor statically resolved),dispatch(the best route needs at least oneinterface_dispatchorpython_subclass_dispatchedge and noname_onlyedge; the finding staysreachable, and the dispatch hop is the frame whoseentry_resolutionnames it), orname_only(every route needs aname_onlyedge; the verdict isunknownwithunresolved_dispatch). A finding specialized by aparameter_conditionspredicate that removed chains ratesroute_evidenceandno_callers_onlyfrom its surviving chains only, so a condition that refutes the direct route cannot leavedirectbehind; with no chain left both are absent. The verdict follows the same chains: a specialized finding whose surviving chains all cross aname_onlyedge readsunknownwithunresolved_dispatch, even when another value of the same function has an exact route. A value reached only from a caller nothing calls keeps the verdict any finding so reached has (reachable, flaggedno_callers_only). It is present withpaths_totalon the live export, and on stitched findings that arereachablethrough a chain orunresolved_dispatch; a stitched finding proven only by a dependency’s mine-time entry-point index has no frame-by-frame route and carries none.analysis.no_callers_onlyistruewhen every root of the routes that support the verdict hasroot_kindno_callers: the crypto is reached only from application code nothing in the application calls, not from a recognizedmainor framework entry. The supporting routes are those free ofname_onlyedges, or every route when none is, and all of them count, not only the kept chains. It is omitted when false. The stitched export sets it the same way, over every root that reaches the finding (not only the kept chains), but only when the root component’s fragment recorded entry kinds (graph-fragment-1.14): a root of an older fragment could be a framework entry the scan could not mark, so such a finding never claims it, and its chains carry noroot_kind. On the stitched export a root readsmainorframework_entryfrom the fragment’sentry_kind, andno_callerswhen it has none and nothing in the stitched graph calls it;depth_limitnever appears there. A finding proven only through a dependency’s entry-point index has no route and does not claim it. The stitched export, which follows noname_onlyedge, reads such a findingunknownwithunresolved_reasonunresolved_dispatchand no chain, and a dependency’s mine-time entry-point index does not upgrade it toreachable; it sets no otherunresolved_reason. CLI omits the optional complete reverse-reach index unless--export-callgraph-entry-points=true; sample budgets do not filter that index. Policy: ADR 0002. -
functions[](schema6.14+) is the interned identity catalog for those emitted routes.finding_graphs[].call_chain_indexeslists the same walks as integer positions into that catalog. Schema6.14also inlines those fields oncall_chainsframes. Schemas6.15,6.16and6.17(local CLI default) join identity through the catalog alone. -
Each chain node carries hop-specific fields: optional
entry_call,entry_resolution, andcrypto_callon the last node. Compatibility schema6.14also inlines function identity (function_name,file_path,canonical_signature,dependency_info) on each frame. The interned render (6.15+) keeps that identity only infunctions[]. -
entry_calldescribes how execution entered the current node from the previous step. Itsfile_pathandlinerefer to the call site in the previous node’s source file. -
entry_resolutionsays how the edge from the previous step was resolved:exact(the receiver’s static type and the call’s argument types identify the target, including a method inherited from the nearest declaring superclass, and a declared function handed to a known callback-invoking API such asthreading.Thread(target=fn),sort.Slice(xs, fn)orsetTimeout(fn, 1), reached from the function that registers it),interface_dispatch(an interface or abstract-class call expanded to a method of a real subtype ofentry_declared_type, from the bytecode hierarchy or the parsed extends/implements clauses),python_subclass_dispatch, orname_only(a match by method name and arity with no type proof: a fluent-chain fallback, or a dispatch candidate whose class hierarchy is only partly known or names a type whose simple name several known types share, kept because nothing proves it is not a subtype; with dependencies scanned, such a candidate is kept only when its artifact is the declared type’s, is the project, or depends on the declared type’s artifact). A Java type name is treated as certain only when the source spells it fully qualified, or when the file declares no type parameter or local class of that name and at most one type of that simple name exists in the scanned source and indexed bytecode; Java’s scoping rules are not modeled beyond that. The graph cannot see a subtype relation it has no record of, for example one through a member type inherited from a supertype in an unindexed jar when a graph type of the same simple name sits in the caller’s package, so such a gap can still hide a path. The local CLI export and the stitched export both fill it; it is absent on a chain’s first frame. -
The last node in each chain carries
crypto_call, which is the matched crypto-relevant call for the finding. -
entry_call.parameters[]andcrypto_call.parameters[]both use the same parameter model:parameter_index(always0-based), best-efforttype,argument_expression,resolved_value,variable_namefor simple identifiers only, and recursivesource_nodes. -
supporting_calls[].supporting_call.resolved_key_lengthis optional evidence scoped to structurally derived key-generation configuration calls, currently the JCA set:javax.crypto.KeyGenerator.init(int),java.security.KeyPairGenerator.initialize(int[, SecureRandom]), and theRSAKeyGenParameterSpec,ECGenParameterSpec, andSecretKeySpecconstructors whose value reaches such a call.source_call.function_namenames the call the size was read from, which is the spec constructor when the size travels through a parameter object. Join the supporting declaration to a finding throughfinding_graphs[].supporting_call_ids. It reports raw key bits when known and otherwise retainsprovenance: "unknown"plussource_call; consumers must not infer a key-size threshold from it. The terminalcrypto_callremains the detected operation and does not carry this field. -
supporting_calls[].supporting_call.resolved_key_length.rule_conflictreports that a detection rule declared a statickeyLengthdisagreeing with the resolvedbits. The resolved value remains primary and the rule value is preserved inrule_declared_bits, so neither side is lost. Both fields are absent when the two agree, when no key length was resolved, or when no rule declared one. A supporting call is shared by every finding that reaches the same crypto object, so the marker is a property of that shared evidence: it means at least one referencing finding declared a different key length, not that every one did, and it does not identify which. When several referencing findings disagree,rule_declared_bitsreports the smallest disagreeing value, which keeps the output stable regardless of rule ordering. Per-finding attribution is not recoverable from this field. -
For Java scans,
scan_metadatamay also includejava_requested_jdk_major,java_runtime_version,java_platform_signatures_used,java_platform_signature_source, andjava_platform_signature_unavailable_reasonto show which JDK major was requested and whether JDK platform signatures contributed to type enrichment. -
source_nodescan span multiple wrapper hops. A localPARAMETERnode may contain nested upstream provenance such asPARAMETER -> PARAMETER -> VALUE, and propagated nested nodes keeplocation.file_pathpluslocation.linewhen known. -
Method-call provenance is preserved as
CALL_RESULTnodes. When the parser can resolve the invoked method, the node also exportscall_target, and any traceable receiver value is nested under thatCALL_RESULTviasource_nodes(for exampleCALL_RESULT -> PARAMETER alg -> VALUE SignatureAlgorithm.HS256). -
finding_graphs[].reachabilityisnot_applicablefor every finding of a scan that resolved no dependency set, because such a target is read as a library scanned on its own, where a function nothing calls is public API rather than dead code.--export-callgraph-project-reachabilitytreats the target as an application instead: its own source packages become the user code, the same set a--scan-dependenciesrun uses for first-party findings, so first-party verdicts match that run. First-party code that only a dependency reaches, through a callback or an interface the dependency calls, readsunreachablehere because the dependency’s code is not in the graph;--scan-dependenciesfollows it. A scan that resolved dependencies ignores the flag, and--export-graph-fragmentis unaffected. -
A finding whose rule carries
parameter_conditionskeeps only the chains whose matched call satisfies them, reading each argument as the chain’s caller passes it. A chain is dropped when a resolved argument contradicts a condition; a chain whose arguments did not resolve is kept when no chain matched. When every route was examined and every one contradicts the condition, the finding readsunreachable(reachable: false, nocall_chains): the crypto the rule describes runs on no known route, and the graph fragment’scrypto_entry_pointsdo not list it either. When the chain budget (--export-callgraph-max-chains) left a route or a call site unexamined, it readsunknownwithunresolved_reason: "traversal_truncated"andanalysis.call_chains: "partial". -
A dependency finding carries
finding_graphs[].dependency, on the live and stitched exports: the dependency it sits in (module,version,purl, the versioned package URL),relationship(directwhen the application declares it,transitiveotherwise) andpath, its shortest route from the application in the resolved dependency graph, not in the call graph.pathlistsmodule,versionandpurlper dependency, a direct dependency first and the finding’s own dependency last; the application itself isscan_metadata.root_moduleand is not listed. Where several routes exist, one through dependencies parsed with source is preferred over a shorter one that is not, and ties break on module name. A module the resolution picks at more than one version (npm can install one package twice) is told apart by version: with a versioned dependency graph each copy has the route it really has, solodash4.17.21 can bedirectwhile the copy nested under another package istransitive, and each path step names its ownversion. Without one,relationshipandpathare absent for a finding in such a module, and for any dependency whose route crosses one, rather than reporting a route that may belong to the other copy;purlandversionstay.relationshipandpathare absent when the dependency graph was not resolved (for example a Maven tree that timed out) or does not connect the dependency to the application. A first-party finding has nodependencyblock; its top-levelpurlkeeps its meaning, the package URL of the rule’s library, and stays empty on a dependency finding. The block is optional and new in interned schema6.17. The inlined6.14compatibility render carries it too, andschemas/callgraph-schema.jsondeclares it there. -
A dependency that joins the call graph without source code (a Java dependency with no source JAR is indexed for its types only; in other ecosystems it is left out) holds no call edges, so no call chain crosses it. On the live export such a dependency on a finding’s
pathcarrieswithout_source: true. When every route from the application to a finding’s dependency crosses one and no chain reaches the finding, the finding readsunknownwithunresolved_reason: "dependency_without_source"andanalysis.call_chains: "partial", instead ofunreachable: the chain may run through the dependency whose calls are missing. The stitched export has no such case, because a stitch with a component fragment missing fails. -
Findings missing a containing function or crypto-call match are still exported with
finding_locationandunresolved_reason.no_containing_functionmeans the finding’s language was analyzed and no function encloses it;no_crypto_call_matchmeans no call at the finding’s position matched.language_not_analyzedmeans no call graph exists for the finding’s language in this scan, because the language has no call graph parser (Kotlin, for example) or its graph failed to build; itsreachabilityisnot_applicablebecause nothing about it was examined. -
A target with first-party findings in several supported ecosystems resolves each finding against the call graph of its own language. Those graphs cover first-party source only: dependencies are resolved for the primary ecosystem alone, the dominant language by file count or
--dep-ecosystem. The first-party findings of the other ecosystems are classified the way the primary ecosystem’s are, against their own source packages when dependencies were resolved or--export-callgraph-project-reachabilityis set, andnot_applicableotherwise. -
crypto_entry_points[]is the stitch/API index. Each entry carriesfunction_key, canonical/display symbols, aliases, andreachable_findings[]/reachable_supporting_calls[]. -
supporting_calls[]carries config/lifecycle/context crypto-adjacent calls, such as builder options or parameter setup. These calls are not findings and do not inflatefinding_graphs[]. As of6.5,role: operationcontract methods (the calls where the cryptographic computation actually executes, e.g. block-processing/finalization methods) are also exported here with a category and referenced viasupporting_call_ids. -
Constructor joins remain canonical (
<init>), while display fields and aliases expose IBM-style names such ascom.acme.Factory.Factory. -
entry_point_indexis not emitted by schema6.0. Consumers should migrate tocrypto_entry_points[].
Compatibility and consumer gating
The published JSON Schemas areschemas/interim-report-schema.json, schemas/callgraph-schema.json (inlined 6.14), schemas/callgraph-schema-6.15.json (interned 6.15), schemas/callgraph-schema-6.16.json (interned 6.16), and schemas/callgraph-schema-6.17.json (interned 6.17, the local CLI default). Validate against the exact emitted version; undeclared properties are contract failures. Consumers without catalog hydration must explicitly request --export-callgraph-interned-frames=false.
- Adding an optional field requires a schema-version bump, a schema update, and documentation. Consumers must validate an artifact against the published schema for its exact version; an artifact with no matching published schema must fail closed with a clear upgrade message.
- Removing or renaming a field, changing a field’s JSON type or meaning, or making an optional field required is breaking and requires a schema-version bump plus a migration note in
CHANGELOG.md. - Consumers must gate parsing on the artifact’s
version(interim report) orschema_version(callgraph), then validate against the matching published schema before processing it. - Schema changes and version bumps must update this document, the schema file, generated-export validation, and
CHANGELOG.mdin the same change.
--export-callgraph-interned-frames=false, schema 6.14; identity is inlined on frames and also in functions[]):
Example Output
Single Rule Detection:Use Cases
- Integration with SCANOSS platform
- Custom analysis pipelines
- Detailed cryptographic asset tracking
- Security auditing and compliance
Graph Fragment Export
When--export-graph-fragment <file> is enabled, Crypto Finder writes a
reusable structural graph fragment for the scanned component: its call
graph plus rules-versioned crypto annotations. Unlike the finding-centric call
graph export (above), a fragment is designed to be composed with other fragments
across a dependency tree to answer “what crypto is transitively reachable from
artifact X?” The pure model and the stitcher that composes fragments live in the
public package github.com/scanoss/crypto-finder/pkg/graphfrag, so downstream
consumers can use one contract instead of reimplementing schema knowledge.
Current schema version: graph-fragment-1.14 (pkg/graphfrag.SchemaVersion).
Since graph-fragment-1.3, a fragment is self-contained enough to reconstruct
the two artifacts a live --scan-dependencies run would produce — see
Rendered artifacts below.
Fragment schema history (all changes are additive; older fragments decode with
the missing fields empty and are handled fail-closed):
Structure
Per-call data flow: entry_call (1.2+)
Every internal_edges[] and external_calls[] entry may carry an entry_call
describing the call-site argument data-flow for that edge — the same model the
finding-centric call graph export uses (see Call Graph Export above):
entry_call.parameters[] each have parameter_index, best-effort type,
argument_expression, resolved_value, variable_name (simple identifiers
only), and recursive source_nodes provenance. Carrying it on the edge is
what lets the stitcher rebuild full per-frame value flow when composing chains
across components, so a stitched chain matches a live run frame-for-frame.
Crypto annotation fields (1.2+)
Agraph-fragment-1.2+ crypto_annotations[] entry carries enough to
reconstruct a full findings.json entry for the matched crypto call:
Rendered artifacts: ToCallgraphExport / ToFindingsEnvelope
Because a 1.3+ fragment carries per-call data flow, full crypto-annotation,
supporting-call, and entrypoint metadata, pkg/graphfrag can render a stitched
Result into the same two artifacts a live --scan-dependencies run produces:
Result.ToCallgraphExport(root, meta)— renders the stitched result into a callgraph (zero-value meta stamps inlined6.14;ScanMeta.InternedFramesselects6.17). Align the render and sample budget explicitly when comparing with local CLI exports. Dep-component findings getmodule@version/-prefixedfinding_ids, matching live output.ToFindingsEnvelope(root, deps, fragments, meta)— reconstructs the findings.json v1.7 envelope (asset metadata, including directpurl). Itsfinding_ids are computed with the same inputs asToCallgraphExport, so the two agree: consumers join assets (envelope) to call chains (callgraph) by(finding_id, occurrence_key)when the key is present, or byfinding_idfor legacy records withoutoccurrence_key.
pkg/graphfrag/equiv is a semantic diff tool that asserts a stitched callgraph
equals a live one minus the chains intentionally dropped by resolution
suppression (see below) — the equivalence guarantee these renderers rely on.
Edge resolution metadata (v1.1+)
Everyinternal_edges[] and external_calls[] entry carries resolution
metadata describing how confidently the edge was resolved. This lets a
consumer distinguish exact typed calls from over-broad name/arity dispatch
guesses, and refuse to present the latter as typed reachability proof.
resolution values:
exact— the receiver’s static type was known and the method resolved to a unique declared target on that type (or an overload set on that exact type).interface_dispatch— the target was found by expanding an interface/abstract method to concrete implementations matching name + arity within a namespace root. Trustworthy only when exactly one implementation is present in the dependency closure; otherwise it is an ambiguous guess.name_only— the target was guessed by method name + arity (plus namespace heuristics) with no receiver-type anchor (e.g. fluent-chain fallback).
method_name + arity + the call-site line let a consumer group sibling
candidates of one call site so ambiguity can be detected across edges that
span the component boundary. The reference consumer (pkg/graphfrag’s stitcher)
applies a tiered, fail-closed policy: traverse exact edges and
interface_dispatch edges with exactly one implementation in the dependency
closure; drop ambiguous interface dispatch (>1 impl) and name_only edges,
recording them rather than emitting a chain. This is what prevents a DRBG’s
generate() from name-colliding with BCrypt.generate#3 (or
provider.get(...) fanning out to unrelated get(...) methods) from being
reported as reachable crypto.
Fragments exported by older versions (without resolution) decode as
unresolved and are treated as untrusted (fail-closed): under-report, never a
false positive.
Example
decryptPrivateKeyInfo has one exact edge to the real
InputDecryptorProvider.get and one over-broad interface_dispatch edge to
an unrelated get#1 from the same call site (line: 90). A stitcher that sees
more than one implementation for that call site drops the ambiguous group.
CycloneDX CBOM Format
CycloneDX 1.6 compatible Cryptography Bill of Materials format for standardized reporting.Features
- Schema Validation: Validates against CycloneDX 1.6 specification
- Standardized Components: Maps cryptographic assets to standardized component types
- Rich Metadata: Includes algorithm properties, evidence, and provenance
- Industry Standard: Compatible with CycloneDX ecosystem tools
Supported Asset Types
Example Output
Converting Formats
Use theconvert command to transform interim JSON to CycloneDX:
Integration
CycloneDX CBOM output can be consumed by:- Dependency track systems
- Software Bill of Materials (SBOM) aggregators
- Security scanning platforms
- Compliance reporting tools
- Supply chain risk management systems