Metadata-Version: 2.4
Name: avaril-cli
Version: 0.1.0
Summary: Command-line tool that checks your codebase against privacy constraints, verifies enforcement of your processing activities and reports results to Avaril.
Author: Avaril
License-Expression: ISC
Project-URL: Homepage, https://avaril.net
Project-URL: Documentation, https://docs.avaril.net
Keywords: privacy,privacy-by-design,data-protection,dpdp,dpdpa,compliance,governance,cli,static-analysis,sarif
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Environment :: Console
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: avaril<3,>=2.0.0
Requires-Dist: click>=8.1
Requires-Dist: cryptography>=41
Requires-Dist: msgpack>=1.0
Requires-Dist: requests>=2.31
Requires-Dist: pyyaml>=6.0
Requires-Dist: jsonschema>=4.0
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Dynamic: license-file

# avaril-cli

Avaril is a privacy governance layer for building software that meets its
legal obligations by design. It gives teams a privacy framework that fits
into the software development lifecycle.

Verifies that a codebase actually enforces the privacy rules a company
claims it enforces: reads a manifest of declared data entities/purposes/
retention/processors, runs detectors against the code, and produces one
signed, structured result — the evidence behind an Enforcement Score
that feeds a larger PIA score computed server-side.

Also still manages the separate, older AVRL offline-process-manifest
flow (`avaril manifest pull/verify/inspect`) consumed by `avaril.process`'s
`enforce_manifest` gating in the Python SDK — see the "Two manifests"
note below before touching anything named `manifest`.

## Beta

This package is pre-1.0 and not yet published to PyPI. There is no
CI-specific auth mode (scoped CI tokens via OIDC exchange are not
available yet — see "Authentication in CI" below). `avaril pull`'s
backend endpoint now exists and works, but no constraint pack has been
published anywhere yet, so it returns `pack_not_found` until one is.
Everything that works entirely client-side — `init`, `doctor`, `scan`
against an already-cached pack, `report`, `explain`, `verify`, and
`manifest pull/verify/inspect` — works today. `avaril push` also works
end-to-end against a real server.

## Authentication in CI

There is no scoped, short-lived CI token yet. Use a long-lived
enforcement token as a CI secret, supplied either way:

- `AVARIL_TOKEN` environment variable (checked first, never written to
  disk — the preferred path for CI).
- `avaril login --token <token>`, which writes it to the OS user
  config directory for the duration of the job.

## Install

```
uv tool install avaril-cli
# or
pipx install avaril-cli
```

(Not on PyPI yet — see the beta notice above and
`PUBLISH-CHECKLIST.md`. Once published, both of the above resolve
`avaril` and its own dependencies automatically; there is no manual step
for the end user.)

## Quickstart

```
cd your-repo
avaril init                                    # creates .avaril/
avaril doctor                                  # confirms it's set up correctly
avaril pull --pack-id <id> --pack-version <v>  # not live yet -- see Beta above
avaril scan
avaril report
```

## Commands

| Command | Flags | Notes |
|---|---|---|
| `avaril login` | `--api-key <key> [--base-url <url>]` **or** `--token <token>` | Two unrelated auth paths on one command, picked by flag: `--api-key` is the original flow (`~/.avaril/credentials`, used by `manifest pull`); `--base-url` applies only to `--api-key`. `--token` writes an enforcement token to the OS user config dir (used by `pull`/`scan`/`push`). Never writes a token under `.avaril/`. |
| `avaril init` | `--force`, `--path <repo>` | Creates `.avaril/{config.yaml,manifest.yaml,.gitignore}`. Idempotent — skips existing files unless `--force`. |
| `avaril doctor` | `--path <repo>` | Reports config validity, `.avaril/` writability, auth presence, supported languages. |
| `avaril pull` | `--pack-id <id> --pack-version <version> [--offline] [--path <repo>]` | Fetches and checksum-verifies a constraint pack into `.avaril/packs/<id>/<version>/`, pins it in `config.yaml`. `--offline` uses the cache only, exit 2 if missing. |
| `avaril scan` | `--paths <p>` (repeatable), `--fail-on <severity>`, `--max-unverified <pct>`, `--path <repo>` | Runs the pinned pack's constraints, writes `.avaril/scans/<scan_id>/{result.json,result.sig}` and `scans/latest.json`. Warns (doesn't fail) on a dirty working tree. |
| `avaril report` | `--format table\|json\|sarif`, `--scan-id <id>`, `--fail-on <severity>`, `--path <repo>` | Prints the latest (or named) scan's result. `sarif` validates against the real OASIS SARIF 2.1.0 schema, for code-scanning integration. Respects `NO_COLOR` and TTY detection. |
| `avaril explain <constraint_id>` | `--path <repo>` | Shows a constraint's detectors, expected outcomes, and fix guidance. |
| `avaril verify <result.json>` | — | Checks schema, canonical form, and signature against the sibling `result.sig`. Non-zero exit on any mismatch (tamper-evident). |
| `avaril push` | `--scan-id <id>`, `--path <repo>` | Uploads a result with retries, idempotent on `scan_id`. Payload is checked against a hard-coded allowlist before it's ever sent — see Privacy below. |
| `avaril manifest pull` | `--environment <env>`, `--out <path>` | **AVRL manifest**, unrelated to the above — fetches the signed process-registry snapshot, default `.avaril/manifest.avrl` (cwd-relative, pre-dates repo-root detection). |
| `avaril manifest verify <path>` | — | Verifies an AVRL manifest's signature and expiry. |
| `avaril manifest inspect <path>` | — | Human-readable dump of an AVRL manifest's entries. |

