Metadata-Version: 2.5
Name: cyberxyz-scanner
Version: 1.4.74
Summary: CyberXYZ CLI: supply-chain security for your machines, projects and CI (install-time proxy, audits, depalert gate, code scanning)
Project-URL: Homepage, https://cyberxyz.io
Project-URL: Documentation, https://cyberxyz.io/docs/cli.html
Author-email: CyberXYZ Security Team <support@cyberxyz.io>
License: Proprietary
License-File: LICENSE
Keywords: CVE,EPSS,GHSA,MCP,OSV,scanner,security,vulnerability
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: License :: Other/Proprietary License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
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 :: Libraries :: Python Modules
Requires-Python: >=3.8
Requires-Dist: click>=8.0
Requires-Dist: mcp<3,>=1.2; python_version >= '3.10'
Requires-Dist: pip-audit>=2.6
Requires-Dist: pipdeptree>=2.0
Requires-Dist: python-dotenv>=1.0
Requires-Dist: pyyaml<7,>=6.0.1
Requires-Dist: requests>=2.28
Requires-Dist: rich>=13.0
Requires-Dist: tabulate>=0.9
Description-Content-Type: text/markdown

# cyberxyz-scanner

CyberXYZ Security CLI. Real-time supply-chain protection for npm, PyPI, Go and .NET (NuGet) on macOS, Linux and Windows.

