├── .dockerignore ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── build-docker.yaml │ ├── ci-docker.yaml │ ├── release-assets.yaml │ ├── release-docker.yaml │ └── schedule-docker.yaml ├── .gitignore ├── .golangci.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── common.logger.go ├── common.system.go ├── config └── opts.go ├── go.mod ├── go.sum ├── main.go └── metrics.keyvault.go /.dockerignore: -------------------------------------------------------------------------------- 1 | /azure-keyvault-exporter 2 | /release-assets 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | end_of_line = lf 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [Makefile] 15 | indent_style = tab 16 | 17 | [{*.yml,*.yaml}] 18 | indent_size = 2 19 | 20 | [*.conf] 21 | indent_size = 2 22 | 23 | [*.go] 24 | indent_size = 4 25 | indent_style = tab 26 | ij_continuation_indent_size = 4 27 | ij_go_GROUP_CURRENT_PROJECT_IMPORTS = true 28 | ij_go_add_leading_space_to_comments = true 29 | ij_go_add_parentheses_for_single_import = true 30 | ij_go_call_parameters_new_line_after_left_paren = true 31 | ij_go_call_parameters_right_paren_on_new_line = true 32 | ij_go_call_parameters_wrap = off 33 | ij_go_fill_paragraph_width = 80 34 | ij_go_group_stdlib_imports = true 35 | ij_go_import_sorting = goimports 36 | ij_go_keep_indents_on_empty_lines = false 37 | ij_go_local_group_mode = project 38 | ij_go_move_all_imports_in_one_declaration = true 39 | ij_go_move_all_stdlib_imports_in_one_group = true 40 | ij_go_remove_redundant_import_aliases = false 41 | ij_go_run_go_fmt_on_reformat = true 42 | ij_go_use_back_quotes_for_imports = false 43 | ij_go_wrap_comp_lit = off 44 | ij_go_wrap_comp_lit_newline_after_lbrace = true 45 | ij_go_wrap_comp_lit_newline_before_rbrace = true 46 | ij_go_wrap_func_params = off 47 | ij_go_wrap_func_params_newline_after_lparen = true 48 | ij_go_wrap_func_params_newline_before_rparen = true 49 | ij_go_wrap_func_result = off 50 | ij_go_wrap_func_result_newline_after_lparen = true 51 | ij_go_wrap_func_result_newline_before_rparen = true 52 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | groups: 8 | all-github-actions: 9 | patterns: [ "*" ] 10 | 11 | - package-ecosystem: "docker" 12 | directory: "/" 13 | schedule: 14 | interval: "weekly" 15 | groups: 16 | all-docker-versions: 17 | patterns: [ "*" ] 18 | 19 | - package-ecosystem: "gomod" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | groups: 24 | all-go-mod-patch-and-minor: 25 | patterns: [ "*" ] 26 | update-types: [ "patch", "minor" ] 27 | -------------------------------------------------------------------------------- /.github/workflows/build-docker.yaml: -------------------------------------------------------------------------------- 1 | name: build/docker 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | publish: 7 | required: true 8 | type: boolean 9 | 10 | jobs: 11 | lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | 16 | - name: Set Swap Space 17 | uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c 18 | with: 19 | swap-size-gb: 12 20 | 21 | - uses: actions/setup-go@v5 22 | with: 23 | go-version-file: 'go.mod' 24 | cache-dependency-path: "go.sum" 25 | check-latest: true 26 | 27 | - name: Run Golangci lint 28 | uses: golangci/golangci-lint-action@v7 29 | with: 30 | version: latest 31 | args: --print-resources-usage 32 | 33 | build: 34 | name: "build ${{ matrix.Dockerfile }}:${{ matrix.target }}" 35 | needs: lint 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | include: 40 | - Dockerfile: Dockerfile 41 | target: "final-static" 42 | suffix: "" 43 | latest: "auto" 44 | 45 | runs-on: ubuntu-latest 46 | steps: 47 | - uses: actions/checkout@v4 48 | 49 | - name: Set Swap Space 50 | uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c 51 | with: 52 | swap-size-gb: 12 53 | 54 | - uses: actions/setup-go@v5 55 | with: 56 | go-version-file: 'go.mod' 57 | cache-dependency-path: "go.sum" 58 | check-latest: true 59 | 60 | - name: Docker meta 61 | id: docker_meta 62 | uses: docker/metadata-action@v5 63 | with: 64 | images: | 65 | ${{ github.repository }} 66 | quay.io/${{ github.repository }} 67 | labels: | 68 | io.artifacthub.package.readme-url=https://raw.githubusercontent.com/${{ github.repository }}/${{ github.event.repository.default_branch }}/README.md 69 | flavor: | 70 | latest=${{ matrix.latest }} 71 | suffix=${{ matrix.suffix }} 72 | 73 | - name: Set up QEMU 74 | uses: docker/setup-qemu-action@v3 75 | 76 | - name: Set up Docker Buildx 77 | uses: docker/setup-buildx-action@v3 78 | 79 | - name: Login to DockerHub 80 | uses: docker/login-action@v3 81 | if: ${{ inputs.publish }} 82 | with: 83 | username: ${{ secrets.DOCKERHUB_USERNAME }} 84 | password: ${{ secrets.DOCKERHUB_TOKEN }} 85 | 86 | - name: Login to Quay 87 | uses: docker/login-action@v3 88 | if: ${{ inputs.publish }} 89 | with: 90 | registry: quay.io 91 | username: ${{ secrets.QUAY_USERNAME }} 92 | password: ${{ secrets.QUAY_TOKEN }} 93 | 94 | - name: ${{ inputs.publish && 'Build and push' || 'Build' }} 95 | uses: docker/build-push-action@v6 96 | with: 97 | context: . 98 | file: ./${{ matrix.Dockerfile }} 99 | target: ${{ matrix.target }} 100 | platforms: linux/amd64,linux/arm64 101 | push: ${{ inputs.publish }} 102 | tags: ${{ steps.docker_meta.outputs.tags }} 103 | labels: ${{ steps.docker_meta.outputs.labels }} 104 | -------------------------------------------------------------------------------- /.github/workflows/ci-docker.yaml: -------------------------------------------------------------------------------- 1 | name: "ci/docker" 2 | 3 | on: [pull_request, workflow_dispatch] 4 | 5 | jobs: 6 | build: 7 | uses: ./.github/workflows/build-docker.yaml 8 | secrets: inherit 9 | with: 10 | publish: false 11 | -------------------------------------------------------------------------------- /.github/workflows/release-assets.yaml: -------------------------------------------------------------------------------- 1 | name: "release/assets" 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Set Swap Space 14 | uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c 15 | with: 16 | swap-size-gb: 12 17 | 18 | - uses: actions/setup-go@v5 19 | with: 20 | go-version-file: 'go.mod' 21 | cache-dependency-path: "go.sum" 22 | check-latest: true 23 | 24 | - name: Build 25 | run: | 26 | make release-assets 27 | 28 | - name: Upload assets to release 29 | uses: svenstaro/upload-release-action@v2 30 | with: 31 | repo_token: ${{ secrets.GITHUB_TOKEN }} 32 | file: ./release-assets/* 33 | tag: ${{ github.ref }} 34 | overwrite: true 35 | file_glob: true 36 | -------------------------------------------------------------------------------- /.github/workflows/release-docker.yaml: -------------------------------------------------------------------------------- 1 | name: "release/docker" 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | - 'feature-**' 8 | - 'bugfix-**' 9 | tags: 10 | - '*.*.*' 11 | 12 | jobs: 13 | release: 14 | uses: ./.github/workflows/build-docker.yaml 15 | secrets: inherit 16 | with: 17 | publish: ${{ github.event_name != 'pull_request' }} 18 | -------------------------------------------------------------------------------- /.github/workflows/schedule-docker.yaml: -------------------------------------------------------------------------------- 1 | name: "schedule/docker" 2 | 3 | on: 4 | schedule: 5 | - cron: '45 6 * * 1' 6 | 7 | jobs: 8 | schedule: 9 | uses: ./.github/workflows/build-docker.yaml 10 | secrets: inherit 11 | with: 12 | publish: true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /azure-keyvault-exporter 3 | /release-assets 4 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - asciicheck 5 | - bidichk 6 | - bodyclose 7 | - copyloopvar 8 | - errorlint 9 | - gomodguard 10 | - gosec 11 | settings: 12 | gomodguard: 13 | blocked: 14 | modules: 15 | - github.com/Azure/go-autorest/autorest/azure/auth: 16 | reason: deprecated 17 | gosec: 18 | confidence: low 19 | config: 20 | global: 21 | audit: true 22 | exclusions: 23 | generated: lax 24 | presets: 25 | - comments 26 | - common-false-positives 27 | - legacy 28 | - std-error-handling 29 | paths: 30 | - third_party$ 31 | - builtin$ 32 | - examples$ 33 | formatters: 34 | enable: 35 | - gofmt 36 | - goimports 37 | exclusions: 38 | generated: lax 39 | paths: 40 | - third_party$ 41 | - builtin$ 42 | - examples$ 43 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ############################################# 2 | # Build 3 | ############################################# 4 | FROM --platform=$BUILDPLATFORM golang:1.24-alpine AS build 5 | 6 | RUN apk upgrade --no-cache --force 7 | RUN apk add --update build-base make git 8 | 9 | WORKDIR /go/src/github.com/webdevops/azure-keyvault-exporter 10 | 11 | # Dependencies 12 | COPY go.mod go.sum . 13 | RUN go mod download 14 | 15 | # Compile 16 | COPY . . 17 | RUN make test 18 | ARG TARGETOS TARGETARCH 19 | RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} make build 20 | 21 | ############################################# 22 | # Test 23 | ############################################# 24 | FROM gcr.io/distroless/static AS test 25 | USER 0:0 26 | WORKDIR /app 27 | COPY --from=build /go/src/github.com/webdevops/azure-keyvault-exporter/azure-keyvault-exporter . 28 | RUN ["./azure-keyvault-exporter", "--help"] 29 | 30 | ############################################# 31 | # Final 32 | ############################################# 33 | FROM gcr.io/distroless/static AS final-static 34 | ENV LOG_JSON=1 35 | WORKDIR / 36 | COPY --from=test /app . 37 | USER 1000:1000 38 | ENTRYPOINT ["/azure-keyvault-exporter"] 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 WebDevOps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME := $(shell basename $(CURDIR)) 2 | GIT_TAG := $(shell git describe --dirty --tags --always) 3 | GIT_COMMIT := $(shell git rev-parse --short HEAD) 4 | LDFLAGS := -X "main.gitTag=$(GIT_TAG)" -X "main.gitCommit=$(GIT_COMMIT)" -extldflags "-static" -s -w 5 | 6 | FIRST_GOPATH := $(firstword $(subst :, ,$(shell go env GOPATH))) 7 | GOLANGCI_LINT_BIN := $(FIRST_GOPATH)/bin/golangci-lint 8 | 9 | .PHONY: all 10 | all: vendor build 11 | 12 | .PHONY: clean 13 | clean: 14 | git clean -Xfd . 15 | 16 | ####################################### 17 | # builds 18 | ####################################### 19 | 20 | .PHONY: vendor 21 | vendor: 22 | go mod tidy 23 | go mod vendor 24 | go mod verify 25 | 26 | .PHONY: build-all 27 | build-all: 28 | GOOS=linux GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o '$(PROJECT_NAME)' . 29 | GOOS=darwin GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o '$(PROJECT_NAME).darwin' . 30 | GOOS=windows GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o '$(PROJECT_NAME).exe' . 31 | 32 | .PHONY: build 33 | build: 34 | GOOS=${GOOS} GOARCH=${GOARCH} CGO_ENABLED=0 go build -ldflags '$(LDFLAGS)' -o $(PROJECT_NAME) . 35 | 36 | .PHONY: image 37 | image: image 38 | docker build -t $(PROJECT_NAME):$(GIT_TAG) . 39 | 40 | .PHONY: build-push-development 41 | build-push-development: 42 | docker buildx create --use 43 | docker buildx build -t webdevops/$(PROJECT_NAME):development --platform linux/amd64,linux/arm,linux/arm64 --push . 44 | 45 | ####################################### 46 | # quality checks 47 | ####################################### 48 | 49 | .PHONY: check 50 | check: vendor lint test 51 | 52 | .PHONY: test 53 | test: 54 | time go test ./... 55 | 56 | .PHONY: lint 57 | lint: $(GOLANGCI_LINT_BIN) 58 | time $(GOLANGCI_LINT_BIN) run --verbose --print-resources-usage 59 | 60 | $(GOLANGCI_LINT_BIN): 61 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(FIRST_GOPATH)/bin 62 | 63 | ####################################### 64 | # release assets 65 | ####################################### 66 | 67 | RELEASE_ASSETS = \ 68 | $(foreach GOARCH,amd64 arm64,\ 69 | $(foreach GOOS,linux darwin windows,\ 70 | release-assets/$(GOOS).$(GOARCH))) \ 71 | 72 | word-dot = $(word $2,$(subst ., ,$1)) 73 | 74 | .PHONY: release-assets 75 | release-assets: clean-release-assets vendor $(RELEASE_ASSETS) 76 | 77 | .PHONY: clean-release-assets 78 | clean-release-assets: 79 | rm -rf ./release-assets 80 | mkdir -p ./release-assets 81 | 82 | release-assets/windows.%: $(SOURCE) 83 | echo 'build release-assets for windows/$(call word-dot,$*,2)' 84 | GOOS=windows \ 85 | GOARCH=$(call word-dot,$*,1) \ 86 | CGO_ENABLED=0 \ 87 | time go build -ldflags '$(LDFLAGS)' -o './release-assets/$(PROJECT_NAME).windows.$(call word-dot,$*,1).exe' . 88 | 89 | release-assets/%: $(SOURCE) 90 | echo 'build release-assets for $(call word-dot,$*,1)/$(call word-dot,$*,2)' 91 | GOOS=$(call word-dot,$*,1) \ 92 | GOARCH=$(call word-dot,$*,2) \ 93 | CGO_ENABLED=0 \ 94 | time go build -ldflags '$(LDFLAGS)' -o './release-assets/$(PROJECT_NAME).$(call word-dot,$*,1).$(call word-dot,$*,2)' . 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Azure Keyvault Exporter 2 | 3 | [![license](https://img.shields.io/github/license/webdevops/azure-keyvault-exporter.svg)](https://github.com/webdevops/azure-keyvault-exporter/blob/master/LICENSE) 4 | [![DockerHub](https://img.shields.io/badge/DockerHub-webdevops%2Fazure--keyvault--exporter-blue)](https://hub.docker.com/r/webdevops/azure-keyvault-exporter/) 5 | [![Quay.io](https://img.shields.io/badge/Quay.io-webdevops%2Fazure--keyvault--exporter-blue)](https://quay.io/repository/webdevops/azure-keyvault-exporter) 6 | [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/azure-keyvault-exporter)](https://artifacthub.io/packages/search?repo=azure-keyvault-exporter) 7 | 8 | Prometheus exporter for Azure KeyVaults which exports general information and dates (created, updated, notBefore, expiry) for secrets, keys and certificates. 9 | 10 | ## Configuration 11 | 12 | ``` 13 | Usage: 14 | azure-keyvault-exporter [OPTIONS] 15 | 16 | Application Options: 17 | --log.debug debug mode [$LOG_DEBUG] 18 | --log.devel development mode [$LOG_DEVEL] 19 | --log.json Switch log output to json format [$LOG_JSON] 20 | --azure.environment= Azure environment name (default: AZUREPUBLICCLOUD) [$AZURE_ENVIRONMENT] 21 | --azure.subscription= Azure subscription ID (space delimiter) [$AZURE_SUBSCRIPTION_ID] 22 | --azure.resource-tag= Azure Resource tags (space delimiter) (default: owner) [$AZURE_RESOURCE_TAG] 23 | --keyvault.filter= Filter KeyVaults via ResourceGraph kusto filter, query: 'resource | ${filter} | project id' [$KEYVAULT_FILTER] 24 | --keyvault.content.tag= KeyVault content (secret, key, certificates) tags (space delimiter) [$KEYVAULT_CONTENT_TAG] 25 | --cache.path= Cache path (to folder, file://path... or azblob://storageaccount.blob.core.windows.net/containername) 26 | [$CACHE_PATH] 27 | --scrape.time= Default scrape time (time.duration) (default: 5m) [$SCRAPE_TIME] 28 | --scrape.concurrency= Defines who many Keyvaults can be scraped at the same time (default: 10) [$SCRAPE_CONCURRENCY] 29 | --server.bind= Server address (default: :8080) [$SERVER_BIND] 30 | --server.timeout.read= Server read timeout (default: 5s) [$SERVER_TIMEOUT_READ] 31 | --server.timeout.write= Server write timeout (default: 10s) [$SERVER_TIMEOUT_WRITE] 32 | 33 | Help Options: 34 | -h, --help Show this help message 35 | ``` 36 | 37 | for Azure API authentication (using ENV vars) see following documentations: 38 | - https://github.com/webdevops/go-common/blob/main/azuresdk/README.md 39 | - https://docs.microsoft.com/en-us/azure/developer/go/azure-sdk-authentication 40 | 41 | ## Metrics 42 | 43 | | Metric | Description | 44 | |----------------------------------------|---------------------------------------------------------------------| 45 | | `azurerm_keyvault_info` | Azure KeyVault information | 46 | | `azurerm_keyvault_status` | Azure KeyVault status information (eg. if accessable from exporter) | 47 | | `azurerm_keyvault_entries` | Count of entries (seperated by type) inside Azure KeyVault | 48 | | `azurerm_keyvault_key_info` | General inforamtions about keys | 49 | | `azurerm_keyvault_key_status` | Status information (notBefore & expiry date) | 50 | | `azurerm_keyvault_secret_info` | General inforamtions about secrets | 51 | | `azurerm_keyvault_secret_status` | Status information (notBefore & expiry date) | 52 | | `azurerm_keyvault_certificate_info` | General inforamtions about certificate | 53 | | `azurerm_keyvault_certificate_status` | Status information (notBefore & expiry date) | 54 | 55 | ### ResourceTags handling 56 | 57 | see [armclient tagmanager documentation](https://github.com/webdevops/go-common/blob/main/azuresdk/README.md#tag-manager) 58 | 59 | ### AzureTracing metrics 60 | 61 | see [armclient tracing documentation](https://github.com/webdevops/go-common/blob/main/azuresdk/README.md#azuretracing-metrics) 62 | 63 | ### Caching 64 | 65 | see [prometheus collector cache documentation](https://github.com/webdevops/go-common/blob/main/prometheus/README.md#caching) 66 | 67 | -------------------------------------------------------------------------------- /common.logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | ) 7 | 8 | var ( 9 | logger *zap.SugaredLogger 10 | ) 11 | 12 | func initLogger() *zap.SugaredLogger { 13 | var config zap.Config 14 | if Opts.Logger.Development { 15 | config = zap.NewDevelopmentConfig() 16 | config.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 17 | } else { 18 | config = zap.NewProductionConfig() 19 | } 20 | 21 | config.Encoding = "console" 22 | config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 23 | 24 | // debug level 25 | if Opts.Logger.Debug { 26 | config.Level = zap.NewAtomicLevelAt(zapcore.DebugLevel) 27 | } 28 | 29 | // json log format 30 | if Opts.Logger.Json { 31 | config.Encoding = "json" 32 | 33 | // if running in containers, logs already enriched with timestamp by the container runtime 34 | config.EncoderConfig.TimeKey = "" 35 | } 36 | 37 | // build logger 38 | log, err := config.Build() 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | logger = log.Sugar() 44 | 45 | return logger 46 | } 47 | -------------------------------------------------------------------------------- /common.system.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/webdevops/go-common/system" 5 | ) 6 | 7 | func initSystem() { 8 | system.AutoProcMemLimit(logger) 9 | } 10 | -------------------------------------------------------------------------------- /config/opts.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type ( 9 | Opts struct { 10 | // logger 11 | Logger struct { 12 | Debug bool `long:"log.debug" env:"LOG_DEBUG" description:"debug mode"` 13 | Development bool `long:"log.devel" env:"LOG_DEVEL" description:"development mode"` 14 | Json bool `long:"log.json" env:"LOG_JSON" description:"Switch log output to json format"` 15 | } 16 | 17 | // azure 18 | Azure struct { 19 | Environment *string `long:"azure.environment" env:"AZURE_ENVIRONMENT" description:"Azure environment name" default:"AZUREPUBLICCLOUD"` 20 | Subscription []string `long:"azure.subscription" env:"AZURE_SUBSCRIPTION_ID" env-delim:" " description:"Azure subscription ID (space delimiter)"` 21 | ResourceTags []string `long:"azure.resource-tag" env:"AZURE_RESOURCE_TAG" env-delim:" " description:"Azure Resource tags (space delimiter)" default:"owner"` 22 | } 23 | 24 | KeyVault struct { 25 | Filter string `long:"keyvault.filter" env:"KEYVAULT_FILTER" description:"Filter KeyVaults via ResourceGraph kusto filter, query: 'resource | ${filter} | project id'"` 26 | Content struct { 27 | Tags []string `long:"keyvault.content.tag" env:"KEYVAULT_CONTENT_TAG" env-delim:" " description:"KeyVault content (secret, key, certificates) tags (space delimiter)"` 28 | } 29 | } 30 | 31 | // caching 32 | Cache struct { 33 | Path string `long:"cache.path" env:"CACHE_PATH" description:"Cache path (to folder, file://path... or azblob://storageaccount.blob.core.windows.net/containername)"` 34 | } 35 | 36 | // scrape times 37 | Scrape struct { 38 | Time time.Duration `long:"scrape.time" env:"SCRAPE_TIME" description:"Default scrape time (time.duration)" default:"5m"` 39 | Concurrency int `long:"scrape.concurrency" env:"SCRAPE_CONCURRENCY" description:"Defines who many Keyvaults can be scraped at the same time" default:"10"` 40 | } 41 | 42 | Server struct { 43 | // general options 44 | Bind string `long:"server.bind" env:"SERVER_BIND" description:"Server address" default:":8080"` 45 | ReadTimeout time.Duration `long:"server.timeout.read" env:"SERVER_TIMEOUT_READ" description:"Server read timeout" default:"5s"` 46 | WriteTimeout time.Duration `long:"server.timeout.write" env:"SERVER_TIMEOUT_WRITE" description:"Server write timeout" default:"10s"` 47 | } 48 | } 49 | ) 50 | 51 | func (o *Opts) GetCachePath(path string) (ret *string) { 52 | if o.Cache.Path != "" { 53 | tmp := o.Cache.Path + "/" + path 54 | ret = &tmp 55 | } 56 | 57 | return 58 | } 59 | 60 | func (o *Opts) GetJson() []byte { 61 | jsonBytes, err := json.Marshal(o) 62 | if err != nil { 63 | panic(err) 64 | } 65 | return jsonBytes 66 | } 67 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/webdevops/azure-keyvault-exporter 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0 9 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 10 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.3.1 11 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 12 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 13 | github.com/jessevdk/go-flags v1.6.1 14 | github.com/prometheus/client_golang v1.22.0 15 | github.com/webdevops/go-common v0.0.0-20250501163022-5a5152626efb 16 | go.uber.org/zap v1.27.0 17 | go.uber.org/zap/exp v0.3.0 18 | ) 19 | 20 | require ( 21 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect 22 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 // indirect 23 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect 24 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect 25 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 // indirect 26 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect 27 | github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 // indirect 28 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect 29 | github.com/KimMachineGun/automemlimit v0.7.1 // indirect 30 | github.com/beorn7/perks v1.0.1 // indirect 31 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 32 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 33 | github.com/dustin/go-humanize v1.0.1 // indirect 34 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 35 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 36 | github.com/go-logr/logr v1.4.2 // indirect 37 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 38 | github.com/go-openapi/jsonreference v0.21.0 // indirect 39 | github.com/go-openapi/swag v0.23.1 // indirect 40 | github.com/gogo/protobuf v1.3.2 // indirect 41 | github.com/golang-jwt/jwt/v5 v5.2.2 // indirect 42 | github.com/google/gnostic-models v0.6.9 // indirect 43 | github.com/google/go-cmp v0.7.0 // indirect 44 | github.com/google/uuid v1.6.0 // indirect 45 | github.com/josharian/intern v1.0.0 // indirect 46 | github.com/json-iterator/go v1.1.12 // indirect 47 | github.com/kylelemons/godebug v1.1.0 // indirect 48 | github.com/mailru/easyjson v0.9.0 // indirect 49 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 50 | github.com/modern-go/reflect2 v1.0.2 // indirect 51 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 52 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 53 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect 54 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect 55 | github.com/pkg/errors v0.9.1 // indirect 56 | github.com/prometheus/client_model v0.6.2 // indirect 57 | github.com/prometheus/common v0.63.0 // indirect 58 | github.com/prometheus/procfs v0.16.1 // indirect 59 | github.com/remeh/sizedwaitgroup v1.0.0 // indirect 60 | github.com/robfig/cron v1.2.0 // indirect 61 | github.com/x448/float16 v0.8.4 // indirect 62 | go.uber.org/automaxprocs v1.6.0 // indirect 63 | go.uber.org/multierr v1.11.0 // indirect 64 | golang.org/x/crypto v0.37.0 // indirect 65 | golang.org/x/net v0.39.0 // indirect 66 | golang.org/x/oauth2 v0.29.0 // indirect 67 | golang.org/x/sys v0.32.0 // indirect 68 | golang.org/x/term v0.31.0 // indirect 69 | golang.org/x/text v0.24.0 // indirect 70 | golang.org/x/time v0.11.0 // indirect 71 | google.golang.org/protobuf v1.36.6 // indirect 72 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 73 | gopkg.in/inf.v0 v0.9.1 // indirect 74 | gopkg.in/yaml.v3 v3.0.1 // indirect 75 | k8s.io/api v0.33.0 // indirect 76 | k8s.io/apimachinery v0.33.0 // indirect 77 | k8s.io/client-go v0.33.0 // indirect 78 | k8s.io/klog/v2 v2.130.1 // indirect 79 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 80 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect 81 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 82 | sigs.k8s.io/randfill v1.0.0 // indirect 83 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 84 | sigs.k8s.io/yaml v1.4.0 // indirect 85 | ) 86 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= 2 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= 3 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0 h1:OVoM452qUFBrX+URdH3VpR299ma4kfom0yB0URYky9g= 4 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.9.0/go.mod h1:kUjrAo8bgEwLeZ/CmHqNl3Z/kPm7y6FKfxxK0izYUg4= 5 | github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= 6 | github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= 7 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= 8 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= 9 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do= 10 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI= 11 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw= 12 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38= 13 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0 h1:nnQ9vXH039UrEFxi08pPuZBE7VfqSJt343uJLw0rhWI= 14 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault v1.5.0/go.mod h1:4YIVtzMFVsPwBvitCDX7J9sqthSj43QD1sP6fYc1egc= 15 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o= 16 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0= 17 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 h1:zLzoX5+W2l95UJoVwiyNS4dX8vHyQ6x2xRLoBBL9wMk= 18 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk= 19 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM= 20 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE= 21 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0 h1:wxQx2Bt4xzPIKvW59WQf1tJNx/ZZKPfN+EhPX3Z6CYY= 22 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0/go.mod h1:TpiwjwnW/khS0LKs4vW5UmmT9OWcxaveS8U7+tlknzo= 23 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0 h1:LR0kAX9ykz8G4YgLCaRDVJ3+n43R8MneB5dTy2konZo= 24 | github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.0/go.mod h1:DWAciXemNf++PQJLeXUB4HHH5OpsAh12HZnu2wXE1jA= 25 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.3.1 h1:HUJQzFYTv7t3V1dxPms52eEgl0l9xCNqutDrY45Lvmw= 26 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.3.1/go.mod h1:ig/8nSkzmfxm5QGeIy5JYIEj8JEFy5JxvY3OB1YNRC4= 27 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w= 28 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14= 29 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 h1:mrkDCdkMsD4l9wjFGhofFHFrV43Y3c53RSLKOCJ5+Ow= 30 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1/go.mod h1:hPv41DbqMmnxcGralanA/kVlfdH5jv3T4LxGku2E1BY= 31 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= 32 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= 33 | github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1 h1:lhZdRq7TIx0GJQvSyX2Si406vrYsov2FXGp/RnSEtcs= 34 | github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.1/go.mod h1:8cl44BDmi+effbARHMQjgOKA2AYvcohNm7KEt42mSV8= 35 | github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= 36 | github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= 37 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= 38 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 39 | github.com/KimMachineGun/automemlimit v0.7.1 h1:QcG/0iCOLChjfUweIMC3YL5Xy9C3VBeNmCZHrZfJMBw= 40 | github.com/KimMachineGun/automemlimit v0.7.1/go.mod h1:QZxpHaGOQoYvFhv/r4u3U0JTC2ZcOwbSr11UZF46UBM= 41 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 42 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 43 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 44 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 45 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 46 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 47 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 48 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 49 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 50 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 51 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 52 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 53 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 54 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 55 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 56 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 57 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 58 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 59 | github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= 60 | github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= 61 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 62 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 63 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 64 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 65 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 66 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 67 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 68 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 69 | github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= 70 | github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 71 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 72 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 73 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 74 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 75 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 76 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 77 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 78 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 79 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 80 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 81 | github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= 82 | github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= 83 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 84 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 85 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 86 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 87 | github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= 88 | github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= 89 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 90 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 91 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 92 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 93 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 94 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 95 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 96 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 97 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 98 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 99 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 100 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 101 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 102 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 103 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 104 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 105 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 106 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 107 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 108 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 109 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 110 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 111 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 112 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 113 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 114 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 115 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 116 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 117 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 118 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 119 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 120 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 121 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 122 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 123 | github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= 124 | github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= 125 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 126 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 127 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 128 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 129 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 130 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 131 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 132 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 133 | github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= 134 | github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= 135 | github.com/remeh/sizedwaitgroup v1.0.0 h1:VNGGFwNo/R5+MJBf6yrsr110p0m4/OX4S3DCy7Kyl5E= 136 | github.com/remeh/sizedwaitgroup v1.0.0/go.mod h1:3j2R4OIe/SeS6YDhICBy22RWjJC5eNCJ1V+9+NVNYlo= 137 | github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= 138 | github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= 139 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 140 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 141 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 142 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 143 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 144 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 145 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 146 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 147 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 148 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 149 | github.com/webdevops/go-common v0.0.0-20250501163022-5a5152626efb h1:QX8WvRfo9FHM3hAiIh8+V81pu2hxZYcSm9T08ugZK4Q= 150 | github.com/webdevops/go-common v0.0.0-20250501163022-5a5152626efb/go.mod h1:GzD/xLtTZ5Vh3aHTi02g0OlfDUoiDx44OHeUnqWO2CI= 151 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 152 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 153 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 154 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 155 | go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= 156 | go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= 157 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 158 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 159 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 160 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 161 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 162 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 163 | go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U= 164 | go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ= 165 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 166 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 167 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 168 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 169 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 170 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 171 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 172 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 173 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 174 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 175 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 176 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 177 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 178 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 179 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 180 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 181 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 182 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 183 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 184 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 186 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 187 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 188 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 189 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 190 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 191 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 192 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 193 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 194 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 195 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 196 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 197 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 198 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 199 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 200 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 201 | golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= 202 | golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= 203 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 204 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 205 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 206 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 207 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 208 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 209 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 210 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 211 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 212 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 213 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 214 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 215 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 216 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 217 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 218 | k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= 219 | k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= 220 | k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= 221 | k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 222 | k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= 223 | k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= 224 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 225 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 226 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 227 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 228 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= 229 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 230 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 231 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 232 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 233 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 234 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 235 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= 236 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 237 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 238 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 239 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "runtime" 9 | 10 | flags "github.com/jessevdk/go-flags" 11 | "github.com/prometheus/client_golang/prometheus/promhttp" 12 | "github.com/webdevops/go-common/prometheus/collector" 13 | "go.uber.org/zap" 14 | 15 | "github.com/webdevops/go-common/azuresdk/armclient" 16 | "github.com/webdevops/go-common/azuresdk/prometheus/tracing" 17 | 18 | "github.com/webdevops/azure-keyvault-exporter/config" 19 | ) 20 | 21 | const ( 22 | Author = "webdevops.io" 23 | UserAgent = "azure-keyvault-exporter/" 24 | ) 25 | 26 | var ( 27 | argparser *flags.Parser 28 | Opts config.Opts 29 | 30 | AzureClient *armclient.ArmClient 31 | AzureSubscriptionsIterator *armclient.SubscriptionsIterator 32 | AzureResourceTagManager *armclient.ResourceTagManager 33 | 34 | // Git version information 35 | gitCommit = "" 36 | gitTag = "" 37 | 38 | // cache config 39 | cacheTag = "v1" 40 | ) 41 | 42 | func main() { 43 | initArgparser() 44 | initLogger() 45 | 46 | logger.Infof("starting azure-keyvault-exporter v%s (%s; %s; by %v)", gitTag, gitCommit, runtime.Version(), Author) 47 | logger.Info(string(Opts.GetJson())) 48 | initSystem() 49 | 50 | logger.Infof("init Azure connection") 51 | initAzureConnection() 52 | 53 | logger.Infof("starting metrics collection") 54 | initMetricCollector() 55 | 56 | logger.Infof("Starting http server on %s", Opts.Server.Bind) 57 | startHttpServer() 58 | } 59 | 60 | func initArgparser() { 61 | argparser = flags.NewParser(&Opts, flags.Default) 62 | _, err := argparser.Parse() 63 | 64 | // check if there is an parse error 65 | if err != nil { 66 | var flagsErr *flags.Error 67 | if ok := errors.As(err, &flagsErr); ok && flagsErr.Type == flags.ErrHelp { 68 | os.Exit(0) 69 | } else { 70 | fmt.Println() 71 | argparser.WriteHelp(os.Stdout) 72 | os.Exit(1) 73 | } 74 | } 75 | } 76 | 77 | func initAzureConnection() { 78 | var err error 79 | AzureClient, err = armclient.NewArmClientWithCloudName(*Opts.Azure.Environment, logger) 80 | if err != nil { 81 | logger.Fatal(err.Error()) 82 | } 83 | 84 | AzureClient.SetUserAgent(UserAgent + gitTag) 85 | 86 | // limit subscriptions (if filter is set) 87 | if len(Opts.Azure.Subscription) >= 1 { 88 | AzureClient.SetSubscriptionID(Opts.Azure.Subscription...) 89 | } 90 | 91 | // init subscription iterator 92 | AzureSubscriptionsIterator = armclient.NewSubscriptionIterator(AzureClient) 93 | 94 | // init resource tag manager 95 | AzureResourceTagManager, err = AzureClient.TagManager.ParseTagConfig(Opts.Azure.ResourceTags) 96 | if err != nil { 97 | logger.Fatalf(`unable to parse resourceTag configuration "%s": %v"`, Opts.Azure.ResourceTags, err.Error()) 98 | } 99 | } 100 | 101 | func initMetricCollector() { 102 | collectorName := "keyvault" 103 | if Opts.Scrape.Time.Seconds() > 0 { 104 | c := collector.New(collectorName, &MetricsCollectorKeyvault{}, logger) 105 | c.SetScapeTime(Opts.Scrape.Time) 106 | c.SetConcurrency(Opts.Scrape.Concurrency) 107 | c.SetCache( 108 | Opts.GetCachePath(collectorName+".json"), 109 | collector.BuildCacheTag(cacheTag, Opts.Azure, Opts.KeyVault), 110 | ) 111 | if err := c.Start(); err != nil { 112 | logger.Fatal(err.Error()) 113 | } 114 | } else { 115 | logger.With(zap.String("collector", collectorName)).Info("collector disabled") 116 | } 117 | } 118 | 119 | // start and handle prometheus handler 120 | func startHttpServer() { 121 | mux := http.NewServeMux() 122 | 123 | // healthz 124 | mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { 125 | if _, err := fmt.Fprint(w, "Ok"); err != nil { 126 | logger.Error(err) 127 | } 128 | }) 129 | 130 | // readyz 131 | mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { 132 | if _, err := fmt.Fprint(w, "Ok"); err != nil { 133 | logger.Error(err) 134 | } 135 | }) 136 | 137 | mux.Handle("/metrics", tracing.RegisterAzureMetricAutoClean(promhttp.Handler())) 138 | 139 | srv := &http.Server{ 140 | Addr: Opts.Server.Bind, 141 | Handler: mux, 142 | ReadTimeout: Opts.Server.ReadTimeout, 143 | WriteTimeout: Opts.Server.WriteTimeout, 144 | } 145 | logger.Fatal(srv.ListenAndServe()) 146 | } 147 | -------------------------------------------------------------------------------- /metrics.keyvault.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/keyvault/armkeyvault" 10 | "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions" 11 | "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates" 12 | "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys" 13 | "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" 14 | "github.com/prometheus/client_golang/prometheus" 15 | "github.com/webdevops/go-common/azuresdk/armclient" 16 | "github.com/webdevops/go-common/prometheus/collector" 17 | "github.com/webdevops/go-common/utils/to" 18 | "go.uber.org/zap" 19 | ) 20 | 21 | var ( 22 | azureTagNameToPrometheusNameRegExp = regexp.MustCompile("[^_a-zA-Z0-9]") 23 | ) 24 | 25 | type MetricsCollectorKeyvault struct { 26 | collector.Processor 27 | 28 | contentTagManager ContentTagManager 29 | 30 | prometheus struct { 31 | // general 32 | keyvault *prometheus.GaugeVec 33 | keyvaultStatus *prometheus.GaugeVec 34 | keyvaultAccessPolicy *prometheus.GaugeVec 35 | keyvaultEntryCount *prometheus.GaugeVec 36 | 37 | // key 38 | keyvaultKeyInfo *prometheus.GaugeVec 39 | keyvaultKeyStatus *prometheus.GaugeVec 40 | 41 | // secret 42 | keyvaultSecretInfo *prometheus.GaugeVec 43 | keyvaultSecretStatus *prometheus.GaugeVec 44 | 45 | // certs 46 | keyvaultCertificateInfo *prometheus.GaugeVec 47 | keyvaultCertificateStatus *prometheus.GaugeVec 48 | } 49 | } 50 | 51 | type ( 52 | ContentTagManager struct { 53 | config []ContentTagConfig 54 | } 55 | 56 | ContentTagConfig struct { 57 | Label string 58 | Tag string 59 | } 60 | ) 61 | 62 | // AddTag adds tag to configuration 63 | func (ctm *ContentTagManager) AddTag(tagName string) { 64 | labelName := fmt.Sprintf( 65 | "tag_%s", 66 | azureTagNameToPrometheusNameRegExp.ReplaceAllLiteralString(strings.ToLower(tagName), "_"), 67 | ) 68 | 69 | ctm.config = append( 70 | ctm.config, 71 | ContentTagConfig{ 72 | Tag: tagName, 73 | Label: labelName, 74 | }, 75 | ) 76 | } 77 | 78 | // AddContentTags adds content tags to prometheus labels for metric 79 | func (ctm *ContentTagManager) AddContentTags(labels prometheus.Labels, tags map[string]*string) prometheus.Labels { 80 | for _, row := range ctm.config { 81 | // default value 82 | labels[row.Label] = "" 83 | 84 | if val, exists := tags[row.Tag]; exists { 85 | labels[row.Label] = to.String(val) 86 | } 87 | } 88 | 89 | return labels 90 | } 91 | 92 | // AddToPrometheusLabels adds prometheus labels for metric definition 93 | func (ctm *ContentTagManager) AddToPrometheusLabels(val []string) []string { 94 | for _, row := range ctm.config { 95 | val = append(val, row.Label) 96 | } 97 | 98 | return val 99 | } 100 | 101 | func (m *MetricsCollectorKeyvault) Setup(collector *collector.Collector) { 102 | m.Processor.Setup(collector) 103 | 104 | m.contentTagManager = ContentTagManager{ 105 | config: []ContentTagConfig{}, 106 | } 107 | 108 | for _, tagName := range Opts.KeyVault.Content.Tags { 109 | m.contentTagManager.AddTag(tagName) 110 | } 111 | 112 | m.prometheus.keyvault = prometheus.NewGaugeVec( 113 | prometheus.GaugeOpts{ 114 | Name: "azurerm_keyvault_info", 115 | Help: "Azure KeyVault information", 116 | }, 117 | AzureResourceTagManager.AddToPrometheusLabels( 118 | []string{ 119 | "subscriptionID", 120 | "subscriptionName", 121 | "resourceID", 122 | "vaultName", 123 | "location", 124 | "resourceGroup", 125 | }, 126 | ), 127 | ) 128 | m.Collector.RegisterMetricList("keyvault", m.prometheus.keyvault, true) 129 | 130 | m.prometheus.keyvaultStatus = prometheus.NewGaugeVec( 131 | prometheus.GaugeOpts{ 132 | Name: "azurerm_keyvault_status", 133 | Help: "Azure KeyVault status", 134 | }, 135 | []string{ 136 | "resourceID", 137 | "vaultName", 138 | "type", 139 | "scope", 140 | }, 141 | ) 142 | m.Collector.RegisterMetricList("keyvaultStatus", m.prometheus.keyvaultStatus, true) 143 | 144 | m.prometheus.keyvaultEntryCount = prometheus.NewGaugeVec( 145 | prometheus.GaugeOpts{ 146 | Name: "azurerm_keyvault_entries", 147 | Help: "Azure KeyVault entries", 148 | }, 149 | []string{ 150 | "resourceID", 151 | "vaultName", 152 | "type", 153 | }, 154 | ) 155 | m.Collector.RegisterMetricList("keyvaultEntryCount", m.prometheus.keyvaultEntryCount, true) 156 | 157 | // ------------------------------------------ 158 | // key 159 | m.prometheus.keyvaultKeyInfo = prometheus.NewGaugeVec( 160 | prometheus.GaugeOpts{ 161 | Name: "azurerm_keyvault_key_info", 162 | Help: "Azure KeyVault key information", 163 | }, 164 | m.contentTagManager.AddToPrometheusLabels( 165 | []string{ 166 | "resourceID", 167 | "vaultName", 168 | "keyName", 169 | "keyID", 170 | "enabled", 171 | }, 172 | ), 173 | ) 174 | m.Collector.RegisterMetricList("keyvaultKeyInfo", m.prometheus.keyvaultKeyInfo, true) 175 | 176 | m.prometheus.keyvaultKeyStatus = prometheus.NewGaugeVec( 177 | prometheus.GaugeOpts{ 178 | Name: "azurerm_keyvault_key_status", 179 | Help: "Azure KeyVault key status", 180 | }, 181 | []string{ 182 | "resourceID", 183 | "vaultName", 184 | "keyID", 185 | "type", 186 | }, 187 | ) 188 | m.Collector.RegisterMetricList("keyvaultKeyStatus", m.prometheus.keyvaultKeyStatus, true) 189 | 190 | // ------------------------------------------ 191 | // secret 192 | m.prometheus.keyvaultSecretInfo = prometheus.NewGaugeVec( 193 | prometheus.GaugeOpts{ 194 | Name: "azurerm_keyvault_secret_info", 195 | Help: "Azure KeyVault secret information", 196 | }, 197 | m.contentTagManager.AddToPrometheusLabels( 198 | []string{ 199 | "resourceID", 200 | "vaultName", 201 | "secretName", 202 | "secretID", 203 | "enabled", 204 | }, 205 | ), 206 | ) 207 | m.Collector.RegisterMetricList("keyvaultSecretInfo", m.prometheus.keyvaultSecretInfo, true) 208 | 209 | m.prometheus.keyvaultSecretStatus = prometheus.NewGaugeVec( 210 | prometheus.GaugeOpts{ 211 | Name: "azurerm_keyvault_secret_status", 212 | Help: "Azure KeyVault secret status", 213 | }, 214 | []string{ 215 | "resourceID", 216 | "vaultName", 217 | "secretID", 218 | "type", 219 | }, 220 | ) 221 | m.Collector.RegisterMetricList("keyvaultSecretStatus", m.prometheus.keyvaultSecretStatus, true) 222 | 223 | // ------------------------------------------ 224 | // certificate 225 | m.prometheus.keyvaultCertificateInfo = prometheus.NewGaugeVec( 226 | prometheus.GaugeOpts{ 227 | Name: "azurerm_keyvault_certificate_info", 228 | Help: "Azure KeyVault certificate information", 229 | }, 230 | m.contentTagManager.AddToPrometheusLabels( 231 | []string{ 232 | "resourceID", 233 | "vaultName", 234 | "certificateName", 235 | "certificateID", 236 | "enabled", 237 | }, 238 | ), 239 | ) 240 | m.Collector.RegisterMetricList("keyvaultCertificateInfo", m.prometheus.keyvaultCertificateInfo, true) 241 | 242 | m.prometheus.keyvaultCertificateStatus = prometheus.NewGaugeVec( 243 | prometheus.GaugeOpts{ 244 | Name: "azurerm_keyvault_certificate_status", 245 | Help: "Azure KeyVault certificate status", 246 | }, 247 | []string{ 248 | "resourceID", 249 | "vaultName", 250 | "certificateID", 251 | "type", 252 | }, 253 | ) 254 | m.Collector.RegisterMetricList("keyvaultCertificateStatus", m.prometheus.keyvaultCertificateStatus, true) 255 | 256 | } 257 | 258 | func (m *MetricsCollectorKeyvault) Reset() {} 259 | 260 | func (m *MetricsCollectorKeyvault) Collect(callback chan<- func()) { 261 | var filterResourceIdMap *map[string]string 262 | ctx := m.Context() 263 | 264 | if len(Opts.KeyVault.Filter) > 0 { 265 | // get list of subscriptions 266 | subscriptionList, err := AzureSubscriptionsIterator.ListSubscriptions() 267 | if err != nil { 268 | panic(err) 269 | } 270 | 271 | filterSubscriptions := []string{} 272 | for _, subscription := range subscriptionList { 273 | filterSubscriptions = append(filterSubscriptions, *subscription.SubscriptionID) 274 | } 275 | 276 | filters := []string{ 277 | `where type =~ "microsoft.keyvault/vaults"`, 278 | Opts.KeyVault.Filter, 279 | } 280 | 281 | // get list of resourceids based on kusto query 282 | opts := armclient.ResourceGraphOptions{ 283 | Subscriptions: filterSubscriptions, 284 | } 285 | resourceIdMap, err := AzureClient.ListResourceIdsWithKustoFilter(ctx, filters, opts) 286 | if err != nil { 287 | logger.Fatal(err) 288 | } 289 | 290 | filterResourceIdMap = &resourceIdMap 291 | } 292 | 293 | err := AzureSubscriptionsIterator.ForEachAsync(m.Logger(), func(subscription *armsubscriptions.Subscription, logger *zap.SugaredLogger) { 294 | m.collectSubscription(ctx, callback, subscription, logger, filterResourceIdMap) 295 | }) 296 | if err != nil { 297 | m.Logger().Panic(err) 298 | } 299 | } 300 | 301 | func (m *MetricsCollectorKeyvault) collectSubscription(ctx context.Context, callback chan<- func(), subscription *armsubscriptions.Subscription, logger *zap.SugaredLogger, filterResourceIdMap *map[string]string) { 302 | var err error 303 | 304 | keyvaultClient, err := armkeyvault.NewVaultsClient(*subscription.SubscriptionID, AzureClient.GetCred(), AzureClient.NewArmClientOptions()) 305 | if err != nil { 306 | logger.Panic(err) 307 | } 308 | 309 | pager := keyvaultClient.NewListBySubscriptionPager(nil) 310 | 311 | for pager.More() { 312 | result, err := pager.NextPage(m.Context()) 313 | if err != nil { 314 | logger.Panic(err) 315 | } 316 | 317 | if result.Value == nil { 318 | continue 319 | } 320 | 321 | for _, row := range result.Value { 322 | keyvault := row 323 | 324 | if filterResourceIdMap != nil { 325 | // filter is active, check if resourceid was found earlier using $filter list call 326 | resourceId := to.StringLower(keyvault.ID) 327 | if _, exists := (*filterResourceIdMap)[resourceId]; !exists { 328 | logger.Debugf(`ignoring %v, not matching keyvault filter`, resourceId) 329 | continue 330 | } 331 | } 332 | 333 | azureResource, _ := armclient.ParseResourceId(*keyvault.ID) 334 | 335 | contextLogger := logger.With( 336 | zap.String("keyvault", azureResource.ResourceName), 337 | zap.String("location", to.String(keyvault.Location)), 338 | zap.String("resourceGroup", azureResource.ResourceGroup), 339 | ) 340 | 341 | m.WaitGroup().Add() 342 | go func(keyvault *armkeyvault.Vault, contextLogger *zap.SugaredLogger) { 343 | defer m.WaitGroup().Done() 344 | contextLogger.Info("collecting keyvault metrics") 345 | m.collectKeyVault(callback, subscription, keyvault, contextLogger) 346 | }(keyvault, contextLogger) 347 | } 348 | } 349 | } 350 | 351 | func (m *MetricsCollectorKeyvault) collectKeyVault(callback chan<- func(), subscription *armsubscriptions.Subscription, vault *armkeyvault.Vault, logger *zap.SugaredLogger) (status bool) { 352 | status = true 353 | 354 | vaultMetrics := m.Collector.GetMetricList("keyvault") 355 | vaultStatusMetrics := m.Collector.GetMetricList("keyvaultStatus") 356 | vaultKeyMetrics := m.Collector.GetMetricList("keyvaultKeyInfo") 357 | vaultKeyStatusMetrics := m.Collector.GetMetricList("keyvaultKeyStatus") 358 | vaultSecretMetrics := m.Collector.GetMetricList("keyvaultSecretInfo") 359 | vaultSecretStatusMetrics := m.Collector.GetMetricList("keyvaultSecretStatus") 360 | vaultCertificateMetrics := m.Collector.GetMetricList("keyvaultCertificateInfo") 361 | vaultCertificateStatusMetrics := m.Collector.GetMetricList("keyvaultCertificateStatus") 362 | vaultEntryCountMetrics := m.Collector.GetMetricList("keyvaultEntryCount") 363 | 364 | vaultUrl := to.String(vault.Properties.VaultURI) 365 | 366 | vaultResourceId := to.StringLower(vault.ID) 367 | 368 | azureResource, _ := armclient.ParseResourceId(vaultResourceId) 369 | 370 | entrySecretsCount := float64(0) 371 | entryKeysCount := float64(0) 372 | entryCertsCount := float64(0) 373 | 374 | // ######################## 375 | // Vault 376 | // ######################## 377 | 378 | vaultLabels := prometheus.Labels{ 379 | "subscriptionID": azureResource.Subscription, 380 | "subscriptionName": to.String(subscription.DisplayName), 381 | "resourceID": vaultResourceId, 382 | "vaultName": azureResource.ResourceName, 383 | "location": to.String(vault.Location), 384 | "resourceGroup": azureResource.ResourceGroup, 385 | } 386 | vaultLabels = AzureResourceTagManager.AddResourceTagsToPrometheusLabels(m.Context(), vaultLabels, vaultResourceId) 387 | vaultMetrics.AddInfo(vaultLabels) 388 | 389 | // ######################## 390 | // Keys 391 | // ######################## 392 | 393 | keyOpts := azkeys.ClientOptions{ 394 | ClientOptions: *AzureClient.NewAzCoreClientOptions(), 395 | } 396 | keyClient, err := azkeys.NewClient(vaultUrl, AzureClient.GetCred(), &keyOpts) 397 | if err != nil { 398 | logger.Panic(err.Error()) 399 | } 400 | 401 | keyPager := keyClient.NewListKeyPropertiesPager(nil) 402 | 403 | keyStatus := float64(1) 404 | for keyPager.More() { 405 | result, err := keyPager.NextPage(m.Context()) 406 | if err != nil { 407 | logger.Warn(err) 408 | keyStatus = 0 409 | break 410 | } 411 | 412 | if result.Value == nil { 413 | continue 414 | } 415 | 416 | for _, row := range result.Value { 417 | item := row 418 | entryKeysCount++ 419 | 420 | itemID := string(*item.KID) 421 | itemName := item.KID.Name() 422 | 423 | vaultKeyMetrics.AddInfo( 424 | m.contentTagManager.AddContentTags( 425 | prometheus.Labels{ 426 | "resourceID": vaultResourceId, 427 | "vaultName": azureResource.ResourceName, 428 | "keyName": itemName, 429 | "keyID": itemID, 430 | "enabled": to.BoolString(to.Bool(item.Attributes.Enabled)), 431 | }, 432 | item.Tags, 433 | ), 434 | ) 435 | 436 | // expiry date 437 | expiryDate := float64(0) 438 | if item.Attributes.Expires != nil { 439 | expiryDate = float64(item.Attributes.Expires.Unix()) 440 | } 441 | vaultKeyStatusMetrics.Add(prometheus.Labels{ 442 | "resourceID": vaultResourceId, 443 | "vaultName": azureResource.ResourceName, 444 | "keyID": itemID, 445 | "type": "expiry", 446 | }, expiryDate) 447 | 448 | // not before 449 | notBeforeDate := float64(0) 450 | if item.Attributes.NotBefore != nil { 451 | notBeforeDate = float64(item.Attributes.NotBefore.Unix()) 452 | } 453 | vaultKeyStatusMetrics.Add(prometheus.Labels{ 454 | "resourceID": vaultResourceId, 455 | "vaultName": azureResource.ResourceName, 456 | "keyID": itemID, 457 | "type": "notBefore", 458 | }, notBeforeDate) 459 | 460 | // created 461 | createdDate := float64(0) 462 | if item.Attributes.Created != nil { 463 | createdDate = float64(item.Attributes.Created.Unix()) 464 | } 465 | vaultKeyStatusMetrics.Add(prometheus.Labels{ 466 | "resourceID": vaultResourceId, 467 | "vaultName": azureResource.ResourceName, 468 | "keyID": itemID, 469 | "type": "created", 470 | }, createdDate) 471 | 472 | // updated 473 | updatedDate := float64(0) 474 | if item.Attributes.Updated != nil { 475 | updatedDate = float64(item.Attributes.Updated.Unix()) 476 | } 477 | vaultKeyStatusMetrics.Add(prometheus.Labels{ 478 | "resourceID": vaultResourceId, 479 | "vaultName": azureResource.ResourceName, 480 | "keyID": itemID, 481 | "type": "updated", 482 | }, updatedDate) 483 | } 484 | } 485 | 486 | vaultStatusMetrics.Add(prometheus.Labels{ 487 | "resourceID": vaultResourceId, 488 | "vaultName": azureResource.ResourceName, 489 | "type": "access", 490 | "scope": "keys", 491 | }, keyStatus) 492 | 493 | // ######################## 494 | // Secrets 495 | // ######################## 496 | 497 | secretOpts := azsecrets.ClientOptions{ 498 | ClientOptions: *AzureClient.NewAzCoreClientOptions(), 499 | } 500 | secretClient, err := azsecrets.NewClient(vaultUrl, AzureClient.GetCred(), &secretOpts) 501 | if err != nil { 502 | logger.Panic(err.Error()) 503 | } 504 | secretPager := secretClient.NewListSecretPropertiesPager(nil) 505 | 506 | secretStatus := float64(1) 507 | for secretPager.More() { 508 | result, err := secretPager.NextPage(m.Context()) 509 | if err != nil { 510 | logger.Warn(err) 511 | secretStatus = 0 512 | break 513 | } 514 | 515 | if result.Value == nil { 516 | continue 517 | } 518 | 519 | for _, row := range result.Value { 520 | item := row 521 | entrySecretsCount++ 522 | 523 | itemID := string(*item.ID) 524 | itemName := item.ID.Name() 525 | 526 | vaultSecretMetrics.AddInfo( 527 | m.contentTagManager.AddContentTags( 528 | prometheus.Labels{ 529 | "resourceID": vaultResourceId, 530 | "vaultName": azureResource.ResourceName, 531 | "secretName": itemName, 532 | "secretID": itemID, 533 | "enabled": to.BoolString(to.Bool(item.Attributes.Enabled)), 534 | }, 535 | item.Tags, 536 | ), 537 | ) 538 | 539 | // expiry date 540 | expiryDate := float64(0) 541 | if item.Attributes.Expires != nil { 542 | expiryDate = float64(item.Attributes.Expires.Unix()) 543 | } 544 | vaultSecretStatusMetrics.Add(prometheus.Labels{ 545 | "resourceID": vaultResourceId, 546 | "vaultName": azureResource.ResourceName, 547 | "secretID": itemID, 548 | "type": "expiry", 549 | }, expiryDate) 550 | 551 | // notbefore 552 | notBeforeDate := float64(0) 553 | if item.Attributes.NotBefore != nil { 554 | notBeforeDate = float64(item.Attributes.NotBefore.Unix()) 555 | } 556 | vaultSecretStatusMetrics.Add(prometheus.Labels{ 557 | "resourceID": vaultResourceId, 558 | "vaultName": azureResource.ResourceName, 559 | "secretID": itemID, 560 | "type": "notBefore", 561 | }, notBeforeDate) 562 | 563 | // created 564 | createdDate := float64(0) 565 | if item.Attributes.Created != nil { 566 | createdDate = float64(item.Attributes.Created.Unix()) 567 | } 568 | vaultSecretStatusMetrics.Add(prometheus.Labels{ 569 | "resourceID": vaultResourceId, 570 | "vaultName": azureResource.ResourceName, 571 | "secretID": itemID, 572 | "type": "created", 573 | }, createdDate) 574 | 575 | // updated 576 | updatedDate := float64(0) 577 | if item.Attributes.Updated != nil { 578 | updatedDate = float64(item.Attributes.Updated.Unix()) 579 | } 580 | vaultSecretStatusMetrics.Add(prometheus.Labels{ 581 | "resourceID": vaultResourceId, 582 | "vaultName": azureResource.ResourceName, 583 | "secretID": itemID, 584 | "type": "updated", 585 | }, updatedDate) 586 | } 587 | } 588 | 589 | vaultStatusMetrics.Add(prometheus.Labels{ 590 | "resourceID": vaultResourceId, 591 | "vaultName": azureResource.ResourceName, 592 | "type": "access", 593 | "scope": "secrets", 594 | }, secretStatus) 595 | 596 | // ######################## 597 | // Certificate 598 | // ######################## 599 | 600 | certificateOpts := azcertificates.ClientOptions{ 601 | ClientOptions: *AzureClient.NewAzCoreClientOptions(), 602 | } 603 | certificateClient, err := azcertificates.NewClient(vaultUrl, AzureClient.GetCred(), &certificateOpts) 604 | if err != nil { 605 | logger.Panic(err.Error()) 606 | } 607 | certificatePager := certificateClient.NewListCertificatePropertiesPager(nil) 608 | 609 | certificateStatus := float64(1) 610 | for certificatePager.More() { 611 | result, err := certificatePager.NextPage(m.Context()) 612 | if err != nil { 613 | logger.Warn(err) 614 | certificateStatus = 0 615 | break 616 | } 617 | 618 | if result.Value == nil { 619 | continue 620 | } 621 | 622 | for _, row := range result.Value { 623 | item := row 624 | entryCertsCount++ 625 | 626 | itemID := string(*item.ID) 627 | itemName := item.ID.Name() 628 | 629 | vaultCertificateMetrics.AddInfo( 630 | m.contentTagManager.AddContentTags( 631 | prometheus.Labels{ 632 | "resourceID": vaultResourceId, 633 | "vaultName": azureResource.ResourceName, 634 | "certificateName": itemName, 635 | "certificateID": itemID, 636 | "enabled": to.BoolString(to.Bool(item.Attributes.Enabled)), 637 | }, 638 | item.Tags, 639 | ), 640 | ) 641 | 642 | // expiry 643 | expiryDate := float64(0) 644 | if item.Attributes.Expires != nil { 645 | expiryDate = float64(item.Attributes.Expires.Unix()) 646 | } 647 | vaultCertificateStatusMetrics.Add(prometheus.Labels{ 648 | "resourceID": vaultResourceId, 649 | "vaultName": azureResource.ResourceName, 650 | "certificateID": itemID, 651 | "type": "expiry", 652 | }, expiryDate) 653 | 654 | // notBefore 655 | notBeforeDate := float64(0) 656 | if item.Attributes.NotBefore != nil { 657 | notBeforeDate = float64(item.Attributes.NotBefore.Unix()) 658 | } 659 | vaultCertificateStatusMetrics.Add(prometheus.Labels{ 660 | "resourceID": vaultResourceId, 661 | "vaultName": azureResource.ResourceName, 662 | "certificateID": itemID, 663 | "type": "notBefore", 664 | }, notBeforeDate) 665 | 666 | // created 667 | createdDate := float64(0) 668 | if item.Attributes.Created != nil { 669 | createdDate = float64(item.Attributes.Created.Unix()) 670 | } 671 | vaultCertificateStatusMetrics.Add(prometheus.Labels{ 672 | "resourceID": vaultResourceId, 673 | "vaultName": azureResource.ResourceName, 674 | "certificateID": itemID, 675 | "type": "created", 676 | }, createdDate) 677 | 678 | // updated 679 | updatedDate := float64(0) 680 | if item.Attributes.Updated != nil { 681 | updatedDate = float64(item.Attributes.Updated.Unix()) 682 | } 683 | vaultCertificateStatusMetrics.Add(prometheus.Labels{ 684 | "resourceID": vaultResourceId, 685 | "vaultName": azureResource.ResourceName, 686 | "certificateID": itemID, 687 | "type": "updated", 688 | }, updatedDate) 689 | 690 | } 691 | } 692 | 693 | vaultStatusMetrics.Add(prometheus.Labels{ 694 | "resourceID": vaultResourceId, 695 | "vaultName": azureResource.ResourceName, 696 | "type": "access", 697 | "scope": "certificates", 698 | }, certificateStatus) 699 | 700 | vaultEntryCountMetrics.Add(prometheus.Labels{ 701 | "resourceID": vaultResourceId, 702 | "vaultName": azureResource.ResourceName, 703 | "type": "secrets", 704 | }, entrySecretsCount) 705 | 706 | vaultEntryCountMetrics.Add(prometheus.Labels{ 707 | "resourceID": vaultResourceId, 708 | "vaultName": azureResource.ResourceName, 709 | "type": "keys", 710 | }, entryKeysCount) 711 | 712 | vaultEntryCountMetrics.Add(prometheus.Labels{ 713 | "resourceID": vaultResourceId, 714 | "vaultName": azureResource.ResourceName, 715 | "type": "certificates", 716 | }, entryCertsCount) 717 | 718 | return 719 | } 720 | --------------------------------------------------------------------------------