# 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. The Coding Agent (MCP) preset, with triage left off #### 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. The install snippet for each client, built with your own Earnie address ### 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 Members 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). Audit Log ## 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 SBOMs 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 Integrations 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 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. Signing In ### 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 Welcome to Earnie 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 Install Github App 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. Install GitLab 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 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 Members 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) Starter Templates 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. All Projects 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. Dashboard ## 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**. All seven pull-request check controls in one card, at their defaults 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. The Self-checks list, one row per run, with search, Gate, Producer, and status filters * **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 A blocked run: the violation names the policy, the detections that fired it, and the prompt an agent can act on 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 Setting Policies 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 Triaging Findings 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.