Skip to main content

cyberxyz-scanner

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

PyPI version Python 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 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

pip install cyberxyz-scanner

With uv

uv pip install cyberxyz-scanner

Verify the install:

xyz --help

Quick start (one-time per machine)

# 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:

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.

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:

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

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

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

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

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 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.

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

# 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.

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 agents .                  # code AI agents and editors run when the checkout is opened (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)
agents built in (no engine)
all secrets + iac + code + agents, 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-compromised-action critical uses: of an action known to have run malicious code, from an offline list of verified entries: actions-cool/issues-helper and actions-cool/maintain-one-comment (every ref of each; all their tags were moved to the Mini Shai-Hulud worm and GitHub disabled both; the maintainers' -backup forks are separate actions and are not flagged), tj-actions/changed-files before 46.0.1 or at its malicious commit (GHSA-mrrh-fwg8-r2c3), reviewdog/action-setup@v1 or its malicious commit (GHSA-qmg3-hpqr-gqvc). Replaces the pin findings for that line; a bare # xyz:ignore does not hide it, only # xyz:ignore[ci-compromised-action]
ci-unpinned-action high (branch) / medium (tag); high for a tag too when anyone can trigger the workflow (issues, issue_comment, discussion, discussion_comment) and the job's token can write or is the repository default; 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 (high on such a trigger, as above) 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
ci-secrets-dump critical (to a file, an artifact or the network) / medium (any other use) ${{ toJSON(secrets) }}, every secret of the repository in one value. Written to a file (>, >>, tee), held by a job that uploads an artifact, or sent with curl/wget/... is critical: that is the Mini Shai-Hulud kit's "Run Copilot" workflow, whose artifact anyone who can read the repository can download. Passing it to a step is medium (and an unpinned action receiving it is ci-secrets-to-unpinned-action)
ci-oidc-root-script high / critical a job with id-token: write that runs a _-named script from the repository root with bun, node or deno (bun run _index.js). Critical with the kit's marks: a workflow named "Dependabot Updates" (GitHub's own Dependabot runs have no workflow file) or OIDC_PACKAGES / WORKFLOW_ID / REPO_ID_SUFFIX in its env, the kit's npm trusted-publishing hijack
ci-action-appended-script high / critical a composite action.yml step that runs bun run $GITHUB_ACTION_PATH/....js (or ${{ github.action_path }}) with if: always(); critical when it is the kit's "Cleanup Action" step, which it appends to every action it can push to before moving the version tags

A trigger counts as "anyone can fire it" unless every activity type it lists needs a maintainer (issues: types: [labeled] alone does not count; opened or edited does). The message says which events and which token. This is the shape of the September 2026 ant-design/pro-components incident: an issues: [opened, edited] workflow with the default token ran actions-cool/issues-helper@v2.2.0 after that tag was pointed at a malicious commit.

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).

Agent and editor autorun (agents). Claude Code and Gemini CLI run the hook commands in a project's .claude/ or .gemini/ settings.json; VS Code and Cursor run a .vscode/tasks.json task marked runOn: folderOpen; agents follow standing instructions in CLAUDE.md, AGENTS.md, .cursor/rules/*.mdc and the like. The Mini Shai-Hulud worm commits exactly these: a SessionStart hook and a folderOpen task that both run node .claude/setup.mjs, which starts .claude/index.js, one obfuscated line of about 500 KB. Opening such a checkout runs the payload. xyz code-scan agents reads those files (it reads and hashes scripts, never runs them) with the same patterns and the same 5,000-character payload line as the CyberXYZ commit watcher. Findings are category iac and tagged agents:

