# List (and page) policy approval history for a project
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/list-and-page-policy-approval-history-for-a-project
/api-reference/earnie/policies-openapi.json get /v1/projects/{projectId}/approvals
Returns one page of approval history, newest first, optionally narrowed by `policy_id` and/or `status`. Omitting `limit` applies the default page size of 50 and 100 is the hard maximum; `limit=0` returns zero rows and only the `total` — a cheap count. `status` filters on the four user-visible statuses, of which `revoked` is derived — an approved row whose `revoked_at` is set — so `approved` excludes revoked rows.
# List current policy verdicts for a finding
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/list-current-policy-verdicts-for-a-finding
/api-reference/earnie/policies-openapi.json get /v1/findings/{findingId}/policy-verdicts
# List named regulatory policy template sets
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/list-named-regulatory-policy-template-sets
/api-reference/earnie/policies-openapi.json get /v1/policies/template-sets
Returns fixed clause-to-template mappings. Sets whose templates belong to a scanner the organization has not enabled are withheld.
# List the projects a policy is attached to
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/list-the-projects-a-policy-is-attached-to
/api-reference/earnie/policies-openapi.json get /v1/policies/{id}/attachments
Live attachment state for one policy: every non-archived project in the caller's organization that the policy is attached to, plus the repository bound to that project when one exists. A policy is a single organization-wide rule, so this is how a reader learns the real fan-out of an edit. A Project-restricted API key sees only the attachments inside its own Projects.
# Create a new project
Source: https://docs.scanoss.com/earnie/api-reference/projects/projects/create-a-new-project
/api-reference/earnie/projects-openapi.json post /v1/projects
# Get a project by ID
Source: https://docs.scanoss.com/earnie/api-reference/projects/projects/get-a-project-by-id
/api-reference/earnie/projects-openapi.json get /v1/projects/{projectId}
# List projects for the authenticated organisation
Source: https://docs.scanoss.com/earnie/api-reference/projects/projects/list-projects-for-the-authenticated-organisation
/api-reference/earnie/projects-openapi.json get /v1/projects
# Resolve one accessible project for a customer CLI
Source: https://docs.scanoss.com/earnie/api-reference/projects/projects/resolve-one-accessible-project-for-a-customer-cli
/api-reference/earnie/projects-openapi.json get /v1/projects:resolve
Resolves exactly one Project by its UUID or slug, or by a normalized source provider/repository identity. The server applies API-key Project restrictions and never substitutes a default Project.
# Update a project's name or description
Source: https://docs.scanoss.com/earnie/api-reference/projects/projects/update-a-projects-name-or-description
/api-reference/earnie/projects-openapi.json patch /v1/projects/{projectId}
# Abandon an upload
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/abandon-an-upload
/api-reference/earnie/scans-openapi.json delete /v1/projects/{projectId}/model-uploads/{uploadId}
Removes the upload and the bytes received so far. A Model it sealed into is kept.
# Cancel an in-progress scan
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/cancel-an-in-progress-scan
/api-reference/earnie/scans-openapi.json post /v1/scans/{scanId}:cancel
Stops every in-flight job for this scan (keep-set ingest, git clone/fetch, River pipeline, crypto scanner) and marks the scan terminal `cancelled` with `cancellation_reason=user_requested`. Already-cancelled scans are idempotent. Completed scans return `409 scan_not_cancellable`.
# Compare crypto findings with another completed scan
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/compare-crypto-findings-with-another-completed-scan
/api-reference/earnie/scans-openapi.json get /v1/scans/{scanId}/crypto/diff
Compares immutable crypto observations by stable finding identity. Both scans must belong to the authenticated organization's same project, be completed, and carry full-coverage crypto observations. The `against` scan must precede `scanId` in canonical `(created_at, id)` order. Empty arrays are returned for empty buckets, including a clean crypto scan.
# Create a receiving scan for a resumable archive object
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/create-a-receiving-scan-for-a-resumable-archive-object
/api-reference/earnie/scans-openapi.json post /v1/scans:plan-archive
Creates a scan in `status=receiving` for a user-supplied `.zip` or `.tar.gz`. Upload the object with `PUT /v1/scans/{scanId}/archive` (Content-Range resume is supported). When the object is complete the server unpacks through the keep engine and seals the keep-set zip. Legacy one-shot `POST /v1/scans` multipart `file` remains valid and also runs the keep engine.
# Crypto algorithms detected in one project
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/crypto-algorithms-detected-in-one-project
/api-reference/earnie/scans-openapi.json get /v1/projects/{projectId}/crypto-algorithms
One row per concrete algorithm (AES-256-GCM, RSA-2048), folding spelling variants. Read in one batch and grouped server side with the same asset identity as `CryptoRollup`. Empty when crypto never ran or the caller cannot see the crypto scanner.
# Declare a model upload, or resume the one in flight
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/declare-a-model-upload-or-resume-the-one-in-flight
/api-reference/earnie/scans-openapi.json post /v1/projects/{projectId}/model-uploads
Starts an upload of one model file or snapshot tar. The size cap is checked before any row or byte exists. Declaring the same file again (same size and head digest) while its upload is in flight answers that upload with `200` and its offset: that is how an upload resumes after a reload. Send the bytes with `PUT /v1/projects/{projectId}/model-uploads/{uploadId}`.
# Delete a Model
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/delete-a-model
/api-reference/earnie/scans-openapi.json delete /v1/projects/{projectId}/models/{modelId}
Cancels its scan if one is running, resolves its findings as absent (their triage is kept, and uploading the same bytes again reopens them) and removes its stored bytes.
# Dependency-internal crypto inventory for one component
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/dependency-internal-crypto-inventory-for-one-component
/api-reference/earnie/scans-openapi.json get /v1/projects/{projectId}/crypto-inventory
Returns the crypto assets embedded inside one declared dependency, identified by its base purl. This is read-only evidence, never a triage item: a dependency's embedded crypto carries no Decide verb of its own (design D8b) — the containing dependency's OWN declared-dependency finding is what a client resolves (`accept_dependency` / `dismiss_dependency`), and only `dismiss_dependency` or `mark_original` removes the assets returned here from CBOM and posture on the next read.
Not a field on the component list: the workspace fetches components unbounded for the whole project, and one dependency alone can carry hundreds of distinct assets across thousands of findings — embedding this here would put the response's largest object on every row of an eagerly-loaded list. Fetch it once, when a component is selected.
Returns an empty `assets` array (with all totals zero) when the purl has no dependency-internal crypto findings, rather than 404 — an unscanned or clean dependency is a valid answer, not an error.
# Explicitly rerun a scan
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/explicitly-rerun-a-scan
/api-reference/earnie/scans-openapi.json post /v1/scans/{scanId}:rerun
Creates a new Scan for the same server-resolved source and effective configuration. The new resource carries `rerun_of`; completed scans are not reused for this explicit request, while concurrent identical reruns coalesce through Scan Intent. An optional fetch_lfs_models override can recover missing Git LFS model weights without changing project settings.
# Fetch a single scan by id
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/fetch-a-single-scan-by-id
/api-reference/earnie/scans-openapi.json get /v1/scans/{scanId}
# Get authoritative project scan summary
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/get-authoritative-project-scan-summary
/api-reference/earnie/scans-openapi.json get /v1/scans/projects/{projectId}/state
# Get one Model
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/get-one-model
/api-reference/earnie/scans-openapi.json get /v1/projects/{projectId}/models/{modelId}
# Get one Self-check
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/get-one-self-check
/api-reference/earnie/scans-openapi.json get /v1/self-checks/{selfCheckId}
The polling target for a creation call that returned `status: running`, and the deep link's target.
A purged Self-check answers `404 self_check_not_found`: after 30 days the run is gone by design, and its `selfcheck.*` decision log in the Audit Log is what remains.
# Get the project's AI scanner settings
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/get-the-projects-ai-scanner-settings
/api-reference/earnie/scans-openapi.json get /v1/projects/{projectId}/ai-settings
A project that never saved them answers the defaults.
# Get this deployment's upload limits
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/get-this-deployments-upload-limits
/api-reference/earnie/scans-openapi.json get /v1/scans/upload-limits
The caps the api enforces on an uploaded archive and an imported SBOM document, read from `SCANS_MAX_UPLOAD_MB` and `SCANS_MAX_SBOM_MB` when the api starts. A client checks a file against them before it uploads; the api refuses a larger body regardless.
# Import a foreign SBOM document as a partial-coverage scan
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/import-a-foreign-sbom-document-as-a-partial-coverage-scan
/api-reference/earnie/scans-openapi.json post /v1/scans:import-sbom
Accepts a CycloneDX or SPDX 2.3 JSON document describing a project's declared components. The format is detected from the document's content (`bomFormat` vs `spdxVersion`), never from the filename or the client's Content-Type — the upload is untrusted input. Tag-value SPDX is not accepted; JSON only. Unlike `POST /v1/scans`, no source code is scanned — the resulting scan always carries `source_type: sbom` and `coverage: partial`: an imported document is a snapshot of declared components, not a full-codebase scan, so it never marks a prior full-coverage scan's findings absent.
A component with a `purl` is enriched (vulnerabilities/licenses) exactly like a code-matched dependency. A component with a name but no `purl` is still imported — it simply carries no enrichments, since there is no identity to look one up by.
The import pipeline (parse → emit findings → enrich) runs in the background. Clients poll `GET .../scans/{id}` for progress, same as any other scan.
Re-importing the exact same document (same `project_id` + content hash) reuses the existing Scan Intent rather than creating a duplicate scan.
The `scanners` field is not accepted because an SBOM import does not run source-code scanner phases; if present, the request is rejected with 400.
# Keep-set ingest cursor — received and missing paths
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/keep-set-ingest-cursor-—-received-and-missing-paths
/api-reference/earnie/scans-openapi.json get /v1/scans/{scanId}/ingest
Returns accepted keep-set entries (path + content checksum) and the paths still missing. Reconnecting clients send only missing entries.
# Lazy folder expansion with aggregated state
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/lazy-folder-expansion-with-aggregated-state
/api-reference/earnie/scans-openapi.json get /v1/scans/{scanId}/tree
Returns the direct children (files + immediate subfolders) of the requested folder prefix, with aggregated state for each subfolder (ISTR-1).
# List all files in the scan (flat)
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/list-all-files-in-the-scan-flat
/api-reference/earnie/scans-openapi.json get /v1/scans/{scanId}/files
Returns every file in the scan as a flat list of `TreeEntry` file rows (no folder rows, no one-level projection). The client builds the folder tree from the flat paths. Mirrors the snapshot semantics of the tree endpoint.
# List scans
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/list-scans
/api-reference/earnie/scans-openapi.json get /v1/scans
# List Self-checks
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/list-self-checks
/api-reference/earnie/scans-openapi.json get /v1/self-checks
Self-checks are listed here and nowhere else: `GET /v1/scans` returns governed Scans only, so a developer's twenty commits a day never crowd out a Project's scan history.
# List the project's Models and its uploads in flight
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/list-the-projects-models-and-its-uploads-in-flight
/api-reference/earnie/scans-openapi.json get /v1/projects/{projectId}/models
Newest first. When `ai` is hidden from the caller both lists are empty; the Models are kept and reappear when `ai` is enabled again.
# Match metadata + component + licenses for one file
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/match-metadata-+-component-+-licenses-for-one-file
/api-reference/earnie/scans-openapi.json get /v1/scans/{scanId}/matches
Returns all matches for the file at `?path=` with embedded component and licenses (ISFV-1). For a `no_match`/`filtered` file the response is `200` with an empty array.
# Per-scan rollup for the results-overview dashboard
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/per-scan-rollup-for-the-results-overview-dashboard
/api-reference/earnie/scans-openapi.json get /v1/scans/{scanId}/summary
Returns a `ScanSummary` rollup computed from the scan's persisted inventory (scans.* tables). Only available once the scan reaches `status=completed`; returns 404 while the scan is still in progress.
`dependency_count` counts distinct dependency PURLs persisted by the always-on dependency producer for this scan.
`matched_components` is capped at 100 entries, ordered by `file_count DESC`.
# Plan a folder scan — keep engine then 202 receiving
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/plan-a-folder-scan-—-keep-engine-then-202-receiving
/api-reference/earnie/scans-openapi.json post /v1/scans:plan-folder
Submits a directory inventory plus optional root `.gitignore` and settings-file bytes. A diff producer lists removed paths as inventory entries with `deleted: true` and `size: 0`; those deletion markers are retained with the receiving plan but never enter the keep set or ingest. The server runs the keep engine (SDK scanning built-ins, gitignore, settings, org/project skip_patterns) and returns `202` with the scan (`status=receiving`) and the keep set.
The keep-set transfer is NOT complete: the client streams kept file bytes via `PUT /v1/scans/{scanId}/ingest`, then `POST /v1/scans/{scanId}:commit-ingest`. Caps apply to keep-set bytes and entry count (`keep_set_too_large`).
# Posture for every project in scope, in one response
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/posture-for-every-project-in-scope-in-one-response
/api-reference/earnie/scans-openapi.json get /v1/scans/projects:posture
Powers the projects dashboard: one row per project, aggregated server-side so the response cost scales with the org's data rather than with its project count.
The colon form cannot collide, and the repo already uses it for `:reset` and `:import-sbom`.
Scope defaults to every non-archived project the caller's organization owns. Passing `projects` narrows it to an explicit set — the multi-select scope in the UI. Ids outside the caller's org are silently dropped rather than 403'd: scope is a view, and an id the caller cannot see simply is not in their estate.
Projects with no completed scan ARE included, carrying `coverage: none` and null metrics. Omitting them would hide exactly the projects that most need attention — an unscanned project is a governance gap, not an absence of one.
Scope today is the whole organization, optionally narrowed to an explicit project set. Named groups are a future scope kind; when they land they narrow this same endpoint rather than adding another.
# Project crypto posture rollup
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/project-crypto-posture-rollup
/api-reference/earnie/scans-openapi.json get /v1/scans/projects/{projectId}/summary/crypto
Returns the crypto rollup for the project's posture source scan, resolved by the same rule as `GET /v1/scans/projects/{projectId}/summary`. Split out of that response because it reads every active crypto finding in the project; a client fetches both at once and renders the crypto section when this one arrives.
`crypto` is absent when crypto never ran for the project and it holds no active crypto findings, or when the caller's organization does not have the crypto scanner enabled.
# Project posture rollup for the Audit landing
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/project-posture-rollup-for-the-audit-landing
/api-reference/earnie/scans-openapi.json get /v1/scans/projects/{projectId}/summary
Returns a `ProjectSummary` rollup computed from the project's posture source scan over scans.* tables: the latest successful full-coverage scan from the default branch. Partial and non-default-branch scans do not publish posture; the last confirmed default-branch result remains visible while a newer head is running or cancelled. Returns 404 when the project has no eligible posture scan yet.
`dependency_count` counts distinct dependency PURLs persisted by the always-on dependency producer for the selected source scan.
`matched_components` is capped at 100 entries, ordered by `file_count DESC`.
# Proxy the bytes of an OSS file from the SCANOSS file-content service
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/proxy-the-bytes-of-an-oss-file-from-the-scanoss-file-content-service
/api-reference/earnie/scans-openapi.json get /v1/scans/{scanId}/oss-files/content
Fetches the OSS file content for a given `source_hash` from the SCANOSS API file-content service (api.scanoss.com) and streams the bytes back to the caller.
The `source_hash` MUST correspond to a match present in this scan; unrelated hashes return `404 hash_not_in_scan`.
# Read an upload and its offset
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/read-an-upload-and-its-offset
/api-reference/earnie/scans-openapi.json get /v1/projects/{projectId}/model-uploads/{uploadId}
# Read the upload offset of an archive object
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/read-the-upload-offset-of-an-archive-object
/api-reference/earnie/scans-openapi.json head /v1/scans/{scanId}/archive
Reports how many bytes of the archive object the server holds, so a client can resume `PUT /v1/scans/{scanId}/archive` after a reload or a lost response. The next window starts at `Upload-Offset`.
# Replace the project's AI scanner settings
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/replace-the-projects-ai-scanner-settings
/api-reference/earnie/scans-openapi.json put /v1/projects/{projectId}/ai-settings
# Retry a degraded provider publication
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/retry-a-degraded-provider-publication
/api-reference/earnie/scans-openapi.json post /v1/scans/{scanId}:retry-publication
Re-enqueues every `degraded` publication of this scan with the content it already holds, and any `pending`/`publishing` publication that has made no progress for 15 minutes — a job discarded after exhausting its own retries, or one lost right after being claimed, has no other way back. It never re-scans and never changes the verdict; it asks the provider to be told again.
Requires `scans:write`. Answers `409 publication_not_retryable` when nothing qualifies: the scan has no degraded publication and every pending/publishing one is still making progress, or nothing was ever published for it.
# Run a Self-check over an inline SourceSet
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/run-a-self-check-over-an-inline-sourceset
/api-reference/earnie/scans-openapi.json post /v1/self-checks
Creates a Self-check from an inline SourceSet supplied in this one request — `files: [{path, content}]` or a unified `diff`, no plan-folder / ingest / commit-ingest round trips. The server materializes a tmpdir and runs the same scanners, enrichments and policy engine a governed Scan runs.
A Self-check never materializes Findings, never touches the Project's posture, and is purged after 30 days. It consumes credits exactly as a Scan does, because the same engines compute.
`coverage` is always `partial`: everything the submitted content allows to detect fires — a crypto call in a snippet, an OSS snippet match and the purls it enriches to, a manifest's declared dependencies — while a policy the SourceSet cannot answer is reported in `not_evaluated[]` with a reason rather than passing silently.
The call is synchronous within a budget (default 45 s, organization-configurable). Past it the response is still `200`, with `status: running` and the Self-check's `id`; poll `GET /v1/self-checks/{selfCheckId}` with that id. Both shapes are the same `SelfCheck` resource, so a client never branches on two schemas.
# Seal the keep-set archive and start scanning
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/seal-the-keep-set-archive-and-start-scanning
/api-reference/earnie/scans-openapi.json post /v1/scans/{scanId}:commit-ingest
Requires every keep-set path to be accepted. Seals a zip STORE archive of kept files only, points `source_ref` at it, and leaves `receiving` for the existing pipeline (`scanning` …). Missing paths return `409 ingest_incomplete`.
# Send one window of an upload
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/send-one-window-of-an-upload
/api-reference/earnie/scans-openapi.json put /v1/projects/{projectId}/model-uploads/{uploadId}
Appends the bytes `Content-Range` names. A window must start at the upload offset, carry exactly `end-start+1` bytes, and name the declared size as its total. A rejected window leaves the offset unchanged. When the window ending at `total-1` lands, the upload seals into a Model and its scan starts; the response then carries `model_id`. Sending that last window again answers the sealed upload.
# Stream a single file's bytes from the retained archive
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/stream-a-single-files-bytes-from-the-retained-archive
/api-reference/earnie/scans-openapi.json get /v1/scans/{scanId}/files/content
Lazy-decompresses one entry from the retained source archive. The implementation MUST NOT extract the whole archive; only the requested entry's bytes are returned. Content-Type is inferred from the extension (`text/plain; charset=utf-8` by default). Path traversal attempts (`..` segments, absolute paths) return `422 invalid_path` (ISF-2, ISF-3).
# Stream kept files as a tar
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/stream-kept-files-as-a-tar
/api-reference/earnie/scans-openapi.json put /v1/scans/{scanId}/ingest
Tar stream of kept file members. Optional first member `__earnie_checksums.json` maps path → hex SHA-256. The server hashes each member; a path already accepted with the same checksum is idempotent; a checksum mismatch rejects that entry (`checksum_mismatch`) without replacing a sealed keep set.
Per-file HTTP for tens of thousands of files is not this operation — batch files in the tar. Paths not in the keep set are ignored.
# The curated rules Earnie assesses crypto findings against
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/the-curated-rules-earnie-assesses-crypto-findings-against
/api-reference/earnie/scans-openapi.json get /v1/crypto/assessment-rules
Returns the two curated datasets every crypto severity, post-quantum verdict and export signal is derived from: the risk tables and the export-control regime dataset. Severity provenance already travels with each finding, so a reader can see WHICH vintage judged it; this is what the rules of that vintage actually are.
The payload is projected from the same in-memory tables the scanner reads, so it cannot drift from the assessment it describes. It carries no tenant data and is identical for every organisation on a given binary — it changes only when the binary ships new datasets, which is why the response is safe to cache against its `ETag`.
Access is gated on the `crypto` scanner rather than on a permission slug: the payload is documentation about the product, and the curated algorithm vocabulary it publishes is the same dataset a non-entitled organisation is already withheld on `/v1/policies/fields/{path}/values`. Any authenticated principal of an entitled organisation may read it.
Every row states whether it is cited. Nothing here classifies anything: no ECCN is derived, printed or suggested, and FIPS 140-3 validation status is out of scope.
# The organization's AI provenance posture across a scope of projects
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/the-organizations-ai-provenance-posture-across-a-scope-of-projects
/api-reference/earnie/scans-openapi.json get /v1/ai/posture
The estate answer to "what AI do we ship, from which vendors, under which model licenses", in one response. Scoped, authorized and degraded exactly like `GET /v1/crypto/posture`: org-scoped by authentication, `projects`, `batch` and `include_archived` narrow it as they narrow `GET /v1/scans/projects:posture`, ids the caller cannot see are dropped silently, and it needs `scans:read` with no AI-specific 403.
The numbers are the sum of the per-project `ProjectSummary.ai` rollups, computed by the same pure counting over each project's findings, which are read for the whole scope in one batched query. See `EstateAIPosture` for the measured rule.
# The organization's crypto posture across a scope of projects
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/the-organizations-crypto-posture-across-a-scope-of-projects
/api-reference/earnie/scans-openapi.json get /v1/crypto/posture
OMB M-23-02's federal PQC inventory is posed at exactly this altitude.
Org-scoped by AUTHENTICATION, not by path. The organization is not a path parameter here because no route in this repo takes one: the organization is a per-deployment singleton and org scope is the tenant boundary resolved from the authenticated principal. A project-bound principal is narrowed to its own project, exactly as `GET /v1/scans/projects:posture` narrows one.
Scope uses the SAME vocabulary `projects:posture` accepts — `projects` and `include_archived` — so the estate canvas narrows both reads with one control. Ids outside the caller's organization are dropped SILENTLY rather than rejected: scope is a view, and naming a project you cannot see should return an estate without it rather than leak that it exists through a 403.
A SEPARATE endpoint from `projects:posture`, deliberately. This is the largest read on the projects surface — it reads every active crypto finding in scope — so the project table must not pay for it on every load. The cost is still a fixed number of queries: the findings for every in-scope project are read in ONE batched query and the aggregation is linear Go work, so the response cost tracks the organization's crypto data rather than its project count.
Numbers here are the SUM of the per-project crypto rollups `GET /v1/scans/projects/{projectId}/summary` returns, computed by the same pure aggregator run once per project. That equality is a property of the construction, and `crypto.projects` carries the per-project values so a client can verify it by addition. See `EstateCryptoAggregate` for the inventory/outstanding-work line, the per-project asset-distinctness rule, and the one field that is NOT derivable by addition.
Requires `scans:read` — the same permission the rest of the posture surface requires. There is deliberately NO crypto-specific 403 and no `crypto_not_entitled` code: an organization without the scanner reaches the ordinary absent response through the ordinary code path (no completed crypto stages, no crypto findings), byte-identical to an entitled organization that has never run crypto. A distinguishable refusal would advertise entitlement state on a read surface that carries none today.
A trend at this altitude is deliberately absent: an arbitrary project scope has no history to recompute against. Crypto history is per-project and belongs to the scan-anchored surfaces.
# The Scan's Audit Trail
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/the-scans-audit-trail
/api-reference/earnie/scans-openapi.json get /v1/scans/{scanId}/audit
Returns the Scan's full, oldest-first Audit Trail: every audit event whose subject is this Scan — submission (`scan.submitted`), the gate it was given (`scan.gate_evaluated`), later Approval overlays (`scan.gate_updated`), and its provider publications (`publication.*`) — so anyone with `scans:read` can answer "which code, policy, actor and outcome produced this gate".
`actor.display` is the identity frozen at emit time; it is never re-resolved.
A Scan that exists but pre-dates subject-addressed emission answers an empty `events` array, not a 404. A Self-check is not a Scan and answers 404 here; its decision log is read through `GET /v1/audit/events?subject_kind=selfcheck`.
# Upload a source archive and start a scan
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/upload-a-source-archive-and-start-a-scan
/api-reference/earnie/scans-openapi.json post /v1/scans
Magic-byte validation is performed server-side; client Content-Type is NOT trusted. The `project_id` form field is **required** — every new scan must be linked to a project at creation time (identifications and policy evaluation are project-scoped).
JSON `GitScanRequest` returns `202 Accepted` for a new Scan and `200 OK` with `reused: true` when the resolved Scan Intent already exists (a bad ref creates no row). The repository is materialized with clone/fetch, not GitHub's zipball API.
Folder, git, and archive source ingest use `status=receiving` until the keep-set is sealed; SBOM import stays on `pending` then `importing`. The pipeline runs in the background. Clients poll `GET .../scans/{id}` for progress.
# Upload or resume a user-supplied archive object
Source: https://docs.scanoss.com/earnie/api-reference/scans/scans/upload-or-resume-a-user-supplied-archive-object
/api-reference/earnie/scans-openapi.json put /v1/scans/{scanId}/archive
Raw `.zip` or `.tar.gz` bytes for a scan created by `POST /v1/scans:plan-archive`. `Content-Range` (bytes) appends one window; omitting it replaces and completes the object. A window must start at the upload offset (`HEAD` reports it), carry exactly `end-start+1` bytes, and name the same total as the first window. A rejected window leaves the offset unchanged, so the client can send it again. When the window that ends at `total-1` lands, unpacking through the keep engine starts.
# List the durable audit-event trail for a subject (e.g. a finding)
Source: https://docs.scanoss.com/earnie/api-reference/triage/audit/list-the-durable-audit-event-trail-for-a-subject-eg-a-finding
/api-reference/earnie/triage-openapi.json get /v1/audit/events
Returns the ordered (oldest-first) audit-event history for one subject. Authorization dispatches on subject_kind — the caller must be allowed to read the underlying entity (for findings, findings:read plus same-org; for selfcheck, scans:read, authorized entirely from the matching audit rows' own org_id — the underlying Self-check row is never queried, because it is purged after 30 days while its trail is not).
# Assign a finding to an actor (also serves as defer/reassign)
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/assign-a-finding-to-an-actor-also-serves-as-deferreassign
/api-reference/earnie/triage-openapi.json post /v1/findings/{findingId}/actions/assign
# Bulk-reopen findings by component/path scope
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/bulk-reopen-findings-by-componentpath-scope
/api-reference/earnie/triage-openapi.json post /v1/findings/actions/bulk-reopen
Reopens all findings matching the given project, domain_slug, and optional purl/path_prefix scope that are in a reopenable state (resolved, superseded_by_rule → reopened; assigned → open) in a single transaction — the symmetric undo of bulk-resolve. The operation is all-or-nothing: a failure on any finding rolls back all reopens. Already-open findings in scope are skipped idempotently. Each reopen clears the finding's resolution pointer and emits a finding.reopened audit event.
# Bulk-resolve open OSS findings across every project
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/bulk-resolve-open-oss-findings-across-every-project
/api-reference/earnie/triage-openapi.json post /v1/findings/actions/bulk-resolve-org-wide
Organization-wide sibling of bulk-resolve: resolves all open findings matching the given domain_slug and optional purl/path_prefix scope across every project in the organization, in a single transaction. All bulk-resolve semantics apply unchanged (all-or-nothing, idempotent skip of already-resolved findings, suppressed findings withheld, reason-required rule). This is the confirm step of the Scan Configuration save offer: an explicit, user-confirmed action — never triggered implicitly by saving configuration.
# Bulk-resolve open OSS findings by component/path scope
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/bulk-resolve-open-oss-findings-by-componentpath-scope
/api-reference/earnie/triage-openapi.json post /v1/findings/actions/bulk-resolve
Resolves all open OSS findings matching the given project, domain_slug, and optional purl/path_prefix scope in a single transaction. The operation is all-or-nothing: a failure on any finding rolls back all resolutions. Already-resolved findings in scope are skipped idempotently. The same reason-required rule as single-finding resolve applies.
A crypto reason (confirm_usage / false_positive / accepted_risk) scopes itself to `evidence.source = 'direct'` findings on the purl — the dependency-internal crypto inventory never resolves through this or any bulk verb, however many findings share the purl.
# Count the open findings a bulk-resolve would affect
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/count-the-open-findings-a-bulk-resolve-would-affect
/api-reference/earnie/triage-openapi.json post /v1/findings/actions/bulk-resolve-preview
Read-only preview of a bulk-resolve: returns how many currently open, unsuppressed findings match the given selection — without resolving anything. project_id scopes the count to one project; when absent the count spans every project in the organization. The selection semantics (reason-derived domain family, suppressed exclusion, purl/path matching) are shared with bulk-resolve so the preview can never diverge from what a confirm would do.
# Create a configuration rule directly from a finding (mark-as-original)
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/create-a-configuration-rule-directly-from-a-finding-mark-as-original
/api-reference/earnie/triage-openapi.json post /v1/findings/{findingId}/actions/create-rule
Creates a configuration rule for the given finding without going through the pending-review queue. Derives the rule scope from the finding unless an explicit scope override is supplied. Requires findings:write permission.
# Create a new finding (source=api)
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/create-a-new-finding-source=api
/api-reference/earnie/triage-openapi.json post /v1/findings
# Get a single finding by ID
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/get-a-single-finding-by-id
/api-reference/earnie/triage-openapi.json get /v1/findings/{findingId}
# List findings aggregated by component (purl@version)
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/list-findings-aggregated-by-component-purl@version
/api-reference/earnie/triage-openapi.json get /v1/findings/components
A crypto finding on first-party code carries no purl and is therefore not a component. Each item represents a distinct base purl, grouped across its observed versions, and a purl seen by more than one scanner is ONE item. The list reflects the project's CURRENT posture: only present findings (absent_at IS NULL) are aggregated — findings superseded by a later scan are excluded. Enrichment (license, vulnerabilities, geoprovenance) is attached via a single batched Enricher.Enrich call — absent when no scan-time data exists for a component (not an error).
# List findings aggregated by source file (review queue rows)
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/list-findings-aggregated-by-source-file-review-queue-rows
/api-reference/earnie/triage-openapi.json get /v1/findings/files
Each item is one source file — the review workspace's Findings queue grouping unit — rolling up every finding that resolves to that file's path after the facet filters below. This replaces the client-side groupByFile rollup, which required eagerly fetching the full findings array: past a few thousand findings that retained hundreds of MiB of browser heap and silently truncated. A file's findings can never straddle a page boundary, so paging the raw finding list cannot back this view — the aggregation has to happen in SQL. `counts` is computed over the SAME filtered population as `items`, before the `bucket` split, so one request renders both the "N to review" and "M resolved" section headers.
# List findings with optional filters
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/list-findings-with-optional-filters
/api-reference/earnie/triage-openapi.json get /v1/findings
# List the filterable facet values a set of findings carries, with counts
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/list-the-filterable-facet-values-a-set-of-findings-carries-with-counts
/api-reference/earnie/triage-openapi.json get /v1/findings/facets
One GROUP BY over the derived facet index, not one query per value. This is the surface that makes a filter panel correct at estate scale: the review workspace filters a client-side array capped at 5,000 findings while printing the server's true total beside it, so on a real project the filter narrows a truncated set and the count describes a different one. Counts are computed over the population the SAME narrowing parameters would return from GET /v1/findings, so a value's count is exactly how many findings selecting it would show. The `facet` selection is applied disjunctively: a path's values are counted over the population narrowed by every OTHER selected path, never by its own. Selecting one licence therefore leaves the other licences' counts where they were, so the dropdown still offers the values that widen the selection, while every other path narrows to the selection.
# Membership Audit Trail for a component (base purl) selection
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/membership-audit-trail-for-a-component-base-purl-selection
/api-reference/earnie/triage-openapi.json get /v1/findings/components/audit-trail
Returns a membership projection of durable Finding audit events for all present posture findings that join the selected component's base purl within a project. Equivalent sibling events (same actor id, event_type, reason_slug, occurred_at, replacement_purl) are collapsed into beats with file membership. Spine events only (finding.detected/resolved/reopened/ suppressed/unsuppressed/superseded_by_rule, resolution.autonomous/ confirmed). Ordered oldest→newest with Detected pinned when present and up to K=20 other latest collapsed beats; more_available signals rail budget truncation. This is NOT a component audit subject — full history lives in the organization Audit Log.
# Reopen a resolved or assigned finding
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/reopen-a-resolved-or-assigned-finding
/api-reference/earnie/triage-openapi.json post /v1/findings/{findingId}/actions/reopen
On a finding that exists only in a pull request scan, reopen withdraws the decision it carries, so nothing applies when it reaches the default branch. The response is then read from that pull request scan. A pull request finding with no pending decision is already open (400 `invalid_transition`). Reopening a confirmed code match also reopens the other candidates of its file that the confirmation resolved (`resolved_alternatives` on the resolve response). A candidate decided again since then stays as it is.
# Resolve a finding with a reason
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/resolve-a-finding-with-a-reason
/api-reference/earnie/triage-openapi.json post /v1/findings/{findingId}/actions/resolve
# Stream the dependency's resolved source for a crypto finding
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/stream-the-dependencys-resolved-source-for-a-crypto-finding
/api-reference/earnie/triage-openapi.json get /v1/findings/{findingId}/crypto-source
Reads the file containing a `source: dependency` crypto occurrence's call site from the resolved-source cache crypto-finder populated during the scan, and streams its bytes.
The request carries ONLY the finding id. The module, version and path are read server-side from the finding's own stored evidence, so there is no client-supplied path to traverse with. Content-Type is `text/plain; charset=utf-8`.
Coverage follows crypto-finder's own resolvers: today only Maven and Gradle populate that cache, so a finding from any other ecosystem answers `404 source_not_available`. Only the documented 404 codes `source_not_available` and `not_found` mean absence and allow fallback to the matched line. Successful immutable source bytes may be cached indefinitely.
# Suppress or unsuppress a finding (orthogonal to triage state)
Source: https://docs.scanoss.com/earnie/api-reference/triage/findings/suppress-or-unsuppress-a-finding-orthogonal-to-triage-state
/api-reference/earnie/triage-openapi.json post /v1/findings/{findingId}/actions/suppress
# List resolutions (pending review queue)
Source: https://docs.scanoss.com/earnie/api-reference/triage/resolutions/list-resolutions-pending-review-queue
/api-reference/earnie/triage-openapi.json get /v1/resolutions
# Revert a pending_review resolution
Source: https://docs.scanoss.com/earnie/api-reference/triage/resolutions/revert-a-pending_review-resolution
/api-reference/earnie/triage-openapi.json post /v1/resolutions/{resolutionId}/actions/revert
# Weekly decision throughput for a project, split by actor class
Source: https://docs.scanoss.com/earnie/api-reference/triage/resolutions/weekly-decision-throughput-for-a-project-split-by-actor-class
/api-reference/earnie/triage-openapi.json get /v1/resolutions/throughput
Returns how many findings were CLOSED per week for one project, split into machine-closed (agent / CI) and human-closed. Powers the Dashboard's Decision throughput card. Reverted resolutions are excluded: a decision that was undone did not close anything. `pending_review` rows ARE counted; the finding is closed from the reviewer's point of view the moment the resolution is written, and excluding them would make an org that reviews its own decisions look idle. Actor classing follows the acting principal, not the permission: a resolution is MACHINE when `autonomous_run` is set or the resolving actor kind is api_key / worker / agent, and HUMAN otherwise. The two are disjoint and sum to the week's total. Weeks are ISO weeks in UTC, newest last, and every week in the window is present — a week with no decisions returns explicit zeros rather than being omitted, so the client never has to distinguish a gap in the array from a quiet week. This is a SEPARATE endpoint from ProjectSummary on purpose: when the findings store is unreachable this request fails alone and the card reads "Unavailable" while every other card on the dashboard still renders. Folding it into the summary would take the whole page down with it.
# API keys
Source: https://docs.scanoss.com/en/latest/earnie/administration/api-keys
How pipelines and scripts authenticate with Earnie, how to create and scope a key, and why each automation consumer should have its own.
An **API key** is a credential that lets a pipeline, script, or coding agent act in Earnie without using a person's account. You need one whenever something other than a person signs in, for example a CI pipeline running the [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli), or a coding agent connected over MCP.
This page explains how to create a key, choose what it can do, use it, and revoke it when it's no longer needed.
Creating keys needs the Admin or Operator role. Operators can manage only the keys they created. Admins can manage every key in the organisation. See [Team & roles](/en/latest/earnie/administration/team-and-roles).
## Why API keys
A pipeline or script shouldn't sign in as a person. An API key gives it its own identity, so Earnie records its actions against the key, not against whoever set it up.
A person always creates a key, and the audit log records that person as its owner. When you create a key, you decide what it can do (its permissions), which projects it can reach, and when it stops working (its expiry).
## Creating a key
1. Go to **Settings → API keys** and select **Generate key**.
2. Enter a **Key name**. Name it after the pipeline or agent that will hold it, for example `ci-main`.
3. Choose its permissions, projects, and expiry, as described below.
4. Generate the key, then copy it straight away.
### Permissions
Under **What this key will hold**, choose what the key may do. These are the same permissions that roles are built from. Three presets tick the boxes for common jobs, and you can then adjust the individual read and write permissions:
| Preset | Scopes it grants | Use it for |
| - | - | - |
| **CI pipeline** | `scans:read`, `scans:write`, `findings:read`, `policies:read` | A pipeline that scans and checks the gate |
| **Coding agent (MCP)** | `projects:read`, `scans:read`, `scans:write`, `findings:read`, `policies:read` | A coding agent connected over MCP |
| **Read-only reporter** | `projects:read`, `scans:read`, `findings:read`, `policies:read` | Reporting and dashboards that change nothing |
A preset only ticks boxes for you. The key stores its scopes, not the preset, so you can start from a preset and add or remove scopes before you generate the key. If your deployment doesn't offer a scope that a preset lists, the preset leaves that scope out.
You can only grant a key permissions you hold yourself, so a key never reaches further than the person accountable for it.
#### Scope reference
These are all the scopes a key can hold:
| Scope | What it allows |
| - | - |
| `organization:read` | Read the organization's details and logo |
| `projects:read` | List, read, and look up projects |
| `projects:write` | Create projects and update their details |
| `scans:read` | Read scans and their results (files, matches, cryptography, AI models, posture), self-checks, SBOM snapshots, and a project's local hook settings |
| `scans:write` | Submit, cancel, and rerun scans and self-checks, import an SBOM, upload AI models, and generate, render, or delete SBOM snapshots |
| `findings:read` | Read findings, their facets, files, components, and history |
| `findings:write` | Triage findings (assign, resolve, reopen, suppress, create rules), revert a resolution, and list the review queue |
| `policies:read` | Read policies, templates, project posture, violations, evaluations, verdicts, and approval history |
| `policies:write` | Create, update, delete, attach, and clone policies, and turn on policy templates |
| `scanconfig:read` | Read scan configuration, automatic scanning, and pull request feedback settings, for the organization and for each project |
| `scanconfig:write` | Change those settings, and a project's local hook settings |
| `integrations:read` | Read connected source-code providers, their health, and the repositories they can reach |
| `audit:read` | Read the organization's audit log |
Earnie refuses a request that needs a scope the key doesn't hold with `403`. The [API reference](/en/latest/earnie/api/using-the-api) lists every operation an API key can call.
A key can never hold some permissions, however it requests them. Only a person can:
* approving, rejecting, or revoking a policy approval (`policies:approve`), which also covers confirming or promoting a decision in the review queue
* managing members (`members:*`)
* SSO (`sso:*`)
* changing organisation settings (`organization:write`)
* connecting or disconnecting integrations (`integrations:write`)
* creating or revoking API keys (`apikeys:*`)
A few actions also need a signed-in person, whatever the key holds: deleting or restoring a project, connecting or disconnecting its repository, importing repositories, resetting a project's scan history, and requesting a policy approval.
Reading isn't restricted in the same way. You can grant a key read access to integrations or the audit log.
#### Letting an agent triage
With the **Coding agent (MCP)** preset, you can also turn on **Allow agents to triage findings**. It's off by default, and it adds the `findings:write` permission.
**Think before turning this on.** A key with triage permission can close
findings and record the reason, including reasons that stop a finding
counting towards a pull request's gate. An automation can then turn a
failing gate green without a person checking it.
A person doing triage has the same authority, and you undo a key's decision
the same way. The decision goes to the review queue, where someone can revert it, and
reopening the finding puts it back in the gate. A key can never
accept a violation it agrees is real. That needs a policy approval request, and
only a person can make one.
### Projects
Under **Projects**, choose **All projects**, or **Specific** to pick a set. Earnie refuses a key that tries to act on a project outside its set.
The **Coding agent (MCP)** preset doesn't cover all projects by default, so the project list opens with none ticked, and **Generate** stays off until you tick at least one project or choose **All projects**. A line under **Projects** says so.
#### How a project-scoped key is refused
Earnie checks a key limited to specific projects on every request. When it calls the API directly, it can get one of two `403` errors:
| Error code | When it happens | What to do |
| - | - | - |
| `project_out_of_scope` | The request names a project, in its path or in a project filter, that isn't one of the key's projects | Use a project the key covers, or use a key that covers this project |
| `project_scope_required` | The request would return data from across projects and names none, for example listing all findings, all scans, or all self-checks without a project filter | Add a project filter that names one of the key's own projects |
The event history for a single finding or other item (`GET /v1/audit/events`) can't be narrowed to a project, so a project-scoped key can't read it.
Some lists don't need a filter. Listing projects, the scan posture overview, the resolution queue, the audit log, and a policy's attachments return only the key's own projects.
When a request identifies something by its own ID rather than by project, such as a scan, a finding, or an SBOM snapshot, and it belongs to a project outside the key's set, the key gets `404`, as if it didn't exist.
### Expiry
Every key expires. Under **Expires in**, choose **30 days**, **90 days** (the default), **365 days**, or a **Custom** date. 365 days is the longest allowed.
### Copying the key
**Earnie shows the key only once.** Copy it with **Copy key** before you close
the panel. You can't retrieve it afterwards.
The same panel, under **Connect your coding agent**, shows an install snippet for each coding-agent client: Claude Code, Cursor, VS Code, Codex, and Claude.ai / ChatGPT. Each snippet uses your own Earnie address. Copy the one you need and paste it where that client expects it. [Connecting an AI client](/en/latest/earnie/mcp/connecting-a-client#option-b-use-an-install-snippet-from-earnie) explains each client's snippet.
Optionally, choose a **Default project for this client**, which the snippet sends as an `X-Earnie-Project` header.
Like the key, the snippets disappear once you close the panel. If you need one later, generate a new key.
### Using a key with the Earnie CLI
On your own machine, sign in and enter the key when prompted:
```bash theme={null}
earnie auth login --api-url https://your-company.earnie.dev
```
In CI, pipe the key in rather than passing it as a command argument, so it doesn't appear in logs or process lists:
```bash theme={null}
printf %s "$EARNIE_API_KEY" | earnie --api-url https://your-company.earnie.dev auth login --api-key-stdin
```
The CLI stores a login in your operating system's keychain when one is available, or otherwise in a local file that only your user can read. `EARNIE_API_KEY` can supply a credential for a single run. The CLI never writes it to the client configuration.
See the [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli) reference for scanning, verdicts, policies, and connecting a coding agent over MCP.
## Managing keys
The **API keys** page lists your keys. For each one, it shows:
| Column | What it shows |
| - | - |
| **Key** | The key's name and the last characters of the key, so you can recognise it without seeing the secret again. A key that isn't active also shows its status, expired or revoked. |
| **Access** | The key's permissions. |
| **Scope** | The projects the key can reach. |
| **Last used** | When the key was last used. |
| **Expires** | When the key expires. |
Use the status filter to show **Active** keys (the default), keys **Expiring** soon, **Revoked** keys, or **All**. Search by name, key prefix, or owner. Select a key to see its full details, including its owner.
Earnie keeps revoked keys for the audit log rather than deleting them, and hides them from the default view.
## Revoking a key
To revoke a key, select **Revoke key** and confirm.
Revoking takes effect immediately and you can't undo it. If you need the access again, generate a new key. A revoked key stays in the audit log, so past actions keep naming it.
**Give each consumer its own key.** Use one key per pipeline, named for that
pipeline, with only the permissions that pipeline needs. Then, when something
needs revoking, you revoke exactly one thing, and the audit log tells you
which automation did what.
**Removing a member revokes their keys.** To keep their automation running,
re-issue the keys under a current member *before* you
remove the member.
Two other states can appear on older keys. A key whose creator was never
recorded shows as having **No active owner**. A key made before permissions
existed shows **None**. Re-issue it before it can do anything.
Some older keys are also limited to particular scanners. That limit is
deprecated. Such a key gets `403 module_scope_forbidden` for data from any
other scanner. Re-issue the key to remove the limit.
## What's next
To call the Earnie API with a key, see [Using the API](/en/latest/earnie/api/using-the-api).
This is the last page of the setup walkthrough. For the concepts, go back to the [Introduction](/en/latest/earnie/introduction).
# Your team
Source: https://docs.scanoss.com/en/latest/earnie/administration/team-and-roles
The three roles Earnie uses to separate who can change the rules from who can work the queue, how to add members, and the full permissions matrix.
A **member** is a person who can sign in to your organisation in Earnie. Each member holds one **role**, which decides what they can see and do. This page explains the three roles, how to add members, and which role can do what.
Not everyone who works with Earnie needs to be a member. Developers and coding agents usually work through pull request checks, the CLI, and the API, using [API keys](/en/latest/earnie/administration/api-keys) rather than a seat in the web app.
Adding members and changing roles needs the **Admin** role.
## Three roles
Earnie has three roles. They separate the people who change the rules from the people who do the daily review work. The roles are built in and you can't customise them.
| Role | Who it's for | Can do |
| - | - | - |
| **Admin** | The people who own the organisation's setup and rules. | Everything, including SSO, members, integrations, and the audit log. |
| **Operator** | The people who review findings and manage projects. | The daily work: projects, scans, triage, policies, and scan configuration. Not organisation settings. |
| **Viewer** | People who need to see results but not act on them. | Read everything they have access to. Records no decisions. |
## Adding a member
1. Go to **Settings → Members**.
2. Select **Add member**.
3. Enter the person's **Email**, and choose their **Role**.
4. Select **Add member**.
On a workspace that signs in with SSO (single sign-on) only, the new member signs in with your organisation's existing SSO. You don't issue a password or send an invitation email.
### Giving a member a password
On a workspace that signs in with an email and password (see [Workspace setup](/en/latest/earnie/getting-started/workspace-setup#email-and-password)), a member you add has no password, so they can't sign in until they choose one.
Right after **Add member**, Earnie shows a one-time **set-password link** for the new member. If a member still hasn't chosen a password later, for example because the link expired, open the row menu and select **Create set-password link**.
* **Earnie shows the link once.** Select **Copy link** and send it to the member privately, for example in a direct message. The dialog stays open until you select **I have copied the link**, because Earnie keeps only a fingerprint of the link and can't show it again.
* **It works once and expires after 72 hours.** It also stops working as soon as you create another one for the same member, or remove the member.
* **It's for a first password only.** Once a member has a password, their row says **Already has a password** instead. You can't create a link for yourself.
Anyone who holds the link can set that member's password until it expires, so treat it like a password. Earnie records both creating and using a link in the [audit log](/en/latest/earnie/evidence/audit-log), without the link itself. On a workspace that signs in with SSO only, the action doesn't appear.
### Resetting a member's password
Only Admins can reset a password, and only on workspaces that sign in with an email and password. When a member has forgotten theirs, or you suspect someone else has it, open the row menu and select **Reset password**. The item appears only for a member who already has a password, and never on your own row. To change your own password, see [Changing your password](/en/latest/earnie/getting-started/workspace-setup#changing-your-password).
Earnie shows a one-time link, the same way it shows a set-password link. Copy it once and send it to the member privately. The member opens it, enters a new password twice, and signs in with it.
* A reset link works once and expires after 24 hours. Creating another reset link for the same member ends the earlier one.
* As soon as the member uses it, their old password stops working, and Earnie signs them out of every device. A device that was already signed in may keep working for up to 15 minutes more.
* Earnie records creating and using a reset link in the audit log, without the link or the password.
## Managing members
The **Members** page lists everyone who can sign in to the organisation, with the role each one holds. You can filter the list by email or role.
* To change a role, pick a new role from the member's row. You can change a role at any time, but not your own.
* To see what a member did, choose **View in audit log** from the row's actions menu.
* To remove a member, choose **Remove from organization** from the row's actions menu. You can't remove yourself.
To compare the three roles side by side in the app, select **Compare all capabilities**.
Earnie hides what a role can't do, rather than showing disabled buttons. If a
control you expect isn't there, your role doesn't allow it.
## Branding
**Settings → Branding** holds your organisation's logo. Earnie prints it in the header of the HTML, Excel, and PDF snapshot reports that you render from the **SBOMs** page.
* To upload a logo, drop a PNG or JPEG of up to 512 KB onto the page. Earnie saves it as soon as the upload succeeds.
* To replace the logo, drop a new image. To remove it, select **Remove logo**. Without a logo, reports have no logo in the header.
A report keeps the logo it was generated with. Changing the logo affects only reports you generate afterwards, never one that already exists.
Every role can see the current logo. Only an Admin can upload, replace, or remove it.
## Who can do what
In this table, ✓ means the role can do it, and a dash means it can't. Some entries are more specific:
| Entry | Meaning |
| - | - |
| **view** | The role can see it but not change it. |
| **own** | The role can manage only the API keys it created. |
| **all** | The role can manage every API key in the organisation. |
| Capability | Admin | Operator | Viewer |
| - | - | - | - |
| View findings, scans, and policies | ✓ | ✓ | ✓ |
| Run and cancel scans | ✓ | ✓ | — |
| Record triage decisions | ✓ | ✓ | — |
| Create and change policies | ✓ | ✓ | — |
| Request a policy approval | ✓ | ✓ | — |
| Approve or reject a request | ✓ | — | — |
| Generate SBOM snapshots and render notice files | ✓ | ✓ | — |
| Arrange your own Dashboard and All projects cards | ✓ | ✓ | ✓ |
| Scan configuration | ✓ | ✓ | view |
| Read the crypto assessment rules (where Cryptography is enabled) | ✓ | ✓ | ✓ |
| Upload or delete an AI model (where AI is enabled) | ✓ | ✓ | — |
| Manage members | ✓ | — | — |
| Reset a member's password | ✓ | — | — |
| SSO configuration | ✓ | — | — |
| Organisation logo (Settings → Branding) | ✓ | view | view |
| Connect or disconnect integrations | ✓ | view | — |
| Organisation audit log | ✓ | — | — |
| API keys | all | own | — |
## What's next
Pipelines and scripts don't sign in as members. Next, set up [API keys for your pipelines and scripts](/en/latest/earnie/administration/api-keys).
# Using the API
Source: https://docs.scanoss.com/en/latest/earnie/api/using-the-api
Call the Earnie API from a script or pipeline: your deployment's base URL, authenticating with an API key, choosing scopes, a first request, and how paging and errors work.
The Earnie API lets a script, pipeline, or internal tool do what a person does in Earnie: submit scans, read findings and posture, triage, manage policies, and export SBOMs. You can call every operation in this API reference with an Earnie API key.
## Base URL
Each organization has its own Earnie deployment at its own address. It's the address you use to sign in, for example:
```text theme={null}
https://your-company.earnie.dev
```
Every API path starts with `/v1/` on that address. In the API reference, set the `tenant` field to your organization's part of the address to send requests to your own deployment.
## Authenticating
Send an API key in the `Authorization` header of every request:
```text theme={null}
Authorization: Bearer sk_earnie_...
```
An admin or operator creates keys under **Settings → API keys**. Earnie shows the key only once, so store it in your pipeline's secret store as soon as you generate it. See [API keys](/en/latest/earnie/administration/api-keys) for how to create one.
Every key expires, after 365 days at most. When a key stops working, the error code says why:
| Status | Error code | Meaning |
| - | - | - |
| `401` | `unauthenticated` | The request carried no key |
| `401` | `invalid_key` | The key isn't a valid Earnie key |
| `401` | `key_expired` | The key has expired. Generate a new one |
| `401` | `key_revoked` | The key was revoked. Ask an admin why before replacing it |
## Choosing scopes
A key's **scopes** decide which operations it can call. For example, listing projects needs `projects:read`, and submitting a scan needs `scans:write`. Give each key only the scopes its job needs.
Three presets cover the common jobs:
* **CI pipeline**: submit scans and read verdicts
* **Coding agent (MCP)**: read posture, findings, and policies, and submit scans
* **Read-only reporter**: read posture without changing anything
The [scope reference](/en/latest/earnie/administration/api-keys#scope-reference) lists every scope and what it allows. A key can also be limited to specific projects. See [How a project-scoped key is refused](/en/latest/earnie/administration/api-keys#how-a-project-scoped-key-is-refused) for the errors that limit produces.
A key can never perform some actions, such as approving a policy request, managing members, or connecting an integration. Those operations aren't in this reference.
## Your first request
List the projects in your organization. The key needs `projects:read`.
```bash theme={null}
curl https://your-company.earnie.dev/v1/projects \
-H "Authorization: Bearer $EARNIE_API_KEY"
```
The response is a JSON array of projects:
```json theme={null}
[
{
"id": "0198f0c2-6a2e-7b1c-9d3e-2f4a5b6c7d8e",
"org_id": "0198f0c2-1b2c-7d3e-8f4a-5b6c7d8e9f0a",
"slug": "billing-service",
"name": "Billing Service",
"is_default": false,
"created_at": "2026-09-14T10:22:31Z",
"updated_at": "2026-09-30T08:05:12Z"
}
]
```
Use a project's `id` in the paths of other operations, for example `GET /v1/projects/{projectId}`. To find a project's `id` from its slug or repository, use `GET /v1/projects:resolve`.
## Paging through lists
Earnie pages lists that can grow large. There are two styles, and each operation's reference shows which one it uses.
**Offset paging.** Send `limit` and `offset`. The response carries the page in `items` and the full count in `total`. For example, the findings list returns 20 findings by default and up to 1000 per page:
```bash theme={null}
curl "https://your-company.earnie.dev/v1/findings?project_id=$PROJECT_ID&limit=100&offset=200" \
-H "Authorization: Bearer $EARNIE_API_KEY"
```
**Cursor paging.** Send `limit`, and leave out `cursor` for the first page. Each response carries a `next_cursor`. Pass it as `cursor` to get the next page. On the last page, `next_cursor` is `null` or absent. The audit log, policy violations, and policy evaluations use cursors.
Some short lists, such as the projects list, return everything in one response.
## Errors
Every error response has the same JSON body:
```json theme={null}
{
"code": "project_out_of_scope",
"message": "project 0198f0c2-6a2e-7b1c-9d3e-2f4a5b6c7d8e is outside the API key scope"
}
```
Use `code` in your scripts. It is a fixed, machine-readable value. `message` is for people to read and can change. A few validation errors add fields that explain what to fix, and each operation's reference documents them.
| Status | What it means |
| - | - |
| `400` | The request is malformed, for example a parameter has the wrong type |
| `401` | The key is missing, invalid, expired, or revoked |
| `403` | The key is valid but not allowed to do this: a missing scope, a project outside the key's projects, or a scanner your organization hasn't enabled |
| `404` | The item doesn't exist, or it belongs to a project the key can't reach |
| `409` | The request conflicts with the item's current state, for example editing a policy revision that has since changed |
| `422` | The request is well-formed but its content isn't valid |
| `429` | Too many failed authentication attempts. Wait for the number of seconds in the `Retry-After` header, then retry |
## Failed authentication attempts
Earnie limits how quickly one client address can retry after failed authentication. After a burst of rejected keys it answers `429` with the code `rate_limited` and a `Retry-After` header. Earnie doesn't count requests made with a valid key. If you see `429`, check the key before you retry.
## What's next
* [API keys](/en/latest/earnie/administration/api-keys): create and scope a key
* [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli): scan and check verdicts from a pipeline without calling the API yourself
# Deployment & data flow
Source: https://docs.scanoss.com/en/latest/earnie/deployment
Where Earnie can run, how each customer's environment is isolated, what stays inside it, and what it sends to SCANOSS and to other services.
Every customer gets an isolated Earnie environment, a deployment of its own that is separate from every other customer's. Your source code, the Earnie database, your scan archives, and your results stay in that environment.
To match your code against open source, Earnie sends fingerprints and hashes to a SCANOSS API. It never sends your source code there.
This page describes where your environment can run, what runs in it, what leaves it, and why.
## Deployment options
You choose who hosts your Earnie environment and which SCANOSS API it talks to.
| Option | Earnie hosted by | SCANOSS API and knowledge base hosted by | Where your source code lives |
| - | - | - | - |
| **SCANOSS-hosted** | SCANOSS | SCANOSS, at `api.scanoss.com` | Your Earnie environment on SCANOSS servers |
| **Self-hosted Earnie** | You | SCANOSS, at `api.scanoss.com` | Your own servers |
| **Fully on-premises** | You | You | Your own servers |
```mermaid theme={null}
flowchart LR
subgraph A["SCANOSS-hosted"]
direction TB
A1["Earnie (SCANOSS servers)"] --> A2["api.scanoss.com"]
end
subgraph B["Self-hosted Earnie"]
direction TB
B1["Earnie (your servers)"] -- "fingerprints only" --> B2["api.scanoss.com"]
end
subgraph C["Fully on-premises"]
direction TB
C1["Earnie (your servers)"] --> C2["SCANOSS API and KB (your servers)"]
end
style A fill:#fff,stroke:#0B1F3A,color:#0B1F3A
style B fill:#fff,stroke:#0B1F3A,color:#0B1F3A
style C fill:#fff,stroke:#0B1F3A,color:#0B1F3A
style A1 fill:#FF6A1A,stroke:#E85D0F,color:#fff
style B1 fill:#FF6A1A,stroke:#E85D0F,color:#fff
style C1 fill:#FF6A1A,stroke:#E85D0F,color:#fff
style A2 fill:#0B1F3A,stroke:#FF6A1A,color:#fff
style B2 fill:#0B1F3A,stroke:#FF6A1A,color:#fff
style C2 fill:#0B1F3A,stroke:#FF6A1A,color:#fff
```
### SCANOSS-hosted
SCANOSS runs your Earnie environment on its own servers and operates it for you, including upgrades, backups, and monitoring. Your source code is uploaded to, or fetched into, that environment. It stays isolated from every other customer's environment and isn't sent anywhere else. You don't need any hardware.
### Self-hosted Earnie
You run Earnie on your own servers and it uses the SCANOSS platform at `api.scanoss.com` for matching. Your source code never goes to SCANOSS. Only the fingerprints, hashes, and package identifiers described in [What leaves your environment](#what-leaves-your-environment) go to SCANOSS.
Most customers who want to keep source code in-house choose this option. It needs one server. See [Self-hosting requirements](/en/latest/earnie/self-hosting-requirements).
### Fully on-premises
You run Earnie and your own SCANOSS installation, the SCANOSS API and knowledge base. Earnie sends its fingerprints and package identifiers to your SCANOSS API instead of `api.scanoss.com`. To set this up, point Earnie's `SCANOSS_API_URL` setting at your SCANOSS API.
This option needs the Earnie server and the much larger SCANOSS knowledge-base server. See [Self-hosting requirements](/en/latest/earnie/self-hosting-requirements) and the [SCANOSS on-premises hardware requirements](/en/latest/installation/on-prem/deployment-guide/hardware-requirements).
An on-premises SCANOSS installation doesn't include everything `api.scanoss.com` serves. In the fully on-premises option:
* **AI provenance still uses `api.scanoss.com`.** The AI model knowledge base isn't shipped for on-premises installations. Earnie fingerprints each AI model file inside your environment and sends only that fingerprint to `api.scanoss.com` to identify the model. If you don't use AI provenance, nothing goes to SCANOSS servers.
* **The side-by-side comparison of a match isn't available.** The open-source file content it shows isn't shipped for on-premises installations. Earnie still reports each match.
These limits apply to on-premises SCANOSS installations today and may change. Contact SCANOSS for the current status.
A fully on-premises Earnie still needs some outbound internet access, for example to download dependencies for cryptography scans and the CISA Known Exploited Vulnerabilities feed. Earnie doesn't support fully air-gapped networks yet. [Network access](/en/latest/earnie/self-hosting-requirements#network-access) lists every connection.
## What runs in your environment
Your environment runs all of Earnie:
* **The web app and the API.** Everyone in your organisation signs in to these, and the CLI, the pre-commit hook, CI pipelines, and coding agents connect to them over MCP.
* **The scanners.** Open-source matching, cryptography detection, and AI provenance each run as a separate scanner process. Your environment runs only the scanners your organisation has.
* **The database.** It's PostgreSQL. It holds your projects, findings, decisions, policies, approvals, and audit trail.
* **A data volume.** It holds the scan archives, which are the source each scan received, along with scan results and working caches.
Your source reaches Earnie in one of two ways. Earnie fetches it from a connected GitHub or GitLab repository, or a producer uploads it. Producers are the [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli), the pre-commit hook, and the upload in the web app. Either way, the source goes to your own environment and nowhere else.
## What leaves your environment
```mermaid theme={null}
flowchart LR
subgraph ENV["Your Isolated Earnie Environment"]
direction TB
E1["Source code and scan archives"]
E2["Database: findings, decisions, policies, audit"]
E3["Scan results and evidence"]
end
ENV -- "Fingerprints and hashes, package identifiers" --> SC
SC -- "Matches, licences, vulnerabilities, matched open-source file content" --> ENV
subgraph SC["SCANOSS Platform"]
direction TB
S1["api.scanoss.com"]
end
ENV <-- "Source fetched in; checks and comments posted" --> GH["Your GitHub or GitLab"]
style ENV fill:#FFF3EA,stroke:#FF6A1A,color:#0B1F3A
style SC fill:#fff,stroke:#0B1F3A,color:#0B1F3A
style GH fill:#fff,stroke:#0B1F3A,color:#0B1F3A
style E1 fill:#FF6A1A,stroke:#E85D0F,color:#fff
style E2 fill:#FF6A1A,stroke:#E85D0F,color:#fff
style E3 fill:#FF6A1A,stroke:#E85D0F,color:#fff
style S1 fill:#0B1F3A,stroke:#FF6A1A,color:#fff
```
Everything inside the orange box stays there. The arrows are the only traffic that crosses its edge.
### To the SCANOSS platform
In the SCANOSS-hosted and self-hosted Earnie options, this traffic goes to `api.scanoss.com`. In the fully on-premises option, it goes to your own SCANOSS API instead.
| What Earnie sends | Why |
| - | - |
| **Fingerprints of each scanned file**, with the file's CRC64 checksum, its size, and its path within the project | To match your files against the SCANOSS knowledge base. The fingerprints are computed inside your environment. The path lets Earnie report each match against the right file. |
| **Package identifiers**, meaning package URLs and versions, of the components a scan found | To look up licences, vulnerabilities, component status, and available versions for those components. |
| **Package identifiers and version requirements of your dependencies**, where cryptography is enabled | To fetch the cryptography SCANOSS has already found in those packages, instead of analysing each one again. |
| **A fingerprint of each AI model file**, where AI provenance is enabled | To identify the model. The model file is fingerprinted inside your environment, then deleted. |
| **The hash of a matched open-source file**, when you open it in the Review Workspace | To fetch that open-source file's content for the side-by-side comparison. |
Two other things come back from SCANOSS:
* **Matched open-source file content.** When you compare a match side by side, your environment downloads the published open-source file from SCANOSS, by its hash. Only that public file travels, and only towards you. Your own file is read from your environment. This isn't available in the fully on-premises option.
* **Detection rules.** Your environment downloads the cryptography detection rules from SCANOSS. Detection itself runs inside your environment.
### To other services
| Destination | What happens |
| - | - |
| **Your GitHub or GitLab** | Earnie fetches the source of the repositories you connected, using the access you granted. It posts its check, a summary comment, and inline comments on pull and merge requests. Those comments describe findings and verdicts. See [Connecting a repository](/en/latest/earnie/getting-started/connecting-a-repository). |
| **Public package registries** | Where cryptography is enabled, Earnie downloads your project's dependencies, such as source packages from Maven Central, to analyse the cryptography inside them. This runs in a sandbox whose network access is restricted to package registries. |
| **The CISA Known Exploited Vulnerabilities feed** | Earnie downloads the public list of known exploited vulnerabilities, once a day by default, to flag them on findings and in policies. Earnie sends nothing to CISA except the download request. |
| **Your identity provider** | If your organisation signs in with single sign-on, sign-in goes through your own identity provider. |
Earnie doesn't send your code, findings, or results to any AI model provider.
### Operational telemetry
In the SCANOSS-hosted option, SCANOSS monitors the health of your environment. To do this, your environment sends operational metrics to SCANOSS. These are counters, durations, and identifiers, such as how many scans ran and how long they took, labelled with your environment's name. They also include the host's CPU, memory, disk, and network usage.
The metrics contain no source code, file content, findings, or scan results. The metrics collector can't read your environment's configuration or secrets.
When you host Earnie yourself, telemetry is off. Earnie sends metrics only if you set a metrics destination, which can be your own monitoring system.
## What never leaves
* **Your source code.** It's fingerprinted inside your environment. Only the fingerprints and hashes described above reach SCANOSS.
* **Your findings, triage decisions, policies, and approvals.** They stay in your environment's database.
* **Your scan archives and results.** They stay on your environment's data volume.
* **Your SBOMs and other evidence.** They're generated inside your environment, and you decide where they go.
## What's next
* [Self-hosting requirements](/en/latest/earnie/self-hosting-requirements) lists the hardware, software, and network access you need to run Earnie yourself.
* [Connecting a repository](/en/latest/earnie/getting-started/connecting-a-repository) lists the exact permissions Earnie asks your Git provider for.
* [Your first scan](/en/latest/earnie/getting-started/first-scan) shows how source reaches Earnie during a scan.
# Cryptography
Source: https://docs.scanoss.com/en/latest/earnie/domains/cryptography
How Earnie finds the cryptography in your code and its dependencies, judges it, and carries it into review, policies, and evidence.
Cryptography is one of the domains Earnie governs, alongside open source and AI provenance. It isn't a separate area of the product. Cryptographic findings appear in the same Review Workspace as every other finding. The same policies judge them, they gate the same pull requests, and Earnie exports them in the same SBOM snapshots.
This page explains what Earnie does with cryptography and links to the pages that cover each part in full.
**Cryptography is available where your organisation has it enabled.**
Without it, Earnie hides the pages, filters, policy fields, and export options
this page describes.
## What Earnie finds
A cryptography scan looks for every place your code uses cryptography. It records one finding for each use of one of these assets:
| Asset | Example |
| - | - |
| **Algorithm** | AES in CBC mode, RSA, SHA-1, ECDSA on P-256 |
| **Protocol** | TLS 1.0, SSL 3.0 |
| **Certificate** | A certificate your code uses |
| **Key material** | A key your code uses with an algorithm |
Earnie looks in two places:
* **Your own code.** Detection rules find calls to cryptography APIs and libraries, and record the exact lines.
* **Your dependencies.** Earnie resolves the libraries your project depends on and finds the cryptography inside them. This catches a weak algorithm that a library uses on your behalf.
Each finding records what Earnie could resolve about the asset, such as its primitive, mode, padding, key size, or elliptic curve, and which library provides it.
Detection runs inside your own Earnie environment. See [Deployment & data flow](/en/latest/earnie/deployment).
### Running a cryptography scan
Cryptography runs as part of a normal scan whenever your organisation has it enabled. You can also choose it for each scan. Select **Cryptography** under **Scanners** on the **New scan** page, or pass `--scanners crypto` to the [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli#choosing-scanners). While it runs, the scan shows the **Detecting cryptography** stage.
## How Earnie judges it
Earnie gives every cryptographic finding three judgements:
* **Severity.** How weak the cryptography is. Earnie looks this up in curated tables of algorithm families, key sizes, modes, and protocol versions. MD5 is weak whatever you use it for. RSA with a 1024-bit key is weaker than RSA with a 3072-bit key.
* **Post-quantum status.** Whether a future quantum computer breaks it. The status is **Vulnerable**, **Safe**, **Not applicable**, or **Unassessed**. **Not applicable** covers hashes and symmetric ciphers, which have nothing for a quantum computer to attack.
* **Export-control signals.** Evidence for your own export-control self-classification. Earnie never states an export classification for you.
Earnie only judges what its tables cover. Earnie marks an algorithm it hasn't curated as **Unassessed** rather than guessing a severity.
### Crypto assessment rules
Earnie publishes the tables behind those judgements at **Settings → Crypto Assessment**. The page is read-only and the same for every organisation. It shows the rules Earnie applies, so you can check why a finding got its severity.
The page has one table, with one row per family. A **scope** switch moves between **Algorithms**, **Key material**, and **Protocols**. You can search for a family, an alternative name, or a protocol, and use **Show** to narrow the table to **High severity**, **Quantum-vulnerable**, or **Export-controlled** rows.
Each row gives the family's **Base severity**, its **Post-quantum** verdict (for algorithms), and its **Export threshold**. Select a row to open its details drawer, which shows:
* the base severity, and any key-size or mode variants that change it
* the post-quantum verdict and the reasoning behind it
* the export-control line the family falls under
* the published sources each value cites
**Export ruleset** downloads the whole published ruleset as JSON. See [Where the severity comes from](/en/latest/earnie/using-earnie/triaging-findings#where-the-severity-comes-from) for how a base severity becomes a finding's severity.
## Reachability
A weak algorithm your application never runs is less urgent than one on a code path that runs. Earnie traces calls through your code and its dependencies to tell them apart:
| Reachability | What it means |
| - | - |
| **Reachable** | Earnie traced a call path from an entry point of your application. |
| **Not reached** | Earnie looked and found no such path. |
| **Unknown** | The analysis couldn't decide, and Earnie says so rather than guess. |
When a finding is reachable, you can open its **Call trace**. It shows the path from the entry point to the cryptographic call, hop by hop, including the libraries it passes through. Earnie raises a reachable finding with an assessed severity by one level, and the finding explains why. An unassessed finding is never raised.
**Not reached is not the same as not used.** Reachability is static
analysis. It can't follow code that runs only through reflection or
configuration. Treat a reachable finding as more urgent, but never treat an
unreached one as harmless.
See [Reachability](/en/latest/earnie/using-earnie/triaging-findings#reachability) for path strength, dependency chains, and reading the call trace.
## Reviewing cryptographic findings
### In the Review Workspace
In the [Review Workspace](/en/latest/earnie/using-earnie/triaging-findings#cryptographic-findings), a cryptographic finding highlights the exact lines in the same source panel as any other finding. To see only cryptography, open **+ Filter** and choose the **Cryptography** evidence type, or use one of the preset views: **Weak crypto**, **Quantum vulnerable**, or **Reachable crypto**. Cryptography also has its own filters, such as algorithm, algorithm family, reachability, post-quantum status, and key size.
You can record one of three decisions on each finding:
| Decision | Use it when |
| - | - |
| **Confirm usage** | The cryptography is in use. |
| **False positive** | It isn't cryptography. |
| **Accepted risk** | The usage is real, and you accept the risk. |
There's no "Fixed" decision. You fix a weak algorithm by removing it, and the next scan reports it gone. See [Making a decision](/en/latest/earnie/using-earnie/triaging-findings#making-a-decision).
### On the Algorithms page
Each project's sidebar has an **Algorithms** page that lists every algorithm detected in the project, one row per algorithm. Each row shows its key size, quantum status, severity, how many assets and files use it, its open findings, and the libraries that provide it.
Use it to answer questions such as "where do we use RSA?" or "what here isn't quantum-safe?". Select a row to open the Review Workspace filtered to that algorithm. See [Starting from the Algorithms page](/en/latest/earnie/using-earnie/triaging-findings#starting-from-the-algorithms-page).
### On the Dashboard
The project [Dashboard](/en/latest/earnie/using-earnie/dashboard) has a **Cryptography** tab with cards for reachability, asset composition, post-quantum exposure, export readiness, and algorithm families. Trend lines for **Reachable weak crypto** and **PQC-vulnerable assets** track how your cryptography changes across scans.
## What changed between scans
A completed scan's page has a **Crypto changes** card. It compares the scan with the nearest earlier scan that measured the project's cryptography in full, and counts findings as **New**, **Resolved**, **Re-detected**, or **Still open**. **Resolved** means the finding is absent from this scan.
The card appears only when there's an earlier scan to compare against. Without one, Earnie hides the card rather than showing zeros. See [Crypto changes between scans](/en/latest/earnie/getting-started/first-scan#crypto-changes-between-scans).
## Policies for cryptography
The same [policies](/en/latest/earnie/using-earnie/setting-policies) govern cryptographic findings as every other finding. With cryptography enabled, the policy editor offers a **Crypto** field group and seven cryptography templates:
* **Block reachable weak cryptography**
* **Flag quantum-vulnerable cryptography**
* **Block weak finite-field key sizes**
* **Block weak elliptic curves**
* **Warn on legacy protocol versions**
* **Warn on confidentiality crypto over export threshold**
* **Warn on unresolved export evidence**
Regulatory template sets, such as PCI DSS 4.0 and the EU Cyber Resilience Act, select all the templates mapped to a clause in one action. See [Rules about cryptography](/en/latest/earnie/using-earnie/setting-policies#rules-about-cryptography) for every template and field.
A cryptography policy gates pull requests, the pre-commit hook, and coding agents the same way as any other policy. See [When the gate blocks a merge](/en/latest/earnie/using-earnie/merge-gate).
## Evidence: CBOM and export control
### CBOM
A **CBOM** (cryptography bill of materials) lists your cryptographic assets in a standard format. In Earnie, it's a CycloneDX 1.7 document generated from an SBOM snapshot.
On a project's **SBOMs** page, select **Generate snapshot** and choose the **CBOM** scope. To get one document holding everything, choose **xBOM**, or turn on **Cryptographic assets** alongside open-source components. Each asset carries its cryptographic properties, where it occurs in your code, its reachability, and its export-control signals. SPDX doesn't support cryptographic assets, so a CBOM is always CycloneDX.
From the command line, `earnie export --include crypto` writes the same document. See [Exporting SBOMs](/en/latest/earnie/evidence/exporting-sboms#cryptographic-asset-details).
### The export-control evidence report
A snapshot that includes cryptographic assets also produces an **export-control evidence report**. This is one HTML file with the evidence you need to classify your own product under export-control rules. It lists the detected assets, their export-control signals, what Earnie scanned, and which ruleset it used.
The report is evidence for your own self-classification. It isn't legal advice, and it states no export classification. Download it from the snapshot's row menu, or from the Dashboard's **Download** menu. See [The export-control evidence report](/en/latest/earnie/evidence/exporting-sboms#the-export-control-evidence-report).
## What's next
* [Triaging findings](/en/latest/earnie/using-earnie/triaging-findings#cryptographic-findings) explains how to work through cryptographic findings in the Review Workspace.
* [Setting policies](/en/latest/earnie/using-earnie/setting-policies#rules-about-cryptography) explains how to turn your cryptography standards into rules.
* [Exporting SBOMs](/en/latest/earnie/evidence/exporting-sboms) explains how to produce a CBOM for a customer or an auditor.
# Proving it later
Source: https://docs.scanoss.com/en/latest/earnie/evidence/audit-log
What the organisation-wide audit log records, how to filter it, and when to use it instead of a finding's own audit trail.
The **audit log** is Earnie's permanent record of what happened across the organisation, who did it, and when. It records every action, whether a person, an API key, or the system took it.
Use it when you need to show why something happened, often long after the fact. For example, months after a release, a customer or an auditor may ask why your team accepted a particular component. The audit log shows who decided, when, and what the situation was at the time. The same applies to other decisions, such as granting a Policy Approval or changing someone's role.
**Before you start:** The audit log is at **Settings → Audit log**, and only Admins can open it. Anyone else sees "Audit Log unavailable". See [Team & roles](/en/latest/earnie/administration/team-and-roles).
## What gets recorded
The log records almost everything that happens in your organisation, for example:
* scans submitted, and their gates evaluated
* findings resolved
* policies attached and changed
* policy approvals requested and granted
* members and roles changed
* API keys created and revoked
* integrations connected, disconnected, or changing health, and a connection's CA certificate replaced. The entry names each trusted certificate by its subject and expiry and never includes the certificate itself.
* set-password and password-reset links created and used, and passwords changed. The entry never includes the link or the password.
* SSO configuration changes
## Finding an event
The log groups entries by day. It folds repeated identical events into a single row until you expand it, and marks an event that records a failure with a **Failed** badge.
To narrow the list, use the filters above it:
| Filter | What it does |
| - | - |
| **Search** | Matches the actor or the event type. |
| **Time range** | **All time**, **24 hours**, **7 days**, **30 days**, or a **Custom** range between two dates. |
| **Actor** | Shows only actions taken by one person or API key. |
| **Type** | Shows only one kind of event. |
| **Project** | Shows only events for one project. |
| **System activity** | Includes events the system recorded on its own. Off by default. |
Each active filter appears as a chip under the filter bar, and you can clear each chip on its own. **Refresh** reloads the list with the same filters.
### Reading an event
Select a row to see the full detail of that event:
* **Time**, and the same time in **UTC**
* **Actor**, the person, key, or worker that took the action
* **Target**, the thing the action was taken on
* **Project**
* **Event ID**
* **Occurrences**, each time the event happened, for a folded row
* **Payload**, the full recorded data, which you can copy with **Copy JSON**
## What makes it evidence
Two properties make the audit log usable as evidence:
* **Earnie never edits or deletes an entry.** Each entry stays exactly as Earnie recorded it.
* **Earnie names automation as the actor.** When an API key or a background worker takes an action, the entry credits that key or worker, not a person.
## Two levels of history
For a single item, a narrower view is usually faster than the audit log:
| To see the history of | Use |
| - | - |
| One finding | The **Audit trail** on that finding in the [Review Workspace](/en/latest/earnie/using-earnie/triaging-findings#the-audit-trail). It shows that finding's history on the finding itself. |
| One scan run | The [Audit trail card](/en/latest/earnie/getting-started/first-scan#the-audit-trail-of-a-run) on the run's page, with its submission, gate, approvals, and publications. |
| Anything, across projects or people | The organisation-wide audit log, described on this page. |
## What's next
To control who can make these decisions, see [your team's roles and API keys](/en/latest/earnie/administration/team-and-roles).
# Exporting an SBOM
Source: https://docs.scanoss.com/en/latest/earnie/evidence/exporting-sboms
How to generate an SBOM snapshot, choose its format, and render the notice file (attribution document) that carries the licence text you're obliged to distribute.
An **SBOM** (software bill of materials) lists the components your software contains, with their versions and licences. You give it to a customer, an auditor, or a regulator when they ask what's in your product.
This page explains how to generate an SBOM in Earnie, which formats and extras to choose, and how to produce two related documents, the export-control evidence report and the notice file.
**Before you start:**
* The project needs at least one completed scan. See [Your first scan](/en/latest/earnie/getting-started/first-scan).
* Triage the findings first. An SBOM records your current decisions, so a project you haven't triaged produces an SBOM with low review coverage. See [Triaging findings](/en/latest/earnie/using-earnie/triaging-findings).
* You need the Operator or Admin role to generate a snapshot. Viewers can download existing snapshots but can't create them.
## What an SBOM is, in Earnie
In Earnie, an SBOM is a **snapshot**. It records the project's inventory and your triage decisions at one point in time, and Earnie never rewrites it.
To find snapshots, select **SBOMs** in a project's sidebar to open the **SBOM snapshots** page. Everything in a project's Dashboard [Download menu](/en/latest/earnie/using-earnie/dashboard#downloading-evidence-from-the-dashboard) comes from the snapshot attached to the scan behind the Dashboard's figures. The SBOM snapshots page is where those files are stored.
**Snapshots are immutable.** Earnie never edits a snapshot. If the inventory changes, you
generate a new one. A document you sent to a customer last quarter still says exactly what it
said when you sent it.
## Generating a snapshot
You can start from either of two places:
* **SBOMs** in the sidebar → **Generate snapshot**
* **Review** in the sidebar → **Export SBOM**, which generates a snapshot of the scan you're reviewing
Both open the same **Generate snapshot** panel.
1. **Choose a scope.** A scope preset picks which sections the snapshot holds. The panel offers only the presets your organisation's scanning covers:
| Scope | Holds |
| - | - |
| **SBOM** | Open-source components |
| **CBOM** | Cryptographic assets |
| **AIBOM** | AI models |
| **xBOM** | All three |
| **Custom** | Any other combination you choose in the sections below |
SPDX is available only for the SBOM scope. Earnie names downloaded files after the scope, for example `…-cbom`, or `…-sbom-aibom` for a custom mix.
2. **Choose one or more documents.** You must choose at least one, and you can choose both:
| Document | Use it for |
| - | - |
| **CycloneDX 1.7** | Broad tool support |
| **SPDX 2.3 Lite** | A licence-focused profile |
You can render the other format later from the same snapshot.
3. **Choose what to embed in CycloneDX (optional).** These options are available only when you select CycloneDX. SPDX doesn't support them. Both are off by default.
* **Known vulnerabilities** embeds known vulnerability data. Each advisory names only the affected versions in the snapshot. When several sources report the same CVE, the document keeps the highest severity and scores, and names every source that reported it.
* **Cryptographic assets** adds detected algorithms as CycloneDX cryptographic assets, which makes the document a CBOM (cryptography bill of materials). It also produces the [export-control evidence report](#the-export-control-evidence-report). This option appears only when cryptography scanning is enabled for your organisation.
If your organisation has the **Enrichment** option and you choose both, each library that a cryptographic asset comes from and that has a known vulnerability appears once as a component carrying that vulnerability. This holds even when OSS matching also found the library.
4. Select **Generate snapshot**.
Some scans finished before Earnie began keeping SBOM evidence. They have nothing to freeze, and Earnie doesn't rebuild them from today's catalogue. If you choose one, Earnie tells you to run a new scan, and the API refuses with `422 scan_evidence_unavailable`. Earnie stores nothing. **Generate SBOMs** on [All projects](/en/latest/earnie/using-earnie/all-projects) reports such a project the same way and generates SBOMs for the other projects.
### Licences and tool information in the document
* **Concluded and detected licences.** A component you confirmed carries the licence you concluded and, next to it, the licence the scan detected. CycloneDX records them as separate licence entries marked `concluded` and `declared`. SPDX records them as `licenseConcluded` and `licenseDeclared`. The notice file names only the concluded licence.
* **Generating tool.** Every CycloneDX and SPDX document names Earnie and the build that generated it as its tool, for example `earnie` and its version. A document filed with an auditor says which release produced it. If you render a format later from the same snapshot, that document names the build that rendered it, which can be a newer release.
### What the snapshot records
Earnie stamps each snapshot with:
* **Reviewed**, the review coverage when the snapshot was generated
* **Components**, the number of components
* **Generated by**, the person who generated it
### Cryptographic asset details
When **Cryptographic assets** is on, the CycloneDX document has extra detail for each asset:
* **Export-control metadata.** Each cryptographic-asset component has `scanoss:exportControl:*` properties. They record the rolled-up signal state, the asset's purpose, and the control regime and regime version Earnie evaluated the signals against.
* **Sizes.** Key, nonce, salt, and IV assets have their size under `relatedCryptoMaterialProperties.size`, when Earnie resolved it.
* **Curves.** Elliptic-curve algorithms have their curve under `algorithmProperties`, when Earnie resolved it.
### When a snapshot is out of date
If triage has changed since a snapshot was generated, its row shows a **Stale** badge. Hover it to see "Triage has changed since this snapshot was generated." Generate a new snapshot before you send it out.
### How decisions carry into a snapshot
A new SBOM or CBOM uses each finding's current decision, never a withdrawn or superseded one.
* **Reopening a finding** restores its unreviewed evidence for the next snapshot.
* **Excluding or replacing one occurrence** of a component doesn't remove another occurrence of the same component that's still present.
* **If the remaining occurrences disagree** on known component metadata, generation stops with a `resolution_conflict` error. Reconcile those findings' decisions in Review, then generate again. Existing snapshots and downloads don't change.
### CRA SBOM annex
The **CRA SBOM annex** is a CycloneDX 1.7 export for the EU Cyber Resilience Act (CRA). It includes the detected cryptographic assets as components. It goes with the product SBOM. It is not a CRA conformity assessment.
**Not the same CRA reference as a policy.** This annex isn't tied to a specific CRA clause. The
**CRA — Annex I(2)(e)** and **CRA — Annex I Part II(1)** [regulatory template
sets](/en/latest/earnie/using-earnie/setting-policies#regulatory-template-sets) are policies, not
an SBOM export. All three carry a "CRA" label, but they're unrelated features.
Neither the Generate snapshot panel nor the [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli#exporting-an-sbom) lets you choose the CRA SBOM annex. When a snapshot has one, its row shows a **CRA** badge.
Earnie doesn't verify FIPS 140-3 module validation.
## The snapshot list
Each row on the SBOM snapshots page shows when the snapshot was generated, the scan it came from, its documents, and the stamp described above.
The **Documents** column shows a badge for each document the snapshot has:
| Badge | Document |
| - | - |
| **CDX** | CycloneDX SBOM |
| **SPDX** | SPDX SBOM |
| **NOTICE** | [Notice file](#notice-files-attribution-documents) |
| **ECR** | [Export-control evidence report](#the-export-control-evidence-report) |
| **CRA** | CRA SBOM annex |
| **HTML**, **CSV**, **XLSX**, **PDF** | Snapshot report, in that format. The HTML, Excel, and PDF reports show your organisation's logo if one is set in [Branding](/en/latest/earnie/administration/team-and-roles#branding). The CSV report downloads as a zip with one file per section. |
From each row's menu you can download a document, render a format the snapshot doesn't have yet, copy the snapshot ID, regenerate from the same scan, or delete the snapshot.
## The export-control evidence report
Some countries control the export of software that uses cryptography. The **export-control evidence report** collects the evidence you need to classify your own product under those rules.
When a snapshot was generated with **Cryptographic assets** on, its row menu offers **Download Export-control evidence report**. The report is a self-contained HTML file, frozen with that snapshot. It records:
* the detected cryptographic assets
* their export-control signals
* the detection surface it cites, meaning what was scanned
* the identity of the ruleset Earnie used
The report is evidence for your own self-classification. It isn't legal advice or a determination, and it doesn't state or imply an ECCN (export classification).
The report's header names the export dataset and the date it was reviewed. It then tells you to verify the evidence against the current 15 CFR 740.17 and 15 CFR Part 774 Supplement No. 1, Category 5 Part 2, before you rely on it. Its purpose legend separates confidentiality from Technical Note 1 exclusions, and it describes the Technical Note 2.c test in plain language.
The report states that all detected cryptography maps to published standards only when its
frozen evidence shows that for the named detection surface. Otherwise it marks the statement as
not established. Signature-based detection can't prove that no non-standard cryptography is
present. To cover a later scan, generate a new snapshot with Cryptographic assets on.
When cryptography is enabled for your organisation, the Cryptography card on the Dashboard opens the Dashboard's Cryptography tab. To see available reports or generate the next snapshot, open **SBOMs** in the sidebar.
## Notice files (attribution documents)
An SBOM and a notice file are different documents, and one can't replace the other:
* An **SBOM** lists what's present and names each component's licence.
* A **notice file**, also called an attribution document, contains the text you're obliged to distribute. An OSPO (Open Source Program Office) ships it with a release.
For every component, the notice file includes:
* its licences
* the full text of its `LICENSE` and `NOTICE` files
* its copyright holders
* its obligations
### Rendering a notice file
You don't create a notice file in the Generate snapshot panel. Earnie renders it from a snapshot that already exists:
1. On the SBOM snapshots page, open the snapshot's row menu.
2. Choose **Render Notice file**.
Earnie fetches the licence data for every component in that snapshot's inventory and writes it to a plain-text file. The row then shows a **NOTICE** badge.
If Earnie couldn't resolve a component's licence data, it doesn't drop the component. It lists it at the end of the file under **Unresolved components**, so you can see what the file doesn't cover.
**Frozen on first render.** The first time you render a snapshot's notice file, Earnie fixes
its content. Later downloads return exactly the same file, even if the upstream licence text
changes. Hover the **NOTICE** badge to see when Earnie fetched the licence text, and compare that
date with what you're about to ship.
### Notice files elsewhere in Earnie
The same document is available in two other places:
* **On a project's Dashboard**, the Download menu offers it as **Notice file**. Earnie renders it from a snapshot, so a snapshot has to exist first. If there's none, the Dashboard says "No SBOM snapshot exists for the current scan" instead of offering the notice file.
* **On [All projects](/en/latest/earnie/using-earnie/all-projects)**, the **Generate notice files** action doesn't need an existing snapshot. For a project with no current snapshot, it generates one and then renders the notice file from it, in one step.
## What's next
When someone asks, months from now, why a component was accepted, look in [the audit log](/en/latest/earnie/evidence/audit-log).
# Connecting a repository
Source: https://docs.scanoss.com/en/latest/earnie/getting-started/connecting-a-repository
What access Earnie asks for when you connect a repository, how each connection is made, and how to manage or disconnect it afterwards.
Before Earnie can scan your code, it needs access to the repository that holds it. Earnie connects to **GitHub** through an installed GitHub App, and to **GitLab** with a personal, project, or group access token. You can connect both.
You make this connection during [workspace setup](/en/latest/earnie/getting-started/workspace-setup#2-connect-a-provider), or later from **Settings → Integrations**. This page explains what access you're granting, what happens at each step, and how to manage the connection afterwards.
## Before you begin
For GitHub:
* **Installing the app requires a GitHub organisation owner.** If you're not an owner, you can still start the connection and hand the final steps to someone who is. See [If you are not an organisation owner](#if-you-are-not-an-organisation-owner).
* **Managing the connection afterwards requires the Admin role in Earnie.** Only Admins can connect, change, or disconnect an integration. See [Team & roles](/en/latest/earnie/administration/team-and-roles).
For GitLab:
* **Creating the token requires access to a GitLab group, subgroup, or project.** Use a group or project access token rather than a personal one, so the connection doesn't depend on one person's account. See [Choosing a GitLab token](#choosing-a-gitlab-token).
* **Managing the connection afterwards requires the Admin role in Earnie.** See [Team & roles](/en/latest/earnie/administration/team-and-roles).
## What Earnie asks for
### GitHub permissions
Earnie's access to your source code is read-only and covers only the repositories you choose. Earnie also needs to write to pull requests and checks, so it can report its results where your team reviews code.
Earnie lists each permission, and why it needs it, before you approve anything:
| Permission | Why Earnie needs it |
| - | - |
| Repository contents (read) | Earnie reads your source at scan time to fingerprint it and match it against the SCANOSS knowledgebase. |
| Metadata (read) | Earnie uses repository names, the default branch, and languages to organise findings by project. |
| Pull requests (read & write) | Earnie comments on and reviews pull requests with its findings. |
| Checks (write) | Earnie posts its policy verdict as a check on each pull request. |
### Choosing a GitLab token
The token must meet three requirements:
* The host it was created on runs **GitLab 17.4 or later**. The host can be gitlab.com, GitLab Dedicated, or self-managed.
* It has the **`api`** and **`read_repository`** scopes.
* It is **active and unexpired** at the time you connect.
Earnie checks these before saving anything and refuses a token that fails any of them.
Earnie works out the token's kind from the account GitLab created it for:
| Token kind | Acts for |
| - | - |
| **Group access token** | The group and all its subgroups. Recommended. |
| **Project access token** | One project. |
| **Service account token** | Whatever the service account can reach. |
| **A person's own token** | Accepted, but flagged in Settings, because the connection depends on that person's account. |
**Fine-grained personal access tokens aren't supported yet.** They carry
per-resource permissions instead of scopes, so Earnie's scope check refuses
them. Use a project, group, or classic personal access token instead.
### Merge blocking depends on your GitLab plan
GitLab's API shows a group's plan only to a token with the **Owner** role. A token with less access, such as a Maintainer-level service account, can't see it. If Earnie can see an **Ultimate** plan, the connection can enforce a merge block the same way a GitHub check does. Otherwise the connection is **advisory**. Earnie still comments and posts a status, but states that it can't enforce a block. Settings and the scan page both show which mode a connection is in.
## How the connection is made
### Connecting GitHub
The connection takes four steps. Some happen in Earnie and some in a GitHub window that Earnie opens for you.
1. **Authorise Earnie to see your organisations.** Earnie shows a short code. Enter it on GitHub, in the window that opens, so Earnie can list the GitHub organisations you belong to.
2. **Choose the organisation that will own the connection.** This is usually the organisation that owns the repositories you want to scan.
3. **Earnie creates a GitHub App** owned by that organisation. The app holds the permissions listed above.
4. **Install the app and choose which repositories it may read.** You can grant access to all repositories in the organisation, or only to the ones you select.
After you install the app, Earnie moves on without further input. In the setup wizard, the next step is choosing which repository your first project will govern.
#### If you are not an organisation owner
Only an owner of the GitHub organisation can install the app. If you choose an organisation where you're a member but not an owner, Earnie creates a single-use hand-off link that you can send to an owner. The link shows the date it expires.
The owner opens the link and completes the two remaining steps on GitHub, which are creating the app and installing it. They don't need an Earnie account. Your setup continues on its own once they're done.
#### Connecting GitHub Enterprise Server
To connect your own GitHub Enterprise Server, choose **GitHub Enterprise Server** in the first step and enter its address, such as `https://github.example.com`.
* **Requirements.** Earnie needs GitHub Enterprise Server 3.4 or later, and shows an error when the server is older, unreachable, or not trusted. Your server must be able to reach Earnie to send it webhooks.
* **Internal CA.** If an internal CA signs the server's certificate, paste that CA certificate too. Earnie trusts it for this server only.
* **No authorisation code.** Instead of entering a code, type the organisation that will own the app, or leave it empty for your personal account.
You can connect github.com and any number of Enterprise Servers side by side.
### Connecting GitLab
1. From onboarding or **Settings → Integrations**, choose **GitLab**.
2. Enter the **host**. The default is `gitlab.com`. For a self-managed instance, enter your host's URL.
3. Paste the **access token**.
4. If your host uses a certificate signed by an internal certificate authority, see [Trusting a private CA](#trusting-a-private-ca) below.
5. Select **Connect**. Earnie verifies the host and the token before saving anything.
**Someone else can connect it for you.** From a **hand-off link**, a group
Owner can paste a token at a public `/connect/gitlab` page without an Earnie
account or session. The link works once.
#### Trusting a private CA
If a company's own internal CA signs a self-managed GitLab host's certificate, verification fails by default. Earnie's environment doesn't trust your company's internal CA out of the box. See [Deployment & data flow](/en/latest/earnie/deployment) for where Earnie runs. To fix this, paste the CA certificate in PEM format into **My GitLab uses an internal CA** on the connect form. The form opens that field on its own if the host's certificate fails to verify. The connection's card shows the trusted certificate's name and expiry, with a **Replace CA certificate** action for when the certificate is renewed.
#### Importing projects
Once you're connected, **New scan → Repository**, or the repository picker during onboarding, lists the projects the token's account can reach. A group token lists the group and all its subgroups, a project token lists its one project, and a personal token lists your own memberships. Earnie groups projects by their nested namespace, such as `acme/platform/infra`.
* **Archived projects are left out.**
* **The list stops at 5,000 projects**, sorted by path, and says so. A search goes to GitLab, which matches a project's name or path across the whole group, so a search finds projects beyond the cap.
* **With more than one connection**, a selector above the list, for example **GitLab · acme**, chooses which connection you're picking from.
Pick a project to import it as a new Earnie project, or connect it to an existing one. Its first full scan then starts on its own. To import every project at once, see [Importing all repositories at once](#importing-all-repositories-at-once).
Two GitLab hosts can each hold a project at the same path, such as `acme/api` on gitlab.com and on your own host. Earnie imports each one through the connection you picked it from, and each becomes its own Earnie project.
**Importing and connecting a project needs Maintainer access on GitLab**,
because Earnie registers a project webhook at import time. Earnie refuses a
token below Maintainer with an error that names the problem, and creates nothing.
Disconnecting a project or a connection removes its hook.
#### What triggers a scan
With a project imported, Earnie scans in two cases:
* **Opening or updating a merge request** starts a partial-coverage scan of the merge request's head, the same as for a GitHub pull request.
* **A push to the default branch** starts a full rescan, the same as a push to GitHub's default branch.
A title or label edit, or an approval, triggers nothing. Only a branch push counts. Earnie ignores deleted branches and tags.
#### Feedback on a merge request
A scanned merge request gets the same three things a GitHub pull request does, under GitLab's names for them:
| What | Where |
| - | - |
| **A commit status** | Named `Earnie`, on the merge request's pipeline. It reads running, success, failed, or skipped once a result is no longer conclusive. |
| **A summary note** | One note, edited in place on every scan. Earnie always posts it, regardless of your comment-mode setting. |
| **Inline discussions** | Placed on the exact changed lines. Earnie resolves each one with "Resolved in ``" once its finding is gone, and never reopens one a person has resolved. |
If your connection is advisory rather than enforcing (see [Merge blocking depends on your GitLab plan](#merge-blocking-depends-on-your-gitlab-plan)), a blocking gate adds a line to the note. The line says that Earnie recommends blocking but this connection can't enforce it.
## Managing the connection
After setup, you can change the connection at any time from **Settings → Integrations**.
### Managing GitHub
| To do this | Use |
| - | - |
| Connect another GitHub organisation | **Add organisation**, which opens GitHub so you can install the app on that organisation. |
| Connect another host, such as a GitHub Enterprise Server | **Connect another GitHub**. The card lists each host. |
| Change which repositories Earnie can read | **Manage repositories**, which opens the app's installation on GitHub. |
| Import every repository at once | **Import all repositories**. See [Importing all repositories at once](#importing-all-repositories-at-once). |
| Remove Earnie's access entirely | **Disconnect**. With more than one host connected, each host has its own. |
#### Disconnecting GitHub
Disconnecting removes the Earnie app from your GitHub organisation and deletes its stored credentials. Earnie loses access to your repositories, and future scans stop.
Earnie keeps the findings and decisions you've already recorded.
### Managing GitLab
The GitLab card sits beside GitHub's. It shows health, scopes, the connected account, token kind, and an expiry countdown that changes its warning at 14 days and again at 3 days before expiry. From the card you can:
| To do this | Use |
| - | - |
| Renew before expiry | **Replace token** |
| Update the trusted CA | **Replace CA certificate** |
| Add another host or group | **Add connection** |
| Import every project at once | **Import all repositories** |
| Remove Earnie's access entirely | **Disconnect** |
Below the connections, **Repositories** previews the GitLab projects your tokens reach, as on the GitHub card. **View all** lists every one with its language, visibility, and default branch, and lets you filter them. The list shows what Earnie can read, not what's imported.
Disconnecting removes any hooks the connection registered on GitLab and deletes its stored credentials. Projects bound through it keep their findings and decisions but stop scanning. If you connect GitLab again, those projects get their connection back and Earnie registers their webhooks again.
## Importing all repositories at once
To get a report on all your repositories on day one, import every repository of a connection in one go instead of one at a time. This needs the **Admin** role, the same as connecting a repository.
1. On **Settings → Integrations**, select **Import all repositories** on the GitHub or GitLab card.
2. Pick the connection, if there are several.
3. Optionally, narrow the repositories by path. `*` stands for anything, so `acme/platform/*` takes one subgroup and `*-service` takes every service. Earnie leaves out forks unless you select **Include forks**. It always leaves out archived repositories, since they receive no changes.
4. Select **Preview**. Earnie lists the repositories it would import, how many are already imported, and how many the filter leaves out. Nothing is created yet.
5. Select **Import**. Earnie creates one project per repository, connects it with its webhook, and scans it once.
The import runs in the background, so you can leave the page or close the browser. An **Importing** indicator at the top of every page shows its progress and opens the import's page. That page counts the repositories in each state and estimates how long is left. Earnie notifies you once when the import finishes.
Projects appear within minutes. Their first scans run at a lower priority than the scans someone is waiting on, so a large import never holds up a pull or merge request.
* **A repository that fails shows the reason**, for example a GitLab token below Maintainer that can't add the webhook. Fix the cause and select **Retry failed**. Earnie imports only the failed repositories again.
* **Stop import** skips the repositories not started yet. The projects already created stay.
* **Importing again later** adds only the repositories that are new since the last import.
## Connection health
Earnie checks on a schedule that each connection still has the access it needs.
### GitHub connection health
Earnie also runs the check when you connect the app or update its permissions. The card for each connection on **Settings → Integrations** shows one of three states:
| State | What it means |
| - | - |
| **Healthy** | The app is installed and has every permission Earnie needs. |
| **Degraded** | The app is suspended, a permission is missing, or GitHub is permanently refusing Earnie's requests. |
| **Disconnected** | The app has been removed from the organisation on GitHub. |
A temporary GitHub outage never changes the state. Only a permanent problem does.
#### Fixing a missing permission
When a permission is missing, the card lists it and offers **Review on GitHub**. Select it, then accept the new permission request on the app's installation page. The state returns to **Healthy** on the next check.
#### When the app is removed on GitHub
If someone uninstalls the app on GitHub, the card shows **Disconnected**. Earnie compares its connections with GitHub on a schedule, and the next time it does, it removes the connection entirely. You can then install the app again to restore access.
The state also shows on projects. A project whose repository belongs to an affected
connection shows a **GitHub connection degraded** or **GitHub connection
disconnected** badge in its header, which links to Settings → Integrations.
Earnie records every change of state in the [organisation audit
log](/en/latest/earnie/evidence/audit-log) as **Integrations connection health
changed**.
### GitLab connection health
Earnie re-checks each connection's token and webhook on a schedule:
| State | What it means |
| - | - |
| **Healthy** | The token is valid and unexpired, and its webhook is active. |
| **Degraded** | The token expires within 30 days, has expired, was rejected, or its webhook was disabled. An expired token reads "GitLab no longer accepts this token" until you replace it. |
| **Disconnected** | The connection was removed. |
GitLab disables a webhook temporarily after 4 consecutive failed deliveries, and re-enables it once deliveries succeed again. After 40 consecutive failures, GitLab disables it permanently. Earnie's maintenance pass detects this and tries to re-enable the hook with a test delivery. If that fails, or the hook is gone, the connection degrades with the reason **webhook disabled**, shown on the GitLab card.
## What's next
Once a repository is connected and imported, Earnie starts [your first scan](/en/latest/earnie/getting-started/first-scan) on its own. The next page follows that scan through each stage and explains what it found.
# Your first scan
Source: https://docs.scanoss.com/en/latest/earnie/getting-started/first-scan
How a scan runs, the ways you can start one, what each stage means, and how scan coverage affects what the Dashboard and Review Workspace show you.
A **scan** is one analysis of your code at a specific point in time. It identifies the open-source components and copied code in your project, with their licences and known vulnerabilities. If your organisation has enabled them, it also finds the cryptography your code uses and the provenance of its AI models. The results become the findings you review, and Earnie checks them against your policies.
If you've completed [workspace setup](/en/latest/earnie/getting-started/workspace-setup), your first scan is already running. This page explains:
* what a scan does, and the ways to start one
* what each stage on the progress page means
* what to do when a run doesn't finish
* where Earnie keeps past runs
* how scan coverage affects what the Dashboard and Review Workspace show you
## How a scan runs
Every source-code scan follows the same sequence:
1. **Read.** Earnie collects the files to scan from your repository, folder, or archive.
2. **Fingerprint.** Earnie reduces each file to a fingerprint, a compact signature that identifies its content. Earnie leaves out licence headers, documentation comments, and leading imports, so two files that share only a licence block don't look alike.
3. **Match.** Earnie compares the fingerprints against the SCANOSS knowledgebase, a large index of known open-source code.
4. **Enrich.** Earnie adds data to each match, such as licence and vulnerability data.
You can watch each stage on the scan's progress page as it runs.
## Starting a scan
A scan starts in one of three ways:
| How | When it happens |
| - | - |
| **Automatically, on connection** | When you first connect and import a repository. |
| **Automatically, on merge** | When a change is merged to the repository's default branch, usually `main`. This is on by default for an imported repository. To turn it off, use **Automatic scanning** in the project's Scan Configuration. |
| **On demand** | When you select **New scan**. Choose **Repository** to scan a connected GitHub or GitLab repository, **Upload** to scan a folder or a `.zip`/`.tar.gz` archive, or **Import SBOM** to import an existing SBOM. |
Earnie also scans pull requests on its own. See [Pull requests](#pull-requests-and-the-same-commit-twice) below.
### Importing an SBOM
An **SBOM** (software bill of materials) is a document that lists the components a piece of software contains. If you already have one, you can import it instead of scanning source code. Earnie accepts CycloneDX and SPDX documents, in JSON format only.
An imported SBOM lists declared components only. There's no code to match, so
the run always counts as partial coverage. See [Coverage: full vs
partial](#coverage-full-vs-partial).
When the SBOM declares a licence for a component, Earnie keeps it and labels it **From SBOM**, so you can tell it apart from a licence SCANOSS detected itself. If both exist for the same component, the licence SCANOSS detected always takes precedence.
### Pull requests, and the same commit twice
Earnie scans a pull request on its own commits, so you see its findings before the change is merged, not only after it lands on the default branch.
* **Opening a pull request** starts a scan.
* **Pushing new commits** to an open pull request starts another scan.
* **If an earlier scan on the same pull request is still running** when new commits arrive, the new scan replaces it. The new scan doesn't queue behind the earlier one or run alongside it.
* **Once the pull request merges**, the scan of the merged commit sets the default branch's state. The last preview scan of the pull request doesn't.
* **If the pull request is merged or closed while its scan is still running**, Earnie cancels that scan. The Earnie check finishes as cancelled, titled **Pull request closed before the scan finished**. Earnie posts no summary comment, and Scan history shows the run as **Cancelled**. A scan that had already finished keeps its result, and Earnie never cancels default-branch scans this way.
A pull request scan stands alone. It never updates the project's current findings, cryptography ledger, or posture. Those change only once the code reaches the default branch. If a pull request stays open without merging or closing, Earnie clears its scan after 30 days.
On a large project with a small change, Earnie reuses earlier results where it can do so safely. For a file the change didn't touch, Earnie takes the open-source matches from the branch's baseline scan instead of matching the file again. Earnie also skips the cryptography scanner entirely when the change touches no dependency manifest and no file that could contain cryptography. Either kind of reuse falls back to a full scan when Earnie can't be confident, for example after a large change, a change to scan configuration, or an update to the SCANOSS knowledgebase.
You can still resolve or reopen a finding a pull request introduces before the pull request merges. See [Deciding a pull request's own findings](/en/latest/earnie/using-earnie/triaging-findings#deciding-a-pull-requests-own-findings).
**The same commit, submitted twice.** A commit can reach Earnie in two ways.
The GitHub App's or GitLab's webhook can push it. A webhook is an automatic notification your
provider sends when code changes. Or a pipeline running
the [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli) can submit it directly. When both paths
submit the same commit for the same scan, Earnie records one scan's worth
of findings, never two, and records which path the commit came from.
### Choosing scanners
A **scanner** analyses your source code. Which scanners you see depends on what your organisation has enabled. Source scans have a **Scanners** section for choosing between them.
* Where enabled, **Cryptography** and **AI** are each selected by default.
* You can change the selection for each new scan, but at least one scanner must stay selected. From the [CLI](/en/latest/earnie/using-earnie/earnie-cli#choosing-scanners), pass `--scanners` to choose explicitly.
* SBOM imports don't show scanner choices, because they process the supplied document rather than source code.
The scan summary lists scanners (OSS, Cryptography, and AI) separately from intelligence layers. Intelligence layers are optional additions to the results, such as vulnerability and licence data. When a run completes, its **Configuration** card names the scanners whose stages ran.
Earnie offers intelligence layers while OSS matching is selected. If your operator has enabled the **Enrichment** option for your organisation, Earnie offers them with any scanner. Enrichment adds licence and vulnerability details to the components that Cryptography and AI provenance find, such as the library a cryptographic call comes from. Policies and filters can then use licence and vulnerability fields even without OSS.
## How your source reaches Earnie
Earnie doesn't scan every file in your project. A set of **keep rules** decides which files to scan, and Earnie sends or stores only the files those rules retain. Those files are the **keep-set**. You configure keep rules at **Project** or **Settings → Scan Configuration** (see [Scan Configuration](/en/latest/earnie/using-earnie/scan-configuration)). The same keep rules apply whichever way your source arrives:
| Source | What happens |
| - | - |
| **Folder upload** | Your browser doesn't zip the folder. Earnie first lists every file it can reach, applies the keep rules, and sends only the retained files. |
| **Git scan** | Earnie clones the repository on the server and applies the keep rules. |
| **Archive** | Earnie unpacks the archive through the keep rules. The stored scan is the kept files, not the original archive. |
Earnie normally leaves out generated source, such as the contents of `node_modules`. It may still keep a dependency manifest from inside one of those directories, because later scan stages need it. A dependency manifest is a file such as `package.json` that lists a project's dependencies. As a result, the file viewer can show that directory with only the retained manifest in it, and none of the skipped source beside it.
### Large folders and archives
For a folder, Earnie works out which files to keep in a separate **Selecting files** stage on the progress page. **Start scan** therefore responds immediately, however large the folder. If the resulting set of files is too large to scan, the run fails with `keep_set_too_large` on the progress page, rather than the first click failing. If a transfer error keeps repeating, an **Upload stalled** banner appears with **Retry upload**. Retrying resumes the transfer from where it stopped, as long as you stay in the same browser tab. Reloading the page doesn't resume a partial upload.
For an archive, the browser sends the upload in pieces rather than as one long transfer, so the progress bar advances as Earnie accepts each piece. A failed piece cancels the scan instead of offering a retry that resumes, and the New scan page shows a network-error message. Start the upload again from there.
A large keep-set can take a while to transfer. That's expected. You can select
**Cancel scan** on the progress page at any in-progress stage, which stops the
transfer and every job for that run. Viewers don't see the button. If no files
arrive for an hour during a folder or archive upload, Earnie fails the scan.
The **Import SBOM** drop area names the document limit. Earnie refuses a file over either limit before sending any of it, with a message that names the file's size and the limit.
## The stages of a run
The progress page lists each stage of the run. The percentage in its header stays under 100 while a later stage is still running, even when the file count already reads complete. Each stage shows one of these statuses:
| Status | What it means |
| - | - |
| **Pending** | Queued and waiting for its turn. It hasn't stalled. |
| **Running** | In progress. |
| **Done** | Finished successfully. |
| **Skipped** | Not run for this scan. |
| **Failed** | Stopped with an error. The stage shows the cause, so you know which part to re-run. |
### A standard scan
A standard source-code scan runs through these stages:
1. **Receiving files**, the keep-set transfer, for folder, Git, and archive scans.
2. **Fingerprinting** your files.
3. **Matching** against the SCANOSS index.
4. **Checking component status** and **checking component versions**. These start as soon as matching finishes and run alongside any intelligence layers you enabled, so they can finish first. They run even if an intelligence layer fails.
5. **Finalising** the run.
Folder and Git scans don't extract a full copy of the original tree, so those runs have no **Extracting archive** stage. Earnie still unpacks an uploaded archive through the keep rules before fingerprinting.
### Optional scanners and intelligence layers
Optional scanners you selected start as soon as your source is ready and run alongside matching rather than after it. With Cryptography on, for example, a **Detecting cryptography** stage runs at the same time as matching, so a long cryptography scan doesn't add its time to the run.
The intelligence layers run after the scanners, with one stage for each layer you enabled.
An optional intelligence layer can fail without failing the whole scan. The
live progress view and the completed run both show a warning beneath that
layer, for example **Vulnerability enrichment did not complete**. Earnie keeps
the technical cause in the server log and doesn't show it on the page. Your core
scan results remain available. Re-run the scan after the service recovers to
fill in that layer's data.
### When one stage fails
Some stages run at the same time, so a failure in one stops the others:
* The stage that failed reports its own cause.
* Any stage that was still running is marked **Failed** with the warning `cancelled: sibling phase failed`. Earnie stopped it because the run was already over, not because of anything it found.
Fix the underlying cause, then submit the scan again. A failed run doesn't change any of your findings.
### Cryptography steps
The cryptography scan reports its own steps underneath its stage, as it reaches them:
* **Cryptography: scanning source**
* **Cryptography: scanning dependencies**
* **Cryptography: building call graph**
A large repository can spend a long time in one of these steps, and the steps show whether the scan is still working or has stalled. A step appears only once the scan reaches it. A skipped step gives the reason, for example that the project has no dependency manifest the scan can read.
### SBOM imports
An imported SBOM runs fewer stages. There's no archive to extract and no code to fingerprint, so it shows **Importing SBOM**, then finalising, then the intelligence layers.
If the document contains anything Earnie can't read, such as a component with
neither a name nor a package identifier, the Importing SBOM stage shows a
warning that names it. Earnie doesn't drop it without telling you.
### When a run doesn't finish cleanly
A run that ends without results opens on its own page. The page explains what happened instead of showing only "Scan failed". There are two outcomes:
| Label | What it means |
| - | - |
| **Cancelled** | You stopped the run. Earnie kept nothing and nothing in the project changed. |
| **Failed** | Something went wrong. The page describes the cause in plain language and what to do about it. For example, a folder transfer that timed out asks you to keep the tab open until receiving finishes. |
The **Run details** section shows:
* the stage the run stopped in, and how far it got
* the source
* when it started and ended
* the reason code
* the scan ID, which you quote when you ask an administrator to check the logs
## Scan history
Scan history lists every run for the project. Earnie keeps every run, and you can filter the list by status and search it by source.
### The audit trail of a run
Every completed or failed run ends with an **Audit trail** card. It lists the run's own history, oldest entry first. Earnie reads it from the [organisation audit log](/en/latest/earnie/evidence/audit-log) rather than keeping a separate record.
The trail answers the question a reviewer asks months later: which code, which policy, which actor, and which outcome produced this gate? The **gate** is Earnie's overall decision for a run, such as pass, warn, or block, based on your policies. The trail records, in order:
1. **The submission.** Who asked for the run, which can be a person, an API key named for its pipeline, or the GitHub App. It also records the commit and branch, what was requested and what ran, and the policy revisions in force at that moment.
2. **The gate.** The gate the run was given, and the policy verdicts behind it. If a Policy Approval later changes that gate without a re-scan, the approval appears as its own entry.
3. **Publications to the provider.** For example, the check run posted on the pull request, including any retry someone requested and any publication Earnie had to give up on.
Earnie records actors as they were at the time and never looks them up again. An API key that's renamed later still appears under the name it had when it ran.
Automation can read the same trail with [`earnie scan audit `](/en/latest/earnie/using-earnie/earnie-cli#non-blocking-and-follow-up-commands). [Self-checks](/en/latest/earnie/using-earnie/self-checks) are not scans. They keep their own trail on the Self-check page.
## Coverage: full vs partial
Every run has a coverage level:
* **Full coverage** means the run scanned the whole codebase.
* **Partial coverage** means the run saw only part of it, for example an imported SBOM or a [Self-check](/en/latest/earnie/using-earnie/self-checks).
In Scan history, a run submitted for a pull request is labelled **PR scan** instead of **Partial**. It compares the pull request against the project and doesn't resolve findings. Hover over or focus a run's badge to see why it's partial. A file count such as `74 of 80 files` appears only when a run read fewer files than it was given.
Open a run to see its totals. On a full scan, the **Findings** tile counts the project's posture after that run. A PR scan holds the whole codebase at the pull request's head, default-branch findings included. Its tile therefore leads with the findings the pull request introduced, labelled **new in this pull request** (or **nothing new in this pull request**). The line under it, **N across the codebase with this change**, is the total the project would carry once the change merges. An MR scan says merge request instead. The tile changes only what the run page counts. The gate and the pull request check decide as before.
A partial run only adds and updates what it saw. It never marks anything as absent, because it can't tell you what isn't there. For this reason an imported SBOM can never remove a finding that an earlier full scan established.
The Dashboard and Review Workspace both use your most recent full-coverage run. If a project has only partial runs, they fall back to the latest of those and say so, so an imported SBOM still gives you a posture to work with. Treat that posture as a minimum, not a complete picture. It lists what the document declared, and nothing about code the document never mentioned.
**You can re-scan as often as you like.** Decisions you've already recorded survive
a re-scan. A new run tells you what was added, what's still present, and what
has gone away. It never asks you to triage the same component twice.
The exception is a file that now matches something different. For example, when Earnie
started leaving licence headers out of fingerprints, some files
matched a different component or version, or stopped matching. Such a file
appears as a new finding to review. Earnie hides its earlier decision but doesn't
delete it.
### Crypto changes between scans
When the current run measured cryptography in full, Earnie compares it with the nearest earlier completed run that also has a full cryptography measurement.
* Earnie skips a nearer run that skipped cryptography or covered only part of the project, so that run doesn't block comparison with an older valid baseline.
* If no earlier eligible run exists, Earnie leaves the comparison out rather than treating the missing evidence as zero.
## What's next
With a full-coverage scan complete, read the [Dashboard](/en/latest/earnie/using-earnie/dashboard) next. It shows whether you can ship, which findings need a person to decide, and which are high risk.
# Setting up your workspace
Source: https://docs.scanoss.com/en/latest/earnie/getting-started/workspace-setup
How to sign in to Earnie with corporate SSO, or with your work email and password where your administrator has enabled it, and how to complete the six-step wizard that takes a new workspace from empty to running its first scan.
A **workspace** is your organisation's space in Earnie. It holds your projects, your members and their roles, and the audit history of every decision made in it.
## Before you begin
To complete the setup wizard you need two things.
* **A way to sign in.** This is access through your organisation's corporate SSO (single sign-on). If your administrator has enabled password sign-in, you can use your work email address instead, with a password you set from a link they send you.
* **Ownership or Maintainer access on the organisation, group, or project that holds the repository you want to scan.** Connecting a repository requires an organisation owner. If you're not one, Earnie gives you a link to send to one.
## Signing in
The login page shows the sign-in methods your workspace uses. Each workspace runs one of three sign-in modes:
| Mode | What the login page shows |
| - | - |
| Corporate SSO only | The **Sign in with corporate SSO** button. This is the standard setup. |
| SSO and password | The SSO button, plus an email and password form. |
| Password only | The email and password form, with no SSO button. |
Password sign-in is off unless your administrator has enabled it for your workspace. If you don't see an email and password form, your workspace signs in through SSO.
### Email and password
You can use this method only if your administrator has enabled password sign-in. Enter your work email address and password, then select **Sign in**.
#### Choosing your password
When an administrator adds you to a workspace that signs in with a password, they send you a link that opens **Set your password**.
The link works once and expires after 72 hours. If it says it has expired or was already used, ask your administrator for a new one. The old link stops working as soon as the administrator creates a new one.
If you forget your password later, ask an Admin for a reset link.
#### Changing your password
If you sign in with a password, open the **User menu** at the top right and select **Change password**. Enter your current password, then your new one twice, and select **Change password**.
You stay signed in on the device you used. Earnie signs you out of every other device, so sign in again there with the new password.
### Corporate SSO
Select **Sign in with corporate SSO**. Earnie redirects you to your organisation's identity provider, the service your company uses to manage logins. After you authenticate there, the identity provider sends you back to Earnie and you're signed in to your workspace.
Both methods sign you in to the same workspace, with the same members, roles, permissions, and audit history.
### After you sign in
Where you land depends on how you arrived:
* On your first sign-in as an Admin, Earnie takes you straight into the [setup wizard](#the-setup-wizard).
* If you open a link to a specific Earnie page while signed out, for example from a pull request comment, Earnie takes you to that page after you sign in, whichever sign-in method you use.
* When you return, Earnie opens the last project you were viewing, or [All Projects](/en/latest/earnie/using-earnie/all-projects) if that's where you were. If your organisation has only one project, you go straight into it.
## The setup wizard
Earnie takes the first person to sign in to a new workspace through a six-step setup wizard. When it ends, Earnie is connected to GitHub or GitLab, your first project exists, and its first scan is running.
The rail on the left lists the steps and marks the ones you've completed. You can leave the wizard and come back later, and Earnie reopens it at the first step you haven't completed. **Skip setup**, at the top right, leaves the wizard at any point. Skipping doesn't dismiss the **Finish setup** card in the sidebar. The card stays until you finish the steps or dismiss it yourself.
| Step | What you do | Required? |
| - | - | - |
| **1. Welcome** | Read what the wizard will set up, then start. | Yes |
| **2. Connect a Provider** | Connect Earnie to your GitHub organisation or GitLab group. | Yes |
| **3. Select Repository** | Choose the code to scan and name your first project. | Yes |
| **4. First Scan** | Nothing. The scan starts on its own. | Automatic |
| **5. Add Members** | Add the people who will review findings. | Optional |
| **6. Starter Policy** | Turn on one or more ready-made rules. | Optional |
### 1. Welcome
The first screen explains what the wizard builds: one governed inventory of all your code. The inventory covers the open-source components in your code, snippets copied from elsewhere, their licences, known vulnerabilities, and provenance, meaning where the code came from.
Select **GitHub** or **GitLab** to begin.
### 2. Connect a provider
Earnie reads your code through a GitHub App. A GitHub organisation installs an app to give a service limited access to its repositories. Earnie's access to your source code is read-only and covers only the repositories you choose. Earnie can also comment on pull requests and post checks, so it reports its results where your team reviews code.
Before you grant anything, Earnie lists each permission it's asking for. To connect:
1. Authorise Earnie once, so it can see which GitHub organisations you belong to.
2. Choose the organisation that will own the connection.
3. Install the app.
After you install the app, the wizard moves to the next step. [Connecting a repository](/en/latest/earnie/getting-started/connecting-a-repository) explains each permission and each step.
Earnie connects to GitLab with an access token instead of an installed app. Earnie can reach whatever the token can reach, and it verifies the token before saving anything.
To connect:
1. Enter your GitLab host. The default is `gitlab.com`. For a self-managed instance, enter your host's URL.
2. Paste a group, project, or personal access token.
3. Select **Connect**.
After Earnie verifies the token, the wizard moves to the next step. [Connecting a repository](/en/latest/earnie/getting-started/connecting-a-repository#choosing-a-gitlab-token) explains the token requirements and what each kind of token can do.
With either provider, the next step is the same repository picker, which lists the repositories the connection can reach.
### 3. Select repository
In this step you create your first **project**, the codebase Earnie governs together with its scans, decisions, and rules.
1. **Pick the repository** this project will govern.
2. **Name the project.** Earnie fills this field with the repository's name when you pick the repository. You can also add a description.
3. **Choose the scanners and intelligence layers** for the first scan:
* **Scanners** analyse the source code itself. Open-source matching always runs. If your workspace has **Cryptography** or **AI** enabled, Earnie selects each of them by default. See [AI provenance](/en/latest/earnie/using-earnie/ai-provenance) for what the AI scanner identifies.
* **Download Git LFS models** appears once **AI provenance** is on. It controls whether Earnie fetches model weights stored in Git LFS so it can identify them. Use the project preference, or download or skip for this scan only. Downloads use the repository owner's Git LFS bandwidth quota.
* **Intelligence layers** add data to what the scanners find, such as known vulnerabilities and licence data.
Turning off a layer makes the first scan finish sooner. You can re-run a scan with more layers at any time.
### 4. First scan
The first scan starts on its own as soon as Earnie imports the repository, so you have nothing to do in this step.
You don't need to wait for the scan to finish. It keeps running in the background while you complete the last two steps.
### 5. Add members (optional)
Add the people who will triage findings, meaning they review each finding and record a decision about it. For each person, enter their email address and choose a role: Admin, Operator, or Viewer. [Team & roles](/en/latest/earnie/administration/team-and-roles) explains what each role can do.
Developers and automated agents don't need seats. They use Earnie through CI, the CLI, and the API instead.
You can skip this step and add members later from **Settings → Members**.
### 6. Starter policy (optional)
A **policy** is a rule that tells Earnie what to do when a scan finds something, for example to block a merge that introduces a banned licence. Earnie provides policy templates for common rules.
Pick one or more templates to begin with. A template is the quickest way to watch the gate work from start to finish. The gate is Earnie's pass or fail check on a change. Each template shows the action it applies, such as warn or block.
Policies you turn on here are enabled immediately. Templates you add later from a project's **Policies** page start as drafts instead.
When you finish the wizard, Earnie opens the results of your first scan. If you skip this step, you can write policies later, as described in [Setting policies](/en/latest/earnie/using-earnie/setting-policies).
## What's next
Your first scan is now running. Next, read [Connecting a repository](/en/latest/earnie/getting-started/connecting-a-repository) to see what access you granted, then [Your first scan](/en/latest/earnie/getting-started/first-scan) to follow the scan through to completion.
# Introduction
Source: https://docs.scanoss.com/en/latest/earnie/introduction
Earnie is SCANOSS's agent-native platform for open-source, cryptographic, and AI-provenance governance. You write a policy once and Earnie applies it to every change, whether a person or an AI coding tool wrote the code.
Earnie scans your code and finds the open-source components, licences, vulnerabilities, cryptography, and AI models in it. It checks each result against rules your team writes and records every decision, so months later you can show why a component was accepted or rejected.
This page covers Earnie's model, the terms the rest of the guide uses, and where to go next. You don't need to set anything up yet.
## One policy, everywhere code changes
A scanner gives you a list of matches and leaves the follow-up to you. Earnie connects each finding to a decision and keeps the evidence for it.
The same policy applies in three places:
| Where | What it covers | What Earnie does |
| - | - | - |
| **Development** | Coding agents over MCP, the Earnie CLI, and Git hooks | Gives the agent or developer the project's policy, and runs [Self-checks](/en/latest/earnie/using-earnie/self-checks) on changed files before anything is committed. Self-checks don't create findings. |
| **Pull requests & CI** | Pull requests and CI pipelines | Scans the change and posts a result on the pull request: pass, warn, require approval, or block. |
| **Governance** | The web app | Keeps the project's posture current from full scans, records the triage history, and exports inventories such as SBOMs. |
A licence rule that blocks a pull request is the same rule a coding agent receives while it writes code, and the same rule an auditor sees in the project's history.
Earnie covers three domains in one list of findings: open source, cryptography, and AI provenance. Which ones you see depends on what your organisation has enabled.
## Key terms
A **project** is scanned. The **scan** produces **findings**. Earnie checks each finding against your **policies**, and the result is a **verdict**.
| Term | Meaning |
| - | - |
| **Project** | The codebase you govern, with its scans, decisions, and rules. |
| **Scan** | One analysis of that code at a point in time. |
| **Finding** | One result someone can review, such as a matched component, a licence, or a vulnerable dependency. |
| **Policy** | A rule that says what happens when a matching finding appears. |
| **Verdict** | Pass, Warn, Require approval (held until an Admin approves), or Block. A project with no policy result yet shows **Not evaluated**, which is not a pass. |
### How a scan becomes a verdict
1. Earnie fingerprints each file. A fingerprint identifies the content without being a copy of it.
2. SCANOSS matches the fingerprints against its knowledge base of open-source code.
3. Earnie adds licence, vulnerability, and provenance data to each match. Each enriched match is a finding.
4. Earnie checks each finding against your policies.
5. The policies produce the verdict.
### Other terms
| Term | Meaning |
| - | - |
| **Review Workspace** | The screen where you review findings and record decisions. It's **Review** in a project's sidebar. See [Triaging findings](/en/latest/earnie/using-earnie/triaging-findings). |
| **Posture** | A project's state as of its latest scan, shown on the Dashboard. See [Reading the dashboard](/en/latest/earnie/using-earnie/dashboard). |
| **Merge gate** | Earnie's decision on whether a change can merge, posted on the pull request. See [Merge gate](/en/latest/earnie/using-earnie/merge-gate). |
| **Policy Approval** | An Admin's recorded decision that a specific finding may pass despite a policy. See [Requesting an approval](/en/latest/earnie/using-earnie/merge-gate#route-3-request-an-approval). |
| **Self-check** | A quick review of changed files, run by the pre-commit hook or a coding agent. It creates no findings. See [Self-checks](/en/latest/earnie/using-earnie/self-checks). |
| **SBOM snapshot** | A frozen record of a project's inventory and decisions that you can export. See [Exporting SBOMs](/en/latest/earnie/evidence/exporting-sboms). |
| **Coverage** | Whether a run saw the whole codebase (Full) or part of it (Partial). See [Coverage](/en/latest/earnie/getting-started/first-scan#coverage-full-vs-partial). |
## Who uses what
* **Security and compliance leads** (AppSec, platform security, OSPO) use the web app to triage findings, write policies, read posture, and manage the team.
* **Developers and coding agents** mostly never open the web app. They get policy context and Self-checks through the CLI, hooks, and MCP, and results on their pull requests.
* **People who need the records**, such as CISOs, legal counsel, and AI risk owners, use what Earnie exports: SBOMs, notice files, and the audit log.
Inside the web app, what someone can do depends on their role: Admin, Operator, or Viewer. See [Team & roles](/en/latest/earnie/administration/team-and-roles).
Earnie doesn't send your source code to SCANOSS. It sends file fingerprints, each with the file's
checksum, size, and path, plus the package identifiers of components it found. See
[Deployment & data flow](/en/latest/earnie/deployment) for exactly what leaves your environment.
## Where to go next
The sidebar follows the same three places. If you're setting Earnie up, start with
[Workspace setup](/en/latest/earnie/getting-started/workspace-setup). If you're a developer, go to
Development and Pull requests & CI. If you're moving off scanoss-py, the SCANOSS GitHub Action, or the
SCANOSS pre-commit hook, read [Migrating from SCANOSS tools](/en/latest/earnie/migrating/from-scanoss-tools).
Connect Claude Code, Cursor, VS Code, or Codex, and use Earnie's tools from the agent.
Scan from the command line, install the pre-commit hook, and run Earnie in CI.
What Earnie posts on a pull request, and what to do when a policy blocks a merge.
Work through the backlog and record a decision on each finding.
Turn a decision you keep making by hand into a rule.
Call Earnie's REST API with an API key.
# Connecting an AI client
Source: https://docs.scanoss.com/en/latest/earnie/mcp/connecting-a-client
How to create an API key for a coding agent, connect Claude Code, Cursor, VS Code, or Codex to Earnie's MCP server, and check that the connection works.
To connect a client, you create an API key for the agent, find your MCP address, add Earnie's MCP server to the client, and check the connection. This page covers each step.
If you haven't read it yet, [Earnie MCP](/en/latest/earnie/mcp/overview) explains what the connection gives you and what you need first.
## Step 1: Create an API key
The client authenticates to Earnie with an API key, sent with every request as an `Authorization: Bearer ` header.
1. In Earnie, go to **Settings → API keys** and select **Generate key**.
2. Name the key after the agent that will use it, for example `claude-code-alex`.
3. Under **What this key will hold**, choose the **Coding agent (MCP)** preset.
4. Leave **Allow agents to triage findings** off unless you want the agent to record triage decisions. See [Letting an agent triage](/en/latest/earnie/administration/api-keys#letting-an-agent-triage).
5. Choose the projects the key can reach, and its expiry.
6. Generate the key, and copy it straight away. Earnie shows it only once.
The Coding agent (MCP) preset grants these permissions. Each one makes certain tools visible to the agent:
| Permission | Tools it unlocks |
| - | - |
| (any valid key) | `platform_health_ping` |
| `projects:read` | `earnie_list_projects` |
| `findings:read` | `earnie_search_findings`, `earnie_get_finding` |
| `scans:read` | `earnie_project_posture`, `earnie_export_bom` |
| `policies:read` | `earnie_policy_context` |
| `scans:write` | `earnie_review_code` |
| `findings:write` | `earnie_triage_finding` (only with **Allow agents to triage findings**) |
If the key lacks a tool's permission, the agent doesn't see that tool at all. [Using Earnie through MCP](/en/latest/earnie/mcp/tools) describes each tool.
## Step 2: Find your MCP address
Earnie's MCP server is part of your Earnie deployment, at your Earnie address followed by `/mcp/v1`. For example, if you open Earnie at `https://your-company.earnie.dev`, the MCP address is:
```text theme={null}
https://your-company.earnie.dev/mcp/v1
```
Each deployment has its own address. The panel that shows your new key also shows this address and uses it in its install snippets.
## Step 3: Add Earnie to your client
You can connect in one of two ways:
| Route | Best for | What you get |
| - | - | - |
| **Earnie CLI (`earnie mcp setup`)** | Working in a repository | The MCP connection, a managed policy block, and review hooks, set up in one command. |
| **Install snippet from Earnie** | Connecting a client quickly, or without the CLI | The MCP connection only. |
### Option A: Set up with the Earnie CLI
We recommend this route for a repository, because it also writes your project's policies into the agent's instructions file and installs review hooks.
1. [Install the Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli#installing).
2. Sign in with the key you created:
```bash theme={null}
earnie auth login --api-url https://your-company.earnie.dev
```
3. From inside your repository, run setup:
```bash theme={null}
earnie mcp setup
```
Setup detects the clients installed on your machine and preselects them. It takes the project from `--project`, `EARNIE_PROJECT`, or the repository's Git remote. If none of those identifies the project, setup asks you to pick one.
It then writes:
* **The connection.** The MCP address and your key go in each client's user-level configuration, under a server named `earnie`.
* **The managed policy block.** Your project's policies, as plain rules, go in `CLAUDE.md`, `.cursor/rules/earnie.mdc`, `.github/copilot-instructions.md`, or `AGENTS.md`.
* **Review hooks**, for Claude Code, Cursor, and Codex.
* **A session-start hook**, for Claude Code, Cursor, and Codex. It refreshes the managed policy block when the project's policies change. See [Keeping the policy block current](/en/latest/earnie/using-earnie/earnie-cli#keeping-the-policy-block-current).
Restart your client after setup so it picks up the new configuration.
For the full list of flags, the exact file each client uses, and how to remove the setup again, see [Connecting a coding agent](/en/latest/earnie/using-earnie/earnie-cli#connecting-a-coding-agent-mcp) in the CLI reference.
**Codex needs one more step.** Codex doesn't run project hooks until you
approve them. Inside Codex, run `/hooks` and trust Earnie's hook
definitions.
### Option B: Use an install snippet from Earnie
When you generate a key with the Coding agent (MCP) preset, the panel that shows the key also shows **Connect your coding agent**, with an install snippet for each client. The snippets use your own MCP address, and Earnie shows them only once, with the key.
Some snippets include the key. Others leave it out, so the key doesn't end up in your browser history. Follow the instructions for your client:
#### Claude Code
Run the command in the repository you want the agent to work in. It includes your key:
```bash theme={null}
claude mcp add --transport http earnie https://your-company.earnie.dev/mcp/v1 --header "Authorization: Bearer "
```
#### Cursor
Open the link from the panel. Cursor offers to install the server, already configured. The link doesn't contain your key, so set it as an environment variable named `EARNIE_API_KEY` before you start Cursor. The installed configuration reads it as `Bearer ${env:EARNIE_API_KEY}`.
#### VS Code
Open the link from the panel. VS Code offers to install the server in your user profile, and asks for your key as a password when you install it. The link itself doesn't contain the key.
#### Codex
Add this block to `~/.codex/config.toml`. It includes your key:
```toml theme={null}
[mcp_servers.earnie]
url = "https://your-company.earnie.dev/mcp/v1"
[mcp_servers.earnie.http_headers]
Authorization = "Bearer "
```
#### Setting a default project
If the key can reach more than one project, the panel offers **Default project for this client**. Choosing one adds an `X-Earnie-Project` header to the snippet, set to that project's ID:
```text theme={null}
X-Earnie-Project:
```
The agent then uses that project whenever it doesn't name one. The header never widens what the key can reach. Without a default, the agent has to name a project in every question, unless the key can reach only one project. See [How the agent chooses a project](/en/latest/earnie/mcp/tools#how-the-agent-chooses-a-project).
### Claude.ai and ChatGPT
Claude.ai and ChatGPT connect from the vendor's own cloud rather than your machine. This has two consequences:
* They can only reach an Earnie environment that the public internet can reach. See [Deployment & data flow](/en/latest/earnie/deployment).
* They expect OAuth, which Earnie doesn't offer yet, so an API key alone isn't enough.
The one exception is a Claude.ai organisation in Anthropic's **Request headers** beta. Such an organisation can add a custom connector with a fixed header. Enter the name `Authorization` and the value `Bearer `, including the space after `Bearer`. You can't edit a Claude connector's authentication afterwards, so to rotate the key you remove the connector and add it again.
For a developer working in a repository, the four clients above are the supported route.
## Step 4: Check the connection
1. **Restart your client**, so it loads the new configuration.
2. **Ask the agent to ping Earnie**, for example:
> Use the Earnie health ping tool to check the connection.
The agent calls `platform_health_ping`, which works with any valid key. A working connection returns the server's `status`, the current `timestamp`, and the server's name and version.
3. **Ask a real question**, for example:
> Which Earnie projects can you see?
The agent calls `earnie_list_projects` and lists the projects your key can reach, with a link to each project's Dashboard.
If you set up with the Earnie CLI, you can also check the installation from the command line, without changing anything:
```bash theme={null}
earnie mcp doctor
```
Doctor reports whether your key is valid, which project it would use, and whether the agent can see each Earnie tool. For a missing tool, it gives the reason. If either check fails, see [Troubleshooting](/en/latest/earnie/mcp/troubleshooting).
## For administrators: MCP behaviour
Every Earnie environment runs the MCP server, and there's no setting to turn it on. These limits apply to every client:
* **Review rate.** One API key can start up to 30 code reviews per minute.
* **Review wait.** `earnie_review_code` waits for a result for as long as the organisation's **Self-check wait budget** allows. You set the budget in **Settings → Scan Configuration**, on the **Self-check** tab. Its default is 45 seconds, and the wait never exceeds 120 seconds.
* **Browser access.** Earnie always accepts requests from MCP clients. It refuses calls from a web page on another site unless that site has been allowed for your environment.
To change the review rate or allow a browser origin, contact your Earnie administrator or SCANOSS support.
## What's next
With the client connected, see [Using Earnie through MCP](/en/latest/earnie/mcp/tools) for what each tool does and how to ask for it.
# Earnie MCP
Source: https://docs.scanoss.com/en/latest/earnie/mcp/overview
What Earnie's MCP server is, how it connects an AI coding agent to your Earnie projects and policies, which clients are supported, and what you need before you set it up.
**MCP** (Model Context Protocol) is an open standard that lets an AI coding agent call tools on an external service. Earnie runs an MCP server, so a coding agent such as Claude Code, Cursor, VS Code, or Codex can ask Earnie questions and check its own work while it writes code.
With Earnie connected, an agent can:
* read the policies in force for your project, and follow them as it works
* check the code it has just written against those policies, before showing it to you
* look up a project's findings, posture, and merge gate
* export a project's bill of materials
* record a triage decision you've asked it to make, if you allow it
The agent gets your policies while it writes the code, before the code is committed and before it reaches a pull request.
## How it fits together
An MCP connection has three parts:
| Part | What it is |
| - | - |
| **Your AI client** | The coding agent you work with, for example Claude Code in your terminal or Copilot in VS Code. |
| **Earnie's MCP server** | Part of your Earnie deployment, at `/mcp/v1`. It offers a set of **tools**, each one a specific question or action. |
| **An API key** | The credential the client sends with every request. The key decides which tools the agent can see and which projects it can reach. |
```mermaid theme={null}
flowchart LR
You(["You"]) --> AC
subgraph AC["Your AI Client"]
direction TB
A1["Claude Code, Cursor, VS Code, or Codex"]
end
AC --> ED
subgraph ED["Your Earnie Deployment"]
direction TB
B1["MCP Server /mcp/v1"] --> B2["Projects, Findings, and Policies"]
end
style You fill:#fff,stroke:#0B1F3A,color:#0B1F3A
style AC fill:#FFF3EA,stroke:#FF6A1A,color:#0B1F3A
style ED fill:#FFF3EA,stroke:#FF6A1A,color:#0B1F3A
style A1 fill:#FF6A1A,stroke:#E85D0F,color:#fff
style B1 fill:#FF6A1A,stroke:#E85D0F,color:#fff
style B2 fill:#FF6A1A,stroke:#E85D0F,color:#fff
linkStyle 2 stroke:#0B1F3A,stroke-width:1px
```
The client sends each tool call to the MCP server over HTTPS, with your API key.
You don't call the tools yourself. You ask the agent something in plain language, such as "is this project blocked?", and the agent decides which Earnie tool to call. [Using Earnie through MCP](/en/latest/earnie/mcp/tools) lists every tool.
### The connection and the repository setup
Connecting a client to the MCP server gives the agent its tools. If you set up with the [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli), the CLI also writes two things into your repository:
* **A managed policy block.** The CLI writes your project's policies as plain rules into the file your agent reads for instructions, such as `CLAUDE.md` or `AGENTS.md`.
* **Review hooks.** For Claude Code, Cursor, and Codex, a hook reviews the agent's changed files at the end of each turn. Each review is a [Self-check](/en/latest/earnie/using-earnie/self-checks).
[Connecting an AI client](/en/latest/earnie/mcp/connecting-a-client) explains both routes.
## Supported clients
| Client | Support |
| - | - |
| **Claude Code** | Supported. Setup with the Earnie CLI also installs review hooks. |
| **Cursor** | Supported. Setup with the Earnie CLI also installs review hooks. |
| **VS Code** | Supported. No review hooks yet, because VS Code's hook API is in Preview. |
| **Codex** | Supported. Setup with the Earnie CLI also installs review hooks, which you must approve inside Codex. |
| **Claude.ai, ChatGPT** | Limited. They connect from the vendor's own cloud, so they need an Earnie deployment that the public internet can reach. They also expect OAuth, which Earnie doesn't offer yet. See [Claude.ai and ChatGPT](/en/latest/earnie/mcp/connecting-a-client#claude-ai-and-chatgpt). |
## Before you start
You need:
* **An Earnie deployment your machine can reach.** Each deployment has its own address, and its MCP server is at that address followed by `/mcp/v1`.
* **A project that has been scanned**, so there's something for the agent to ask about. See [Your first scan](/en/latest/earnie/getting-started/first-scan).
* **Policies attached to that project**, if you want the agent to follow rules and review its code. See [Setting policies](/en/latest/earnie/using-earnie/setting-policies).
* **An API key created with the Coding agent (MCP) preset.** Creating one needs the Admin or Operator role. See [Creating a key](/en/latest/earnie/administration/api-keys#creating-a-key).
* **A supported AI client**, installed on your machine.
* **The Earnie CLI.** It's optional, but we recommend it when you work in a repository. See [Installing](/en/latest/earnie/using-earnie/earnie-cli#installing).
**What the agent can do depends on the key.** The Coding agent (MCP) preset
lets the agent read projects, policies, findings, and scans, and submit
scans. It can't change triage decisions unless you turn on **Allow agents to
triage findings** when you create the key.
## What's next
Start by [connecting your AI client](/en/latest/earnie/mcp/connecting-a-client) to Earnie.
# Using Earnie through MCP
Source: https://docs.scanoss.com/en/latest/earnie/mcp/tools
The Earnie MCP tools a coding agent can call, what each one does, the permission it needs, and example requests you can give your agent.
Earnie's MCP server offers nine **tools**. Each tool is one specific question or action, such as "search this project's findings" or "review this code against policy".
You don't call tools directly. You ask your agent in plain language, and the agent chooses the tool, fills in the arguments, and reports the answer to you. The examples on this page are requests you can give your agent.
## Before you ask
### Which tools the agent can see
The agent sees only the tools your API key's permissions allow. If the key lacks a tool's permission, the tool doesn't appear at all. If the agent tries to call it anyway, Earnie refuses with a message that names the missing permission. See [Step 1: Create an API key](/en/latest/earnie/mcp/connecting-a-client#step-1-create-an-api-key) for which permission unlocks which tool.
### How the agent chooses a project
Most tools act on one project. Earnie decides which one in this order:
1. **The `project` argument**, if the agent passes one. It can be the project's ID or its slug, the short name in its URL.
2. **The `X-Earnie-Project` header**, if your client configuration sets a [default project](/en/latest/earnie/mcp/connecting-a-client#setting-a-default-project).
3. **The key's only project**, if the key can reach exactly one.
If none of these applies, the tool asks for a project and lists up to 20 the key can reach. Earnie refuses a project the key can't reach with the same message as a project that doesn't exist.
Two tools, `earnie_search_findings` and `earnie_project_posture`, also work without a project. Without one, they cover every active project the key can read, so the agent can answer questions about the whole organisation.
### Long lists
Tools that return lists return 50 rows by default. The agent can ask for between 1 and 200, and fetch the next page with the `next_cursor` value from the previous answer.
## The tools
| Tool | What it does | Permission | Changes anything? |
| - | - | - | - |
| `platform_health_ping` | Checks the connection. | Any valid key | No |
| `earnie_list_projects` | Lists the projects the key can reach. | `projects:read` | No |
| `earnie_project_posture` | Reports a project's merge gate and what needs attention. | `scans:read` | No |
| `earnie_search_findings` | Searches a project's findings. | `findings:read` | No |
| `earnie_get_finding` | Reads one finding in full. | `findings:read` | No |
| `earnie_policy_context` | Returns the project's policies as plain rules. | `policies:read` | No |
| `earnie_review_code` | Reviews code against the project's policies. | `scans:write` | Creates a Self-check |
| `earnie_export_bom` | Returns the project's CycloneDX bill of materials. | `scans:read` | No |
| `earnie_triage_finding` | Records a triage decision on a finding. | `findings:write` | **Yes** |
### `platform_health_ping`
Returns the server's status, the current time, and the server's name and version. Use it to check the connection. It works with any valid key.
> Ping Earnie to check the connection is working.
### `earnie_list_projects`
Lists the projects your key can reach, with each one's slug, ID, and Dashboard link. The agent calls this first when it doesn't know which project you mean, or when Earnie rejected a project it named.
> Which Earnie projects can you see?
### `earnie_project_posture`
Reports a project's governance posture. That covers its merge gate, what needs attention, its counts of open high-severity CVEs and weak cryptography, and the scan those figures come from. Without a project, it covers every project the key can read.
> Is the payments-service project currently blocked from merging?
> Which of our projects have a blocking verdict right now?
### `earnie_search_findings`
Searches findings in a project that Earnie has already scanned. Findings include open-source matches, declared dependencies, cryptographic assets, and AI provenance. The agent can narrow the search with these arguments:
| Argument | What it filters on |
| - | - |
| `domain` | `oss`, `crypto`, `ai`, or `all` (the default). |
| `family` | A finer category within a domain, such as `deps` for declared dependencies only. |
| `state` | `open`, `assigned`, `resolved`, `superseded_by_rule`, or `reopened`. |
| `ai_kind` | Within the `ai` domain, `model` for model findings or `usage` for AI-usage findings. |
| `purl` | A component's package URL, with or without a version, for example `pkg:npm/lodash`. |
| `path_prefix` | A file, or a directory to search beneath. |
| `scan` | Only the files one scan covered. Needs `project`. |
| `scan_only` | With `true`, only the findings that scan introduced, such as what a pull request scan added on top of the default branch. Needs `scan`. |
It returns each finding's ID, which `earnie_get_finding` and `earnie_triage_finding` use.
> What open cryptography findings does the atlas project have under `src/auth`?
> Show me every finding for pkg:npm/lodash in this project.
An `open` finding isn't necessarily outstanding. A suppressed finding stays
`open`, so ask about the gate with `earnie_project_posture` if you want to
know what's blocking.
### `earnie_get_finding`
Reads one finding in full. The answer includes its stored evidence, the policy verdicts that judge it, whether a [Policy Approval](/en/latest/earnie/using-earnie/merge-gate#route-3-request-an-approval) currently lets it through, and a link to it in the [Review Workspace](/en/latest/earnie/using-earnie/triaging-findings). The agent usually calls it after a search, when it's about to explain or fix one finding.
By default the answer is concise. With `response_format: detailed`, it includes the full stored evidence, such as cryptographic call chains, up to 256 KiB.
> Explain why that lodash finding is blocking, and what would fix it.
### `earnie_policy_context`
Returns the policies in force for a project as plain rules, one sentence each, for the agent to follow. The agent should call it once at the start of a session, or when the project changes.
* **`format: rules`** (the default) returns the managed policy block, capped at 4 KiB. This is the same block `earnie mcp setup` writes into your repository.
* **`format: json`** returns one entry for each policy.
> Read this project's Earnie policies and follow them while you work.
### `earnie_review_code`
Checks code against the project's policies **before** the agent shows it to you. The agent should call it after it writes or edits code that adds or changes a dependency, copies code from elsewhere, or uses cryptography.
The agent sends either the changed `files` or a unified `diff`, not both. Earnie reviews only what it's sent. Each call creates a [Self-check](/en/latest/earnie/using-earnie/self-checks). A Self-check creates no findings and doesn't change the project's posture.
If the review takes longer than the deployment's wait budget, the tool returns `status: "running"` with a `selfcheck_id`. The agent calls the tool again with only that `selfcheck_id` to read the verdict.
> Review the changes you just made against Earnie's policies before you show me the code.
**Limits.** One review can include up to 200 files, 512 KiB per file, and
1 MiB in total. For anything larger, use `earnie scan diff` from the [Earnie
CLI](/en/latest/earnie/using-earnie/earnie-cli#running-a-scan). By default, a
key can start 30 reviews a minute. Checking on a running review doesn't
count towards that.
### `earnie_export_bom`
Returns a project's CycloneDX 1.7 bill of materials, with software components, cryptographic assets, and AI provenance. The tool reads the project's most recent [SBOM snapshot](/en/latest/earnie/evidence/exporting-sboms), or the snapshot of a scan the agent names.
* **`include`** narrows the export to one or more of `oss`, `crypto`, or `ai`. Without it, the export includes every section your organisation has enabled.
* The answer includes a document up to 64 KiB in full. For a larger one it returns a `document_url`, which the agent fetches with the same API key.
* The tool only reads snapshots that already exist. If the project has never generated one, the tool says so rather than creating one.
> Export the atlas project's CBOM.
### `earnie_triage_finding`
Records a triage decision on one finding. It's available only if the key was created with **Allow agents to triage findings**. The agent should call it only when you've told it what to do with a specific finding, and always pass your reason.
| Action | What it does | Also needs |
| - | - | - |
| `resolve` | Closes the finding with a decision. | `decision` |
| `suppress` | Keeps the finding open, but removes it from the gate. | Nothing else |
| `reopen` | Undoes an earlier decision. | Nothing else |
| `assign` | Assigns the finding to a person. | `assignee` (email address or user ID) |
The `decision` for `resolve` matches the [decisions in the Review Workspace](/en/latest/earnie/using-earnie/triaging-findings#making-a-decision): `confirm_match`, `mark_original`, `replace_component`, `accept_dependency`, `dismiss_dependency`, `confirm_usage`, `false_positive`, `accepted_risk`, or, for an [AI model finding](/en/latest/earnie/using-earnie/triaging-findings#ai-model-findings), `approve_model`, `reject_model`, or `wrong_identification`. An **AI-usage finding** uses its own three instead: `confirm_ai_usage`, `not_ai_usage`, or `approve_ai_usage`. `replace_component` also needs a `replacement_purl`, the versioned package URL of the component that matched. Earnie refuses a decision meant for a different finding kind, such as a model decision on a usage finding, and records nothing. Every action needs a `reason`, up to 2,000 bytes.
> That match in `lib/crypto.js` is our own code. Mark it original, with the reason "written in-house in 2021".
**An agent's decision takes effect straight away.** Earnie marks a resolution from an API
key as pending human review, but the finding still closes and its
effect on the gate applies immediately. Earnie records every decision in the
finding's audit trail and attributes it to the key. See [Letting an agent
triage](/en/latest/earnie/administration/api-keys#letting-an-agent-triage).
## What's next
If a tool doesn't behave as expected, see [Troubleshooting](/en/latest/earnie/mcp/troubleshooting).
# Troubleshooting Earnie MCP
Source: https://docs.scanoss.com/en/latest/earnie/mcp/troubleshooting
How to diagnose and fix common problems connecting an AI client to Earnie's MCP server: connection, authentication, configuration, and tool errors.
Most MCP problems fall into four groups. The client can't reach Earnie, Earnie rejects the key, the client's configuration doesn't match the CLI, or a tool refuses a request. This page covers each group and quotes the messages Earnie returns.
## Start with doctor
If you set up with the Earnie CLI, run doctor first. It checks the installation without changing anything:
```bash theme={null}
earnie mcp doctor
```
Doctor reports:
* whether the CLI is compatible with your Earnie deployment
* whether your key is valid, and whether it came from the stored login or `EARNIE_API_KEY`
* the project it would use
* whether the agent can see each Earnie tool, and the reason for any missing tool
It also reports review hooks that an earlier CLI installed without the [session-start hook](/en/latest/earnie/using-earnie/earnie-cli#keeping-the-policy-block-current). Run `earnie mcp setup --hooks` to add it.
It ends with a suggested fix, such as the `earnie mcp setup` command to run. To check only the connection and key, without a repository, run `earnie mcp doctor --global`. Doctor exits `0` when everything is healthy and `9` when it finds a problem.
## Connection problems
| Symptom | Cause and fix |
| - | - |
| The client can't connect at all. | Check that the MCP address is your Earnie address followed by `/mcp/v1`, and that your machine can reach it. See [Step 2](/en/latest/earnie/mcp/connecting-a-client#step-2-find-your-mcp-address). |
| `405`, "the MCP endpoint accepts POST only" | Something sent a `GET` or `DELETE` request, often a browser or a client configured for another transport. Configure the client for streamable HTTP, not SSE. |
| `406`, "Accept must include application/json" | The client asked for a streamed response only. Earnie's MCP server always answers with JSON and never streams. |
| `403`, `origin_not_allowed` | The request came from a web page on another site. Ask your Earnie administrator or SCANOSS support to allow that origin for your environment. Desktop and terminal clients aren't affected. |
| `413`, `payload_too_large` | The request was over 8 MiB. Review fewer files at once. |
| Claude.ai or ChatGPT can't connect. | They connect from the vendor's cloud, so the public internet must be able to reach your Earnie, and they expect OAuth. See [Claude.ai and ChatGPT](/en/latest/earnie/mcp/connecting-a-client#claude-ai-and-chatgpt). |
## Authentication problems
Earnie rejects a missing or invalid key with HTTP `401`. The response names the reason:
| Code | Message | Fix |
| - | - | - |
| `unauthenticated` | "authentication required" | The client sent no key. Check that the `Authorization: Bearer ` header is configured, including the space after `Bearer`. |
| `invalid_key` | "API key invalid" | The key is wrong or incomplete. Copy it again, or generate a new one. |
| `key_revoked` | "API key has been revoked" | Generate a new key, and update the client. |
| `key_expired` | "API key has expired" | Generate a new key, and update the client. See [Expiry](/en/latest/earnie/administration/api-keys#expiry). |
If a client keeps retrying with a bad key, Earnie starts answering `429`, "too many authentication attempts". Fix the key, then wait for the time given in the `Retry-After` header.
**Using Cursor's install link?** Its configuration reads the key from an
environment variable named `EARNIE_API_KEY`. If that variable isn't set in
the environment Cursor starts from, Cursor sends no key. Set the variable,
then restart Cursor.
## Configuration problems
Doctor reports these messages when the client's configuration doesn't match the CLI:
| Doctor says | Fix |
| - | - |
| "configured MCP URL does not match the resolved API URL" | The client points at a different Earnie deployment. Run `earnie mcp setup` again. |
| "native config credential does not match the current CLI credential" | The client holds an old key. Run `earnie auth login` or `earnie mcp setup`, then restart the client. |
| "native config still references EARNIE\_API\_KEY; Claude Code and Cursor cannot read the CLI credential store" | Run `earnie auth login` or `earnie mcp setup`, then restart the client. |
| "repository MCP config still has an Earnie server; identity belongs in the user-level client" | An older setup left the connection in the repository. Run `earnie mcp setup` to move it to your user-level configuration. |
| "Cursor has Earnie in both repository and global configuration" | Cursor doesn't define which one wins. Run `earnie mcp setup` to remove the repository copy. |
| "managed block revision does not match the server" | The project's policies have changed. Run `earnie mcp setup` to refresh the policy block. |
| "Codex project hooks are installed but not trusted" | Inside Codex, run `/hooks` and approve Earnie's hook definitions. |
**Windows and WSL use different home directories.** The Windows build of
`earnie`, including when run from Git Bash, writes to `%USERPROFILE%`, for
example `%USERPROFILE%\.claude.json`. A WSL build writes to your Linux home
directory, which Claude Code and VS Code on Windows don't read. If setup
can't find a client you expect, check which `earnie` binary ran.
## Tool problems
| Symptom or message | Cause and fix |
| - | - |
| A tool is missing from the agent's list. | The key lacks that tool's permission. Doctor shows it as "hidden because this key is missing" the permission. Generate a key with the permission. See [Step 1](/en/latest/earnie/mcp/connecting-a-client#step-1-create-an-api-key). |
| "this API key lacks the … scope, which … requires" | The agent called a tool the key can't use. Ask an organisation Admin for a key with that permission. |
| "This tool needs a Project. Pass `project` with one of: …" | The agent didn't name a project, and there's no default. Name the project in your request, or [set a default project](/en/latest/earnie/mcp/connecting-a-client#setting-a-default-project). |
| "This API key is not authorized for project … It can act on: …" | The project doesn't exist, or the key can't reach it. Earnie gives the same answer for both. Use a project from the list, or a key that covers it. |
| "This API key can act on no Project" | The key reaches no active project. Generate a key with the projects you need. |
| "Rate limit: this API key may start … self-checks a minute" | The agent started too many code reviews. Wait the time given, then retry. The default limit is 30 a minute. |
| "This change is too large to review in one call" | The change has over 200 files, over 512 KiB in one file, or over 1 MiB in total. Use `earnie scan diff` from the [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli#running-a-scan). |
| "Project … is archived, so no new check can run in it" | Restore the project first. See [Archiving, restoring, and resetting](/en/latest/earnie/using-earnie/all-projects#archiving-restoring-and-resetting). |
| "Earnie cannot review this change for this organization" | No scanner that could review the change is enabled for your organisation. Ask an administrator which scanners are enabled. |
| A review returns `status: "running"` instead of a verdict. | This isn't an error. The review took longer than the wait budget. The agent should call `earnie_review_code` again with only the `selfcheck_id`. |
| "No self-check … in project …" | The Self-check ID is wrong, or Earnie purged the check. Earnie keeps Self-checks for 30 days. |
| "There is no SBOM Snapshot for … yet" | `earnie_export_bom` only reads existing snapshots. [Generate a snapshot](/en/latest/earnie/evidence/exporting-sboms#generating-a-snapshot) first. |
| "limit must be between 1 and 200" | The agent asked for too many rows. Ask for fewer. |
| `earnie_review_code` refuses a session: "This credential cannot start a self-check" | The client signed in as a person instead of using an API key. Configure the client with an API key. |
## Still stuck?
* Check the key's permissions and projects on **Settings → API keys**. See [Managing keys](/en/latest/earnie/administration/api-keys#managing-keys).
* Check the CLI's own reference for [setup, doctor, and uninstall](/en/latest/earnie/using-earnie/earnie-cli#connecting-a-coding-agent-mcp).
## What's next
With your agent connected, Earnie records every review the agent runs as a Self-check. See [Self-checks](/en/latest/earnie/using-earnie/self-checks) to learn how to read one.
# Migrating from SCANOSS tools
Source: https://docs.scanoss.com/en/latest/earnie/migrating/from-scanoss-tools
Move from the SCANOSS pre-commit hook, scanoss-py, Code Compare, the SCANOSS GitHub Action, and Dependency-Track policies to Earnie, in five phases.
This guide moves a team from the standalone SCANOSS toolchain to Earnie. It replaces:
* the `scanoss/pre-commit-hooks` pre-commit hook
* local `scanoss-py` scans and their `.scanoss/results.json` output
* triage in **Code Compare**, written into `scanoss.json`
* the `scanoss/actions-scan` GitHub Action and its three policy checks
* Dependency-Track as the place your policies, violations, and exceptions live
In Earnie, one product covers what that chain did: projects, scans, triage, organisation-wide policies, a merge gate, pull-request feedback, approvals, audit, and SBOM export.
**There's no side-by-side mode.** Earnie doesn't run in parallel with the old chain to compare verdicts. Phases 1 to 3 leave your existing gate in force. Phase 4 switches it over in one pull request per repository.
## What replaces what
| Legacy step | Earnie replacement |
| - | - |
| `scanoss-py scan` in a pre-commit hook, blocking on any undeclared match | The Earnie pre-commit hook, which checks your staged changes against the project's policies and produces a [Self-check](/en/latest/earnie/using-earnie/self-checks). |
| Code Compare triage, written as `bom.include`, `bom.remove`, and `bom.replace` in `scanoss.json` | [Triage](/en/latest/earnie/using-earnie/triaging-findings) in the Review Workspace. Every decision records who made it, when, and why. |
| `scanoss.json` skip patterns and component decisions | [Scan Configuration](/en/latest/earnie/using-earnie/scan-configuration), at organisation and project level. Earnie still reads your committed `scanoss.json`, and never writes to it. |
| `scanoss/actions-scan` running `scanoss-py` on your CI runners | The Earnie GitHub App or GitLab connection scans every push and pull request, inside your Earnie environment. For other pipelines, or if you prefer a CI step, run the [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli#running-in-a-pipeline). |
| Three checks: `Policy Check: Copyleft`, `Policy Check: Undeclared`, `Policy Check: Dependency Track` | One check named **Earnie** per pull request. |
| Commit comments on matched lines, a pull-request comment, and a job summary | One summary comment per pull request, updated in place, plus inline comments on the lines the pull request added. See [Merge gate](/en/latest/earnie/using-earnie/merge-gate). |
| `scanoss-py convert` to CycloneDX, SPDX Lite, or CSV | [SBOM snapshots](/en/latest/earnie/evidence/exporting-sboms) in CycloneDX 1.7 and SPDX 2.3 Lite, plus reports as HTML, CSV, Excel, or PDF, notice files, and more. `earnie export` writes one from a pipeline. |
| `scanoss-py export dt`, uploading the SBOM to Dependency-Track | Earnie keeps each project's inventory, posture, licences, and vulnerabilities itself. There's no upload step. |
| Dependency-Track policies and violation audit | Organisation-wide [Policies](/en/latest/earnie/using-earnie/setting-policies) attached to projects, and **Policy Approvals** as the one way to make an exception. |
Earnie also gives the same policies to coding agents over MCP. The legacy chain has no equivalent. See [Earnie MCP](/en/latest/earnie/mcp/overview).
## The five phases
Each phase ends with one exit criterion. Don't start the next phase until you meet it.
### Phase 1: Connect and scan
Set up your organisation, then connect your Git provider and import the repositories you want to govern. Earnie connects to GitHub, including GitHub Enterprise Server, through the Earnie GitHub App. It connects to GitLab, on gitlab.com or self-managed, with an access token. **Import all repositories** on **Settings → Integrations** imports a whole organisation or group in one action.
See [Workspace setup](/en/latest/earnie/getting-started/workspace-setup) and [Connecting a repository](/en/latest/earnie/getting-started/connecting-a-repository).
**Exit criterion:** every project in scope has one completed **full-coverage** scan of its default branch, and every connection on **Settings → Integrations** reads **Healthy**.
**Expect a backlog on the first full scan.** If you ran the GitHub Action in
delta mode only, nothing has ever assessed your whole repository. The first
full scan reports findings the legacy chain never showed you. That's not a
regression. Plan time to triage it.
### Phase 2: Bring over your identifications
Earnie reads each repository's committed `scanoss.json` on every scan. You don't need to import it.
| `scanoss.json` entry | What Earnie does |
| - | - |
| Skip patterns | Added to the organisation's and project's skip patterns. Earnie doesn't scan a file that any layer skips. |
| `bom.remove` | Settles the matching findings as **Mark original**. The code is your own, and the findings leave the gate. |
| `bom.replace` | Settles the matching findings as **Replace component**, against the package you named. |
| `bom.include` (or `bom.identify`) | Settles the matching findings as **Confirm match**. The component is real and declared. |
| `settings.file_snippet` header skipping and ranking threshold | Applied, unless the project sets its own value under **Snippet tuning** in Scan Configuration. The repository's value takes precedence over the organisation's. Earnie doesn't apply other snippet-matching settings in the file, and Scan Configuration says so. |
| `bom.ignore` and `bom.exclude` | Not applied. Earnie records every match as a finding and lets you decide it, rather than dropping matches before anyone sees them. |
Earnie records each settled finding in its audit trail and names the `scanoss.json` file as the source of the decision. See [Component overrides](/en/latest/earnie/using-earnie/triaging-findings#component-overrides).
**Watch the difference between declaring and accepting.** In Code Compare, *Include* declared a component, and that satisfied the undeclared-component check. In Earnie, **Confirm match** records that the match is real, and the finding **still counts** towards your policies. A copyleft policy still catches a confirmed GPL component. Only **Mark original** removes a finding from the gate, and it means "this is our own code", not "we accept this component". To accept a real component that a policy flags, use a Policy Approval, covered in phase 3.
Triage what remains in the [Review Workspace](/en/latest/earnie/using-earnie/triaging-findings). In Components mode, **Bulk decide** settles every open file of a component at once.
**Exit criterion:** no project shows findings your team considers already decided, and your reviewers agree that what's left open is real work.
### Phase 3: Rebuild your policies
Rebuild your Dependency-Track policies as Earnie policies, using the [equivalence table](#dependency-track-to-earnie-policies) below. There's no automatic Dependency-Track importer, because the fields, operators, and expression languages all differ.
Start from Earnie's built-in templates where you can. They cover the rules most teams gate on, and they're quicker to review than a hand-written rule:
| Template | Action | Replaces in Dependency-Track |
| - | - | - |
| **Block banned licenses** | Block | A `LICENSE` deny list |
| **Enforce license allowlist** | Require approval | A `LICENSE` allow list |
| **Require approval for strong copyleft** | Require approval | The strong half of `LICENSE_GROUP` = Copyleft |
| **Warn on weak copyleft** | Warn | The weak half of `LICENSE_GROUP` = Copyleft, and the Action's copyleft check |
| **Flag unknown licenses** | Require approval | Nothing. A new safety net. |
| **Block high-severity vulnerabilities** | Block | `SEVERITY` |
| **Block known exploited vulnerabilities** | Block | `VULNERABILITY_ID` against a known-exploited list |
| **Warn on high EPSS** | Warn | `EPSS` |
| **Warn on outdated components** | Warn | `AGE` and `VERSION_DISTANCE` |
| **Require approval for deprecated components** | Require approval | Nothing. A new safety net. |
Where your organisation has cryptography or AI provenance enabled, more templates are available. See [Cryptography](/en/latest/earnie/domains/cryptography#policies-for-cryptography) and [AI provenance](/en/latest/earnie/using-earnie/ai-provenance).
**One copyleft policy becomes two.** Dependency-Track ships one Copyleft
licence group. Earnie separates strong copyleft from weak copyleft, and the
two templates act differently. Strong copyleft gets Require approval, and weak copyleft gets Warn.
If you enable only the first, Earnie stops gating every weak-copyleft licence
you used to gate on, without any warning. Enable both, or write one policy on **Copyleft**
if you want them treated alike.
A policy belongs to your organisation, and you **attach** it to the projects it should gate. A per-repository rule becomes a per-project attachment. You can't override a policy for one project. To vary the rule itself, clone it. See [Where a policy applies](/en/latest/earnie/using-earnie/setting-policies#where-a-policy-applies).
**Exit criterion:** for each project, Earnie's gate agrees with the Dependency-Track verdict on the same code, or your policy owner has written down and accepted every difference. To compare, use the read-only policy commands. They change nothing and never re-evaluate:
```bash theme={null}
earnie policy list --project
earnie policy violations --project
```
### Phase 4: Switch the gate
In **one pull request per repository**:
1. Add **Earnie** as a required status check.
2. Remove the three `Policy Check: *` required checks.
3. Delete the GitHub Action workflow and its Dependency-Track upload step.
Do all three in the same pull request. Branch protection names the legacy checks literally. A required check whose workflow you deleted never reports, and every pull request then waits for it forever. If both sets stay required, every pull request is gated twice.
The Earnie check reports the gate like this:
| Gate | Check |
| - | - |
| Pass, Warn, or no policy applied | Success |
| Block | Failure |
| Require approval | Failure, until approved |
| Evaluation failed | Failure |
| Base branch moved, or no baseline | Neutral |
| Scan cancelled | Cancelled |
On a pull request, **the findings the pull request introduced** decide the check. The summary comment lists findings the default branch already carried, but they don't hold the pull request.
**A neutral check means "not comparable", not "pass".** It appears when Earnie has no completed scan of the default branch to compare against (*No baseline to compare against; result is not conclusive*), or when the default branch has moved since the pull request was scanned (*Base branch moved; result is not conclusive*). Push a commit, or re-run the scan, to get a real answer. See [When the branch you're merging into moves](/en/latest/earnie/using-earnie/merge-gate#when-the-branch-youre-merging-into-moves).
**An approval updates the pull request without a push.** When an Admin grants a Policy Approval, Earnie updates that scan's check and comments straight away. Earnie doesn't re-scan anything, so the check turns green only if the approval cleared every violation holding the gate. Revoking an approval works the same way in reverse.
**A skipped pull request gets no check at all.** A project can skip draft
pull requests, bot-authored pull requests, or pull requests outside chosen
base branches or paths. Earnie never scans a skipped pull request, so a
required **Earnie** check never arrives. Either leave those controls off where
**Earnie** is required, or agree that those pull requests merge without an
Earnie gate. See [Choosing which pull requests Earnie
checks](/en/latest/earnie/using-earnie/merge-gate#choosing-which-pull-requests-earnie-checks).
On GitLab, Earnie can enforce a block only where your group's plan allows it. See [Merge blocking depends on your GitLab plan](/en/latest/earnie/getting-started/connecting-a-repository#merge-blocking-depends-on-your-gitlab-plan).
**Exit criterion:** in each repository, one pull request has merged with **Earnie** as the only required policy check, no workflow still runs `scanoss/actions-scan`, and you have checked any pull-request filters against the repositories where **Earnie** is required.
### Phase 5: Move developer tooling
**Install the CLI** wherever developers or pipelines need it:
```bash theme={null}
brew install scanoss/dist/earnie
# or
curl -fsSL https://github.com/scanoss/earnie-cli/releases/latest/download/install.sh | sh
```
Sign in once with `earnie auth login`. See [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli#installing).
**Replace the pre-commit hook.** If your repositories use the pre-commit framework, replace the `scanoss/pre-commit-hooks` entry in `.pre-commit-config.yaml`:
```yaml theme={null}
repos:
- repo: https://github.com/scanoss/earnie-pre-commit
rev: vX.Y.Z # pin to a release tag
hooks:
- id: earnie
```
Then run `pre-commit install --install-hooks` and `pre-commit run earnie` once, so the framework downloads and verifies the pinned CLI before the first commit. Without the framework, run `earnie hook install` in each checkout instead.
Either way, the hook runs `earnie scan staged --format hook`. It checks what you've staged in Git, not your working files, and creates a Self-check. A Self-check never adds findings or changes a project's posture.
| Verdict | Commit |
| - | - |
| Pass, Warn, or no policy applied | Allowed |
| Block | Stopped |
| Require approval | Allowed, with an approval notice. The pull-request check still decides. |
By default, the hook reports a technical failure but lets the commit through. Technical failures include a network or authentication error and hitting the 60-second time limit. An Operator or Admin can make a technical failure stop the commit instead, at **Project → Scan Configuration → Pre-commit technical failures**. See [When the check can't run](/en/latest/earnie/using-earnie/earnie-cli#when-the-check-cant-run).
**Optionally, connect coding agents.** `earnie mcp setup` connects Claude Code, Cursor, VS Code, or Codex to Earnie, so agents follow your policies while they write code. See [Connecting an AI client](/en/latest/earnie/mcp/connecting-a-client).
Finally, uninstall Code Compare.
**Exit criterion:** no repository's developer instructions still mention `scanoss/pre-commit-hooks`, Code Compare, or `scanoss-py`, and a test commit in each repository produces a Self-check in Earnie.
## Dependency-Track to Earnie policies
This table covers the fifteen condition subjects of Dependency-Track's classic policy model. It isn't a complete account of every way a Dependency-Track instance can express a policy. In particular, Dependency-Track 5.0 adds expression-based policies, which no row below covers. Read each one and rewrite it. Earnie policies also use CEL, but the two object models differ, so you can rarely copy an expression across unchanged.
**List the policies in your own Dependency-Track instance first**, and note its version. Use this table to translate what you find, not to decide what exists.
| Dependency-Track subject | Earnie field (CEL path) | Operators | Notes |
| - | - | - | - |
| `SEVERITY` | Vulnerability Severity (`vulnerability.severity`) | equals, not equals, in | Values `critical`, `high`, `medium`, `low`, `none`. |
| `EPSS` | EPSS Score, EPSS Percentile (`vulnerability.epss_score`, `vulnerability.epss_percentile`) | equals, less than, greater than (and or-equal) | |
| `VULNERABILITY_ID` | CVE Id (`vulnerability.cve_id`) | equals, not equals | For "known exploited", use Known Exploited Vulnerability (`vulnerability.kev`) instead of a list of IDs. |
| `LICENSE` | SPDX License Id (`license.spdx_id`) | equals, not equals, in | Allow and deny lists. |
| `LICENSE_GROUP` | License Category (`license.category`), Copyleft (`license.copyleft`) | equals, in / equals | Categories `permissive`, `copyleft-weak`, `copyleft-strong`, `proprietary`, `public-domain`, `unknown`. Copyleft splits into weak and strong, so counts will move. |
| `AGE` | Component Age in Days (`component.age_days`) | equals, less than, greater than (and or-equal) | |
| `VERSION_DISTANCE` | Versions Behind (`component.versions_behind`) | equals, less than, greater than (and or-equal) | A single count of versions, not Dependency-Track's major, minor, and patch distance. Re-express the threshold. |
| `PACKAGE_URL` | Package URL (`component.purl`) | equals, contains | No regular expressions. Rewrite a `MATCHES` condition as exact values or a substring. |
| `COORDINATES` | Package URL, Component Version (`component.purl`, `component.version`) | equals, contains / equals, not equals | Partial. Express what you can as a package URL plus a version. |
| `VERSION` | Component Version (`component.version`) | equals, not equals | Text comparison only. There are no numeric version comparisons. |
| `CWE` | CWE (`vulnerability.cwe`) | None | Not available yet, because Earnie produces no CWE data. The policy editor shows the field as unavailable. Earnie can't evaluate a policy written on it, so the policy fails the gate as **Evaluation failed**. Don't rebuild CWE policies. |
| `CPE` | None | None | No equivalent. |
| `SWID_TAGID` | None | None | No equivalent. |
| `COMPONENT_HASH` | None | None | No equivalent. |
| `IS_INTERNAL` | None | None | No equivalent. Scope the policy by attaching it to the right projects instead. |
Earnie also has fields that Dependency-Track lacks, which you may want to use in the rebuild: Dual Licensed, Patent Clauses, License Data Source, Component Status, Match Percentage, the finding's State and Domain, and the whole cryptography and AI provenance field groups where enabled. `earnie policy show ` prints the exact expression a policy runs.
### Violation states and exceptions
Start from what your pipeline does **today**. The legacy GitHub Action failed on **any** Dependency-Track violation, whatever its state. `INFO`, `WARN`, and `FAIL` all turned the check red, so in that pipeline every violation blocks.
If your CI read Dependency-Track's API or project badge directly instead, it could tell the states apart and gate on `FAIL` alone. Check which setup you're replacing before you map anything.
No rule translates one into the other. Choosing an Earnie action decides how your gate behaves, so make the choice with your policy owner:
| Dependency-Track | Earnie action | Consequence |
| - | - | - |
| `INFO` | Warn | The check stays green. **This loosens your gate**, because these violations fail the build today. |
| `WARN` | Warn, or Block to change nothing | Warn loosens the gate. Block keeps today's behaviour, since the Action already fails on `WARN`. |
| `FAIL` | Block, or Require approval | Block keeps today's behaviour. Require approval adds an approver in front of each violation, which needs someone who'll answer. |
| Violation audit: approved and suppressed | Require approval, plus a **Policy Approval** | Where you suppressed a violation to unblock a pipeline, the gate now holds until an approval exists. |
Whichever you choose, write down the mapping and the reason for it. Your build behaviour changes either way, and you'll need the reason when the first unexpected green check appears.
A Policy Approval records a justification category from CISA's VEX categories, a written justification, the approver, an optional expiry, and any later revocation. Requesting an approval never changes the verdict. Only an Admin's decision does. Each approval covers one finding. See [Request an approval](/en/latest/earnie/using-earnie/merge-gate#route-3-request-an-approval).
Every scan also records the exact revision of each policy that judged it, so you can still see why a check failed months later.
## What changes for your team
* **One check, not three.** Branch protection names **Earnie** instead of the three `Policy Check: *` entries.
* **Decisions carry a name, a time, and a reason.** A `scanoss.json` entry recorded none of those. Earnie records all three for every triage decision and Policy Approval, in the finding's audit trail. See [Audit log](/en/latest/earnie/evidence/audit-log). `earnie scan audit ` prints a scan's own trail: its submission, its gate, approvals that moved it, and its publications to the pull request.
* **You fix findings in Earnie, not in files.** Instead of pasting a `scanoss.json` snippet and pushing, you decide the finding or request an approval in Earnie, and the pull request updates without a push. A fix to the code itself still needs a push before Earnie can scan it.
* **No local results file.** Earnie doesn't write `.scanoss/results.json`. The hook prints a short summary and a link, and the results are in Earnie. Remove `.scanoss/` from the repository and from `.gitignore`.
* **No upload to Dependency-Track.** If other tools also feed Dependency-Track and you want to keep it, upload Earnie's SBOM yourself. Run `earnie export --bom-format cyclonedx --output sbom.json` in a pipeline step, then upload the file with your own step. Dependency-Track's project versions have no equivalent. One Earnie project is one codebase for its whole life.
* **Code Compare is retired as a scanner.** The CLI sends source to your Earnie environment, and scanning and policy evaluation run there. By design, there's no offline mode and no local policy evaluation.
* **Inline comments are limited.** Earnie comments only on lines the pull request added, from Medium severity up, and at most ten per review by default. Findings it can't place on a line go in the summary comment. See [Inline comments on the changed lines](/en/latest/earnie/using-earnie/merge-gate#inline-comments-on-the-changed-lines).
* **Publishing is separate from the verdict.** If GitHub or GitLab rejects Earnie's write, the scan's verdict still stands and the scan page offers **Retry publication**. Retrying never re-runs the scan.
* **Feedback appears on the pull request and in Earnie.** Earnie doesn't send policy-violation notifications by email or chat. If you relied on Dependency-Track's notifications, plan a replacement.
## Before you start
Check these against your setup before phase 1:
* **CWE policies** can't be rebuilt yet. See the [table](#dependency-track-to-earnie-policies).
* **Policy migration is manual.** There's no Dependency-Track policy importer.
* **Approvals are per finding.** A component matched in many files needs an approval for each finding a policy flags.
* **No sub-path or dependency-scope setting.** In a monorepo, narrow a project's scope with skip patterns.
* **`bom.ignore` isn't applied.** Repositories that rely on it will see those matches as findings to decide.
## What's next
Start phase 1 with [Workspace setup](/en/latest/earnie/getting-started/workspace-setup), then [connect your repositories](/en/latest/earnie/getting-started/connecting-a-repository).
# Self-hosting requirements
Source: https://docs.scanoss.com/en/latest/earnie/self-hosting-requirements
The hardware, software, and network access you need to run Earnie on your own servers.
This page applies when you host Earnie yourself, in the self-hosted Earnie or fully on-premises option. If SCANOSS hosts Earnie for you, you don't need any of it. See [Deployment options](/en/latest/earnie/deployment#deployment-options).
Earnie runs on one Linux server with Docker Compose. A PostgreSQL database holds its data. You can run the database as a container on the same server, or use a PostgreSQL server you already have.
In the fully on-premises option you also run the SCANOSS API and knowledge base. They have their own, much larger, [hardware requirements](/en/latest/installation/on-prem/deployment-guide/hardware-requirements). Run them on a separate server.
## Hardware
### CPU and memory
The scanners your organisation has decide how much CPU and memory Earnie needs. Open-source matching is light. Cryptography detection builds your dependencies in a sandbox and needs the most CPU. AI provenance needs the most memory, because it loads model files.
| Scanners | Minimum | Recommended |
| - | - | - |
| Open source | 2 vCPU, 4 GB RAM | 2 vCPU, 8 GB RAM |
| Open source and cryptography | 16 vCPU, 32 GB RAM | 16 vCPU, 32 GB RAM |
| Open source and AI provenance | 4 vCPU, 20 GB RAM | 8 vCPU, 32 GB RAM |
| Open source, cryptography, and AI provenance | 16 vCPU, 48 GB RAM | 16 vCPU, 64 GB RAM |
In the fully on-premises option, AI provenance still sends model fingerprints to `api.scanoss.com`. See [Fully on-premises](/en/latest/earnie/deployment#fully-on-premises).
These figures are for Earnie alone. If you run the database on the same server, add the database's CPU and memory from [Database](#database).
Cryptography scans also run on fewer cores, but more slowly. In SCANOSS's measurements, the first scan of a Java project with 131 dependencies took about 10 minutes on 14 cores, 15 minutes on 8 cores, and 25 minutes on 4 cores. Memory peaked near 7.5 GB at every core count. Later scans of the same project reuse cached dependencies and are faster.
### Disk
Use SSD storage, preferably NVMe.
| What | Size |
| - | - |
| **Data volume** | 50 GB minimum. 200 GB for larger organisations or many repositories. |
| **Container images** | About 6 GB, or about 12 GB with AI provenance. |
| **Operating system** | As your operating system needs. |
The data volume holds the scan archives, scan results, dependency caches for cryptography, and uploaded AI model files. Earnie mounts it at `/var/lib/earnie` by default. Back it up, along with the database. If you run the database as a container, its data is on this volume too.
### Database
Earnie needs PostgreSQL 16. It doesn't need any PostgreSQL extensions, and it doesn't need any other data store, message broker, or object storage.
| Organisation size | Database CPU and memory | Notes |
| - | - | - |
| Small | 1–2 vCPU, 4 GB RAM | A single server is enough |
| Medium and large | 2–4 vCPU, 7–15 GB RAM | Consider a high-availability pair |
Earnie opens up to 25 database connections by default.
## Software
* **Operating system:** a 64-bit Linux distribution on x86-64 (amd64). SCANOSS runs Earnie on Ubuntu 24.04. Earnie's server images aren't available for ARM.
* **Docker:** Docker Engine with the Docker Compose v2 plugin, version 2.20 or later.
* **A DNS name** for Earnie, such as `earnie.example.com`, that your users, CI pipelines, and Git provider can reach.
SCANOSS gives you the Earnie installation bundle for each release. Its installer, `install.sh`, pulls the container images, or loads them from an offline image archive if your server can't reach the container registry.
## Network access
### Inbound
| Port | Purpose |
| - | - |
| 443 (TCP) | The web app, the API, the CLI, MCP clients, and webhooks from GitHub or GitLab. |
| 80 (TCP) | Redirects to HTTPS, and Let's Encrypt certificate checks. |
Earnie gets its TLS certificate from Let's Encrypt by default, which needs the DNS name to be publicly resolvable. You can instead run Earnie behind your own load balancer and terminate TLS there.
### Outbound
| Destination | When it's needed |
| - | - |
| **SCANOSS API** | Always. This is `api.scanoss.com`, or your own SCANOSS API in the fully on-premises option. |
| **`api.scanoss.com`, fully on-premises** | When AI provenance is enabled, to identify AI models by their fingerprints. |
| **Container registry (`ghcr.io`)** | To install and upgrade, unless you use the offline image archive. |
| **Let's Encrypt** | Unless you terminate TLS on your own load balancer. |
| **CISA (`www.cisa.gov`)** | To download the Known Exploited Vulnerabilities feed, once a day by default. |
| **Public package registries** | When cryptography is enabled, to download your dependencies. See below. |
| **Your GitHub or GitLab** | When you connect repositories. GitHub Enterprise Server and self-managed GitLab are supported. |
| **Your identity provider** | When your organisation signs in with single sign-on. |
| **Your metrics system** | Only if you turn on telemetry. |
Cryptography scans build your dependencies in a sandbox. The sandbox can reach only package registries, such as Maven Central, npm, PyPI, crates.io, and the Go module proxy, over ports 80 and 443. It can't reach your network, the Earnie server, or anything else. If you use private registry mirrors, add their addresses to Earnie's sandbox allowlist with the `SCANS_SANDBOX_MIRROR_CIDRS` setting.
Earnie doesn't support fully air-gapped networks yet. Even in the fully on-premises option, it needs to reach the CISA feed, and, for cryptography scans, your dependencies' package registries or your mirrors of them.
## What's next
* [Deployment & data flow](/en/latest/earnie/deployment) explains exactly what Earnie sends to SCANOSS and to other services.
* [Workspace setup](/en/latest/earnie/getting-started/workspace-setup) covers the first steps after Earnie is running.
# AI provenance
Source: https://docs.scanoss.com/en/latest/earnie/using-earnie/ai-provenance
How Earnie identifies AI model files in a repository or uploaded directly, what it records about each one, how AI-usage findings differ, and how to write policies against both.
**AI provenance** is Earnie's third domain, alongside open source and cryptography. It identifies the AI models and AI services your code depends on. It's available where your organisation has **AI** governance enabled, and you select it next to **Cryptography** in scanner selection.
The AI scanner produces two kinds of finding:
* A **model finding** is an AI model file in your repository or uploaded directly, with what's known about its licence, lineage, and datasets. Most of this page is about model findings.
* An **AI-usage finding** is code that calls an AI service or SDK. It isn't a model file. Earnie identifies and triages these the same way as model findings (see [AI model findings](/en/latest/earnie/using-earnie/triaging-findings#ai-model-findings)), and this page doesn't cover them in detail.
This page explains how Earnie identifies a model, the two ways a model reaches Earnie, and how to write policies against what it finds.
## Identifying a model file
When the **AI** scanner runs, Earnie identifies a model file committed to your repository as a model instead of skipping it as a binary. This covers `.safetensors`, `.gguf`, `.bin`, `.pt`/`.pth` (PyTorch), `.onnx`, `.h5`/`.keras`, and similar weights formats. Earnie reports:
* A **purl** that identifies the model.
* **How it matched**, and a **confidence band** shown as text, never as a raw score.
* The **nearest known relatives** that the matcher couldn't fully separate it from, when there are any.
* What the SCANOSS model catalogue records about it: the **licence**, the **lineage** (the base models it was derived from), the **datasets**, and the **model card**.
A model that matches nothing, or that Earnie can't match, still produces a finding. Its status explains why:
| Status | Meaning |
| - | - |
| **Identified** | Matched against the SCANOSS model catalogue. |
| **Unidentified** | Matching ran but found nothing. |
| **Matching unavailable** | Matching couldn't run this time. The cause is one of these: there's no SCANOSS key, the key was rejected, the service is rate-limited, or the scan ran offline. Earnie falls back to an offline guess where it can, and never fails the scan. |
| **Too large** | The file is over the size Earnie will fingerprint. Uploads have the same limit, described in [Uploading a model](#uploading-a-model). |
| **LFS pointer** | The repository keeps this model in Git LFS, and Earnie has only the pointer. See [Git LFS models](#git-lfs-models). |
Earnie counts a model it couldn't match as scanned, not skipped. Coverage stays full, and the
file still appears as a finding even though no match was found.
## Uploading a model
Some models your organisation uses don't live in a scanned repository. You can upload one directly from a project's **Models** page:
* Upload one file, or a Hugging Face snapshot as a tar archive. You don't need to download and re-upload each file of a model by hand.
* The upload can be as large as your organisation's configured cap, which is **3 GB by default**.
* If your connection drops or you reload the page, select the same file again. Earnie **resumes** the upload from where it stopped.
Earnie scans an uploaded model the same way it scans a committed one. It runs identification, the licence and lineage lookup, triage, and policy evaluation, and it includes the model in the SBOM. The model appears on the project's Models page with the same finding detail a committed model gets, linked to the scan that identified it.
**Earnie doesn't keep the weights.** It deletes the uploaded file once the scan finishes,
whatever the outcome. It keeps no fingerprint either, so to re-identify the same model against
a newer catalogue you have to upload it again. Earnie refuses a model over the size cap before
the upload starts, with a note that fingerprinting a model that large isn't available yet.
When you delete a model from the Models page, Earnie marks its finding absent and removes the stored bytes. It keeps the triage history, as it does for any resolved finding.
## Git LFS models
A repository that keeps its models in Git LFS commits only pointers. By default an AI scan reports each one as **LFS pointer** and can't fingerprint it.
An Operator or Admin can turn on fetching for a project in **Project → [Scan Configuration](/en/latest/earnie/using-earnie/scan-configuration) → AI provenance**. After that, a repository scan that runs the AI scanner fetches each model a pointer refers to, up to the same size cap as an upload, and identifies it like a committed file.
**Off by default.** Fetching an LFS object uses the repository's own LFS bandwidth quota, so
Earnie fetches only for projects that opt in. If a model fails to fetch, Earnie leaves it as a
pointer and the scan continues.
## Writing policies against AI findings
Once the AI scanner has run, every finding carries an `ai` field group. Its `kind` field tells a model finding (`"model"`) from an **AI-usage finding** (`"usage"`), which is code that calls an AI service or SDK. You triage the two kinds separately too. See [AI model findings](/en/latest/earnie/using-earnie/triaging-findings#ai-model-findings) in Triaging findings.
These are the model fields most policies need:
| Field | CEL path | Meaning |
| - | - | - |
| **Kind** | `finding.ai.kind` | `"model"` for an identified or unmatched model file. AI-usage findings have a different value. |
| **Model status** | `finding.ai.model_status` | `identified`, `unidentified`, `matching_unavailable`, `too_large`, or `lfs_pointer`. |
| **Model licence** | `finding.ai.model_license` | The licence the catalogue records for the model. |
| **Model licence category** | `finding.ai.model_license_category` | How Earnie classifies that licence, for example non-commercial or use-restricted. |
| **Model publisher** | `finding.ai.model_publisher` | The organisation that published the model. |
| **Model format / transform** | `finding.ai.model_format`, `finding.ai.model_transform_kind`, `finding.ai.model_is_adapter` | The file format, for example pickle-based. Also whether the model is fine-tuned, merged, distilled, or an adapter on top of another model. |
| **Match method / confidence band** | `finding.ai.match_method`, `finding.ai.confidence_band` | How Earnie made the match, and how confident it is in it. |
An AI-usage finding has its own fields instead, such as `finding.ai.vendor`, `finding.ai.sdk`, and `finding.ai.provider`. They identify the AI service or library the code calls.
These fields are empty on findings they don't apply to. A rule that reads them evaluates to false on other kinds of finding instead of raising an error.
The Sentence builder offers no negation on AI fields, because "AI SDK is not openai" would also match every open-source finding. **AI Vendor** is the exception. It also offers **is not** and **is not one of**, so you can keep a list of approved vendors. Put a negated vendor condition next to **AI Finding Kind is usage** and **AI Vendor is not empty**, as the **Enforce approved AI vendors** template does. Without them, the rule also matches every finding that names no vendor.
### Starter templates
Earnie has seven built-in AI templates. Six evaluate model findings, and **Enforce approved AI vendors** evaluates AI-usage findings. Add them from **New policy → Pick templates**, the same as any other template:
| Template | Action | Default |
| - | - | - |
| **Require review for unidentified models** | Require | `unidentified`, `matching_unavailable`, `lfs_pointer`, `too_large`, or a filename match |
| **Block non-commercial model licences** | Block | Non-commercial licence terms |
| **Require review for restricted model licences** | Require | Use-restricted or unknown licence terms |
| **Enforce approved AI vendors** | Require | An allowlist you set. It starts empty, so the first evaluation lists every vendor in use |
| **Block unsafe model formats** | Block | Pickle-based formats |
| **Require review for restricted publishers** | Require | A publisher list you set. It starts empty |
| **Warn on derived models** | Warn | Fine-tuned, adapter, merged, or distilled models |
Every AI template needs the AI scanner to have run. If a scan skipped it, Earnie reports the
policy as **not evaluated**, not as a pass.
The **CRA — Annex I Part II(1)** regulatory template set adds the unidentified-model and restricted-licence templates in one action. It works the same way as the [cryptography regulatory template sets](/en/latest/earnie/using-earnie/setting-policies#regulatory-template-sets). See [Setting policies](/en/latest/earnie/using-earnie/setting-policies) for how templates, the rule builder, and template sets work in general.
## In the SBOM
Earnie exports an identified model as a CycloneDX `machine-learning-model` component, with its purl, SHA-256, licence, lineage (as pedigree ancestors), and model card. Cryptographic assets have an include switch, but models don't. A model is in the SBOM by default whenever your organisation has AI governance enabled.
## What's next
You triage, review, and gate an AI finding the same way as an open-source or cryptography finding. See [Triaging findings](/en/latest/earnie/using-earnie/triaging-findings) for what happens after Earnie finds one.
# Across every project
Source: https://docs.scanoss.com/en/latest/earnie/using-earnie/all-projects
The All projects view shows which projects are blocked, which have never been scanned, and which are missing evidence, and lets you act on several projects at once.
A project's [Dashboard](/en/latest/earnie/using-earnie/dashboard) covers one codebase. The **All projects** view answers the same questions for every project in your organisation: which projects are blocked, which have never been scanned, and which are missing evidence. From this view you can also act on several projects in one step.
To open it, select **All projects** at the top of the sidebar, or open the project switcher at the top left and choose **All projects**. The page is titled **Projects**.
You also manage the project list here. Create a project with **New project**, and edit or archive one from its row in the table.
## Getting around: two levels
Earnie has two levels. The project switcher at the top left always shows which one you're on:
| Level | What it covers |
| - | - |
| **All projects** | Every project in the organisation, shown as cards and a ranked table. |
| **A project's name** | One project. Dashboard, Review, Scan history, Self-checks, SBOMs, Policies, and Scan Configuration live here, because each of them is about one codebase. |
Open the switcher to move between the two levels. When you switch from one project to another, you stay on the same page. For example, if you were reading Review, you land on the new project's Review.
The sidebar keeps the same layout everywhere:
* **All projects** sits at the top.
* The current project's pages sit below it.
* **Settings** sits at the bottom.
The entries never change order. Only the highlight moves.
Opening Settings from inside a project keeps that project selected, so the
sidebar is unchanged when you come back. Settings holds organisation
administration (members, roles, API keys, the audit log), and it never
belongs to one project.
## Narrowing the view with Filter projects
Beside the page title is a **Filter projects** button. It narrows what this page counts, meaning the cards, the table, and the bulk actions, without moving you into a project. The switcher at the top left still reads **All projects**, because filtering doesn't change which level you're on.
* **Every project** is the default. The page counts every project you can see.
* To pick **A selection**, tick one or more projects. The menu stays open while you choose. The button then reads `In scope: 4`, or the project's name if you ticked only one. Tick a project again to remove it, or choose **Every project** to start over.
You can't yet save a selection as a named group for your colleagues. A
selection lasts only while you're using it.
## Choosing a view
This page shows little detail about many projects, so it has no tabs. A dropdown switches between views instead. Each view changes the cards and the table's metric columns to answer one question.
Every view uses the same order: **Needs attention** first, then the project table, then that view's other cards. Changing the view changes what's below the table and leaves the row you were reading in place.
| View | Answers | Cards after the table | Table metric columns |
| - | - | - | - |
| **Overview** (default) | What needs a decision, then the standing of every project. | Gate state, Evidence readiness, License exposure, Cryptography | Attention, High CVEs, Crypto, Coverage |
| **Licensing** | Licence obligations and unclassified licences. | License exposure, Evidence readiness | Unclassified, Weak copyleft, Notice file |
| **Vulnerabilities** | Where the exploitable risk is. | Gate state, Evidence readiness | High CVEs, Attention, Coverage |
| **Cryptography** | Reachable weak cryptography across every project. | Cryptography | Crypto, Coverage |
| **Evidence** | What can be handed over today. | Evidence readiness | SBOM, Notice file, Coverage |
Earnie doesn't offer a view that depends on a scanner your organisation doesn't have. Licensing and Vulnerabilities need open-source scanning, and Cryptography needs the cryptography scanner. Earnie leaves those cards out instead of showing them empty. If only one view is available, the dropdown doesn't appear.
There's no organisation-wide trend card. Earnie has no history to recompute for a set of projects you picked a moment ago, so it doesn't draw a trend. A single project follows the same rule with [Baseline established](/en/latest/earnie/using-earnie/dashboard#reading-the-numbers): Earnie never invents history.
If the page can't load project data, it shows **Could not load project
posture** with a **Retry** button. Earnie never shows a failed request as if
you had no projects.
## The cards
The cards are cross-project versions of the cards on a project's Dashboard:
| Card | What it shows |
| - | - |
| **Needs attention** | Conditions across the projects in scope, each with one action. See [Acting on many projects at once](#acting-on-many-projects-at-once). |
| **Gate state** | How many projects have each policy verdict: Block, Warn, Pass, or Not evaluated. |
| **Evidence readiness** | How much of the scope can be handed over, split into **Ready**, **No notice file**, and **No SBOM**. Earnie counts each project once, because the two gaps need different work. |
**Customise** works the same way as on a project's Dashboard, including reordering with the keyboard. See [Making the canvas your own](/en/latest/earnie/using-earnie/dashboard#making-the-canvas-your-own). Earnie saves your layout on this page for you alone.
## The project table
The table has one row per project. Earnie **ranks the rows instead of sorting them alphabetically**, so the first screenful shows the work that needs doing:
1. Projects with blocking verdicts come first.
2. Next come projects with the most conditions that need attention.
3. Ties are ordered by how long ago the project was last scanned.
You can also sort by any column. Select its header, or focus the header and press Enter or Space.
### What each row shows
* **Project** shows the project and its repository, with a **Default** or **Archived** badge where it applies.
* **Owner** shows the project's owner.
* **Gate** shows the policy verdict: **Block**, **Warn**, **Pass**, or **Not evaluated**.
* The view's metric columns depend on the view. See [Choosing a view](#choosing-a-view).
* **Last scan** shows how long ago the project was last scanned. This is the latest scan that updates the project's posture, so a pull request scan or a Self-check never sets this date. See [Scan history](/en/latest/earnie/getting-started/first-scan#scan-history) for every run, including those.
### Searching and filtering the table
Above the table are:
* a search field, which matches projects, repositories, and owners
* **Any gate**, **Any coverage**, and **Any crypto** filters
* a **Show archived** switch, which adds archived projects back to the list
If the filters leave no rows, the empty message appears inside the table, under its headers. The columns stay in place, so you can see which filter to widen.
### What counts towards "Attention"
The Attention count includes the conditions that **Needs attention** lists, and only those:
* a blocked verdict
* no policy ever evaluated
* a scan that never ran, or one that's gone stale
* components with no licence assertion
* missing evidence, meaning no current notice file or SBOM
Three conditions appear in their own cards but don't raise the count: a policy that only warns, a scan that covered only part of the codebase, and weak cryptography. None of them has a project-wide action to take.
The reading rules from a project's Dashboard apply here, column by column.
None of these values is a zero, and you can't add any of them up with
projects that do have numbers:
* `—` means that measurement doesn't exist for that project.
* **Not scanned** means that scanner never ran there. A project showing Not
scanned under Crypto hasn't been shown to be free of weak cryptography.
Nobody has looked.
* **Never scanned** in the Coverage column means the project has no completed
scan at all.
## Archiving, restoring, and resetting
People often confuse these actions, and you undo them in different ways. Archiving and restoring undo each other. Nothing undoes a reset.
| Action | Where | What it does | Reversible? |
| - | - | - | - |
| **Archive** | The row's actions menu | Stops new work on the project. Deletes nothing. | Yes, with Restore. |
| **Restore** | The archived row's actions menu, or the project's own page | Makes an archived project active again. | Yes, by archiving. |
| **Reset all scans** | The **…** menu on the project's Dashboard | Deletes the project's scans, findings, and decisions, and keeps only the project itself. | **No.** |
| **Disconnect repository** | Project → Scan Configuration | Unbinds the repository and deletes nothing. Earnie keeps scans, findings, and history, and stops future automatic scans. The GitHub App connection itself is unaffected. | Yes, by connecting a repository again. |
### Archive
Archiving keeps everything the project has and stops new work on it. Earnie runs no scans, triage, or policy evaluation on an archived project. Before you confirm, the dialog lists what changes: Earnie adds no new findings, API keys bound to the project stop working, and Earnie keeps existing findings and audit history.
Archived projects leave the project switcher and the active list. **Show archived** brings their rows back. You can't archive the default project, and the menu says why instead of only disabling the action.
After you archive a project, a message with **Undo** appears for a few seconds.
### Restore
Restore makes an archived project active again. It's on the archived row's actions menu and on the archived project's own page, so you can still use it after the **Undo** message has gone.
Restoring doesn't reconnect a repository that archiving disconnected. You need to connect it again yourself.
### Opening an archived project
If you open any page of an archived project, such as its Dashboard, Review, Scan history, SBOMs, or Policies, Earnie takes you to that project's own page instead. The page shows the project's name, an **Archived** badge, its details as read-only, and two buttons: **Restore project** and **Back to Projects**.
A bookmarked link to an archived project always opens that page, and Earnie never reports the project as unknown. A link to a project that doesn't exist still reports it as unknown.
### Reset all scans
Reset is a separate action. It's in the **…** menu on the project's own Dashboard, not on this page.
Reset deletes all of the project's scans and their results, including source evidence, findings, every triage decision, and every policy approval. It keeps the project itself and the audit log. To confirm, you type the project's name. The dialog also offers to disconnect the project's repository, and that option is unticked by default.
**Resetting is irreversible.** Archiving deletes nothing, so archive a
project if you only want to stop work on it. To stop scanning a repository
without deleting anything, use **Disconnect repository** in Scan
Configuration instead. When you reconnect a repository, it keeps the
automatic-scan setting it had before you disconnected it. Reconnecting
doesn't turn automatic scans back on.
## Acting on many projects at once
You can't tick rows in the table to act on them. **Filter projects** already selects projects, so the table has no second selection control. The actions in **Needs attention** apply to the projects the page is showing: every project, or the smaller set you picked with Filter projects.
Needs attention can show seven rows. Five of them run their action as a batch across every project the row counts:
| Row | Action |
| - | - |
| Projects never evaluated against a policy | **Apply a policy** |
| Projects never scanned | **Request scans** |
| Projects not scanned recently | **Request scans** |
| Projects with no current notice file | **Generate notice files** |
| Projects with no current SBOM snapshot | **Generate SBOMs** |
Each batch reports the outcome for every project, never a single pass or fail for the whole batch. If three succeed and two fail, you see that, with each failure named.
The buttons depend on your role. **Request scans**, **Generate notice files**, and **Generate SBOMs** need permission to run scans, and **Apply a policy** needs permission to change policies. A Viewer sees each row and its count, but not the button.
Each SBOM these actions generate holds the same sections as one generated from the project itself: open-source components, plus identified models when your organisation has AI provenance. An API key scoped to AI provenance alone gets SBOMs that hold only models, so **Generate notice files** reports those projects as having no components to attribute.
The other two rows don't run in bulk, because someone still has to decide them one project at a time:
| Row | Action | What happens |
| - | - | - |
| Projects blocked by policy | **Review blocks** | With one matching project, opens it. With several, narrows the page filter to them. |
| Projects with components that carry no licence assertion | **Assign licenses** | With one matching project, opens it. With several, narrows the page filter to them. |
If some projects aren't eligible for an action, the dialog says so before
anything runs, for example `3 of 5 eligible`, and reports the rest as skipped.
A scan request, for instance, needs a connected repository. The dialog never
leaves projects out without saying so, and never reports the whole batch as
done when it wasn't.
**Request scans** and **Apply a policy** finish while you wait. **Generate
notice files** and **Generate SBOMs** run in the background, and the dialog
updates as each project finishes.
## What's next
To look at one project in detail, open its [Dashboard](/en/latest/earnie/using-earnie/dashboard). To work through its findings one by one, go to [Triaging findings](/en/latest/earnie/using-earnie/triaging-findings).
Once your projects and policies are set up, read about the developer's side of Earnie: what happens on a pull request [when the gate blocks a merge](/en/latest/earnie/using-earnie/merge-gate).
# Reading the dashboard
Source: https://docs.scanoss.com/en/latest/earnie/using-earnie/dashboard
The Dashboard's cards: tabs, customising your own layout, what each card shows, and how to read the numbers.
Each project has a Dashboard. Check it first after a scan. It answers three questions:
* Can we ship?
* What needs a person to act?
* Which findings are dangerous?
The Dashboard is made up of **cards**. Each card is a panel that reports on one part of the project. You can rearrange, add, and remove cards.
To open it, select a project, then **Dashboard** in the sidebar.
## What the Dashboard is for
The Dashboard reports the project's posture, meaning its state as of the latest scan, and links to the next action.
Every action on the Dashboard applies to the project as a whole. Examples are requesting an exception to a policy (a [Policy Approval](/en/latest/earnie/using-earnie/merge-gate#route-3-request-an-approval)), generating an SBOM, running a scan, or setting up a policy. You don't decide individual findings here. You assign, resolve, defer, and reopen findings in the [Review Workspace](/en/latest/earnie/using-earnie/triaging-findings) (**Review** in the sidebar), and every card that points at findings links there.
### The header
Above the cards, the header shows:
* a coverage badge, **Full coverage** or **Partial coverage** (see [Coverage: Full vs Partial](/en/latest/earnie/getting-started/first-scan#coverage-full-vs-partial))
* how the code arrived, for example **Repository scan**
* when the project was last scanned, for example **Scanned 23 Sept 2026**. This is the latest scan that updates posture, so a pull request scan or a Self-check never sets this date.
On the right are **New scan** and **Download**.
## Tabs, and why you may not have any
Earnie builds the tabs from the scanners your organisation has.
* If Components and Cryptography are both enabled, you get an **Overview** tab, a **Components** tab, and a **Cryptography** tab. Overview lists what needs doing. The other two tabs each cover one subject in depth.
* If only one of them is enabled, there's nothing to switch between, so the Dashboard shows no tab strip. You get a single merged set of cards instead.
| Tab | What it holds |
| - | - |
| Overview | Needs attention, Merge gate, Evidence, Provenance, the Components and Cryptography summaries, Posture trend, and Decision throughput. |
| Components | Vulnerability severity, License exposure, Component matrix, Origin split, Provenance. |
| Cryptography | Crypto reachability, Asset composition, Post-quantum exposure, Export readiness, Algorithm families. |
| AI provenance | AI usage and Models. Present only when your organisation has AI provenance. See [The AI provenance tab](#the-ai-provenance-tab). |
## The control row
To the right of the tabs is a row of controls: a history period, a card-set dropdown on some tabs, and **Customise**.
### History period
The period dropdown offers **Last 30 days**, **Last 90 days** (the default), or **All scans**. It narrows **history only**. It changes the **Posture trend** and **Decision throughput** cards, not your findings. Posture always reflects the latest scan, whatever period you pick, so changing the period never hides a current problem.
### Choosing a card set
The Components and Cryptography tabs have a second dropdown, which starts on **All cards**. Each option replaces the tab's cards with a smaller set for one question:
| Tab | Options |
| - | - |
| Components | **All cards**, **License compliance**, **Vulnerability**, **Audit export** |
| Cryptography | **All cards**, **Reachability**, **Post-quantum**, **Export evidence** |
Overview has no card-set dropdown. It always lists what needs doing, which doesn't change by question.
## Making the canvas your own
You can change which cards appear on a tab and how Earnie lays them out. Each tab, and each card set on a tab, keeps its own layout.
1. Select **Customise**. The cards become editable, and the button changes to **Done**.
2. Every card now shows a move handle on the left and a remove button on the right. Make your changes:
* To reorder, drag a card onto another, or use the keyboard.
* To resize, drag a card's right edge. Widths change in whole columns, from a quarter to the full width. Resizing is available only on a wide window. On narrower windows the layout is fixed to halves or full width.
* To add a card, select **Add cards** to open the card library. It lists every card this view can hold, each with a one-line description. Cards already on the Dashboard appear greyed out, so the library always shows the full set. Select a card to add it at the end. **Hide library** closes the library.
* To remove a card, select the **×** on it.
3. Select **Done** when you've finished.
Once a layout differs from its default, **Edited · Reset** appears in the control row. **Reset** returns that tab and card set to the default layout and leaves your other layouts alone.
Earnie saves rapid edits in order. If the latest save or reset fails, Earnie
reports the error and restores the last layout the server confirmed. This
way, you never see an arrangement on screen that would disappear when you
reload the page.
## What each card tells you
### Needs attention
**Needs attention** sits at the top of the page, and it's the one card you must read. It lists the things that need someone to act, each with a button that takes you to the next step.
A row appears only when its condition is true:
| Row | When it appears | Next step |
| - | - | - |
| **Not evaluated** | No policy has been evaluated for this project. | **Set up policies** |
| **Blocked** | One or more policy rules block a merge. **Request exception** opens the project's Policies page, where you can request a Policy Approval. | **Request exception** |
| **Warned** | One or more rules warn. Merging is allowed, but the record isn't clean. | **Open rule** |
| **Coverage** | Coverage is partial, so posture is declared-only and Earnie matched no files. | **Run a full scan** |
| **Ambiguous** | One or more components carry no licence assertion. | **Assign licenses** |
| **Evidence** | No SBOM snapshot exists for the current scan, or nobody has generated a notice file for it. | **Generate SBOM** or **Generate notice file** |
| **Export** | One or more crypto findings have incomplete export-control evidence. | **Complete evidence** |
When no rows apply, the card reads **Clear**: "Nothing blocks a merge and no evidence is missing."
### The rest of Overview
| Card | What it shows |
| - | - |
| **Merge gate** | The policy verdict for the project, such as **Blocked**, **Needs approval**, **Warned**, or **Passing**, and how many rules it rests on. Below the verdict are the Block, Warn, and Pass counts, the time of the evaluation, and how many approvals are active. Each count links to the policies behind it. **evaluation errors** opens Policies filtered to **Evaluation error**. |
| **Evidence** | Whether an SBOM snapshot exists for this scan. If one exists, the card reads **Ready** and lists the formats already generated and your review coverage. If not, it reads **No snapshot yet** and offers **Generate SBOM**. |
| **Provenance** | The share of files matched to known components, for example "5% of 2,828 files matched to known components". Earnie measures this from snippet fingerprints, not from dependency manifests. |
| **Components** | Open CVEs (publicly known vulnerabilities) by severity, and the number of components, licences, and copyleft licences. It counts every open CVE, including those listed as **Unrated**, so its number matches the Components tab. **Components →** opens the Components tab. |
| **Cryptography** | How much weak cryptography your code calls, the number of cryptographic assets, how many are quantum-vulnerable, and how many are unresolved for export. **Cryptography →** opens the Cryptography tab. |
| **Posture trend** | Sparklines (small trend lines) across completed scans for High-severity CVEs, Files matched to components, Blocking verdicts, Components, Reachable weak crypto, and PQC-vulnerable assets. Each sparkline shows its current value and how it moved. |
| **Decision throughput** | How many decisions on findings were closed, split between **Agent / CI** and **Human**. Until someone closes a decision, it reads **No decisions closed yet** and offers **Open Review**. |
### The AI provenance tab
The **AI provenance** tab shows what AI the project uses and which models it ships. It has two cards:
* **AI usage** counts the detections Earnie found in your source and manifests by kind: AI SDK, AI dependency, AI agent, AI tool, embedding model, vector store, and dataset. Only kinds Earnie found get a row.
* **Models** counts model files by the outcome of identifying them: **Identified**, **Not identified**, **Matching unavailable**, **Too large to fingerprint**, and **Git LFS pointer**. Earnie keeps these apart because each outcome needs a different next step.
Both cards show how many findings are **Open** and how many are **Decided**, counted the same way as in Review. A decided finding is one that someone resolved or suppressed with a decision, such as approving a model. A decided finding stays in the total, because deciding it doesn't remove the AI from your code. **Open in Review** opens Review filtered to AI findings, and **Open Models** opens the Models page.
If AI provenance has never run on the project, the tab says **Not scanned** and shows no numbers. If it ran and found nothing, the tab says **Clean**. These cards don't show policy verdicts on AI findings. Those appear in **Merge gate** and on the Policies page. See [AI provenance](/en/latest/earnie/using-earnie/ai-provenance).
#### Cryptography trend lines
The **Cryptography** summary card includes sparklines for **Reachable weak crypto** and **PQC-vulnerable assets**, meaning assets a future quantum computer could attack. The same rows appear in **Posture trend**.
* Each sparkline uses the latest twelve completed scans.
* A scan that didn't run cryptography leaves a gap in those rows, not a zero. Only a cryptography scan that ran and found nothing plots zero.
### Component matrix
On the Components tab, the **Component matrix** lists the top components by matched files. Each row shows the package identifier, version, origin, licence, and open CVEs (publicly known vulnerabilities).
### Export readiness
Some countries control the export of software that uses cryptography. On the Cryptography tab, **Export readiness** shows whether your export-control evidence is complete.
The card never states or implies an ECCN (Export Control Classification Number, the official classification an item receives under export-control rules), and says so on the card: "Evidence only, never a determination; no ECCN is stated or implied." You still decide how to classify your product.
The card shows one of three readings:
| Reading | What it means | What to do |
| - | - | - |
| **Ready** | Every active finding has complete evidence under the export regime version in use today. | Nothing. |
| **Not ready** | Evidence is missing or unresolved. | Close the evidence gap. |
| **Stale** | The evidence exists, but all of it predates an update to the export regime. | Re-scan. Nothing is wrong with the code. |
If only part of the project predates the update, the card keeps its normal reading and adds the number of findings that carry only superseded evidence. A re-scan would change those findings.
Earnie can decide some mass-market reporting-line evidence only over the
whole project. An RSA or elliptic-curve finding gets a final answer only after
Earnie checks the same scan for symmetric confidentiality algorithms. AES
counts. Authentication, integrity, hash, MAC, and signature mechanisms such as
HMAC don't. Earnie resolves this project-level condition before it computes
readiness, and keeps the original per-finding evidence unchanged for audit.
Under the disclaimer, the card names the two curated datasets behind it:
* `Risk dataset — last reviewed ` set the severity of every finding counted here.
* `Export dataset — last reviewed ` answered every export test.
The export-control report's header names the same two datasets, so the Dashboard and a filed report always refer to the same version of the data. Below those two lines, **What these datasets say** opens **Settings → Crypto Assessment**, which shows the published rules. See [Triaging findings](/en/latest/earnie/using-earnie/triaging-findings#reading-the-evidence).
### Post-quantum exposure
A large enough quantum computer could break some of the cryptography in use today. **Post-quantum exposure** shows how much of your cryptography is at risk.
* It assesses every type of cryptographic asset (algorithms, protocols, certificates, and key material) by its resolved algorithm family.
* It counts only cryptography a quantum computer breaks, which is the classical public-key families. A broken hash such as MD5 is a severity problem, never a migration item here.
* Earnie lists assets it couldn't judge separately as unassessed. It never counts them as quantum-safe, and never presents a gap in its curated data as migration work.
## Reading the numbers
Read this section before you rely on any card. Earnie uses different words for **a measurement that came back zero** and **a measurement that was never taken**, and never rolls either into a score or a grade.
| You see | It means |
| - | - |
| Not scanned | That scanner has never run and holds no findings. It's not zero: "Cryptography has never run for this project and no crypto findings are on record." If a later scan skips cryptography but earlier findings are still open, the card keeps showing them and adds a line saying when cryptography last ran. |
| Clean | The scanner ran and nothing matched: "Cryptography ran and nothing matched a rule. These are real zeros." |
| `—` | The measurement doesn't exist for this project. It never means zero or "none". You see it where a project has never been scanned, and where partial coverage leaves nothing to compute. |
| Stale | On Export readiness, the evidence exists, but Earnie computed all of it under a curated dataset that a newer one has since replaced. It's neither a gap nor a pass. The evidence is older than the rules it's measured against. |
| Unavailable | Earnie couldn't read a data store behind that one card. On Decision throughput it means the findings store was unreachable, not that nobody made decisions. On Merge gate it means Earnie couldn't read the policy posture, which is not a pass. Every other card still renders. |
| Baseline established | On Posture trend, this project has one completed scan, so there's no history to draw. Earnie won't draw a trend through a single point. |
| Not evaluated | On Merge gate, no policy has run against this project yet. It is not a pass. |
### Partial coverage changes what the numbers mean
When the header badge reads **Partial coverage**, Earnie built the posture from declared components only. These come from the manifests and lockfiles that list a project's dependencies, with no code matching behind them. In that case:
* **Provenance** and **Origin split** show `—` instead of a percentage.
* The **Component matrix** reads **Declared**, with `—` for files.
* **Needs attention** shows "Posture is declared-only: no files were matched."
Read it as a list of what the project *says* it uses, not of what's in the code.
**If the page can't load.** When Earnie can't fetch the project's scans, you
see "Could not load scans" and a **Retry** button, and no merge-gate verdict.
Earnie hides the verdict because a verdict next to "could not load" would read
as a verdict about the failure.
## Downloading evidence from the Dashboard
**Download** in the header gives you every file this project can hand to someone else, such as a customer or an auditor.
* Downloads come from the snapshot for the exact scan behind the posture on screen. Earnie doesn't substitute a newer snapshot from another scan.
* Selecting an item downloads that file immediately. It doesn't take you to the SBOM ledger.
* The menu offers only formats already generated on that snapshot, so a snapshot has to exist first. If there isn't one, Needs attention says so and offers **Generate SBOM**. See [Exporting an SBOM](/en/latest/earnie/evidence/exporting-sboms).
The menu can include:
* **SBOM: CycloneDX**, the most widely supported inventory format.
* **SBOM: SPDX**, the licence-focused format.
* **Notice file (NOTICE.txt)**, the attribution text you must distribute with your software. See [Notice files](/en/latest/earnie/evidence/exporting-sboms#notice-files-attribution-documents).
* **Export-control report**, available where cryptography is enabled and the scan found something. It's evidence for your own self-classification and states no ECCN. Its header names the curated datasets behind it and the date each was last reviewed.
## What's next
After the Dashboard shows what needs attention, open the [Review Workspace and triage those findings](/en/latest/earnie/using-earnie/triaging-findings) one by one. Once you govern more than one project, [All projects](/en/latest/earnie/using-earnie/all-projects) answers the same questions for every project in your organisation.
# Earnie CLI
Source: https://docs.scanoss.com/en/latest/earnie/using-earnie/earnie-cli
Use the earnie command-line tool to authenticate, run scans, read verdicts and policies from a pipeline, and connect a coding agent to Earnie over MCP.
The `earnie` CLI (command-line interface) lets pipelines, scripts, developers, and coding agents work with Earnie without opening the web app. Use it to:
* run a scan from a CI pipeline and fail the build when a policy blocks
* check your staged changes before you commit
* read a scan's verdict, or a project's policies, from a script
* connect a coding agent such as Claude Code or Cursor to Earnie
Every command follows two rules:
* **It never picks a project for you.** You say which project a command applies to, or the CLI works it out from your Git repository. See [Choosing the project](#choosing-the-project).
* **Its JSON output is versioned.** Automation built on version 1 keeps working when the CLI adds fields. See [JSON output](#json-output).
**Before you start, you need:**
* **Your Earnie API URL**, which is the address of your organisation's Earnie instance, for example `https://your-company.earnie.dev`.
* **An API key**, which is the credential the CLI uses to act on Earnie's behalf. See [API keys](/en/latest/earnie/administration/api-keys).
## Installing
Choose one of four ways to install the CLI.
**Homebrew**
```bash theme={null}
brew install scanoss/dist/earnie
```
**Install script (Linux or macOS)**
The installer verifies the download's checksum and supports `amd64` and `arm64`:
```bash theme={null}
curl -fsSL https://github.com/scanoss/earnie-cli/releases/latest/download/install.sh | sh
```
It installs to `~/.local/bin` by default and never elevates its own permissions. Set `EARNIE_INSTALL_DIR` to install somewhere else.
**Release archive (Linux, macOS, or Windows)**
Each release publishes prebuilt archives on the [Earnie CLI releases page](https://github.com/scanoss/earnie-cli/releases), with a `checksums.txt` file of SHA-256 sums:
* Linux and macOS, for `amd64` and `arm64`, as `.tar.gz` archives
* Windows, for `amd64`, as a `.zip` archive
Download the archive for your platform, check it against `checksums.txt`, and put the `earnie` binary (`earnie.exe` on Windows) somewhere on your `PATH`.
**Container image**
The container image is `ghcr.io/scanoss/earnie-cli:X.Y.Z`. Pass the API URL, project, and API key as arguments or environment variables at run time. Don't build credentials into an image.
### Checking the version
```bash theme={null}
earnie version
```
This always prints the CLI version and the source commit it was built from.
When an API URL is configured, it also reads these values from your Earnie instance, without an API key:
* the instance's API version
* the minimum CLI version it supports
* the CLI JSON schema versions it accepts
`earnie --json version` returns the same data as JSON, under `schema_version "1"`.
The server declares which CLI versions it accepts, and the CLI enforces that. If your CLI is older than the instance's minimum, it refuses any authenticated command with exit `8` and names the version to install.
A released CLI may print an update notice on standard error after a successful
command. The check is best effort. It never changes the command's exit code, and
never installs anything. Turn it off for one command with `--no-update-check`,
or set `EARNIE_NO_UPDATE_CHECK=1` for automation. Earnie has no automatic
update path.
## Signing in
The CLI needs to know which Earnie instance to talk to and which API key to use.
**On your own machine**, sign in interactively:
```bash theme={null}
earnie auth login --api-url https://your-company.earnie.dev
```
**In CI**, where there's no one to answer a prompt, pass the key on standard input:
```bash theme={null}
printf %s "$EARNIE_API_KEY" | earnie --api-url https://your-company.earnie.dev auth login --api-key-stdin
```
## Running a scan
Three commands choose what to scan:
| Command | What it scans |
| - | - |
| `earnie scan path [path]` | The paths you give it. |
| `earnie scan staged` | The changes you've staged in Git. |
| `earnie scan diff --base origin/main` | The changes between your branch and a base branch. |
A scan doesn't send symlinks, submodules, or special files such as sockets. The CLI never follows a link. If the link's target is inside the tree, the CLI scans the target at its own path, and it reads nothing outside the scanned path. The CLI refuses a scan path that is itself a symlink.
When `earnie scan staged` finds nothing staged, or `earnie scan diff` finds no changed source files, the command has nothing to check. It sends nothing to Earnie, prints `nothing to check:` and the reason on standard error, and exits `0`. Under `--format json` it prints one document with gate `none`, a `null` scan, and `wait.outcome` `nothing_to_check`.
### Choosing the project
The CLI works out which project to scan from the first of these that's set:
1. the `--project` flag
2. the `EARNIE_PROJECT` environment variable
3. the Git remote of the repository you're in
If none of them identifies a project, the command stops. It doesn't fall back to a default project.
### Choosing scanners
By default, a scan runs whichever scanners your organisation has enabled. Pass `--scanners` to run only some of them for this one scan, as a comma-separated list from `oss`, `crypto`, and `ai`:
```bash theme={null}
earnie scan path . --scanners oss,crypto
```
Without the flag, the server's default selection applies, the same choice the web app's New scan page offers.
### Output
| Option | Effect |
| - | - |
| `--format json` (or `--json`) | Prints a single machine-readable result. See [JSON output](#json-output). |
| `--quiet` | Hides progress messages. Errors are always shown. |
| `--color` | Controls coloured output: `auto`, `always`, or `never`. |
| `--details` | Lists the policies and findings in the human-readable output. |
Progress and diagnostics go to standard error, so they never mix with the result. When the output isn't a terminal, for example in a CI log, the CLI writes plain lines.
### Exit codes
By default, a scan command waits for both the scan and policy evaluation to finish, then exits with a code based on the gate. A pipeline can use this code to pass or fail a build:
| Exit | Meaning |
| - | - |
| `0` | `pass`, `warn`, or `none` (no policy in force, reported as "scan completed without enforcement") |
| `1` | `block` |
| `2` | `require`. Returns immediately with the Approval link, instead of waiting for a person to decide |
| `3` | Scanning, evaluation, or the gate failed, or its result is unknown |
| `4` | Invalid or expired API key |
| `5` | The key lacks a required scope, or the project is restricted |
| `6` | Retryable network or API failures were exhausted |
| `7` | Local timeout (95 minutes by default; change it with `--timeout`) |
| `8` | Invalid usage or configuration, including a missing required flag or a conflicting flag pair |
| `9` | Only from `earnie mcp doctor` and `earnie mcp setup --check`, when they found a problem |
| `130` | Interrupted |
Codes `6`, `7`, and `130` stop the CLI waiting, but never cancel the scan. The scan keeps running on the server, and the CLI prints its ID so you can check on it later.
A policy that reads only a scanner the scan didn't select, such as an AI policy on an OSS-only scan, is reported as `not_applicable` and doesn't decide the gate. The exit code comes from the policies that did apply. If a scanner the scan did select fails, the command still exits `3`.
A server error (5xx) prints a fixed message with the request ID, for example `Internal Server Error. Quote request ID when you report this.` Quote that ID to your administrator, who can match it to the cause in the API log.
### Non-blocking and follow-up commands
To keep a pipeline step from waiting, start the scan without waiting and check on it later:
| Command | What it does |
| - | - |
| `--no-wait`, on `scan path`, `scan staged`, or `scan diff` | Exits `0` as soon as the scan is accepted, returning its ID with a `null` gate. Can't be combined with `--format hook`. |
| `earnie scan wait ` | Waits for that scan, then exits with the gate [exit codes](#exit-codes) above. |
| `earnie scan status ` | Shows the scan's current state. |
| `earnie scan list` | Lists the project's scans, 50 at a time. Filter with `--status` (`pending`, `receiving`, `importing`, `extracting`, `scanning`, `completed`, `failed`, or `cancelled`), and page with `--limit` (1–200, default 50) and `--offset`. |
| `earnie verdict ` | Reads the gate a completed scan was given and the policy revisions that produced it, with any current Policy Approvals applied. It never re-evaluates. After an approval decision, running it again shows the updated gate. |
| `earnie scan audit ` | Prints the scan's audit trail, oldest first: submission, gate, approvals, and publications, one line per event with when, what, and who. |
`scan status`, `scan list`, and `scan audit` only report. They exit `0` whenever the query succeeds, whatever the scan's state or gate.
### JSON output
With `--format json`, the CLI prints one JSON document carrying `schema_version: "1"`. It includes:
* the project, and how it was resolved
* the scan, with its effective scanners, enrichments, and policy snapshot
* the evaluation, and the gate with its reason
* one entry for each policy revision
* a findings summary
* links
* a typed `error`, whose `code`, `category`, `retryable`, and `request_id` fields are stable for automation
By default, findings are summarised rather than listed: `included: false`, a `total` (`null` when the key can't read findings), and a `findings_url`. Add `--include-findings` to fetch every finding, with its verdicts and evidence. If the CLI can't fetch them all, the command fails instead of returning a partial list.
An [AI model finding](/en/latest/earnie/using-earnie/ai-provenance) carries its own `model` detail (status, purl, name, match method, and confidence band) in the JSON output and with `--details`, alongside the fields any other finding has.
Version 1 may gain new optional fields. No field is removed or renamed without a new version.
`earnie scan audit ` also has a `--format json` form. The trail is an optional `audit` member, and each event carries its type, its time, the actor as recorded at the time, and a fixed set of payload keys a script can rely on.
## Running in a pipeline
In GitHub Actions and GitLab CI, `earnie scan path` and `earnie scan diff` read details of the pipeline they run in and send them with the scan: the repository, the pull or merge request, the commit, the branch, and a link to the run. The scan list then shows the run as CI with its pull or merge request.
* Only a pull request pipeline (the `pull_request` event) or a merge request pipeline names a pull or merge request. Any other pipeline is a branch pipeline. A scan that names a pull or merge request is partial.
* The job's exit code comes from the gate, as for any blocking scan. See [Exit codes](#exit-codes).
* Keep the API key in a secret or a masked CI/CD variable, never in the pipeline file.
**GitHub Actions**
Check out the pull request's head and run the Earnie action, pinned to a CLI release:
```yaml theme={null}
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: scanoss/earnie-cli@vX.Y.Z
with:
api-key: ${{ secrets.EARNIE_API_KEY }}
api-url: https://your-company.earnie.dev
```
The action also takes a `project` input (the Project UUID or slug, which defaults to the one named by the Git remote) and an `args` input (the arguments to `earnie scan`, `path .` by default).
**GitLab CI**
Add `EARNIE_API_KEY` (masked) and `EARNIE_API_URL` as CI/CD variables, then include the Earnie template. It runs on merge request pipelines, and on branch pipelines of branches with no open merge request:
```yaml theme={null}
include:
- remote: https://raw.githubusercontent.com/scanoss/earnie-cli/vX.Y.Z/gitlab/earnie-scan.yml
```
The template runs `earnie scan path .`. Its `args` input changes the arguments, and its `stage` input sets the stage.
**Self-hosted Git.** The CLI can't work out the Project from a GitHub
Enterprise Server or self-managed GitLab remote, so set `EARNIE_PROJECT` (or
the action's `project` input). On GitHub Enterprise Server, `uses:
scanoss/earnie-cli@vX.Y.Z` works once your administrator has enabled GitHub
Connect or synced the action with `actions-sync`. On a GitLab instance that
can't reach the internet, copy `earnie-scan.yml` into a project of your own,
include it from there with `include: project`, and mirror the
`ghcr.io/scanoss/earnie-cli` image it names.
The CLI refuses a pipeline it can't read with exit `8`, before uploading anything: `ci_ambiguous` when both GitHub Actions and GitLab CI variables are set, and `ci_event_unreadable` when a pull or merge request pipeline doesn't name its pull or merge request. `earnie scan staged` ignores the pipeline and stays a pre-commit Self-check.
## Checking before you commit
A **pre-commit hook** is a script Git runs before each commit. Earnie's hook checks your staged changes against the project's policies, so you find out about a problem before you push, not when the pull request is checked.
### Installing the hook
Run this once, from inside the repository:
```bash theme={null}
earnie hook install
```
The installed pre-commit hook runs exactly `earnie scan staged --format hook`, so it runs every scanner your organisation has enabled. If you add `--scanners` to that command in your own hook, it runs only the scanners you name.
* It respects Git's effective hooks directory, including a configured `core.hooksPath` and linked worktrees.
* You can run the install again safely if the existing hook is Earnie's own.
* If another tool or a hand-written hook already owns `pre-commit`, Earnie leaves it untouched and tells you to add the command to that hook instead.
### What the hook checks
The check reads the Git index (what you've staged), not your working files:
* The hook scans added, copied, modified, and renamed staged files byte for byte, exactly as they'll be committed.
* For a staged deletion, and the old side of a staged rename, the hook sends a marker that the file is gone, so policy evaluation knows what was removed.
* The hook ignores unstaged edits.
### Reading the result
The hook prints plain text in a fixed format, so its output is the same locally and in tooling. It shows the project, the submitted and included file counts, any scanners the Self-check didn't run, the verdict, a limited list of findings sorted as `path:line`, and a link to the Self-check. There's no colour or progress animation.
If your deployment can't run a scanner your organisation has enabled, the Self-check runs the scanners it has and reports the missing one as unavailable. A policy whose scanner didn't run is reported as not evaluated, rather than passing.
| Verdict | Commit | Exit |
| - | - | - |
| `Pass`, `Warn`, or `No policy applied` | Allowed | `0` |
| `Block` | Stopped | `1` |
| `Require` | Allowed, with an approval notice. The pull-request check still decides. | `0` |
Each run creates a [Self-check](/en/latest/earnie/using-earnie/self-checks), listed with **Pre-commit hook** as its producer. It's the same page an agent's MCP review lands on.
### When the check can't run
The hook shows a technical error, such as an authentication, authorisation, network, settings, scanner, or timeout error. By default it is **fail-open**: it reports that the Self-check was unavailable and lets the commit continue.
The whole hook run has a 60-second limit, from working out the project and credentials through to waiting for the result.
To make technical errors stop the commit instead (**fail-closed**):
* For a whole project, an Operator or Admin sets **Project → Scan Configuration → Pre-commit technical failures**.
* For your own checkout only, set `EARNIE_HOOK_FAIL_CLOSED=1`.
The environment variable can only make things stricter. Setting it to `0` or `false`, or leaving it unset, never weakens a project that the setting has already made fail-closed.
Keep the pull-request check required in GitHub's branch protection. The local
hook gives early feedback, and the pull-request check decides what merges.
## Policies (read-only)
Three commands let you read a project's policies from the command line. They find the project the same way `scan` does (see [Choosing the project](#choosing-the-project)).
| Command | What it shows |
| - | - |
| `earnie policy list` | The policies attached to the project, which gate its next scan. One line each, with slug, action, whether it's enabled, its latest state, and its revision. |
| `earnie policy show ` | One policy in full: name, source, domain and category, action, the sentence given to an agent (flagged when out of date), the CEL expression, each parameter's effective value, and the project's latest result for it. |
| `earnie policy violations ` | The findings currently violating that policy. One line each, with finding ID, verdict, the status of any Policy Approval, component, and file. Page through results with `--limit` (1–200, default 50) and `--offset`. |
You can't change a policy from the CLI. Use the web app to change policies.
Every policy carries a `revision`: the `sha256:` digest of what the gate does. It's the same value a scan's policy snapshot and `earnie verdict` report, so you can tell whether the policy that blocked a scan is the one in force now.
All three commands exit `0` whenever the query succeeds, whatever the policies say. An unknown slug exits `8` with the typed error `policy_not_found`.
## Exporting an SBOM
```bash theme={null}
earnie export --include oss --output sbom.json
```
`earnie export` writes an [SBOM](/en/latest/earnie/evidence/exporting-sboms) to a file, from the command line:
| Flag | What it does |
| - | - |
| `--include` | Which sections to export, comma-separated, from `oss`, `crypto`, and `ai`. An unknown section is refused before any request is made. |
| `--bom-format` | The document format to write: `cyclonedx` (the default) or `spdx`. SPDX needs the `oss` section. |
| `--output` | The file path to write to. The write is atomic: the file only ever contains a complete document. Without it, the file is named `earnie---.json`. |
| `--scan` | Export from a specific scan instead of the project's newest completed one. |
| `--fresh` | Always generates a new SBOM snapshot, instead of reusing the latest one. |
`earnie export` doesn't generate a new [SBOM snapshot](/en/latest/earnie/evidence/exporting-sboms) on every run, because a snapshot is an immutable governance record. It downloads the scan's latest snapshot in the requested `--bom-format` that holds:
* with `--include`, exactly the sections you named
* without `--include`, the sections Earnie exports by default for your key and plan (open-source components, plus AI models where your plan includes them)
It generates a new snapshot only when none matches, or when triage decisions changed after the project's latest snapshot was generated. In that case it says so on standard error (`--quiet` silences the note). Generating needs `scans:write`. Pass `--fresh` to generate a new snapshot regardless.
A scan that finished before Earnie began keeping SBOM evidence can't be exported. The command fails with the error code `scan_evidence_unavailable`: run a new scan, then export again.
## Connecting a coding agent (MCP)
**MCP** (Model Context Protocol) is a standard way for AI coding agents to call external tools. When you connect an agent to Earnie over MCP, it can ask what your policies say and have its changes reviewed while it works.
This section is the CLI reference for `earnie mcp`. For a step-by-step guide
to connecting an agent, the tools it gets, and troubleshooting, see [Earnie
MCP](/en/latest/earnie/mcp/overview).
A coding agent such as Claude Code, Cursor, VS Code, or Codex can talk to Earnie over MCP with one API key. What it can ask for depends on the Earnie tools your deployment offers and the permissions on that key. The agent lists its granted tools when it connects.
### Creating the key
Create the key with the **Coding agent (MCP)** preset. See [API keys](/en/latest/earnie/administration/api-keys). This preset grants:
* read access to projects, policies, findings, and scans
* permission to submit a scan
That's the same permission a CI key holds, so the same key can start a full scan that records findings and changes the project's posture.
Triage stays off unless **Allow agents to triage findings** is enabled. The audit log attributes each decision the agent makes to the key.
### Setting up an agent
1. Sign in, if you haven't already:
```bash theme={null}
earnie auth login --api-url https://your-company.earnie.dev
```
2. Run setup from inside your repository:
```bash theme={null}
earnie mcp setup
```
Setup writes the stored key into each client's user-level configuration, so you don't need to export `EARNIE_API_KEY` for Claude Code, Cursor, VS Code, or Codex.
Earnie detects installed clients from their executables, application paths, and configuration files, and preselects what it finds. In scripts and non-interactive runs, pass `--client detected` or an explicit list such as `--client claude,codex`. Without `--client`, a non-interactive setup stops rather than guessing.
Setting up a repository finds the project the same way `scan` does (`--project`, `EARNIE_PROJECT`, or the Git remote). It needs `policies:read`, because it fetches the project's managed policy block before changing any file.
Useful flags:
| Flag | What it does |
| - | - |
| `--dry-run` | Prints the exact change to each file, without writing anything. |
| `--json` | Prints one machine-readable result. |
| `--no-color` (or `NO_COLOR`) | Turns off coloured output. |
| `--global` | Installs only the user-level client configuration: no project, no policy instructions. Can't be combined with `--project`. |
| `--no-hooks` / `--hooks` | Skips, or explicitly installs or upgrades, the [review hooks](#review-hooks-and-self-checks). |
Setup checks the complete change first, then applies the identity, policy instructions, and hooks as one transaction. If a write fails, the previous files are restored.
### Where setup writes
For each client, setup writes the identity (the API URL and key) to the user's configuration, and a managed policy block (instructions telling the agent what your policies say) into the repository:
| Client | Identity | Repository files |
| - | - | - |
| Claude Code | `~/.claude.json` | `CLAUDE.md`, review hooks in `.claude/settings.json` |
| Cursor | `~/.cursor/mcp.json` | `.cursor/rules/earnie.mdc`, review hooks in `.cursor/hooks.json` |
| VS Code | native user-profile `mcp.json` | `.github/copilot-instructions.md` (no review hook, because the VS Code hook contract is in Preview) |
| Codex | `~/.codex/config.toml` | `AGENTS.md`, review hooks in `.codex/hooks.json` |
Earnie changes only the text between its own markers, which start with ``. In Cursor's dedicated `earnie.mdc`, it also owns the front matter that makes the rule apply to every chat. It leaves every other byte, and the file's permissions, alone. If it finds duplicate, nested, incomplete, or hand-edited markers, it stops rather than guessing.
Codex project hooks don't run until you review and trust their exact
definition with `/hooks` inside Codex. Setup never bypasses that trust
control.
**Windows and WSL use different home directories.** The Windows build of
`earnie` and a WSL-hosted build write their identity files to different
places. Running the Windows `earnie.exe` from Git Bash, even against an API
served from WSL, still writes to `%USERPROFILE%\.claude.json` and the other
Windows paths, not the WSL home path. If setup can't find a client you expect,
check which `earnie` binary ran.
### Review hooks and Self-checks
A **review hook** asks Earnie to review an agent's work at the end of each turn, before anything is committed.
The first time you set up a repository, setup turns on project-local end-of-turn review hooks for Claude Code, Cursor, and Codex. VS Code doesn't have one yet.
What happens at the end of each turn:
1. The hook sends the files the agent changed during the turn to Earnie for review, once. This includes documentation-only changes.
2. Earnie runs a **Self-check**, not a scan. It creates no findings and doesn't change the project's posture.
3. The agent gets a link to the result. The project's [Self-checks](/en/latest/earnie/using-earnie/self-checks) page also lists every Self-check for 30 days.
4. If the result blocks, the agent gets one turn to correct it. The next end-of-turn review always ends the loop, reporting whatever findings remain.
To run a check yourself, outside the hook flow:
```bash theme={null}
earnie mcp review --staged
earnie mcp review --diff
earnie mcp review [...]
```
Set `EARNIE_SKIP_REVIEW=1` (or `true`, `yes`, or `on`) to record a visible, user-requested skip instead of making a review call. `0`, `false`, `no`, or an empty value doesn't skip.
`earnie mcp review` exits from the review's gate, the same way a blocking scan does:
| Exit | Meaning |
| - | - |
| `0` | `pass`, `warn`, or `none`, or the review was skipped |
| `1` | `block` |
| `2` | `require` |
| `3` | The review didn't complete (for example, it's still running), or its gate is an error or unknown |
#### Keeping the policy block current
`earnie mcp setup` (and `--hooks`) also installs a **session-start hook** for Claude Code, Cursor, and Codex. It refreshes the managed policy block when the project's policies change:
* When a Claude Code or Codex session starts or resumes (not on `/clear` or compaction), or a Cursor session starts, the hook asks Earnie for the project's rules.
* If the set revision differs from the one in the block, it replaces only what lies between the markers. When they match, it changes nothing.
* It never adds a block to a file that doesn't already have one.
The hook never fails a session. An unreachable server adds at most about three seconds. A timeout, a missing key, a missing project, or damaged markers leave the file untouched, and the reason goes to standard error. The agent may already have read the file when the refresh lands, so a new block applies in full from the next session. VS Code has no hook, so run `earnie mcp setup` again to refresh it.
Anything you change between the markers is overwritten the next time the
revision changes. Put your own instructions outside them. If you edit the
markers themselves, the hook stops and leaves the file alone, and `earnie mcp
setup` reports an error until you restore them.
Hooks installed by an earlier CLI don't include the session-start hook. `earnie mcp doctor` reports this (`policy_refresh` in `--json`), and `earnie mcp setup --hooks` adds it.
#### How long a Self-check waits
An Operator or Admin can set how long a Self-check waits for a result before returning, at **Settings → Scan Configuration**, on the **Self-check** tab, under **Self-check wait budget**.
* The value is a whole number of seconds, from 0 to 120. Zero uses the server default of 45 seconds.
* A positive `wait_seconds` on the API call overrides the setting for that one request. Zero, or leaving it out, falls back to the organisation setting.
When the wait runs out, the check keeps running and returns its ID for you to check later, instead of a verdict. A running check is not a verdict.
### Checking and removing the setup
To check an installation without changing anything:
```bash theme={null}
earnie mcp doctor
# equivalent to:
earnie mcp setup --check
```
Doctor reports:
* whether the API URL is compatible
* whether `EARNIE_API_KEY` or a stored key is valid
* the project it would use
* the user-level identity
It also calls the server's live MCP `tools/list`, and reports each tool's visibility, the scope it needs, and why it's missing if it is. It never grants a hidden tool. With `--global`, it checks only the connection and authentication. Doctor never changes configuration, rules, hooks, or credentials.
Doctor also reports each domain, **OSS**, **Crypto**, and **AI**, as visible or hidden, and why: not enabled for your organisation, or not in this key's scope. The same reasons explain a scanner missing from a scan's results, or a tool your agent can't see.
Doctor takes the same `--client` selection as setup, plus `configured` to check only the clients that already have Earnie configured.
Doctor exits `0` when everything is healthy and `9` when it finds a problem.
**Behaviour change for CI scripts (CLI 0.3.0).** `earnie mcp doctor` and
`earnie mcp setup --check` used to exit `2` on a problem. `2` is the gate's
`require` code, so a script that read doctor's `2` as "problems found" must
now test for `9`.
To remove Earnie from a repository:
```bash theme={null}
earnie mcp uninstall
```
This removes leftover Earnie repository MCP servers, managed policy blocks, and Earnie's own review-hook and session-start hook handlers. It uses the same client detection and `--client` selection as setup.
It does **not** remove the user-level API URL and key, so your other checkouts keep working. To remove the user-level client configuration instead, without touching any repository's rules, pass `--global`. `--dry-run` previews every removal, and running it more than once is safe.
## What's next
To connect a coding agent step by step, and see every tool it can use, continue to [Earnie MCP](/en/latest/earnie/mcp/overview). The pre-commit hook and an agent's review hooks both produce Self-checks, covered in [Self-checks](/en/latest/earnie/using-earnie/self-checks).
Every command on this page authenticates with an API key. See [API keys](/en/latest/earnie/administration/api-keys) for how to create one, choose its scopes, and pick the right preset for a pipeline or a coding agent.
Need help? [Contact our AI assistant](?assistant=open)
# When the gate blocks a merge
Source: https://docs.scanoss.com/en/latest/earnie/using-earnie/merge-gate
How Earnie comments on a pull or merge request, which ones it checks, the three routes available when a policy blocks one, and how policy approvals are tracked and revoked.
The **merge gate** is Earnie's decision on whether a pull request (GitHub) or merge request (GitLab) can merge. When someone opens or updates one, Earnie scans the change, checks its findings against the project's [policies](/en/latest/earnie/using-earnie/setting-policies), and reports the result on it. This page says "pull request" throughout. Everything on it also applies to a GitLab merge request unless a note says otherwise.
This page explains what Earnie posts on a pull request, how to control which pull requests it checks, and what to do when a policy blocks a merge.
**GitLab enforcement depends on your plan.** Earnie's check can block a merge, instead of only
recommending against it, only where the provider allows it. On GitHub this always works. On
GitLab, Earnie must be able to see that your group's plan is **Ultimate**. Otherwise the
connection is **advisory**. Earnie still comments and posts a status, but it says it can't
enforce a block. See [Merge blocking depends on your GitLab
plan](/en/latest/earnie/getting-started/connecting-a-repository#merge-blocking-depends-on-your-gitlab-plan).
**Before you start:**
* Connect the project's repository through the Earnie GitHub App or a GitLab token. See [Connecting a repository](/en/latest/earnie/getting-started/connecting-a-repository).
* Attach at least one policy to the project. Without a policy, the gate has nothing to check.
## What Earnie posts on a pull request
Each completed pull-request scan produces three things, on GitHub and GitLab:
| What | Where | What it's for |
| - | - | - |
| **The Earnie check** | The pull request's checks list (a commit status on a GitLab merge request) | The verdict. It fails when a policy blocks. On GitHub you can make it a required check. |
| **A summary comment** | The pull request's conversation (a note on a GitLab merge request) | The overall gate result and the findings behind it. |
| **Inline comments** | The changed lines of the pull request | Individual findings, placed on the lines that introduced them, each with a suggested fix. |
Earnie publishes one review for each completed pull-request scan. On GitHub it's a `COMMENT` review, and on GitLab it's the equivalent.
### Inline comments on the changed lines
Earnie places an inline comment only when a finding meets all of these conditions:
* No approval covers it.
* It's new, and Earnie can attribute it to this pull request.
* It overlaps a line the pull request added.
If the provider's record of the changed lines is missing or incomplete, Earnie doesn't guess where a finding belongs. It puts that finding in the summary comment instead. Findings beyond the project's comment cap also go in the summary comment.
Every inline comment has the same sections in the same order: **What**, **Why**, **Evidence**, **Remediation**, **Links**, and a **Prompt** you can copy into a coding agent.
The remediation uses the best evidence Earnie already has, for example an explicit fixed version or a policy parameter. When Earnie doesn't know an exact fix, it suggests the next step it can support with evidence.
By default, inline comments start at **Medium** severity, with at most **ten** per review. A project can change both. See [Choosing which pull requests Earnie checks](#choosing-which-pull-requests-earnie-checks).
### When a finding is resolved
* If a later push removes the finding, Earnie replies with the commit that resolved it and resolves its review thread.
* If a person resolves the thread while the finding still exists, Earnie treats that as an acknowledgement for this pull request and doesn't reopen the thread. If a finding returns after Earnie resolved its thread automatically, it can get a new thread.
An acknowledgement is not a Policy Approval. It doesn't change the finding or the project's gate.
### The summary comment
By default, Earnie posts the summary comment whether the gate passes or fails, so a clean pull request also has a record of what Earnie checked.
You can set a project to comment only when the gate fails. Even then, if a pull request already has a summary comment, Earnie updates it when the pull request passes. A failing verdict doesn't stay on the pull request after the fix.
## When the branch you're merging into moves
Earnie measures a pull request's result against the commit the pull request was branched from. If someone merges or pushes to that base branch, the result may no longer describe what merging would produce.
Earnie marks the old result as out of date:
1. Earnie scans the new head of the default branch.
2. When that scan finishes, the Earnie check on every open pull request still based on the old head turns **neutral**, with the text *Base branch moved; result is not conclusive*.
3. Push a commit or re-run the scan, and the check gives a real result again.
Neutral is not a failure. Earnie hasn't found anything wrong, and it hasn't cleared anything either.
Earnie doesn't re-scan any pull request for this. The check only reports that its result is out of date. Earnie leaves merged and closed pull requests as they are, because their checks record a decision that was already taken, and Earnie doesn't rewrite those.
**GitLab has no neutral status.** GitLab commit statuses don't support an inconclusive state.
On a merge request the check shows as skipped instead of showing a "not conclusive" badge. Push
a commit or re-run the scan to get a real result back.
## Choosing which pull requests Earnie checks
On a busy repository, not every pull request needs a check. An Operator or Admin can choose which pull requests Earnie scans, and how much it writes, in the **Pull-request feedback** card at **Project → Scan Configuration**.
The card has seven controls:
| Control | Options | Default |
| - | - | - |
| **Summary comment** | Every verdict, or Only on failure | Every verdict |
| **Inline comment floor** | The lowest severity that gets an inline comment | Medium |
| **Inline comment cap** | 0 to 10 inline comments per review. Zero turns inline comments off and keeps the summary. Earnie posts a review only when it has at least one inline comment to place, so a cap of zero never leaves an empty review. | 10 |
| **Draft pull requests** | Scan them like any other, or skip them until they're marked ready for review | Scan |
| **Bot-authored pull requests** | Scan or skip pull requests opened by a bot, such as a dependency updater or release automation. On many repositories, bots open most of the pull requests. | Scan |
| **Scan only these base branches** | A list of branches to check | Empty, meaning every branch |
| **Scan only these paths** | A list of file patterns. Earnie scans a pull request when it touches at least one of them. | Empty, meaning every path |
None of these controls changes a verdict. They decide which pull requests Earnie scans and how much it writes. Earnie judges every pull request it scans against the same policies, and no setting here can turn a block into a pass.
The path and base-branch controls fail toward scanning. If Earnie can't find out which files a pull request touched, or can see only part of the list, it scans the pull request instead of skipping it.
**A skipped pull request has no check.** If you've made the Earnie check a required status
(GitHub branch protection, or the equivalent merge check on GitLab), a pull request that Earnie
skips never receives the check and can't merge. Keep that in mind when you narrow the
base-branch and path lists.
### Setting the controls for the whole organisation
The same controls are available for every project at **Settings → Scan Configuration**. Turn on **Lock these controls for every project** to make every project use the organisation's values.
While the lock is on:
* Each project's form is read-only and says that your organisation has locked these controls.
* Earnie keeps each project's own saved choices. They take effect again when an Admin turns the lock off.
## Three routes when a policy blocks a merge
When a policy blocks a pull request, someone has to make a decision, and Earnie records it. The pull request shows the finding, the rule that fired, and the evidence. You have three routes, and each one leaves a record of who decided and why:
| Route | Use it when |
| - | - |
| **1. Fix it** | The risk is real and you can remove it. |
| **2. Correct the finding** | The finding is wrong. |
| **3. Request an approval** | The risk is real, understood, and accepted. |
## Route 1: Fix it
Upgrade or replace the component. The next scan re-evaluates the change, and the gate clears without further action.
## Route 2: Correct the finding
A finding can be wrong. For example, your own code matched a published copy of it, or Earnie identified the wrong package. In that case, [triage it](/en/latest/earnie/using-earnie/triaging-findings). Marking a finding as original removes it from the gate, because the policy doesn't apply to it.
## Route 3: Request an approval
When the risk is real, understood, and accepted, request a **Policy Approval**. It's a recorded decision that a specific finding may pass despite the policy.
1. Open the finding in [Review](/en/latest/earnie/using-earnie/triaging-findings), or open the policy and find the finding in its violations table.
2. Select **Request approval**.
3. Fill in the three fields:
* **Justification category** says why the finding doesn't apply or is acceptable. The options are Component not present, Vulnerable code not present, Vulnerable code not in execute path, Vulnerable code cannot be controlled by adversary, Inline mitigations exist, Fixed, Under investigation, Incorrect data, and Other.
* **Justification** is your reasoning, in your own words.
* **Expiry** is **30 days** (the default), **90 days**, **Custom date**, or **No expiry (not recommended)**.
4. Select **Request approval** to submit.
### What happens next
1. The request is pending. The finding shows **Pending approval**. A pending request doesn't change the verdict.
2. An Admin reviews it. Only someone with approval authority, which means an Admin, can decide. They select **Approve** or **Reject** and record their own reason.
3. Only an approval clears the finding. The gate holds it until then.
**Give a request an expiry.** Nobody revisits an approval that never expires. The Policies
pages mark approvals that are close to expiring, so you can review them before they lapse.
Rejected and pending requests both stay in the finding's history. Earnie discards nothing, so the record shows every request, not only the one that was approved.
### How an approval updates the pull request
When an approval clears a gate, Earnie updates the Earnie check and the comment for the same scan right away. The decision shows up on the pull request without anyone pushing a commit.
Earnie doesn't re-scan or re-evaluate anything. The scan, its findings, and the policy revisions it was judged against stay the same. Only the approval is new, and the check now reflects it.
The check turns green only if the approval cleared every violation still holding the gate. If another violation has no approval, the check stays red.
Revoking an approval updates the pull request the same way, in reverse.
## Finding what's waiting on you
An Admin can find pending requests without opening each policy.
The project's **Policies** page has an **Approvals** tab next to **All policies**. It counts the requests still awaiting a decision across every policy attached to the project. It counts pending requests only, so it shows zero once the queue is clear.
* **Approve** or **Reject** a request from its row.
* Use the **Status** filter to list **Approved**, **Rejected**, or **Revoked** requests instead.
* To withdraw an approval you've already granted, find it under **Approved** and select **Revoke**. You can revoke only approvals that haven't expired.
The queue covers one project at a time.
## What's next
The merge gate checks a change once it reaches a pull request. The [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli) applies the same policies earlier, in CI pipelines, in a pre-commit hook on a developer's machine, and in coding agents.
# Scan configuration
Source: https://docs.scanoss.com/en/latest/earnie/using-earnie/scan-configuration
How to keep files out of a scan with skip patterns and size rules, how to tune snippet matching, and how organisation, project, and repository settings combine.
**Scan Configuration** controls what a scan does before and after it runs: which files it reads, how it handles pull requests, and what it saves for the next scan. Projects and the organisation each have a Scan Configuration page with several sections. This page covers two of them. **Skip patterns** keeps files out of a scan. **Snippet tuning** controls how Earnie fingerprints files and which weak matches it keeps.
**Before you start:** You need the Operator or Admin role to change Scan Configuration. See [Team & roles](/en/latest/earnie/administration/team-and-roles).
## The other sections
Other pages document the remaining sections of Scan Configuration:
* **Pull-request feedback** sets which pull requests Earnie checks, and how much it writes. See [Merge gate](/en/latest/earnie/using-earnie/merge-gate#choosing-which-pull-requests-earnie-checks).
* **Component overrides** are saved corrections that settle a package's findings on every scan. See [Triaging findings](/en/latest/earnie/using-earnie/triaging-findings#component-overrides).
* **AI provenance** lets a project opt in to fetching models stored as Git LFS pointers. See [AI provenance](/en/latest/earnie/using-earnie/ai-provenance#git-lfs-models).
* **Self-check** sets how long a Self-check waits before it returns a result. See [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli#how-long-a-self-check-waits).
* **Automatic scanning** and **Pre-commit** are on a project's own page. They set whether a push to the default branch starts a scan, and whether a technical failure in the pre-commit hook blocks a commit. See [Your first scan](/en/latest/earnie/getting-started/first-scan#starting-a-scan) and [Earnie CLI](/en/latest/earnie/using-earnie/earnie-cli#when-the-check-cant-run).
## Three layers
Skip patterns and size rules can come from three layers:
1. **The repository's own `scanoss.json`** file, when it has one.
2. **The project's Scan Configuration**, set from the project's own **Scan Configuration** page.
3. **The organisation's Scan Configuration**, set at **Settings → Scan Configuration**.
These rules **accumulate**. If any layer skips a file, Earnie skips it, whichever layer added the rule. [Component overrides](/en/latest/earnie/using-earnie/triaging-findings#component-overrides) work differently. Earnie reads the project, `scanoss.json`, and the organisation in a fixed order of precedence, and only the most specific one applies. Keep rules from all layers add up, and no layer overrides another.
A project's **Inherited layers** view lists the rules that come from `scanoss.json` and from the organisation. Use it to find where a rule came from when it isn't in the project's own list.
## Keeping files out of a scan
Before fingerprinting, Earnie applies **keep rules**. These are path patterns and size limits that decide which files Earnie scans. See [How your source reaches Earnie](/en/latest/earnie/getting-started/first-scan#how-your-source-reaches-earnie) for where this step fits in a scan.
### Skip patterns
A **skip pattern** excludes files or directories by path, in gitignore syntax, for example `vendor/**` or `**/*.min.js`.
### Size rules
A **size rule** excludes files by size, instead of or as well as by path. Use size rules for large binary files, such as AI model weights, which can raise a scan's memory use far above what a source file needs.
Add or edit a size rule from the **Size rules** table under Skip patterns, on the project's or the organisation's Scan Configuration page:
1. Select **Add rule**, or open an existing one.
2. Enter one or more **patterns** the rule applies to.
3. Enter an amount and a unit (for example `50 MB`; 1 MB = 1024 KB) for **minimum**, **maximum**, or both.
Earnie skips a matching file when it's **smaller than the minimum** or **larger than the maximum**. A blank bound means there's no limit on that side. A rule needs at least one pattern and at least one bound, and the minimum can't be larger than the maximum.
**Changing a size rule can trigger a full rescan.** Earnie fingerprints your effective
configuration, including size rules, so that it doesn't mistake a change in which files it keeps
for a change in your code. When you edit a size rule, Earnie plans the project's next scan as
full, not incremental.
## Snippet tuning
**Snippet tuning** controls how Earnie fingerprints your files and which weaker open-source matches it keeps. It appears on the project's and the organisation's Scan Configuration page wherever open-source scanning is enabled.
| Setting | What it does |
| - | - |
| **Skip licence headers** | Removes each file's licence header, comments, and imports before fingerprinting, so that unrelated files with the same boilerplate stop matching each other. Choose **Inherit**, **On**, or **Off**. |
| **Header line cap** | The maximum number of leading lines the header filter may remove. Zero means no cap. Earnie ignores it while the header filter is off. |
| **Ranking threshold** | Hides matches whose component ranks worse than this number. Rank 1 is the strongest match, so a lower threshold keeps fewer matches. Zero or below keeps every match. Accepts whole numbers from -1 to 10. It hides weaker matches from the results. It doesn't change what the SCANOSS knowledgebase finds. |
Leave a setting empty, or on **Inherit**, to take it from the next layer. Snippet settings don't add up the way skip patterns do. Earnie resolves each setting on its own, and the most specific layer that sets it wins:
1. The project's own Scan Configuration.
2. The repository's `scanoss.json` file.
3. The organisation's Scan Configuration.
4. The SCANOSS default.
On a project's page, each setting shows its **Effective** value for the next scan and the layer it comes from, for example `Effective: Keep matches ranked 3 or better · from scanoss.json`.
The panel also lists four **Matching engine** settings that a `scanoss.json` file can contain: **Minimum snippet hits**, **Minimum snippet lines**, **Honour file extensions**, and **Engine ranking**. Earnie can't apply these yet, so the panel shows them disabled. If a project's `scanoss.json` sets one, the project page marks it **ignored by the scanner**.
**Changing snippet tuning triggers a full rescan.** Snippet settings are part of the
configuration Earnie fingerprints, as size rules are, so the project's next scan runs in full
instead of incrementally.
## What's next
To see how these rules fit into a run, go back to [Your first scan](/en/latest/earnie/getting-started/first-scan). If the files you're keeping out are model weights, see [AI provenance](/en/latest/earnie/using-earnie/ai-provenance).
# Self-checks
Source: https://docs.scanoss.com/en/latest/earnie/using-earnie/self-checks
How a Self-check reviews a developer's or coding agent's changes against the project's policies before anything is pushed, how to read one, and why a pass doesn't cover everything the project enforces.
A **Self-check** reviews a few files against the project's policies before anything is pushed. It tells a developer, or their coding agent, what the [merge gate](/en/latest/earnie/using-earnie/merge-gate) would say, within seconds, while the change is still on their machine.
A Self-check is not a scan. It creates no findings and doesn't change the project's posture, which is its overall status on the Dashboard. The Dashboard stays as it was. A Self-check is advice for the person making the change, not a record of the project.
**Before you start:** One of two tools runs Self-checks, so you need one of them set up:
* the Earnie [pre-commit hook](/en/latest/earnie/using-earnie/earnie-cli#checking-before-you-commit), or
* a coding agent connected to [Earnie MCP](/en/latest/earnie/mcp/overview), either with review hooks from `earnie mcp setup` or calling the `earnie_review_code` tool itself.
## When a Self-check runs
A Self-check runs when:
* a coding agent finishes a turn and calls its review tool, or
* the pre-commit hook runs before a commit lands.
Earnie reviews only the files it receives, not the whole codebase, against the project's policies.
## Finding a Self-check
A project's sidebar has a **Self-checks** entry next to **Scan history**. Self-checks never appear in the scan history.
* **Search** matches a policy name, a detection's file path, or its summary.
* **Gate** narrows the list to one verdict.
* **Producer** narrows the list to one source.
* A status toggle narrows the list to Running, Completed, or Failed.
The list has four columns:
| Column | What it shows |
| - | - |
| **Status** | The lifecycle: Running, Completed, or Failed. It says nothing about the outcome, and Completed doesn't mean "passed". If a run's upload stops, for example because a hook timed out or a commit was interrupted, Earnie gives up on it after an hour idle and it reads Failed. |
| **Gate** | The outcome, in the same terms a scan's gate uses: Pass, Warn, Require, Block, or Error. Two more values are never shown as green. **Not judged yet** means the run is still going and has no verdict. **No policy applied** means the run finished, but Earnie couldn't enforce any policy on the files it received. |
| **Producer** | Where the run came from: Coding agent (MCP) or Pre-commit hook. |
| **Expires** | How long the run has left, such as "in 30 days" or "today". Hover to see the exact date, in UTC. |
## Reading a Self-check
Open a run to see its detail:
* The **Verdict** states the gate in one sentence.
* **Violations** has one entry for each policy the files broke. Each entry names the policy at the revision Earnie evaluated, and lists the detections that triggered it, each with a file and line, the kind of detection, and what Earnie saw. Where Earnie can work one out, the entry also offers a remediation, with a short summary and a prompt written for a coding agent. **Copy prompt** copies that prompt to your clipboard exactly as written.
Detections are not findings. Earnie keeps them only as long as the Self-check, and they never enter the Review queue.
### Why a pass doesn't cover everything
A Self-check sees only some of the codebase, so it can't answer some policies. The **Not evaluated** list shows each of these policies with the reason, for example:
* an enrichment that didn't complete
* an expression that Earnie couldn't evaluate
* a policy whose inputs none of the submitted files carried
* a policy whose scanner didn't run for this Self-check
The Verdict card repeats the count. A **Pass** above that list means nothing Earnie reviewed violated a policy. It doesn't mean the change meets everything the project enforces.
For the same reason, a Self-check's coverage is always **Partial**. An inline check reviews the files it was given, never the whole codebase, and a partial run can't mark anything absent. See [Coverage: Full vs Partial](/en/latest/earnie/getting-started/first-scan#coverage-full-vs-partial).
### The Self-check trail
Below the Not evaluated list, the **Trail** section shows the run's own history:
* **Submitted** shows how many files the run received, and when it expires.
* **Completed** shows the gate, the coverage, how many policies Earnie evaluated, and the violation and detection counts.
* **Failed** shows the error code and message, for a run whose scanner never produced a verdict.
**Earnie keeps Self-checks for 30 days**, then purges the run. A Self-check is advice to a
developer at one point in time, not a record of the project.
The Trail outlives the run. Earnie reads the Trail from the audit log, not from the run, so a
run's link keeps showing its history for as long as the audit log keeps it. The page for a
purged run still shows the Trail, with a note that the run was purged.
If a link returns "Self-check not found" with no Trail, the ID is wrong or belongs to another
organisation. An expired run would still show its Trail.
## What's next
Self-checks and the merge gate keep problems out of your code. To give people outside your team evidence of what your code contains, [export an SBOM](/en/latest/earnie/evidence/exporting-sboms).
# Setting your first policy
Source: https://docs.scanoss.com/en/latest/earnie/using-earnie/setting-policies
How to write a policy, choose its action, see which projects it applies to, and clone one to start a new rule.
A **policy** is a rule that tells Earnie what to do when a scan finds something. For example, a policy can block any change that introduces a banned licence, or warn when a component is out of date.
Earnie uses policies to decide whether a change can merge. Each time Earnie scans a project, it checks the findings against the policies attached to that project. The result is the **verdict**, and the verdict decides the merge gate. This page explains how to create a policy, choose its action, and control which projects it applies to.
**Before you start:**
* The project needs at least one completed scan, so the policy has findings to evaluate. See [Your first scan](/en/latest/earnie/getting-started/first-scan).
* You need the Operator or Admin role to create and edit policies. See [Team & roles](/en/latest/earnie/administration/team-and-roles).
## What a policy does
A policy records a decision you would otherwise make by hand on every scan. For example, if you always reject components under a particular licence, a policy rejects them for you each time a scan finds one.
Each policy shows its rule and the findings it matches right now.
You manage policies from **Policies** in a project's sidebar. Policies belong to your organisation, and the page lists the ones attached to the current project. See [Where a policy applies](#where-a-policy-applies).
## Three possible actions
Every policy has one action. The action decides what happens when a finding matches the rule:
| Action | What happens at merge time |
| - | - |
| **Block** | The gate fails. The change doesn't merge until someone resolves or approves it. |
| **Require approval** | Earnie holds the change until someone with approval authority (an Admin) approves it. |
| **Warn** | Earnie shows the finding but gates nothing. |
**Start with Warn.** A new rule set to Warn shows you how many findings it
affects without stopping anyone's merge. Change it to Block once you've dealt
with the backlog of findings it reports.
A policy gates a run only when the run used the scanner the policy reads. If a policy reads only a scanner the run didn't select, Earnie marks it **not applicable** for that run. For example, an OSS-only scan doesn't answer an AI vendor policy, so that policy neither passes nor fails the run. When every policy in force is not applicable, the gate reads **No policy applied**. If a scanner the run did select fails or isn't available, the gate reads **Gate could not be evaluated**. Missing evidence never counts as a pass.
## Writing a policy
You can start from Earnie's built-in templates, or write your own rule from scratch.
### Starting from templates
Templates are ready-made policies for the rules most teams set up first.
1. On the project's **Policies** page, select **New policy**. If the project has no policies yet, select **Pick templates** instead.
2. The picker lists every built-in template in a sortable table, with its category and action. Select **Details** on a template to read more about it.
3. Tick the templates you want, then select **Use *n* templates** (for example **Use 3 templates**) to create them all at once.
Policies created from templates start as **drafts** and affect no merge until you turn them on. Open each policy and switch it to **Enabled** when you want Earnie to enforce it. The exception is templates you turned on in the [setup wizard](/en/latest/earnie/getting-started/workspace-setup#6-starter-policy-optional). Those are enabled straight away.
The built-in templates are:
| Category | Templates |
| - | - |
| Licences | Block banned licenses, Enforce license allowlist, Flag unknown licenses, Require approval for strong copyleft, Warn on weak copyleft |
| Vulnerabilities | Block high-severity vulnerabilities, Block known exploited vulnerabilities, Warn on high EPSS |
| Components | Require approval for deprecated components, Warn on outdated components |
| Cryptography | Seven templates, described in [Rules about cryptography](#rules-about-cryptography). Available only with Cryptography enabled. |
| AI | Seven templates, described in [AI provenance](/en/latest/earnie/using-earnie/ai-provenance#starter-templates). Available only with AI governance enabled for your workspace. |
### Starting from scratch
Select **Start from scratch** to write a rule yourself. You then give the policy a name and choose what it evaluates. See [What a policy evaluates](#what-a-policy-evaluates).
### Building the rule
Under **The rule**, you can write the condition in two ways. Earnie keeps them in sync, so a change in one shows up in the other.
* In **Sentence**, you build the condition by picking fields, operators, and values, with no syntax to learn. If you wrote a condition in CEL with an operator or value the builder doesn't offer for that field, the builder still shows it as written, listed beside the options it offers.
* In **CEL**, you write the condition as an expression in CEL (Common Expression Language), with autocomplete and live validation. Use it for conditions the Sentence builder can't express.
Choose the action from the **then** dropdown, then select **Save policy**.
A policy you write in the editor starts enabled, and the **Enable on save** switch beside **Save policy** shows this. The policy applies from the next evaluation. Switch it off to save the policy as a draft, which changes no verdict until you enable it. When you edit an existing policy, the same switch reads **Enabled** or **Disabled** to show its current state.
### Parameter checks on save
When you save, Earnie checks every parameter value against the parameter it belongs to:
* a number parameter must hold a number, and a whole-number parameter must hold a whole number
* a choice must be one of its listed options
* a list must hold only text
* Earnie refuses a value for a parameter the policy doesn't declare
Every parameter needs a value or a default. A parameter you leave empty takes its default. If it has no default either, Earnie refuses the save and asks you to add a value. When Earnie refuses a save, the editor names the parameter and the kind of value it needs.
Policies saved before this check existed keep their values until you next change their values or parameters. Earnie checks a copy made with **Clone policy** when it creates the copy.
## What a policy evaluates
Every policy evaluates one kind of finding. When you create a policy, you choose this under **Evaluates**, next to **Category**:
* **OSS findings** are findings from open-source scanning.
* **Crypto findings** are findings from cryptography scanning. Earnie offers this option only if your organisation has Cryptography enabled.
* **AI findings** are findings from AI provenance scanning, covering both identified models and AI usage. Earnie offers this option only if your workspace has AI governance enabled. See [AI provenance](/en/latest/earnie/using-earnie/ai-provenance#writing-policies-against-ai-findings) for the field group and the seven starter templates.
A policy created from a template already has the template's choice set.
You can't change the Evaluates choice after you save the policy. It decides which fields the rule can read, so changing it would break the rule. The policy's **Where this applies** panel repeats the choice, for example "Evaluates Crypto findings".
**Some fields must be checked before they're read.** Not every finding has
every field. For example, a key size exists only where Earnie resolved an
actual key length. A rule that asks whether a key is under 2048 bits has to
limit itself to findings where the key length is known.
The Sentence builder writes that check for you. In CEL, you write it with
`has()`. The editor refuses to save a rule that reads such a field without
the check, and names the field and the check to add, for example:
`field "finding.crypto.key_size" is optional — guard with has(finding.crypto.key_size)`.
## Rules about cryptography
With Cryptography enabled, the rule builder gains a **Crypto** field group, and seven cryptography templates become available:
| Template | What it catches |
| - | - |
| **Block reachable weak cryptography** | Assessed algorithms, at the severities you choose, that Earnie also traced a call path to. These are algorithms that are broken *and* provably in use. Certificates, key material, and cryptography Earnie hasn't assessed never reach this gate, because they have no assigned severity. |
| **Flag quantum-vulnerable cryptography** | Cryptography a quantum computer breaks, meaning the classical public-key families, wherever it appears. This includes certificates and the transport your code negotiates. Hashes and symmetric ciphers are out of scope by design. Add `unassessed` to the template's status list to also include algorithms Earnie hasn't curated. |
| **Block weak finite-field key sizes** | RSA, DSA, and Diffie-Hellman keys below a modulus length you set, 2048 bits by default. Earnie measures these three families in the same unit, so one threshold covers them all. The template considers only occurrences whose key length Earnie resolved. An unresolved size isn't evidence of a small one. |
| **Block weak elliptic curves** | Elliptic-curve keys below a curve size you set, 224 bits by default. This blocks P-192 and leaves P-256 and above alone. Curve bits aren't modulus bits. P-256 is roughly as strong as RSA-3072, so a curve needs its own threshold, separate from the one you set for RSA. |
| **Warn on legacy protocol versions** | Every version of SSL, and TLS 1.0 and 1.1. |
| **Warn on confidentiality crypto over export threshold** | Evidence that a confidentiality mechanism is over the cited EAR threshold. |
| **Warn on unresolved export evidence** | A confidentiality mechanism whose export threshold Earnie couldn't resolve. Signatures, hashes, and MACs never raise this, because they fall outside the threshold by design. |
EAR, the US Export Administration Regulations, is the set of export-control rules these export fields and templates refer to.
### Crypto fields for export control
The Crypto group includes the finding's purpose and three export-control signals. These fields are evidence only. Each export signal has one of four values: `met`, `not_met`, `unknown`, or `not_evaluated`.
| Field | CEL path | Meaning |
| - | - | - |
| **Crypto Purpose** | `finding.crypto.purpose` | The mechanism's purpose class under EAR Technical Note 1: `confidentiality`, `excluded`, `support`, or `unknown`. |
| **Export Confidentiality** | `finding.crypto.export_confidentiality` | `met` means the cited confidentiality threshold is exceeded. `not_met` means it isn't. `unknown` means Earnie can't resolve an applicable threshold. `not_evaluated` means the occurrence has no applicable threshold evidence. |
| **Export TN 2.c** | `finding.crypto.export_pqc` | `met` means the algorithm is on Technical Note 2.c's list of described algorithms. It doesn't mean the algorithm is post-quantum in general. ML-KEM is `met`. SLH-DSA is quantum-safe but `not_met` for this test. |
| **Export Mass-Market Line** | `finding.crypto.export_mass_market_line` | `met` or `not_met` reflects the cited reporting-line evidence. HMAC and other Technical Note 1 exclusions are `not_met`. Earnie may resolve applicable asymmetric evidence over the complete project snapshot. |
When a Sentence rule uses one of these fields, the builder shows a fixed
disclaimer: "Export-control evidence supports your self-classification.
It is not legal advice and does not determine or imply an ECCN."
The values mean different things, so write rules with care:
* **An absent field** means the producer supplied no value. Use `has()` when a field may be absent.
* **`unknown`** means the producer supplied an applicable test but couldn't resolve it.
* **`not_evaluated`** means the test had no applicable occurrence evidence.
### Fields that read more precisely than they look
* **Route Evidence** (`finding.crypto.route_evidence`) is the weakest call on the strongest path to the finding. The value is `direct` when Earnie resolved every call statically, `dispatch` when the path needs an interface or runtime dispatch call (the finding stays reachable), and `name_only` when every path needs a call matched by method name alone, so reachability is unknown.
* **No Known Callers** (`finding.crypto.no_callers_only`) is `true` when every path starts in application code that nothing in the application calls, instead of at a `main` or a recognised framework entry point. To match "reachable from a known caller", write `has(finding.crypto.no_callers_only) && !finding.crypto.no_callers_only`.
* **Dependency Relationship** (`finding.crypto.dependency_relationship`) is `direct` when your project declares the library a dependency finding sits in, and `transitive` when the library arrives through another dependency. To match "cryptography in a transitive dependency", write `has(finding.crypto.dependency_relationship) && finding.crypto.dependency_relationship == "transitive"`.
These three fields are absent when the scan traced no path to the finding, and on scans from crypto-finder releases that don't report them. Dependency Relationship is also absent on findings in your own code. Guard all three with `has()`.
* **Reachability** has three values, not two. A rule writes them as `reachable`, `unreachable`, and `unknown`, and the finding shows them as Reachable, Not reached, and Unknown. A rule that should fire only on proven usage must say `reachable`. A rule that asks for "not unreachable" also matches every finding the analysis couldn't decide.
* **Key Size (bits)** exists only where Earnie resolved an actual key length. Where it couldn't, the field is absent, and a key-size rule skips the finding instead of guessing.
* **Elliptic Curve** (`crypto.elliptic_curve`) names the curve an algorithm or a piece of key material uses, for example `secg/secp256k1`, when Earnie resolved one. It's free text, not a fixed list, because CycloneDX's own curve vocabulary has hundreds of entries.
* **Risk Assessed** records whether Earnie's curated tables decided this finding's severity. When it's false, the severity is a placeholder, not a judgement, and reachability doesn't raise it. This is why an unassessed finding never escalates to a blocking severity.
**Post-Quantum Status** has four values, and you need to tell them apart:
| Value | Meaning |
| - | - |
| `Vulnerable` | A quantum computer breaks it. This covers the classical public-key families, at any key length. |
| `Safe` | A post-quantum algorithm. |
| `Not applicable` | A quantum computer has nothing to attack. Hashes, symmetric ciphers, MACs, and key-derivation functions belong here, so a broken hash such as MD5 is a severity problem, never a migration item. |
| `Unassessed` | Earnie hasn't curated that algorithm and makes no claim either way. |
**Use Post-Quantum Status, not Quantum Safe.** The older **Quantum Safe**
yes/no field still works but is deprecated, because a yes/no value can't
separate "we checked, it's fine" from "we haven't looked". On an unassessed
asset the yes/no field is absent, not false. A rule on it must therefore say
"Quantum Safe is set and is false". A rule that only says "Quantum Safe is
false" errors on the findings it can't answer for. Write new rules against the
four-value field instead.
The field picker's Crypto group links to **Settings → Crypto Assessment**, which lists every algorithm family and verdict a cryptography rule can match on. Check it when you decide which family to name in a rule. The link opens in a new tab, so you don't lose an unsaved policy draft.
Without Cryptography enabled, Earnie hides the Crypto field group, the
cryptography templates, and the regulatory template sets.
## Regulatory template sets
Some regulations and standards require specific cryptography controls. A **regulatory template set** selects every template mapped to one clause of a standard in a single action, so you don't have to find them one by one.
The sets appear as **Quick start** cards on a project with no policies yet, and under **Start from a framework** in the template picker. Each card shows the clause it maps to.
| Template set and clause | Mapped templates |
| - | - |
| **PCI DSS 4.0 — 12.3.3** | Legacy protocols; Weak algorithms; Weak key sizes — finite-field; Weak key sizes — elliptic curves |
| **CRA — Annex I(2)(e)** | Legacy protocols; Weak algorithms |
| **CRA — Annex I Part II(1)** | Require review for unidentified models; Require review for restricted model licences |
Neither template set blocks reachable TLS 1.3. Earnie doesn't verify FIPS
140-3 module validation.
## Where a policy applies
A policy belongs to your organisation, not to a single project, so you can attach the same rule to several projects.
Open a policy to see its **Where this applies** panel. It names every project the policy is attached to and the repository connected to each one, so you can see which repositories the policy covers.
A policy governs a project's applicable findings whether or not the project
has a repository connected. Earnie covers an upload-only project the same way
as one backed by a repository. The panel shows the two facts separately: what
the policy governs, and whether the project has a repository connected.
### Editing a shared policy
Because the rule is shared, editing it changes the gate for every project it's attached to.
* If the policy is attached to more than one project, Earnie asks you to confirm before saving and lists the projects the change affects.
* If the policy is attached to one project, it saves without the confirmation.
## Cloning a policy
Cloning starts a new rule from an existing one, so you don't begin from a blank template.
1. Open the policy you want to copy.
2. Open the **More actions** menu and choose **Clone policy**.
3. Confirm in the dialog.
The copy:
* has the original's name with "(copy)" added to the end
* has its own identifier
* is attached to the project you cloned it from, so you can find and adjust it straight away
* is enabled or a draft, matching the original
The copy starts with the same rule and settings as the original, including parameter values. After that, you edit the two separately, and a change to one never affects the other.
A copy of a built-in template becomes a regular custom policy that you can
edit.
## What's next
Once a policy is live, you can apply it to more projects. [All projects](/en/latest/earnie/using-earnie/all-projects) shows every project's standing on one page and can apply a policy to several projects in one action. Then read [Merge gate](/en/latest/earnie/using-earnie/merge-gate) to see what happens on a pull request when the gate blocks a merge.
# Triaging findings
Source: https://docs.scanoss.com/en/latest/earnie/using-earnie/triaging-findings
How to work through the Review Workspace backlog, record a decision you can defend on each finding, and use keyboard shortcuts to move through findings faster.
Each scan produces **findings**. A finding is one item someone needs to review, such as a file that matches an open-source component, a declared dependency, or a use of a cryptographic algorithm. **Triage** means looking at each finding and recording a decision about what it is.
Your decisions change what your policies see. A file you mark as your own code stops counting against the merge gate, and confirming a match validates its attribution. Earnie records every decision, so you can show later why a component was accepted.
You triage in the **Review Workspace**, which is **Review** in a project's sidebar.
**Before you start:**
* The project needs at least one completed scan. See [Your first scan](/en/latest/earnie/getting-started/first-scan).
* To record decisions, you need a role that can triage, such as Operator or Admin. Viewers can move through findings but can't decide them. See [Team & roles](/en/latest/earnie/administration/team-and-roles).
## The Review Workspace
The Review Workspace has three panels:
1. **What to review**, on the left, lists the findings.
2. **The evidence**, in the middle, shows the matched source code.
3. **The record**, on the right, shows the component's details and your decision.
## Three ways to work
The toggle at the top switches how the backlog is organised:
| Mode | Groups by | Use it to |
| - | - | - |
| **Files** | Your source tree | See a match in the context of the file it's in. |
| **Findings** | File, as a queue | Work through findings one file at a time. |
| **Components** | Package | Clear large volumes, one component at a time. |
Start in Components to clear volume, and switch to Files when you need to see a match in context.
When every finding is resolved, the Findings queue still lists the resolved files. Select one to see its evidence and decision.
A filter always applies to findings, so it carries across all three views. Files and Findings keep the files that hold a matching finding, and Components keeps the components that hold one.
## Searching, filtering, and what the counts mean
### Searching
The search box matches file name, file path, component PURL (the package's standard identifier), and finding title. For a cryptographic finding it also matches the algorithm, so `md5` finds an MD5 hash even when the rule that detected it never names MD5.
It matches letters in order, not as a single block, so `lgn` finds `login.go`.
* The box shows your typing at once. The list updates only when you pause, and shows **Searching…** until it does. This way the list never shows results for a query you're still typing.
* Clearing the box takes effect at once.
Each row marks the letters that matched. When a row matched on something it doesn't display, such as a component PURL, a finding's title, or an algorithm, it says **matched component, title or algorithm** instead of marking unrelated letters in the file name.
### Filtering
**+ Filter** opens the filter menu on **Views**, a set of one-click presets built from your project's data: **Weak crypto**, **Quantum vulnerable**, **Reachable crypto**, **Copyleft**, **Has CVEs**, and **Snippet matches**. Earnie offers a view only when the project has matching data. Selecting a view replaces your current filter, and selecting the active view again clears it.
Below Views are the filter dimensions, grouped by where the fact comes from:
| Group | Includes dimensions such as |
| - | - |
| **Finding** | Evidence (open-source match, declared dependency, cryptography), Status (Open, Resolved), Severity |
| **Open source** | Match type (File, Snippet), License, License category, Vulnerability |
| **Cryptography** | Algorithm, Algorithm family, Reachability, Post-quantum status, and other crypto-specific facts such as key size and cipher mode |
You can also filter cryptographic findings from newer scans by these dimensions:
* **Route evidence** shows how strong the best call path to the finding is: **Direct calls**, **Dispatch only**, or **Name match only**. See [How strong the path is](#how-strong-the-path-is).
* **No known callers** is **Yes** when every path starts in code that nothing in your application calls.
* **Dependency** applies to cryptography found in your dependencies. It's **Direct** when your project declares the library, and **Transitive** when the library arrives through another dependency.
Route evidence and No known callers don't appear for a finding with no traced path. Findings in your own code, and findings from scans that don't report it, have no Dependency value.
A dimension appears only when your findings carry at least two different values for it. A project with no cryptographic findings shows no Cryptography dimensions, and the list you see depends on what your project's findings contain. Selecting a dimension opens a searchable list of its values, each with the number of findings you'd see if you added it. Ticking more than one value in a dimension widens the result. Adding a second dimension narrows the result to findings that match both.
Each active dimension shows as a chip beside **+ Filter**. Remove one chip on its own, or select **Clear all** to remove all of them. The number on **+ Filter** counts the dimensions you're filtering by, not the values you've ticked.
**Filters are stored in the page's address.** A link you share opens with the
same filters applied, and reloading the page keeps them. The one exception is
Components' own **Status** (Open, Resolved, Inventory), which belongs to
that view and never appears in the link.
In Files and Findings mode, the counts in **+ Filter** can include cryptography found inside a dependency. Those two views don't otherwise show it, because it has no file of its own in your repository. When this happens, the menu adds a line: **Counts include cryptography found inside dependencies, which this list does not show.**
### Reading the counts
Every header names its unit, because the numbers count different things and you can't add or subtract one from another:
* **Components** are packages. **Review groups** hold findings that belong to no component. `3 components · 1 review group` is two separate counts, never a total of four.
* **Files** are the rows of the Findings queue. **Findings** are what those rows contain. `116 files · 323 findings` is not a discrepancy.
When a filter narrows the list, the header shows both numbers, for example `1 matching component of 3`. The full total stays in the header when a filter is on.
## Reviewing an earlier run
You can open the Review Workspace from a past run in **Scan history**. When you do, you stay on that run:
* The bar at the top names the run and marks it **Selected run**.
* The run stays in the address bar, so Back, Forward, and a shared link all open the same run.
* **Current review**, beside it, returns you to the project's current review.
The bar also states that the **findings and decisions shown are current**. The run shows the source and the evidence as they were at the time. It doesn't show what was decided on that day.
### Switching projects
When you switch project from the switcher at the top left, you stay on the same kind of page if it exists in the other project. Review stays Review, and Scan history stays Scan history.
If the page showed one specific item, such as a run, a policy, or a Self-check, you land on the new project's list of those items. Earnie never carries an identifier from one project into another. A run opened under the wrong project reports itself as not found, instead of showing another project's numbers under this project's name.
## Reviewing a component
In Components mode, the left-hand list shows one row per component. Each row has its name, the number of versions matched (for example `2 versions`), the vendor or package ecosystem (such as `npm`), and how many of its findings are decided, for example `0/113`.
Selecting a component opens its findings in the middle panel and its details on the right.
### The middle panel
The toolbar above the list has **Expand loaded** and **Collapse all**, which open or close the file cards, and **Bulk decide**, which decides every open file at once. See [Deciding a whole component at once](#deciding-a-whole-component-at-once).
Most components show a single list of **matched files**, which are your own source files that match the package's published code. Each file is a card showing:
* the file path, for example `flash/PooledSocket.as`
* the match percentage (100% for a whole-file match, lower for a snippet)
* the match type (**File** or **Snippet**) and the matched version
* the file's state, such as **open**
Expand a card to see the matched source, with the decision buttons below it: **Confirm match** (`1`), **Mark original** (`2`), and **Replace component** (`3`). **Compare to OSS** shows your file beside the published open-source version, where that version is available. You decide each matched file on its own.
### Extra sections for some components
Some components also have declared dependencies or cryptography. Only then does the middle panel add a labelled section for each, with a heading and a count:
| Section | Appears when | What it shows | Decided |
| - | - | - | - |
| **DECLARED** | A manifest (for example `package.json`) declares the package as a dependency. | The manifest entry that pulled the package into your project. Shown above the matched files. | Once for the whole dependency: **Accept into SBOM** or **Dismiss**. |
| **CRYPTOGRAPHY** | Your own source contains cryptographic findings that call this package. | Those cryptographic findings. Shown below the matched files. | Exactly like a cryptographic finding in Findings mode, with the same three decisions. |
| **INVENTORY** | Earnie found cryptography inside the dependency's own sources. | Every algorithm and protocol found in the package itself. Shown last, and collapsed by default. | Never. It's evidence, not a queue. |
The matched files list has no heading. If a component has no declared dependency or cryptography, as is common for a package matched only by file, you see the list of files and nothing else. Earnie never shows an empty section. A crypto-only package, for instance, shows no Declared section and no matched files.
**INVENTORY** shows whatever data is available: names, types, primitives, PQC (post-quantum cryptography) status, reachability counts, and expandable lists of source files. Earnie doesn't infer missing metadata.
Even when collapsed, the Inventory header says how many of the dependency's findings are reachable from your application, and lists the first three reachable ones under it. Expanding it shows **Findings in this dependency**, one verdict at a time (**Reachable**, **Unknown**, or **Unreachable**), each with its count.
Each row names the algorithm, the file and line inside the dependency, and how many call paths the scanner found. On newer scans it also shows how strong the best path is. When a verdict's findings differ in strength, a second row of buttons filters them by strength. Selecting a row opens that finding in the triage panel for reading, where **View call trace** shows how your application reaches it. You decide a dependency's cryptography once, for the whole package, from the component.
If you dismiss the declared dependency, or mark it original, Earnie removes its embedded cryptography from your CBOM (cryptography bill of materials) and posture the next time they're read. This never changes the underlying findings. They stay open, in case you accept the dependency again later.
### The right-hand panel
The **Triage** panel on the right shows the component's state (for example **Open**), then its name, vendor, and package identifier (PURL), followed by groups of details. A group appears only when there's data for it:
| Group | What it shows |
| - | - |
| **Evidence** | How Earnie matched the component, for example `File match · 100%`. |
| **Vulnerabilities** | Known CVEs, each linked to its public record. |
| **Licenses** | The detected licences. Where someone has concluded a licence, it shows **Concluded license** and **Detected license** separately. |
| **Versions** | The matched versions. |
| **Lifecycle** | How old the matched version is, and how many versions behind the latest it is. |
| **Audit trail** | Every recorded action on this component. See [The audit trail](#the-audit-trail). |
Below the groups, **Detected** shows when the finding was detected, by which scanner (for example `scanoss-go`), and in which scan.
The panel opens on this component summary. Earnie expands the first open card in the middle panel, but the right-hand panel stays on the summary until you pick a card. Clicking a card, or moving to one with `j` or `k`, switches the panel to that finding. Press `Esc`, or close the card, to return to the component summary. A link that already names a finding opens on that finding.
**The two counts measure different things.** The count on the component's
row, such as `0/113`, answers "is this specific package fully decided?". It
never includes a dependency's embedded Inventory, because there's nothing
there to decide. The Review counter answers "how much of this scan's
reviewable work is done?", so the two numbers don't have to match.
## Reading the evidence
### The source panel
The middle panel shows the matched source, with the match percentage: 100% for a whole-file match, lower for a snippet.
* The selected match is highlighted in its family colour, green for open source and red for cryptography, so you work on one match at a time.
* Other matches in the same file keep a coloured rail in the gutter, and the rest of the file isn't highlighted.
* Hovering over another match previews it, but only when that match doesn't already cover your selection.
* When a file holds several matches, opening it selects the first one by line. The toolbar list names each finding, with a small family icon after the name.
### The component panel
The right-hand panel identifies the component (name, vendor, and package identifier), with its licence, known vulnerabilities, and how far behind the current release it is.
## Cryptographic findings
A cryptographic finding highlights the exact lines where an algorithm is used, in the same source panel as an open-source match.
What the finding names depends on where the cryptography is:
* Inside a dependency, it names that package.
* In your own code that calls a known cryptography library, it names that library.
* In plain standard-library usage, it names no package.
When a file holds several cryptographic findings, the toolbar list names each one by its algorithm, not by the library package, since many algorithms can share one package.
### Starting from the Algorithms page
Where Cryptography is enabled, a project's sidebar also has an **Algorithms** page. It lists every cryptographic algorithm detected in the project, one row per algorithm. Each row shows the algorithm's key size, its **Quantum status** (Vulnerable, Unassessed, or Safe), its severity, how many assets and files use it (including how many uses sit inside dependencies), its open findings, and the libraries that provide it.
Search by algorithm, family, or library, and narrow the list by primitive, quantum status, or severity, or with **Weak only**. Select a row to open the Review Workspace filtered to that algorithm, with the first file that implements it already open. An algorithm found only inside dependencies opens in Components mode instead, because there's no file of yours to show.
Use the page to answer "where do we use RSA?" or "what here isn't quantum-safe?" before you work through individual findings.
### The evidence card
The right-hand evidence card groups its facts by the kind of asset: algorithm, key material, certificate, or protocol. It shows only the facts that kind of asset has. An algorithm asset shows primitive, mode, padding, and key size, for instance. A fact the asset doesn't have gets no row, instead of an empty one.
* A protocol asset's title comes from the protocol's own name, so an SSLv3 finding reads **SSL 3.0**, not "TLS 3.0".
* What provides the cryptography (the library, API, and provider) sits in its own **Implementation** group.
* When the scanner didn't name an algorithm, the card names the matched API or the asset type, never an internal rule ID.
* When the finding also names a package, the card adds the licence, known vulnerabilities, and how far behind the current release it is, the same groups as for open source.
### Reachability
The evidence card has a reachability pill, with one line explaining it. Reachability tells you whether your application can run the code in question:
| Pill | What it means |
| - | - |
| **Reachable** | Earnie traced a call path from an entry point of your application to that code. |
| **Not reached** | Earnie looked and found no such path. |
| **Unknown** | The analysis couldn't decide, for example when call paths were found but none of them start from an entry point, every path relies on a call matched by method name only, or the path into a dependency crosses a library scanned without its source (marked **(no source)**). |
A project with no lockfile or build file isn't Unknown for that reason alone. Earnie then traces reachability through the project's own source code.
**Not reached is not the same as not used.** The trace is static analysis,
which reads the code without running it. It can't follow code reached through
reflection, dynamic dispatch, or configuration, and some languages have no
call-graph support at all. In those cases Earnie says Unknown instead of
guessing. Treat a reachable finding as more urgent, but never treat an
unreachable one as harmless.
#### How strong the path is
Some calls on a path are less certain than others. When your code calls a method through an interface, the scanner can't always tell which class receives the call, so it follows every class that could. Newer scans say how strong the best path to a finding is, under the verdict in the triage panel and at the top of the call trace:
| Wording | What it means |
| - | - |
| **Reached through direct calls** | Every call on the path was resolved to exactly one method. |
| **Reached only through runtime dispatch over** `` | Every path needs at least one call through an interface or base type. The finding stays Reachable, but on weaker evidence. Check that the class the path continues into really receives that call. |
| **Unknown: every path relies on a call matched by method name only** | The paths may be real or may be a guess, so the scanner doesn't claim the code is reached. The frame reached that way is marked **Matched by method name only**. |
A finding can also say **Reached only from code with no known callers**. This means every path starts at a function that nothing in your application calls, such as a command-line tool, a test helper, or code that only reflection reaches. The finding stays Reachable, because that function may run, but nothing shows that it does. Older scans show neither wording.
#### How your application depends on the library
On newer scans, the triage panel of a dependency finding says under the verdict whether the library is a **Direct dependency** (your project declares it) or a **Transitive dependency** (it arrives through another one). It also shows the shortest chain of declarations that brings the library in, such as `App → tika-parsers 1.28.5 → bcprov-jdk15on 1.70`. A library the scanner read without its source is marked **(no source)**.
This chain isn't the same as the libraries a call trace passes through. The trace follows calls, which can go through libraries that declare nothing about this one. To stop shipping the library, change the declaration the panel names. To stop reaching its code, change the calls the trace shows.
An assessed, reachable finding whose severity sits one level above its curated [base severity](#where-the-severity-comes-from) shows a **severity escalation** row under the reachability pill, explaining why. The row appears only for that exact combination: assessed, reachable, and exactly one level up. An unassessed or unreached finding, or one already at its base severity, shows no such row.
### The call trace
When there's a path, the branch icon on the evidence card (**View call trace**) opens **Call trace**, which lists the retained paths, entry point first.
* The first path is expanded and the rest are collapsed.
* Earnie keeps the shortest distinct paths, not everything the scanner walked. When it kept fewer paths than the scanner found, the panel says so at the top, for example `Truncated: 128 paths found, 20 shown.` On newer scans the count is of distinct routes. When the scanner itself exported fewer paths than it found, the note says how many it kept, as in `Truncated: 40 paths found, 4 kept by the scan, 1 shown.`
**Where a path starts.** Each path says where it starts, for example **Starts at StatementHandler.handle in application code, a framework entry point**. On newer scans this names the kind of start. It can be the program's `main`, or a framework entry point such as a request handler, a scheduled job, a message listener, or a method a framework calls through an interface. It can also be a function that nothing in the application calls, often a tool, a test helper, or code called by reflection. Or it can be the point where the trace stopped at the depth limit, which means the path is cut and its real start lies further up.
**Paths through dependencies.** A path can start in your code and end in a library your code never calls directly. Above the frames, each path lists the libraries it passes through, in call order, such as `App → tika-core 1.28.5 → … → bcprov-jdk15on 1.70`. Each frame inside a library is labelled with that library and version, and a line marks each crossing, such as **Leaves application code, calls into tika-core 1.28.5**. Frames in your own code have a hollow marker, and frames in a library have a filled one.
For new scans, each hop (each step in the path) shows a short, syntax-highlighted excerpt of the scanned source around the call. Selecting the excerpt opens that file and line in the middle panel, without changing the finding you're reviewing.
The last card in the path shows the cryptographic call itself. The highlighted call and the matched API sit beside statements built from the scanner's recorded argument evidence, such as `algorithm resolved statically to "SHA-256"`.
* A parameter without a proven value says `not statically resolved`.
* A hop whose archived source is no longer available says `Archived source unavailable for this frame`, and shows a compact function-and-file row instead of an empty excerpt.
* A library frame's source isn't part of your scanned files, so it never opens a file. It says **Source is inside** ``**, not in the scanned files**, and shows the file and line within that library as plain text. The exception is the crypto call itself. When the scan cached the dependency's source (Maven and Gradle today), Earnie shows its excerpt. Paths recorded before Earnie kept each frame's library say so. Rescan to see which library each frame belongs to.
### Findings inside a dependency
When a finding sits inside a dependency, its code lives in that package's own source, which isn't among your scanned files. What you see depends on what Earnie could resolve during the scan:
1. **The package's source was resolved.** Earnie shows the real file, opened at the line where the algorithm is used, in the same source panel as your own code. Today this works for Java projects built with Maven or Gradle.
2. **Only the matched line is available.** The panel shows that single line, syntax highlighted, with its real line number in the gutter.
3. **Nothing is available.** The panel says **Code not available**, instead of showing what looks like an empty file.
The decision controls stay available in every case.
### Where the severity comes from
Earnie rates the severity of cryptographic findings from curated datasets that ship with Earnie. The datasets are tables of algorithms, protocols, and key material with their risk levels.
Earnie publishes the datasets at **Settings → Crypto Assessment**. The page shows what each dataset contains, not the assessment of any one finding. It has one table, with one row per family:
* **Scope** switches between **Algorithms**, **Key material**, and **Protocols**.
* **Search** finds a family, an alternative name, or a protocol.
* **Show** narrows the table to **High severity**, **Quantum-vulnerable**, or **Export-controlled** rows.
* Each row gives its **Base severity**, its **Post-quantum** verdict (for algorithm families), and its **Export threshold**.
Select a row to open its details drawer. It shows the base severity, any key-size and mode variants that override it, the post-quantum verdict, the export-control line the family falls under, and the sources each value cites. Below the table, two short explainers describe how reachability escalates a base severity and what the post-quantum verdicts mean. Two buttons open the full export-control regime and the numbered list of every document the page cites.
A strip at the top shows the version of each dataset and when it was last reviewed. **Export ruleset** downloads the whole published ruleset as JSON, exactly as it appears on screen.
The page is read-only, because these datasets ship with Earnie and nobody can edit them. Every role can open it wherever Cryptography is enabled.
Where a published document sets a migration deadline for an algorithm family,
such as the 2030 retirement of the classical public-key families, the panel
adds a **Migration horizon 2030** marker: "Migrate before 2030, a published
deadline, not a risk level; this finding's severity is unchanged by it." A
deadline is not a severity. RSA-2048 is Low today and also has a date to
migrate by. Earnie reports both, and the deadline doesn't change the
severity. Use the deadline to plan migration work, not to decide findings.
## Making a decision
Select **Decide** and pick the answer that's true. Earnie moves you to the next open finding after each decision, and every decision offers **Undo**. Hover over an option to see its effect on the gate before you choose.
### Open-source matches
| Decision | Use it when | Effect on the gate |
| - | - | - |
| **Confirm match** | It really is that third-party component. | Stays, with attribution now validated. |
| **Mark original** | It's your own code that happens to match a published copy. | Removed from the gate. |
| **Replace component** | It's a real match, but identified as the wrong package. | Stays, against the correct package. |
### Declared dependencies
For a declared dependency, the choice is **Accept into SBOM** (`1`) or **Dismiss** (`2`). When the component has nothing else left open, Earnie moves you to the next open component.
### Cryptographic findings
| Decision | Use it when | Effect on the gate |
| - | - | - |
| **Confirm usage** | The algorithm really is in use. | Stays in the gate. |
| **False positive** | It isn't actually cryptography. | Removed from the gate. |
| **Accepted risk** | The usage is real, and you're choosing to live with it. | Removed from the gate. |
All three decisions close the finding and keep its row, so an accepted risk stays visible in the list. Whether the algorithm is reachable may change what you decide, but never which decisions you're offered.
**There is no "Fixed" for cryptography.** You record a fix by removing the
weak algorithm and re-scanning. The next run reports it as gone and keeps your
decision history. Earnie can verify that from the scan, instead of relying on
your word.
### AI model findings
Where [AI provenance](/en/latest/earnie/using-earnie/ai-provenance) is enabled, a model file gets its own decisions, separate from a usage finding:
| Decision | Use it when | Effect on the gate |
| - | - | - |
| **Approve model** | The model, its licence, and its lineage are acceptable. | Removed from the gate. |
| **Reject model** | The model shouldn't be used as identified. | Stays in the gate. |
| **Wrong identification** | Earnie matched the file to the wrong model. | Removed from the gate. |
An **AI usage** finding is code that calls an AI service or SDK, not a model file. You decide it separately: **Confirm AI usage**, **Not AI usage**, or **Approve AI usage**.
You can't apply a model-finding decision to a usage finding, or the reverse. Earnie refuses the mismatch instead of recording a decision that doesn't fit the evidence.
### Deciding a pull request's own findings
A pull request can introduce a finding that has no counterpart yet on the project's default branch. You can still resolve or reopen that finding from the pull request's own Review Workspace. Until the change merges there's no standing finding to attach the decision to, so Earnie records it as **Applies on merge**. The finding shows as decided in that pull request's view right away, including for its gate. Once the same code reaches the default branch, Earnie applies the original decision, and any Policy Approval requested against it, to the project's real findings.
**Assign**, **Suppress**, and saving a correction to Scan Configuration aren't available for these findings until they exist as standing findings after merge. **Bulk decide** skips them too, so you decide them one at a time.
### Deciding a whole component at once
In Components mode, **Bulk decide** applies a decision to every open file for that component. With the keyboard, `Shift+1` confirms all and `Shift+2` marks all as original. On a large first triage, this saves the most time.
### Saving a correction for future scans
When you replace a component, you can also tick **Also apply to future matches** of that package in this project. Earnie saves the correction to the project's Scan Configuration, and future scans apply it without asking you again.
### Component overrides
A **component override** settles a package's findings automatically on every scan, so you don't have to decide them again. Each override names a package, optionally limited to a path, and one decision:
| Override | Effect |
| - | - |
| **Remove as original** | The matches are your own code, and are removed from the gate. |
| **Replace** | The matches stay, attributed to the package you name. |
| **Confirm as declared** | The matches stay in the SBOM and still count towards the gate. Confirming doesn't hide them. |
Earnie reads overrides from three places:
1. **The project's Scan Configuration**, for example a correction saved with **Also apply to future matches**.
2. **The repository's `scanoss.json`** file: `bom.remove`, `bom.replace`, and `bom.include` (Confirm as declared). Earnie also accepts `bom.identify` as another name for `bom.include`. Earnie reads this file but never writes to it. See [Declaring components](/en/latest/getting-started/declaring-components#bom) for the file format.
3. **The organisation's Scan Configuration**.
When an override settles a finding, Earnie records it in the finding's audit trail, the same way it records a decision a person made.
**Existing `bom.include` entries now take effect.** If your repository's
`scanoss.json` already lists `bom.include` or `bom.identify` entries, Earnie
applies them from the next scan. A `path` ending in `/`, such as `"src/"`,
also matches now.
#### When overrides overlap
When more than one override names the same package, Earnie applies them in this order, and the first one to claim a path wins:
1. **By source**, from most to least specific: the project, then `scanoss.json`, then the organisation.
2. **Within one source**, remove, then replace, then include.
Paths overlap when they're identical, when one is inside the other, or when either has no path (the whole project). Earnie drops a later override whose path overlaps an earlier one. It drops the whole override, not only the overlapping part. Overrides for the same package at paths that don't overlap all apply.
### How a decision moves the Dashboard
**False positive** and **Accepted risk** take the finding out of these Cryptography Dashboard numbers:
* the reachable-and-weak headline
* the post-quantum migration counts
* the algorithm ranking
Those numbers count the work that's left, and a finding you've decided isn't left to do. The finding is still counted elsewhere. The same cards report the decided findings beside the headline, split by the decision that closed them, so a decision moves a finding from one number to another instead of removing it.
**Confirm usage works differently.** It does **not** reduce those counts.
Confirming that an algorithm is in use confirms that the risk is real, so
Earnie keeps the finding in those counts. The merge gate counts it either
way. The card shows how many findings you've confirmed, so your work is
visible and the risk stays counted.
**Deciding a finding doesn't shrink your inventory.** The counts that
describe how much cryptography is in the product ignore your decisions:
total findings, distinct assets, the reachability split, first-party versus
dependency, and the asset-type breakdown. Confirming or accepting an
algorithm doesn't remove it from the build, so it doesn't remove it from the
inventory. Only the counts of work left to do respond to triage.
Export readiness is a third case. It measures the evidence you hold for what
you ship, and deciding a finding never turns it green by itself. A false
positive drops out, because it wasn't cryptography. An accepted risk and a
confirmed usage are both still in your CBOM, and you still need to gather
export evidence for them.
### The audit trail
Earnie writes every action to the finding's **Audit trail**: what was decided, by whom, when, and what the state was before. Use this record to show later why a decision was made.
## Keyboard shortcuts
Press `?` anywhere in the Review Workspace to open the **Keyboard shortcuts** panel, which lists the shortcuts for the mode you're in.
Shortcuts do nothing while you're typing in a field. Decision keys appear only for roles that can record decisions.
### Components mode
| Key | Does |
| - | - |
| `j` / `k` | Next / previous file in this component |
| `l` / `h` | Next / previous finding in this file |
| `↓` / `↑` | Next / previous file (same as `j` / `k`) |
| `Shift+j` / `Shift+k` | Next / previous component |
| `1` / `2` / `3` | Confirm match / Mark original / Replace component, for the active file |
| `Shift+1` / `Shift+2` / `Shift+3` | Confirm all / Mark all original / Replace all, for every open file |
| `1` / `2` | Accept into SBOM / Dismiss, for the active declared dependency |
| `Esc` | Close the open card and return the right-hand panel to the component summary |
### Findings and Files modes
| Key | Does |
| - | - |
| `j` / `k` | Next / previous finding in this file (in Files mode, in tree order) |
| `Shift+j` / `Shift+k` | Next / previous file with open work (in Files mode, folder with open work) |
| `N` | Skip to the next open finding |
| `1` to `4` | Record the first to fourth decision in the Decide menu. `1` records immediately. |
| `Ctrl+Enter` / `⌘+Enter` | Record a staged decision (AI findings) |
| `Esc` | Cancel a staged decision |
The Dashboard has keys of its own, but only while **Customise** is on. Focus a
card's move handle, then use `←` / `→` to move it one place, `Home` / `End` to
move it to the start or end, and, on a wide window, `Shift+←` / `Shift+→` to
resize it. See [Making the canvas your
own](/en/latest/earnie/using-earnie/dashboard#making-the-canvas-your-own).
## Finding states
Each finding shows its state:
| State | Meaning |
| - | - |
| **Open** | Found, and nobody has decided yet. |
| **Assigned** | Someone owns it. |
| **Resolved** | Decided and closed. |
| **Rule-resolved** | Closed automatically by a rule you configured. |
| **Reopened** | Was closed, then reopened. |
| **Suppressed** | Excluded from the gate and from metrics. This is a flag, not a state, so it can sit alongside any of the above. A decision that removes a finding from the gate sets it, and reopening clears it. |
In the Files tree, each file's marker groups these into three statuses: **pending review** (needs you), **identified** (decided), and **marked as original** (your own code).
## What's next
Once your findings are triaged, turn the decisions you keep repeating into [a policy](/en/latest/earnie/using-earnie/setting-policies) that Earnie applies on every scan.
# List versions (with licenses) for a given purl
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/components/list-versions-with-licenses-for-a-given-purl
/api-reference/developer-tools/v3-openapi.yaml get /v3/components/versions
# Release notes / changelog for a component
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/components/release-notes-changelog-for-a-component
/api-reference/developer-tools/v3-openapi.yaml get /v3/components/releases
With no `requirement`, lists every version that has release notes, newest-first and paginated. With a `requirement` (an exact version or a semver range, interpreted the same way as the other component endpoints), returns the notes of the matching version(s). When the requirement matches version(s) that have no notes row, the response is `200` with an empty `releases` list and `component.info_code = RELEASE_NOTES_UNAVAILABLE`.
# Release notes for many purls in one call (SBOM use case)
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/components/release-notes-for-many-purls-in-one-call-sbom-use-case
/api-reference/developer-tools/v3-openapi.yaml post /v3/components/releases
# Resolve lifecycle status for a single purl
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/components/resolve-lifecycle-status-for-a-single-purl
/api-reference/developer-tools/v3-openapi.yaml get /v3/components/status
# Resolve lifecycle status for many purls in one call
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/components/resolve-lifecycle-status-for-many-purls-in-one-call
/api-reference/developer-tools/v3-openapi.yaml post /v3/components/status
# Search components by free term, vendor, component or both
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/components/search-components-by-free-term-vendor-component-or-both
/api-reference/developer-tools/v3-openapi.yaml get /v3/components/search
At least one of `search`, `vendor`, `component` must be present.
When `search` is set it takes precedence and a 6-pass plan is run
across `projects.component`, `projects.vendor` and
`projects.purl_name`. Otherwise the handler picks a 2- or 4-pass
plan based on which filters are populated. Results are deduped by
purl across passes and truncated to `limit`.
# Distinct copyright holders for one or many components
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/copyright/distinct-copyright-holders-for-one-or-many-components
/api-reference/developer-tools/v3-openapi.yaml post /v3/copyright/holders
For each component, returns the deduplicated copyright holders with a
per-holder detection count.
NOTE: provisional response shape, pending a definitive spec. Copyright
data is not yet available in the knowledge base, so the endpoint
resolves the component identity and returns an empty `holders` array
with `info_code: NO_INFO`.
# Per-file copyright evidence for one or many components
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/copyright/per-file-copyright-evidence-for-one-or-many-components
/api-reference/developer-tools/v3-openapi.yaml post /v3/copyright/evidence
Same per-file shape as `/v3/license/evidence`, but each detection
carries a `copyright` statement (and optional parsed `holder`) instead
of a `license_id` / `confidence`.
NOTE: copyright detection is not yet available in the knowledge base.
The endpoint currently resolves the component identity and returns an
empty `files` array with `info_code: NO_INFO`. The response contract
is final.
# Algorithms detected across every version satisfying a semver range
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography/algorithms-detected-across-every-version-satisfying-a-semver-range
/api-reference/developer-tools/v3-openapi.yaml get /v3/cryptography/algorithms/range
# Algorithms in range, batch
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography/algorithms-in-range-batch
/api-reference/developer-tools/v3-openapi.yaml post /v3/cryptography/algorithms/range
# Crypto hints, batch
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography/crypto-hints-batch
/api-reference/developer-tools/v3-openapi.yaml post /v3/cryptography/hints
# Crypto library hints (SDKs/protocols/libraries) at one resolved version
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography/crypto-library-hints-sdksprotocolslibraries-at-one-resolved-version
/api-reference/developer-tools/v3-openapi.yaml get /v3/cryptography/hints
# Cryptographic algorithms detected at one (resolved) version of a component
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography/cryptographic-algorithms-detected-at-one-resolved-version-of-a-component
/api-reference/developer-tools/v3-openapi.yaml get /v3/cryptography/algorithms
# Cryptographic algorithms for many components
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography/cryptographic-algorithms-for-many-components
/api-reference/developer-tools/v3-openapi.yaml post /v3/cryptography/algorithms
# Partition versions in a semver range into those with and without crypto
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography/partition-versions-in-a-semver-range-into-those-with-and-without-crypto
/api-reference/developer-tools/v3-openapi.yaml get /v3/cryptography/algorithms/versions/range
# Versions in range, batch
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography/versions-in-range-batch
/api-reference/developer-tools/v3-openapi.yaml post /v3/cryptography/algorithms/versions/range
# Attribution files (LICENSE/NOTICE/…) for one or many components
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/licenses/attribution-files-licensenotice…-for-one-or-many-components
/api-reference/developer-tools/v3-openapi.yaml post /v3/license/attribution
For each component, returns the attribution files associated with it
plus a URL to fetch the full content of each. Per-item failures appear
in `components` carrying `info_code` / `info_message`; `status.status`
summarises the outcome (`SUCCESS`, `PARTIAL_SUCCESS`, `ERROR`).
# Declared licenses for many components (batch)
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/licenses/declared-licenses-for-many-components-batch
/api-reference/developer-tools/v3-openapi.yaml post /v3/licenses
Batch form of `GET /v3/licenses`. Each component is resolved
independently; per-component outcome is carried in `info_code` /
`info_message`. Mirrors papi `GetComponentsLicenses`.
# Declared licenses for one component (papi License service)
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/licenses/declared-licenses-for-one-component-papi-license-service
/api-reference/developer-tools/v3-openapi.yaml get /v3/licenses
Resolves the declared licenses of a single component from the
`purl_licenses` knowledge base, walking the configured source priority
and falling back from the exact version to the nearest mined version and
then the unversioned entry. `info_code` reports `REQUIREMENT_NOT_MET` /
`VERSION_NOT_FOUND` / `NO_INFO`. Mirrors papi `GetComponentLicenses`.
# OSADL compliance obligations for a license id
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/licenses/osadl-compliance-obligations-for-a-license-id
/api-reference/developer-tools/v3-openapi.yaml get /v3/licenses/obligations
Returns the OSADL compliance block (copyleft/patent flags,
compatibility lists) for the given license id (case-insensitive).
Mirrors papi `GetObligations`.
# Per-file license evidence for one or many components
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/licenses/per-file-license-evidence-for-one-or-many-components
/api-reference/developer-tools/v3-openapi.yaml post /v3/license/evidence
For each component, returns the license evidence detected in its
files, grouped per file. Each detection carries the detector, a line
range, a URL to the backing fragment and the evidence `source`.
Per-item failures use `info_code` / `info_message`.
Evidence whose backing content the deployment does not host is omitted:
source-code evidence (`source` = `header_declared` / `spdx_tag`) is
dropped when `sources_kb` is not configured, and metadata evidence
(`metadata_declared`) when `metadata_kb` is not. Notice evidence
(`notice_declared`) is always present. A component left with no files
after this filtering returns `info_code: NO_INFO`.
# SPDX registry metadata + OSADL data for a license id
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/licenses/spdx-registry-metadata-+-osadl-data-for-a-license-id
/api-reference/developer-tools/v3-openapi.yaml get /v3/licenses/details
Returns SPDX registry metadata (name, URLs, deprecation/OSI flags) and
the OSADL compliance block for the given license id (case-insensitive).
Mirrors papi `GetDetails`.
# Get one audit event
Source: https://docs.scanoss.com/earnie/api-reference/audit/get-one-audit-event
/api-reference/earnie/audit-openapi.json get /v1/audit-log/{eventId}
# List instance-wide audit events
Source: https://docs.scanoss.com/earnie/api-reference/audit/list-instance-wide-audit-events
/api-reference/earnie/audit-openapi.json get /v1/audit-log
# Search the SCANOSS component knowledgebase
Source: https://docs.scanoss.com/earnie/api-reference/components/search-the-scanoss-component-knowledgebase
/api-reference/earnie/components-openapi.json get /v1/components
# Attach a policy to every selected, eligible project
Source: https://docs.scanoss.com/earnie/api-reference/estate/estate/attach-a-policy-to-every-selected-eligible-project
/api-reference/earnie/estate-openapi.json post /v1/estate/projects:apply-policy
Attaches one policy to every selected project, synchronously, and reports the outcome per project. Requires `policies:write`.
# Generate an SBOM snapshot for every selected, eligible project
Source: https://docs.scanoss.com/earnie/api-reference/estate/estate/generate-an-sbom-snapshot-for-every-selected-eligible-project
/api-reference/earnie/estate-openapi.json post /v1/estate/projects:generate-sbom
Queued: one job per selected project on the `estate-actions` queue. Returns immediately with a run to poll; per-project failures (e.g. `scan_not_found`) never affect the rest of the batch. Each snapshot holds the sections the project generate route mints by default for the caller: components, and models when the caller sees `ai`. A caller without `oss` gets only the sections it sees, so an `ai`-only key's snapshot holds only models. `include` names the sections instead, so one call can mint SBOMs, CBOMs, AIBOMs or xBOMs.
# Poll a queued fan-out run
Source: https://docs.scanoss.com/earnie/api-reference/estate/estate/poll-a-queued-fan-out-run
/api-reference/earnie/estate-openapi.json get /v1/estate/actions/{runId}
Returns the run's current per-project items. Clients poll while any item has not yet reached a terminal status. There is no run-level success flag — `items` is the whole truth.
# Render a notice file for every selected, eligible project
Source: https://docs.scanoss.com/earnie/api-reference/estate/estate/render-a-notice-file-for-every-selected-eligible-project
/api-reference/earnie/estate-openapi.json post /v1/estate/projects:render-notice
Queued, with a per-project auto-chain: a selected project with no current SBOM snapshot is first generated, then rendered, as one logical action, reporting which step it reached. `render_notice` performs a live upstream attribution fetch that MAY fail per project (`upstream_unavailable`), independent of every other project in the batch.
# Request a re-scan for every selected, eligible project
Source: https://docs.scanoss.com/earnie/api-reference/estate/estate/request-a-re-scan-for-every-selected-eligible-project
/api-reference/earnie/estate-openapi.json post /v1/estate/projects:request-scan
Synchronous: one `POST /v1/scans` call per selected project, made on the caller's behalf. `POST /v1/scans` already returns `202` and completes asynchronously via the existing pipeline, so no new queue work is needed for this verb.
A project with no bound repository (`repo_full_name` null) is structurally ineligible for the JSON scan-request path and is reported `skipped` with `error_code: repo_not_bound`, never rejected outright.
# Each connection's recorded health, and their aggregate
Source: https://docs.scanoss.com/earnie/api-reference/integrations/integrations/each-connections-recorded-health-and-their-aggregate
/api-reference/earnie/integrations-openapi.json get /v1/integrations/connections
# GitHub connection status (liveness-checked)
Source: https://docs.scanoss.com/earnie/api-reference/integrations/integrations/github-connection-status-liveness-checked
/api-reference/earnie/integrations-openapi.json get /v1/integrations/github
# List GitLab connections with what their tokens were checked to be
Source: https://docs.scanoss.com/earnie/api-reference/integrations/integrations/list-gitlab-connections-with-what-their-tokens-were-checked-to-be
/api-reference/earnie/integrations-openapi.json get /v1/integrations/gitlab/connections
# List the repositories the connections can access (live)
Source: https://docs.scanoss.com/earnie/api-reference/integrations/integrations/list-the-repositories-the-connections-can-access-live
/api-reference/earnie/integrations-openapi.json get /v1/integrations/repositories
# Enable or pause a project's auto-scan-on-merge
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/enable-or-pause-a-projects-auto-scan-on-merge
/api-reference/earnie/platform-openapi.json put /v1/projects/{projectId}/auto-scan
Sets whether automatic scan-on-merge is active for the project by creating or removing its `provider.push` event subscription. Disabling it does not cancel a scan already in progress, and re-enabling it does not retroactively scan commits merged while paused.
# Get a project's auto-scan-on-merge setting
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/get-a-projects-auto-scan-on-merge-setting
/api-reference/earnie/platform-openapi.json get /v1/projects/{projectId}/auto-scan
Returns whether automatic scan-on-merge is currently active for the project. Backed by the project's `provider.push` event subscription.
# Get a project's pre-commit hook setting
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/get-a-projects-pre-commit-hook-setting
/api-reference/earnie/platform-openapi.json get /v1/projects/{projectId}/hook-settings
Returns whether the Project requires the Earnie pre-commit hook to fail closed on technical errors. A Project that has never been configured reads `false`. GET uses `scans:read` so the same scoped credential that submits and reads a Self-check can read the setting.
# Get a project's pull-request feedback setting
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/get-a-projects-pull-request-feedback-setting
/api-reference/earnie/platform-openapi.json get /v1/projects/{projectId}/pr-feedback
Returns the pull-request controls in force for the project: when the Earnie summary comment appears, the inline review floor and cap, and the noise controls that decide which pull requests are scanned at all. A project that has never been configured reads the documented defaults — comment on success, medium floor, cap 10, scan drafts, scan bot authors, every base branch, every path — and the absence of a stored row IS that default rather than a missing value. `locked` and `source` say where the answer came from. When an organization has locked these controls, `locked` is true, `source` is `organization`, and the values returned are the organization's.
# Get a project's Scan Configuration sources
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/get-a-projects-scan-configuration-sources
/api-reference/earnie/platform-openapi.json get /v1/projects/{projectId}/scan-configuration
Returns the project's own editable Scan Configuration, the read-only entries contributed by the repo's committed scanoss.json (as captured by the most recent scan), and the inherited organization document.
# Get organization profile
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/get-organization-profile
/api-reference/earnie/platform-openapi.json get /v1/organization
# Get the organization logo
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/get-the-organization-logo
/api-reference/earnie/platform-openapi.json get /v1/organization/branding/logo
Returns the logo image itself. Readable by every member. Snapshot reports freeze the logo at generate time, so replacing it never changes a report that already exists.
# Get the organization's pull-request controls
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/get-the-organizations-pull-request-controls
/api-reference/earnie/platform-openapi.json get /v1/organization/pr-feedback
Returns the organization layer of the pull-request controls and whether it is locked. An organization that never saved one reads the product defaults, unlocked — the state in which every project answers for itself.
# Get the organization's Scan Configuration
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/get-the-organizations-scan-configuration
/api-reference/earnie/platform-openapi.json get /v1/organization/scan-configuration
# List the scanners enabled for your organization
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/list-the-scanners-enabled-for-your-organization
/api-reference/earnie/platform-openapi.json get /v1/organization/modules
Returns the capabilities enabled for your organization, by slug (for example `oss`, `crypto`, `ai`).
# Replace a project's Scan Configuration
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/replace-a-projects-scan-configuration
/api-reference/earnie/platform-openapi.json put /v1/projects/{projectId}/scan-configuration
Replaces the project's own editable layer only — the repo's committed scanoss.json and the organization document are read-only here.
# Replace the organization's pull-request controls
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/replace-the-organizations-pull-request-controls
/api-reference/earnie/platform-openapi.json put /v1/organization/pr-feedback
Replaces the organization layer, lock included. Every control is required: this is a complete snapshot, not a patch. Setting `locked` to true puts these values in force for every project in the organization and makes project-level writes return `409 settings_locked`. Project settings are left exactly as they are, so releasing the lock restores what each project had chosen. Like the project-level controls, nothing here changes a verdict.
# Replace the organization's Scan Configuration
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/replace-the-organizations-scan-configuration
/api-reference/earnie/platform-openapi.json put /v1/organization/scan-configuration
# Set a project's pre-commit hook setting
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/set-a-projects-pre-commit-hook-setting
/api-reference/earnie/platform-openapi.json put /v1/projects/{projectId}/hook-settings
Sets whether technical failures in the Earnie pre-commit hook block the commit. This cannot make a local override less strict, and does not replace the authoritative PR check.
# Set a project's pull-request controls
Source: https://docs.scanoss.com/earnie/api-reference/platform/organization/set-a-projects-pull-request-controls
/api-reference/earnie/platform-openapi.json put /v1/projects/{projectId}/pr-feedback
Replaces the project's pull-request controls. Every control is required: this is a complete snapshot, not a patch, so a client sends back what it read. `summary_comment` decides when the Earnie summary comment appears; `inline_severity_floor` and `inline_comment_cap` shape the inline review. The four noise controls decide which pull requests are scanned at all — drafts, bot authors, base branches and changed paths. None of them changes a verdict. Policy evaluation, the gate and the `Earnie` check run are unaffected, and nothing here retroactively adds or removes anything on pull requests already scanned. Refused with `409 settings_locked` when the organization has locked these controls; change them at `/v1/organization/pr-feedback` instead. `locked` and `source` in the body are read-only and ignored.
# Create a policy
Source: https://docs.scanoss.com/earnie/api-reference/policies/create-a-policy
/api-reference/earnie/policies-openapi.json post /v1/policies
Creates a new policy. The expression is compiled via `cel-go` before persistence; every `params.X` reference MUST resolve to a `parameter_spec` entry. Typical client flow: fetch the templates catalog via `GET /v1/policies/templates`, pick a seeded template, POST a body sourced from that row (the "Add from template" UX in the web client).
Supplying `attach_project_id` makes this one atomic create-and-attach: a 201 means the policy exists AND is attached to that project, and any failure leaves neither. There is no separate attach call to lose, and therefore no orphan definition to clean up — which matters because a policy is a shared organization-wide resource the client cannot delete on its own behalf.
`Idempotency-Key` makes the whole operation repeatable, which is what a retry after a lost response needs: without it, the retry creates a second shared definition.
# Delete a policy
Source: https://docs.scanoss.com/earnie/api-reference/policies/delete-a-policy
/api-reference/earnie/policies-openapi.json delete /v1/policies/{id}
Deleting a policy detaches it from every project it governs, so it is conditional on the policy's `revision` the same way an edit is. Evaluations a past scan was gated by survive the delete — the scan-anchored verdict read does not join `policies`.
# Get the field catalog visible to the caller
Source: https://docs.scanoss.com/earnie/api-reference/policies/get-the-field-catalog-visible-to-the-caller
/api-reference/earnie/policies-openapi.json get /v1/policies/fields
Returns the boot-validated field catalog grouped by domain, filtered to the scanners the caller's organization has enabled: a scanner-gated domain and any carrier reaching into it are omitted entirely rather than returned and hidden by the client.
The response carries a strong `ETag` header over the schema, the embedded value lists AND the withheld scanner set, so the validator differs between two organisations on the same schema; repeat requests with a matching `If-None-Match` receive `304 Not Modified` with an empty body. A fully entitled caller gets the unfiltered validator unchanged.
# List policies
Source: https://docs.scanoss.com/earnie/api-reference/policies/list-policies
/api-reference/earnie/policies-openapi.json get /v1/policies
Returns every policy persisted for the caller's Org, except the policies of a scanner the caller may not see. Those stay stored and are listed again once the scanner is visible. The same rule holds for a project's policies, their violations, evaluations, approvals and verdicts, and for the posture counts and scan gates they feed: a hidden policy, or a verdict on a hidden scanner's finding, is left out, and a hidden policy addressed by id answers as not found.
# List seeded policy templates
Source: https://docs.scanoss.com/earnie/api-reference/policies/list-seeded-policy-templates
/api-reference/earnie/policies-openapi.json get /v1/policies/templates
Returns only the seeded built-in templates, filtered to the scanners the caller's organization has enabled — a template over a vocabulary the organisation does not own is omitted, not returned and disabled. The source of truth is the embedded seed manifest mirrored into persisted rows with `source = seed`.
# Partial update of a policy
Source: https://docs.scanoss.com/earnie/api-reference/policies/partial-update-of-a-policy
/api-reference/earnie/policies-openapi.json patch /v1/policies/{id}
Server classifies the request by payload:
- Only `values` / `enabled` / `name` / `action` → tweak (no recompile; audit `policy.values_updated`).
- `expression` or `parameter_spec` present → authoring (recompile + revalidate; audit `policy.expression_updated`). The `change_kind` field on the response carries the classification.
The update is conditional on the policy's `revision`: the caller passes the revision it last read as `If-Match`, and a concurrent editor who moved the policy on gets `409 policy_conflict` instead of silently losing their edit.
# Attach a policy directly to a project
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/attach-a-policy-directly-to-a-project
/api-reference/earnie/policies-openapi.json put /v1/projects/{projectId}/policies/{policyId}
Attaching puts the policy in force for the project's next scan, so the caller states which revision it reviewed: `If-Match` carries the policy `revision`, and an attach against a revision the policy has moved on from is `409 policy_conflict`. Repeating the attach at the current revision stays idempotent.
# Clone a policy attached (or attachable) to a project
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/clone-a-policy-attached-or-attachable-to-a-project
/api-reference/earnie/policies-openapi.json post /v1/projects/{projectId}/policies/{policyId}/clone
Creates a fully independent copy of the source policy — its own row, its own slug, a deep copy of `parameter_spec` and `values` — and attaches the clone to `projectId` in the same transaction as the copy's creation. A cloned seed template becomes a `custom` policy. The clone's name is the source name suffixed with " (copy)".
# Enable one named regulatory policy template set
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/enable-one-named-regulatory-policy-template-set
/api-reference/earnie/policies-openapi.json post /v1/projects/{projectId}/policy-template-sets/{setSlug}:enable
Atomically materializes independent project policies for every fixed template in the named set. Repeated calls reuse and re-enable semantically equivalent project policies rather than creating duplicates.
# Get the project's append-only posture time series
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/get-the-projects-append-only-posture-time-series
/api-reference/earnie/policies-openapi.json get /v1/projects/{projectId}/posture-history
Returns posture-history observations newest first, optionally bounded by `from`/`to`. Two independent posture-mutating moments write points: a completed full-coverage inventory-source scan (inventory metrics), every completed crypto scan (crypto metrics), and a policy evaluation that persists at least one new evaluation row (gate metrics). A crypto-only point leaves inventory fields absent. History accrues from the moment this endpoint's capability deployed — there is no backfill for prior events, so an empty or sparse series is expected right after rollout.
# Get the project's current policy posture
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/get-the-projects-current-policy-posture
/api-reference/earnie/policies-openapi.json get /v1/projects/{projectId}/policy-posture
# List current findings violating an attached policy
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/list-current-findings-violating-an-attached-policy
/api-reference/earnie/policies-openapi.json get /v1/projects/{projectId}/policies/{policyId}/violations
One page of the current evaluation's violations, newest-evaluated first with a `finding_id` tiebreak so a row can neither repeat nor be skipped while paging. `total` is the whole evaluation's count and never the page length.
The page is cut from ONE immutable evaluation. `next_cursor` carries that evaluation's id, so once a re-evaluation replaces the current result the old cursor is refused with `409 stale_cursor` instead of silently splicing two snapshots into one list.
# List immutable evaluation snapshots for an attached policy
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/list-immutable-evaluation-snapshots-for-an-attached-policy
/api-reference/earnie/policies-openapi.json get /v1/projects/{projectId}/policies/{policyId}/evaluations
One page of evaluation history, newest-finished first with an `id` tiebreak. `total` is the whole history's length, never the page length. `limit=1` is the bounded read the policy detail header uses for "last evaluated" without loading the history tab.
# List policies directly attached to a project
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/list-policies-directly-attached-to-a-project
/api-reference/earnie/policies-openapi.json get /v1/projects/{projectId}/policies
Leaves out the policies of a scanner the caller may not see.
# Materialize an arbitrary, operator-chosen list of seeded templates
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/materialize-an-arbitrary-operator-chosen-list-of-seeded-templates
/api-reference/earnie/policies-openapi.json post /v1/projects/{projectId}/policies:from-templates
Atomically materializes one project policy per requested seeded template slug, in the order the operator picked them — the multi-select counterpart to `:enable`'s fixed regulatory sets. A slug repeated in the request collapses to the one policy its first occurrence names. Repeated calls with the same slugs reuse and, when `enabled` is true, re-enable semantically equivalent project policies rather than creating duplicates — the same idempotent materialization `:enable` gives a fixed set.
`enabled` defaults to false: the picker's "New policy" flow creates drafts an operator reviews and turns on individually. An equivalent policy already attached and disabled is left exactly as it is when `enabled` is false — no flip, no audit event — and is only re-enabled when the request asks for `enabled: true`.
# What this scan was gated by
Source: https://docs.scanoss.com/earnie/api-reference/policies/policies/what-this-scan-was-gated-by
/api-reference/earnie/policies-openapi.json get /v1/scans/{scanId}/policy-verdicts
Returns the **scan-anchored** policy result: the policy revisions this scan evaluated, the per-policy verdicts and the per-finding evidence those evaluations produced, with the *current* Policy Approval overlay applied.
The collapsed `gate` lives on `GET /v1/scans/{scanId}`, which knows the scan's own lifecycle; this route explains it policy by policy.
The read joins the scan's own evaluation links, not the project's current results. That is the whole point: editing a policy, disabling it, detaching it from the project or deleting it outright never rewrites what an earlier scan appears to have been gated by. The one thing that can still move is what this same scan effectively enforces, and only through a Policy Approval — reported as `effective_verdict` and `gate_updated_at`, never by touching the recorded verdict.
A scan nothing was recorded for returns empty `policies` and `verdicts` rather than a 404, provided the scan is observable at all. A scan that produced neither is indistinguishable from one that never existed and answers 404 — the same limitation the current-state read had. `GET /v1/scans/{scanId}` answers for such a scan.
# Read one policy
Source: https://docs.scanoss.com/earnie/api-reference/policies/read-one-policy
/api-reference/earnie/policies-openapi.json get /v1/policies/{id}
The authoritative answer to "does this policy exist, and is it here?".
A client that proves a policy's absence from a cached collection cannot tell a genuinely missing policy from a list that is merely stale or failed to load, and renders all three as a 404. This read separates them: `404` means the policy does not exist (or, with `project_id`, is not attached to that project), and any other failure is reported as what it is.
With `project_id` the response carries the project-scoped `result_summary`, exactly as the project's policy list does, so a detail view needs no second shape.
# Search values for an endpoint-sourced field
Source: https://docs.scanoss.com/earnie/api-reference/policies/search-values-for-an-endpoint-sourced-field
/api-reference/earnie/policies-openapi.json get /v1/policies/fields/{path}/values
Returns searchable `{value, label}` pairs for a field whose `values.kind = endpoint` (e.g. `license.spdx_id` backed by the embedded SPDX list).
A path for a scanner the caller's organization has not enabled answers `404` with the same body as an unknown path, so the value list of a gated field cannot be read and its existence is not disclosed.
# Delete a snapshot (metadata + rendered artifacts + inventory)
Source: https://docs.scanoss.com/earnie/api-reference/sbom/sbom/delete-a-snapshot-metadata-+-rendered-artifacts-+-inventory
/api-reference/earnie/sbom-openapi.json delete /v1/sbom/snapshots/{snapshotId}
Hard-deletes the snapshot's metadata row and all its stored objects (rendered artifacts + the neutral inventory). Behind a destructive confirmation in the UI. Other snapshots are untouched.
# Download a rendered SBOM document (original bytes)
Source: https://docs.scanoss.com/earnie/api-reference/sbom/sbom/download-a-rendered-sbom-document-original-bytes
/api-reference/earnie/sbom-openapi.json get /v1/sbom/snapshots/{snapshotId}/document
Returns the byte-exact rendered document for the requested format (default `cyclonedx`). A format not yet rendered responds `404` (`format_not_rendered`). Never mutates the snapshot.
The response body is JSON for `cyclonedx`/`spdx`/`cra-sbom-annex`, plain text (`text/plain; charset=utf-8`) for `attribution` — the NOTICE-style document is not a JSON structure — HTML (`text/html; charset=utf-8`) for `export-control` and `report-html`, a ZIP archive (`application/zip`) of CSV files for `report-csv`, an Excel workbook (`application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`) for `report-xlsx` and a PDF (`application/pdf`) for `report-pdf`. The four report formats are served as an attachment.
# Fetch snapshot metadata
Source: https://docs.scanoss.com/earnie/api-reference/sbom/sbom/fetch-snapshot-metadata
/api-reference/earnie/sbom-openapi.json get /v1/sbom/snapshots/{snapshotId}
# Generate an SBOM snapshot from a scan
Source: https://docs.scanoss.com/earnie/api-reference/sbom/sbom/generate-an-sbom-snapshot-from-a-scan
/api-reference/earnie/sbom-openapi.json post /v1/sbom/scans/{scanId}:generate
Reconciles the scan's evidence with each finding's current resolution, renders the requested format(s), and mints an immutable snapshot. Never blocked by incomplete review (soft gate): coverage is computed and stamped on the snapshot. Accepts a user session or an organization API key; the snapshot records which one generated it.
Reopened or superseded decisions are history only. Decisions apply per finding occurrence before surviving component identities are unioned. Incompatible known metadata on surviving occurrences is rejected with `409 resolution_conflict`; no snapshot is minted. Already-frozen inventories and artifacts remain unchanged.
`formats` accepts `cyclonedx`, `spdx`, and `cra-sbom-annex`. The CRA variant requires crypto entitlement and a completed crypto scan; otherwise the request is rejected with `crypto_evidence_unavailable`. `attribution` is **rejected here** with `422 unsupported_format`. Use `POST .../snapshots/{snapshotId}:render` to add it afterwards.
A snapshot holds only the scanners the caller may see: a caller who cannot see `oss` mints a CBOM with no OSS component section (and no vulnerabilities), and one who cannot see `crypto` mints no crypto section. A caller who sees neither is refused with `403 module_disabled`.
# List a project's SBOM snapshots (newest first)
Source: https://docs.scanoss.com/earnie/api-reference/sbom/sbom/list-a-projects-sbom-snapshots-newest-first
/api-reference/earnie/sbom-openapi.json get /v1/sbom/projects/{projectId}/snapshots
Lists only the snapshots the caller may read whole: one with an OSS component section needs `oss` visible, and one that includes crypto needs `crypto` visible. Every by-id route answers a withheld snapshot exactly like one that does not exist (`snapshot_not_found`). The snapshot is kept, and reads again once the scanner is enabled.
# Render an existing snapshot into another format
Source: https://docs.scanoss.com/earnie/api-reference/sbom/sbom/render-an-existing-snapshot-into-another-format
/api-reference/earnie/sbom-openapi.json post /v1/sbom/snapshots/{snapshotId}:render
Renders the requested format from the snapshot's stored inventory and attaches it. Idempotent — an already-rendered format is returned unchanged; existing artifacts are never re-rendered or mutated.
`attribution` is only ever produced here: it additionally performs a live attribution fetch from the upstream intelligence source and stamps the returned artifact's `attribution_fetched_at`. An upstream failure (or an unresolvable component set) refuses to store or freeze anything and responds `502 upstream_unavailable`; an inventory with zero components responds `422 no_components`. A partial upstream fetch (some components unresolved) IS still frozen — the document lists what could not be resolved instead of dropping it silently.
# Configuration
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/configuration
Configure Crypto Finder with command-line flags, environment variables, a config file, or a scanoss.json file in your project.
## Configuration priority
Crypto Finder applies settings in this order, from highest to lowest priority:
1. **Command-line flags** (e.g., `--api-key`, `--scanner`)
2. **Environment variables** (e.g., `SCANOSS_API_KEY`)
3. **Config file** (`~/.scanoss/crypto-finder/config.json`)
4. **Project settings** (`scanoss.json` in target directory)
5. **Default values**
## Application configuration
### Config file location
```
~/.scanoss/crypto-finder/config.json
```
### Setting up configuration
Use the `configure` command to set persistent application settings:
```bash theme={null}
# Configure API key
crypto-finder configure --api-key YOUR_API_KEY
# Configure custom API URL
crypto-finder configure --api-url https://custom.scanoss.com
# Configure both
crypto-finder configure --api-key YOUR_KEY --api-url https://custom.scanoss.com
```
### Config file format
```json theme={null}
{
"api_key": "your-scanoss-api-key",
"api_url": "https://api.scanoss.com",
"java_jdk_major": "auto",
"java_jdk_homes": {
"8": "/opt/jdks/jdk8",
"11": "/opt/jdks/jdk11",
"17": "/opt/jdks/jdk17",
"21": "/opt/jdks/jdk21"
}
}
```
### Environment variables
| Variable | Description | Example |
| - | - | - |
| `SCANOSS_API_KEY` | SCANOSS API key for remote rulesets | `export SCANOSS_API_KEY=abc123` |
| `SCANOSS_API_URL` | Custom API base URL | `export SCANOSS_API_URL=https://custom.com` |
| `SCANOSS_JAVA_JDK_MAJOR` | Java JDK major for Java dependency resolution and type enrichment (`auto`, `8`, `11`, `17`, `21`) | `export SCANOSS_JAVA_JDK_MAJOR=21` |
| `SCANOSS_JAVA_JDK_HOMES` | Comma-separated Java home mappings used for explicit JDK selection | `export SCANOSS_JAVA_JDK_HOMES=17=/opt/jdks/jdk17,21=/opt/jdks/jdk21` |
| `SCANOSS_FINDINGS_CACHE_BACKEND` | FindingsCache backend (`disk` or `postgres`); default `disk` | `export SCANOSS_FINDINGS_CACHE_BACKEND=postgres` |
| `SCANOSS_FINDINGS_CACHE_DSN` | Postgres connection string used when backend is `postgres` (required) | `export SCANOSS_FINDINGS_CACHE_DSN=postgres://user:pass@host:5432/db?sslmode=require` |
| `SCANOSS_FINDINGS_CACHE_TABLE` | Optional table name override; default `findings_cache` | `export SCANOSS_FINDINGS_CACHE_TABLE=cf_findings_cache` |
### Java runtime selection
For Java dependency scans and call graph export, Crypto Finder can use a specific JDK major version for each scan. The supported majors are `8`, `11`, `17`, and `21`.
On the command line, use `--java-jdk-major` and the repeatable `--java-jdk-home =`. In the environment or the config file, use `SCANOSS_JAVA_JDK_MAJOR`, `SCANOSS_JAVA_JDK_HOMES`, or the matching config file keys. The standalone CLI defaults to `auto`, which uses the ambient `JAVA_HOME` when it is available.
Example:
```bash theme={null}
crypto-finder scan \
--scan-dependencies \
--java-jdk-major 21 \
--java-jdk-home 17=/opt/jdks/jdk17 \
--java-jdk-home 21=/opt/jdks/jdk21 \
/path/to/java-project
```
### FindingsCache backend
Crypto Finder caches each dependency's scan results in a `FindingsCache`, so a warm scan skips the OpenGrep work for dependencies it has already analysed. There are two backends:
| Backend | Storage | When to use |
| - | - | - |
| `disk` (default) | `~/.scanoss/crypto-finder/cache/findings/` | Local development, single-machine repeated scans |
| `postgres` | a Postgres database (`findings_cache` table) | Multi-worker fleet deployments where the cache must be shared across machines |
Select the backend in any of these ways:
| Source | Setting |
| - | - |
| CLI | `--findings-cache=disk` or `--findings-cache=postgres` |
| Environment | `SCANOSS_FINDINGS_CACHE_BACKEND=postgres` |
| Config file | `findings_cache_backend: postgres` |
With `postgres`, you MUST provide the connection string in `SCANOSS_FINDINGS_CACHE_DSN`. If it is missing, the scan exits with a clear error. It never falls back to disk silently, because a misconfigured fleet would then lose cache sharing and nobody would notice.
Crypto Finder creates the table on first use with an idempotent `CREATE TABLE IF NOT EXISTS`, so there is no separate migration step. The default table name is `findings_cache`. If you run several deployments in the same database, override it with `SCANOSS_FINDINGS_CACHE_TABLE`.
Example:
```bash theme={null}
export SCANOSS_FINDINGS_CACHE_DSN="postgres://cf:cf@db.internal:5432/scanoss?sslmode=require"
crypto-finder scan \
--scan-dependencies \
--findings-cache=postgres \
--export-callgraph callgraph.json \
/path/to/project
```
How the cache behaves:
* The cache key includes the rules hash, so changing the rules invalidates entries automatically. You never need to evict entries by hand.
* A disk backend failure isn't fatal. Crypto Finder logs a warning and the scan continues without caching. A postgres backend failure, such as a missing DSN, an unreachable database, or a schema bootstrap error, is fatal.
* Each `crypto-finder scan` invocation opens its own Postgres pool and closes it when the scan finishes. For long-lived workers, reuse a single binary process where you can, so the pool setup cost is spread over more scans.
## Project configuration (scanoss.json)
The `scanoss.json` file in your project directory configures scan behavior and skip patterns.
### File location
Place `scanoss.json` in the root of the directory you're scanning:
```
your-project/
├── scanoss.json
├── src/
└── ...
```
### Configuration schema
Crypto Finder follows the [SCANOSS Settings Schema](https://scanoss.readthedocs.io/projects/scanoss-py/en/latest/scanoss_settings_schema.html).
### Basic example
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["node_modules/", "target/", "venv/", "*.min.js"]
},
"sizes": {
"max_file_size": 10485760
}
}
}
}
```
## Skip patterns
Skip patterns control which files and directories Crypto Finder leaves out of a scan.
### Default skip patterns
Crypto Finder excludes these patterns automatically:
| Category | Patterns |
| - | - |
| Version control | `.git/`, `.svn/`, `.hg/`, `.bzr/` |
| Dependencies | `node_modules/`, `vendor/`, `venv/`, `virtualenv/`, `__pycache__/` |
| Build artifacts | `dist/`, `build/`, `target/`, `*.min.js`, `*.min.css` |
| Archives | `*.zip`, `*.tar`, `*.tar.gz`, `*.tar.bz2`, `*.jar`, `*.war`, `*.ear` |
| Binaries | `*.exe`, `*.dll`, `*.so`, `*.dylib`, `*.bin` |
| Generated or vendored stubs | `shaded/`, `*OuterClass.java`, `*_pb2.py`, `*_pb2_grpc.py`, `*.pb.go`, `zz_generated*.go`, `*_pb.js`, `*_pb.ts`, `*.pb.cc`, `*.pb.h`, `*.grpc.pb.cc`, `*.grpc.pb.h`. Also files whose header is the protocol-buffer compiler marker (or prost/bindgen), except protobuf runtimes scanned as the target artifact. |
For the complete list, see the [default skip patterns](https://github.com/scanoss/crypto-finder/blob/main/internal/skip/source_defaults.go).
### Custom skip patterns
#### Pattern types
1. **Directory patterns** (end with `/`):
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["custom-dir/", "temp/"]
}
}
}
}
```
2. **File extension patterns**:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["*.log", "*.tmp", "*.cache"]
}
}
}
}
```
3. **Specific file patterns**:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["package-lock.json", "yarn.lock"]
}
}
}
}
```
4. **Path patterns**:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["src/generated/", "test/fixtures/"]
}
}
}
}
```
### Size limits
Set the maximum size of a file to scan:
```json theme={null}
{
"settings": {
"skip": {
"sizes": {
"max_file_size": 10485760
}
}
}
}
```
## Advanced configuration examples
### Monorepo configuration
For large monorepos with several subprojects:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": [
"*/node_modules/",
"*/dist/",
"*/build/",
"*/target/",
"docs/",
"scripts/",
"*.test.js",
"*.spec.ts"
]
}
}
}
}
```
### Frontend project configuration
For JavaScript and TypeScript projects:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": [
"node_modules/",
"dist/",
"build/",
".next/",
".nuxt/",
"coverage/",
"*.min.js",
"*.bundle.js",
"*.map"
]
}
}
}
}
```
### Backend project configuration
For Java, Python, and Go projects:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": [
"target/",
"venv/",
"vendor/",
"__pycache__/",
"*.pyc",
"*.class",
"*.jar"
]
}
}
}
}
```
### CI/CD configuration
This configuration scans as little as possible, to keep CI/CD pipelines fast:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": [
"node_modules/",
"vendor/",
"venv/",
"target/",
"dist/",
"build/",
"test/",
"tests/",
"*.test.*",
"*.spec.*",
"*.min.*"
]
}
}
}
}
```
## Scanner configuration
### Choosing a scanner
Crypto Finder supports more than one scanner. Select one with a command-line flag:
```bash theme={null}
# Use OpenGrep (default, recommended)
crypto-finder scan /path/to/code
# Use Semgrep
crypto-finder scan --scanner semgrep /path/to/code
```
### Cross-file analysis (Semgrep Pro)
With the Semgrep scanner, you can turn on cross-file (interfile) analysis with the `--interfile` flag. The flag adds `--pro` to the underlying Semgrep command, which turns on Semgrep Pro features such as cross-file taint tracking and type inference.
```bash theme={null}
# Enable cross-file analysis with Semgrep Pro
crypto-finder scan --scanner semgrep --interfile /path/to/code
```
> **Note:** `--interfile` works only with `--scanner semgrep`, and using it with any other scanner is an error. It requires a valid Semgrep Pro license.
## Language detection
### Automatic detection
By default, crypto-finder uses [go-enry](https://github.com/go-enry/go-enry) to detect a project's languages.
### Manual override
You can override the detected languages:
```bash theme={null}
# Scan only Java and Python files
crypto-finder scan --languages java,python /path/to/code
# Scan single language
crypto-finder scan --languages go /path/to/code
```
### Supported languages
The scanner has rules for these languages, among others:
* C/C++
* C#
* Go
* Java
* JavaScript/TypeScript
* Kotlin
* PHP
* Python
* Ruby
* Rust
* Swift
Because of language detection, Crypto Finder loads only the rules that apply to the project, which makes scans faster.
## Timeout configuration
### Default timeout
The default scan timeout is **10 minutes**.
### Custom timeout
```bash theme={null}
# 30 minute timeout
crypto-finder scan --timeout 30m /path/to/code
# 2 hour timeout
crypto-finder scan --timeout 2h /path/to/code
# 90 second timeout
crypto-finder scan --timeout 90s /path/to/code
```
### Recommended timeouts
| Project size | Recommended timeout |
| - | - |
| Small (\<1000 files) | 5m |
| Medium (1000-10000 files) | 15m |
| Large (10000-50000 files) | 30m |
| Very large (>50000 files) | 1h+ |
## Output configuration
### Output destination
```bash theme={null}
# Write to file
crypto-finder scan --output results.json /path/to/code
# Write to stdout (default)
crypto-finder scan /path/to/code
# Pipe to another tool
crypto-finder scan /path/to/code | jq '.findings | length'
```
### Output format
```bash theme={null}
# SCANOSS Interim JSON format (default)
crypto-finder scan --format json /path/to/code
# CycloneDX CBOM format
crypto-finder scan --format cyclonedx /path/to/code
```
## Logging configuration
### Verbosity levels
```bash theme={null}
# Normal output
crypto-finder scan /path/to/code
# Verbose logging (info level)
crypto-finder scan -v /path/to/code
crypto-finder scan --verbose /path/to/code
# Debug mode (debug info)
crypto-finder scan -d /path/to/code
crypto-finder scan --debug /path/to/code
```
# Dependency scanning and call chain attribution
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/dependency-scanning
How crypto-finder finds cryptographic usage in project dependencies and traces it back to user code through call graph analysis.
Contract note: the interim report holds findings metadata only (`source`, `dependency_info`, `finding_id`, and optional `occurrence_key`). The finding-centric reachability slices, such as `call_chains`, `entry_call`, and `crypto_call`, are in the separate `--export-callgraph` artifact, not in the interim report.
## Overview
With `--scan-dependencies`, crypto-finder scans more than the user's source code. It resolves the project's dependency tree, scans each dependency for cryptographic usage, builds a cross-package call graph, and traces each finding back to the user's code. The question it answers is **"Does my code actually reach this crypto function?"**
```mermaid theme={null}
flowchart TB
subgraph Input
UC[User Code your project]
DEP[Dependencies go.sum / pom.xml / ...]
end
subgraph Pipeline["ScanWithDependencies Pipeline"]
direction TB
S1["① Resolve Dependencies"]
S2["② Load & Filter Rules"]
S3["③ Scan Dependencies in Parallel"]
S4["④ Build Call Graph"]
S5["⑤ Trace & Attribute Findings"]
S6["⑥ Merge Reports"]
S1 --> S2 --> S3 --> S4 --> S5 --> S6
end
UC --> S1
DEP --> S1
S6 --> OUT[Final Report with source, finding_id, optional occurrence_key, dependency_info]
```
## The six-step pipeline
The full pipeline is in [`DependencyScanner.ScanWithDependencies()`](https://github.com/scanoss/crypto-finder/blob/main/internal/engine/dependency_scanner.go). The sections below explain what each step does and why.
### Step 1: Resolve dependencies
```mermaid theme={null}
flowchart LR
Target["Project Dir"] --> Resolver
Resolver --> RR["ResolveResult"]
RR --> Modules["[]Dependency {Module, Version, Dir}"]
RR --> RootMod["RootModule e.g. github.com/myorg/app"]
```
The [`Resolver` interface](https://github.com/scanoss/crypto-finder/blob/main/internal/dependency/resolver.go) discovers all dependencies and locates their source code on disk. Each ecosystem has its own resolver implementation (see [Supported ecosystems](#supported-ecosystems) below). Each `Dependency` carries:
| Field | Go Example | Java Example | Python Example | Rust Example | Purpose |
| - | - | - | - | - | - |
| `Module` | `golang.org/x/crypto` | `org.bouncycastle:bcprov-jdk18on` | `cryptography` | `ring` | Import path / coordinate |
| `Version` | `v0.17.0` | `1.77` | `42.0.5` | `0.17.8` | Resolved version |
| `Dir` | `~/go/pkg/mod/golang.org/x/crypto@v0.17.0` | `~/.crypto-finder/cache/sources/org.bouncycastle:bcprov-jdk18on/1.77/` | `~/.local/lib/python3.x/site-packages/cryptography/` | `~/.cargo/registry/src/.../ring-0.17.8/` | Filesystem path to scan |
| `Files` | `[.../x/crypto@v0.17.0/chacha20poly1305/chacha20poly1305.go, ...]` | unset | `[.../site-packages/google/auth/_helpers.py, ...]` for a namespace distribution, `[.../site-packages/configobj/__init__.py, ..., .../site-packages/validate/__init__.py]` for one with several top-level roots, otherwise unset | unset | The only files of `Dir` the dependency's source holds: the Go files the host build compiles in Go's imported packages, or a Python distribution's own files in a shared namespace directory; unset scans all of `Dir` |
The `RootModule` (e.g. `github.com/myorg/app` for Go, `com.myorg` for Java) is the default user-code prefix. For Java, packages of functions whose files live in the scanned project tree are also user code. That matters for Gradle projects that omit `group` and therefore export a project name rather than a Java package prefix.
### Step 2: Load and filter rules
Crypto-finder loads the rules once and keeps only those for the ecosystem's languages. A Go project keeps only `go` rules, and a Java project only `java` rules. Crypto-finder never runs rules for other languages against the source, which cuts scanner overhead.
The kept rules are then written to one merged file, which every dependency scan reuses. OpenGrep loads each config file separately on every run, so one file loads faster than hundreds. The merged file is named `merged-rules.yaml` but holds JSON text, one rule per line: JSON is valid YAML, and OpenGrep parses it about seven times faster. Each merged rule ID carries its source file's directory, the prefix OpenGrep derived from the file location before, so finding rule IDs do not change. Files the merge cannot represent exactly are passed to the scanner unchanged. These are invalid YAML, `*.test.yaml` fixtures, hidden paths, and YAML with no JSON form of the same meaning, such as a number written `0x10` or an alias.
### Step 3: Scan dependencies in parallel
```mermaid theme={null}
flowchart TB
Work["Deduplicated Deps"] --> Cache{"Findings cache per dependency"}
Cache -->|hit| R0["Cached report"]
Cache -->|miss| API{"SCANOSS API findings published for the package version, with an API key"}
API -->|published| RA["Published report"]
API -->|none| Shape["Batches balanced by source bytes, at most 16 roots each"]
Shape --> Pool["Worker Pool default: half the cores, max 8"]
Pool --> W1["Worker 1: one scanner process over roots A, B, C"] --> R1["Reports A, B, C"]
Pool --> W2["Worker 2: one scanner process over roots D, E"] --> R2["Reports D, E"]
```
Dependencies are deduplicated by `module@version` and processed in a stable order (`module`, `version`, `dir`) so repeated scans produce deterministic report and call graph inputs. The findings cache is consulted per dependency; the misses are then grouped into batches and each worker runs **one scanner process per batch**, over every root in it, instead of one process per dependency. OpenGrep loads the rules once per process, and that load is the dominant cost of scanning a small dependency (about 20 s for the JavaScript and TypeScript rules), so a 50-package npm project pays it a handful of times instead of 50. Each process gets `--jobs` sized to its share of the cores (see the `scannerJobs` log field).
**With an API key, published findings stand in for a dependency's detection.** The SCANOSS mining service scans each package version once and publishes its findings. When an API key is configured, a dependency the findings cache does not hold is looked up by versionless purl and version (`POST /v3/cryptography/reachability/component`, purl and requirement only). The published findings are taken as they are, including the closest mined patch the API serves for that version and the rules version it was mined with, and are kept to the dependency's own files. The dependency is then not scanned. A dependency the API has not mined, a lookup error, a refused key, or an API that does not offer the endpoint (HTTP 404) scans the dependency locally; a refused key or a missing endpoint stops further lookups for that scan, with one warning. Published findings are not written to the findings cache. The call graph, conditioned findings and reachability are always built from the local sources, so findings derived from calls between dependencies are kept. Each lookup sends the package's purl and version to the API. `--no-dependency-findings-api` scans every dependency locally (`internal/engine/dependency_findings_source.go`).
Each root's report is what a scan of that root alone produces. The OpenGrep adapter (`ScanRoots`, `internal/scanner/opengrep/batch.go`) attributes every result and error to the root holding its file (longest prefix) and runs the same transformation per root, with that root as the target, so paths, deduplication, rule IDs and finding IDs are unchanged. An error without a file, such as a memory limit reported for the whole process, marks every root in the batch incomplete. The dependency scanner then stamps the ruleset and enriches each report as it does for a single scan, and writes each dependency to the findings cache under its own key, still skipping a dependency whose scan stopped at a limit.
Batch shaping (`internal/engine/dependency_batches.go`):
* **Nested roots never share a process.** An npm dependency's scan excludes its own `node_modules/`, where another dependency may be installed, and an exclusion applies to the whole process. Roots are assigned a nesting level (0 when no other root holds them, otherwise one more than the root that does; a repeated directory counts as nested) and only roots of the same level are batched together. A scoped root that names none of another root's files does not hold it (`scanner.Disjoint`): Python namespace siblings, and a Python distribution rooted at `site-packages` beside the distributions installed below it, share a level. Scoped roots carry no exclusion anchored at their own directory, so sharing a process hides none of the other root's files.
* **Balanced by weight.** Each dependency is weighed by the bytes of its source files for the ecosystem's languages (a cheap walk that skips nested `node_modules`). Within a level, the misses are split into `max(ceil(n / 16), min(workers, n))` batches: heaviest dependency first, each into the lightest batch, so the workers finish together and a very large dependency keeps a process to itself. Batches are queued heaviest first.
* **Process timeout.** A batch gets the single-scan `--timeout` once per `ceil(roots / jobs)`: OpenGrep analyzes `jobs` files at a time, so the roots consume about that many single-scan budgets of wall time. With the default 10 minutes and 4 jobs, a 16-root batch may run for 40 minutes before it is treated as failed.
* **Failure isolation.** When a batch's process fails (exit status above 1, unreadable output, timeout), every dependency in it is scanned alone, as before batching, so one faulty dependency fails only itself and the others keep their findings and their cache entries. A canceled scan fails the batch's dependencies and rescans nothing.
* A batch of one root, which includes every dependency when the scanner cannot batch (the Semgrep adapter), goes through the single-scan path unchanged.
**npm dependencies are the production tree.** `NpmResolver` (`internal/dependency/npm_resolver.go`) drops the lockfile entries marked `"dev": true` (in a v1 lockfile, a dev entry together with the entries nested under it), so an installed development-only package is neither scanned nor put in the dependency graph. `devOptional` packages stay, because npm installs them without dev when an optional production dependency needs them, and workspace members stay because they are the project's own code. `--include-dev-dependencies` (`WithNpmDevDependencies`) restores the development-only packages. Absent dev packages, as after `npm install --omit=dev`, are never an error either way.
**Go dependencies scan only the files their imported packages compile.** A Go program contains only the packages it imports, and of each package only the files its build compiles, so the Go resolver lists, for each package in the production import closure, the Go files `go list -e -deps` reports in `GoFiles` and `CgoFiles` (`Files`), and the scan reads only those files: not the module's other packages, not the subdirectories of an imported package, which are packages of their own, and not the files that build constraints exclude. Build constraints apply file by file, for the scanning host (see Host build configuration under Go-specific): a file for another `GOOS` or `GOARCH` (`*_windows.go`, `//go:build darwin`), one whose build tag is unset, a `//go:build ignore` generator, and with `CGO_ENABLED=0` a file that imports `"C"` are not scanned. Only Go files are listed, because a Go dependency scan runs only Go rules and the Go call graph parses only Go files: a package's C, assembly and other files, and its `_test.go` files, never yielded a finding. modernc.org/libc, for example, ships one 4 MB `ccgo__.go` per platform, and the host build compiles one of them. The dependency's root stays the module directory, so attribution, result paths, finding IDs and the batch nesting levels are those of a whole-module scan; only the targets change. The scanner receives the files as named targets (a `DetectionScope`, the same mechanism as `--detect-paths-from`, with `--force-exclude` so the dependency's skip patterns still apply to them). Files are leaves, so the packages of one module, such as `foo/` and `foo/bar/`, never hide one another, and a batch over many files is split into several processes when the file list exceeds the command-line budget. OpenGrep keeps its language and size filters for named files, so a file in an imported package yields the findings it yielded in a whole-module scan. The findings cache key includes the scoped file list, so a whole-module entry is never served for a package-scoped scan, or one set of packages for another. A scanner that cannot limit detection to files (the Semgrep adapter) still scans the whole module, under the whole-module key, and the dependency keeps only the findings in its files.
**Python namespace distributions scan only their own files.** Several distributions can install into one namespace directory: protobuf, googleapis-common-protos, google-auth and google-api-core all resolve to `site-packages/google`. When two or more distributions resolve to the same `Dir`, the pip resolver gives each one the files under it that its `*.dist-info/RECORD` lists and that exist (`Files`). A distribution without a RECORD owns the files no sibling's RECORD lists; `top_level.txt` names import roots, not files, so it can choose `Dir` but cannot split it. A distribution with a directory of its own keeps `Files` unset and scans its whole `Dir` as before. The shared directory stays the root, so a finding's path is relative to it, as in a whole-directory scan (`auth/_helpers.py` for google-auth), and the files go to the scanner as named targets through the same `DetectionScope` as Go's packages. Siblings whose files are disjoint share a batch: the scanner attributes each result by the exact file named in a scope, so they are not nesting levels of one another. Siblings whose files overlap (two RECORDs listing one file, or two distributions without a RECORD) are still scanned in separate processes. Each file is therefore scanned once and reported under the one distribution that installed it. Previously every sibling scanned the whole namespace in its own process and reported every finding in it. A scanner that cannot limit detection to files (the Semgrep adapter) still scans the whole directory for each sibling, and each sibling keeps only the findings in its own files.
**Python distributions scan every top-level package and module they installed.** `packages_distributions()` maps import names to distributions, so one distribution can have several: `configobj` installs `configobj/` and `validate/`, `PyYAML` installs `yaml/` and `_yaml/`, setuptools installs `setuptools/`, `pkg_resources/` and `_distutils_hack/`. The resolver takes each name that is a directory in `site-packages`, or a module file `name.py` there (`six.py`), drops a name inside another one (`googleapiclient/discovery_cache`) names that are neither (compiled extensions), and `__pycache__`, which importlib infers from the bytecode of module files and which all module files share, and sorts the rest. A distribution with exactly one package directory keeps it as `Dir` and `ImportPath`, so its paths, finding IDs and cache keys are unchanged. Any other distribution is rooted at `site-packages`, the one directory that holds all its roots, with an empty `ImportPath` and `Files` listing every file under each of its package directories and each module file; its share of a namespace directory it also installs into is the files its RECORD lists, as above. Its finding paths therefore start with the package (`validate/__init__.py`, `six.py`). Previously the resolver kept whichever name it met first in a Go map, a different one from run to run: `configobj` scanned `configobj/` in one run and `validate/` in the next, setuptools one of its three packages, and a distribution whose first name was a module file, such as `six`, was skipped as a single-file module. Findings and call-graph functions of these distributions changed between runs of the same binary on the same environment; they no longer do. The scope of a distribution at `site-packages` names none of the other distributions' files, so it batches with them (see above) and never reports their findings.
**Dependency scans use their own exclusions.** `buildDepScanOptions` (`internal/engine/dependency_scanner.go`) replaces the primary scan's skip patterns with `skip.OnlyDefaultTestPatterns`, which keeps only the built-in test patterns (`DefaultSkippedTestPatterns` in `internal/skip/source_defaults.go`: `test/`, `tests/`, `src/test/`, `src/tests/`, `__tests__/`, `**/*Test.java`, `**/*Tests.java`, `**/*_test.go`, `**/test_*.py`). For npm it adds an anchor on the dependency's own `node_modules/`, because the dependency directory usually sits inside a `node_modules` tree itself and each child package is scanned under its own identity. It also sets `IncludeGitIgnored`, which passes the OpenGrep flag that disables `.gitignore` handling. As a result, none of these apply inside a dependency root:
* `--exclude` and `settings.skip.patterns.scanning` from `scanoss.json`. `--exclude` is not a way to hide dependency findings.
* The built-in skipped directories (`dist`, `build`, `vendor`, `target`, `docs`, `shaded`, ...) and the generated-stub globs (`*.pb.go`, `*_pb2.py`, ...). `--no-default-exclusions` has nothing to remove here.
* `.gitignore` files.
`--include-tests` leaves the test patterns out of the list, so a dependency scan then reads its test files too. The reason for the narrow list: a published package is not a checkout, and often ships its only code in `dist/` or `build/`, which the source-tree defaults would hide. Go dependencies narrow the scan further to the Go files each imported package compiles for the host build (see above), which never include `_test.go` files.
Dependencies without a usable local source directory are **not** sent to the scanner. They are logged as `Skipping dependency source scan: no local source directory` instead of triggering empty-path scanner failures. For Java, those dependencies still proceed to step 4 as **type-only** inputs as long as `module@version` can be resolved to a compiled JAR.
### Step 4: Build the call graph
The call graph builder uses **syntactic parsing** to process source files. It works on raw source, without a full Go toolchain, Java compiler, Python interpreter, or Rust toolchain. The [`Parser` interface](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/builder.go) abstracts all language-specific behavior:
```go theme={null}
type Parser interface {
ParseDirectory(dir string, packagePath string) ([]*FileAnalysis, error)
SubPackagePath(parentPath, dirName string) string
PackageSeparator() string // "/" for Go, "." for Java/Python, "::" for Rust
}
```
The [`NewParserForEcosystem()`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/parser_registry.go) factory selects the right parser (`GoParser`, `JavaParser`, `PythonParser`, or `RustParser`) based on the detected ecosystem.
```mermaid theme={null}
flowchart TB
subgraph "Graph Packages (full source parsing)"
U["User Code Dir + import path"]
D1["Dep with findings golang.org/x/crypto"]
D2["Dep with findings org.bouncycastle:bcprov-jdk18on"]
end
subgraph "Type-Only Packages (bytecode index only)"
T1["Dep without findings org.springframework:spring-core"]
T2["Dep without findings com.fasterxml.jackson:jackson-core"]
end
subgraph "Builder.BuildFromDirectories()"
direction TB
Parse["Phase 1: Parser.ParseDirectory() syntactic parsing per source file"]
FnMap["Functions map FunctionID → FunctionDecl"]
RevIdx["Callers reverse index callee → []callerID"]
TypeRes["Phase 2: Bytecode type index class names + method signatures + return types"]
Fluent["Fluent chain resolution propagate return types"]
Parse --> FnMap
FnMap --> RevIdx
RevIdx --> TypeRes
TypeRes --> Fluent
end
U --> Parse
D1 --> Parse
D2 --> Parse
T1 --> TypeRes
T2 --> TypeRes
Fluent --> CG["CallGraph"]
```
> **Performance note:** only dependencies with crypto findings get full source parsing.
> Dependencies without findings contribute only their bytecode type signatures (class names,
> method signatures, return types, interface hierarchy). This preserves 100% type resolution
> accuracy for fluent chains while skipping expensive source parsing for \~80% of dependencies.
A dependency's `Files` becomes the `IncludeFiles` of its `PackageDir`. For Go, the builder parses only the files the host build compiles in the imported packages (the Go parser skips an unlisted file before reading it, so modernc.org/libc's seven other `ccgo_linux_.go` files cost nothing) and still walks through their parent directories so every package keeps the import path a whole-module walk gives it. This loses no reachability. A Go function can call only functions of packages its own package imports, which are in the closure too, and an interface value can only hold a type of a package the program contains, so the module's other packages hold no call edge and no dispatch target of the program. Likewise a file that build constraints exclude is not part of the program the host builds.
For a Python namespace distribution, the builder parses only its own files of the shared namespace directory, under the import path a whole walk gives them (`google.auth._helpers`), and the Python dependency type resolver indexes only them. Each function of a namespace directory is parsed once and belongs to the distribution that installed it, so containing-function lookups, synthesized entry-point findings and the call-graph export name that distribution. A distribution rooted at `site-packages` parses under the empty import path, so each package and module keeps its own name (`configobj`, `validate.check`, `six`), and both the builder and the type resolver walk only the directories that hold its files.
#### What the parser extracts
Each language parser extracts the same semantic information into the shared `FileAnalysis` / `FunctionDecl` structures. For Go (`.go` files, excluding `_test.go`):
| Extracted | Example | Stored As |
| - | - | - |
| Package imports | `import "crypto/aes"` | `Imports["aes"] = "crypto/aes"` |
| Function declarations | `func Encrypt(...)` | `FunctionDecl{ID, FilePath, StartLine, EndLine, Calls}` |
| Method declarations | `func (b *Block) Seal(...)` | Same, with `Type = "*Block"` |
| Call expressions | `aes.NewCipher(key)` | `FunctionCall{Callee: {Package: "crypto/aes", Name: "NewCipher"}}` |
For Java (`.java` files):
| Extracted | Example | Stored As |
| - | - | - |
| Package declaration | `package javax.crypto;` | `PackagePath = "javax.crypto"` |
| Imports (explicit) | `import javax.crypto.Cipher;` | `Imports["Cipher"] = "javax.crypto"` |
| Imports (wildcard) | `import java.security.*;` | `WildcardImports = ["java.security"]` |
| Class methods | `class Cipher { getInstance(...) }` | `FunctionDecl{..., Type: "Cipher", Name: "getInstance"}` |
| Constructors | `new SecretKeySpec(...)` | Call to `FunctionID{..., Type: "SecretKeySpec", Name: ""}` |
| Method invocations | `Cipher.getInstance("AES")` | Resolved via imports → `FunctionID{Package: "javax.crypto", Type: "Cipher", Name: "getInstance"}` |
| Local variable types | `Cipher cipher = Cipher.getInstance(...)` | Enables `cipher.doFinal()` → resolves `cipher` to `Cipher` type |
For Python (`.py` files, excluding `test_*.py` and `*_test.py`):
| Extracted | Example | Stored As |
| - | - | - |
| Import statements | `import hashlib` | `Imports["hashlib"] = "hashlib"` |
| From imports | `from cryptography.hazmat.primitives import Cipher` | `Imports["Cipher"] = "cryptography.hazmat.primitives"` |
| Wildcard imports | `from hashlib import *` | `WildcardImports = ["hashlib"]` |
| Aliased imports | `import hashlib as hl` | `Imports["hl"] = "hashlib"` |
| Function definitions | `def encrypt(key, data):` | `FunctionDecl{ID, FilePath, StartLine, EndLine, Calls}` |
| Class methods | `class Cipher: def __init__(self):` | `FunctionDecl{..., Type: "Cipher", Name: ""}` |
| Attribute calls | `hashlib.sha256()` | Resolved via imports → `FunctionID{Package: "hashlib", Name: "sha256"}` |
| Chained calls | `cryptography.hazmat.primitives.hashes.SHA256()` | Resolved via first segment import |
| `self` calls | `self.encrypt()` | `FunctionID{Package: current_package, Name: "encrypt"}` |
For Rust (`.rs` files, excluding `*_test.rs` and `tests.rs`):
| Extracted | Example | Stored As |
| - | - | - |
| Use declarations | `use ring::aead::Aead;` | `Imports["Aead"] = "ring::aead"` |
| Scoped use lists | `use ring::aead::{Aead, AeadCore};` | `Imports["Aead"] = "ring::aead"`, `Imports["AeadCore"] = "ring::aead"` |
| Wildcard use | `use ring::aead::*;` | `WildcardImports = ["ring::aead"]` |
| Free functions | `fn encrypt(key: &[u8]) {...}` | `FunctionDecl{ID, FilePath, StartLine, EndLine, Calls}` |
| Impl methods | `impl Aead { fn new(...) {...} }` | `FunctionDecl{..., Type: "Aead", Name: "new"}` |
| Scoped calls | `Aead::new(...)` | Resolved via imports → `FunctionID{Package: "ring::aead", Type: "Aead", Name: "new"}` |
| Field calls | `self.encrypt(...)` | `FunctionID{Package: current_module, Name: "encrypt"}` |
#### The two data structures
The `CallGraph` holds two maps:
```
Functions: "crypto/aes.NewCipher" → FunctionDecl{...}
"example.com/app.Encrypt" → FunctionDecl{Calls: [...]}
"example.com/app.main" → FunctionDecl{Calls: [...]}
Callers: "crypto/aes.NewCipher" → ["example.com/app.Encrypt"]
"example.com/app.Encrypt" → ["example.com/app.main"]
```
* **`Functions`** maps `FunctionID.String()` → `*FunctionDecl` (forward: function → its outgoing calls)
* **`Callers`** maps callee → `[]callerID` (reverse: who calls this function?)
The reverse index makes **backward tracing** possible: starting from a crypto finding and walking up to user code.
#### Edge resolution kinds
While building the caller index, the builder records **how** each edge was
resolved in `CallGraph.EdgeResolutions`: `exact` (receiver type known, unique
target or overload set on that type), `interface_dispatch` (an interface/abstract
method expanded to concrete implementations by name+arity within a namespace
root), or `name_only` (a fluent-fallback guess with no receiver-type anchor).
This classification is emitted on the graph-fragment export (`graph-fragment-1.3`,
see [Output formats](/en/latest/cli/crypto-finder/output-formats#graph-fragment-export)) so downstream
consumers can fail closed on over-broad dispatch instead of reporting it as
typed reachability. The dispatch heuristics below (interface expansion, fluent
fallback) are exactly the edges tagged non-`exact`. The same fragment also
exposes `crypto_entry_points[]` and `supporting_calls[]` so consumers can use a
stable API index rather than the removed `entry_point_index` projection.
#### Type resolution
After building the caller index, the builder runs additional resolution passes to improve type accuracy:
1. **`TypeResolver`** (language-specific): For Java, a bytecode-based resolver reads `.class` files from resolver-supplied dependency JARs (Maven or Gradle) plus the selected JDK platform archives to extract fully-qualified method signatures. JAR indexing runs in parallel and uses a per-artifact bytecode cache under `~/.scanoss/crypto-finder/cache/bytecode/`, keyed by exact artifact identity. This provides accurate parameter types (e.g., `io.jsonwebtoken.SignatureAlgorithm` instead of generic `K`) and return types for fluent chain resolution. The Java resolver can be configured per scan with `java_jdk_major` (`auto`, `8`, `11`, `17`, `21`) and `java_jdk_homes`, which also makes Java dependency resolution JDK-aware. The `TypeResolver` interface is extensible, so each language can implement its own approach, such as `go/types` for Go, `.pyi` stubs for Python, or `rust-analyzer` for Rust.
2. **Fluent chain resolution**: For chained calls like `Jwts.builder().setId(id).signWith(algo, key)`, return types are propagated through the chain. If `builder()` returns `JwtBuilder`, then `setId()` is resolved as `JwtBuilder.setId`. Interface inheritance is also followed (e.g., `JwtBuilder` extends `ClaimsMutator`, so `setId` resolves to `ClaimsMutator.setId`).
3. **Argument source tracing**: For each function call, the parser traces where argument values come from: literal constants, local variables, class fields, method parameters, or call results. This produces recursive `source_nodes` showing the data flow into each argument.
### Step 5: Trace and attribute findings
Attribution works like this for each crypto finding in a dependency:
```mermaid theme={null}
flowchart TB
Finding["Crypto Finding file: chacha20.go, line: 42"] --> Contain["FindContainingFunction() Which function spans line 42?"]
Contain --> FnID["FunctionID chacha20poly1305.New"]
FnID --> Trace["TraceBack(target, userPackages)"]
subgraph "BFS Backward Trace"
direction TB
Start["Start: chacha20poly1305.New"]
Mid["Caller: mypkg.SecureEncrypt"]
End["Caller: main.main ✓ user package reached!"]
Start -.->|"Callers[] lookup"| Mid
Mid -.->|"Callers[] lookup"| End
end
Trace --> BFS
BFS --> Chain["CallChain [main.main → mypkg.SecureEncrypt → chacha20poly1305.New]"]
```
#### How `TraceBack` works (BFS)
1. **Start** with the target function (where the crypto finding was detected)
2. **Look up** all callers via the reverse index (`graph.Callers[targetKey]`)
3. **Prepend** each caller to the chain being built (so the chain grows backward: `[caller, ...existing]`)
4. **Terminate** when a root function is reached (a function with no callers, e.g., `main`)
5. **Validate** that the complete chain passes through at least one user-package function
6. **Cycle detection** prevents infinite loops in recursive call graphs
7. **Return** all complete chains (BFS finds all paths from entry points to the crypto call site)
The result is an ordered array, **`[program_entry_point, ..., intermediate, ..., crypto_call_site]`**, where position `[i]` calls position `[i+1]`.
#### Attribution output
After tracing, each finding gets structured attribution metadata in the interim report, and the detailed reachability slices are emitted by the separate call graph export.
A dependency finding:
```json theme={null}
{
"source": "dependency",
"finding_id": "a1b2c3d4",
"dependency_info": {
"module": "golang.org/x/crypto",
"version": "v0.17.0"
}
}
```
Dependency finding `file_path` values are relative to the dependency root. The artifact identity stays in `dependency_info`, so consumers do not need to parse `module@version` back out of the path string.
A user code finding:
```json theme={null}
{
"source": "direct",
"finding_id": "e5f6a7b8"
}
```
A call graph export slice:
```json theme={null}
{
"finding_id": "a1b2c3d4",
"call_chains": [
[
{ "function_name": "main", "file_path": "main.go", "start_line": 15 },
{
"function_name": "example.com/app/mypkg.SecureEncrypt",
"file_path": "mypkg/crypto.go",
"start_line": 8
},
{
"function_name": "golang.org/x/crypto/chacha20poly1305.New",
"file_path": "chacha20.go",
"start_line": 42,
"dependency_info": {
"module": "golang.org/x/crypto",
"version": "v0.17.0"
}
}
]
]
}
```
### Step 6: Merge reports
```mermaid theme={null}
flowchart TB
UR["User Report source: direct"]
DR1["Dep Report 1"]
DR2["Dep Report 2"]
UR --> Merged["Merged Report"]
DR1 --> Merged
DR2 --> Merged
```
Crypto-finder merges dependency findings into the interim report and leaves reachability slicing to the call graph export. A finding appears in the interim report whether or not it later produces an exported call chain.
User code findings are always included and marked with `source: "direct"`.
***
## Practical walkthrough
This section traces the entire pipeline using the test project at `testdata/projects/go_with_crypto_dep/`. Every value shown is real output from running `crypto-finder scan --scan-dependencies` against this project.
### The source code
Three files make up the project:
**`go.mod`** declares one direct dependency:
```
module example.com/crypto-test
go 1.21
require golang.org/x/crypto v0.31.0
require golang.org/x/sys v0.28.0 // indirect
```
**`main.go`** is the entry point. It does **not** use crypto directly:
```go theme={null}
package main
import "example.com/crypto-test/mypkg"
func main() {
key := make([]byte, 32)
message := []byte("Hello, crypto-finder dependency scanning!")
encrypted, err := mypkg.SecureEncrypt(key, message) // line 14
decrypted, err := mypkg.SecureDecrypt(key, encrypted) // line 19
}
```
**`mypkg/crypto.go`** is the user's wrapper package. It uses crypto from a dependency:
```go theme={null}
package mypkg
import (
"crypto/rand"
"golang.org/x/crypto/chacha20poly1305"
)
func SecureEncrypt(key []byte, plaintext []byte) ([]byte, error) { // line 12
aead, err := chacha20poly1305.New(key) // line 13 — crypto!
nonce := make([]byte, aead.NonceSize())
rand.Read(nonce) // line 19 — crypto!
ciphertext := aead.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
func SecureDecrypt(key []byte, ciphertext []byte) ([]byte, error) { // line 28
aead, err := chacha20poly1305.New(key) // line 29 — crypto!
// ...
}
```
### Step 1: Scan user code (the normal scan)
The orchestrator runs Semgrep/OpenGrep rules against user code. It finds **3 assets** in `mypkg/crypto.go`:
| Line | Match | Rule |
| - | - | - |
| 13 | `chacha20poly1305.New(key)` | `go.xcrypto.chacha20poly1305.aead` |
| 19 | `rand.Read(nonce)` | `go.crypto.rand.usage` |
| 29 | `chacha20poly1305.New(key)` | `go.xcrypto.chacha20poly1305.aead` |
This produces the **user report**. At this point there are no `source`, `call_chain`, or `dependency_info` fields, only raw findings.
### Step 2: Resolve dependencies
The Go resolver lists the main module with `go list -m -json`, then runs `go list -e -deps` over the main module's packages and keeps every module that provides a package in that non-test import closure. Build constraints are evaluated for the scanning host: on amd64, `chacha20poly1305` imports `golang.org/x/sys/cpu`, so both modules below are in the closure, while on arm64 only `golang.org/x/crypto` is. On amd64 it returns:
```
RootModule: "example.com/crypto-test"
Dependencies:
Module Version Dir
golang.org/x/crypto v0.31.0 ~/go/pkg/mod/golang.org/x/crypto@v0.31.0
golang.org/x/sys v0.28.0 ~/go/pkg/mod/golang.org/x/sys@v0.28.0
```
The `RootModule` (`example.com/crypto-test`) is the Go user-code prefix. Any package whose import path starts with it is **user code**. Java additionally treats packages declared in project source files as user code, even when `RootModule` is a Gradle project name rather than a package prefix. Everything else is a dependency.
### Step 3: Scan dependencies in parallel
Each dependency gets scanned with the same rules, limited to Go rules only, and only in the packages the program imports: `chacha20poly1305`, `chacha20`, `internal/alias` and `internal/poly1305` of `golang.org/x/crypto`, and `cpu` of `golang.org/x/sys`. Measured on 2026-10-02 with the current rule set:
| Dependency | Crypto Assets Found | Why |
| - | -: | - |
| `golang.org/x/crypto` | 4 | `chacha20poly1305/chacha20poly1305_generic.go` and `chacha20poly1305/xchacha20poly1305.go`. A whole-module scan reports 378 in 58 files, all in packages this program does not contain |
| `golang.org/x/sys` | 0 | `cpu` holds no crypto operation |
Total: **4 dependency findings**. Only `golang.org/x/crypto` has findings, so it proceeds to step 4 together with the user code.
### Step 4: Build the call graph
The builder receives the user code and the crypto-bearing dependency, limited to its imported packages:
```
PackageDirs = [
{Dir: ".../go_with_crypto_dep", ImportPath: "example.com/crypto-test"},
{Dir: ".../golang.org/x/crypto@v0.31.0", ImportPath: "golang.org/x/crypto",
IncludeFiles: [".../chacha20/chacha_generic.go", ..., ".../chacha20poly1305/chacha20poly1305.go", ..., ".../internal/poly1305/sum_generic.go", ...]},
]
```
The syntactic parser processes every `.go` file and extracts function declarations with their calls.
**From `main.go`:**
```
FunctionDecl: example.com/crypto-test.main
File: main.go, Lines: 10–26
Calls:
→ example.com/crypto-test/mypkg.SecureEncrypt (line 14)
→ example.com/crypto-test/mypkg.SecureDecrypt (line 19)
```
**From `mypkg/crypto.go`:**
```
FunctionDecl: example.com/crypto-test/mypkg.SecureEncrypt
File: mypkg/crypto.go, Lines: 12–25
Calls:
→ golang.org/x/crypto/chacha20poly1305.New (line 13)
→ crypto/rand.Read (line 19)
FunctionDecl: example.com/crypto-test/mypkg.SecureDecrypt
File: mypkg/crypto.go, Lines: 28–46
Calls:
→ golang.org/x/crypto/chacha20poly1305.New (line 29)
```
**From `golang.org/x/crypto/...`:** hundreds more function declarations.
Then `buildCallerIndex()` creates the **reverse index** (callee → who calls it):
```
Callers["example.com/crypto-test/mypkg.SecureEncrypt"]
= ["example.com/crypto-test.main"]
Callers["example.com/crypto-test/mypkg.SecureDecrypt"]
= ["example.com/crypto-test.main"]
Callers["golang.org/x/crypto/chacha20poly1305.New"]
= ["example.com/crypto-test/mypkg.SecureEncrypt",
"example.com/crypto-test/mypkg.SecureDecrypt"]
```
Backward tracing depends on this reverse index.
### Step 5: Trace and attribute
The system now traces **each finding** back through the call graph to user code. Three different scenarios play out:
#### Scenario A: user finding at `chacha20poly1305.New`, line 13
This finding is in `mypkg/crypto.go` (user code). The enrichment flow:
**5a-1. Find the containing function.**
`FindContainingFunction("mypkg/crypto.go", 13)` iterates all `FunctionDecl`s, looking for one whose `FilePath` matches and whose `StartLine..EndLine` spans line 13. It finds **`SecureEncrypt`** (lines 12 to 25).
**5a-2. Trace back to the entry point.**
`TraceBack(SecureEncrypt, userPackages={"example.com/crypto-test"}, maxDepth=0)`:
```
BFS queue: [ [SecureEncrypt] ]
Iteration 1:
chain = [SecureEncrypt]
head = SecureEncrypt
Look up Callers["...mypkg.SecureEncrypt"] → ["...main"]
Prepend caller: chain becomes [main, SecureEncrypt]
→ push to queue
Iteration 2:
chain = [main, SecureEncrypt]
head = main
Look up Callers["...main"] → [] (no callers — root function)
Chain reaches user code? YES (both functions are user code)
Chain length > 1? YES
→ CHAIN COMPLETE! Add to results.
```
**5a-3. Export the slice.**
```json theme={null}
"source": "direct",
"finding_id": "a1b2c3d4",
"call_chains": [
[
{"function_name": "main", "namespace": "example.com/crypto-test", "file_path": "main.go", "line": 14},
{"function_name": "SecureEncrypt", "namespace": "example.com/crypto-test/mypkg", "file_path": "mypkg/crypto.go", "line": 12}
]
]
```
Note: `main`'s line is **14** (the line where `main` *calls* `SecureEncrypt`), not line 10 where `main` is declared. This is because `findCallLine()` searches `main`'s `Calls` list for the specific call to `SecureEncrypt` and returns that call-site line number.
#### Scenario B: user finding at `chacha20poly1305.New`, line 29
Same logic, but traces through `SecureDecrypt`:
```
FindContainingFunction("mypkg/crypto.go", 29) → SecureDecrypt (lines 28–46)
TraceBack(SecureDecrypt):
[SecureDecrypt]
→ prepend caller → [main, SecureDecrypt]
→ main has no callers (root function), chain reaches user code → CHAIN COMPLETE
```
```json theme={null}
"source": "direct",
"finding_id": "b2c3d4e5",
"call_chains": [
[
{"function_name": "main", "namespace": "example.com/crypto-test", "file_path": "main.go", "line": 19},
{"function_name": "SecureDecrypt", "namespace": "example.com/crypto-test/mypkg", "file_path": "mypkg/crypto.go", "line": 28}
]
]
```
Note: `main`'s line is now **19**, the line where `main` calls `SecureDecrypt`, not 14.
#### Scenario C: unreachable dependency finding in deep `x/crypto` internal functions
Take any internal function in `golang.org/x/crypto`, say `ssh.newAESCTR`:
```
FindContainingFunction(".../ssh/cipher.go", N) → ssh.newAESCTR
TraceBack(ssh.newAESCTR):
[ssh.newAESCTR]
→ Callers: maybe some internal SSH functions
→ Keep walking back to root functions...
→ Root functions found, but no chain passes through "example.com/crypto-test"
→ All chains discarded (no user code reached).
```
Result: `call_chains` is **empty**.
The finding may still be in the interim report, but it adds no useful reachability slice to the call graph export.
So the useful downstream signal stays narrow even when a dependency uses a lot of crypto internally. The vast majority of `golang.org/x/crypto`'s internal crypto usage is not reachable from the user's `main()`.
### Step 6: Merge
```
User report (findings metadata)
+ Dependency reports (findings metadata)
─────────────────────────────────────
= Merged interim report
```
In this test project, the useful reachability slices all start from user code findings, because the user code calls `chacha20poly1305.New` **directly** inside `mypkg/crypto.go`, a file in the user's own module. The crypto usage is already captured as `source: "direct"`. There's no intermediate dependency wrapper that the user calls which *then* reaches crypto.
With a longer chain, such as `main → mypkg.Encrypt → someMiddleware.Process → chacha20poly1305.New` where `someMiddleware` is a dependency, a dependency-backed reachability slice would appear in the call graph export.
### Actual interim report output
The final interim report looks like this:
```json theme={null}
{
"version": "1.5",
"tool": { "name": "crypto-finder", "version": "dev" },
"findings": [
{
"file_path": "mypkg/crypto.go",
"language": "go",
"cryptographic_assets": [
{
"start_line": 13,
"end_line": 13,
"match": "aead, err := chacha20poly1305.New(key)",
"rules": [
{
"id": "go.xcrypto.chacha20poly1305.aead",
"message": "Detected ChaCha20-Poly1305 AEAD usage",
"severity": "INFO"
}
],
"status": "pending",
"metadata": {
"algorithmFamily": "ChaCha20",
"assetType": "algorithm",
"...": "..."
},
"source": "direct",
"finding_id": "a1b2c3d4"
},
{
"start_line": 19,
"end_line": 19,
"match": "if _, err := rand.Read(nonce); err != nil {",
"rules": [
{
"id": "go.crypto.rand.usage",
"message": "Detected cryptographically secure random number generation",
"severity": "INFO"
}
],
"status": "pending",
"metadata": {
"algorithmFamily": "CSPRNG",
"assetType": "algorithm",
"...": "..."
},
"source": "direct",
"finding_id": "a1b2c3d4"
},
{
"start_line": 29,
"end_line": 29,
"match": "aead, err := chacha20poly1305.New(key)",
"rules": [
{
"id": "go.xcrypto.chacha20poly1305.aead",
"message": "Detected ChaCha20-Poly1305 AEAD usage",
"severity": "INFO"
}
],
"status": "pending",
"metadata": {
"algorithmFamily": "ChaCha20",
"assetType": "algorithm",
"...": "..."
},
"source": "direct",
"finding_id": "b2c3d4e5"
}
]
}
]
}
```
The corresponding reachability slices are emitted by the separate call graph export and joined through `finding_id`.
### Visual summary
```
main.go:14 ──calls──→ mypkg/crypto.go:13 ──calls──→ x/crypto/chacha20poly1305.New
(main) (SecureEncrypt) (dependency function)
│
├── Finding: chacha20poly1305.New at L13
│ source: "direct" (it's in user code)
│ exported slice: [[main@L14 → SecureEncrypt@L12]]
│
└── Finding: rand.Read at L19
source: "direct"
exported slice: [[main@L14 → SecureEncrypt@L12]]
main.go:19 ──calls──→ mypkg/crypto.go:29 ──calls──→ x/crypto/chacha20poly1305.New
(main) (SecureDecrypt)
│
└── Finding: chacha20poly1305.New at L29
source: "direct"
exported slice: [[main@L19 → SecureDecrypt@L28]]
golang.org/x/crypto/chacha20poly1305/*.go (inside the imported package)
│
└── 4 findings with NO call chain
source: "dependency"
→ no exported reachability slice
```
***
## Walkthrough 2: multi-hop dependency chain
In the first walkthrough, user code calls crypto directly, so the useful reachability slices start from direct findings. This second walkthrough uses `testdata/projects/go_with_dep_chain/` to show a **multi-hop chain**, where the crypto usage sits deep inside a dependency and the dependency-backed reachability slices are the useful output.
### The source code
Here, user code never touches `chacha20poly1305` directly. Instead, it calls through a wrapper dependency (`cryptowrapper_dep/`), which has an internal function layer.
**`main.go`** is the entry point and calls `mypkg`:
```go theme={null}
func main() {
encrypted, err := mypkg.SecureEncrypt(key, message) // line 14
decrypted, err := mypkg.SecureDecrypt(key, encrypted) // line 19
}
```
**`mypkg/crypto.go`** is user code that delegates to the dependency. It has **no crypto imports**.
```go theme={null}
import "example.com/cryptowrapper"
func SecureEncrypt(key, plaintext []byte) ([]byte, error) { // line 15
encrypted, err := cryptowrapper.Encrypt(key, plaintext) // line 16
return encrypted, nil
}
func SecureDecrypt(key, ciphertext []byte) ([]byte, error) { // line 24
decrypted, err := cryptowrapper.Decrypt(key, ciphertext) // line 25
return decrypted, nil
}
```
**`../cryptowrapper_dep/wrapper.go`** is the dependency, a separate module. It has a public API and an internal function:
```go theme={null}
// Public: called by user code
func Encrypt(key, plaintext []byte) ([]byte, error) { // line 19
aead, err := newAEAD(key) // line 20 — calls internal fn
nonce := make([]byte, aead.NonceSize())
rand.Read(nonce) // line 26 — crypto!
return aead.Seal(nonce, nonce, plaintext, nil), nil
}
func Decrypt(key, ciphertext []byte) ([]byte, error) { // line 36
aead, err := newAEAD(key) // line 37 — calls internal fn
// ...
}
// Internal: adds depth to the call chain
func newAEAD(key []byte) (cipher.AEAD, error) { // line 58
return chacha20poly1305.New(key) // line 59 — crypto!
}
```
The intended call chains:
```
main → mypkg.SecureEncrypt → cryptowrapper.Encrypt → cryptowrapper.newAEAD → chacha20poly1305.New
main → mypkg.SecureDecrypt → cryptowrapper.Decrypt → cryptowrapper.newAEAD → chacha20poly1305.New
```
### What the scan produces
Running with `--scan-dependencies`:
```
Finding groups: 1 (wrapper.go in the dependency)
Total assets: 2 (both from the dependency, both reachable)
```
Only a small subset of the traced findings produce meaningful exported slices. Deep `x/crypto` and `x/sys` internals may still be scanned and identified, but they do not help downstream stitching unless the graph can connect them back to component-owned or user-owned entry points.
### Tracing the 3-step chain
The finding at `chacha20poly1305.New` (line 59 of `wrapper.go`) produces a **3-step chain**. This is the BFS trace:
```
FindContainingFunction("wrapper.go", 59) → cryptowrapper.newAEAD (lines 58–59)
TraceBack(newAEAD, userPackages={"example.com/dep-chain-test"}):
BFS queue: [ [newAEAD] ]
Iteration 1:
chain = [newAEAD]
head = newAEAD
Is "example.com/cryptowrapper" a user package?
→ Does it start with "example.com/dep-chain-test/"? NO
Look up Callers["example.com/cryptowrapper.newAEAD"]
→ ["example.com/cryptowrapper.Encrypt", "example.com/cryptowrapper.Decrypt"]
Prepend each: push [Encrypt, newAEAD] and [Decrypt, newAEAD] to queue
Iteration 2a:
chain = [Encrypt, newAEAD]
head = Encrypt
Is "example.com/cryptowrapper" a user package? NO
Look up Callers["example.com/cryptowrapper.Encrypt"]
→ ["example.com/dep-chain-test/mypkg.SecureEncrypt"]
Prepend: push [SecureEncrypt, Encrypt, newAEAD]
Iteration 2b:
chain = [Decrypt, newAEAD]
head = Decrypt
Is "example.com/cryptowrapper" a user package? NO
Look up Callers["example.com/cryptowrapper.Decrypt"]
→ ["example.com/dep-chain-test/mypkg.SecureDecrypt"]
Prepend: push [SecureDecrypt, Decrypt, newAEAD]
Iteration 3a:
chain = [SecureEncrypt, Encrypt, newAEAD]
head = SecureEncrypt
Look up Callers["...mypkg.SecureEncrypt"] → ["...main"]
Prepend: push [main, SecureEncrypt, Encrypt, newAEAD]
Iteration 3b:
chain = [SecureDecrypt, Decrypt, newAEAD]
head = SecureDecrypt
Look up Callers["...mypkg.SecureDecrypt"] → ["...main"]
Prepend: push [main, SecureDecrypt, Decrypt, newAEAD]
Iteration 4a:
chain = [main, SecureEncrypt, Encrypt, newAEAD]
head = main
Look up Callers["...main"] → [] (no callers — root function)
Chain reaches user code? YES
→ CHAIN COMPLETE! (4 steps)
Iteration 4b:
chain = [main, SecureDecrypt, Decrypt, newAEAD]
head = main
→ Also a complete chain! (4 steps)
```
BFS found **two** complete chains. Both are stored in `call_chains`:
```json theme={null}
{
"source": "dependency",
"dependency_info": {
"module": "example.com/cryptowrapper",
"version": "v0.0.0"
},
"finding_id": "c3d4e5f6",
"call_chains": [
[
{
"function_name": "main",
"namespace": "example.com/dep-chain-test",
"file_path": "main.go",
"line": 14
},
{
"function_name": "SecureEncrypt",
"namespace": "example.com/dep-chain-test/mypkg",
"file_path": "mypkg/crypto.go",
"line": 16
},
{
"function_name": "Encrypt",
"namespace": "example.com/cryptowrapper",
"file_path": "wrapper.go",
"line": 20
},
{
"function_name": "newAEAD",
"namespace": "example.com/cryptowrapper",
"file_path": "wrapper.go",
"line": 58
}
],
[
{
"function_name": "main",
"namespace": "example.com/dep-chain-test",
"file_path": "main.go",
"line": 19
},
{
"function_name": "SecureDecrypt",
"namespace": "example.com/dep-chain-test/mypkg",
"file_path": "mypkg/crypto.go",
"line": 25
},
{
"function_name": "Decrypt",
"namespace": "example.com/cryptowrapper",
"file_path": "wrapper.go",
"line": 37
},
{
"function_name": "newAEAD",
"namespace": "example.com/cryptowrapper",
"file_path": "wrapper.go",
"line": 58
}
]
]
}
```
### Full trace to `main`
The BFS walks all the way to **root functions** (functions with no callers, like `main`). This means the full chain `main → SecureEncrypt → Encrypt → newAEAD` is preserved. A chain is valid if it passes through at least one user-package function, so chains that only traverse dependency code are discarded.
### Visual summary
```
main.go:14 → mypkg/crypto.go:16 → wrapper.go:20 → wrapper.go:59
(main) (SecureEncrypt) (Encrypt) (newAEAD)
│ │ │ │
│ user code │ user code │ dependency │ dependency
│ │ │ │
└── chain starts here (root fn) │ └── finding: chacha20poly1305.New
│ source: "dependency"
└── intermediate hop in chain
main.go:19 → mypkg/crypto.go:25 → wrapper.go:37 → wrapper.go:59
(main) (SecureDecrypt) (Decrypt) (newAEAD)
│ │ │ │
└── chain starts here (root fn) │ └── same finding, alternate path
└── intermediate hop
```
What this shows:
* User code has **0** crypto findings, because `mypkg` has no crypto imports.
* **2** dependency findings survive reachability, because the call graph proves that user code reaches them.
* **492** dependency findings are dropped, because they are deep `x/crypto` internals that user code can't reach.
* `main` is at the head of each chain, because BFS walks to root functions, which have no callers.
* Both the Encrypt and Decrypt paths are kept, because every chain is stored in `call_chains`.
***
## Interim report contract (v1.7)
Version 1.7 keeps the attribution fields needed to join findings to the separate reachability export and adds an optional AST-anchored structural identity when callgraph evidence is available. Dependency-backed paths are dependency-root-relative; `dependency_info` remains the canonical place for dependency module, version, and package URL. Direct findings may additionally expose a valid rule package URL at the asset-level `purl`; it is version-enriched only when one unambiguous direct dependency match exists.
| Field | Type | When Present | Description |
| - | - | - | - |
| `source` | `string` | Always (when dependency scanning) | `"direct"` or `"dependency"` |
| `dependency_info` | `object` | Dependency findings only | `{module, version, purl?}` |
| `purl` | `string` | Direct findings with valid rule metadata | Canonical package identity, optionally enriched from the direct dependency graph |
| `finding_id` | `string` | Always (when dependency scanning) | Short hash (SHA-256) for cross-referencing with the callgraph export |
| `occurrence_key` | `string` | When a terminal AST anchor is available | Rule-independent `v1:<16 lowercase hex>` structural identity for the canonical finding |
| `conditioned_value` | `string` | Per-value assets only | The resolved condition the asset was specialized for; part of its `finding_id` and `occurrence_key` (v1.7+) |
| `terminal_start_col`, `terminal_end_col` | `integer` | Assets matched on an argument span | Columns of the enclosing call, used to locate the crypto call (v1.7+) |
## Call graph export
When `--export-callgraph` is enabled, Crypto Finder emits a finding-centric JSON export that uses the same relative-path convention as the main report.
Schema note: call graph export version `6.12` is current and carries optional direct finding `purl` values at the finding level, optional `occurrence_key` structural identities, canonical dependency package URLs inside `dependency_info.purl`, and contract-scoped `supporting_calls[].supporting_call.resolved_key_length` evidence for structurally derived Java key-generation configuration calls, including the `rule_declared_bits` value and `rule_conflict` marker when a rule-declared key length disagrees with the resolved one. Java runtime provenance remains available in `scan_metadata` for JDK-aware platform signature enrichment.
* Each top-level record stays keyed by `finding_id`, which is the join key back to the interim report. `occurrence_key`, when present, is a separate structural identity and does not replace that join.
* `call_chains` is the primary value-flow structure. Each chain is ordered from the first reachable caller to the function that contains the matched crypto call.
* Each chain node contains a fully qualified `function_name`, a normalized `file_path`, `start_line`, optional `dependency_info`, and optional `entry_call`.
* `entry_call` describes how execution entered the current function from the previous step. Its `file_path` and `line` are the call-site location in the previous node's source file.
* The last node in a chain carries `crypto_call`, which is the matched crypto-relevant call that triggered the finding.
* `entry_call.parameters[]` and `crypto_call.parameters[]` both export `parameter_index` (always `0`-based), best-effort `type`, `argument_expression`, `resolved_value`, `variable_name` for simple identifiers only, and recursive `source_nodes`.
* For Java scans, `scan_metadata` may also include `java_requested_jdk_major`, `java_runtime_version`, `java_platform_signatures_used`, `java_platform_signature_source`, and `java_platform_signature_unavailable_reason` to show which JDK major was requested and whether JDK platform signatures were available for enrichment.
* `source_nodes` can now carry interprocedural provenance across wrapper hops, for example `PARAMETER -> PARAMETER -> VALUE`, and propagated nested nodes keep `location.file_path` plus `location.line` when known.
* Method-call expressions are preserved as `CALL_RESULT` nodes instead of flattening away their receivers. When the invoked method can be resolved, the node also exports `call_target`, and receiver provenance stays nested under the `CALL_RESULT` (for example `CALL_RESULT -> PARAMETER alg -> VALUE SignatureAlgorithm.HS256`).
* Findings that cannot be resolved to a containing function or a specific crypto call remain in the export with `finding_location` and `unresolved_reason`.
### Call chains ordering
The `call_chains` field in the call graph export contains all traced paths from program entry points to the crypto call site. Each inner array is one complete path, ordered from **program entry point** (index 0) to **crypto call site** (last index). Entry `[i]` calls entry `[i+1]`.
Example:
```json theme={null}
{
"finding_id": "69669f02",
"call_chains": [
[
{
"function_name": "io.jsonwebtoken.jjwtfun.controller.SecretsController.traceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java",
"start_line": 33
},
{
"function_name": "io.jsonwebtoken.jjwtfun.service.SecretService.issueTraceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/service/SecretService.java",
"start_line": 72,
"entry_call": {
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java",
"line": 34,
"parameters": [
{
"parameter_index": 0,
"type": "io.jsonwebtoken.SignatureAlgorithm",
"argument_expression": "SignatureAlgorithm.HS256",
"resolved_value": "SignatureAlgorithm.HS256"
}
]
}
},
{
"function_name": "org.springframework.security.core.token.Sha512DigestUtils.getSha512Digest",
"file_path": "org/springframework/security/core/token/Sha512DigestUtils.java",
"start_line": 43,
"dependency_info": {
"module": "org.springframework.security:spring-security-core",
"version": "5.7.11"
},
"crypto_call": {
"function_name": "java.security.MessageDigest.getInstance",
"line": 45,
"parameters": [
{
"parameter_index": 0,
"type": "String",
"argument_expression": "\"SHA-512\"",
"resolved_value": "\"SHA-512\""
}
]
}
}
]
]
}
```
## Findings cache
Dependency scanning is dominated by opengrep execution time (\~93% of pipeline time). Because `module@version` produces identical scan results with the same ruleset, caching removes that repeated work. On a second scan with the same dependencies and rules, the dependency scanning phase drops from minutes to near-zero.
### How it works
The cache sits between Step 2 (rule loading) and Step 3 (parallel scanning) in the pipeline. Before scanning, `lookupDependency` checks each dependency for a cached result. The misses are scanned in batches (Step 3) and each dependency's report is stored under its own key, unless its scan stopped at a time or memory limit.
```mermaid theme={null}
flowchart LR
Dep["module@version"] --> Check{"Cache hit?"}
Check -->|Yes| Report["Cached InterimReport"]
Check -->|No| Scan["batched scanner process"]
Scan --> Store["Store in cache"]
Store --> Report
```
### Cache key design
The key captures everything that affects scan output:
```
@:
```
* **Module + version**: e.g., `org.bouncycastle:bcprov-jdk18on@1.78`
* **Rules hash**: the first 16 hex characters of a SHA-256 over the sorted rule file **contents**. If any rule is edited, the cache invalidates automatically
Example: `org.bouncycastle:bcprov-jdk18on@1.78:a3f8b2c1d4e5f678`
The `rulesHash` is computed once per scan (not per-dep), so I/O cost is negligible.
### Storage layout
The default implementation (`DiskFindingsCache`) stores results as JSON files:
```
~/.scanoss/crypto-finder/cache/findings/
org.bouncycastle:bcprov-jdk18on@1.78:a3f8b2c1d4e5f678.json
com.google.guava:guava@33.0.0-jre:a3f8b2c1d4e5f678.json
golang.org_x_crypto@v0.31.0:a3f8b2c1d4e5f678.json
```
Forward slashes in module paths (e.g., `golang.org/x/crypto`) are replaced with `_` for filesystem safety. Writes use temp file + atomic rename to prevent corruption from interrupted scans.
### `FindingsCache` interface
```go theme={null}
type FindingsCache interface {
Get(ctx context.Context, key string) (*entities.InterimReport, bool, error)
Put(ctx context.Context, key string, report *entities.InterimReport) error
}
```
The interface accepts `context.Context` on both methods to support network-backed implementations with timeouts and cancellation. The pipeline doesn't depend on which backend is behind the interface.
### Distributed backends
The `FindingsCache` interface is the extension point for multi-node scanning:
| Backend | Implementation | Use Case |
| - | - | - |
| **Disk** | `DiskFindingsCache` | Single-node, dev workflow |
| **Redis** | `RedisFindingsCache` | Multi-node cluster, shared LAN |
| **S3/GCS** | `S3FindingsCache` | Global fleet, persist across deploys |
| **Two-tier** | `TieredCache{L1: memory, L2: redis}` | Hot + warm layers |
Each one only implements `Get`/`Put`. The scanning pipeline works the same with any storage backend.
***
## Architecture map
```
internal/
├── cli/scan.go # CLI wiring: ecosystem detection, registry setup, pipeline invocation
├── dependency/
│ ├── resolver.go # Resolver interface + Dependency/ResolveResult types
│ ├── registry.go # Ecosystem → Resolver registry
│ ├── go_resolver.go # Go: production import closure via `go list -deps`
│ ├── java_resolver.go # Java: auto-detect Maven vs Gradle
│ ├── maven_resolver.go # Java/Maven: `mvn dependency:list/sources/tree`
│ ├── gradle_resolver.go # Java/Gradle: init-script export via `gradlew` / `gradle`
│ ├── pip_resolver.go # Python: `pip list` + `pip show`
│ ├── cargo_resolver.go # Rust: `cargo metadata --format-version=1`
│ └── source_cache.go # Shared: ZIP/JAR extraction to ~/.crypto-finder/cache/sources/
├── callgraph/
│ ├── types.go # FunctionID, FunctionDecl, FileAnalysis, CallGraph types
│ ├── builder.go # Parser interface + language-agnostic CallGraph construction
│ ├── parser_registry.go # Ecosystem → Parser factory (NewParserForEcosystem)
│ ├── go_parser.go # Go: syntactic parsing of Go source
│ ├── java_parser.go # Java: syntactic parsing of Java source
│ ├── bytecode_cache.go # Java: per-artifact bytecode index cache
│ ├── python_parser.go # Python: syntactic parsing of Python source
│ ├── rust_parser.go # Rust: syntactic parsing of Rust source
│ └── tracer.go # BFS backward tracer with configurable package separator
└── engine/
├── dependency_scanner.go # DependencyScanner: the 6-step pipeline (language-agnostic)
└── findings_cache.go # FindingsCache interface + DiskFindingsCache implementation
```
## Supported ecosystems
To add a new language, you implement two interfaces and register them. These ecosystems are supported today:
### Go
| Property | Value |
| - | - |
| Resolver | [`GoResolver`](https://github.com/scanoss/crypto-finder/blob/main/internal/dependency/go_resolver.go), which uses `go list -m -json` for the main modules and `go list -e -deps` for the modules in their production import closure and the Go files their imported packages compile for the host build (`GoFiles`, `CgoFiles`), the only ones scanned and parsed |
| Parser | [`GoParser`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/go_parser.go), which parses Go source syntactically |
| Manifest | `go.mod` |
| Module format | Go import path (e.g., `golang.org/x/crypto`) |
| Package separator | `/` |
| Source location | Go module cache (`$GOPATH/pkg/mod/`) |
### Java (Maven / Gradle)
| Property | Value |
| - | - |
| Resolver | [`JavaResolver`](https://github.com/scanoss/crypto-finder/blob/main/internal/dependency/java_resolver.go), which auto-detects Maven vs Gradle at the project root |
| Parser | [`JavaParser`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/java_parser.go), which parses Java source syntactically |
| Manifest | `pom.xml`, `build.gradle`, `build.gradle.kts`, `settings.gradle`, `settings.gradle.kts` |
| Module format | `groupId:artifactId` (e.g., `org.bouncycastle:bcprov-jdk18on`) |
| Package separator | `.` |
| Source location | Source JARs resolved by the active build tool and extracted to `~/.scanoss/crypto-finder/cache/sources/` |
#### Maven resolution details
The `MavenResolver` uses a **three-tier fallback strategy** to recover as many dependencies as it can, especially in multi-module projects:
Every Maven invocation receives `-Dmaven.repo.local=/.m2/repository`, so Maven writes artifacts to the same repository that Crypto Finder reads even when the JVM reports a different `user.home`. The operating-system user home is `HOME` on Unix and `USERPROFILE` on Windows.
**Tier 1: reactor with `--fail-never`** (always attempted):
* Runs `mvn dependency:list --fail-never -DappendOutput=true -DincludeScope=compile`
* The `--fail-never` flag continues past module failures; `-DappendOutput=true` accumulates results from all succeeding modules into a single output file
* If some modules resolve successfully, their dependencies are collected even if other modules fail
**Tier 2: per-module resolution** (if Tier 1 yields zero dependencies on a multi-module project):
* Detects modules from `` in the parent `pom.xml`
* Runs `mvn dependency:list -pl ` for each module independently
* Modules that fail are skipped; dependencies from succeeding modules are deduplicated and collected
**Tier 3: local install and retry** (if Tier 2 yields zero dependencies and inter-module failure is detected):
* Runs `mvn install -DskipTests --fail-never` to build all modules locally, populating `~/.m2/repository` with inter-module artifacts
* Retries Tier 1 after install
* This is expensive (requires compilation) but is the only way to resolve inter-module transitive dependencies
After dependency listing, the resolver also runs:
* **`mvn dependency:sources`** downloads `-sources.jar` files to `~/.m2/repository/` (best-effort; \~65% of Java libraries publish source JARs)
* **`mvn dependency:tree --fail-never -DappendOutput=true`** builds the dependency graph adjacency list (best-effort)
Dependencies without source JARs are included in the resolution results but without a source directory. They are skipped for source scanning and logged explicitly; if the compiled artifact is present in `~/.m2/repository`, Java bytecode indexing can still use it as a type-only dependency.
#### Gradle resolution details
The `GradleResolver` asks Gradle itself for a machine-readable dependency model via a temporary init script:
* Prefers `./gradlew` and falls back to `gradle` from `PATH`
* Uses the caller's `GRADLE_USER_HOME` when set; otherwise defaults it to `/.gradle` using the same operating-system user home
* Resolves the main Java compile classpath for single-project and multi-project builds
* Treats included Gradle subprojects as `WorkspaceMembers` rather than external dependencies
* Captures external module coordinates, versioned dependency edges, compiled JAR paths, and best-effort source archive paths
* Reuses the shared source extraction cache so Gradle and Maven dependencies flow through the same Java scanning pipeline
* Reachability still traces from application Java packages when the project has no `group` and `RootModule` is the Gradle project name
#### Multi-module project support
Crypto-finder detects multi-module Maven projects (a parent POM with ``) automatically. For these projects:
* All modules are registered as `WorkspaceMembers`, meaning they are treated as **user code** for call chain tracing (same as Cargo workspace members)
* The `WorkspaceMember.Name` follows the format `groupId:moduleDirName`
* The three-tier fallback strategy handles common multi-module failures:
* **Inter-module dependencies** (e.g., `eladmin-logging` depends on `eladmin-common`) are resolved through Tier 3.
* **HTTP mirror blocks** (Maven 3.8.1+ blocks insecure HTTP repositories) still yield partial results through Tier 1.
* **Missing parent POMs** or private repositories fall back to the partial results of Tier 1 or Tier 2.
#### Java call resolution
The `JavaParser` resolves method calls through import analysis:
1. **Explicit imports**: `import javax.crypto.Cipher;` → `Cipher.getInstance(...)` resolves to package `javax.crypto`
2. **Wildcard imports**: `import java.security.*;` → class names matched against wildcard packages
3. **Local variable types**: `Cipher c = Cipher.getInstance(...)` → `c.doFinal()` resolves `c` to type `Cipher` via local variable tracking
4. **Field types**: Class fields are tracked similarly to local variables
5. **Fallback**: Unresolved calls default to the current package (same as Go's behavior for unresolved variables)
### Python (pip)
| Property | Value |
| - | - |
| Resolver | [`PipResolver`](https://github.com/scanoss/crypto-finder/blob/main/internal/dependency/pip_resolver.go), which uses `python -m pip list --format=json` + `python -m pip show` to resolve packages with the same interpreter used for metadata lookups |
| Parser | [`PythonParser`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/python_parser.go), which parses Python source syntactically |
| Manifests | `pyproject.toml`, `requirements.txt`, `Pipfile`, `setup.py` |
| Module format | Python package name (e.g., `cryptography`) |
| Package separator | `.` |
| Source location | Site-packages directory (e.g., `~/.local/lib/python3.x/site-packages/`) |
#### Python resolution details
The `PipResolver` executes the following steps:
1. **Root module detection** reads `pyproject.toml` for `[project] name` or `[tool.poetry] name`; with neither the root module is empty and the project's own symbols are keyed at the scan root, never by the directory name
2. **`python -m pip list --format=json`** lists all installed packages with versions
3. **`python -m pip show `** gets location and dependency info for each package (batched in groups of 50)
4. **Distribution-to-import mapping** uses that SAME interpreter's `importlib.metadata.packages_distributions()` (Python 3.10+) to map distribution names to import names. Falls back to scanning `*.dist-info` directories (`top_level.txt` → `RECORD` file) for older Python versions
5. **Package root resolution** uses the import mapping, then heuristic name normalization, to find every top-level package directory and module file of the distribution (see "Python distributions scan every top-level package and module they installed"). C-extension packages are skipped
#### Python call resolution
The `PythonParser` resolves calls through import analysis:
1. **`import X`**: `X.func()` resolves `X` via imports
2. **`from X import Y`**: `Y()` resolves to package `X`, treated as constructor `Y.()`
3. **Chained attributes**: in `a.b.c.func()`, the first segment is resolved through imports and the rest is chained
4. **`self` calls**: `self.method()` resolves to the current package
5. **Wildcard imports**: `from X import *` recorded for fallback resolution
6. **Aliased imports**: in `import X as Y`, `Y` maps to `X`
7. **Fallback**: Unresolved calls default to the current package
### Rust (Cargo)
| Property | Value |
| - | - |
| Resolver | [`CargoResolver`](https://github.com/scanoss/crypto-finder/blob/main/internal/dependency/cargo_resolver.go), which uses `cargo metadata --format-version=1` |
| Parser | [`RustParser`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/rust_parser.go), which parses Rust source syntactically |
| Manifest | `Cargo.toml` |
| Module format | Crate name (e.g., `ring`) |
| Package separator | `::` |
| Source location | Cargo registry cache (e.g., `~/.cargo/registry/src/.../-/`) |
#### Rust resolution details
The `CargoResolver` runs `cargo metadata --format-version=1` which provides:
1. **All packages** with name, version, and manifest path
2. **Resolve graph** with dependency edges between packages
3. **Workspace detection**: packages with `source: null` (local/workspace crates) are treated as user code; all others are dependencies
Workspace members count as user code, so in a multi-crate workspace every workspace crate is "user code" for reachability analysis.
#### Rust call resolution
The `RustParser` resolves calls through `use` declaration analysis:
1. **Scoped identifiers**: in `Aead::new(...)`, `Aead` is resolved through `use` imports
2. **Qualified paths**: in `ring::aead::new(...)`, the first segment is resolved through imports
3. **Scoped use lists**: in `use ring::aead::{Aead, AeadCore}`, each item is registered separately
4. **Wildcard use**: `use ring::aead::*` recorded for fallback resolution
5. **`self` calls**: `self.method()` resolves to the current module
6. **`src/` transparency**: The `src/` directory is transparent in module paths (e.g., `ring/src/aead/` → `ring::aead`, not `ring::src::aead`)
7. **Impl blocks**: Methods in `impl Type { fn method() {} }` are extracted with their type association
8. **Fallback**: Unresolved calls default to the current module
### Adding a new language
To add support for a new ecosystem:
1. **Implement `callgraph.Parser`** with syntactic parsing for the target language.
2. **Implement `dependency.Resolver`**, which shells out to the ecosystem's package manager.
3. **Register the parser** in [`parser_registry.go`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/parser_registry.go) by adding one `case`.
4. **Register the resolver** in [`scan.go`](https://github.com/scanoss/crypto-finder/blob/main/internal/cli/scan.go) by adding one `depRegistry.Register()` call.
5. **Add manifest detection** in `detectEcosystem()` by adding one `if` that checks for the manifest file.
You don't need to change `builder.go`, `tracer.go`, `dependency_scanner.go`, the entities, or the schemas.
## Performance
### Two-phase call graph build
The call graph build is the most expensive step in the dependency scanning pipeline. To keep the cost down without losing any type resolution accuracy, the builder works in **two phases**:
**Phase 1, targeted source parsing.** Only dependencies with crypto findings + user code modules get full source parsing via `Parser.ParseDirectory()`. This builds `FunctionDecl` entries with call sites, parameters, and return types.
**Phase 2, bytecode type indexing for every dependency.** ALL dependencies (including those without findings) are indexed via `JavaBytecodeTypeResolver`. This reads `.class` files from Maven JARs to extract class names, method signatures, return types, and interface hierarchy. The type index is used to resolve fluent chains and enrich parameter types across dependency boundaries.
Both phases are needed because Java fluent APIs (e.g., `Jwts.builder().signWith(key)`) require knowing return types from one dependency to resolve calls in another. A dependency without crypto findings may define the return type that bridges a call chain from user code to a crypto finding. Skipping its type information would break backward tracing.
#### Benchmarks on eladmin: 160 deps, 27 with findings, 269 crypto assets
Current warm-run numbers with findings cache + Java bytecode cache enabled:
| Metric | Current |
| - | - |
| Packages source-parsed | 32 |
| Functions in graph | 168,386 |
| Caller index entries | 190,688 |
| Bytecode type packages | 165 |
| JARs indexed | 160 |
| Bytecode cache hits | 157 |
| Bytecode resolution duration | \~1.27s |
| Full dependency pipeline | \~33.0s |
| Wall time | \~44.0s |
| Findings | 269 |
| Exported call graph edges | 498,271 |
The largest recent improvements came from these changes:
* In the two-phase call graph build, only dependencies with findings get full source parsing.
* Exact-version Java JARs are indexed in parallel.
* A per-artifact bytecode cache means repeated scans don't reparse unchanged JARs.
### Current bottlenecks
The pipeline has three main time consumers:
1. **Source parsing for graph packages** (\~23s on warm `eladmin`): Parses `.java` files from 32 packages (user code + 27 deps with findings) to build 168K function declarations with call sites.
2. **Dependency scanning with opengrep**: Still dominates cold scans. On warm scans most dependencies hit the findings cache; on first scans the cost depends on dependency source size and worker count (`--dep-workers`). Batching removed the per-dependency rule load, so the remaining cost is the analysis itself.
3. **Call graph post-processing** (\~2-3s): Caller index construction, bytecode merge/rewrite, and fluent-chain resolution are no longer dominant but still scale with graph size.
### Future optimization opportunities
#### Source parsing and graph size reduction
The bytecode resolution bottleneck is largely gone. The remaining performance opportunities are upstream:
* **Reduce graph packages further**: Keep shrinking the set of dependencies that require full source parsing without breaking attribution accuracy.
* **Smarter pre-scan eligibility**: Skip dependencies that have source directories but no scannable files before invoking opengrep.
* **Containing-function lookup index**: `Tracer.FindContainingFunction()` still scans functions linearly; indexing by file could cut repeated lookups on large reports.
* **Selective bytecode indexing**: Only index JARs whose types appear in unresolved calls from graph packages. More complex, but now one of the few remaining bytecode-side wins.
#### Opengrep scanning
* **Result caching**: The existing `DiskFindingsCache` caches dependency scan results by `module@version:rulesHash`. On repeated scans of the same project, most dependencies hit the cache.
* **Cold-scan throughput**: First-scan performance still depends heavily on opengrep throughput over large dependency sources. The next step there is to scan only the directories a Go module actually imports, rather than the whole module.
#### Call graph export
* **Export is fast** (\~10-15s for 269 findings) and not currently a bottleneck. The finding-centric export only traces paths reachable from findings, producing a compact JSON (\~9.5K lines for eladmin) regardless of full graph size.
## Limitations
### General
* **Static analysis.** The call graph is built from syntactic call expressions. It cannot resolve interface dispatch, reflection-based calls, or function values passed as arguments.
* **One module at two versions.** A finding's `dependency.relationship` and `dependency.path` come from the resolved dependency graph. When a module resolves at more than one version (npm installs a package twice when versions conflict), a resolver that records versioned edges (npm, Maven, Gradle, Cargo) gives each copy its own route. For a resolver that does not, the finding in such a module, and any dependency reached through one, carries no `relationship` or `path`; its `purl` and `version` stay.
* **All paths stored.** When several call chains exist (BFS finds all paths), all of them are stored in `call_chains`, so no reachability information is lost.
### Go-specific
* **Production import closure only.** The inventory holds the modules that provide a package imported, directly or transitively, by a non-test package of a main module. Modules required only by `_test.go` files or by build-tagged tool files such as `tools.go`, and requirements nothing imports, are not resolved or scanned. `--include-tests` does not widen the inventory.
* **Host build configuration.** `go list -deps` evaluates build constraints for the scanning host's `GOOS`, `GOARCH` and build tags (including any in `GOFLAGS`), so a module imported only on another platform is not inventoried, and within an imported package only the files that build compiles are scanned and parsed. `CGO_ENABLED` decides whether files that import `"C"` count.
* **Packages that fail to load.** A package the go tool cannot load, such as a directory that mixes two package names or imports a module missing from the module cache while offline, is skipped with a warning. Modules imported only through it are not inventoried; the rest of the closure is.
* **No cross-module method resolution.** Method calls on variables (e.g., `cipher.Encrypt()`) are recorded with the variable name as the type, not the resolved type. Cross-package type resolution would require full type analysis.
### Java-specific
* **Gradle source archives are best-effort.** Gradle dependency resolution provides binary artifact paths deterministically, but source archive availability still depends on what upstream repositories publish.
* **Missing source JARs.** Dependencies without sources are skipped for source scanning, but they can still contribute Java bytecode types if the compiled JAR is present locally. They cannot produce source-level findings until sources are available. Their code has no calls in the graph, so no call chain crosses them: in the callgraph export such a dependency on a finding's `dependency.path` carries `without_source: true`, and a finding whose every route from the application crosses one, with no chain reaching it, reads `unknown` (`dependency_without_source`) instead of `unreachable`.
* **Wildcard import resolution.** When multiple wildcard imports could match a class name, resolution is best-effort.
* **Limited inheritance and polymorphism.** Variable types are tracked syntactically, but interface method call sites and abstract-class method call sites are fanned out to same-name/arity concrete implementations and subclass overrides within the same namespace root (see `expandInterfaceDispatch`/`expandAbstractClassDispatch` in `internal/callgraph/builder.go`). This is a name+arity heuristic, not full type resolution, so it can over-match unrelated overloads across sibling implementations.
* **Multi-module Maven partial resolution.** Multi-module Maven projects are supported through a three-tier fallback strategy. Tier 3 (`mvn install -DskipTests`) requires compilation and may fail if the project needs specific JDK versions or build tools not available in the scan environment.
### Python-specific
* **Requires a Python interpreter with `pip`.** The resolver runs `python -m pip` and `importlib.metadata` through the same interpreter. If `VIRTUAL_ENV` is set, that environment's Python is preferred; otherwise it falls back to `python3` then `python` from PATH.
* **C-extension packages are skipped.** Packages without Python source on disk (compiled C extensions) cannot be scanned.
* **Distribution-to-import mapping.** The resolver relies on `importlib.metadata` (Python 3.10+) or `*.dist-info` fallback; packages with non-standard layouts may not be resolved.
* **No dynamic dispatch.** Calls resolved through `getattr`, `__getattr__`, or metaclass magic are not tracked.
### Rust-specific
* **Requires `cargo` in PATH.** The resolver shells out to `cargo metadata`.
* **No trait dispatch.** Method calls on trait objects (e.g., `dyn Cipher`) are resolved syntactically by type name; trait implementations are not followed.
* **Macro-generated code.** Functions generated by macros (e.g., `proc_macro`) are invisible to syntactic parsing.
* **`src/` transparency assumption.** The parser assumes `src/` is the crate root; non-standard `[lib] path` configurations may produce incorrect module paths.
# Docker usage
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/docker-usage
Run Crypto Finder from its official Docker images, locally, in CI/CD, or with dependency scanning.
## Available images
Crypto Finder has three images. They differ in size and in what they include:
| Image | Tag | Size | Use case |
| - | - | - | - |
| **Standard** | `latest` | \~800MB | Default: binary + OpenGrep + Semgrep |
| **Slim** | `latest-slim` | \~15MB | Binary only, bring your own scanner |
| **Deps** | `latest-deps` | \~3-4GB | All language toolchains for dependency scanning |
### Standard image (recommended)
The standard image includes OpenGrep 1.12.1+ and Semgrep 1.145.0+, so every scanning capability works out of the box.
```bash theme={null}
docker pull ghcr.io/scanoss/crypto-finder:latest
```
### Slim image
The slim image contains only the `crypto-finder` binary, which keeps it small. You must supply an external OpenGrep or Semgrep installation, for example by mounting it into the container or by building a derived image. Use it when your workflow manages scanner versions itself.
```bash theme={null}
docker pull ghcr.io/scanoss/crypto-finder:latest-slim
```
### Deps image (dependency scanning)
The deps image adds the language toolchains needed to scan third-party dependencies with `--scan-dependencies`. It detects cryptographic usage in those dependencies and traces the call chains that reach it.
```bash theme={null}
docker pull ghcr.io/scanoss/crypto-finder:latest-deps
```
It has everything in the standard image, plus:
* The Go toolchain, for dependency resolution with `go list` and `go mod graph`.
* Java (JDK 8, 11, 17, and 21, with Maven and Gradle), for JDK-aware Java dependency resolution and deterministic platform signature indexing.
* Rust (Cargo), for dependency resolution with `cargo metadata`.
* Python in an isolated virtualenv, for pip dependency resolution.
## Basic usage
### Scanning with mounted volumes
```bash theme={null}
# Scan code directory with remote rulesets
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/output:/workspace/output \
-e SCANOSS_API_KEY=YOUR_KEY \
ghcr.io/scanoss/crypto-finder:latest \
scan --output /workspace/output/results.json /workspace/code
# Scan with local rules
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/rules:/workspace/rules:ro \
-v $(pwd)/output:/workspace/output \
ghcr.io/scanoss/crypto-finder:latest \
scan --no-remote-rules --rules-dir /workspace/rules \
--output /workspace/output/results.json /workspace/code
# Generate CycloneDX CBOM
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/output:/workspace/output \
-e SCANOSS_API_KEY=YOUR_KEY \
ghcr.io/scanoss/crypto-finder:latest \
scan --format cyclonedx --output /workspace/output/cbom.json /workspace/code
```
### Volume mounting patterns
| Mount point | Purpose | Recommended mode |
| - | - | - |
| `/workspace/code` | Source code to scan | `:ro` (read-only) |
| `/workspace/rules` | Custom rule files | `:ro` (read-only) |
| `/workspace/output` | Scan results | `:rw` (read-write) |
| `~/.scanoss/crypto-finder/cache` | Ruleset cache (optional) | `:rw` (read-write) |
### Preserving the cache between runs
```bash theme={null}
# Create a named volume for cache persistence
docker volume create crypto-finder-cache
# Use the volume in scans
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/output:/workspace/output \
-v crypto-finder-cache:/root/.scanoss/crypto-finder/cache \
-e SCANOSS_API_KEY=YOUR_KEY \
ghcr.io/scanoss/crypto-finder:latest \
scan --output /workspace/output/results.json /workspace/code
```
## CI/CD integration
### GitHub Actions
This workflow scans code on every push and pull request:
```yaml theme={null}
name: Crypto Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Run Crypto Finder
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-e SCANOSS_API_KEY=${{ secrets.SCANOSS_API_KEY }} \
ghcr.io/scanoss/crypto-finder:latest \
scan --output /workspace/results.json /workspace/src
- name: Upload results
uses: actions/upload-artifact@v3
if: always()
with:
name: crypto-scan-results
path: results.json
- name: Check for findings
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
ghcr.io/scanoss/crypto-finder:latest \
scan --fail-on-findings /workspace/src
```
#### With custom rules
```yaml theme={null}
name: Crypto Scan (Custom Rules)
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Run Crypto Finder with custom rules
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
ghcr.io/scanoss/crypto-finder:latest \
scan --no-remote-rules \
--rules-dir /workspace/.crypto-rules \
--output /workspace/results.json \
/workspace/src
- name: Generate CBOM
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
ghcr.io/scanoss/crypto-finder:latest \
convert /workspace/results.json --output /workspace/cbom.json
- name: Upload CBOM
uses: actions/upload-artifact@v3
with:
name: crypto-cbom
path: cbom.json
```
## Dependency scanning
The `latest-deps` image includes every language toolchain that dependency scanning needs. Use it with the `--scan-dependencies` flag.
### Go projects
```bash theme={null}
docker run --rm \
-v $(pwd):/workspace/code:ro \
ghcr.io/scanoss/crypto-finder:latest-deps \
scan --scan-dependencies /workspace/code
```
### Java (Maven or Gradle) projects
```bash theme={null}
docker run --rm \
-v $(pwd):/workspace/code:ro \
ghcr.io/scanoss/crypto-finder:latest-deps \
scan --scan-dependencies /workspace/code
```
### Rust projects
```bash theme={null}
docker run --rm \
-v $(pwd):/workspace/code:ro \
ghcr.io/scanoss/crypto-finder:latest-deps \
scan --scan-dependencies /workspace/code
```
### Python projects
For Python projects, install the dependencies before scanning, because pip needs them to be resolved:
```bash theme={null}
docker run --rm \
-v $(pwd):/workspace/code:ro \
--entrypoint sh \
ghcr.io/scanoss/crypto-finder:latest-deps -c \
"pip install -r /workspace/code/requirements.txt 2>/dev/null; \
crypto-finder scan --scan-dependencies /workspace/code"
```
> **Note:** The deps image keeps project dependencies in an isolated virtualenv, separate from the scanner tooling (semgrep), so `pip install` affects only the target project's environment.
### CI/CD with dependency scanning (GitHub Actions)
```yaml theme={null}
name: Crypto Scan with Dependencies
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Run Crypto Finder with dependency scanning
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-e SCANOSS_API_KEY=${{ secrets.SCANOSS_API_KEY }} \
ghcr.io/scanoss/crypto-finder:latest-deps \
scan --scan-dependencies --output /workspace/results.json /workspace
```
## Advanced Docker usage
### Running with a specific scanner
```bash theme={null}
# Use Semgrep instead of default OpenGrep
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/output:/workspace/output \
-e SCANOSS_API_KEY=YOUR_KEY \
ghcr.io/scanoss/crypto-finder:latest \
scan --scanner semgrep --output /workspace/output/results.json /workspace/code
```
### Custom timeout and language override
```bash theme={null}
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/output:/workspace/output \
-e SCANOSS_API_KEY=YOUR_KEY \
ghcr.io/scanoss/crypto-finder:latest \
scan \
--timeout 30m \
--languages java,python,go \
--output /workspace/output/results.json \
/workspace/code
```
## Docker Compose
An example `docker-compose.yml` for local development:
```yaml theme={null}
version: "3.8"
services:
crypto-finder:
image: ghcr.io/scanoss/crypto-finder:latest
volumes:
- ./code:/workspace/code:ro
- ./rules:/workspace/rules:ro
- ./output:/workspace/output
- crypto-cache:/root/.scanoss/crypto-finder/cache
environment:
- SCANOSS_API_KEY=${SCANOSS_API_KEY}
command: >
scan
--rules-dir /workspace/rules
--output /workspace/output/results.json
/workspace/code
volumes:
crypto-cache:
```
Start it with:
```bash theme={null}
SCANOSS_API_KEY=your_key docker-compose up
```
# Installation
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/installation
Install SCANOSS Crypto Finder with Go, from a Docker image, or by building it from source.
## Prerequisites
You need the following before you install Crypto Finder:
* A scanner, either [OpenGrep (recommended)](https://github.com/opengrep/opengrep)
or [Semgrep](https://github.com/semgrep/semgrep). The Docker images include both.
```bash theme={null}
# OpenGrep v1.12.1: download from https://github.com/opengrep/opengrep/releases/tag/v1.12.1
# Semgrep:
pip install semgrep==1.145.0
```
Crypto Finder uses OpenGrep as its **default scanner**.
* [Go 1.25+](https://go.dev/dl/) and [Git](https://git-scm.com/install/), but only if you
build from source or install with the Go package manager.
## Installation
### Go
If you have Go installed, install Crypto Finder from its package path:
```bash theme={null}
go install github.com/scanoss/crypto-finder/cmd/crypto-finder@latest
```
To verify the installation, run:
```bash theme={null}
crypto-finder
```
### Docker
Crypto Finder has three Docker images. The larger images include more tooling:
```bash theme={null}
# Full image with scanners included (recommended)
docker pull ghcr.io/scanoss/crypto-finder:latest
# Slim image (requires an external scanner; see Prerequisites)
docker pull ghcr.io/scanoss/crypto-finder:latest-slim
# Deps image: adds Go, Java (JDK 8/11/17/21 + Maven/Gradle), Rust, and Python
# toolchains for --scan-dependencies
docker pull ghcr.io/scanoss/crypto-finder:latest-deps
```
[Docker usage](/en/latest/cli/crypto-finder/docker-usage) compares the images in full and covers dependency-scanning workflows.
### Build from source
#### Windows
1. Open PowerShell and run:
```powershell theme={null}
git clone https://github.com/scanoss/crypto-finder.git
cd crypto-finder
go build -o crypto-finder.exe ./cmd/crypto-finder
```
2. Optionally, add the folder to your PATH:
```powershell theme={null}
$currentPath = (Get-Location).Path
[Environment]::SetEnvironmentVariable("PATH", "$env:PATH;$currentPath",
[EnvironmentVariableTarget]::User)
```
> **Note:** Restart PowerShell after you change the PATH, so the change takes effect.
3. Verify the installation:
```powershell theme={null}
# If added to PATH
crypto-finder
# Or run from the current directory
.\crypto-finder.exe
```
#### macOS
1. Clone, compile, and install:
```bash theme={null}
git clone https://github.com/scanoss/crypto-finder.git
cd crypto-finder
make build
sudo make install
```
2. Verify the installation:
```bash theme={null}
crypto-finder
```
#### Linux
1. Clone, compile, and install:
```bash theme={null}
git clone https://github.com/scanoss/crypto-finder.git
cd crypto-finder
make build
sudo make install
```
2. Verify the installation:
```bash theme={null}
crypto-finder
```
# Output formats
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/output-formats
Crypto Finder writes two output formats: interim JSON for detailed analysis, and CycloneDX CBOM for standard Bill of Materials reporting.
## Interim JSON format
Interim JSON is the default output format. It holds detailed cryptographic asset information and is designed for the SCANOSS ecosystem.
The interim report is the primary findings artifact. It contains finding metadata such as `finding_id` and optional structural `occurrence_key`, but it does not embed the finding-centric reachability slices produced by `--export-callgraph`.
### Format specification
```json theme={null}
{
"version": "1.5",
"tool": {
"name": "crypto-finder",
"version": "0.1.0"
},
"findings": [
{
"file_path": "path/to/file",
"language": "language_name",
"cryptographic_assets": [
{
"start_line": 123,
"end_line": 123,
"match": "code snippet",
"rules": [
{
"id": "rule.id",
"message": "description",
"severity": "INFO|WARNING|ERROR"
}
],
"status": "pending|identified|dismissed|reviewed",
"metadata": {
"assetType": "algorithm|certificate|protocol|related-crypto-material",
"algorithmFamily": "algorithm_family",
"algorithmPrimitive": "primitive_type",
"algorithmMode": "mode_of_operation",
"algorithmPadding": "padding_scheme"
},
"source": "direct|dependency",
"dependency_info": {
"module": "golang.org/x/crypto",
"version": "v0.17.0",
"purl": "pkg:golang/golang.org/x/crypto@v0.17.0"
},
"finding_id": "a1b2c3d4",
"occurrence_key": "v1:0123456789abcdef"
}
]
}
]
}
```
> **Note:** Version 1.1 introduced the `rules` array field (replacing single `rule` field) to support per-line deduplication. Version 1.2 added `source` and `dependency_info` for dependency scanning attribution. Version 1.3 adds `finding_id` for cross-referencing with the callgraph export. Version 1.5 adds optional `occurrence_key` for canonical findings, using AST call evidence when available and a deterministic file/module-level fallback for valid top-level calls. Dependency-backed `file_path` values are dependency-root-relative; the package identity stays in `dependency_info`. Reachability slices such as `call_chains` are emitted by the dedicated call graph export, not by the interim report. See [Dependency scanning](/en/latest/cli/crypto-finder/dependency-scanning) for details.
### Field descriptions
| Field | Description |
| - | - |
| `version` | Format version (currently "1.5") |
| `tool.name` | Scanner used (crypto-finder) |
| `tool.version` | Scanner version |
| `findings` | Array of file-level findings |
| `file_path` | Relative path to scanned file |
| `language` | Detected programming language |
| `cryptographic_assets` | Array of crypto findings in the file |
| `start_line` | First line where the asset was detected |
| `end_line` | Last line where the asset was detected |
| `match` | Actual code snippet matched |
| `rules` | Array of detection rules that identified this asset |
| `rules[].id` | Unique rule identifier |
| `rules[].message` | Human-readable description |
| `rules[].severity` | Finding severity level |
| `status` | Finding status (pending, identified, dismissed, reviewed) |
| `metadata` | Key-value pairs with asset-specific metadata |
| `metadata.assetType` | Asset classification |
| `metadata.algorithmFamily` | Algorithm/protocol family name |
| `metadata.algorithmPrimitive` | Cryptographic primitive type |
| `metadata.algorithmMode` | Mode of operation (for block ciphers) |
| `metadata.algorithmPadding` | Padding scheme used |
| `source` | `"direct"` (user code) or `"dependency"` (v1.2+) |
| `dependency_info` | Attribution for dependency findings: `module`, `version`, and optional `purl` (v1.5+) |
| `purl` | Optional canonical package URL promoted from direct rule metadata; it stays versionless unless one unambiguous direct dependency version is available (v1.6+) |
| `finding_id` | Stable short hash used to join the interim report to the call graph export (v1.3+) |
| `occurrence_key` | `v1:<16 lowercase hex>` structural identity, present on every asset whenever the scan built a call graph, so every asset that has a finding graph carries the key its graph carries. It excludes rules, source text, metadata, reachability, and severity. It uses the AST anchor of the matched call when there is one. A match with no call to anchor, such as a type usage (a cast or a declaration) or a library API declaration surfaced as an entry point, is keyed by the function that holds it; a valid top-level call uses a deterministic file/module-level fallback (v1.5+). A per-value asset, one a rule's `parameterCondition` specialized to a resolved value such as `param[0]==SHA-256`, also hashes that value, so the values of one call have distinct keys and `finding_id`s. A synthesized entry point (rule id `crypto-finder.api-entry-point`) that has several variants at one declaration, such as `PBKDF2` next to `PBKDF2-SHA-256` selected by an argument type (`param[0]:type==...`), or `ECDSA` next to `ECDSA-deterministic`, gives each variant except the base its own `finding_id` and `occurrence_key`, derived from the variant's `algorithmName`, `algorithmHashFunction` and `parameterCondition` (variants that share all three hash their whole metadata instead); the base keeps the identity a lone entry point has. Two calls of one API in a statement that spans lines get distinct keys; `finding_id` hashes file, start line and rule, so such a pair shares it and `(finding_id, occurrence_key)` is the unique pair. Every other asset hashes exactly the structural identity above. A report written without a call graph may omit it. |
| `parameter_conditions` | Structured argument predicates parsed from the rule's `parameterCondition` metadata — which argument value/type selects this asset variant (v1.4+, omitted when the rule carries no predicate) |
| `conditioned_value` | The resolved condition, such as `param[0]==HmacSHA256`, a per-value asset was specialized for. It is part of the asset's `finding_id` and `occurrence_key`, so a report read back by another scan, such as published dependency findings, keeps one identity per value (v1.7+, omitted on every other asset) |
| `terminal_start_col`, `terminal_end_col` | Columns of the call that encloses an asset matched on an argument span, such as `new KeyParameter(key)` inside `cipher.init(...)`. They locate the crypto call for the callgraph export and the `occurrence_key`, so a report read back by another scan finds the same call (v1.7+, omitted on every other asset) |
| `file_path` | For dependency findings, path relative to the dependency root; use `dependency_info` for artifact identity |
### Public Go contract
Go consumers can import `github.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 ` is passed, Crypto Finder also writes a separate finding-centric call graph JSON file to ``. 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[].dependency` names the dependency a dependency finding sits in and its route from the application in the resolved dependency graph, with `without_source` on a path step parsed without source. `finding_graphs[].analysis` gains `paths_total`, `paths_kept`, `route_evidence` and `no_callers_only`. The first frame of each live chain carries `root_kind` (`main`, `framework_entry`, `no_callers`, `depth_limit`). `unresolved_reason` gains three values for an attributed finding whose verdict is `unknown`: `traversal_truncated`, `unresolved_dispatch` and `dependency_without_source`; the stitched export now writes `unresolved_reason` too, with `unresolved_dispatch` only. Every new field is optional, so a `6.16` reader that ignores unknown properties keeps working; a reader that switches on `unresolved_reason` must treat an unknown value as unknown. Frame shape is the `6.15` interned shape. The stitched export now also writes `root_kind` and `no_callers_only`, when the fragments recorded entry kinds (`graph-fragment-1.14`).
* **`6.16`** (SDK opt-in; the CLI default before `6.17`) adds optional `scan_metadata.ecosystems` to 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 with `ecosystem`, `root_module`, `function_count` and `edge_count`. `scan_metadata.ecosystem`, `root_module`, `function_count` and `edge_count` keep 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 inlined `6.14` compatibility render. Frame shape is the `6.15` interned shape.
* **`6.15`** (SDK opt-in; the CLI default before `6.16`) contracts `call_chains` frames: they no longer repeat catalog identity fields (`function_name`, `file_path`, `canonical_signature`, `dependency_info`, and the rest of the interned identity). Join through `functions[]` plus `finding_graphs[].call_chain_indexes`, and through `canonical_signature` on `crypto_entry_points` and catalog rows. Hop-specific fields (`entry_call`, `crypto_call`, `entry_resolution`) stay on the frames. Enable with `--export-callgraph-interned-frames` or `ScanMeta.InternedFrames`. Zero-value SDK/stitch stays on `6.14`; the CLI defaulted to `6.15` until `6.16`. Parsers that only read `call_chains[]` objects must gate on `schema_version` or call `HydrateChainIdentities`.
* **`6.14`** adds a top-level interned `functions[]` catalog and `finding_graphs[].call_chain_indexes` (0-based positions into that catalog) beside the existing inlined `call_chains`. The index lists reconstruct the same N-sample of routes. Inlined frames keep their previous field names and meaning. `crypto_entry_points` is still the complete reverse-reach set. Both surfaces can explicitly select this compatibility render.
* **`6.13`** adds `call_chains[].entry_resolution` and `entry_declared_type`, reporting how the call arriving at each frame was established.
* **`6.12`** adds the rule-vs-callgraph key-length conflict marker to `supporting_calls[].supporting_call.resolved_key_length`. When a detection rule declares a static `keyLength` and the callgraph resolves a different value for a finding referencing that evidence, the resolved `bits` stay primary, the rule value is retained as `rule_declared_bits`, and `rule_conflict` is `true`. Agreement, an unresolved key length, and a rule that declares no `keyLength` all 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.11`** adds optional `supporting_calls[].supporting_call.resolved_key_length` for structurally derived Java key-generation configuration calls referenced by `finding_graphs[].supporting_call_ids`. It contains raw integer `bits` only when static analysis resolves a literal or simple propagated constant, `provenance` (`constant` or `unknown`), and required `source_call` (`function_name`, `line`, `parameter_index`) for the contributing argument. It is preserved by live, graph-fragment, and stitched callgraph exports; terminal `crypto_call` records do not carry it, and it does not populate CBOM properties or express a security threshold.
* **`6.10`** carries an optional top-level `purl` for direct findings and an optional canonical `purl` inside 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.10`** adds optional `occurrence_key` to 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 using `finding_id` alone.
* **`6.9`** makes `crypto_entry_points[]` a reverse-reachability answer rather than a projection of the exported `call_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_depth` is 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.8`** adds `finding_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 to `unknown`), `finding_graphs[].analysis` (`call_chains`/`parameters` completeness), and `crypto_entry_points[].root`. `root` is the explicit chain-root classification, while the index itself stays deliberately broader. The legacy `reachable` bool is unchanged.
* **`6.7`** adds `reachable` to each finding graph when dependency scanning makes user-code reachability applicable; it is omitted for a standalone library scan.
* **`6.6`** adds deterministic `forward_calls.ambiguous_calls` groups for fail-closed interface dispatch: completeness state, stable group/candidate IDs, complete callable identities, and preserved call-site argument provenance. It does not promote ambiguous candidates to resolved edges.
* **`6.5`** makes `role: operation` contract methods **supporting-call-only**: they are exported as categorized `supporting_calls` referenced by `supporting_call_ids` (including interface-authored contracts resolved to concrete implementations) and are no longer synthesized as operation-only `crypto_entry_points` in live, fragment, or stitched exports.
* **`6.4`** adds `method_role`, `role_provenance`, and `parameter_roles`, a method and parameter role classification derived from the contracts KB (`factory`/`config`/`output`/`operation` on `method_role`; per-parameter `operation-determining`/`metadata-contributing`/`none` with a `contributes: {property, derivation}` block on `parameter_roles`). `method_role`/`role_provenance` appear on `crypto_entry_points`; `parameter_roles` appears on `crypto_entry_points` and on the supporting-call declaration, index-aligned with `parameter_types`, and 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.3`** added the optional per-finding `forward_calls` block, which holds the finding anchor's forward call closure: deduped nodes with `depth`/`crypto_relevant`/`supporting_category`, plus traversed edges whose `entry_call` carries the call-site argument data-flow. The block is emitted only when the stitch runs with `StitchOptions.ForwardClosure`. Caps on depth, nodes, and edges are reported through `max_depth` and an explicit `truncated` flag, never applied silently.
* **`6.2`** exposed `supporting_calls`, `crypto_entry_points`, and `graph_algo_version` end-to-end.
* **`6.0`** removed the legacy `entry_point_index` projection and made `crypto_entry_points[]` canonical.
* **`4.3`** added Java runtime provenance in `scan_metadata` for JDK-aware platform signature enrichment.
* Each top-level record preserves `finding_id`. When `occurrence_key` is 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 without `occurrence_key` use `finding_id` alone.
* `call_chains` is 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 128` opts in to a larger sample. SDK/stitch zero or omitted `StitchOptions.MaxChains` remains 128. The live export has no depth cap. The sample is spent on the strongest evidence first (see `analysis.route_evidence`): every route whose calls all resolved statically, then routes that need a dispatch edge, then routes that need a `name_only` edge. 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 carries `root_kind`: `main` (a Java `static main(String[])`, or a free function named `main`), `framework_entry` (a Java method that is an `@Override` of a method of a type outside the application, such as `HttpHandler.handle` or `Runnable.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), or `depth_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 as `main` or `framework_entry` and no application code calls it: a handler doing crypto inline reads `reachable`, a helper nothing calls reads `unreachable`. `crypto_entry_points[].root` follows 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//.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 the `root_kind` it 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 typed `Express`); 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 typed `chi.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: required` asks 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 add `directory` to count only files under a directory of that name, and name `` for the class body: the body of a Django `Migration` under a `migrations` directory is an entry point, since the migration loader runs it and reaches the `RunPython` callbacks 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 generated `RegisterServer` or `RegisterHandlerServer` function, whose `names` are 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) and `file_convention` (an export a framework finds by the file's location, counted only when the nearest `package.json` declares 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 under `internal/scan/testdata/entry_rules//` with a case in `internal/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 nearest `package.json` names in `main`, `module` or `exports` (`dist/`, `build/`, `lib/`, `out/`, `esm/`, `cjs/` and `es/` paths also match the same path under `src/` and at the package root), and `main` for the top level (``) of that module, of a module `bin` names, of a module a `package.json` script runs with `node`, `nodejs`, `tsx`, `ts-node`, `bun` or `deno` (`tsx scripts/seed.ts`, also after `&&`, `||`, `;` or `|`, with the same `dist/` to `src/` mapping), of a `.js`, `.mjs`, `.cjs`, `.ts`, `.mts` or `.cts` file whose first line is a shebang naming one of those runtimes, and of a module with `if (require.main === module)`; Python: `main` for the functions the nearest `pyproject.toml` (`[project.scripts]`, `[project.gui-scripts]`, `[tool.poetry.scripts]`), `setup.cfg` (`[options.entry_points]`) or `setup.py` names as `module:function`, and for the top level (``) of a module with `if __name__ == "__main__":`; Go: `main` for `init` functions and the synthetic `` package variable initializers, which the runtime runs before `main`. 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 or `new` argument, 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 under `test/`, `tests/` or `__tests__/` or named like a test (`*Test.java`, `*_test.go`, `test_*.py`, `*_test.py`, `conftest.py`, `*.test.*`, `*.spec.*`), keeps `no_callers`. Test sources are in the graph only under `--include-tests`.
* `analysis.paths_total` is the number of distinct routes, from a chain root to the finding, the graph holds, and `analysis.paths_kept` the number the finding's `call_chains` show; chains that differ only in a call-site line count once. `paths_total` saturates at the largest int64 and is then a lower bound. Both are present on live findings with a chain: reachable ones and `unresolved_dispatch` ones. `analysis.call_chains` is `partial` whenever 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 reads `reachable`, not `unknown` with no chains. A depth limit that cuts every route before a root reads `unknown` with `unresolved_reason` `traversal_truncated`; that reason, unlike the others, comes without `finding_location`, since the finding is attributed. A finding whose every route crosses a `name_only` edge reads `unknown` with `unresolved_reason` `unresolved_dispatch`, also without `finding_location`: its chains are exported as evidence of how the crypto may run, and the legacy `reachable` flag is omitted. One route free of `name_only` edges keeps it `reachable`. `analysis.route_evidence` rates the finding's strongest route by its weakest call: `direct` (every call `exact` or statically resolved), `dispatch` (the best route needs at least one `interface_dispatch` or `python_subclass_dispatch` edge and no `name_only` edge; the finding stays `reachable`, and the dispatch hop is the frame whose `entry_resolution` names it), or `name_only` (every route needs a `name_only` edge; the verdict is `unknown` with `unresolved_dispatch`). A finding specialized by a `parameter_conditions` predicate that removed chains rates `route_evidence` and `no_callers_only` from its surviving chains only, so a condition that refutes the direct route cannot leave `direct` behind; with no chain left both are absent. The verdict follows the same chains: a specialized finding whose surviving chains all cross a `name_only` edge reads `unknown` with `unresolved_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`, flagged `no_callers_only`). It is present with `paths_total` on the live export, and on stitched findings that are `reachable` through a chain or `unresolved_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_only` is `true` when every root of the routes that support the verdict has `root_kind` `no_callers`: the crypto is reached only from application code nothing in the application calls, not from a recognized `main` or framework entry. The supporting routes are those free of `name_only` edges, 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 no `root_kind`. On the stitched export a root reads `main` or `framework_entry` from the fragment's `entry_kind`, and `no_callers` when it has none and nothing in the stitched graph calls it; `depth_limit` never 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 no `name_only` edge, reads such a finding `unknown` with `unresolved_reason` `unresolved_dispatch` and no chain, and a dependency's mine-time entry-point index does not upgrade it to `reachable`; it sets no other `unresolved_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](https://github.com/scanoss/crypto-finder/blob/main/docs/adr/0002-call-chains-sample-size.md).
* `functions[]` (schema `6.14`+) is the interned identity catalog for those emitted routes. `finding_graphs[].call_chain_indexes` lists the same walks as integer positions into that catalog. Schema `6.14` also inlines those fields on `call_chains` frames. Schemas `6.15`, `6.16` and `6.17` (local CLI default) join identity through the catalog alone.
* Each chain node carries hop-specific fields: optional `entry_call`, `entry_resolution`, and `crypto_call` on the last node. Compatibility schema `6.14` also inlines function identity (`function_name`, `file_path`, `canonical_signature`, `dependency_info`) on each frame. The interned render (`6.15`+) keeps that identity only in `functions[]`.
* `entry_call` describes how execution entered the current node from the previous step. Its `file_path` and `line` refer to the call site in the previous node's source file.
* `entry_resolution` says 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 as `threading.Thread(target=fn)`, `sort.Slice(xs, fn)` or `setTimeout(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 of `entry_declared_type`, from the bytecode hierarchy or the parsed extends/implements clauses), `python_subclass_dispatch`, or `name_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[]` and `crypto_call.parameters[]` both use the same parameter model: `parameter_index` (always `0`-based), best-effort `type`, `argument_expression`, `resolved_value`, `variable_name` for simple identifiers only, and recursive `source_nodes`.
* `supporting_calls[].supporting_call.resolved_key_length` is 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 the `RSAKeyGenParameterSpec`, `ECGenParameterSpec`, and `SecretKeySpec` constructors whose value reaches such a call. `source_call.function_name` names 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 through `finding_graphs[].supporting_call_ids`. It reports raw key bits when known and otherwise retains `provenance: "unknown"` plus `source_call`; consumers must not infer a key-size threshold from it. The terminal `crypto_call` remains the detected operation and does not carry this field.
* `supporting_calls[].supporting_call.resolved_key_length.rule_conflict` reports that a detection rule declared a static `keyLength` disagreeing with the resolved `bits`. The resolved value remains primary and the rule value is preserved in `rule_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_bits` reports 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_metadata` may also include `java_requested_jdk_major`, `java_runtime_version`, `java_platform_signatures_used`, `java_platform_signature_source`, and `java_platform_signature_unavailable_reason` to show which JDK major was requested and whether JDK platform signatures contributed to type enrichment.
* `source_nodes` can span multiple wrapper hops. A local `PARAMETER` node may contain nested upstream provenance such as `PARAMETER -> PARAMETER -> VALUE`, and propagated nested nodes keep `location.file_path` plus `location.line` when known.
* Method-call provenance is preserved as `CALL_RESULT` nodes. When the parser can resolve the invoked method, the node also exports `call_target`, and any traceable receiver value is nested under that `CALL_RESULT` via `source_nodes` (for example `CALL_RESULT -> PARAMETER alg -> VALUE SignatureAlgorithm.HS256`).
* `finding_graphs[].reachability` is `not_applicable` for 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-reachability` treats the target as an application instead: its own source packages become the user code, the same set a `--scan-dependencies` run 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, reads `unreachable` here because the dependency's code is not in the graph; `--scan-dependencies` follows it. A scan that resolved dependencies ignores the flag, and `--export-graph-fragment` is unaffected.
* A finding whose rule carries `parameter_conditions` keeps 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 reads `unreachable` (`reachable: false`, no `call_chains`): the crypto the rule describes runs on no known route, and the graph fragment's `crypto_entry_points` do not list it either. When the chain budget (`--export-callgraph-max-chains`) left a route or a call site unexamined, it reads `unknown` with `unresolved_reason: "traversal_truncated"` and `analysis.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` (`direct` when the application declares it, `transitive` otherwise) and `path`, its shortest route from the application in the resolved dependency graph, not in the call graph. `path` lists `module`, `version` and `purl` per dependency, a direct dependency first and the finding's own dependency last; the application itself is `scan_metadata.root_module` and 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, so `lodash` 4.17.21 can be `direct` while the copy nested under another package is `transitive`, and each path step names its own `version`. Without one, `relationship` and `path` are 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; `purl` and `version` stay. `relationship` and `path` are 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 no `dependency` block; its top-level `purl` keeps 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 schema `6.17`. The inlined `6.14` compatibility render carries it too, and `schemas/callgraph-schema.json` declares 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 `path` carries `without_source: true`. When every route from the application to a finding's dependency crosses one and no chain reaches the finding, the finding reads `unknown` with `unresolved_reason: "dependency_without_source"` and `analysis.call_chains: "partial"`, instead of `unreachable`: 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_location` and `unresolved_reason`. `no_containing_function` means the finding's language was analyzed and no function encloses it; `no_crypto_call_match` means no call at the finding's position matched. `language_not_analyzed` means 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; its `reachability` is `not_applicable` because 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-reachability` is set, and `not_applicable` otherwise.
* `crypto_entry_points[]` is the stitch/API index. Each entry carries `function_key`, canonical/display symbols, aliases, and `reachable_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 inflate `finding_graphs[]`. As of `6.5`, `role: operation` contract methods (the calls where the cryptographic computation actually executes, e.g. block-processing/finalization methods) are also exported here with a category and referenced via `supporting_call_ids`.
* Constructor joins remain canonical (``), while display fields and aliases expose IBM-style names such as `com.acme.Factory.Factory`.
* `entry_point_index` is not emitted by schema `6.0`. Consumers should migrate to `crypto_entry_points[]`.
### Compatibility and consumer gating
The published JSON Schemas are [`schemas/interim-report-schema.json`](https://github.com/scanoss/crypto-finder/blob/main/schemas/interim-report-schema.json), [`schemas/callgraph-schema.json`](https://github.com/scanoss/crypto-finder/blob/main/schemas/callgraph-schema.json) (inlined `6.14`), [`schemas/callgraph-schema-6.15.json`](https://github.com/scanoss/crypto-finder/blob/main/schemas/callgraph-schema-6.15.json) (interned `6.15`), [`schemas/callgraph-schema-6.16.json`](https://github.com/scanoss/crypto-finder/blob/main/schemas/callgraph-schema-6.16.json) (interned `6.16`), and [`schemas/callgraph-schema-6.17.json`](https://github.com/scanoss/crypto-finder/blob/main/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) or `schema_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.md` in the same change.
Example (`--export-callgraph-interned-frames=false`, schema `6.14`; identity is inlined on frames and also in `functions[]`):
```json theme={null}
{
"functions": [
{
"function_name": "io.jsonwebtoken.jjwtfun.controller.SecretsController.traceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java",
"start_line": 33
},
{
"function_name": "io.jsonwebtoken.jjwtfun.service.SecretService.issueTraceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/service/SecretService.java",
"start_line": 72
},
{
"function_name": "org.springframework.security.core.token.Sha512DigestUtils.getSha512Digest",
"file_path": "org/springframework/security/core/token/Sha512DigestUtils.java",
"start_line": 43,
"dependency_info": {
"module": "org.springframework.security:spring-security-core",
"version": "5.7.11"
}
}
],
"finding_graphs": [
{
"finding_id": "69669f02",
"call_chain_indexes": [[0, 1, 2]],
"call_chains": [
[
{
"function_name": "io.jsonwebtoken.jjwtfun.controller.SecretsController.traceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java",
"start_line": 33
},
{
"function_name": "io.jsonwebtoken.jjwtfun.service.SecretService.issueTraceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/service/SecretService.java",
"start_line": 72,
"entry_call": {
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java",
"line": 34,
"parameters": [
{
"parameter_index": 0,
"type": "io.jsonwebtoken.SignatureAlgorithm",
"argument_expression": "SignatureAlgorithm.HS256",
"resolved_value": "SignatureAlgorithm.HS256"
}
]
}
},
{
"function_name": "org.springframework.security.core.token.Sha512DigestUtils.getSha512Digest",
"file_path": "org/springframework/security/core/token/Sha512DigestUtils.java",
"start_line": 43,
"dependency_info": {
"module": "org.springframework.security:spring-security-core",
"version": "5.7.11"
},
"crypto_call": {
"function_name": "java.security.MessageDigest.getInstance",
"line": 45,
"parameters": [
{
"parameter_index": 0,
"type": "String",
"argument_expression": "\"SHA-512\"",
"resolved_value": "\"SHA-512\""
}
]
}
}
]
]
}
]
}
```
### Example output
A finding matched by a single rule:
```json theme={null}
{
"version": "1.1",
"tool": {
"name": "crypto-finder",
"version": "0.1.0"
},
"findings": [
{
"file_path": "src/crypto/Example.java",
"language": "java",
"cryptographic_assets": [
{
"start_line": 29,
"end_line": 29,
"match": "cipher = Cipher.getInstance(\"AES/CBC/PKCS5Padding\");",
"rules": [
{
"id": "java.crypto.cipher-aes-cbc",
"message": "AES cipher usage detected",
"severity": "INFO"
}
],
"status": "pending",
"metadata": {
"assetType": "algorithm",
"algorithmFamily": "AES",
"algorithmPrimitive": "block-cipher",
"algorithmMode": "CBC",
"algorithmPadding": "PKCS5Padding"
}
}
]
}
]
}
```
A finding matched by several rules, deduplicated:
```json theme={null}
{
"version": "1.1",
"tool": {
"name": "crypto-finder",
"version": "0.1.0"
},
"findings": [
{
"file_path": "src/crypto/cipher.go",
"language": "go",
"cryptographic_assets": [
{
"start_line": 42,
"end_line": 42,
"match": "cipher.NewGCM(block)",
"rules": [
{
"id": "go-crypto-aes-gcm",
"message": "AES-GCM encryption detected",
"severity": "INFO"
},
{
"id": "go-crypto-authenticated-encryption",
"message": "Authenticated encryption pattern detected",
"severity": "INFO"
}
],
"status": "pending",
"metadata": {
"assetType": "algorithm",
"algorithmFamily": "AES",
"algorithmPrimitive": "ae",
"algorithmParameterSetIdentifier": "256",
"algorithmMode": "GCM"
}
}
]
}
]
}
```
### Use cases
Use interim JSON to integrate with the SCANOSS platform, to feed custom analysis pipelines, to track cryptographic assets in detail, and for security auditing and compliance.
## Graph fragment export
With `--export-graph-fragment `, Crypto Finder writes a
**reusable structural graph fragment** for the scanned component. The fragment holds
the component's call graph plus crypto annotations versioned by rules. The finding-centric
call graph export above describes one scan. A fragment is different: you compose it 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):
| Version | Change |
| - | - |
| `1.1` | Per-edge resolution metadata (`resolution`, `declared_type`, `method_name`, `arity`). |
| `1.2` | Per-edge call-site data-flow (`entry_call`) and full crypto-call identity + asset metadata on `crypto_annotations`. |
| `1.3` | Customer-facing reachability projections: `crypto_entry_points`, `supporting_calls`, display aliases for constructor symbols. |
| `1.4` | Call-site object identity (`ReceiverVar`/`AssignedVar`/`ChainID`) and match columns on annotations/supporting calls, enabling cache-side object-lifecycle re-derivation. |
| `1.5` | `supporting_calls`, `crypto_entry_points`, `graph_algo_version` exposed end-to-end (paired with callgraph schema `6.2`). |
| `1.6` | Optional `resolved_receiver_type` on internal edges and external calls, which lets the stitcher disambiguate interface-dispatch groups with more than one candidate in closure. |
| `1.7` | `internal_edges_compact` + `internal_edge_strings`, a string/key-indexed compact edge encoding to keep large dependency fragments small. |
| `1.8` | `role: operation` contract methods exported as categorized supporting calls, no longer as operation-only entry points (paired with callgraph schema `6.5`). |
| `1.9` | Generic-erased function join signatures for Java source-type hierarchy stitching and optional direct-finding `purl` on crypto annotations, carried into rendered findings envelopes and finding graphs. |
| `1.10` | Optional `occurrence_key` on canonical `crypto_annotations`, propagated to stitched callgraph and findings-envelope outputs. |
| `1.11` | Optional `supporting_calls[].supporting_call.resolved_key_length` raw key-bit evidence for structurally derived configuration calls, including provenance and a source-call parameter reference, preserved to stitched callgraph output. |
| `1.12` | Optional `rule_declared_bits` and `rule_conflict` on that key-length evidence, marking a rule-declared key length that disagrees with the resolved value without overwriting either side. |
| `1.14` | Optional `functions[].entry_kind` (`main` or `framework_entry`) on a function the producing scan recognized as an entry point, and `scan_metadata.entry_kinds`, which says the scan recorded entry kinds at all. The stitched callgraph export reads them to write `root_kind` and `analysis.no_callers_only`. A fragment without `scan_metadata.entry_kinds` (`1.13` and older) still loads; its roots get no `root_kind` and its findings never claim `no_callers_only`. |
### Structure
| Field | Description |
| - | - |
| `schema_version` | Fragment schema version (currently `graph-fragment-1.14`). |
| `scan_metadata` | Ecosystem, root module, tool/rules versions, `graph_algo_version` (callgraph-construction algorithm version; cache key for annotate-only re-annotation), and per-array counts. |
| `functions[]` | Callable nodes. `key` is the stable function identity (`pkg.(Type).name#arity`); also carries `file_path`, `package`, `type`, `name`, signature, etc., and (1.14+) `entry_kind` when the scan recognized the function as an entry point. |
| `internal_edges[]` | Caller→callee edges **within** the component (both functions are in this fragment). Each edge may carry `entry_call` (1.2+, see below). |
| `internal_edges_compact[]` / `internal_edge_strings[]` | Compact (1.7+) encoding of internal edges: same fields as `internal_edges`, with repeated strings and function keys indexed into `internal_edge_strings` to keep large dependency fragments small. |
| `external_calls[]` | Calls whose target may live in **another** component; resolved at stitch time against the dependency tree. Each edge may carry `entry_call` (1.2+, see below). |
| `crypto_annotations[]` | Terminal crypto findings attached to a function. Beyond `function_key`/`finding_id`/optional `occurrence_key`/`rule_id`/`symbol`, a 1.2+ annotation carries the data-flow and metadata needed to reconstruct a findings entry (see *Crypto annotation fields (1.2+)* below). A component with no crypto still emits a fragment (zero `crypto_annotations`) so it can serve as a bridge in transitive chains. |
| `supporting_calls[]` | Non-finding config/lifecycle/context calls useful for explaining crypto behavior without increasing finding counts. |
| `crypto_entry_points[]` | Canonical reachability index: API functions plus display aliases and links to reachable findings/supporting calls. |
### 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. It uses the same model as the
finding-centric call graph export (see *Call graph export* above).
Each `entry_call.parameters[]` entry has `parameter_index`, best-effort `type`,
`argument_expression`, `resolved_value`, `variable_name` (simple identifiers
only), and recursive `source_nodes` provenance. Because the data travels **on the edge**,
the stitcher can rebuild the full per-frame value flow when it composes chains
across components, and a stitched chain matches a live run frame for frame.
### Crypto annotation fields (1.2+)
A `graph-fragment-1.2+` `crypto_annotations[]` entry carries enough to
reconstruct a full findings.json entry for the matched crypto call:
| Field | Description |
| - | - |
| `crypto_call` | Identity and call-site argument data-flow of the matched crypto call (`function_name`, `line`, `parameters[]` — same parameter model as `entry_call`). |
| `oid` | Object Identifier for the cryptographic algorithm, when known. |
| `metadata` | Raw asset metadata block from the scanner. |
| `source` | How the finding was discovered: `direct` or `indirect`. |
| `matched_operation` | Kind / symbol / `expression` of the matched crypto operation. |
| `end_line` | Last source line of the crypto finding (often equal to its start line). |
| `match` / expression | The exact source expression that triggered the detection. |
### 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 inlined `6.14`; `ScanMeta.InternedFrames` selects `6.17`). Align the render and sample budget explicitly when comparing with local CLI exports. Dep-component findings get
`module@version/`-prefixed `finding_id`s, matching live output.
* **`ToFindingsEnvelope(root, deps, fragments, meta)`** reconstructs the
findings.json v1.7 envelope (asset metadata, including direct `purl`). Its `finding_id`s are computed
with the **same inputs** as `ToCallgraphExport`, so the two agree: consumers
join assets (envelope) to call chains (callgraph) by `(finding_id, occurrence_key)` when the key is present,
or by `finding_id` for legacy records without `occurrence_key`.
`pkg/graphfrag/equiv` is a semantic diff tool. It asserts that a stitched callgraph
equals a live one, minus the chains that resolution suppression drops on purpose
(see below). The renderers rely on that equivalence guarantee.
### Edge resolution metadata (v1.1+)
Every `internal_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.
| Field | Description |
| - | - |
| `resolution` | How the target was resolved: `exact`, `interface_dispatch`, or `name_only`. Absent ⇒ treat as unresolved/untrusted. |
| `declared_type` | The static/interface type at the call site (e.g. the interface whose method was dispatched). Present on dispatch edges. |
| `method_name` | The invoked method name, independent of the resolved target. |
| `arity` | The argument count of the call. |
| `resolved_receiver_type` | (1.6+) Concrete receiver type resolved by the producer's contracts-KB / return-type inference for an interface-dispatch call site, when available. Lets the stitcher disambiguate a dispatch group with more than one candidate; empty ⇒ fail-closed behavior unchanged. |
`resolution` values:
* **`exact`**: the receiver's static type was known, and the method resolved to
a unique declared target on that type, or to an overload set on that exact type.
* **`interface_dispatch`**: the target was found by expanding an
interface or abstract method to the concrete implementations that match its name and arity
within a namespace root. It is trustworthy only when exactly one implementation is
present in the dependency closure. Otherwise it is an ambiguous guess.
* **`name_only`**: the target was guessed from the method name and arity, plus
namespace heuristics, with no receiver-type anchor. A fluent-chain fallback is one example.
`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 policy 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
```json theme={null}
{
"schema_version": "graph-fragment-1.14",
"scan_metadata": {
"ecosystem": "java",
"root_module": "org.bouncycastle:bcpkix-jdk18on",
"graph_algo_version": "graph-algo-7",
"function_count": 4000,
"internal_edge_count": 6417,
"external_call_count": 9469,
"crypto_operation_count": 160,
"supporting_call_count": 12,
"crypto_entry_point_count": 42
},
"functions": [
{
"key": "org.bouncycastle.pkcs.(PKCS8EncryptedPrivateKeyInfo).decryptPrivateKeyInfo#1",
"file_path": "org/bouncycastle/pkcs/PKCS8EncryptedPrivateKeyInfo.java"
}
],
"external_calls": [
{
"caller_key": "org.bouncycastle.pkcs.(PKCS8EncryptedPrivateKeyInfo).decryptPrivateKeyInfo#1",
"target_key": "org.bouncycastle.operator.(InputDecryptorProvider).get#1",
"line": 90,
"resolution": "exact",
"method_name": "get",
"arity": 1,
"entry_call": {
"file_path": "org/bouncycastle/pkcs/PKCS8EncryptedPrivateKeyInfo.java",
"line": 90,
"parameters": [
{
"parameter_index": 0,
"type": "org.bouncycastle.operator.InputDecryptorProvider",
"argument_expression": "inputDecryptorProvider"
}
]
}
},
{
"caller_key": "org.bouncycastle.pkcs.(PKCS8EncryptedPrivateKeyInfo).decryptPrivateKeyInfo#1",
"target_key": "org.bouncycastle.cms.(RecipientInformationStore).get#1",
"line": 90,
"resolution": "interface_dispatch",
"declared_type": "org.bouncycastle.operator.InputDecryptorProvider",
"method_name": "get",
"arity": 1
}
],
"supporting_calls": [
{
"supporting_id": "cfg123",
"function_key": "org.example.(Builder).configure#0",
"category": "config",
"matched_operation": {
"kind": "call",
"symbol": "org.example.Builder.withParameter"
}
}
],
"crypto_entry_points": [
{
"function_key": "org.example.(Facade).encrypt#1",
"function_name": "org.example.Facade.encrypt",
"display_symbol": "org.example.Facade.encrypt",
"reachable_findings": [
{
"finding_id": "abc123",
"chain_depth": 3,
"finding_graph_ref": "abc123"
}
],
"reachable_supporting_calls": [
{ "supporting_id": "cfg123", "chain_depth": 2 }
]
}
],
"crypto_annotations": [
{
"function_key": "org.bouncycastle.asn1.pkcs.(EncryptedPrivateKeyInfo).getEncryptedData#0",
"finding_id": "abc123",
"symbol": "getEncryptedData",
"source": "direct",
"end_line": 142,
"match": "getEncryptedData()",
"oid": "1.2.840.113549.1.5.13",
"matched_operation": {
"kind": "decrypt",
"symbol": "getEncryptedData",
"expression": "getEncryptedData()"
},
"crypto_call": {
"function_name": "org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo.getEncryptedData",
"line": 142,
"parameters": []
}
}
]
}
```
In this slice, `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
The CycloneDX output is a Cryptography Bill of Materials compatible with CycloneDX 1.7, for standard reporting.
### Features
The output validates against the CycloneDX 1.7 specification and works with CycloneDX ecosystem tools. Crypto Finder maps each cryptographic asset to a standard component type, including the 1.7 `key-wrap` primitive. Each component includes algorithm properties, evidence, and provenance.
### Supported asset types
| Type | Description |
| - | - |
| `algorithm` | Cryptographic algorithms (AES, RSA, SHA-256, etc.) |
| `certificate` | Digital certificates and certificate chains |
| `protocol` | Cryptographic protocols (TLS, SSH, etc.) |
| `related-crypto-material` | Keys, seeds, nonces, and other crypto material |
### Example output
```json theme={null}
{
"bomFormat": "CycloneDX",
"specVersion": "1.7",
"version": 1,
"metadata": {
"timestamp": "2025-01-15T10:00:00Z",
"tools": [
{
"vendor": "SCANOSS",
"name": "crypto-finder",
"version": "0.1.0"
}
],
"component": {
"type": "application",
"name": "scanned-project"
}
},
"components": [
{
"type": "cryptographic-asset",
"name": "AES",
"cryptoProperties": {
"assetType": "algorithm",
"algorithmProperties": {
"primitive": "block-cipher",
"mode": "CBC",
"padding": "PKCS5Padding"
}
},
"evidence": {
"occurrences": [
{
"location": "src/crypto/Example.java:29"
}
]
}
}
]
}
```
### Converting formats
Use the `convert` command to turn interim JSON into CycloneDX:
```bash theme={null}
# Convert from file
crypto-finder convert results.json --output cbom.json
# Convert from stdin (pipe from scan)
crypto-finder scan /path/to/code | crypto-finder convert --output cbom.json
# Direct output during scan
crypto-finder scan --format cyclonedx --output cbom.json /path/to/code
```
### Integration
Dependency track systems, Software Bill of Materials (SBOM) aggregators, security scanning platforms, compliance reporting tools, and supply chain risk management systems can all read CycloneDX CBOM output.
## Format comparison
| Feature | Interim JSON | CycloneDX CBOM |
| - | - | - |
| **Ecosystem** | SCANOSS-specific | Industry standard |
| **Detail level** | High (findings metadata, code snippets) | Medium (structured metadata) |
| **File size** | Larger | Smaller |
| **Best for** | Deep analysis, custom tooling | Compliance, integration, reporting |
| **Schema** | SCANOSS interim spec | CycloneDX 1.7 |
| **Validation** | SCANOSS tools | CycloneDX validators |
# Overview
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/overview
[Crypto Finder](https://github.com/scanoss/crypto-finder) is a CLI tool that detects cryptographic algorithm usage in source code repositories. It scans codebases with one of several scanning engines and writes results in standard formats, including JSON and CycloneDX.
## Why inventory cryptography
Most teams can't say with confidence which algorithms, certificates, protocols, and keys their code uses. That inventory matters for several reasons:
* Standards such as **PCI-DSS**, and frameworks published by **NIST** such as SP 800-175B, require an accurate cryptographic inventory.
* Outdated or weak cryptographic implementations introduce exploitable vulnerabilities.
* Advances in quantum computing will make many current encryption schemes obsolete.
* Manual audits are slow and inconsistent, and they don't scale to large or fast-changing codebases.
Crypto Finder automates cryptographic discovery, so teams can see what cryptography their code contains and keep it secure.
## What Crypto Finder does
Crypto Finder runs OpenGrep by default and also supports Semgrep, which adds taint analysis. It fetches curated rules from the SCANOSS API and caches them locally. You can combine those remote rules with your own local rules, and configure Crypto Finder with CLI flags, environment variables, or config files.
Results come out as interim JSON or as a CycloneDX 1.7 CBOM. Docker images let you run Crypto Finder in GitHub Actions, GitLab CI, Jenkins, and other CI/CD systems.
Dependency scanning detects cryptographic usage in third-party dependencies and traces the call chains that reach it. It supports Go, Java through Maven or Gradle, Python, Rust, and Node through npm.
The rules cache has a time-to-live (TTL). If the cache has expired and the API can't be reached, Crypto Finder falls back to the stale cache automatically. Pass `--strict` to turn that fallback off.
When only the ruleset has changed, the `annotate` command re-runs detection against a cached graph fragment. It skips the call graph rebuild, which is the expensive part of a scan.
## How it works
1. You point `crypto-finder` at a source code repository.
2. Crypto Finder detects the programming languages in it and fetches the matching cryptographic detection rules.
3. OpenGrep or Semgrep scans the code for cryptographic patterns using those rules.
4. Crypto Finder writes the results to a CycloneDX CBOM or a JSON file.
```mermaid theme={null}
graph LR
SRC["Source Code Repository"] --> CF
subgraph CF["Crypto Finder"]
LD["Language Detection"]
SCN["Scanner Engine (OpenGrep / Semgrep)"]
RP["Result Processor"]
LD --> SCN --> RP
end
CF --> OUT["Output"]
OUT --> FINAL["JSON / CycloneDX CBOM"]
%% --- Styles ---
style SRC fill:#fff,stroke:#081320,stroke-width:2px,color:#081320
style CF fill:#FFF3EA,stroke:#FF6A1A,stroke-width:2px,color:#081320
style LD fill:#FF6A1A,stroke:#E85D0F,stroke-width:2px,color:#081320
style SCN fill:#FF6A1A,stroke:#E85D0F,stroke-width:2px,color:#081320
style RP fill:#FF6A1A,stroke:#E85D0F,stroke-width:2px,color:#081320
style OUT fill:#081320,stroke:#FF6A1A,stroke-width:2px,color:#fff
style FINAL fill:#081320,stroke:#FF6A1A,stroke-width:2px,color:#fff
```
## Dead code filtering (C/C++)
In C and C++ codebases, Crypto Finder automatically excludes cryptographic findings inside statically dead preprocessor regions. These regions hold code that the compiler never includes in the final binary.
### Supported preprocessor patterns
Crypto Finder finds dead regions by evaluating these common preprocessor directives:
| Pattern | Example | Behaviour |
| - | - | - |
| `#if 0` | `#if 0 ... #endif` | Always false, so the whole block is excluded |
| `#ifdef` | `#ifdef UNDEFINED_MACRO` | Excluded when macro is not defined |
| `#ifndef` | `#ifndef DEFINED_MACRO` | Excluded when macro is defined |
| `#elif 0` | `#elif 0` | Subsequent branch treated as dead |
| `#else` | Paired with a live `#if` | Excluded when the preceding branch is live |
| Nested regions | `#if 0 { #if 1 ... } #endif` | Nested conditions are evaluated recursively |
### Why dead code is filtered
Without this filtering, code inside `#if 0` guards, or in platform-specific blocks that are never compiled, would produce false positives. Crypto Finder would report algorithms that the program never uses at runtime.
Crypto Finder applies the filter whenever it detects C or C++ files in the target directory. It needs no configuration.
## Use cases
| Use case | How Crypto Finder helps |
| - | - |
| Security auditing | Lists the cryptographic implementations in a codebase so you can check them against security standards and compliance requirements. |
| Cryptography Bill of Materials (CBOM) | Produces an inventory of cryptographic assets for regulatory compliance, for example NIST SP 800-175B or FIPS 140, and for security assessments. |
| Vulnerability management | Finds deprecated or weak algorithms, such as MD5, SHA-1, and DES, that may introduce security risk. |
| Supply chain security | Tracks cryptographic dependencies and implementations across the software supply chain. |
| Compliance reporting | Writes reports in a standard format, CycloneDX CBOM, that compliance teams and auditors can read. |
## Cryptography service
The SCANOSS Cryptography Service delivers remote cryptographic rulesets through the SCANOSS API.
### Automatic rule fetching
During each scan, Crypto Finder detects the programming languages in the target project and fetches the matching detection rules from the SCANOSS API. It caches the rules locally for up to **7 days**, the cache's time-to-live, so scans depend less on the network and run faster.
### Offline mode
When the SCANOSS API is unavailable, or the environment is air-gapped, Crypto Finder falls back to **offline mode**. It keeps scanning with the most recently cached rules.
### Flow
```mermaid theme={null}
graph TD
A["Crypto Finder Scan Engine"] -->|Online| B["SCANOSS Cryptography API"]
B --> C["Local Cache (7-day TTL)"]
A -->|Offline / Air-Gapped| C
style A fill:#FF6A1A,stroke:#E85D0F,stroke-width:2px,color:#081320
style B fill:#081320,stroke:#FF6A1A,stroke-width:2px,color:#fff
style C fill:#FFF3EA,stroke:#FF6A1A,stroke-width:2px,color:#081320
```
# Quick start
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/quick-start
Run your first cryptographic scan with Crypto Finder, then configure it and look up its main flags.
## First scan
### Basic scanning
Scan with the remote rulesets, which SCANOSS curates and maintains. This is the recommended setup:
```bash theme={null}
crypto-finder scan /path/to/code
```
Scan with local rules only:
```bash theme={null}
crypto-finder scan --no-remote-rules --rules-dir ./rules /path/to/code
```
Generate a CycloneDX Cryptography Bill of Materials (CBOM):
```bash theme={null}
crypto-finder scan --format cyclonedx --output cbom.json /path/to/code
```
### Common use cases
In CI/CD, fail the build when the scan finds anything:
```bash theme={null}
# Fail the build if findings are detected
crypto-finder scan --fail-on-findings /path/to/code
```
Combine the remote rulesets with your own rules:
```bash theme={null}
# Combine remote rulesets with local custom rules
crypto-finder scan --rules-dir ./custom-rules /path/to/code
```
Force a fresh download of the remote rulesets:
```bash theme={null}
# Bypass the cache and force a fresh download of remote rulesets
crypto-finder scan --no-cache /path/to/code
```
Convert results to a CycloneDX CBOM after a scan:
```bash theme={null}
# Convert existing scan results to CycloneDX CBOM format
crypto-finder convert results.json --output cbom.json
# Or pipe directly from a scan
crypto-finder scan /path/to/code | crypto-finder convert --output cbom.json
```
Re-annotate when the rules changed but the source didn't. Building the call graph, which means parsing and type inference, is the expensive part of a scan, and it doesn't depend on the rules. When only the detection ruleset has changed, `annotate` re-runs detection against a cached graph fragment instead of rebuilding the graph:
```bash theme={null}
# One-time: full scan, cache the structural fragment
crypto-finder scan --export-graph-fragment fragment.json /path/to/code
# Every rules change afterwards: detection-only re-annotation
crypto-finder annotate --import-fragment fragment.json --source /path/to/code --output annotation.json
```
Use `scan` when the source code has changed, and `annotate` when only the rules have changed. On a large library, a re-scan that took several minutes takes a fraction of that time with `annotate`. `annotate` also accepts `--scanner-jobs`, and honors `SCANOSS_SCANNER_JOBS`, for its OpenGrep detection process.
### Configuration
You can configure Crypto Finder with command-line flags, environment variables, or
configuration files.
```bash theme={null}
# Set an API key
crypto-finder configure --api-key YOUR_API_KEY
# Set a custom API URL
crypto-finder configure --api-url https://custom.scanoss.com
```
To use environment variables instead:
```bash theme={null}
export SCANOSS_API_KEY=your-key
export SCANOSS_API_URL=https://custom.scanoss.com
export SCANOSS_SCANNER_JOBS=2 # optional: OpenGrep jobs for scan and annotate
```
For project-level settings, use `scanoss.json`:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["node_modules/", "target/", "venv/"]
}
}
}
}
```
### Command-line arguments
```bash theme={null}
crypto-finder scan [flags]
```
Common options:
| Flag | Description |
| - | - |
| `--rules ` | Custom rule file (repeatable) |
| `--rules-dir ` | Rule directory (repeatable) |
| `--no-remote-rules` | Disable remote ruleset fetching |
| `--no-cache` | Force fresh download, bypass cache |
| `--strict` | Fail if the rules cache expired and the API is unreachable (no stale-cache fallback) |
| `--max-stale-age ` | Maximum age for stale cache fallback (default `30d`, max `90d`) |
| `--scanner ` | Scanner to use: `opengrep` (default), `semgrep` |
| `--scanner-jobs ` | Parallel jobs for the primary scan's OpenGrep process. The default `0` uses OpenGrep's own default, one job per detected core. You can also set it with `SCANOSS_SCANNER_JOBS`, and the flag wins. Lower it when several scans share a host. See [Scanner configuration](/en/latest/cli/crypto-finder/configuration#scanner-configuration) |
| `--format ` | Output format: `json` (default), `cyclonedx` |
| `--output ` | Output file path (default: stdout) |
| `--languages ` | Override language detection (comma-separated) |
| `--fail-on-findings` | Exit with error if findings detected |
| `--timeout ` | Scan timeout (default: 10m) |
| `--include-tests` | Include test sources in findings and dependency scans |
| `--no-dedup` | Disable per-line deduplication of findings |
| `--no-default-exclusions` | Disable built-in exclusions (`vendor`, `node_modules`, `shaded/`, generated protobuf stubs, ...). Combine with `--exclude` to re-add specific paths |
| `--exclude ` | Gitignore-style pattern to skip (repeatable), added on top of the defaults |
| `--detect-paths-from ` | Scope detection to the files listed in ``, without shrinking the call graph those files are traced against. The file has one path per line, relative to the target, and `-` reads stdin. See [Scoping detection to changed files](#scoping-detection-to-changed-files) |
| `--scan-dependencies` | Scan third-party dependencies for cryptographic usage (requires deps image or local toolchains) |
| `--dep-ecosystem ` | Dependency ecosystem: `auto` (default), `go`, `java`, `python`, `rust`, `node` |
| `--dep-workers ` | Parallel dependency scan workers. The default `0` uses half of the CPU cores, up to 8, and up to 2 for Java. Concurrent dependency scans share the CPU cores |
| `--include-dev-dependencies` | Also scan npm development-only dependencies, the packages marked `dev` in `package-lock.json`. By default, dependency scans cover the production tree |
| `--no-dependency-findings-api` | Scan every dependency locally instead of using the findings the SCANOSS API publishes for its package version. Applies only when an API key is configured. See [Dependency scanning](/en/latest/cli/crypto-finder/dependency-scanning#step-3-scan-dependencies-in-parallel) |
| `--findings-cache ` | Dependency findings cache backend: `disk` (default), `none`, `postgres` |
| `--export-callgraph ` | Write the finding-centric crypto call graph (reachability slices) to ``. See [Output formats](/en/latest/cli/crypto-finder/output-formats) |
| `--export-callgraph-max-chains ` | Per-finding sampled route budget for `--export-callgraph` (default `8` since v0.28.0; pass `128` for a larger sample) |
| `--export-callgraph-entry-points` | Include the optional full `crypto_entry_points` reverse-reachability index in `--export-callgraph` (default `false` since v0.28.0) |
| `--export-callgraph-interned-frames` | Emit call graph schema `6.17`, with frame identity in `functions[]` (default `true` since v0.28.0; set `false` for the inlined `6.14` schema) |
| `--export-callgraph-format ` | Call graph export format (currently only `json`) |
| `--export-callgraph-project-reachability` | When `--scan-dependencies` is off or resolved nothing, classify first-party reachability against the target's own source packages instead of reporting `not_applicable`. Leave this off when scanning a library on its own, where an uncalled function is public API rather than dead code. See [Reachability without dependency resolution](#reachability-without-dependency-resolution) |
| `--export-graph-fragment ` | Write a reusable structural graph fragment to ``, for caching or `annotate` |
| `--export-graph-fragment-format ` | Graph fragment export format (currently only `json`) |
| `--java-jdk-major ` | Java JDK major for Java dependency resolution/type enrichment: `auto`, `8`, `11`, `17`, `21` |
| `--java-jdk-home ` | Java JDK home mapping for explicit Java runtime selection (repeatable) |
| `--java-compiled-artifact ` | Compiled Java artifact used for standalone call graph/type enrichment |
| `--interfile` | Enable cross-file analysis (Semgrep Pro only) |
| `--error-format ` | Terminal error rendering: `text` (default) or `json` |
| `--progress` | Write scan lifecycle JSONL to stderr; findings remain on stdout or `--output` |
| `--verbose`, `-v` | Enable verbose (info-level) logging |
| `--quiet`, `-q` | Error-level logging only |
| `--help` | Display help information |
For a complete list of commands and options, run `crypto-finder --help`.
### Checking your version
```bash theme={null}
crypto-finder version
```
## Advanced topics
### Scoping detection to changed files
Detection, call graph construction, and dependency root discovery all share one matcher, `--exclude`. If you exclude the files a change didn't touch, they also drop out of the call graph. A changed file's own findings can then flip from reachable to unreachable, because their callers were excluded too.
`--detect-paths-from ` scopes only the detection pass. The call graph, reachability, and dependency root discovery still read the whole target, so callers outside the list are still seen and reachability stays correct. `` is a plain text file with one path per line, relative to the target. Pass `-` to read the list from stdin:
```bash theme={null}
git diff --name-only origin/main... > changed.txt
crypto-finder scan --detect-paths-from changed.txt --scan-dependencies --export-callgraph callgraph.json /path/to/repo
# or read the list from stdin
git diff --name-only origin/main... | crypto-finder scan --detect-paths-from - /path/to/repo
```
Listed files still go through `--exclude`, the default exclusions, and `.gitignore`, as in a full scan, so the scope never includes a file that a full scan would skip. The option has these limitations:
* **OpenGrep only.** Semgrep doesn't implement the scoping interface and returns `invalid_arguments`.
* With `--scan-dependencies`, Crypto Finder still scans dependencies in full. The path list scopes only the target's own files.
* Crypto Finder skips a listed file that no longer exists, such as one deleted in the diff, instead of failing. A listed directory, or a path outside the target, is an error.
* An empty or all-deleted list scans nothing. It never falls back to scanning the whole target.
### Reachability without dependency resolution
Without `--scan-dependencies`, or when it resolves no dependencies, crypto-finder has no set of "user packages" to classify reachability against. Every finding, including those in your own first-party code, then reports `not_applicable` instead of `reachable` or `unreachable`. This is a problem for an incremental or CI re-scan that skips dependency resolution on purpose because no manifest or lockfile changed.
`--export-callgraph-project-reachability` builds that set from the target's own source packages instead. First-party findings then get the same `reachable` or `unreachable` verdicts that a full `--scan-dependencies` run would give them. The flag never classifies findings in dependency code, which still need `--scan-dependencies`. It also treats every source file under the target as first-party. A vendored package that the default exclusions don't cover is therefore misclassified as project code instead of being attributed to a dependency.
Leave this flag off when scanning a library on its own. A function with no
caller in that case is public API, not dead code, and treating the library
as a self-contained project would misclassify it as unreachable.
### Features
Crypto Finder supports OpenGrep (recommended) and Semgrep as configurable scan engines. Semgrep adds taint analysis, which tracks data flow across procedures.
In C and C++ codebases, Crypto Finder removes findings inside statically dead preprocessor regions, such as `#if 0`, `#ifdef`, and `#ifndef` blocks, to avoid false positives. It does this automatically whenever it detects C or C++ files, with no extra flags.
Call graph construction and `--export-callgraph` reachability cover C, C++, Go, Java, JavaScript/TypeScript, Python, and Rust. Recursive dependency scanning (`--scan-dependencies`) currently resolves third-party packages for Go, Java (Maven/Gradle), Python (pip), Rust (Cargo), and Node (npm). See [Dependency scanning](/en/latest/cli/crypto-finder/dependency-scanning#supported-ecosystems).
Crypto Finder fetches SCANOSS-maintained rulesets from the SCANOSS API and caches them locally. If the remote is unavailable, it uses the local cache instead. You can combine the remote rulesets with local custom rules, and configure Crypto Finder with CLI flags, environment variables, or a `scanoss.json` configuration file.
Output is JSON or a CycloneDX 1.7 CBOM. Official Docker images run in GitHub Actions, GitLab CI, Jenkins, and other CI/CD platforms.
The remote ruleset cache has a configurable time-to-live (TTL). When the remote is unavailable, Crypto Finder falls back to expired cache entries. Pass `--strict` to turn that fallback off.
# Configuration
Source: https://docs.scanoss.com/en/latest/cli/scanoss-go/configuration
Store credentials, proxy and CA settings once with scanoss-cli config, instead of passing flags on every run.
## Storing your API key
```bash theme={null}
scanoss-cli config set api-key SC_abc123def456
scanoss-cli scan ./my-project --output results.json
```
Settings use the same names as the flags: `api-key`, `api-url`, `proxy`, and `ca-cert`. They are
stored in `~/.scanoss/settings.json`:
```json theme={null}
{
"api_key": "SC_abc123def456",
"api_url": "https://api.scanoss.com",
"proxy": "http://proxy.example.com:8080",
"ca_cert": "/etc/ssl/corp-ca.pem"
}
```
The command line always uses the dashed names. The file stores keys in `snake_case`, but that is only
the file's format, and you can't type a key that way on the command line.
## Precedence
Every setting resolves the same way, and each has a matching flag:
```
--flag > environment variable > ~/.scanoss/settings.json > built-in default
```
The environment variable is the setting name in upper case with a `SCANOSS_` prefix:
`SCANOSS_API_KEY`, `SCANOSS_API_URL`, `SCANOSS_PROXY`, `SCANOSS_CA_CERT`. The CLI treats an empty
value from the environment or the file as unset and moves on to the next source.
```bash theme={null}
scanoss-cli config set api-url https://scanoss.internal.example.com
scanoss-cli scan . # 1. stored value wins
SCANOSS_API_URL=https://scanoss.staging.example.com scanoss-cli scan . # 2. env overrides file
SCANOSS_API_URL=https://scanoss.staging.example.com \
scanoss-cli scan . --api-url https://api.scanoss.com # 3. flag overrides both
```
`--verbose` reports which source won for each setting. It prints the source only, never the key's
value.
## Inspecting configuration
```console theme={null}
$ scanoss-cli config list
api-key ******** (env: SCANOSS_API_KEY)
api-url https://scanoss.internal.example.com (config file)
Config file: /Users/you/.scanoss/settings.json
```
The CLI never prints the API key. `list` and `get` always show it as `********`, and no flag
reveals it, so it can't end up in your shell history or a CI log. `config get api-key` only
tells you whether the key is set, through its exit code: `0` if set, `1` if not. Scripts that
need the value should use `$SCANOSS_API_KEY`. To read your own file directly, run
`cat "$(scanoss-cli config path)"`.
Non-secret values print normally:
```console theme={null}
$ scanoss-cli config get api-url
https://scanoss.internal.example.com
```
## On-prem endpoint
A custom API URL may not require an API key, so pointing the CLI at an internal deployment takes
one command:
```bash theme={null}
scanoss-cli config set api-url https://scanoss.internal.example.com
scanoss-cli scan .
```
## Proxy and custom CA
The CLI honours `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` without any flags. `--proxy` overrides
them for one run, and `--ca-cert` trusts a CA that the system pool doesn't have:
```bash theme={null}
scanoss-cli scan . --proxy http://proxy.example.com:8080
scanoss-cli scan . --ca-cert /etc/ssl/corp-ca.pem
```
`--ca-cert` *adds* the CA to the system pool, so the public API keeps working and TLS verification
stays on. `--ignore-cert-errors` is different. It turns off TLS verification entirely, which is
insecure, so use it only to test against a self-signed internal endpoint. `--proxy` and
`--ca-cert` work on every command that reaches the API. The CLI doesn't support proxy
auto-configuration (PAC). Read the proxy address from the PAC file and pass it with `--proxy`.
You can store both settings, so you don't have to repeat either flag:
```bash theme={null}
scanoss-cli config set proxy http://proxy.example.com:8080
scanoss-cli config set ca-cert /etc/ssl/corp-ca.pem
scanoss-cli scan .
```
A stored `proxy` takes precedence over `HTTP_PROXY` and `HTTPS_PROXY`. You can't store
`--ignore-cert-errors`, so turning off verification is always a choice you make for one run.
## CI
In CI, set the key in the environment instead of a config file. You don't need `config set`, and
the key stays off the command line, where it would end up in build logs:
```yaml theme={null}
- name: SCANOSS scan
env:
SCANOSS_API_KEY: ${{ secrets.SCANOSS_API_KEY }}
run: scanoss-cli scan . --output results.json
```
## Rotating and removing
```bash theme={null}
scanoss-cli config set api-key SC_newkey789 # overwrite in place
scanoss-cli config unset api-key # remove the key
scanoss-cli config path # print the file location
```
You can edit the file by hand. `config set` leaves keys that the current version doesn't
recognize untouched.
# Decoration and enrichment
Source: https://docs.scanoss.com/en/latest/cli/scanoss-go/decoration-and-enrichment
Query SCANOSS component data by PURL, or refresh an existing inventory's layers without re-scanning.
## Enrich an existing inventory
`enrich` decorates an existing inventory or SBOM with purl-keyed layers through the SCANOSS API.
It needs no source tree, and it doesn't fingerprint or re-scan anything. Because it works only from
PURLs, you can re-run it on the same file, weekly for example, to refresh the layers.
```bash theme={null}
# Refresh vulns/licenses/crypto on a raw inventory (raw in, raw out)
scanoss-cli enrich inv.json --include vulns,licenses,crypto --api-key "$SCANOSS_API_KEY" > enriched.json
# Enrich an SPDX document (spdx in, spdx out)
scanoss-cli enrich sbom.spdx.json --include licenses --api-key "$SCANOSS_API_KEY" > enriched.spdx.json
# Enrich a CycloneDX document and convert to SPDX in one pass
scanoss-cli enrich sbom.cdx.json --include licenses --format spdx --api-key "$SCANOSS_API_KEY" > enriched.spdx.json
```
`enrich` accepts a SCANOSS raw inventory, a CycloneDX document, or an SPDX document, all in JSON,
and detects which one it has from the content. The layers you can pass to `--include` are `vulns`,
`licenses`, `crypto`, and `geo`. `deps` is **not** an enrich layer. Dependency analysis needs
manifests or a source tree, so `--include deps` returns an error here.
By default the output keeps the input's format, so raw stays raw, CycloneDX stays CycloneDX, and
SPDX stays SPDX. Pass `-f, --format` to convert in the same pass. If the output format can't
represent a layer, `enrich` skips that layer up front and prints a notice, following the same rules
as `scan`. `spdx` skips `vulns`, `crypto`, and `geo`, and `cyclonedx` skips `crypto` and `geo`. A
failed service doesn't stop enrichment. `enrich` logs the failure, skips that service, and still
writes a partial result.
`enrich` takes `--include` and `-f, --format` (`raw`, `spdx`, or `cyclonedx`), plus the
[API flags](/en/latest/cli/scanoss-go/scanning-your-project#flags-reference) (`--api-url`,
`--api-key`, `--proxy`, `--ca-cert`, `--ignore-cert-errors`, `-o, --output`).
## Dependencies
`dependencies` has two modes. Local mode parses the manifests under a path. API mode looks up one
component by PURL:
```bash theme={null}
# Local mode: parse manifests under a path and query the API
scanoss-cli dependencies ./my-project --extract-local --output deps.json
# API mode: direct dependencies of a component
scanoss-cli dependencies --purl 'pkg:github/scanoss/engine' --requirement '5.4.7' \
--api-key "$SCANOSS_API_KEY"
# API mode: transitive dependencies, custom depth/limit
scanoss-cli dependencies --purl 'pkg:github/scanoss/engine' --requirement '5.4.7' --transient \
--depth 5 --limit 20 --api-key "$SCANOSS_API_KEY"
```
| Flag | Default | Notes |
| - | - | - |
| `--extract-local` | `false` | Local mode: parse manifest files under the path |
| `--settings` | auto-detected | `scanoss.json`. Its `skip` rules apply under the `dependencies` key |
| `--purl` | — | API mode: the component to query |
| `--requirement` | — | API mode: version or range (optional) |
| `--transient` | `false` | API mode: walk transitive dependencies |
| `--depth` | `10` | Transitive walk depth (with `--transient`) |
| `--limit` | `10` | Transitive result limit (with `--transient`) |
`dependencies` also takes the API flags.
## Decoration lookups
Each decoration command queries the SCANOSS v3 API about one or more components. Each one is a
parent command with one subcommand per operation, and running a command with no subcommand uses its
default operation. The input is a list of PURLs, which you give by repeating `--purl` or by passing
`--input`. The CLI splits the list into chunks and queries them concurrently.
All decoration commands share these flags: `--purl` (repeatable), `--requirement` (version or
range), `--input` (a PURL file, either newline-delimited `purl[,requirement]` or JSON
`{"components":[...]}`), `--chunk-size` (10), and `-t, --workers` (5). They also take the API flags.
| Command | Subcommands |
| - | - |
| `vulnerabilities` | `components` (default), `cpes` |
| `cryptography` | `algorithms` (default), `algorithms-range`, `versions-range`, `hints`, `hints-range` |
| `licenses` | `declared` (default), `attribution`, `evidence` |
| `geoprovenance` | `origin` (default), `countries` |
| `copyright` | `evidence` (default), `holders` |
| `components` | `search` (default), `versions`, `releases`, `status` |
```bash theme={null}
scanoss-cli vulnerabilities --purl 'pkg:github/scanoss/engine' --api-key "$SCANOSS_API_KEY"
scanoss-cli cryptography algorithms-range --purl 'pkg:github/scanoss/engine' --requirement '>5.0.0' --api-key "$SCANOSS_API_KEY"
scanoss-cli licenses attribution --purl 'pkg:github/scanoss/engine' --api-key "$SCANOSS_API_KEY"
scanoss-cli geoprovenance countries --purl 'pkg:github/scanoss/engine' --api-key "$SCANOSS_API_KEY"
scanoss-cli copyright holders --purl 'pkg:github/scanoss/engine' --api-key "$SCANOSS_API_KEY"
```
`components search`, `versions` and `releases` take their own flags instead of a PURL list:
```bash theme={null}
# Search by vendor/component/term
scanoss-cli components --vendor scanoss --component engine --limit 20 --api-key "$SCANOSS_API_KEY"
# Known versions (with licences) for a purl
scanoss-cli components versions --purl 'pkg:github/scanoss/engine' --limit 50 --api-key "$SCANOSS_API_KEY"
# Release notes: all, a single version, or a semver range
scanoss-cli components releases --purl 'pkg:github/scanoss/engine' --requirement '>=1.0.0, <=2.0.0' --api-key "$SCANOSS_API_KEY"
# Lifecycle status for a PURL
scanoss-cli components status --purl 'pkg:github/scanoss/engine' --requirement '1.2.3' --api-key "$SCANOSS_API_KEY"
# Free-text search, paginated
scanoss-cli components search --search engine --purl-type github --limit 20 --offset 0 --api-key "$SCANOSS_API_KEY"
```
| Subcommand | Flags |
| - | - |
| `components search` | `--search`, `--vendor`, `--component` (at least one), `--purl-type` (default `github`), `--limit`, `--offset` |
| `components versions` | `--purl`, `--limit` (`0` = server default) |
| `components releases` | `--purl` (required), `--requirement` (exact version or semver range), `--limit`, `--offset`. With no `--requirement`, lists all releases. |
| `components status` | Shared decoration flags (PURL list) |
When a component exists but has no release notes for the resolved version, `components releases`
prints a "no release notes available" notice on stderr, still emits the JSON, and exits `0`.
# Installation
Source: https://docs.scanoss.com/en/latest/cli/scanoss-go/installation
scanoss-cli is a Go command-line tool and SDK for scanning source code and querying the SCANOSS platform. It fingerprints a project, uploads the fingerprints to the SCANOSS v3 API, and decorates the results with vulnerability, licence, cryptography, geoprovenance, copyright, and dependency data.
[scanoss.go](https://github.com/scanoss/scanoss.go) is SCANOSS's Go CLI and SDK. It is the CLI we
recommend from now on for customers who integrate SCANOSS into their pipelines. It installs as the
`scanoss-cli` binary, named after its Go package directory. The name also avoids a clash on your
`PATH` with the SCANOSS scan engine, which is called `scanoss`.
`scanoss-cli` talks to the SCANOSS v3 API and is not compatible with OSSKB.org. If you
depend on OSSKB.org, use [SCANOSS-PY](/en/latest/cli/scanoss-py/installation) instead.
## Prerequisites
* [Go 1.25+](https://go.dev/dl/). You need it only if you install with `go install` or build
from source.
## Installation
### go install
```bash theme={null}
go install github.com/scanoss/scanoss.go/cmd/scanoss-cli@latest
```
### Prebuilt binary
Download the archive for your platform from the
[releases page](https://github.com/scanoss/scanoss.go/releases), extract it, and move the
`scanoss-cli` binary onto your `PATH`:
```bash theme={null}
# Linux (amd64) — adjust the archive name for your OS/arch
tar xzf scanoss-cli-linux-amd64.tar.gz
sudo mv scanoss-cli /usr/local/bin/
```
On Windows, unzip the archive and add the folder to your `PATH`. On macOS, Gatekeeper may
quarantine an unsigned direct download. To clear the quarantine, run:
```bash theme={null}
xattr -d com.apple.quarantine ./scanoss-cli
```
Verify a download against the published checksums:
```bash theme={null}
sha256sum -c --ignore-missing checksums.txt
```
### Docker
Every release publishes multi-arch images for `linux/amd64` and `linux/arm64` to GitHub Container
Registry:
```bash theme={null}
docker run --rm -v "$PWD:/src" ghcr.io/scanoss/scanoss:latest \
scan /src --api-key "$SCANOSS_API_KEY" > results.json
```
Use `:latest` or a specific version tag such as `:0.9.0`.
[Using Docker](/en/latest/cli/scanoss-go/using-docker) explains a permission issue with writing
output and has more examples.
### Build from source
```bash theme={null}
git clone https://github.com/scanoss/scanoss.go.git
cd scanoss.go
make build # or: go build -o scanoss-cli ./cmd/scanoss-cli
```
### Go SDK
The same repository is also a Go SDK, `github.com/scanoss/scanoss.go/pkg/scanoss`. It provides
scanning, a parallel decoration pipeline over PURLs, and a client for each service. See the
[repository README](https://github.com/scanoss/scanoss.go#go-sdk--decoration-pipeline) for usage.
## Verify installation
```bash theme={null}
scanoss-cli --version
scanoss-cli --help
```
## What's next
* Scan a project in one command with the [quick start](/en/latest/cli/scanoss-go/quick-start).
* Store your API key once instead of passing it on every command. See
[Configuration](/en/latest/cli/scanoss-go/configuration).
# Output formats
Source: https://docs.scanoss.com/en/latest/cli/scanoss-go/output-formats
The raw, SPDX 2.3, and CycloneDX 1.7 formats that scanoss-cli writes, and how to convert SBOMs offline.
## Formats
```bash theme={null}
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" --format raw --output results.json
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" --format spdx --output sbom-spdx.json
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" --format cyclonedx --output sbom-cdx.json
```
| Format | Notes |
| - | - |
| `raw` | **Default.** The neutral inventory: components tagged by `scope`, per-component layers inline, a flat vulnerabilities list, wrapped in a versioned envelope. Renders every `--include` layer. |
| `spdx` | SPDX 2.3. Drops `vulns`/`crypto`/`geo` layers. Multiple licences on a component are combined with `AND`. |
| `cyclonedx` | CycloneDX 1.7, with licences, evidence, and vulnerabilities. Drops `crypto`/`geo` layers. |
The CLI merges components with the same identity, meaning the same PURL and version, into one. A
package listed in both `package.json` and `package-lock.json`, or one that is both detected and
declared, appears once. Different versions of the same PURL stay separate.
## Converting formats offline
The `sbom` command converts between formats without contacting the API. It detects the input
format from the file's content:
```bash theme={null}
# SPDX -> CycloneDX
scanoss-cli sbom bom.spdx.json --format cyclonedx --output bom.cdx.json
# CycloneDX -> SPDX
scanoss-cli sbom bom.cdx.json --format spdx --output bom.spdx.json
# scanoss raw result -> CycloneDX or SPDX
scanoss-cli sbom results.json --format cyclonedx --output bom.cdx.json
```
Conversion is **best-effort**. The CLI drops any data the target format can't represent and prints
a warning. For example, SPDX 2.3 has no vulnerability model, so converting to `spdx` leaves out the
vulnerabilities.
`sbom` takes `-f, --format` (`cyclonedx` or `spdx`) and `-o, --output`.
# Quick start
Source: https://docs.scanoss.com/en/latest/cli/scanoss-go/quick-start
Scan a project, generate fingerprints only, or refresh an existing inventory with scanoss-cli.
## Scan a project
```bash theme={null}
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" --output results.json
```
The default endpoint, `https://api.scanoss.com`, requires an API key. A custom API URL, such as
an on-prem deployment, may not require one. See
[Configuration](/en/latest/cli/scanoss-go/configuration#on-prem-endpoint).
## Generate fingerprints only
This uploads nothing and needs no API key:
```bash theme={null}
scanoss-cli wfp ./my-project > project.wfp
```
## Refresh an existing inventory
Add or update the vulnerability, licence, crypto, and geoprovenance layers on a result you already
have, without re-scanning:
```bash theme={null}
scanoss-cli enrich results.json --include vulns,licenses --api-key "$SCANOSS_API_KEY" > enriched.json
```
## Debug logging
Add `-v` or `--verbose` to any command to write structured debug logs to stderr. The logs cover
the scan flow, the fingerprinting detail, and each API request with its method, URL, status, and
duration. Stdout carries only results, so logs never corrupt `--output` or piped JSON.
```bash theme={null}
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" --verbose
```
## Commands at a glance
| Command | Purpose |
| - | - |
| `scan ` | Fingerprint a folder/file, scan against the SCANOSS v3 API, and output results. |
| `scan wfp ` | Scan a pre-generated WFP file (no fingerprinting). |
| `wfp ` | Generate WFP fingerprints only (no upload). |
| `results ` | Resume or poll a scan by its id. |
| `sbom ` | Produce an SBOM from a raw inventory, or convert between formats, offline. |
| `enrich ` | Add purl-keyed layers (vulns/licenses/crypto/geo) to a raw or SBOM file. |
| `dependencies [path]` | Extract local dependencies, or query direct/transitive deps for a PURL. |
| `vulnerabilities`, `cryptography`, `licenses`, `geoprovenance`, `copyright`, `components` | Decoration lookups by PURL. |
| `config` | Store settings in `~/.scanoss/settings.json`. |
[Scanning your project](/en/latest/cli/scanoss-go/scanning-your-project) and
[Decoration and enrichment](/en/latest/cli/scanoss-go/decoration-and-enrichment) cover each command
in full. You can also run `scanoss-cli --help`.
## What's next
* Store your API key, proxy, and CA settings once. See
[Configuration](/en/latest/cli/scanoss-go/configuration).
* Choose between `raw`, SPDX 2.3, and CycloneDX 1.7 output. See
[Output formats](/en/latest/cli/scanoss-go/output-formats).
# Scanning your project
Source: https://docs.scanoss.com/en/latest/cli/scanoss-go/scanning-your-project
Scan flags, file skipping, and BOM rules for scanoss-cli.
## Scanning
```bash theme={null}
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY"
scanoss-cli scan ./src/main.go --api-key "$SCANOSS_API_KEY"
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" --output results.json
```
`scan` fingerprints the target, uploads the WFP in blocks to `POST /v3/wfp/scan`, and polls
`GET /v3/wfp/scan/` until the scan completes. It prints the scan id as soon as the upload
finishes, so you can resume an interrupted scan with [`results`](#resuming-a-scan).
### Tuning throughput
```bash theme={null}
scanoss-cli scan ./my-project \
--api-key "$SCANOSS_API_KEY" \
--threads 20 \
--chunk-size 2097152 # 2 MiB upload blocks (default 1 MiB)
```
### Keeping the generated WFP
```bash theme={null}
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" --save-wfp project.wfp
```
The CLI writes the copy as the scan generates it, in completion order, and that order varies
between runs.
Use the standalone [`wfp`](/en/latest/cli/scanoss-go/quick-start#generate-fingerprints-only)
command when you need byte-reproducible output to diff or hash.
### Scanning a pre-generated WFP
```bash theme={null}
scanoss-cli scan wfp project.wfp --api-key "$SCANOSS_API_KEY"
```
## Resuming a scan
```bash theme={null}
scanoss-cli results --api-key "$SCANOSS_API_KEY" --output results.json
```
`results` works after a Ctrl+C because the uploaded WFP is resumable. A resumed scan applies the
same rules as a direct one, so `results` accepts `--settings`, `-i`/`-n`, `--ranking-threshold`, `--include`
(`vulns`, `licenses`, `crypto`, `geo`), `-f, --format` and `--poll-interval`, plus the
[API flags](#flags-reference).
## Flags reference
The API flags work on every command that reaches the API: `scan`, `scan wfp`, `results`,
`enrich`, `dependencies`, and the decoration commands.
| Flag | Default | Notes |
| - | - | - |
| `--api-url` | `https://api.scanoss.com` | Default endpoint requires `--api-key` |
| `--api-key` | — | |
| `--proxy` | — | Overrides `HTTP_PROXY`/`HTTPS_PROXY`. See [Configuration](/en/latest/cli/scanoss-go/configuration#proxy-and-custom-ca) |
| `--ca-cert` | — | PEM file added to the system trust pool |
| `--ignore-cert-errors` | `false` | Disables TLS verification (insecure) |
| `-o, --output` | stdout | |
`scan` and `scan wfp` share the scan flags.
| Flag | Default | Notes |
| - | - | - |
| `-f, --format` | `raw` | `raw` / `spdx` / `cyclonedx` |
| `--include` | — | Extra output layers. See [Output layers](#output-layers). `scan wfp` accepts all except `deps` |
| `--settings` | auto-detected `scanoss.json` | |
| `--chunk-size` | `1048576` (1 MiB) | WFP upload block size, bytes |
| `--poll-interval` | `2s` | Scan status poll cadence |
| `-i, --identify `, `-n, --ignore ` | — | A component list, CycloneDX, SPDX, or raw result file. See [BOM rules](#bom-rules) |
| `--ranking-threshold` | `-1` (off) | `-1`..`10`; `-1` and `0` both mean off |
The fingerprinting flags work only on `scan ` and the standalone
[`wfp`](#fingerprinting-only) command. `scan wfp` receives a WFP that is already assembled, so it
doesn't accept them, and passing `--skip-headers` or `--skip-headers-limit` to it is an error.
| Flag | Default | Notes |
| - | - | - |
| `-t, --threads` | `10` | Fingerprint workers |
| `--save-wfp` | — | Keep the generated WFP alongside the scan (`scan` only) |
| `--min-size` / `--max-size` | `0` / `0` | Bytes. `0` max means unlimited |
| `--gitignore` | `true` | |
| `--all-extensions`, `--all-folders`, `--all-hidden` | `false` | See [Skipping files](#skipping-files) |
| `--skip-headers` | `true` | Drops each file's leading licence header, comments and imports from its fingerprint |
| `--skip-headers-limit` | `0` (no limit) | |
`--skip-headers` changes the fingerprint. A WFP taken with it off won't match one taken with it
on, so keep the setting consistent across runs you intend to compare.
## Fingerprinting only
`wfp` generates fingerprints without contacting the API, so it needs no key:
```bash theme={null}
scanoss-cli wfp ./my-project > project.wfp
scanoss-cli wfp ./src/main.go -o main.wfp
scanoss-cli wfp ./my-project --threads 20 --min-size 100
```
It accepts the fingerprinting flags above plus `-o, --output` and `--settings`. Unlike `scan --save-wfp`, its output is byte-reproducible, so use it when you need to diff or hash a WFP.
## Skipping files
Before scanning, the CLI filters out build directories, vendored dependencies, generated and
binary files, model weights (`.safetensors`, `.gguf`, `.onnx`, `.pt`, `.bin`), and oversized files.
There is **no minimum file size by default**. Set one with `--min-size`:
```bash theme={null}
# Skip files below 100 bytes
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" --min-size 100
# Keep files between 100 bytes and 1 MiB
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" --min-size 100 --max-size 1048576
# Fingerprint everything the built-in lists would drop, and ignore .gitignore
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" \
--all-extensions --all-folders --gitignore=false
```
`--all-extensions` turns off the built-in *file* rules, which match extensions, name endings, and
exact names. `--all-folders` turns off the *directory* rules. They are separate switches because
someone who wants every folder scanned rarely wants every binary fingerprinted too. `--all-hidden`
includes dotted entries, `.git` among them.
To set skip rules for the whole project, use `scanoss.json`:
```json theme={null}
{
"settings": {
"skip": {
"patterns": { "scanning": ["dist/**", "**/*.min.js", "docs/"] },
"sizes": { "scanning": [{ "patterns": ["*.bin"], "min": 0, "max": 1048576 }] }
}
}
}
```
You can use the flags and `scanoss.json` together. `--min-size` and `--max-size` apply to every
file, while a `skip.sizes` rule applies only to the files its `patterns` match.
## BOM rules
Configure a Bill of Materials in `scanoss.json`. The CLI finds the file in the target
automatically, or you can pass its path with `--settings`:
```json theme={null}
{
"bom": {
"include": [{ "purl": "pkg:github/scanoss/engine" }],
"remove": [{ "purl": "pkg:github/scanoss/scanoss" }],
"replace": [{ "purl": "pkg:github/wrong/lib", "replace_with": "pkg:github/right/lib@2.1.0" }]
}
}
```
```bash theme={null}
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" --settings my-config.json
```
The CLI applies every rule on the client side, after results come back, in this order:
1. **`bom.ignore`** (alias `bom.exclude`) drops the components it names from each file's
matches.
2. **`bom.identify`** (alias `bom.include`) marks the components it names as `"identified":
true` and moves them to the front of their file's matches.
3. **`--ranking-threshold`** drops matches whose component ranks worse than the threshold.
4. **`bom.remove`** neutralizes whole files.
5. **`bom.replace`** re-points the survivors at their `replace_with` component.
Details that change results:
* **Scoping.** An entry may be scoped by `purl`, by `path`, or both. Where several entries cover
the same file, the most specific one wins:
```json theme={null}
{ "bom": { "identify": [{ "purl": "pkg:npm/vue@2.6.14", "path": "vendor/vue/" }] } }
```
* **Canonical PURLs only.** `bom.ignore` and `bom.identify` match a component's canonical PURL,
not its aliases. `bom.remove` and `bom.replace` do match aliases.
* **`bom.identify` wins.** It protects its PURLs from every filter in this list: `bom.ignore`,
`bom.remove`, and the ranking threshold.
* **Ranking threshold.** Rank is how well a component explains a match; lower is stronger, and
real ranks run from 1 to 9. Matches with no rank, or the engine's `999` "no ranking information"
sentinel, are never filtered.
* **Where `identified` shows up.** The CLI writes the flag on each matched file's evidence, and on the
component as a summary, because a `path`-scoped rule claims a component only in the files it
covers. CycloneDX output carries the component-level flag as a `scanoss:identified` property.
Only `raw` output has the per-file detail.
`-i, --identify ` and `-n, --ignore ` on the command line add unscoped entries to the
same rules, and you can combine them with `--settings`. Each takes a file, which can be a SCANOSS
component list (`{"components":[{"purl":"..."}]}`), CycloneDX, SPDX, or this CLI's own `raw`
output. The CLI detects the type from the content.
## Snippet settings in scanoss.json
`scanoss.json` can also carry fingerprinting and filtering settings:
```json theme={null}
{
"settings": {
"file_snippet": {
"skip_headers": true,
"skip_headers_limit": 50,
"ranking_threshold": 5
}
}
}
```
These keys **override** the `--skip-headers`, `--skip-headers-limit` and `--ranking-threshold`
flags. That is the reverse of every other setting in this CLI. SCANOSS-PY behaves the same way,
so both clients read one settings file identically. If a flag seems to have no effect, check
for the key in `scanoss.json` and remove it to let the flag win.
`ranking_threshold` accepts values from `-1` to `10`, where `-1` and `0` both mean off. The CLI
clamps out-of-range values and prints a warning. Other `file_snippet` keys, such as
`min_snippet_hits`, `ranking_enabled`, and `honour_file_exts`, tune the matching engine. This CLI
ignores them because it doesn't forward scan settings to the server.
## Output layers
By default a scan reports only the components it detected. `--include` adds extra layers, which
cover both detected and declared components:
| Layer | What it adds |
| - | - |
| `deps` | Declared dependencies parsed from the project's manifests and resolved. Needs a source tree, so `scan wfp` rejects it with an error. |
| `vulns` | Known vulnerabilities. |
| `licenses` | Declared/concluded licences per component. |
| `crypto` | Cryptographic algorithms. |
| `geo` | Contributor geographic provenance. |
```bash theme={null}
scanoss-cli scan ./my-project --api-key "$SCANOSS_API_KEY" --include deps,vulns,licenses
```
If the chosen `--format` can't represent a layer, the CLI doesn't gather that layer and prints a
message up front. `raw` renders every layer, `cyclonedx` drops `crypto` and `geo`, and `spdx` drops
`vulns`, `crypto`, and `geo`.
# Using Docker
Source: https://docs.scanoss.com/en/latest/cli/scanoss-go/using-docker
Run scanoss-cli from a container, with no local Go install.
## Running a scan
Every release publishes multi-arch images for `linux/amd64` and `linux/arm64` to GitHub Container
Registry. Mount the code you want to scan and pass CLI arguments after the image name:
```bash theme={null}
docker run --rm -v "$PWD:/src" ghcr.io/scanoss/scanoss:latest \
scan /src --api-key "$SCANOSS_API_KEY" > results.json
```
Use `:latest`, or pin a version in CI with a specific tag such as `:0.9.0`.
## Writing output into the mounted folder
The image runs as a non-root user, so writing output directly into the mounted folder (`--output
/src/results.json`) can fail with a permission error. Either redirect stdout on the host, as
above, or run the container as your own user so that you own the files it writes:
```bash theme={null}
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/src" \
ghcr.io/scanoss/scanoss:latest scan /src --api-key "$SCANOSS_API_KEY" --output /src/results.json
```
## CI example
```yaml theme={null}
- name: SCANOSS scan
run: |
docker run --rm -v "$PWD:/src" ghcr.io/scanoss/scanoss:latest \
scan /src --api-key "${{ secrets.SCANOSS_API_KEY }}" --output /src/results.json
```
# v3 API overview
Source: https://docs.scanoss.com/en/latest/developer-tools/api-overview
Base URL, authentication, request and response conventions, PURL-keyed batch requests, and the fingerprint scan flow of the public SCANOSS v3 API.
The SCANOSS v3 API is the public HTTP API behind scanoss-cli. Use it directly when you build your
own client, or when you need SCANOSS component data inside another service. If you only need
results in a pipeline, [scanoss-cli](/en/latest/cli/scanoss-go/installation) already implements
everything on this page.
The **API Reference** group in the sidebar lists every endpoint with its request and response
schema.
## Base URL
```
https://api.scanoss.com
```
All endpoints are under the `/v3/` prefix. An on-premise deployment exposes the same paths under
its own base URL.
## Authentication
Send your API key in the `x-api-key` header on every request:
```bash theme={null}
curl -sS "https://api.scanoss.com/v3/components/status" \
--get --data-urlencode "purl=pkg:github/scanoss/engine" \
-H "x-api-key: $SCANOSS_API_KEY"
```
HTTP header names are not case-sensitive, so `X-Api-Key` works too. The API rejects a missing or
invalid key with HTTP `401`. To get a key, see
[API keys and authentication](/en/latest/developer-tools/authentication).
## What the API covers
| Area | Example endpoints | Returns |
| - | - | - |
| Vulnerabilities | `/v3/vulnerabilities/vulnerabilities`, `/v3/vulnerabilities/cpes` | Known CVEs (with CVSS and EPSS data) and CPEs per component |
| Licences | `/v3/licenses`, `/v3/license/attribution`, `/v3/license/evidence` | Declared licences, attribution files, per-file licence evidence |
| Cryptography | `/v3/cryptography/algorithms`, `/v3/cryptography/hints` | Algorithms and crypto libraries per component version or version range |
| Components | `/v3/components/search`, `/v3/components/versions`, `/v3/components/status`, `/v3/components/releases` | Search, known versions, lifecycle status, release notes |
| Dependencies | `/v3/dependencies/dependencies`, `/v3/dependencies/transitive` | Declared and transitive dependencies |
| Geoprovenance | `/v3/geoprovenance/countries`, `/v3/geoprovenance/origin` | Contributor locations and origin distribution |
| Copyright | `/v3/copyright/evidence`, `/v3/copyright/holders` | Copyright statements and holders |
| Scanning | `/v3/wfp/scan`, `/v3/wfp/scan/{id}` | Open source matches for fingerprinted source code |
## Identify components by PURL
Component data is keyed by [Package URL (PURL)](https://github.com/package-url/purl-spec), for
example `pkg:github/scanoss/engine` or `pkg:npm/lodash`. An optional `requirement` narrows the
request to a version or, on range endpoints, a version range:
| Field | Required | Example |
| - | - | - |
| `purl` | Yes | `pkg:github/scanoss/engine` |
| `requirement` | No | `v5.4.5` |
Most lookup endpoints accept both forms:
* **`GET` for one component.** Pass `purl` and `requirement` as query parameters.
* **`POST` for many components.** Send a JSON body with a `components` array. This is the batch
form.
```bash theme={null}
curl -sS https://api.scanoss.com/v3/vulnerabilities/vulnerabilities \
-H "x-api-key: $SCANOSS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"components": [
{ "purl": "pkg:github/madler/zlib", "requirement": "1.2.11" },
{ "purl": "pkg:npm/lodash" }
]
}'
```
A batch request must contain at least one component. To send a list larger than you want in one
request, split it into chunks and send the chunks in parallel. scanoss-cli does this with a
default chunk size of 10 PURLs.
### Search options
A batch body can also carry a `search` object, and the same options are available as query
parameters on `GET` requests:
| Option | Default | Meaning |
| - | - | - |
| `max_results_per_purl` | `0` (no limit) | Caps the results returned for each PURL |
| `show_related_results` | `false` | Also look up the component's source PURL |
| `on_version_not_found` | `strict` | Version fallback: `strict`, `show_latest`, `show_closest_lt`, `show_closest_gt`, `show_any`. Not applied to range endpoints. |
| `encoded` | `none` | Encoding for content strings in the response: `none`, `url_encoded`, `base64_encoded` |
Endpoints that support these options say so in the API Reference. Today, `/v3/license/evidence` is
the main endpoint that uses them.
## Response conventions
### Batch status and per-item info codes
A batch request returns HTTP `200` even when some components could not be resolved. Each response
has two levels of outcome:
* **`status.status`** for the request as a whole: `SUCCESS`, `PARTIAL_SUCCESS`, or `ERROR`, with
a human-readable `status.message`.
* **`info_code`** and **`info_message`** on each item in `components`, when that item did not
resolve cleanly.
This is an illustrative response for the request above, where the second PURL is not known:
```json theme={null}
{
"components": [
{
"purl": "pkg:github/madler/zlib",
"requirement": "1.2.11",
"version": "1.2.11",
"vulnerabilities": [
{
"id": "CVE-2022-37434",
"cve": "CVE-2022-37434",
"severity": "Critical",
"source": "NVD"
}
]
},
{
"purl": "pkg:npm/does-not-exist",
"info_code": "COMPONENT_NOT_FOUND",
"info_message": "component not found"
}
],
"status": { "status": "PARTIAL_SUCCESS", "message": "..." }
}
```
| `info_code` | Meaning |
| - | - |
| `REQUIREMENT_NOT_MET` | Informational. The requested version had no data, so the API used the nearest version instead. The item still has results. |
| `INVALID_PURL` | The API could not parse the PURL. |
| `COMPONENT_NOT_FOUND` | The component is not in the SCANOSS Knowledge Base. |
| `VERSION_NOT_FOUND` | The component is known, but the requested version is not. |
| `RESOLVE_FAILED` | The lookup failed for this item. |
| `RELEASE_NOTES_UNAVAILABLE` | No release notes exist for the resolved version. |
| `WARNING` | The item resolved with a warning. Read `info_message`. |
A single-component `GET` reports the same `status.status`, derived from that one item's
`info_code`.
In a client, treat `SUCCESS` and `PARTIAL_SUCCESS` as usable responses, and read `info_code` on
each item to decide what to retry or report. Do not retry a whole batch because one item
returned `COMPONENT_NOT_FOUND`.
### Errors
Requests that fail as a whole, such as a malformed body or a server error, return an HTTP error
status with this body:
```json theme={null}
{
"status": "error",
"error": { "code": "INVALID_QUERY", "message": "purl query parameter is required" },
"timestamp": "2026-05-19T11:39:56Z"
}
```
`error.code` is a stable identifier, so parse it instead of `message`. Common codes include
`INVALID_BODY`, `INVALID_QUERY`, `EMPTY_BATCH`, `INVALID_PURL`, `COMPONENT_NOT_FOUND`,
`VERSION_NOT_FOUND`, `TIMEOUT`, and `INTERNAL_ERROR`.
| HTTP status | Typical cause |
| - | - |
| `400` | Invalid parameters or body |
| `401` | Missing or invalid API key |
| `404` | Component, version, or scan not found |
| `410` | Scan session expired |
| `413` | Upload larger than the allowed size |
| `500` | Unexpected server error |
| `504` | The request exceeded the server's deadline |
## Scanning source code: the fingerprint flow
The API never receives your source code. You send **WFP fingerprints**, which are compact hashes
of each file and of its snippets. Generate them with `scanoss-cli wfp`, the
[Go SDK](https://github.com/scanoss/scanoss.go), or another SCANOSS client, rather than writing
your own fingerprinter:
```bash theme={null}
scanoss-cli wfp ./my-project --output project.wfp
```
`POST /v3/wfp/scan` accepts a WFP in one of two modes. The `X-Scan-Id` request header selects the
mode. Both return the same `ScanEnvelope`, and the final `result` is identical in both.
### Small inputs: one synchronous request
Without an `X-Scan-Id` header, send the whole WFP as the body. The request blocks until the scan
finishes and returns `200` with `status: completed` and the `result`:
```bash theme={null}
curl -sS -X POST https://api.scanoss.com/v3/wfp/scan \
-H "x-api-key: $SCANOSS_API_KEY" \
-H "Content-Type: application/octet-stream" \
--data-binary @project.wfp
```
The request timeout and the maximum upload size limit this mode. Use it only for small inputs.
### Large inputs: upload in blocks, then poll
For anything larger, upload the WFP in blocks and poll for the result. This is what
`scanoss-cli scan` does.
Generate a new UUID version 7 in canonical lowercase form, for example
`018f7b2c-9a3e-7d41-8b6a-2c1d4e5f6a7b`. Your client creates this ID, not the server. The
server rejects any other ID format with `400 INVALID_SCAN_ID`.
Split the WFP into byte ranges (scanoss-cli uses 1 MiB blocks by default). Send each one as
`POST /v3/wfp/scan` with:
* `X-Scan-Id`: the same scan ID on every block
* `Content-Range: bytes -/`: the block's position, zero-based, end inclusive
* `Content-Type: application/octet-stream`
```bash theme={null}
curl -sS -X POST https://api.scanoss.com/v3/wfp/scan \
-H "x-api-key: $SCANOSS_API_KEY" \
-H "X-Scan-Id: $SCAN_ID" \
-H "Content-Range: bytes 0-1048575/3145728" \
-H "Content-Type: application/octet-stream" \
--data-binary @block-0.bin
```
Each accepted block returns `202` with `status: uploading` and `received_bytes`. Blocks can
arrive in any order and in parallel. If you resend a block the server already has, with the
same bytes, it returns `202` again, so you can retry a block safely. When the blocks cover the
whole range with no gaps, the server starts the scan.
```bash theme={null}
curl -sS "https://api.scanoss.com/v3/wfp/scan/$SCAN_ID" -H "x-api-key: $SCANOSS_API_KEY"
```
`status` moves through `uploading`, `queued`, `scanning`, and finally `completed`, `failed`,
or `expired`. While scanning, `phase`, `phase_done`, and `phase_total` report progress. Once
`completed`, the envelope carries `result`. The API reports a `failed` scan in the body with an
`error` string, not as an HTTP error. scanoss-cli polls every 2 seconds by default.
A block upload can return these errors:
| Response | Cause |
| - | - |
| `400 INVALID_SCAN_ID` | The scan ID is not a canonical lowercase UUID version 7 |
| `400 MISSING_RANGE` | `Content-Range` is missing |
| `400 INVALID_RANGE` | `Content-Range` is malformed, or the body length does not match it |
| `409 RANGE_CONFLICT` | A block overlaps received bytes with different content, or the scan ID belongs to a finished upload |
| `413 PAYLOAD_TOO_LARGE` | A block or the declared total is larger than allowed |
The server keeps a scan session for a limited time. Polling an unknown ID returns `404 SCAN_NOT_FOUND`,
and polling an expired one returns `410 SCAN_EXPIRED`.
### The scan result
`result` contains:
* `files`: one entry per scanned file, with its `path`, its `match_type` (`file`, `snippet`, or
`none`), and a `matches` list. Each match names a component release by `url_hash` and carries a
`confidence` grade (`LOW`, `MEDIUM`, or `HIGH`). Snippet matches also carry
`match_percentage` and the matched line ranges in your file and in the open source file.
* `components`: the matched component releases, keyed by `url_hash`, with `component`, `vendor`,
`version`, `url`, `release_date`, and `purls`.
scanoss-cli turns this result into its raw inventory, SPDX, or CycloneDX output and can add
licence, vulnerability, cryptography, and geoprovenance data with `--include`. If you call the
API yourself, you add that data by sending the matched PURLs to the lookup endpoints above.
# API keys and authentication
Source: https://docs.scanoss.com/en/latest/developer-tools/authentication
Get a SCANOSS API key and give it to scanoss-cli, Crypto Finder, or your own API client. Covers flags, environment variables, config files, precedence, custom API URLs, proxies, and custom CAs.
## Get an API key
SCANOSS issues API keys for the hosted SCANOSS API (`https://api.scanoss.com`) under a commercial
licence. To get one, contact [sales@scanoss.com](mailto:sales@scanoss.com) or
[support@scanoss.com](mailto:support@scanoss.com).
One key works for every tool on this page. Treat it as a password:
* Keep it in your CI system's secret store, and pass it to the tools as the `SCANOSS_API_KEY`
environment variable.
* Do not type it on a command line in CI. CI systems often write commands to build logs.
* Do not commit it to a repository, and do not put it in `scanoss.json`.
## What needs a key
| Operation | Needs a key? |
| - | - |
| `scanoss-cli scan`, `scan wfp`, `results`, `enrich`, `dependencies`, and the lookup commands (`vulnerabilities`, `licenses`, `cryptography`, and others) against `https://api.scanoss.com` | Yes |
| `scanoss-cli wfp` (fingerprints only) and `scanoss-cli sbom` (offline conversion) | No. They never contact the API. |
| Any `scanoss-cli` command against a custom API URL | Only if that server requires one |
| Crypto Finder with SCANOSS remote rulesets | Crypto Finder sends your key when it downloads rulesets and when it fetches published dependency findings |
| Crypto Finder with local rules only (`--no-remote-rules --rules-dir `) | No |
| Direct calls to the SCANOSS API | Yes |
## scanoss-cli
### Ways to provide the key
```bash theme={null}
# 1. Environment variable (recommended for CI)
export SCANOSS_API_KEY=""
scanoss-cli scan . --output results.json
# 2. Stored in your user config file (recommended for workstations)
scanoss-cli config set api-key ""
scanoss-cli scan . --output results.json
# 3. Flag, for a single run
scanoss-cli scan . --api-key "$SCANOSS_API_KEY" --output results.json
```
`config set` writes to `~/.scanoss/settings.json`. Run `scanoss-cli config path` to print the
exact location.
### Precedence
Each setting resolves in the same order. The first source that has a non-empty value wins:
```
--flag > environment variable > ~/.scanoss/settings.json > built-in default
```
| Setting | Flag | Environment variable | Default |
| - | - | - | - |
| `api-key` | `--api-key` | `SCANOSS_API_KEY` | none |
| `api-url` | `--api-url` | `SCANOSS_API_URL` | `https://api.scanoss.com` |
| `proxy` | `--proxy` | `SCANOSS_PROXY` | none (`HTTP_PROXY`/`HTTPS_PROXY` are honoured) |
| `ca-cert` | `--ca-cert` | `SCANOSS_CA_CERT` | none |
### Check what is in effect
```console theme={null}
$ scanoss-cli config list
api-key ******** (env: SCANOSS_API_KEY)
api-url https://api.scanoss.com (default)
ca-cert (unset)
proxy (unset)
Config file: /home/you/.scanoss/settings.json
```
The CLI never prints the key. `scanoss-cli config get api-key` only reports whether a key is set, and
exits `0` if it is and `1` if it is not, so you can use it in a script:
```bash theme={null}
scanoss-cli config get api-key >/dev/null || echo "No SCANOSS API key configured"
```
Add `--verbose` to any command to log which source each setting came from. The CLI never logs the
key's value.
### What happens without a key
If no key is set and the API URL is the default `https://api.scanoss.com`, every command that
calls the API stops before it sends anything. It prints a "No API key provided" notice to stderr
and exits with code `1`.
If the API rejects the key (HTTP 401), the CLI prints
`Unauthorized: missing or invalid API key` and exits with code `1`.
### Rotate or remove a stored key
```bash theme={null}
scanoss-cli config set api-key "" # overwrite
scanoss-cli config unset api-key # remove
```
## Crypto Finder
### Ways to provide the key
```bash theme={null}
# 1. Environment variable (recommended for CI)
export SCANOSS_API_KEY=""
crypto-finder scan .
# 2. Stored in the Crypto Finder config file
crypto-finder configure --api-key ""
crypto-finder scan .
# 3. Flag, for a single run
crypto-finder scan --api-key "$SCANOSS_API_KEY" .
```
`crypto-finder configure` writes to `~/.scanoss/crypto-finder/config.json`. This file is separate
from the scanoss-cli file. If the file is readable by other users, Crypto Finder restricts it to
the owner when it loads it.
### Precedence
1. Command-line flags (`--api-key`, `--api-url`)
2. Environment variables (`SCANOSS_API_KEY`, `SCANOSS_API_URL`)
3. Config file (`~/.scanoss/crypto-finder/config.json`)
4. Project settings (`scanoss.json` in the scanned directory)
5. Defaults (`https://api.scanoss.com`)
Because both tools read `SCANOSS_API_KEY` and `SCANOSS_API_URL`, one pair of environment
variables configures both in the same pipeline.
## Use a custom API URL
Point the tools at an on-premise SCANOSS deployment, or any other SCANOSS endpoint, with the API
URL setting:
```bash theme={null}
# scanoss-cli
scanoss-cli config set api-url https://scanoss.internal.example.com
# or: export SCANOSS_API_URL=https://scanoss.internal.example.com
# or: scanoss-cli scan . --api-url https://scanoss.internal.example.com
# Crypto Finder
crypto-finder configure --api-url https://scanoss.internal.example.com
# or: crypto-finder scan --api-url https://scanoss.internal.example.com .
```
scanoss-cli only requires a key for the default endpoint. Against a custom URL it runs without a
key, so a deployment that does not use keys needs no extra setup.
## Proxies and custom certificate authorities
### scanoss-cli
scanoss-cli honours `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` without any flag. To override
them, or to trust a private CA, use the flags or store the settings:
```bash theme={null}
scanoss-cli scan . --proxy http://proxy.example.com:8080
scanoss-cli scan . --ca-cert /etc/ssl/corp-ca.pem
# Or store them once
scanoss-cli config set proxy http://proxy.example.com:8080
scanoss-cli config set ca-cert /etc/ssl/corp-ca.pem
```
* `--ca-cert` adds a PEM file to the system trust store. Verification stays on, and the public
API still works.
* A stored or flag `proxy` takes precedence over `HTTP_PROXY`/`HTTPS_PROXY`.
* scanoss-cli does not support proxy auto-configuration (PAC) files. Read the proxy address from
the PAC file and pass it with `--proxy`.
* `--ignore-cert-errors` turns off all TLS verification. Use it only to test against a
self-signed internal endpoint. It cannot be stored in the config file.
### Crypto Finder
Crypto Finder has no proxy or CA flags. It uses the Go standard HTTP client, which means it:
* honours the `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` environment variables
* trusts the operating system's certificate store
On Linux, add your corporate CA to the system store, or set `SSL_CERT_FILE` to a bundle that
contains it. `SSL_CERT_FILE` replaces the default bundle, so the file must also contain the public
root certificates.
## Calling the API directly
Send the key in the `x-api-key` request header. Header names are not case-sensitive.
```bash theme={null}
curl -sS "https://api.scanoss.com/v3/components/status" \
--get --data-urlencode "purl=pkg:github/scanoss/engine" \
-H "x-api-key: $SCANOSS_API_KEY"
```
See [v3 API overview](/en/latest/developer-tools/api-overview) for request and response
conventions.
## CI example
Store the key as a secret in your CI system and expose it to the job as `SCANOSS_API_KEY`. The
commands then need no key flag:
```bash theme={null}
# SCANOSS_API_KEY is injected by the CI system from its secret store
scanoss-cli scan . --output scanoss-results.json
crypto-finder scan --output crypto-results.json .
```
# Choose your path
Source: https://docs.scanoss.com/en/latest/developer-tools/overview
Use Earnie for out-of-the-box governance, or build your own pipeline from SCANOSS's standalone CLIs, public API, and copy-pasteable Recipes.
SCANOSS gives you two ways to answer the same questions: what open source is in this code, under
which licences, with which known vulnerabilities, and which cryptography it uses. Both ways use
the same SCANOSS Knowledge Base. The difference is how much of the work around the results SCANOSS
does for you.
## Option 1: Earnie, ready to use
[Earnie](/en/latest/earnie/introduction) is SCANOSS's governance platform. Choose it if you want
SCANOSS to run the whole workflow for you:
* policies that you define once and apply to every repository
* a merge gate on every pull request
* a review workspace to triage findings and record decisions
* dashboards across all your projects
* exportable evidence for customers and auditors
You connect your repositories, and Earnie runs the scans, applies your policies, and records each
decision. You write no pipeline code.
Policies, merge gates, triage, dashboards, and evidence in one platform.
## Option 2: Standalone tools, build your own pipeline
Choose the standalone tools if you already have a pipeline, a results store, or a review process,
and you want SCANOSS data inside it. You run the tools, you keep the output, and you decide what
fails a build.
SCANOSS does not maintain integrations for specific CI platforms. Instead, we publish
[Recipes](/en/latest/developer-tools/recipes). Each Recipe is a complete workflow written in plain
shell commands, with an explanation of every step. Recipes run in any CI system, in a Git hook, or
on a laptop.
| Tool | What it does | Output |
| - | - | - |
| [**scanoss-cli**](/en/latest/cli/scanoss-go/installation) | Fingerprints source code, scans it against the SCANOSS API, and adds licence, vulnerability, cryptography, geoprovenance, and dependency data. Also converts and refreshes SBOMs, and looks up component data by PURL. | SCANOSS raw inventory (JSON), SPDX 2.3, CycloneDX 1.7 |
| [**Crypto Finder**](/en/latest/cli/crypto-finder/overview) | Finds cryptographic algorithm, protocol, certificate, and key usage in your source code and, optionally, in your third-party dependencies. Can trace reachability through the call graph. | Findings JSON, CycloneDX 1.7 CBOM |
| [**SCANOSS API (v3)**](/en/latest/developer-tools/api-overview) | The public HTTP API that both tools call. Call it directly when you build your own client or need component data in another service. | JSON |
`scanoss-cli` uses the SCANOSS v3 API and does not work with OSSKB.org. If you depend on
OSSKB.org, use [SCANOSS-PY](/en/latest/cli/scanoss-py/installation).
## Which one fits?
| If you want to | Use |
| - | - |
| Block risky changes on pull requests, without writing pipeline code | Earnie |
| Triage findings and keep a record of every decision | Earnie |
| Add a SCANOSS check to a pipeline you already maintain | scanoss-cli and a [Recipe](/en/latest/developer-tools/recipes/scan-pull-requests) |
| Publish an SBOM with every release | scanoss-cli ([Recipe](/en/latest/developer-tools/recipes/release-sbom)) |
| Build a cryptographic inventory for post-quantum planning | Crypto Finder ([Recipe](/en/latest/developer-tools/recipes/crypto-inventory-cbom)) |
| Scan code that cannot leave a restricted network | scanoss-cli fingerprints ([Recipe](/en/latest/developer-tools/recipes/fingerprint-only-scanning)) |
| Put SCANOSS component data in your own service | [SCANOSS API](/en/latest/developer-tools/api-overview) |
## Where to start
The hosted SCANOSS API at `https://api.scanoss.com` requires an API key. See
[API keys and authentication](/en/latest/developer-tools/authentication).
Install scanoss-cli, scan a project, and generate an SBOM in about five minutes. See
[Quickstart](/en/latest/developer-tools/quickstart).
Copy a complete workflow into your pipeline. See [Recipes](/en/latest/developer-tools/recipes).
## Running SCANOSS in your own infrastructure
If you need the SCANOSS Knowledge Base inside your own network instead of calling the hosted API,
see the Knowledge Base and deployment pages in this section. You then set a custom API URL so both
tools send requests to your deployment. See
[Use a custom API URL](/en/latest/developer-tools/authentication#use-a-custom-api-url).
# Quickstart
Source: https://docs.scanoss.com/en/latest/developer-tools/quickstart
Install scanoss-cli, scan a project, read the result, and generate an SBOM in under five minutes.
This guide installs scanoss-cli, scans a project, and turns the result into a CycloneDX SBOM.
You need:
* a SCANOSS API key (see [API keys and authentication](/en/latest/developer-tools/authentication))
* a project directory to scan
* [`jq`](https://jqlang.org/), to read the JSON output (optional, but used in the examples)
Download the prebuilt binary for your platform from the
[releases page](https://github.com/scanoss/scanoss.go/releases) and put it on your `PATH`.
For Linux on x86-64:
```bash theme={null}
curl -sSLO https://github.com/scanoss/scanoss.go/releases/download/v0.9.0/scanoss-cli-linux-amd64.tar.gz
tar xzf scanoss-cli-linux-amd64.tar.gz scanoss-cli
sudo mv scanoss-cli /usr/local/bin/
scanoss-cli --version
```
Each release has archives for Linux (`amd64`, `arm64`, `armv7`), macOS (`darwin-amd64`,
`darwin-arm64`), and Windows (`windows-amd64.zip`, `windows-arm64.zip`). If you have Go 1.25
or later, you can install with Go instead:
```bash theme={null}
go install github.com/scanoss/scanoss.go/cmd/scanoss-cli@latest
```
For Docker and other options, see [Installation](/en/latest/cli/scanoss-go/installation).
```bash theme={null}
export SCANOSS_API_KEY=""
```
To keep the key between shell sessions, store it once with
`scanoss-cli config set api-key ""` instead.
```bash theme={null}
cd my-project
scanoss-cli scan . --include licenses,vulns --output results.json
```
The CLI fingerprints every file locally, uploads only the fingerprints, and waits for the
result. Progress and the scan ID go to stderr. The result goes to `results.json`.
`--include` adds optional data to the result. `licenses` adds the licences of each component,
and `vulns` adds known vulnerabilities. Add `deps` to also list the dependencies declared in
your manifest files.
`results.json` is the SCANOSS raw inventory. This is an abbreviated example of its shape:
```json theme={null}
{
"schema_version": "2.0",
"metadata": { "tool": "scanoss", "tool_version": "v0.9.0" },
"components": [
{
"purl": "pkg:github/madler/zlib",
"scope": "detected",
"name": "zlib",
"vendor": "madler",
"version": "1.2.11",
"licenses": [{ "id": "Zlib" }],
"evidence": [
{ "path": "third_party/zlib/inflate.c", "match_type": "file" }
]
}
],
"vulnerabilities": [
{
"id": "CVE-2022-37434",
"severity": "CRITICAL",
"source": "NVD",
"purls": ["pkg:github/madler/zlib@1.2.11"]
}
]
}
```
| Field | Meaning |
| - | - |
| `components[]` | One entry per component, identified by its PURL and version. |
| `scope` | `detected`: found by matching your files. `declared`: listed in a manifest (only with `--include deps`). |
| `evidence[]` | Where the component was found. `match_type` is `file` (a whole file matches), `snippet` (part of a file matches), or `declared` (a manifest entry). |
| `licenses[]` | SPDX licence IDs for the component (with `--include licenses`). |
| `vulnerabilities[]` | Known vulnerabilities, linked to components through `purls` (with `--include vulns`). |
Summarise it with `jq`:
```bash theme={null}
# Components found, with version and licences
jq -r '.components[] | "\(.purl)@\(.version // "?") \([.licenses[]?.id] | join(","))"' results.json
# Vulnerabilities by severity
jq -r '.vulnerabilities[]? | "\(.severity // "unknown") \(.id) \(.purls // [] | join(","))"' results.json
```
An empty `components` list means no open source was matched.
Convert the inventory you already have. The conversion runs offline and does not scan again:
```bash theme={null}
scanoss-cli sbom results.json --format cyclonedx --output sbom.cdx.json
scanoss-cli sbom results.json --format spdx --output sbom.spdx.json
```
CycloneDX 1.7 keeps the vulnerabilities. SPDX 2.3 has no vulnerability model, so the CLI leaves
them out of the SPDX file and warns you.
## Exit codes
`scanoss-cli` exits `0` when a command completes, whatever it found, and `1` when the command
fails. A missing API key, an unreachable API, an invalid flag, or an output file that cannot be
written all cause exit code `1`. Findings never change the exit code. To fail a build on findings, check the
JSON output, as the [Recipes](/en/latest/developer-tools/recipes) show.
## Next steps
* [Recipes](/en/latest/developer-tools/recipes) show how to scan every pull request, gate on
licences and vulnerabilities, publish release SBOMs, and more.
* [Scanning your project](/en/latest/cli/scanoss-go/scanning-your-project) covers every scan flag,
file skipping, and BOM rules.
* [Output formats](/en/latest/cli/scanoss-go/output-formats) describes the raw, SPDX, and CycloneDX
formats in detail.
# Approve components with scanoss.json
Source: https://docs.scanoss.com/en/latest/developer-tools/recipes/approve-components-bom-rules
Declare the open source components your project knowingly uses in scanoss.json, so they stop failing your checks, and handle false matches with ignore, remove, and replace rules.
## Goal
Keep a reviewed list of the open source components your project is allowed to contain, in a
`scanoss.json` file in the repository. scanoss-cli marks the components on the list as approved
in every scan, so your checks fail only on something new.
## When to use it
* Your [pull request check](/en/latest/developer-tools/recipes/scan-pull-requests) fails on
components you already know about and accept.
* A scan reports a match you have reviewed and know to be wrong.
* You want approvals to go through code review, with a history in Git.
## Prerequisites
* scanoss-cli and `SCANOSS_API_KEY` (see [Recipes](/en/latest/developer-tools/recipes#before-you-use-a-recipe))
* `jq`, to bootstrap the file from an existing scan
## How scanoss-cli finds scanoss.json
scanoss-cli looks for `scanoss.json` (then `settings.json`) in the directory you scan. To use a
file somewhere else, pass `--settings `. scanoss-cli applies the rules after the results
come back from the API, so they work the same for `scan`, `scan wfp`, and `results`.
## Step 1: Bootstrap the approved list
Scan the project once, and turn every component it detected into an approval rule:
```bash theme={null}
scanoss-cli scan . --output scanoss-results.json
jq '{
bom: {
identify: ([.components[]
| select((.scope // "detected") == "detected")
| {purl}] | unique)
}
}' scanoss-results.json > scanoss.bootstrap.json
```
`scanoss.bootstrap.json` now contains one rule per component:
```json theme={null}
{
"bom": {
"identify": [
{ "purl": "pkg:github/madler/zlib" },
{ "purl": "pkg:npm/lodash" }
]
}
}
```
Review the list before you use it. Every component in it counts as approved from now on. Then save it as
`scanoss.json`. If you already have a `scanoss.json` (for example, with skip settings), copy the
`bom` section into it instead of overwriting the file.
Commit `scanoss.json` to the repository. Require a review from your open source owner for changes
to it, using your code hosting's review rules. That review is your approval process.
## Step 2: Approve new components as they appear
When a check reports a new component and you accept it, add a rule in the same pull request:
```json theme={null}
{
"bom": {
"identify": [
{ "purl": "pkg:github/madler/zlib" },
{ "purl": "pkg:npm/lodash" },
{ "purl": "pkg:github/nlohmann/json", "path": "third_party/json/" }
]
}
}
```
The third rule has a `path`. It approves the component only in files under `third_party/json/`.
If the same code turns up somewhere else, the scan still reports it.
scanoss-cli matches rules by PURL without the version. On its own, a rule for
`pkg:npm/vue@2.6.14` marks every version of `vue` as identified, including a later one with a
different licence or a known vulnerability. The gate below closes that gap. When a rule names a
version, the gate treats only that version as approved. Leave the version out only when you mean to
approve every version.
## Step 3: Handle matches that are wrong
Not every match is a component you use. Four rule types cover the common cases:
| Rule | Effect | Use it when |
| - | - | - |
| `bom.identify` (also spelled `bom.include`) | Marks the component as approved (`"identified": true`) and puts it first in each matching file's matches. Protects it from `ignore`, `remove`, and the ranking threshold. | You use the component and accept it. |
| `bom.ignore` (also spelled `bom.exclude`) | Drops that component from each file's matches. Other matches for the same file remain. | A file matches the wrong project, for example a common upstream that many projects copy. |
| `bom.remove` | Clears all matches for the files where the component matched, so those files report no match. | A file is your own code, or code you already account for elsewhere, and should not report a match at all. |
| `bom.replace` | Changes the matched component to the one in `replace_with`. | The match points at a fork or mirror, and you want the original project reported. |
```json theme={null}
{
"bom": {
"identify": [
{ "purl": "pkg:github/madler/zlib" }
],
"ignore": [
{ "purl": "pkg:github/some-mirror/zlib-copy", "path": "third_party/zlib/" }
],
"remove": [
{ "purl": "pkg:github/example/our-old-public-repo" }
],
"replace": [
{ "purl": "pkg:github/someone/lodash-fork", "replace_with": "pkg:npm/lodash@4.17.21" }
]
}
}
```
Paths in rules are relative to the scanned directory:
* `dir/` matches every file under `dir`, at any depth.
* A glob, such as `dir/*.c`, matches with standard glob rules. `*` does not cross a `/`.
* Any other value must match the file path exactly.
Every rule needs a `purl`. The rules run in this order: ignore, identify, the optional
`--ranking-threshold` filter, remove, and replace.
## Approve a baseline without editing scanoss.json
To approve everything in an earlier result for one run, pass it with `-i, --identify`. Each
component in the file becomes an approval rule without a path:
```bash theme={null}
scanoss-cli scan . --identify baseline-results.json --output scanoss-results.json
```
`--identify` accepts a scanoss-cli raw result, a CycloneDX or SPDX file, or a component list
(`{"components":[{"purl":"..."}]}`). `-n, --ignore ` works the same way for ignore rules.
Both can be combined with `scanoss.json`.
Use this to adopt SCANOSS on a large existing codebase. Keep `baseline-results.json` as a build
artefact and fail only on what appears after it. For a reviewed, long-term record, prefer
`scanoss.json`.
## What the output means
In the raw result, an approved component carries `"identified": true`, both on the component and
on the file evidence the rule covered:
```json theme={null}
{
"purl": "pkg:github/nlohmann/json",
"identified": true,
"evidence": [
{ "path": "third_party/json/json.hpp", "match_type": "file", "identified": true }
]
}
```
A component approved only for some paths still shows `"identified": true` at component level.
Check the `identified` value on each `evidence` entry to see which files the rule covered. In
CycloneDX output, the component-level flag appears as the `scanoss:identified` property.
## How it fails the pipeline
`scanoss.json` changes what the result contains, not the exit code. Your gate decides what fails.
Save this filter in your repository as `scanoss/unapproved.jq`. The other recipes use it too. It
prints each detected component that `scanoss.json` doesn't approve:
```jq title="scanoss/unapproved.jq" theme={null}
# Detected components that scanoss.json does not approve.
# A bom.identify rule that pins a version approves only that version.
# A rule without a version approves every version of the component.
def base: sub("@[^@/]+$"; "");
def ver: (capture("@(?[^@/]+)$") | .v) // null;
(($rules[0] // {}) | .bom | (.identify // .include // [])) as $r
| [ $r[] | select(.purl) | {b: (.purl | base), v: (.purl | ver)} ] as $pins
| .components[]
| select((.scope // "detected") == "detected")
| . as $c
| ($pins | map(select(.b == ($c.purl | base)))) as $m
| select(
($c.identified | not)
or (($m | length) > 0
and ($m | all(.v != null))
and (($m | map(.v) | index($c.version)) == null))
)
```
Run it after the scan. It fails with exit code `2` when it prints anything:
```bash theme={null}
rules=scanoss.json; [ -f "$rules" ] || rules=/dev/null
unapproved=$(jq -r --slurpfile rules "$rules" -f scanoss/unapproved.jq scanoss-results.json \
| jq -rs '.[] | "\(.purl)@\(.version // "unknown")"')
if [ -n "$unapproved" ]; then
echo "Components not approved in scanoss.json:"
echo "$unapproved"
exit 2
fi
```
With the rules from Step 2, every version of `zlib`, `lodash`, and `json` passes. Change the
`lodash` rule to `pkg:npm/lodash@4.17.21` and only that version passes. An upgrade then fails the
gate until someone approves the new version. To bootstrap pinned rules instead, use
`{purl: (if .version then .purl + "@" + .version else .purl end)}` in place of `{purl}` in Step 1.
## Related settings in scanoss.json
The same file also controls which files scanoss-cli scans (`settings.skip`), and Crypto Finder
reads its skip patterns too. See
[Scanning your project](/en/latest/cli/scanoss-go/scanning-your-project#skipping-files).
The `settings.file_snippet` keys `skip_headers`, `skip_headers_limit`, and `ranking_threshold`
in `scanoss.json` override the matching command-line flags. If a flag seems to have no effect,
check for these keys.
# Build a cryptographic inventory (CBOM) for post-quantum readiness
Source: https://docs.scanoss.com/en/latest/developer-tools/recipes/crypto-inventory-cbom
Use Crypto Finder to inventory the cryptography in your code and dependencies, export a CycloneDX 1.7 CBOM, list the quantum-vulnerable algorithms you will need to migrate, and fail the build on broken algorithms.
## Goal
Produce a cryptographic inventory of a codebase:
* a findings file with every place your code (and, optionally, your dependencies) uses
cryptography
* a CycloneDX 1.7 Cryptography Bill of Materials (CBOM) to share with security, compliance,
or customers
* two short reports: the quantum-vulnerable public-key algorithms you will have to migrate,
and any broken algorithms you should remove now
## When to use it
* You are planning a migration to post-quantum cryptography and need to know where your code
uses RSA, elliptic-curve, and Diffie-Hellman algorithms.
* A customer or a framework asks for a cryptographic inventory or a CBOM.
* You want to stop new uses of broken algorithms such as MD5, SHA-1, DES, or RC4.
## Prerequisites
* Docker, to run the Crypto Finder image, which includes the scanning engine. To run Crypto
Finder without Docker, install the binary and OpenGrep as described in
[Installation](/en/latest/cli/crypto-finder/installation).
* `SCANOSS_API_KEY`, used to download the SCANOSS-maintained detection rules
* `jq`
## The script
Save this as `ci/crypto-inventory.sh` and run it from the repository root.
```bash theme={null}
#!/usr/bin/env bash
# Build a cryptographic inventory and CBOM with Crypto Finder.
#
# Environment:
# SCANOSS_API_KEY required for SCANOSS remote rules
# CRYPTO_FINDER_IMAGE image to run (default ghcr.io/scanoss/crypto-finder:latest)
# FAIL_ON_BROKEN "true" (default) fails when broken algorithms are found
set -euo pipefail
IMAGE="${CRYPTO_FINDER_IMAGE:-ghcr.io/scanoss/crypto-finder:latest}"
FAIL_ON_BROKEN="${FAIL_ON_BROKEN:-true}"
OUT="crypto-inventory"
mkdir -p "$OUT"
# 1. Scan the code. Errors are printed to stderr as one JSON object.
docker run --rm \
-v "$PWD:/workspace/code:ro" \
-v "$PWD/$OUT:/workspace/output" \
-e SCANOSS_API_KEY \
"$IMAGE" \
scan --exclude "$OUT/" --error-format json \
--output /workspace/output/crypto-findings.json /workspace/code
# 2. Convert the findings to a CycloneDX 1.7 CBOM.
docker run --rm \
-v "$PWD/$OUT:/workspace/output" \
"$IMAGE" \
convert /workspace/output/crypto-findings.json --output /workspace/output/cbom.cdx.json
findings="$OUT/crypto-findings.json"
# 3. Inventory: how often each algorithm family is used.
echo "Algorithm families:"
jq -r '
[.findings[].cryptographic_assets[] | .metadata.algorithmFamily // .metadata.assetType // "unknown"]
| group_by(.) | map({family: .[0], count: length}) | sort_by(-.count)[]
| " \(.count)\t\(.family)"
' "$findings" | tee "$OUT/families.txt"
# 4. Post-quantum report: public-key algorithms broken by a quantum computer.
jq -r '
.findings[] | .file_path as $file
| .cryptographic_assets[]
| select((.metadata.algorithmFamily // "") | test("^(RSA|DSA|ECDSA|ECDH|ECIES|EdDSA|FFDH|DH)"; "i"))
| "\($file):\(.start_line)\t\(.metadata.algorithmFamily)\t\(
if .source == "dependency" then "dependency \(.dependency_info.module)@\(.dependency_info.version)"
else "own code" end)"
' "$findings" > "$OUT/quantum-vulnerable.tsv"
echo "Quantum-vulnerable uses: $(wc -l < "$OUT/quantum-vulnerable.tsv")"
# 5. Gate: broken algorithms.
jq -r '
.findings[] | .file_path as $file
| .cryptographic_assets[]
| select((.metadata.algorithmFamily // "") | test("^(MD4|MD5|SHA-?1|DES|3DES|RC4)$"; "i"))
| "\($file):\(.start_line)\t\(.metadata.algorithmFamily)"
' "$findings" > "$OUT/broken.tsv"
if [ -s "$OUT/broken.tsv" ]; then
echo "Broken algorithms in use:"
cat "$OUT/broken.tsv"
[ "$FAIL_ON_BROKEN" = "true" ] && exit 2
fi
echo "CBOM written to $OUT/cbom.cdx.json"
```
## What each step does
1. **Scan.** Crypto Finder detects the languages in the repository, downloads the matching
SCANOSS detection rules (cached for 7 days), and runs them. The script mounts the repository
read-only.
`--exclude` keeps the script's own output folder out of the scan. `--error-format json`
prints any failure as one JSON object with a stable `code`, which is easier to read in CI logs
and to parse.
2. **Convert.** `convert` turns the findings into a CycloneDX 1.7 CBOM and validates it against
the CycloneDX schema. The CBOM includes only findings with complete metadata, so it can list
fewer assets than the findings file.
3. **Inventory.** The script counts the findings per algorithm family, such as `AES`, `SHA-2`, or `ECDSA`.
4. **Post-quantum report.** The script lists every use of a public-key family that a large quantum computer
could break: RSA (including `RSASSA-PKCS1`, `RSASSA-PSS`, `RSAES-OAEP`, and `RSAES-PKCS1`),
DSA, ECDSA, ECDH, ECIES, EdDSA, and finite-field Diffie-Hellman. These are the uses to
replace with post-quantum algorithms such as `ML-KEM` and `ML-DSA`, which Crypto Finder also
reports.
5. **Gate.** The script lists uses of broken algorithms and fails the build if it finds any.
The family lists in steps 4 and 5 are this recipe's policy, not a Crypto Finder feature. Change
the regular expressions to match your own cryptography policy.
## What the output means
| File | Contains |
| - | - |
| `crypto-findings.json` | Every finding: file, lines, matched code, the rules that matched, and metadata such as `assetType`, `algorithmFamily`, `algorithmPrimitive`, and `algorithmMode`. See [Output formats](/en/latest/cli/crypto-finder/output-formats). |
| `cbom.cdx.json` | CycloneDX 1.7 CBOM: one `cryptographic-asset` component per algorithm, protocol, certificate, or key material, with `cryptoProperties` and the locations where it occurs |
| `families.txt` | Number of findings per algorithm family |
| `quantum-vulnerable.tsv` | One line per quantum-vulnerable use: location, family, and whether it is in your code or a dependency |
| `broken.tsv` | One line per broken algorithm use |
A sample of `quantum-vulnerable.tsv`:
```
src/auth/token.go:42 ECDSA own code
src/tls/handshake.go:118 ECDH own code
```
Each line is one migration task. It gives the location and the algorithm to replace.
## How it fails the pipeline
| Exit code | Meaning |
| - | - |
| `0` | Inventory produced, no broken algorithms (or `FAIL_ON_BROKEN=false`) |
| `1` | Crypto Finder failed. Read the JSON error on stderr, for example `rules_load_failed` or `scanner_timeout`. |
| `2` | Broken algorithms found |
Crypto Finder also has `--fail-on-findings`, which exits `1` when it finds any
cryptography. That is useful to keep cryptography out of a component that must not have any.
For an inventory it is too broad, because almost every codebase uses some cryptography, so this
recipe filters the findings with `jq` instead. With `--error-format json`, a failure caused by
`--fail-on-findings` has the code `findings_detected`, so you can tell it apart from a tool
error.
## Include your dependencies
In most projects, the bulk of the cryptography is in third-party libraries. To scan your dependencies as well, use
the `latest-deps` image, which includes the language toolchains needed to resolve them, and add
`--scan-dependencies`:
```bash theme={null}
docker run --rm \
-v "$PWD:/workspace/code:ro" \
-v "$PWD/crypto-inventory:/workspace/output" \
-e SCANOSS_API_KEY \
ghcr.io/scanoss/crypto-finder:latest-deps \
scan --scan-dependencies --exclude "crypto-inventory/" --error-format json \
--output /workspace/output/crypto-findings.json /workspace/code
```
Dependency scanning supports Go, Java (Maven and Gradle), Python, Rust, and Node (npm). Findings
in dependencies have `"source": "dependency"` and a `dependency_info` object with the module and
version. The post-quantum report above already labels them. See
[Dependency scanning](/en/latest/cli/crypto-finder/dependency-scanning) for reachability and
call-chain analysis, which tells you whether your code reaches a dependency's cryptography.
## Scan only what a pull request changed
To check a pull request for new broken algorithms without re-reporting the whole codebase, scope
detection to the changed files:
```bash theme={null}
git diff --name-only "origin/main...HEAD" > changed.txt
crypto-finder scan --detect-paths-from changed.txt --error-format json \
--output crypto-findings.json .
```
Then run step 5 of the script on `crypto-findings.json`. Crypto Finder checks only the listed
files, but it still reads the rest of the repository, so the results for those files match a full
scan.
`--detect-paths-from` needs the default OpenGrep scanner.
## Keep the inventory over time
* Run the full script on a schedule or on each release, and store the outputs with the release.
* Compare `families.txt` and `quantum-vulnerable.tsv` between runs to track migration progress.
* Pin the image to a version tag in CI rather than `latest`, so a new release cannot change your
inventory without a code change.
# Fingerprint-only scanning for restricted environments
Source: https://docs.scanoss.com/en/latest/developer-tools/recipes/fingerprint-only-scanning
Generate WFP fingerprints on a machine with no network access, move only the fingerprint file across, and scan it with scanoss-cli scan wfp from a connected machine.
## Goal
Scan code that lives on a machine that cannot reach the internet, or that policy does not allow
to send anything out. You split the work in two:
1. On the restricted machine, generate fingerprints. This step needs no network access and no API
key.
2. On a connected machine, scan the fingerprint file. The source code never leaves the
restricted side.
## When to use it
* Your build systems run in an isolated network.
* Your code is under an export, defence, or customer restriction that does not allow a direct
connection to an external service.
* A third party audits your code by scanning your fingerprints, without receiving your source
code.
If the restricted network can reach your own SCANOSS deployment instead, you do not need this
split. Point scanoss-cli at that deployment with a custom API URL. See
[Use a custom API URL](/en/latest/developer-tools/authentication#use-a-custom-api-url).
## What a fingerprint file contains
A WFP file contains, for each file:
* a hash of the whole file, its size, and its path relative to the scanned directory
* hashes of overlapping fragments of the file, used to find snippet matches
It does not contain the source code. It does contain your file paths, so if paths are
sensitive, review the file before it leaves the restricted side.
## Prerequisites
* scanoss-cli on both machines, at the same version (see [Quickstart](/en/latest/developer-tools/quickstart))
* `SCANOSS_API_KEY` on the connected machine only
* An approved way to move one file from the restricted machine to the connected one
## Step 1: Fingerprint on the restricted machine
```bash theme={null}
cd /path/to/project
# Fingerprint the project. No network access is used.
scanoss-cli wfp . --settings scanoss.json --output project.wfp
# Record a checksum to verify the transfer.
sha256sum project.wfp > project.wfp.sha256
```
Leave out `--settings scanoss.json` if the project has no `scanoss.json`. When it is present, its
skip rules decide which files scanoss-cli fingerprints.
`wfp` accepts the same file selection flags as `scan`: `--threads`, `--min-size`, `--max-size`,
`--gitignore`, `--all-extensions`, `--all-folders`, `--all-hidden`, `--skip-headers`, and
`--skip-headers-limit`. Its output is byte-for-byte reproducible. The same tree and flags give
the same file, so you can hash it, diff it, and store it as evidence.
`--skip-headers` changes the fingerprints. Fingerprints made with it on do not match
fingerprints made with it off, so use the same setting every time you fingerprint the same
project.
## Step 2: Move the file
Transfer `project.wfp` and `project.wfp.sha256` with your approved process. On the connected
machine, check that the file arrived unchanged:
```bash theme={null}
sha256sum -c project.wfp.sha256
```
## Step 3: Scan the fingerprints on the connected machine
```bash theme={null}
export SCANOSS_API_KEY=""
scanoss-cli scan wfp project.wfp \
--settings scanoss.json \
--include licenses,vulns \
--output scanoss-results.json
```
`scan wfp` uploads the file as it is, waits for the result, and writes the same raw inventory as
a normal `scan`. It accepts the scan flags: `--format`, `--include`, `--settings`, `--identify`,
`--ignore`, `--ranking-threshold`, `--chunk-size`, and `--poll-interval`.
Copy the project's `scanoss.json` to the connected machine if you use one. scanoss-cli applies its BOM
rules (approved and ignored components) on this side, after the results come back. They match on the
file paths stored in the fingerprints.
scanoss-cli prints the scan ID when the upload finishes. If the connection drops while you wait, resume
with:
```bash theme={null}
scanoss-cli results --settings scanoss.json --include licenses,vulns --output scanoss-results.json
```
## Step 4: Use the result
The result is a normal scanoss-cli raw inventory, so every other recipe applies to it:
```bash theme={null}
# Approval gate, as in the pull request recipe (needs scanoss/unapproved.jq)
rules=scanoss.json; [ -f "$rules" ] || rules=/dev/null
unapproved="$(jq -c --slurpfile rules "$rules" -f scanoss/unapproved.jq scanoss-results.json)"
[ -z "$unapproved" ] || { echo "$unapproved"; exit 2; }
# Licence and vulnerability policy
ci/scanoss-policy.sh scanoss-results.json
# SBOMs
scanoss-cli sbom scanoss-results.json --format cyclonedx --output sbom.cdx.json
```
See [Approve components with scanoss.json](/en/latest/developer-tools/recipes/approve-components-bom-rules),
[the licence and vulnerability gate](/en/latest/developer-tools/recipes/licence-and-vulnerability-gate),
and [Generate an SBOM for each release](/en/latest/developer-tools/recipes/release-sbom).
## What you give up
| Feature | Available with fingerprints only? |
| - | - |
| Open source matches (file and snippet) | Yes |
| Licences, vulnerabilities, cryptography, geoprovenance (`--include licenses,vulns,crypto,geo`) | Yes. The API looks them up by component, not by source. |
| Declared dependencies (`--include deps`) | No. scanoss-cli reads them from manifest files in the source tree, so `scan wfp` rejects `deps`. |
| BOM rules in `scanoss.json` | Yes, applied on the connected machine |
To include declared dependencies, move the manifest files (for example `package.json`,
`package-lock.json`, or `go.mod`) to the connected machine too, if your policy allows it, and run
`scanoss-cli dependencies --extract-local --output deps.json` on that copy.
## How it fails the pipeline
| Step | Fails with |
| - | - |
| `wfp` cannot read the project | Exit code `1` |
| The checksum does not match | Non-zero from `sha256sum -c` |
| `scan wfp` cannot reach the API, or has no key | Exit code `1` |
| The result contains unapproved components, or breaks your policy | Exit code `2` from the gates in step 4 |
As with every scanoss-cli command, `scan wfp` exits `0` after a successful scan whatever it
finds. The gates in step 4 turn findings into a failure.
# Recipes
Source: https://docs.scanoss.com/en/latest/developer-tools/recipes/index
Copy-pasteable, explained workflows built from scanoss-cli and Crypto Finder. Each one runs as plain shell commands in any CI system, Git hook, or terminal.
SCANOSS does not maintain plugins for individual CI platforms. Recipes take their place. Each
recipe is a complete workflow built from the standalone tools and written as plain shell commands,
with an explanation of every step and its output.
Because a recipe is plain shell commands, it runs anywhere you can run a shell: a CI job, a Git
hook, a scheduled job, or your terminal. Where a recipe shows CI configuration, it is only an
example of how to call the same script.
If you would rather not build and maintain this yourself, [Earnie](/en/latest/earnie/introduction)
provides policies, merge gates, triage, dashboards, and evidence out of the box.
## Available recipes
Scan each pull request and fail it when open source appears that you have not approved.
A small policy script over the scan result, with a deny-list of licences and a severity threshold.
Declare known components once, so they stop failing your checks.
Scan only the staged files before each commit.
One scan, then SPDX and CycloneDX files to publish with the release.
A weekly vulnerability and licence refresh with `enrich`, without re-scanning.
Inventory cryptography with Crypto Finder for post-quantum readiness.
Fingerprint offline, then scan the fingerprints from a connected machine.
## Before you use a recipe
Every recipe assumes:
* scanoss-cli is installed and on your `PATH` (see [Quickstart](/en/latest/developer-tools/quickstart)).
The cryptography recipe needs Crypto Finder instead.
* `SCANOSS_API_KEY` is set in the environment. In CI, inject it from your secret store. See
[API keys and authentication](/en/latest/developer-tools/authentication).
* `jq` is installed. The tools write JSON, and the recipes use `jq` to read it.
* Bash runs the scripts. They start with `set -euo pipefail`, so any failing command stops
the script with a non-zero exit code.
## How the tools signal failure
Recipes that fail a build rely on these exit codes:
| Tool | Exit `0` | Non-zero |
| - | - | - |
| scanoss-cli | The command completed, **whatever it found** | `1`: the command failed (no API key, API unreachable, invalid flag, output not writable, and so on) |
| Crypto Finder | The scan completed | `1`: the scan failed, or it detected findings and `--fail-on-findings` was set |
scanoss-cli has no option to fail on findings. The recipes add a short `jq` check on its JSON
output for that, and exit with code `2` when the check fails. This keeps "the tool failed" (`1`)
apart from "the policy failed" (`2`) in your pipeline logs.
## Pin your versions
In CI, install a fixed version of each tool rather than `latest`, so a new release cannot change
your results without a code change. The recipes use scanoss-cli `v0.9.0`. Check the
[scanoss-cli releases](https://github.com/scanoss/scanoss.go/releases) and
[Crypto Finder releases](https://github.com/scanoss/crypto-finder/releases) for newer versions.
# Fail the build on a disallowed licence or a known vulnerability
Source: https://docs.scanoss.com/en/latest/developer-tools/recipes/licence-and-vulnerability-gate
A small policy script over the scanoss-cli scan result: fail when a component carries a licence on your deny-list, or a known vulnerability at or above a severity you choose.
## Goal
Turn a scan result into a pass or fail decision based on two rules:
1. No component may carry a licence that your organisation does not allow.
2. No component may have a known vulnerability at or above a chosen severity.
## When to use it
* In a pull request job, after the [pull request scan](/en/latest/developer-tools/recipes/scan-pull-requests).
* In a release job, before you publish an SBOM.
* In a scheduled job over a [refreshed inventory](/en/latest/developer-tools/recipes/refresh-sbom-with-enrich),
to catch vulnerabilities disclosed after release.
scanoss-cli reports what it finds and exits `0` whatever it finds. It does not decide what is
allowed. This recipe writes that decision down as a script.
## Prerequisites
* scanoss-cli, `jq`, and `SCANOSS_API_KEY` (see [Recipes](/en/latest/developer-tools/recipes#before-you-use-a-recipe))
* A scan result in the raw format (the default) that includes the `licenses` and `vulns`
layers:
```bash theme={null}
scanoss-cli scan . --include licenses,vulns --output scanoss-results.json
```
Add `deps` (`--include deps,licenses,vulns`) to also check the dependencies declared in your
manifest files, not only the components detected in your code.
* `scanoss/unapproved.jq` committed to the repository (see [Approve components](/en/latest/developer-tools/recipes/approve-components-bom-rules#how-it-fails-the-pipeline))
Without `--include licenses,vulns`, the result has no licence or vulnerability data and this
check always passes. Always produce the input with both layers.
## The script
Save this as `ci/scanoss-policy.sh` and make it executable.
```bash theme={null}
#!/usr/bin/env bash
# Fail when a scanoss-cli raw result contains a disallowed licence, or a
# known vulnerability at or above a severity threshold.
#
# Usage: ci/scanoss-policy.sh scanoss-results.json
#
# Environment:
# DENY_LICENSES regular expression matched against SPDX licence IDs
# (default: GPL, AGPL and SSPL families, not LGPL)
# FAIL_ON_SEVERITY lowest severity that fails: critical, high, medium, low
# (default: high)
# SKIP_IDENTIFIED "true" exempts components approved in scanoss.json
# from the licence rule (default: false). A rule that
# pins a version exempts only that version.
set -euo pipefail
RESULTS="${1:?usage: $0 }"
DENY_LICENSES="${DENY_LICENSES:-^(AGPL|GPL|SSPL)-}"
FAIL_ON_SEVERITY="${FAIL_ON_SEVERITY:-high}"
SKIP_IDENTIFIED="${SKIP_IDENTIFIED:-false}"
# Components scanoss.json doesn't approve (filter from the "Approve components" recipe).
rules=scanoss.json; [ -f "$rules" ] || rules=/dev/null
unapproved="$(jq -c --slurpfile rules "$rules" -f scanoss/unapproved.jq "$RESULTS" \
| jq -cs 'map("\(.purl)@\(.version)")')"
case "$FAIL_ON_SEVERITY" in
critical|high|medium|low) ;;
*) echo "FAIL_ON_SEVERITY must be critical, high, medium, or low" >&2; exit 1 ;;
esac
# Rule 1: licences on the deny-list.
licence_hits="$(jq -r --arg deny "$DENY_LICENSES" --arg skip "$SKIP_IDENTIFIED" \
--argjson unapproved "$unapproved" '
.components[]
| select($skip != "true" or (.identified | not)
or ("\(.purl)@\(.version)" as $key | $unapproved | index($key)))
| [.licenses[]?.id | select(test($deny))] as $bad
| select($bad | length > 0)
| "\(.purl)@\(.version // "unknown") \($bad | unique | join(", "))"
' "$RESULTS")"
# Rule 2: vulnerabilities at or above the threshold.
vuln_hits="$(jq -r --arg min "$FAIL_ON_SEVERITY" '
{"critical": 4, "high": 3, "medium": 2, "low": 1} as $rank
| .vulnerabilities[]?
| select(($rank[(.severity // "") | ascii_downcase] // 0) >= $rank[$min])
| "\(.id) \(.severity) \(.purls // [] | join(", "))"
' "$RESULTS")"
status=0
if [ -n "$licence_hits" ]; then
echo "Disallowed licences (pattern: $DENY_LICENSES):"
echo "$licence_hits"
status=2
fi
if [ -n "$vuln_hits" ]; then
echo "Vulnerabilities at or above '$FAIL_ON_SEVERITY':"
echo "$vuln_hits"
status=2
fi
[ "$status" -eq 0 ] && echo "Licence and vulnerability policy passed."
exit "$status"
```
Run it after the scan:
```bash theme={null}
scanoss-cli scan . --include licenses,vulns --output scanoss-results.json
ci/scanoss-policy.sh scanoss-results.json
```
## How the rules read the result
Both rules read fields of the scanoss-cli raw inventory:
| Rule | Field | Notes |
| - | - | - |
| Licences | `components[].licenses[].id` | SPDX licence IDs, such as `GPL-2.0-only` or `MIT`. Unknown licences can appear as IDs that start with `LicenseRef-`. |
| Vulnerabilities | `vulnerabilities[].severity` | `critical`, `high`, `medium`, `low`, or `none`, in any letter case. Entries without a severity never fail the rule. |
| Vulnerabilities | `vulnerabilities[].purls` | The affected components, with a version when the source states one. |
| Exemptions | `components[].identified` | `true` when a `bom.identify` rule in `scanoss.json` approved the component. |
### Adjust the licence pattern
`DENY_LICENSES` is a regular expression (jq `test`). The default `^(AGPL|GPL|SSPL)-` matches
`GPL-2.0-only`, `GPL-3.0-or-later`, `AGPL-3.0-only`, and `SSPL-1.0`, but not `LGPL-2.1-only`.
Some examples:
```bash theme={null}
# Only strong copyleft network licences
DENY_LICENSES='^(AGPL|SSPL)-' ci/scanoss-policy.sh scanoss-results.json
# An explicit list of IDs
DENY_LICENSES='^(GPL-3\.0-only|GPL-3\.0-or-later|AGPL-3\.0-only)$' ci/scanoss-policy.sh scanoss-results.json
```
Licence policy is a legal decision. Agree the pattern with whoever owns open source compliance in
your organisation, and keep it in the repository next to the script.
### Approved exceptions
If your organisation accepts a component on the deny-list for your project, approve it with a
`bom.identify` rule in `scanoss.json` and run the check with `SKIP_IDENTIFIED=true`. The
component then passes the licence rule, but the vulnerability rule still checks it. See
[Approve components with scanoss.json](/en/latest/developer-tools/recipes/approve-components-bom-rules).
## What the output means
```
Disallowed licences (pattern: ^(AGPL|GPL|SSPL)-):
pkg:github/example/readline@8.1 GPL-3.0-or-later
Vulnerabilities at or above 'high':
CVE-2022-37434 CRITICAL pkg:github/madler/zlib@1.2.11
```
Each licence line names the component and the licences that matched the pattern. Each
vulnerability line names the advisory, its severity, and the affected components. To fix a
vulnerability, upgrade the component to a version without the advisory, and scan again.
## How it fails the pipeline
| Exit code | Meaning |
| - | - |
| `0` | No rule matched |
| `1` | The script could not run: a missing argument, an invalid `FAIL_ON_SEVERITY`, or a file `jq` cannot read |
| `2` | At least one licence or vulnerability matched |
If the scan before it fails, scanoss-cli exits `1` and the script never runs.
## Use it with an SBOM instead
If you start from a CycloneDX file rather than the raw inventory, the same checks read different
fields:
```bash theme={null}
# Licences
jq -r --arg deny '^(AGPL|GPL|SSPL)-' '
.components[]
| [.licenses[]?.license.id // empty | select(test($deny))] as $bad
| select($bad | length > 0)
| "\(.purl) \($bad | join(", "))"' sbom.cdx.json
# Vulnerabilities rated critical or high
jq -r '
.vulnerabilities[]?
| select([.ratings[]?.severity] | any(. == "critical" or . == "high"))
| "\(.id) \([.affects[]?.ref] | join(", "))"' sbom.cdx.json
```
SPDX 2.3 has no vulnerability model, so only the licence rule applies to SPDX documents.
# Local pre-commit check with scanoss-cli
Source: https://docs.scanoss.com/en/latest/developer-tools/recipes/pre-commit-check
A Git pre-commit hook that scans only the staged files with scanoss-cli and stops the commit when it finds open source you have not approved.
## Goal
Before each commit, scan the files you are about to commit. If they contain open source that
`scanoss.json` does not approve, stop the commit and show what the scan found. This catches copied
code before it reaches a pull request.
## When to use it
* Developers on your team paste code from the internet or from AI coding tools, and you want
feedback before review.
* You want the same rule as your
[pull request check](/en/latest/developer-tools/recipes/scan-pull-requests), but earlier.
The hook gives developers early feedback, but it cannot enforce anything. Anyone can skip it with
`git commit --no-verify`. Keep the pull request check as the gate.
## Prerequisites
* scanoss-cli on the developer's `PATH` (see [Quickstart](/en/latest/developer-tools/quickstart))
* An API key stored once on the workstation:
```bash theme={null}
scanoss-cli config set api-key ""
```
* `jq` and `bash`
* Network access to the SCANOSS API (or your own deployment) at commit time
* `scanoss/unapproved.jq` committed to the repository (see [Approve components](/en/latest/developer-tools/recipes/approve-components-bom-rules#how-it-fails-the-pipeline))
## The hook
Save this as `.git/hooks/pre-commit` in your repository and make it executable
(`chmod +x .git/hooks/pre-commit`).
```bash theme={null}
#!/usr/bin/env bash
# Scan the staged files with scanoss-cli before each commit.
# Skip once with: git commit --no-verify
set -euo pipefail
repo_root="$(git rev-parse --show-toplevel)"
cd "$repo_root"
# 1. Copy the staged version of each added, copied, modified, or renamed
# file into a temporary directory, keeping its path.
staged_dir="$(mktemp -d)"
results="$(mktemp)"
trap 'rm -rf "$staged_dir" "$results"' EXIT
git diff --cached --name-only -z --diff-filter=ACMR \
| git checkout-index --prefix="${staged_dir}/" -z --stdin
if [ -z "$(ls -A "$staged_dir")" ]; then
exit 0 # nothing to scan, for example a commit that only deletes files
fi
# 2. Apply the repository's approvals and skip rules.
settings_flag=""
if [ -f scanoss.json ]; then
settings_flag="--settings=${repo_root}/scanoss.json"
fi
# 3. Scan. If the scan cannot run (no key, offline), say so and stop.
if ! scanoss-cli scan "$staged_dir" ${settings_flag:+"$settings_flag"} --output "$results"; then
echo "SCANOSS pre-commit check could not run. Fix the error above, or skip once with: git commit --no-verify" >&2
exit 1
fi
# 4. Stop the commit on unapproved open source.
# scanoss/unapproved.jq is the filter from the "Approve components" recipe.
rules=scanoss.json; [ -f "$rules" ] || rules=/dev/null
unapproved="$(jq -c --slurpfile rules "$rules" -f scanoss/unapproved.jq "$results" \
| jq -r '" \(.purl)@\(.version // "unknown") in: \([.evidence[]?.path] | join(", "))"')"
if [ -n "$unapproved" ]; then
echo "SCANOSS found open source that is not approved in scanoss.json:" >&2
echo "$unapproved" >&2
echo "Approve it in scanoss.json, remove it, or skip once with: git commit --no-verify" >&2
exit 1
fi
```
## What each step does
1. **Export the staged files.** `git diff --cached` lists the staged files.
`git checkout-index` writes the staged version of each one into a temporary directory,
not the version in your working tree. The hook checks exactly what the commit will contain,
even if you staged only part of your changes.
2. **Settings.** The temporary directory has no `scanoss.json`, so the hook passes the
repository's file with `--settings`. Paths keep their layout, so path-scoped rules still match.
3. **Scan.** scanoss-cli fingerprints the files locally and uploads only the fingerprints to the
API. Your source code does not leave the machine. The scan uses the key stored by
`scanoss-cli config set`.
4. **Gate.** The hook runs the same `jq` check as the pull request recipe. Any detected component
without a `bom.identify` approval stops the commit.
## What the output means
```
SCANOSS found open source that is not approved in scanoss.json:
pkg:github/madler/zlib@1.2.11 in: src/compress/inflate.c
Approve it in scanoss.json, remove it, or skip once with: git commit --no-verify
```
Each line names the matched component and the staged files where the scan found it. To continue:
* If you meant to include it, add a `bom.identify` rule to `scanoss.json` and stage that change
too. See [Approve components with scanoss.json](/en/latest/developer-tools/recipes/approve-components-bom-rules).
* If you did not, remove the code and stage again.
## How it fails
Git aborts the commit when the hook exits with a non-zero code:
| Exit code | Meaning |
| - | - |
| `0` | Nothing staged to scan, or no unapproved components. The commit continues. |
| `1` | The scan could not run, or it found unapproved components. The commit stops. |
If you prefer the hook to warn instead of block when the API cannot be reached (for example, on
a train), change the `exit 1` in step 3 to `exit 0`. The pull request check still applies.
## Share the hook with your team
Git does not commit files in `.git/hooks`. To share the hook, commit it to the repository, for
example as `scripts/hooks/pre-commit`, and ask each developer to point Git at that directory once:
```bash theme={null}
git config core.hooksPath scripts/hooks
```
If your team already uses a hook manager, call the same script from it.
## Keep it fast
The hook scans only the staged files, so most commits finish in seconds. For large commits,
such as an initial import, skip the hook with `--no-verify` and rely on the pull request check.
# Keep an existing SBOM current
Source: https://docs.scanoss.com/en/latest/developer-tools/recipes/refresh-sbom-with-enrich
Refresh the vulnerability and licence data of a released inventory or SBOM every week with scanoss-cli enrich, without the source code and without re-scanning, and fail when new advisories appear.
## Goal
A release does not change after you ship it, but the list of known vulnerabilities in its
components does. New advisories come out every day. This recipe takes the inventory you stored at release
time, refreshes its vulnerability and licence data every week, and reports the advisories that are
new since the last run.
## When to use it
* You ship releases that customers run for months or years, and you must know when a new
vulnerability affects one of them.
* You keep the inventory or SBOM from each release (for example, from the
[release SBOM recipe](/en/latest/developer-tools/recipes/release-sbom)).
* The job that runs the refresh may not have the source code. `enrich` does not need it.
## How enrich works
`scanoss-cli enrich` reads an existing inventory or SBOM, looks up each component by its PURL
through the SCANOSS API, and writes the file again with fresh data. It does not fingerprint or
scan anything, so it is quick, and you can run it as often as you like on the same file.
| Input (detected from the content) | Output by default |
| - | - |
| scanoss-cli raw inventory | raw |
| CycloneDX JSON | CycloneDX |
| SPDX JSON | SPDX |
Choose the layers with `--include`: `vulns`, `licenses`, `crypto`, and `geo`. Use `-f, --format`
to write a different format in the same pass.
## Prerequisites
* scanoss-cli, `jq`, and `SCANOSS_API_KEY` (see [Recipes](/en/latest/developer-tools/recipes#before-you-use-a-recipe))
* A stored inventory, for example `inventory-2.4.0.json` from a release
* A scheduler: your CI system's scheduled pipelines, or `cron`
## The script
Save this as `ci/scanoss-refresh.sh`.
```bash theme={null}
#!/usr/bin/env bash
# Refresh vulnerabilities and licences on a stored inventory, and fail when
# advisories at or above a severity appear that were not there last time.
#
# Usage: ci/scanoss-refresh.sh []
# FAIL_ON_SEVERITY lowest severity of a NEW advisory that fails the job:
# critical, high, medium, low (default: high)
set -euo pipefail
INVENTORY="${1:?usage: $0 [previous-refresh.json]}"
PREVIOUS="${2:-$INVENTORY}"
FAIL_ON_SEVERITY="${FAIL_ON_SEVERITY:-high}"
refreshed="${INVENTORY%.json}.refreshed-$(date +%F).json"
log="$(mktemp)"
# 1. Refresh. enrich exits 0 even when a lookup service fails, so check its log.
scanoss-cli enrich "$INVENTORY" --include vulns,licenses --output "$refreshed" 2> "$log" \
|| { cat "$log" >&2; exit 1; }
cat "$log" >&2
if grep -qE "Enrichment incomplete|layer returned no data for some components" "$log"; then
echo "Refresh incomplete; keeping the previous data. See the log above." >&2
rm -f "$refreshed"
exit 1
fi
# 2. List advisories that were not in the previous file.
new_advisories="$(jq -r --slurpfile prev "$PREVIOUS" '
($prev[0].vulnerabilities // [] | map(.id)) as $known
| .vulnerabilities[]?
| select(.id | IN($known[]) | not)
| "\(.id)\t\(.severity // "unknown")\t\(.purls // [] | join(", "))"
' "$refreshed")"
echo "Refreshed inventory: $refreshed"
if [ -z "$new_advisories" ]; then
echo "No new advisories since $(basename "$PREVIOUS")."
exit 0
fi
echo "New advisories since $(basename "$PREVIOUS"):"
echo "$new_advisories"
# 3. Fail when a new advisory is at or above the threshold.
blocking="$(jq -r --slurpfile prev "$PREVIOUS" --arg min "$FAIL_ON_SEVERITY" '
{"critical": 4, "high": 3, "medium": 2, "low": 1} as $rank
| ($prev[0].vulnerabilities // [] | map(.id)) as $known
| .vulnerabilities[]?
| select(.id | IN($known[]) | not)
| select(($rank[(.severity // "") | ascii_downcase] // 0) >= $rank[$min])
| .id
' "$refreshed")"
if [ -n "$blocking" ]; then
echo "New advisories at or above '$FAIL_ON_SEVERITY':" $blocking
exit 2
fi
```
Run it every week against the latest refresh, so each run reports only what is new since the
week before:
```bash theme={null}
# First run: compare against the release inventory itself
ci/scanoss-refresh.sh sbom/inventory-2.4.0.json
# Later runs: compare against last week's refresh
ci/scanoss-refresh.sh sbom/inventory-2.4.0.json sbom/inventory-2.4.0.refreshed-2026-09-29.json
```
Store each refreshed file where the next run can find it, for example in the same artefact store
as the release.
## What each step does
1. **Refresh.** `enrich` replaces the vulnerability and licence data of every component with the
current data. It does not change the components or their evidence. If a lookup fails, `enrich` still
writes a file and exits `0`, but prints a warning on stderr: `Enrichment incomplete` when a
whole layer failed, or `layer returned no data for some components` when part of it failed.
The script treats both as a failure, so you never compare against partial data.
2. **Compare.** `jq` reads the previous file with `--slurpfile` and lists every advisory ID in the
new file that the previous file did not have.
3. **Gate.** Only new advisories at or above `FAIL_ON_SEVERITY` fail the job. Known advisories,
which you have already seen, do not fail it again.
## What the output means
```
Refreshed inventory: sbom/inventory-2.4.0.refreshed-2026-10-06.json
New advisories since inventory-2.4.0.refreshed-2026-09-29.json:
CVE-2026-12345 high pkg:github/example/libfoo@1.4.2
New advisories at or above 'high': CVE-2026-12345
```
Each line lists the advisory ID, its severity, and the affected components of that release. Use
it to decide whether to ship a patch release. To see every advisory that affects the release,
not only the new ones, run the
[licence and vulnerability gate](/en/latest/developer-tools/recipes/licence-and-vulnerability-gate)
on the refreshed file.
## How it fails the pipeline
| Exit code | Meaning |
| - | - |
| `0` | Refresh succeeded, and no new advisory reached the threshold |
| `1` | The refresh failed or was incomplete (no key, API unreachable, a lookup failed) |
| `2` | At least one new advisory at or above `FAIL_ON_SEVERITY` |
## Schedule it
Any scheduler works. With `cron`, on a machine that has the files and the key:
```bash theme={null}
# Every Monday at 06:00
0 6 * * 1 cd /srv/releases && SCANOSS_API_KEY="$(cat /etc/scanoss/key)" ci/scanoss-refresh.sh sbom/inventory-2.4.0.json
```
In a CI system, use its scheduled pipelines and loop over the releases you still support.
## Refresh an SBOM instead of the raw inventory
`enrich` also reads CycloneDX and SPDX files, and the comparison above works on raw files. For a
CycloneDX SBOM, refresh it in place:
```bash theme={null}
scanoss-cli enrich sbom-2.4.0.cdx.json --include vulns,licenses --output sbom-2.4.0.refreshed.cdx.json
```
SPDX 2.3 cannot hold vulnerabilities, so `enrich` skips the `vulns` layer for SPDX output and prints a
notice. To get vulnerability data for a release you only have as SPDX, write CycloneDX in the same
pass:
```bash theme={null}
scanoss-cli enrich sbom-2.4.0.spdx.json --include vulns,licenses --format cyclonedx \
--output sbom-2.4.0.refreshed.cdx.json
```
`enrich` cannot add the `deps` layer, because declared dependencies come from manifest files in
the source tree. Include `deps` when you first scan the release, as the
[release SBOM recipe](/en/latest/developer-tools/recipes/release-sbom) does.
# Generate an SBOM for each release
Source: https://docs.scanoss.com/en/latest/developer-tools/recipes/release-sbom
Scan a release once with scanoss-cli, then produce SPDX 2.3 and CycloneDX 1.7 SBOMs from the same result, offline, ready to publish with the release.
## Goal
For every release, publish a Software Bill of Materials (SBOM) in both common formats:
* CycloneDX 1.7, with components, licences, evidence, and known vulnerabilities
* SPDX 2.3, with components and licences
Keep the scanoss-cli raw inventory as well, so you can
[refresh the SBOM later](/en/latest/developer-tools/recipes/refresh-sbom-with-enrich) without
scanning again.
## When to use it
* A customer, a regulation, or your own policy asks for an SBOM with each release.
* You want a single record of what a release contained, stored next to the release.
## Prerequisites
* scanoss-cli, `jq`, and `SCANOSS_API_KEY` (see [Recipes](/en/latest/developer-tools/recipes#before-you-use-a-recipe))
* The source tree checked out at the release tag
* Optional: a `scanoss.json` with your approvals, so the SBOM marks approved components
## The script
Save this as `ci/scanoss-release-sbom.sh` and run it from the repository root in your release
job.
```bash theme={null}
#!/usr/bin/env bash
# Produce the SBOMs for one release.
# Usage: ci/scanoss-release-sbom.sh e.g. ci/scanoss-release-sbom.sh 2.4.0
set -euo pipefail
VERSION="${1:?usage: $0 }"
OUT="sbom"
mkdir -p "$OUT"
inventory="${OUT}/inventory-${VERSION}.json"
cyclonedx="${OUT}/sbom-${VERSION}.cdx.json"
spdx="${OUT}/sbom-${VERSION}.spdx.json"
# 1. One scan, with the declared dependencies, licences, and vulnerabilities.
scanoss-cli scan . \
--include deps,licenses,vulns \
--output "$inventory"
# 2. Convert offline: no second scan, no API calls.
scanoss-cli sbom "$inventory" --format cyclonedx --output "$cyclonedx"
scanoss-cli sbom "$inventory" --format spdx --output "$spdx"
# 3. Sanity checks before publishing.
jq -e '.bomFormat == "CycloneDX"' "$cyclonedx" > /dev/null
jq -e '.spdxVersion | startswith("SPDX-2.")' "$spdx" > /dev/null
# 4. Checksums, so consumers can verify the files.
( cd "$OUT" && sha256sum "$(basename "$inventory")" "$(basename "$cyclonedx")" "$(basename "$spdx")" > "SHA256SUMS-${VERSION}" )
jq -r '"Components: \(.components | length) Vulnerabilities: \(.vulnerabilities // [] | length)"' "$inventory"
```
## What each step does
1. **Scan once.** `scan .` fingerprints the release, matches it against the SCANOSS Knowledge
Base, and writes the raw inventory. `--include` adds three layers:
* `deps`: dependencies declared in manifest files such as `package.json` or `go.mod`,
resolved through the API. They appear with `"scope": "declared"`.
* `licenses`: the licences of every component, detected or declared.
* `vulns`: known vulnerabilities for every component.
2. **Convert.** `scanoss-cli sbom` reads the inventory and writes the requested format. It does
not contact the API, so it is fast and gives the same result each time. `--format` is
required.
3. **Check.** `jq -e` exits non-zero if a file is not the expected format, so the job stops
before it publishes a broken SBOM.
4. **Checksums.** A `SHA256SUMS` file lets consumers confirm they have the files you published.
Attach the files in `sbom/` to the release, or upload them to wherever you store release
artefacts.
## What the output means
| File | Contains | Use it for |
| - | - | - |
| `inventory-.json` | The scanoss-cli raw inventory: every component with its evidence, licences, and a flat vulnerability list. Includes all layers. | Your own record, and the input for a later [refresh](/en/latest/developer-tools/recipes/refresh-sbom-with-enrich) |
| `sbom-.cdx.json` | CycloneDX 1.7: components, licences, evidence, vulnerabilities | Security tooling and customers that ask for CycloneDX |
| `sbom-.spdx.json` | SPDX 2.3: components and licences. The CLI combines several licences on one component with `AND`. | Licence compliance and customers that ask for SPDX |
What each format can hold differs:
| Layer | raw | CycloneDX | SPDX |
| - | - | - | - |
| Components and evidence | Yes | Yes | Yes |
| Licences | Yes | Yes | Yes |
| Vulnerabilities | Yes | Yes | No. The CLI leaves them out and prints a warning. |
| Cryptography, geoprovenance | Yes | No | No |
A component that is both detected in your code and declared in a manifest appears once. Components
approved in `scanoss.json` carry the `scanoss:identified` property in CycloneDX.
## How it fails the pipeline
The script uses `set -euo pipefail`, so it stops at the first failing command:
| Failure | Exit code |
| - | - |
| The scan fails (no key, API unreachable) | `1`, from scanoss-cli |
| A conversion fails | `1`, from scanoss-cli |
| A file is not the expected format | `1`, from `jq -e` |
An SBOM documents what a release contains. It does not decide whether the release is allowed. To
block a release on licences or vulnerabilities, run the
[licence and vulnerability gate](/en/latest/developer-tools/recipes/licence-and-vulnerability-gate)
on `inventory-.json` before you publish:
```bash theme={null}
ci/scanoss-policy.sh "sbom/inventory-${VERSION}.json"
```
## Variations
* **One format only.** Skip the raw inventory and scan straight into a format:
`scanoss-cli scan . --include deps,licenses,vulns --format cyclonedx --output sbom.cdx.json`.
You lose the raw inventory, but you can still [refresh](/en/latest/developer-tools/recipes/refresh-sbom-with-enrich)
the CycloneDX file later.
* **Convert an SBOM you already have.** `scanoss-cli sbom` also converts between CycloneDX and
SPDX: `scanoss-cli sbom bom.cdx.json --format spdx --output bom.spdx.json`.
* **Run in a container.** See [Using Docker](/en/latest/cli/scanoss-go/using-docker).
# Scan every pull request in CI
Source: https://docs.scanoss.com/en/latest/developer-tools/recipes/scan-pull-requests
Scan the files each pull request adds or changes with scanoss-cli, and fail the pull request when it brings in open source you have not approved.
## Goal
Run a SCANOSS scan on every pull request. Fail the check when the change contains open source
code that your project's `scanoss.json` does not approve yet, and keep the full result as a build
artefact.
## When to use it
* You want to catch copied or vendored open source code before anyone merges it.
* You already have a CI system and want SCANOSS as one more check in it.
* You approve components through code review of `scanoss.json` (see
[Approve components with scanoss.json](/en/latest/developer-tools/recipes/approve-components-bom-rules)).
To also fail on licences or vulnerabilities, add the
[licence and vulnerability gate](/en/latest/developer-tools/recipes/licence-and-vulnerability-gate)
to the same job.
## Prerequisites
* A Linux x86-64 CI runner with `bash`, `git`, `curl`, `jq`, and `sha256sum`
* `SCANOSS_API_KEY` available to the job as a secret environment variable
* The pull request's target branch available in the checkout, with enough history to find the
merge base (a full clone is simplest)
* `scanoss/unapproved.jq` committed to the repository (see [Approve components](/en/latest/developer-tools/recipes/approve-components-bom-rules#how-it-fails-the-pipeline))
## The script
Save this as `ci/scanoss-pr.sh` in your repository and make it executable
(`chmod +x ci/scanoss-pr.sh`).
```bash theme={null}
#!/usr/bin/env bash
# Scan a pull request with scanoss-cli and fail on unapproved open source.
#
# Environment:
# SCANOSS_API_KEY required, from your CI secret store
# BASE_REF the branch the pull request targets, e.g. origin/main
# SCAN_MODE "changed" (default): scan only added/modified files
# "full": scan the whole repository
# SCANOSS_CLI_VERSION scanoss-cli release to install (default v0.9.0)
set -euo pipefail
BASE_REF="${BASE_REF:-origin/main}"
SCAN_MODE="${SCAN_MODE:-changed}"
SCANOSS_CLI_VERSION="${SCANOSS_CLI_VERSION:-v0.9.0}"
RESULTS="${RESULTS:-scanoss-results.json}"
# 1. Install scanoss-cli outside the repository, so it is not scanned itself.
tools_dir="$(mktemp -d)"
archive="scanoss-cli-linux-amd64.tar.gz"
release_url="https://github.com/scanoss/scanoss.go/releases/download/${SCANOSS_CLI_VERSION}"
(
cd "$tools_dir"
curl -sSLO "${release_url}/${archive}"
curl -sSLO "${release_url}/checksums.txt"
sha256sum -c --ignore-missing checksums.txt
tar xzf "$archive" scanoss-cli
)
export PATH="${tools_dir}:${PATH}"
scanoss-cli --version
# 2. Choose what to scan.
target="."
if [ "$SCAN_MODE" = "changed" ]; then
# Copy the added, copied, modified, and renamed files into a temporary
# directory, keeping their paths.
target="$(mktemp -d)"
git diff --name-only -z --diff-filter=ACMR "${BASE_REF}...HEAD" \
| git checkout-index --prefix="${target}/" -z --stdin
if [ -z "$(ls -A "$target")" ]; then
echo "No added or modified files to scan."
exit 0
fi
fi
# 3. Use the repository's scanoss.json, if there is one.
settings_flag=""
if [ -f scanoss.json ]; then
settings_flag="--settings=scanoss.json"
fi
# 4. Scan. Exits 1 if the scan itself fails.
scanoss-cli scan "$target" \
--include licenses,vulns \
${settings_flag:+"$settings_flag"} \
--output "$RESULTS"
# 5. Gate: every detected component must be approved in scanoss.json.
# scanoss/unapproved.jq is the filter from the "Approve components" recipe.
rules=scanoss.json; [ -f "$rules" ] || rules=/dev/null
unapproved="$(jq -c --slurpfile rules "$rules" -f scanoss/unapproved.jq "$RESULTS" \
| jq -r '"\(.purl)@\(.version // "unknown") in: \([.evidence[]?.path] | join(", "))"')"
if [ -n "$unapproved" ]; then
echo "Open source components that are not approved in scanoss.json:"
echo "$unapproved"
exit 2
fi
echo "No unapproved open source components."
```
## What each step does
1. **Install.** Downloads a pinned scanoss-cli release, checks it against the published
`checksums.txt`, and puts it on the `PATH`. It goes into a temporary directory so a full scan
does not fingerprint the binary.
2. **Choose the target.** In `changed` mode, `git diff` lists the files the pull request adds or
modifies compared to the merge base with `BASE_REF`. `git checkout-index` copies exactly those
files into a temporary directory with the same relative paths. The script leaves out deleted
files, because they cannot bring in new code. In `full` mode the script scans the whole
repository, which suits small repositories or a nightly job.
3. **Settings.** When the script scans the changed-files directory, the CLI cannot find the
repository's `scanoss.json` by itself, so the script passes it with `--settings`. Its BOM rules
(approved components) and skip rules then apply. Because the copied files keep their relative
paths, rules scoped to a `path` still match.
4. **Scan.** The CLI fingerprints the files locally, uploads only the fingerprints, waits for the
result, and writes the raw inventory to `scanoss-results.json`. `--include licenses,vulns`
adds licence and vulnerability data, so the same file can feed the
[licence and vulnerability gate](/en/latest/developer-tools/recipes/licence-and-vulnerability-gate).
5. **Gate.** The [`unapproved.jq` filter](/en/latest/developer-tools/recipes/approve-components-bom-rules#how-it-fails-the-pipeline)
lists every component detected in the code that `scanoss.json` doesn't approve. When a rule
pins a version, it approves only that version. Any line in the list fails the check.
## What the output means
A failing run prints one line per unapproved component:
```
Open source components that are not approved in scanoss.json:
pkg:github/madler/zlib@1.2.11 in: third_party/zlib/inflate.c, third_party/zlib/inflate.h
```
For each line, the reviewer decides:
* **The component is acceptable.** Add it to `bom.identify` in `scanoss.json` in the same pull
request, so reviewers see the change to `scanoss.json` in code review.
* **The match is not real**, for example your own code that resembles a public project. Add a
`bom.ignore` rule for that component, scoped to the path if you can.
* **The code should not be there.** Remove it from the pull request.
See [Approve components with scanoss.json](/en/latest/developer-tools/recipes/approve-components-bom-rules)
for each rule.
## How it fails the pipeline
| Exit code | Meaning |
| - | - |
| `0` | Scan completed, and every detected component is approved, or nothing changed |
| `1` | The scan or the install failed. Check the API key, network access, and the log. |
| `2` | The scan completed and found unapproved components |
scanoss-cli itself always exits `0` after a successful scan, whatever it finds. The `jq` gate in
step 5 turns findings into a failure.
## Run it in your CI system
All the logic is in the script. Your CI configuration only needs to check out the code with history,
expose the secret, set `BASE_REF`, call the script, and keep `scanoss-results.json`. These two
examples show the pattern. The same steps work in any CI system.
```yaml theme={null}
# .github/workflows/scanoss.yml
name: SCANOSS
on: pull_request
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Scan changed files
env:
SCANOSS_API_KEY: ${{ secrets.SCANOSS_API_KEY }}
BASE_REF: origin/${{ github.base_ref }}
run: ./ci/scanoss-pr.sh
- uses: actions/upload-artifact@v4
if: always()
with:
name: scanoss-results
path: scanoss-results.json
if-no-files-found: ignore
```
```yaml theme={null}
# .gitlab-ci.yml
scanoss:
image: debian:stable-slim
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
variables:
GIT_DEPTH: "0"
BASE_REF: origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME
before_script:
- apt-get update && apt-get install -y --no-install-recommends bash ca-certificates curl git jq
- git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
script:
- ./ci/scanoss-pr.sh
artifacts:
when: always
paths:
- scanoss-results.json
```
Define `SCANOSS_API_KEY` as a masked CI/CD variable.
## Variations
* **Scan the whole repository on a schedule.** Run the same script with `SCAN_MODE=full` from a
nightly job. A full scan also re-checks files that did not change, for example after you
remove an approval from `scanoss.json`.
* **Use the container image instead of downloading the binary.** Replace step 1 with
`docker run --rm -v "$PWD:/src" -e SCANOSS_API_KEY ghcr.io/scanoss/scanoss: scan /src ...`.
See [Using Docker](/en/latest/cli/scanoss-go/using-docker) for the output-permission caveat.
* **Speed up large scans.** Raise `--threads`, the number of fingerprint workers (default `10`).
# Fetch the raw text of a metadata fragment from the mz store
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/contents/fetch-the-raw-text-of-a-metadata-fragment-from-the-mz-store
/api-reference/developer-tools/v3-openapi.yaml get /v3/metadata-contents/{id}
Serves content referenced by evidence whose `source` is
`metadata_declared` (package manifests, build metadata).
Only registered when `metadata_kb` is set in the server config. When
omitted, this endpoint returns 404 at the router level and the
`evidence_url` field keeps pointing at the externally-hosted SCANOSS
metadata-contents endpoint.
# Fetch the raw text of a notice from the mz store
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/contents/fetch-the-raw-text-of-a-notice-from-the-mz-store
/api-reference/developer-tools/v3-openapi.yaml get /v3/notice-contents/{id}
Serves content referenced by evidence whose `source` is
`notice_declared` (LICENSE/NOTICE files).
# Fetch the raw text of a source file from the mz store
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/contents/fetch-the-raw-text-of-a-source-file-from-the-mz-store
/api-reference/developer-tools/v3-openapi.yaml get /v3/file-contents/{id}
Serves content referenced by evidence whose `source` is
`header_declared` or `spdx_tag` (detections inside source files).
Only registered when `sources_kb` is set in the server config. When
omitted, this endpoint returns 404 at the router level and the
`evidence_url` field in license responses keeps pointing at the
externally-hosted SCANOSS file-contents endpoint.
# Reachability for a caller-supplied frozen dependency tree.
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography-reachability/reachability-for-a-caller-supplied-frozen-dependency-tree
/api-reference/developer-tools/v3-openapi.yaml post /v3/cryptography/reachability/dep-tree
A **batch of independent `GetComponentReachability` evaluations**. The
`dependencies` field is a FLAT list of already-resolved components; the
server does not auto-resolve the list itself.
Every requested dependency yields exactly one `data[]` block, in
request order, carrying its own `info_code`. **A dependency without
data no longer suppresses the others** — branch on each block's
`info_code`, not on the envelope's.
Each block reports that component's full contained tree: its own crypto
plus crypto reached through its own recorded dependency closure
(`source: "indirect"`).
Exact versions in the request are treated as globally authoritative and
are applied wherever that component appears, including inside another
root's closure — see `applied_dependency_overrides`. Two different
exact versions for one component reject the request with
`INVALID_REQUEST`.
Best-effort stored membership plus client pins; not Maven-coherent
re-resolution.
# Reachability for a component stitched across its declared dep tree.
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography-reachability/reachability-for-a-component-stitched-across-its-declared-dep-tree
/api-reference/developer-tools/v3-openapi.yaml post /v3/cryptography/reachability/component
Returns the stitched cryptographic assets across the component and its
transitive dependencies, plus (optionally) per-asset call chains,
bounded forward calls, the crypto entry-point surface, supporting calls,
and the unfiltered callgraph. Reads from the separate crypto reachability results database
(enabled only when `reachability_db.dsn` is configured). Inspect
`info_code` for READY / error.
**Findings-only requests** (only `purl` and `requirement`: no
`include_*` flag and no `entry_point_signatures`) serve the report
crypto-finder produced when it mined the served version — the stored
findings for that `purl`, `version` and `rules_version`, after the same
version resolution and fallback (`actual_mined_version`) as any other
request. Every asset is passed through as mined: `finding_id`,
`occurrence_key`, `start_col`/`end_col`, `status`, `conditioned_value`,
`dependency_info` and every rule with its `message` and `severity`. The
result is the same as scanning that component with crypto-finder.
`finding_count` is the number of assets in the stored report, and
`schemas.findings` is that report's own version. When the stored row has
no readable report, the findings are rebuilt from the crypto
annotations instead, as every other request does; the request does not
fail.
Any other request rebuilds the findings from the crypto annotations,
which carry less per asset: no columns, status, occurrence key or
conditioned value, only the first rule (`id` only), and a `finding_id`
recomputed from the stitched file path.
# Crypto hints across every version satisfying a semver range
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography/crypto-hints-across-every-version-satisfying-a-semver-range
/api-reference/developer-tools/v3-openapi.yaml get /v3/cryptography/hints/range
# Crypto hints in range, batch
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography/crypto-hints-in-range-batch
/api-reference/developer-tools/v3-openapi.yaml post /v3/cryptography/hints/range
# Download a ruleset tarball
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/cryptography/download-a-ruleset-tarball
/api-reference/developer-tools/v3-openapi.yaml get /v3/cryptography/rulesets/{ruleset_name}/{version}/download
Only registered when `rulesets.storage_path` is set in the server
config. `version` is `latest` (resolved via a symlink at
`//latest`) or a semver string. The response is
`application/gzip` with `SCANOSS-Ruleset-*` and `X-Checksum-SHA256`
headers describing the manifest.
# Resolve declared dependencies + licenses for one purl
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/dependencies/resolve-declared-dependencies-+-licenses-for-one-purl
/api-reference/developer-tools/v3-openapi.yaml get /v3/dependencies/dependencies
# Resolve declared dependencies for many purls
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/dependencies/resolve-declared-dependencies-for-many-purls
/api-reference/developer-tools/v3-openapi.yaml post /v3/dependencies/dependencies
# Walk declared dependencies bounded by depth and limit
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/dependencies/walk-declared-dependencies-bounded-by-depth-and-limit
/api-reference/developer-tools/v3-openapi.yaml post /v3/dependencies/transitive
All entry components must share a purl_type and that purl_type
must be in the supported set (composer, crates, gem, maven, npm).
`depth` defaults to 2 (max 2); `limit` defaults to 1000 (max
5000). The response groups the results per requested component:
each entry is echoed (purl, requirement, resolved version) with its
own transitive dependencies. An entry requested without a version or
requirement resolves to the component's latest version.
# Contributor origin distribution by timezone-based country
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/geoprovenance/contributor-origin-distribution-by-timezone-based-country
/api-reference/developer-tools/v3-openapi.yaml get /v3/geoprovenance/origin
# Contributor origin distribution for many purls
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/geoprovenance/contributor-origin-distribution-for-many-purls
/api-reference/developer-tools/v3-openapi.yaml post /v3/geoprovenance/origin
# Declared + curated contributor locations for a single purl
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/geoprovenance/declared-+-curated-contributor-locations-for-a-single-purl
/api-reference/developer-tools/v3-openapi.yaml get /v3/geoprovenance/countries
# Declared + curated contributor locations for many purls
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/geoprovenance/declared-+-curated-contributor-locations-for-many-purls
/api-reference/developer-tools/v3-openapi.yaml post /v3/geoprovenance/countries
# Components for a file MD5
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/scan/components-for-a-file-md5
/api-reference/developer-tools/v3-openapi.yaml get /v3/raw/scan
Look up a file by hash and return its matching components (`-P`). Also served at the legacy `/v3/scan/raw` alias (used by the batchScanner pipeline) — **deprecated, provisional for compatibility and to be removed in a future version**; new clients should use `/v3/raw/scan`.
# Components for one or more urlhashes
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/scan/components-for-one-or-more-urlhashes
/api-reference/developer-tools/v3-openapi.yaml post /v3/components/scan
Resolve one or more components by urlhash (`-C`). `urlhash` accepts a
comma-separated list to batch. The optional JSON body is an SBOM passed
to the engine for context (`-s`). Also served at the legacy
`/v3/scan/components` alias (used by the batchScanner pipeline) —
**deprecated, provisional for compatibility and to be removed in a future
version**; new clients should use `/v3/components/scan`.
# Match a raw WFP body (snippets)
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/scan/match-a-raw-wfp-body-snippets
/api-reference/developer-tools/v3-openapi.yaml post /v3/snippets/scan
Match a raw WFP body (`text/plain`) against the KB (`-S`). The body must
contain at least one snippet-hash line (`N=`); a WFP with only a
file hash yields `500`. Optional `threshold` sets the minimum match
percentage (`-T`). Also served at the legacy `/v3/scan/snippets` alias
(used by the batchScanner pipeline) — **deprecated, provisional for
compatibility and to be removed in a future version**; new clients should
use `/v3/snippets/scan`.
# Poll progress / result of an async (Mode A) batch scan
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/scan/poll-progress-result-of-an-async-mode-a-batch-scan
/api-reference/developer-tools/v3-openapi.yaml get /v3/wfp/scan/{id}
Report the state of an async session by `scan_id` (given as a path
segment). Reflects upload progress (`uploading`), then scan progress
(`scanning`), then the result (`completed`, with `result`). Terminal
engine failures are reported in-band as `status: failed` with an `error`
string — not as an HTTP error code. Only Mode A creates a session;
Mode B returns its result synchronously and has nothing to poll.
# Scan a WFP — synchronous single-shot, or one block of a chunked async upload
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/scan/scan-a-wfp-—-synchronous-single-shot-or-one-block-of-a-chunked-async-upload
/api-reference/developer-tools/v3-openapi.yaml post /v3/wfp/scan
Dual-mode upload, selected by the presence of the `X-Scan-Id` request
header. Both modes return the same `ScanEnvelope` envelope, and the
`result` payload is byte-identical to the polled async result (both run
the same batchScanner pipeline → same `Report`).
**Mode B — synchronous single-shot (no `X-Scan-Id`).** The request body
is one complete WFP (`application/octet-stream`, capped at
`scan.max_upload_size`), scanned inline. Responds `200` with a
`ScanEnvelope` already at `status: completed`, carrying `result`,
`files_total` and a fresh `scan_id` (for traceability only — no session
is created, nothing to poll). `Content-Range` is ignored. An
empty/whitespace body → `400 INVALID_BODY`. The request blocks for the
whole scan, so this mode is bounded by the request timeout and intended
for small inputs.
**Mode A — chunked async (`X-Scan-Id` present).** One block of a
multi-block upload too large for a single request. The client generates
the session id — a **canonical lowercase UUIDv7** — and sends it on
**every** block (it is what a load balancer hashes on to keep all blocks
on one instance). `Content-Range: bytes -/` is
**required**. Blocks are reassembled by their declared byte offset, so
they **may be uploaded in any order or concurrently**; the first
received block need not start at offset 0. Each block responds `202`
with a `ScanEnvelope` (`status: uploading`). When the received ranges
fully cover `[0, total)` with no gaps, the upload is sealed and the
batchScanner pipeline is queued (its stages call the scan API over
HTTP); poll `GET /v3/wfp/scan/{id}` for progress/result.
`received_bytes` reports the total distinct bytes covered so far.
Re-POSTing an already-received range with identical bytes is an
idempotent `202` no-op.
Mode A errors: a non-canonical / non-UUIDv7 id → `400 INVALID_SCAN_ID`;
missing `Content-Range` → `400 MISSING_RANGE`; malformed `Content-Range`
(or a body length that disagrees with the range) → `400 INVALID_RANGE`;
an overlapping block carrying **conflicting** bytes, or reuse of a
sealed id → `409 RANGE_CONFLICT` (note: out-of-order arrival alone is
**not** a conflict); a block or declared total over the configured
limits → `413 PAYLOAD_TOO_LARGE`.
# Scan an uploaded WFP / source archive (synchronous)
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/scan/scan-an-uploaded-wfp-source-archive-synchronous
/api-reference/developer-tools/v3-openapi.yaml post /v3/wfp/scan/direct
Upload a fingerprint (WFP) or source archive as `multipart/form-data`
(single file part `file`) and scan it with the engine (`-w`). Blocks
until the engine returns; the body is the engine's native scan output.
# CPEs detected for a single (purl, requirement)
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/vulnerabilities/cpes-detected-for-a-single-purl-requirement
/api-reference/developer-tools/v3-openapi.yaml get /v3/vulnerabilities/cpes
# CPEs for many components
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/vulnerabilities/cpes-for-many-components
/api-reference/developer-tools/v3-openapi.yaml post /v3/vulnerabilities/cpes
# Vulnerabilities for a single (purl, requirement)
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/vulnerabilities/vulnerabilities-for-a-single-purl-requirement
/api-reference/developer-tools/v3-openapi.yaml get /v3/vulnerabilities/vulnerabilities
Merges the local SCANOSS KB (source=NVD) with osv.dev (source=OSV)
and enriches every CVE with EPSS data. Either source can be
disabled via the server config — at least one must be on.
# Vulnerabilities for many components
Source: https://docs.scanoss.com/developer-tools/api-reference/v3/vulnerabilities/vulnerabilities-for-many-components
/api-reference/developer-tools/v3-openapi.yaml post /v3/vulnerabilities/vulnerabilities
# Configuration
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/configuration
Configure Crypto Finder with command-line flags, environment variables, a config file, or a scanoss.json file in your project.
## Configuration priority
Crypto Finder applies settings in this order, from highest to lowest priority:
1. **Command-line flags** (e.g., `--api-key`, `--scanner`)
2. **Environment variables** (e.g., `SCANOSS_API_KEY`)
3. **Config file** (`~/.scanoss/crypto-finder/config.json`)
4. **Project settings** (`scanoss.json` in target directory)
5. **Default values**
## Application configuration
### Config file location
```
~/.scanoss/crypto-finder/config.json
```
### Setting up configuration
Use the `configure` command to set persistent application settings:
```bash theme={null}
# Configure API key
crypto-finder configure --api-key YOUR_API_KEY
# Configure custom API URL
crypto-finder configure --api-url https://custom.scanoss.com
# Configure both
crypto-finder configure --api-key YOUR_KEY --api-url https://custom.scanoss.com
```
### Config file format
```json theme={null}
{
"api_key": "your-scanoss-api-key",
"api_url": "https://api.scanoss.com",
"java_jdk_major": "auto",
"java_jdk_homes": {
"8": "/opt/jdks/jdk8",
"11": "/opt/jdks/jdk11",
"17": "/opt/jdks/jdk17",
"21": "/opt/jdks/jdk21"
}
}
```
### Environment variables
| Variable | Description | Example |
| - | - | - |
| `SCANOSS_API_KEY` | SCANOSS API key for remote rulesets | `export SCANOSS_API_KEY=abc123` |
| `SCANOSS_API_URL` | Custom API base URL | `export SCANOSS_API_URL=https://custom.com` |
| `SCANOSS_JAVA_JDK_MAJOR` | Java JDK major for Java dependency resolution and type enrichment (`auto`, `8`, `11`, `17`, `21`) | `export SCANOSS_JAVA_JDK_MAJOR=21` |
| `SCANOSS_JAVA_JDK_HOMES` | Comma-separated Java home mappings used for explicit JDK selection | `export SCANOSS_JAVA_JDK_HOMES=17=/opt/jdks/jdk17,21=/opt/jdks/jdk21` |
| `SCANOSS_FINDINGS_CACHE_BACKEND` | FindingsCache backend (`disk` or `postgres`); default `disk` | `export SCANOSS_FINDINGS_CACHE_BACKEND=postgres` |
| `SCANOSS_FINDINGS_CACHE_DSN` | Postgres connection string used when backend is `postgres` (required) | `export SCANOSS_FINDINGS_CACHE_DSN=postgres://user:pass@host:5432/db?sslmode=require` |
| `SCANOSS_FINDINGS_CACHE_TABLE` | Optional table name override; default `findings_cache` | `export SCANOSS_FINDINGS_CACHE_TABLE=cf_findings_cache` |
### Java runtime selection
For Java dependency scans and call graph export, Crypto Finder can use a specific JDK major version for each scan. The supported majors are `8`, `11`, `17`, and `21`.
On the command line, use `--java-jdk-major` and the repeatable `--java-jdk-home =`. In the environment or the config file, use `SCANOSS_JAVA_JDK_MAJOR`, `SCANOSS_JAVA_JDK_HOMES`, or the matching config file keys. The standalone CLI defaults to `auto`, which uses the ambient `JAVA_HOME` when it is available.
Example:
```bash theme={null}
crypto-finder scan \
--scan-dependencies \
--java-jdk-major 21 \
--java-jdk-home 17=/opt/jdks/jdk17 \
--java-jdk-home 21=/opt/jdks/jdk21 \
/path/to/java-project
```
### FindingsCache backend
Crypto Finder caches each dependency's scan results in a `FindingsCache`, so a warm scan skips the OpenGrep work for dependencies it has already analysed. There are two backends:
| Backend | Storage | When to use |
| - | - | - |
| `disk` (default) | `~/.scanoss/crypto-finder/cache/findings/` | Local development, single-machine repeated scans |
| `postgres` | a Postgres database (`findings_cache` table) | Multi-worker fleet deployments where the cache must be shared across machines |
Select the backend in any of these ways:
| Source | Setting |
| - | - |
| CLI | `--findings-cache=disk` or `--findings-cache=postgres` |
| Environment | `SCANOSS_FINDINGS_CACHE_BACKEND=postgres` |
| Config file | `findings_cache_backend: postgres` |
With `postgres`, you MUST provide the connection string in `SCANOSS_FINDINGS_CACHE_DSN`. If it is missing, the scan exits with a clear error. It never falls back to disk silently, because a misconfigured fleet would then lose cache sharing and nobody would notice.
Crypto Finder creates the table on first use with an idempotent `CREATE TABLE IF NOT EXISTS`, so there is no separate migration step. The default table name is `findings_cache`. If you run several deployments in the same database, override it with `SCANOSS_FINDINGS_CACHE_TABLE`.
Example:
```bash theme={null}
export SCANOSS_FINDINGS_CACHE_DSN="postgres://cf:cf@db.internal:5432/scanoss?sslmode=require"
crypto-finder scan \
--scan-dependencies \
--findings-cache=postgres \
--export-callgraph callgraph.json \
/path/to/project
```
How the cache behaves:
* The cache key includes the rules hash, so changing the rules invalidates entries automatically. You never need to evict entries by hand.
* A disk backend failure isn't fatal. Crypto Finder logs a warning and the scan continues without caching. A postgres backend failure, such as a missing DSN, an unreachable database, or a schema bootstrap error, is fatal.
* Each `crypto-finder scan` invocation opens its own Postgres pool and closes it when the scan finishes. For long-lived workers, reuse a single binary process where you can, so the pool setup cost is spread over more scans.
## Project configuration (scanoss.json)
The `scanoss.json` file in your project directory configures scan behavior and skip patterns.
### File location
Place `scanoss.json` in the root of the directory you're scanning:
```
your-project/
├── scanoss.json
├── src/
└── ...
```
### Configuration schema
Crypto Finder follows the [SCANOSS Settings Schema](https://scanoss.readthedocs.io/projects/scanoss-py/en/latest/scanoss_settings_schema.html).
### Basic example
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["node_modules/", "target/", "venv/", "*.min.js"]
},
"sizes": {
"max_file_size": 10485760
}
}
}
}
```
## Skip patterns
Skip patterns control which files and directories Crypto Finder leaves out of a scan.
### Default skip patterns
Crypto Finder excludes these patterns automatically:
| Category | Patterns |
| - | - |
| Version control | `.git/`, `.svn/`, `.hg/`, `.bzr/` |
| Dependencies | `node_modules/`, `vendor/`, `venv/`, `virtualenv/`, `__pycache__/` |
| Build artifacts | `dist/`, `build/`, `target/`, `*.min.js`, `*.min.css` |
| Archives | `*.zip`, `*.tar`, `*.tar.gz`, `*.tar.bz2`, `*.jar`, `*.war`, `*.ear` |
| Binaries | `*.exe`, `*.dll`, `*.so`, `*.dylib`, `*.bin` |
| Generated or vendored stubs | `shaded/`, `*OuterClass.java`, `*_pb2.py`, `*_pb2_grpc.py`, `*.pb.go`, `zz_generated*.go`, `*_pb.js`, `*_pb.ts`, `*.pb.cc`, `*.pb.h`, `*.grpc.pb.cc`, `*.grpc.pb.h`. Also files whose header is the protocol-buffer compiler marker (or prost/bindgen), except protobuf runtimes scanned as the target artifact. |
For the complete list, see the [default skip patterns](https://github.com/scanoss/crypto-finder/blob/main/internal/skip/source_defaults.go).
### Custom skip patterns
#### Pattern types
1. **Directory patterns** (end with `/`):
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["custom-dir/", "temp/"]
}
}
}
}
```
2. **File extension patterns**:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["*.log", "*.tmp", "*.cache"]
}
}
}
}
```
3. **Specific file patterns**:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["package-lock.json", "yarn.lock"]
}
}
}
}
```
4. **Path patterns**:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["src/generated/", "test/fixtures/"]
}
}
}
}
```
### Size limits
Set the maximum size of a file to scan:
```json theme={null}
{
"settings": {
"skip": {
"sizes": {
"max_file_size": 10485760
}
}
}
}
```
## Advanced configuration examples
### Monorepo configuration
For large monorepos with several subprojects:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": [
"*/node_modules/",
"*/dist/",
"*/build/",
"*/target/",
"docs/",
"scripts/",
"*.test.js",
"*.spec.ts"
]
}
}
}
}
```
### Frontend project configuration
For JavaScript and TypeScript projects:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": [
"node_modules/",
"dist/",
"build/",
".next/",
".nuxt/",
"coverage/",
"*.min.js",
"*.bundle.js",
"*.map"
]
}
}
}
}
```
### Backend project configuration
For Java, Python, and Go projects:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": [
"target/",
"venv/",
"vendor/",
"__pycache__/",
"*.pyc",
"*.class",
"*.jar"
]
}
}
}
}
```
### CI/CD configuration
This configuration scans as little as possible, to keep CI/CD pipelines fast:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": [
"node_modules/",
"vendor/",
"venv/",
"target/",
"dist/",
"build/",
"test/",
"tests/",
"*.test.*",
"*.spec.*",
"*.min.*"
]
}
}
}
}
```
## Scanner configuration
### Choosing a scanner
Crypto Finder supports more than one scanner. Select one with a command-line flag:
```bash theme={null}
# Use OpenGrep (default, recommended)
crypto-finder scan /path/to/code
# Use Semgrep
crypto-finder scan --scanner semgrep /path/to/code
```
### Cross-file analysis (Semgrep Pro)
With the Semgrep scanner, you can turn on cross-file (interfile) analysis with the `--interfile` flag. The flag adds `--pro` to the underlying Semgrep command, which turns on Semgrep Pro features such as cross-file taint tracking and type inference.
```bash theme={null}
# Enable cross-file analysis with Semgrep Pro
crypto-finder scan --scanner semgrep --interfile /path/to/code
```
> **Note:** `--interfile` works only with `--scanner semgrep`, and using it with any other scanner is an error. It requires a valid Semgrep Pro license.
## Language detection
### Automatic detection
By default, crypto-finder uses [go-enry](https://github.com/go-enry/go-enry) to detect a project's languages.
### Manual override
You can override the detected languages:
```bash theme={null}
# Scan only Java and Python files
crypto-finder scan --languages java,python /path/to/code
# Scan single language
crypto-finder scan --languages go /path/to/code
```
### Supported languages
The scanner has rules for these languages, among others:
* C/C++
* C#
* Go
* Java
* JavaScript/TypeScript
* Kotlin
* PHP
* Python
* Ruby
* Rust
* Swift
Because of language detection, Crypto Finder loads only the rules that apply to the project, which makes scans faster.
## Timeout configuration
### Default timeout
The default scan timeout is **10 minutes**.
### Custom timeout
```bash theme={null}
# 30 minute timeout
crypto-finder scan --timeout 30m /path/to/code
# 2 hour timeout
crypto-finder scan --timeout 2h /path/to/code
# 90 second timeout
crypto-finder scan --timeout 90s /path/to/code
```
### Recommended timeouts
| Project size | Recommended timeout |
| - | - |
| Small (\<1000 files) | 5m |
| Medium (1000-10000 files) | 15m |
| Large (10000-50000 files) | 30m |
| Very large (>50000 files) | 1h+ |
## Output configuration
### Output destination
```bash theme={null}
# Write to file
crypto-finder scan --output results.json /path/to/code
# Write to stdout (default)
crypto-finder scan /path/to/code
# Pipe to another tool
crypto-finder scan /path/to/code | jq '.findings | length'
```
### Output format
```bash theme={null}
# SCANOSS Interim JSON format (default)
crypto-finder scan --format json /path/to/code
# CycloneDX CBOM format
crypto-finder scan --format cyclonedx /path/to/code
```
## Logging configuration
### Verbosity levels
```bash theme={null}
# Normal output
crypto-finder scan /path/to/code
# Verbose logging (info level)
crypto-finder scan -v /path/to/code
crypto-finder scan --verbose /path/to/code
# Debug mode (debug info)
crypto-finder scan -d /path/to/code
crypto-finder scan --debug /path/to/code
```
# Dependency scanning and call chain attribution
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/dependency-scanning
How crypto-finder finds cryptographic usage in project dependencies and traces it back to user code through call graph analysis.
Contract note: the interim report holds findings metadata only (`source`, `dependency_info`, `finding_id`, and optional `occurrence_key`). The finding-centric reachability slices, such as `call_chains`, `entry_call`, and `crypto_call`, are in the separate `--export-callgraph` artifact, not in the interim report.
## Overview
With `--scan-dependencies`, crypto-finder scans more than the user's source code. It resolves the project's dependency tree, scans each dependency for cryptographic usage, builds a cross-package call graph, and traces each finding back to the user's code. The question it answers is **"Does my code actually reach this crypto function?"**
```mermaid theme={null}
flowchart TB
subgraph Input
UC[User Code your project]
DEP[Dependencies go.sum / pom.xml / ...]
end
subgraph Pipeline["ScanWithDependencies Pipeline"]
direction TB
S1["① Resolve Dependencies"]
S2["② Load & Filter Rules"]
S3["③ Scan Dependencies in Parallel"]
S4["④ Build Call Graph"]
S5["⑤ Trace & Attribute Findings"]
S6["⑥ Merge Reports"]
S1 --> S2 --> S3 --> S4 --> S5 --> S6
end
UC --> S1
DEP --> S1
S6 --> OUT[Final Report with source, finding_id, optional occurrence_key, dependency_info]
```
## The six-step pipeline
The full pipeline is in [`DependencyScanner.ScanWithDependencies()`](https://github.com/scanoss/crypto-finder/blob/main/internal/engine/dependency_scanner.go). The sections below explain what each step does and why.
### Step 1: Resolve dependencies
```mermaid theme={null}
flowchart LR
Target["Project Dir"] --> Resolver
Resolver --> RR["ResolveResult"]
RR --> Modules["[]Dependency {Module, Version, Dir}"]
RR --> RootMod["RootModule e.g. github.com/myorg/app"]
```
The [`Resolver` interface](https://github.com/scanoss/crypto-finder/blob/main/internal/dependency/resolver.go) discovers all dependencies and locates their source code on disk. Each ecosystem has its own resolver implementation (see [Supported ecosystems](#supported-ecosystems) below). Each `Dependency` carries:
| Field | Go Example | Java Example | Python Example | Rust Example | Purpose |
| - | - | - | - | - | - |
| `Module` | `golang.org/x/crypto` | `org.bouncycastle:bcprov-jdk18on` | `cryptography` | `ring` | Import path / coordinate |
| `Version` | `v0.17.0` | `1.77` | `42.0.5` | `0.17.8` | Resolved version |
| `Dir` | `~/go/pkg/mod/golang.org/x/crypto@v0.17.0` | `~/.crypto-finder/cache/sources/org.bouncycastle:bcprov-jdk18on/1.77/` | `~/.local/lib/python3.x/site-packages/cryptography/` | `~/.cargo/registry/src/.../ring-0.17.8/` | Filesystem path to scan |
| `Files` | `[.../x/crypto@v0.17.0/chacha20poly1305/chacha20poly1305.go, ...]` | unset | `[.../site-packages/google/auth/_helpers.py, ...]` for a namespace distribution, `[.../site-packages/configobj/__init__.py, ..., .../site-packages/validate/__init__.py]` for one with several top-level roots, otherwise unset | unset | The only files of `Dir` the dependency's source holds: the Go files the host build compiles in Go's imported packages, or a Python distribution's own files in a shared namespace directory; unset scans all of `Dir` |
The `RootModule` (e.g. `github.com/myorg/app` for Go, `com.myorg` for Java) is the default user-code prefix. For Java, packages of functions whose files live in the scanned project tree are also user code. That matters for Gradle projects that omit `group` and therefore export a project name rather than a Java package prefix.
### Step 2: Load and filter rules
Crypto-finder loads the rules once and keeps only those for the ecosystem's languages. A Go project keeps only `go` rules, and a Java project only `java` rules. Crypto-finder never runs rules for other languages against the source, which cuts scanner overhead.
The kept rules are then written to one merged file, which every dependency scan reuses. OpenGrep loads each config file separately on every run, so one file loads faster than hundreds. The merged file is named `merged-rules.yaml` but holds JSON text, one rule per line: JSON is valid YAML, and OpenGrep parses it about seven times faster. Each merged rule ID carries its source file's directory, the prefix OpenGrep derived from the file location before, so finding rule IDs do not change. Files the merge cannot represent exactly are passed to the scanner unchanged. These are invalid YAML, `*.test.yaml` fixtures, hidden paths, and YAML with no JSON form of the same meaning, such as a number written `0x10` or an alias.
### Step 3: Scan dependencies in parallel
```mermaid theme={null}
flowchart TB
Work["Deduplicated Deps"] --> Cache{"Findings cache per dependency"}
Cache -->|hit| R0["Cached report"]
Cache -->|miss| API{"SCANOSS API findings published for the package version, with an API key"}
API -->|published| RA["Published report"]
API -->|none| Shape["Batches balanced by source bytes, at most 16 roots each"]
Shape --> Pool["Worker Pool default: half the cores, max 8"]
Pool --> W1["Worker 1: one scanner process over roots A, B, C"] --> R1["Reports A, B, C"]
Pool --> W2["Worker 2: one scanner process over roots D, E"] --> R2["Reports D, E"]
```
Dependencies are deduplicated by `module@version` and processed in a stable order (`module`, `version`, `dir`) so repeated scans produce deterministic report and call graph inputs. The findings cache is consulted per dependency; the misses are then grouped into batches and each worker runs **one scanner process per batch**, over every root in it, instead of one process per dependency. OpenGrep loads the rules once per process, and that load is the dominant cost of scanning a small dependency (about 20 s for the JavaScript and TypeScript rules), so a 50-package npm project pays it a handful of times instead of 50. Each process gets `--jobs` sized to its share of the cores (see the `scannerJobs` log field).
**With an API key, published findings stand in for a dependency's detection.** The SCANOSS mining service scans each package version once and publishes its findings. When an API key is configured, a dependency the findings cache does not hold is looked up by versionless purl and version (`POST /v3/cryptography/reachability/component`, purl and requirement only). The published findings are taken as they are, including the closest mined patch the API serves for that version and the rules version it was mined with, and are kept to the dependency's own files. The dependency is then not scanned. A dependency the API has not mined, a lookup error, a refused key, or an API that does not offer the endpoint (HTTP 404) scans the dependency locally; a refused key or a missing endpoint stops further lookups for that scan, with one warning. Published findings are not written to the findings cache. The call graph, conditioned findings and reachability are always built from the local sources, so findings derived from calls between dependencies are kept. Each lookup sends the package's purl and version to the API. `--no-dependency-findings-api` scans every dependency locally (`internal/engine/dependency_findings_source.go`).
Each root's report is what a scan of that root alone produces. The OpenGrep adapter (`ScanRoots`, `internal/scanner/opengrep/batch.go`) attributes every result and error to the root holding its file (longest prefix) and runs the same transformation per root, with that root as the target, so paths, deduplication, rule IDs and finding IDs are unchanged. An error without a file, such as a memory limit reported for the whole process, marks every root in the batch incomplete. The dependency scanner then stamps the ruleset and enriches each report as it does for a single scan, and writes each dependency to the findings cache under its own key, still skipping a dependency whose scan stopped at a limit.
Batch shaping (`internal/engine/dependency_batches.go`):
* **Nested roots never share a process.** An npm dependency's scan excludes its own `node_modules/`, where another dependency may be installed, and an exclusion applies to the whole process. Roots are assigned a nesting level (0 when no other root holds them, otherwise one more than the root that does; a repeated directory counts as nested) and only roots of the same level are batched together. A scoped root that names none of another root's files does not hold it (`scanner.Disjoint`): Python namespace siblings, and a Python distribution rooted at `site-packages` beside the distributions installed below it, share a level. Scoped roots carry no exclusion anchored at their own directory, so sharing a process hides none of the other root's files.
* **Balanced by weight.** Each dependency is weighed by the bytes of its source files for the ecosystem's languages (a cheap walk that skips nested `node_modules`). Within a level, the misses are split into `max(ceil(n / 16), min(workers, n))` batches: heaviest dependency first, each into the lightest batch, so the workers finish together and a very large dependency keeps a process to itself. Batches are queued heaviest first.
* **Process timeout.** A batch gets the single-scan `--timeout` once per `ceil(roots / jobs)`: OpenGrep analyzes `jobs` files at a time, so the roots consume about that many single-scan budgets of wall time. With the default 10 minutes and 4 jobs, a 16-root batch may run for 40 minutes before it is treated as failed.
* **Failure isolation.** When a batch's process fails (exit status above 1, unreadable output, timeout), every dependency in it is scanned alone, as before batching, so one faulty dependency fails only itself and the others keep their findings and their cache entries. A canceled scan fails the batch's dependencies and rescans nothing.
* A batch of one root, which includes every dependency when the scanner cannot batch (the Semgrep adapter), goes through the single-scan path unchanged.
**npm dependencies are the production tree.** `NpmResolver` (`internal/dependency/npm_resolver.go`) drops the lockfile entries marked `"dev": true` (in a v1 lockfile, a dev entry together with the entries nested under it), so an installed development-only package is neither scanned nor put in the dependency graph. `devOptional` packages stay, because npm installs them without dev when an optional production dependency needs them, and workspace members stay because they are the project's own code. `--include-dev-dependencies` (`WithNpmDevDependencies`) restores the development-only packages. Absent dev packages, as after `npm install --omit=dev`, are never an error either way.
**Go dependencies scan only the files their imported packages compile.** A Go program contains only the packages it imports, and of each package only the files its build compiles, so the Go resolver lists, for each package in the production import closure, the Go files `go list -e -deps` reports in `GoFiles` and `CgoFiles` (`Files`), and the scan reads only those files: not the module's other packages, not the subdirectories of an imported package, which are packages of their own, and not the files that build constraints exclude. Build constraints apply file by file, for the scanning host (see Host build configuration under Go-specific): a file for another `GOOS` or `GOARCH` (`*_windows.go`, `//go:build darwin`), one whose build tag is unset, a `//go:build ignore` generator, and with `CGO_ENABLED=0` a file that imports `"C"` are not scanned. Only Go files are listed, because a Go dependency scan runs only Go rules and the Go call graph parses only Go files: a package's C, assembly and other files, and its `_test.go` files, never yielded a finding. modernc.org/libc, for example, ships one 4 MB `ccgo__.go` per platform, and the host build compiles one of them. The dependency's root stays the module directory, so attribution, result paths, finding IDs and the batch nesting levels are those of a whole-module scan; only the targets change. The scanner receives the files as named targets (a `DetectionScope`, the same mechanism as `--detect-paths-from`, with `--force-exclude` so the dependency's skip patterns still apply to them). Files are leaves, so the packages of one module, such as `foo/` and `foo/bar/`, never hide one another, and a batch over many files is split into several processes when the file list exceeds the command-line budget. OpenGrep keeps its language and size filters for named files, so a file in an imported package yields the findings it yielded in a whole-module scan. The findings cache key includes the scoped file list, so a whole-module entry is never served for a package-scoped scan, or one set of packages for another. A scanner that cannot limit detection to files (the Semgrep adapter) still scans the whole module, under the whole-module key, and the dependency keeps only the findings in its files.
**Python namespace distributions scan only their own files.** Several distributions can install into one namespace directory: protobuf, googleapis-common-protos, google-auth and google-api-core all resolve to `site-packages/google`. When two or more distributions resolve to the same `Dir`, the pip resolver gives each one the files under it that its `*.dist-info/RECORD` lists and that exist (`Files`). A distribution without a RECORD owns the files no sibling's RECORD lists; `top_level.txt` names import roots, not files, so it can choose `Dir` but cannot split it. A distribution with a directory of its own keeps `Files` unset and scans its whole `Dir` as before. The shared directory stays the root, so a finding's path is relative to it, as in a whole-directory scan (`auth/_helpers.py` for google-auth), and the files go to the scanner as named targets through the same `DetectionScope` as Go's packages. Siblings whose files are disjoint share a batch: the scanner attributes each result by the exact file named in a scope, so they are not nesting levels of one another. Siblings whose files overlap (two RECORDs listing one file, or two distributions without a RECORD) are still scanned in separate processes. Each file is therefore scanned once and reported under the one distribution that installed it. Previously every sibling scanned the whole namespace in its own process and reported every finding in it. A scanner that cannot limit detection to files (the Semgrep adapter) still scans the whole directory for each sibling, and each sibling keeps only the findings in its own files.
**Python distributions scan every top-level package and module they installed.** `packages_distributions()` maps import names to distributions, so one distribution can have several: `configobj` installs `configobj/` and `validate/`, `PyYAML` installs `yaml/` and `_yaml/`, setuptools installs `setuptools/`, `pkg_resources/` and `_distutils_hack/`. The resolver takes each name that is a directory in `site-packages`, or a module file `name.py` there (`six.py`), drops a name inside another one (`googleapiclient/discovery_cache`) names that are neither (compiled extensions), and `__pycache__`, which importlib infers from the bytecode of module files and which all module files share, and sorts the rest. A distribution with exactly one package directory keeps it as `Dir` and `ImportPath`, so its paths, finding IDs and cache keys are unchanged. Any other distribution is rooted at `site-packages`, the one directory that holds all its roots, with an empty `ImportPath` and `Files` listing every file under each of its package directories and each module file; its share of a namespace directory it also installs into is the files its RECORD lists, as above. Its finding paths therefore start with the package (`validate/__init__.py`, `six.py`). Previously the resolver kept whichever name it met first in a Go map, a different one from run to run: `configobj` scanned `configobj/` in one run and `validate/` in the next, setuptools one of its three packages, and a distribution whose first name was a module file, such as `six`, was skipped as a single-file module. Findings and call-graph functions of these distributions changed between runs of the same binary on the same environment; they no longer do. The scope of a distribution at `site-packages` names none of the other distributions' files, so it batches with them (see above) and never reports their findings.
**Dependency scans use their own exclusions.** `buildDepScanOptions` (`internal/engine/dependency_scanner.go`) replaces the primary scan's skip patterns with `skip.OnlyDefaultTestPatterns`, which keeps only the built-in test patterns (`DefaultSkippedTestPatterns` in `internal/skip/source_defaults.go`: `test/`, `tests/`, `src/test/`, `src/tests/`, `__tests__/`, `**/*Test.java`, `**/*Tests.java`, `**/*_test.go`, `**/test_*.py`). For npm it adds an anchor on the dependency's own `node_modules/`, because the dependency directory usually sits inside a `node_modules` tree itself and each child package is scanned under its own identity. It also sets `IncludeGitIgnored`, which passes the OpenGrep flag that disables `.gitignore` handling. As a result, none of these apply inside a dependency root:
* `--exclude` and `settings.skip.patterns.scanning` from `scanoss.json`. `--exclude` is not a way to hide dependency findings.
* The built-in skipped directories (`dist`, `build`, `vendor`, `target`, `docs`, `shaded`, ...) and the generated-stub globs (`*.pb.go`, `*_pb2.py`, ...). `--no-default-exclusions` has nothing to remove here.
* `.gitignore` files.
`--include-tests` leaves the test patterns out of the list, so a dependency scan then reads its test files too. The reason for the narrow list: a published package is not a checkout, and often ships its only code in `dist/` or `build/`, which the source-tree defaults would hide. Go dependencies narrow the scan further to the Go files each imported package compiles for the host build (see above), which never include `_test.go` files.
Dependencies without a usable local source directory are **not** sent to the scanner. They are logged as `Skipping dependency source scan: no local source directory` instead of triggering empty-path scanner failures. For Java, those dependencies still proceed to step 4 as **type-only** inputs as long as `module@version` can be resolved to a compiled JAR.
### Step 4: Build the call graph
The call graph builder uses **syntactic parsing** to process source files. It works on raw source, without a full Go toolchain, Java compiler, Python interpreter, or Rust toolchain. The [`Parser` interface](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/builder.go) abstracts all language-specific behavior:
```go theme={null}
type Parser interface {
ParseDirectory(dir string, packagePath string) ([]*FileAnalysis, error)
SubPackagePath(parentPath, dirName string) string
PackageSeparator() string // "/" for Go, "." for Java/Python, "::" for Rust
}
```
The [`NewParserForEcosystem()`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/parser_registry.go) factory selects the right parser (`GoParser`, `JavaParser`, `PythonParser`, or `RustParser`) based on the detected ecosystem.
```mermaid theme={null}
flowchart TB
subgraph "Graph Packages (full source parsing)"
U["User Code Dir + import path"]
D1["Dep with findings golang.org/x/crypto"]
D2["Dep with findings org.bouncycastle:bcprov-jdk18on"]
end
subgraph "Type-Only Packages (bytecode index only)"
T1["Dep without findings org.springframework:spring-core"]
T2["Dep without findings com.fasterxml.jackson:jackson-core"]
end
subgraph "Builder.BuildFromDirectories()"
direction TB
Parse["Phase 1: Parser.ParseDirectory() syntactic parsing per source file"]
FnMap["Functions map FunctionID → FunctionDecl"]
RevIdx["Callers reverse index callee → []callerID"]
TypeRes["Phase 2: Bytecode type index class names + method signatures + return types"]
Fluent["Fluent chain resolution propagate return types"]
Parse --> FnMap
FnMap --> RevIdx
RevIdx --> TypeRes
TypeRes --> Fluent
end
U --> Parse
D1 --> Parse
D2 --> Parse
T1 --> TypeRes
T2 --> TypeRes
Fluent --> CG["CallGraph"]
```
> **Performance note:** only dependencies with crypto findings get full source parsing.
> Dependencies without findings contribute only their bytecode type signatures (class names,
> method signatures, return types, interface hierarchy). This preserves 100% type resolution
> accuracy for fluent chains while skipping expensive source parsing for \~80% of dependencies.
A dependency's `Files` becomes the `IncludeFiles` of its `PackageDir`. For Go, the builder parses only the files the host build compiles in the imported packages (the Go parser skips an unlisted file before reading it, so modernc.org/libc's seven other `ccgo_linux_.go` files cost nothing) and still walks through their parent directories so every package keeps the import path a whole-module walk gives it. This loses no reachability. A Go function can call only functions of packages its own package imports, which are in the closure too, and an interface value can only hold a type of a package the program contains, so the module's other packages hold no call edge and no dispatch target of the program. Likewise a file that build constraints exclude is not part of the program the host builds.
For a Python namespace distribution, the builder parses only its own files of the shared namespace directory, under the import path a whole walk gives them (`google.auth._helpers`), and the Python dependency type resolver indexes only them. Each function of a namespace directory is parsed once and belongs to the distribution that installed it, so containing-function lookups, synthesized entry-point findings and the call-graph export name that distribution. A distribution rooted at `site-packages` parses under the empty import path, so each package and module keeps its own name (`configobj`, `validate.check`, `six`), and both the builder and the type resolver walk only the directories that hold its files.
#### What the parser extracts
Each language parser extracts the same semantic information into the shared `FileAnalysis` / `FunctionDecl` structures. For Go (`.go` files, excluding `_test.go`):
| Extracted | Example | Stored As |
| - | - | - |
| Package imports | `import "crypto/aes"` | `Imports["aes"] = "crypto/aes"` |
| Function declarations | `func Encrypt(...)` | `FunctionDecl{ID, FilePath, StartLine, EndLine, Calls}` |
| Method declarations | `func (b *Block) Seal(...)` | Same, with `Type = "*Block"` |
| Call expressions | `aes.NewCipher(key)` | `FunctionCall{Callee: {Package: "crypto/aes", Name: "NewCipher"}}` |
For Java (`.java` files):
| Extracted | Example | Stored As |
| - | - | - |
| Package declaration | `package javax.crypto;` | `PackagePath = "javax.crypto"` |
| Imports (explicit) | `import javax.crypto.Cipher;` | `Imports["Cipher"] = "javax.crypto"` |
| Imports (wildcard) | `import java.security.*;` | `WildcardImports = ["java.security"]` |
| Class methods | `class Cipher { getInstance(...) }` | `FunctionDecl{..., Type: "Cipher", Name: "getInstance"}` |
| Constructors | `new SecretKeySpec(...)` | Call to `FunctionID{..., Type: "SecretKeySpec", Name: ""}` |
| Method invocations | `Cipher.getInstance("AES")` | Resolved via imports → `FunctionID{Package: "javax.crypto", Type: "Cipher", Name: "getInstance"}` |
| Local variable types | `Cipher cipher = Cipher.getInstance(...)` | Enables `cipher.doFinal()` → resolves `cipher` to `Cipher` type |
For Python (`.py` files, excluding `test_*.py` and `*_test.py`):
| Extracted | Example | Stored As |
| - | - | - |
| Import statements | `import hashlib` | `Imports["hashlib"] = "hashlib"` |
| From imports | `from cryptography.hazmat.primitives import Cipher` | `Imports["Cipher"] = "cryptography.hazmat.primitives"` |
| Wildcard imports | `from hashlib import *` | `WildcardImports = ["hashlib"]` |
| Aliased imports | `import hashlib as hl` | `Imports["hl"] = "hashlib"` |
| Function definitions | `def encrypt(key, data):` | `FunctionDecl{ID, FilePath, StartLine, EndLine, Calls}` |
| Class methods | `class Cipher: def __init__(self):` | `FunctionDecl{..., Type: "Cipher", Name: ""}` |
| Attribute calls | `hashlib.sha256()` | Resolved via imports → `FunctionID{Package: "hashlib", Name: "sha256"}` |
| Chained calls | `cryptography.hazmat.primitives.hashes.SHA256()` | Resolved via first segment import |
| `self` calls | `self.encrypt()` | `FunctionID{Package: current_package, Name: "encrypt"}` |
For Rust (`.rs` files, excluding `*_test.rs` and `tests.rs`):
| Extracted | Example | Stored As |
| - | - | - |
| Use declarations | `use ring::aead::Aead;` | `Imports["Aead"] = "ring::aead"` |
| Scoped use lists | `use ring::aead::{Aead, AeadCore};` | `Imports["Aead"] = "ring::aead"`, `Imports["AeadCore"] = "ring::aead"` |
| Wildcard use | `use ring::aead::*;` | `WildcardImports = ["ring::aead"]` |
| Free functions | `fn encrypt(key: &[u8]) {...}` | `FunctionDecl{ID, FilePath, StartLine, EndLine, Calls}` |
| Impl methods | `impl Aead { fn new(...) {...} }` | `FunctionDecl{..., Type: "Aead", Name: "new"}` |
| Scoped calls | `Aead::new(...)` | Resolved via imports → `FunctionID{Package: "ring::aead", Type: "Aead", Name: "new"}` |
| Field calls | `self.encrypt(...)` | `FunctionID{Package: current_module, Name: "encrypt"}` |
#### The two data structures
The `CallGraph` holds two maps:
```
Functions: "crypto/aes.NewCipher" → FunctionDecl{...}
"example.com/app.Encrypt" → FunctionDecl{Calls: [...]}
"example.com/app.main" → FunctionDecl{Calls: [...]}
Callers: "crypto/aes.NewCipher" → ["example.com/app.Encrypt"]
"example.com/app.Encrypt" → ["example.com/app.main"]
```
* **`Functions`** maps `FunctionID.String()` → `*FunctionDecl` (forward: function → its outgoing calls)
* **`Callers`** maps callee → `[]callerID` (reverse: who calls this function?)
The reverse index makes **backward tracing** possible: starting from a crypto finding and walking up to user code.
#### Edge resolution kinds
While building the caller index, the builder records **how** each edge was
resolved in `CallGraph.EdgeResolutions`: `exact` (receiver type known, unique
target or overload set on that type), `interface_dispatch` (an interface/abstract
method expanded to concrete implementations by name+arity within a namespace
root), or `name_only` (a fluent-fallback guess with no receiver-type anchor).
This classification is emitted on the graph-fragment export (`graph-fragment-1.3`,
see [Output formats](/en/latest/cli/crypto-finder/output-formats#graph-fragment-export)) so downstream
consumers can fail closed on over-broad dispatch instead of reporting it as
typed reachability. The dispatch heuristics below (interface expansion, fluent
fallback) are exactly the edges tagged non-`exact`. The same fragment also
exposes `crypto_entry_points[]` and `supporting_calls[]` so consumers can use a
stable API index rather than the removed `entry_point_index` projection.
#### Type resolution
After building the caller index, the builder runs additional resolution passes to improve type accuracy:
1. **`TypeResolver`** (language-specific): For Java, a bytecode-based resolver reads `.class` files from resolver-supplied dependency JARs (Maven or Gradle) plus the selected JDK platform archives to extract fully-qualified method signatures. JAR indexing runs in parallel and uses a per-artifact bytecode cache under `~/.scanoss/crypto-finder/cache/bytecode/`, keyed by exact artifact identity. This provides accurate parameter types (e.g., `io.jsonwebtoken.SignatureAlgorithm` instead of generic `K`) and return types for fluent chain resolution. The Java resolver can be configured per scan with `java_jdk_major` (`auto`, `8`, `11`, `17`, `21`) and `java_jdk_homes`, which also makes Java dependency resolution JDK-aware. The `TypeResolver` interface is extensible, so each language can implement its own approach, such as `go/types` for Go, `.pyi` stubs for Python, or `rust-analyzer` for Rust.
2. **Fluent chain resolution**: For chained calls like `Jwts.builder().setId(id).signWith(algo, key)`, return types are propagated through the chain. If `builder()` returns `JwtBuilder`, then `setId()` is resolved as `JwtBuilder.setId`. Interface inheritance is also followed (e.g., `JwtBuilder` extends `ClaimsMutator`, so `setId` resolves to `ClaimsMutator.setId`).
3. **Argument source tracing**: For each function call, the parser traces where argument values come from: literal constants, local variables, class fields, method parameters, or call results. This produces recursive `source_nodes` showing the data flow into each argument.
### Step 5: Trace and attribute findings
Attribution works like this for each crypto finding in a dependency:
```mermaid theme={null}
flowchart TB
Finding["Crypto Finding file: chacha20.go, line: 42"] --> Contain["FindContainingFunction() Which function spans line 42?"]
Contain --> FnID["FunctionID chacha20poly1305.New"]
FnID --> Trace["TraceBack(target, userPackages)"]
subgraph "BFS Backward Trace"
direction TB
Start["Start: chacha20poly1305.New"]
Mid["Caller: mypkg.SecureEncrypt"]
End["Caller: main.main ✓ user package reached!"]
Start -.->|"Callers[] lookup"| Mid
Mid -.->|"Callers[] lookup"| End
end
Trace --> BFS
BFS --> Chain["CallChain [main.main → mypkg.SecureEncrypt → chacha20poly1305.New]"]
```
#### How `TraceBack` works (BFS)
1. **Start** with the target function (where the crypto finding was detected)
2. **Look up** all callers via the reverse index (`graph.Callers[targetKey]`)
3. **Prepend** each caller to the chain being built (so the chain grows backward: `[caller, ...existing]`)
4. **Terminate** when a root function is reached (a function with no callers, e.g., `main`)
5. **Validate** that the complete chain passes through at least one user-package function
6. **Cycle detection** prevents infinite loops in recursive call graphs
7. **Return** all complete chains (BFS finds all paths from entry points to the crypto call site)
The result is an ordered array, **`[program_entry_point, ..., intermediate, ..., crypto_call_site]`**, where position `[i]` calls position `[i+1]`.
#### Attribution output
After tracing, each finding gets structured attribution metadata in the interim report, and the detailed reachability slices are emitted by the separate call graph export.
A dependency finding:
```json theme={null}
{
"source": "dependency",
"finding_id": "a1b2c3d4",
"dependency_info": {
"module": "golang.org/x/crypto",
"version": "v0.17.0"
}
}
```
Dependency finding `file_path` values are relative to the dependency root. The artifact identity stays in `dependency_info`, so consumers do not need to parse `module@version` back out of the path string.
A user code finding:
```json theme={null}
{
"source": "direct",
"finding_id": "e5f6a7b8"
}
```
A call graph export slice:
```json theme={null}
{
"finding_id": "a1b2c3d4",
"call_chains": [
[
{ "function_name": "main", "file_path": "main.go", "start_line": 15 },
{
"function_name": "example.com/app/mypkg.SecureEncrypt",
"file_path": "mypkg/crypto.go",
"start_line": 8
},
{
"function_name": "golang.org/x/crypto/chacha20poly1305.New",
"file_path": "chacha20.go",
"start_line": 42,
"dependency_info": {
"module": "golang.org/x/crypto",
"version": "v0.17.0"
}
}
]
]
}
```
### Step 6: Merge reports
```mermaid theme={null}
flowchart TB
UR["User Report source: direct"]
DR1["Dep Report 1"]
DR2["Dep Report 2"]
UR --> Merged["Merged Report"]
DR1 --> Merged
DR2 --> Merged
```
Crypto-finder merges dependency findings into the interim report and leaves reachability slicing to the call graph export. A finding appears in the interim report whether or not it later produces an exported call chain.
User code findings are always included and marked with `source: "direct"`.
***
## Practical walkthrough
This section traces the entire pipeline using the test project at `testdata/projects/go_with_crypto_dep/`. Every value shown is real output from running `crypto-finder scan --scan-dependencies` against this project.
### The source code
Three files make up the project:
**`go.mod`** declares one direct dependency:
```
module example.com/crypto-test
go 1.21
require golang.org/x/crypto v0.31.0
require golang.org/x/sys v0.28.0 // indirect
```
**`main.go`** is the entry point. It does **not** use crypto directly:
```go theme={null}
package main
import "example.com/crypto-test/mypkg"
func main() {
key := make([]byte, 32)
message := []byte("Hello, crypto-finder dependency scanning!")
encrypted, err := mypkg.SecureEncrypt(key, message) // line 14
decrypted, err := mypkg.SecureDecrypt(key, encrypted) // line 19
}
```
**`mypkg/crypto.go`** is the user's wrapper package. It uses crypto from a dependency:
```go theme={null}
package mypkg
import (
"crypto/rand"
"golang.org/x/crypto/chacha20poly1305"
)
func SecureEncrypt(key []byte, plaintext []byte) ([]byte, error) { // line 12
aead, err := chacha20poly1305.New(key) // line 13 — crypto!
nonce := make([]byte, aead.NonceSize())
rand.Read(nonce) // line 19 — crypto!
ciphertext := aead.Seal(nonce, nonce, plaintext, nil)
return ciphertext, nil
}
func SecureDecrypt(key []byte, ciphertext []byte) ([]byte, error) { // line 28
aead, err := chacha20poly1305.New(key) // line 29 — crypto!
// ...
}
```
### Step 1: Scan user code (the normal scan)
The orchestrator runs Semgrep/OpenGrep rules against user code. It finds **3 assets** in `mypkg/crypto.go`:
| Line | Match | Rule |
| - | - | - |
| 13 | `chacha20poly1305.New(key)` | `go.xcrypto.chacha20poly1305.aead` |
| 19 | `rand.Read(nonce)` | `go.crypto.rand.usage` |
| 29 | `chacha20poly1305.New(key)` | `go.xcrypto.chacha20poly1305.aead` |
This produces the **user report**. At this point there are no `source`, `call_chain`, or `dependency_info` fields, only raw findings.
### Step 2: Resolve dependencies
The Go resolver lists the main module with `go list -m -json`, then runs `go list -e -deps` over the main module's packages and keeps every module that provides a package in that non-test import closure. Build constraints are evaluated for the scanning host: on amd64, `chacha20poly1305` imports `golang.org/x/sys/cpu`, so both modules below are in the closure, while on arm64 only `golang.org/x/crypto` is. On amd64 it returns:
```
RootModule: "example.com/crypto-test"
Dependencies:
Module Version Dir
golang.org/x/crypto v0.31.0 ~/go/pkg/mod/golang.org/x/crypto@v0.31.0
golang.org/x/sys v0.28.0 ~/go/pkg/mod/golang.org/x/sys@v0.28.0
```
The `RootModule` (`example.com/crypto-test`) is the Go user-code prefix. Any package whose import path starts with it is **user code**. Java additionally treats packages declared in project source files as user code, even when `RootModule` is a Gradle project name rather than a package prefix. Everything else is a dependency.
### Step 3: Scan dependencies in parallel
Each dependency gets scanned with the same rules, limited to Go rules only, and only in the packages the program imports: `chacha20poly1305`, `chacha20`, `internal/alias` and `internal/poly1305` of `golang.org/x/crypto`, and `cpu` of `golang.org/x/sys`. Measured on 2026-10-02 with the current rule set:
| Dependency | Crypto Assets Found | Why |
| - | -: | - |
| `golang.org/x/crypto` | 4 | `chacha20poly1305/chacha20poly1305_generic.go` and `chacha20poly1305/xchacha20poly1305.go`. A whole-module scan reports 378 in 58 files, all in packages this program does not contain |
| `golang.org/x/sys` | 0 | `cpu` holds no crypto operation |
Total: **4 dependency findings**. Only `golang.org/x/crypto` has findings, so it proceeds to step 4 together with the user code.
### Step 4: Build the call graph
The builder receives the user code and the crypto-bearing dependency, limited to its imported packages:
```
PackageDirs = [
{Dir: ".../go_with_crypto_dep", ImportPath: "example.com/crypto-test"},
{Dir: ".../golang.org/x/crypto@v0.31.0", ImportPath: "golang.org/x/crypto",
IncludeFiles: [".../chacha20/chacha_generic.go", ..., ".../chacha20poly1305/chacha20poly1305.go", ..., ".../internal/poly1305/sum_generic.go", ...]},
]
```
The syntactic parser processes every `.go` file and extracts function declarations with their calls.
**From `main.go`:**
```
FunctionDecl: example.com/crypto-test.main
File: main.go, Lines: 10–26
Calls:
→ example.com/crypto-test/mypkg.SecureEncrypt (line 14)
→ example.com/crypto-test/mypkg.SecureDecrypt (line 19)
```
**From `mypkg/crypto.go`:**
```
FunctionDecl: example.com/crypto-test/mypkg.SecureEncrypt
File: mypkg/crypto.go, Lines: 12–25
Calls:
→ golang.org/x/crypto/chacha20poly1305.New (line 13)
→ crypto/rand.Read (line 19)
FunctionDecl: example.com/crypto-test/mypkg.SecureDecrypt
File: mypkg/crypto.go, Lines: 28–46
Calls:
→ golang.org/x/crypto/chacha20poly1305.New (line 29)
```
**From `golang.org/x/crypto/...`:** hundreds more function declarations.
Then `buildCallerIndex()` creates the **reverse index** (callee → who calls it):
```
Callers["example.com/crypto-test/mypkg.SecureEncrypt"]
= ["example.com/crypto-test.main"]
Callers["example.com/crypto-test/mypkg.SecureDecrypt"]
= ["example.com/crypto-test.main"]
Callers["golang.org/x/crypto/chacha20poly1305.New"]
= ["example.com/crypto-test/mypkg.SecureEncrypt",
"example.com/crypto-test/mypkg.SecureDecrypt"]
```
Backward tracing depends on this reverse index.
### Step 5: Trace and attribute
The system now traces **each finding** back through the call graph to user code. Three different scenarios play out:
#### Scenario A: user finding at `chacha20poly1305.New`, line 13
This finding is in `mypkg/crypto.go` (user code). The enrichment flow:
**5a-1. Find the containing function.**
`FindContainingFunction("mypkg/crypto.go", 13)` iterates all `FunctionDecl`s, looking for one whose `FilePath` matches and whose `StartLine..EndLine` spans line 13. It finds **`SecureEncrypt`** (lines 12 to 25).
**5a-2. Trace back to the entry point.**
`TraceBack(SecureEncrypt, userPackages={"example.com/crypto-test"}, maxDepth=0)`:
```
BFS queue: [ [SecureEncrypt] ]
Iteration 1:
chain = [SecureEncrypt]
head = SecureEncrypt
Look up Callers["...mypkg.SecureEncrypt"] → ["...main"]
Prepend caller: chain becomes [main, SecureEncrypt]
→ push to queue
Iteration 2:
chain = [main, SecureEncrypt]
head = main
Look up Callers["...main"] → [] (no callers — root function)
Chain reaches user code? YES (both functions are user code)
Chain length > 1? YES
→ CHAIN COMPLETE! Add to results.
```
**5a-3. Export the slice.**
```json theme={null}
"source": "direct",
"finding_id": "a1b2c3d4",
"call_chains": [
[
{"function_name": "main", "namespace": "example.com/crypto-test", "file_path": "main.go", "line": 14},
{"function_name": "SecureEncrypt", "namespace": "example.com/crypto-test/mypkg", "file_path": "mypkg/crypto.go", "line": 12}
]
]
```
Note: `main`'s line is **14** (the line where `main` *calls* `SecureEncrypt`), not line 10 where `main` is declared. This is because `findCallLine()` searches `main`'s `Calls` list for the specific call to `SecureEncrypt` and returns that call-site line number.
#### Scenario B: user finding at `chacha20poly1305.New`, line 29
Same logic, but traces through `SecureDecrypt`:
```
FindContainingFunction("mypkg/crypto.go", 29) → SecureDecrypt (lines 28–46)
TraceBack(SecureDecrypt):
[SecureDecrypt]
→ prepend caller → [main, SecureDecrypt]
→ main has no callers (root function), chain reaches user code → CHAIN COMPLETE
```
```json theme={null}
"source": "direct",
"finding_id": "b2c3d4e5",
"call_chains": [
[
{"function_name": "main", "namespace": "example.com/crypto-test", "file_path": "main.go", "line": 19},
{"function_name": "SecureDecrypt", "namespace": "example.com/crypto-test/mypkg", "file_path": "mypkg/crypto.go", "line": 28}
]
]
```
Note: `main`'s line is now **19**, the line where `main` calls `SecureDecrypt`, not 14.
#### Scenario C: unreachable dependency finding in deep `x/crypto` internal functions
Take any internal function in `golang.org/x/crypto`, say `ssh.newAESCTR`:
```
FindContainingFunction(".../ssh/cipher.go", N) → ssh.newAESCTR
TraceBack(ssh.newAESCTR):
[ssh.newAESCTR]
→ Callers: maybe some internal SSH functions
→ Keep walking back to root functions...
→ Root functions found, but no chain passes through "example.com/crypto-test"
→ All chains discarded (no user code reached).
```
Result: `call_chains` is **empty**.
The finding may still be in the interim report, but it adds no useful reachability slice to the call graph export.
So the useful downstream signal stays narrow even when a dependency uses a lot of crypto internally. The vast majority of `golang.org/x/crypto`'s internal crypto usage is not reachable from the user's `main()`.
### Step 6: Merge
```
User report (findings metadata)
+ Dependency reports (findings metadata)
─────────────────────────────────────
= Merged interim report
```
In this test project, the useful reachability slices all start from user code findings, because the user code calls `chacha20poly1305.New` **directly** inside `mypkg/crypto.go`, a file in the user's own module. The crypto usage is already captured as `source: "direct"`. There's no intermediate dependency wrapper that the user calls which *then* reaches crypto.
With a longer chain, such as `main → mypkg.Encrypt → someMiddleware.Process → chacha20poly1305.New` where `someMiddleware` is a dependency, a dependency-backed reachability slice would appear in the call graph export.
### Actual interim report output
The final interim report looks like this:
```json theme={null}
{
"version": "1.5",
"tool": { "name": "crypto-finder", "version": "dev" },
"findings": [
{
"file_path": "mypkg/crypto.go",
"language": "go",
"cryptographic_assets": [
{
"start_line": 13,
"end_line": 13,
"match": "aead, err := chacha20poly1305.New(key)",
"rules": [
{
"id": "go.xcrypto.chacha20poly1305.aead",
"message": "Detected ChaCha20-Poly1305 AEAD usage",
"severity": "INFO"
}
],
"status": "pending",
"metadata": {
"algorithmFamily": "ChaCha20",
"assetType": "algorithm",
"...": "..."
},
"source": "direct",
"finding_id": "a1b2c3d4"
},
{
"start_line": 19,
"end_line": 19,
"match": "if _, err := rand.Read(nonce); err != nil {",
"rules": [
{
"id": "go.crypto.rand.usage",
"message": "Detected cryptographically secure random number generation",
"severity": "INFO"
}
],
"status": "pending",
"metadata": {
"algorithmFamily": "CSPRNG",
"assetType": "algorithm",
"...": "..."
},
"source": "direct",
"finding_id": "a1b2c3d4"
},
{
"start_line": 29,
"end_line": 29,
"match": "aead, err := chacha20poly1305.New(key)",
"rules": [
{
"id": "go.xcrypto.chacha20poly1305.aead",
"message": "Detected ChaCha20-Poly1305 AEAD usage",
"severity": "INFO"
}
],
"status": "pending",
"metadata": {
"algorithmFamily": "ChaCha20",
"assetType": "algorithm",
"...": "..."
},
"source": "direct",
"finding_id": "b2c3d4e5"
}
]
}
]
}
```
The corresponding reachability slices are emitted by the separate call graph export and joined through `finding_id`.
### Visual summary
```
main.go:14 ──calls──→ mypkg/crypto.go:13 ──calls──→ x/crypto/chacha20poly1305.New
(main) (SecureEncrypt) (dependency function)
│
├── Finding: chacha20poly1305.New at L13
│ source: "direct" (it's in user code)
│ exported slice: [[main@L14 → SecureEncrypt@L12]]
│
└── Finding: rand.Read at L19
source: "direct"
exported slice: [[main@L14 → SecureEncrypt@L12]]
main.go:19 ──calls──→ mypkg/crypto.go:29 ──calls──→ x/crypto/chacha20poly1305.New
(main) (SecureDecrypt)
│
└── Finding: chacha20poly1305.New at L29
source: "direct"
exported slice: [[main@L19 → SecureDecrypt@L28]]
golang.org/x/crypto/chacha20poly1305/*.go (inside the imported package)
│
└── 4 findings with NO call chain
source: "dependency"
→ no exported reachability slice
```
***
## Walkthrough 2: multi-hop dependency chain
In the first walkthrough, user code calls crypto directly, so the useful reachability slices start from direct findings. This second walkthrough uses `testdata/projects/go_with_dep_chain/` to show a **multi-hop chain**, where the crypto usage sits deep inside a dependency and the dependency-backed reachability slices are the useful output.
### The source code
Here, user code never touches `chacha20poly1305` directly. Instead, it calls through a wrapper dependency (`cryptowrapper_dep/`), which has an internal function layer.
**`main.go`** is the entry point and calls `mypkg`:
```go theme={null}
func main() {
encrypted, err := mypkg.SecureEncrypt(key, message) // line 14
decrypted, err := mypkg.SecureDecrypt(key, encrypted) // line 19
}
```
**`mypkg/crypto.go`** is user code that delegates to the dependency. It has **no crypto imports**.
```go theme={null}
import "example.com/cryptowrapper"
func SecureEncrypt(key, plaintext []byte) ([]byte, error) { // line 15
encrypted, err := cryptowrapper.Encrypt(key, plaintext) // line 16
return encrypted, nil
}
func SecureDecrypt(key, ciphertext []byte) ([]byte, error) { // line 24
decrypted, err := cryptowrapper.Decrypt(key, ciphertext) // line 25
return decrypted, nil
}
```
**`../cryptowrapper_dep/wrapper.go`** is the dependency, a separate module. It has a public API and an internal function:
```go theme={null}
// Public: called by user code
func Encrypt(key, plaintext []byte) ([]byte, error) { // line 19
aead, err := newAEAD(key) // line 20 — calls internal fn
nonce := make([]byte, aead.NonceSize())
rand.Read(nonce) // line 26 — crypto!
return aead.Seal(nonce, nonce, plaintext, nil), nil
}
func Decrypt(key, ciphertext []byte) ([]byte, error) { // line 36
aead, err := newAEAD(key) // line 37 — calls internal fn
// ...
}
// Internal: adds depth to the call chain
func newAEAD(key []byte) (cipher.AEAD, error) { // line 58
return chacha20poly1305.New(key) // line 59 — crypto!
}
```
The intended call chains:
```
main → mypkg.SecureEncrypt → cryptowrapper.Encrypt → cryptowrapper.newAEAD → chacha20poly1305.New
main → mypkg.SecureDecrypt → cryptowrapper.Decrypt → cryptowrapper.newAEAD → chacha20poly1305.New
```
### What the scan produces
Running with `--scan-dependencies`:
```
Finding groups: 1 (wrapper.go in the dependency)
Total assets: 2 (both from the dependency, both reachable)
```
Only a small subset of the traced findings produce meaningful exported slices. Deep `x/crypto` and `x/sys` internals may still be scanned and identified, but they do not help downstream stitching unless the graph can connect them back to component-owned or user-owned entry points.
### Tracing the 3-step chain
The finding at `chacha20poly1305.New` (line 59 of `wrapper.go`) produces a **3-step chain**. This is the BFS trace:
```
FindContainingFunction("wrapper.go", 59) → cryptowrapper.newAEAD (lines 58–59)
TraceBack(newAEAD, userPackages={"example.com/dep-chain-test"}):
BFS queue: [ [newAEAD] ]
Iteration 1:
chain = [newAEAD]
head = newAEAD
Is "example.com/cryptowrapper" a user package?
→ Does it start with "example.com/dep-chain-test/"? NO
Look up Callers["example.com/cryptowrapper.newAEAD"]
→ ["example.com/cryptowrapper.Encrypt", "example.com/cryptowrapper.Decrypt"]
Prepend each: push [Encrypt, newAEAD] and [Decrypt, newAEAD] to queue
Iteration 2a:
chain = [Encrypt, newAEAD]
head = Encrypt
Is "example.com/cryptowrapper" a user package? NO
Look up Callers["example.com/cryptowrapper.Encrypt"]
→ ["example.com/dep-chain-test/mypkg.SecureEncrypt"]
Prepend: push [SecureEncrypt, Encrypt, newAEAD]
Iteration 2b:
chain = [Decrypt, newAEAD]
head = Decrypt
Is "example.com/cryptowrapper" a user package? NO
Look up Callers["example.com/cryptowrapper.Decrypt"]
→ ["example.com/dep-chain-test/mypkg.SecureDecrypt"]
Prepend: push [SecureDecrypt, Decrypt, newAEAD]
Iteration 3a:
chain = [SecureEncrypt, Encrypt, newAEAD]
head = SecureEncrypt
Look up Callers["...mypkg.SecureEncrypt"] → ["...main"]
Prepend: push [main, SecureEncrypt, Encrypt, newAEAD]
Iteration 3b:
chain = [SecureDecrypt, Decrypt, newAEAD]
head = SecureDecrypt
Look up Callers["...mypkg.SecureDecrypt"] → ["...main"]
Prepend: push [main, SecureDecrypt, Decrypt, newAEAD]
Iteration 4a:
chain = [main, SecureEncrypt, Encrypt, newAEAD]
head = main
Look up Callers["...main"] → [] (no callers — root function)
Chain reaches user code? YES
→ CHAIN COMPLETE! (4 steps)
Iteration 4b:
chain = [main, SecureDecrypt, Decrypt, newAEAD]
head = main
→ Also a complete chain! (4 steps)
```
BFS found **two** complete chains. Both are stored in `call_chains`:
```json theme={null}
{
"source": "dependency",
"dependency_info": {
"module": "example.com/cryptowrapper",
"version": "v0.0.0"
},
"finding_id": "c3d4e5f6",
"call_chains": [
[
{
"function_name": "main",
"namespace": "example.com/dep-chain-test",
"file_path": "main.go",
"line": 14
},
{
"function_name": "SecureEncrypt",
"namespace": "example.com/dep-chain-test/mypkg",
"file_path": "mypkg/crypto.go",
"line": 16
},
{
"function_name": "Encrypt",
"namespace": "example.com/cryptowrapper",
"file_path": "wrapper.go",
"line": 20
},
{
"function_name": "newAEAD",
"namespace": "example.com/cryptowrapper",
"file_path": "wrapper.go",
"line": 58
}
],
[
{
"function_name": "main",
"namespace": "example.com/dep-chain-test",
"file_path": "main.go",
"line": 19
},
{
"function_name": "SecureDecrypt",
"namespace": "example.com/dep-chain-test/mypkg",
"file_path": "mypkg/crypto.go",
"line": 25
},
{
"function_name": "Decrypt",
"namespace": "example.com/cryptowrapper",
"file_path": "wrapper.go",
"line": 37
},
{
"function_name": "newAEAD",
"namespace": "example.com/cryptowrapper",
"file_path": "wrapper.go",
"line": 58
}
]
]
}
```
### Full trace to `main`
The BFS walks all the way to **root functions** (functions with no callers, like `main`). This means the full chain `main → SecureEncrypt → Encrypt → newAEAD` is preserved. A chain is valid if it passes through at least one user-package function, so chains that only traverse dependency code are discarded.
### Visual summary
```
main.go:14 → mypkg/crypto.go:16 → wrapper.go:20 → wrapper.go:59
(main) (SecureEncrypt) (Encrypt) (newAEAD)
│ │ │ │
│ user code │ user code │ dependency │ dependency
│ │ │ │
└── chain starts here (root fn) │ └── finding: chacha20poly1305.New
│ source: "dependency"
└── intermediate hop in chain
main.go:19 → mypkg/crypto.go:25 → wrapper.go:37 → wrapper.go:59
(main) (SecureDecrypt) (Decrypt) (newAEAD)
│ │ │ │
└── chain starts here (root fn) │ └── same finding, alternate path
└── intermediate hop
```
What this shows:
* User code has **0** crypto findings, because `mypkg` has no crypto imports.
* **2** dependency findings survive reachability, because the call graph proves that user code reaches them.
* **492** dependency findings are dropped, because they are deep `x/crypto` internals that user code can't reach.
* `main` is at the head of each chain, because BFS walks to root functions, which have no callers.
* Both the Encrypt and Decrypt paths are kept, because every chain is stored in `call_chains`.
***
## Interim report contract (v1.7)
Version 1.7 keeps the attribution fields needed to join findings to the separate reachability export and adds an optional AST-anchored structural identity when callgraph evidence is available. Dependency-backed paths are dependency-root-relative; `dependency_info` remains the canonical place for dependency module, version, and package URL. Direct findings may additionally expose a valid rule package URL at the asset-level `purl`; it is version-enriched only when one unambiguous direct dependency match exists.
| Field | Type | When Present | Description |
| - | - | - | - |
| `source` | `string` | Always (when dependency scanning) | `"direct"` or `"dependency"` |
| `dependency_info` | `object` | Dependency findings only | `{module, version, purl?}` |
| `purl` | `string` | Direct findings with valid rule metadata | Canonical package identity, optionally enriched from the direct dependency graph |
| `finding_id` | `string` | Always (when dependency scanning) | Short hash (SHA-256) for cross-referencing with the callgraph export |
| `occurrence_key` | `string` | When a terminal AST anchor is available | Rule-independent `v1:<16 lowercase hex>` structural identity for the canonical finding |
| `conditioned_value` | `string` | Per-value assets only | The resolved condition the asset was specialized for; part of its `finding_id` and `occurrence_key` (v1.7+) |
| `terminal_start_col`, `terminal_end_col` | `integer` | Assets matched on an argument span | Columns of the enclosing call, used to locate the crypto call (v1.7+) |
## Call graph export
When `--export-callgraph` is enabled, Crypto Finder emits a finding-centric JSON export that uses the same relative-path convention as the main report.
Schema note: call graph export version `6.12` is current and carries optional direct finding `purl` values at the finding level, optional `occurrence_key` structural identities, canonical dependency package URLs inside `dependency_info.purl`, and contract-scoped `supporting_calls[].supporting_call.resolved_key_length` evidence for structurally derived Java key-generation configuration calls, including the `rule_declared_bits` value and `rule_conflict` marker when a rule-declared key length disagrees with the resolved one. Java runtime provenance remains available in `scan_metadata` for JDK-aware platform signature enrichment.
* Each top-level record stays keyed by `finding_id`, which is the join key back to the interim report. `occurrence_key`, when present, is a separate structural identity and does not replace that join.
* `call_chains` is the primary value-flow structure. Each chain is ordered from the first reachable caller to the function that contains the matched crypto call.
* Each chain node contains a fully qualified `function_name`, a normalized `file_path`, `start_line`, optional `dependency_info`, and optional `entry_call`.
* `entry_call` describes how execution entered the current function from the previous step. Its `file_path` and `line` are the call-site location in the previous node's source file.
* The last node in a chain carries `crypto_call`, which is the matched crypto-relevant call that triggered the finding.
* `entry_call.parameters[]` and `crypto_call.parameters[]` both export `parameter_index` (always `0`-based), best-effort `type`, `argument_expression`, `resolved_value`, `variable_name` for simple identifiers only, and recursive `source_nodes`.
* For Java scans, `scan_metadata` may also include `java_requested_jdk_major`, `java_runtime_version`, `java_platform_signatures_used`, `java_platform_signature_source`, and `java_platform_signature_unavailable_reason` to show which JDK major was requested and whether JDK platform signatures were available for enrichment.
* `source_nodes` can now carry interprocedural provenance across wrapper hops, for example `PARAMETER -> PARAMETER -> VALUE`, and propagated nested nodes keep `location.file_path` plus `location.line` when known.
* Method-call expressions are preserved as `CALL_RESULT` nodes instead of flattening away their receivers. When the invoked method can be resolved, the node also exports `call_target`, and receiver provenance stays nested under the `CALL_RESULT` (for example `CALL_RESULT -> PARAMETER alg -> VALUE SignatureAlgorithm.HS256`).
* Findings that cannot be resolved to a containing function or a specific crypto call remain in the export with `finding_location` and `unresolved_reason`.
### Call chains ordering
The `call_chains` field in the call graph export contains all traced paths from program entry points to the crypto call site. Each inner array is one complete path, ordered from **program entry point** (index 0) to **crypto call site** (last index). Entry `[i]` calls entry `[i+1]`.
Example:
```json theme={null}
{
"finding_id": "69669f02",
"call_chains": [
[
{
"function_name": "io.jsonwebtoken.jjwtfun.controller.SecretsController.traceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java",
"start_line": 33
},
{
"function_name": "io.jsonwebtoken.jjwtfun.service.SecretService.issueTraceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/service/SecretService.java",
"start_line": 72,
"entry_call": {
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java",
"line": 34,
"parameters": [
{
"parameter_index": 0,
"type": "io.jsonwebtoken.SignatureAlgorithm",
"argument_expression": "SignatureAlgorithm.HS256",
"resolved_value": "SignatureAlgorithm.HS256"
}
]
}
},
{
"function_name": "org.springframework.security.core.token.Sha512DigestUtils.getSha512Digest",
"file_path": "org/springframework/security/core/token/Sha512DigestUtils.java",
"start_line": 43,
"dependency_info": {
"module": "org.springframework.security:spring-security-core",
"version": "5.7.11"
},
"crypto_call": {
"function_name": "java.security.MessageDigest.getInstance",
"line": 45,
"parameters": [
{
"parameter_index": 0,
"type": "String",
"argument_expression": "\"SHA-512\"",
"resolved_value": "\"SHA-512\""
}
]
}
}
]
]
}
```
## Findings cache
Dependency scanning is dominated by opengrep execution time (\~93% of pipeline time). Because `module@version` produces identical scan results with the same ruleset, caching removes that repeated work. On a second scan with the same dependencies and rules, the dependency scanning phase drops from minutes to near-zero.
### How it works
The cache sits between Step 2 (rule loading) and Step 3 (parallel scanning) in the pipeline. Before scanning, `lookupDependency` checks each dependency for a cached result. The misses are scanned in batches (Step 3) and each dependency's report is stored under its own key, unless its scan stopped at a time or memory limit.
```mermaid theme={null}
flowchart LR
Dep["module@version"] --> Check{"Cache hit?"}
Check -->|Yes| Report["Cached InterimReport"]
Check -->|No| Scan["batched scanner process"]
Scan --> Store["Store in cache"]
Store --> Report
```
### Cache key design
The key captures everything that affects scan output:
```
@:
```
* **Module + version**: e.g., `org.bouncycastle:bcprov-jdk18on@1.78`
* **Rules hash**: the first 16 hex characters of a SHA-256 over the sorted rule file **contents**. If any rule is edited, the cache invalidates automatically
Example: `org.bouncycastle:bcprov-jdk18on@1.78:a3f8b2c1d4e5f678`
The `rulesHash` is computed once per scan (not per-dep), so I/O cost is negligible.
### Storage layout
The default implementation (`DiskFindingsCache`) stores results as JSON files:
```
~/.scanoss/crypto-finder/cache/findings/
org.bouncycastle:bcprov-jdk18on@1.78:a3f8b2c1d4e5f678.json
com.google.guava:guava@33.0.0-jre:a3f8b2c1d4e5f678.json
golang.org_x_crypto@v0.31.0:a3f8b2c1d4e5f678.json
```
Forward slashes in module paths (e.g., `golang.org/x/crypto`) are replaced with `_` for filesystem safety. Writes use temp file + atomic rename to prevent corruption from interrupted scans.
### `FindingsCache` interface
```go theme={null}
type FindingsCache interface {
Get(ctx context.Context, key string) (*entities.InterimReport, bool, error)
Put(ctx context.Context, key string, report *entities.InterimReport) error
}
```
The interface accepts `context.Context` on both methods to support network-backed implementations with timeouts and cancellation. The pipeline doesn't depend on which backend is behind the interface.
### Distributed backends
The `FindingsCache` interface is the extension point for multi-node scanning:
| Backend | Implementation | Use Case |
| - | - | - |
| **Disk** | `DiskFindingsCache` | Single-node, dev workflow |
| **Redis** | `RedisFindingsCache` | Multi-node cluster, shared LAN |
| **S3/GCS** | `S3FindingsCache` | Global fleet, persist across deploys |
| **Two-tier** | `TieredCache{L1: memory, L2: redis}` | Hot + warm layers |
Each one only implements `Get`/`Put`. The scanning pipeline works the same with any storage backend.
***
## Architecture map
```
internal/
├── cli/scan.go # CLI wiring: ecosystem detection, registry setup, pipeline invocation
├── dependency/
│ ├── resolver.go # Resolver interface + Dependency/ResolveResult types
│ ├── registry.go # Ecosystem → Resolver registry
│ ├── go_resolver.go # Go: production import closure via `go list -deps`
│ ├── java_resolver.go # Java: auto-detect Maven vs Gradle
│ ├── maven_resolver.go # Java/Maven: `mvn dependency:list/sources/tree`
│ ├── gradle_resolver.go # Java/Gradle: init-script export via `gradlew` / `gradle`
│ ├── pip_resolver.go # Python: `pip list` + `pip show`
│ ├── cargo_resolver.go # Rust: `cargo metadata --format-version=1`
│ └── source_cache.go # Shared: ZIP/JAR extraction to ~/.crypto-finder/cache/sources/
├── callgraph/
│ ├── types.go # FunctionID, FunctionDecl, FileAnalysis, CallGraph types
│ ├── builder.go # Parser interface + language-agnostic CallGraph construction
│ ├── parser_registry.go # Ecosystem → Parser factory (NewParserForEcosystem)
│ ├── go_parser.go # Go: syntactic parsing of Go source
│ ├── java_parser.go # Java: syntactic parsing of Java source
│ ├── bytecode_cache.go # Java: per-artifact bytecode index cache
│ ├── python_parser.go # Python: syntactic parsing of Python source
│ ├── rust_parser.go # Rust: syntactic parsing of Rust source
│ └── tracer.go # BFS backward tracer with configurable package separator
└── engine/
├── dependency_scanner.go # DependencyScanner: the 6-step pipeline (language-agnostic)
└── findings_cache.go # FindingsCache interface + DiskFindingsCache implementation
```
## Supported ecosystems
To add a new language, you implement two interfaces and register them. These ecosystems are supported today:
### Go
| Property | Value |
| - | - |
| Resolver | [`GoResolver`](https://github.com/scanoss/crypto-finder/blob/main/internal/dependency/go_resolver.go), which uses `go list -m -json` for the main modules and `go list -e -deps` for the modules in their production import closure and the Go files their imported packages compile for the host build (`GoFiles`, `CgoFiles`), the only ones scanned and parsed |
| Parser | [`GoParser`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/go_parser.go), which parses Go source syntactically |
| Manifest | `go.mod` |
| Module format | Go import path (e.g., `golang.org/x/crypto`) |
| Package separator | `/` |
| Source location | Go module cache (`$GOPATH/pkg/mod/`) |
### Java (Maven / Gradle)
| Property | Value |
| - | - |
| Resolver | [`JavaResolver`](https://github.com/scanoss/crypto-finder/blob/main/internal/dependency/java_resolver.go), which auto-detects Maven vs Gradle at the project root |
| Parser | [`JavaParser`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/java_parser.go), which parses Java source syntactically |
| Manifest | `pom.xml`, `build.gradle`, `build.gradle.kts`, `settings.gradle`, `settings.gradle.kts` |
| Module format | `groupId:artifactId` (e.g., `org.bouncycastle:bcprov-jdk18on`) |
| Package separator | `.` |
| Source location | Source JARs resolved by the active build tool and extracted to `~/.scanoss/crypto-finder/cache/sources/` |
#### Maven resolution details
The `MavenResolver` uses a **three-tier fallback strategy** to recover as many dependencies as it can, especially in multi-module projects:
Every Maven invocation receives `-Dmaven.repo.local=/.m2/repository`, so Maven writes artifacts to the same repository that Crypto Finder reads even when the JVM reports a different `user.home`. The operating-system user home is `HOME` on Unix and `USERPROFILE` on Windows.
**Tier 1: reactor with `--fail-never`** (always attempted):
* Runs `mvn dependency:list --fail-never -DappendOutput=true -DincludeScope=compile`
* The `--fail-never` flag continues past module failures; `-DappendOutput=true` accumulates results from all succeeding modules into a single output file
* If some modules resolve successfully, their dependencies are collected even if other modules fail
**Tier 2: per-module resolution** (if Tier 1 yields zero dependencies on a multi-module project):
* Detects modules from `` in the parent `pom.xml`
* Runs `mvn dependency:list -pl ` for each module independently
* Modules that fail are skipped; dependencies from succeeding modules are deduplicated and collected
**Tier 3: local install and retry** (if Tier 2 yields zero dependencies and inter-module failure is detected):
* Runs `mvn install -DskipTests --fail-never` to build all modules locally, populating `~/.m2/repository` with inter-module artifacts
* Retries Tier 1 after install
* This is expensive (requires compilation) but is the only way to resolve inter-module transitive dependencies
After dependency listing, the resolver also runs:
* **`mvn dependency:sources`** downloads `-sources.jar` files to `~/.m2/repository/` (best-effort; \~65% of Java libraries publish source JARs)
* **`mvn dependency:tree --fail-never -DappendOutput=true`** builds the dependency graph adjacency list (best-effort)
Dependencies without source JARs are included in the resolution results but without a source directory. They are skipped for source scanning and logged explicitly; if the compiled artifact is present in `~/.m2/repository`, Java bytecode indexing can still use it as a type-only dependency.
#### Gradle resolution details
The `GradleResolver` asks Gradle itself for a machine-readable dependency model via a temporary init script:
* Prefers `./gradlew` and falls back to `gradle` from `PATH`
* Uses the caller's `GRADLE_USER_HOME` when set; otherwise defaults it to `/.gradle` using the same operating-system user home
* Resolves the main Java compile classpath for single-project and multi-project builds
* Treats included Gradle subprojects as `WorkspaceMembers` rather than external dependencies
* Captures external module coordinates, versioned dependency edges, compiled JAR paths, and best-effort source archive paths
* Reuses the shared source extraction cache so Gradle and Maven dependencies flow through the same Java scanning pipeline
* Reachability still traces from application Java packages when the project has no `group` and `RootModule` is the Gradle project name
#### Multi-module project support
Crypto-finder detects multi-module Maven projects (a parent POM with ``) automatically. For these projects:
* All modules are registered as `WorkspaceMembers`, meaning they are treated as **user code** for call chain tracing (same as Cargo workspace members)
* The `WorkspaceMember.Name` follows the format `groupId:moduleDirName`
* The three-tier fallback strategy handles common multi-module failures:
* **Inter-module dependencies** (e.g., `eladmin-logging` depends on `eladmin-common`) are resolved through Tier 3.
* **HTTP mirror blocks** (Maven 3.8.1+ blocks insecure HTTP repositories) still yield partial results through Tier 1.
* **Missing parent POMs** or private repositories fall back to the partial results of Tier 1 or Tier 2.
#### Java call resolution
The `JavaParser` resolves method calls through import analysis:
1. **Explicit imports**: `import javax.crypto.Cipher;` → `Cipher.getInstance(...)` resolves to package `javax.crypto`
2. **Wildcard imports**: `import java.security.*;` → class names matched against wildcard packages
3. **Local variable types**: `Cipher c = Cipher.getInstance(...)` → `c.doFinal()` resolves `c` to type `Cipher` via local variable tracking
4. **Field types**: Class fields are tracked similarly to local variables
5. **Fallback**: Unresolved calls default to the current package (same as Go's behavior for unresolved variables)
### Python (pip)
| Property | Value |
| - | - |
| Resolver | [`PipResolver`](https://github.com/scanoss/crypto-finder/blob/main/internal/dependency/pip_resolver.go), which uses `python -m pip list --format=json` + `python -m pip show` to resolve packages with the same interpreter used for metadata lookups |
| Parser | [`PythonParser`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/python_parser.go), which parses Python source syntactically |
| Manifests | `pyproject.toml`, `requirements.txt`, `Pipfile`, `setup.py` |
| Module format | Python package name (e.g., `cryptography`) |
| Package separator | `.` |
| Source location | Site-packages directory (e.g., `~/.local/lib/python3.x/site-packages/`) |
#### Python resolution details
The `PipResolver` executes the following steps:
1. **Root module detection** reads `pyproject.toml` for `[project] name` or `[tool.poetry] name`; with neither the root module is empty and the project's own symbols are keyed at the scan root, never by the directory name
2. **`python -m pip list --format=json`** lists all installed packages with versions
3. **`python -m pip show `** gets location and dependency info for each package (batched in groups of 50)
4. **Distribution-to-import mapping** uses that SAME interpreter's `importlib.metadata.packages_distributions()` (Python 3.10+) to map distribution names to import names. Falls back to scanning `*.dist-info` directories (`top_level.txt` → `RECORD` file) for older Python versions
5. **Package root resolution** uses the import mapping, then heuristic name normalization, to find every top-level package directory and module file of the distribution (see "Python distributions scan every top-level package and module they installed"). C-extension packages are skipped
#### Python call resolution
The `PythonParser` resolves calls through import analysis:
1. **`import X`**: `X.func()` resolves `X` via imports
2. **`from X import Y`**: `Y()` resolves to package `X`, treated as constructor `Y.()`
3. **Chained attributes**: in `a.b.c.func()`, the first segment is resolved through imports and the rest is chained
4. **`self` calls**: `self.method()` resolves to the current package
5. **Wildcard imports**: `from X import *` recorded for fallback resolution
6. **Aliased imports**: in `import X as Y`, `Y` maps to `X`
7. **Fallback**: Unresolved calls default to the current package
### Rust (Cargo)
| Property | Value |
| - | - |
| Resolver | [`CargoResolver`](https://github.com/scanoss/crypto-finder/blob/main/internal/dependency/cargo_resolver.go), which uses `cargo metadata --format-version=1` |
| Parser | [`RustParser`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/rust_parser.go), which parses Rust source syntactically |
| Manifest | `Cargo.toml` |
| Module format | Crate name (e.g., `ring`) |
| Package separator | `::` |
| Source location | Cargo registry cache (e.g., `~/.cargo/registry/src/.../-/`) |
#### Rust resolution details
The `CargoResolver` runs `cargo metadata --format-version=1` which provides:
1. **All packages** with name, version, and manifest path
2. **Resolve graph** with dependency edges between packages
3. **Workspace detection**: packages with `source: null` (local/workspace crates) are treated as user code; all others are dependencies
Workspace members count as user code, so in a multi-crate workspace every workspace crate is "user code" for reachability analysis.
#### Rust call resolution
The `RustParser` resolves calls through `use` declaration analysis:
1. **Scoped identifiers**: in `Aead::new(...)`, `Aead` is resolved through `use` imports
2. **Qualified paths**: in `ring::aead::new(...)`, the first segment is resolved through imports
3. **Scoped use lists**: in `use ring::aead::{Aead, AeadCore}`, each item is registered separately
4. **Wildcard use**: `use ring::aead::*` recorded for fallback resolution
5. **`self` calls**: `self.method()` resolves to the current module
6. **`src/` transparency**: The `src/` directory is transparent in module paths (e.g., `ring/src/aead/` → `ring::aead`, not `ring::src::aead`)
7. **Impl blocks**: Methods in `impl Type { fn method() {} }` are extracted with their type association
8. **Fallback**: Unresolved calls default to the current module
### Adding a new language
To add support for a new ecosystem:
1. **Implement `callgraph.Parser`** with syntactic parsing for the target language.
2. **Implement `dependency.Resolver`**, which shells out to the ecosystem's package manager.
3. **Register the parser** in [`parser_registry.go`](https://github.com/scanoss/crypto-finder/blob/main/internal/callgraph/parser_registry.go) by adding one `case`.
4. **Register the resolver** in [`scan.go`](https://github.com/scanoss/crypto-finder/blob/main/internal/cli/scan.go) by adding one `depRegistry.Register()` call.
5. **Add manifest detection** in `detectEcosystem()` by adding one `if` that checks for the manifest file.
You don't need to change `builder.go`, `tracer.go`, `dependency_scanner.go`, the entities, or the schemas.
## Performance
### Two-phase call graph build
The call graph build is the most expensive step in the dependency scanning pipeline. To keep the cost down without losing any type resolution accuracy, the builder works in **two phases**:
**Phase 1, targeted source parsing.** Only dependencies with crypto findings + user code modules get full source parsing via `Parser.ParseDirectory()`. This builds `FunctionDecl` entries with call sites, parameters, and return types.
**Phase 2, bytecode type indexing for every dependency.** ALL dependencies (including those without findings) are indexed via `JavaBytecodeTypeResolver`. This reads `.class` files from Maven JARs to extract class names, method signatures, return types, and interface hierarchy. The type index is used to resolve fluent chains and enrich parameter types across dependency boundaries.
Both phases are needed because Java fluent APIs (e.g., `Jwts.builder().signWith(key)`) require knowing return types from one dependency to resolve calls in another. A dependency without crypto findings may define the return type that bridges a call chain from user code to a crypto finding. Skipping its type information would break backward tracing.
#### Benchmarks on eladmin: 160 deps, 27 with findings, 269 crypto assets
Current warm-run numbers with findings cache + Java bytecode cache enabled:
| Metric | Current |
| - | - |
| Packages source-parsed | 32 |
| Functions in graph | 168,386 |
| Caller index entries | 190,688 |
| Bytecode type packages | 165 |
| JARs indexed | 160 |
| Bytecode cache hits | 157 |
| Bytecode resolution duration | \~1.27s |
| Full dependency pipeline | \~33.0s |
| Wall time | \~44.0s |
| Findings | 269 |
| Exported call graph edges | 498,271 |
The largest recent improvements came from these changes:
* In the two-phase call graph build, only dependencies with findings get full source parsing.
* Exact-version Java JARs are indexed in parallel.
* A per-artifact bytecode cache means repeated scans don't reparse unchanged JARs.
### Current bottlenecks
The pipeline has three main time consumers:
1. **Source parsing for graph packages** (\~23s on warm `eladmin`): Parses `.java` files from 32 packages (user code + 27 deps with findings) to build 168K function declarations with call sites.
2. **Dependency scanning with opengrep**: Still dominates cold scans. On warm scans most dependencies hit the findings cache; on first scans the cost depends on dependency source size and worker count (`--dep-workers`). Batching removed the per-dependency rule load, so the remaining cost is the analysis itself.
3. **Call graph post-processing** (\~2-3s): Caller index construction, bytecode merge/rewrite, and fluent-chain resolution are no longer dominant but still scale with graph size.
### Future optimization opportunities
#### Source parsing and graph size reduction
The bytecode resolution bottleneck is largely gone. The remaining performance opportunities are upstream:
* **Reduce graph packages further**: Keep shrinking the set of dependencies that require full source parsing without breaking attribution accuracy.
* **Smarter pre-scan eligibility**: Skip dependencies that have source directories but no scannable files before invoking opengrep.
* **Containing-function lookup index**: `Tracer.FindContainingFunction()` still scans functions linearly; indexing by file could cut repeated lookups on large reports.
* **Selective bytecode indexing**: Only index JARs whose types appear in unresolved calls from graph packages. More complex, but now one of the few remaining bytecode-side wins.
#### Opengrep scanning
* **Result caching**: The existing `DiskFindingsCache` caches dependency scan results by `module@version:rulesHash`. On repeated scans of the same project, most dependencies hit the cache.
* **Cold-scan throughput**: First-scan performance still depends heavily on opengrep throughput over large dependency sources. The next step there is to scan only the directories a Go module actually imports, rather than the whole module.
#### Call graph export
* **Export is fast** (\~10-15s for 269 findings) and not currently a bottleneck. The finding-centric export only traces paths reachable from findings, producing a compact JSON (\~9.5K lines for eladmin) regardless of full graph size.
## Limitations
### General
* **Static analysis.** The call graph is built from syntactic call expressions. It cannot resolve interface dispatch, reflection-based calls, or function values passed as arguments.
* **One module at two versions.** A finding's `dependency.relationship` and `dependency.path` come from the resolved dependency graph. When a module resolves at more than one version (npm installs a package twice when versions conflict), a resolver that records versioned edges (npm, Maven, Gradle, Cargo) gives each copy its own route. For a resolver that does not, the finding in such a module, and any dependency reached through one, carries no `relationship` or `path`; its `purl` and `version` stay.
* **All paths stored.** When several call chains exist (BFS finds all paths), all of them are stored in `call_chains`, so no reachability information is lost.
### Go-specific
* **Production import closure only.** The inventory holds the modules that provide a package imported, directly or transitively, by a non-test package of a main module. Modules required only by `_test.go` files or by build-tagged tool files such as `tools.go`, and requirements nothing imports, are not resolved or scanned. `--include-tests` does not widen the inventory.
* **Host build configuration.** `go list -deps` evaluates build constraints for the scanning host's `GOOS`, `GOARCH` and build tags (including any in `GOFLAGS`), so a module imported only on another platform is not inventoried, and within an imported package only the files that build compiles are scanned and parsed. `CGO_ENABLED` decides whether files that import `"C"` count.
* **Packages that fail to load.** A package the go tool cannot load, such as a directory that mixes two package names or imports a module missing from the module cache while offline, is skipped with a warning. Modules imported only through it are not inventoried; the rest of the closure is.
* **No cross-module method resolution.** Method calls on variables (e.g., `cipher.Encrypt()`) are recorded with the variable name as the type, not the resolved type. Cross-package type resolution would require full type analysis.
### Java-specific
* **Gradle source archives are best-effort.** Gradle dependency resolution provides binary artifact paths deterministically, but source archive availability still depends on what upstream repositories publish.
* **Missing source JARs.** Dependencies without sources are skipped for source scanning, but they can still contribute Java bytecode types if the compiled JAR is present locally. They cannot produce source-level findings until sources are available. Their code has no calls in the graph, so no call chain crosses them: in the callgraph export such a dependency on a finding's `dependency.path` carries `without_source: true`, and a finding whose every route from the application crosses one, with no chain reaching it, reads `unknown` (`dependency_without_source`) instead of `unreachable`.
* **Wildcard import resolution.** When multiple wildcard imports could match a class name, resolution is best-effort.
* **Limited inheritance and polymorphism.** Variable types are tracked syntactically, but interface method call sites and abstract-class method call sites are fanned out to same-name/arity concrete implementations and subclass overrides within the same namespace root (see `expandInterfaceDispatch`/`expandAbstractClassDispatch` in `internal/callgraph/builder.go`). This is a name+arity heuristic, not full type resolution, so it can over-match unrelated overloads across sibling implementations.
* **Multi-module Maven partial resolution.** Multi-module Maven projects are supported through a three-tier fallback strategy. Tier 3 (`mvn install -DskipTests`) requires compilation and may fail if the project needs specific JDK versions or build tools not available in the scan environment.
### Python-specific
* **Requires a Python interpreter with `pip`.** The resolver runs `python -m pip` and `importlib.metadata` through the same interpreter. If `VIRTUAL_ENV` is set, that environment's Python is preferred; otherwise it falls back to `python3` then `python` from PATH.
* **C-extension packages are skipped.** Packages without Python source on disk (compiled C extensions) cannot be scanned.
* **Distribution-to-import mapping.** The resolver relies on `importlib.metadata` (Python 3.10+) or `*.dist-info` fallback; packages with non-standard layouts may not be resolved.
* **No dynamic dispatch.** Calls resolved through `getattr`, `__getattr__`, or metaclass magic are not tracked.
### Rust-specific
* **Requires `cargo` in PATH.** The resolver shells out to `cargo metadata`.
* **No trait dispatch.** Method calls on trait objects (e.g., `dyn Cipher`) are resolved syntactically by type name; trait implementations are not followed.
* **Macro-generated code.** Functions generated by macros (e.g., `proc_macro`) are invisible to syntactic parsing.
* **`src/` transparency assumption.** The parser assumes `src/` is the crate root; non-standard `[lib] path` configurations may produce incorrect module paths.
# Docker usage
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/docker-usage
Run Crypto Finder from its official Docker images, locally, in CI/CD, or with dependency scanning.
## Available images
Crypto Finder has three images. They differ in size and in what they include:
| Image | Tag | Size | Use case |
| - | - | - | - |
| **Standard** | `latest` | \~800MB | Default: binary + OpenGrep + Semgrep |
| **Slim** | `latest-slim` | \~15MB | Binary only, bring your own scanner |
| **Deps** | `latest-deps` | \~3-4GB | All language toolchains for dependency scanning |
### Standard image (recommended)
The standard image includes OpenGrep 1.12.1+ and Semgrep 1.145.0+, so every scanning capability works out of the box.
```bash theme={null}
docker pull ghcr.io/scanoss/crypto-finder:latest
```
### Slim image
The slim image contains only the `crypto-finder` binary, which keeps it small. You must supply an external OpenGrep or Semgrep installation, for example by mounting it into the container or by building a derived image. Use it when your workflow manages scanner versions itself.
```bash theme={null}
docker pull ghcr.io/scanoss/crypto-finder:latest-slim
```
### Deps image (dependency scanning)
The deps image adds the language toolchains needed to scan third-party dependencies with `--scan-dependencies`. It detects cryptographic usage in those dependencies and traces the call chains that reach it.
```bash theme={null}
docker pull ghcr.io/scanoss/crypto-finder:latest-deps
```
It has everything in the standard image, plus:
* The Go toolchain, for dependency resolution with `go list` and `go mod graph`.
* Java (JDK 8, 11, 17, and 21, with Maven and Gradle), for JDK-aware Java dependency resolution and deterministic platform signature indexing.
* Rust (Cargo), for dependency resolution with `cargo metadata`.
* Python in an isolated virtualenv, for pip dependency resolution.
## Basic usage
### Scanning with mounted volumes
```bash theme={null}
# Scan code directory with remote rulesets
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/output:/workspace/output \
-e SCANOSS_API_KEY=YOUR_KEY \
ghcr.io/scanoss/crypto-finder:latest \
scan --output /workspace/output/results.json /workspace/code
# Scan with local rules
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/rules:/workspace/rules:ro \
-v $(pwd)/output:/workspace/output \
ghcr.io/scanoss/crypto-finder:latest \
scan --no-remote-rules --rules-dir /workspace/rules \
--output /workspace/output/results.json /workspace/code
# Generate CycloneDX CBOM
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/output:/workspace/output \
-e SCANOSS_API_KEY=YOUR_KEY \
ghcr.io/scanoss/crypto-finder:latest \
scan --format cyclonedx --output /workspace/output/cbom.json /workspace/code
```
### Volume mounting patterns
| Mount point | Purpose | Recommended mode |
| - | - | - |
| `/workspace/code` | Source code to scan | `:ro` (read-only) |
| `/workspace/rules` | Custom rule files | `:ro` (read-only) |
| `/workspace/output` | Scan results | `:rw` (read-write) |
| `~/.scanoss/crypto-finder/cache` | Ruleset cache (optional) | `:rw` (read-write) |
### Preserving the cache between runs
```bash theme={null}
# Create a named volume for cache persistence
docker volume create crypto-finder-cache
# Use the volume in scans
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/output:/workspace/output \
-v crypto-finder-cache:/root/.scanoss/crypto-finder/cache \
-e SCANOSS_API_KEY=YOUR_KEY \
ghcr.io/scanoss/crypto-finder:latest \
scan --output /workspace/output/results.json /workspace/code
```
## CI/CD integration
### GitHub Actions
This workflow scans code on every push and pull request:
```yaml theme={null}
name: Crypto Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Run Crypto Finder
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-e SCANOSS_API_KEY=${{ secrets.SCANOSS_API_KEY }} \
ghcr.io/scanoss/crypto-finder:latest \
scan --output /workspace/results.json /workspace/src
- name: Upload results
uses: actions/upload-artifact@v3
if: always()
with:
name: crypto-scan-results
path: results.json
- name: Check for findings
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
ghcr.io/scanoss/crypto-finder:latest \
scan --fail-on-findings /workspace/src
```
#### With custom rules
```yaml theme={null}
name: Crypto Scan (Custom Rules)
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Run Crypto Finder with custom rules
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
ghcr.io/scanoss/crypto-finder:latest \
scan --no-remote-rules \
--rules-dir /workspace/.crypto-rules \
--output /workspace/results.json \
/workspace/src
- name: Generate CBOM
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
ghcr.io/scanoss/crypto-finder:latest \
convert /workspace/results.json --output /workspace/cbom.json
- name: Upload CBOM
uses: actions/upload-artifact@v3
with:
name: crypto-cbom
path: cbom.json
```
## Dependency scanning
The `latest-deps` image includes every language toolchain that dependency scanning needs. Use it with the `--scan-dependencies` flag.
### Go projects
```bash theme={null}
docker run --rm \
-v $(pwd):/workspace/code:ro \
ghcr.io/scanoss/crypto-finder:latest-deps \
scan --scan-dependencies /workspace/code
```
### Java (Maven or Gradle) projects
```bash theme={null}
docker run --rm \
-v $(pwd):/workspace/code:ro \
ghcr.io/scanoss/crypto-finder:latest-deps \
scan --scan-dependencies /workspace/code
```
### Rust projects
```bash theme={null}
docker run --rm \
-v $(pwd):/workspace/code:ro \
ghcr.io/scanoss/crypto-finder:latest-deps \
scan --scan-dependencies /workspace/code
```
### Python projects
For Python projects, install the dependencies before scanning, because pip needs them to be resolved:
```bash theme={null}
docker run --rm \
-v $(pwd):/workspace/code:ro \
--entrypoint sh \
ghcr.io/scanoss/crypto-finder:latest-deps -c \
"pip install -r /workspace/code/requirements.txt 2>/dev/null; \
crypto-finder scan --scan-dependencies /workspace/code"
```
> **Note:** The deps image keeps project dependencies in an isolated virtualenv, separate from the scanner tooling (semgrep), so `pip install` affects only the target project's environment.
### CI/CD with dependency scanning (GitHub Actions)
```yaml theme={null}
name: Crypto Scan with Dependencies
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Run Crypto Finder with dependency scanning
run: |
docker run --rm \
-v ${{ github.workspace }}:/workspace \
-e SCANOSS_API_KEY=${{ secrets.SCANOSS_API_KEY }} \
ghcr.io/scanoss/crypto-finder:latest-deps \
scan --scan-dependencies --output /workspace/results.json /workspace
```
## Advanced Docker usage
### Running with a specific scanner
```bash theme={null}
# Use Semgrep instead of default OpenGrep
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/output:/workspace/output \
-e SCANOSS_API_KEY=YOUR_KEY \
ghcr.io/scanoss/crypto-finder:latest \
scan --scanner semgrep --output /workspace/output/results.json /workspace/code
```
### Custom timeout and language override
```bash theme={null}
docker run --rm \
-v $(pwd)/code:/workspace/code:ro \
-v $(pwd)/output:/workspace/output \
-e SCANOSS_API_KEY=YOUR_KEY \
ghcr.io/scanoss/crypto-finder:latest \
scan \
--timeout 30m \
--languages java,python,go \
--output /workspace/output/results.json \
/workspace/code
```
## Docker Compose
An example `docker-compose.yml` for local development:
```yaml theme={null}
version: "3.8"
services:
crypto-finder:
image: ghcr.io/scanoss/crypto-finder:latest
volumes:
- ./code:/workspace/code:ro
- ./rules:/workspace/rules:ro
- ./output:/workspace/output
- crypto-cache:/root/.scanoss/crypto-finder/cache
environment:
- SCANOSS_API_KEY=${SCANOSS_API_KEY}
command: >
scan
--rules-dir /workspace/rules
--output /workspace/output/results.json
/workspace/code
volumes:
crypto-cache:
```
Start it with:
```bash theme={null}
SCANOSS_API_KEY=your_key docker-compose up
```
# Installation
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/installation
Install SCANOSS Crypto Finder with Go, from a Docker image, or by building it from source.
## Prerequisites
You need the following before you install Crypto Finder:
* A scanner, either [OpenGrep (recommended)](https://github.com/opengrep/opengrep)
or [Semgrep](https://github.com/semgrep/semgrep). The Docker images include both.
```bash theme={null}
# OpenGrep v1.12.1: download from https://github.com/opengrep/opengrep/releases/tag/v1.12.1
# Semgrep:
pip install semgrep==1.145.0
```
Crypto Finder uses OpenGrep as its **default scanner**.
* [Go 1.25+](https://go.dev/dl/) and [Git](https://git-scm.com/install/), but only if you
build from source or install with the Go package manager.
## Installation
### Go
If you have Go installed, install Crypto Finder from its package path:
```bash theme={null}
go install github.com/scanoss/crypto-finder/cmd/crypto-finder@latest
```
To verify the installation, run:
```bash theme={null}
crypto-finder
```
### Docker
Crypto Finder has three Docker images. The larger images include more tooling:
```bash theme={null}
# Full image with scanners included (recommended)
docker pull ghcr.io/scanoss/crypto-finder:latest
# Slim image (requires an external scanner; see Prerequisites)
docker pull ghcr.io/scanoss/crypto-finder:latest-slim
# Deps image: adds Go, Java (JDK 8/11/17/21 + Maven/Gradle), Rust, and Python
# toolchains for --scan-dependencies
docker pull ghcr.io/scanoss/crypto-finder:latest-deps
```
[Docker usage](/en/latest/cli/crypto-finder/docker-usage) compares the images in full and covers dependency-scanning workflows.
### Build from source
#### Windows
1. Open PowerShell and run:
```powershell theme={null}
git clone https://github.com/scanoss/crypto-finder.git
cd crypto-finder
go build -o crypto-finder.exe ./cmd/crypto-finder
```
2. Optionally, add the folder to your PATH:
```powershell theme={null}
$currentPath = (Get-Location).Path
[Environment]::SetEnvironmentVariable("PATH", "$env:PATH;$currentPath",
[EnvironmentVariableTarget]::User)
```
> **Note:** Restart PowerShell after you change the PATH, so the change takes effect.
3. Verify the installation:
```powershell theme={null}
# If added to PATH
crypto-finder
# Or run from the current directory
.\crypto-finder.exe
```
#### macOS
1. Clone, compile, and install:
```bash theme={null}
git clone https://github.com/scanoss/crypto-finder.git
cd crypto-finder
make build
sudo make install
```
2. Verify the installation:
```bash theme={null}
crypto-finder
```
#### Linux
1. Clone, compile, and install:
```bash theme={null}
git clone https://github.com/scanoss/crypto-finder.git
cd crypto-finder
make build
sudo make install
```
2. Verify the installation:
```bash theme={null}
crypto-finder
```
# Output formats
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/output-formats
Crypto Finder writes two output formats: interim JSON for detailed analysis, and CycloneDX CBOM for standard Bill of Materials reporting.
## Interim JSON format
Interim JSON is the default output format. It holds detailed cryptographic asset information and is designed for the SCANOSS ecosystem.
The interim report is the primary findings artifact. It contains finding metadata such as `finding_id` and optional structural `occurrence_key`, but it does not embed the finding-centric reachability slices produced by `--export-callgraph`.
### Format specification
```json theme={null}
{
"version": "1.5",
"tool": {
"name": "crypto-finder",
"version": "0.1.0"
},
"findings": [
{
"file_path": "path/to/file",
"language": "language_name",
"cryptographic_assets": [
{
"start_line": 123,
"end_line": 123,
"match": "code snippet",
"rules": [
{
"id": "rule.id",
"message": "description",
"severity": "INFO|WARNING|ERROR"
}
],
"status": "pending|identified|dismissed|reviewed",
"metadata": {
"assetType": "algorithm|certificate|protocol|related-crypto-material",
"algorithmFamily": "algorithm_family",
"algorithmPrimitive": "primitive_type",
"algorithmMode": "mode_of_operation",
"algorithmPadding": "padding_scheme"
},
"source": "direct|dependency",
"dependency_info": {
"module": "golang.org/x/crypto",
"version": "v0.17.0",
"purl": "pkg:golang/golang.org/x/crypto@v0.17.0"
},
"finding_id": "a1b2c3d4",
"occurrence_key": "v1:0123456789abcdef"
}
]
}
]
}
```
> **Note:** Version 1.1 introduced the `rules` array field (replacing single `rule` field) to support per-line deduplication. Version 1.2 added `source` and `dependency_info` for dependency scanning attribution. Version 1.3 adds `finding_id` for cross-referencing with the callgraph export. Version 1.5 adds optional `occurrence_key` for canonical findings, using AST call evidence when available and a deterministic file/module-level fallback for valid top-level calls. Dependency-backed `file_path` values are dependency-root-relative; the package identity stays in `dependency_info`. Reachability slices such as `call_chains` are emitted by the dedicated call graph export, not by the interim report. See [Dependency scanning](/en/latest/cli/crypto-finder/dependency-scanning) for details.
### Field descriptions
| Field | Description |
| - | - |
| `version` | Format version (currently "1.5") |
| `tool.name` | Scanner used (crypto-finder) |
| `tool.version` | Scanner version |
| `findings` | Array of file-level findings |
| `file_path` | Relative path to scanned file |
| `language` | Detected programming language |
| `cryptographic_assets` | Array of crypto findings in the file |
| `start_line` | First line where the asset was detected |
| `end_line` | Last line where the asset was detected |
| `match` | Actual code snippet matched |
| `rules` | Array of detection rules that identified this asset |
| `rules[].id` | Unique rule identifier |
| `rules[].message` | Human-readable description |
| `rules[].severity` | Finding severity level |
| `status` | Finding status (pending, identified, dismissed, reviewed) |
| `metadata` | Key-value pairs with asset-specific metadata |
| `metadata.assetType` | Asset classification |
| `metadata.algorithmFamily` | Algorithm/protocol family name |
| `metadata.algorithmPrimitive` | Cryptographic primitive type |
| `metadata.algorithmMode` | Mode of operation (for block ciphers) |
| `metadata.algorithmPadding` | Padding scheme used |
| `source` | `"direct"` (user code) or `"dependency"` (v1.2+) |
| `dependency_info` | Attribution for dependency findings: `module`, `version`, and optional `purl` (v1.5+) |
| `purl` | Optional canonical package URL promoted from direct rule metadata; it stays versionless unless one unambiguous direct dependency version is available (v1.6+) |
| `finding_id` | Stable short hash used to join the interim report to the call graph export (v1.3+) |
| `occurrence_key` | `v1:<16 lowercase hex>` structural identity, present on every asset whenever the scan built a call graph, so every asset that has a finding graph carries the key its graph carries. It excludes rules, source text, metadata, reachability, and severity. It uses the AST anchor of the matched call when there is one. A match with no call to anchor, such as a type usage (a cast or a declaration) or a library API declaration surfaced as an entry point, is keyed by the function that holds it; a valid top-level call uses a deterministic file/module-level fallback (v1.5+). A per-value asset, one a rule's `parameterCondition` specialized to a resolved value such as `param[0]==SHA-256`, also hashes that value, so the values of one call have distinct keys and `finding_id`s. A synthesized entry point (rule id `crypto-finder.api-entry-point`) that has several variants at one declaration, such as `PBKDF2` next to `PBKDF2-SHA-256` selected by an argument type (`param[0]:type==...`), or `ECDSA` next to `ECDSA-deterministic`, gives each variant except the base its own `finding_id` and `occurrence_key`, derived from the variant's `algorithmName`, `algorithmHashFunction` and `parameterCondition` (variants that share all three hash their whole metadata instead); the base keeps the identity a lone entry point has. Two calls of one API in a statement that spans lines get distinct keys; `finding_id` hashes file, start line and rule, so such a pair shares it and `(finding_id, occurrence_key)` is the unique pair. Every other asset hashes exactly the structural identity above. A report written without a call graph may omit it. |
| `parameter_conditions` | Structured argument predicates parsed from the rule's `parameterCondition` metadata — which argument value/type selects this asset variant (v1.4+, omitted when the rule carries no predicate) |
| `conditioned_value` | The resolved condition, such as `param[0]==HmacSHA256`, a per-value asset was specialized for. It is part of the asset's `finding_id` and `occurrence_key`, so a report read back by another scan, such as published dependency findings, keeps one identity per value (v1.7+, omitted on every other asset) |
| `terminal_start_col`, `terminal_end_col` | Columns of the call that encloses an asset matched on an argument span, such as `new KeyParameter(key)` inside `cipher.init(...)`. They locate the crypto call for the callgraph export and the `occurrence_key`, so a report read back by another scan finds the same call (v1.7+, omitted on every other asset) |
| `file_path` | For dependency findings, path relative to the dependency root; use `dependency_info` for artifact identity |
### Public Go contract
Go consumers can import `github.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 ` is passed, Crypto Finder also writes a separate finding-centric call graph JSON file to ``. 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[].dependency` names the dependency a dependency finding sits in and its route from the application in the resolved dependency graph, with `without_source` on a path step parsed without source. `finding_graphs[].analysis` gains `paths_total`, `paths_kept`, `route_evidence` and `no_callers_only`. The first frame of each live chain carries `root_kind` (`main`, `framework_entry`, `no_callers`, `depth_limit`). `unresolved_reason` gains three values for an attributed finding whose verdict is `unknown`: `traversal_truncated`, `unresolved_dispatch` and `dependency_without_source`; the stitched export now writes `unresolved_reason` too, with `unresolved_dispatch` only. Every new field is optional, so a `6.16` reader that ignores unknown properties keeps working; a reader that switches on `unresolved_reason` must treat an unknown value as unknown. Frame shape is the `6.15` interned shape. The stitched export now also writes `root_kind` and `no_callers_only`, when the fragments recorded entry kinds (`graph-fragment-1.14`).
* **`6.16`** (SDK opt-in; the CLI default before `6.17`) adds optional `scan_metadata.ecosystems` to 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 with `ecosystem`, `root_module`, `function_count` and `edge_count`. `scan_metadata.ecosystem`, `root_module`, `function_count` and `edge_count` keep 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 inlined `6.14` compatibility render. Frame shape is the `6.15` interned shape.
* **`6.15`** (SDK opt-in; the CLI default before `6.16`) contracts `call_chains` frames: they no longer repeat catalog identity fields (`function_name`, `file_path`, `canonical_signature`, `dependency_info`, and the rest of the interned identity). Join through `functions[]` plus `finding_graphs[].call_chain_indexes`, and through `canonical_signature` on `crypto_entry_points` and catalog rows. Hop-specific fields (`entry_call`, `crypto_call`, `entry_resolution`) stay on the frames. Enable with `--export-callgraph-interned-frames` or `ScanMeta.InternedFrames`. Zero-value SDK/stitch stays on `6.14`; the CLI defaulted to `6.15` until `6.16`. Parsers that only read `call_chains[]` objects must gate on `schema_version` or call `HydrateChainIdentities`.
* **`6.14`** adds a top-level interned `functions[]` catalog and `finding_graphs[].call_chain_indexes` (0-based positions into that catalog) beside the existing inlined `call_chains`. The index lists reconstruct the same N-sample of routes. Inlined frames keep their previous field names and meaning. `crypto_entry_points` is still the complete reverse-reach set. Both surfaces can explicitly select this compatibility render.
* **`6.13`** adds `call_chains[].entry_resolution` and `entry_declared_type`, reporting how the call arriving at each frame was established.
* **`6.12`** adds the rule-vs-callgraph key-length conflict marker to `supporting_calls[].supporting_call.resolved_key_length`. When a detection rule declares a static `keyLength` and the callgraph resolves a different value for a finding referencing that evidence, the resolved `bits` stay primary, the rule value is retained as `rule_declared_bits`, and `rule_conflict` is `true`. Agreement, an unresolved key length, and a rule that declares no `keyLength` all 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.11`** adds optional `supporting_calls[].supporting_call.resolved_key_length` for structurally derived Java key-generation configuration calls referenced by `finding_graphs[].supporting_call_ids`. It contains raw integer `bits` only when static analysis resolves a literal or simple propagated constant, `provenance` (`constant` or `unknown`), and required `source_call` (`function_name`, `line`, `parameter_index`) for the contributing argument. It is preserved by live, graph-fragment, and stitched callgraph exports; terminal `crypto_call` records do not carry it, and it does not populate CBOM properties or express a security threshold.
* **`6.10`** carries an optional top-level `purl` for direct findings and an optional canonical `purl` inside 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.10`** adds optional `occurrence_key` to 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 using `finding_id` alone.
* **`6.9`** makes `crypto_entry_points[]` a reverse-reachability answer rather than a projection of the exported `call_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_depth` is 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.8`** adds `finding_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 to `unknown`), `finding_graphs[].analysis` (`call_chains`/`parameters` completeness), and `crypto_entry_points[].root`. `root` is the explicit chain-root classification, while the index itself stays deliberately broader. The legacy `reachable` bool is unchanged.
* **`6.7`** adds `reachable` to each finding graph when dependency scanning makes user-code reachability applicable; it is omitted for a standalone library scan.
* **`6.6`** adds deterministic `forward_calls.ambiguous_calls` groups for fail-closed interface dispatch: completeness state, stable group/candidate IDs, complete callable identities, and preserved call-site argument provenance. It does not promote ambiguous candidates to resolved edges.
* **`6.5`** makes `role: operation` contract methods **supporting-call-only**: they are exported as categorized `supporting_calls` referenced by `supporting_call_ids` (including interface-authored contracts resolved to concrete implementations) and are no longer synthesized as operation-only `crypto_entry_points` in live, fragment, or stitched exports.
* **`6.4`** adds `method_role`, `role_provenance`, and `parameter_roles`, a method and parameter role classification derived from the contracts KB (`factory`/`config`/`output`/`operation` on `method_role`; per-parameter `operation-determining`/`metadata-contributing`/`none` with a `contributes: {property, derivation}` block on `parameter_roles`). `method_role`/`role_provenance` appear on `crypto_entry_points`; `parameter_roles` appears on `crypto_entry_points` and on the supporting-call declaration, index-aligned with `parameter_types`, and 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.3`** added the optional per-finding `forward_calls` block, which holds the finding anchor's forward call closure: deduped nodes with `depth`/`crypto_relevant`/`supporting_category`, plus traversed edges whose `entry_call` carries the call-site argument data-flow. The block is emitted only when the stitch runs with `StitchOptions.ForwardClosure`. Caps on depth, nodes, and edges are reported through `max_depth` and an explicit `truncated` flag, never applied silently.
* **`6.2`** exposed `supporting_calls`, `crypto_entry_points`, and `graph_algo_version` end-to-end.
* **`6.0`** removed the legacy `entry_point_index` projection and made `crypto_entry_points[]` canonical.
* **`4.3`** added Java runtime provenance in `scan_metadata` for JDK-aware platform signature enrichment.
* Each top-level record preserves `finding_id`. When `occurrence_key` is 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 without `occurrence_key` use `finding_id` alone.
* `call_chains` is 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 128` opts in to a larger sample. SDK/stitch zero or omitted `StitchOptions.MaxChains` remains 128. The live export has no depth cap. The sample is spent on the strongest evidence first (see `analysis.route_evidence`): every route whose calls all resolved statically, then routes that need a dispatch edge, then routes that need a `name_only` edge. 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 carries `root_kind`: `main` (a Java `static main(String[])`, or a free function named `main`), `framework_entry` (a Java method that is an `@Override` of a method of a type outside the application, such as `HttpHandler.handle` or `Runnable.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), or `depth_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 as `main` or `framework_entry` and no application code calls it: a handler doing crypto inline reads `reachable`, a helper nothing calls reads `unreachable`. `crypto_entry_points[].root` follows 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//.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 the `root_kind` it 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 typed `Express`); 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 typed `chi.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: required` asks 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 add `directory` to count only files under a directory of that name, and name `` for the class body: the body of a Django `Migration` under a `migrations` directory is an entry point, since the migration loader runs it and reaches the `RunPython` callbacks 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 generated `RegisterServer` or `RegisterHandlerServer` function, whose `names` are 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) and `file_convention` (an export a framework finds by the file's location, counted only when the nearest `package.json` declares 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 under `internal/scan/testdata/entry_rules//` with a case in `internal/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 nearest `package.json` names in `main`, `module` or `exports` (`dist/`, `build/`, `lib/`, `out/`, `esm/`, `cjs/` and `es/` paths also match the same path under `src/` and at the package root), and `main` for the top level (``) of that module, of a module `bin` names, of a module a `package.json` script runs with `node`, `nodejs`, `tsx`, `ts-node`, `bun` or `deno` (`tsx scripts/seed.ts`, also after `&&`, `||`, `;` or `|`, with the same `dist/` to `src/` mapping), of a `.js`, `.mjs`, `.cjs`, `.ts`, `.mts` or `.cts` file whose first line is a shebang naming one of those runtimes, and of a module with `if (require.main === module)`; Python: `main` for the functions the nearest `pyproject.toml` (`[project.scripts]`, `[project.gui-scripts]`, `[tool.poetry.scripts]`), `setup.cfg` (`[options.entry_points]`) or `setup.py` names as `module:function`, and for the top level (``) of a module with `if __name__ == "__main__":`; Go: `main` for `init` functions and the synthetic `` package variable initializers, which the runtime runs before `main`. 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 or `new` argument, 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 under `test/`, `tests/` or `__tests__/` or named like a test (`*Test.java`, `*_test.go`, `test_*.py`, `*_test.py`, `conftest.py`, `*.test.*`, `*.spec.*`), keeps `no_callers`. Test sources are in the graph only under `--include-tests`.
* `analysis.paths_total` is the number of distinct routes, from a chain root to the finding, the graph holds, and `analysis.paths_kept` the number the finding's `call_chains` show; chains that differ only in a call-site line count once. `paths_total` saturates at the largest int64 and is then a lower bound. Both are present on live findings with a chain: reachable ones and `unresolved_dispatch` ones. `analysis.call_chains` is `partial` whenever 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 reads `reachable`, not `unknown` with no chains. A depth limit that cuts every route before a root reads `unknown` with `unresolved_reason` `traversal_truncated`; that reason, unlike the others, comes without `finding_location`, since the finding is attributed. A finding whose every route crosses a `name_only` edge reads `unknown` with `unresolved_reason` `unresolved_dispatch`, also without `finding_location`: its chains are exported as evidence of how the crypto may run, and the legacy `reachable` flag is omitted. One route free of `name_only` edges keeps it `reachable`. `analysis.route_evidence` rates the finding's strongest route by its weakest call: `direct` (every call `exact` or statically resolved), `dispatch` (the best route needs at least one `interface_dispatch` or `python_subclass_dispatch` edge and no `name_only` edge; the finding stays `reachable`, and the dispatch hop is the frame whose `entry_resolution` names it), or `name_only` (every route needs a `name_only` edge; the verdict is `unknown` with `unresolved_dispatch`). A finding specialized by a `parameter_conditions` predicate that removed chains rates `route_evidence` and `no_callers_only` from its surviving chains only, so a condition that refutes the direct route cannot leave `direct` behind; with no chain left both are absent. The verdict follows the same chains: a specialized finding whose surviving chains all cross a `name_only` edge reads `unknown` with `unresolved_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`, flagged `no_callers_only`). It is present with `paths_total` on the live export, and on stitched findings that are `reachable` through a chain or `unresolved_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_only` is `true` when every root of the routes that support the verdict has `root_kind` `no_callers`: the crypto is reached only from application code nothing in the application calls, not from a recognized `main` or framework entry. The supporting routes are those free of `name_only` edges, 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 no `root_kind`. On the stitched export a root reads `main` or `framework_entry` from the fragment's `entry_kind`, and `no_callers` when it has none and nothing in the stitched graph calls it; `depth_limit` never 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 no `name_only` edge, reads such a finding `unknown` with `unresolved_reason` `unresolved_dispatch` and no chain, and a dependency's mine-time entry-point index does not upgrade it to `reachable`; it sets no other `unresolved_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](https://github.com/scanoss/crypto-finder/blob/main/docs/adr/0002-call-chains-sample-size.md).
* `functions[]` (schema `6.14`+) is the interned identity catalog for those emitted routes. `finding_graphs[].call_chain_indexes` lists the same walks as integer positions into that catalog. Schema `6.14` also inlines those fields on `call_chains` frames. Schemas `6.15`, `6.16` and `6.17` (local CLI default) join identity through the catalog alone.
* Each chain node carries hop-specific fields: optional `entry_call`, `entry_resolution`, and `crypto_call` on the last node. Compatibility schema `6.14` also inlines function identity (`function_name`, `file_path`, `canonical_signature`, `dependency_info`) on each frame. The interned render (`6.15`+) keeps that identity only in `functions[]`.
* `entry_call` describes how execution entered the current node from the previous step. Its `file_path` and `line` refer to the call site in the previous node's source file.
* `entry_resolution` says 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 as `threading.Thread(target=fn)`, `sort.Slice(xs, fn)` or `setTimeout(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 of `entry_declared_type`, from the bytecode hierarchy or the parsed extends/implements clauses), `python_subclass_dispatch`, or `name_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[]` and `crypto_call.parameters[]` both use the same parameter model: `parameter_index` (always `0`-based), best-effort `type`, `argument_expression`, `resolved_value`, `variable_name` for simple identifiers only, and recursive `source_nodes`.
* `supporting_calls[].supporting_call.resolved_key_length` is 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 the `RSAKeyGenParameterSpec`, `ECGenParameterSpec`, and `SecretKeySpec` constructors whose value reaches such a call. `source_call.function_name` names 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 through `finding_graphs[].supporting_call_ids`. It reports raw key bits when known and otherwise retains `provenance: "unknown"` plus `source_call`; consumers must not infer a key-size threshold from it. The terminal `crypto_call` remains the detected operation and does not carry this field.
* `supporting_calls[].supporting_call.resolved_key_length.rule_conflict` reports that a detection rule declared a static `keyLength` disagreeing with the resolved `bits`. The resolved value remains primary and the rule value is preserved in `rule_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_bits` reports 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_metadata` may also include `java_requested_jdk_major`, `java_runtime_version`, `java_platform_signatures_used`, `java_platform_signature_source`, and `java_platform_signature_unavailable_reason` to show which JDK major was requested and whether JDK platform signatures contributed to type enrichment.
* `source_nodes` can span multiple wrapper hops. A local `PARAMETER` node may contain nested upstream provenance such as `PARAMETER -> PARAMETER -> VALUE`, and propagated nested nodes keep `location.file_path` plus `location.line` when known.
* Method-call provenance is preserved as `CALL_RESULT` nodes. When the parser can resolve the invoked method, the node also exports `call_target`, and any traceable receiver value is nested under that `CALL_RESULT` via `source_nodes` (for example `CALL_RESULT -> PARAMETER alg -> VALUE SignatureAlgorithm.HS256`).
* `finding_graphs[].reachability` is `not_applicable` for 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-reachability` treats the target as an application instead: its own source packages become the user code, the same set a `--scan-dependencies` run 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, reads `unreachable` here because the dependency's code is not in the graph; `--scan-dependencies` follows it. A scan that resolved dependencies ignores the flag, and `--export-graph-fragment` is unaffected.
* A finding whose rule carries `parameter_conditions` keeps 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 reads `unreachable` (`reachable: false`, no `call_chains`): the crypto the rule describes runs on no known route, and the graph fragment's `crypto_entry_points` do not list it either. When the chain budget (`--export-callgraph-max-chains`) left a route or a call site unexamined, it reads `unknown` with `unresolved_reason: "traversal_truncated"` and `analysis.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` (`direct` when the application declares it, `transitive` otherwise) and `path`, its shortest route from the application in the resolved dependency graph, not in the call graph. `path` lists `module`, `version` and `purl` per dependency, a direct dependency first and the finding's own dependency last; the application itself is `scan_metadata.root_module` and 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, so `lodash` 4.17.21 can be `direct` while the copy nested under another package is `transitive`, and each path step names its own `version`. Without one, `relationship` and `path` are 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; `purl` and `version` stay. `relationship` and `path` are 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 no `dependency` block; its top-level `purl` keeps 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 schema `6.17`. The inlined `6.14` compatibility render carries it too, and `schemas/callgraph-schema.json` declares 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 `path` carries `without_source: true`. When every route from the application to a finding's dependency crosses one and no chain reaches the finding, the finding reads `unknown` with `unresolved_reason: "dependency_without_source"` and `analysis.call_chains: "partial"`, instead of `unreachable`: 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_location` and `unresolved_reason`. `no_containing_function` means the finding's language was analyzed and no function encloses it; `no_crypto_call_match` means no call at the finding's position matched. `language_not_analyzed` means 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; its `reachability` is `not_applicable` because 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-reachability` is set, and `not_applicable` otherwise.
* `crypto_entry_points[]` is the stitch/API index. Each entry carries `function_key`, canonical/display symbols, aliases, and `reachable_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 inflate `finding_graphs[]`. As of `6.5`, `role: operation` contract methods (the calls where the cryptographic computation actually executes, e.g. block-processing/finalization methods) are also exported here with a category and referenced via `supporting_call_ids`.
* Constructor joins remain canonical (``), while display fields and aliases expose IBM-style names such as `com.acme.Factory.Factory`.
* `entry_point_index` is not emitted by schema `6.0`. Consumers should migrate to `crypto_entry_points[]`.
### Compatibility and consumer gating
The published JSON Schemas are [`schemas/interim-report-schema.json`](https://github.com/scanoss/crypto-finder/blob/main/schemas/interim-report-schema.json), [`schemas/callgraph-schema.json`](https://github.com/scanoss/crypto-finder/blob/main/schemas/callgraph-schema.json) (inlined `6.14`), [`schemas/callgraph-schema-6.15.json`](https://github.com/scanoss/crypto-finder/blob/main/schemas/callgraph-schema-6.15.json) (interned `6.15`), [`schemas/callgraph-schema-6.16.json`](https://github.com/scanoss/crypto-finder/blob/main/schemas/callgraph-schema-6.16.json) (interned `6.16`), and [`schemas/callgraph-schema-6.17.json`](https://github.com/scanoss/crypto-finder/blob/main/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) or `schema_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.md` in the same change.
Example (`--export-callgraph-interned-frames=false`, schema `6.14`; identity is inlined on frames and also in `functions[]`):
```json theme={null}
{
"functions": [
{
"function_name": "io.jsonwebtoken.jjwtfun.controller.SecretsController.traceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java",
"start_line": 33
},
{
"function_name": "io.jsonwebtoken.jjwtfun.service.SecretService.issueTraceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/service/SecretService.java",
"start_line": 72
},
{
"function_name": "org.springframework.security.core.token.Sha512DigestUtils.getSha512Digest",
"file_path": "org/springframework/security/core/token/Sha512DigestUtils.java",
"start_line": 43,
"dependency_info": {
"module": "org.springframework.security:spring-security-core",
"version": "5.7.11"
}
}
],
"finding_graphs": [
{
"finding_id": "69669f02",
"call_chain_indexes": [[0, 1, 2]],
"call_chains": [
[
{
"function_name": "io.jsonwebtoken.jjwtfun.controller.SecretsController.traceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java",
"start_line": 33
},
{
"function_name": "io.jsonwebtoken.jjwtfun.service.SecretService.issueTraceToken",
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/service/SecretService.java",
"start_line": 72,
"entry_call": {
"file_path": "src/main/java/io/jsonwebtoken/jjwtfun/controller/SecretsController.java",
"line": 34,
"parameters": [
{
"parameter_index": 0,
"type": "io.jsonwebtoken.SignatureAlgorithm",
"argument_expression": "SignatureAlgorithm.HS256",
"resolved_value": "SignatureAlgorithm.HS256"
}
]
}
},
{
"function_name": "org.springframework.security.core.token.Sha512DigestUtils.getSha512Digest",
"file_path": "org/springframework/security/core/token/Sha512DigestUtils.java",
"start_line": 43,
"dependency_info": {
"module": "org.springframework.security:spring-security-core",
"version": "5.7.11"
},
"crypto_call": {
"function_name": "java.security.MessageDigest.getInstance",
"line": 45,
"parameters": [
{
"parameter_index": 0,
"type": "String",
"argument_expression": "\"SHA-512\"",
"resolved_value": "\"SHA-512\""
}
]
}
}
]
]
}
]
}
```
### Example output
A finding matched by a single rule:
```json theme={null}
{
"version": "1.1",
"tool": {
"name": "crypto-finder",
"version": "0.1.0"
},
"findings": [
{
"file_path": "src/crypto/Example.java",
"language": "java",
"cryptographic_assets": [
{
"start_line": 29,
"end_line": 29,
"match": "cipher = Cipher.getInstance(\"AES/CBC/PKCS5Padding\");",
"rules": [
{
"id": "java.crypto.cipher-aes-cbc",
"message": "AES cipher usage detected",
"severity": "INFO"
}
],
"status": "pending",
"metadata": {
"assetType": "algorithm",
"algorithmFamily": "AES",
"algorithmPrimitive": "block-cipher",
"algorithmMode": "CBC",
"algorithmPadding": "PKCS5Padding"
}
}
]
}
]
}
```
A finding matched by several rules, deduplicated:
```json theme={null}
{
"version": "1.1",
"tool": {
"name": "crypto-finder",
"version": "0.1.0"
},
"findings": [
{
"file_path": "src/crypto/cipher.go",
"language": "go",
"cryptographic_assets": [
{
"start_line": 42,
"end_line": 42,
"match": "cipher.NewGCM(block)",
"rules": [
{
"id": "go-crypto-aes-gcm",
"message": "AES-GCM encryption detected",
"severity": "INFO"
},
{
"id": "go-crypto-authenticated-encryption",
"message": "Authenticated encryption pattern detected",
"severity": "INFO"
}
],
"status": "pending",
"metadata": {
"assetType": "algorithm",
"algorithmFamily": "AES",
"algorithmPrimitive": "ae",
"algorithmParameterSetIdentifier": "256",
"algorithmMode": "GCM"
}
}
]
}
]
}
```
### Use cases
Use interim JSON to integrate with the SCANOSS platform, to feed custom analysis pipelines, to track cryptographic assets in detail, and for security auditing and compliance.
## Graph fragment export
With `--export-graph-fragment `, Crypto Finder writes a
**reusable structural graph fragment** for the scanned component. The fragment holds
the component's call graph plus crypto annotations versioned by rules. The finding-centric
call graph export above describes one scan. A fragment is different: you compose it 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):
| Version | Change |
| - | - |
| `1.1` | Per-edge resolution metadata (`resolution`, `declared_type`, `method_name`, `arity`). |
| `1.2` | Per-edge call-site data-flow (`entry_call`) and full crypto-call identity + asset metadata on `crypto_annotations`. |
| `1.3` | Customer-facing reachability projections: `crypto_entry_points`, `supporting_calls`, display aliases for constructor symbols. |
| `1.4` | Call-site object identity (`ReceiverVar`/`AssignedVar`/`ChainID`) and match columns on annotations/supporting calls, enabling cache-side object-lifecycle re-derivation. |
| `1.5` | `supporting_calls`, `crypto_entry_points`, `graph_algo_version` exposed end-to-end (paired with callgraph schema `6.2`). |
| `1.6` | Optional `resolved_receiver_type` on internal edges and external calls, which lets the stitcher disambiguate interface-dispatch groups with more than one candidate in closure. |
| `1.7` | `internal_edges_compact` + `internal_edge_strings`, a string/key-indexed compact edge encoding to keep large dependency fragments small. |
| `1.8` | `role: operation` contract methods exported as categorized supporting calls, no longer as operation-only entry points (paired with callgraph schema `6.5`). |
| `1.9` | Generic-erased function join signatures for Java source-type hierarchy stitching and optional direct-finding `purl` on crypto annotations, carried into rendered findings envelopes and finding graphs. |
| `1.10` | Optional `occurrence_key` on canonical `crypto_annotations`, propagated to stitched callgraph and findings-envelope outputs. |
| `1.11` | Optional `supporting_calls[].supporting_call.resolved_key_length` raw key-bit evidence for structurally derived configuration calls, including provenance and a source-call parameter reference, preserved to stitched callgraph output. |
| `1.12` | Optional `rule_declared_bits` and `rule_conflict` on that key-length evidence, marking a rule-declared key length that disagrees with the resolved value without overwriting either side. |
| `1.14` | Optional `functions[].entry_kind` (`main` or `framework_entry`) on a function the producing scan recognized as an entry point, and `scan_metadata.entry_kinds`, which says the scan recorded entry kinds at all. The stitched callgraph export reads them to write `root_kind` and `analysis.no_callers_only`. A fragment without `scan_metadata.entry_kinds` (`1.13` and older) still loads; its roots get no `root_kind` and its findings never claim `no_callers_only`. |
### Structure
| Field | Description |
| - | - |
| `schema_version` | Fragment schema version (currently `graph-fragment-1.14`). |
| `scan_metadata` | Ecosystem, root module, tool/rules versions, `graph_algo_version` (callgraph-construction algorithm version; cache key for annotate-only re-annotation), and per-array counts. |
| `functions[]` | Callable nodes. `key` is the stable function identity (`pkg.(Type).name#arity`); also carries `file_path`, `package`, `type`, `name`, signature, etc., and (1.14+) `entry_kind` when the scan recognized the function as an entry point. |
| `internal_edges[]` | Caller→callee edges **within** the component (both functions are in this fragment). Each edge may carry `entry_call` (1.2+, see below). |
| `internal_edges_compact[]` / `internal_edge_strings[]` | Compact (1.7+) encoding of internal edges: same fields as `internal_edges`, with repeated strings and function keys indexed into `internal_edge_strings` to keep large dependency fragments small. |
| `external_calls[]` | Calls whose target may live in **another** component; resolved at stitch time against the dependency tree. Each edge may carry `entry_call` (1.2+, see below). |
| `crypto_annotations[]` | Terminal crypto findings attached to a function. Beyond `function_key`/`finding_id`/optional `occurrence_key`/`rule_id`/`symbol`, a 1.2+ annotation carries the data-flow and metadata needed to reconstruct a findings entry (see *Crypto annotation fields (1.2+)* below). A component with no crypto still emits a fragment (zero `crypto_annotations`) so it can serve as a bridge in transitive chains. |
| `supporting_calls[]` | Non-finding config/lifecycle/context calls useful for explaining crypto behavior without increasing finding counts. |
| `crypto_entry_points[]` | Canonical reachability index: API functions plus display aliases and links to reachable findings/supporting calls. |
### 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. It uses the same model as the
finding-centric call graph export (see *Call graph export* above).
Each `entry_call.parameters[]` entry has `parameter_index`, best-effort `type`,
`argument_expression`, `resolved_value`, `variable_name` (simple identifiers
only), and recursive `source_nodes` provenance. Because the data travels **on the edge**,
the stitcher can rebuild the full per-frame value flow when it composes chains
across components, and a stitched chain matches a live run frame for frame.
### Crypto annotation fields (1.2+)
A `graph-fragment-1.2+` `crypto_annotations[]` entry carries enough to
reconstruct a full findings.json entry for the matched crypto call:
| Field | Description |
| - | - |
| `crypto_call` | Identity and call-site argument data-flow of the matched crypto call (`function_name`, `line`, `parameters[]` — same parameter model as `entry_call`). |
| `oid` | Object Identifier for the cryptographic algorithm, when known. |
| `metadata` | Raw asset metadata block from the scanner. |
| `source` | How the finding was discovered: `direct` or `indirect`. |
| `matched_operation` | Kind / symbol / `expression` of the matched crypto operation. |
| `end_line` | Last source line of the crypto finding (often equal to its start line). |
| `match` / expression | The exact source expression that triggered the detection. |
### 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 inlined `6.14`; `ScanMeta.InternedFrames` selects `6.17`). Align the render and sample budget explicitly when comparing with local CLI exports. Dep-component findings get
`module@version/`-prefixed `finding_id`s, matching live output.
* **`ToFindingsEnvelope(root, deps, fragments, meta)`** reconstructs the
findings.json v1.7 envelope (asset metadata, including direct `purl`). Its `finding_id`s are computed
with the **same inputs** as `ToCallgraphExport`, so the two agree: consumers
join assets (envelope) to call chains (callgraph) by `(finding_id, occurrence_key)` when the key is present,
or by `finding_id` for legacy records without `occurrence_key`.
`pkg/graphfrag/equiv` is a semantic diff tool. It asserts that a stitched callgraph
equals a live one, minus the chains that resolution suppression drops on purpose
(see below). The renderers rely on that equivalence guarantee.
### Edge resolution metadata (v1.1+)
Every `internal_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.
| Field | Description |
| - | - |
| `resolution` | How the target was resolved: `exact`, `interface_dispatch`, or `name_only`. Absent ⇒ treat as unresolved/untrusted. |
| `declared_type` | The static/interface type at the call site (e.g. the interface whose method was dispatched). Present on dispatch edges. |
| `method_name` | The invoked method name, independent of the resolved target. |
| `arity` | The argument count of the call. |
| `resolved_receiver_type` | (1.6+) Concrete receiver type resolved by the producer's contracts-KB / return-type inference for an interface-dispatch call site, when available. Lets the stitcher disambiguate a dispatch group with more than one candidate; empty ⇒ fail-closed behavior unchanged. |
`resolution` values:
* **`exact`**: the receiver's static type was known, and the method resolved to
a unique declared target on that type, or to an overload set on that exact type.
* **`interface_dispatch`**: the target was found by expanding an
interface or abstract method to the concrete implementations that match its name and arity
within a namespace root. It is trustworthy only when exactly one implementation is
present in the dependency closure. Otherwise it is an ambiguous guess.
* **`name_only`**: the target was guessed from the method name and arity, plus
namespace heuristics, with no receiver-type anchor. A fluent-chain fallback is one example.
`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 policy 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
```json theme={null}
{
"schema_version": "graph-fragment-1.14",
"scan_metadata": {
"ecosystem": "java",
"root_module": "org.bouncycastle:bcpkix-jdk18on",
"graph_algo_version": "graph-algo-7",
"function_count": 4000,
"internal_edge_count": 6417,
"external_call_count": 9469,
"crypto_operation_count": 160,
"supporting_call_count": 12,
"crypto_entry_point_count": 42
},
"functions": [
{
"key": "org.bouncycastle.pkcs.(PKCS8EncryptedPrivateKeyInfo).decryptPrivateKeyInfo#1",
"file_path": "org/bouncycastle/pkcs/PKCS8EncryptedPrivateKeyInfo.java"
}
],
"external_calls": [
{
"caller_key": "org.bouncycastle.pkcs.(PKCS8EncryptedPrivateKeyInfo).decryptPrivateKeyInfo#1",
"target_key": "org.bouncycastle.operator.(InputDecryptorProvider).get#1",
"line": 90,
"resolution": "exact",
"method_name": "get",
"arity": 1,
"entry_call": {
"file_path": "org/bouncycastle/pkcs/PKCS8EncryptedPrivateKeyInfo.java",
"line": 90,
"parameters": [
{
"parameter_index": 0,
"type": "org.bouncycastle.operator.InputDecryptorProvider",
"argument_expression": "inputDecryptorProvider"
}
]
}
},
{
"caller_key": "org.bouncycastle.pkcs.(PKCS8EncryptedPrivateKeyInfo).decryptPrivateKeyInfo#1",
"target_key": "org.bouncycastle.cms.(RecipientInformationStore).get#1",
"line": 90,
"resolution": "interface_dispatch",
"declared_type": "org.bouncycastle.operator.InputDecryptorProvider",
"method_name": "get",
"arity": 1
}
],
"supporting_calls": [
{
"supporting_id": "cfg123",
"function_key": "org.example.(Builder).configure#0",
"category": "config",
"matched_operation": {
"kind": "call",
"symbol": "org.example.Builder.withParameter"
}
}
],
"crypto_entry_points": [
{
"function_key": "org.example.(Facade).encrypt#1",
"function_name": "org.example.Facade.encrypt",
"display_symbol": "org.example.Facade.encrypt",
"reachable_findings": [
{
"finding_id": "abc123",
"chain_depth": 3,
"finding_graph_ref": "abc123"
}
],
"reachable_supporting_calls": [
{ "supporting_id": "cfg123", "chain_depth": 2 }
]
}
],
"crypto_annotations": [
{
"function_key": "org.bouncycastle.asn1.pkcs.(EncryptedPrivateKeyInfo).getEncryptedData#0",
"finding_id": "abc123",
"symbol": "getEncryptedData",
"source": "direct",
"end_line": 142,
"match": "getEncryptedData()",
"oid": "1.2.840.113549.1.5.13",
"matched_operation": {
"kind": "decrypt",
"symbol": "getEncryptedData",
"expression": "getEncryptedData()"
},
"crypto_call": {
"function_name": "org.bouncycastle.asn1.pkcs.EncryptedPrivateKeyInfo.getEncryptedData",
"line": 142,
"parameters": []
}
}
]
}
```
In this slice, `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
The CycloneDX output is a Cryptography Bill of Materials compatible with CycloneDX 1.7, for standard reporting.
### Features
The output validates against the CycloneDX 1.7 specification and works with CycloneDX ecosystem tools. Crypto Finder maps each cryptographic asset to a standard component type, including the 1.7 `key-wrap` primitive. Each component includes algorithm properties, evidence, and provenance.
### Supported asset types
| Type | Description |
| - | - |
| `algorithm` | Cryptographic algorithms (AES, RSA, SHA-256, etc.) |
| `certificate` | Digital certificates and certificate chains |
| `protocol` | Cryptographic protocols (TLS, SSH, etc.) |
| `related-crypto-material` | Keys, seeds, nonces, and other crypto material |
### Example output
```json theme={null}
{
"bomFormat": "CycloneDX",
"specVersion": "1.7",
"version": 1,
"metadata": {
"timestamp": "2025-01-15T10:00:00Z",
"tools": [
{
"vendor": "SCANOSS",
"name": "crypto-finder",
"version": "0.1.0"
}
],
"component": {
"type": "application",
"name": "scanned-project"
}
},
"components": [
{
"type": "cryptographic-asset",
"name": "AES",
"cryptoProperties": {
"assetType": "algorithm",
"algorithmProperties": {
"primitive": "block-cipher",
"mode": "CBC",
"padding": "PKCS5Padding"
}
},
"evidence": {
"occurrences": [
{
"location": "src/crypto/Example.java:29"
}
]
}
}
]
}
```
### Converting formats
Use the `convert` command to turn interim JSON into CycloneDX:
```bash theme={null}
# Convert from file
crypto-finder convert results.json --output cbom.json
# Convert from stdin (pipe from scan)
crypto-finder scan /path/to/code | crypto-finder convert --output cbom.json
# Direct output during scan
crypto-finder scan --format cyclonedx --output cbom.json /path/to/code
```
### Integration
Dependency track systems, Software Bill of Materials (SBOM) aggregators, security scanning platforms, compliance reporting tools, and supply chain risk management systems can all read CycloneDX CBOM output.
## Format comparison
| Feature | Interim JSON | CycloneDX CBOM |
| - | - | - |
| **Ecosystem** | SCANOSS-specific | Industry standard |
| **Detail level** | High (findings metadata, code snippets) | Medium (structured metadata) |
| **File size** | Larger | Smaller |
| **Best for** | Deep analysis, custom tooling | Compliance, integration, reporting |
| **Schema** | SCANOSS interim spec | CycloneDX 1.7 |
| **Validation** | SCANOSS tools | CycloneDX validators |
# Overview
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/overview
[Crypto Finder](https://github.com/scanoss/crypto-finder) is a CLI tool that detects cryptographic algorithm usage in source code repositories. It scans codebases with one of several scanning engines and writes results in standard formats, including JSON and CycloneDX.
## Why inventory cryptography
Most teams can't say with confidence which algorithms, certificates, protocols, and keys their code uses. That inventory matters for several reasons:
* Standards such as **PCI-DSS**, and frameworks published by **NIST** such as SP 800-175B, require an accurate cryptographic inventory.
* Outdated or weak cryptographic implementations introduce exploitable vulnerabilities.
* Advances in quantum computing will make many current encryption schemes obsolete.
* Manual audits are slow and inconsistent, and they don't scale to large or fast-changing codebases.
Crypto Finder automates cryptographic discovery, so teams can see what cryptography their code contains and keep it secure.
## What Crypto Finder does
Crypto Finder runs OpenGrep by default and also supports Semgrep, which adds taint analysis. It fetches curated rules from the SCANOSS API and caches them locally. You can combine those remote rules with your own local rules, and configure Crypto Finder with CLI flags, environment variables, or config files.
Results come out as interim JSON or as a CycloneDX 1.7 CBOM. Docker images let you run Crypto Finder in GitHub Actions, GitLab CI, Jenkins, and other CI/CD systems.
Dependency scanning detects cryptographic usage in third-party dependencies and traces the call chains that reach it. It supports Go, Java through Maven or Gradle, Python, Rust, and Node through npm.
The rules cache has a time-to-live (TTL). If the cache has expired and the API can't be reached, Crypto Finder falls back to the stale cache automatically. Pass `--strict` to turn that fallback off.
When only the ruleset has changed, the `annotate` command re-runs detection against a cached graph fragment. It skips the call graph rebuild, which is the expensive part of a scan.
## How it works
1. You point `crypto-finder` at a source code repository.
2. Crypto Finder detects the programming languages in it and fetches the matching cryptographic detection rules.
3. OpenGrep or Semgrep scans the code for cryptographic patterns using those rules.
4. Crypto Finder writes the results to a CycloneDX CBOM or a JSON file.
```mermaid theme={null}
graph LR
SRC["Source Code Repository"] --> CF
subgraph CF["Crypto Finder"]
LD["Language Detection"]
SCN["Scanner Engine (OpenGrep / Semgrep)"]
RP["Result Processor"]
LD --> SCN --> RP
end
CF --> OUT["Output"]
OUT --> FINAL["JSON / CycloneDX CBOM"]
%% --- Styles ---
style SRC fill:#fff,stroke:#081320,stroke-width:2px,color:#081320
style CF fill:#FFF3EA,stroke:#FF6A1A,stroke-width:2px,color:#081320
style LD fill:#FF6A1A,stroke:#E85D0F,stroke-width:2px,color:#081320
style SCN fill:#FF6A1A,stroke:#E85D0F,stroke-width:2px,color:#081320
style RP fill:#FF6A1A,stroke:#E85D0F,stroke-width:2px,color:#081320
style OUT fill:#081320,stroke:#FF6A1A,stroke-width:2px,color:#fff
style FINAL fill:#081320,stroke:#FF6A1A,stroke-width:2px,color:#fff
```
## Dead code filtering (C/C++)
In C and C++ codebases, Crypto Finder automatically excludes cryptographic findings inside statically dead preprocessor regions. These regions hold code that the compiler never includes in the final binary.
### Supported preprocessor patterns
Crypto Finder finds dead regions by evaluating these common preprocessor directives:
| Pattern | Example | Behaviour |
| - | - | - |
| `#if 0` | `#if 0 ... #endif` | Always false, so the whole block is excluded |
| `#ifdef` | `#ifdef UNDEFINED_MACRO` | Excluded when macro is not defined |
| `#ifndef` | `#ifndef DEFINED_MACRO` | Excluded when macro is defined |
| `#elif 0` | `#elif 0` | Subsequent branch treated as dead |
| `#else` | Paired with a live `#if` | Excluded when the preceding branch is live |
| Nested regions | `#if 0 { #if 1 ... } #endif` | Nested conditions are evaluated recursively |
### Why dead code is filtered
Without this filtering, code inside `#if 0` guards, or in platform-specific blocks that are never compiled, would produce false positives. Crypto Finder would report algorithms that the program never uses at runtime.
Crypto Finder applies the filter whenever it detects C or C++ files in the target directory. It needs no configuration.
## Use cases
| Use case | How Crypto Finder helps |
| - | - |
| Security auditing | Lists the cryptographic implementations in a codebase so you can check them against security standards and compliance requirements. |
| Cryptography Bill of Materials (CBOM) | Produces an inventory of cryptographic assets for regulatory compliance, for example NIST SP 800-175B or FIPS 140, and for security assessments. |
| Vulnerability management | Finds deprecated or weak algorithms, such as MD5, SHA-1, and DES, that may introduce security risk. |
| Supply chain security | Tracks cryptographic dependencies and implementations across the software supply chain. |
| Compliance reporting | Writes reports in a standard format, CycloneDX CBOM, that compliance teams and auditors can read. |
## Cryptography service
The SCANOSS Cryptography Service delivers remote cryptographic rulesets through the SCANOSS API.
### Automatic rule fetching
During each scan, Crypto Finder detects the programming languages in the target project and fetches the matching detection rules from the SCANOSS API. It caches the rules locally for up to **7 days**, the cache's time-to-live, so scans depend less on the network and run faster.
### Offline mode
When the SCANOSS API is unavailable, or the environment is air-gapped, Crypto Finder falls back to **offline mode**. It keeps scanning with the most recently cached rules.
### Flow
```mermaid theme={null}
graph TD
A["Crypto Finder Scan Engine"] -->|Online| B["SCANOSS Cryptography API"]
B --> C["Local Cache (7-day TTL)"]
A -->|Offline / Air-Gapped| C
style A fill:#FF6A1A,stroke:#E85D0F,stroke-width:2px,color:#081320
style B fill:#081320,stroke:#FF6A1A,stroke-width:2px,color:#fff
style C fill:#FFF3EA,stroke:#FF6A1A,stroke-width:2px,color:#081320
```
# Quick start
Source: https://docs.scanoss.com/en/latest/cli/crypto-finder/quick-start
Run your first cryptographic scan with Crypto Finder, then configure it and look up its main flags.
## First scan
### Basic scanning
Scan with the remote rulesets, which SCANOSS curates and maintains. This is the recommended setup:
```bash theme={null}
crypto-finder scan /path/to/code
```
Scan with local rules only:
```bash theme={null}
crypto-finder scan --no-remote-rules --rules-dir ./rules /path/to/code
```
Generate a CycloneDX Cryptography Bill of Materials (CBOM):
```bash theme={null}
crypto-finder scan --format cyclonedx --output cbom.json /path/to/code
```
### Common use cases
In CI/CD, fail the build when the scan finds anything:
```bash theme={null}
# Fail the build if findings are detected
crypto-finder scan --fail-on-findings /path/to/code
```
Combine the remote rulesets with your own rules:
```bash theme={null}
# Combine remote rulesets with local custom rules
crypto-finder scan --rules-dir ./custom-rules /path/to/code
```
Force a fresh download of the remote rulesets:
```bash theme={null}
# Bypass the cache and force a fresh download of remote rulesets
crypto-finder scan --no-cache /path/to/code
```
Convert results to a CycloneDX CBOM after a scan:
```bash theme={null}
# Convert existing scan results to CycloneDX CBOM format
crypto-finder convert results.json --output cbom.json
# Or pipe directly from a scan
crypto-finder scan /path/to/code | crypto-finder convert --output cbom.json
```
Re-annotate when the rules changed but the source didn't. Building the call graph, which means parsing and type inference, is the expensive part of a scan, and it doesn't depend on the rules. When only the detection ruleset has changed, `annotate` re-runs detection against a cached graph fragment instead of rebuilding the graph:
```bash theme={null}
# One-time: full scan, cache the structural fragment
crypto-finder scan --export-graph-fragment fragment.json /path/to/code
# Every rules change afterwards: detection-only re-annotation
crypto-finder annotate --import-fragment fragment.json --source /path/to/code --output annotation.json
```
Use `scan` when the source code has changed, and `annotate` when only the rules have changed. On a large library, a re-scan that took several minutes takes a fraction of that time with `annotate`. `annotate` also accepts `--scanner-jobs`, and honors `SCANOSS_SCANNER_JOBS`, for its OpenGrep detection process.
### Configuration
You can configure Crypto Finder with command-line flags, environment variables, or
configuration files.
```bash theme={null}
# Set an API key
crypto-finder configure --api-key YOUR_API_KEY
# Set a custom API URL
crypto-finder configure --api-url https://custom.scanoss.com
```
To use environment variables instead:
```bash theme={null}
export SCANOSS_API_KEY=your-key
export SCANOSS_API_URL=https://custom.scanoss.com
export SCANOSS_SCANNER_JOBS=2 # optional: OpenGrep jobs for scan and annotate
```
For project-level settings, use `scanoss.json`:
```json theme={null}
{
"settings": {
"skip": {
"patterns": {
"scanning": ["node_modules/", "target/", "venv/"]
}
}
}
}
```
### Command-line arguments
```bash theme={null}
crypto-finder scan [flags]
```
Common options:
| Flag | Description |
| - | - |
| `--rules ` | Custom rule file (repeatable) |
| `--rules-dir ` | Rule directory (repeatable) |
| `--no-remote-rules` | Disable remote ruleset fetching |
| `--no-cache` | Force fresh download, bypass cache |
| `--strict` | Fail if the rules cache expired and the API is unreachable (no stale-cache fallback) |
| `--max-stale-age ` | Maximum age for stale cache fallback (default `30d`, max `90d`) |
| `--scanner ` | Scanner to use: `opengrep` (default), `semgrep` |
| `--scanner-jobs ` | Parallel jobs for the primary scan's OpenGrep process. The default `0` uses OpenGrep's own default, one job per detected core. You can also set it with `SCANOSS_SCANNER_JOBS`, and the flag wins. Lower it when several scans share a host. See [Scanner configuration](/en/latest/cli/crypto-finder/configuration#scanner-configuration) |
| `--format ` | Output format: `json` (default), `cyclonedx` |
| `--output ` | Output file path (default: stdout) |
| `--languages ` | Override language detection (comma-separated) |
| `--fail-on-findings` | Exit with error if findings detected |
| `--timeout ` | Scan timeout (default: 10m) |
| `--include-tests` | Include test sources in findings and dependency scans |
| `--no-dedup` | Disable per-line deduplication of findings |
| `--no-default-exclusions` | Disable built-in exclusions (`vendor`, `node_modules`, `shaded/`, generated protobuf stubs, ...). Combine with `--exclude` to re-add specific paths |
| `--exclude ` | Gitignore-style pattern to skip (repeatable), added on top of the defaults |
| `--detect-paths-from ` | Scope detection to the files listed in ``, without shrinking the call graph those files are traced against. The file has one path per line, relative to the target, and `-` reads stdin. See [Scoping detection to changed files](#scoping-detection-to-changed-files) |
| `--scan-dependencies` | Scan third-party dependencies for cryptographic usage (requires deps image or local toolchains) |
| `--dep-ecosystem ` | Dependency ecosystem: `auto` (default), `go`, `java`, `python`, `rust`, `node` |
| `--dep-workers ` | Parallel dependency scan workers. The default `0` uses half of the CPU cores, up to 8, and up to 2 for Java. Concurrent dependency scans share the CPU cores |
| `--include-dev-dependencies` | Also scan npm development-only dependencies, the packages marked `dev` in `package-lock.json`. By default, dependency scans cover the production tree |
| `--no-dependency-findings-api` | Scan every dependency locally instead of using the findings the SCANOSS API publishes for its package version. Applies only when an API key is configured. See [Dependency scanning](/en/latest/cli/crypto-finder/dependency-scanning#step-3-scan-dependencies-in-parallel) |
| `--findings-cache ` | Dependency findings cache backend: `disk` (default), `none`, `postgres` |
| `--export-callgraph ` | Write the finding-centric crypto call graph (reachability slices) to ``. See [Output formats](/en/latest/cli/crypto-finder/output-formats) |
| `--export-callgraph-max-chains ` | Per-finding sampled route budget for `--export-callgraph` (default `8` since v0.28.0; pass `128` for a larger sample) |
| `--export-callgraph-entry-points` | Include the optional full `crypto_entry_points` reverse-reachability index in `--export-callgraph` (default `false` since v0.28.0) |
| `--export-callgraph-interned-frames` | Emit call graph schema `6.17`, with frame identity in `functions[]` (default `true` since v0.28.0; set `false` for the inlined `6.14` schema) |
| `--export-callgraph-format ` | Call graph export format (currently only `json`) |
| `--export-callgraph-project-reachability` | When `--scan-dependencies` is off or resolved nothing, classify first-party reachability against the target's own source packages instead of reporting `not_applicable`. Leave this off when scanning a library on its own, where an uncalled function is public API rather than dead code. See [Reachability without dependency resolution](#reachability-without-dependency-resolution) |
| `--export-graph-fragment ` | Write a reusable structural graph fragment to ``, for caching or `annotate` |
| `--export-graph-fragment-format ` | Graph fragment export format (currently only `json`) |
| `--java-jdk-major ` | Java JDK major for Java dependency resolution/type enrichment: `auto`, `8`, `11`, `17`, `21` |
| `--java-jdk-home ` | Java JDK home mapping for explicit Java runtime selection (repeatable) |
| `--java-compiled-artifact ` | Compiled Java artifact used for standalone call graph/type enrichment |
| `--interfile` | Enable cross-file analysis (Semgrep Pro only) |
| `--error-format ` | Terminal error rendering: `text` (default) or `json` |
| `--progress` | Write scan lifecycle JSONL to stderr; findings remain on stdout or `--output` |
| `--verbose`, `-v` | Enable verbose (info-level) logging |
| `--quiet`, `-q` | Error-level logging only |
| `--help` | Display help information |
For a complete list of commands and options, run `crypto-finder --help`.
### Checking your version
```bash theme={null}
crypto-finder version
```
## Advanced topics
### Scoping detection to changed files
Detection, call graph construction, and dependency root discovery all share one matcher, `--exclude`. If you exclude the files a change didn't touch, they also drop out of the call graph. A changed file's own findings can then flip from reachable to unreachable, because their callers were excluded too.
`--detect-paths-from ` scopes only the detection pass. The call graph, reachability, and dependency root discovery still read the whole target, so callers outside the list are still seen and reachability stays correct. `` is a plain text file with one path per line, relative to the target. Pass `-` to read the list from stdin:
```bash theme={null}
git diff --name-only origin/main... > changed.txt
crypto-finder scan --detect-paths-from changed.txt --scan-dependencies --export-callgraph callgraph.json /path/to/repo
# or read the list from stdin
git diff --name-only origin/main... | crypto-finder scan --detect-paths-from - /path/to/repo
```
Listed files still go through `--exclude`, the default exclusions, and `.gitignore`, as in a full scan, so the scope never includes a file that a full scan would skip. The option has these limitations:
* **OpenGrep only.** Semgrep doesn't implement the scoping interface and returns `invalid_arguments`.
* With `--scan-dependencies`, Crypto Finder still scans dependencies in full. The path list scopes only the target's own files.
* Crypto Finder skips a listed file that no longer exists, such as one deleted in the diff, instead of failing. A listed directory, or a path outside the target, is an error.
* An empty or all-deleted list scans nothing. It never falls back to scanning the whole target.
### Reachability without dependency resolution
Without `--scan-dependencies`, or when it resolves no dependencies, crypto-finder has no set of "user packages" to classify reachability against. Every finding, including those in your own first-party code, then reports `not_applicable` instead of `reachable` or `unreachable`. This is a problem for an incremental or CI re-scan that skips dependency resolution on purpose because no manifest or lockfile changed.
`--export-callgraph-project-reachability` builds that set from the target's own source packages instead. First-party findings then get the same `reachable` or `unreachable` verdicts that a full `--scan-dependencies` run would give them. The flag never classifies findings in dependency code, which still need `--scan-dependencies`. It also treats every source file under the target as first-party. A vendored package that the default exclusions don't cover is therefore misclassified as project code instead of being attributed to a dependency.
Leave this flag off when scanning a library on its own. A function with no
caller in that case is public API, not dead code, and treating the library
as a self-contained project would misclassify it as unreachable.
### Features
Crypto Finder supports OpenGrep (recommended) and Semgrep as configurable scan engines. Semgrep adds taint analysis, which tracks data flow across procedures.
In C and C++ codebases, Crypto Finder removes findings inside statically dead preprocessor regions, such as `#if 0`, `#ifdef`, and `#ifndef` blocks, to avoid false positives. It does this automatically whenever it detects C or C++ files, with no extra flags.
Call graph construction and `--export-callgraph` reachability cover C, C++, Go, Java, JavaScript/TypeScript, Python, and Rust. Recursive dependency scanning (`--scan-dependencies`) currently resolves third-party packages for Go, Java (Maven/Gradle), Python (pip), Rust (Cargo), and Node (npm). See [Dependency scanning](/en/latest/cli/crypto-finder/dependency-scanning#supported-ecosystems).
Crypto Finder fetches SCANOSS-maintained rulesets from the SCANOSS API and caches them locally. If the remote is unavailable, it uses the local cache instead. You can combine the remote rulesets with local custom rules, and configure Crypto Finder with CLI flags, environment variables, or a `scanoss.json` configuration file.
Output is JSON or a CycloneDX 1.7 CBOM. Official Docker images run in GitHub Actions, GitLab CI, Jenkins, and other CI/CD platforms.
The remote ruleset cache has a configurable time-to-live (TTL). When the remote is unavailable, Crypto Finder falls back to expired cache entries. Pass `--strict` to turn that fallback off.
# Command parameters and flags
Source: https://docs.scanoss.com/en/latest/core/engine/command-parameters-flags
Reference for the SCANOSS Engine CLI parameters, flags, and environment variables that configure and control a scan.
## Input and mode parameters
| Parameter | Short | Description |
| - | - | - |
| `--wfp` | `-w` | Process TARGET as a `.wfp` fingerprint file, whatever its extension |
| `--hpsm` | `-H` | Enable High Precision Snippet Matching mode (requires `libhpsm.so` installed on the system) |
| `--sbom FILE` | `-s` | Use assets declared in the provided CycloneDX/SPDX 2.2 JSON SBOM to guide component identification |
| `--blacklist FILE` | `-b` | Exclude assets listed in the provided CycloneDX/SPDX 2.2 JSON SBOM from scan results |
| `--force-snippet` | | Force snippet scanning (disables full file matching) |
| `--component HINT` | `-c` | Provide a component name hint to guide scan results |
## Matching and tuning parameters
| Parameter | Short | Description |
| - | - | - |
| `--max-snippets NUM` | `-M` | Search for up to NUM distinct components per file (maximum: 9) |
| `--max-components NUM` | `-N` | Set the maximum number of components returned per match (default: 5) |
| `--tolerance NUM` | `-T` | Set snippet matching tolerance percentage (default: 0.1) |
| `--rank NUM` | `-r` | Set the maximum component rank accepted (default: 11). A lower rank means a closer match to the scanned file |
| `--max-files NUM` | | Set the maximum number of files to fetch during matching (default: 12000) |
| `--min-snippet-hits NUM` | | Set the minimum number of snippet ID hits required for a match (default: 3). Overrides automatic threshold adjustment |
| `--min-snippet-lines NUM` | | Set the minimum number of matched lines required per range (default: 10). Overrides automatic threshold adjustment |
| `--range-tolerance NUM` | | Set the maximum number of non-matched lines tolerated within a range (default: 5) |
| `--ignore-file-ext` | | Ignore file extension during snippet matching (default: honour extension) |
## Output and utility parameters
| Parameter | Short | Description |
| - | - | - |
| `--attribution FILE` | `-a` | Show attribution notices for the components declared in the provided `sbom.json` file |
| `--key KEY` | `-k` | Show the contents of the specified KEY file from the MZ sources archive |
| `--max-file-content-size MB` | | Set the maximum file content size in MB printed by `-k` (default: 50) |
| `--purl MD5` | `-P` | Return the PURLs and versions related to the given file MD5 (JSON output) |
| `--url-hash MD5` | `-C` | Return the details of the component identified by the given URL hash (JSON output) |
| `--snippet-scan WFP` | `-S` | Run a snippet-only scan. Takes a single-file WFP block and returns JSON with candidate file MD5s and their line ranges, filtered by the tolerance set with `-T`. Pass `-` to read the WFP from stdin |
| `--license LICENSE` | `-l` | Display Open Source Automation Development Lab (OSADL) metadata for the given SPDX licence ID |
| `--full-license` | `-L` | Enable full licence report |
| `--flags FLAGS` | `-F` | Set engine scanning flags (see [Flag-based configuration](#flag-based-configuration) below) |
| `--name NAME` | `-n` | Set the database name (default: `oss`) |
| `--debug` | `-d` | Write debugging information to disk (`/tmp`) |
| `--quiet` | `-q` | Suppress JSON output (the engine still writes debugging information to STDERR) |
| `--test` | `-t` | Run engine performance tests |
| `--version` | `-v` | Display the installed version of the SCANOSS Engine |
| `--help` | `-h` | Display help information and exit |
## Flag-based configuration
The `-F` or `--flags` parameter takes a numeric bitmask. The bitmask controls which
data categories the scan output includes or leaves out. You can also set flags in
`/etc/scanoss_flags.cfg` so they apply to every scan without passing them on the
command line.
To combine flags, add their values. For example, to disable both dependencies (4)
and licences (8), pass `-F 12`.
| Flag | Setting |
| - | - |
| 1 | Disable snippet matching (default: enabled) |
| 2 | Enable snippet IDs in output (default: disabled) |
| 4 | Disable dependency data (default: enabled) |
| 8 | Disable licence data (default: enabled) |
| 16 | Disable copyright data (default: enabled) |
| 32 | Disable vulnerability data (default: enabled) |
| 64 | Disable quality data (default: enabled) |
| 128 | Disable cryptography data (default: enabled) |
| 256 | Disable best-match-only filtering (default: enabled) |
| 512 | Hide identified files from output (default: disabled) |
| 1024 | Enable download URL in output (default: disabled) |
| 2048 | Enable path hint logic (default: disabled) |
| 4096 | Disable extended server statistics (default: enabled) |
| 8192 | Disable health layer data (default: enabled) |
| 16384 | Enable increased matching precision (reduced scan speed; default: disabled) |
| 32768 | Enable full licence report (default: disabled) |
## Environment variables
| Variable | Description |
| - | - |
| `SCANOSS_MATCHMAP_MAX` | Set the snippet matching map size (default: 10000) |
| `SCANOSS_FILE_CONTENTS_URL` | Define the API URL endpoint for retrieving source file contents. If this variable is not set, scan output omits source file URLs. |
## Configuration file
To avoid passing flags on every run, write the flag value to:
```
/etc/scanoss_flags.cfg
```
The file holds a single integer, the combined flag value. For example, `12` disables
both dependencies and licences.
## Examples
```bash theme={null}
# Scan DIRECTORY without licence and dependency data
scanoss -F 12 DIRECTORY
scanoss --flags 12 DIRECTORY
# Scan TARGET including SBOM assets
scanoss --sbom my_sbom.json TARGET
# Scan with custom snippet matching parameters
scanoss --min-snippet-hits 5 --min-snippet-lines 15 TARGET
# Scan with custom range tolerance
scanoss --range-tolerance 10 TARGET
# Ignore file extensions during matching
scanoss --ignore-file-ext TARGET
```
# Scanning a file or directory
Source: https://docs.scanoss.com/en/latest/core/engine/scanning-file-or-directory
How to use the SCANOSS Engine to scan a file or directory against the SCANOSS Knowledgebase, and how to read the identification results.
## Overview
The SCANOSS Engine is a command-line tool. It compares files and directories against
the SCANOSS Knowledgebase (KB) to find open-source component matches. It prints results
to `STDOUT` as JSON, with licence, copyright, and component identification data.
The engine runs on the server. It receives WFP (Winnowing FingerPrint) files from
clients through the SCANOSS API's `../scan-direct` path. Each WFP file uses this
format:
```
file=FILE_MD5,FILE_SIZE,FILE_NAME
LINE_NUMBER_1=CRC32_1A,CRC32_1B.....
LINE_NUMBER_2=CRC32_2A,CRC32_2B.....
```
The winnowing algorithm produces each CRC32 value. Each value is a fingerprint of a
specific line range in the file.
Basic syntax:
```bash theme={null}
scanoss [parameters] [TARGET]
```
`TARGET` can be a single file, a `.wfp` fingerprint file, or a directory.
## Scanning procedure overview
The engine gives each scanned file one of three match types:
* None. The engine found nothing in the KB that matches the scanned file.
* Snippet. Part of the scanned file matches part of a known open-source file.
* Full. The whole scanned file matches an existing file in the KB.
Whatever the match type, every scan follows the same five steps:
1. Determine the type of match (full file or snippet).
2. For snippet matches only: identify the most meaningful open-source file (MD5) among multiple candidates containing the matched snippet.
3. Link the matched file with component information, such as URL, purl, and release date. There may be more than one candidate. By default, the engine selects the best match.
4. Query component-level information, such as licence and vulnerabilities.
5. Produce the JSON report.
The sections below describe each step.
## File matching logic
The engine tries to match each scanned file against the KB in this order:
1. URL match. If the file exactly matches a known package archive at a registered
URL, the identification type (`id`) is `"url"`.
2. File match. The engine checks the "file table" for the file's MD5 (`FILE_MD5`).
If the KB has it, the identification type (`id`) is `"file"`. If not, snippet
analysis begins.
3. Snippet match. If neither of the above applies, the engine compares snippets
using the CRC32 fingerprints from the WFP. The identification type (`id`) is
`"snippet"`.
4. Binary match. The engine identifies binary files through binary fingerprinting.
The identification type (`id`) is `"binary"`.
5. No match. If none of the above apply, the identification type (`id`) is
`"none"`.
## Snippet matching in detail
When the engine finds no full file match, it runs snippet analysis. Snippet analysis
takes several steps and trades some match accuracy for processing speed.
### How snippet matching works
Each WFP fragment is a CRC32 value that fingerprints a piece of code. In the KB, each
fragment links to a series of file MD5s. When two WFP fragments share an MD5, that
MD5 gets a "hit". The more hits an MD5 has, the larger the snippet the scanned file
shares with the matched open-source file.
Each CRC32 can link to many MD5s, so the number of comparisons can grow large. To keep
processing time down, the engine handles CRC32s linked to fewer MD5s first. These are
rarer code fragments, so they say more about where the code came from.
### Snippet scanning procedure
1. Pre-processing map. The engine queries the KB for the MD5s linked to each CRC32
and builds a map in this format:
```
CRC32-1, CRC32-1-LINE, CRC32-1-LIST_SIZE, MD5-1...MD5-N
CRC32-2, CRC32-2-LINE, CRC32-2-LIST_SIZE, MD5-1...MD5-N
...
```
Each row records a CRC32, its line number, the number of linked MD5s (`LIST_SIZE`),
and the MD5s themselves.
2. Sort by list size. The engine sorts the map by list size, smallest first, so it
handles the rarer fingerprints first.
3. Build the matchmap. The engine adds each MD5 to a matchmap. If the MD5 is already
in the map, it gets a hit. The matchmap tracks line ranges and holds at most
10,000 MD5s. Processing stops when the engine has handled every CRC32 or the
matchmap is full.
4. Select the largest snippets. The engine selects the MD5s with the most hits, which
are the largest shared snippets. It may select one or more. The next steps run for
each selected MD5.
5. Resolve to components. Each selected MD5 may appear in several repositories, such
as clones, forks, or dependents. The engine resolves these to a list of candidate
components. See [Component ranking logic](#component-ranking-logic) below.
6. Refine line ranges. The engine removes small or spurious line ranges as false
positives and merges overlapping ranges.
### Line range generation
Snippet matches report approximate line ranges, because of how the winnowing
algorithm works. Each matched CRC32 carries two positions:
* The line number is the line in the *scanned* file where the fingerprint starts.
* The OSS line number is the line in the *matching open-source* file where the fingerprint starts.
While it builds the matchmap, the engine groups nearby fingerprints into contiguous
line ranges. When it adds a new fingerprint, one of two things happens:
1. If a line range already exists within a configurable tolerance gap, the engine
extends that range to include the new fingerprint.
2. If no nearby range exists, the engine creates a new range.
After this first pass, the engine runs a final step. It widens the tolerance gap and
merges ranges until there are 10 or fewer. This keeps the JSON report short and keeps
the largest match regions.
For example, an initial pass might produce 14 ranges:
```
144467 Accepted ranges (min lines range = 2):
144470 0 = 62 to 66 - OSS from: 171
144471 1 = 87 to 90 - OSS from: 199
144475 2 = 98 to 98 - OSS from: 125
144477 3 = 108 to 114 - OSS from: 131
144480 4 = 120 to 120 - OSS from: 138
144483 5 = 128 to 134 - OSS from: 141
144486 6 = 136 to 148 - OSS from: 40
144490 7 = 156 to 156 - OSS from: 87
144494 8 = 206 to 209 - OSS from: 171
144496 9 = 233 to 240 - OSS from: 199
144499 10 = 283 to 296 - OSS from: 125
144500 11 = 301 to 310 - OSS from: 141
144503 12 = 313 to 320 - OSS from: 43
144507 13 = 325 to 330 - OSS from: 110
```
The final step then widens the tolerance and merges nearby ranges:
```
144509 Range tolerance: 8
144512 join range 2 with 1
144513 join range 4 with 2
144518 join range 5 with 2
144519 join range 6 with 2
144521 join range 7 with 2
144523 join range 11 with 5
144526 join range 12 with 5
144528 join range 13 with 5
144530 Final ranges:
144533 0 = 62 to 66 - OSS from: 171
144534 1 = 87 to 98 - OSS from: 199
144537 2 = 108 to 156 - OSS from: 131
144539 3 = 206 to 209 - OSS from: 171
144540 4 = 233 to 240 - OSS from: 199
144541 5 = 283 to 330 - OSS from: 125
```
The 14 initial ranges become 6 final ranges, each covering a wider match region. The
`OSS from` value is the starting line in the matching open-source file.
## Component ranking logic
When a file appears in several components or versions in the KB, the engine picks the
best match with these rules:
* Context comes first. The engine can receive context, such as an SBOM (Software Bill
of Materials) passed with `--sbom`. The engine always selects any component that
matches this context and puts it at the top of the candidate list, whatever its
release date.
* Without a context match, the engine picks the oldest component in the KB, the first
one released.
* If two components share a release date, the engine uses the project-level release
date to break the tie.
* The scanning client can pass a component hint, the name of the most recently
detected component. The engine favours files that belong to a component matching
this hint.
The engine keeps candidate components in a fixed-size linked list sorted by date, with
context-matched components always at the front. If the list is full and a
higher-priority component arrives, the engine drops the lowest-priority entry.
This process runs separately for each of the N matched MD5s. The engine then compares
the top-ranked component from each MD5's candidate list and reports the single best
one.
## JSON report
After it selects the best match and its component, the engine queries more
component-level metadata, such as licences and known vulnerabilities. It writes
everything as a JSON result to `STDOUT`.
By default, the output includes only the best match. To see the full candidate list,
the top M components for each of the N matched files, pass the `-F256` flag. By
default, M is 3. The engine sets N at scan time.
# SCANOSS LDB
Source: https://docs.scanoss.com/en/latest/core/scanoss-ldb
An overview of the LDB (Linked-list Database), the headless, read-optimised database engine at the core of SCANOSS. It handles single-key lookups across large amounts of open source data.
## Overview
The LDB (Linked-list Database) is the database engine at the core of SCANOSS. It is
headless, which means it has no user interface or client layer. It is built for
single-key, read-optimised access across large volumes of data. It keeps memory and
disk use low and makes few kernel-level system calls. It stores data in linked lists.
SCANOSS SaaS runs on a distributed infrastructure built around the LDB. Load balancers
spread software analysis requests across distributed LDB data nodes, so the service
handles high volumes and keeps running when a node fails.
## How load balancing works
The load balancer distributes incoming requests across several processing nodes. Each
processing node runs the
[SCANOSS Engine](https://docs.scanoss.com/en/latest/core/scanoss-engine) and queries a
shared LDB Knowledge Base, a read-optimised store of open source component data. The
Engine matches file fingerprints against known open source components. A fingerprint
is a cryptographic hash of a source code snippet.
This architecture gives you:
* Even workload across processing nodes.
* High availability through redundancy.
* Horizontal scaling to handle more requests.
## Example deployment scenario
A typical large enterprise deployment has these layers:
1. An external request endpoint receives incoming scan requests.
2. A load balancer distributes requests across processing nodes.
3. Processing nodes each run the SCANOSS Engine.
4. The LDB Knowledge Base is the shared, read-optimised data layer that all nodes query.
## Architecture diagram
```mermaid theme={null}
graph TD
A[Customer Requests] --> B[Load Balancer]
B --> C[Node 1]
B --> D[Node 2]
C --> E[(LDB Knowledge Base)]
D --> E
style A fill:#fff,stroke:#081320,stroke-width:2px,color:#081320
style B fill:#081320,stroke:#FF6A1A,stroke-width:2px,color:#fff
style C fill:#FF6A1A,stroke:#E85D0F,stroke-width:2px,color:#081320
style D fill:#FF6A1A,stroke:#E85D0F,stroke-width:2px,color:#081320
style E fill:#FFF3EA,stroke:#FF6A1A,stroke-width:2px,color:#081320
```
## Reliability and scalability properties
The architecture is built for:
* Fault tolerance. Redundant processing nodes remove single points of failure.
* Consistent query performance. The LDB layer is read-optimised.
* High availability. The load balancer keeps the service running when a node fails.
* Horizontal scaling. You can add processing nodes without downtime.
## LDB and Engine version compatibility
LDB and the [SCANOSS Engine](/en/latest/core/engine/scanning-file-or-directory) ship as two parallel release lines. You must upgrade them together.
* The traditional line is LDB 4.x and Engine 5.x (for example, tag `v5.5.1`).
* The CRC64-compatible line is LDB 5.x and Engine 6.x and later. Its tags carry a mandatory `-crc64` suffix (for example, `v5.0.0-crc64`, `v6.0.0-crc64`).
A CRC64-line LDB requires an Engine from the CRC64 line, and the reverse is also true. An Engine from before 6.0.0-crc64 cannot read tables built by LDB 5.x-crc64 and doesn't link against its library API. The split covers the on-disk table layout and the library API. It doesn't decide which hash a table uses, so a CRC64-line deployment can still run MD5-hashed tables.
At startup, Engine compares the LDB version it was built against with the LDB version it runs against. On a mismatch it refuses to start, so it can't fail in unpredictable ways later. `ldb -v`, `scanoss -v`, and the JSON `server.version` field all report which line a binary belongs to. Check them when you plan an upgrade.
# Cloud deployment
Source: https://docs.scanoss.com/en/latest/installation/cloud-deployment
SCANOSS offers two managed cloud deployment models, Dedicated SaaS and Shared SaaS. SCANOSS operates both and hosts them in European data centres.
Scanning in every SCANOSS cloud service runs inside the customer environment. Only file fingerprints (cryptographic hashes) and metadata go to the SCANOSS knowledge base for matching. SCANOSS never receives or stores proprietary source code.
SCANOSS designs its cloud services to meet SOC 2 Type II, ISO/IEC 27001 (ISMS), and GDPR requirements for security, availability, and confidentiality.
## Dedicated SaaS deployment
A Dedicated SaaS deployment is a private SCANOSS cloud environment that SCANOSS hosts and manages. It runs on infrastructure provisioned for a single customer, so no other organisation shares its compute resources or API capacity.
You get a fully managed cloud service with stronger isolation and predictable performance.
A Dedicated SaaS deployment has:
* A single-tenant environment. Infrastructure and service capacity are reserved for one organisation.
* Dedicated throughput, with no resource contention from other tenants.
* High availability. Deployments include redundant infrastructure and failover to keep the service running.
* Managed infrastructure. SCANOSS handles maintenance, updates, monitoring, and scaling.
* Hosting in European data centres.
* Access through dedicated API credentials.
* Privacy-preserving scanning. Only fingerprints and metadata go to the SCANOSS knowledge base, and source code stays in the customer environment.
This model suits organisations that need more isolation, predictable performance, and dedicated capacity in a fully managed cloud service.
## Shared SaaS deployment
The Shared SaaS deployment gives access to SCANOSS through a multi-tenant cloud environment that SCANOSS manages. Several organisations use the same service infrastructure, and each one accesses it with its own API keys.
You can add SCANOSS to your workflows at lower cost, without deploying or operating infrastructure.
A Shared SaaS deployment has:
* A multi-tenant environment. Several organisations share the same cloud service infrastructure.
* API-based access. Each customer uses its own API keys.
* Shared throughput. Tenants share a common pool of system resources.
* High availability. Redundant systems and failover keep the service running if a component fails.
* A fully managed service. SCANOSS manages infrastructure, updates, scaling, and monitoring.
* Hosting in European data centres.
* Privacy-preserving scanning. Only fingerprints and metadata go to SCANOSS for matching, and source code stays in the customer environment.
This model suits organisations that want a lower-cost, fast way to add SCANOSS to their development or security workflows.
# API configuration
Source: https://docs.scanoss.com/en/latest/installation/on-prem/configuration/api-configuration
Configure the SCANOSS API service with the application configuration file.
A JSON file at `/opt/scanoss/tmp/scripts/app-config-prod.json` configures the SCANOSS API.
The `APP_DIR` variable in [`config.sh`](https://github.com/scanoss/on-premise/blob/main/install/config.sh) sets this path. If you changed the default installation directory during setup, adjust the path to match.
The file lists every available option from the start. Some fields may be empty. Fill them in as you need them.
## Applying configuration changes
After you change the configuration file, run [`install-scanoss.sh`](https://github.com/scanoss/on-premise/blob/main/install/install-scanoss.sh). Select **5) Install Application**, then **5) API** from the sub-menu. The script restarts the API service with the new configuration.
# API service management
Source: https://docs.scanoss.com/en/latest/installation/on-prem/configuration/api-service-management
Start, stop, and check the status of the SCANOSS API service.
The SCANOSS API runs as a `systemd` service called `scanoss-go-api.service`. The installation script doesn't start the service, so start it yourself after installation.
## Start the service
```bash theme={null}
sudo systemctl start scanoss-go-api.service
```
## Stop the service
```bash theme={null}
sudo systemctl stop scanoss-go-api.service
```
## Check the service status
```bash theme={null}
sudo systemctl status scanoss-go-api.service
```
# Hardware requirements
Source: https://docs.scanoss.com/en/latest/installation/on-prem/deployment-guide/hardware-requirements
Recommended hardware specifications for running SCANOSS on-premise.
Hardware requirements depend on which Knowledge Base you install. During setup, the installer asks you to choose the full SCANOSS KB or the Test KB. The Test KB is for non-production testing only and needs much less storage.
Supported operating systems are Debian 11, Debian 12, and CentOS.
## Full Knowledge Base requirements
| Component | Minimum | Recommended |
| - | - | - |
| **CPU** | 8-core x64, 3.5 GHz | 32-core x64, 3.6 GHz |
| **RAM** | 32 GB | 128 GB |
| **HDD** | 25.0TB SSD (NVMe preferred) | 27.0TB SSD (NVMe preferred) |
## Test Knowledge Base requirements
| Component | Minimum | Recommended |
| - | - | - |
| **CPU** | 8-core x64, 3.5 GHz | 32-core x64, 3.6 GHz |
| **RAM** | 16 GB | 32 GB |
| **HDD** | 700 GB SSD (NVMe preferred) | 1 TB SSD (NVMe preferred) |
# Install applications
Source: https://docs.scanoss.com/en/latest/installation/on-prem/deployment-guide/install-applications
Install SCANOSS system dependencies and applications using the `install-scanoss.sh` script.
The [`install-scanoss.sh`](https://github.com/scanoss/on-premise/blob/main/install/install-scanoss.sh) script installs SCANOSS. It sets up system dependencies and SFTP credentials, then downloads and installs each application component.
## Running the script
```bash theme={null}
sudo ./install-scanoss.sh
```
The script shows this menu:
```bash theme={null}
SCANOSS Installation Menu
------------------------
1) Install Everything
2) Install Dependencies
3) Setup SFTP Credentials
4) Download Application
5) Install Application
6) Quit
Enter your choice [1-6]:
```
For a standard installation, select option **1) Install everything**.
When the installation finishes, start the API service:
```bash theme={null}
sudo systemctl start scanoss-go-api.service
```
## Next steps
With the applications installed, continue to [Install Knowledge Base](/en/latest/installation/on-prem/deployment-guide/install-knowledge-base).
# Install Knowledge Base
Source: https://docs.scanoss.com/en/latest/installation/on-prem/deployment-guide/install-knowledge-base
Download and install the SCANOSS Knowledge Base or Test KB using the `kb-download.sh` script.
The [`kb-download.sh`](https://github.com/scanoss/on-premise/blob/main/install/kb-download.sh) script downloads packages from the SCANOSS SFTP server. It can fetch the full Knowledge Base (KB), a KB update, the test KB, a SQLite KB snapshot, or an HFH KB snapshot. It connects to the server, lists the available versions, and lets you pick a package. Before it downloads, it checks that you have enough free disk space.
A KB update package includes a separate import utility, `ldb-import.sh`. Use it to import the update into your local LDB instance.
## KB download prerequisites
* `lftp` is recommended. It gives faster, parallel, resumable downloads. Install it with `apt install lftp`. If `lftp` isn't available, the script can fall back to the standard `sftp` client. With the `-y` flag, the script falls back to `sftp` without asking.
* `sshpass` is required only for the `sftp` fallback. It passes the password to `sftp` non-interactively. Install it with `apt install sshpass`. You don't need it when you use `lftp`.
* SFTP credentials come from the SCANOSS sales team: host, port, username, and password.
## Running the script
Make sure the script has execution permissions:
```bash theme={null}
chmod +x install/kb-download.sh
```
Run the script without arguments for an interactive session:
```bash theme={null}
./kb-download.sh
```
## Command-line options
You can pass the connection details, paths, and download mode as arguments. The script then skips the prompt for each value you pass:
```bash theme={null}
./kb-download.sh -m update -h sftp.test.xyz -P 12345 -u USER21 -p mypassword
```
**Connection and mode**
| Flag | Description |
| - | - |
| `-m` | Download mode: `full` (full KB), `update` (KB update), `test` (test KB), `sqlite` (SQLite KB snapshot), or `hfh` (HFH KB snapshot) |
| `-h` | SFTP host |
| `-P` | SFTP port |
| `-u` | SFTP username |
| `-p` | SFTP password |
| `-t` | lftp parallel download threads (default: 25) |
| `-d` | Force download tool: `lftp` or `sftp` |
| `-w` | Connection timeout in seconds (default: 15). Applies to the pre-flight connection check and every sftp/lftp connection, so a wrong host or port fails fast instead of hanging. |
| `-C` | Enable SSH transport compression. Off by default. Useful on slow links. On fast links it usually *slows* the transfer, because zlib costs more CPU than it saves in bandwidth for dense payloads like hashes and indexes. |
**Path and version overrides**
| Flag | Description |
| - | - |
| `-V` | KB version (full/update/sqlite/hfh mode); default: latest from `LATEST.txt` on the server |
| `-o` | Destination path. Test mode: test-KB destination. Full mode: `oss` folder destination. (default: `/var/lib/ldb/oss`) |
| `-r` | Full mode only: destination for non-oss items (default: `/tmp/scanoss_kb_full_`) |
| `-D` | Update / sqlite / hfh mode: download base directory. Final path is `/` for update/sqlite and `/.zip` for hfh. Defaults: `/tmp/scanoss_kb_update` for update, current directory for sqlite and hfh |
**Non-interactive flags**
| Flag | Description |
| - | - |
| `-y` | Don't prompt. Use defaults for unspecified values and confirm the download without asking. You must supply the required values (`-m`, `-h`, `-P`, `-u`, `-p`). The script exits with an error if any are missing. |
| `-f` | Force the download even if free disk space is below the required size. Only has an effect with `-y`. Without `-y`, the script asks you to confirm. |
| `-?` | Show help |
Unless you set `-y`, the script prompts for any option you didn't pass on the command line.
## Non-interactive examples
```bash theme={null}
# Defaults (latest version, /var/lib/ldb/oss + /tmp/scanoss_kb_full_):
./kb-download.sh -y -m full -h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# Pin a version, custom paths, force on low disk space:
./kb-download.sh -y -f -m full -V 26.03 \
-o /data/oss -r /data/kb-rest \
-h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# Update mode with a custom download base directory:
./kb-download.sh -y -m update -D /opt/scanoss/updates \
-h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# SQLite KB into the current directory (final path: ./):
./kb-download.sh -y -m sqlite \
-h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# SQLite KB into a specific base directory:
./kb-download.sh -y -m sqlite -D /opt/scanoss/sqlite \
-h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# HFH KB (single .zip; lands at ./.zip):
./kb-download.sh -y -m hfh -h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# Test KB:
./kb-download.sh -y -m test -h sftp.test.xyz -P 12345 -u USER21 -p mypassword
```
## How it works
1. The script prompts for the SFTP host, port, username, and password, or reads them from arguments.
2. You choose what to download: the full KB, a KB update, the test KB, a SQLite KB snapshot, or an HFH snapshot.
3. For full, update, sqlite, and hfh modes, the script connects to the SFTP server, lists every available version of the chosen type, and marks the latest. The test KB has no versions. The server holds one current test KB.
4. For full, update, sqlite, and hfh modes, you choose a version. The default is the latest.
5. The script fetches the metadata and checks for enough free disk space. If there isn't enough, it warns you and asks you to confirm before it continues. For HFH, it reads the size from `ls -l` on the remote `.zip` instead of `metadata.json`.
6. The script downloads the package:
* The full KB goes to two destinations. The `oss` folder, which holds the main LDB data, goes to its own destination (default: `/var/lib/ldb/oss`). The remaining files and folders go to a separate directory (default: `/tmp/scanoss_kb_full_`). You can change both destinations at the prompt.
* An update downloads as one folder to a single directory (default: `/tmp/scanoss_kb_update/`).
* The test KB's `oss` folder goes to a single destination (default: `/var/lib/ldb/oss`). This fills your LDB with the test data for verification.
* A SQLite KB downloads as a version folder to `/` (default base: the current working directory). The folder holds one or more `.sqlite` files and a `metadata.json`. The script downloads every file you have access to in that folder. Different users may see different sets of `.sqlite` files, depending on their SFTP permissions.
* An HFH KB downloads as a single `.zip` archive to `/.zip` (default base: the current working directory). With lftp, the script fetches the file in parallel chunks (`pget -n `). With sftp, it uses a single-stream `get` with the live progress poller. The script doesn't extract the archive. Leave that to your downstream tooling.
7. For updates only, run the `ldb-import.sh` script in the downloaded folder after the download finishes.
## Example sessions
### Update
```bash theme={null}
SCANOSS Knowledge Base Download
================================
Using lftp for downloads (parallel, resumable).
What do you want to download?
1) Full KB
2) KB update
3) Test KB
4) SQLite KB
5) HFH KB
Select [1-5]: 2
SFTP host: sftp.test.xyz
SFTP port: 12345
SFTP username: USER21
SFTP password:
Fetching available update versions...
Available update versions:
-------------------
1) 26.01
2) 26.02
3) 26.03 (latest)
-------------------
Select a update version to download [1-3] (default: 3):
Selected update: 26.03
Download directory [/tmp/scanoss_kb_update]:
Fetching update metadata...
Update size: 5.2G
Free space: 120G (on /dev/sda1)
Disk space OK.
lftp parallel threads [25]:
Download update 26.03 to /tmp/scanoss_kb_update? [Y/n]
Downloading with lftp (25 parallel threads, resumable)...
Update downloaded to /tmp/scanoss_kb_update/26.03
Finished downloading update.
Please run the ldb-import.sh script provided inside the update folder to import into the KB:
/tmp/scanoss_kb_update/26.03/ldb-import.sh
```
### Full KB
```bash theme={null}
./kb-download.sh -m full -h sftp.test.xyz -P 12345 -u USER21
SCANOSS Knowledge Base Download
================================
Using lftp for downloads (parallel, resumable).
SFTP password:
Fetching available full KB versions...
Available full KB versions:
-------------------
1) 26.01
2) 26.02
3) 26.03 (latest)
-------------------
Select a full KB version to download [1-3] (default: 3):
Selected full KB: 26.03
Destination for 'oss' folder [/var/lib/ldb/oss]:
Destination for remaining files/folders [/tmp/scanoss_kb_full_26.03]:
Fetching full KB metadata...
Full KB size: 210G
Free space: 500G (on /dev/sda1)
Disk space OK.
lftp parallel threads [25]:
Download full KB 26.03 to /var/lib/ldb/oss + /tmp/scanoss_kb_full_26.03? [Y/n]
Downloading kb/full/26.03/oss with lftp (25 parallel threads, resumable)...
Downloading kb/full/26.03/md5sum with lftp (25 parallel threads, resumable)...
Downloading kb/full/26.03/26.03_files.md5sum with lftp (25 parallel threads, resumable)...
Full KB downloaded:
oss folder: /var/lib/ldb/oss
other files: /tmp/scanoss_kb_full_26.03
Finished downloading full KB.
```
### Test KB
```bash theme={null}
./kb-download.sh -m test -h sftp.test.xyz -P 12345 -u USER21
SCANOSS Knowledge Base Download
================================
Using lftp for downloads (parallel, resumable).
SFTP password:
Destination for test KB [/var/lib/ldb/oss]:
Fetching test KB metadata...
Test KB size: 180M
Free space: 500G (on /dev/sda1)
Disk space OK.
lftp parallel threads [25]:
Download test KB to /var/lib/ldb/oss? [Y/n]
Downloading kb/test/oss with lftp (25 parallel threads, resumable)...
Test KB downloaded to /var/lib/ldb/oss
Finished downloading test KB.
```
### SQLite KB
```bash theme={null}
./kb-download.sh -m sqlite -h sftp.test.xyz -P 12345 -u USER21
SCANOSS Knowledge Base Download
================================
Using lftp for downloads (parallel, resumable).
SFTP password:
Fetching available sqlite KB versions...
Available sqlite KB versions:
-------------------
1) 26.04 (latest)
-------------------
Select a sqlite KB version to download [1-1] (default: 1):
Selected sqlite KB: 26.04
Download directory [.]:
Fetching sqlite KB metadata...
Sqlite KB size: 1.2G
Free space: 500G (on /dev/sda1)
Disk space OK.
lftp parallel threads [25]:
Download sqlite KB 26.04 to ./26.04? [Y/n]
Downloading kb/sqlite/26.04 with lftp (25 parallel threads, resumable)...
Sqlite KB downloaded to ./26.04
Finished downloading sqlite KB.
```
The downloaded folder holds every `.sqlite` file the SFTP user has access to, and `metadata.json`. Different users may see different sets of files, depending on their permissions.
## HFH KB
```
./kb-download.sh -m hfh -h sftp.test.xyz -P 12345 -u USER21
SCANOSS Knowledge Base Download
================================
Using lftp for downloads (parallel, resumable).
SFTP password:
Fetching available hfh KB versions...
Available hfh KB versions:
-------------------
1) 26.06 (latest)
-------------------
Select a hfh KB version to download [1-1] (default: 1):
Selected hfh KB: 26.06
Download directory [.]:
Fetching hfh KB file size...
Hfh KB size: 35G
Free space: 1.7T (on /dev/sda1)
Disk space OK.
lftp parallel threads [25]:
Download hfh KB 26.06 to ./26.06.zip? [Y/n]
Downloading kb/hfh/26.06.zip with lftp (25 parallel chunks, resumable)...
Hfh KB downloaded to ./26.06.zip
Finished downloading hfh KB.
```
HFH ships as a single `.zip` archive with no `metadata.json`. The disk-space check reads the file size from the remote `ls -l` output. The script doesn't extract the archive. Leave that to your downstream tooling.
## Verifying a KB update
After you import an update, check it by scanning the test WFP files in the update directory:
```bash theme={null}
scanoss-py scan /tmp/scanoss_kb_update/26.03/26.03_test_file.wfp
scanoss-py scan /tmp/scanoss_kb_update/26.03/26.03_test_snippet.wfp
```
If both scans return the expected matches, the import worked.
> An interrupted import can corrupt the existing LDB.
## Next steps
When the Knowledge Base is installed, continue to [Verify installation](/en/latest/installation/on-prem/deployment-guide/verify-installation).
# Prepare the environment
Source: https://docs.scanoss.com/en/latest/installation/on-prem/deployment-guide/prepare-environment
Clone the on-premise repository and configure script permissions before running the SCANOSS installation.
Before you run the installation scripts, clone the [scanoss/on-premise](https://github.com/scanoss/on-premise) repository and give the scripts the right permissions.
> Run `install-scanoss.sh` with root privileges, either with `sudo` or as the root user. `kb-download.sh` doesn't need elevated privileges, so you can run it as a regular user.
## 1. Clone the repository
```bash theme={null}
git clone https://github.com/scanoss/on-premise.git
```
## 2. Navigate to the install directory
```bash theme={null}
cd on-premise/install/
```
## 3. Add execution permissions
```bash theme={null}
chmod +x *.sh
```
## SFTP credentials
During installation, the script asks for SFTP credentials. It uses them to download the SCANOSS application binaries and the Knowledge Base.
The SCANOSS team sends you these credentials before installation. If you don't have them, contact [support@scanoss.com](mailto:support@scanoss.com) before you continue.
## Next steps
With the repository cloned and permissions set, continue to [Install applications](/en/latest/installation/on-prem/deployment-guide/install-applications).
# Verify installation
Source: https://docs.scanoss.com/en/latest/installation/on-prem/deployment-guide/verify-installation
Check that each SCANOSS component works after installation.
The [`test.sh`](https://github.com/scanoss/on-premise/blob/main/install/test.sh) script checks that the SCANOSS Engine, Knowledge Base, and API work after installation.
> Before you run this script, make sure the `resources/` directory has read
> permissions. If it doesn't, run `chmod -R +r /resources`.
## Running the script
```bash theme={null}
./test.sh
```
The script shows this menu:
```bash theme={null}
Starting verification script for SCANOSS...
Select options from the menu to verify different aspects of your environment
----------------------------------------------------------------------------
1) Verify scanning feature
2) Check API service status
3) Check API service metrics
4) Quit
```
Option 1, **Verify scanning feature**, generates a `scan_results.json` file. The file should include a match for the test file in the `/resources` directory.
Option 2, **Check API service status**, should return:
```json theme={null}
{ "alive": true }
```
Option 3, **Check API service metrics**, returns JSON with request counts, performed scans, and related metrics.
If all three checks return the expected output, your on-premise SCANOSS installation is complete.
# KB update guide
Source: https://docs.scanoss.com/en/latest/installation/on-prem/kb-update-guide
How to download and update the SCANOSS on-premise Knowledge Base (KB).
## Download the KB with `kb-download.sh`
`kb-download.sh` is the supported way to download the KB and its updates. It connects to
the SCANOSS SFTP server, lists the available versions, and lets you pick one. It then
checks that you have enough free disk space and downloads the package. It can fetch the
full KB, a KB update, the test KB, a SQLite KB snapshot, or an HFH KB snapshot.
### Update frequency and size
SCANOSS publishes a KB update every month, versioned `YY.MM`, for example `26.01`,
`26.02`, `26.03`. To stay current, run `kb-download.sh` in `update` mode against the same
LDB about once a month. You never need to download the full KB again.
Each monthly update is a small incremental download, not another full snapshot. It holds
only the changes since the previous version. The script reports the exact size before it
downloads anything, for example `Update size: 5.2G` in the sample session below. The size
changes from month to month, depending on how much the upstream knowledgebase changed.
Plan bandwidth and download time around the printed size, not a fixed number. For the
full KB's footprint on disk, see
[Hardware requirements](/en/latest/installation/on-prem/deployment-guide/hardware-requirements).
### Prerequisites
* lftp is recommended. It gives parallel, resumable downloads. Install it with
`apt install lftp`. If lftp isn't installed, the script offers to fall back to the
standard `sftp` client.
* sshpass is needed only for the `sftp` fallback. Install it with `apt install sshpass`.
You don't need it with lftp.
* You need SFTP credentials: host, port, username, and password. The SCANOSS team sends
them by email.
### Get the script
The script is in the SCANOSS on-premise repository. Clone the repository, or pull the
latest changes if you already have it, and make the scripts executable:
```bash theme={null}
git clone https://github.com/scanoss/on-premise.git
cd on-premise/install/
chmod +x *.sh
```
The script is also documented in the on-premise
[install README](https://github.com/scanoss/on-premise/tree/main/install).
### Run it interactively
Run the script with no arguments for a guided session. It prompts for your connection
details, the download mode, and the version:
```bash theme={null}
./kb-download.sh
```
The script asks what to download, then lists the available versions and marks the
latest. For example:
```text theme={null}
SCANOSS Knowledge Base Download
================================
Using lftp for downloads (parallel, resumable).
What do you want to download?
1) Full KB
2) KB update
3) Test KB
4) SQLite KB
5) HFH KB
Select [1-5]: 2
SFTP host: sftp.test.xyz
SFTP port: 12345
SFTP username: USER21
SFTP password:
Fetching available update versions...
Available update versions:
-------------------
1) 26.01
2) 26.02
3) 26.03 (latest)
-------------------
Select a update version to download [1-3] (default: 3):
Selected update: 26.03
Download directory [/tmp/scanoss_kb_update]:
Fetching update metadata...
Update size: 5.2G
Free space: 120G (on /dev/sda1)
Disk space OK.
lftp parallel threads [25]:
Download update 26.03 to /tmp/scanoss_kb_update? [Y/n]
Downloading with lftp (25 parallel threads, resumable)...
Update downloaded to /tmp/scanoss_kb_update/26.03
Finished downloading update.
Please run the ldb-import.sh script provided inside the update folder to import into the KB:
/tmp/scanoss_kb_update/26.03/ldb-import.sh
```
The version list has one entry per month, `26.01`, `26.02`, `26.03`, and marks the
newest `(latest)`. The `Update size: 5.2G` line above the download prompt is that
month's incremental download size. The script checks it against your free disk space.
Read both before you confirm, especially on a metered or slow connection.
> For long downloads such as the full KB, run the script inside a `tmux` session
> so the transfer survives a dropped connection.
> Start one with `tmux new-session -s scanoss`, run the script, then detach with
> `Ctrl+B` `d` and reattach later with `tmux attach -t scanoss`.
### Run it non-interactively
To skip a prompt, pass the matching flag. Add `-y` to accept the defaults and run with no
prompts. With `-y`, the connection flags `-m`, `-h`, `-P`, `-u`, and `-p` are required:
```bash theme={null}
# Defaults (latest version, /var/lib/ldb/oss + /tmp/scanoss_kb_full_):
./kb-download.sh -y -m full -h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# Pin a version, custom paths, force on low disk space:
./kb-download.sh -y -f -m full -V 26.03 \
-o /data/oss -r /data/kb-rest \
-h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# Update mode with a custom download base directory:
./kb-download.sh -y -m update -D /opt/scanoss/updates \
-h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# SQLite KB into the current directory (final path: ./):
./kb-download.sh -y -m sqlite \
-h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# SQLite KB into a specific base directory:
./kb-download.sh -y -m sqlite -D /opt/scanoss/sqlite \
-h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# HFH KB (single .zip; lands at ./.zip):
./kb-download.sh -y -m hfh \
-h sftp.test.xyz -P 12345 -u USER21 -p mypassword
# Test KB:
./kb-download.sh -y -m test -h sftp.test.xyz -P 12345 -u USER21 -p mypassword
```
Common options:
| Flag | Description |
| - | - |
| `-m` | Download mode: `full`, `update`, `test`, `sqlite`, or `hfh` |
| `-h` | SFTP host |
| `-P` | SFTP port |
| `-u` | SFTP username |
| `-p` | SFTP password |
| `-V` | KB version (default: the latest reported by the server) |
| `-D` | Download directory for `update` / `sqlite` / `hfh` modes |
| `-o` | Destination for the `oss` folder (full/test modes; default `/var/lib/ldb/oss`) |
| `-y` | Non-interactive: use defaults and auto-confirm |
| `-?` | Show the full list of options |
Run `./kb-download.sh -?` for every flag, including thread count, connection timeout,
compression, the disk-space override, and path overrides. The full option reference and
more example sessions are in the
[install README](https://github.com/scanoss/on-premise/tree/main/install#command-line-options).
### After downloading an update
A KB update download includes an `ldb-import.sh` helper. After the download finishes, run
it from inside the downloaded folder. It imports the update into your local database
(LDB):
```bash theme={null}
/tmp/scanoss_kb_update/26.03/ldb-import.sh
```
The script prints the exact path to `ldb-import.sh` when it finishes.
> Do not interrupt the import. An interrupted import can corrupt the existing
> LDB.
### Verify the update
Each update ships with test WFP files. After the import, scan them to confirm the update
worked:
```bash theme={null}
scanoss-py scan /tmp/scanoss_kb_update/26.03/26.03_test_file.wfp
scanoss-py scan /tmp/scanoss_kb_update/26.03/26.03_test_snippet.wfp
```
If both scans return matches, the import worked.
**A collate error `E078` means records were dropped.** Since LDB 4.3.0 and 5.0.0-crc64, a collate fails with error `E078` when it has to discard records because a single key holds more than its configured cap. Earlier versions dropped those records and still reported success. If you see `E078` in an import or collate log, run the collate again with a smaller `MAX_RECORD` for the affected table, or contact SCANOSS support.
**Upgrade LDB before importing with collate.** LDB 4.1.12 and 5.0.2-crc64
fix collate bugs that could drop records while still reporting the full
record count. Before 5.0.2-crc64, LDB could discard a record of an adjacent key
whose data was identical. This affected only tables with variable-length
records. Before 4.1.12, LDB could lose a record group whose first record had an
all-zero subkey. Tables collated with an earlier version may be missing records.
Collating again doesn't recover them. Re-import those tables with the fixed
version.
# KB Docker example
Source: https://docs.scanoss.com/en/latest/installation/on-prem/local-kb-docker-example
Build a local SCANOSS Knowledge Base with Docker and test file matching and snippet matching against it.
## Tools included
The image contains three tools:
* `ldb` is the Linked-list database that stores Knowledge Base (KB) data.
* `minr` is the mining tool that downloads and indexes open-source software (OSS) components.
* `scanoss` is the scanning engine that queries the KB.
## Quick start
### 1. Build the Docker image
```bash theme={null}
docker build -t scanoss-stack .
```
### 2. Start the container
```bash theme={null}
# sleep infinity keeps the container running so subsequent commands can be executed against it
docker run -d --name scanoss scanoss-stack sleep infinity
```
### 3. Create a Knowledge Base
```bash theme={null}
# Download and index an OSS component into the mined/ directory
docker exec scanoss minr -d scanoss,webhook,1.0,20200320,BSD-3-Clause,pkg:github/scanoss/webhook \
-u https://github.com/scanoss/webhook/archive/1.0.tar.gz
# Extract snippet fingerprints from the mined/ directory
docker exec scanoss minr -z mined
# Create the version file required by the minr import step
docker exec scanoss bash -c "echo '{\"monthly\":\"25.01\", \"daily\":\"25.01.12\"}' > mined/version.json"
# Import the mined data into the KB
docker exec scanoss minr -i mined/
```
### 4. Download test files
Download and extract the same archive to get a file to scan:
```bash theme={null}
docker exec scanoss curl -sL https://github.com/scanoss/webhook/archive/1.0.tar.gz -o /tmp/test.tar.gz
docker exec scanoss tar -xzf /tmp/test.tar.gz -C /tmp
```
### 5. Scan the original file
An unmodified file should produce a 100% file match against the KB:
```bash theme={null}
docker exec scanoss scanoss /tmp/webhook-1.0/scanoss/github.py | jq
```
Expected output:
```json theme={null}
{
"github.py": [{
"id": "file",
"matched": "100%",
"purl": ["pkg:github/scanoss/webhook"],
...
}]
}
```
### 6. Modify the file
```bash theme={null}
docker exec scanoss bash -c "echo '# modified by user' >> /tmp/webhook-1.0/scanoss/github.py"
```
### 7. Scan the modified file
The modified file should produce a snippet match, because only part of its code matches:
```bash theme={null}
docker exec scanoss scanoss /tmp/webhook-1.0/scanoss/github.py | jq
```
Expected output:
```json theme={null}
{
"github.py": [{
"id": "snippet",
"matched": "97%",
"lines": "1-192",
"oss_lines": "3-194",
"purl": ["pkg:github/scanoss/webhook"],
...
}]
}
```
### 8. Cleanup
```bash theme={null}
docker stop scanoss && docker rm scanoss
```
## End-to-end example
```bash theme={null}
# Build and start
docker build -t scanoss-stack .
docker run -d --name scanoss scanoss-stack sleep infinity
# Create Knowledge Base
docker exec scanoss minr -d scanoss,webhook,1.0,20200320,BSD-3-Clause,pkg:github/scanoss/webhook \
-u https://github.com/scanoss/webhook/archive/1.0.tar.gz
docker exec scanoss minr -z mined
docker exec scanoss bash -c "echo '{\"monthly\":\"25.01\", \"daily\":\"25.01.12\"}' > mined/version.json"
docker exec scanoss minr -i mined/
# Download test files
docker exec scanoss curl -sL https://github.com/scanoss/webhook/archive/1.0.tar.gz -o /tmp/test.tar.gz
docker exec scanoss tar -xzf /tmp/test.tar.gz -C /tmp
# Test file matching (100%)
docker exec scanoss scanoss /tmp/webhook-1.0/scanoss/github.py | jq
# Modify file
docker exec scanoss bash -c "echo '# modified' >> /tmp/webhook-1.0/scanoss/github.py"
# Test snippet matching
docker exec scanoss scanoss /tmp/webhook-1.0/scanoss/github.py | jq
# Cleanup
docker stop scanoss && docker rm scanoss
```
## Scanning your own files
### Copy files into the container
To scan files from your host machine, copy them into the container first:
```bash theme={null}
# Copy a file into the container
docker cp /path/to/your/file.py scanoss:/tmp/file.py
# Scan it
docker exec scanoss scanoss /tmp/file.py
```
### Mount a volume
To scan a directory without copying each file, mount a volume when you start the container:
```bash theme={null}
docker run -d --name scanoss -v $(pwd)/mycode:/code scanoss-stack sleep infinity
docker exec scanoss scanoss /code/myfile.py
```
# Overview
Source: https://docs.scanoss.com/en/latest/installation/on-prem/overview
A step-by-step guide to installing SCANOSS in an on-premises environment.
This section covers how to install, configure, and verify a self-hosted SCANOSS deployment. The installation scripts and resources this guide uses are in the [scanoss/on-premise](https://github.com/scanoss/on-premise) repository.
## TL;DR
The short version:
```bash theme={null}
# 1. Clone the repository
git clone https://github.com/scanoss/on-premise.git
# 2. Add execution permissions to the installation scripts
cd on-premise/install/ && chmod +x *.sh
# 3. Install SCANOSS applications — choose option 1) Install everything
sudo ./install-scanoss.sh
# 4. Run the Knowledge Base script — choose whether to install the Full KB, the Test KB, update an existing KB, a SQLite KB snapshot, or an HFH KB snapshot.
sudo ./kb-download.sh
```
> If you downloaded a KB update, run the `ldb-import.sh` script included in the download folder to import it into your local LDB.
## Installation flow
The diagram below shows the installation steps.
```mermaid theme={null}
flowchart TD
START([Start Installation]) --> PREP[Prepare Environment Set permissions]
PREP --> INSTALL[Run sudo install-scanoss.sh]
INSTALL --> MENU{Installation Menu}
MENU -->|Option 1| ALL[Install Everything Complete automation]
MENU -->|Options 2-5| MANUAL[Manual Step-by-step]
ALL --> DEPS[Install Dependencies Based on OS]
MANUAL --> DEPS
DEPS --> SFTP[Setup SFTP Credentials Username & Password]
SFTP --> DOWNLOAD[Download Applications From SFTP Server]
DOWNLOAD --> APPS[Install SCANOSS Apps Engine, API, LDB, Encoder]
APPS --> API_START{Start API Service?}
API_START -->|Yes| START_API[systemctl start scanoss-go-api.service]
API_START -->|No| KB_DOWNLOAD
START_API --> KB_DOWNLOAD[Run kb-download.sh]
KB_DOWNLOAD --> KB_MENU{Download Mode}
KB_MENU -->|Full KB| FULL_KB[Download Full SCANOSS KB Split destinations Use tmux recommended]
KB_MENU -->|Update| UPDATE_KB[Download KB Update Then run ldb-import.sh]
KB_MENU -->|Test KB| TEST_KB[Download Test KB Small size, for testing]
FULL_KB --> VERIFY[Run test.sh]
UPDATE_KB --> VERIFY
TEST_KB --> VERIFY
VERIFY --> TEST_MENU{Verification Menu}
TEST_MENU --> SCAN_TEST[Test Scanning Feature Check scan_results.json]
TEST_MENU --> API_TEST[Check API Status Expect alive response]
TEST_MENU --> METRICS[Check API Metrics Request counts, etc.]
SCAN_TEST --> SUCCESS
API_TEST --> SUCCESS
METRICS --> SUCCESS[Installation Complete! SCANOSS Ready to Use]
SUCCESS --> END([End])
style START fill:#081320,stroke:#FF6A1A,stroke-width:2px,color:#fff
style SUCCESS fill:#081320,stroke:#FF6A1A,stroke-width:2px,color:#fff
style END fill:#081320,stroke:#FF6A1A,stroke-width:2px,color:#fff
style FULL_KB fill:#FFF3EA,stroke:#FF6A1A,stroke-width:2px,color:#081320
style UPDATE_KB fill:#FFF3EA,stroke:#FF6A1A,stroke-width:2px,color:#081320
style TEST_KB fill:#FFF3EA,stroke:#FF6A1A,stroke-width:2px,color:#081320
style API_START fill:#FFF3EA,stroke:#FF6A1A,stroke-width:2px,color:#081320
```