# Vygl — Full Documentation
> Vygl is a security scanning SaaS platform (SAST, SCA, secrets, IaC, container images). The CLI runs every scan engine locally — in your dev environment or CI runner — and pushes only findings metadata to the cloud; on the CLI path, source never leaves your environment. Opt-in managed scans (GitHub/GitLab App) run the engines server-side instead.
Source: https://docs.vygl.io
Generated: 2026-07-07
This file is the complete documentation corpus concatenated into a single markdown document so AI engines can ingest it in one fetch. The per-page version of each section lives at the URL shown above its heading.
---
# Introduction
## Introduction
URL: https://docs.vygl.io/
> Vygl is a security scanning platform that finds vulnerabilities in your code, dependencies, secrets, IaC, and container images. The CLI scans locally with only findings metadata sent to the cloud; managed scans can run server-side instead.
Vygl is a security scanning platform that finds vulnerabilities in your code, dependencies, secrets, infrastructure-as-code, and container images. With the **CLI**, scans run **locally** — in your developer environment or CI runner — and only findings metadata is pushed to the cloud, so your source never leaves your environment. If you'd rather not run anything yourself, **managed scans** ([connect a repository](/connect/overview/)) run the same engines server-side on each push — there, your source is fetched into the scanning environment.
## What Vygl scans
- **Static analysis (SAST)** — SQL injection, XSS, command injection, insecure crypto, and other code-level vulnerabilities, via OpenGrep with community rules and your own custom rules.
- **Supply chain (SCA)** — direct and transitive dependencies checked against the OSV vulnerability database. Supports Python, Node.js, Go, Ruby, Rust, PHP, and Java.
- **Secrets** — hardcoded API keys, passwords, tokens, and private keys via Gitleaks (600+ built-in patterns) plus Vygl's custom rules.
- **Infrastructure-as-code (IaC)** — Terraform, Dockerfile, Kubernetes, and CloudFormation misconfigurations via Checkov.
- **Container images** — OS package CVEs and embedded application dependencies, scanned layer by layer.
## How it works
The Vygl CLI runs every scan engine locally on your code. Only the resulting **findings metadata** — rule IDs, file paths, line numbers, normalized snippets — is sent to the dashboard. (Prefer zero local setup? **Managed scans** run those same engines on Vygl's workers when you connect GitHub/GitLab — see [Connect your repository](/connect/overview/).) The platform deduplicates findings by fingerprint, preserves triage state across scans, can run AI verification on potential false positives, and routes notifications to Slack, Microsoft Teams, email, generic webhooks, or as PR comments on your repository.
## Where to go next
---
# Getting Started
## Onboarding Walkthrough
URL: https://docs.vygl.io/getting-started/onboarding/
> A guided four-step setup that connects your first integration, gets a CLI scan running, and discovers repositories.
When a new organization is created, the onboarding flow guides admins through four steps to go from zero to a running scan. Progress is saved in your browser so you can refresh the page and pick up where you left off.
## The four steps
1. **Welcome.** A short overview of what Vygl does and what you'll set up.
2. **Choose how to scan.** Either install a managed integration (GitHub, GitLab, Bitbucket) and let Vygl run scans for you, or take the CLI/CI route and drive scans from your own pipeline. Step 2 has these two branches; both lead to step 3.
3. **Connect repositories** (managed branch) **or pull the scanner** (CLI branch). Managed: pick the repos to scan from the discovered list — each becomes a Vygl project. CLI: `docker pull vygl/vygl-cli:latest`, mint an API key in **Settings → API Keys**, and run a one-line `docker run vygl/vygl-cli scan` snippet.
4. **You're all set.** A summary of your provider, repository count, and the scan engines that will run. Click **Go to dashboard** to start triaging.
## Skipping and resuming
You can skip any step and come back later. Step state is kept in browser storage, so the flow resumes on the same device but restarts on a new one. To revisit completed steps, every onboarding action also has a permanent home in **Settings**:
- **Connections** — manage SCM integrations and the per-connection repository list (steps 2 and 3, managed branch).
- **API Keys** — manage CI keys (step 3, CLI branch).
## Permissions
API key creation requires the `admin` or `owner` role. SCM integration installs are gated on provider-side permissions — installing a GitHub App on an org needs GitHub-side admin rights regardless of your Vygl role. Members can walk through the wizard but won't be able to complete admin-only setup steps.
API keys created from the wizard ship with the platform's default scope set (`read`, `write`, `scan:read`, `scan:write`, `triage:write`, `memory:write`). Today there's no UI to narrow this — for least-privilege CI keys, mint via the API and pass the `scopes` array.
---
## Quickstart
URL: https://docs.vygl.io/getting-started/quickstart/
> Pull the Vygl CLI Docker image, run a scan against a local directory, and push findings to the dashboard — in five minutes.
The fastest way to evaluate Vygl: run the Docker image against a local checkout, see findings, then add an API key to push results to the dashboard.
## Pull the image
The CLI ships as a single Docker image with every scan engine bundled — OpenGrep (SAST), Gitleaks (secrets), osv-scanner (SCA), Checkov (IaC), and GuardDog (malicious packages).
```bash
docker pull vygl/vygl-cli:latest
```
## Scan locally (no cloud required)
Run a scan against the current directory. The `--no-upload` flag keeps everything local.
```bash
docker run --rm \
-v "$(pwd)":/scan \
vygl/vygl-cli scan /scan --no-upload
```
You'll see a finding table printed to stdout. The CLI exits 0 unless you set a severity gate (covered in [Severity Gates](/scanning/severity-gates/)).
## Push findings to the dashboard
1. Open Vygl, go to **Settings → API Keys**, click **Generate Key**, copy it. The key starts with `vgl_`.
2. Re-run the scan with the API key set. The CLI auto-detects your git remote and creates or matches a project. The CLI uploads to `https://api.vygl.io` by default; set `VYGL_API_URL` only when targeting a self-hosted instance.
```bash
docker run --rm \
-v "$(pwd)":/scan \
-e VYGL_API_KEY=vgl_your_api_key_here \
vygl/vygl-cli scan /scan
```
3. Open the dashboard. The scan appears under **Projects**, with findings listed and ready to triage.
## Where to next
- **In a CI pipeline?** Continue to [CI/CD Pipelines](/scanning/ci-cd/).
- **Want to connect a repo for automatic scans + PR comments?** See [Connecting Your Code](/connect/overview/).
- **Need to tune what gets scanned?** See [Configuration](/scanning/configuration/).
---
## Sign Up & Sign In
URL: https://docs.vygl.io/getting-started/sign-in/
> Authenticate with email and password or use any of the supported single sign-on providers — Google, Microsoft Entra, GitHub, or GitLab.
Vygl supports email/password sign-in plus four SSO providers. On first SSO sign-in (when self-service signup is enabled), a new organization is created automatically; if an account with the same email already exists, the SSO login links to it. GitHub additionally requires the email to be a verified primary; the other providers trust whatever the IdP returns.
## Sign-in methods
The default sign-in method. Visit `/login`, enter your email and password, and you're in. The login endpoint enforces a rate limit (10 attempts per IP per 15 minutes) to deter brute force; the password-reset endpoint applies a tighter 5-attempt window to discourage probing.
If you've forgotten your password, click **Forgot Password** — Vygl emails a reset token valid for 15 minutes. The reset endpoint returns 200 regardless of whether the email exists, so it's safe against account-enumeration probes.
Sign in with any Google account. Vygl requests the standard OpenID profile and email scopes. On first sign-in, your name, email, and profile picture are imported automatically.
Sign in with Microsoft Entra ID (formerly Azure AD). Uses OpenID Connect — Vygl validates the signed `id_token` against Microsoft's published key set rather than trusting the access token. The Entra tenant can be restricted to a single directory or left open to any Entra directory.
Scopes: `openid profile email`.
Sign in with your GitHub account. Vygl requests the `user:email` scope to read your verified primary email. This is **separate** from the GitHub App installed for repository scanning — sign-in just identifies you as a user.
Sign in with your GitLab.com or self-hosted GitLab account. Scopes: `read_user`. As with GitHub, this is separate from the GitLab integration installed for repository access.
## Session model
After sign-in, Vygl issues a JWT access token (30 minutes) backed by a refresh token (7 days). The browser refreshes transparently in the background; there's no manual token handling for end users. Tokens are stateless JWTs — sign-out happens client-side by discarding them.
## Organizations
Every user belongs to one or more **organizations**. The first time you sign in, an org is created for you; if you accept an invite from an existing org, you join that org instead. Switch between orgs with the dropdown in the top nav — no re-authentication needed.
For more on organization structure, see [Members & Roles](/team/members/).
## Self-service signup
Self-service signup is **disabled in production** — accounts are created via invitation. To get an account, ask an existing org owner or admin to invite you. The invite is one-time, expires in seven days, and includes the role you'll have when you join.
Self-hosted Vygl instances can enable self-service signup via the `SSO_ALLOW_REGISTRATION` setting at deployment time.
---
# Connecting Your Code
## Bitbucket Integration
URL: https://docs.vygl.io/connect/bitbucket/
> Connect Vygl to Bitbucket Cloud via OAuth for automatic scans and pull-request comments.
Authorize Vygl on Bitbucket Cloud to discover repositories, register webhooks, and post pull-request comments.
## Install
1. Open **Settings → Connections** in Vygl.
2. Click **Connect Bitbucket**. You're redirected to Bitbucket's OAuth authorization screen.
3. **Authorize.** Approve the requested scopes — `repository`, `pullrequest:write`, and `webhook`.
4. **Pick repositories.** Bitbucket redirects back to Vygl. Select the repositories you want scanned and (optionally) configure branch filtering.
Webhooks register automatically. Pushes and pull requests trigger scans.
## Scopes
| Scope | Why |
|---|---|
| `repository` (read) | Clone the repository for scanning |
| `pullrequest:write` | Post comments on pull requests, set build status |
| `webhook` | Create and manage per-repo scan-trigger webhooks |
OAuth tokens are refreshed transparently before expiry.
## What happens on a push or PR
1. Bitbucket fires a webhook for the push or PR event. Vygl verifies an HMAC-SHA256 signature over the body using the per-repo webhook secret.
2. Vygl clones the repository at the new commit.
3. Scan engines run server-side.
4. Findings appear in the dashboard, deduplicated against history.
5. For pull requests, Vygl posts a fresh summary comment (one per scan) and writes build status.
## Bitbucket Server (self-hosted)
The managed OAuth flow is **Bitbucket Cloud only** — endpoints, scopes, and API paths are Cloud-specific. To scan code on Bitbucket Server / Data Center, use the [CI + Git-token](/scanning/ci-cd/) flow: PR-comment posting picks up the Server REST API automatically when the project's repository URL points to a non-`bitbucket.org` host.
## Removing access
Teardown has two independent halves — do both for a clean removal:
- **In Vygl** — disable a single repository, or disconnect the whole provider, from **Settings → Connections**. See [Managing connected repositories](/connect/overview/#managing-connected-repositories).
- **On Bitbucket** — revoke the authorization (**Personal settings → App authorizations**). Webhook events stop immediately.
Existing scans and findings are retained in Vygl regardless.
---
## GitHub App
URL: https://docs.vygl.io/connect/github/
> Install the Vygl GitHub App to automatically scan pushes, gate pull requests, and post findings as PR comments.
The Vygl GitHub App grants access to selected repositories without requiring personal access tokens. Once installed, Vygl receives webhook events for pushes and pull requests, runs scans automatically, and can post finding summaries directly on PRs.
## Install
1. Open **Settings → Connections** in Vygl.
2. Click **Install on GitHub**. You're redirected to GitHub's app-install page.
3. **Select the account.** Pick the user or organization that owns the repositories.
4. **Choose repositories.** Select all repositories or pick a subset. You can change this later from your GitHub org's app settings.
5. **Approve.** GitHub redirects back to Vygl and stores the installation ID. Vygl mints short-lived installation access tokens on demand from the App's private key — no long-lived OAuth token is ever persisted.
After installation, Vygl discovers your repositories — pick which ones to scan from the **Connections** tab. Webhooks register automatically.
## Permissions
The Vygl GitHub App requests:
| Permission | Why |
|---|---|
| **Contents: Read** | Clone the repository for scanning |
| **Metadata: Read** | Discover branches and commit metadata |
| **Pull requests: Write** | Post finding-summary comments on PRs |
| **Commit statuses: Write** | Post commit status (so branch protection can require passing scans) |
GitHub treats apps differently from personal access tokens — these permissions apply only to the repositories you explicitly select.
## What happens on a push or PR
1. GitHub fires a webhook to Vygl. Vygl verifies the HMAC signature; unsigned or mis-signed requests are rejected.
2. Vygl clones the repository at the new commit (`--depth 1` for efficiency).
3. The four scan engines run server-side.
4. Findings are written to the dashboard, deduplicated against past scans.
5. For pull requests, Vygl posts (or updates) the summary comment and writes commit status. See [PR / MR Comments](/connect/pr-comments/).
## Branch filtering
Each repository's `scan_config.branches` list filters which branch pushes trigger a scan. Patterns are **glob** (`fnmatch`) — e.g. `main`, `release/*`, `*` (default). PR scans always run regardless of the filter.
Today the filter has no UI — set it via the API by patching the repository's `scan_config`. A connection-tab control is on the roadmap.
## Self-hosted GitHub Enterprise
The managed App flow is `github.com`-only today — installation tokens are minted against `https://api.github.com`. To scan code on GitHub Enterprise Server, use the [CI + Git-token](/scanning/ci-cd/) flow: the token's API base URL is derived from your project's repository URL (`https:///api/v3/...`).
## Removing access
Teardown has two independent halves — do both for a clean removal:
- **In Vygl** — disable a single repository, or disconnect the whole provider, from **Settings → Connections**. See [Managing connected repositories](/connect/overview/#managing-connected-repositories).
- **On GitHub** — uninstall the app (your org's GitHub Settings → Installed Apps). Webhook events stop immediately.
Doing only one leaves the other half live: removing the Vygl connection but leaving the App installed is harmless but untidy; uninstalling on GitHub but leaving the connection leaves a dead connection row. Existing scans and findings remain in Vygl either way.
---
## GitLab Integration
URL: https://docs.vygl.io/connect/gitlab/
> Connect Vygl to GitLab.com or self-hosted GitLab via OAuth — auto-scan pushes and merge requests.
Authorize Vygl on GitLab.com or your self-hosted GitLab instance to discover projects, register webhooks, and post merge-request comments. The integration uses standard OAuth 2.0 with automatic token refresh.
## Install
1. Open **Settings → Connections** in Vygl.
2. Click **Connect GitLab**. You're redirected to GitLab's authorization page.
3. **Authorize.** Approve the requested `api` scope.
4. **Pick projects.** GitLab redirects back to Vygl. Select the projects you want scanned.
Webhooks register automatically. Pushes and merge requests trigger scans.
## Scopes
| Scope | Why |
|---|---|
| `api` | Full API access — needed for project metadata, source fetch, MR comments, and webhook CRUD. GitLab doesn't offer a finer-grained scope that covers this combination. |
GitLab tokens have expiries; Vygl refreshes them transparently in the background.
## What happens on a push or MR
1. GitLab fires a webhook with the push/MR event. Vygl verifies the shared-secret token sent in the `X-Gitlab-Token` header against the per-repo secret.
2. Vygl clones the project at the new commit.
3. Scan engines run server-side.
4. Findings appear in the dashboard, deduplicated against history.
5. For merge requests, Vygl posts a fresh summary comment (one per scan) and updates pipeline status.
## Branch filtering
Each project's `scan_config.branches` list filters which branch pushes trigger a scan. Patterns are **glob** (`fnmatch`) — e.g. `main`, `release/*`, `*` (default). MR scans always run. Today the filter is API-only — no UI yet.
## Self-hosted GitLab
Self-hosted GitLab Community and Enterprise editions are supported. The base URL is set globally per Vygl deployment via the `GITLAB_URL` env var — multi-tenant SaaS users are pinned to `gitlab.com`. For self-hosted, override `GITLAB_URL` at deploy time and authorize the OAuth app on your instance.
## Removing access
Teardown has two independent halves — do both for a clean removal:
- **In Vygl** — disable a single project, or disconnect the whole provider, from **Settings → Connections**. See [Managing connected repositories](/connect/overview/#managing-connected-repositories).
- **On GitLab** — revoke the authorization (**User Settings → Applications**). Webhook events stop immediately.
Existing scans and findings are retained in Vygl regardless.
---
## Connecting Your Code
URL: https://docs.vygl.io/connect/overview/
> Three ways to connect your repositories — managed SCM apps, CI/CD pipelines, or the local CLI. Pick one; they're alternatives, not stages.
Vygl scans code three ways. **Pick one** — these are alternative ways to trigger scans, not stages you need to combine. Most teams only ever use one.
| | Managed SCM | CI/CD pipeline | Local CLI |
|---|---|---|---|
| **Where scans run** | Vygl's servers (clones your repo) | Your CI runner (your code stays put) | Your laptop |
| **What triggers a scan** | GitHub/GitLab/Bitbucket webhooks (every push & PR) | Your pipeline (every build) | You, manually |
| **Setup** | One click — install an App / OAuth | Add a step to `.github/workflows/`, `.gitlab-ci.yml`, etc. | `docker pull` |
| **PR / MR comments** | ✓ Built in | ✓ With a Git Token | — |
| **Blocks merges via CI status** | ✓ Commit status check | ✓ Pipeline exit code | — |
| **Source code leaves your env?** | ✓ Cloned to Vygl | ✗ Stays in your runner | ✗ Stays local |
If your code is on GitHub.com, GitLab (Cloud or self-hosted), or Bitbucket Cloud, **managed SCM is the simplest path** — same automation, less wiring. CI/CD is the right pick when your runners must do the cloning (private network, Bitbucket Server / GitHub Enterprise Server, regulated environments).
## Managed SCM integration (recommended)
Install the Vygl GitHub App or authorize the GitLab / Bitbucket OAuth integration. Once connected, Vygl receives webhook events for pushes and pull requests, runs scans automatically, and can post finding summaries directly on PRs.
**Best for:** teams on GitHub.com, GitLab (Cloud or self-hosted), or Bitbucket Cloud that want the whole flow automated end-to-end.
**Setup:** one click, browser OAuth, repository selection. No CI configuration required.
## CI pipeline + API key
Add a single `vygl scan` step to your CI pipeline (GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins, anything with Docker). The CLI auto-detects branch, commit, and repo URL from the CI environment and pushes findings to the dashboard. Pair it with a Git Token to enable PR comments.
**Best for:** self-hosted SCMs that aren't on the managed-OAuth path (Bitbucket Server, GitHub Enterprise Server), private-network repos that Vygl can't reach, or teams who'd rather drive scans from their own runner. Also fine if you simply prefer it — the dashboard experience is identical.
**Setup:** copy a config snippet into `.github/workflows/`, `.gitlab-ci.yml`, or your equivalent. Add `VYGL_API_KEY` as a CI secret.
## Local CLI (no SCM at all)
Run `vygl scan` from a developer laptop or any environment with Docker. The CLI works offline (`--no-upload`) for evaluation, or with an API key to push results to the dashboard.
**Best for:** evaluation, ad-hoc audits, monorepos that span multiple SCM providers, repos that aren't pushed anywhere yet.
## Managing connected repositories
Once a provider is connected, **Settings → Connections** lists every connection and, beneath it, an **Enabled repositories** table of the repositories Vygl scans.
### Disabling scanning on one repository
1. Open **Settings → Connections**.
2. Find the repository in the **Enabled repositories** table.
3. Click its disable (trash) action.
Vygl stops scanning that repository on future pushes and pull requests. The SCM connection itself stays intact, and the repository's existing scans and findings are kept — you can re-enable it any time from the **Add Repository** dialog on the [Projects](/using-vygl/projects/) page.
### Disconnecting an entire SCM provider
1. Open **Settings → Connections**.
2. Click the disconnect (unplug) action on the connection and confirm.
The connection is removed and webhooks for every repository under it are torn down — no more managed scans from that provider.
## Do I need to combine them?
**No.** Pick one. The three approaches *can* coexist, but for almost every team a single approach is enough:
- Managed SCM **already** scans every push and PR, posts comments, and gates merges via commit status — there's no separate "CI gate" you need to add.
- CI/CD **already** runs on every push and PR (because your pipeline does), and `--fail-on` blocks the merge via the pipeline's exit code.
The only time it makes sense to layer them is the **local CLI alongside one of the cloud paths** — running `vygl scan --no-upload` on a developer laptop for a fast pre-commit check, while managed SCM or CI handles the authoritative scan on push. That's the only "combining" pattern most teams will ever need.
---
## PR / MR Comments
URL: https://docs.vygl.io/connect/pr-comments/
> Post scan summaries directly on pull and merge requests — severity breakdown, new vs recurring findings, deep links.
Vygl can post scan summaries on pull requests across GitHub, GitLab, and Bitbucket. The comment includes a severity breakdown, new vs recurring counts, and a findings table whose titles deep-link to the dashboard.
## Enable comments
Comments are off by default. Enable them in two steps:
1. Add a **Git Token** on the project's settings tab. Vygl uses the token to post the comment back to your SCM.
2. Pass `--pr ` to `vygl scan` in CI when running on a PR/MR. The CLI auto-detects the PR number on GitHub Actions, GitLab CI, and Bitbucket Pipelines, so the flag is only needed in unsupported environments. The CI snippets in [CI/CD Pipelines](/scanning/ci-cd/) wire this up for each provider.
If your code is connected via the managed GitHub App / GitLab / Bitbucket OAuth integration, no Git Token is needed — Vygl posts the comment using the integration's own credentials.
## Tokens by provider
| Provider | Required token & scope |
|---|---|
| **GitHub** | Personal access token (classic) with `repo` scope, or fine-grained token with **Pull requests: write**. Works with `github.com` and GitHub Enterprise Server (the host is parsed from the project's repository URL). |
| **GitLab** | Personal access token with `api` scope. Works with GitLab.com and self-hosted. |
| **Bitbucket** | App password (Cloud) or HTTP access token (Server) authorized for pull-request write. |
The token lives in the project's `Git Token` field on **Project → Settings**. Saving overwrites the previous value; rotate by replacing it. The current value is editable in the same form — it is not encrypted at the database layer, so treat it like any other secret in your project config.
## What's in the comment
- **Header** — project name, branch, scan types that ran.
- **Severity breakdown** — counts by critical / high / medium / low.
- **New vs recurring** — newly-introduced findings on this PR vs ones already present on the target branch.
- **Findings table** — columns `Severity | Type | Finding | File`. The "Finding" cell is a markdown link to the finding detail page in Vygl, where you'll see the rule ID, snippet, AI verdict, and full reasoning. The table caps at 20 new + 30 recurring rows; overflow is summarized in the footer.
- **Footer** — link to the full scan in Vygl plus an attribution line.
Each scan posts a fresh comment — comments are not edited in place today. If you re-run a scan on the same PR multiple times, expect one comment per run. (Update-in-place is on the roadmap.)
## What if the SCM is self-hosted?
For the Git-token + CI flow, the API base URL is **derived from the project's repository URL**. Set the project's Repository URL to your self-hosted host (e.g. `https://gitlab.example.com/group/project` or `https://ghe.example.com/org/repo`) and Vygl will route the comment through the matching API endpoint — `https:///api/v4/...` for GitLab, `https:///api/v3/...` for GitHub Enterprise, `https:///rest/api/1.0/...` for Bitbucket Server.
---
# Scanning
## CI/CD Pipelines
URL: https://docs.vygl.io/scanning/ci-cd/
> Drop-in pipeline configs for GitHub Actions, GitLab CI, Bitbucket Pipelines, and any Docker-based CI.
Add a single Vygl step to your pipeline to scan every push and pull request. The CLI auto-detects branch, commit, and repository URL from CI environment variables — the only secret you need to configure is `VYGL_API_KEY`.
## Pipeline configs
```yaml
name: Vygl Security Scan
on:
push:
branches: [main]
pull_request:
jobs:
security-scan:
runs-on: ubuntu-latest
container:
image: vygl/vygl-cli:latest
options: --entrypoint ""
env:
VYGL_API_KEY: ${{ secrets.VYGL_API_KEY }}
steps:
- uses: actions/checkout@v4
- run: vygl scan --fail-on high .
# Optional: post scan summary as a PR comment.
# Add a Git Token in project settings, then uncomment:
# - run: vygl scan --fail-on high --pr ${{ github.event.pull_request.number }} .
```
Add `VYGL_API_KEY` under **Settings → Secrets and variables → Actions**. Branch, commit, and repo URL are auto-detected from the GitHub Actions environment.
```yaml
vygl-scan:
stage: test
image:
name: vygl/vygl-cli:latest
entrypoint: [""]
variables:
VYGL_API_KEY: $VYGL_API_KEY
script:
- vygl scan --fail-on high .
# Optional: post scan summary as an MR comment.
# Add a Git Token in project settings, then use:
# - vygl scan --fail-on high --pr $CI_MERGE_REQUEST_IID .
# Optional: restrict to MRs and the default branch only.
# rules:
# - if: $CI_PIPELINE_SOURCE == "merge_request_event"
# - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
```
Add `VYGL_API_KEY` under **Settings → CI/CD → Variables** (mark as masked). Works with GitLab.com and self-hosted instances.
```yaml
pipelines:
default:
- step:
name: Vygl Security Scan
image:
name: vygl/vygl-cli:latest
entrypoint: [""]
script:
- vygl scan --fail-on high .
# Optional: post scan summary as a PR comment.
# Add a Git Token in project settings, then use:
# - vygl scan --fail-on high --pr $BITBUCKET_PR_ID .
pull-requests:
'**':
- step:
name: Vygl Security Scan
image:
name: vygl/vygl-cli:latest
entrypoint: [""]
script:
- vygl scan --fail-on high .
# Uncomment to post scan summary as a PR comment:
# - vygl scan --fail-on high --pr $BITBUCKET_PR_ID .
```
Add `VYGL_API_KEY` as a repository variable in **Repository settings → Pipelines → Variables**.
```bash
docker run --rm \
-v "$(pwd)":/scan \
-e VYGL_API_KEY=vgl_your_key \
vygl/vygl-cli scan --fail-on high /scan
# Optional: add --pr to post a scan summary as a PR/MR comment
```
Works with any CI system that supports Docker containers (Jenkins, CircleCI, Drone, Buildkite, Gitea Actions, …).
## What gets auto-detected
When run inside CI, the CLI reads environment variables to fill in scan metadata:
- **Repository URL, branch, commit SHA** — from CI-native variables (`GITHUB_REPOSITORY`, `CI_COMMIT_REF_NAME`, `BITBUCKET_BRANCH`, etc.).
- **PR number** — auto-detected on GitHub Actions, GitLab CI, and Bitbucket Pipelines (from `GITHUB_REF=refs/pull/N/merge`, `CI_MERGE_REQUEST_IID`, and `BITBUCKET_PR_ID` respectively). Use `--pr` only to override or in unsupported CI systems.
- **Trigger type** (push / pull_request / scheduled) — derived from CI variables.
- **Commit author** (email, name, external ID) — from CI env or `git log`. Disable with `scan.send_committer: false` in `.vygl.yml` if you don't want author metadata in the dashboard.
If detection fails, override with `--repo-url`, `--branch`, `--commit`, `--pr`, or `--git-provider` flags (`VYGL_GIT_PROVIDER` env var also works).
---
## Running Scans (CLI)
URL: https://docs.vygl.io/scanning/cli/
> Use the vygl CLI to run scans locally or in CI — every command, flag, and common pattern.
The Vygl CLI runs every scan engine locally and uploads only findings metadata. It works on a developer laptop, a CI runner, or any environment with Docker.
## The `scan` command
```bash
vygl scan [OPTIONS] [PATH]
```
The most common flags:
| Flag (long / short) | Purpose |
|---|---|
| `--scan-types` / `-t` | Comma-separated list of engines (default: `sast,sca,secrets,iac,malware`). Valid values: `sast`, `sca`, `secrets`, `iac`, `malware`, `container`. `malware` and `container` are gated by org/project settings — when the cloud policy says they're off, the CLI skips them automatically and prints a short notice. |
| `--image` | Scan a container image instead of a directory. Forces `--scan-types container`. |
| `--fail-on` | Exit non-zero when findings reach this severity. One of `critical`, `high`, `medium`, `low`. |
| `--no-upload` | Run engines locally without pushing results to the cloud. |
| `--name` / `-n` | Project name (use when there's no git remote). |
| `--project-id` / `-p` | Bind to an existing project explicitly (also `VYGL_PROJECT_ID`). |
| `--branch` / `-b` | Override the auto-detected branch (useful in detached-HEAD CI). |
| `--commit` | Override the auto-detected commit SHA. |
| `--repo-url` | Override the auto-detected repository URL. |
| `--git-provider` | Force the SCM type (`github`, `gitlab`, `bitbucket`, `gitea`); also `VYGL_GIT_PROVIDER`. |
| `--pr` | Pull/merge request number — enables PR comment posting. Auto-detected on GitHub Actions, GitLab CI, and Bitbucket Pipelines. |
| `--config` / `-c` | Path to a `.vygl.yml` (overrides auto-discovery). |
| `--show-all` | Print every finding to stdout, not just the gate-triggering ones. |
| `--trigger` | Mark the trigger source on the dashboard (default `ci`; e.g. `manual`, `scheduled`). |
| `--api-url` | Override the API base URL. Defaults to `VYGL_API_URL` then `https://api.vygl.io`. |
## Common patterns
**Scan only secrets and dependencies, fail on high or above:**
```bash
vygl scan --scan-types secrets,sca --fail-on high .
```
**Local-only run (no upload):**
```bash
vygl scan --no-upload .
```
**Scan a container image:**
```bash
vygl scan --image myorg/myapp:1.2.3
```
**CI run with explicit branch (detached HEAD):**
```bash
vygl scan --branch main .
```
**Scan a PR and post a comment:**
```bash
vygl scan --fail-on high --pr 1234 .
```
## Auth commands
| Command | What it does |
|---|---|
| `vygl auth set-key ` | Store an API key locally in `~/.vygl/credentials` (chmod 600). |
| `vygl auth test` | Verify your key works and show the org it belongs to. |
## Project commands
| Command | What it does |
|---|---|
| `vygl project list` | List all projects in your organization. |
## Results
| Command | What it does |
|---|---|
| `vygl results ` | Fetch and print results from a previous scan. |
---
## Configuration
URL: https://docs.vygl.io/scanning/configuration/
> Configure scan behavior with .vygl.yml — engine exclusions, severity gates, project binding.
A `.vygl.yml` file at your repository root configures per-engine exclusions, the CI severity gate, and project binding. Settings the file doesn't define fall back to project-level settings in the dashboard, then to defaults.
## Full example
```yaml
project_id: prj_abc123 # Optional — overrides VYGL_PROJECT_ID and auto-detection
scan:
fail_on: high
send_committer: true # Default true — set false to omit author metadata
sast:
exclude_paths:
- "vendor/**"
- "tests/**"
exclude_rules:
- "javascript.express.audit.express-cookie-session-no-secure"
sca:
exclude_packages:
- "pkg:npm/lodash"
iac:
frameworks:
- terraform
- dockerfile
exclude_checks:
- CKV_AWS_46
```
## Top-level keys
### `project_id`
Optional. Binds the scan to a specific Vygl project ID. Equivalent to `--project-id` on the CLI or `VYGL_PROJECT_ID` in the env. CLI flag wins; otherwise the precedence is env → file → auto-detect from git remote.
### `scan.fail_on`
The CI severity gate. Valid values: `critical`, `high`, `medium`, `low`. The CLI exits non-zero when findings at or above this level are present. See [Severity Gates](/scanning/severity-gates/) for the precedence rules.
### `scan.send_committer`
Default `true`. When `false`, the CLI omits commit author email/name/external-id from the upload. Useful when CI runs as a service identity and you don't want that account credited as the committer.
### `scan.sast.exclude_paths`
Glob patterns to skip during SAST. Common excludes: `vendor/**`, `node_modules/**`, `tests/**`, `.git/**`, `.venv/**`.
### `scan.sast.exclude_rules`
Rule-ID list passed to OpenGrep as `--exclude-rule`. Useful for rules that are noisy in your codebase but you don't want to disable for the whole org.
### `scan.sca.exclude_packages`
Skip findings for specific packages, written as PURLs (`pkg:npm/lodash`, `pkg:pypi/requests`). Useful when a vulnerable dependency has a documented mitigation in your environment.
### `scan.iac.frameworks`
Restrict Checkov to a subset of `terraform`, `dockerfile`, `kubernetes`, `cloudformation`. Default is to scan everything Checkov can identify.
### `scan.iac.exclude_checks`
Checkov check IDs to skip (e.g. `CKV_AWS_46`). Use this when a specific check is deliberately accepted across the whole repo.
## Suppressing findings
For per-finding suppression, use:
- **Inline `novygl` comments** in source — the engine drops the finding at scan time. See [Inline Suppression](/findings/suppression/).
- **Triage in the dashboard** — mark findings `false_positive`, `ignored`, or `suppressed`. The decision is sticky across rescans.
There is no `.vygl.yml` fingerprint or path-based suppression for the Secrets engine today; rely on `novygl` comments or in-dashboard triage.
## Environment variables
The CLI also reads these environment variables (CLI flags take precedence over both the env and the config file):
| Variable | Purpose |
|---|---|
| `VYGL_API_KEY` | API key for authentication (required for cloud upload). |
| `VYGL_API_URL` | API base URL (default: `https://api.vygl.io`). |
| `VYGL_PROJECT_ID` | Override auto-detected project ID. |
| `VYGL_GIT_PROVIDER` | Force the SCM type (`github`, `gitlab`, `bitbucket`, `gitea`). |
CI-native vars (`GITHUB_*`, `CI_*`, `BITBUCKET_*`, `GITEA_*`) are read for branch / commit / repo / PR / committer auto-detection — see [CI/CD Pipelines](/scanning/ci-cd/).
---
## Custom Rules
URL: https://docs.vygl.io/scanning/custom-rules/
> Extend Vygl's SAST coverage with your own OpenGrep YAML rules — written in the dashboard, imported from git, or uploaded directly.
Custom rules let you encode organization-specific patterns alongside the community ruleset — internal API misuse, deprecated helpers, banned crypto, secret formats unique to your stack. Each rule has a mode that controls whether it raises findings, fails CI, or stays disabled.
## Rule modes
| Mode | Behavior |
|---|---|
| **Monitor** | Findings appear in the dashboard. CI passes (exit 0). The default for all rules. |
| **Block** | Findings appear in the dashboard. CI fails (exit 1) when the rule fires. |
| **Disabled** | Rule is not included in scans. No findings generated. |
Switch modes from the **Rules** page; bulk operations let you flip many rules at once.
## Authoring rules
Custom rules use OpenGrep's YAML format. Three ways to add them:
- **In the dashboard** — paste YAML into the create-rule form.
- **Import from git** — point Vygl at a repository containing rule files; the platform polls and re-imports on a schedule.
- **Upload a file** — drop a `.yml` or `.yaml` file directly.
A minimal rule:
```yaml
rules:
- id: internal.deprecated-auth-helper
languages: [python]
severity: WARNING
message: |
The deprecated_auth() helper is being removed in 2026.
Migrate to AuthContext.from_request(...).
pattern: deprecated_auth(...)
```
## Community rules
Community rules — bundled with the engine — can be enabled, disabled, or moved to Block mode, but not edited or deleted. To override community-rule behavior, duplicate it as a custom rule, edit the duplicate, and disable the original.
## Recommended workflow
Author a new rule with mode **Monitor**.
Wait one or two scan cycles to confirm the rule fires only where expected (no false positives).
Promote to **Block** so the rule starts gating CI.
If the rule turns out noisy, demote back to Monitor or Disabled — no code changes needed.
---
## Scan Engines
URL: https://docs.vygl.io/scanning/engines/
> Vygl bundles five engines — OpenGrep (SAST), osv-scanner (SCA), Gitleaks (secrets), Checkov (IaC), GuardDog (malicious packages) — plus container scanning.
On the CLI, each scan runs five engines locally and pushes only findings metadata to the cloud; managed scans run the same engines server-side instead. Container scanning is a sixth, separate flow covered in [Container Scanning](/containers/overview/).
## Static Analysis (SAST)
**OpenGrep** with community rules and your own custom rules. Detects SQL injection, XSS, command injection, insecure crypto, hardcoded credentials in code, missing authentication checks, and other code-level vulnerabilities. Custom rules let you encode organization-specific patterns.
## Supply Chain (SCA)
**osv-scanner** against the OSV vulnerability database. Checks direct and transitive dependencies for known CVEs. Supports Python (`requirements.txt`, `pyproject.toml`, `poetry.lock`, `Pipfile.lock`), Node.js (`package.json`, `package-lock.json`, `yarn.lock`), Go (`go.mod`, `go.sum`), Ruby (`Gemfile.lock`), Rust (`Cargo.lock`), PHP (`composer.lock`), and Java (`pom.xml`, `gradle.lockfile`).
## Secret Detection
**Gitleaks** with 200+ built-in patterns plus Vygl's custom rules. Detects hardcoded API keys, passwords, tokens, and private keys. Suppress test fixtures with inline `novygl` comments (the same comment marker works for SAST too).
## Infrastructure as Code (IaC)
**Checkov** with the latest community ruleset. Scans Terraform, Dockerfile, Kubernetes manifests, and CloudFormation for misconfigurations — public S3 buckets, missing encryption, overly permissive IAM, exposed ports, and more.
## Malicious Packages (Malware)
**GuardDog** — heuristic detection of malicious open-source packages. Where SCA asks "does this dependency have a known CVE?", Malware asks "is this dependency itself trustworthy?". Catches typosquats (`reqeusts` vs `requests`), install-script malware, exec/base64 payloads, exfiltration patterns, hijacked GitHub Actions (the `tj-actions/changed-files` attack class), and brand-new zero-history packages.
Covers five ecosystems:
- **PyPI** — Python packages from any manifest the catalog parser finds.
- **npm** — JavaScript/TypeScript packages.
- **Go** — modules from `go.mod` / `go.sum`.
- **RubyGems** — gems from `Gemfile.lock` / `Gemfile`.
- **GitHub Actions** — `uses:` references in `.github/workflows/*.yml`.
The engine fetches each package's contents from the public registry, runs GuardDog's heuristic rules, and emits a finding when a rule fires. Per-rule severity defaults are curated by Vygl — `install-script` and `release-zero` are intentionally softened (HIGH severity but LOW confidence) so legitimate native-build packages don't dominate the queue. Common false-positive packages (`psycopg2`, `pillow`, `lxml`, `numpy`, `cryptography`, ...) are muted by default.
Malware detection is **opt-in** — new orgs ship with it off so the first scan isn't dominated by a noisy new engine. Org admins turn it on from **Settings → Organization → Scan Engines**, and each project can override with a tri-state (use org default / on / off) under **Project Settings → Scanning & AI Overrides**. The org-level switch is a hard gate: a project's "on" override has no effect if the org has the engine off.
Individual GuardDog rules can be disabled or have their severity overridden from **Rules → Malicious Packages**.
## How engines pick what to scan
The CLI walks your repo once and picks engines based on what's present:
- SAST runs on any source code in supported languages.
- SCA runs when manifest files (`package.json`, `requirements.txt`, etc.) are detected.
- Secrets runs on every text file (with binary detection to skip images and archives).
- IaC runs when Terraform, Dockerfile, Kubernetes, or CloudFormation files are present.
- Malware runs against the same manifests SCA reads, plus any `.github/workflows/*.yml`.
Restrict which engines run with `--scan-types sast,secrets` on the CLI.
## Tuning per engine
Each engine has its own configuration block in `.vygl.yml`:
- `scan.sast.exclude_paths` — skip vendor/test/generated code.
- `scan.sast.exclude_rules` — drop specific rule IDs (passed to OpenGrep as `--exclude-rule`).
- `scan.sca.exclude_packages` — skip dependencies with documented mitigations.
- `scan.iac.frameworks` — restrict Checkov to a subset (`terraform`, `dockerfile`, `kubernetes`, `cloudformation`).
- `scan.iac.exclude_checks` — skip specific Checkov check IDs (e.g. `CKV_AWS_46`).
- `scan.malware.mute` — additional `(ecosystem, package)` pairs to skip on top of the default mute list (example: `[["PyPI", "my-internal-native-pkg"]]`).
For Secrets, suppression today is via `novygl` comments in source — there is no `.vygl.yml` allow-list that the active Gitleaks engine reads. See [Configuration](/scanning/configuration/) for the full schema.
## Engine isolation
A failure in one engine never blocks the others. If GuardDog crashes mid-scan, the SAST/SCA/Secrets/IaC findings still come back. Each engine runs under a wall-clock budget (default 10 minutes, override via `scan.engine_timeout_seconds`), and timeouts, constructor errors, or malformed cloud-rule policies are surfaced as a per-engine status line in the CLI summary — not as a scan abort.
## Custom rules
SAST rules can be extended in the dashboard or imported from a git repository — see [Custom Rules](/scanning/custom-rules/).
## Browsing results
SCA findings roll up into the org-wide [Dependency Inventory](/using-vygl/dependencies/) — searchable by package and ecosystem, with a where-used breakdown and vulnerability status per package.
---
## Merge Gating
URL: https://docs.vygl.io/scanning/merge-gating/
> Block pull and merge requests on managed SCM connections when findings reach a chosen severity — configured per repository.
Merge gating posts a **check** on the pull/merge requests of your managed SCM repositories and decides whether they're clear to merge. It's configured per repository in **Settings → Merge Gating**.
## Merge gating vs. severity gates
These are the two ways Vygl can stop a risky change — they work in different places:
| | Merge Gating | [Severity Gates](/scanning/severity-gates/) |
|---|---|---|
| **Where it runs** | On managed GitHub / GitLab / Bitbucket connections | In your own CI/CD pipeline |
| **How it gates** | A commit-status / check on the PR/MR | The CLI's exit code |
| **Set up in** | Settings → Merge Gating | `.vygl.yml`, a CLI flag, or a project's fail-on setting |
Merge gating applies **only to managed SCM connections**. If you scan through the CI/CD or local CLI paths, use severity gates instead.
## Per-repository policy
Each enabled repository has its own merge policy:
| Setting | Effect |
|---|---|
| **Mode** | `off` (no check posted), `advisory` (a check is posted but never blocks), or `blocking` (the check fails and blocks the merge). |
| **Minimum severity** | The lowest finding severity that counts against the PR. |
| **AI verdict filter** | Count all findings, or only those AI verification has confirmed as true positives. |
| **Delta baseline** | Gate on **all** open findings, or only those **net-new** in the PR. |
| **Branches** | Which target branches the policy applies to. |
A net-new baseline with `blocking` mode is the common choice: a PR is blocked only when it *introduces* a finding at or above your threshold, so existing debt doesn't freeze every merge.
## Turning on blocking
A policy is **advisory** by default. Switch to **blocking** only after the check is wired into your SCM's branch protection — otherwise the check posts but nothing enforces it.
1. In **Settings → Merge Gating**, edit the repository's policy and set the mode to **blocking**.
2. Vygl posts a one-time **seed check** so the SCM registers the check by name.
3. In your SCM's branch-protection settings, add that check as a **required status check** on the protected branch.
4. The repository's enforcement status advances to **enabled** — from then on, failing checks block merges.
The **Last verdict** column shows the most recent check result for each repository.
## Per-project overrides
The policy is stored per repository, so different projects can gate at different thresholds — a public-facing service at `high`, an internal tool at `critical` only. There is no separate org-wide default to override; you set each repository's policy directly.
---
## Severity Gates
URL: https://docs.vygl.io/scanning/severity-gates/
> Fail your CI/CD build when findings reach a chosen severity level — configurable per-project, per-repo, or per-run.
A severity gate makes the CLI exit non-zero when findings at or above a chosen level are present, failing your CI build. Three places can set it.
## Setting the gate
### Project setting (recommended for teams)
Open **Projects → (your project) → Settings → Scanning & AI Overrides** and pick a level under **Fail CI/CD build on severity**. The CLI picks it up automatically — pipelines stay simple.
```bash
vygl scan . # Picks up the project setting
```
### `.vygl.yml`
Set `scan.fail_on` in your repo's config file. Useful for repos that should always have a stricter gate than the project default.
```yaml
scan:
fail_on: high
```
### CLI flag
A local `--fail-on` flag wins over both, so individual developers can tighten the gate temporarily:
```bash
vygl scan --fail-on critical .
```
## Severity levels
| Level | Meaning |
|---|---|
| `critical` | Fail only on critical findings (most permissive) |
| `high` | Fail on critical or high |
| `medium` | Fail on critical, high, or medium |
| `low` | Fail on any finding (most strict) |
When no gate is set, the CLI exits 0 regardless of findings — useful for dashboard-only "Monitor" mode.
## Precedence
When multiple sources are set, Vygl picks the value in this order:
1. CLI `--fail-on` flag
2. `.vygl.yml` `scan.fail_on`
3. Project setting in the dashboard
4. No gate (exit 0)
## What "fail" means
When the gate triggers:
- The CLI prints a summary table of triggering findings.
- The process exits with code 1 (CI build fails).
- Findings are still uploaded — your dashboard reflects the scan whether or not the gate fired.
The CLI also exits 1 — independent of `--fail-on` — when any finding is produced by a cloud-policy rule whose mode is **Block**. Block-mode rules let you gate CI on specific high-stakes patterns regardless of the org-wide severity threshold.
---
# Container Security
## Source ↔ Container Correlation
URL: https://docs.vygl.io/containers/correlation/
> Map vulnerabilities found in container images back to the source dependencies that share the same CVE.
When the same project scans both as source code (SCA) and as a container image, Vygl links findings that share a CVE. If a `lodash` CVE is flagged on both your source `package-lock.json` and inside `/app/node_modules` of your container, Vygl ties the two findings — a high-confidence signal that the CVE is actually deployed, not just theoretical.
## How matches are computed
Vygl matches on two facts:
- **Package name** — exact equality between the SCA finding's `package_name` and the container finding's `package_name`.
- **At least one shared CVE ID** — non-empty intersection between the two findings' `cve_ids`.
When both line up, the SCA and container findings are linked. They remain separate rows (each has its own triage state and AI verdict) — the link is metadata. Ecosystem and package version are **not** part of the match today, so the linkage is "this package is named the same in both worlds and they share at least one advisory" rather than "the exact same artifact."
## Where to find correlations
**Image detail.** The image detail page has a **Source Correlations** section listing every match for that image, grouped by source project.
**API.** `GET /containers/correlations/{project_id}` returns correlations from the project side; this isn't surfaced as a panel on the project detail page yet, but AI assistants can call it via MCP (`get_container_correlations`).
## Why correlation matters
Container scans tend to be noisy: a base image might have 200 CVEs, of which only a handful are in packages your code actually uses. Source ↔ container correlation surfaces the subset that's both in your code *and* deployed in your image — the highest-priority subset for triage.
A few practical use cases:
- **"Which CVEs are reachable in production?"** — start with the correlations.
- **"Is this base-image CVE actually a problem for us?"** — if it doesn't correlate to a source dep with a shared CVE, your code isn't using a vulnerable version of that package.
- **"Did my npm upgrade actually ship?"** — re-scan the container; if the prior correlation drops off, the upgrade landed in the image.
---
## Image Detail & Layer Analysis
URL: https://docs.vygl.io/containers/image-detail/
> Inspect individual images — layer-by-layer breakdown, vulnerable packages by ecosystem, base-layer flagging, AI risk summary.
Each scanned image gets a detail page that combines raw scan output with AI-assisted analysis to help you decide what to fix first.
## Anatomy of the page
**Header.** Image reference (registry/repo:tag), detected OS (e.g. `debian:12`), severity chips, fixable count, project name, and "scanned X ago".
**AI Analysis.** A pre-computed summary of the image's risk posture and remediation priorities, written by your configured LLM. See [Container AI Analysis](/ai/container-analysis/) for details.
**Layer breakdown.** The image's layers in order, each annotated with the packages it introduced and the CVEs found in those packages. Layers attributed to the base image (via osv-scanner's `base_image_index` heuristic) are flagged so you can tell inherited issues from build-introduced ones.
**Vulnerable packages.** Two side-by-side tables — **OS Packages** and **App Packages** — each grouping CVEs per package, plus a separate flat **All Vulnerabilities** table at finding grain (one row per CVE).
**Source correlations.** When a vulnerable container package shares a CVE with a dependency in one of your source projects, Vygl links the two. Click through to see whether this is a "real" deployed vulnerability or just something present in a build artifact. See [Source ↔ Container Correlation](/containers/correlation/).
## Reading the layer view
Layers are displayed bottom-to-top — the base image is at the bottom, your application at the top. Use this to answer "where did this CVE come from?" without manual `docker history`:
- **Base layers (flagged).** Your base image (`debian:12`, `alpine:3.19`, etc.). CVEs here are inherited; the fix is usually a base-image upgrade.
- **Middle layers.** Package installs (`apt-get install`, `pip install`, `npm install`). CVEs are introduced by your install steps.
- **Top layers.** Application code. Rarely has CVEs in its own packages, but `node_modules` or `vendor/` might.
## Base-image upgrade prompt
When the base layer carries a meaningful share of the image's CVEs, the detail page shows a prominent callout — "Upgrading the base image would resolve N of M vulnerabilities." Vygl flags the impact but does not currently propose a specific upgrade target image; **always validate compatibility** before swapping a base, since runtime libraries and entrypoints can shift between minor versions.
## Triage in the same flow
Each finding on the detail page has the same triage controls as a source finding — mark `false_positive`, `ignored`, or `fixed`, with optional reasons and AI verification. See [Triage States & Lifecycle](/findings/triage/).
---
## Container Scanning
URL: https://docs.vygl.io/containers/overview/
> Scan container images for OS-package and application-dependency vulnerabilities — locally with the CLI or automatically via a connected registry.
Container scans inspect every layer of a Docker image and report CVEs in OS packages (apt, apk) and application dependencies (pip, npm, gem, go modules). Scan a local image with the CLI, or [connect a registry](/containers/registries/) for automatic scanning on every push.
## What's scanned
| Type | Examples |
|---|---|
| **OS packages** | `apt` (Debian/Ubuntu) and `apk` (Alpine) — the engine classifies these as OS packages and tags them as such. |
| **Application dependencies** | npm `node_modules`, Python `site-packages`, Go binaries, Ruby gems, Maven JARs, Composer packages. |
The engine walks the image layer by layer, enumerates every package present at runtime, and matches each one against the OSV vulnerability database. Findings are tagged with the layer that introduced the package, so you can pinpoint whether an issue comes from your base image or your build.
OS packages from RHEL/CentOS, pacman, or zypper are scanned by osv-scanner if present, but Vygl currently only flags Debian/Ubuntu/Alpine entries with the `is_os_package` tag.
## Scanning a local image
Use `--image` to scan a container image instead of a directory:
```bash
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-e VYGL_API_KEY=vgl_your_key \
vygl/vygl-cli scan --image myorg/myapp:1.2.3
```
`--image` forces `--scan-types container`. Mounting the Docker socket lets the CLI pull and inspect the image. For local-only runs, add `--no-upload`.
## Scanning from a registry
Connecting a registry is the recommended approach for production workloads. See [Connecting Registries](/containers/registries/) for the supported registries.
## What you'll see
For each image, Vygl produces:
- **Severity counts** — critical / high / medium / low across OS and app packages.
- **Layer attribution** — which layer introduced each vulnerable package.
- **Base-layer flagging** — layers attributed to the base image (using osv-scanner's `base_image_index` heuristic) are flagged in the UI; CVEs concentrated there usually mean a base-image upgrade is the right fix.
- **AI risk summary** — prioritized remediation written by your configured LLM (Anthropic, OpenAI, Bedrock, or OpenAI-compatible). See [Container AI Analysis](/ai/container-analysis/).
- **Source correlation** — links to source-code dependencies that share a CVE with the container finding. See [Source ↔ Container Correlation](/containers/correlation/).
## Finding metadata
Every container finding carries:
- `scan_type=CONTAINER`
- `package_name`, `package_version`, `fixed_versions`
- `metadata.image_ref` — the image reference scanned.
- `metadata.pkg_type` — package format (`deb`, `apk`, `pypi`, `npm`, …).
- `metadata.ecosystem` — full OSV ecosystem string.
- `metadata.layer_index` — index into the image's layer list (numeric, not a digest).
- `metadata.is_os_package` — boolean flag for OS-vs-app classification.
- `metadata.aliases` — alternate CVE/advisory IDs.
- `metadata.advisory_url`, `metadata.urgency` — when known.
The detected base OS lives on the **scan record** (`metadata_.image_metadata.os` — e.g. `debian:12`), not on individual findings.
These fields appear in the finding detail view and in API responses (`GET /findings/{id}`).
---
## Connecting Registries
URL: https://docs.vygl.io/containers/registries/
> Connect Harbor, AWS ECR, Google Artifact Registry, GitHub GHCR, or Docker Hub — Vygl discovers images, scans them, and runs a daily catch-up rescan.
Connect a registry so Vygl can discover your images, scan them for OS-package and dependency vulnerabilities, and keep watching them as new CVEs are disclosed. Five providers are supported: **Harbor**, **AWS ECR**, **Google Artifact Registry**, **GitHub Container Registry (GHCR)**, and **Docker Hub**.
All five are scanned on a **daily catch-up rescan**. **Harbor** additionally supports **webhook-driven scanning** — newly pushed images are scanned within seconds. The other providers don't deliver push webhooks to Vygl yet, so their images are picked up on the next nightly rescan.
## Setup
1. Open **Settings → Registries** and click **Connect Registry**.
2. Pick your provider and give the connection a **Display Name**.
3. Enter the **Registry URL** and the provider-specific credentials (below).
4. Click **Connect**. Vygl tests the credentials against the registry and saves the connection **only on success**. (The "Test connection" button is a smoke check on the form fields — the real verification happens on Connect.)
5. Pick the repositories to enable. Each enabled repo becomes a Vygl project automatically.
## Harbor
| Field | Value |
|---|---|
| Registry URL | `https://harbor.example.com` |
| Username | Harbor robot account or user |
| Password | Matching password / token |
Harbor is the only provider with real-time, push-driven scanning. After you connect, Vygl shows a **one-time webhook secret** — copy it immediately, it is not shown again.
**Webhook setup.** In your Harbor project, add a webhook policy with:
| Harbor field | Value |
|---|---|
| Notify Endpoint URL | `https://api.vygl.io/api/v1/registries/webhooks/harbor/` (shown on the connection detail page) |
| Auth Header | `Secret ` |
Vygl verifies the HMAC on every delivery, so pushed images are scanned within seconds. Without the webhook, pushes are still caught by the daily rescan.
## AWS ECR
| Field | Value |
|---|---|
| Registry URL | `123456789012.dkr.ecr.us-east-1.amazonaws.com` (account host, **no** `https://`) |
| AWS Access Key ID | `AKIA…` |
| AWS Secret Access Key | the matching secret |
| AWS Region | e.g. `us-east-1` (must match the region in the URL) |
Create an IAM user with a read-only policy granting these actions:
```
ecr:DescribeRepositories
ecr:DescribeImages
ecr:GetAuthorizationToken
ecr:BatchGetImage
ecr:GetDownloadUrlForLayer
```
ECR images are picked up by the nightly rescan; there is no push webhook.
## Google Artifact Registry
| Field | Value |
|---|---|
| Registry URL | `us-central1-docker.pkg.dev` (region host — must match the Location below) |
| Service Account JSON | the full key file contents, pasted in |
| Location | the AR location, e.g. `us-central1` |
| Project ID | your GCP project, e.g. `my-gcp-project` |
Create a service account and grant it **`roles/artifactregistry.reader`** on the project (or a finer-grained grant on each Artifact Registry repository). Download a JSON key for it and paste the entire file into the **Service Account JSON** field.
GAR images are picked up by the nightly rescan; there is no push webhook.
## GitHub Container Registry (GHCR)
| Field | Value |
|---|---|
| Registry URL | `ghcr.io` (always) |
| GitHub Username / Org | the **package owner** — your username, or an org name for org-owned packages |
| Personal Access Token | a classic PAT with the `read:packages` scope |
The username field doubles as the package owner Vygl enumerates: pass an **org name** to scan that org's packages, or your username for personal packages. Generate the PAT at **GitHub → Settings → Developer settings → Personal access tokens (classic)** with only `read:packages`.
GHCR images are picked up by the nightly rescan; there is no push webhook.
## Docker Hub
| Field | Value |
|---|---|
| Registry URL | `docker.io` (always) |
| Docker Hub Username | your Docker Hub account |
| Access Token (PAT) | `dckr_pat_…` |
| Namespace | *(optional)* an org/namespace you can read; defaults to your username |
Create the token at **hub.docker.com → Account Settings → Security → New Access Token**. The default **Read** scope is enough. Use the **Namespace** field to scan an organization's repositories you have read access to.
Docker Hub images are picked up by the nightly rescan; there is no push webhook.
## Scan cadence
| Trigger | When | Providers |
|---|---|---|
| **Webhook** | Within seconds of an image push | Harbor only |
| **Daily catch-up rescan** | 03:00 UTC, dispatches a fresh scan for the latest tag of every enabled repo whose `nightly_rescan` flag is on (default: on) | All providers |
The catch-up rescan re-scans even unchanged images, so a CVE disclosed today surfaces on tomorrow's scan. Findings land in the connected repo's project as container findings, alongside your SAST/SCA/secrets/IaC results.
## Tag filtering
Per-repo `tag_patterns` (glob, default `["*"]`) and `exclude_tags` fields exist on the connection model and are exposed in the API, but they are **not yet enforced** by the rescan or webhook handlers — every pushed/latest tag is scanned today. A user-facing tag filter is on the roadmap.
---
# Findings & Triage
## Auto-Fix Reconciliation
URL: https://docs.vygl.io/findings/auto-fix/
> How Vygl detects fixed findings — by comparing fingerprints across full scans, per branch and per scan-type.
When you ship a fix, Vygl notices automatically: a previously-seen fingerprint that doesn't reappear in a full scan is marked `auto_fixed`. Reconciliation is per-branch and per-scan-type, so each branch evolves independently and a SAST-only scan can't accidentally close SCA findings.
## When auto-fix runs
After a scan ingest, reconciliation runs when:
1. **`full_scan=true`** — the scan covered the entire repository, not a PR diff or a partial run.
2. **The ingest specifies a branch** — any branch, not just the project's default. Each branch reconciles its own row state.
3. **Eligible scan types** — only findings whose `scan_type` is in the ingest's `scan_types` are considered. A SAST-only full scan won't close SCA, Secrets, IaC, or Malware rows.
4. **Same origin** — `origin=scan` ingests reconcile `origin=scan` rows; `origin=cve_watch` ingests reconcile their own. The two never cross.
5. **Eligible status** — only findings in `open`, `acknowledged`, or `fix_claimed` are reconciled. Sticky decisions (`fixed`, `false_positive`, `ignored`, `suppressed`, `auto_fixed`) survive.
Per-row, the rule is simple: if the fingerprint isn't in the new scan's results, the row flips to `auto_fixed`. CVE Watch hourly ingests run their own reconciliation pass for `origin=cve_watch` rows, so an OSV advisory that's withdrawn or a package that's upgraded out of the vulnerable range closes its CVE-watch finding the same way.
## Why per-branch?
Each branch has its own row state for a given fingerprint, so reconciliation is naturally scoped: a feature-branch full scan auto-fixes its own findings without touching `main`, and a `main` full scan only acts on `main`'s rows. There's no cross-branch flapping when developers branch and merge.
## Why per-scan-type?
Each engine knows what it scanned. Marking a finding `auto_fixed` because the scan that ran "didn't include it" — when the engine for that scan type didn't even run — would be a false positive on the platform. Per-scan-type gating fixes that.
## Verifying a `fix_claimed` row
When a `fix_claimed` row's fingerprint is missing from a qualifying scan, it's promoted to `auto_fixed` and decorated with `metadata.claim_attribution` (`claimed_by_user_id`, `claimed_by_actor_kind`, `claimed_at`, `verified_by_scan_id`, `verified_at`). A `finding.status_change.claim_verified` audit event is emitted alongside the promotion — your fix-claim got verified by the next eligible scan.
If a `fix_claimed` row's fingerprint **does** reappear, the row flips back to `open` with `claim_disproven_at` set and a `finding.status_change.claim_disproven` audit event. See [Triage](/findings/triage/) for the full lifecycle.
## What's preserved
When a finding flips to `auto_fixed`:
- Its **history** stays — first-seen scan, last-seen scan, every prior status transition.
- Its **AI verdict** stays in `metadata.ai_verification`.
- Its **fingerprint** stays — if the same issue is reintroduced, this row is the one that resurrects rather than a duplicate being created.
- `metadata.claim_attribution` stays for verified `fix_claimed → auto_fixed` rows.
## Reopening an auto-fixed finding
If an `auto_fixed` finding's fingerprint reappears, the row is automatically flipped back to `open`:
- `status_reason` is set to `"Re-detected by scan {short_scan_id}"`.
- `status_changed_by` is cleared (the system, not a user, did this).
- A `finding.status_change.auto_fixed_resurrected` audit entry is emitted.
- Prior triage history and AI verdict survive on the row's metadata.
This handles the regression case cleanly — the same finding appears in the timeline as `first_detected → auto_fixed → resurrected → ...`, all on one row.
---
## Detection History
URL: https://docs.vygl.io/findings/history/
> Every finding has two parallel timelines — a scan-by-scan detection record and an audit-driven status-transition log.
Every finding carries two parallel histories. **Detection history** records which scans saw the fingerprint and how (new / recurring / auto_fixed). **Status-transition history** records every triage action — human and system — with actor, prior status, new status, and reason.
## What's tracked
Per finding:
- **First detection** — `first_seen_scan_id` is set on initial insert and never overwritten. This pointer is per (project, branch, fingerprint) row, so a fingerprint first seen on `feature-x` then reaching `main` will have a separate first-seen on the main row.
- **Re-detections** — every subsequent scan that contained the same fingerprint, joined via the `scan_findings` table, with `detection ∈ {new, recurring, auto_fixed}` recording how the scan saw the row.
- **Status transitions** — every status change with actor, timestamp, and reason. Includes both human triage (`finding.triage`, `finding.bulk_triage`) and system events (`finding.status_change.{claim_verified, claim_disproven, auto_fixed_resurrected, suppression_expired}`).
- **AI verdict (current)** — the latest verdict, confidence, and reasoning live in `metadata.ai_verification`. Re-running AI verification overwrites the prior verdict — it isn't versioned.
## Reasons in lieu of comments
Per-finding comments aren't a feature today. Each triage action accepts an optional `status_reason` string instead — recorded on the row itself and on the audit-log entry. The status-history view renders the reason next to each status change, making it the conventional place to leave triage context like:
- *"Vendor patch ETA mid-May, accepting until then"* — context for `acknowledged`.
- *"WAF rule deployed, no code change needed"* — explanation for `false_positive`.
- *"Tracked in JIRA-1234"* — link to external work.
`status_reason` is updated on each new triage rather than appended to. Use the audit log if you need to read the full sequence of reasons over time.
## Where to find it
On any finding detail page:
- The **History** section shows scan-by-scan detection with branch and commit SHA per scan.
- The **Status history** section reconstructs status transitions from the audit log, including system events.
For programmatic access:
- `GET /api/v1/findings/{id}/history` — returns the scan-detection event stream from the `scan_findings` join (`detection ∈ {new, recurring, auto_fixed}`).
- `GET /api/v1/findings/{id}/status-history` — returns the audit-derived status-transition stream.
## What history doesn't have (yet)
- **Per-finding threaded comments** — use `status_reason` for now or link to an external tracker.
- **AI verdict versioning** — only the latest verdict is kept; re-verification overwrites prior verdicts.
- **Full-text search** across reasons.
---
## Findings Overview
URL: https://docs.vygl.io/findings/overview/
> A unified view of every vulnerability across every scan — searchable, filterable, deduplicated by fingerprint per branch.
The **Findings** view is the central triage surface in Vygl. Every issue from every scan is here, deduplicated per (project, branch, fingerprint) so the same problem detected across multiple scans of the same branch appears as a single row.
## What you see
Each row is a unique finding with:
- **Severity** — critical / high / medium / low / info.
- **Type** — SAST, SCA, Secrets, IaC, Malware, or Container.
- **Title** — human-readable summary from the rule.
- **File location** — `path/to/file.py:42`.
- **Status** — `open` / `acknowledged` / `fix_claimed` / `fixed` / `false_positive` / `ignored` / `suppressed` / `auto_fixed` (the last is system-applied).
- **AI verdict** — true positive / false positive / uncertain (when AI verification has run).
- **First seen / last seen** — when the issue first appeared on this branch and most recently re-detected.
## Filters
Combine filters to narrow the list:
- **Severity** — multi-select.
- **Status** — open by default; flip to include suppressed/fixed for historical views.
- **Scan type** — focus on secrets, dependencies, etc.
- **Project** — single project or org-wide.
- **Rule** — drill into one specific rule.
- **AI verdict** — show `true_positive`, `false_positive`, `uncertain`, or `unanalyzed`.
- **Branch** — sentinels `default` (project default with fallback) or `all`, or a literal branch name. Works org-wide as well as per-project.
- **Origin** — `scan` vs `cve_watch` vs `manual`.
- **Created / resolved time windows** — `created_after`, `resolved_after`.
Filters persist in the URL — bookmark or share a triage view directly.
## Deduplication
Findings are uniquely keyed by `(project_id, branch, fingerprint)`. Within a branch, the same fingerprint detected by multiple scans collapses into one row; **across** branches, the same fingerprint is one row per branch — but sticky triage decisions (`false_positive`, `ignored`, `fixed`, `suppressed`) and AI verdicts are seeded onto a sibling-branch row the first time the fingerprint is detected there, so triage doesn't get re-litigated when a finding spreads from a feature branch to main.
The fingerprint formula varies by scan type — for SAST/Secrets/IaC it's `sha256(scan_type || rule_id || file_path || start_line || normalized_snippet)`; SCA and Container have engine-specific recipes that incorporate ecosystem and package identity instead of source-line position. See [Inline Suppression](/findings/suppression/) for what's stable and what isn't.
## Bulk actions
Select multiple rows and pick a status from the bulk-action dropdown — every user-settable status is supported, including `fix_claimed`, `false_positive`, `ignored`, `suppressed`, and `acknowledged`. Optionally set a `reason` text, which is recorded on each row's `status_reason` field and on the audit-log entry.
You can also queue AI verification across many findings: **Analyze unverified** dispatches verifications for every unverified finding matching the current filter (capped at 500 per click); selecting rows and clicking **Re-Analyze** forces re-verification on already-verified rows.
## Exports
Two export formats from the Findings view:
- **CSV** — assembled client-side by paginating every finding matching the active filter (no row cap; the export hits the API once per page until done). Columns: Severity, Type, Rule ID, Title, File, Line, Status, AI Verdict, Confidence, Project, Created.
- **SARIF 2.1.0** — server-side, applies the active filter, capped at 25,000 findings (the endpoint returns 413 above that). Compatible with GitHub Code Scanning, Azure DevOps, JFrog Xray.
See [Exports](/reference/exports/) for the full schema.
---
## Inline Suppression
URL: https://docs.vygl.io/findings/suppression/
> Suppress findings directly in your source code with `novygl` comments — or use in-dashboard triage when you'd rather keep an audit trail.
Two mechanisms suppress individual findings without disabling the underlying rule for the whole project: inline `novygl` comments in source, or in-dashboard triage to `false_positive` / `ignored` / `suppressed`. They behave very differently — pick the one that matches whether you want the suppression visible or invisible to the dashboard.
## Inline `novygl` comments
Add a comment on the finding's line **or the line directly above it** to suppress it.
```python
# Suppress a specific rule on this line
password = "test123" # novygl: secrets.generic.password-assignment
# Suppress all Vygl findings on this line
api_key = "AKIAIOSFODNN7EXAMPLE" # novygl
# Multi-rule suppression in one comment
# novygl: secrets.aws.access-key-id, secrets.generic.password-assignment
ARTIFACT = build_artifact(...)
```
The form is `# novygl: ` (specific), `# novygl: rule-a, rule-b` (multiple), or `# novygl` (all rules on this line). The marker is **case-insensitive** — `NOVYGL` works the same. Supported comment styles are `//`, `#`, `/* ... */`, and `--`:
```c
/* novygl: secrets.aws.access-key-id */
```
```sql
-- novygl: secrets.generic.connection-string
```
HTML / XML / template-style comments (``) are not currently recognized.
The rule ID matches the one shown in the finding detail view. Findings suppressed inline are **dropped at scan time** — they never reach the dashboard, never appear in finding lists, and never show up in compliance exports. If you need the suppression to be auditable, use in-dashboard triage instead.
## In-dashboard triage (auditable suppression)
When you want the suppression visible alongside other triage state, set the finding's status to one of:
- **`false_positive`** — the rule fired incorrectly. Sticky across rescans; appears in dashboard filters.
- **`ignored`** — accepted risk. Sticky, but supports an optional snooze (see [Triage](/findings/triage/)).
- **`suppressed`** — finding-specific suppression with a full audit trail. Used heavily by CVE Watch but available to any caller.
Each transition emits an audit-log entry (`finding.triage` or `finding.bulk_triage`) with actor, prior status, new status, and reason. Compliance reviews can reconstruct exactly who suppressed what and when.
## When to use which
- **Inline `novygl`** — the suppression should travel with the code (e.g. test fixtures with deliberately-insecure values, documented hardcoded examples in tutorials). Trade-off: the finding never appears anywhere in Vygl, so compliance reviewers can't see it without reading source.
- **In-dashboard `false_positive` / `ignored` / `suppressed`** — when you want the suppression visible and auditable, when modifying source isn't appropriate (vendored code, generated files), or when reviewers expect to see the decision in the platform.
## What "stable fingerprint" really means
Fingerprints normalize whitespace — spaces and tabs are stripped from the snippet before hashing — but they're sensitive to other edits:
- ✅ Reformatting (whitespace-only changes) preserves the fingerprint.
- ❌ Adding or removing lines above the finding shifts `start_line` and changes the fingerprint.
- ❌ Renaming a variable, splitting a long expression across lines, or otherwise changing the snippet's content changes the fingerprint.
- ❌ Moving the file changes the fingerprint.
Use the audit-trail path (`false_positive` / `ignored` / `suppressed`) when you expect the surrounding code to keep evolving — sticky triage migrates across branches automatically and survives the kind of refactors that would invalidate a hard-coded fingerprint.
---
## Triage States & Lifecycle
URL: https://docs.vygl.io/findings/triage/
> Open, acknowledged, fix_claimed, fixed, false_positive, ignored, suppressed, auto_fixed — what each means, how snoozes expire, and how fix-claims get verified.
Every finding has a status. Most transitions are explicit (a user marks something false-positive); a few — `auto_fixed`, the resurrection of an `auto_fixed` row, the verification or disproof of a `fix_claimed` row, and the lazy-revert of an expired `ignored` snooze — are applied by Vygl automatically when a scan re-detects (or stops re-detecting) a fingerprint.
## Statuses
| Status | Meaning |
|---|---|
| `open` | New finding, not yet triaged. The default state when a finding first appears. |
| `acknowledged` | Triaged but not yet fixed — "we see it, working on it". Auto-fix can still close this row. |
| `fix_claimed` | Falsifiable assertion the issue is fixed. The next full scan that doesn't re-detect the fingerprint promotes the row to `auto_fixed`; a re-detection flips it back to `open` with `claim_disproven_at` set. |
| `fixed` | Manually marked fixed. Sticky — re-detection does not reopen this row. |
| `false_positive` | Rule fired incorrectly. Sticky across rescans. |
| `ignored` | Accepted risk or deliberate suppression. Sticky, but supports a snooze (see below). |
| `suppressed` | Finding-specific suppression with a full audit trail. Used by CVE Watch and any UI/API caller that wants the suppression visible in the dashboard. |
| `auto_fixed` | Vygl detected the fingerprint disappeared from a full scan. Resurrects to `open` automatically if it reappears. |
There's no enforced workflow DAG — any user-settable status can transition to any other. Common paths:
- `open → acknowledged` — *"I see it, will fix later"*.
- `open → fix_claimed` — *"I've shipped a fix; verify on the next scan"*.
- `open → false_positive` — *"Rule is wrong"*.
- `open → ignored` (with snooze) — *"Revisit in 30 days"*.
- `acknowledged → fixed` (or auto-promoted to `auto_fixed`) — *"Done"*.
## Triaging a finding
From the finding detail page:
1. Click the **Status** dropdown.
2. Pick the new status.
3. Optionally add a `reason` — recorded on the row and on the audit log entry.
4. Save.
Bulk triage works the same way from the **Findings** list — select rows, use the bulk-action dropdown.
## The fix-claim lifecycle
`fix_claimed` is a "claim" of a fix that the next scan can verify or disprove:
- **Claim is verified.** A `full_scan` runs, the fingerprint is missing, the row promotes to `auto_fixed` with `metadata.claim_attribution` (who claimed, when, which scan verified) and a `finding.status_change.claim_verified` audit entry.
- **Claim is disproven.** A scan re-detects the fingerprint, the row flips back to `open`, `claim_disproven_at` is set, and a `finding.status_change.claim_disproven` audit entry is emitted. The dashboard's "Disproven claims" tile keys off this column.
Use `fix_claimed` rather than `fixed` when you want the platform to verify your assertion rather than trust it.
## Cross-branch sticky triage
When a fingerprint first appears on a new branch, Vygl seeds it with the most recent sticky decision (`false_positive`, `ignored`, `fixed`, `suppressed`) and AI verdict from any sibling-branch row. So if you triaged a finding as `false_positive` on `main`, the same finding's first appearance on a feature branch arrives pre-decided — no second triage required.
## Snoozable ignore
`ignored` can be temporary. When marking ignored, optionally set an expiry via API:
- `suppression_days` — 1 to 3650.
- `suppression_expires_at` — explicit ISO datetime.
- Both null (default) — permanent.
Reads use a computed effective status: an expired `ignored` row resolves to `open` immediately for query and display, even before any scan touches it. The DB row is **physically** flipped back to `open` on the next scan that re-detects the fingerprint (or the next CVE Watch ingest), and that flip emits a `finding.status_change.suppression_expired` audit entry. There's no wall-clock background sweeper today — the row stays `ignored` in storage until something writes to it.
## What "fixed" really means
There are two paths to a fixed status:
- **`fixed`** — explicit, manual, **sticky**. If the same fingerprint reappears later, the row stays `fixed`. Use when you want re-detection to be a no-op (or use `acknowledged` if you'd rather have it reopen).
- **`auto_fixed`** — automatic, **resurrects on re-detection**. If the fingerprint reappears, the row flips back to `open` with reason `"Re-detected by scan {short}"`, prior triage and AI verdict preserved on the row's metadata. The audit trail records `finding.status_change.auto_fixed_resurrected`.
See [Auto-Fix Reconciliation](/findings/auto-fix/) for the full rules.
## Required scope
Triage endpoints accept either `triage:write` or the broader `write` scope. The `read` scope can view findings but not modify state. See [API Keys](/team/api-keys/).
---
# AI Features
## Security Brief
URL: https://docs.vygl.io/ai/consultation/
> A pre-computed AI brief per project — top risks, remediation paths, suggested triage actions — with a follow-up chat to drill in.
The Security Brief is a project-scoped AI summary: Vygl pre-aggregates the relevant context (recent scans, open findings, dependencies, container vulnerabilities, your organizational memory) and the LLM produces a structured brief with the project's top risks and recommended next steps. A chat thread is attached to the brief for follow-up questions.
## Generating a brief
1. Open a project's detail page.
2. Click the **Security Brief** tab.
3. Click **Generate** on first visit, or open an existing brief if one's already there.
Vygl pre-aggregates the project's findings, scans, dependencies, and any matching organizational memory into a structured `data_summary`, then asks the LLM to produce a sectioned brief from it. The result is persisted as a `Consultation` row — your team sees the same brief next time.
## What's in the brief
A typical brief has sections like *Top Risks*, *What changed since last scan*, *Recommended actions*, and an `actions` block with concrete triage operations the LLM thinks are safe to apply.
## Apply suggested triage actions
When the brief surfaces an action ("8 findings under `tests/fixtures/` look like documented placeholders — mark them all `false_positive`?"), it renders as a one-click button on the brief itself. Clicking **Apply** runs it via the Consultation Actions API.
The whitelist is narrow:
| Action | What it does |
|---|---|
| `bulk_triage_rule` | Triage every open finding from one rule (with optional `triage_status` and `reason`). |
| `bulk_triage_findings` | Triage a specific list of finding IDs (with optional `triage_status` and `reason`). |
Default `triage_status` is `false_positive`; you can override via the API. Both actions hit the same audited triage path as a manual bulk-triage from the Findings list. Anything outside this whitelist — opening PRs, creating tickets, modifying settings — is intentionally out of reach.
## Chat about the brief
Open the chat panel under the brief to ask follow-up questions. Chat messages stream token-by-token. The chat works against the brief's `data_summary` and the prior message history — it doesn't re-query findings or memories live, so for the freshest context, regenerate the brief.
Useful prompts:
- *"Why is the second item on the priority list ranked over the third?"*
- *"What would removing this dependency cost us in terms of CVE exposure?"*
- *"Walk me through the remediation steps for the highest-priority item."*
- *"Is there a single fix that closes multiple top-risks?"*
## Brief regeneration
Vygl tracks a fingerprint over the project's open findings. When the fingerprint stops matching the brief's frozen state — typically after a new scan or a meaningful triage shift — the page surfaces a **Regenerate** button. Regenerating creates a new `Consultation` row and starts a fresh chat thread; your prior brief and chat history remain in the database, just out of view.
Container images get their own pre-computed AI summary, separate from the project-level brief. See [Container AI Analysis](/ai/container-analysis/).
---
## Container AI Analysis
URL: https://docs.vygl.io/ai/container-analysis/
> A pre-computed AI risk summary for every scanned image — risk posture, remediation priorities, base-image guidance.
After a container scan, Vygl runs a one-shot AI analysis that produces a markdown summary: overall risk posture, the few CVEs worth fixing first, and base-image guidance when an upgrade would materially reduce inherited vulnerabilities. The result is cached against the image's open-finding state, so re-scanning the same image with the same findings returns the cached analysis instantly.
## What the summary covers
A typical Container AI Analysis has four sections:
| Section | Content |
|---|---|
| **Overall risk** | A short paragraph framing the image's posture — high/medium/low risk and why. |
| **Priorities** | The 3–5 specific CVEs or packages that should be addressed first, with reasoning. |
| **Base image guidance** | When the base layer carries a meaningful share of the image's CVEs, a callout describing the impact of upgrading. The summary names the impact, not a specific target image — confirm a concrete upgrade target before swapping. |
| **Remediation steps** | Practical next steps — base-image upgrade, package upgrade, removal of unused packages, layer reordering. |
## Caching
The analysis is keyed on a hash over the image's open-finding fingerprints. Re-scanning the same image returns the cached `sections` blob — no LLM call. New findings (or a digest change that brings new findings) invalidate the cache and trigger re-analysis.
## Streaming endpoint
If you'd rather see the analysis token-by-token (in the AI Consultation chat or programmatically):
```
GET /containers/images/ai-analysis/stream?image_ref=...&force=false
```
Returns a Server-Sent Events stream with each section as it generates. Pass `force=true` to bypass the cache and re-run the LLM unconditionally.
## How prioritization is computed
Before calling the LLM, Vygl pre-aggregates findings into a structured summary — severity counts, fixable counts, top vulnerable packages, OS vs application split, trend delta from the previous scan. The model works from this summary plus a fingerprint of the worst findings, not from a raw 200-finding table. This keeps the input small enough to fit in token budgets while still giving the model the data it needs to prioritize.
---
## Organizational Memory
URL: https://docs.vygl.io/ai/memory/
> Teach Vygl the context behind your codebase — eliminate recurring false positives across AI verdicts, briefs, and container analyses.
Organizational memory captures the context that makes a generic finding a false positive in *your* codebase: a sanitization helper the SAST rule doesn't know about, a directory of intentional fixtures, a parameterized ORM that defeats a SQL-injection pattern, a CVE you've already accepted in a specific package. Once a memory is in place, every AI feature — verification, the security brief, container analysis — applies it consistently, so the same false positive never reaches your queue twice.
## The false-positive problem memory solves
Static rules fire on patterns. They don't know that *your* `db.execute()` calls all flow through a parameterizing wrapper, that `tests/fixtures/` is supposed to contain credential-shaped strings, or that your team accepts `dangerouslySetInnerHTML` inside `components/markdown/` because content is sanitized upstream. Without that context, the same noisy verdict gets re-litigated by every developer who opens the finding.
Memory closes the gap. Concretely, well-scoped memories:
- **Eliminate recurring false positives.** A verdict that flipped from `true_positive` to `false_positive` once a memory was in place stays that way for every future finding the memory matches — across new files, new branches, and new contributors.
- **Replace dozens of identical reviews.** A triage decision encoded as a memory does the work of repeated "is this a real bug?" reviews on every future occurrence.
- **Travel with new code.** A project-scoped memory applies automatically to findings in code that didn't exist when the memory was written, so onboarding a module doesn't restart the noise cycle.
- **Compose.** Org-wide and project-scoped memories combine; rule-, package-, CVE-, and image-bound memories layer on top so you can encode broad guidance and narrow exceptions side by side.
The net effect: triage time spent on AI-verified findings drops over weeks as your team's institutional knowledge accumulates in memory rather than in Slack threads.
## Authoring a memory
1. Open **Settings → AI Knowledge** (or the **AI Memory** tab on a project page if the memory should be project-scoped).
2. Click **Add memory** — or, on a finding detail page, click **Add memory from this finding** to draft one with the relevant binding pre-filled.
3. Write the fact in 1–2 sentences. Be specific.
4. Pick a **binding** — *None* (applies broadly), or one of *Rule* (a specific rule ID), *Package* (a package name), *CVE* (an advisory ID), or *Image* (an image reference). One binding type per memory.
5. (Optional) Tick **This project only** to scope to one project. Leave unticked for org-wide.
6. (Optional) Tick **Re-evaluate these N findings after saving** to refresh existing AI verdicts that match the new memory.
7. Save.
The next AI run picks up matching memories automatically.
## What makes a good memory
**Good (specific, contextual, durable):**
- *"All SQL access goes through `repo/` modules that parameterize via SQLAlchemy. Direct `db.execute()` calls outside `repo/` are FP."* — bind to the relevant SAST rule.
- *"The `shared/fixtures/` directory contains test doubles. Hardcoded credentials and tokens there are deliberate."* — bind to the relevant secrets rule, or leave unbound for org-wide.
- *"We accept CVE-2024-1234 on `package-x` until 2026-Q3 — vendor patch ETA, mitigated by WAF rule 412."* — bind to the CVE.
**Bad (vague, time-bound, sensitive):**
- *"Ignore false positives"* — too broad to act on; the verifier already tries to do this.
- *"John is on vacation, defer triage"* — ephemeral.
- *"Our AWS root key is `AKIA...`"* — never put secrets in memory.
The pattern: a good memory names a place, a rule, a package, a CVE, or a code construct, and states a durable fact about it. Vague memories don't move verdicts; specific ones do.
## How memories combine
Multiple memories can apply to a single AI call:
- **Org-wide** memories apply to every AI run in your organization.
- **Project-scoped** memories add on top when analyzing that project.
- **Bound** memories (rule / package / CVE / image) add on top when the finding's facets match.
There's no exclude/override semantics — memories accumulate. To narrow application, scope down rather than negate. When two active memories with the same binding express opposing directives ("always FP" vs "always TP"), Vygl flags the conflict on save with an inline warning; both still apply, but the warning is your hint to retire one.
## Refresh existing verdicts
New verifications pick up matching memories automatically. **Existing** verdicts don't refresh on their own — they're cached by fingerprint. Two ways to bring them up to date:
- Tick **Re-evaluate these N findings after saving** in the Add/Edit Memory dialog. Vygl queues re-verifications for findings that match the new memory's facets.
- Hit `POST /memories/{id}/reverify` later from the API.
Or use **Re-Analyze** on the Findings list to force re-verification across selected rows.
## Toggling without deleting
Each memory has an **active** toggle. Disable a memory temporarily without losing the wording — useful for "did this memory cause the regression?" experiments and for retiring a memory once the underlying code pattern is gone.
## Measuring impact
Two signals tell you a memory is doing its job:
- **Verification verdicts shift.** Open a previously-verified `true_positive` after adding a contradicting memory and re-verify — the verdict should flip to `false_positive` with reasoning that references the new context.
- **Re-triage drops off.** Findings whose verdicts the memory now governs stop showing up in your "uncertain" or "open" buckets after the next pass.
If a memory isn't moving verdicts the way you expected, make it more specific — name the file path, the rule ID, the package name, or the function signature it's meant to govern.
---
## AI Verification
URL: https://docs.vygl.io/ai/verification/
> Ask Claude to verify whether a finding is a true positive — get a verdict, confidence, reasoning, and a suggested fix.
AI verification reads the surrounding code context plus the rule that fired and returns a structured verdict — `true_positive`, `false_positive`, or `uncertain` — with a confidence score, written rationale, and a suggested fix when appropriate. Run it on a single finding, in bulk, or automatically as new findings arrive.
## Configuring AI verification (Settings → AI)
AI verification needs an LLM provider. Configure one in **Settings → AI**:
| Control | Effect |
|---|---|
| **Provider** | OpenAI, Anthropic, AWS Bedrock, or any OpenAI-compatible API (Ollama, LM Studio, vLLM, Azure OpenAI, and similar). |
| **API key** | The provider credential. Leave it blank when re-saving to keep the existing key. On Bedrock running on EC2 you can omit it and rely on an attached IAM role. |
| **Model** | The model to call — chosen from a list for OpenAI and Anthropic, or typed free-form for Bedrock and OpenAI-compatible endpoints. |
| **Base URL** | The endpoint for OpenAI-compatible or self-hosted models; optional for OpenAI. |
| **Automatic verification** | When on, new findings from each scan are verified automatically — see [below](#running-verification). |
| **Minimum severity** | With auto-verify on, only verify findings at or above this severity. |
| **System prompt** | The base instruction sent to the model. **Reset to defaults** restores the built-in prompt. |
| **Custom instructions** | Appended to the system prompt — add context about your stack, frameworks, or security policy to sharpen verdicts. |
## Running verification
**On a single finding.** Open the finding detail page and click **Verify with AI**. The job is queued; the verdict appears on the same page within seconds.
**In bulk.** Two paths from the **Findings** list:
- **Analyze unverified** — dispatches verification across every unverified finding matching the current filter (capped at 500 per click). No row selection needed.
- **Re-Analyze** — select rows and click this to force re-verification, even on already-verified fingerprints. Useful after editing organizational memory if you want existing verdicts to refresh.
Each finding is queued as a separate job — there's no batching today.
**Automatically.** Enable **Auto-verify** in **Settings → AI** and pick a minimum severity (e.g. high+ to skip lows and mediums). A 1-minute sweeper picks up unverified findings at or above the threshold and dispatches verification jobs in batches of up to 100 per org per pass. Verdicts are advisory — they ride alongside the finding's status without auto-closing it.
## What the verdict contains
| Field | Description |
|---|---|
| `verdict` | `true_positive` / `false_positive` / `uncertain` |
| `confidence` | `low` / `medium` / `high` |
| `reasoning` | A short paragraph explaining the conclusion |
| `suggested_fix` | (true positives only) Concrete code change |
The full block (verdict, reasoning, suggested fix) is visible on the finding detail page and ships in SARIF exports. CSV exports include the verdict and confidence columns. PR comments and Slack/Teams notifications use the verdict to **gate delivery** (see "Use it as a notification filter" below) but don't render the verdict body inline today.
## How verdicts are scoped
Verdicts are **cached by fingerprint** across your entire org — once a fingerprint is verified anywhere, every other finding with the same fingerprint inherits the verdict. This includes findings on different branches and in different projects that happen to share a fingerprint. If you change the underlying code such that the fingerprint changes (different file, different line, different normalized snippet), the new finding gets a fresh verification.
## What context goes into the prompt
Each verification call sends Claude:
- The **rule** that fired (description, tags, severity).
- A **snippet of code** around the finding (trimmed to fit token budget).
- The **finding metadata** — file path, line number, package version (for SCA).
- Per-scan-type guidance — e.g. for SAST, "consider whether the sink receives attacker-controlled input"; for Secrets, "check if the secret is real or a documented placeholder".
- For SCA findings: **EPSS** (exploit probability) and **CISA KEV** (actively exploited) signals when known.
Your [organizational memory](/ai/memory/) also shapes the verdict — matching memories are applied to every verification run so verdicts reflect your codebase's conventions, not just the rule's defaults.
## Use it as a notification filter
Each Slack, Teams, email, and webhook integration has an **AI verdict filter** with three modes that control when a per-finding alert (`critical_finding` / `high_finding`) actually fires:
| Mode | When it pings |
|---|---|
| **All findings** (default) | Immediately on every new critical/high finding — the historical behaviour. |
| **AI-verified TPs only** | Waits until verification commits, fires only when the verdict is `true_positive`. |
| **TPs and uncertain** | Waits until verification commits, fires for `true_positive` or `uncertain`, skips clear `false_positive`. |
The verified-only modes are the way to keep a noisy channel quiet — they trade a few seconds-to-minutes of delay for false-positive suppression.
**Prerequisites.** The verified-only modes need org-level AI verification turned on (provider, model/API key set, and **Auto-verify** in **Settings → AI**). The integration form blocks the verified-only modes until that's true; the API enforces the same rule with a 400 if you bypass the UI.
**Resurrected findings notify immediately.** When a finding is re-detected after being auto-fixed (or first appears on a new branch where a sibling already has a verdict), it inherits the cached verdict at scan time. Verified-only integrations use that inherited verdict immediately — no second wait.
**What's exempt from the filter.** `scan_completed`, `scan_failed`, and all `cve_*` events bypass verdict gating. Scan-level summaries fire regardless, and CVE Watch findings don't traverse the LLM verifier at all. Recurring findings (still-open from a prior scan) never trigger per-finding events to begin with — they only show up inside the `scan_completed` digest.
**Edge case.** If verification permanently fails for a specific finding (three retried LLM errors), no per-finding alert fires for verified-only integrations on that finding. This is a narrow gap — the broader "AI is off entirely" case is prevented by the prerequisite gate.
---
# CVE Watch
## CVE Watch
URL: https://docs.vygl.io/cve-watch/
> Hourly monitoring of your dependencies for new CVEs — enriched with EPSS exploit probability and CISA KEV.
CVE Watch checks your dependency inventory against the OSV database every hour. When a new CVE is disclosed that affects a package you already use, Vygl fires an alert — without rescanning your code. Alerts are enriched daily with **EPSS** (probability of exploitation in the next 30 days) and **CISA KEV** (Known Exploited Vulnerabilities — actively exploited in the wild).
## Enabling CVE Watch
Toggle **CVE Watch enabled** in **Settings → CVE Watch Settings**. Once enabled, your organization is included in the hourly run. There's no per-project opt-in; CVE Watch operates on your entire dependency inventory.
## How it works
| Cadence | What runs |
|---|---|
| **Every 6 hours** | Vygl syncs the OSV database incrementally. New CVEs are mirrored. |
| **Every hour** | Vygl checks each enabled org's dependency inventory against the local OSV mirror; new matches generate alerts. |
| **Daily at 04:00 UTC** | EPSS and CISA KEV feeds are refreshed and applied to existing alerts. |
End-to-end alert lag is up to ~6 hours from the time a CVE is published to OSV. EPSS/KEV signals arrive with up to 24-hour delay relative to upstream.
## Alert lifecycle
Each alert has its own status:
| Status | Meaning |
|---|---|
| `new` | Just detected; awaiting triage. |
| `acknowledged` | Triaged, work in progress. |
| `fixed` | The vulnerable dependency has been upgraded. |
| `dismissed` | Won't fix (with reason — accepted risk, mitigated upstream, etc.). |
Triage from the **CVE Watch** page. Bulk actions are supported.
## Sorting & prioritization
Three sort options:
- **CVSS** — base severity score from the upstream advisory.
- **EPSS** — predicted exploit probability. A CVE in the 95th EPSS percentile is far more likely to be attacked than one in the 5th.
- **KEV-first** — CISA KEV-listed CVEs sort to the top.
Filter to **KEV only** when you want to focus on what's actively being exploited right now.
## Notifications
CVE Watch routes through the same integrations as scan findings. Subscribe channels (Slack, Teams, email, webhooks) to:
- `cve_critical` — per-alert, critical severity.
- `cve_high` — per-alert, high severity.
- `cve_batch_summary` — end-of-run digest summarizing all new alerts.
See the [Slack](/integrations/slack/), [Teams](/integrations/teams/), [Email](/integrations/email/), or [Webhooks](/integrations/webhooks/) integration pages for setup.
## What's matched
CVE Watch matches on **ecosystem + package name + version range**. Range matching uses ecosystem-specific semantics — npm tilde/caret ranges, Python PEP 440, Go module proxy semantics, etc. Vygl applies the OSV `affected.ranges` definition for each ecosystem.
---
# Integrations
## Email Notifications
URL: https://docs.vygl.io/integrations/email/
> Send finding summaries, CVE alerts, and scan results to one or more email recipients.
Vygl sends two kinds of email: **transactional** (org invites, password resets — these go automatically and aren't configurable) and **event notifications** (scan results, finding alerts, CVE Watch — opt-in).
## Setting up event notifications
1. Open **Settings → Integrations** and click **Add Email**.
2. Enter one or more recipient addresses (comma-separated).
3. Pick events: `scan_completed`, `critical_finding`, `high_finding`, `cve_critical`, `cve_high`, `cve_batch_summary`.
4. Apply filters (project, severity, scan type) if you want to scope.
5. Pick an **AI verdict filter** — keep the default to email every critical/high alert immediately, or pick **AI-verified TPs only** / **TPs and uncertain** to wait for the LLM verdict and skip false positives. Requires AI verification to be enabled. See [AI Verification → notification filter](/ai/verification/#use-it-as-a-notification-filter).
6. Save. Email integrations don't have a working **Test** button today — confirm by triggering a real scan or CVE Watch event.
## What's in a notification email
Branded HTML with the Vygl logo and a responsive layout. Each event type has its own template:
- **Scan completion** — summary table, severity breakdown, link to scan detail.
- **Finding alert** — finding title, file, rule, link to finding detail.
- **CVE Watch** — affected packages, OSV ID, severity, link to alert.
Notifications are HTML-only — there's no plain-text alternative today.
## Transactional emails
These send automatically — no configuration needed:
- **Member invites** — sent when an admin invites a user. The link expires in 7 days.
- **Password reset** — sent when a user clicks *Forgot Password*. The link expires in 15 minutes.
Transactional emails carry one-time tokens that hash on the server; the raw token never goes to the database.
---
## Jira
URL: https://docs.vygl.io/integrations/jira/
> Auto-create Jira tickets from findings and sync triage state. Currently in development.
When complete, the Jira integration will create tickets automatically for findings that meet a configurable severity/type threshold, link the ticket back to the finding in Vygl, and sync triage state in both directions.
*This page will be updated when the feature ships.*
---
## Slack
URL: https://docs.vygl.io/integrations/slack/
> Send scan summaries, finding alerts, and CVE notifications to Slack via incoming webhooks. Filter by severity, scan type, or project.
Connect a Slack incoming webhook to receive real-time scan results, critical-finding alerts, and CVE Watch notifications. Each integration can subscribe to specific event types and filter by severity, scan type, or project so each channel only gets what its team needs.
## Setup
1. **Create a Slack incoming webhook.** In Slack, go to [api.slack.com/messaging/webhooks](https://api.slack.com/messaging/webhooks), pick the workspace and channel, and copy the webhook URL.
2. **Add the integration in Vygl.** Open **Settings → Integrations**, click **Add Slack**, paste the webhook URL. The URL must begin with `https://hooks.slack.com/` — the integration form rejects anything else.
3. **Subscribe to events.** Pick which events should fire to this channel — see the [event list](#available-events) below.
4. **Apply filters** — restrict to specific projects, scan types, or severities. Useful when one channel cares only about production findings.
5. **Pick an AI verdict filter** — by default every per-finding alert pings immediately. To quiet noisy channels, pick **AI-verified TPs only** or **TPs and uncertain** so Vygl waits for the LLM verdict before notifying. Requires AI verification to be enabled for your org. See [AI Verification → notification filter](/ai/verification/#use-it-as-a-notification-filter).
6. **Test.** Click the **Test** button on the integration row; a sample message should appear in your Slack channel within a few seconds.
## Available events
- `scan_completed` — every successful scan.
- `scan_failed` — engine errors or upload failures.
- `critical_finding` / `high_finding` — new findings at that severity.
- `cve_critical` / `cve_high` / `cve_batch_summary` — CVE Watch alerts.
## What's in a notification
A typical scan-completion message includes:
- Project name, branch, scan types that ran, duration, trigger (push / PR / manual / scheduled), commit SHA.
- Severity breakdown with emoji indicators.
- All new findings with type (SAST / SCA / Secrets / IaC / Malware), rule ID, file location, and clickable links.
- All recurring findings with first-seen and last-seen dates.
- A link to the full scan detail in Vygl.
CVE Watch messages are scoped to the affected dependency and include the OSV ID, severity, package, fixed versions, and the affected-project list.
When AI verification is enabled and an integration is on a verified-only filter, the AI verdict gates **whether** the message is sent — it isn't rendered inline in the message body today.
## Disabling temporarily
Toggle the integration's **Enabled** switch off to pause it without deleting the configuration. Useful for maintenance windows or noisy onboarding periods.
---
## Microsoft Teams
URL: https://docs.vygl.io/integrations/teams/
> Send scan summaries, finding alerts, and CVE notifications to Teams via Power Automate Workflows — Adaptive Card formatted.
Microsoft Teams integration uses **Power Automate Workflows** ("Post to a channel when a webhook request is received"). Vygl posts Adaptive Cards with the same content the Slack integration sends — severity breakdown, finding tables, AI verdicts, and links back into Vygl.
## Setup
1. **Create a Power Automate Workflow.** In Teams, open the target channel, click **⋯ → Workflows**, and search for "Post to a channel when a webhook request is received". Run the wizard, picking the channel that should receive notifications. Power Automate creates the workflow and shows a **HTTP POST URL** — copy it.
2. **Add the integration in Vygl.** Open **Settings → Integrations**, click **Add Microsoft Teams**, paste the URL.
3. **Subscribe to events.** Pick `scan_completed`, `critical_finding`, `cve_critical`, and any others you want.
4. **Apply filters** — restrict by project, scan type, or severity.
5. **Pick an AI verdict filter** — by default every per-finding alert pings immediately. To quiet noisy channels, pick **AI-verified TPs only** or **TPs and uncertain** so Vygl waits for the LLM verdict before notifying. Requires AI verification to be enabled for your org. See [AI Verification → notification filter](/ai/verification/#use-it-as-a-notification-filter).
6. **Test.** Click **Test** on the integration row; an Adaptive Card should arrive in your channel within a few seconds.
## What the card looks like
A typical critical-finding card includes:
- **Header** — project name, finding title, severity badge.
- **Facts** — severity, scan type, project, branch, file path, rule ID, status.
- **Action button** — *Open in Vygl* deep link.
CVE Watch alerts use the same card pattern, scoped to the affected dependency. AI verdict gating decides whether a card is sent for verified-only filters; the verdict isn't rendered inline in the card body today.
## Async delivery
Power Automate Workflows return HTTP `202 Accepted` immediately and post the card to the channel asynchronously. If a card doesn't appear in your channel:
1. Check the **Run history** of the Power Automate workflow — failures show up there with full diagnostic info.
2. Verify the workflow's Teams connection is still authorized.
3. Try the **Test** button in Vygl to fire a synthetic event.
## Retries
Notification deliveries that raise a transport error are retried up to 3 times with a fixed 5-second delay (Celery default). HTTP error responses (4xx, 5xx) don't currently trigger a retry — the worker logs the failure and moves on. If a Teams card doesn't arrive, fix the upstream workflow and re-fire via **Test** rather than relying on automatic retries.
---
## Generic Webhooks
URL: https://docs.vygl.io/integrations/webhooks/
> Generic webhook delivery is on the roadmap — POST-to-any-URL is not implemented today.
Generic webhooks — POSTing every Vygl event to a URL of your choice for SIEM ingestion, custom dashboards, or downstream automation — are on the roadmap. The provider isn't wired end-to-end today, so the **Add Webhook** option is disabled in the integrations dialog.
In the meantime, the closest equivalents:
- **Slack-formatted webhooks** ([Slack integration](/integrations/slack/)) — works with any tool that accepts Slack-incoming-webhook payloads at `https://hooks.slack.com/...` URLs.
- **Microsoft Teams Workflows** ([Teams integration](/integrations/teams/)) — Adaptive Card delivery to a Power Automate Workflow URL.
- **Email** ([Email integration](/integrations/email/)) — for "email-as-an-API" workflows feeding ticket systems via parsing addresses.
- **Direct API polling** ([REST API reference](/reference/api/)) — pull `GET /api/v1/findings`, `/scans`, and `/cve-watch/alerts` on a schedule for systems that prefer pull over push.
---
# AI Tools / MCP
## MCP Server Overview
URL: https://docs.vygl.io/mcp/overview/
> Vygl exposes a Model Context Protocol (MCP) server so AI tools can query findings, security posture, and projects directly.
The MCP server lets AI assistants — Claude Code, Cursor, OpenCode, Codex, Claude Web, Claude Desktop — read your Vygl data and trigger AI verification, all without leaving the editor. Authenticate with an API key for editor clients or via OAuth for web clients (no key needed).
## Tools exposed
**Reads**
| Tool | What it does |
|---|---|
| `list_authorized_orgs` | List the orgs this connector can act in (the entry point for multi-org users). |
| `list_projects` | List all projects in your organization. |
| `search_findings` | Search findings with filters — severity, status, scan type, file path, project, rule, AI verdict. |
| `get_finding_detail` | Full context on a finding — snippet, AI verdict, reasoning, history. |
| `get_security_posture` | Org-wide summary — severity counts, MTTR, AI coverage. |
| `get_project_health` | Per-project health score with severity breakdown. |
| `list_container_images` | All scanned images with CVE counts. |
| `get_container_image_detail` | Image layer breakdown and vulnerable packages. |
| `search_image_vulnerabilities` | Vulnerabilities (CVEs) in container images. |
| `get_container_correlations` | Source dependencies that share a CVE with container findings. |
| `get_container_analysis` | Cached AI risk summary for an image. |
| `list_memories` | List organizational memory entries. |
| `preview_memory` | Render a draft memory's effective system prompt without saving. |
| `suggest_memory_from_finding` | Ask the LLM to draft a memory based on a specific finding. |
**Writes**
| Tool | What it does |
|---|---|
| `ai_verify_finding` | Trigger AI verification on a finding; returns verdict + reasoning + suggested fix. |
| `triage_finding` | Set a single finding's status (`false_positive`, `ignored`, `acknowledged`, etc.). |
| `bulk_triage_findings` | Triage multiple findings in one call. |
| `create_memory` | Create an organizational memory entry. |
## Authentication
| Client | Auth |
|---|---|
| Claude Code, Cursor, OpenCode, Codex | API key (Bearer token). |
| Claude Web, Claude Desktop | OAuth 2.1 Dynamic Client Registration. |
Editor clients use the same API keys you generate in **Settings → API Keys**. Web clients click "Add custom integration" in Claude, enter the Vygl MCP URL, and complete a browser OAuth flow — no key management.
## Data scoping
Every MCP request is scoped to the organization that owns the API key (or the user's signed-in org for OAuth). Permissions and rate limits match the API key's scopes — the MCP server is not a privilege bypass.
If your connector is authorized for **multiple Vygl orgs** (e.g. an OAuth user who belongs to several), call `list_authorized_orgs` first to discover the available `org_id`s, then pass `org_id` on subsequent tool calls to disambiguate.
## What's next
See [IDE Setup](/mcp/setup/) for ready-to-paste configs for every supported client.
---
## IDE Setup
URL: https://docs.vygl.io/mcp/setup/
> Connect Claude Code, Cursor, OpenCode, Codex, Claude Web, or Claude Desktop to the Vygl MCP server.
Each AI client has its own configuration file format. The MCP server URL is the same — `https://app.vygl.io/mcp`.
## With an API key (editor clients)
1. Generate an API key in **Settings → API Keys**. Copy it (the prefix is `vgl_`).
2. Add the MCP config to your client (pick your tool below).
3. Start asking questions: *"What are my top three risks?"*, *"Verify finding f-abc123"*, *"Show me unfixed criticals on the auth-service project."*
Add to `.mcp.json` at your project root:
```json
{
"mcpServers": {
"vygl": {
"type": "http",
"url": "https://app.vygl.io/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
Or via the CLI:
```bash
claude mcp add-json vygl '{"type":"http","url":"https://app.vygl.io/mcp","headers":{"Authorization":"Bearer YOUR_API_KEY"}}'
```
Add to `.cursor/mcp.json` at your project root:
```json
{
"mcpServers": {
"vygl": {
"type": "http",
"url": "https://app.vygl.io/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
Add to `opencode.json` at your project root:
```json
{
"mcp": {
"vygl": {
"type": "remote",
"url": "https://app.vygl.io/mcp",
"headers": {
"Authorization": "Bearer YOUR_API_KEY"
}
}
}
}
```
The Vygl MCP server uses streamable-HTTP transport. If your OpenCode version expects `"type": "http"` instead of `"remote"`, use that — both should accept the same URL.
Add to `~/.codex/config.toml` (global) or `.codex/config.toml` (project):
```ini
[mcp_servers.vygl]
type = "http"
url = "https://app.vygl.io/mcp"
[mcp_servers.vygl.headers]
Authorization = "Bearer YOUR_API_KEY"
```
## With OAuth (Claude Web & Claude Desktop)
For browser-based Claude clients there's no API key — sign in with your Vygl credentials (email/password or any SSO provider).
1. Open **claude.ai → Settings → Integrations**.
2. Click **Add custom integration**.
3. Enter the URL:
```
https://app.vygl.io/mcp
```
4. Complete the Vygl sign-in popup when prompted.
Once connected, Vygl tools are available in every Claude conversation. The sign-in session lasts seven days; renew when prompted.
1. Open **Claude Desktop → Settings → Integrations**.
2. Click **Add custom integration**.
3. Enter the URL:
```
https://app.vygl.io/mcp
```
4. Complete the Vygl sign-in when prompted.
If the integration doesn't appear immediately, restart Claude Desktop.
---
# Team & Access
## API Keys
URL: https://docs.vygl.io/team/api-keys/
> Create scoped API keys for the CLI, MCP, and direct REST API access.
API keys are organization-scoped credentials with selectable permissions and an optional expiry. The same key powers the CLI, third-party integrations, and the MCP server in static-token mode.
## Creating a key
1. Open **Settings → API Keys** and click **Create Key**.
2. Give the key a name — something descriptive like "GitHub Actions CI" or "PagerDuty webhook".
3. Optionally set an **expiry**. Keys without an expiry never expire.
4. Click **Create**. Vygl shows the key **once** — copy it immediately. The raw key is never displayed again.
Keys start with the `vgl_` prefix; the first eight characters are visible in the UI for identification, the rest is secret.
Keys created from the dashboard ship with a default scope set: `read`, `write`, `scan:read`, `scan:write`, `triage:write`, `memory:write`. There's no UI to narrow that today — for least-privilege keys, mint via the API and pass an explicit `scopes` array.
## Scopes
| Scope | Grants |
|---|---|
| `read` | Read all resources (findings, projects, scans, dependencies). |
| `write` | Full write access (broader than `triage:write` / `memory:write`; reach for it only when needed). |
| `scan:read` | Read scans only. |
| `scan:write` | Submit scans (the typical CI scope). |
| `triage:write` | Change finding status (single and bulk). |
| `memory:write` | Create / edit organizational memory. |
| `admin` | Admin operations (create members, integrations, etc.). |
Most automation needs only `scan:write` or `read`. Reach for broader scopes only when you have a specific reason.
## Where to put the key
| Use case | How |
|---|---|
| **CI / CD pipeline** | Set `VYGL_API_KEY` as a CI secret (GitHub Secrets, GitLab CI variables, Bitbucket repository variables) |
| **Local CLI** | `vygl auth set-key ` stores it at `~/.vygl/credentials` (chmod 600) |
| **REST API** | `Authorization: Bearer vgl_...` header |
| **MCP (editor clients)** | The client config file — see [IDE Setup](/mcp/setup/) |
## Rotating keys
There's no built-in rotation endpoint — to rotate, **create a new key, update your systems, then revoke the old one**. The two-key window prevents downtime.
## Revoking
From **Settings → API Keys**, click **Revoke** on the row. Revocation is immediate; subsequent requests with that key return 401. The audit log preserves the key's history (who created it, when it was last used, when it was revoked).
## Last-used tracking
Each key shows when it was last used — useful for spotting stale keys that should be retired. Keys unused for 90+ days are good rotation candidates.
---
## Audit Log
URL: https://docs.vygl.io/team/audit-log/
> An immutable record of every triage action, member change, integration update, and scan submission.
Every state-changing action in Vygl produces an audit-log entry. Triage decisions, member invites, integration setup, custom rule edits, scan submissions, API key creation — all logged with the actor, timestamp, target resource, HTTP context, and a structured event payload. Useful for compliance reviews, post-incident reconstruction, and detecting abuse.
## What's recorded
Each entry includes:
| Field | Example |
|---|---|
| `action` | `finding.triage`, `org.member.invite`, `integration.create` |
| `actor` (user or API key) | The user ID or key prefix |
| `auth_type` | `jwt` or `api_key` |
| `resource_type` / `resource_id` | The thing being mutated (`finding`, `member`, `rule`, …) |
| `org_id` | Always present — every entry is org-scoped |
| `ip_address` | The request IP |
| `user_agent` | The client that made the request |
| `http_method` / `http_path` / `status_code` | Full request context |
| `details` | Structured JSON specific to the action (old value, new value, reason, …) |
| `created_at` | UTC timestamp |
The action vocabulary spans roughly 60 distinct types covering auth, organizations, members, projects, scans, findings, rules, integrations, memory, registries, SCM, CVE Watch, and admin actions. MCP-originated calls show as `auth_type=api_key` (or `jwt` for OAuth flows) — there's no separate `mcp` auth type because MCP forwards the underlying credential.
## Querying
Today the audit log is **superadmin-only** — there's no in-app audit-log surface for org admins. If you need a compliance review, request an export from Vygl support. An org-admin-facing audit-log view is on the roadmap.
Filters available to superadmins:
- **Action category** — `auth.*`, `finding.*`, `scan.*`, `project.*`, `org.*`, `rule.*`, `integration.*`, `memory.*`, `admin.*`.
- **Actor** — by user ID or API key prefix.
- **Date range**.
- **Path / method** — useful for tracking specific endpoints.
- **Status code** — 4xx / 5xx for failure analysis.
## Append-only by design
Audit entries are **append-only** — there's no API to edit or delete them. This is intentional: the audit log's value as a compliance artifact comes from immutability. To remove an entry, ops would need direct database access plus a paper trail.
## Common queries
**Who marked these findings false-positive?**
Filter `action = finding.triage`, `details.status = false_positive`, group by actor. False-positive abuse — a user mass-marking findings FP to silence them — is detectable from this query (cross-check with `finding.bulk_triage` for batch operations).
**Did a fix-claim get verified or disproven?**
Filter `action ∈ {finding.status_change.claim_verified, finding.status_change.claim_disproven}`. Each entry carries the scan ID that produced the verdict.
**Who set up the Slack integration?**
Filter `action = integration.create`, then look at `details` for the provider field.
**Did this API key do anything unexpected?**
Filter by the key's prefix in `actor`. The full request history of a key is reconstructable from the log.
## Retention
Audit entries are retained indefinitely — there's no automated pruning. Retention is implemented as "the row is never deleted", not as a scheduled archive policy.
---
## Members & Roles
URL: https://docs.vygl.io/team/members/
> Invite teammates, assign roles, and manage organization access. Three roles map to three permission tiers.
Vygl's RBAC has three roles in a strict hierarchy. Each role is a superset of the next: an owner has every admin permission, an admin has every member permission.
## The roles
| Role | Permissions |
|---|---|
| **Owner** | Everything: change org settings, manage members, manage API keys, manage integrations, delete the org (when supported). Created automatically for the user who first signs up an org. |
| **Admin** | Members management (invite / remove), API keys, integrations, SCM and registry connections, custom rules, organizational memory, settings. |
| **Member** | Create projects, run scans, triage findings, read everything. Can't manage members, API keys, or integrations. |
Roles apply organization-wide — Vygl doesn't have per-project roles today. A member who can read findings reads all findings in their org. Large orgs needing strict per-team isolation should split into separate organizations.
## Inviting a member
1. Open **Settings → Members**.
2. Click **Invite Member**.
3. Enter the invitee's email and pick the role (`Member` or `Admin` — the dropdown only exposes those two; `Owner` is reserved for the org creator).
4. Click **Send Invite**.
The invitee receives an email with a link valid for **7 days**. Clicking it lands them on a sign-up or sign-in page and adds them to the org with the chosen role.
## Changing a role
There's no role-change action today. To shift a teammate from member → admin (or vice versa), **remove the member and re-invite at the new role**. The new role takes effect on the next sign-in.
## Removing a member
From **Settings → Members**, click the menu next to a member and choose **Remove**. The user loses org access immediately for new requests. A JWT issued shortly before removal can still authenticate API calls until it expires (default 30 minutes), so for high-stakes off-boarding, also revoke any API keys the user created. If the member belongs to other organizations, those memberships are unaffected.
## API keys vs roles
API keys have their own permission system — **scopes** — which is **separate** from a user's role. A key can have narrow permissions like `scan:write` (can submit scans, can't read findings) regardless of who created it. See [API Keys](/team/api-keys/) for the scope list.
## Audit trail
Membership changes emit audit events:
- `org.member.invite` — admin sent an invite.
- `org.member.remove` — member was removed.
See [Audit Log](/team/audit-log/) for how to query these.
---
## Organization Settings
URL: https://docs.vygl.io/team/organization/
> Your organization's name, plan, and org-wide scanning defaults — plus switching between organizations you belong to.
**Settings → Organization** holds your organization's identity and the scanning defaults every project inherits.
## Organization profile
| Field | Notes |
|---|---|
| **Name** | The display name shown across the app. Editable. |
| **Slug** | The URL-safe identifier, unique within Vygl. Set at creation and read-only afterward. |
| **Plan** | Your current subscription tier. Read-only here — see [Usage & Limits](/team/usage/). |
## Scanning defaults
Org-wide defaults applied to every project that doesn't override them:
| Setting | Effect |
|---|---|
| **Minimum rule severity** | Only rules at or above this severity are included in the scan policy. Lower-severity rules are excluded from every scan. Choose `All severities`, `Low+`, `Medium+`, `High+`, or `Critical only`. |
A project can override this in its own [Settings tab](/using-vygl/project-detail/) — the project value wins where the two disagree.
## Switching organizations
If your account belongs to more than one organization, the Organization tab lists them all with your role in each. Switching makes that organization the active context for every page — dashboard, projects, findings, and settings all re-scope to it.
---
## Usage & Limits
URL: https://docs.vygl.io/team/usage/
> Review your subscription plan and how your organization's usage tracks against its quota.
**Settings → Usage & Limits** shows your subscription plan and how the organization's activity tracks against its quota.
## Your plan
The page header shows the active plan. The plan determines your quota limits; some plans set a quota to **Unlimited**.
## Quota cards
Each metered resource has a card showing usage over a **rolling 30-day window**:
- The current usage and the limit, with the unit (for example, scans).
- A progress bar — neutral normally, **amber** once you pass 80%, **red** once you reach the limit.
- A **Limit reached** badge when usage is at or over the limit.
Because the window rolls, usage from more than 30 days ago drops off continuously rather than resetting on a fixed calendar date.
---
# Reference
## REST API
URL: https://docs.vygl.io/reference/api/
> Vygl's REST API powers the dashboard, the CLI, and every integration. Authenticate with a bearer API key.
The REST API serves every feature in the dashboard. Authenticate with an API key in the `Authorization: Bearer …` header. Pagination uses `page` and `page_size`; filtering uses query parameters.
## Base URL
```
https://api.vygl.io
```
Self-hosted instances use whatever URL points at the API container; for local development that's typically `http://localhost:8000`.
## Authentication
Every request needs a bearer token:
```http
GET /api/v1/findings
Host: api.vygl.io
Authorization: Bearer vgl_your_api_key_here
```
Generate keys at **Settings → API Keys**. Each key has scopes that control which endpoints it can call — see [API Keys](/team/api-keys/) for the scope list.
## Versioning
All routes live under `/api/v1`. Future major versions will get their own prefix; v1 is supported indefinitely.
## Pagination
List endpoints typically accept `page` (1-based) and `page_size`. The Findings list defaults to 50 / max 200; other endpoints can have different ceilings — check the response shape per endpoint.
```http
GET /api/v1/findings?page=1&page_size=100
```
The response includes `total`, `page`, `page_size`, and `items[]`.
## Error format
Errors return JSON with a `detail` field:
```json
{
"detail": "API key lacks required scope: triage:write"
}
```
HTTP status codes follow REST conventions: 200 for success, 400 for validation, 401 for missing auth, 403 for insufficient scopes, 404 for missing resources, 5xx for server errors.
## Interactive docs
## Rate limits
Today the REST API enforces rate limits on a narrow set of endpoints — `auth/login` and `auth/forgot-password` (10 and 5 attempts per IP per 15 minutes respectively). General endpoints aren't rate-limited at the application layer; capacity is shaped by upstream infrastructure. The MCP server has its own tiered limits — see [MCP Server Overview](/mcp/overview/) for that surface. There are no `X-RateLimit-*` response headers on REST API responses today.
## Where to next
- [API Keys](/team/api-keys/) — create and scope keys
- [CLI Reference](/reference/cli/) — the CLI is built on top of the same API
- [MCP Server](/mcp/overview/) — AI-assistant access uses the same auth
---
## CLI Reference
URL: https://docs.vygl.io/reference/cli/
> Complete reference for every vygl command, flag, environment variable, and exit code.
The Vygl CLI is distributed as a Docker image (`vygl/vygl-cli:latest`). For walkthroughs and patterns, see [Running Scans (CLI)](/scanning/cli/); this page is the terse reference.
## `vygl scan`
```
vygl scan [OPTIONS] [PATH]
```
Scan a directory or, with `--image`, a container image.
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
| `--scan-types` | `-t` | comma list | `sast,sca,secrets,iac,malware` | Engines to run. Valid: `sast`, `sca`, `secrets`, `iac`, `malware`, `container`. `malware` and `container` may be turned off by org/project settings — the CLI fetches the policy on each run and skips disabled engines automatically. |
| `--image` | | string | — | Scan a container image; forces `--scan-types container`. |
| `--fail-on` | | severity | unset | Severity that exits non-zero. One of `critical`, `high`, `medium`, `low`. |
| `--no-upload` | | flag | false | Skip pushing findings to the cloud. |
| `--name` | `-n` | string | (auto) | Project name (used when no git remote). |
| `--project-id` | `-p` | UUID | (auto) | Bind to a specific project. Overrides `VYGL_PROJECT_ID` and the YAML `project_id`. |
| `--branch` | `-b` | string | (auto) | Override branch detection. |
| `--commit` | | sha | (auto) | Override commit detection. |
| `--repo-url` | | URL | (auto) | Override git-remote detection. |
| `--git-provider` | | string | (auto) | Force SCM type (`github`, `gitlab`, `bitbucket`, `gitea`); also `VYGL_GIT_PROVIDER`. |
| `--pr` | | int | (auto) | PR/MR number — enables comment posting. Auto-detected on GitHub Actions, GitLab CI, Bitbucket Pipelines. |
| `--config` | `-c` | path | (auto) | Path to a `.vygl.yml`; overrides auto-discovery. |
| `--show-all` | | flag | false | Print every finding to stdout, not just gate-triggering ones. |
| `--trigger` | | string | `ci` | Mark the trigger source on the dashboard (e.g. `manual`, `scheduled`). |
| `--api-url` | | URL | from env | Override API base URL. Effective default: `VYGL_API_URL`, falling back to `https://api.vygl.io`. |
## `vygl auth`
| Command | Description |
|---|---|
| `vygl auth set-key ` | Store API key in `~/.vygl/credentials` (chmod 600). |
| `vygl auth test` | Verify the stored key and print the org it belongs to. |
## `vygl project`
| Command | Description |
|---|---|
| `vygl project list` | List all projects in your organization. |
## `vygl results`
| Command | Description |
|---|---|
| `vygl results ` | Print findings from a previous scan. |
## Environment variables
| Variable | Purpose |
|---|---|
| `VYGL_API_KEY` | API key (required for cloud upload). |
| `VYGL_API_URL` | API base URL (default `https://api.vygl.io`). |
| `VYGL_PROJECT_ID` | Override auto-detected project ID. |
| `VYGL_GIT_PROVIDER` | Force SCM type (`github`, `gitlab`, `bitbucket`, `gitea`). |
CI-native vars (`GITHUB_*`, `CI_*`, `BITBUCKET_*`, `GITEA_*`) are read for branch / commit / repo / PR / committer auto-detection.
## Exit codes
| Code | Meaning |
|---|---|
| `0` | Scan completed; no findings ≥ `--fail-on` threshold and no Block-mode rule fired. |
| `1` | Scan completed; either findings at/above `--fail-on` are present, OR a cloud-policy rule in **Block** mode produced a finding (independent of `--fail-on`), OR the run errored (engine failure, network error, invalid auth, malformed config). |
The CLI today returns 0 or 1 — there's no separate "run errored" exit code.
## CI auto-detection
The CLI reads CI-native environment variables to fill scan metadata automatically. Supported providers:
- GitHub Actions (`GITHUB_*`).
- GitLab CI (`CI_*`).
- Bitbucket Pipelines (`BITBUCKET_*`).
- Gitea Actions.
- Generic git fallback (uses `git rev-parse` and `git remote get-url origin`).
Override any auto-detected value with the corresponding flag, or with `VYGL_GIT_PROVIDER` to force the SCM family.
## Config file
The loader accepts any of `.vygl.yml`, `.vygl.yaml`, `vygl.yml`, `.vigil.yml`, `.vigil.yaml`, `vigil.yml` at the repo root. See [Configuration](/scanning/configuration/) for the schema.
`fail_on` precedence (highest to lowest): `--fail-on` flag → `scan.fail_on` in the YAML → project setting in the dashboard.
---
## Exports — SARIF, CSV, SBOM
URL: https://docs.vygl.io/reference/exports/
> Download findings as SARIF 2.1.0 or CSV, and dependency inventories as CycloneDX 1.4 or SPDX 2.3 SBOMs.
Vygl exports findings and dependency inventories in three formats. Each is designed for a specific downstream consumer.
## CSV (findings)
| Where | **Findings** list → **CSV** button. |
|---|---|
| Format | RFC 4180 CSV, UTF-8. |
| Scope | All findings matching the active filter (built client-side by paginating the API; no row cap). |
| Columns | `Severity, Type, Rule ID, Title, File, Line, Status, AI Verdict, Confidence, Project, Created`. |
Use for spreadsheet-based offline triage, ad-hoc reporting, and exec dashboards.
## SARIF 2.1.0 (findings)
| Where | **Findings** list → **SARIF** button. |
|---|---|
| Format | SARIF 2.1.0 JSON. |
| Scope | All findings matching current filters. |
| Hard cap | 25,000 findings — the endpoint returns 413 above that. Narrow filters or paginate. |
SARIF is the industry-standard format for static-analysis output. Compatible with:
- **GitHub Code Scanning** (upload via GitHub Actions for in-PR annotations).
- **Azure DevOps** code scanning.
- **JFrog Xray**.
- Any tool that ingests SARIF.
**Mappings Vygl applies:**
- Severity → SARIF level: `critical`/`high` → `error`, `medium` → `warning`, `low`/`info` → `note`.
- CVSS numeric value → `properties.security-severity` (so consumers that prioritize by CVSS read the right number).
- KEV / EPSS / CVSS surface as result properties (`vigil/kev`, `vigil/epss_score`, `vigil/cvss_score`) and as rule tags (`kev`, `epss-high`).
- **Triage state** — `false_positive` and `ignored` render as SARIF `suppressions`; `fixed` and `auto_fixed` set `baselineState: absent` so re-uploads close the issue downstream.
- Every result carries `partialFingerprints.vigilFingerprintV1` — Vygl's stable dedup hash — so consumers can track findings across branches.
- The full `metadata.ai_verification` block (verdict, confidence, reasoning, suggested fix) ships as result properties when AI verification has run.
**`helpUri` priority:** OSV → NVD → CWE → omitted. URLs are real; Vygl never fabricates them.
**Snippets in secrets findings** are stripped by default to prevent raw secret leakage. Pass `include_snippets=true` to override (typically only for local debugging).
## SBOM (CycloneDX 1.4 / SPDX 2.3)
| Where | **Dependencies** page → **Download SBOM**, or per-project detail. |
|---|---|
| Formats | CycloneDX 1.4 JSON, SPDX 2.3 JSON. |
| Scope | Per-project / per-branch, or org-wide (respects current filters). |
Pass `format=cyclonedx` or `format=spdx` to the SBOM endpoint to pick the format. Both contain:
- **Components** — every dependency as a SBOM component with PURL, name, version, and license (when available).
- **Metadata** — project name, scan timestamp, tool identifier (`vygl`).
Use for supply-chain audits (SOC 2, ISO 27001, FedRAMP), regulatory submissions, and customer security questionnaires.
## What's not in exports
- **Raw source files** — only snippet excerpts (≤3000 chars per finding) are included.
- **Triage reasons and audit trail** — current status is reflected in SARIF suppressions / baselineState; the full transition history lives in Vygl's audit log.
- **AI consultation transcripts** — chat logs are stored in Vygl only; not exported.
For deeper data, the [REST API](/reference/api/) returns the full structured representation of every finding, dependency, and scan.
---