Metadata-Version: 2.5
Name: bezoar
Version: 1.0.0a3
Summary: Open-source security scanner for the AI-agent supply chain — skills, MCPs, plugins, subagents
License: Apache-2.0
License-File: LICENSE
License-File: NOTICE
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Security
Requires-Python: >=3.11
Requires-Dist: pyyaml>=6.0
Provides-Extra: console
Requires-Dist: bezoar-console==1.0.0a3; extra == 'console'
Provides-Extra: dev
Requires-Dist: hypothesis; extra == 'dev'
Requires-Dist: jsonschema; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-xdist; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: types-pyyaml; extra == 'dev'
Provides-Extra: sbom
Requires-Dist: cyclonedx-bom; extra == 'sbom'
Description-Content-Type: text/markdown

# bezoar

**The open-source security scanner for the AI-agent supply chain** — skills, MCP servers,
plugins, subagents, and CLI add-ons across every major agent platform.

> *Bezoar: the legendary stone believed to neutralize any poison. This one neutralizes
> poisoned agent add-ons.*

`bezoar scan` audits an add-on before it enters your agent's context: static detectors,
four verdict **bands**, and a policy layer that turns those bands into **decisions**.
Think `npm audit` for agent add-ons — offline-first, deterministic, CI-friendly,
SARIF/JSON output.

