- Status: Project-owned
- Repository Type: cli-tool
The published CLI runs on Node.js >=24.0.0. Bun is used for repository
development, tests, and packaging.
| Command | Contract |
|---|---|
ohrisk scan |
Scan local, archive, or supported public GitHub repository dependency evidence and render a non-failing report unless input preparation or output/write fails. |
ohrisk ci |
Run a scan and exit non-zero when active findings meet --fail-on after waiver handling. |
ohrisk diff <ref> |
Compare findings against a git baseline and classify new, meaningfully changed, and resolved risk. |
ohrisk explain <expr> |
Classify one license expression for the selected profile without scanning a project; --policy applies license-level organization rules only. |
| `ohrisk cache status | prune |
--lockfile <path>selects one supported input;--alldiscovers and merges all supported lockfiles at the selected project root. They are mutually exclusive for scan, CI, and diff. A remote repository scan automatically merges multiple supported inputs at its one selected project root; local, archive, CI, and diff inputs keep the explicit--allopt-in.scan|ci --archive <path>scans a ZIP, TAR, TAR.GZ, or TGZ as a read-only in-memory virtual project. It is mutually exclusive with--lockfileand--workspace-root, may be combined with--all, and is not supported bydiff.scan [repository-url]andscan --repo <url>scan one public GitHub HTTPS repository through a bounded temporary shallow clone. Remote repository input may combine with a safe repository-relative--lockfile, is mutually exclusive with--archive,--workspace-root, and--offline, and is not supported byci,diff, or the GitHub Action input contract.--policy <path>selects a workspace-contained policy file; otherwise.ohrisk.ymlis loaded when present.explain --policy <path>accepts--workspace-root <path>for the inheritance boundary. It reports policy sources as relative paths and never applies package rules because explain has no package identity.--profile saas|distributed-appselects the shipping model, with organization policy overrides applied afterward.--prodnarrows scans to production-relevant dependencies when supported by the input ecosystem.--fail-on unknown|review|high|lowcontrols CI failure threshold.--json,--markdown,--html,--sarif, and--cyclonedxselect report formats.--output <path>writes a report artifact.--output <path>creates missing parent directories one component at a time, resolving and containment-checking every existing component against the project root before creating anything. A rejected output path, including one whose symlink or junction parent points outside the project, returnsREPORT_OUTPUT_PATH_OUTSIDE_PROJECTwithout creating a new file or directory inside or outside the project. Parent symlinks that resolve inside the project remain supported.- A remote repository scan with
--htmland no explicit--outputwrites<repository>-ohrisk.htmlunder the invocation directory. Local and archive HTML scans keep their existing stdout behavior. --submodules ignore|rejectcontrols remote Git submodule gitlinks. The defaultignoremode reports incomplete coverage without fetching submodules;rejectfails on the first submodule path.--language <locale>localizes HTML report chrome and Ohrisk-generated review text.--no-waiversignores local waiver files;--strict-waiversfails on expired or unmatched waivers.--offlineforbids network requests and permits only local or verified cached evidence.--cache-dir <path>,--jobs <1..64>, and--timeout <duration>configure persistent cache location and bounded evidence collection.cache status|prune|clearaccepts--cache-dirand--json;cache prunealso accepts--max-size <bytes>and--max-age <duration>.--registry-url <https-url>,--registry-token-env <name>, and repeatable--allow-host <hostname>configure a private npm-compatible registry without accepting raw token arguments.--allow-hostalso permits an exact matching HTTPS Maven repository already declared by the scanned project; it never invents a repository URL or permits arbitrary Maven hosts.
Archive scanning never extracts files to disk. ZIP support is limited to stored or deflated entries; TAR and compressed TAR support accepts regular files and directories, including bounded PAX/GNU long-name metadata. Nested archives are opaque files. Encrypted, ZIP64, multi-disk, unsupported-compression, special-file, malformed, type-conflicting, path-traversing, and integrity-failing archives are rejected.
The default reader limits are 256 MiB input, 50,000 entries, 4,096 UTF-8 bytes and 64 segments per path, 255 UTF-8 bytes per segment, 50 MiB per entry, 512 MiB total expansion, 128 MiB materialized data, a 200:1 compression ratio for data of at least 1 MiB, and a 30-second work budget. Crossing a limit fails the scan; limits do not silently truncate the project.
Because compressed TAR must be indexed as one bounded stream, TAR.GZ and TGZ also use the 128 MiB materialized-data ceiling for the expanded TAR container.
.ohrisk.yml and .ohrisk-waivers.json inside an archive are untrusted data and
are never auto-loaded. Policy inheritance and waiver loading continue from the
host invocation directory, so host policy and waivers remain authoritative.
Remote repository input accepts only https://github.com/<owner>/<repository>[.git] without
credentials, ports, query strings, fragments, encoded path components, or extra path segments.
Private repositories and alternate Git hosts or protocols are not supported. Ohrisk invokes Git
from PATH without a shell or interactive credential prompt, uses a depth-one single-branch clone, disables
submodule recursion and symlink checkout, inspects the Git tree before checkout, and removes its
owned temporary directory after success or failure.
Symbolic-link entries are never followed. Ohrisk records their safe repository-relative paths, removes their checkout materializations before project discovery, verifies that no symbolic link or unexpected special entry remains, and marks every report as incomplete coverage. A symbolic link cannot supply a lockfile or manifest.
When a repository has no supported input at its root, Ohrisk searches only
inside the validated checkout. It automatically selects one nested dependency
project, or merges every supported input across multiple nested project roots
for a repository-wide scan. For example, a plain Mbed TLS scan selects
docs/requirements.txt, while an Ente-style monorepo keeps mobile, server,
and web inputs in one graph with per-lockfile provenance. Automatic fan-out is
limited to 64 project roots and 128 dependency inputs. --lockfile still narrows
the scan to one explicitly selected repository-relative input. Inputs at each
selected root are merged without preferring one ecosystem and silently omitting
the others. Standalone pyproject.toml manifests participate in automatic
discovery only when their dependency entries are exact name==version pins;
ranges and direct references require a resolved lockfile and are not treated as
concrete automatic inputs. Gradle version catalogs likewise participate only
when every library alias resolves to an exact Maven version; catalogs containing
BOM-managed, rich, or otherwise unresolved versions remain available through an
explicit --lockfile selection, which reports the unsupported entry instead of
silently guessing it.
Dependency-free .csproj files and yarn.lock files without a same-directory
root package.json are likewise ignored as non-concrete automatic-discovery
candidates. Explicit --lockfile selection remains strict and reports the
missing dependency input instead of silently treating it as an empty graph.
SBOM files containing unresolved uppercase @BUILD_VARIABLE@ placeholders are
treated as build templates rather than concrete automatic-discovery candidates.
Absolute, empty-segment, dot-segment, and traversal paths are rejected before
resolution inside the validated temporary checkout.
The pre-checkout tree allows at most 100,000 entries, 100 MiB per blob, 640 MiB total blob
content, 4,096 UTF-8 bytes and 64 segments per path, and 255 UTF-8 bytes per segment. Symbolic
links are skipped without following their targets. Regular files with Windows-reserved names,
unsupported characters or suffixes, overlong segments, or case/Unicode normalization collisions
are excluded through a NUL-delimited literal Git pathspec before checkout and reported as
non-portable paths. Structural traversal, .git segments, and other special entries are rejected.
Submodule gitlinks are skipped without fetching in the
default --submodules ignore mode. Reports include the total skipped count, at most 100 safe
relative paths for each skipped entry type, including non-portable files, and whether each path list was truncated;
--submodules reject instead
fails with the first detected path. Temporary clone storage is capped at 1 GiB and the full
operation uses separate hard stage budgets: two minutes for clone, 30 seconds for tree inspection,
and three minutes for checkout. These are fail-closed limits, not
truncation rules.
When no supported dependency manifest, lockfile, or SBOM exists, NO_SUPPORTED_LOCKFILE reports
that no dependency project was detected and points to ohrisk help scan; it does not print the
entire supported-input catalog. A detected project manifest without a lockfile receives a distinct
message telling the user to add or select a supported lockfile.
Policy and waiver files in the cloned repository are untrusted and are not auto-loaded. The directory where Ohrisk was invoked remains the configuration, waiver, cache, and report-output root. General package-cache, install-tree, and vendored-source evidence from the temporary checkout is disabled. Project-contained source metadata and license files explicitly referenced by a selected Python lockfile local-source record are parser inputs and remain bounded by the validated checkout; lockfile-embedded evidence and the bounded npm/PyPI/Maven/Go/NuGet remote package-evidence pipeline also remain available. Shareable reports and errors redact the temporary checkout path.
--all only merges inputs discovered at the same project root. Package records
are deduplicated by Package URL while the original package identifier and every
contributing lockfile remain available as provenance. Conflicting package
metadata is reported instead of silently replacing the first deterministic
record.
Maven aggregator pom.xml inputs recursively scan project-contained <module>
POMs. Module paths, nesting depth, total module count, file size, cycles, and
missing module POMs fail closed. Child modules inherit matching aggregator
parent properties and dependencyManagement; property expressions in project,
dependency, and managed dependency coordinates are resolved before matching;
exact reactor-internal module
dependencies are excluded from the external package graph. External parent and
imported BOM versions use already available local Maven repository POMs first.
Filesystem scan and ci runs may then fetch at most 32 exact, identity-checked
Maven Central model POMs, each bounded to 2 MiB and eight parent/BOM levels, and
reuse them from the artifact cache in --offline mode. Incomplete model
resolution fails closed. diff, archive inputs, and project-declared or
alternate Maven repositories remain local-only for dependency-model resolution.
Package license evidence may separately fall back to exact-version Maven Central
POMs, explicitly allowed project repositories, and a bounded inherited
parent-POM chain.
diff <ref> --all independently discovers the supported input set in the
current worktree and the baseline git tree, parses each set with the same
ecosystem adapters, and then compares their merged findings. JSON diff output
includes lockfileChanges.current, baseline, added, and removed; terminal
and Markdown output summarize added and removed inputs. JSON also separates
newFindings, changedFindings, and resolvedFindings, while findings
remains the combined new-and-changed threshold set. A baseline with no
supported input is represented as an empty dependency graph instead of forcing
the current lockfile path to exist in that ref.
For requirements.txt, plain pins and entries whose provenance cannot be
resolved are treated as direct dependencies. Inline or following pip-compile
# via annotations restore bounded parent paths when every named parent maps
unambiguously to another pin in the same parsed requirements set. A -r source
marks a direct requirement; a -c constraint alone does not. Cycles, excessive
depth, and excessive path fan-out cannot make graph reconstruction unbounded.
For Cargo.lock, graph traversal is iterative and retains every reachable
crate while storing at most 64 dependency paths per crate. Additional paths are
reported through a dependency_paths_truncated graph diagnostic rather than
expanding path combinations without a bound. For a crates.io registry record,
remote scans preserve the Cargo.lock SHA-256 and fetch the exact .crate only
from static.crates.io; checksum, archive root, package name, package version,
and Cargo.toml identity must all match before license evidence is trusted.
Git, path, alternate-registry, and checksumless crate sources are not fetched.
For modern npm package-lock.json and npm-shrinkwrap.json, graph traversal is
also iterative and retains every reachable package while storing at most 64
dependency paths per package. Additional paths use the same typed truncation
diagnostic instead of expanding path combinations without a bound.
For modern Dart and Flutter pubspec.lock, exact pub.dev hosted records retain
their archive SHA-256 and fixed archive URL. Remote evidence verifies the full
archive digest plus root pubspec.yaml name/version before trusting bounded
root license files. Custom hosted registries, Git/path sources, and hashless
legacy hosted records remain local-only.
For pnpm-lock.yaml, every importer remains a graph root and every reachable
package remains in the result, while each package stores at most 64 dependency
paths. Additional paths emit the same typed truncation diagnostic rather than
materializing combinatorial workspace path fan-out.
For uv.lock, a remote Git package record is retained only when uv's resolved
source ends in a full 40- or 64-hex commit. Ohrisk does not fetch that VCS source
or substitute PyPI evidence for the same package name; the package receives
unavailable evidence and remains an unknown finding until the exact commit is
reviewed. Branches, tags, short revisions, unresolved URLs, and malformed remote
sources fail closed, and rejected-source diagnostics redact credentials and URL
parameters.
For SPDX JSON and RDF SBOMs, dependency graph traversal is iterative and retains
every reachable package while storing at most 64 dependency paths per package,
with path depth bounded at 256 packages and explicit budgets for total stored
paths and stored path segments. Node discovery runs separately from path
storage: every reachable package is discovered and expanded once even when a
path, work, or segment budget is exhausted, so truncation can never remove a
dependency or its embedded evidence from the graph. Every node keeps at least
one deterministic representative path, summarized with a placeholder segment
when the budget is tight, and per-node caps are filled with distinct paths up to
the 64-path limit. Deeper paths are summarized with a placeholder segment, and
limit overruns are reported through the dependency_paths_truncated and
dependency_path_depth_summarized graph diagnostics, so combinatorial path
expansion cannot inflate finding identities or crash the scan. Discovery above
the supported node limit fails closed with a typed
DEPENDENCY_GRAPH_LIMIT_EXCEEDED error instead of silently omitting nodes.
Relationship adjacency is built with set-backed dedup and paths are assigned
parents-before-children, so wide relationship lists and multi-root documents
retain complete parent-path unions without quadratic per-edge work.
For Nix flake.lock, dependency graph traversal uses the same bounded iterative
walk: every reachable input node is retained while storing at most 64 dependency
paths per node, path depth is bounded at 256 nodes, and total traversal work and
stored path segments have explicit budgets. As with SPDX, node discovery is
separate from path storage: budget exhaustion truncates extra paths but never
drops reachable input nodes or their descendants, every node keeps at least one
deterministic path, and discovery above the supported node limit fails closed
with the same typed error. Nodes beyond the depth bound keep a summarized
placeholder path, and limit overruns are reported through the same
dependency_paths_truncated and dependency_path_depth_summarized graph
diagnostics instead of materializing combinatorial input-path fan-out.
Local uv.lock directory sources are resolved from the lockfile location. In a
repository-wide remote scan they may reference another selected project inside
the validated repository checkout, while paths that escape the repository scan
root remain rejected.
uv.lock dependency traversal is iterative and retains every reachable package
while storing at most 64 dependency paths per package. Additional paths are
reported through the same dependency_paths_truncated graph diagnostic used by
Cargo and modern npm graphs, preventing combinatorial path expansion from
inflating finding identities and reports without silently dropping packages.
For Go 1.17 and later, go.mod requirements define the current dependency
graph and adjacent go.sum entries are used only as module ZIP and /go.mod
checksum records.
Older or versionless modules conservatively retain go.sum-only fallback nodes.
Filesystem and checked-out repository scans use bounded .go source imports,
test files, non-default custom build constraints, and standard tool directives
to identify development roots. Requirements from checksum-verified module ZIP
go.mod files or separately checksum-verified fixed-proxy .mod responses
propagate that scope through the selected module graph. A module ZIP without a
usable root go.mod does not contribute an empty requirement set. A module
reachable from production remains production; missing or unverified edges never
downgrade a transitive module to development. This classification controls
--prod without changing license severity semantics.
After local cache, vendor, and contained replacement evidence is exhausted,
Ohrisk may fetch an exact module ZIP or standalone .mod only from the fixed
public Go proxy when the matching artifact-specific h1 checksum exists. It
verifies the complete ZIP checksum and requested module/version root before
trusting root license files, and verifies the separate /go.mod checksum before
using strict requirement edges. Local replacements, checksumless artifacts, and
caller-selected private proxies are not fetched.
For *.csproj, Ohrisk scans direct PackageReference and PackageDownload
items. PackageReference may use a matching literal central PackageVersion;
PackageDownload may use an exact range containing one unconditional
same-file property reference. Conditional, ambiguous, composite, wildcard, and
open version expressions fail closed instead of being guessed.
- JSON, Markdown, HTML, SARIF, and CycloneDX behavior is owned by
docs/report-formats.md. - Scan, diff, and explain JSON documents include
$schemaandschemaVersion; incompatible contract changes require a schema-version change. Schema 3.0 rejects unknown properties, separates diff classifications, and validates typed evidence and dependency-graph diagnostics alongside findings, licenses, policy summaries, waivers, thresholds, and lockfile changes. Explain JSON includes its redacted policy summary and the fixedlicense-onlypolicy scope. .ohrisk-waivers.jsonhas its own closed Draft 2020-12 input contract atschemas/waiver-file.schema.json; the parser and schema both reject unknown root and item fields.- Shareable formats must not expose absolute local project roots, lockfiles, policy paths, cache paths, or credentials.
- Machine-readable IDs, fingerprints, enum values, and paths must remain stable unless the change is documented and tested.
- Finding IDs canonicalize the dependency path set before encoding: exact duplicate paths are removed and the remaining paths are sorted by stable code-unit order, independent of locale and input ordering. Relationship order, lockfile order, graph merge order, and duplicate paths therefore cannot change a finding ID, while adding or removing a dependency path changes the ID because paths are part of the finding identity. Waivers written against the previous raw-order identity keep matching through a legacy ID and fingerprint alias applied only during waiver matching; reports, diffs, and new waivers always use the canonical identity.
Policy inheritance, package exceptions, and registry settings are owned by
docs/policy.md. Cache layout, offline behavior, host allowlists, and token
scoping are owned by docs/cache-and-registries.md and
docs/remote-fetching.md.
- Run
bun run typecheck,bun run lint,bun run format:check, andbun testfor CLI behavior changes. - Run
bun run test:coveragewhen parser, policy, evidence, or report branches change. - Run
bun run verify:releasefor package, bundled Action, schema, documentation, or published CLI surface changes.