# safe-deps

`safe-deps` is a Rust CLI linter for package-management security practices
(reproducibility, integrity, registry/TLS safety, supply-chain hardening). It is
built for both developer terminals and CI/CD pipelines.

`safe-deps check` is **static, deterministic, and offline by design**: it does
not install dependencies, execute project code, or make network calls. The
separate `safe-deps audit` command is the *only* networked mode — it explicitly
queries a vulnerability database (OSV).

> Status: the CLI is implemented (Phases 1–5) and published to
> [crates.io](https://crates.io/crates/safe-deps); install with Homebrew or
> `cargo install`, grab a prebuilt binary from GitHub Releases, or build from
> source (all below). Minimum supported Rust version (MSRV): **1.86**.

## Install

### Homebrew (macOS / Linux)

```bash
brew install baneido/tap/safe-deps
```

Installs the prebuilt binary from [GitHub Releases](https://github.com/baneido/safe-deps/releases)
via the [`baneido/homebrew-tap`](https://github.com/baneido/homebrew-tap) tap — no
Rust toolchain required. Covers macOS and Linux on both arm64 and x86-64. The
formula is refreshed automatically on every tagged release. `brew upgrade safe-deps`
moves you to the latest version.

### From crates.io

```bash
cargo install safe-deps --locked
```

Builds and installs the latest [crates.io](https://crates.io/crates/safe-deps)
release into `~/.cargo/bin`. Needs a Rust toolchain (MSRV 1.86); `--locked` builds
against the crate's published `Cargo.lock` for a reproducible dependency set.

### Prebuilt binaries

Each tagged release publishes binaries for Linux and macOS (x86-64 and arm64)
and Windows (x86-64) to [GitHub Releases](https://github.com/baneido/safe-deps/releases),
with a SHA-256 checksum per archive, a signed `SHA256SUMS` manifest, and a
CycloneDX SBOM — prefer these when you want a verified artifact without a Rust
toolchain.

Download the archive for your platform, then verify it before use:

```bash
# Linux, or macOS with GNU coreutils:
sha256sum --check --ignore-missing SHA256SUMS

# macOS default tools:
shasum -a 256 --check --ignore-missing SHA256SUMS

# Windows PowerShell:
Get-FileHash .\safe-deps-<target>.zip -Algorithm SHA256
# Compare the hash with the matching line in SHA256SUMS or the .sha256 file.

# Verify the manifest signature with cosign (keyless; no key to manage).
cosign verify-blob \
  --bundle SHA256SUMS.cosign.bundle \
  --certificate-identity-regexp '^https://github\.com/baneido/safe-deps/\.github/workflows/release\.yml@refs/tags/v.*$' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  SHA256SUMS
```

Then extract the archive and put `safe-deps` on your `PATH`.

### From source

```bash
cargo build --release        # binary at target/release/safe-deps
cargo run -- check .         # or run directly from source
```

`cargo install --path .` installs it into `~/.cargo/bin` from a local checkout.

## Usage

```bash
safe-deps check .                    # lint the current directory (offline)
safe-deps check . --format json      # text | json | sarif | junit
safe-deps check . --profile strict   # balanced (default) | strict | permissive
safe-deps check . --fail-on warning  # error (default) | warning | info | none
safe-deps audit .                    # query OSV for known vulnerabilities (network)
safe-deps audit . --offline          # use only the local OSV cache
safe-deps explain SD006              # describe a rule
safe-deps list-rules                 # list all rules
safe-deps init                       # write a commented safe-deps.toml
```

`check` exit codes: `0` clean, `1` findings at/above `--fail-on`, `2`
usage/config error, `3` internal error, `4` parse failure under
`--strict-parser-errors`.

## Try it

[`examples/` in the repository](https://github.com/baneido/safe-deps/tree/main/examples)
holds small, self-contained projects that each trip one rule, plus a hardened
baseline that stays clean. From a checkout of the repository:

```bash
safe-deps check examples/missing-lockfile        # SD001
safe-deps check examples/npm-insecure-registry   # SD003
safe-deps check examples/pip-extra-index          # SD007 (+ SD004)
safe-deps check examples/clean-baseline          # no findings, exit 0
```

See [`examples/README.md`](https://github.com/baneido/safe-deps/blob/main/examples/README.md)
for what each demonstrates.

## Supported ecosystems

| Ecosystem  | Package managers     |
| ---------- | -------------------- |
| JavaScript | npm, Yarn, pnpm, Bun |
| Python     | pip, uv              |
| Rust       | Cargo                |
| Go         | Go modules           |

## Rules

| ID    | Summary                                                                  |
| ----- | ------------------------------------------------------------------------ |
| SD001 | Lockfile missing for a manifest that declares dependencies.              |
| SD002 | CI installs should use a frozen/locked command, not a resolving one.     |
| SD003 | Registry or index uses HTTP or TLS verification is disabled.             |
| SD004 | Integrity or checksum validation is disabled.                            |
| SD005 | Dependency build/lifecycle scripts are broadly enabled.                  |
| SD006 | Dependency resolves from an unsafe source (floating git, tarball, path). |
| SD007 | Index/source config exposes the project to dependency confusion.         |
| SD008 | CI installs dependencies but no audit command is visible.                |
| SD009 | CI install commands use a flag that bypasses dependency safety checks.   |

`safe-deps explain <ID>` prints the full rationale and remediation for a rule.

### Ecosystem × rule coverage

`✓` = the rule can fire for that ecosystem today; `–` = not applicable or not yet
implemented. Rules marked **(CI)** are derived from CI commands and only fire
when a supported CI configuration is present (see CI provider support below).

| Ecosystem | SD001 | SD002 (CI) | SD003 | SD004 | SD005 | SD006 | SD007 | SD008 (CI) | SD009 (CI) |
| --------- | :---: | :--------: | :---: | :---: | :---: | :---: | :---: | :--------: | :--------: |
| npm       |   ✓   |     ✓      |   ✓   |   ✓   |   –   |   ✓   |   –   |     ✓      |     ✓      |
| Yarn      |   ✓   |     ✓      |   ✓   |   ✓   |   –   |   ✓   |   –   |     ✓      |     ✓      |
| pnpm      |   ✓   |     ✓      |   ✓   |   –   |   ✓   |   ✓   |   –   |     ✓      |     ✓      |
| Bun       |   ✓   |     ✓      |   –   |   –   |   ✓   |   ✓   |   –   |     ✓      |     ✓      |
| pip       |   –   |     ✓      |   ✓   |   ✓   |   –   |   ✓   |   ✓   |     ✓      |     ✓      |
| uv        |   ✓   |     ✓      |   ✓   |   –   |   –   |   ✓   |   ✓   |     ✓      |     ✓      |
| Cargo     |   ✓   |     ✓      |   –   |   –   |   –   |   ✓   |   –   |     –      |     –      |
| Go        |   ✓   |     ✓      |   –   |   –   |   –   |   ✓   |   –   |     –      |     –      |

Notes:

- pip has no conventional lockfile, so SD001 does not apply; its integrity is
  assessed through `--require-hashes` (SD004) instead.
- SD006 (unsafe dependency source) covers all four ecosystems: JavaScript and
  Python manifests, plus Cargo (`git`/`path` dependencies, `[patch]`/`[replace]`
  redirects, and `.cargo/config.toml` `[source]` `replace-with` redirects whose
  target is a remote `registry`/`git` source — redirects to a local
  `directory`/`local-registry` vendoring target are deterministic/offline and not
  flagged; ancestor configs are scanned per Cargo's hierarchical lookup) and Go
  (local-path `replace` targets, and CI `env` that *globally* disables the module
  checksum database — `GOSUMDB=off`, or `GONOSUMCHECK`/`GONOSUMDB` set to the
  wildcard `*`). `GOINSECURE`/`GOFLAGS=-insecure` allow insecure transport rather
  than disabling the checksum database, so they are not treated as a sumdb bypass.
  Remaining deeper detections (Cargo sparse-index/VCS-URL nuance, Go `go env -w`
  command parsing and `GOPRIVATE`/`GONOPROXY` review) are tracked in
  [#65](https://github.com/baneido/safe-deps/issues/65).
- For Cargo/Go, SD002 flags a non-reproducible CI build (`cargo build`/`test`
  without `--locked`/`--frozen`; `go build`/`test` with `-mod=mod`). SD008/SD009
  do not yet recognize Cargo/Go commands.

## CI provider support

CI-aware rules (SD002, SD008, SD009) read commands and `env` from these CI
providers:

| Provider       | Config file(s)                  |
| -------------- | ------------------------------- |
| GitHub Actions | `.github/workflows/*.yml\|yaml` |
| GitLab CI      | `.gitlab-ci.yml`                |
| CircleCI       | `.circleci/config.yml`          |

Other providers (Jenkins, Azure Pipelines, …) are not yet parsed.

## Output formats

`text` (default) and `json` are the primary formats. `sarif` (2.1.0, for GitHub
code scanning) and `junit` (for generic CI test dashboards) are also supported.

## Configuration

Configuration is optional. `safe-deps init` writes a commented `safe-deps.toml`.
Resolution precedence is: CLI flag → `safe-deps.toml` → environment variable
(`SAFE_DEPS_PROFILE`, `SAFE_DEPS_FORMAT`) → default.

Key settings:

- **Profiles** — `balanced` (default), `strict`, `permissive` adjust severities.
- **`[policy]`** — `application_roots` / `library_roots` (globs that classify a
  project's kind), `allow_git_dependencies`, `allow_local_path_dependencies`,
  and `external_audit` (opt out of SD008 when audits run elsewhere).
- **`[[suppressions]]`** — silence a rule for a path, with an optional expiry.
- **`[[advisory_ignores]]`** — for `audit`: ignore an advisory id with a required
  reason and optional expiry; expired ignores stop applying and surface a note.

## `safe-deps audit`

`audit` is a separate, explicitly-networked pipeline. It extracts pinned,
registry-sourced coordinates from `Cargo.lock` and `package-lock.json`, queries
OSV for known advisories, and caches results on disk (default TTL 24h).
`--offline` uses only the cache. `check` never touches the network.

### Audit cache directory

The OSV result cache is stored in `$XDG_CACHE_HOME/safe-deps/osv` when
`$XDG_CACHE_HOME` is set, or `$HOME/.cache/safe-deps/osv` otherwise.

When **neither** `$HOME` nor `$XDG_CACHE_HOME` is set (e.g. inside a minimal
container or a `sudo`-stripped environment) the cache falls back to
`<temp_dir>/safe-deps/osv` (typically `/tmp/safe-deps/osv` on Linux). Be aware
of the following caveats in that case:

- **Ownership**: the system temp directory is typically world-writable; another
  user or process could replace or delete cache files between runs.
- **Visibility**: on Linux, `/tmp` is not namespaced per user, so cache entries
  may be visible to other local users.
- **Lifetime**: many systems clear `/tmp` on reboot or via a tmpwatch timer, so
  the cache may be shorter-lived than the configured TTL.

For production use, ensure `$HOME` (or `$XDG_CACHE_HOME`) is set so the cache
lands in a user-private, persistent location.

Cache writes are atomic (written to a temp file then renamed) so a concurrent
or interrupted run cannot leave a partially-written entry. Write failures are
surfaced as non-fatal diagnostics in the audit output.

HTTP uses an in-process client (`ureq` with rustls) by default, so the binary is
self-contained and cross-platform. Building with
`--no-default-features --features curl-transport` instead shells out to the
system `curl` (a TLS-crate-free build, e.g. for toolchains without a C compiler).

## Contributing

- [CONTRIBUTING.md](https://github.com/baneido/safe-deps/blob/main/CONTRIBUTING.md) — how to report issues and land a change.
- [DEVELOPMENT.md](https://github.com/baneido/safe-deps/blob/main/DEVELOPMENT.md) — build, test, and architecture.
- [RELEASING.md](https://github.com/baneido/safe-deps/blob/main/RELEASING.md) — the release checklist (maintainers).

## Design documents

- [Security best practices research](https://github.com/baneido/safe-deps/blob/main/docs/security-best-practices.md)
- [CLI architecture design](https://github.com/baneido/safe-deps/blob/main/docs/design/safe-deps-cli-design.md)

## Roadmap

The detailed roadmap lives in the
[CLI architecture design](https://github.com/baneido/safe-deps/blob/main/docs/design/safe-deps-cli-design.md#roadmap). At a high
level, Phases 1–5 below are implemented; remaining work is tracked in the issue
tracker.

- Phase 0: design and research. ✅
- Phase 1: Rust static linter MVP for npm, Yarn, pnpm, Bun, pip, and uv. ✅
- Phase 2: CI-aware checks (GitHub Actions) and SARIF output. ✅
- Phase 3: supply-chain hardening rules and policy profiles. ✅
- Phase 4: additional ecosystems (Cargo, Go) and JUnit output. ✅
- Phase 5: explicit networked audit mode (OSV). ✅

Planned next: a production Rust CI workflow, more CI providers, deeper Cargo/Go
source detections (see [#65](https://github.com/baneido/safe-deps/issues/65)), and
additional ecosystems (Composer, Bundler, Gradle/Maven, NuGet).

## License

This project is licensed under the MIT License. See [LICENSE](LICENSE).
Third-party dependency licenses are catalogued in
[THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md).