[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![PyPI](https://img.shields.io/pypi/v/bezoar.svg)](https://pypi.org/project/bezoar/)
[![CI](https://github.com/IsmailKharoub/bezoar/actions/workflows/ci.yml/badge.svg)](https://github.com/IsmailKharoub/bezoar/actions/workflows/ci.yml)
[![SARIF](https://img.shields.io/badge/SARIF-2.1.0-informational)](docs/ci.md)

**1.0.0a2** ships the scanner, CLI, SARIF/JSON reporters, GitHub Action, and web
console. The commands below and the guides in `docs/` describe that shipped
surface.

## 60-second quickstart

Requires Python ≥ 3.11.

```bash
# From this checkout (works before PyPI)
uv tool install .

# After the 1.0.0a2 tag is published
# uv tool install bezoar==1.0.0a2

bezoar version
bezoar scan tests/corpus/benign/claude_code/skill-markdown-lint --fail-on unsafe
```

Point `bezoar scan` at any skill directory or repo root. Flags: `--format
text|json|sarif` (reports go to stdout; there is no `--output`), `--fail-on`,
`--config`, `--changed-since`, `--baseline`. Exit `0` = gate passed, `1` =
band ≥ `--fail-on`, `2` usage, `3` incomplete, `4` internal.

See [Getting started](docs/getting-started.md) for reading findings.

### Illustrative text report

The live text reporter prints a band table, then per-component rows.
The excerpt below is the finding shape: `band` is a fact; `decision` is
policy; locations are raw `file:line:column`; rule ids use `<family>/<rule>`.

```text
✗  band=malicious  decision=block  components=1  findings=4

generic:skill:pdf-optimizer@<hash12>  malicious → block  (band_default)

  [critical] harness_forgery/control-tag-close      SKILL.md:16:1  [hidden]
             Close of a trusted harness wrapper — the breakout primitive
             evidence: </system-reminder>

  [    high] harness_forgery/control-tag-open       SKILL.md:17:1  [hidden]
             Open of a reminder-role control tag with an imperative body
             evidence: <system-reminder priority="critical">

  [    high] harness_forgery/prose-terminator       SKILL.md:22:1  [hidden]
             Prose-form function-end marker (semantic escape)
             evidence: [Function results halted by user]

  [critical] prompt_injection/instruction-override  SKILL.md:27:1
             Instruction-override language
             evidence: Ignore all previous instructions
```

`--format json` and `--format sarif` are the stable machine interfaces.
JSON/SARIF are byte-identical across runs (modulo `stats.duration_ms`).

## Bands and decisions

**Band is a fact about the component. Decision is what the org does.** No exception,
pin, baseline, or vendor trust rewrites a band.

| Band (fact) | Default decision (policy) |
|---|---|
| `safe` | `allow` |
| `potential_risk` | `allow_with_notice` |
| `unsafe` | `alert` |
| `malicious` | `block` |

Four bands, four decisions. The three enforcement outcomes people quote
(allow / alert / block) are the same table: `allow_with_notice` is the fourth decision —
allow, but surface the risk. Capability-only findings (permissions, unpinned-only
supply-chain, metadata) cap the band at `potential_risk` unless an intent-bearing
finding corroborates them. Details: [Policy](docs/policy.md).

## Why bezoar

Enterprises deploy coding agents with no seatbelt. Skills and MCPs are executable
supply-chain artifacts that load straight into an agent's context. Public scanners
catch generic jailbreak phrases and leaked keys; they miss the class that actually
steers the model — **harness-envelope forgery** (content impersonating the host
harness's own control syntax). bezoar maps findings to the
[OWASP Agentic Skills Top 10](https://owasp.org/www-project-agentic-skills-top-10/)
and adds that missing family. See [Harness-envelope forgery](docs/harness-forgery.md).

A default scan performs **no network requests** and **never executes** scanned
artifacts. Same inputs → same JSON/SARIF.

## Comparison

Public posture as of 2026-09. “Harness-envelope forgery” means a first-class,
per-platform vocabulary of harness control syntax plus structural-sequence
matchers — not generic “ignore previous instructions” or “system message”
regexes.

| | Offline | Deterministic | Harness-forgery detection | Policy-as-code | SARIF | Price |
|---|---|---|---|---|---|---|
| **bezoar** | Yes (default) | Yes | Yes | Yes (`.bezoar.yml`) | Yes | Apache-2.0 |
| [Snyk agent-scan](https://github.com/snyk/agent-scan) | No (`SNYK_TOKEN` + hosted analysis) | No | No | No (Snyk org / API) | No (JSON) | Free Snyk account required; commercial platform |
| [Cisco skill-scanner](https://github.com/cisco-ai-defense/skill-scanner) | Partial (patterns/YARA offline; LLM optional) | Partial (LLM judge optional) | No | Yes (YAML presets) | Yes | Apache-2.0 |
| [invariantlabs mcp-scan](https://github.com/invariantlabs-ai/mcp-scan) | No (Invariant API or OpenAI; connects to MCP servers) | No | No | Partial (proxy guardrails) | No | Apache-2.0 + cloud / OpenAI |
| [AIR](https://air.security) | No (inline firewall + cloud) | No | No | Yes (product policy) | Not documented | Commercial |

`invariantlabs-ai/mcp-scan` now redirects into Snyk agent-scan; the column reflects
the historical Invariant CLI that still appears in comparisons. AIR is the commercial
category reference — see [FAQ](docs/faq.md#how-does-bezoar-relate-to-air).

## Docs

| Doc | What it covers |
|---|---|
| [Getting started](docs/getting-started.md) | Install, first scan, reading findings, exit codes |
| [Policy](docs/policy.md) | `.bezoar.yml`, bands vs decisions, exceptions, vendor trust |
| [Rules authoring](docs/rules-authoring.md) | Rule-pack schema, severity / disposition / confidence, `bezoar rules lint` |
| [CI](docs/ci.md) | GitHub Action, baselines, SARIF, shipping reports to a console |
| [Web console](docs/console.md) | Static JSON viewer and `bezoar serve` |
| [Self-hosting](docs/self-hosting.md) | One-click deploys, compose, `/data`, upgrade, backup |
| [Serve config](docs/serve-config.md) | `bezoar-serve.yml` keys, types, env vars |
| [SSO and RBAC](docs/sso-rbac.md) | token / OIDC / proxy, roles, sessions |
| [Audit and retention](docs/audit-retention.md) | Hash-chained JSONL, report pruning |
| [Harness-envelope forgery](docs/harness-forgery.md) | The differentiator, per-platform vocabularies, tuning |
| [Platforms](docs/platforms.md) | Discovery matrix and per-platform limits |
| [FAQ](docs/faq.md) | Offline, determinism, LLM judge, AIR, license, SSO |

## Web console

The console **never computes security facts** — it renders `bezoar.scan/v1` JSON
from the CLI.

- **Viewer:** open the static export (`console/apps/web/out/index.html` or
  `bezoar_console.dist_path()` after `pip install bezoar-console`). Drag-drop a
  JSON report; it stays in IndexedDB. SARIF is rejected — re-scan with
  `--format json`.
- **Serve:** `bezoar serve` (loopback + `/api/v1`). Needs the console extra:

```bash
pip install 'bezoar[console]'   # bezoar-console==1.0.0a2
bezoar serve
```

Details: [docs/console.md](docs/console.md).

## Self-host the console — enterprise features are a config file

Everything the AI-agent security vendors charge enterprise prices for — SSO, role-based
access, a tamper-evident audit log, retention — ships in bezoar as optional YAML.
No SKU, no sales call, no per-seat pricing. One container, one volume, Apache-2.0.

[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/IsmailKharoub/bezoar)
<!-- TODO(ismail): railway template id -->

```bash
# docker compose (any VPS)
curl -fsSLO https://raw.githubusercontent.com/IsmailKharoub/bezoar/v1.0.0a3/deploy/compose/docker-compose.yml
BEZOAR_SERVE_TOKEN="$(openssl rand -hex 32)" docker compose up -d
# → http://127.0.0.1:8080  (add the `tls` profile + BEZOAR_DOMAIN for HTTPS via Caddy)

# Fly.io
fly launch --from https://github.com/IsmailKharoub/bezoar --no-deploy
fly secrets set BEZOAR_SERVE_TOKEN="$(openssl rand -hex 32)" && fly deploy
```

Then turn on the "enterprise" bits by editing `bezoar-serve.yml`:

```yaml
auth:
  mode: oidc
  oidc: { issuer: https://login.example.com, client_id: bezoar, client_secret_file: /run/secrets/oidc }
  roles:
    default: viewer
    groups: { admin: [security-leads], editor: [security-eng] }
audit: { enabled: true }
retention: { reports: { max_age_days: 180 } }
```

| | bezoar | AIR | Alice (Caterpillar) |
|---|---|---|---|
| Self-hosted console | **Yes** — one container | No (cloud-only) | No |
| SSO (OIDC) | **Yes, config** | Enterprise contract | — |
| RBAC | **Yes, config** (viewer / editor / admin) | Enterprise contract | — |
| Audit log | **Yes** — hash-chained JSONL, `bezoar audit verify` | Enterprise contract | — |
| Pricing | Apache-2.0 | Demo-gated, annual | SaaS |

Default posture is unchanged: `bezoar serve` still binds loopback with a random bearer token
and touches no network. Every feature above is opt-in. → [Self-hosting](docs/self-hosting.md)

## GitHub Action

```yaml
# permissions: { contents: read, security-events: write }
- uses: IsmailKharoub/bezoar@v1.0.0a2
  with:
    path: .
    fail-on: malicious
    upload-sarif: true
```

The Action always writes `bezoar.sarif` from `bezoar scan --format sarif`
(stdout; the CLI has no `--output`), uploads it to Code Scanning when
`upload-sarif` is true — including after a failed gate — then re-raises the
CLI exit code.

## License

Apache-2.0. Rule-pack pattern attributions (gitleaks) are in [`NOTICE`](NOTICE)
and [`src/bezoar/data/NOTICE`](src/bezoar/data/NOTICE).
