├── CODEOWNERS ├── .nancy-ignore ├── .dockerignore ├── .nancy-ignore.generated ├── .abs └── main.yaml ├── SECURITY.md ├── .ats └── main.yaml ├── utils ├── slice_contains.go ├── jitter.go ├── sharding_test.go └── sharding.go ├── .github ├── pull_request_template.md └── workflows │ ├── zz_generated.gitleaks.yaml │ ├── zz_generated.validate_changelog.yaml │ ├── zz_generated.check_values_schema.yaml │ ├── zz_generated.fix_vulnerabilities.yaml │ ├── zz_generated.run_ossf_scorecard.yaml │ ├── zz_generated.create_release_pr.yaml │ ├── zz_generated.add-team-labels.yaml │ ├── zz_generated.add-to-project-board.yaml │ └── zz_generated.create_release.yaml ├── controllers ├── key.go ├── configauditreport │ ├── configauditreport_metrics.go │ └── configauditreport_controller.go └── vulnerabilityreport │ ├── vulnerabilityreport_metrics.go │ └── vulnerabilityreport_controller.go ├── helm └── starboard-exporter │ ├── templates │ ├── service-account.yaml │ ├── service.yaml │ ├── networkpolicy.yaml │ ├── scaledobject.yaml │ ├── customMetricsHpa.yaml │ ├── vpa.yaml │ ├── psp.yaml │ ├── _resource.tpl │ ├── _helpers.tpl │ ├── servicemonitor.yaml │ ├── rbac.yaml │ ├── deployment.yaml │ └── grafana-dashboard.yaml │ ├── Chart.yaml │ ├── values.yaml │ └── values.schema.json ├── tests └── ats │ ├── Pipfile │ └── test_basic_cluster.py ├── PROJECT ├── .gitignore ├── renovate.json5 ├── hack └── boilerplate.go.txt ├── Dockerfile ├── Makefile.gen.md ├── Makefile ├── .pre-commit-config.yaml ├── DCO ├── docs └── custom_metrics_hpa.md ├── Makefile.gen.app.mk ├── Makefile.gen.go.mk ├── .circleci └── config.yml ├── go.mod ├── README.md ├── main.go ├── CHANGELOG.md └── LICENSE /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # generated by giantswarm/github actions - changes will be overwritten 2 | * @giantswarm/team-shield 3 | -------------------------------------------------------------------------------- /.nancy-ignore: -------------------------------------------------------------------------------- 1 | CVE-2025-22872 until=2025-08-27 # golang.org/x/net@v0.37.0 2 | CVE-2025-53547 until=2025-08-09 # helm.sh/helm/v3@v3.17.3 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /.nancy-ignore.generated: -------------------------------------------------------------------------------- 1 | # This file is generated by https://github.com/giantswarm/github 2 | # Repository specific ignores should be added to .nancy-ignore 3 | -------------------------------------------------------------------------------- /.abs/main.yaml: -------------------------------------------------------------------------------- 1 | replace-app-version-with-git: true 2 | replace-chart-version-with-git: true 3 | generate-metadata: true 4 | chart-dir: ./helm/starboard-exporter 5 | destination: ./build 6 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please visit for information on reporting security issues. 6 | -------------------------------------------------------------------------------- /.ats/main.yaml: -------------------------------------------------------------------------------- 1 | smoke-tests-cluster-type: kind 2 | 3 | skip-steps: [functional] 4 | 5 | upgrade-tests-cluster-type: kind 6 | upgrade-tests-app-catalog-url: https://giantswarm.github.io/giantswarm-catalog 7 | -------------------------------------------------------------------------------- /utils/slice_contains.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | func SliceContains(s []string, value string) bool { 4 | for _, item := range s { 5 | if item == value { 6 | return true 7 | } 8 | } 9 | return false 10 | } 11 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Checklist 2 | 3 | - [ ] Update changelog in CHANGELOG.md. 4 | - [ ] Make sure `values.yaml` and `values.schema.json` are valid. 5 | - [ ] (Giant Swarm) If creating a release, bump the `version` and `appVersion` in Chart.yaml. 6 | -------------------------------------------------------------------------------- /controllers/key.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import "time" 4 | 5 | const ( 6 | DefaultServiceName = "starboard-exporter" 7 | ShardOwnerLabel = "starboard-exporter.giantswarm.io/shard-owner" 8 | ) 9 | 10 | var DefaultRequeueDuration = (time.Minute * 5) 11 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/service-account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: {{ include "resource.default.name" . }} 5 | namespace: {{ include "resource.default.namespace" . }} 6 | labels: 7 | {{- include "labels.common" . | nindent 4 }} 8 | -------------------------------------------------------------------------------- /tests/ats/Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [packages] 7 | pytest-helm-charts = ">=0.6.0" 8 | pytest = ">=6.2.5" 9 | pykube-ng = ">=21.10.0" 10 | pytest-rerunfailures = ">=10.2" 11 | 12 | [requires] 13 | python_version = "3" 14 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.gitleaks.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/ad0a25fbf301b2513e169ec964a8785d28f75be4/pkg/gen/input/workflows/internal/file/gitleaks.yaml.template 6 | # 7 | name: gitleaks 8 | 9 | on: 10 | - pull_request 11 | 12 | jobs: 13 | publish: 14 | uses: giantswarm/github-workflows/.github/workflows/gitleaks.yaml@main 15 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | domain: giantswarm 2 | layout: 3 | - go.kubebuilder.io/v3 4 | projectName: starboard-exporter 5 | repo: github.com/giantswarm/starboard-exporter 6 | resources: 7 | - controller: true 8 | domain: giantswarm 9 | group: aquasecurity.github.io 10 | kind: VulnerabilityReport 11 | version: v1alpha1 12 | - controller: true 13 | domain: giantswarm 14 | group: aquasecurity.github.io 15 | kind: ConfigAuditReport 16 | version: v1alpha1 17 | version: "3" 18 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "resource.default.name" . }} 5 | namespace: {{ include "resource.default.namespace" . }} 6 | labels: 7 | {{- include "labels.common" . | nindent 4 }} 8 | annotations: 9 | prometheus.io/scrape: "true" 10 | spec: 11 | ports: 12 | - name: metrics 13 | port: 8080 14 | targetPort: 8080 15 | selector: 16 | {{- include "labels.selector" . | nindent 4 }} 17 | -------------------------------------------------------------------------------- /helm/starboard-exporter/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: 0.8.1 3 | name: starboard-exporter 4 | description: A Helm chart for starboard-exporter, which exposes Prometheus metrics from Aqua VulnerabilityReport and other custom resources. 5 | engine: gotpl 6 | home: https://github.com/giantswarm/starboard-exporter 7 | icon: https://s.giantswarm.io/app-icons/prometheus/1/light.svg 8 | version: 0.8.2 9 | annotations: 10 | application.giantswarm.io/team: "shield" 11 | config.giantswarm.io/version: 1.x.x 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Binaries for programs and plugins 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | bin 9 | testbin/* 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Kubernetes Generated files - skip generated files, except for vendored files 18 | 19 | !vendor/**/zz_generated.* 20 | 21 | # editor and IDE paraphernalia 22 | .idea 23 | *.swp 24 | *.swo 25 | *~ 26 | 27 | # project binary 28 | /starboard-exporter 29 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>giantswarm/renovate-presets:default.json5", 4 | "github>giantswarm/renovate-presets:lang-go.json5", 5 | "github>giantswarm/renovate-presets:disable-vendir.json5" 6 | ], 7 | "packageRules": [ 8 | { 9 | "description": "Automerge architect updates", 10 | "matchFileNames": [ 11 | ".circleci/config.yml" 12 | ], 13 | "matchDepNames": [ 14 | "architect" 15 | ], 16 | "groupName": "Architect", 17 | "automerge": true 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/networkpolicy.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.networkpolicy.enabled }} 2 | kind: NetworkPolicy 3 | apiVersion: networking.k8s.io/v1 4 | metadata: 5 | name: {{ include "resource.networkPolicy.name" . }} 6 | namespace: {{ include "resource.default.namespace" . }} 7 | labels: 8 | {{- include "labels.common" . | nindent 4 }} 9 | spec: 10 | podSelector: 11 | matchLabels: 12 | {{- include "labels.selector" . | nindent 6 }} 13 | ingress: 14 | - ports: 15 | - port: 8080 16 | protocol: TCP 17 | egress: 18 | - {} 19 | policyTypes: 20 | - Egress 21 | - Ingress 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.validate_changelog.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/ad0a25fbf301b2513e169ec964a8785d28f75be4/pkg/gen/input/workflows/internal/file/validate_changelog.yaml.template 6 | # 7 | name: Validate changelog 8 | 9 | on: 10 | pull_request: 11 | types: [opened, synchronize, reopened] 12 | paths: 13 | - 'CHANGELOG.md' 14 | 15 | permissions: 16 | contents: read 17 | pull-requests: write 18 | 19 | jobs: 20 | validate-changelog: 21 | uses: giantswarm/github-workflows/.github/workflows/validate-changelog.yaml@main 22 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/scaledobject.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.kedaScaledObject.enabled -}} 2 | apiVersion: keda.sh/v1alpha1 3 | kind: ScaledObject 4 | metadata: 5 | name: {{ include "resource.default.name" . }} 6 | namespace: {{ include "resource.default.namespace" . }} 7 | spec: 8 | scaleTargetRef: 9 | apiVersion: apps/v1 10 | kind: Deployment 11 | name: {{ include "resource.default.name" . }} 12 | {{- with .Values.kedaScaledObject.triggers }} 13 | triggers: 14 | {{- . | toYaml | nindent 2 }} 15 | {{- end }} 16 | minReplicaCount: {{ .Values.kedaScaledObject.minReplicas }} 17 | maxReplicaCount: {{ .Values.kedaScaledObject.maxReplicas }} 18 | {{- end -}} 19 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.check_values_schema.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/ad0a25fbf301b2513e169ec964a8785d28f75be4/pkg/gen/input/workflows/internal/file/check_values_schema.yaml.template 6 | # 7 | 8 | name: 'Values and schema' 9 | 10 | on: 11 | pull_request: 12 | branches: 13 | - master 14 | - main 15 | paths: 16 | - 'helm/**/values.yaml' # default helm chart values 17 | - 'helm/**/values.schema.json' # schema 18 | - 'helm/**/ci/ci-values.yaml' # overrides for CI (can contain required entries) 19 | 20 | jobs: 21 | check: 22 | uses: giantswarm/github-workflows/.github/workflows/chart-values.yaml@main 23 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/customMetricsHpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.customMetricsHPA.enabled -}} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: starboard-exporter-custom-metrics-hpa 6 | namespace: {{ include "resource.default.namespace" . }} 7 | labels: 8 | {{- include "labels.common" . | nindent 4 }} 9 | spec: 10 | scaleTargetRef: 11 | apiVersion: apps/v1 12 | kind: Deployment 13 | name: {{ include "resource.default.name" . }} 14 | minReplicas: {{ .Values.customMetricsHPA.minReplicas }} 15 | maxReplicas: {{ .Values.customMetricsHPA.maxReplicas }} 16 | metrics: 17 | - type: Pods 18 | pods: 19 | metricName: {{ .Values.customMetricsHPA.metricName }} 20 | targetAverageValue: {{ .Values.customMetricsHPA.targetAverageValueSeconds }} 21 | {{- end -}} 22 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/vpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.verticalPodAutoscaler.enabled -}} 2 | apiVersion: autoscaling.k8s.io/v1 3 | kind: VerticalPodAutoscaler 4 | metadata: 5 | name: starboard-exporter-vpa 6 | namespace: {{ include "resource.default.namespace" . }} 7 | labels: 8 | {{- include "labels.common" . | nindent 4 }} 9 | spec: 10 | resourcePolicy: 11 | containerPolicies: 12 | - containerName: '*' 13 | {{- if .Values.verticalPodAutoscaler.containerPolicies }} 14 | {{- with .Values.verticalPodAutoscaler.containerPolicies -}} 15 | {{ tpl (toYaml .) $ | nindent 6 }} 16 | {{- end }} 17 | {{- end }} 18 | targetRef: 19 | apiVersion: "apps/v1" 20 | kind: Deployment 21 | name: {{ include "resource.default.name" . }} 22 | updatePolicy: 23 | updateMode: Auto 24 | {{- end -}} 25 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.25.4 AS builder 3 | 4 | WORKDIR /workspace 5 | # Copy the Go Modules manifests 6 | COPY go.mod go.mod 7 | COPY go.sum go.sum 8 | # cache deps before building and copying source so that we don't need to re-download as much 9 | # and so that source changes don't invalidate our downloaded layer 10 | RUN go mod download 11 | 12 | # Copy the go source 13 | COPY main.go main.go 14 | # COPY api/ api/ 15 | COPY controllers/ controllers/ 16 | COPY utils/ utils/ 17 | 18 | # Build 19 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o manager main.go 20 | 21 | # Use distroless as minimal base image to package the manager binary 22 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 23 | FROM gcr.io/distroless/static:nonroot 24 | WORKDIR / 25 | COPY --from=builder /workspace/manager . 26 | USER 65532:65532 27 | 28 | ENTRYPOINT ["/manager"] 29 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/psp.yaml: -------------------------------------------------------------------------------- 1 | {{- if not .Values.global.podSecurityStandards.enforced }} 2 | apiVersion: policy/v1beta1 3 | kind: PodSecurityPolicy 4 | metadata: 5 | name: {{ include "resource.psp.name" . }} 6 | labels: 7 | {{- include "labels.common" . | nindent 4 }} 8 | annotations: 9 | seccomp.security.alpha.kubernetes.io/allowedProfileNames: 'runtime/default' 10 | spec: 11 | privileged: false 12 | fsGroup: 13 | rule: MustRunAs 14 | ranges: 15 | - min: 1 16 | max: 65535 17 | runAsUser: 18 | rule: MustRunAsNonRoot 19 | runAsGroup: 20 | rule: MustRunAs 21 | ranges: 22 | - min: 1 23 | max: 65535 24 | seLinux: 25 | rule: RunAsAny 26 | supplementalGroups: 27 | rule: RunAsAny 28 | volumes: 29 | - 'secret' 30 | - 'configMap' 31 | allowPrivilegeEscalation: false 32 | hostNetwork: false 33 | hostIPC: false 34 | hostPID: false 35 | {{- end }} 36 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.fix_vulnerabilities.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/251fa7d9bd403e23321bad6714c1e26c375fedf3/pkg/gen/input/workflows/internal/file/fix_vulnerabilities.yaml.template 6 | # 7 | 8 | name: Fix Go vulnerabilities 9 | 10 | on: 11 | schedule: 12 | - cron: '0 9 * * 1-5' 13 | workflow_dispatch: 14 | inputs: 15 | branch: 16 | description: Branch on which to fix vulnerabilities 17 | required: true 18 | type: string 19 | 20 | jobs: 21 | fix: 22 | uses: giantswarm/github-workflows/.github/workflows/fix-vulnerabilities.yaml@main 23 | with: 24 | branch: ${{ inputs.branch || github.ref }} 25 | secrets: 26 | HERALD_APP_ID: ${{ secrets.HERALD_APP_ID }} 27 | HERALD_APP_KEY: ${{ secrets.HERALD_APP_KEY }} 28 | NANCY_USER: ${{ secrets.NANCY_USER }} 29 | NANCY_TOKEN: ${{ secrets.NANCY_TOKEN }} 30 | -------------------------------------------------------------------------------- /controllers/configauditreport/configauditreport_metrics.go: -------------------------------------------------------------------------------- 1 | package configauditreport 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "sigs.k8s.io/controller-runtime/pkg/metrics" 6 | ) 7 | 8 | const ( 9 | metricNamespace = "starboard_exporter" 10 | metricSubsystem = "configauditreport" 11 | ) 12 | 13 | var metricLabels = []string{ 14 | "report_name", 15 | "resource_name", 16 | "resource_namespace", 17 | "severity", 18 | } 19 | 20 | // Gauge for the count of all config audit rules summary 21 | var ( 22 | ConfigAuditSummary = prometheus.NewGaugeVec( 23 | prometheus.GaugeOpts{ 24 | Namespace: metricNamespace, 25 | Subsystem: metricSubsystem, 26 | Name: "resource_checks_summary_count", 27 | Help: "Exposes the number of checks of a particular severity.", 28 | }, 29 | metricLabels, 30 | ) 31 | ) 32 | 33 | func init() { 34 | // Register custom metrics with the global prometheus registry 35 | metrics.Registry.MustRegister(ConfigAuditSummary) 36 | } 37 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/_resource.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Create a name stem for resource names 4 | 5 | When pods for deployments are created they have an additional 16 character 6 | suffix appended, e.g. "-957c9d6ff-pkzgw". Given that Kubernetes allows 63 7 | characters for resource names, the stem is truncated to 47 characters to leave 8 | room for such suffix. 9 | */}} 10 | {{- define "resource.default.name" -}} 11 | {{- .Release.Name | replace "." "-" | replace "_" "-" | replace "#" "-" | trunc 47 | trimSuffix "-" -}} 12 | {{- end -}} 13 | 14 | {{- define "resource.default.namespace" -}} 15 | {{ .Release.Namespace }} 16 | {{- end -}} 17 | 18 | {{- define "resource.networkPolicy.name" -}} 19 | {{- include "resource.default.name" . -}}-network-policy 20 | {{- end -}} 21 | 22 | {{- define "resource.psp.name" -}} 23 | {{- include "resource.default.name" . -}}-psp 24 | {{- end -}} 25 | 26 | {{- define "resource.pullSecret.name" -}} 27 | {{- include "resource.default.name" . -}}-pull-secret 28 | {{- end -}} 29 | -------------------------------------------------------------------------------- /Makefile.gen.md: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl@4.9.2 4 | # 5 | 6 | include Makefile.*.mk 7 | 8 | ##@ General 9 | 10 | # The help target prints out all targets with their descriptions organized 11 | # beneath their categories. The categories are represented by '##@' and the 12 | # target descriptions by '##'. The awk commands is responsible for reading the 13 | # entire set of makefiles included in this invocation, looking for lines of the 14 | # file as xyz: ## something, and then pretty-format the target and help. Then, 15 | # if there's a line with ##@ something, that gets pretty-printed as a category. 16 | # More info on the usage of ANSI control characters for terminal formatting: 17 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 18 | # More info on the awk command: 19 | # http://linuxcommand.org/lc3_adv_awk.php 20 | 21 | .PHONY: help 22 | help: ## Display this help. 23 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 24 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.run_ossf_scorecard.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/ad0a25fbf301b2513e169ec964a8785d28f75be4/pkg/gen/input/workflows/internal/file/run_ossf_scorecard.yaml.template 6 | # 7 | 8 | # This workflow uses actions that are not certified by GitHub. They are provided 9 | # by a third-party and are governed by separate terms of service, privacy 10 | # policy, and support documentation. 11 | 12 | name: Scorecard supply-chain security 13 | on: 14 | # For Branch-Protection check. Only the default branch is supported. See 15 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 16 | branch_protection_rule: {} 17 | # To guarantee Maintained check is occasionally updated. See 18 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 19 | schedule: 20 | - cron: '15 15 15 * *' 21 | push: 22 | branches: 23 | - main 24 | - master 25 | workflow_dispatch: {} 26 | 27 | jobs: 28 | analysis: 29 | uses: giantswarm/github-workflows/.github/workflows/ossf-scorecard.yaml@main 30 | secrets: 31 | scorecard_token: ${{ secrets.SCORECARD_TOKEN }} 32 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "name" -}} 6 | {{- .Chart.Name | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create chart name and version as used by the chart label. 11 | */}} 12 | {{- define "chart" -}} 13 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 14 | {{- end -}} 15 | 16 | {{/* 17 | Common labels 18 | */}} 19 | {{- define "labels.common" -}} 20 | application.giantswarm.io/team: {{ index .Chart.Annotations "application.giantswarm.io/team" | quote }} 21 | {{ include "labels.monitoring" . }} 22 | {{- end -}} 23 | 24 | {{/* 25 | Monitoring labels 26 | */}} 27 | {{- define "labels.monitoring" -}} 28 | app: {{ include "name" . | quote }} 29 | {{ include "labels.selector" . }} 30 | app.kubernetes.io/managed-by: {{ .Release.Service | quote }} 31 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 32 | helm.sh/chart: {{ include "chart" . | quote }} 33 | {{- end -}} 34 | 35 | {{/* 36 | Selector labels 37 | */}} 38 | {{- define "labels.selector" -}} 39 | app.kubernetes.io/name: {{ include "name" . | quote }} 40 | app.kubernetes.io/instance: {{ .Release.Name | quote }} 41 | {{- end -}} 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/6a704f7e2a8b0f09e82b5bab88f17971af849711/pkg/gen/input/makefile/internal/file/Makefile.template 6 | # 7 | 8 | include Makefile.*.mk 9 | 10 | ##@ General 11 | 12 | # The help target prints out all targets with their descriptions organized 13 | # beneath their categories. The categories are represented by '##@' and the 14 | # target descriptions by '##'. The awk commands is responsible for reading the 15 | # entire set of makefiles included in this invocation, looking for lines of the 16 | # file as xyz: ## something, and then pretty-format the target and help. Then, 17 | # if there's a line with ##@ something, that gets pretty-printed as a category. 18 | # More info on the usage of ANSI control characters for terminal formatting: 19 | # https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters 20 | # More info on the awk command: 21 | # http://linuxcommand.org/lc3_adv_awk.php 22 | 23 | .PHONY: help 24 | help: ## Display this help. 25 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z%\\\/_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 26 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.create_release_pr.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/ad0a25fbf301b2513e169ec964a8785d28f75be4/pkg/gen/input/workflows/internal/file/create_release_pr.yaml.template 6 | # 7 | name: Create Release PR 8 | on: 9 | push: 10 | branches: 11 | - 'legacy#release#v*.*.*' 12 | - 'main#release#v*.*.*' 13 | - 'main#release#major' 14 | - 'main#release#minor' 15 | - 'main#release#patch' 16 | - 'master#release#v*.*.*' 17 | - 'master#release#major' 18 | - 'master#release#minor' 19 | - 'master#release#patch' 20 | - 'release#v*.*.*' 21 | - 'release#major' 22 | - 'release#minor' 23 | - 'release#patch' 24 | - 'release-v*.*.x#release#v*.*.*' 25 | # "!" negates previous positive patterns so it has to be at the end. 26 | - '!release-v*.x.x#release#v*.*.*' 27 | workflow_call: 28 | inputs: 29 | branch: 30 | required: true 31 | type: string 32 | 33 | jobs: 34 | publish: 35 | uses: giantswarm/github-workflows/.github/workflows/create-release-pr.yaml@main 36 | with: 37 | branch: ${{ inputs.branch }} 38 | secrets: 39 | TAYLORBOT_GITHUB_ACTION: ${{ secrets.TAYLORBOT_GITHUB_ACTION }} 40 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/servicemonitor.yaml: -------------------------------------------------------------------------------- 1 | {{ if and (.Capabilities.APIVersions.Has "monitoring.coreos.com/v1") .Values.monitoring.serviceMonitor.enabled }} 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | name: {{ include "resource.default.name" . }} 7 | namespace: {{ include "resource.default.namespace" . }} 8 | labels: 9 | {{- include "labels.monitoring" . | nindent 4 }} 10 | {{- with .Values.monitoring.serviceMonitor.labels }} 11 | {{- toYaml . | nindent 4 }} 12 | {{- end }} 13 | spec: 14 | endpoints: 15 | - path: /metrics 16 | port: metrics 17 | {{- if .Values.monitoring.serviceMonitor.interval}} 18 | interval: {{ .Values.monitoring.serviceMonitor.interval }} 19 | {{- end }} 20 | {{- if .Values.monitoring.serviceMonitor.scrapeTimeout}} 21 | scrapeTimeout: {{ .Values.monitoring.serviceMonitor.scrapeTimeout }} 22 | {{- end }} 23 | {{- with .Values.monitoring.serviceMonitor.relabelings }} 24 | relabelings: 25 | {{- toYaml . | nindent 6 }} 26 | {{- end }} 27 | {{- with .Values.monitoring.serviceMonitor.metricRelabelings }} 28 | metricRelabelings: 29 | {{- toYaml . | nindent 6 }} 30 | {{- end }} 31 | selector: 32 | matchLabels: 33 | {{- include "labels.selector" . | nindent 6 }} 34 | {{- end }} 35 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # This file is maintained centrally at 2 | # https://github.com/giantswarm/github/blob/main/languages/go/.pre-commit-config.yaml 3 | 4 | minimum_pre_commit_version: '2.17' 5 | repos: 6 | # shell scripts 7 | - repo: https://github.com/detailyang/pre-commit-shell 8 | rev: '1.0.5' 9 | hooks: 10 | - id: shell-lint 11 | args: [ --format=json ] 12 | exclude: ".*\\.template" 13 | 14 | - repo: https://github.com/pre-commit/pre-commit-hooks 15 | rev: v6.0.0 16 | hooks: 17 | - id: check-added-large-files 18 | - id: check-merge-conflict 19 | - id: check-shebang-scripts-are-executable 20 | - id: detect-private-key 21 | - id: end-of-file-fixer 22 | exclude: ".*testdata/.*" 23 | - id: mixed-line-ending 24 | - id: trailing-whitespace 25 | exclude: ".*testdata/.*" 26 | 27 | - repo: https://github.com/dnephin/pre-commit-golang 28 | rev: v0.5.1 29 | hooks: 30 | - id: go-fmt 31 | - id: go-mod-tidy 32 | - id: golangci-lint 33 | args: 34 | - -E=gosec 35 | - -E=goconst 36 | - -E=govet 37 | # timeout is needed for CI 38 | - --timeout=300s 39 | # List all issues found 40 | - --max-same-issues=0 41 | - --max-issues-per-linter=0 42 | - id: go-imports 43 | args: [ -local, github.com/giantswarm/starboard-exporter ] 44 | -------------------------------------------------------------------------------- /DCO: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 660 York Street, Suite 102, 6 | San Francisco, CA 94110 USA 7 | 8 | Everyone is permitted to copy and distribute verbatim copies of this 9 | license document, but changing it is not allowed. 10 | 11 | 12 | Developer's Certificate of Origin 1.1 13 | 14 | By making a contribution to this project, I certify that: 15 | 16 | (a) The contribution was created in whole or in part by me and I 17 | have the right to submit it under the open source license 18 | indicated in the file; or 19 | 20 | (b) The contribution is based upon previous work that, to the best 21 | of my knowledge, is covered under an appropriate open source 22 | license and I have the right under that license to submit that 23 | work with modifications, whether created in whole or in part 24 | by me, under the same open source license (unless I am 25 | permitted to submit under a different license), as indicated 26 | in the file; or 27 | 28 | (c) The contribution was provided directly to me by some other 29 | person who certified (a), (b) or (c) and I have not modified 30 | it. 31 | 32 | (d) I understand and agree that this project and the contribution 33 | are public and that a record of the contribution (including all 34 | personal information I submit with it, including my sign-off) is 35 | maintained indefinitely and may be redistributed consistent with 36 | this project or the open source license(s) involved. 37 | -------------------------------------------------------------------------------- /docs/custom_metrics_hpa.md: -------------------------------------------------------------------------------- 1 | ### Enabling custom metrics HPA for starboard-exporter 2 | 3 | #### Required components 4 | 5 | [Prometheus Adapter](https://github.com/kubernetes-sigs/prometheus-adapter) 6 | 7 | #### Steps 8 | 9 | 1. Install prometheus adapter. 10 | 1. Configure helm chart to connect with prometheus to get the metrics. Change the values as required. 11 | ``` 12 | prometheus: 13 | # Value is templated 14 | url: http://prometheus-operated.monitoring.svc 15 | port: 9090 16 | path: "" 17 | ``` 18 | 2. Create seriesQuery & metricsQuery to fetch metrics for the required components. For example, labels like `app="starboard-exporter"` or `job="starboard-exporter"` can be used to filter the `starboard-exporter` metrics in prometheus. Change the values as required. 19 | ``` 20 | rules: 21 | custom: 22 | - seriesQuery: 'scrape_duration_seconds{job="starboard-exporter"}' 23 | seriesFilters: [] 24 | resources: 25 | template: <<.Resource>> 26 | name: 27 | as: "scrapedurationseconds" 28 | metricsQuery: scrape_duration_seconds{job="starboard-exporter"} 29 | ``` 30 | 31 | #### Make sure prometheus adapter is scraping the metrics & exposing the custom metric 32 | 33 | ```bash 34 | export TEST_NAMESPACE=giantswarm 35 | kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/$TEST_NAMESPACE/pods/*/scrapedurationseconds" 36 | kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/$TEST_NAMESPACE/services/*/scrapedurationseconds" | jq -r . 37 | ``` 38 | 39 | 2. Enable `customMetricsHPA` from `values.yaml` 40 | -------------------------------------------------------------------------------- /utils/jitter.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "time" 7 | 8 | "github.com/go-logr/logr" 9 | ctrl "sigs.k8s.io/controller-runtime" 10 | "sigs.k8s.io/controller-runtime/pkg/reconcile" 11 | ) 12 | 13 | // JitterRequeue accepts a default requeue time, a maximum percentage to jitter, and a logger, 14 | // and returns a Result containing a requeue time which has been randomized with jitter. 15 | func JitterRequeue(defaultDuration time.Duration, maxJitterPercent int, log logr.Logger) reconcile.Result { 16 | 17 | after, err := Jitter(defaultDuration, maxJitterPercent) 18 | if err != nil { 19 | log.Error(err, "Failed to calculate jitter") 20 | after = defaultDuration 21 | } 22 | 23 | return ctrl.Result{ 24 | Requeue: true, 25 | RequeueAfter: after, 26 | } 27 | } 28 | 29 | // Jitter accepts a Duration and maximum percentage to jitter (as an int), 30 | // and returns a random Duration in the range t +/- maxJitterPercent. 31 | func Jitter(t time.Duration, maxJitterPercent int) (time.Duration, error) { 32 | // Get the maximum jitter length as a duration. 33 | // Max = t * maxJitterPercent / 100. 34 | 35 | // Decimal representation of the maximum jitter. E.g. 25% --> 0.25. 36 | jitterMultiplier := float64(maxJitterPercent) / 100.00 37 | 38 | // Maximum length of time, in milliseconds, which we can add or subtract from our target time. 39 | jitterDuration := int64( 40 | float64(t.Milliseconds()) * jitterMultiplier) 41 | 42 | // Maximum length of jitter time as a Go Duration. 43 | maxJitter, err := time.ParseDuration(fmt.Sprintf("%dms", jitterDuration)) 44 | if err != nil { 45 | return t, err 46 | } 47 | 48 | // Calcluate the minimum time we have to wait. 49 | minDuration := t - maxJitter 50 | 51 | // Set the final duration to the min + a random duration between 0 and our max jitter. 52 | return minDuration + time.Duration(rand.Int63n(int64(maxJitter))), nil // nolint:gosec // rand not used for crypto. 53 | } 54 | -------------------------------------------------------------------------------- /Makefile.gen.app.mk: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/eea19f200d7cfd27ded22474b787563bbfdb8ec4/pkg/gen/input/makefile/internal/file/Makefile.gen.app.mk.template 6 | # 7 | 8 | ##@ App 9 | 10 | YQ=docker run --rm -u $$(id -u) -v $${PWD}:/workdir mikefarah/yq:4.29.2 11 | HELM_DOCS=docker run --rm -u $$(id -u) -v $${PWD}:/helm-docs jnorwood/helm-docs:v1.11.0 12 | 13 | ifdef APPLICATION 14 | DEPS := $(shell find $(APPLICATION)/charts -maxdepth 2 -name "Chart.yaml" -printf "%h\n") 15 | endif 16 | 17 | .PHONY: lint-chart check-env update-chart helm-docs update-deps $(DEPS) 18 | 19 | lint-chart: IMAGE := giantswarm/helm-chart-testing:v3.0.0-rc.1 20 | lint-chart: check-env ## Runs ct against the default chart. 21 | @echo "====> $@" 22 | rm -rf /tmp/$(APPLICATION)-test 23 | mkdir -p /tmp/$(APPLICATION)-test/helm 24 | cp -a ./helm/$(APPLICATION) /tmp/$(APPLICATION)-test/helm/ 25 | architect helm template --dir /tmp/$(APPLICATION)-test/helm/$(APPLICATION) 26 | docker run -it --rm -v /tmp/$(APPLICATION)-test:/wd --workdir=/wd --name ct $(IMAGE) ct lint --validate-maintainers=false --charts="helm/$(APPLICATION)" 27 | rm -rf /tmp/$(APPLICATION)-test 28 | 29 | update-chart: check-env ## Sync chart with upstream repo. 30 | @echo "====> $@" 31 | vendir sync 32 | $(MAKE) update-deps 33 | 34 | update-deps: check-env $(DEPS) ## Update Helm dependencies. 35 | cd $(APPLICATION) && helm dependency update 36 | 37 | $(DEPS): check-env ## Update main Chart.yaml with new local dep versions. 38 | dep_name=$(shell basename $@) && \ 39 | new_version=`$(YQ) .version $(APPLICATION)/charts/$$dep_name/Chart.yaml` && \ 40 | $(YQ) -i e "with(.dependencies[]; select(.name == \"$$dep_name\") | .version = \"$$new_version\")" $(APPLICATION)/Chart.yaml 41 | 42 | helm-docs: check-env ## Update $(APPLICATION) README. 43 | $(HELM_DOCS) -c $(APPLICATION) -g $(APPLICATION) 44 | 45 | check-env: 46 | ifndef APPLICATION 47 | $(error APPLICATION is not defined) 48 | endif 49 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.add-team-labels.yaml: -------------------------------------------------------------------------------- 1 | name: Add appropriate labels to issue 2 | 3 | on: 4 | issues: 5 | types: [assigned] 6 | 7 | jobs: 8 | build_user_list: 9 | name: Get yaml config of GS users 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | steps: 14 | - name: Get user-mapping 15 | env: 16 | GH_TOKEN: ${{ secrets.ISSUE_AUTOMATION }} 17 | run: | 18 | mkdir -p artifacts 19 | gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \ 20 | /repos/giantswarm/github/contents/tools/issue-automation/user-mapping.yaml \ 21 | | jq -r '.content' \ 22 | | base64 -d > artifacts/users.yaml 23 | - name: Upload Artifact 24 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 25 | with: 26 | name: users 27 | path: artifacts/users.yaml 28 | retention-days: 1 29 | 30 | add_label: 31 | name: Add team label when assigned 32 | runs-on: ubuntu-latest 33 | needs: build_user_list 34 | steps: 35 | - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 36 | id: download-users 37 | with: 38 | name: users 39 | - name: Find team label based on user names 40 | run: | 41 | event_assignee=$(cat $GITHUB_EVENT_PATH | jq -r .assignee.login | tr '[:upper:]' '[:lower:]') 42 | echo "Issue assigned to: ${event_assignee}" 43 | 44 | TEAMS=$(cat ${{steps.download-users.outputs.download-path}}/users.yaml | tr '[:upper:]' '[:lower:]' | yq ".${event_assignee}.teams" -o csv | tr ',' ' ') 45 | 46 | echo "LABEL<> $GITHUB_ENV 47 | for team in ${TEAMS}; do 48 | echo "Team: ${team} | Label: team/${team}" 49 | echo "team/${team}" >> $GITHUB_ENV 50 | done 51 | echo "EOF" >> $GITHUB_ENV 52 | - name: Apply label to issue 53 | if: ${{ env.LABEL != '' && env.LABEL != 'null' && env.LABEL != null }} 54 | uses: actions-ecosystem/action-add-labels@bd52874380e3909a1ac983768df6976535ece7f8 # v1.1.3 55 | with: 56 | github_token: ${{ secrets.ISSUE_AUTOMATION }} 57 | labels: | 58 | ${{ env.LABEL }} 59 | -------------------------------------------------------------------------------- /tests/ats/test_basic_cluster.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from contextlib import contextmanager 3 | from pathlib import Path 4 | from typing import Dict, List 5 | 6 | import pykube 7 | import pytest 8 | from pytest_helm_charts.clusters import Cluster 9 | from pytest_helm_charts.k8s.deployment import wait_for_deployments_to_run 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | namespace_name = "default" 14 | 15 | timeout: int = 360 16 | 17 | 18 | @pytest.mark.smoke 19 | def test_api_working(kube_cluster: Cluster) -> None: 20 | """Very minimalistic example of using the [kube_cluster](pytest_helm_charts.fixtures.kube_cluster) 21 | fixture to get an instance of [Cluster](pytest_helm_charts.clusters.Cluster) under test 22 | and access its [kube_client](pytest_helm_charts.clusters.Cluster.kube_client) property 23 | to get access to Kubernetes API of cluster under test. 24 | Please refer to [pykube](https://pykube.readthedocs.io/en/latest/api/pykube.html) to get docs 25 | for [HTTPClient](https://pykube.readthedocs.io/en/latest/api/pykube.html#pykube.http.HTTPClient). 26 | """ 27 | assert kube_cluster.kube_client is not None 28 | assert len(pykube.Node.objects(kube_cluster.kube_client)) >= 1 29 | 30 | 31 | @pytest.mark.smoke 32 | def test_cluster_info( 33 | kube_cluster: Cluster, cluster_type: str, test_extra_info: Dict[str, str] 34 | ) -> None: 35 | """Example shows how you can access additional information about the cluster the tests are running on""" 36 | logger.info(f"Running on cluster type {cluster_type}") 37 | key = "external_cluster_type" 38 | if key in test_extra_info: 39 | logger.info(f"{key} is {test_extra_info[key]}") 40 | assert kube_cluster.kube_client is not None 41 | assert cluster_type != "" 42 | 43 | 44 | # scope "module" means this is run only once, for the first test case requesting! It might be tricky 45 | # if you want to assert this multiple times 46 | @pytest.fixture(scope="module") 47 | def app_deployment(kube_cluster: Cluster) -> List[pykube.Deployment]: 48 | deployments = wait_for_deployments_to_run( 49 | kube_cluster.kube_client, 50 | ["starboard-exporter"], 51 | "default", 52 | timeout, 53 | ) 54 | return deployments 55 | 56 | 57 | # when we start the tests on circleci, we have to wait for pods to be available, hence 58 | # this additional delay and retries 59 | @pytest.mark.smoke 60 | @pytest.mark.upgrade 61 | @pytest.mark.flaky(reruns=5, reruns_delay=10) 62 | def test_pods_available(kube_cluster: Cluster, app_deployment: List[pykube.Deployment]): 63 | for d in app_deployment: 64 | assert int(d.obj["status"]["readyReplicas"]) > 0 65 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: {{ include "resource.default.name" . }} 5 | labels: 6 | {{- include "labels.common" . | nindent 4 }} 7 | rules: 8 | - apiGroups: 9 | - aquasecurity.github.io 10 | resources: 11 | - vulnerabilityreports 12 | - configauditreports 13 | verbs: 14 | - get 15 | - list 16 | - watch 17 | - update 18 | - patch 19 | - nonResourceURLs: 20 | - "/" 21 | - "/healthz" 22 | verbs: 23 | - get 24 | --- 25 | apiVersion: rbac.authorization.k8s.io/v1 26 | kind: ClusterRoleBinding 27 | metadata: 28 | name: {{ include "resource.default.name" . }} 29 | labels: 30 | {{- include "labels.common" . | nindent 4 }} 31 | subjects: 32 | - kind: ServiceAccount 33 | name: {{ include "resource.default.name" . }} 34 | namespace: {{ include "resource.default.namespace" . }} 35 | roleRef: 36 | kind: ClusterRole 37 | name: {{ include "resource.default.name" . }} 38 | apiGroup: rbac.authorization.k8s.io 39 | --- 40 | apiVersion: rbac.authorization.k8s.io/v1 41 | kind: Role 42 | metadata: 43 | name: {{ include "resource.default.name" . }} 44 | namespace: {{ include "resource.default.namespace" . }} 45 | labels: 46 | {{- include "labels.common" . | nindent 4 }} 47 | rules: 48 | - apiGroups: 49 | - "" 50 | resources: 51 | - endpoints 52 | verbs: 53 | - get 54 | - list 55 | - watch 56 | --- 57 | apiVersion: rbac.authorization.k8s.io/v1 58 | kind: RoleBinding 59 | metadata: 60 | name: {{ include "resource.default.name" . }} 61 | namespace: {{ include "resource.default.namespace" . }} 62 | labels: 63 | {{- include "labels.common" . | nindent 4 }} 64 | subjects: 65 | - kind: ServiceAccount 66 | name: {{ include "resource.default.name" . }} 67 | namespace: {{ include "resource.default.namespace" . }} 68 | roleRef: 69 | kind: Role 70 | name: {{ include "resource.default.name" . }} 71 | apiGroup: rbac.authorization.k8s.io 72 | --- 73 | {{ if not .Values.global.podSecurityStandards.enforced }} 74 | apiVersion: rbac.authorization.k8s.io/v1 75 | kind: ClusterRole 76 | metadata: 77 | name: {{ include "resource.psp.name" . }} 78 | labels: 79 | {{- include "labels.common" . | nindent 4 }} 80 | rules: 81 | - apiGroups: 82 | - extensions 83 | resources: 84 | - podsecuritypolicies 85 | verbs: 86 | - use 87 | resourceNames: 88 | - {{ include "resource.psp.name" . }} 89 | --- 90 | apiVersion: rbac.authorization.k8s.io/v1 91 | kind: ClusterRoleBinding 92 | metadata: 93 | name: {{ include "resource.psp.name" . }} 94 | labels: 95 | {{- include "labels.common" . | nindent 4 }} 96 | subjects: 97 | - kind: ServiceAccount 98 | name: {{ include "resource.default.name" . }} 99 | namespace: {{ include "resource.default.namespace" . }} 100 | roleRef: 101 | kind: ClusterRole 102 | name: {{ include "resource.psp.name" . }} 103 | apiGroup: rbac.authorization.k8s.io 104 | {{- end }} 105 | -------------------------------------------------------------------------------- /helm/starboard-exporter/values.yaml: -------------------------------------------------------------------------------- 1 | replicas: 1 2 | registry: 3 | domain: gsoci.azurecr.io 4 | 5 | image: 6 | name: "giantswarm/starboard-exporter" 7 | 8 | imagePullSecrets: [] 9 | 10 | global: 11 | podSecurityStandards: 12 | enforced: true 13 | 14 | pod: 15 | user: 16 | id: 1000 17 | group: 18 | id: 1000 19 | 20 | nodeSelector: {} 21 | tolerations: [] 22 | 23 | # Additional custom pod labels 24 | podLabels: {} 25 | 26 | # Pod securityContext 27 | podSecurityContext: 28 | runAsNonRoot: true 29 | seccompProfile: 30 | type: RuntimeDefault 31 | 32 | # Container securityContext 33 | securityContext: 34 | allowPrivilegeEscalation: false 35 | capabilities: 36 | drop: 37 | - ALL 38 | privileged: false 39 | readOnlyRootFilesystem: true 40 | runAsNonRoot: true 41 | seccompProfile: 42 | type: RuntimeDefault 43 | 44 | resources: 45 | requests: 46 | cpu: 100m 47 | memory: 220Mi 48 | limits: 49 | cpu: 100m 50 | memory: 220Mi 51 | 52 | exporter: 53 | requeueMaxJitterPercent: 10 54 | 55 | configAuditReports: 56 | enabled: true 57 | 58 | vulnerabilityReports: 59 | enabled: true 60 | targetLabels: [] 61 | # - image_namespace 62 | # - image_repository 63 | # - image_tag 64 | # - vulnerability_id 65 | 66 | monitoring: 67 | serviceMonitor: 68 | enabled: true 69 | labels: {} 70 | relabelings: 71 | - action: labeldrop 72 | regex: pod|service|container 73 | metricRelabelings: [] 74 | 75 | grafanaDashboard: 76 | enabled: true 77 | # namespace: "" 78 | 79 | networkpolicy: 80 | enabled: true 81 | 82 | podAnnotations: {} 83 | 84 | minReplicas: &minReplicas 2 85 | maxReplicas: &maxReplicas 97 # The number of replicas is limited to 97 by the current sharding math. See https://www.giantswarm.io/blog/cveing-is-believing 86 | 87 | customMetricsHPA: 88 | enabled: false 89 | minReplicas: *minReplicas 90 | maxReplicas: *maxReplicas 91 | metricName: scrapedurationseconds 92 | targetAverageValueSeconds: 10 # Scrape duration seconds timeout 93 | 94 | verticalPodAutoscaler: 95 | enabled: true 96 | containerPolicies: 97 | minAllowed: 98 | cpu: 50m 99 | memory: 100Mi 100 | maxAllowed: 101 | cpu: 1 102 | memory: 4Gi 103 | 104 | kedaScaledObject: 105 | enabled: false 106 | minReplicas: *minReplicas 107 | maxReplicas: *maxReplicas 108 | triggers: [] 109 | # https://keda.sh/docs/2.12/concepts/scaling-deployments/#triggers 110 | # - type: prometheus 111 | # metadata: 112 | # serverAddress: 113 | # metricName: 114 | # query: 115 | # threshold: 116 | # authModes: 117 | # authenticationRef: # configurable block, might not be required, depends on the server 118 | # name: 119 | # kind: 120 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.add-to-project-board.yaml: -------------------------------------------------------------------------------- 1 | name: Add Issue to Project when assigned 2 | 3 | on: 4 | issues: 5 | types: 6 | - assigned 7 | - labeled 8 | 9 | jobs: 10 | build_user_list: 11 | name: Get yaml config of GS users 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | steps: 16 | - name: Get user-mapping 17 | env: 18 | GH_TOKEN: ${{ secrets.ISSUE_AUTOMATION }} 19 | run: | 20 | mkdir -p artifacts 21 | gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \ 22 | /repos/giantswarm/github/contents/tools/issue-automation/user-mapping.yaml \ 23 | | jq -r '.content' \ 24 | | base64 -d > artifacts/users.yaml 25 | - name: Upload Artifact 26 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 27 | with: 28 | name: users 29 | path: artifacts/users.yaml 30 | retention-days: 1 31 | - name: Get label-mapping 32 | env: 33 | GH_TOKEN: ${{ secrets.ISSUE_AUTOMATION }} 34 | run: | 35 | mkdir -p artifacts 36 | gh api -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" \ 37 | /repos/giantswarm/github/contents/tools/issue-automation/label-mapping.yaml \ 38 | | jq -r '.content' \ 39 | | base64 -d > artifacts/labels.yaml 40 | - name: Upload Artifact 41 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 42 | with: 43 | name: labels 44 | path: artifacts/labels.yaml 45 | retention-days: 1 46 | 47 | add_to_personal_board: 48 | name: Add issue to personal board 49 | runs-on: ubuntu-latest 50 | needs: build_user_list 51 | if: github.event.action == 'assigned' 52 | steps: 53 | - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 54 | id: download-users 55 | with: 56 | name: users 57 | - name: Find personal board based on user names 58 | run: | 59 | event_assignee=$(cat $GITHUB_EVENT_PATH | jq -r .assignee.login | tr '[:upper:]' '[:lower:]') 60 | echo "Issue assigned to: ${event_assignee}" 61 | 62 | BOARD=($(cat ${{steps.download-users.outputs.download-path}}/users.yaml | tr '[:upper:]' '[:lower:]' | yq ".${event_assignee}.personalboard")) 63 | echo "Personal board URL: ${BOARD}" 64 | 65 | echo "BOARD=${BOARD}" >> $GITHUB_ENV 66 | - name: Add issue to personal board 67 | if: ${{ env.BOARD != 'null' && env.BOARD != '' && env.BOARD != null }} 68 | uses: actions/add-to-project@9bfe908f2eaa7ba10340b31e314148fcfe6a2458 # v1.0.1 69 | with: 70 | project-url: ${{ env.BOARD }} 71 | github-token: ${{ secrets.ISSUE_AUTOMATION }} 72 | 73 | add_to_team_board: 74 | name: Add issue to team board 75 | runs-on: ubuntu-latest 76 | needs: build_user_list 77 | if: github.event.action == 'labeled' 78 | steps: 79 | - uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7 80 | id: download-labels 81 | with: 82 | name: labels 83 | - name: Find team board based on label 84 | run: | 85 | event_label=$(cat $GITHUB_EVENT_PATH | jq -r .label.name | tr '[:upper:]' '[:lower:]') 86 | echo "Issue labelled with: ${event_label}" 87 | 88 | BOARD=($(cat ${{steps.download-labels.outputs.download-path}}/labels.yaml | tr '[:upper:]' '[:lower:]' | yq ".[\"${event_label}\"].projectboard")) 89 | echo "Team board URL: ${BOARD}" 90 | 91 | echo "BOARD=${BOARD}" >> $GITHUB_ENV 92 | - name: Add issue to team board 93 | if: ${{ env.BOARD != 'null' && env.BOARD != '' && env.BOARD != null }} 94 | uses: actions/add-to-project@9bfe908f2eaa7ba10340b31e314148fcfe6a2458 # v1.0.1 95 | with: 96 | project-url: ${{ env.BOARD }} 97 | github-token: ${{ secrets.ISSUE_AUTOMATION }} 98 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "resource.default.name" . }} 5 | namespace: {{ include "resource.default.namespace" . }} 6 | labels: 7 | {{- include "labels.common" . | nindent 4 }} 8 | spec: 9 | replicas: {{ .Values.replicas }} 10 | selector: 11 | matchLabels: 12 | {{- include "labels.selector" . | nindent 6 }} 13 | strategy: 14 | type: Recreate 15 | template: 16 | metadata: 17 | {{- with .Values.podAnnotations }} 18 | annotations: 19 | {{- . | toYaml | nindent 8 }} 20 | {{- end }} 21 | labels: 22 | {{- include "labels.common" . | nindent 8 }} 23 | {{- if .Values.podLabels }} 24 | {{- toYaml .Values.podLabels | nindent 8 }} 25 | {{- end }} 26 | spec: 27 | {{- with .Values.nodeSelector }} 28 | nodeSelector: 29 | {{- . | toYaml | nindent 8 }} 30 | {{- end }} 31 | {{- with .Values.tolerations }} 32 | tolerations: 33 | {{- . | toYaml | nindent 8 }} 34 | {{- end }} 35 | affinity: 36 | podAntiAffinity: 37 | preferredDuringSchedulingIgnoredDuringExecution: 38 | - podAffinityTerm: 39 | labelSelector: 40 | matchLabels: 41 | {{- include "labels.selector" . | nindent 18 }} 42 | topologyKey: kubernetes.io/hostname 43 | weight: 100 44 | nodeAffinity: 45 | requiredDuringSchedulingIgnoredDuringExecution: 46 | nodeSelectorTerms: 47 | - matchExpressions: 48 | - key: kubernetes.io/os 49 | operator: In 50 | values: 51 | - linux 52 | - key: kubernetes.io/arch 53 | operator: In 54 | values: 55 | - amd64 56 | serviceAccountName: {{ include "resource.default.name" . }} 57 | securityContext: 58 | runAsUser: {{ .Values.pod.user.id }} 59 | runAsGroup: {{ .Values.pod.group.id }} 60 | {{- with .Values.podSecurityContext }} 61 | {{- . | toYaml | nindent 8 }} 62 | {{- end }} 63 | {{- with .Values.imagePullSecrets }} 64 | imagePullSecrets: 65 | {{- toYaml . | nindent 8 }} 66 | {{- end }} 67 | containers: 68 | - name: {{ include "name" . }} 69 | image: "{{ .Values.registry.domain }}/{{ .Values.image.name }}:{{ .Chart.Version }}" 70 | args: 71 | - --pod-ip=$(MY_POD_IP) 72 | - --service-name={{ include "resource.default.name" . }} 73 | - --service-namespace={{ include "resource.default.namespace" . }} 74 | {{- if .Values.exporter.vulnerabilityReports.targetLabels }} 75 | - --target-labels={{ .Values.exporter.vulnerabilityReports.targetLabels | join "," }} 76 | {{- end }} 77 | # A little magic for handling defaulting with booleans https://github.com/helm/helm/issues/3308#issuecomment-701367019 78 | - --config-audits-enabled={{ hasKey .Values.exporter.configAuditReports "enabled" | ternary .Values.exporter.configAuditReports.enabled true }} 79 | - --vulnerability-scans-enabled={{ hasKey .Values.exporter.vulnerabilityReports "enabled" | ternary .Values.exporter.vulnerabilityReports.enabled true }} 80 | {{- if .Values.exporter.requeueMaxJitterPercent }} 81 | - --max-jitter-percent={{ .Values.exporter.requeueMaxJitterPercent }} 82 | {{- end }} 83 | env: 84 | - name: MY_POD_IP 85 | valueFrom: 86 | fieldRef: 87 | fieldPath: status.podIP 88 | ports: 89 | - containerPort: 8080 90 | name: metrics 91 | protocol: TCP 92 | - containerPort: 8081 93 | name: probes 94 | protocol: TCP 95 | livenessProbe: 96 | httpGet: 97 | path: /healthz 98 | port: 8081 99 | initialDelaySeconds: 30 100 | timeoutSeconds: 1 101 | resources: 102 | {{ toYaml .Values.resources | indent 10 }} 103 | {{- with .Values.securityContext }} 104 | securityContext: 105 | {{- . | toYaml | nindent 10 }} 106 | {{- end }} 107 | -------------------------------------------------------------------------------- /Makefile.gen.go.mk: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/bf7f386ac6a4e807dde959892df1369fee6d789f/pkg/gen/input/makefile/internal/file/Makefile.gen.go.mk.template 6 | # 7 | 8 | APPLICATION := $(shell go list -m | cut -d '/' -f 3) 9 | BUILDTIMESTAMP := $(shell date -u '+%FT%TZ') 10 | GITSHA1 := $(shell git rev-parse --verify HEAD) 11 | MODULE := $(shell go list -m) 12 | OS := $(shell go env GOOS) 13 | SOURCES := $(shell find . -name '*.go') 14 | VERSION := $(shell architect project version) 15 | ifeq ($(OS), linux) 16 | EXTLDFLAGS := -static 17 | endif 18 | LDFLAGS ?= -w -linkmode 'auto' -extldflags '$(EXTLDFLAGS)' \ 19 | -X '$(shell go list -m)/pkg/project.buildTimestamp=${BUILDTIMESTAMP}' \ 20 | -X '$(shell go list -m)/pkg/project.gitSHA=${GITSHA1}' 21 | 22 | .DEFAULT_GOAL := build 23 | 24 | ##@ Go 25 | 26 | .PHONY: build build-darwin build-darwin-64 build-linux build-linux-arm64 build-windows-amd64 27 | build: $(APPLICATION) ## Builds a local binary. 28 | @echo "====> $@" 29 | build-darwin: $(APPLICATION)-darwin ## Builds a local binary for darwin/amd64. 30 | @echo "====> $@" 31 | build-darwin-arm64: $(APPLICATION)-darwin-arm64 ## Builds a local binary for darwin/arm64. 32 | @echo "====> $@" 33 | build-linux: $(APPLICATION)-linux ## Builds a local binary for linux/amd64. 34 | @echo "====> $@" 35 | build-linux-arm64: $(APPLICATION)-linux-arm64 ## Builds a local binary for linux/arm64. 36 | @echo "====> $@" 37 | build-windows-amd64: $(APPLICATION)-windows-amd64.exe ## Builds a local binary for windows/amd64. 38 | @echo "====> $@" 39 | 40 | $(APPLICATION): $(APPLICATION)-v$(VERSION)-$(OS)-amd64 41 | @echo "====> $@" 42 | cp -a $< $@ 43 | 44 | $(APPLICATION)-darwin: $(APPLICATION)-v$(VERSION)-darwin-amd64 45 | @echo "====> $@" 46 | cp -a $< $@ 47 | 48 | $(APPLICATION)-darwin-arm64: $(APPLICATION)-v$(VERSION)-darwin-arm64 49 | @echo "====> $@" 50 | cp -a $< $@ 51 | 52 | $(APPLICATION)-linux: $(APPLICATION)-v$(VERSION)-linux-amd64 53 | @echo "====> $@" 54 | cp -a $< $@ 55 | 56 | $(APPLICATION)-linux-arm64: $(APPLICATION)-v$(VERSION)-linux-arm64 57 | @echo "====> $@" 58 | cp -a $< $@ 59 | 60 | $(APPLICATION)-windows-amd64.exe: $(APPLICATION)-v$(VERSION)-windows-amd64.exe 61 | @echo "====> $@" 62 | cp -a $< $@ 63 | 64 | $(APPLICATION)-v$(VERSION)-%-amd64: $(SOURCES) 65 | @echo "====> $@" 66 | CGO_ENABLED=0 GOOS=$* GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o $@ . 67 | 68 | $(APPLICATION)-v$(VERSION)-%-arm64: $(SOURCES) 69 | @echo "====> $@" 70 | CGO_ENABLED=0 GOOS=$* GOARCH=arm64 go build -trimpath -ldflags "$(LDFLAGS)" -o $@ . 71 | 72 | $(APPLICATION)-v$(VERSION)-windows-amd64.exe: $(SOURCES) 73 | @echo "====> $@" 74 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -o $@ . 75 | 76 | .PHONY: install 77 | install: ## Install the application. 78 | @echo "====> $@" 79 | go install -ldflags "$(LDFLAGS)" . 80 | 81 | .PHONY: run 82 | run: ## Runs go run main.go. 83 | @echo "====> $@" 84 | go run -ldflags "$(LDFLAGS)" -race . 85 | 86 | .PHONY: clean 87 | clean: ## Cleans the binary. 88 | @echo "====> $@" 89 | rm -f $(APPLICATION)* 90 | go clean 91 | 92 | .PHONY: imports 93 | imports: ## Runs goimports. 94 | @echo "====> $@" 95 | goimports -local $(MODULE) -w . 96 | 97 | .PHONY: lint 98 | lint: ## Runs golangci-lint. 99 | @echo "====> $@" 100 | golangci-lint run -E gosec -E goconst --timeout=15m ./... 101 | 102 | .PHONY: fmt 103 | fmt: ## Run go fmt against code. 104 | go fmt ./... 105 | 106 | .PHONY: vet 107 | vet: ## Run go vet against code. 108 | go vet ./... 109 | 110 | .PHONY: nancy 111 | nancy: ## Runs nancy (requires v1.0.37 or newer). 112 | @echo "====> $@" 113 | CGO_ENABLED=0 go list -json -deps ./... | nancy sleuth --skip-update-check --quiet --exclude-vulnerability-file ./.nancy-ignore --additional-exclude-vulnerability-files ./.nancy-ignore.generated 114 | 115 | .PHONY: test 116 | test: ## Runs go test with default values. 117 | @echo "====> $@" 118 | go test -ldflags "$(LDFLAGS)" -race ./... 119 | 120 | .PHONY: build-docker 121 | build-docker: build-linux ## Builds docker image to registry. 122 | @echo "====> $@" 123 | cp -a $(APPLICATION)-linux $(APPLICATION) 124 | docker build -t ${APPLICATION}:${VERSION} . 125 | -------------------------------------------------------------------------------- /controllers/vulnerabilityreport/vulnerabilityreport_metrics.go: -------------------------------------------------------------------------------- 1 | package vulnerabilityreport 2 | 3 | import ( 4 | "github.com/prometheus/client_golang/prometheus" 5 | "sigs.k8s.io/controller-runtime/pkg/metrics" 6 | 7 | "github.com/giantswarm/starboard-exporter/utils" 8 | ) 9 | 10 | const ( 11 | metricNamespace = "starboard_exporter" 12 | metricSubsystem = "vulnerabilityreport" 13 | 14 | LabelGroupAll = "all" 15 | labelGroupSummary = "summary" 16 | ) 17 | 18 | type FieldScope string 19 | 20 | const ( 21 | FieldScopeReport FieldScope = "report" 22 | FieldScopeVulnerability FieldScope = "vulnerability" 23 | ) 24 | 25 | type VulnerabilityLabel struct { 26 | Name string 27 | Groups []string 28 | Scope FieldScope 29 | // Handler valueFromReport 30 | } 31 | 32 | var metricLabels = []VulnerabilityLabel{ 33 | { 34 | Name: "report_name", 35 | Groups: []string{LabelGroupAll, labelGroupSummary}, 36 | Scope: FieldScopeReport, 37 | }, 38 | { 39 | Name: "image_namespace", 40 | Groups: []string{LabelGroupAll, labelGroupSummary}, 41 | Scope: FieldScopeReport, 42 | }, 43 | { 44 | Name: "image_registry", 45 | Groups: []string{LabelGroupAll, labelGroupSummary}, 46 | Scope: FieldScopeReport, 47 | }, 48 | { 49 | Name: "image_repository", 50 | Groups: []string{LabelGroupAll, labelGroupSummary}, 51 | Scope: FieldScopeReport, 52 | }, 53 | { 54 | Name: "image_tag", 55 | Groups: []string{LabelGroupAll, labelGroupSummary}, 56 | Scope: FieldScopeReport, 57 | }, 58 | { 59 | Name: "image_digest", 60 | Groups: []string{LabelGroupAll, labelGroupSummary}, 61 | Scope: FieldScopeReport, 62 | }, 63 | { 64 | Name: "severity", 65 | // Note - Summary metrics use a different severity field than per-vulnerability severity. 66 | Groups: []string{LabelGroupAll, labelGroupSummary}, 67 | Scope: FieldScopeVulnerability, 68 | }, 69 | { 70 | Name: "vulnerability_id", 71 | Groups: []string{LabelGroupAll}, 72 | Scope: FieldScopeVulnerability, 73 | }, 74 | { 75 | Name: "vulnerable_resource_name", 76 | Groups: []string{LabelGroupAll}, 77 | Scope: FieldScopeVulnerability, 78 | }, 79 | { 80 | Name: "installed_resource_version", 81 | Groups: []string{LabelGroupAll}, 82 | Scope: FieldScopeVulnerability, 83 | }, 84 | { 85 | Name: "fixed_resource_version", 86 | Groups: []string{LabelGroupAll}, 87 | Scope: FieldScopeVulnerability, 88 | }, 89 | { 90 | Name: "vulnerability_title", 91 | Groups: []string{LabelGroupAll}, 92 | Scope: FieldScopeVulnerability, 93 | }, 94 | { 95 | Name: "vulnerability_link", 96 | Groups: []string{LabelGroupAll}, 97 | Scope: FieldScopeVulnerability, 98 | }, 99 | } 100 | 101 | func LabelWithName(name string) (label VulnerabilityLabel, ok bool) { 102 | for _, label := range metricLabels { 103 | if label.Name == name { 104 | return label, true 105 | } 106 | } 107 | return VulnerabilityLabel{}, false 108 | } 109 | 110 | func LabelsForGroup(group string) []VulnerabilityLabel { 111 | l := []VulnerabilityLabel{} 112 | for _, label := range metricLabels { 113 | if utils.SliceContains(label.Groups, group) { 114 | l = append(l, label) 115 | } 116 | } 117 | return l 118 | } 119 | 120 | func labelNamesForGroup(group string) []string { 121 | l := []string{} 122 | for _, label := range metricLabels { 123 | if utils.SliceContains(label.Groups, group) { 124 | l = append(l, label.Name) 125 | } 126 | } 127 | return l 128 | } 129 | 130 | func LabelNamesForList(list []VulnerabilityLabel) []string { 131 | l := []string{} 132 | for _, label := range list { 133 | l = append(l, label.Name) 134 | } 135 | return l 136 | } 137 | 138 | // Gauge for the count of all vulnerabilities of a particular severity contained in an image. 139 | var ( 140 | VulnerabilitySummary = prometheus.NewGaugeVec( 141 | prometheus.GaugeOpts{ 142 | Namespace: metricNamespace, 143 | Subsystem: metricSubsystem, 144 | Name: "image_vulnerability_severity_count", 145 | Help: "Exposes the number of vulnerabilities of a particular severity per-image.", 146 | }, 147 | labelNamesForGroup(labelGroupSummary), 148 | ) 149 | ) 150 | 151 | // Gauge reporting the score of each CVE present in an image. 152 | // Registered during first reconcile loop in registerMetrics(). 153 | var VulnerabilityInfo *prometheus.GaugeVec 154 | 155 | func init() { 156 | // Register custom metrics with the global prometheus registry 157 | metrics.Registry.MustRegister(VulnerabilitySummary) // VulnerabilityInfo 158 | } 159 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | architect: giantswarm/architect@6.11.0 4 | 5 | workflows: 6 | package-and-push-chart-on-tag: 7 | jobs: 8 | - architect/go-build: 9 | name: go-build 10 | binary: starboard-exporter 11 | resource_class: large 12 | filters: 13 | tags: 14 | only: /^v.*/ 15 | 16 | - architect/push-to-registries: 17 | context: architect 18 | name: push-to-registries 19 | requires: 20 | - go-build 21 | filters: 22 | tags: 23 | only: /^v.*/ 24 | branches: 25 | ignore: 26 | - main 27 | - master 28 | 29 | - architect/push-to-app-catalog: 30 | context: architect 31 | executor: app-build-suite 32 | name: push-starboard-exporter-to-control-plane-catalog 33 | app_catalog: control-plane-catalog 34 | app_catalog_test: control-plane-test-catalog 35 | chart: starboard-exporter 36 | requires: 37 | - push-to-registries 38 | filters: 39 | tags: 40 | only: /^v.*/ 41 | branches: 42 | ignore: 43 | - main 44 | - master 45 | 46 | - architect/push-to-app-catalog: 47 | context: architect 48 | executor: app-build-suite 49 | name: push-starboard-exporter-to-giantswarm-catalog 50 | app_catalog: giantswarm-catalog 51 | app_catalog_test: giantswarm-test-catalog 52 | chart: starboard-exporter 53 | persist_chart_archive: true 54 | requires: 55 | - push-to-registries 56 | filters: 57 | tags: 58 | only: /^v.*/ 59 | # Run app-test-suite tests. 60 | branches: 61 | ignore: 62 | - main 63 | - master 64 | 65 | - architect/run-tests-with-ats: 66 | name: execute chart tests 67 | filters: 68 | # Do not trigger the job on merge to main. 69 | branches: 70 | ignore: 71 | - main 72 | requires: 73 | - push-starboard-exporter-to-giantswarm-catalog 74 | 75 | - architect/push-to-app-collection: 76 | context: architect 77 | name: push-starboard-exporter-to-capa-app-collection 78 | app_name: starboard-exporter 79 | app_collection_repo: capa-app-collection 80 | requires: 81 | - push-starboard-exporter-to-control-plane-catalog 82 | filters: 83 | # Only do this when a new tag is created. 84 | branches: 85 | ignore: /.*/ 86 | tags: 87 | only: /^v.*/ 88 | 89 | - architect/push-to-app-collection: 90 | context: architect 91 | name: push-starboard-exporter-to-capz-app-collection 92 | app_name: starboard-exporter 93 | app_collection_repo: capz-app-collection 94 | requires: 95 | - push-starboard-exporter-to-control-plane-catalog 96 | filters: 97 | # Only do this when a new tag is created. 98 | branches: 99 | ignore: /.*/ 100 | tags: 101 | only: /^v.*/ 102 | 103 | - architect/push-to-app-collection: 104 | context: architect 105 | name: push-starboard-exporter-to-cloud-director-app-collection 106 | app_name: starboard-exporter 107 | app_collection_repo: cloud-director-app-collection 108 | requires: 109 | - push-starboard-exporter-to-control-plane-catalog 110 | filters: 111 | # Only do this when a new tag is created. 112 | branches: 113 | ignore: /.*/ 114 | tags: 115 | only: /^v.*/ 116 | 117 | - architect/push-to-app-collection: 118 | context: architect 119 | name: push-starboard-exporter-to-proxmox-app-collection 120 | app_name: starboard-exporter 121 | app_collection_repo: proxmox-app-collection 122 | requires: 123 | - push-starboard-exporter-to-control-plane-catalog 124 | filters: 125 | # Only do this when a new tag is created. 126 | branches: 127 | ignore: /.*/ 128 | tags: 129 | only: /^v.*/ 130 | 131 | - architect/push-to-app-collection: 132 | context: architect 133 | name: push-starboard-exporter-to-vsphere-app-collection 134 | app_name: starboard-exporter 135 | app_collection_repo: vsphere-app-collection 136 | requires: 137 | - push-starboard-exporter-to-control-plane-catalog 138 | filters: 139 | # Only do this when a new tag is created. 140 | branches: 141 | ignore: /.*/ 142 | tags: 143 | only: /^v.*/ 144 | -------------------------------------------------------------------------------- /utils/sharding_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/go-logr/logr/testr" 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/google/go-cmp/cmp/cmpopts" 10 | "gotest.tools/assert" 11 | corev1 "k8s.io/api/core/v1" 12 | ) 13 | 14 | func Test_getEndpointChanges(t *testing.T) { 15 | testCases := []struct { 16 | name string 17 | current *corev1.Endpoints 18 | previous *corev1.Endpoints 19 | expectedAdded []string 20 | expectedKept []string 21 | expectedRemoved []string 22 | }{ 23 | { 24 | name: "add one new endpoint with no previous state", 25 | current: &corev1.Endpoints{ 26 | Subsets: []corev1.EndpointSubset{ 27 | { 28 | Addresses: []corev1.EndpointAddress{ 29 | { 30 | IP: "1.2.3.4", 31 | }, 32 | }, 33 | }, 34 | }, 35 | }, 36 | expectedAdded: []string{"1.2.3.4"}, 37 | expectedKept: []string{}, 38 | expectedRemoved: []string{}, 39 | }, 40 | { 41 | name: "add one new endpoint to one previous endpoint", 42 | current: &corev1.Endpoints{ 43 | Subsets: []corev1.EndpointSubset{ 44 | { 45 | Addresses: []corev1.EndpointAddress{ 46 | { 47 | IP: "1.2.3.4", 48 | }, 49 | { 50 | IP: "5.6.7.8", 51 | }, 52 | }, 53 | }, 54 | }, 55 | }, 56 | previous: &corev1.Endpoints{ 57 | Subsets: []corev1.EndpointSubset{ 58 | { 59 | Addresses: []corev1.EndpointAddress{ 60 | { 61 | IP: "1.2.3.4", 62 | }, 63 | }, 64 | }, 65 | }, 66 | }, 67 | expectedAdded: []string{"5.6.7.8"}, 68 | expectedKept: []string{"1.2.3.4"}, 69 | expectedRemoved: []string{}, 70 | }, 71 | { 72 | name: "add multiple new endpoints to two previous endpoints", 73 | current: &corev1.Endpoints{ 74 | Subsets: []corev1.EndpointSubset{ 75 | { 76 | Addresses: []corev1.EndpointAddress{ 77 | { 78 | IP: "8.8.8.8", 79 | }, 80 | { 81 | IP: "8.8.4.4", 82 | }, 83 | { 84 | IP: "1.2.3.4", 85 | }, 86 | { 87 | IP: "5.6.7.8", 88 | }, 89 | }, 90 | }, 91 | }, 92 | }, 93 | previous: &corev1.Endpoints{ 94 | Subsets: []corev1.EndpointSubset{ 95 | { 96 | Addresses: []corev1.EndpointAddress{ 97 | { 98 | IP: "1.2.3.4", 99 | }, 100 | { 101 | IP: "5.6.7.8", 102 | }, 103 | }, 104 | }, 105 | }, 106 | }, 107 | expectedAdded: []string{"8.8.4.4", "8.8.8.8"}, 108 | expectedKept: []string{"1.2.3.4", "5.6.7.8"}, 109 | expectedRemoved: []string{}, 110 | }, 111 | { 112 | name: "remove multiple endpoints", 113 | current: &corev1.Endpoints{ 114 | Subsets: []corev1.EndpointSubset{ 115 | { 116 | Addresses: []corev1.EndpointAddress{ 117 | { 118 | IP: "8.8.8.8", 119 | }, 120 | { 121 | IP: "1.2.3.4", 122 | }, 123 | }, 124 | }, 125 | }, 126 | }, 127 | previous: &corev1.Endpoints{ 128 | Subsets: []corev1.EndpointSubset{ 129 | { 130 | Addresses: []corev1.EndpointAddress{ 131 | { 132 | IP: "8.8.8.8", 133 | }, 134 | { 135 | IP: "8.8.4.4", 136 | }, 137 | { 138 | IP: "1.2.3.4", 139 | }, 140 | { 141 | IP: "5.6.7.8", 142 | }, 143 | }, 144 | }, 145 | }, 146 | }, 147 | expectedAdded: []string{}, 148 | expectedKept: []string{"1.2.3.4", "8.8.8.8"}, 149 | expectedRemoved: []string{"5.6.7.8", "8.8.4.4"}, 150 | }, 151 | { 152 | name: "add and remove endpoints in one udpate", 153 | current: &corev1.Endpoints{ 154 | Subsets: []corev1.EndpointSubset{ 155 | { 156 | Addresses: []corev1.EndpointAddress{ 157 | { 158 | IP: "8.8.4.4", 159 | }, 160 | { 161 | IP: "1.2.3.4", 162 | }, 163 | { 164 | IP: "5.6.7.8", 165 | }, 166 | }, 167 | }, 168 | }, 169 | }, 170 | previous: &corev1.Endpoints{ 171 | Subsets: []corev1.EndpointSubset{ 172 | { 173 | Addresses: []corev1.EndpointAddress{ 174 | { 175 | IP: "8.8.8.8", 176 | }, 177 | { 178 | IP: "1.2.3.4", 179 | }, 180 | }, 181 | }, 182 | }, 183 | }, 184 | expectedAdded: []string{"5.6.7.8", "8.8.4.4"}, 185 | expectedKept: []string{"1.2.3.4"}, 186 | expectedRemoved: []string{"8.8.8.8"}, 187 | }, 188 | } 189 | 190 | // Logger to pass to helper functions. Wraps testing.T. 191 | log := testr.New(t) 192 | 193 | for i, tc := range testCases { 194 | t.Run(strconv.Itoa(i), func(t *testing.T) { 195 | var previous *corev1.Endpoints 196 | { 197 | previous = nil 198 | if tc.previous != nil { 199 | previous = tc.previous 200 | } 201 | } 202 | 203 | // Calculate endpoint updates. 204 | added, kept, removed, ok := getEndpointChanges(tc.current, previous, log) 205 | 206 | t.Logf("case %v: added: %v, kept: %v, removed: %v\n", tc, added, kept, removed) 207 | 208 | if !ok { 209 | t.Fatalf("unable to parse endpoint changes for case %v: added: %s, kept: %s, removed: %s\n", tc, added, kept, removed) 210 | } 211 | 212 | compareStringFunc := func(a, b string) bool { return a < b } 213 | 214 | // Check added, kept, and removed contain the expected items, ignoring order. 215 | assert.Assert(t, cmp.Equal(tc.expectedAdded, added, cmpopts.EquateEmpty(), cmpopts.SortSlices(compareStringFunc)), "test case %v failed.", tc.name) 216 | assert.Assert(t, cmp.Equal(tc.expectedKept, kept, cmpopts.EquateEmpty(), cmpopts.SortSlices(compareStringFunc)), "test case %v failed.", tc.name) 217 | assert.Assert(t, cmp.Equal(tc.expectedRemoved, removed, cmpopts.EquateEmpty(), cmpopts.SortSlices(compareStringFunc)), "test case %v failed.", tc.name) 218 | }) 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/giantswarm/starboard-exporter 2 | 3 | go 1.24.4 4 | 5 | toolchain go1.25.4 6 | 7 | require ( 8 | github.com/aquasecurity/trivy-operator v0.28.0 9 | github.com/buraksezer/consistent v0.10.0 10 | github.com/cespare/xxhash/v2 v2.3.0 11 | github.com/go-logr/logr v1.4.3 12 | github.com/google/go-cmp v0.7.0 13 | github.com/pkg/errors v0.9.1 14 | github.com/prometheus/client_golang v1.23.2 15 | gotest.tools v2.2.0+incompatible 16 | k8s.io/api v0.34.1 17 | k8s.io/apimachinery v0.34.1 18 | k8s.io/client-go v0.34.1 19 | sigs.k8s.io/controller-runtime v0.22.4 20 | ) 21 | 22 | require ( 23 | github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce // indirect 24 | github.com/aquasecurity/go-npm-version v0.0.2 // indirect 25 | github.com/aquasecurity/go-pep440-version v0.0.1 // indirect 26 | github.com/aquasecurity/go-version v0.0.1 // indirect 27 | github.com/aquasecurity/table v1.11.0 // indirect 28 | github.com/aquasecurity/tml v0.6.1 // indirect 29 | github.com/aquasecurity/trivy v0.65.0 // indirect 30 | github.com/aquasecurity/trivy-checks v1.11.3-0.20250604022615-9a7efa7c9169 // indirect 31 | github.com/aquasecurity/trivy-db v0.0.0-20250723062229-56ec1e482238 // indirect 32 | github.com/beorn7/perks v1.0.1 // indirect 33 | github.com/bitnami/go-version v0.0.0-20231130084017-bb00604d650c // indirect 34 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 35 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 36 | github.com/evanphx/json-patch/v5 v5.9.11 // indirect 37 | github.com/fatih/color v1.18.0 // indirect 38 | github.com/fsnotify/fsnotify v1.9.0 // indirect 39 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 40 | github.com/go-logr/zapr v1.3.0 // indirect 41 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 42 | github.com/go-openapi/jsonreference v0.21.0 // indirect 43 | github.com/go-openapi/swag v0.23.1 // indirect 44 | github.com/gogo/protobuf v1.3.2 // indirect 45 | github.com/google/btree v1.1.3 // indirect 46 | github.com/google/gnostic-models v0.7.0 // indirect 47 | github.com/google/go-containerregistry v0.20.6 // indirect 48 | github.com/google/uuid v1.6.0 // indirect 49 | github.com/hashicorp/errwrap v1.1.0 // indirect 50 | github.com/hashicorp/go-multierror v1.1.1 // indirect 51 | github.com/josharian/intern v1.0.0 // indirect 52 | github.com/json-iterator/go v1.1.12 // indirect 53 | github.com/mailru/easyjson v0.9.0 // indirect 54 | github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a // indirect 55 | github.com/mattn/go-colorable v0.1.14 // indirect 56 | github.com/mattn/go-isatty v0.0.20 // indirect 57 | github.com/mattn/go-runewidth v0.0.16 // indirect 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 59 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 60 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 61 | github.com/oklog/ulid/v2 v2.1.1 // indirect 62 | github.com/package-url/packageurl-go v0.1.3 // indirect 63 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 64 | github.com/prometheus/client_model v0.6.2 // indirect 65 | github.com/prometheus/common v0.66.1 // indirect 66 | github.com/prometheus/procfs v0.16.1 // indirect 67 | github.com/rivo/uniseg v0.4.7 // indirect 68 | github.com/samber/lo v1.51.0 // indirect 69 | github.com/samber/oops v1.18.1 // indirect 70 | github.com/spf13/pflag v1.0.7 // indirect 71 | github.com/stretchr/objx v0.5.2 // indirect 72 | github.com/stretchr/testify v1.11.1 // indirect 73 | github.com/x448/float16 v0.8.4 // indirect 74 | github.com/xlab/treeprint v1.2.0 // indirect 75 | go.etcd.io/bbolt v1.4.2 // indirect 76 | go.opentelemetry.io/otel v1.36.0 // indirect 77 | go.opentelemetry.io/otel/trace v1.36.0 // indirect 78 | go.uber.org/multierr v1.11.0 // indirect 79 | go.uber.org/zap v1.27.0 // indirect 80 | go.yaml.in/yaml/v2 v2.4.2 // indirect 81 | go.yaml.in/yaml/v3 v3.0.4 // indirect 82 | golang.org/x/net v0.43.0 // indirect 83 | golang.org/x/oauth2 v0.30.0 // indirect 84 | golang.org/x/sync v0.16.0 // indirect 85 | golang.org/x/sys v0.35.0 // indirect 86 | golang.org/x/term v0.34.0 // indirect 87 | golang.org/x/text v0.28.0 // indirect 88 | golang.org/x/time v0.12.0 // indirect 89 | golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect 90 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 91 | google.golang.org/protobuf v1.36.8 // indirect 92 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 93 | gopkg.in/inf.v0 v0.9.1 // indirect 94 | gopkg.in/yaml.v3 v3.0.1 // indirect 95 | k8s.io/apiextensions-apiserver v0.34.1 // indirect 96 | k8s.io/klog/v2 v2.130.1 // indirect 97 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect 98 | k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect 99 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 100 | sigs.k8s.io/randfill v1.0.0 // indirect 101 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 102 | sigs.k8s.io/yaml v1.6.0 // indirect 103 | ) 104 | 105 | replace ( 106 | github.com/go-git/go-git/v5 v5.10.1 => github.com/go-git/go-git/v5 v5.11.0 107 | github.com/go-git/go-git/v5 v5.12.0 => github.com/go-git/go-git/v5 v5.13.2 108 | ) 109 | 110 | replace go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.42.0 => go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 111 | 112 | replace github.com/moby/buildkit v0.12.3 => github.com/moby/buildkit v0.12.5 113 | 114 | replace github.com/hashicorp/go-getter v1.7.4 => github.com/hashicorp/go-getter v1.7.6 115 | 116 | replace github.com/open-policy-agent/opa v0.65.0 => github.com/open-policy-agent/opa v0.70.0 117 | 118 | replace golang.org/x/crypto v0.28.0 => golang.org/x/crypto v0.36.0 119 | 120 | replace ( 121 | github.com/containerd/containerd v1.7.23 => github.com/containerd/containerd v1.7.27 122 | github.com/containerd/containerd v1.7.25 => github.com/containerd/containerd v1.7.27 123 | ) 124 | 125 | replace helm.sh/helm/v3 v3.17.1 => helm.sh/helm/v3 v3.17.3 126 | -------------------------------------------------------------------------------- /controllers/configauditreport/configauditreport_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package configauditreport 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | 23 | aqua "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1" 24 | "github.com/go-logr/logr" 25 | "github.com/pkg/errors" 26 | "github.com/prometheus/client_golang/prometheus" 27 | apierrors "k8s.io/apimachinery/pkg/api/errors" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | apitypes "k8s.io/apimachinery/pkg/types" 30 | ctrl "sigs.k8s.io/controller-runtime" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 33 | "sigs.k8s.io/controller-runtime/pkg/log" 34 | 35 | "github.com/giantswarm/starboard-exporter/controllers" 36 | "github.com/giantswarm/starboard-exporter/utils" 37 | ) 38 | 39 | const ( 40 | ConfigAuditReportFinalizer = "starboard-exporter.giantswarm.io/configauditreport" 41 | ) 42 | 43 | // ConfigAuditReportReconciler reconciles a ConfigAuditReport object 44 | type ConfigAuditReportReconciler struct { 45 | client.Client 46 | Log logr.Logger 47 | Scheme *runtime.Scheme 48 | 49 | MaxJitterPercent int 50 | ShardHelper *utils.ShardHelper 51 | } 52 | 53 | // +kubebuilder:rbac:groups=aquasecurity.github.io.giantswarm,resources=configauditreports,verbs=get;list;watch;create;update;patch;delete 54 | // +kubebuilder:rbac:groups=aquasecurity.github.io.giantswarm,resources=configauditreports/status,verbs=get;update;patch 55 | // +kubebuilder:rbac:groups=aquasecurity.github.io.giantswarm,resources=configauditreports/finalizers,verbs=update 56 | func (r *ConfigAuditReportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 57 | _ = log.FromContext(ctx) 58 | _ = r.Log.WithValues("configauditreport", req.NamespacedName) 59 | 60 | deletedSummaries := ConfigAuditSummary.DeletePartialMatch(prometheus.Labels{"report_name": req.String()}) 61 | 62 | shouldOwn := r.ShardHelper.ShouldOwn(req.String()) 63 | if shouldOwn { 64 | 65 | // Try to get the report. It might not exist anymore, in which case we don't need to do anything. 66 | report := &aqua.ConfigAuditReport{} 67 | if err := r.Get(ctx, req.NamespacedName, report); err != nil { 68 | if apierrors.IsNotFound(err) { 69 | // Most likely the report was deleted. 70 | return ctrl.Result{}, nil 71 | } 72 | 73 | // Error reading the object. 74 | r.Log.Error(err, "Unable to read report") 75 | return ctrl.Result{}, err 76 | } 77 | 78 | r.Log.Info(fmt.Sprintf("Reconciled %s || Found (C/H/M/L): %d/%d/%d/%d", 79 | req.NamespacedName, 80 | report.Report.Summary.CriticalCount, 81 | report.Report.Summary.HighCount, 82 | report.Report.Summary.MediumCount, 83 | report.Report.Summary.LowCount, 84 | )) 85 | 86 | // Publish summary and CVE metrics for this report. 87 | publishSummaryMetrics(report) 88 | 89 | if utils.SliceContains(report.GetFinalizers(), ConfigAuditReportFinalizer) { 90 | // Remove the finalizer if we're the shard owner. 91 | ctrlutil.RemoveFinalizer(report, ConfigAuditReportFinalizer) 92 | if err := r.Update(ctx, report); err != nil { 93 | return ctrl.Result{}, err 94 | } 95 | } 96 | 97 | // Add a label to this report so any previous owners will reconcile and drop the metric. 98 | report.Labels[controllers.ShardOwnerLabel] = r.ShardHelper.PodIP 99 | err := r.Update(ctx, report, &client.UpdateOptions{}) 100 | if err != nil { 101 | r.Log.Error(err, "unable to add shard owner label") 102 | } 103 | } else { 104 | if deletedSummaries > 0 { 105 | r.Log.Info(fmt.Sprintf("cleared %d summary metrics", deletedSummaries)) 106 | } 107 | } 108 | 109 | return utils.JitterRequeue(controllers.DefaultRequeueDuration, r.MaxJitterPercent, r.Log), nil 110 | } 111 | 112 | // SetupWithManager sets up the controller with the Manager. 113 | func (r *ConfigAuditReportReconciler) SetupWithManager(mgr ctrl.Manager) error { 114 | err := ctrl.NewControllerManagedBy(mgr). 115 | For(&aqua.ConfigAuditReport{}). 116 | Complete(r) 117 | if err != nil { 118 | return errors.Wrap(err, "failed setting up controller with controller manager") 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func RequeueReportsForPod(c client.Client, log logr.Logger, podIP string) { 125 | reportList := &aqua.ConfigAuditReportList{} 126 | opts := []client.ListOption{ 127 | client.MatchingLabels{controllers.ShardOwnerLabel: podIP}, 128 | } 129 | 130 | // Get the list of reports with our label. 131 | err := c.List(context.Background(), reportList, opts...) 132 | if err != nil { 133 | log.Error(err, "unable to fetch configauditreport") 134 | } 135 | 136 | for _, r := range reportList.Items { 137 | // Retrieve the individual report. 138 | log.Info(fmt.Sprintf("re-queueing configauditreport %s", r.Name)) 139 | report := &aqua.ConfigAuditReport{} 140 | err := c.Get(context.Background(), client.ObjectKey{Name: r.Name, Namespace: r.Namespace}, report) 141 | if err != nil { 142 | log.Error(err, "unable to fetch configauditreport") 143 | } 144 | 145 | // Clear the shard-owner label if it still has our label 146 | if r.Labels[controllers.ShardOwnerLabel] == podIP { 147 | r.Labels[controllers.ShardOwnerLabel] = "" 148 | err = c.Update(context.Background(), report, &client.UpdateOptions{}) 149 | if err != nil { 150 | log.Error(err, fmt.Sprintf("unable to remove %s label", controllers.ShardOwnerLabel)) 151 | } 152 | } 153 | } 154 | } 155 | 156 | func getCountPerSeverity(report *aqua.ConfigAuditReport) map[string]float64 { 157 | // Format is e.g. {CRITICAL: 10}. 158 | return map[string]float64{ 159 | string(aqua.SeverityCritical): float64(report.Report.Summary.CriticalCount), 160 | string(aqua.SeverityHigh): float64(report.Report.Summary.HighCount), 161 | string(aqua.SeverityMedium): float64(report.Report.Summary.MediumCount), 162 | string(aqua.SeverityLow): float64(report.Report.Summary.LowCount), 163 | } 164 | } 165 | 166 | func publishSummaryMetrics(report *aqua.ConfigAuditReport) { 167 | summaryValues := valuesForReport(report, metricLabels) 168 | 169 | // Add the severity label after the standard labels and expose each severity metric. 170 | for severity, count := range getCountPerSeverity(report) { 171 | v := summaryValues 172 | v["severity"] = severity 173 | 174 | // Expose the metric. 175 | ConfigAuditSummary.With( 176 | v, 177 | ).Set(count) 178 | } 179 | } 180 | 181 | func valuesForReport(report *aqua.ConfigAuditReport, labels []string) map[string]string { 182 | result := map[string]string{} 183 | for _, label := range labels { 184 | result[label] = reportValueFor(label, report) 185 | } 186 | return result 187 | } 188 | 189 | func reportValueFor(field string, report *aqua.ConfigAuditReport) string { 190 | switch field { 191 | case "report_name": 192 | return apitypes.NamespacedName{Name: report.Name, Namespace: report.Namespace}.String() 193 | case "resource_name": 194 | return report.Name 195 | case "resource_namespace": 196 | return report.Namespace 197 | case "severity": 198 | return "" // this value will be overwritten in publishSummaryMetrics 199 | default: 200 | // Error? 201 | return "" 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/giantswarm/starboard-exporter.svg?style=shield)](https://circleci.com/gh/giantswarm/starboard-exporter) 2 | [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/giantswarm/starboard-exporter/badge)](https://securityscorecards.dev/viewer/?uri=github.com/giantswarm/starboard-exporter) 3 | 4 | # starboard-exporter 5 | 6 | Exposes Prometheus metrics from [Trivy Operator][trivy-operator-upstream]'s `VulnerabilityReport`, `ConfigAuditReport`, and other custom resources (CRs). 7 | 8 | ## Metrics 9 | 10 | This exporter exposes several types of metrics: 11 | 12 | ### CIS Benchmarks 13 | 14 | #### Report Summary 15 | 16 | A report summary series exposes the count of checks of each status reported in a given `CISKubeBenchReport`. For example: 17 | 18 | ```shell 19 | starboard_exporter_ciskubebenchreport_report_summary_count{ 20 | node_name="bj56o-master-bj56o-000000" 21 | status="FAIL" 22 | } 31 23 | ``` 24 | 25 | #### Section Summary 26 | 27 | For slightly more granular reporting, a section summary series exposes the count of checks of each status reported in a given `CISKubeBenchSection`. For example: 28 | 29 | ```shell 30 | starboard_exporter_ciskubebenchreport_section_summary_count{ 31 | node_name="bj56o-master-bj56o-000000" 32 | node_type="controlplane" 33 | section_name="Control Plane Configuration" 34 | status="WARN" 35 | } 4 36 | ``` 37 | 38 | #### Result Detail 39 | 40 | A CIS benchmark result info series exposes fields from each instance of an Aqua `CISKubeBenchResult`. For example: 41 | 42 | ```shell 43 | starboard_exporter_ciskubebenchreport_result_info{ 44 | node_name="bj56o-master-bj56o-000000" 45 | node_type="controlplane" 46 | pod="starboard-exporter-859955f485-cwkj6" 47 | section_name="Control Plane Configuration" 48 | test_desc="Client certificate authentication should not be used for users (Manual)" 49 | test_number="3.1.1" 50 | test_status="WARN" 51 | } 1 52 | ``` 53 | 54 | ### Vulnerability Reports 55 | 56 | #### Report Summary 57 | 58 | A summary series exposes the count of CVEs of each severity reported in a given `VulnerabilityReport`. For example: 59 | 60 | ```shell 61 | starboard_exporter_vulnerabilityreport_image_vulnerability_severity_count{ 62 | image_digest="", 63 | image_namespace="demo", 64 | image_registry="quay.io", 65 | image_repository="giantswarm/starboard-operator", 66 | image_tag="0.11.0", 67 | report_name="replicaset-starboard-app-6894945788-starboard-app", 68 | severity="MEDIUM" 69 | } 4 70 | ``` 71 | 72 | This indicates that the `giantswarm/starboard-operator` image in the `demo` namespace contains 4 medium-severity vulnerabilities. 73 | 74 | #### Vulnerability Details 75 | 76 | A "detail" or "vulnerability" series exposes fields from each instance of an Aqua `Vulnerability`. The value of the metric is the `Score` for the vulnerability. For example: 77 | 78 | ```shell 79 | starboard_exporter_vulnerabilityreport_image_vulnerability{ 80 | fixed_resource_version="1.1.1l-r0", 81 | image_digest="", 82 | image_namespace="demo", 83 | image_registry="quay.io", 84 | image_repository="giantswarm/starboard-operator", 85 | image_tag="0.11.0", 86 | installed_resource_version="1.1.1k-r0", 87 | report_name="replicaset-starboard-app-6894945788-starboard-app", 88 | severity="HIGH", 89 | vulnerability_id="CVE-2021-3712", 90 | vulnerability_link="https://avd.aquasec.com/nvd/cve-2021-3712", 91 | vulnerability_title="openssl: Read buffer overruns processing ASN.1 strings", 92 | vulnerable_resource_name="libssl1.1" 93 | } 7.4 94 | ``` 95 | 96 | This indicates that the vulnerability with the id `CVE-2021-3712` was found in the `giantswarm/starboard-operator` image in the `demo` namespace, and it has a CVSS 3.x score of 7.4. 97 | 98 | An additional series would be exposed for every combination of those labels. 99 | 100 | ### Config Audit Reports 101 | 102 | #### Report Summary 103 | 104 | A summary series exposes the count of checks of each severity reported in a given `ConfigAuditReport`. For example: 105 | 106 | ```shell 107 | starboard_exporter_configauditreport_resource_checks_summary_count{ 108 | resource_name="replicaset-chart-operator-748f756847", 109 | resource_namespace="giantswarm", 110 | severity="LOW" 111 | } 7 112 | ``` 113 | 114 | #### A Note on Cardinality 115 | 116 | For some use cases, it is helpful to export additional fields from `VulnerabilityReport` CRs. However, because many fields contain unbounded arbitrary data, including them in Prometheus metrics can lead to extremely high cardinality. This can drastically impact Prometheus performance. For this reason, we only expose summary data by default and allow users to opt-in to higher-cardinality fields. 117 | 118 | ### Sharding Reports 119 | 120 | In large clusters or environments with many reports and/or vulnerabilities, a single exporter can consume a large amount of memory, and Prometheus may need a long time to scrape the exporter, leading to scrape timeouts. To help spread resource consumption and scrape effort, `starboard-exporter` watches its own service endpoints and will shard metrics for all report types across the available endpoints. In other words, if there are 3 exporter instances, each instance will serve roughly 1/3 of the metrics. This behavior is enabled by default and does not require any additional configuration. To use it, simply change the number of replicas in the Deployment. However, you should read the section on cardinality and be aware that consuming large amounts of high-cardinality data can have performance impacts on Prometheus. 121 | 122 | ## Customization 123 | 124 | Summary metrics of the format described above are always enabled. 125 | 126 | To enable an additional detail series *per Vulnerability*, use the `--target-labels` flag to specify which labels should be exposed. For example: 127 | 128 | ```shell 129 | # Expose only select image and CVE fields. 130 | --target-labels=image_namespace,image_repository,image_tag,vulnerability_id 131 | 132 | # Run with (almost) all fields exposed as labels, if you're feeling really wild. 133 | --target-labels=all 134 | ``` 135 | 136 | Target labels can also be set via Helm values: 137 | 138 | ```yaml 139 | exporter: 140 | vulnerabilityReports: 141 | targetLabels: 142 | - image_namespace 143 | - image_repository 144 | - image_tag 145 | - vulnerability_id 146 | - ... 147 | ``` 148 | 149 | The same can be done for CIS Benchmark Results. To enable an additional detail series *per CIS Benchmark Result*, use the `--cis-detail-report-labels` flag to specify which labels should be exposed. For example: 150 | 151 | ```shell 152 | # Expose only section_name, test_name and test_status 153 | --cis-detail-report-labels=section_name,test_name,test_status 154 | 155 | # Run with (almost) all fields exposed as labels. 156 | --cis-detail-report-labels=all 157 | ``` 158 | 159 | CIS detail target labels can also be set via Helm values: 160 | 161 | ```yaml 162 | exporter: 163 | CISKubeBenchReports: 164 | targetLabels: 165 | - node_name 166 | - node_type 167 | - section_name 168 | - test_name 169 | - test_status 170 | - ... 171 | ``` 172 | 173 | ## Helm 174 | 175 | How to install the starboard-exporter using helm: 176 | 177 | ```shell 178 | helm repo add giantswarm https://giantswarm.github.io/giantswarm-catalog 179 | helm repo update 180 | helm upgrade -i starboard-exporter --namespace giantswarm/starboard-exporter 181 | ``` 182 | 183 | ## Scaling for Prometheus scrape timeouts 184 | 185 | When exporting a large volume of metrics, Prometheus might time out before retrieving them all from a single exporter instance. It is possible to automatically scale the number of exporters to keep the scrape time below the configured timeout. To enable HPA scaling based on Prometheus metrics, [here](./docs/custom_metrics_hpa.md) 186 | 187 | 188 | [trivy-operator-upstream]: https://github.com/aquasecurity/trivy-operator 189 | -------------------------------------------------------------------------------- /utils/sharding.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | 8 | "github.com/buraksezer/consistent" 9 | "github.com/go-logr/logr" 10 | corev1 "k8s.io/api/core/v1" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | "k8s.io/apimachinery/pkg/runtime" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | "k8s.io/client-go/dynamic" 16 | "k8s.io/client-go/dynamic/dynamicinformer" 17 | "k8s.io/client-go/tools/cache" 18 | ctrl "sigs.k8s.io/controller-runtime" 19 | ) 20 | 21 | type ShardHelper struct { 22 | PodIP string 23 | ServiceName string 24 | ServiceNamespace string 25 | mu *sync.RWMutex 26 | ring *consistent.Consistent 27 | } 28 | 29 | // Returns the number of members/peers currently in the hash ring. 30 | func (r *ShardHelper) MemberCount() int { 31 | r.mu.RLock() 32 | defer r.mu.RUnlock() 33 | return len(r.ring.GetMembers()) 34 | } 35 | 36 | // Returns the name (IP) of the shard which should own a provided object name. 37 | func (r *ShardHelper) GetShardOwner(input string) string { 38 | r.mu.RLock() 39 | defer r.mu.RUnlock() 40 | return r.ring.LocateKey([]byte(input)).String() 41 | } 42 | 43 | // Returns whether the current shard should own the object with the provided name. 44 | func (r *ShardHelper) ShouldOwn(input string) bool { 45 | return r.GetShardOwner(input) == r.PodIP 46 | } 47 | 48 | // SetMembers accepts a map where the keys are member IPs and uses those IPs as the members for sharding. 49 | func (r *ShardHelper) SetMembers(newMembers map[string]struct{}) { 50 | r.mu.Lock() 51 | defer r.mu.Unlock() 52 | 53 | // Add new members. Add returns immediately if the member already exists. 54 | for newMember := range newMembers { 55 | r.ring.Add(peer(newMember)) 56 | } 57 | 58 | // Remove members which don't exist anymore. 59 | for _, oldMember := range r.ring.GetMembers() { 60 | if _, ok := newMembers[oldMember.String()]; !ok { 61 | r.ring.Remove(oldMember.String()) 62 | } 63 | } 64 | } 65 | 66 | // SetMembersFromLists is a wrapper around SetMember which accepts slices instead of a map. 67 | func (r *ShardHelper) SetMembersFromLists(lists ...[]string) { 68 | members := make(map[string]struct{}) 69 | for _, l := range lists { 70 | for _, m := range l { 71 | members[m] = struct{}{} 72 | } 73 | } 74 | r.SetMembers(members) 75 | } 76 | 77 | // Helper type for members of peer ring. 78 | type peer string 79 | 80 | func (p peer) String() string { 81 | return string(p) 82 | } 83 | 84 | func BuildPeerHashRing(consistentCfg consistent.Config, podIP string, serviceName string, serviceNamespace string) *ShardHelper { 85 | consistentHashRing := consistent.New(nil, consistentCfg) 86 | mutex := sync.RWMutex{} 87 | return &ShardHelper{ 88 | PodIP: podIP, 89 | ServiceName: serviceName, 90 | ServiceNamespace: serviceNamespace, 91 | mu: &mutex, 92 | ring: consistentHashRing, 93 | } 94 | } 95 | 96 | func BuildPeerInformer(stopper chan struct{}, peerRing *ShardHelper, ringConfig consistent.Config, log logr.Logger) cache.SharedIndexInformer { 97 | 98 | dc, err := dynamic.NewForConfig(ctrl.GetConfigOrDie()) 99 | if err != nil { 100 | log.Error(err, "unable to set up informer") 101 | os.Exit(1) 102 | } 103 | 104 | listOptionsFunc := dynamicinformer.TweakListOptionsFunc(func(options *metav1.ListOptions) { 105 | options.FieldSelector = "metadata.name=" + peerRing.ServiceName 106 | }) 107 | 108 | // Use our namespace and expected endpoints name in our future informer. 109 | factory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(dc, 0, peerRing.ServiceNamespace, listOptionsFunc) 110 | 111 | // Construct an informer for the endpoints. 112 | informer := factory.ForResource(schema.GroupVersionResource{ 113 | Group: "", Version: "v1", Resource: "endpoints"}).Informer() 114 | 115 | // Set handlers for new/updated endpoints. 116 | handlers := cache.ResourceEventHandlerFuncs{ 117 | AddFunc: func(obj interface{}) { 118 | updateEndpoints(obj, nil, peerRing, log) 119 | }, 120 | UpdateFunc: func(oldObj, newObj interface{}) { 121 | // In the future, we might need to re-queue objects which belong to deleted peers. 122 | // When scaling down, it is possible that metrics will be double reported for up to the reconciliation period. 123 | // For now, we'll just set the desired peers. 124 | updateEndpoints(newObj, oldObj, peerRing, log) 125 | }, 126 | // We can add a delete handler here. Not sure yet what it should do. 127 | } 128 | 129 | _, err = informer.AddEventHandler(handlers) 130 | if err != nil { 131 | log.Info(err.Error(), "error adding event handler to informer") 132 | } 133 | return informer 134 | } 135 | 136 | func updateEndpoints(currentObj interface{}, previousObj interface{}, ring *ShardHelper, log logr.Logger) { 137 | current, err := toEndpoint(currentObj, log) 138 | if err != nil { 139 | log.Error(err, "could not convert obj to Endpoints") 140 | return 141 | } 142 | 143 | var previous *corev1.Endpoints 144 | { 145 | previous = nil 146 | 147 | if previousObj != nil { 148 | previous, err = toEndpoint(currentObj, log) 149 | if err != nil { 150 | log.Error(err, "could not convert obj to Endpoints") 151 | return 152 | } 153 | } 154 | } 155 | 156 | added, kept, _, ok := getEndpointChanges(current, previous, log) 157 | if !ok { 158 | return 159 | } 160 | ring.SetMembersFromLists(added, kept) 161 | log.Info(fmt.Sprintf("updated peer list with %d endpoints", len(added)+len(kept))) 162 | } 163 | 164 | // getEndpointChanges takes a current and optional previous object and returns the added, kept, and removed items, plus a success boolean. 165 | func getEndpointChanges(current *corev1.Endpoints, previous *corev1.Endpoints, log logr.Logger) ([]string, []string, []string, bool) { 166 | 167 | currentEndpoints := []string{} // Stores current endpoints to return directly if we don't have a previous state. 168 | currentEndpointsMap := make(map[string]struct{}) // Stores the endpoints as a map for quicker comparisons to previous state. 169 | 170 | // Store all the current endpoints for us to use later. 171 | for _, subset := range current.Subsets { 172 | for _, ip := range subset.Addresses { 173 | // We add to both the list and the map. This wastes a little memory because we only ever need one or the other, 174 | // but it saves cycles to not loop over the endpoints multiple times. We don't expect tons of endpoints. 175 | currentEndpoints = append(currentEndpoints, ip.IP) 176 | currentEndpointsMap[ip.IP] = struct{}{} 177 | } 178 | } 179 | 180 | if previous == nil { 181 | // If there is no previous object, we're only adding new (initial) endpoints. 182 | // Just return the current endpoint list. 183 | return currentEndpoints, nil, nil, true 184 | } 185 | 186 | added := []string{} 187 | kept := []string{} 188 | removed := []string{} 189 | 190 | for _, subset := range previous.Subsets { 191 | for _, ip := range subset.Addresses { 192 | // Each address was either: 193 | // - added (exists in current, not previous) 194 | // - kept (exists in current and previous) 195 | // - removed (not in current, exists in previous) 196 | 197 | if _, found := currentEndpointsMap[ip.IP]; !found { 198 | // Endpoint has been removed in current state. 199 | removed = append(removed, ip.IP) 200 | } else { 201 | // Item existed before, so it has been "kept" and not "added". 202 | kept = append(kept, ip.IP) 203 | delete(currentEndpointsMap, ip.IP) 204 | } 205 | } 206 | } 207 | 208 | // Any remaining items in the added endpoints map were actually new. Return them as a list. 209 | for ip := range currentEndpointsMap { 210 | added = append(added, ip) 211 | } 212 | 213 | return added, kept, removed, true 214 | 215 | } 216 | 217 | func toEndpoint(obj interface{}, log logr.Logger) (*corev1.Endpoints, error) { 218 | ep := &corev1.Endpoints{} 219 | err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.(*unstructured.Unstructured).UnstructuredContent(), ep) 220 | if err != nil { 221 | log.Error(err, "could not convert obj to Endpoints") 222 | return ep, err 223 | } 224 | return ep, nil 225 | } 226 | -------------------------------------------------------------------------------- /helm/starboard-exporter/values.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema#", 3 | "type": "object", 4 | "properties": { 5 | "customMetricsHPA": { 6 | "type": "object", 7 | "properties": { 8 | "enabled": { 9 | "type": "boolean" 10 | }, 11 | "maxReplicas": { 12 | "type": "integer" 13 | }, 14 | "metricName": { 15 | "type": "string" 16 | }, 17 | "minReplicas": { 18 | "type": "integer" 19 | }, 20 | "targetAverageValueSeconds": { 21 | "type": "integer" 22 | } 23 | } 24 | }, 25 | "exporter": { 26 | "type": "object", 27 | "properties": { 28 | "configAuditReports": { 29 | "type": "object", 30 | "properties": { 31 | "enabled": { 32 | "type": "boolean" 33 | } 34 | } 35 | }, 36 | "requeueMaxJitterPercent": { 37 | "type": "integer" 38 | }, 39 | "vulnerabilityReports": { 40 | "type": "object", 41 | "properties": { 42 | "enabled": { 43 | "type": "boolean" 44 | }, 45 | "targetLabels": { 46 | "type": "array" 47 | } 48 | } 49 | } 50 | } 51 | }, 52 | "global": { 53 | "type": "object", 54 | "properties": { 55 | "podSecurityStandards": { 56 | "type": "object", 57 | "properties": { 58 | "enforced": { 59 | "type": "boolean" 60 | } 61 | } 62 | } 63 | } 64 | }, 65 | "image": { 66 | "type": "object", 67 | "properties": { 68 | "name": { 69 | "type": "string" 70 | } 71 | } 72 | }, 73 | "imagePullSecrets": { 74 | "type": "array" 75 | }, 76 | "kedaScaledObject": { 77 | "type": "object", 78 | "properties": { 79 | "enabled": { 80 | "type": "boolean" 81 | }, 82 | "maxReplicas": { 83 | "type": "integer" 84 | }, 85 | "minReplicas": { 86 | "type": "integer" 87 | }, 88 | "triggers": { 89 | "type": "array" 90 | } 91 | } 92 | }, 93 | "maxReplicas": { 94 | "type": "integer" 95 | }, 96 | "minReplicas": { 97 | "type": "integer" 98 | }, 99 | "monitoring": { 100 | "type": "object", 101 | "properties": { 102 | "grafanaDashboard": { 103 | "type": "object", 104 | "properties": { 105 | "enabled": { 106 | "type": "boolean" 107 | } 108 | } 109 | }, 110 | "serviceMonitor": { 111 | "type": "object", 112 | "properties": { 113 | "enabled": { 114 | "type": "boolean" 115 | }, 116 | "labels": { 117 | "type": "object" 118 | }, 119 | "metricRelabelings": { 120 | "type": "array" 121 | }, 122 | "relabelings": { 123 | "type": "array", 124 | "items": { 125 | "type": "object", 126 | "properties": { 127 | "action": { 128 | "type": "string" 129 | }, 130 | "regex": { 131 | "type": "string" 132 | } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | } 139 | }, 140 | "networkpolicy": { 141 | "type": "object", 142 | "properties": { 143 | "enabled": { 144 | "type": "boolean" 145 | } 146 | } 147 | }, 148 | "nodeSelector": { 149 | "type": "object" 150 | }, 151 | "pod": { 152 | "type": "object", 153 | "properties": { 154 | "group": { 155 | "type": "object", 156 | "properties": { 157 | "id": { 158 | "type": "integer" 159 | } 160 | } 161 | }, 162 | "user": { 163 | "type": "object", 164 | "properties": { 165 | "id": { 166 | "type": "integer" 167 | } 168 | } 169 | } 170 | } 171 | }, 172 | "podAnnotations": { 173 | "type": "object" 174 | }, 175 | "podLabels": { 176 | "type": "object" 177 | }, 178 | "podSecurityContext": { 179 | "type": "object", 180 | "properties": { 181 | "runAsNonRoot": { 182 | "type": "boolean" 183 | }, 184 | "seccompProfile": { 185 | "type": "object", 186 | "properties": { 187 | "type": { 188 | "type": "string" 189 | } 190 | } 191 | } 192 | } 193 | }, 194 | "registry": { 195 | "type": "object", 196 | "properties": { 197 | "domain": { 198 | "type": "string" 199 | } 200 | } 201 | }, 202 | "replicas": { 203 | "type": "integer" 204 | }, 205 | "resources": { 206 | "type": "object", 207 | "properties": { 208 | "limits": { 209 | "type": "object", 210 | "properties": { 211 | "cpu": { 212 | "type": "string" 213 | }, 214 | "memory": { 215 | "type": "string" 216 | } 217 | } 218 | }, 219 | "requests": { 220 | "type": "object", 221 | "properties": { 222 | "cpu": { 223 | "type": "string" 224 | }, 225 | "memory": { 226 | "type": "string" 227 | } 228 | } 229 | } 230 | } 231 | }, 232 | "securityContext": { 233 | "type": "object", 234 | "properties": { 235 | "allowPrivilegeEscalation": { 236 | "type": "boolean" 237 | }, 238 | "capabilities": { 239 | "type": "object", 240 | "properties": { 241 | "drop": { 242 | "type": "array", 243 | "items": { 244 | "type": "string" 245 | } 246 | } 247 | } 248 | }, 249 | "privileged": { 250 | "type": "boolean" 251 | }, 252 | "readOnlyRootFilesystem": { 253 | "type": "boolean" 254 | }, 255 | "runAsNonRoot": { 256 | "type": "boolean" 257 | }, 258 | "seccompProfile": { 259 | "type": "object", 260 | "properties": { 261 | "type": { 262 | "type": "string" 263 | } 264 | } 265 | } 266 | } 267 | }, 268 | "tolerations": { 269 | "type": "array" 270 | } 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "flag" 23 | "fmt" 24 | "net" 25 | "os" 26 | "strings" 27 | 28 | // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) 29 | // to ensure that exec-entrypoint and run can make use of them. 30 | 31 | "github.com/buraksezer/consistent" 32 | "github.com/cespare/xxhash/v2" 33 | "github.com/go-logr/logr" 34 | "k8s.io/apimachinery/pkg/runtime" 35 | utilruntime "k8s.io/apimachinery/pkg/util/runtime" 36 | clientgoscheme "k8s.io/client-go/kubernetes/scheme" 37 | _ "k8s.io/client-go/plugin/pkg/client/auth" 38 | ctrl "sigs.k8s.io/controller-runtime" 39 | "sigs.k8s.io/controller-runtime/pkg/client" 40 | "sigs.k8s.io/controller-runtime/pkg/healthz" 41 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 42 | "sigs.k8s.io/controller-runtime/pkg/metrics/server" 43 | 44 | aqua "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1" 45 | 46 | "github.com/giantswarm/starboard-exporter/controllers" 47 | "github.com/giantswarm/starboard-exporter/controllers/configauditreport" 48 | "github.com/giantswarm/starboard-exporter/controllers/vulnerabilityreport" 49 | "github.com/giantswarm/starboard-exporter/utils" 50 | //+kubebuilder:scaffold:imports 51 | ) 52 | 53 | var ( 54 | scheme = runtime.NewScheme() 55 | setupLog = ctrl.Log.WithName("setup") 56 | ) 57 | 58 | type hasher struct{} 59 | 60 | func (h hasher) Sum64(data []byte) uint64 { 61 | // TODO: Investigate hash function options. 62 | return xxhash.Sum64(data) 63 | } 64 | 65 | func init() { 66 | utilruntime.Must(clientgoscheme.AddToScheme(scheme)) 67 | 68 | err := aqua.AddToScheme(scheme) 69 | if err != nil { 70 | setupLog.Error(err, fmt.Sprintf("error registering scheme: %s", err)) 71 | } 72 | 73 | //+kubebuilder:scaffold:scheme 74 | } 75 | 76 | func main() { 77 | var configAuditEnabled bool 78 | var enableLeaderElection bool 79 | var maxJitterPercent int 80 | var metricsAddr string 81 | var podIPString string 82 | var probeAddr string 83 | var serviceName string 84 | var serviceNamespace string 85 | var vulnerabilityScansEnabled bool 86 | targetLabels := []vulnerabilityreport.VulnerabilityLabel{} 87 | 88 | flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.") 89 | flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") 90 | flag.BoolVar(&enableLeaderElection, "leader-elect", false, 91 | "Enable leader election for controller manager. "+ 92 | "Enabling this will ensure there is only one active controller manager.") 93 | 94 | flag.IntVar(&maxJitterPercent, "max-jitter-percent", 10, 95 | "Spreads out re-queue interval of reports by +/- this amount to spread load.") 96 | 97 | flag.StringVar(&podIPString, "pod-ip", "", "The IP address of the current Pod/instance used when sharding reports.") 98 | 99 | flag.StringVar(&serviceName, "service-name", controllers.DefaultServiceName, "When sharding reports, the service endpoints for this service will be used to find peers.") 100 | flag.StringVar(&serviceNamespace, "service-namespace", "", "When sharding reports, the service endpoints in this namespace will be used to find peers.") 101 | 102 | // Read and parse target-labels into known VulnerabilityLabels. 103 | flag.Func("target-labels", 104 | "A comma-separated list of labels to be exposed per-vulnerability. Alias 'all' is supported.", 105 | func(input string) error { 106 | items := strings.Split(input, ",") 107 | for _, i := range items { 108 | if i == vulnerabilityreport.LabelGroupAll { 109 | // Special case for "all". 110 | targetLabels = appendIfNotExists(targetLabels, vulnerabilityreport.LabelsForGroup(vulnerabilityreport.LabelGroupAll)) 111 | continue 112 | } 113 | 114 | label, ok := vulnerabilityreport.LabelWithName(i) 115 | if !ok { 116 | err := errors.New("invalidConfigError") 117 | return err 118 | } 119 | targetLabels = appendIfNotExists(targetLabels, []vulnerabilityreport.VulnerabilityLabel{label}) 120 | } 121 | 122 | // If exposing detail metrics, we must always include the report name in order to delete them by name later. 123 | reportNameLabel, _ := vulnerabilityreport.LabelWithName("report_name") 124 | targetLabels = appendIfNotExists(targetLabels, []vulnerabilityreport.VulnerabilityLabel{reportNameLabel}) 125 | 126 | return nil 127 | }) 128 | 129 | flag.BoolVar(&configAuditEnabled, "config-audits-enabled", true, 130 | "Enable metrics for ConfigAuditReport resources.") 131 | 132 | flag.BoolVar(&vulnerabilityScansEnabled, "vulnerability-scans-enabled", true, 133 | "Enable metrics for VulnerabilityReport resources.") 134 | 135 | opts := zap.Options{ 136 | Development: false, 137 | } 138 | opts.BindFlags(flag.CommandLine) 139 | flag.Parse() 140 | 141 | ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) 142 | 143 | podIP := net.ParseIP(podIPString) 144 | if podIP == nil { 145 | setupLog.Error(nil, fmt.Sprintf("invalid pod IP %s", podIPString)) 146 | os.Exit(1) 147 | } 148 | 149 | if serviceNamespace == "" { 150 | setupLog.Error(nil, "service namespace must not be empty") 151 | os.Exit(1) 152 | } 153 | 154 | setupLog.Info(fmt.Sprintf("this is exporter instance %s", podIP.String())) 155 | 156 | // Print Vulnerabilities target labels. 157 | if len(targetLabels) > 0 { 158 | tl := []string{} 159 | for _, l := range targetLabels { 160 | tl = append(tl, l.Name) 161 | } 162 | setupLog.Info(fmt.Sprintf("Using %d vulnerability target labels: %v", len(tl), tl)) 163 | } 164 | 165 | mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ 166 | Scheme: scheme, 167 | Metrics: server.Options{BindAddress: metricsAddr}, 168 | HealthProbeBindAddress: probeAddr, 169 | LeaderElection: enableLeaderElection, 170 | LeaderElectionID: "58aff8fc.giantswarm", 171 | }) 172 | if err != nil { 173 | setupLog.Error(err, "unable to start manager") 174 | os.Exit(1) 175 | } 176 | 177 | // Set up consistent hashing to shard reports over all of our exporters. 178 | // This is arbitrarily based on the assumption that 97 exporters is a reasonable maximum for now. 179 | // This could be made configurable in the future if actual usage requires it. 180 | consistentCfg := consistent.Config{ 181 | PartitionCount: 97, 182 | ReplicationFactor: 20, 183 | Load: 1.25, 184 | Hasher: hasher{}, 185 | } 186 | 187 | peerRing := utils.BuildPeerHashRing(consistentCfg, podIP.String(), serviceName, serviceNamespace) 188 | 189 | // Create and start the informer which will keep the endpoints in sync in our ring. 190 | stopInformer := make(chan struct{}) 191 | defer close(stopInformer) 192 | 193 | informerLog := ctrl.Log.WithName("informer").WithName("Endpoints") 194 | inf := utils.BuildPeerInformer(stopInformer, peerRing, consistentCfg, informerLog) 195 | go inf.Run(stopInformer) 196 | 197 | // Wait for the ring to be synced for the first time so we can use it immediately. 198 | ctx, cancel := context.WithCancel(context.Background()) 199 | defer cancel() 200 | for peerRing.MemberCount() > 0 || ctx.Err() != nil { 201 | // Just wait for the ring to be populated. 202 | } 203 | 204 | setupLog.Info(fmt.Sprintf("found %d exporters in %s service", peerRing.MemberCount(), peerRing.ServiceName)) 205 | 206 | if configAuditEnabled { 207 | if err = (&configauditreport.ConfigAuditReportReconciler{ 208 | Client: mgr.GetClient(), 209 | Log: ctrl.Log.WithName("controllers").WithName("ConfigAuditReport"), 210 | MaxJitterPercent: maxJitterPercent, 211 | Scheme: mgr.GetScheme(), 212 | ShardHelper: peerRing, 213 | }).SetupWithManager(mgr); err != nil { 214 | setupLog.Error(err, "unable to create controller", "controller", "ConfigAuditReport") 215 | os.Exit(1) 216 | } 217 | } 218 | 219 | if vulnerabilityScansEnabled { 220 | if err = (&vulnerabilityreport.VulnerabilityReportReconciler{ 221 | Client: mgr.GetClient(), 222 | Log: ctrl.Log.WithName("controllers").WithName("VulnerabilityReport"), 223 | MaxJitterPercent: maxJitterPercent, 224 | Scheme: mgr.GetScheme(), 225 | ShardHelper: peerRing, 226 | TargetLabels: targetLabels, 227 | }).SetupWithManager(mgr); err != nil { 228 | setupLog.Error(err, "unable to create controller", "controller", "VulnerabilityReport") 229 | os.Exit(1) 230 | } 231 | } 232 | 233 | //+kubebuilder:scaffold:builder 234 | 235 | if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { 236 | setupLog.Error(err, "unable to set up health check") 237 | os.Exit(1) 238 | } 239 | if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { 240 | setupLog.Error(err, "unable to set up ready check") 241 | os.Exit(1) 242 | } 243 | 244 | shutdownLog := ctrl.Log.WithName("shutdownHook") 245 | defer shutdownRequeue(mgr.GetClient(), shutdownLog, podIP.String()) 246 | 247 | setupLog.Info("starting manager") 248 | if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { 249 | setupLog.Error(err, "problem running manager") 250 | os.Exit(1) 251 | } 252 | } 253 | 254 | func shutdownRequeue(c client.Client, log logr.Logger, podIP string) { 255 | log.Info(fmt.Sprintf("attempting to re-queue reports for instance %s", podIP)) 256 | 257 | vulnerabilityreport.RequeueReportsForPod(c, log, podIP) 258 | 259 | configauditreport.RequeueReportsForPod(c, log, podIP) 260 | 261 | } 262 | 263 | func appendIfNotExists(base []vulnerabilityreport.VulnerabilityLabel, items []vulnerabilityreport.VulnerabilityLabel) []vulnerabilityreport.VulnerabilityLabel { 264 | result := base 265 | contained := make(map[string]bool) 266 | 267 | for _, existingLabelName := range vulnerabilityreport.LabelNamesForList(base) { 268 | contained[existingLabelName] = true 269 | } 270 | 271 | for _, newItem := range items { 272 | if !contained[newItem.Name] { 273 | result = append(result, newItem) 274 | contained[newItem.Name] = true 275 | } 276 | } 277 | 278 | return result 279 | } 280 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.8.2] - 2025-07-03 11 | 12 | ### Changed 13 | 14 | - Fix imagePullSecrets. 15 | 16 | ## [0.8.1] - 2025-04-01 17 | 18 | ### Changed 19 | 20 | - Address new code linter findings for golangci-lint v2. 21 | - Update Go version and various dependencies. 22 | 23 | ## [0.8.0] - 2024-10-23 24 | 25 | ### Added 26 | 27 | - Add Vertical Pod Autoscaler (VPA) configuration, enabled by default. 28 | 29 | ### Changed 30 | 31 | - Disable logger development mode to avoid panicking. 32 | - Disable PodSecurityPolicy by default. 33 | - Expose port 8081 for health/liveness probes. 34 | 35 | ## [0.7.11] - 2024-06-12 36 | 37 | ### Changed 38 | 39 | - Update to go 1.22 and bump dependencies. 40 | 41 | ## [0.7.10] - 2024-05-07 42 | 43 | ### Changed 44 | 45 | - Remove API check for `HorizontalPodautoscaler`. 46 | 47 | ## [0.7.9] - 2024-05-03 48 | 49 | ### Changed 50 | 51 | - Switched API version from the `HorizontalPodAutoscaler` from `autoscaling/v2beta1` to `autoscaling/v1`. 52 | 53 | ## [0.7.8] - 2024-01-16 54 | 55 | ### Changed 56 | 57 | - Switch PSP values from `psp.enabled` to `global.podSecurityStandards.enforced`. 58 | 59 | ## [0.7.7] - 2023-12-19 60 | 61 | ### Added 62 | 63 | - Add a `scaledObject` resource for KEDA support. 64 | 65 | ## [0.7.6] - 2023-12-12 66 | 67 | ### Changed 68 | 69 | - Update go to v1.21 and bump dependencies. 70 | 71 | ## [0.7.5] - 2023-12-06 72 | 73 | ### Changed 74 | 75 | - Configure `gsoci.azurecr.io` as the default container image registry. 76 | 77 | ### Removed 78 | 79 | - Stop pushing to `openstack-app-collection`. 80 | 81 | ## [0.7.4] - 2023-04-24 82 | 83 | ### Added 84 | 85 | - Add icon. 86 | 87 | ## [0.7.3] - 2023-04-12 88 | 89 | ### Changed 90 | 91 | - Removed `application.giantswarm.io/team` label from ServiceMonitor. 92 | 93 | ## [0.7.2] - 2023-02-27 94 | 95 | ### Changed 96 | 97 | - Fix/template RoleBinding for deploying into namespaces other than the release namespace. 98 | 99 | ## [0.7.1] - 2023-01-25 100 | 101 | ### Added 102 | 103 | - Adds `imagePullSecrets` to Chart. 104 | 105 | ## [0.7.0] - 2023-01-11 106 | 107 | ### Changed 108 | 109 | - Replaces starboard library with trivy-operator library. 110 | - Removes CIS benchmarks & reporting capabilities. 111 | 112 | ### Added 113 | 114 | - Add Horizontal Pod Autoscaling based on Prometheus scrape times. 115 | 116 | ## [0.6.3] - 2022-12-02 117 | 118 | ## [0.6.2] - 2022-10-24 119 | 120 | ### Changed 121 | 122 | - Fix schema type for tolerations ([#157](https://github.com/giantswarm/starboard-exporter/issues/157)). 123 | 124 | ## [0.6.1] - 2022-10-21 125 | 126 | ### Changed 127 | 128 | - Make ServiceMonitor relabelings configurable and drop unhelpful pod, container, and service labels by default. 129 | - Build with `app-build-suite`. 130 | - Add `app-test-suite` basic smoke tests. 131 | 132 | ## [0.6.0] - 2022-09-16 133 | 134 | ### Added 135 | 136 | - Add `podLabels` property to allow custom pod labels. 137 | 138 | ### Changed 139 | 140 | - Disable reconciliation of CIS benchmark reports by default. These reports are temporarily removed from `trivy-operator`, to be reintroduced in the future. Reconciliation of CIS benchmarks produced by `starboard` is still supported by setting `exporter.CISKubeBenchReports.enabled: true` in the Helm values. 141 | 142 | ## [0.5.2] - 2022-09-09 143 | 144 | ### Added 145 | 146 | - Make `interval` and `scrapeTimeout` configurable in the service monitor via `monitoring.serviceMonitor.interval` and `monitoring.serviceMonitor.scrapeTimeout` 147 | 148 | ## [0.5.1] - 2022-07-13 149 | 150 | ### Added 151 | 152 | - Allow selectively enabling/disabling controllers for each report type. 153 | 154 | ## [0.5.0] - 2022-06-22 155 | 156 | ### Announcements 157 | 158 | - **Important: the `latest` tag alias is being removed.** Some users have reported issues using the `latest` tag on our hosted registries (Docker Hub, Quay, etc.). We advise against using `latest` tags and don't use them ourselves, so this tag is not kept up to date. Please switch to using a tagged version. We will be removing the `latest` tag from our public registries in the near future to avoid confusion. 159 | 160 | ### Added 161 | 162 | - Add missing monitoring options in the Helm chart values.yaml. 163 | - Support sharding report metrics across multiple instances of the exporter. 164 | - Set `runAsNonRoot` and use `RuntimeDefault` seccomp profile. 165 | - Make replica count configurable in Helm values. 166 | - Add configurable tolerations to Helm values. 167 | - Reconcile and expose metrics for `CISKubeBenchReport` custom resources. 168 | 169 | ## [0.4.1] - 2022-04-26 170 | 171 | ### Added 172 | 173 | - Spread (jitter) re-queueing of reports by +/- 10% by default to help smooth resource utilization. 174 | 175 | ## [0.4.0] - 2022-04-22 176 | 177 | ### Added 178 | 179 | - Reconcile and expose metrics for `ConfigAuditReport` custom resources. **Requires Starboard v0.15.0 or above.** 180 | 181 | ## [0.3.3] - 2022-03-31 182 | 183 | ### Changed 184 | 185 | - Build with [`architect`](https://github.com/giantswarm/architect) instead of [`app-build-suite`](https://github.com/giantswarm/app-build-suite) (reverts change from 0.3.2). 186 | 187 | ## [0.3.2] - 2022-03-28 188 | 189 | ### Added 190 | 191 | - Add configurable nodeSelector to Helm values. 192 | 193 | ### Changed 194 | 195 | - Build with [`app-build-suite`](https://github.com/giantswarm/app-build-suite) instead of [`architect`](https://github.com/giantswarm/architect). 196 | 197 | ## [0.3.1] - 2022-03-15 198 | 199 | ### Added 200 | 201 | - Add NodeAffinity to run the exporter only on Linux Nodes with AMD64. 202 | 203 | ## [0.3.0] - 2022-02-14 204 | 205 | ### Added 206 | 207 | - Add the `image_registry` label exposing the image registry. 208 | 209 | ### Changed 210 | 211 | - Bump `golang`, `prometheus`, and `starboard` dependency versions. 212 | - Update Grafana dashboard to use plugin version 8.3.2 and the new label. 213 | 214 | ## [0.2.1] - 2022-01-24 215 | 216 | ### Added 217 | 218 | - Make pod annotations configurable. 219 | - Bump `golang`, `prometheus`, and `starboard` versions. 220 | 221 | ## [0.2.0] - 2022-01-05 222 | 223 | ### Added 224 | 225 | - Helm, add configurable container securityContext with secure defaults. 226 | 227 | ### Changed 228 | 229 | - Bump `starboard`, `logr`, and `controller-runtime` dependency versions. 230 | - Remove unneeded `releaseRevision` annotation from deployment. 231 | 232 | ### Fixed 233 | 234 | - Helm, fix incomplete metric name in pods with high/critical CVEs panel 235 | 236 | ## [0.1.4] - 2021-12-14 237 | 238 | ### Changed 239 | 240 | - Helm, remove unused RBAC config and add if for PSP and NetworkPolicy yaml. 241 | 242 | ## [0.1.3] - 2021-12-10 243 | 244 | ### Changed 245 | 246 | - Make pod resource requests/limits configurable via helm values. 247 | 248 | ## [0.1.2] - 2021-11-29 249 | 250 | ### Changed 251 | 252 | - Push images to Aliyun. 253 | - Add `starboard-exporter` to AWS and Azure app collections. 254 | 255 | ## [0.1.1] - 2021-11-26 256 | 257 | ### Added 258 | 259 | - Make target labels more easily configurable in `values.yaml`. 260 | 261 | ## [0.1.0] - 2021-11-26 262 | 263 | ### Added 264 | 265 | - Add configurable target labels. 266 | - Add Grafana dashboard. 267 | - Support custom labels for ServiceMonitor. 268 | 269 | ## [0.0.1] - 2021-11-18 270 | 271 | ### Added 272 | 273 | - Add `image_vulnerabilities` metric per-CVE per-image and `image_vulnerabilities_count` metric for summaries. 274 | - Add ServiceMonitor to scrape metrics. 275 | 276 | [Unreleased]: https://github.com/giantswarm/starboard-exporter/compare/v0.8.2...HEAD 277 | [0.8.2]: https://github.com/giantswarm/starboard-exporter/compare/v0.8.1...v0.8.2 278 | [0.8.1]: https://github.com/giantswarm/starboard-exporter/compare/v0.8.0...v0.8.1 279 | [0.8.0]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.11...v0.8.0 280 | [0.7.11]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.10...v0.7.11 281 | [0.7.10]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.9...v0.7.10 282 | [0.7.9]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.8...v0.7.9 283 | [0.7.8]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.7...v0.7.8 284 | [0.7.7]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.6...v0.7.7 285 | [0.7.6]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.5...v0.7.6 286 | [0.7.5]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.4...v0.7.5 287 | [0.7.4]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.3...v0.7.4 288 | [0.7.3]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.2...v0.7.3 289 | [0.7.2]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.1...v0.7.2 290 | [0.7.1]: https://github.com/giantswarm/starboard-exporter/compare/v0.7.0...v0.7.1 291 | [0.7.0]: https://github.com/giantswarm/starboard-exporter/compare/v0.6.3...v0.7.0 292 | [0.6.3]: https://github.com/giantswarm/starboard-exporter/compare/v0.6.2...v0.6.3 293 | [0.6.2]: https://github.com/giantswarm/starboard-exporter/compare/v0.6.1...v0.6.2 294 | [0.6.1]: https://github.com/giantswarm/starboard-exporter/compare/v0.6.0...v0.6.1 295 | [0.6.0]: https://github.com/giantswarm/starboard-exporter/compare/v0.5.2...v0.6.0 296 | [0.5.2]: https://github.com/giantswarm/starboard-exporter/compare/v0.5.1...v0.5.2 297 | [0.5.1]: https://github.com/giantswarm/starboard-exporter/compare/v0.5.0...v0.5.1 298 | [0.5.0]: https://github.com/giantswarm/starboard-exporter/compare/v0.4.1...v0.5.0 299 | [0.4.1]: https://github.com/giantswarm/starboard-exporter/compare/v0.4.0...v0.4.1 300 | [0.4.0]: https://github.com/giantswarm/starboard-exporter/compare/v0.3.3...v0.4.0 301 | [0.3.3]: https://github.com/giantswarm/starboard-exporter/compare/v0.3.2...v0.3.3 302 | [0.3.2]: https://github.com/giantswarm/starboard-exporter/compare/v0.3.1...v0.3.2 303 | [0.3.1]: https://github.com/giantswarm/starboard-exporter/compare/v0.3.0...v0.3.1 304 | [0.3.0]: https://github.com/giantswarm/starboard-exporter/compare/v0.2.1...v0.3.0 305 | [0.2.1]: https://github.com/giantswarm/starboard-exporter/compare/v0.2.0...v0.2.1 306 | [0.2.0]: https://github.com/giantswarm/starboard-exporter/compare/v0.1.4...v0.2.0 307 | [0.1.4]: https://github.com/giantswarm/starboard-exporter/compare/v0.1.3...v0.1.4 308 | [0.1.3]: https://github.com/giantswarm/starboard-exporter/compare/v0.1.2...v0.1.3 309 | [0.1.2]: https://github.com/giantswarm/starboard-exporter/compare/v0.1.1...v0.1.2 310 | [0.1.1]: https://github.com/giantswarm/starboard-exporter/compare/v0.1.0...v0.1.1 311 | [0.1.0]: https://github.com/giantswarm/starboard-exporter/compare/v0.0.1...v0.1.0 312 | [0.0.1]: https://github.com/giantswarm/starboard-exporter/releases/tag/v0.0.1 313 | -------------------------------------------------------------------------------- /controllers/vulnerabilityreport/vulnerabilityreport_controller.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2021. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package vulnerabilityreport 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "sync" 23 | 24 | aqua "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1" 25 | "github.com/go-logr/logr" 26 | "github.com/pkg/errors" 27 | "github.com/prometheus/client_golang/prometheus" 28 | apierrors "k8s.io/apimachinery/pkg/api/errors" 29 | "k8s.io/apimachinery/pkg/runtime" 30 | apitypes "k8s.io/apimachinery/pkg/types" 31 | ctrl "sigs.k8s.io/controller-runtime" 32 | "sigs.k8s.io/controller-runtime/pkg/client" 33 | ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 34 | "sigs.k8s.io/controller-runtime/pkg/log" 35 | "sigs.k8s.io/controller-runtime/pkg/metrics" 36 | 37 | "github.com/giantswarm/starboard-exporter/controllers" 38 | "github.com/giantswarm/starboard-exporter/utils" 39 | ) 40 | 41 | const ( 42 | VulnerabilityReportFinalizer = "starboard-exporter.giantswarm.io/vulnerabilityreport" 43 | ) 44 | 45 | var registerMetricsOnce sync.Once 46 | 47 | // VulnerabilityReportReconciler reconciles a VulnerabilityReport object 48 | type VulnerabilityReportReconciler struct { 49 | client.Client 50 | Log logr.Logger 51 | Scheme *runtime.Scheme 52 | 53 | MaxJitterPercent int 54 | ShardHelper *utils.ShardHelper 55 | TargetLabels []VulnerabilityLabel 56 | } 57 | 58 | //+kubebuilder:rbac:groups=aquasecurity.github.io.giantswarm,resources=vulnerabilityreports,verbs=get;list;watch;create;update;patch;delete 59 | //+kubebuilder:rbac:groups=aquasecurity.github.io.giantswarm,resources=vulnerabilityreports/status,verbs=get;update;patch 60 | //+kubebuilder:rbac:groups=aquasecurity.github.io.giantswarm,resources=vulnerabilityreports/finalizers,verbs=update 61 | 62 | func (r *VulnerabilityReportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 63 | _ = log.FromContext(ctx) 64 | _ = r.Log.WithValues("vulnerabilityreport", req.NamespacedName) 65 | 66 | registerMetricsOnce.Do(r.registerMetrics) 67 | 68 | // The report has changed, meaning our metrics are out of date for this report. Clear them. 69 | deletedSummaries := VulnerabilitySummary.DeletePartialMatch(prometheus.Labels{"report_name": req.String()}) 70 | deletedDetails := VulnerabilityInfo.DeletePartialMatch(prometheus.Labels{"report_name": req.String()}) 71 | 72 | shouldOwn := r.ShardHelper.ShouldOwn(req.String()) 73 | if shouldOwn { 74 | 75 | // Try to get the report. It might not exist anymore, in which case we don't need to do anything. 76 | report := &aqua.VulnerabilityReport{} 77 | if err := r.Get(ctx, req.NamespacedName, report); err != nil { 78 | if apierrors.IsNotFound(err) { 79 | // Most likely the report was deleted. 80 | return ctrl.Result{}, nil 81 | } 82 | 83 | // Error reading the object. 84 | r.Log.Error(err, "Unable to read report") 85 | return ctrl.Result{}, err 86 | } 87 | 88 | r.Log.Info(fmt.Sprintf("Reconciled %s || Found (C/H/M/L/N/U): %d/%d/%d/%d/%d/%d", 89 | req.NamespacedName, 90 | report.Report.Summary.CriticalCount, 91 | report.Report.Summary.HighCount, 92 | report.Report.Summary.MediumCount, 93 | report.Report.Summary.LowCount, 94 | report.Report.Summary.NoneCount, 95 | report.Report.Summary.UnknownCount, 96 | )) 97 | 98 | // Publish summary and CVE metrics for this report. 99 | r.publishImageMetrics(report) 100 | 101 | if utils.SliceContains(report.GetFinalizers(), VulnerabilityReportFinalizer) { 102 | // Remove the finalizer if we're the shard owner. 103 | ctrlutil.RemoveFinalizer(report, VulnerabilityReportFinalizer) 104 | if err := r.Update(ctx, report); err != nil { 105 | return ctrl.Result{}, err 106 | } 107 | } 108 | 109 | // Add a label to this report so any previous owners will reconcile and drop the metric. 110 | report.Labels[controllers.ShardOwnerLabel] = r.ShardHelper.PodIP 111 | err := r.Update(ctx, report, &client.UpdateOptions{}) 112 | if err != nil { 113 | r.Log.Error(err, "unable to add shard owner label") 114 | } 115 | } else { 116 | if deletedSummaries > 0 || deletedDetails > 0 { 117 | r.Log.Info(fmt.Sprintf("cleared %d summary and %d detail metrics", deletedSummaries, deletedDetails)) 118 | } 119 | } 120 | 121 | return utils.JitterRequeue(controllers.DefaultRequeueDuration, r.MaxJitterPercent, r.Log), nil 122 | } 123 | 124 | func (r *VulnerabilityReportReconciler) registerMetrics() { 125 | 126 | VulnerabilityInfo = prometheus.NewGaugeVec( 127 | prometheus.GaugeOpts{ 128 | Namespace: metricNamespace, 129 | Subsystem: metricSubsystem, 130 | Name: "image_vulnerability", 131 | Help: "Indicates the presence of a CVE in an image.", 132 | }, 133 | LabelNamesForList(r.TargetLabels), 134 | ) 135 | 136 | metrics.Registry.MustRegister(VulnerabilityInfo) 137 | } 138 | 139 | // SetupWithManager sets up the controller with the Manager. 140 | func (r *VulnerabilityReportReconciler) SetupWithManager(mgr ctrl.Manager) error { 141 | err := ctrl.NewControllerManagedBy(mgr). 142 | For(&aqua.VulnerabilityReport{}). 143 | Complete(r) 144 | if err != nil { 145 | return errors.Wrap(err, "failed setting up controller with controller manager") 146 | } 147 | 148 | return nil 149 | } 150 | 151 | func (r *VulnerabilityReportReconciler) publishImageMetrics(report *aqua.VulnerabilityReport) { 152 | publishSummaryMetrics(report) 153 | // If we have custom metrics to expose, do it. 154 | if len(r.TargetLabels) > 0 { 155 | publishCustomMetrics(report, r.TargetLabels) 156 | } 157 | } 158 | 159 | func RequeueReportsForPod(c client.Client, log logr.Logger, podIP string) { 160 | vulnList := &aqua.VulnerabilityReportList{} 161 | opts := []client.ListOption{ 162 | client.MatchingLabels{controllers.ShardOwnerLabel: podIP}, 163 | } 164 | 165 | // Get the list of reports with our label. 166 | err := c.List(context.Background(), vulnList, opts...) 167 | if err != nil { 168 | log.Error(err, "unable to fetch vulnerabilityreport") 169 | } 170 | 171 | for _, r := range vulnList.Items { 172 | // Retrieve the individual report. 173 | report := &aqua.VulnerabilityReport{} 174 | err := c.Get(context.Background(), client.ObjectKey{Name: r.Name, Namespace: r.Namespace}, report) 175 | if err != nil { 176 | log.Error(err, "unable to fetch vulnerabilityreport") 177 | } 178 | 179 | // Clear the shard-owner label if it still has our label 180 | if r.Labels[controllers.ShardOwnerLabel] == podIP { 181 | r.Labels[controllers.ShardOwnerLabel] = "" 182 | err = c.Update(context.Background(), report, &client.UpdateOptions{}) 183 | if err != nil { 184 | log.Error(err, fmt.Sprintf("unable to remove %s label", controllers.ShardOwnerLabel)) 185 | } 186 | } 187 | } 188 | } 189 | 190 | func getCountPerSeverity(report *aqua.VulnerabilityReport) map[string]float64 { 191 | // Format is e.g. {CRITICAL: 10}. 192 | return map[string]float64{ 193 | string(aqua.SeverityCritical): float64(report.Report.Summary.CriticalCount), 194 | string(aqua.SeverityHigh): float64(report.Report.Summary.HighCount), 195 | string(aqua.SeverityMedium): float64(report.Report.Summary.MediumCount), 196 | string(aqua.SeverityLow): float64(report.Report.Summary.LowCount), 197 | string(aqua.SeverityUnknown): float64(report.Report.Summary.UnknownCount), 198 | } 199 | } 200 | 201 | func publishSummaryMetrics(report *aqua.VulnerabilityReport) { 202 | summaryValues := valuesForReport(report, LabelsForGroup(labelGroupSummary)) 203 | // Add the severity label after the standard labels and expose each severity metric. 204 | for severity, count := range getCountPerSeverity(report) { 205 | v := summaryValues 206 | v["severity"] = severity 207 | 208 | // Expose the metric. 209 | VulnerabilitySummary.With( 210 | v, 211 | ).Set(count) 212 | } 213 | } 214 | 215 | func publishCustomMetrics(report *aqua.VulnerabilityReport, targetLabels []VulnerabilityLabel) { 216 | reportValues := valuesForReport(report, targetLabels) 217 | for _, v := range report.Report.Vulnerabilities { 218 | vulnValues := valuesForVulnerability(v, targetLabels) 219 | 220 | // Include the Report-level values. 221 | for label, value := range reportValues { 222 | vulnValues[label] = value 223 | } 224 | 225 | // If we have a score, use it for the value. 226 | score := float64(0) 227 | if v.Score != nil { 228 | score = *v.Score 229 | } 230 | 231 | // Expose the metric 232 | VulnerabilityInfo.With( 233 | vulnValues, 234 | ).Set(score) 235 | } 236 | } 237 | 238 | func valuesForReport(report *aqua.VulnerabilityReport, labels []VulnerabilityLabel) map[string]string { 239 | result := map[string]string{} 240 | for _, label := range labels { 241 | if label.Scope == FieldScopeReport { 242 | result[label.Name] = reportValueFor(label.Name, report) 243 | } 244 | } 245 | return result 246 | } 247 | 248 | func valuesForVulnerability(vuln aqua.Vulnerability, labels []VulnerabilityLabel) map[string]string { 249 | result := map[string]string{} 250 | for _, label := range labels { 251 | if label.Scope == FieldScopeVulnerability { 252 | result[label.Name] = vulnValueFor(label.Name, vuln) 253 | } 254 | } 255 | return result 256 | } 257 | 258 | func reportValueFor(field string, report *aqua.VulnerabilityReport) string { 259 | switch field { 260 | case "report_name": 261 | // Construct the namespacedname which we'll later be given at reconciliation. 262 | return apitypes.NamespacedName{Name: report.Name, Namespace: report.Namespace}.String() 263 | case "image_namespace": 264 | return report.Namespace 265 | case "image_registry": 266 | return report.Report.Registry.Server 267 | case "image_repository": 268 | return report.Report.Artifact.Repository 269 | case "image_tag": 270 | return report.Report.Artifact.Tag 271 | case "image_digest": 272 | return report.Report.Artifact.Digest 273 | default: 274 | // Error? 275 | return "" 276 | } 277 | } 278 | 279 | func vulnValueFor(field string, vuln aqua.Vulnerability) string { 280 | switch field { 281 | case "vulnerability_id": 282 | return vuln.VulnerabilityID 283 | case "vulnerable_resource_name": 284 | return vuln.Resource 285 | case "installed_resource_version": 286 | return vuln.InstalledVersion 287 | case "fixed_resource_version": 288 | return vuln.FixedVersion 289 | case "vulnerability_title": 290 | return vuln.Title 291 | case "vulnerability_link": 292 | return vuln.PrimaryLink 293 | case "severity": 294 | // Severity is a custom type in the Aqua library. 295 | return string(vuln.Severity) 296 | default: 297 | // Error? 298 | return "" 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /.github/workflows/zz_generated.create_release.yaml: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT. Generated with: 2 | # 3 | # devctl 4 | # 5 | # https://github.com/giantswarm/devctl/blob/f81dabb94ad79400b60650ce0b6a00d82bb31d0e/pkg/gen/input/workflows/internal/file/create_release.yaml.template 6 | # 7 | name: Create Release 8 | on: 9 | push: 10 | branches: 11 | - 'legacy' 12 | - 'main' 13 | - 'master' 14 | - 'release-v*.*.x' 15 | # "!" negates previous positive patterns so it has to be at the end. 16 | - '!release-v*.x.x' 17 | jobs: 18 | debug_info: 19 | name: Debug info 20 | runs-on: ubuntu-22.04 21 | steps: 22 | - name: Print github context JSON 23 | run: | 24 | cat <> $GITHUB_OUTPUT 55 | - name: Checkout code 56 | if: ${{ steps.get_version.outputs.version != '' }} 57 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 58 | - name: Get project.go path 59 | id: get_project_go_path 60 | if: ${{ steps.get_version.outputs.version != '' }} 61 | run: | 62 | path='./pkg/project/project.go' 63 | if [[ ! -f $path ]] ; then 64 | path='' 65 | fi 66 | echo "path=\"$path\"" 67 | echo "path=${path}" >> $GITHUB_OUTPUT 68 | - name: Check if reference version 69 | id: ref_version 70 | env: 71 | COMMIT_MESSAGE: ${{ github.event.head_commit.message }} 72 | run: | 73 | title=$(echo -n "${COMMIT_MESSAGE}" | head -1) 74 | if echo "${title}" | grep -qE '^release v[0-9]+\.[0-9]+\.[0-9]+([.-][^ .-][^ ]*)?( \(#[0-9]+\))?$' ; then 75 | version=$(echo "${title}" | cut -d ' ' -f 2) 76 | fi 77 | version=$(echo "${title}" | cut -d ' ' -f 2) 78 | version="${version#v}" # Strip "v" prefix. 79 | refversion=false 80 | if [[ "${version}" =~ ^[0-9]+.[0-9]+.[0-9]+-[0-9]+$ ]]; then 81 | refversion=true 82 | fi 83 | echo "refversion =\"${refversion}\"" 84 | echo "refversion=${refversion}" >> $GITHUB_OUTPUT 85 | update_project_go: 86 | name: Update project.go 87 | runs-on: ubuntu-22.04 88 | if: ${{ needs.gather_facts.outputs.version != '' && needs.gather_facts.outputs.project_go_path != '' && needs.gather_facts.outputs.ref_version != 'true' }} 89 | needs: 90 | - gather_facts 91 | steps: 92 | - name: Install architect 93 | uses: giantswarm/install-binary-action@0797deb878056114fa54ee30c519f617716e8c69 # v3.1.1 94 | with: 95 | binary: "architect" 96 | version: "6.14.1" 97 | - name: Install semver 98 | uses: giantswarm/install-binary-action@0797deb878056114fa54ee30c519f617716e8c69 # v3.1.1 99 | with: 100 | binary: "semver" 101 | version: "3.2.0" 102 | download_url: "https://github.com/fsaintjacques/${binary}-tool/archive/${version}.tar.gz" 103 | tarball_binary_path: "*/src/${binary}" 104 | smoke_test: "${binary} --version" 105 | - name: Checkout code 106 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 107 | - name: Update project.go 108 | id: update_project_go 109 | env: 110 | branch: "${{ github.ref }}-version-bump" 111 | run: | 112 | git checkout -b ${{ env.branch }} 113 | file="${{ needs.gather_facts.outputs.project_go_path }}" 114 | version="${{ needs.gather_facts.outputs.version }}" 115 | new_version="$(semver bump patch $version)-dev" 116 | echo "version=\"$version\" new_version=\"$new_version\"" 117 | echo "new_version=${new_version}" >> $GITHUB_OUTPUT 118 | sed -Ei "s/(version[[:space:]]*=[[:space:]]*)\"${version}\"/\1\"${new_version}\"/" $file 119 | if git diff --exit-code $file ; then 120 | echo "error: no changes in \"$file\"" >&2 121 | exit 1 122 | fi 123 | - name: Set up git identity 124 | run: | 125 | git config --local user.email "dev@giantswarm.io" 126 | git config --local user.name "taylorbot" 127 | - name: Commit changes 128 | run: | 129 | file="${{ needs.gather_facts.outputs.project_go_path }}" 130 | git add $file 131 | git commit -m "Bump version to ${{ steps.update_project_go.outputs.new_version }}" 132 | - name: Push changes 133 | env: 134 | REMOTE_REPO: "https://${{ github.actor }}:${{ secrets.TAYLORBOT_GITHUB_ACTION }}@github.com/${{ github.repository }}.git" 135 | branch: "${{ github.ref }}-version-bump" 136 | run: | 137 | git push "${REMOTE_REPO}" HEAD:${{ env.branch }} 138 | - name: Create PR 139 | env: 140 | GITHUB_TOKEN: "${{ secrets.TAYLORBOT_GITHUB_ACTION }}" 141 | base: "${{ github.ref }}" 142 | branch: "${{ github.ref }}-version-bump" 143 | version: "${{ needs.gather_facts.outputs.version }}" 144 | title: "Bump version to ${{ steps.update_project_go.outputs.new_version }}" 145 | run: | 146 | gh pr create --title "${{ env.title }}" --body "" --base ${{ env.base }} --head ${{ env.branch }} --reviewer ${{ github.actor }} 147 | - name: Enable auto-merge for PR 148 | env: 149 | GITHUB_TOKEN: "${{ secrets.TAYLORBOT_GITHUB_ACTION }}" 150 | base: "${{ github.ref }}" 151 | branch: "${{ github.ref }}-version-bump" 152 | version: "${{ needs.gather_facts.outputs.version }}" 153 | title: "Bump version to ${{ steps.update_project_go.outputs.new_version }}" 154 | run: | 155 | gh pr merge --auto --squash "${{ env.branch }}" || echo "::warning::Auto-merge not allowed. Please adjust the repository settings." 156 | create_release: 157 | name: Create release 158 | runs-on: ubuntu-22.04 159 | needs: 160 | - gather_facts 161 | if: ${{ needs.gather_facts.outputs.version }} 162 | outputs: 163 | upload_url: ${{ steps.create_gh_release.outputs.upload_url }} 164 | steps: 165 | - name: Checkout code 166 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 167 | with: 168 | ref: ${{ github.sha }} 169 | - name: Ensure correct version in project.go 170 | if: ${{ needs.gather_facts.outputs.project_go_path != '' && needs.gather_facts.outputs.ref_version != 'true' }} 171 | run: | 172 | file="${{ needs.gather_facts.outputs.project_go_path }}" 173 | version="${{ needs.gather_facts.outputs.version }}" 174 | grep -qE "version[[:space:]]*=[[:space:]]*\"$version\"" $file 175 | - name: Get Changelog Entry 176 | id: changelog_reader 177 | uses: mindsers/changelog-reader-action@32aa5b4c155d76c94e4ec883a223c947b2f02656 # v2.2.3 178 | with: 179 | version: ${{ needs.gather_facts.outputs.version }} 180 | path: ./CHANGELOG.md 181 | - name: Set up git identity 182 | run: | 183 | git config --local user.email "dev@giantswarm.io" 184 | git config --local user.name "taylorbot" 185 | - name: Create tag 186 | run: | 187 | version="${{ needs.gather_facts.outputs.version }}" 188 | git tag "v$version" ${{ github.sha }} 189 | - name: Push tag 190 | env: 191 | REMOTE_REPO: "https://${{ github.actor }}:${{ secrets.TAYLORBOT_GITHUB_ACTION }}@github.com/${{ github.repository }}.git" 192 | run: | 193 | git push "${REMOTE_REPO}" --tags 194 | - name: Create release 195 | id: create_gh_release 196 | uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 197 | env: 198 | GITHUB_TOKEN: "${{ secrets.TAYLORBOT_GITHUB_ACTION }}" 199 | with: 200 | body: ${{ steps.changelog_reader.outputs.changes }} 201 | tag: "v${{ needs.gather_facts.outputs.version }}" 202 | skipIfReleaseExists: true 203 | 204 | create-release-branch: 205 | name: Create release branch 206 | runs-on: ubuntu-22.04 207 | needs: 208 | - gather_facts 209 | if: ${{ needs.gather_facts.outputs.version }} 210 | steps: 211 | - name: Install semver 212 | uses: giantswarm/install-binary-action@0797deb878056114fa54ee30c519f617716e8c69 # v3.1.1 213 | with: 214 | binary: "semver" 215 | version: "3.0.0" 216 | download_url: "https://github.com/fsaintjacques/${binary}-tool/archive/${version}.tar.gz" 217 | tarball_binary_path: "*/src/${binary}" 218 | smoke_test: "${binary} --version" 219 | - name: Check out the repository 220 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 221 | with: 222 | fetch-depth: 0 # Clone the whole history, not just the most recent commit. 223 | - name: Fetch all tags and branches 224 | run: "git fetch --all" 225 | - name: Create long-lived release branch 226 | run: | 227 | current_version="${{ needs.gather_facts.outputs.version }}" 228 | parent_version="$(git describe --tags --abbrev=0 HEAD^ || true)" 229 | parent_version="${parent_version#v}" # Strip "v" prefix. 230 | 231 | if [[ -z "$parent_version" ]] ; then 232 | echo "Unable to find a parent tag version. No branch to create." 233 | exit 0 234 | fi 235 | 236 | echo "current_version=$current_version parent_version=$parent_version" 237 | 238 | current_major=$(semver get major $current_version) 239 | current_minor=$(semver get minor $current_version) 240 | parent_major=$(semver get major $parent_version) 241 | parent_minor=$(semver get minor $parent_version) 242 | echo "current_major=$current_major current_minor=$current_minor parent_major=$parent_major parent_minor=$parent_minor" 243 | 244 | if [[ $current_major -gt $parent_major ]] ; then 245 | echo "Current tag is a new major version" 246 | elif [[ $current_major -eq $parent_major ]] && [[ $current_minor -gt $parent_minor ]] ; then 247 | echo "Current tag is a new minor version" 248 | else 249 | echo "Current tag is not a new major or minor version. Nothing to do here." 250 | exit 0 251 | fi 252 | 253 | release_branch="release-v${parent_major}.${parent_minor}.x" 254 | echo "release_branch=$release_branch" 255 | 256 | if git rev-parse --verify $release_branch ; then 257 | echo "Release branch $release_branch already exists. Nothing to do here." 258 | exit 0 259 | fi 260 | 261 | git branch $release_branch HEAD^ 262 | git push origin $release_branch 263 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 - 2025 Giant Swarm GmbH 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /helm/starboard-exporter/templates/grafana-dashboard.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.monitoring.grafanaDashboard.enabled }} 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | labels: 6 | grafana_dashboard: "1" 7 | name: grafana-starboard 8 | {{- if .Values.monitoring.grafanaDashboard.namespace }} 9 | namespace: {{ .Values.monitoring.grafanaDashboard.namespace }} 10 | {{- else }} 11 | namespace: {{ .Release.Namespace }} 12 | {{- end}} 13 | data: 14 | grafana-starboard.json: |- 15 | { 16 | "__elements": [], 17 | "__requires": [ 18 | { 19 | "type": "grafana", 20 | "id": "grafana", 21 | "name": "Grafana", 22 | "version": "8.3.2" 23 | }, 24 | { 25 | "type": "datasource", 26 | "id": "prometheus", 27 | "name": "Prometheus", 28 | "version": "1.0.0" 29 | }, 30 | { 31 | "type": "panel", 32 | "id": "stat", 33 | "name": "Stat", 34 | "version": "" 35 | }, 36 | { 37 | "type": "panel", 38 | "id": "table", 39 | "name": "Table", 40 | "version": "" 41 | }, 42 | { 43 | "type": "panel", 44 | "id": "timeseries", 45 | "name": "Time series", 46 | "version": "" 47 | } 48 | ], 49 | "annotations": { 50 | "list": [ 51 | { 52 | "builtIn": 1, 53 | "datasource": "-- Grafana --", 54 | "enable": true, 55 | "hide": true, 56 | "iconColor": "rgba(0, 211, 255, 1)", 57 | "name": "Annotations & Alerts", 58 | "target": { 59 | "limit": 100, 60 | "matchAny": false, 61 | "tags": [], 62 | "type": "dashboard" 63 | }, 64 | "type": "dashboard" 65 | } 66 | ] 67 | }, 68 | "editable": true, 69 | "fiscalYearStartMonth": 0, 70 | "graphTooltip": 0, 71 | "id": null, 72 | "iteration": 1644852715758, 73 | "links": [], 74 | "liveNow": false, 75 | "panels": [ 76 | { 77 | "datasource": { 78 | "uid": "$datasource" 79 | }, 80 | "description": "", 81 | "fieldConfig": { 82 | "defaults": { 83 | "color": { 84 | "mode": "thresholds" 85 | }, 86 | "mappings": [], 87 | "thresholds": { 88 | "mode": "absolute", 89 | "steps": [ 90 | { 91 | "color": "green", 92 | "value": null 93 | }, 94 | { 95 | "color": "red", 96 | "value": 80 97 | } 98 | ] 99 | } 100 | }, 101 | "overrides": [] 102 | }, 103 | "gridPos": { 104 | "h": 7, 105 | "w": 4, 106 | "x": 0, 107 | "y": 0 108 | }, 109 | "id": 6, 110 | "options": { 111 | "colorMode": "value", 112 | "graphMode": "area", 113 | "justifyMode": "auto", 114 | "orientation": "auto", 115 | "reduceOptions": { 116 | "calcs": [ 117 | "lastNotNull" 118 | ], 119 | "fields": "", 120 | "values": false 121 | }, 122 | "text": {}, 123 | "textMode": "auto" 124 | }, 125 | "pluginVersion": "8.3.2", 126 | "targets": [ 127 | { 128 | "exemplar": true, 129 | "expr": "sum(starboard_exporter_vulnerabilityreport_image_vulnerability_severity_count{severity=\"CRITICAL\"})", 130 | "interval": "", 131 | "legendFormat": "", 132 | "refId": "A" 133 | } 134 | ], 135 | "title": "Critical CVEs", 136 | "type": "stat" 137 | }, 138 | { 139 | "datasource": { 140 | "uid": "$datasource" 141 | }, 142 | "description": "", 143 | "fieldConfig": { 144 | "defaults": { 145 | "color": { 146 | "mode": "thresholds" 147 | }, 148 | "mappings": [], 149 | "thresholds": { 150 | "mode": "absolute", 151 | "steps": [ 152 | { 153 | "color": "green", 154 | "value": null 155 | }, 156 | { 157 | "color": "red", 158 | "value": 80 159 | } 160 | ] 161 | } 162 | }, 163 | "overrides": [] 164 | }, 165 | "gridPos": { 166 | "h": 7, 167 | "w": 4, 168 | "x": 4, 169 | "y": 0 170 | }, 171 | "id": 7, 172 | "options": { 173 | "colorMode": "value", 174 | "graphMode": "area", 175 | "justifyMode": "auto", 176 | "orientation": "auto", 177 | "reduceOptions": { 178 | "calcs": [ 179 | "lastNotNull" 180 | ], 181 | "fields": "", 182 | "values": false 183 | }, 184 | "text": {}, 185 | "textMode": "auto" 186 | }, 187 | "pluginVersion": "8.3.2", 188 | "targets": [ 189 | { 190 | "exemplar": true, 191 | "expr": "sum(starboard_exporter_vulnerabilityreport_image_vulnerability_severity_count{severity=\"HIGH\"})", 192 | "interval": "", 193 | "legendFormat": "", 194 | "refId": "A" 195 | } 196 | ], 197 | "title": "High CVEs", 198 | "type": "stat" 199 | }, 200 | { 201 | "datasource": { 202 | "uid": "$datasource" 203 | }, 204 | "description": "", 205 | "fieldConfig": { 206 | "defaults": { 207 | "color": { 208 | "mode": "thresholds" 209 | }, 210 | "mappings": [], 211 | "thresholds": { 212 | "mode": "absolute", 213 | "steps": [ 214 | { 215 | "color": "green", 216 | "value": null 217 | }, 218 | { 219 | "color": "red", 220 | "value": 80 221 | } 222 | ] 223 | } 224 | }, 225 | "overrides": [] 226 | }, 227 | "gridPos": { 228 | "h": 7, 229 | "w": 4, 230 | "x": 8, 231 | "y": 0 232 | }, 233 | "id": 8, 234 | "options": { 235 | "colorMode": "value", 236 | "graphMode": "area", 237 | "justifyMode": "auto", 238 | "orientation": "auto", 239 | "reduceOptions": { 240 | "calcs": [ 241 | "lastNotNull" 242 | ], 243 | "fields": "", 244 | "values": false 245 | }, 246 | "text": {}, 247 | "textMode": "auto" 248 | }, 249 | "pluginVersion": "8.3.2", 250 | "targets": [ 251 | { 252 | "exemplar": true, 253 | "expr": "sum(starboard_exporter_vulnerabilityreport_image_vulnerability_severity_count{severity=\"MEDIUM\"})", 254 | "interval": "", 255 | "legendFormat": "", 256 | "refId": "A" 257 | } 258 | ], 259 | "title": "Medium CVEs", 260 | "type": "stat" 261 | }, 262 | { 263 | "datasource": { 264 | "uid": "$datasource" 265 | }, 266 | "description": "", 267 | "fieldConfig": { 268 | "defaults": { 269 | "color": { 270 | "mode": "thresholds" 271 | }, 272 | "mappings": [], 273 | "thresholds": { 274 | "mode": "absolute", 275 | "steps": [ 276 | { 277 | "color": "green", 278 | "value": null 279 | }, 280 | { 281 | "color": "red", 282 | "value": 80 283 | } 284 | ] 285 | } 286 | }, 287 | "overrides": [] 288 | }, 289 | "gridPos": { 290 | "h": 7, 291 | "w": 4, 292 | "x": 12, 293 | "y": 0 294 | }, 295 | "id": 9, 296 | "options": { 297 | "colorMode": "value", 298 | "graphMode": "area", 299 | "justifyMode": "auto", 300 | "orientation": "auto", 301 | "reduceOptions": { 302 | "calcs": [ 303 | "lastNotNull" 304 | ], 305 | "fields": "", 306 | "values": false 307 | }, 308 | "text": {}, 309 | "textMode": "auto" 310 | }, 311 | "pluginVersion": "8.3.2", 312 | "targets": [ 313 | { 314 | "exemplar": true, 315 | "expr": "sum(starboard_exporter_vulnerabilityreport_image_vulnerability_severity_count{severity=\"LOW\"})", 316 | "interval": "", 317 | "legendFormat": "", 318 | "refId": "A" 319 | } 320 | ], 321 | "title": "Low CVEs", 322 | "type": "stat" 323 | }, 324 | { 325 | "datasource": { 326 | "uid": "$datasource" 327 | }, 328 | "description": "", 329 | "fieldConfig": { 330 | "defaults": { 331 | "color": { 332 | "mode": "thresholds" 333 | }, 334 | "mappings": [], 335 | "thresholds": { 336 | "mode": "absolute", 337 | "steps": [ 338 | { 339 | "color": "green", 340 | "value": null 341 | }, 342 | { 343 | "color": "red", 344 | "value": 80 345 | } 346 | ] 347 | } 348 | }, 349 | "overrides": [] 350 | }, 351 | "gridPos": { 352 | "h": 7, 353 | "w": 4, 354 | "x": 16, 355 | "y": 0 356 | }, 357 | "id": 10, 358 | "options": { 359 | "colorMode": "value", 360 | "graphMode": "area", 361 | "justifyMode": "auto", 362 | "orientation": "auto", 363 | "reduceOptions": { 364 | "calcs": [ 365 | "lastNotNull" 366 | ], 367 | "fields": "", 368 | "values": false 369 | }, 370 | "text": {}, 371 | "textMode": "auto" 372 | }, 373 | "pluginVersion": "8.3.2", 374 | "targets": [ 375 | { 376 | "exemplar": true, 377 | "expr": "sum(starboard_exporter_vulnerabilityreport_image_vulnerability_severity_count{severity=\"NONE\"})", 378 | "interval": "", 379 | "legendFormat": "", 380 | "refId": "A" 381 | } 382 | ], 383 | "title": "None CVEs", 384 | "type": "stat" 385 | }, 386 | { 387 | "datasource": { 388 | "uid": "$datasource" 389 | }, 390 | "description": "", 391 | "fieldConfig": { 392 | "defaults": { 393 | "color": { 394 | "mode": "thresholds" 395 | }, 396 | "mappings": [], 397 | "thresholds": { 398 | "mode": "absolute", 399 | "steps": [ 400 | { 401 | "color": "green", 402 | "value": null 403 | }, 404 | { 405 | "color": "red", 406 | "value": 80 407 | } 408 | ] 409 | } 410 | }, 411 | "overrides": [] 412 | }, 413 | "gridPos": { 414 | "h": 7, 415 | "w": 4, 416 | "x": 20, 417 | "y": 0 418 | }, 419 | "id": 11, 420 | "options": { 421 | "colorMode": "value", 422 | "graphMode": "area", 423 | "justifyMode": "auto", 424 | "orientation": "auto", 425 | "reduceOptions": { 426 | "calcs": [ 427 | "lastNotNull" 428 | ], 429 | "fields": "", 430 | "values": false 431 | }, 432 | "text": {}, 433 | "textMode": "auto" 434 | }, 435 | "pluginVersion": "8.3.2", 436 | "targets": [ 437 | { 438 | "exemplar": true, 439 | "expr": "sum(starboard_exporter_vulnerabilityreport_image_vulnerability_severity_count{severity=\"UNKNOWN\"})", 440 | "interval": "", 441 | "legendFormat": "", 442 | "refId": "A" 443 | } 444 | ], 445 | "title": "Unknown CVEs", 446 | "type": "stat" 447 | }, 448 | { 449 | "datasource": { 450 | "uid": "$datasource" 451 | }, 452 | "description": "", 453 | "fieldConfig": { 454 | "defaults": { 455 | "color": { 456 | "mode": "palette-classic" 457 | }, 458 | "custom": { 459 | "axisLabel": "", 460 | "axisPlacement": "auto", 461 | "barAlignment": 0, 462 | "drawStyle": "line", 463 | "fillOpacity": 0, 464 | "gradientMode": "none", 465 | "hideFrom": { 466 | "legend": false, 467 | "tooltip": false, 468 | "viz": false 469 | }, 470 | "lineInterpolation": "linear", 471 | "lineWidth": 1, 472 | "pointSize": 5, 473 | "scaleDistribution": { 474 | "type": "linear" 475 | }, 476 | "showPoints": "auto", 477 | "spanNulls": false, 478 | "stacking": { 479 | "group": "A", 480 | "mode": "none" 481 | }, 482 | "thresholdsStyle": { 483 | "mode": "off" 484 | } 485 | }, 486 | "mappings": [], 487 | "thresholds": { 488 | "mode": "absolute", 489 | "steps": [ 490 | { 491 | "color": "green", 492 | "value": null 493 | }, 494 | { 495 | "color": "red", 496 | "value": 80 497 | } 498 | ] 499 | } 500 | }, 501 | "overrides": [] 502 | }, 503 | "gridPos": { 504 | "h": 16, 505 | "w": 12, 506 | "x": 0, 507 | "y": 7 508 | }, 509 | "id": 4, 510 | "options": { 511 | "legend": { 512 | "calcs": [], 513 | "displayMode": "list", 514 | "placement": "bottom" 515 | }, 516 | "tooltip": { 517 | "mode": "single" 518 | } 519 | }, 520 | "targets": [ 521 | { 522 | "exemplar": true, 523 | "expr": "sum(starboard_exporter_vulnerabilityreport_image_vulnerability_severity_count) by (image_namespace)", 524 | "interval": "", 525 | "legendFormat": "{{"{{image_namespace}}"}}", 526 | "refId": "A" 527 | } 528 | ], 529 | "title": "CVEs per Namespace", 530 | "type": "timeseries" 531 | }, 532 | { 533 | "datasource": { 534 | "uid": "$datasource" 535 | }, 536 | "fieldConfig": { 537 | "defaults": { 538 | "color": { 539 | "mode": "palette-classic" 540 | }, 541 | "custom": { 542 | "axisLabel": "", 543 | "axisPlacement": "auto", 544 | "barAlignment": 0, 545 | "drawStyle": "line", 546 | "fillOpacity": 0, 547 | "gradientMode": "none", 548 | "hideFrom": { 549 | "legend": false, 550 | "tooltip": false, 551 | "viz": false 552 | }, 553 | "lineInterpolation": "linear", 554 | "lineWidth": 1, 555 | "pointSize": 5, 556 | "scaleDistribution": { 557 | "type": "linear" 558 | }, 559 | "showPoints": "auto", 560 | "spanNulls": false, 561 | "stacking": { 562 | "group": "A", 563 | "mode": "none" 564 | }, 565 | "thresholdsStyle": { 566 | "mode": "off" 567 | } 568 | }, 569 | "mappings": [], 570 | "thresholds": { 571 | "mode": "absolute", 572 | "steps": [ 573 | { 574 | "color": "green", 575 | "value": null 576 | }, 577 | { 578 | "color": "red", 579 | "value": 80 580 | } 581 | ] 582 | } 583 | }, 584 | "overrides": [] 585 | }, 586 | "gridPos": { 587 | "h": 9, 588 | "w": 12, 589 | "x": 12, 590 | "y": 7 591 | }, 592 | "id": 2, 593 | "options": { 594 | "legend": { 595 | "calcs": [], 596 | "displayMode": "list", 597 | "placement": "bottom" 598 | }, 599 | "tooltip": { 600 | "mode": "single" 601 | } 602 | }, 603 | "targets": [ 604 | { 605 | "exemplar": true, 606 | "expr": "sum(starboard_exporter_vulnerabilityreport_image_vulnerability_severity_count) by (severity)", 607 | "interval": "", 608 | "legendFormat": "{{"{{severity}}"}}", 609 | "refId": "A" 610 | } 611 | ], 612 | "title": "Cluster CVEs by Severity", 613 | "type": "timeseries" 614 | }, 615 | { 616 | "datasource": { 617 | "type": "prometheus", 618 | "uid": "$datasource" 619 | }, 620 | "fieldConfig": { 621 | "defaults": { 622 | "color": { 623 | "mode": "thresholds" 624 | }, 625 | "custom": { 626 | "align": "auto", 627 | "displayMode": "auto" 628 | }, 629 | "mappings": [], 630 | "thresholds": { 631 | "mode": "absolute", 632 | "steps": [ 633 | { 634 | "color": "green", 635 | "value": null 636 | }, 637 | { 638 | "color": "red", 639 | "value": 80 640 | } 641 | ] 642 | } 643 | }, 644 | "overrides": [] 645 | }, 646 | "gridPos": { 647 | "h": 7, 648 | "w": 12, 649 | "x": 12, 650 | "y": 16 651 | }, 652 | "id": 13, 653 | "options": { 654 | "footer": { 655 | "fields": "", 656 | "reducer": [ 657 | "sum" 658 | ], 659 | "show": false 660 | }, 661 | "showHeader": true 662 | }, 663 | "pluginVersion": "8.3.2", 664 | "targets": [ 665 | { 666 | "datasource": { 667 | "type": "prometheus", 668 | "uid": "$datasource" 669 | }, 670 | "exemplar": false, 671 | "expr": "topk(10, sum(starboard_exporter_vulnerabilityreport_image_vulnerability_severity_count{severity=~\"CRITICAL|HIGH\"}) by (image_registry, image_repository, image_tag))", 672 | "format": "table", 673 | "instant": true, 674 | "interval": "", 675 | "legendFormat": "", 676 | "refId": "A" 677 | } 678 | ], 679 | "title": "Pods with Critical/High CVEs", 680 | "transformations": [ 681 | { 682 | "id": "organize", 683 | "options": { 684 | "excludeByName": { 685 | "Time": true 686 | }, 687 | "indexByName": {}, 688 | "renameByName": {} 689 | } 690 | } 691 | ], 692 | "type": "table" 693 | } 694 | ], 695 | "refresh": "10s", 696 | "schemaVersion": 33, 697 | "style": "dark", 698 | "templating": { 699 | "list": [ 700 | { 701 | "current": { 702 | "selected": false, 703 | "text": "default", 704 | "value": "default" 705 | }, 706 | "hide": 0, 707 | "includeAll": false, 708 | "label": "Datasource", 709 | "multi": false, 710 | "name": "datasource", 711 | "options": [], 712 | "query": "prometheus", 713 | "queryValue": "", 714 | "refresh": 1, 715 | "regex": "", 716 | "skipUrlSync": false, 717 | "type": "datasource" 718 | } 719 | ] 720 | }, 721 | "time": { 722 | "from": "now-1h", 723 | "to": "now" 724 | }, 725 | "timepicker": {}, 726 | "timezone": "", 727 | "title": "Security: Starboard Dashboard", 728 | "uid": "CK_Th9c7k", 729 | "version": 1, 730 | "weekStart": "" 731 | } 732 | {{- end -}} 733 | --------------------------------------------------------------------------------