# Ohrisk

Ohrisk catches open-source license risk before your PR ships.

It is a local CLI for developers who need a quick answer to questions like:

- Did this dependency bring in AGPL, GPL, BUSL, or unknown license evidence?
- Is the risky package production-relevant or dev-only?
- Which parent package introduced the transitive risk?
- Does the answer change for SaaS versus distributed app usage?

Ohrisk is a risk decision aid, not legal advice. It reports `low`, `review`,
`high`, and `unknown` findings for the selected usage profile.

## Quickstart

Install and run your first scan in under a minute:

```bash
npm install -g ohrisk@1.15.2
cd your-project
ohrisk scan
```

The terminal report shows findings sorted by severity:

```text
Risks: 1 high, 1 review, 1 unknown, 2 low
```

- `high` — replace or escalate before shipping
- `review` — check before shipping under the selected profile
- `unknown` — license evidence is missing or unrecognized
- `low` — known low-risk license expression

Gate a production SaaS build by narrowing to production dependencies and the SaaS usage profile:

```bash
ohrisk scan --profile saas --prod
```

Open a browser-friendly HTML report:

```bash
ohrisk scan --html --output ohrisk-report.html --open
```

Scan a public GitHub repository without keeping a checkout. With `--html` and
no `--output`, the report is written to the directory where you ran Ohrisk:

```bash
ohrisk scan --html https://github.com/0disoft/laqu.git
# writes ./laqu-ohrisk.html
```

Git submodules are not fetched. Their paths are skipped by default and the
report is marked as incomplete; use `--submodules reject` when any submodule
must fail the scan instead.

Symbolic links are never followed. Remote scans remove their checkout entries,
continue with ordinary dependency inputs, and record bounded skipped paths as
incomplete coverage.
Regular files whose names cannot be represented portably on supported systems
are also excluded before checkout and reported as incomplete coverage; path
traversal and repository-control paths still fail the scan.

Remote scans automatically merge supported dependency inputs found at the same
project root. Maven aggregator POMs are followed through bounded, project-contained
`<module>` paths, so a root `packaging=pom` file does not produce an empty success
while its child modules contain the actual dependencies.

Maven Central remains the default remote evidence source. A project-declared
HTTPS Maven repository is contacted only when its exact hostname is allowed by
policy or `--allow-host`; allowing a host does not bypass DNS, socket, redirect,
response-size, checksum, or package-identity validation:

```bash
ohrisk scan --allow-host repo.papermc.io https://github.com/PlayPro/CoreProtect
```

When `--language` is omitted, HTML reports use the operating system's language
when Ohrisk supports it and fall back to English otherwise. Pass `--language`
to make the artifact language reproducible or override the OS setting:

```bash
ohrisk scan --html --language ko --output ohrisk-report.html --open
ohrisk scan --html --language es --output ohrisk-report-es.html --open
ohrisk scan --html --language fr --output ohrisk-report-fr.html --open
ohrisk scan --html --language zh --output ohrisk-report-zh.html --open
ohrisk scan --html --language hi --output ohrisk-report-hi.html --open
ohrisk scan --html --language ja --output ohrisk-report-ja.html --open
ohrisk scan --html --language id --output ohrisk-report-id.html --open
ohrisk scan --html --language tr --output ohrisk-report-tr.html --open
ohrisk scan --html --language ru --output ohrisk-report-ru.html --open
ohrisk scan --html --language de --output ohrisk-report-de.html --open
```

Prefer not to install globally? Use `npx ohrisk scan` instead.

Create a minimal policy and a version-pinned pull-request gate without hand-writing YAML:

```bash
ohrisk init
```

Existing configuration is never overwritten. Add `--waivers` when the project
also needs an empty decision-record template, or `--no-workflow` for local-only
setup.