[![PyPI version](https://img.shields.io/pypi/v/cyberxyz-scanner.svg)](https://pypi.org/project/cyberxyz-scanner/)
[![Python](https://img.shields.io/pypi/pyversions/cyberxyz-scanner.svg)](https://pypi.org/project/cyberxyz-scanner/)
[![License](https://img.shields.io/badge/license-Proprietary-blue.svg)](LICENSE)

The CLI pairs with the CyberXYZ platform to give you per-machine package inventory, proxy
enforcement on every `npm`, `pip`, `go` and `dotnet` install, and CI/CD gating on flagged
dependencies. It is the implementer's interface to a platform that also exposes the same
controls in a web dashboard.

**Full documentation:** [cyberxyz.io/docs/cli.html](https://cyberxyz.io/docs/cli.html) covers
install, login, proxy setup, every audit subcommand, API key auth, CI/CD integration,
config / env vars, and troubleshooting. This README is the quick start.

## Install

The package is published on PyPI as `cyberxyz-scanner`. The CLI binary it installs is named
`xyz`.

### With pip

```bash
pip install cyberxyz-scanner
```

### With uv

```bash
uv pip install cyberxyz-scanner
```

Verify the install:

```bash
xyz --help
```

## Quick start (one-time per machine)

```bash
# 1. Sign in. Opens your browser at app.cyberxyz.io; sign in the way you do on the
#    dashboard (password + authenticator app or passkey, or SSO), check the code
#    matches the terminal, and approve. The CLI never sees your password.
xyz login

# 2. Enroll this machine. One command:
#    - Registers the device with your organization
#    - Points npm (~/.npmrc), pip, Go (GOPROXY) and NuGet at the CyberXYZ proxy,
#      for each one that is installed
#    - Installs the background service for dashboard "Scan now" support
#      (LaunchAgent on macOS, systemd --user on Linux, Task Scheduler on Windows)
xyz proxy setup --machine-name "Alex's MacBook"
```

Over SSH or on a headless box, `xyz login --no-browser` prints the link and code to open
on any other device (the browser is also skipped automatically when `SSH_CONNECTION` is set
or Linux has no display). The legacy email and password prompt is still available as
`xyz login --password` (or `$XYZ_EMAIL` / `$XYZ_PASSWORD` for scripts); accounts with MFA
or an SSO-enforcing organization must use the browser login. `xyz logout` revokes the
session on the server and removes it locally.

The CLI session stays signed in while you use it and expires after 90 days without use.
While the CyberXYZ agent runs on the machine it refreshes the session daily, so you only sign in
once; a machine that stays off for 90 days signs out.
It is stored owner-only in `~/.xyz/config.json`. Each
machine's session is listed under **Settings > CLI sessions** in the dashboard, where you
(or an org admin) can revoke it. Login codes expire after 10 minutes and work once: only
approve a code you just requested yourself, never one someone sent you.

That's it. Every later install on this device goes through the CyberXYZ proxy: the exact
package version is checked before it downloads, blocked or quarantined packages are
refused with the reason, and the install shows up in your dashboard.

For fleets, skip the interactive login: an org admin creates an enrollment token in the
dashboard (Machines > Enrollment) and MDM runs
`xyz proxy setup --enrollment-token pxe_xyz_...` (or places the token at
`/Library/Application Support/CyberXYZ/enrollment-token`).

For environments that should not run a long-running background process (CI build agents,
sealed builds), pass `--no-install-daemon`.

On company-managed machines, install the agent at system level so developers cannot stop it:

```bash
sudo xyz proxy setup --system          # macOS / Linux
xyz proxy setup --system               # Windows, from an elevated PowerShell
```

This installs a root/SYSTEM service (macOS LaunchDaemon `io.cyberxyz.agent`, Linux
`cyberxyz-agent.service`, Windows scheduled task `CyberXYZAgent`) that starts at boot and
restarts if it is killed. The macOS `.pkg` does the same, and enrolls automatically when MDM
places an enrollment token at `/Library/Application Support/CyberXYZ/enrollment-token`.
Without `--system` (or without admin rights) setup installs the per-user service as before.

## Always-on protection

Every minute the background agent checks what each package manager will actually use,
repairs anything that no longer points at the CyberXYZ proxy, and reports the result to the
dashboard with its heartbeat. Repairs never remove a private or internal registry, and config
files are only written for tools that are installed:

| Tool | What is checked and repaired |
|---|---|
| npm, pnpm | `~/.npmrc` registry + token, pnpm's global rc; as root also `<npm prefix -g>/etc/npmrc` |
| yarn | `~/.yarnrc.yml` `npmRegistryServer` (token scoped to the proxy host under `npmRegistries`) and `registry` in `~/.yarnrc` |
| bun | `~/.bunfig.toml` `[install] registry` |
| pip | user `pip.conf` / `pip.ini`: the proxy is the `index-url`; extra indexes on pypi.org are removed, private indexes are kept; as root also the system file |
| uv | `uv.toml`: the proxy is the default `[[index]]`; pypi.org indexes are removed, private indexes are kept; as root also the system `uv.toml` |
| Go | `GOPROXY=<proxy>,direct` (the proxy refuses blocked modules with 403, so there is no fall-through); `<proxy>` only while the network lock is on; as root also `$GOROOT/go.env` |
| NuGet | `NuGet.Config`: the CyberXYZ source is added and nuget.org sources removed; private feeds are kept |
| Poetry | cannot be forced globally, so it is covered by the network lock only |

The system-level agent does this for every local user account, writing files owned by that
user. The machine token is stored where the agent can always read it
(`/Library/Application Support/CyberXYZ/machine-token`, `/etc/cyberxyz/machine-token`,
`%ProgramData%\CyberXYZ\machine-token`, or `~/.xyz/machine-token` per user), so deleting a
config file only gets it rewritten. Registry overrides in shell startup files
(`NPM_CONFIG_REGISTRY`, `PIP_INDEX_URL`, `GOPROXY=direct`, ...) and in the Windows user
environment are reported, never edited.

**Network lock.** When an org admin turns it on, the system-level agent also maps the public
registries (registry.npmjs.org, registry.yarnpkg.com, registry.npmmirror.com, pypi.org,
files.pythonhosted.org, proxy.golang.org) to `0.0.0.0` in the hosts file, so tools that ignore
config still cannot reach them. NuGet is not locked yet (it relies on config). Edits to the
lock are reverted and reported, and the block is removed when the org turns it off.

`xyz proxy remove` and uninstalling the service are reported to the dashboard before anything
is removed. Under the system service, only an administrator can remove it
(`sudo xyz proxy remove`). `xyz proxy status` shows the per-tool report, the service level,
the network lock state, when the agent last reported, and whether the agent is running the CLI
version that is installed. It works without sudo under the system service: config tokens are
compared with a fingerprint (sha256) the service publishes, never with a per-user copy.

**Upgrades.** `pip install -U` / `pipx upgrade` cannot restart a root service. The agent
(1.4.66+) notices the new version on disk and restarts onto it by itself within a few minutes,
between jobs. `sudo xyz proxy restart` does it immediately; the first CLI run after an upgrade
says so when the system agent is still on older code.

### If CyberXYZ is unreachable

The proxy reuses its verdict for any package it checked in the last 24 hours. For anything
else, your org's setting decides: **block** (the default; the developer sees "retry in a
minute") or **allow unchecked**. Org admins change it in Settings > Supply-chain proxy.

## Audit installed packages

Each command below audits the matching ecosystem on this machine, runs the CyberXYZ
watchlist + deep check on suspect packages, and uploads the full inventory to the
platform.

```bash
xyz audit npm                  # local + global node_modules
xyz audit python               # active Python environment via pip
xyz audit venvs                # every virtualenv / conda / pipx environment on the disk
xyz audit go                   # modules of the current Go module / GOPATH
xyz audit nuget                # packages.lock.json files under cwd
xyz audit ide                  # VS Code / Cursor / Windsurf / VSCodium extensions
xyz audit browser              # Chrome / Edge / Brave / Firefox extensions
xyz audit system               # Homebrew, apt, rpm, winget, Chocolatey, macOS apps
xyz audit models               # AI model files and Hugging Face references (see below)
xyz audit exposure             # credentials an install-time payload could steal here (see below)
xyz audit                      # npm, python, venvs, go, ide, browser and system back-to-back
```

A bare `xyz audit` does not run `nuget` (it needs a lockfile), `models` or `exposure`; run
those on their own.

By default each command uses the watchlist pre-filter for speed (~25-40s on a typical
machine). Pass `--full` to skip the pre-filter and deep-check every package (slower but
covers advisory-only matches at scan time).

Conda environments (1.4.70+): a Python package that conda installed from an Anaconda or
conda-forge channel is not a PyPI package, so `xyz audit python` / `venvs` tell the API
where it came from. A conda-only name that PyPI does not have (for example
`anaconda-anon-usage`) is listed as `? name@version not checked: conda channel defaults`
instead of being quarantined as "not on the public registry"; there is no conda registry
we index to check it against. Known-malicious and advisory checks still run by version,
and anything pip or uv installed into a conda env gets the full PyPI checks. A package
counts as conda-installed only when its `INSTALLER` file says `conda` and the env's
`conda-meta/` records it.

### `xyz audit exposure`: what an install-time payload could steal

The Shai-Hulud and Hades waves ran a payload from npm/PyPI install hooks that read every
environment variable, `~/.npmrc` tokens, cloud credentials and CI secrets, then published
itself with what it found. `xyz audit exposure` lists what such a payload would find on this
machine or in this CI step:

```bash
xyz audit exposure                       # table
xyz audit exposure --json -o exposure.json
xyz audit exposure --fail-on high        # CI gate: exit 1 on a high item
```

| Category | What is checked | High when |
|---|---|---|
| `env` | environment variables whose name (`*_TOKEN`, `*_SECRET`, `*_PASSWORD`, `*_API_KEY`, `AWS_*`, `PYPI*`, `NPM_TOKEN`, `NODE_AUTH_TOKEN`, `TWINE_PASSWORD`, `GITHUB_TOKEN` ...) or value (npm, PyPI, GitHub, GitLab, AWS key shapes, URLs with a password) looks like a credential | publish token or cloud key |
| `ci` | GitHub Actions / GitLab / Azure Pipelines: `GITHUB_TOKEN`, `ACTIONS_ID_TOKEN_REQUEST_URL` (= `id-token: write`), how many secret-looking variables this step sees | OIDC token request granted |
| `npm` | `_authToken` / `_auth` / `_password` in `~/.npmrc` (or `NPM_CONFIG_USERCONFIG`) and the project `.npmrc`; `npmAuthToken` in `.yarnrc.yml` | always (publish token) |
| `pypi` | `password` in `~/.pypirc` | always |
| `netrc` | `~/.netrc` (`_netrc` on Windows) machine passwords | github.com, gitlab.com, npm, PyPI hosts |
| `docker` | inline `auths` in `~/.docker/config.json`; a `credsStore` / `credHelpers` is listed as a safer setup | always |
| `aws` | `~/.aws/credentials` and `config`: static keys (high), session keys (medium), SSO (safer setup) | static key |
| `gcloud` | `application_default_credentials.json`, `legacy_credentials/*/adc.json`, `credentials.db`, `GOOGLE_APPLICATION_CREDENTIALS` | refresh token or service-account key |
| `kube` | kubeconfig (`KUBECONFIG` list) embedded `token`, `client-key-data`, `password`; `exec` plugins are a safer setup | embedded credential |
| `gh` | `oauth_token` in the gh CLI `hosts.yml` (keyring = safer setup) | always |
| `git` | `~/.git-credentials` | publish hosts |
| `ssh` | private keys in `~/.ssh` without a passphrase (only the key header is read) | never (medium) |

Each item is reported by name, location (your home shown as `~`) and a fingerprint: the
first 8 hex characters of sha256(value), enough to see the same token in two places, not
enough to recover it. Values are never printed, written to a file or uploaded; the audit is
offline, read-only and starts no processes. In CI it also prints what to change: run installs
with `--ignore-scripts`, keep `permissions:` minimal (grant `id-token: write` only to the
deploy job, pass secrets to the one step that needs them) and route installs through the
proxy with `xyz ci protect`. Exit codes: 0, or 1 when an item is at or above `--fail-on`
(default `none`, report only).

## Fix, blast radius, SBOM and AI models (1.4.62+)

All four read the manifests and lockfiles in the current directory, the same ones
`depalert scan` reads: `package.json` / `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`,
`bun.lock`, `requirements*.txt`,
`Pipfile` / `Pipfile.lock`, `pyproject.toml` / `poetry.lock` / `uv.lock`,
`go.mod` / `go.sum`, and NuGet `packages.lock.json`.

### `xyz fix`: an upgrade plan for flagged dependencies

```bash
xyz fix                        # plan only: package, current, issue (KEV badge), → target, why
xyz fix --write                # show a unified diff, confirm, then edit the specs
xyz fix --write --yes          # no prompt (CI bots)
xyz fix --json
```

Every package is checked in one `/proxy/check/batch` call. For each flagged one
(block, quarantine, alert, or any advisory) the plan gives the nearest clean release
above the installed version. A clean installed version is never told to move. A
transitive package is traced to the direct dependency that pulls it ("via express →
debug, bump express"); transitive pins are never edited.

`--write` edits only direct dependency specs: `package.json` (keeps `^` / `~` / `>=`
and the file's formatting; complex ranges are listed for a manual edit) and `==` pins
in `requirements*.txt` (extras, markers and comments kept; hash-pinned lines are left
for `pip-compile`). Lockfiles are never touched: run `npm install`, `poetry lock`,
`uv lock` or `pip-compile` afterwards. Exit codes: `0` plan complete, `1` a flagged
package has no clean target, `4` could not check (backend unreachable, or no credential).

### `xyz impact`: who is exposed when a package goes bad

```bash
xyz impact debug --version 2.6.8          # ecosystem guessed from the project, else npm
xyz impact requests -e pypi --json
```

Prints the direct dependents and whether each declared range admits the affected
version, how many packages reach it on a required path vs only through an optional
extra, and the top paths. Run inside a project, it also says whether this project
reaches it and through which direct dependency. "Not yet indexed" means the path is
unknown, not that there is none.

### `xyz sbom`: CycloneDX 1.5 or SPDX 2.3

```bash
xyz sbom -o sbom.cdx.json                  # CycloneDX 1.5 JSON with a vulnerabilities section
xyz sbom --format spdx -o sbom.spdx.json   # SPDX 2.3 JSON
xyz sbom --no-verdicts                     # components only, no network call
```

Components carry a purl (`pkg:npm/…`, `pkg:pypi/…`, `pkg:golang/…`, `pkg:nuget/…`),
the version and a scope (`required` / `optional`; dev dependencies are `excluded`),
and the dependency graph comes from the lockfile when it records one. Vulnerabilities
list the advisory ids, CVSS and EPSS ratings, a `cyberxyz:kev` property for CISA KEV
entries, and the safe version as the recommendation.

### `xyz audit models`: AI model artifacts

```bash
xyz audit models                           # cwd + the Hugging Face cache
xyz audit models ./ml --no-hf-cache
xyz audit models --aibom -o aibom.cdx.json # CycloneDX 1.6 ML-BOM
```

Finds model files (`.safetensors .bin .pt .pth .ckpt .gguf .onnx .pkl .h5`) and hashes
them with a streamed sha256 (files over `--max-hash-size` are listed as UNHASHED),
Hugging Face references in code (`from_pretrained`, `hf_hub_download`, `pipeline(model=…)`,
`snapshot_download`, flagging `trust_remote_code=True`), and models in
`~/.cache/huggingface/hub`. Files are never loaded or unpickled. The table shows each
repo or file with its verdict, load safety, trust_remote_code and reasons. NOT WATCHED,
PENDING and UNSCANNED are not verdicts. Exit `1` when anything is MALICIOUS.

### KEV

When an advisory is in CISA's Known Exploited Vulnerabilities catalog, a bold **KEV**
marker leads the score cell in `depalert scan`, the vulnerability tables and the
`xyz fix` plan, and SARIF results carry `"kev": true` (plus a `kev` rule tag).

## Use CyberXYZ from AI coding agents (MCP)

`xyz mcp serve` is a [Model Context Protocol](https://modelcontextprotocol.io) server:
Claude Code, Cursor, VS Code (Copilot agent mode), Windsurf, Codex CLI and Gemini CLI
call it to check packages, vulnerabilities, dependencies and AI models *before* they add
or install anything. It uses your `xyz login` session (or `XYZ_API_KEY`), needs
Python 3.10+ (the `mcp` package installs automatically there), and only ever speaks the
protocol on stdout.

```bash
xyz mcp install --client claude-code --hooks   # MCP server + install hook, user scope
xyz mcp install --client cursor --scope project --rules
xyz mcp install --client all --dry-run         # show every diff, change nothing
xyz mcp status                                 # which clients are configured, does auth work
xyz mcp serve --tools                          # list the tools
```

| Client | `--scope user` | `--scope project` |
|---|---|---|
| `claude-code` | `claude mcp add --scope user` (else `~/.claude.json`) | `claude mcp add --scope project` (else `.mcp.json`) |
| `cursor` | `~/.cursor/mcp.json` | `.cursor/mcp.json` |
| `vscode` | printed (`code --add-mcp …` / *MCP: Open User Configuration*) | `.vscode/mcp.json` (`servers`, `type: stdio`) |
| `windsurf` | `~/.codeium/windsurf/mcp_config.json` | global only |
| `codex` | `~/.codex/config.toml` `[mcp_servers.cyberxyz]` | global only |
| `gemini` | `~/.gemini/settings.json` | `.gemini/settings.json` |

Files are merged, never overwritten (other servers and settings stay), the previous
version is kept as `.bak`, and a file that is not plain JSON (comments) is left alone
with the snippet printed. `--rules` adds the CyberXYZ dependency policy where the agent
reads rules (`CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.github/copilot-instructions.md`
between markers, after asking; `.cursor/rules/cyberxyz.mdc`, `.windsurf/rules/cyberxyz.md`).

**Tools**

| Tool | What the agent gets |
|---|---|
| `check_package` / `check_packages` | install-time decision (allow / alert / quarantine / block) with behaviour signals (malicious release, typosquat, install scripts, dependency confusion, org block), advisories with CVSS / EPSS / KEV / fixed-in, a safe version and a one-line recommendation |
| `upgrade_plan` | is the installed version affected, nearest clean release above it, latest clean |
| `scan_project` | every manifest / lockfile in the project checked, flagged packages with the path from a direct dependency, what to bump, npm overrides |
| `dependency_paths` / `package_dependencies` / `blast_radius` | why a transitive package is installed, what a package pulls in, who depends on a bad version |
| `get_vulnerability` / `search_vulnerabilities` | one advisory in full; the advisory corpus by package, text, ecosystem, severity |
| `model_risk` / `model_load_safety` / `check_model_file` | Hugging Face model dependency risk, per-file load safety, lookup by sha256 |
| `machines_with_package` | which machines in your organization installed a package, at which version |

Plus the `cyberxyz-dependency-policy` prompt and resource: check before adding, never
install block / quarantine, prefer the safe version, unknown needs a human, never
`trust_remote_code` or pickle-load a model that is not CLEAN.

**Install hook (Claude Code).** `--hooks` adds a `PreToolUse` hook on Bash that runs
`xyz hook check-install`: `npm|pnpm|yarn|bun add/install <pkg>`, `pip / uv pip install
<pkg>`, `uv add`, `poetry add`, `pipx install`, `go get / go install`, and
`dotnet add package` are checked in one call, and blocked or quarantined packages are
denied with the reason and a safe version. Lockfile installs (`npm install`, `npm ci`,
`pip install -r …`) pass: the proxy checks what they fetch. `--hook-strict` asks you on
alerts; the hook fails open with a warning if CyberXYZ is unreachable unless
`--hook-fail-closed` is set. It never auto-approves a command.

Lookups from the MCP server and the hook identify themselves (`X-XYZ-Client: mcp` /
`agent-hook`) so the platform can keep them out of the install log: asking about a
package is not installing it.

**Which installs did an agent make? (1.4.71+).** Coding agents set a marker in the
shells where they run commands (`AI_AGENT`, `CURSOR_AGENT`, `CODEX_THREAD_ID`,
`COPILOT_AGENT`, `GEMINI_CLI`, ...). The CLI sends it as `X-XYZ-Agent`. For npm and pip,
opt in with `xyz proxy agent-hints --enable` (or `xyz proxy setup --agent-hints`): in an
agent's shell npm and pip then add `xyz-agent/<name>` to their user agent, and the
dashboard marks those installs and registry misses as agent-driven. A person's terminal
is unchanged, nothing is inferred, and `--disable` removes the hook.

## Other useful commands

```bash
# One-off safety check on a single package + version
xyz check axios 1.14.1 -e npm

# CI/CD gate. Non-zero exit on flagged packages.
xyz depalert scan --package-lock package-lock.json --fail-on block
xyz depalert scan --yarn-lock yarn.lock           # yarn 1 classic and yarn 2-4 (berry)
xyz depalert scan --pnpm-lock pnpm-lock.yaml      # lockfileVersion 5.x, 6.x, 9.x
xyz depalert scan --bun-lock bun.lock             # text lockfile; not the binary bun.lockb
xyz depalert scan --requirements requirements.txt --fail-on quarantine
xyz depalert scan --requirements poetry.lock      # also Pipfile.lock, uv.lock
xyz depalert scan --go-sum go.sum
xyz depalert scan -p axios@1.14.1 -p lodash@4.17.21

# SBOM upload (runs syft on a path, or takes a Syft / CycloneDX JSON SBOM)
xyz inventory upload ./my-app
xyz inventory upload --sbom syft.json

# Diagnostic / housekeeping
xyz --version
xyz proxy status               # proxy config per tool, service level, network lock
xyz proxy whoami               # what (org, machine) does my token resolve to
xyz proxy restart              # restart the agent onto the installed CLI (sudo for the system service)
xyz proxy remove               # restore default registries (reported to your dashboard)
xyz scans list                 # history of recent scans for your org
xyz upgrade                    # pull the latest release from PyPI
```

## Code, IaC, secrets and image scanning (`code-scan`)

`xyz code-scan` runs open-source scanners installed on your machine (it can install missing
ones with your own package manager, after asking) and merges what they report into one table,
JSON document or SARIF 2.1.0 file. It works without logging in; when you are signed in the
results also go to your CyberXYZ dashboard (`--no-upload` or `XYZ_NO_UPLOAD=1` to opt out;
secret values are never sent). For installed packages use `xyz audit`.

Run it from a project. `/`, your home, the temp directory, or a folder with no project
markers (`.git`, `package.json`, `pyproject.toml`, `go.mod`, `*.csproj`, ...) is scanned only
after a yes on a terminal; in CI or without a terminal it is refused unless `--force-target`.

```bash
xyz code-scan secrets .                 # hardcoded credentials (values always redacted)
xyz code-scan iac ./infra               # Terraform, CloudFormation, Kubernetes, Helm, Dockerfile
xyz code-scan image python:3.11-slim --sbom sbom.cdx.json   # vulns + misconfig + secrets, CycloneDX SBOM
xyz code-scan code .                    # SAST with the built-in xyz rules
xyz code-scan code . --config ./my-rules.yml                 # add your own opengrep rules
xyz code-scan ci .                      # GitHub Actions / GitLab CI / Azure Pipelines hardening (built in)
xyz code-scan all . --format sarif -o xyz.sarif --fail-on high
xyz code-scan engines                   # which engines are installed, versions, paths
```

| Command | Engine | Fallback / option |
|---|---|---|
| `secrets` | gitleaks (`dir` on 8.19+, `detect --no-git` before) | trivy `fs --scanners secret`, or `--engine trivy`; `--history` adds `gitleaks git` (no trivy fallback) |
| `iac` | trivy `config` | checkov with `--engine checkov` |
| `image` | trivy `image` (vuln, misconfig, secret) | `--sbom FILE` writes CycloneDX |
| `code` | opengrep + built-in xyz rules + any `--config` | without opengrep: bandit for Python, gosec for Go |
| `ci` | built in (no engine) | `--first-party-pins off\|info\|low\|medium\|high` (default `info`) |
| `all` | `secrets` + `iac` + `code`, merged (`--include-ci` adds `ci`; default from the next minor) | `--skip-missing` runs whatever is installed; add `--fail-on-partial` to exit 5 when a scanner was skipped |

The built-in rules (`xyz_cli/rules/`) cover supply-chain patterns: `shell=True` and
`os.system` with dynamic commands, eval/exec of downloaded data, `curl | sh` in scripts, CI
files and Dockerfiles, `pickle.load`, `torch.load` without `weights_only=True`,
`trust_remote_code=True`, hardcoded cloud keys and private keys, disabled TLS verification
(`verify=False`, `NODE_TLS_REJECT_UNAUTHORIZED=0`, `InsecureSkipVerify`), `child_process.exec`
with template strings, and npm install scripts that download binaries. Without opengrep the
built-in rules do not run and xyz says so.

**CI pipeline hardening (`ci`).** Supply-chain attacks now land in the pipeline itself: a
third-party action whose tag is moved to malicious code (tj-actions/changed-files), a
`pull_request_target` workflow that builds a fork's code with the repository's secrets, an
issue title pasted into a shell script, a postinstall hook that reads the deploy token from
the job's environment (Shai-Hulud). Trivy and Checkov barely look at workflow files, so
`xyz code-scan ci` checks them itself: it parses `.github/workflows/*.yml`, `action.yml`,
`.gitlab-ci.yml` and `azure-pipelines*.yml` and needs nothing installed. Findings are
category `iac` (they go to the dashboard's IaC tab) with rule ids:

| Rule | Severity | What it finds |
|---|---|---|
| `ci-unpinned-action` | high (branch) / medium (tag); `actions/*`, `github/*` at `--first-party-pins` (default info) | `uses:` an action or reusable workflow by tag or branch instead of a 40-character commit SHA |
| `ci-unpinned-docker-image` | medium | `uses: docker://image:tag` without `@sha256:` |
| `ci-secrets-to-unpinned-action` | high | an unpinned third-party action or reusable workflow gets secrets (`with:`, `env:`, job env, `secrets: inherit`) or runs in a job with `id-token: write` |
| `ci-pwn-request` | high (low behind a same-repository guard) | `pull_request_target`, `workflow_run` or `issue_comment` workflows that check out or fetch the pull request's code |
| `ci-script-injection` | high | `${{ github.event.pull_request.title }}`, bodies, comments, commit messages, `github.head_ref` and other attacker-controlled values inside `run:` or `actions/github-script`; Azure `$(Build.SourceBranch)`-style macros in scripts |
| `ci-input-injection` | low | free-text `${{ inputs.x }}` inside `run:` (boolean, number and choice inputs are skipped) |
| `ci-permissions-missing` | medium | no top-level `permissions:` and a job without its own |
| `ci-permissions-write-all` | high | `permissions: write-all` |
| `ci-id-token-workflow-level` | medium | `id-token: write` granted to every job of a multi-job workflow |
| `ci-install-scripts-with-secrets` | medium (low for pnpm/bun, or after `xyz ci protect`) | `npm ci`/`npm install`/`yarn`/`pnpm install`/`bun install` without `--ignore-scripts`, or `pip install` of unpinned or unhashed requirements (requirements files are read), in a job holding secrets or an OIDC grant |
| `ci-checkout-persist-credentials` | medium (write token) / low (repository default) / info (read-only) | `actions/checkout` without `persist-credentials: false` in a job that later runs third-party code |
| `ci-gitlab-unpinned-include` | medium | GitLab `include:` of a project without a SHA `ref:`, a `remote:` without `integrity:`, or a component on a moving version |
| `ci-invalid-workflow` | medium | the file is not valid YAML, so none of the checks ran on it (line and column from the parser); never reported as clean |

Safe patterns are not reported: `${{ github.event.pull_request.number }}` or `head.sha` in a
script, untrusted values passed through `env:` and used as `"$TITLE"`, and `actions/checkout`
in a `pull_request_target` workflow without a pull-request `ref:`. Put `# xyz:ignore` (or
`# xyz:ignore[rule-id]`) on a line to accept a finding. Fix guidance is in each message; the
SHA to pin to is not looked up (the check is offline).

Common options: `--format table|json|sarif`, `-o FILE`, `--fail-on critical|high|medium|low|none`
(default `low`, meaning any non-info finding fails), `--timeout SECONDS` per engine (default
900). Severities are normalised to critical/high/medium/low/info, paths are relative to the
scanned directory, and duplicate findings are merged, including the same secret found by two
engines. Secret values are never printed in any format; secret findings carry no snippet.
SARIF suppressions (`# nosec`, `nosemgrep`, `gitleaks:allow`) are honoured.

| Exit | Meaning |
|---|---|
| 0 | Clean: nothing at or above `--fail-on` |
| 1 | Findings at or above `--fail-on` |
| 2 | Usage error |
| 3 | Engine not installed: a requested scanner could not run (the install command for your OS is printed) |
| 4 | Engine error: non-zero exit, timeout, or no report (the engine's stderr tail is printed) |
| 5 | Partial, opt-in (`all --skip-missing --fail-on-partial`): nothing at or above `--fail-on` in the scanners that ran, but at least one was skipped because its engine is missing. Without `--fail-on-partial`, `--skip-missing` exits 0 in that case, as before |

A run that skipped a scanner is never reported as clean. The table ends with, for example,
`Partial: secrets scanned, IaC and SAST skipped (engines missing).` instead of `No findings`,
and JSON, SARIF (`runs[0].properties`) and the dashboard upload carry `status`
(`complete` / `partial` / `failed`), `coverage` and one `scanners` entry per requested scanner
(`ran`, `skipped` with `reason: "engine missing"`, or `failed` with `reason: "engine error"`).

**Secrets in git history (`--history`).** A secret that was committed and later deleted is
gone from the working tree but not from the repository: every clone and the hosting
provider still have it. `xyz code-scan secrets --history` (and `xyz code-scan all --history`)
also runs `gitleaks git` over the commit history. Each history finding carries the commit
sha, its author date, the file path and line at that commit, and `in_head`: whether HEAD still
holds the same secret. One that is gone from HEAD says *removed from tree but still in git
history — rotate it, rewriting history is not enough once pushed*; one committed only on
another branch says so. A secret still in HEAD that the working-tree scan already reports is
not listed twice.

```bash
xyz code-scan secrets . --history                         # every ref (git log --all)
xyz code-scan secrets . --history --history-depth 200     # last 200 commits of HEAD
xyz code-scan secrets . --history --since-commit origin/main   # only what this branch adds
```

The same guarantees hold: values are never printed or uploaded (gitleaks' report is read
from a pipe, never written to disk; xyz checks HEAD for the value by handing it to `git grep`
on stdin, then drops it), the repo's `.gitleaks.toml` is extended and `.gitleaksignore` /
`gitleaks:allow` are honoured, and `--exclude`, the dependency/build-folder skip and the
known-benign values still apply (gitignored paths are *not* skipped in history: a `.env`
committed before it was gitignored is what this is for). Trivy has no git-history mode: with
only trivy installed, `--history` stops with "engine not installed" (exit 3) and scans
nothing; `all --history --skip-missing` reports `secrets history skipped`. History findings
appear in JSON (`commit`, `commit_date`, `in_head`, `reachable_from_head`), in SARIF
`properties` (`commit`, `commitDate`, `inHead`) and in the dashboard upload. In CI, check out
the history (`actions/checkout` with `fetch-depth: 0`); xyz warns when the clone is shallow.

**Installing the engines.** xyz never downloads an engine itself. It looks on `PATH` and in
the usual install folders (Homebrew's `bin`, `~/.local/bin`, `~/.opengrep/cli`). When an
engine is missing and you are at a terminal, it asks once for all of them:

```
Missing engines for this scan:
  trivy     brew install trivy
  opengrep  curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh | bash
            (official opengrep install script; installs to ~/.opengrep/cli and links ~/.local/bin/opengrep)
Install trivy and opengrep now (opengrep runs its official install script)? [Y/n]
```

On yes it runs your own package manager (or opengrep's official script), checks the engine is
now there and prints its version and path, and continues the same scan. `--install-engines`
installs without asking (for setup scripts), `--no-install` never asks. In CI (`CI`,
`GITHUB_ACTIONS`, `GITLAB_CI`, ...) or without a terminal xyz never asks and never installs;
it prints the command. `xyz code-scan engines` lists every engine with its version, path and
install command; `xyz code-scan engines --install` installs the missing ones.

Which installer xyz picks, from what the machine already has:

| Engine | macOS | Linux | Windows |
|---|---|---|---|
| trivy | `brew install trivy` | `brew install trivy` if Homebrew is present; otherwise Aqua's official apt or rpm repository steps are printed for you to run (they need `sudo`) | `winget install --id AquaSecurity.Trivy --exact`, else `scoop install trivy` |
| gitleaks | `brew install gitleaks` | `brew install gitleaks`; otherwise printed: `sudo apt-get install -y gitleaks` or `go install` | `winget install --id Gitleaks.Gitleaks --exact`, else `scoop install gitleaks` |
| opengrep | official script `curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh \| bash` (there is no Homebrew formula and no PyPI package; the script verifies the release signature when `cosign` is installed) | same | official `install.ps1` from the same repo, via PowerShell |

The fallbacks are printed only, never installed by xyz:

| Engine | macOS | Linux | Windows |
|---|---|---|---|
| bandit | `pip install "bandit[sarif]"` | same | same |
| gosec | `brew install gosec` | `go install github.com/securego/gosec/v2/cmd/gosec@latest` | same as Linux |
| checkov | `pip install checkov` | same | same |

**In CI.** GitHub Actions: run `xyz code-scan all . --format sarif -o xyz.sarif` and upload
the file with `github/codeql-action/upload-sarif` (set `if: always()` on the upload so
findings still show when the gate fails). Each run is tagged
`automationDetails.id = xyz-code-scan/<scan>/`, so `secrets`, `iac` and `code` can be
uploaded separately without replacing each other.

```yaml
- run: pip install cyberxyz-scanner "bandit[sarif]"
- run: go install github.com/zricethezav/gitleaks/v8@latest && echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
- run: xyz code-scan all . --skip-missing --format sarif -o xyz.sarif --fail-on high
- uses: github/codeql-action/upload-sarif@v3
  if: always()
  with:
    sarif_file: xyz.sarif
```

GitLab CI: run the same command in a job and keep `xyz.sarif` (or `--format json`) as a job
artifact. GitLab's Security dashboard reads its own report format, not SARIF, so treat the
exit code as the gate.

**Licensing.** xyz uses Trivy (Apache-2.0), Gitleaks (MIT), Opengrep (LGPL-2.1) as external
engines, plus bandit (Apache-2.0), gosec (Apache-2.0) and checkov (Apache-2.0) when present.
They run as separate programs; none of them is bundled. The built-in rules are CyberXYZ's own,
released under MIT. xyz does not use or reference Semgrep Registry rules.

## CI/CD integrations

Set `XYZ_API_KEY` as a secret and add one of these; any push or PR that pulls in a
malicious or vulnerable package fails the build with a clear reason. Create the key at
<https://app.cyberxyz.io/dashboard/api-keys> (a free account is enough:
<https://app.cyberxyz.io/register>). The gate needs it: without a key, or with a key the API
rejects or that is not bound to an organisation, it exits 4 and checks nothing; it never
falls back to anonymous checks.

* GitHub Actions: `uses: CyberXYZSecurity/depalert-action@v1` (GitHub Marketplace)
* GitLab CI/CD catalog: `gitlab.com/cyberxyz/depalert`
* Azure DevOps Pipelines: `integrations/azure-pipelines/cyberxyz-supply-chain.yml`
* Or generate one: `xyz ci init`

### Route the job's own installs through the proxy (`protect`)

`scan` checks lockfiles after the fact. `protect` runs early in the job and points npm,
yarn, pip, uv, Go and NuGet at the CyberXYZ proxy, so a malicious version is refused at
install time, the same way it is on laptops. Recommended: `protect` before the install
steps, `scan` as the gate.

```yaml
# GitHub Actions
- uses: CyberXYZSecurity/depalert-action@v1
  with:
    api-key: ${{ secrets.XYZ_API_KEY }}
    mode: protect          # network-lock: true also blocks the public registries on the runner
- run: npm ci              # goes through the proxy
- uses: CyberXYZSecurity/depalert-action@v1
  with:
    api-key: ${{ secrets.XYZ_API_KEY }}   # mode: scan (default) gates on the lockfiles
```

GitLab: include `gitlab.com/cyberxyz/depalert/protect@1.2.0` and add
`extends: .cyberxyz-protect` (or `- !reference [.cyberxyz-protect, before_script]`) to the
jobs that install dependencies. Any other CI: `eval "$(xyz ci protect --format shell)"`
with `XYZ_API_KEY` set. If CyberXYZ is unreachable, `protect` warns and the build carries
on unprotected; pass `--strict` (`strict: true`) to fail it instead.

The Action (`cli-version`) and the GitLab components (`cli_version`) install the newest
1.4.x CLI, at least 1.4.66 (`>=1.4.66,<1.5`), so CLI fixes reach your pipelines without a
new action release and a 1.5 never arrives unannounced. Pin an exact version (`"1.4.70"`)
if you need reproducible runs, or pass `latest`.

All of them run the same `xyz depalert scan` engine your laptops use. It reads
`package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `bun.lock`, `requirements*.txt`,
`Pipfile.lock`, `poetry.lock`, `uv.lock` and `go.sum`. `--package-lock` also accepts a
yarn, pnpm or bun lockfile and reads it by its name. Each package is checked under its
registry name: npm aliases as the package they install, workspace, `link:`, `file:` and
git dependencies left out. `bun.lockb` is binary and is refused with exit 4: run
`bun install --save-text-lockfile` (the default from bun 1.2) and commit `bun.lock`.

### `depalert scan` exit codes

| Exit | Meaning |
|---|---|
| 0 | Allowed |
| 1 | Block |
| 2 | Quarantine |
| 3 | Alert |
| 4 | Could not check: the API was unreachable, a manifest could not be read, no credential (`xyz login` or `XYZ_API_KEY`), or the credential was rejected / is not bound to an organisation. Never treated as clean; before 1.4.74 a missing credential exited 1, the BLOCK code |

## Re-enroll, rotate, remove

To rotate the proxy token on a device, re-run `xyz proxy setup --machine-name "..."` as
the same user who enrolled it. The platform replaces the old token and the daemon picks up
the new one at next restart. A machine name registered by another member of your org
can only be re-issued by an org admin; pick a different `--machine-name` otherwise.

To remove a device cleanly, delete it from the dashboard Fleet view. The deletion sweeps
proxy_install_log, proxy_tokens, cli_scans, customer_inventory_uploads,
customer_package_inventory and scan_jobs in one transaction. Re-enroll with the same
command above.

## Platform

* Dashboard: <https://app.cyberxyz.io>
* Documentation: <https://cyberxyz.io/docs/cli.html>

## License

Proprietary. See [LICENSE](LICENSE).

## Contact

Email: support@cyberxyz.io
