The vulnerability-operator is a Kubernetes-aware application that periodically scans SBOMs (Software Bill of Materials) or container images for known vulnerabilities using Anchore Grype. It runs as a long-lived process with a cron-based background service, gathers scan inputs from configurable sources (Git repositories or live Kubernetes clusters), and publishes results to configurable targets (JSON files, Prometheus metrics, or Kubernetes PolicyReports).
- Module:
github.com/ckotzbauer/vulnerability-operator - License: MIT (see
LICENSE) - Container image:
ghcr.io/ckotzbauer/vulnerability-operator
| Technology | Version / Details |
|---|---|
| Go | 1.25.0 (from go.mod) |
| Anchore Grype | v0.109.0 (vulnerability scanning engine, used as a library) |
| Anchore Syft | v1.42.1 (SBOM cataloging, used via Grype's pkg.Provide) |
| Kubernetes client-go | v0.35.2 |
| k8s.io/api, k8s.io/apimachinery | v0.35.2 |
| wg-policy-prototypes | v1alpha2 PolicyReport CRD client |
| Prometheus client_golang | v1.23.2 |
| Cobra | v1.10.2 (CLI framework) |
| logrus | v1.9.4 (structured logging) |
| robfig/cron | v1.2.0 (cron scheduler) |
| testify | v1.11.1 (test assertions) |
| goreleaser | v2 (build/release tool) |
| golangci-lint | v2.11.1 |
| gosec | v2.24.7 |
| libstandard | github.com/ckotzbauer/libstandard (config initialization helper) |
| libk8soci | github.com/ckotzbauer/libk8soci (Kubernetes/OCI/Git helpers) |
| Docker base | alpine:3.21 (for CA certs) -> scratch (final image) |
vulnerability-operator/
├── main.go # Entrypoint: Cobra root command, HTTP server (health, metrics, report serving)
├── go.mod / go.sum # Go module definition
├── Makefile # Build, test, lint, tool bootstrap targets
├── Dockerfile # Multi-stage: alpine (CA certs) -> scratch
├── .goreleaser.yml # GoReleaser v2 config (linux/amd64+arm64, cosign signing)
├── config.yaml # Grype ignore rules for known false positives / irrelevant CVEs
├── renovate.json # Renovate dependency update config
├── run.sh # Local development run script
├── deploy/
│ ├── deployment.yaml # Kubernetes Deployment, Service, ServiceMonitor manifests
│ ├── rbac.yaml # ServiceAccount, ClusterRole (pods:list), ClusterRoleBinding
│ └── grafana-dashboard.json # Grafana dashboard for vulnerability metrics
├── reports/
│ └── report.json # Example/output report file
├── internal/vuln/
│ ├── config.go # Config struct with yaml/env/flag tags, config key constants
│ ├── types.go # Vulnerability and ScanResult types
│ ├── util.go # Wildcard pattern matching (IsMatch)
│ ├── util_test.go # Tests for IsMatch
│ ├── daemon/
│ │ └── daemon.go # CronService: orchestrates source -> grype scan -> target pipeline
│ ├── grype/
│ │ ├── grype.go # Grype integration: DB loading, scanning, vulnerability filtering
│ │ ├── grype_test.go # Integration tests: SBOM and image scanning
│ │ └── fixtures/ # Test SBOM fixtures (vulnerable/clean alpine)
│ ├── kubernetes/
│ │ └── kubernetes.go # KubeClient: pod listing, container-to-image mapping, owner resolution
│ ├── source/
│ │ ├── source.go # Source interface + ScanItem interface (Sbom, Image types)
│ │ ├── git/
│ │ │ └── git_source.go # GitSource: clones repo, walks directory tree for SBOM files
│ │ └── kubernetes/
│ │ └── kubernetes_source.go # KubernetesSource: discovers images from running pods
│ ├── target/
│ │ ├── target.go # Target interface (Initialize, ProcessVulns, Finalize)
│ │ ├── json/
│ │ │ └── json_target.go # JsonTarget: writes report.json + audited.json
│ │ ├── metric/
│ │ │ └── metric_target.go # MetricTarget: Prometheus gauges (vuln_operator_cves, vuln_operator_cves_audit)
│ │ └── policyreport/
│ │ └── policyreport_target.go # PolicyReportTarget: creates/updates K8s PolicyReport resources
│ └── filter/
│ ├── type.go # FilterConfig, VulnerabilityFilter, FilterContext types
│ ├── filter.go # FilterEngine: ignore/audit filtering with wildcard context matching
│ └── filter_test.go # Comprehensive table-driven filter tests
├── .github/workflows/
│ ├── code-checks.yml # golangci-lint + gosec on PRs and pushes
│ ├── test.yml # Build + test + coverage + Docker image build
│ ├── create-release.yml # Manual workflow: goreleaser release + multi-arch Docker
│ ├── stale.yml # Nightly stale issue/PR cleanup
│ ├── update-snyk.yml # Weekly Snyk monitoring
│ ├── label-issues.yml # Auto-label issues/PRs
│ └── size-label.yml # PR size labels
└── .vscode/launch.json # VS Code Go debug configuration
The operator follows a source -> scanner -> target pipeline pattern:
- Sources (
source.Sourceinterface) loadScanItems (SBOMs or container image references) - Scanner (Grype) processes each
ScanItem, producingVulnerabilityresults - Targets (
target.Targetinterface) receive the aggregatedScanResultand output it
git: Clones a Git repository, walks the directory tree looking for SBOM files (sbom.json,sbom.txt,sbom.xml,sbom.spdx). Extracts image IDs from the file path structure. Supports token, username/password, and GitHub App authentication.kubernetes: Queries the Kubernetes API for running pods (filtered by namespace and pod label selectors), extracts unique container image IDs for direct registry scanning.
json: Writesreport.json(found vulnerabilities) andaudited.json(audited vulnerabilities) to a configurable directory, served via the built-in HTTP file server at/report/.metrics: Exposes Prometheus gauge metricsvuln_operator_cvesandvuln_operator_cves_auditwith labels:cve,severity,package,version,type,fix_state,image_id,k8s_namespace,k8s_name,k8s_kind,container_name.policyreport: Creates/updates KubernetesPolicyReportresources (wg-policy-prototypes v1alpha2) per pod, with owner references for automatic garbage collection.
Two-tier filtering via a YAML config file (--filter-config-file):
- Ignore filters: Completely suppress matching vulnerabilities from output.
- Audit filters: Move matching vulnerabilities from "found" to "audited" (acknowledged but tracked).
- Filters support wildcard matching (
*,?) on vulnerability ID, package name, and context fields (image, namespace, name, kind). - Grype's own ignore rules (
--grype-config-file) are applied at the scanner level before the filter engine.
--min-severityfilters by severity level (negligible, low, medium, high, critical). Default:medium.--only-fixedlimits results to vulnerabilities with known fixes.
- The
KubeClientmaps image IDs to running containers, resolving owner references up the chain (Pod -> ReplicaSet -> Deployment, Pod -> Job -> CronJob, etc.). - RBAC requires only
pods:listat the cluster level.
Configuration is handled via a unified Config struct in internal/vuln/config.go supporting three methods (in priority order):
- CLI flags (via Cobra/pflag)
- Environment variables (prefixed with
VULN_) - YAML config file (via
--configflag, processed bylibstandard)
Runs on port 8080 with three endpoints:
GET /health- Health check (returns 200 "Running!")GET /report/*- Static file server for JSON reportsGET /metrics- Prometheus metrics endpoint
- Go 1.25+
- GoReleaser v2 (for
make build)
make build # Runs fmt, vet, then goreleaser build (single target, snapshot)This produces a binary at dist/vulnerability-operator_linux_amd64_v1/vulnerability-operator.
make run # Runs fmt, vet, then go run ./main.go
# OR
./run.sh # Runs the built binary with example dev configmake build
docker build -t vulnerability-operator:local .The Dockerfile expects pre-built binaries in dist/ (produced by GoReleaser).
Apply the manifests in deploy/:
kubectl apply -f deploy/rbac.yaml
kubectl apply -f deploy/deployment.yaml- testify (
github.com/stretchr/testify) for assertions (assert.Equal,assert.NotEmpty,assert.ElementsMatch,assert.NoError) - Standard Go
testingpackage with table-driven tests
| File | What it tests |
|---|---|
internal/vuln/util_test.go |
Wildcard pattern matching (IsMatch) |
internal/vuln/filter/filter_test.go |
Filter engine: ignore/audit rules, context matching, multi-container scenarios |
internal/vuln/grype/grype_test.go |
Integration tests: SBOM scanning and image scanning against Grype DB |
make test # Runs go test on all packages with coverageCoverage output goes to cover.out. The Grype tests download the vulnerability database at runtime, so they require network access.
- golangci-lint v2.11.1 - General Go linting with a 5-minute timeout. Installed via
make bootstrap-toolsinto.tmp/. - gosec v2.24.7 - Security-focused static analysis. Installed alongside golangci-lint.
make bootstrap-tools # Download lint tools to .tmp/
make lint # Run golangci-lint
make lintsec # Run gosecgo fmtis enforced (run as part ofmake buildandmake run)go vetis enforced (run as part ofmake buildandmake run)/* #nosec */comments used to suppress gosec false positives (file reads, directory creation)// nolint QF1003used to suppress golangci-lint suggestions for if/else-if chains (kept for readability)- Error handling uses
logrus.WithError(err).Error/Fatal/Warnpattern consistently
All workflows use reusable workflows from ckotzbauer/actions-toolkit (pinned at v0.52.0 by SHA).
| Workflow | Trigger | Purpose |
|---|---|---|
code-checks.yml |
PR + push | Runs golangci-lint and gosec |
test.yml |
PR + push | Build via GoReleaser, run tests with coverage, build Docker image |
create-release.yml |
Manual (workflow_dispatch) |
GoReleaser release, multi-arch Docker (linux/amd64, linux/arm64), pushes to GHCR, cosign signing |
stale.yml |
Daily cron (midnight) | Marks stale issues/PRs |
update-snyk.yml |
Weekly (Monday noon) + manual | Runs snyk monitor for vulnerability tracking |
label-issues.yml |
Issue/PR events | Auto-labels issues and PRs |
size-label.yml |
PR events | Adds size labels to PRs |
Images are published to ghcr.io/ckotzbauer/vulnerability-operator with cosign signatures stored in ghcr.io/ckotzbauer/vulnerability-operator-metadata.
make build # Build binary (fmt + vet + goreleaser snapshot)
make run # Run from source (fmt + vet + go run)
make test # Run all tests with coverage
make lint # Run golangci-lint
make lintsec # Run gosec
make bootstrap-tools # Install golangci-lint and gosec to .tmp/
make fmt # Run go fmt
make vet # Run go vet- No controller-runtime / operator-sdk: Despite the name, this is not a Kubernetes operator in the controller-runtime sense. It is a standalone Go binary that runs a cron-based background service and interacts with the Kubernetes API via client-go directly.
- Interface-based extensibility: Sources and targets are defined as interfaces (
source.Source,target.Target), making it straightforward to add new source/target types. - All source code lives under
internal/vuln/: There is nocmd/,pkg/, orapi/directory. The entrypoint ismain.goat the repository root. - Configuration keys are defined as package-level
varconstants ininternal/vuln/config.go(e.g.,ConfigKeyCron,ConfigKeySources). - Struct tags use three formats:
yamlfor config file,envfor environment variables,flagfor CLI flags. - GoReleaser is used for both local builds (
--snapshot --single-target) and releases. The binary is always built via GoReleaser, not plaingo build. - CGO_ENABLED=0: Builds are statically linked with no CGO.
- ldflags inject version info:
-X main.Version,-X main.Commit,-X main.Date,-X main.BuiltBy. - Renovate manages dependency updates on a weekly schedule, with Kubernetes packages grouped together.
- SBOM file naming: Git source expects SBOM files named
sbom.json,sbom.txt,sbom.xml, orsbom.spdx. The image ID is derived from the directory path structure.