Ready to gate PRs? Run `ohrisk ci --fail-on high` locally, or see the [GitHub Actions guide](https://github.com/0disoft/ohrisk/blob/main/docs/github-actions.md) to wire it into CI.

## When to use it

Run Ohrisk when you are about to add or upgrade a dependency and want a fast,
local read on whether the license evidence introduces risk for your shipping
model. It sits between "I just installed a package" and "legal review."

- before opening a PR that adds or changes dependencies
- before cutting a release or tagging a build
- when a transitive dependency surprise appears in a lockfile diff
- when you need a SARIF or SBOM artifact for a compliance pipeline

Ohrisk does not approve or block packages on its own. It gives you the
evidence and a profile-aware severity so you can decide.

## Commands

| Command | What it answers |
| --- | --- |
| `ohrisk init` | How do I add a safe starter policy and PR gate to this project? |
| `ohrisk scan` | What does my dependency tree look like right now? Non-failing local decision aid. |
| `ohrisk ci` | Should this PR fail the build? Runs a scan and exits non-zero when findings meet `--fail-on`. |
| `ohrisk diff <ref>` | What changed since the baseline git ref? Separates new, meaningfully changed, and resolved findings. |
| `ohrisk explain <expr>` | How would Ohrisk classify this license expression for a profile, optionally with license-level organization policy? |

The npm package also includes focused companion commands:

- `ohrisk-summary` renders an existing JSON report in a GitHub Step Summary.
- `ohrisk-baseline` checks a reviewed finding baseline without Git history.
- `ohrisk-notices` generates deterministic third-party notices from an Ohrisk
  CycloneDX report and reviewed legal files. See the
  [notices guide](docs/third-party-notices.md).

## Usage profiles

Ohrisk evaluates the same dependency tree differently depending on how you ship
software, because redistribution changes license obligations.

- `saas` (default): you run the service and do not redistribute the package
  binaries to users. GPL-only copyleft such as GPL-2.0 and GPL-3.0 is treated
  as `review` rather than an immediate block, because SaaS usage does not
  trigger redistribution obligations. AGPL and source-available restrictions
  remain `high`. MPL file-level copyleft is `low` when no package copy is
  redistributed; LGPL and EPL remain `review`.
- `distributed-app`: you ship the package to users. GPL becomes `high` because
  redistribution obligations apply. Weak copyleft (LGPL, MPL, EPL) is flagged as
  `review`.

Pick the profile that matches how the dependency reaches your users:

```bash
ohrisk scan --profile saas
ohrisk scan --profile distributed-app
```

## Runtime

Ohrisk is distributed as an npm package, and the packaged CLI runs on Node.js
`>=24.0.0`. Bun is used for Ohrisk development, tests, and packaging, but users
do not need Bun installed to run the published CLI.

Ohrisk scans dependency-free `package.json` manifests, Bun, npm package-lock/shrinkwrap, pnpm, Deno npm, Yarn, Rust Cargo,
Go modules and workspaces, Python pyproject/pylock/uv/Pipenv/PDM/Poetry/requirements.txt, Java Gradle lockfiles and version catalogs,
Maven `pom.xml`, Bazel `MODULE.bazel`, .NET NuGet lockfiles, Conan locks, Conda environment specs and locks, vcpkg manifests, Haskell Stack locks, Perl Carton snapshots, LuaRocks locks, Dart/Flutter Pub locks,
Terraform provider locks, Helm chart dependency metadata, Nix flake locks,
Unity Package Manager locks, R renv locks, Julia manifests, SwiftPM pins,
Carthage pins, CocoaPods locks, Elixir Mix locks, Erlang Rebar3 locks, Ruby Bundler lockfiles, and PHP Composer lockfiles, plus
CycloneDX JSON/XML, SPDX JSON/RDF, and SPDX tag-value SBOM inputs, regardless of which
package manager you use to install the CLI.

## Current Scope

The current implementation is the first local dependency-risk vertical slice:

- dependency-free `package.json` manifests, Bun `bun.lock`, npm `package-lock.json`, npm `npm-shrinkwrap.json`, pnpm `pnpm-lock.yaml`, Deno `deno.lock`, Rust `Cargo.lock`, Go `go.work`, Go `go.mod`, Python `pyproject.toml`, Python `pylock.toml`, named Python `pylock.<name>.toml`, Python `uv.lock`, Python Pipenv `Pipfile.lock`, Python PDM `pdm.lock`, Python `poetry.lock`, Python `requirements.txt`, Java Gradle `gradle.lockfile`, Java Gradle `gradle/dependency-locks` directories and `gradle/dependency-locks/*.lockfile`, Java Gradle `gradle/libs.versions.toml`, Java Maven `pom.xml`, Bazel `MODULE.bazel`, .NET NuGet `packages.lock.json`, .NET restored `obj/project.assets.json`, .NET NuGet `packages.config`, .NET `*.csproj`, Conan `conan.lock`, Conda `environment.yml`, Conda `environment.yaml`, Conda `conda-lock.yml`, Conda `conda-lock.yaml`, vcpkg `vcpkg.json`, Terraform `.terraform.lock.hcl`, Helm `Chart.lock`, Helm `Chart.yaml`, Nix `flake.lock`, Unity Package Manager `Packages/packages-lock.json`, R `renv.lock`, Julia `Manifest.toml`, Haskell Stack `stack.yaml.lock`, Perl Carton `cpanfile.snapshot`, LuaRocks `luarocks.lock`, Dart/Flutter `pubspec.lock`, SwiftPM `Package.resolved`, Carthage `Cartfile.resolved`, CocoaPods `Podfile.lock`, Elixir Mix `mix.lock`, Erlang Rebar3 `rebar.lock`, Ruby Bundler `Gemfile.lock`, PHP Composer `composer.lock`, CycloneDX JSON/XML, SPDX JSON/RDF, SPDX tag-value `.spdx`, Yarn classic/Berry `yarn.lock`, and Zig `build.zig.zon` project discovery
- Node-compatible packaged CLI entrypoint for npm, pnpm, Yarn, npx, pnpm dlx, and yarn dlx users
- explicit dependency input selection with `--lockfile <path>` for projects that contain more than one supported input file
- opt-in `--all` discovery that merges every supported input at one project root, deduplicates packages by Package URL, and preserves contributing-lockfile provenance
- direct and transitive dependency graph extraction when the dependency input records parent/child relationships
- Bun, npm, pnpm, and Yarn classic/Berry workspace projects are scanned from every workspace/importer package root
- npm package-lock/shrinkwrap and pnpm traversal retain every reachable package and store at most 64 dependency paths per package with a typed truncation diagnostic
- pnpm `catalog:` and `catalog:<name>` dependency specifiers are resolved from `pnpm-workspace.yaml`
- Deno `deno.lock` projects are scanned for npm package dependencies recorded in `npm:` specifiers; root remote URL imports and JSR packages fail closed instead of being silently skipped
- Rust `Cargo.lock` projects are scanned for crates, using adjacent `Cargo.toml` root dependencies plus literal and segment `*`/`?` Cargo workspace member manifests such as `crates/*`, `crates/app-*`, `tools/?li`, and `crates/*/plugins/*` when available, treating contained root path dependencies as implicit members, honoring workspace `exclude` entries, `crate.workspace = true` dependency keys, workspace dependency package aliases, inherited `[workspace.package]` version/license metadata, member-root legal files with a workspace-root legal-file fallback, and table-form dependency sections such as `[dependencies.foo]`; traversal is iterative, retains every reachable crate, and stores at most 64 dependency paths per crate with a typed truncation diagnostic; remote scans fetch crates.io archives only from the fixed public artifact host and trust them only after Cargo.lock SHA-256 plus archive identity verification, while GitHub Git sources require a full locked commit and exact package identity inside the fixed codeload archive
- Go `go.work` projects are scanned across workspace modules and apply workspace `replace` directives before module-level replacements; Go `go.mod` projects are scanned for required modules, standard `tool` directives, source imports, build constraints, and Go `replace` directives. Line and block comments are ignored before import parsing, so comment punctuation cannot hide a production import. Modules imported only by tests or source files that require a non-default custom build tag are development roots. Checksum-verified module `go.mod` edges propagate that scope only when the complete selected Go graph has verified edges. A module ZIP without a usable root `go.mod` may use the separately checksummed fixed-proxy `.mod` response; otherwise its edges remain unavailable and transitive modules stay in production scope. Adjacent `go.sum` entries are checksum records rather than current dependencies on Go 1.17 and later.
- Python `pylock.toml` and named `pylock.<name>.toml` projects are scanned for versioned PyPI package records and project-root-contained source-tree package records with local source metadata
- Python `pyproject.toml` projects without a companion lockfile are scanned for exact PEP 621 `name==version` direct dependency pins
- Python `uv.lock` projects are scanned for PyPI package dependencies, project-root-contained `directory` or `editable` package source records, and remote Git records resolved to a full immutable commit; immutable Git records remain in the graph with unavailable evidence and are never cloned or replaced with same-name PyPI evidence, while iterative graph traversal retains every reachable package and stores at most 64 dependency paths per package with a typed truncation diagnostic
- Python PDM `pdm.lock` and `poetry.lock` projects are scanned for PyPI package dependencies recorded in the lockfile
- Python Pipenv `Pipfile.lock` projects are scanned for exact `==version` PyPI package entries and project-root-contained local `path` or editable source entries in the `default` and `develop` sections
- Python PDM `pdm.lock` projects use adjacent `pyproject.toml` root dependencies when available, infer roots from lockfile dependency references otherwise, and scan project-root-contained local `path` or relative `file:` source records
- Python `requirements.txt` files are scanned for pinned PyPI packages, project-root-contained local source entries, editable local source entries, nested `-r` requirement files, and exact `-c` constraint pins; pip-compile `# via` annotations restore direct/transitive package paths when every named parent is present
- Java Gradle `gradle.lockfile` and legacy `gradle/dependency-locks` directory projects are scanned for Maven coordinates recorded in dependency locking output; explicit `gradle/dependency-locks/*.lockfile` files are also accepted. Java Gradle `gradle/libs.versions.toml` projects are scanned for exact Maven library aliases from compact notation, `module` plus exact `version`, or `module` plus `version.ref`
- Java Maven `pom.xml` projects are scanned for direct dependencies with explicit, property-resolved, same-file `dependencyManagement`, local `.m2/repository`, or exact Maven Central parent/imported-BOM `dependencyManagement` versions; remote model resolution is limited to 32 identity-checked POMs of at most 2 MiB each and eight parent/BOM levels, and is reusable through the artifact cache in `--offline` mode
- Bazel `MODULE.bazel` projects are scanned for direct `bazel_dep` entries with literal exact `version` strings; nodep `repo_name = None` entries, `include()` expansion, overrides, module extensions, and `MODULE.bazel.lock` resolution output fail closed instead of being partial-scanned
- .NET NuGet `packages.lock.json` and restored `obj/project.assets.json` projects are scanned for direct and transitive package dependencies; .NET NuGet `packages.config` and `*.csproj` files are scanned for direct `PackageReference` and `PackageDownload` items, including central `PackageVersion` entries and exact `PackageDownload` ranges resolved from unconditional same-file properties. A `.csproj` that unconditionally sets `IsTestProject` to `true` or directly references `Microsoft.NET.Test.Sdk` classifies its package inputs as development dependencies
- Conan 2 `conan.lock` projects are scanned for recipe references from `requires`, `build_requires`, and `python_requires`; Conan binary package IDs, settings, options, user/channel, and recipe revisions are not modeled in Package URLs yet
- Conda `environment.yml` and `environment.yaml` projects are scanned for exact Conda `name=version` pins and exact pip `name==version` pins; Conda `conda-lock.yml` and `conda-lock.yaml` projects are scanned for resolved `conda` and `pip` package entries and are preferred when both an environment spec and conda-lock output are present; unpinned environment files are skipped only during repository-wide automatic discovery while explicit `--lockfile` selection remains strict
- vcpkg `vcpkg.json` projects are scanned from installed `vcpkg_installed/vcpkg/status` records when available, or from exact top-level `overrides` when installed status is absent; baseline and `version>=` constraints are not treated as resolved package versions, and unresolved manifests are skipped only during repository-wide automatic discovery while explicit `--lockfile` selection remains strict
- Terraform `.terraform.lock.hcl` projects are scanned for locked provider versions; provider constraints and platform hashes are not modeled in Package URLs yet
- Helm `Chart.lock` and `Chart.yaml` projects are scanned for chart dependency entries; `Chart.lock` is preferred when both files are present
- Nix `flake.lock` projects are scanned for reachable flake inputs from the root input graph; local path inputs use local legal files, while public GitHub inputs and exact NixOS nixpkgs release `nixexprs.tar.xz` inputs under either `/nixpkgs/...` or `/nixos/<channel>/...`, with a full locked commit and SHA-256 `narHash`, use bounded fixed-host archives whose extracted NAR tree must match before root legal files are trusted; NixOS XZ output streams through uniquely named, exclusively created temporary storage, is capped at 320 MiB and 100,000 TAR entries, and is removed on success or failure; Nix derivation package graphs are not reconstructed
- Unity Package Manager `Packages/packages-lock.json` projects are scanned for non-built-in package entries; Unity built-in modules, `Packages/manifest.json` without a lockfile, Asset Store `.unitypackage` archives, Addressables catalogs, and remote UPM registry metadata fetch are not scanned yet
- R `renv.lock` projects are scanned for package records in the lockfile; adjacent root `DESCRIPTION` `Depends`, `Imports`, `LinkingTo`, `Suggests`, and `Enhances` fields are used for production/development root classification when available, while dependency parent graphs, remote CRAN/GitHub/Bioconductor artifact fetch, and Packrat lockfiles are not scanned yet
- Julia `Manifest.toml` projects are scanned for versioned `[[deps.Name]]` records; unversioned standard libraries are skipped, adjacent `Project.toml` `[deps]` and test target `[extras]` entries are used for root/dev classification when available, and remote Julia registry or package server artifact fetch is not scanned yet
- Haskell Stack `stack.yaml.lock` projects are scanned for completed Hackage package pins; local Stack package database license metadata is used when present, followed by checksum-verified current or bounded historical public Hackage Cabal metadata, while snapshot package expansion, git/path extra-deps, and direct/transitive graph reconstruction are not scanned yet
- Perl Carton `cpanfile.snapshot` projects are scanned for Carton snapshot v1 distribution pins and dependency paths inferred from `provides` and `requirements`; local Carton cache archive `META.json` or `META.yml` license metadata is used when present, while MetaCPAN artifact fetch is not scanned yet
- LuaRocks `luarocks.lock` projects are scanned for literal `dependencies` table package pins; local `.rockspec` files in the project root or local rocks tree are used for literal string or string-table license metadata when present, while dependency graph reconstruction and LuaRocks metadata fetch are not scanned yet
- Dart and Flutter `pubspec.lock` projects are scanned for concrete Pub package versions recorded in the lockfile; modern pub.dev hosted records use their archive SHA-256 for verified remote evidence before project-local cache contents, while custom registries and Git/path sources remain local-only
- Swift Package Manager `Package.resolved` projects are scanned for pinned packages with resolved versions, revisions, or branches; Package.resolved does not expose parent dependency graphs, so packages are reported as root-level pins with unknown dependency type
- Carthage `Cartfile.resolved` projects are scanned for resolved GitHub, git, and binary pins; Cartfile.resolved does not expose parent dependency graphs, so packages are reported as root-level pins with unknown dependency type
- CocoaPods `Podfile.lock` projects are scanned for resolved pods; subspecs are collapsed to their root pod identity and dependency type is reported as unknown because Podfile.lock does not encode production/development groups
- Elixir Mix `mix.lock` projects are scanned for resolved Hex package pins; adjacent root `mix.exs` literal `only:` dependency options are used for production/development root classification when available, and public `hexpm` packages with an outer SHA-256 use checksum-verified remote tarball evidence after local evidence, while mix.lock dependency graph reconstruction and custom Hex repositories are not scanned yet
- Erlang Rebar3 `rebar.lock` projects are scanned for Hex `pkg` pins; depth-0 Hex pins are classified as production roots, while git/path deps, plugin locks, profile-specific test deps, and Rebar dependency tree reconstruction are not scanned yet
- Ruby Bundler `Gemfile.lock` projects are scanned for direct and transitive gem dependencies
- PHP Composer `composer.lock` projects are scanned for production and development package dependencies, using adjacent `composer.json` root dependencies when available and installed `vendor/` package sources for license evidence
- CycloneDX JSON/XML, SPDX JSON/RDF, and SPDX tag-value SBOM files are scanned for Package URL-backed package identities, dependency relationships, and embedded license evidence; SPDX `licenseDeclared` and `licenseConcluded` assertions are retained separately, disagreement fails closed as conflicting evidence, and referenced document-local `LicenseRef-*` extracted text is inspected within bounded limits
- explicit `--lockfile` SBOM paths are sniffed by content when their filename does not use a supported SBOM name or suffix
- npm alias dependency resolution, including pnpm alias package keys, with alias context preserved in dependency paths
- production, development, optional, and peer dependency classification
- local `file:` package artifact evidence
- installed `node_modules` package evidence, including npm alias install names, before network fallback
- Yarn Berry `.yarn/cache` package zip evidence before registry fallback for PnP installs without `node_modules`
- checksum- and identity-verified local Cargo registry source or `vendor/<crate>` evidence before a fixed public crates.io artifact fallback; remote `.crate` archives are trusted only after their exact Cargo.lock SHA-256, package root, and Cargo.toml identity are verified; public GitHub Git sources are fetched only when Cargo.lock pins a full commit, and their codeload archive must contain one repository root and one matching package identity
- local Go module cache, `vendor/<module>`, and project-root-contained local `replace` path evidence before a fixed public Go module proxy fallback for `go.work` and `go.mod` scans; remote module ZIPs and standalone `.mod` graph metadata are trusted only against their separate exact `go.sum` `h1` checksums
- Python `.venv` and `venv` `*.dist-info/METADATA` package evidence, plus project-root-contained local source metadata and license files for `uv.lock`, `pylock.toml`, `requirements.txt`, `Pipfile.lock`, and `pdm.lock` local source entries, before unavailable fallback
- exact-version PyPI release evidence for locked Python packages, using release metadata only to select SHA-256-verified source distributions or wheels; license evidence comes only from identity-checked `PKG-INFO`, `METADATA`, and license files inside the verified artifact, and a distribution that exceeds archive inspection limits remains `unknown` without aborting unrelated package findings
- local Maven `.m2/repository` POMs for Maven parent/BOM version management and package license evidence, including bounded parent-POM license inheritance
- exact Maven Central parent/imported-BOM POMs for dependency-version resolution during filesystem `scan`, `ci`, and both current and baseline `diff` parsing, with a 32-document limit per parsed side, 2 MiB per-POM limit, eight-level model depth, exact identity checks, cache reuse, and fail-closed incomplete-model handling
- exact-version Maven Central POM evidence for Maven and Gradle coordinates when local POM evidence is unavailable, plus project-declared HTTPS repositories whose exact hosts are explicitly allowed; all POM requests retain bounded parent inheritance, cache, offline, timeout, redirect, identity, and response-size checks
- checksum-verified Maven JAR license-file fallback when the selected POM chain has no license name, requiring a same-repository SHA-256 sidecar and exact embedded `META-INF/maven/<groupId>/<artifactId>/pom.properties` identity before root or `META-INF` license files are trusted
- Bazel module license evidence uses local Bazel registry `local_path` sources from file-based registries when present; remote Bazel registry metadata fetching is not scanned yet
- local NuGet package cache `.nuspec` evidence before exact public nuget.org fallback for hashed `packages.lock.json` and `obj/project.assets.json` packages; remote NUPKG evidence requires the selected input SHA-512 to match the catalog SHA-512, service-index discovery, exact normalized version selection, byte-size verification, matching package and structurally parsed nuspec identity, and bounded ZIP inspection; hashless `packages.config` and standalone `*.csproj` inputs remain unavailable, with repository-URL reports directing maintainers to commit a generated `packages.lock.json` and local reports directing users to restore and select `obj/project.assets.json` when available
- local Conan cache `conanfile.py` metadata and package source license evidence before unavailable fallback for `conan.lock` recipes
- local Conda package cache `info/index.json` metadata and license files before unavailable fallback for `environment.yml`, `environment.yaml`, `conda-lock.yml`, and `conda-lock.yaml` Conda packages
- local vcpkg `vcpkg_installed/<triplet>/share/<port>/copyright` evidence before unavailable fallback for `vcpkg.json` packages
- local Terraform `.terraform/providers` license file evidence before unavailable fallback for `.terraform.lock.hcl` providers
- local Helm `charts/` `Chart.yaml` metadata and license file evidence before unavailable fallback for `Chart.lock` and `Chart.yaml` dependencies
- local Nix path input license file evidence before unavailable fallback for `flake.lock` inputs
- local Unity `Packages/` and `Library/PackageCache` package source evidence before unavailable fallback for `Packages/packages-lock.json` packages
- local R `renv/library` DESCRIPTION metadata and license file evidence before unavailable fallback for `renv.lock` packages
- local Julia depot `Project.toml` metadata and license file evidence before unavailable fallback for `Manifest.toml` packages
- local Stack `.stack-work/install` package database metadata before checksum-verified public Hackage Cabal metadata fallback; remote metadata is trusted only after the completed lock pin's SHA-256 and exact package identity match
- local Carton cache archive `META.json` or `META.yml` metadata before unavailable fallback for CPAN distributions
- hash-verified pub.dev archive evidence for modern hosted `pubspec.lock` records, preferred over untrusted project-local Pub cache contents; remote archives are trusted only after the lockfile SHA-256 and `pubspec.yaml` package identity match
- local SwiftPM `.build/checkouts` and Xcode `SourcePackages/checkouts` package source evidence before unavailable fallback for `Package.resolved` packages
- local Carthage `Carthage/Checkouts` package source evidence before unavailable fallback for `Cartfile.resolved` packages
- local CocoaPods `Pods/<pod>` source and `Pods/Local Podspecs/<pod>.podspec.json` evidence before unavailable fallback for `Podfile.lock` packages
- local Elixir/Erlang `deps/<package>` source and `mix.exs` or `rebar.config` license metadata before checksum-verified public `mix.lock` Hex tarball evidence or unavailable fallback
- local Bundler/RubyGems install path gemspec evidence for `Gemfile.lock` gems, followed by fixed public RubyGems.org exact-version metadata and checksum-verified `.gem` contents for pure-Ruby packages; registry license claims alone are not evidence
- local `vendor/<vendor>/<package>/composer.json` evidence for Composer packages; unverified `composer.lock` license declarations are not treated as evidence
- remote HTTPS package tarball evidence when the lockfile points to a tarball with supported integrity metadata, with plaintext HTTP, credential-bearing URLs, obvious local, private, special-purpose, and DNS-resolved internal hosts blocked before fetch, connected socket addresses rechecked by the default fetcher, redirects followed only after each target is validated, and transient network failures recorded as unavailable package evidence so other packages can still be scanned
- fixed `proxy.golang.org` module ZIP evidence for Go dependencies with an exact ZIP `go.sum` `h1` checksum, plus a separately checksum-verified `.mod` fallback for internal module edges, including official `storage.googleapis.com` redirects, one bounded transient-failure retry, full checksum verification, and root license-file inspection without npm credentials
- a shared content-addressed artifact cache with HTTP freshness metadata, conditional `ETag`/`Last-Modified` revalidation, valid stale-entry support in `--offline` mode, and automatic 2 GiB LRU trimming
- `ohrisk cache status|prune|clear` commands for cache inspection, age/size cleanup, and bounded removal
- lockfile integrity verification for local and remote package tarballs; remote tarballs without integrity are reported as unavailable instead of being trusted as license evidence
- npm registry metadata lookup when the lockfile does not include a direct tarball URL
- npm registry metadata used only to locate exact-version tarballs, whose integrity and package contents are inspected before license evidence is trusted
- PyPI exact-release metadata lookup only to select a digest-identified distribution when a locked Python package does not include a direct distribution URL; registry license fields are not evidence
- gzipped package tarball evidence
- `package.json` license fields
- Cargo `Cargo.toml` package `license` fields
- Python `METADATA` `License-Expression`, `License`, and recognized license classifier fields
- Maven POM `<licenses>` names from the package POM or a bounded inherited parent POM
- NuGet `.nuspec` `<license>` expressions
- Ruby gemspec `license` and `licenses` fields
- Composer package `composer.json` `license` fields
- CycloneDX JSON/XML and SPDX JSON/RDF/tag-value package license declarations from SBOM metadata; conflicting SPDX declared and concluded assertions remain visible and produce unknown severity
- package metadata and recognizable license-file assertions are reconciled independently; a file expression outside the declared choices produces low-confidence `conflicting-evidence` and unknown severity instead of a false permissive result
- common `LICENSE`, `LICENCE`, `UNLICENSE`, `COPYING`, `NOTICE`, `THIRD_PARTY_NOTICES`, `THIRD_PARTY_LICENSES`, `COPYRIGHT`, `AUTHORS`, `PATENTS`, and `LEGAL` variants, `.license` sidecars, and files directly inside `LICENSES/` when the owning artifact collector exposes those paths
- medium-confidence standard license detection from recognizable `LICENSE` and `COPYING` file text, including SPDX identifiers, GPL-family v2/v3 text, Zlib text, public-domain-style text, and malformed metadata pointers
- SPDX-like license expression parsing against the pinned official license and exception catalog, including explicit medium-confidence provenance for deprecated identifiers and local or external-document `LicenseRef` syntax that remains custom low-confidence evidence
- common human-readable license metadata alias normalization, including slash and comma dual-license shorthands
- low-risk classification for common permissive, Zlib, and public-domain-style SPDX licenses
- NOTICE evidence is surfaced as attribution-preservation action text without raising severity
- distributed-app findings expose `license-text` and `notice-file` obligation evidence with `status: unknown`; Ohrisk does not claim the obligation is satisfied until the actual release artifact is checked
- high-risk classification for common source-available restriction licenses
- package-scoped commercial restriction detection in license evidence and package metadata; only restrictions explicitly limited to documentation or data/corpora remain scoped evidence without raising package-code severity, while mixed or ambiguous scope stays package-level
- profile-aware risk evaluation for `saas` and `distributed-app`
- terminal, JSON, and HTML reports
- SARIF 2.1.0 reports for code scanning upload
- waived findings in SARIF output as externally suppressed results
- Markdown reports for PR comments and release notes
- browser-friendly self-contained HTML reports for local review, with runtime
  search indexing and exact on-demand reconstruction of compacted long
  fingerprints so large monorepo reports do not duplicate multi-megabyte paths
- CycloneDX 1.5 JSON SBOM reports with dependency relationships and Ohrisk risk decision properties
- stable finding IDs for PR comments and local waiver workflows
- local `.ohrisk-waivers.json` waivers by finding ID or fingerprint
- stable diff matching that uses finding IDs and fingerprints to separate new, changed, and resolved findings without being triggered by action prose churn
- exact finding fingerprints for SARIF partial fingerprints and audit trails
- finding fingerprints in terminal and Markdown reports for waiver and audit workflows
- structured dependency type and direct/transitive scope in findings
- report file output with project-relative `--output <file>` paths
- optional browser opening for written HTML reports with `--open` through a temporary `127.0.0.1` URL
- command-specific help with `ohrisk help <command>` and `ohrisk <command> --help`
- standalone license expression explanation
- git ref diff reports that separate new, meaningfully changed, and resolved findings
- `diff --all` comparison of independently discovered current and baseline input sets, including added and removed lockfiles
- strict Draft 2020-12 JSON Schemas for scan, diff, and explain reports, with shared nested definitions and release-time output validation
- JSON threshold outcomes for `ci --fail-on` and `diff --fail-on`
- terminal and Markdown threshold outcomes for `ci --fail-on` and `diff --fail-on`
- strict CI waiver drift checks for expired or unmatched local waivers
- raw scan and CI mode with `--no-waivers` when waiver files should be ignored
- explicit waiver mode in JSON, terminal, Markdown, HTML, and SARIF reports
- explicit waiver mode in CycloneDX SBOM metadata

Central approval workflows, GitHub App checks, Go `go.work` use paths outside the project root, Go local `replace` paths outside the project root, pre-1.17 Go module graph reconstruction beyond the conservative `go.sum` fallback
reconstruction, unpinned or short-reference uv remote VCS entries, Pipenv and PDM remote VCS entries, uv, Pipenv, and PDM local source paths outside the project root, remote VCS `requirements.txt` entries, unpinned requirements ranges without exact constraint pins,
remote parent/BOM model resolution from project-declared or alternate Maven repositories, remote Maven model resolution for archive inputs, Maven transitive graph
resolution, unapproved project-declared Maven repositories, Gradle graph reconstruction, Gradle version catalog rich versions, bundle aliases, plugin aliases, and usage-site configuration reconstruction, Bazel `MODULE.bazel` `include()` expansion, Bazel overrides, module extensions, `MODULE.bazel.lock` graph reconstruction, remote Bazel registry metadata fetching, Conan 1 graph lock support, Conan binary package ID and remote ConanCenter
artifact fetching, unpinned or ranged Conda `environment.yml` specs, Conda environment transitive dependency reconstruction, explicit per-platform `conda-<platform>.lock` exports, remote Conda channel artifact fetching,
Conda build/channel/subdir Package URL qualifiers, Terraform module scanning, remote Terraform Registry metadata fetching, Helm transitive chart graph
reconstruction, remote Helm repository chart fetching, Nix derivation package graph reconstruction,
Nixpkgs package license extraction, vcpkg baseline-only resolution without installed status,
vcpkg feature/platform selection reconstruction, remote vcpkg registry metadata fetching, SwiftPM parent graph reconstruction,
Carthage parent graph reconstruction, remote Swift package checkout fetching,
Carthage remote checkout or binary framework license fetching, CocoaPods remote podspec or source
fetching, Mix and Rebar3 dependency graph reconstruction, Rebar3 git/path deps, Rebar3 plugin locks, remote Hex.pm artifact fetching,
Composer plugin/platform repository resolution, alternate Cargo registries, Cargo path sources, non-GitHub or non-commit-pinned Cargo Git sources, arbitrary or private Go proxies, Maven source archives or JARs without a same-repository SHA-256 sidecar and embedded exact identity, private or alternate NuGet feeds, custom Dart Pub registries and Dart Git/path sources, platform-specific or private RubyGems sources, or Packagist artifact
fetching are not part of this slice yet.

## Usage

Install with another package manager if you do not want npm:

```bash
pnpm add -g ohrisk
yarn global add ohrisk
bun add -g ohrisk
```

Run once with a package-manager exec command:

```bash
pnpm dlx ohrisk scan
yarn dlx ohrisk scan
bunx ohrisk scan
```

Run a local scan from a supported project:

```bash
ohrisk scan
```

Scan a public GitHub repository through a temporary shallow clone:

```bash
ohrisk scan --html https://github.com/0disoft/laqu.git
ohrisk scan --repo https://github.com/0disoft/laqu.git --json
ohrisk scan --html https://github.com/Mbed-TLS/mbedtls.git
ohrisk scan --submodules reject https://github.com/0disoft/laqu.git
```

The first command writes `laqu-ohrisk.html` in the current invocation directory.
Only public `https://github.com/<owner>/<repository>[.git]` URLs are accepted.
The remote scan path requires a Git executable available on `PATH`. Git
submodules are never fetched: the default `--submodules ignore` mode skips their
gitlinks and records bounded paths plus incomplete coverage in every report,
while `--submodules reject` preserves strict failure. Private credentials, other
hosts or protocols, unsafe or oversized trees, and `--offline` are rejected.
Symbolic links are removed without following their targets and recorded as
incomplete coverage. Non-portable regular paths are excluded before checkout
and reported separately. The temporary checkout is removed
after the scan. Policy, waivers, cache, and report output stay rooted in the
directory where Ohrisk was invoked; checkout-local policy and waivers are never
trusted. When the repository root has no supported input, Ohrisk recursively
selects one nested dependency project or merges multiple nested project roots
into one repository-wide graph with per-lockfile provenance. Automatic fan-out
is capped at 64 project roots and 128 inputs; `--lockfile <relative-path>` narrows
the scan explicitly. A known dependency manifest at the repository root without
a supported lockfile fails closed instead of being hidden by a nested project.
Absolute and traversal paths are rejected. Remote repository input is supported by `scan`, not `ci`, `diff`, or
the composite GitHub Action. See the [CLI command contract](docs/cli/command-contract.md#remote-repository-input)
for exact limits.

Scan a source archive without extracting it to disk:

```bash
ohrisk scan --archive artifacts/source.zip
ohrisk ci --archive artifacts/source.tar.gz --fail-on high
```

Archive mode is available for `scan` and `ci`, not `diff`. It accepts ZIP, TAR,
TAR.GZ, and TGZ through a strict read-only in-memory reader; nested archives are
not opened. `--archive` cannot be combined with `--lockfile` or
`--workspace-root`. Supported inputs at one archive project root are merged
automatically; `--all` remains accepted but is not required. Archives containing
multiple independent project roots still require explicit project selection and
fail closed. Policy and waiver files inside an archive are never auto-loaded:
`.ohrisk.yml` and
`.ohrisk-waivers.json` from the host invocation directory remain authoritative.
Encrypted or ZIP64 archives, unsupported compression and entry types, unsafe
paths, integrity failures, and archives over the documented resource limits are
rejected. See the [CLI command contract](docs/cli/command-contract.md#archive-input)
for the supported subset and exact limits.

Beginner HTML report flow on Windows PowerShell:

```powershell
npm install -g ohrisk@1.15.2
ohrisk version
cd C:\path\to\your\project
ohrisk scan --html --output reports\ohrisk-report.html --open
ohrisk scan --html --language ko --output reports\ohrisk-report-ko.html --open
ohrisk scan --html --language es --output reports\ohrisk-report-es.html --open
ohrisk scan --html --language fr --output reports\ohrisk-report-fr.html --open
ohrisk scan --html --language zh --output reports\ohrisk-report-zh.html --open
ohrisk scan --html --language hi --output reports\ohrisk-report-hi.html --open
ohrisk scan --html --language ja --output reports\ohrisk-report-ja.html --open
ohrisk scan --html --language id --output reports\ohrisk-report-id.html --open
ohrisk scan --html --language tr --output reports\ohrisk-report-tr.html --open
ohrisk scan --html --language ru --output reports\ohrisk-report-ru.html --open
ohrisk scan --html --language de --output reports\ohrisk-report-de.html --open
```

The scan prints live terminal progress while it reads the project, collects
license evidence, evaluates risk, and writes the report. In CI or redirected
stderr, Ohrisk keeps the plain append-only progress lines so logs stay readable.
When the scan succeeds, the terminal prints `Wrote report to ...` so you can see
the exact saved file path. With `--open`, Ohrisk opens the report through a
temporary `127.0.0.1` browser URL after the HTML file is written. If the browser
does not open, the scan can still succeed; open the printed HTML file path
manually.

If a lockfile contains local `file:` package dependencies that point to sibling
packages outside the current project repository, keep the default fail-closed
boundary and declare the monorepo/workspace root explicitly:

```bash
ohrisk scan --workspace-root .. --html --output reports/ohrisk-report.html
ohrisk ci --workspace-root C:\path\to\workspace --fail-on high
ohrisk diff main --workspace-root .. --prod
```

`--workspace-root` must point to an existing directory. Local package evidence
is then trusted only when the resolved artifact stays inside the project,
repository root, or that explicit workspace root. Local packages marked
`"private": true` in `package.json` are treated as internal package evidence
when they omit public license metadata, so they do not appear as `unknown`
external OSS findings solely because they have no license field.

Print command help or the installed package version:

```bash
ohrisk help
ohrisk help scan
ohrisk version
```

Supported dependency input files:

- dependency-free `package.json` manifests, reported as an empty dependency graph
- `bun.lock`
- `package-lock.json` with either a modern `packages` section or an npm v1 dependency tree
- `npm-shrinkwrap.json` with the same package-lock parser support
- `pnpm-lock.yaml` with `importers`, `packages`, and `snapshots` sections, including default and named catalogs from `pnpm-workspace.yaml`
- `deno.lock` npm package entries from Deno v3/v4-style lockfiles
- `Cargo.lock` crate entries from Rust Cargo projects, using adjacent `Cargo.toml` root dependencies plus literal and segment `*`/`?` Cargo workspace member manifests such as `crates/*`, `crates/app-*`, `tools/?li`, and `crates/*/plugins/*` when available, honoring workspace `exclude` entries, `crate.workspace = true` dependency keys, workspace dependency package aliases, package-root or workspace-root legal files, and table-form dependency sections such as `[dependencies.foo]`, plus local Cargo registry source for evidence
- `go.work` workspace modules, workspace `replace` directives, module `go.mod` requirements, module `replace` directives, and adjacent `go.sum` ZIP and `/go.mod` checksums when available, using local Go module cache, `vendor/<module>`, project-root-contained local replacement paths, checksum-verified public Go module ZIP evidence, or a separately verified public `.mod` edge fallback
- `go.mod` module requirements, standard `tool` directives, bounded source imports and build constraints, and Go `replace` directives, using adjacent `go.sum` ZIP and `/go.mod` checksums for local or fixed-public-proxy evidence; development-only roots and their verified exclusive module subgraphs are classified as development, production reachability wins for shared modules, incomplete edge evidence remains production, and Go 1.17 and later use the complete `go.mod` requirement graph while earlier or versionless modules conservatively retain `go.sum`-only fallback nodes
- `pylock.toml` and `pylock.<name>.toml` versioned package entries and project-root-contained source-tree records from the PyPA lockfile specification, using dependency references for audit paths and installed `.venv`/`venv` dist-info metadata or local source metadata and license files for local evidence
- `pyproject.toml` exact PEP 621 direct dependency pins such as `name==version`, using installed `.venv`/`venv` dist-info metadata for local evidence
- `uv.lock` package entries from Python uv projects plus project-root-contained `directory` and `editable` records and remote Git records resolved to a full immutable commit, using installed `.venv`/`venv` dist-info metadata or local source metadata for ordinary packages while immutable Git records stay `unknown` until their exact commit license is verified manually
- `Pipfile.lock` exact `==version` entries and project-root-contained local `path` or editable source entries from Python Pipenv projects, using installed `.venv`/`venv` dist-info metadata or local source metadata and license files for local evidence
- `pdm.lock` package entries and project-root-contained local `path` or relative `file:` source records from Python PDM projects, using adjacent `pyproject.toml` root dependencies when available and installed `.venv`/`venv` dist-info metadata or local source metadata and license files for local evidence
- `poetry.lock` package entries from Python Poetry projects, using adjacent `pyproject.toml` root dependencies when available and installed `.venv`/`venv` dist-info metadata for local evidence
- pinned `requirements.txt` entries such as `name==version`, local source entries such as `-e ./local-package`, `./local-package`, `file:./local-package`, and `name @ file:./local-package`, nested `-r` requirement files, and exact `-c` constraint pins for ranged entries, using pip-compile `# via` annotations for bounded dependency-path reconstruction and installed `.venv`/`venv` dist-info metadata or project-root-contained local source metadata and license files for local evidence
- `gradle.lockfile`, legacy `gradle/dependency-locks` directories, and explicit `gradle/dependency-locks/*.lockfile` Maven coordinates from Java Gradle dependency locking, using local `.m2/repository` POM metadata or exact-version Maven Central POM fallback for evidence
- `gradle/libs.versions.toml` Maven library aliases with exact versions or `version.ref` values from the same catalog, using local `.m2/repository` POM metadata or exact-version Maven Central POM fallback for evidence
- Maven `pom.xml` direct dependencies with explicit versions or versions resolved from local `<properties>`, same-file `dependencyManagement`, local `.m2/repository`, or exact Maven Central parent/imported-BOM `dependencyManagement` during filesystem `scan` and `ci`; reactor-internal module dependencies are excluded, and package licenses use local POM metadata, Maven Central, or explicitly allowed project-declared repositories with bounded parent inheritance and checksum/identity-verified JAR fallback
- Bazel `MODULE.bazel` direct `bazel_dep` entries with literal exact versions, failing closed on graph-expanding constructs and using local Bazel registry `local_path` source evidence when present
- .NET NuGet `packages.lock.json` package entries, restored `obj/project.assets.json` package graph entries, `packages.config` package entries, and direct `*.csproj` `PackageReference` or `PackageDownload` entries, resolving central versions from the nearest `Directory.Packages.props` and exact `PackageDownload` ranges from unconditional same-file properties when present, and using local NuGet cache `.nuspec` metadata before lock-and-catalog-SHA-512-verified public nuget.org NUPKG evidence for hashed inputs
- Conan 2 `conan.lock` recipe references from `requires`, `build_requires`, and `python_requires`, using local Conan cache `conanfile.py` metadata and license files for evidence
- Conda `environment.yml` and `environment.yaml` exact package pins plus Conda `conda-lock.yml` and `conda-lock.yaml` package entries, using local Conda package cache `info/index.json` metadata and license files for Conda package evidence
- vcpkg `vcpkg.json` manifest dependencies resolved from installed `vcpkg_installed/vcpkg/status` records or exact top-level overrides, using installed `vcpkg_installed/<triplet>/share/<port>/copyright` files for evidence
- Terraform `.terraform.lock.hcl` provider entries, using local `.terraform/providers` license files for evidence
- Helm `Chart.lock` and `Chart.yaml` chart dependency entries, using local `charts/` `Chart.yaml` metadata and license files for evidence
- Nix `flake.lock` reachable flake input entries, using local path license files or `narHash`-verified public GitHub source-tree root legal files for evidence
- Unity Package Manager `Packages/packages-lock.json` package entries, using local `Packages/` and `Library/PackageCache` source for evidence
- R `renv.lock` package records, using adjacent root `DESCRIPTION` dependency fields for production/development classification and local `renv/library` package source and DESCRIPTION metadata for evidence
- Julia `Manifest.toml` versioned package records, using local Julia depot package source and `Project.toml` metadata for evidence
- Haskell Stack `stack.yaml.lock` completed Hackage package pins, using local Stack package database metadata before checksum-verified public Hackage Cabal metadata fallback
- Perl Carton `cpanfile.snapshot` distribution pins, using local Carton cache archive metadata before unavailable evidence fallback
- LuaRocks `luarocks.lock` dependency pins, using local `.rockspec` license metadata before unavailable evidence fallback
- Dart/Flutter `pubspec.lock` package entries, using SHA-256-verified pub.dev archives first for modern hosted records and local `.dart_tool/package_config.json` or Pub cache sources for non-verifiable records
- SwiftPM `Package.resolved` package pins, using local `.build/checkouts` or `SourcePackages/checkouts` package source for evidence
- Carthage `Cartfile.resolved` package pins, using local `Carthage/Checkouts` package source for evidence
- CocoaPods `Podfile.lock` pod entries, using local `Pods/` source and `Pods/Local Podspecs` metadata for evidence
- Elixir Mix `mix.lock` Hex package pins, using adjacent root `mix.exs` literal `only:` options for production/development classification, local `deps/` package source first, and checksum-verified public `hexpm` tarballs as a fallback; Erlang Rebar3 `rebar.lock` Hex package pins, using depth-0 production root classification and local `deps/` package source and `rebar.config` metadata for evidence
- Ruby Bundler `Gemfile.lock` gem entries, using literal companion `Gemfile` group blocks and inline `group:` options for development classification, local Bundler/RubyGems sources first, and checksum-verified public pure-Ruby `.gem` contents as the remote fallback
- PHP Composer `composer.lock` package entries, using adjacent `composer.json` root dependencies when available and local `vendor/` package metadata for license evidence
- CycloneDX JSON/XML SBOM package entries with Package URL identities, dependency relationships, and embedded license evidence; traversal is iterative, retains every reachable component, stores at most 64 paths per component, and summarizes paths deeper than 256 components with typed graph diagnostics
- SPDX JSON/RDF and tag-value SBOM package entries with Package URL external refs, dependency relationships, and embedded license evidence, including separate declared and concluded assertions plus bounded extracted text for referenced local `LicenseRef-*` identifiers
- Yarn classic/Berry `yarn.lock` with root and workspace dependency sets from `package.json` manifests, plus local `.yarn/cache` zip evidence for Berry/PnP installs

Select a specific dependency input when a project contains more than one supported input file:

```bash
ohrisk scan --lockfile package-lock.json
ohrisk scan --lockfile npm-shrinkwrap.json
ohrisk ci --lockfile pnpm-lock.yaml --fail-on high
ohrisk scan --lockfile deno.lock
ohrisk scan --lockfile Cargo.lock
ohrisk scan --lockfile go.work
ohrisk scan --lockfile go.mod
ohrisk scan --lockfile pylock.toml
ohrisk scan --lockfile pyproject.toml
ohrisk scan --lockfile uv.lock
ohrisk scan --lockfile Pipfile.lock
ohrisk scan --lockfile pdm.lock
ohrisk scan --lockfile poetry.lock
ohrisk scan --lockfile requirements.txt
ohrisk scan --lockfile gradle.lockfile
ohrisk scan --lockfile gradle/dependency-locks
ohrisk scan --lockfile gradle/dependency-locks/runtimeClasspath.lockfile
ohrisk scan --lockfile gradle/libs.versions.toml
ohrisk scan --lockfile pom.xml
ohrisk scan --lockfile MODULE.bazel
ohrisk scan --lockfile packages.lock.json
ohrisk scan --lockfile obj/project.assets.json
ohrisk scan --lockfile packages.config
ohrisk scan --lockfile MyApp.csproj
ohrisk scan --lockfile conan.lock
ohrisk scan --lockfile environment.yml
ohrisk scan --lockfile conda-lock.yml
ohrisk scan --lockfile vcpkg.json
ohrisk scan --lockfile .terraform.lock.hcl
ohrisk scan --lockfile Chart.lock
ohrisk scan --lockfile Chart.yaml
ohrisk scan --lockfile flake.lock
ohrisk scan --lockfile Packages/packages-lock.json
ohrisk scan --lockfile renv.lock
ohrisk scan --lockfile Manifest.toml
ohrisk scan --lockfile stack.yaml.lock
ohrisk scan --lockfile cpanfile.snapshot
ohrisk scan --lockfile luarocks.lock
ohrisk scan --lockfile pubspec.lock
ohrisk scan --lockfile Package.resolved
ohrisk scan --lockfile Cartfile.resolved
ohrisk scan --lockfile Podfile.lock
ohrisk scan --lockfile mix.lock
ohrisk scan --lockfile rebar.lock
ohrisk scan --lockfile Gemfile.lock
ohrisk scan --lockfile composer.lock
ohrisk scan --lockfile cyclonedx.json
ohrisk scan --lockfile licenses.cdx.json
ohrisk scan --lockfile cyclonedx.xml
ohrisk scan --lockfile sbom.cdx.xml
ohrisk scan --lockfile spdx.json
ohrisk scan --lockfile licenses.spdx.json
ohrisk scan --lockfile spdx.rdf
ohrisk scan --lockfile sbom.spdx.rdf.xml
ohrisk scan --lockfile sbom.spdx
ohrisk diff main --lockfile bun.lock
ohrisk diff main --all
```

Pick the usage profile:

```bash
ohrisk scan --profile saas
ohrisk scan --profile distributed-app
```

Limit the scan to production-relevant dependencies by excluding development-only packages:

```bash
ohrisk scan --prod
```

Print machine-readable output:

```bash
ohrisk scan --json
```

Print SARIF output for code scanning upload:

```bash
ohrisk scan --sarif
```

Print a Markdown report:

```bash
ohrisk scan --markdown --prod
```

Print a CycloneDX SBOM:

```bash
ohrisk scan --cyclonedx --prod
```

Write browser, SARIF, SBOM, or PR reports to files:

```bash
ohrisk scan --html --output reports/ohrisk.html --open
ohrisk scan --html --language ko --output reports/ohrisk-ko.html --open
ohrisk scan --html --language es --output reports/ohrisk-es.html --open
ohrisk scan --html --language fr --output reports/ohrisk-fr.html --open
ohrisk scan --html --language zh --output reports/ohrisk-zh.html --open
ohrisk scan --html --language hi --output reports/ohrisk-hi.html --open
ohrisk scan --html --language ja --output reports/ohrisk-ja.html --open
ohrisk scan --html --language id --output reports/ohrisk-id.html --open
ohrisk scan --html --language tr --output reports/ohrisk-tr.html --open
ohrisk scan --html --language ru --output reports/ohrisk-ru.html --open
ohrisk scan --html --language de --output reports/ohrisk-de.html --open
ohrisk scan --sarif --output reports/ohrisk.sarif
ohrisk scan --cyclonedx --output reports/ohrisk.cdx.json
ohrisk diff main --all --prod --markdown --output reports/ohrisk-pr.md
```

`--output` accepts project-relative file paths only. Absolute paths,
drive-relative paths, UNC paths, and `.` or `..` path segments are rejected.
Parent directories are created one component at a time, and every existing
component is resolved and checked against the project root before anything is
created. A rejected output path never leaves a new file or directory inside or
outside the project, including through symlink or junction parents that point
outside the project. Parent symlinks that resolve inside the project remain
supported.

Fail a local CI step when findings meet a threshold:

```bash
ohrisk ci --fail-on high
```

CI fails separately when package evidence or repository coverage is partial,
even if no finding reaches the risk threshold. Use
`--allow-partial-evidence` only for an intentional fail-open exception; reports
record the completeness status and affected counts.

Fail a local CI step when waiver files contain expired or unmatched waivers:

```bash
ohrisk ci --strict-waivers
```

Run a raw audit scan or CI step without reading local waiver files:

```bash
ohrisk scan --no-waivers
ohrisk ci --no-waivers --fail-on high
```

Waive a finding locally by ID or fingerprint in `.ohrisk-waivers.json`:

```json
{
  "waivers": [
    {
      "id": "agpl-child@0.1.0::production::transitive::fixture-bun-project>permissive-parent@1.0.0>agpl-child@0.1.0",
      "reason": "Accepted for this release after internal review.",
      "expiresOn": "2026-09-30"
    }
  ]
}
```

Waived findings are excluded from `ci --fail-on` threshold failures, but scan
and CI JSON, terminal, Markdown, HTML, and SARIF reports still show them. Terminal,
Markdown, and HTML reports include finding fingerprints so waiver files can target
either `id` or `fingerprint`. Expired waivers and unmatched active waivers are
reported separately in JSON, terminal, Markdown, and HTML reports and are not applied.
`ci --strict-waivers` exits non-zero when either expired or unmatched waivers
are present, even if active findings stay below the `--fail-on` threshold. JSON,
terminal, Markdown, HTML, and SARIF outputs include the strict waiver drift result
when that option is enabled. `scan --no-waivers` and `ci --no-waivers` do not
read or apply local waiver files; `ci --no-waivers` cannot be combined with
`--strict-waivers`. Reports include a waiver mode field or summary line so raw
audits can distinguish ignored waiver files from projects with no waivers.
If package names, versions, paths, reasons, or evidence text contain finding
delimiters such as `::`, `>`, or `|`, Ohrisk percent-escapes those characters in
the generated IDs and fingerprints to keep waiver matching unambiguous.
Finding IDs canonicalize each dependency path set: exact duplicate paths are
removed and the remaining paths are sorted by stable code-unit order, so
relationship order, lockfile order, and merge input order cannot change a
finding ID. Adding or removing a dependency path still changes the ID because
paths are part of the finding identity. Waiver files written before this
canonicalization continue to match through a legacy raw-order ID and fingerprint
alias, so existing waivers keep applying to the same dependency graph.

Explain a license expression without scanning a project:

```bash
ohrisk explain AGPL-3.0-only --profile saas
ohrisk explain "GPL-2.0-only WITH Classpath-exception-2.0" --policy .ohrisk.yml --json
```

Compare the current findings against a baseline git ref:

```bash
ohrisk diff main --prod
ohrisk diff main --prod --fail-on unknown
ohrisk diff main --prod --markdown
```

Baseline refs must be branch, tag, or commit-like names such as `main`,
`origin/main`, `release/v1.2.3`, or a commit hash. Git rev syntax such as
`HEAD@{1}`, `HEAD~1`, and `main:path` is rejected. `diff --all` discovers and
merges the current and baseline input sets independently, then reports added and
removed lockfiles alongside separate new, changed, and resolved findings.

Inspect or clean the shared artifact cache:

```bash
ohrisk cache status
ohrisk cache prune --max-age 7d --max-size 1GiB
ohrisk cache clear
```

The default cache follows the platform cache directory, uses a 24-hour fallback
TTL, conditionally revalidates expired HTTP entries, and automatically trims
least-recently-used content above 2 GiB. Add `--json` for machine-readable cache
command output or `--cache-dir <path>` to target an explicit cache.

Print the package version:

```bash
ohrisk --version
```

## Report Shape

The terminal report is designed to show the highest-risk findings first:

```text
Ohrisk scan
Profile: saas
Production only: yes
Risks: 1 high, 1 review, 1 unknown, 2 low
Waiver mode: local (.ohrisk-waivers.json)
Waived: 0 applied, 0 expired, 0 unmatched

Findings:
- [high] agpl-child@0.1.0
  id: agpl-child@0.1.0::production::transitive::fixture-bun-project>permissive-parent@1.0.0>agpl-child@0.1.0
  fingerprint: agpl-child@0.1.0::production::transitive::fixture-bun-project>permissive-parent@1.0.0>agpl-child@0.1.0::high::replace::License expression is high risk for saas.::license: AGPL-3.0-only|dependency: production|transitive dependency|source: local|package.json license: AGPL-3.0-only|file: COPYING (copying)
  License expression is high risk for saas.
  recommendation: replace
  action: Replace this package or escalate before shipping.
  dependency: production transitive
  path: fixture-bun-project -> permissive-parent@1.0.0 -> agpl-child@0.1.0
  evidence: license: AGPL-3.0-only; dependency: production; transitive dependency; source: local; package.json license: AGPL-3.0-only; file: COPYING (copying)
```

JSON output reuses the same finding model:

```json
{
  "status": "profile_risk_evaluated",
  "profile": "saas",
  "prodOnly": true,
  "waiverMode": "local",
  "nextAction": "Replace or escalate high-risk dependencies before shipping.",
  "failOn": "high",
  "failed": true,
  "failingFindingCount": 1,
  "findings": [
    {
      "id": "agpl-child@0.1.0::production::transitive::fixture-bun-project>permissive-parent@1.0.0>agpl-child@0.1.0",
      "fingerprint": "agpl-child@0.1.0::production::transitive::fixture-bun-project>permissive-parent@1.0.0>agpl-child@0.1.0::high::replace::License expression is high risk for saas.::license: AGPL-3.0-only|dependency: production|transitive dependency",
      "packageId": "agpl-child@0.1.0",
      "severity": "high",
      "reason": "License expression is high risk for saas.",
      "recommendation": "replace",
      "action": "Replace this package or escalate before shipping.",
      "dependencyType": "production",
      "dependencyScope": "transitive",
      "paths": [
        [
          "fixture-bun-project",
          "permissive-parent@1.0.0",
          "agpl-child@0.1.0"
        ]
      ]
    }
  ]
}
```

Markdown output keeps the scan summary and PR-facing decision fields together:

```markdown
- Licenses: `4 high-confidence`, `0 medium-confidence`, `1 low-confidence`
- License issues: `1 missing`, `0 malformed`
- Waiver mode: `local (.ohrisk-waivers.json)`
- Threshold: failed on high (1 finding at or above threshold)

| ID | Fingerprint | Severity | Package | Dependency | Reason | Recommendation | Action | Path |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `agpl-child@0.1.0::production::transitive::fixture-bun-project>permissive-parent@1.0.0>agpl-child@0.1.0` | `agpl-child@0.1.0::production::transitive::fixture-bun-project>permissive-parent@1.0.0>agpl-child@0.1.0::high::replace::License expression is high risk for saas.::license: AGPL-3.0-only\|dependency: production\|transitive dependency\|source: local\|package.json license: AGPL-3.0-only\|file: COPYING (copying)` | high | `agpl-child@0.1.0` | production transitive | License expression is high risk for saas. | replace | Replace this package or escalate before shipping. | fixture-bun-project -> permissive-parent@1.0.0 -> agpl-child@0.1.0 |
```

## Risk Language

Ohrisk intentionally avoids legal `safe` or `unsafe` verdicts.

- `low`: known low-risk license expression for the selected profile
- `review`: review before shipping under the selected profile
- `high`: replace or escalate before shipping under the selected profile, including explicit commercial-use restrictions and packages marked `UNLICENSED`
- `unknown`: missing, malformed, or unrecognized license evidence

For example, GPL is treated differently for `saas` and `distributed-app`
because redistribution changes the risk profile.

## Development

Run the test suite:

```bash
bun test
```

Run the release-ready local gate:

```bash
bun run verify:release
```

Run the fixture scan manually:

```bash
cd test/fixtures/bun-project
bun run ../../../src/cli/main.ts scan --profile saas
```

## Documentation

- [Security Policy](https://github.com/0disoft/ohrisk/blob/main/SECURITY.md) — Private vulnerability reporting and supported-version policy
- [Contributing Guide](https://github.com/0disoft/ohrisk/blob/main/CONTRIBUTING.md) — Development workflow, change boundaries, and ecosystem additions
- [Documentation Index](https://github.com/0disoft/ohrisk/blob/main/docs/README.md) — All guides in one place
- [GitHub Actions Guide](https://github.com/0disoft/ohrisk/blob/main/docs/github-actions.md) — PR gates, comments, SARIF upload, and waiver drift checks
- [Risky Demo](https://github.com/0disoft/ohrisk/blob/main/docs/risky-demo.md) — Run the bundled fixture to see high, review, unknown, and low findings
- [CI Usage Guide](https://github.com/0disoft/ohrisk/blob/main/docs/ci.md) — GitHub Actions examples for PR gates and artifacts
- [Waiver Guide](https://github.com/0disoft/ohrisk/blob/main/docs/waivers.md) — Managing license risk waivers safely
- [Profile Guide](https://github.com/0disoft/ohrisk/blob/main/docs/profiles.md) — Choosing between saas and distributed-app
- [Cache and Registry Configuration](https://github.com/0disoft/ohrisk/blob/main/docs/cache-and-registries.md) — Cache freshness, cleanup, offline mode, and registry authentication
- [Report Formats Guide](https://github.com/0disoft/ohrisk/blob/main/docs/report-formats.md) — What each output format includes
- [Accuracy Evidence](https://github.com/0disoft/ohrisk/blob/main/docs/accuracy.md) — Reviewed corpus metrics with explicit denominators and limitations
- [Remote Fetching Boundary](https://github.com/0disoft/ohrisk/blob/main/docs/remote-fetching.md) — Remote evidence scope and safety rules
- [한국어 사용 가이드](https://github.com/0disoft/ohrisk/blob/main/docs/ko/README.md) — Korean usage guide for developers
