Everything you need to run IaC, SCA, and secrets scanning in CI. Free and open source — no account required for any of it. The last section covers what changes if you connect the optional Git Tracking Platform.
Zagware Scanner runs on every pull request and posts a single, focused comment showing only the security findings introduced by that PR — not the hundreds that may already exist in the codebase. Three scan engines ship in one container:
| Engine | What it scans | Detects |
|---|---|---|
| KICS (Checkmarx) | Infrastructure-as-code files (Terraform, Kubernetes, Dockerfile, CloudFormation…) | Misconfigurations, insecure defaults, open ports, missing encryption |
| Grype (Anchore) | Package manifests and lockfiles (npm, pip, Go, Maven, Gem…) | CVEs, GHSA advisories — with CVSS, EPSS, and KEV catalog status |
| betterleaks | Filesystem contents (working-tree state) | Leaked credentials — API keys, tokens, private keys, other secret patterns |
Clone base + PR branch → scan both in parallel → diff by content fingerprint (not line number) → post the delta as a single, in-place PR comment updated on every push.
Everything above runs standalone: PR comments on GitHub, GitLab, Bitbucket, and Azure DevOps; IaC + SCA + secrets scanning; interactive and file-based suppressions; self-hosting; severity filtering and merge-blocking. ZAGWARE_PLATFORM_URL / ZAGWARE_PLATFORM_TOKEN are never required. Apache 2.0 licensed — read, fork, and self-host the image.
Set ZAGWARE_PLATFORM_URL and ZAGWARE_PLATFORM_TOKEN to also upload every scan to app.zagware.io. That unlocks scan history and trend charts per repo, a durable suppression audit trail (who suppressed what, when, and why), CIS GitHub Benchmark compliance scans, and change-management evidence for SOC-2 audits — reporting the scanner itself intentionally doesn't do. See the platform page for details.
Create .github/workflows/zagware-scanner.yml:
GITHUB_TOKEN is provided automatically — no secrets to configure for the scanner itself. Omit ZAGWARE_PLATFORM_URL / ZAGWARE_PLATFORM_TOKEN to run fully standalone (PR comment only, no upload).
GitLab's built-in CI_JOB_TOKEN can't post merge request notes — create a project or group access token with api scope and store it as a masked GITLAB_TOKEN CI/CD variable, then add to .gitlab-ci.yml:
Required repository variables: BITBUCKET_API_TOKEN (Atlassian API token) and ATLASSIAN_EMAIL.
$(System.AccessToken) is available to any step that maps it via env:. Usually needs no manual permission grant beyond that.
| Variable | Default | Description |
|---|---|---|
ZAGWARE_PLATFORM_URL | — | Base URL of the Zagware platform, e.g. https://app.zagware.io. Required for dashboard upload only. |
ZAGWARE_PLATFORM_TOKEN | — | API token (gtp_…) from Settings → API Tokens. Required for dashboard upload only. |
ZAGWARE_MIN_SEVERITY | all | Minimum severity to report: CRITICAL HIGH MEDIUM LOW INFO. |
ZAGWARE_FAIL_ON_NEW | false | Exit 1 when new findings are found at or above the severity threshold — blocks the merge. |
ZAGWARE_EXCLUDE_PATHS | .git | Comma-separated paths or globs to exclude from IaC scanning. |
ZAGWARE_SCA_ENABLED | true | Set false to skip Grype dependency scanning entirely. |
ZAGWARE_SECRETS_ENABLED | true | Set false to skip betterleaks secrets scanning entirely. |
ZAGWARE_SECRETS_FAIL_ON_PUBLIC | true | Exit 1 on a new secret in a public repo, regardless of ZAGWARE_FAIL_ON_NEW. |
ZAGWARE_TELEMETRY | on | Set off to disable anonymous usage telemetry. |
ZAGWARE_TELEMETRY_INCLUDE_REPO_NAME | false | Set true to send your org/repo name in clear instead of a one-way hash. |
| Platform | File types |
|---|---|
| Terraform | .tf, .tfvars |
| Kubernetes | YAML manifests |
| Helm | Chart templates |
| Dockerfile | Dockerfile* |
| AWS CloudFormation | JSON, YAML templates |
| Azure Resource Manager | ARM JSON templates |
| Ansible | Playbooks and task files |
| OpenAPI / Swagger | API specification files |
| Docker Compose | docker-compose.yml |
| GitHub Actions | Workflow files |
| Serverless Framework | serverless.yml |
| Pulumi | Infrastructure programs |
KICS auto-detects file types — no configuration needed.
| Ecosystem | Detected via |
|---|---|
| Node.js / npm | package-lock.json, yarn.lock, pnpm-lock.yaml |
| Python | requirements.txt, Pipfile.lock, poetry.lock |
| Go | go.sum |
| Ruby | Gemfile.lock |
| Java / Maven | pom.xml, build.gradle |
| Rust | Cargo.lock |
| PHP | composer.lock |
| .NET | packages.lock.json |
| OS packages | Alpine (apk), Debian/Ubuntu (dpkg), RHEL/CentOS (rpm) in Dockerfiles |
Enabled by default when manifest files are detected. Set ZAGWARE_SCA_ENABLED=false to disable.
betterleaks scans working-tree contents of both branches for leaked API keys, tokens, private keys, and other secret patterns. Unlike IaC/SCA, it has no severity taxonomy — findings are prioritized by repository visibility: a secret leaked in a public repo is immediately exposed, so public repos are always treated as urgent regardless of ZAGWARE_FAIL_ON_NEW.
Scanning runs against current filesystem state only (working-tree diff, matching the IaC/SCA architecture) — not full git history. If a secret was introduced and later removed within the PR's own history, it won't be re-flagged once it's gone from the working tree.
Findings never include the raw secret value anywhere — PR comment, platform upload, or scan artifacts — only rule id, file path, line number, tags, and whether betterleaks confirmed the credential is live.
Every scan comment lists each new finding with a short id. Post the suggested command as a PR comment:
The scanner resolves the id, writes it to .zagware/suppressions.yaml on the PR branch, commits, pushes, and re-runs in the same comment — no waiting for a second run. Currently GitHub-only; GitLab, Bitbucket, and Azure DevOps use the manual file method below.
Suppressed findings are excluded from the PR comment but still logged in scan output. With the platform connected, every scan uploads the full current set of active suppressions — a durable, queryable audit record of who suppressed what and why, attributed precisely for PR-comment suppressions and via best-effort git blame for hand-edited ones.
ZAGWARE_MIN_SEVERITY | IaC findings shown | SCA findings shown |
|---|---|---|
| (unset) | All (HIGH, MEDIUM, LOW, INFO) | All (CRITICAL … NEGLIGIBLE) |
HIGH | HIGH only | CRITICAL, HIGH |
MEDIUM | HIGH, MEDIUM | CRITICAL, HIGH, MEDIUM |
LOW | HIGH, MEDIUM, LOW | CRITICAL, HIGH, MEDIUM, LOW |
Secrets findings are unaffected by ZAGWARE_MIN_SEVERITY — betterleaks has no severity taxonomy. Use ZAGWARE_SECRETS_FAIL_ON_PUBLIC as the equivalent gate.
sha256(cve_id:package_name:package_version).sha256(betterleaks_fingerprint), where the input is betterleaks' own file_path:rule_id:line.Reformatting or line shifts never create spurious new findings; fixing a finding removes its fingerprint even if similar issues remain elsewhere; moving a vulnerable package to a new manifest file is treated as new.
| Tag | Description |
|---|---|
:<version> | Immutable per release, e.g. :2.2.0. Pin by digest for the strongest guarantee. |
:latest | Newest release. Moves on every tag push. Not security-vetted. |
:stable | Promoted from :latest after a 14-day cooling period + clean CVE scan. |
:secure | Identical digest to :stable; explicitly marks the security-audited image. |
Recommendation: pin :stable (or :secure) by digest for production CI; use :latest for experimentation.
Every release is cosign-signed (keyless, GitHub OIDC → sigstore Rekor), carries an SPDX SBOM as an OCI attestation, and a SLSA Build Level 3 provenance attestation linking the image digest to its exact source commit and workflow run. Every bundled binary (KICS, Syft, Grype, betterleaks) is SHA256-checksum-verified at build time.
Build-time internet access is needed to pull KICS, Syft, Grype, and betterleaks from their public GitHub releases. At scan time, the image only needs access to clone your repository and post the PR comment. Self-hosted builds verify binary SHA256 checksums but not the upstream cosign signatures on Syft/Grype checksum files — that step runs only in the official publish CI.
Anonymous usage telemetry (via PostHog) reports which CI platforms are used and rough usage volume. No code, file paths, finding descriptions, CVE/package details, branch names, commit SHAs, or credentials are ever sent.
| Sent | Never sent |
|---|---|
CI platform, hashed repo/org id, scan durations, which scan types ran, bucketed finding counts (0/1-5/6-20/21+), exit code, scanner version | File contents, file paths, finding descriptions, CVE IDs, package names/versions, branch names, commit SHAs, CI tokens/secrets, IP geolocation |
Disable entirely with ZAGWARE_TELEMETRY: "off". Best-effort and fail-silent — a 3-second timeout, never blocks or slows the pipeline even if unreachable.
No. It runs entirely within your CI environment, clones with your CI token, scans locally, and posts results via your CI platform's API. No code, file contents, or file paths ever leave your infrastructure.
That's exactly the scenario this tool is built for — existing findings are ignored. You only see what the PR under review introduces.
Yes. The scanner posts PR comments standalone. ZAGWARE_PLATFORM_URL and ZAGWARE_PLATFORM_TOKEN are entirely optional.
| Platform | What's needed |
|---|---|
| GitHub Actions | permissions: pull-requests: write (GITHUB_TOKEN is automatic) |
| GitLab CI | GITLAB_TOKEN with api scope |
| Bitbucket | BITBUCKET_API_TOKEN (Atlassian API token) + ATLASSIAN_EMAIL |
| Azure DevOps | Build Service: Contribute to pull requests + OAuth token access |
Organization-wide enforcement (GitHub rulesets / GitLab Pipeline Execution Policy), the complete OpenAPI-style upload payloads, and contribution guidelines live in the repo.
View zagware/zagware-scanner on GitHub