### Two manifests, same directory name

`.avaril/manifest.yaml` (declarative, plain YAML, written by `init` or by
hand — what entities/purposes/retention a scan checks the code against)
and the AVRL binary manifest `avaril manifest pull` fetches (a signed
process-registry snapshot the Python SDK's `enforce_manifest` reads at
runtime) are **two different things that happen to share the word
"manifest."** See `avaril_cli/schemas/README.md` for the full comparison
table. This was a known open question going into Phase 2 and is still
unresolved at the product level — don't assume one is a stand-in for
the other.

## `.avaril/` layout

Every file any command writes into a *scanned* repo lives here — nothing
else in that repo is ever touched (enforced by a dedicated test suite,
`tests/test_e2e.py`/`tests/test_paths.py`, that snapshots the repo
before/after each command and asserts every new path starts with
`.avaril/`):

```
.avaril/
  config.yaml              # pinned pack, thresholds, ignore globs, sink_allowlist (non-secret)
  manifest.yaml            # declared entities/purposes/retention/processors
  .gitignore                # ignores cache/, scans/ by default; config/manifest stay committable
  packs/<pack_id>/<version>/
    pack.json
    constraints/*.yaml
  scans/<scan_id>/
    result.json
    result.sig
  scans/latest.json         # pointer: { "scan_id": "..." }
  cache/                    # call-site AST cache, content-hash keyed, invalidated on CLI-version change
  logs/                     # reserved — path exists (AvarilPaths.logs_dir), no logger writes here yet
```

Credentials and tokens are deliberately **outside** this tree, on
purpose — a credential inside a scanned repo could end up committed by
accident:

- `~/.avaril/credentials` — the original `--api-key` flow (`0600` perms).
- OS user config dir (`$XDG_CONFIG_HOME/avaril/token`, else
  `~/.config/avaril/token`) — the `--token` enforcement token, and a
  lazily-generated local Ed25519 self-signing key
  (`.../avaril/signing_key`).

`AVARIL_TOKEN` (env var) is checked before either on-disk token path, so
CI can supply one without ever writing to disk.

## Exit codes

| Code | Constant | Meaning |
|---|---|---|
| 0 | `EXIT_OK` | Clean — no failures at/above `--fail-on`, coverage at/above `--max-unverified`. |
| 1 | `EXIT_FAIL` | At least one constraint failed at or above the `--fail-on` severity. |
| 2 | `EXIT_USAGE` | Config/usage error (e.g. no pack pinned, `--offline` with nothing cached). |
| 3 | `EXIT_COVERAGE` | Verification coverage fell below `--max-unverified`. |
| 4 | `EXIT_AUTH` | Auth or network failure talking to the backend. |

These are a tested contract (`avaril_cli/errors.py`, `avaril_cli/gate.py`)
for every command added from Phase 0 onward. The two original AVRL
commands (`manifest verify`/`manifest inspect`) predate this contract and
still use their own literal `sys.exit` calls — and disagree with each
other on which code means "corrupt signature" (2 vs. 1). Known,
documented, not fixed in this pass (compatibility — see hard rule 9).

## Privacy guarantees

`avaril push` never uploads source code or snippets — only constraint
id, outcome, severity, file path, line range, evidence hash, counts, and
versions. This isn't just a convention: `avaril_cli/privacy_check.py`'s
`check_payload` is a **default-deny allowlist** over every key anywhere
in the push payload (not a blocklist for "looks like code" names) — an
unrecognized key refuses the push outright. None of the result schemas
have a snippet/source-text field to begin with (Phase 1 design choice);
this check guards against one quietly being added later, or a tampered
`result.json` being pushed. `Finding` records `file`/`lines`/
`evidence_hash` only — the matched source text itself is hashed, never
stored, on disk or in any payload (`tests/test_call_site_e2e.py`'s
`test_result_json_never_contains_fixture_source_text` greps for it).

## CI example (GitHub Actions)

```yaml
- name: Avaril enforcement scan
  env:
    AVARIL_TOKEN: ${{ secrets.AVARIL_TOKEN }}
  run: |
    avaril pull --pack-id dpdpa-core --pack-version 1.0.0
    avaril scan --fail-on high --max-unverified 20
    avaril report --format sarif > avaril-results.sarif

- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: avaril-results.sarif

- name: Push findings
  if: always()
  env:
    AVARIL_TOKEN: ${{ secrets.AVARIL_TOKEN }}
  run: avaril push
```

Running inside GitHub Actions also upgrades the scan's `attestation`
from `self-reported` to `ci-attested` automatically (`scan.py`'s
`detect_ci_attestation`, keyed off `GITHUB_ACTIONS=true`), binding the
result to the run's commit/repo/actor — no extra flag needed.

## Development

```
pip install -e ".[dev]"
pytest
```

`scripts/benchmark_scan.py` times the call-site/sink detectors'
file-scanning core on a synthetic large repo — see `BENCHMARKS.md` for
the last recorded numbers and why it's a process pool, not a thread pool.

`manifest_commands.py` imports `avaril` (the `sdk/avaril-py` package)
directly for the AVRL binary format and Ed25519 signing/verification —
`signing.py` (used by `scan`/`verify`/`push`) reuses the same primitive
rather than building a second signing mechanism.
