Skip to content

Latest commit

 

History

History
274 lines (212 loc) · 13.7 KB

File metadata and controls

274 lines (212 loc) · 13.7 KB

CLAUDE.md

Project Overview

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

Tech Stack

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)

Project Structure

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

Architecture & Patterns

Core Pipeline

The operator follows a source -> scanner -> target pipeline pattern:

  1. Sources (source.Source interface) load ScanItems (SBOMs or container image references)
  2. Scanner (Grype) processes each ScanItem, producing Vulnerability results
  3. Targets (target.Target interface) receive the aggregated ScanResult and output it

Source Implementations

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

Target Implementations

  • json: Writes report.json (found vulnerabilities) and audited.json (audited vulnerabilities) to a configurable directory, served via the built-in HTTP file server at /report/.
  • metrics: Exposes Prometheus gauge metrics vuln_operator_cves and vuln_operator_cves_audit with labels: cve, severity, package, version, type, fix_state, image_id, k8s_namespace, k8s_name, k8s_kind, container_name.
  • policyreport: Creates/updates Kubernetes PolicyReport resources (wg-policy-prototypes v1alpha2) per pod, with owner references for automatic garbage collection.

Filtering System

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.

Severity & Fix Filtering

  • --min-severity filters by severity level (negligible, low, medium, high, critical). Default: medium.
  • --only-fixed limits results to vulnerabilities with known fixes.

Kubernetes Integration

  • The KubeClient maps image IDs to running containers, resolving owner references up the chain (Pod -> ReplicaSet -> Deployment, Pod -> Job -> CronJob, etc.).
  • RBAC requires only pods:list at the cluster level.

Configuration

Configuration is handled via a unified Config struct in internal/vuln/config.go supporting three methods (in priority order):

  1. CLI flags (via Cobra/pflag)
  2. Environment variables (prefixed with VULN_)
  3. YAML config file (via --config flag, processed by libstandard)

HTTP Server

Runs on port 8080 with three endpoints:

  • GET /health - Health check (returns 200 "Running!")
  • GET /report/* - Static file server for JSON reports
  • GET /metrics - Prometheus metrics endpoint

Build & Development

Prerequisites

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.

Run Locally

make run            # Runs fmt, vet, then go run ./main.go
# OR
./run.sh            # Runs the built binary with example dev config

Docker Build

make build
docker build -t vulnerability-operator:local .

The Dockerfile expects pre-built binaries in dist/ (produced by GoReleaser).

Deploy to Kubernetes

Apply the manifests in deploy/:

kubectl apply -f deploy/rbac.yaml
kubectl apply -f deploy/deployment.yaml

Testing

Framework

  • testify (github.com/stretchr/testify) for assertions (assert.Equal, assert.NotEmpty, assert.ElementsMatch, assert.NoError)
  • Standard Go testing package with table-driven tests

Test Files

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

Running Tests

make test           # Runs go test on all packages with coverage

Coverage output goes to cover.out. The Grype tests download the vulnerability database at runtime, so they require network access.

Linting & Code Style

Linters

  • golangci-lint v2.11.1 - General Go linting with a 5-minute timeout. Installed via make bootstrap-tools into .tmp/.
  • gosec v2.24.7 - Security-focused static analysis. Installed alongside golangci-lint.

Running Linters

make bootstrap-tools  # Download lint tools to .tmp/
make lint             # Run golangci-lint
make lintsec          # Run gosec

Code Conventions

  • go fmt is enforced (run as part of make build and make run)
  • go vet is enforced (run as part of make build and make run)
  • /* #nosec */ comments used to suppress gosec false positives (file reads, directory creation)
  • // nolint QF1003 used to suppress golangci-lint suggestions for if/else-if chains (kept for readability)
  • Error handling uses logrus.WithError(err).Error/Fatal/Warn pattern consistently

CI/CD

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

Container Registry

Images are published to ghcr.io/ckotzbauer/vulnerability-operator with cosign signatures stored in ghcr.io/ckotzbauer/vulnerability-operator-metadata.

Key Commands

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

Important Conventions

  • 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 no cmd/, pkg/, or api/ directory. The entrypoint is main.go at the repository root.
  • Configuration keys are defined as package-level var constants in internal/vuln/config.go (e.g., ConfigKeyCron, ConfigKeySources).
  • Struct tags use three formats: yaml for config file, env for environment variables, flag for CLI flags.
  • GoReleaser is used for both local builds (--snapshot --single-target) and releases. The binary is always built via GoReleaser, not plain go 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, or sbom.spdx. The image ID is derived from the directory path structure.