Rule What it finds
agent-hook-command a command in .claude/ or .gemini/ settings.json / settings.local.json: hooks (SessionStart ...), statusLine, apiKeyHelper and the other credential helpers, Gemini MCP server commands
agent-folder-open-task a .vscode/tasks.json task with runOptions.runOn: folderOpen, and the tasks it dependsOn
agent-rule-runs-script agent instructions (CLAUDE.md, AGENTS.md, GEMINI.md, .github/copilot-instructions.md, .cursorrules, .windsurfrules, .cursor/rules/*.mdc) telling the agent to run a script in a dot directory, or to pipe a download into a shell
agent-autorun-payload a script an autorun names, or one at the top of .claude/, .gemini/, .cursor/, .vscode/, .windsurf/ (or their hooks/), with a line of 5,000 characters or more. No snippet is ever shown
agent-known-worm-file a file whose sha256 is a known worm file (the Mini Shai-Hulud settings.json, tasks.json, setup.mjs, index.js and math_init.js seen in ant-design/pro-components and jaredwray/keyv; the June 2026 Hades build's .claude/ and .gemini/settings.json, .vscode/tasks.json, .cursor/rules/setup.mdc and four .github/setup.js runs)
agent-worm-loader a script with the Hades build's loader shape: one line of 1 MB or more that starts try{eval(function(s,n){return s.replace(/[a-zA-Z]/g and ends with a String.fromCharCode join. The loader is re-wrapped with a new key on every run, so its hash is never the same twice. Checked on every script an autorun or build file names and on every .js file directly inside a dot directory (.github/setup.js). Always critical, no snippet
agent-build-file-runs-script a build file that runs node, bun or deno on a .js file inside a dot directory where a build or test runs it unasked. The Hades build injects node .github/setup.js into one build file: package.json test, a Makefile _setup target made the default goal or a prerequisite of all, composer post-install-cmd, the first line of setup.py (os.system), tox.ini, Gemfile / Rakefile / Podfile (system(...)), Makefile.PL, Dockerfile (RUN), CMakeLists.txt (execute_process) or install.sh / setup.sh. Read at the repository root and beside agent config. medium on its own (info for a lifecycle script such as prepare or a script in a subfolder, .github/scripts/x.js; a target or script nothing runs by default is not reported); high when an agent autorun starts the same script; critical when that script is a loader, a payload or a known worm file, or the commit that last changed the build file is forged or backdated
agent-forged-commit a commit on any branch, remote-tracking branch or tag whose author and committer are both its parent's author (name, email, and date to the second) and whose message is the parent's plus [skip ci]: how the Hades build forges the commit it pushes with a stolen token, to up to 10 branches per repository. Critical when it adds or changes agent config, a build file, a dot-directory script or anything in .github/, high otherwise. Checked when the scanned path is the repository root, so a clean default branch still shows infected branches

Severity follows the commit watcher's ladder:

Severity When
info the autorun names no script file (npx prettier, npm run watch): listed, never fails --fail-on, raised only by a strong signal
medium it runs a script file, inline code (node -e, bash -c) or a download piped to a shell, with nothing else pointing at a worm drop
high the same, plus a weak signal: the commit that last changed the file is by a bot (github-actions[bot], renovate ...) or says [skip ci], or copies its parent's author and date while claiming to be committed before its parent; the project sets up autorun for two or more tools (Claude Code hook + VS Code folderOpen task, as the worm does); or a build file runs the same script
critical a strong signal: a payload line in the script it runs or beside it, a known worm file or loader, a backdated commit (authored 365 days or more before it was committed; a rebase of under a year is not backdated), or a forged commit (author and committer copied from the parent, message the parent's plus [skip ci]). agent-autorun-payload, agent-known-worm-file, agent-worm-loader and agent-forged-commit (when it touches config, build files or scripts) are always critical

Commit signals come from local git, from the commit that last changed the autorun file; a shallow clone's boundary commit (CI's default depth 1) says nothing and is skipped.

all runs agents by default. Gate on --fail-on high to fail on drops and print plain hooks that run your own scripts (xyz ci init and the GitHub Action do).

Accepting a reviewed hook. Put xyz:ignore[rule-id] in a comment on the finding's line where the format has comments: // xyz:ignore[agent-folder-open-task] in .vscode/tasks.json (JSONC), <!-- xyz:ignore[agent-rule-runs-script] --> in markdown. The rule id is required. Claude Code and Gemini settings are strict JSON, so for those add an entry to .xyz-ignore.yml at the root of the scanned directory:

agents:
  - rule: agent-hook-command
    path: .claude/settings.json
    command: '"$CLAUDE_PROJECT_DIR"/.claude/hooks/lint.sh'
    reason: our lint hook, reviewed 2026-10-14

An entry accepts that rule, file and exact command (spacing aside), nothing else: a hook the worm merges into the same settings.json is a new finding, and an entry that no longer matches is reported as a warning. Critical findings are never accepted, inline or in the file.

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.

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.

- 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

The GitHub Action (scan mode) and the pipelines xyz ci init writes also run the offline xyz code-scan ci (on .github for GitHub) at --fail-on critical and xyz code-scan agents at --fail-on high: a known-compromised action, or an agent hook or editor task with signs of a worm drop, fails the build. Everything else (an unpinned action that gets secrets, a hook that runs your own script) is printed, so adding the checks does not fail builds over findings a repository already had. The Action's ci-fail-on and agents-fail-on inputs move each threshold (a future v2 of the Action will default ci-fail-on to high); code-scan: "false" turns both off. Neither needs an API key.

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.

# 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

License

Proprietary. See LICENSE.

Contact

Email: support@cyberxyz.io

Metadata

Release files for cyberxyz-scanner 1.4.76

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cyberxyz-scanner 1.4.76
File Size Uploaded Provenance
cyberxyz_scanner-1.4.76.tar.gz 695.3 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for cyberxyz-scanner 1.4.76
File Interpreter ABI Platform
cyberxyz_scanner-1.4.76-py3-none-any.whl Python 3 none any Details

Total release size: 1.1 MB

Release files / cyberxyz_scanner-1.4.76.tar.gz

Download URL cyberxyz_scanner-1.4.76.tar.gz
Size 695.3 kB
Tags Source
SHA-256 checksum
How to use checksums
66f758f88f2e93aab292d9dd910e066090b943ebed14331e3370dd157ae7be60
BLAKE2b-256 checksum
How to use checksums
a7f37a591170422ddcecb9b8d1d6c35e20c09912b889c2b627a9815dbd16aa8b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Release files / cyberxyz_scanner-1.4.76-py3-none-any.whl

Download URL cyberxyz_scanner-1.4.76-py3-none-any.whl
Size 425.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
03fae0189281e91e93fd715a9e5a12e5981cbd57bffef429d378b44678eaf2ff
BLAKE2b-256 checksum
How to use checksums
eb3709fef0fef84091167ff4579d0bf637cf43542884a682efd00149cf54b505
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

Security for cyberxyz-scanner 1.4.76

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cyberxyz_scanner-1.4.76.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 20, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

1.4.76 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page