From 96195912009163a68fd2f68063bd169c2f7fdc16 Mon Sep 17 00:00:00 2001 From: Anurag Singh Rajawat Date: Wed, 8 Jan 2025 18:10:18 +0530 Subject: [PATCH] feat: Add SentryFlow client Signed-off-by: Anurag Singh Rajawat --- docs/getting_started.md | 30 ++-- hack/add-license-header | 2 +- sfctl/Makefile | 44 ++++++ sfctl/Readme.md | 24 ++++ sfctl/cmd/root.go | 42 ++++++ sfctl/go.mod | 88 ++++++++++++ sfctl/go.sum | 244 +++++++++++++++++++++++++++++++++ sfctl/main.go | 12 ++ sfctl/pkg/apievent/apievent.go | 69 ++++++++++ sfctl/pkg/apievent/common.go | 211 ++++++++++++++++++++++++++++ sfctl/pkg/apievent/filter.go | 116 ++++++++++++++++ sfctl/pkg/client/k8sclient.go | 42 ++++++ sfctl/pkg/client/sentryflow.go | 35 +++++ sfctl/pkg/util/logger.go | 27 ++++ sfctl/pkg/version/version.go | 37 +++++ 15 files changed, 1010 insertions(+), 13 deletions(-) create mode 100644 sfctl/Makefile create mode 100644 sfctl/Readme.md create mode 100644 sfctl/cmd/root.go create mode 100644 sfctl/go.mod create mode 100644 sfctl/go.sum create mode 100644 sfctl/main.go create mode 100644 sfctl/pkg/apievent/apievent.go create mode 100644 sfctl/pkg/apievent/common.go create mode 100644 sfctl/pkg/apievent/filter.go create mode 100644 sfctl/pkg/client/k8sclient.go create mode 100644 sfctl/pkg/client/sentryflow.go create mode 100644 sfctl/pkg/util/logger.go create mode 100644 sfctl/pkg/version/version.go diff --git a/docs/getting_started.md b/docs/getting_started.md index a92706f..a542003 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -13,7 +13,8 @@ observability. It includes detailed commands for each step along with their expl ## 2. Deploying SentryFlow -Configure SentryFlow receiver by following [this](receivers.md). Then deploy updated SentryFlow manifest by following `kubectl` command: +Configure SentryFlow receiver by following [this](receivers.md). Then deploy updated SentryFlow manifest by following +`kubectl` command: ```shell kubectl apply -f sentryflow.yaml @@ -29,24 +30,29 @@ NAME READY STATUS RESTARTS AGE sentryflow-cff887bbd-rljm7 1/1 Running 0 73s ``` -## 3. Deploying SentryFlow Clients +## 3. Viewing Captured API Access Events Clients -SentryFlow has now been deployed in the cluster. In addition, SentryFlow exports API access logs through `gRPC`. +SentryFlow has now been deployed in the cluster. In addition, SentryFlow exports API access events through `gRPC`. -For testing purposes, a client has been developed. - -- `log-client`: Simply logs everything on `STDOUT` coming from SentryFlow. - -It can be deployed into the cluster under namespace `sentryflow` by following the command: +You can use `sfctl` the SentryFlow client to view or filter captured API access events ```shell -kubectl apply -f https://raw.githubusercontent.com/5GSEC/SentryFlow/refs/heads/main/deployments/sentryflow-client.yaml +$ sfctl event +{"level":"INFO","timestamp":"2025-01-08T18:15:31.720+0530","caller":"apievent/common.go:165","msg":"starting API Events streaming"} +{"level":"INFO","timestamp":"2025-01-08T18:15:31.771+0530","caller":"apievent/common.go:171","msg":"started API Events streaming"} +# API Access Events +{"metadata":{"context_id":9,"timestamp":1736340391,"istio_version":"1.24.1","mesh_id":"cluster.local","node_name":"kind-control-plane"},"source":{"name":"server-c7669846-w5v8m","namespace":"default","ip":"10.244.0.8","port":57754},"destination":{"namespace":"sentryflow","ip":"10.96.79.211","port":9999},"request":{"headers":{":authority":"sentryflow.sentryflow:9999",":method":"HEAD",":path":"/",":scheme":"http","accept":"*/*","user-agent":"curl/7.88.1","x-forwarded-proto":"http","x-request-id":"9ff1f0fb-adca-4cbb-bfbb-7927d5aa02ae"}},"response":{"headers":{":status":"404","content-length":"19","content-type":"text/plain; charset=utf-8","date":"Wed, 08 Jan 2025 12:46:31 GMT","x-content-type-options":"nosniff"}},"protocol":"HTTP/1.1"} +... ``` -Then, check if it is up and running by: +### Filter API Events based on some Response Status Code ```shell -kubectl get pods -n sentryflow +$ sfctl event filter --status "4xx" +{"level":"INFO","timestamp":"2025-01-08T18:20:37.096+0530","caller":"apievent/common.go:165","msg":"starting API Events streaming"} +{"level":"INFO","timestamp":"2025-01-08T18:20:37.151+0530","caller":"apievent/common.go:171","msg":"started API Events streaming"} +# API Access Events +{"metadata":{"context_id":10,"timestamp":1736340639,"istio_version":"1.24.1","mesh_id":"cluster.local","node_name":"kind-control-plane"},"source":{"name":"server-c7669846-w5v8m","namespace":"default","ip":"10.244.0.8","port":37154},"destination":{"namespace":"sentryflow","ip":"10.96.79.211","port":9999},"request":{"headers":{":authority":"sentryflow.sentryflow:9999",":method":"HEAD",":path":"/",":scheme":"http","accept":"*/*","user-agent":"curl/7.88.1","x-forwarded-proto":"http","x-request-id":"e20a1002-09d1-4f3f-936e-ce688652ea4d"}},"response":{"headers":{":status":"404","content-length":"19","content-type":"text/plain; charset=utf-8","date":"Wed, 08 Jan 2025 12:50:39 GMT","x-content-type-options":"nosniff"}},"protocol":"HTTP/1.1"} ``` -If you observe `log-client`, is running, the setup has been completed successfully. +For more info check [this](../sfctl/README.md). diff --git a/hack/add-license-header b/hack/add-license-header index a92bfb5..e80eef6 100755 --- a/hack/add-license-header +++ b/hack/add-license-header @@ -8,7 +8,7 @@ if ! command -v addlicense >/dev/null; then fi GIT_ROOT=$(git rev-parse --show-toplevel) -LICENSE_HEADER=${GIT_ROOT}/scripts/license.header +LICENSE_HEADER=${GIT_ROOT}/hack/license.header if [ -z "$1" ]; then echo "No Argument Supplied, Checking and Fixing all files from project root" diff --git a/sfctl/Makefile b/sfctl/Makefile new file mode 100644 index 0000000..c11eeb0 --- /dev/null +++ b/sfctl/Makefile @@ -0,0 +1,44 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright 2024 Authors of SentryFlow + +BINARY_NAME ?= sfctl +VERSION ?= $(shell git rev-parse HEAD) +BUILD_TS ?= $(shell date) + +.PHONY: help +help: ## Display this help + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +.DEFAULT_GOAL := help + +##@ Development +.PHONY: fmt +fmt: ## Run go fmt against code + @go fmt ./... + +.PHONY: vet +vet: ## Run go vet against code + @go vet ./... + +GOLANGCI_LINT = $(shell pwd)/bin/golangci-lint +GOLANGCI_LINT_VERSION ?= v1.63.0 +golangci-lint: + @[ -f $(GOLANGCI_LINT) ] || { \ + set -e ;\ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell dirname $(GOLANGCI_LINT)) $(GOLANGCI_LINT_VERSION) ;\ + } + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter + @$(GOLANGCI_LINT) run + +.PHONY: license +license: ## Check and fix license header on all go files + @../hack/add-license-header + +##@ Build + +.PHONY: build +build: fmt vet ## Build sfctl binary + @go mod tidy + @CGO_ENABLED=0 go build -ldflags="-s" -o bin/"${BINARY_NAME}" . diff --git a/sfctl/Readme.md b/sfctl/Readme.md new file mode 100644 index 0000000..3cb28a7 --- /dev/null +++ b/sfctl/Readme.md @@ -0,0 +1,24 @@ +# sfctl + +SentryFlow CLI to view and filter captured API events streamed from SentryFlow +```shell +$ sfctl +SentryFlow command-line utility for managing captured API events. + +Usage: + sfctl [flags] + sfctl [command] + +Available Commands: + completion Generate the autocompletion script for the specified shell + event Print the captured API Events + help Help about any command + version Display version information + +Flags: + --context string name of the kubeconfig context to use + -h, --help help for sfctl + --kubeconfig string path to the kubeconfig file + +Use "sfctl [command] --help" for more information about a command. +``` \ No newline at end of file diff --git a/sfctl/cmd/root.go b/sfctl/cmd/root.go new file mode 100644 index 0000000..00fd2b1 --- /dev/null +++ b/sfctl/cmd/root.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Authors of SentryFlow + +package cmd + +import ( + "os" + + "github.com/spf13/cobra" + + "github.com/5GSEC/SentryFlow/sfctl/pkg/apievent" + "github.com/5GSEC/SentryFlow/sfctl/pkg/version" +) + +var ( + kubeConfig string + context string +) + +// Todo: Improve the description + +func init() { + rootCmd.PersistentFlags().StringVar(&kubeConfig, "kubeconfig", "", "path to the kubeconfig file") + rootCmd.PersistentFlags().StringVar(&context, "context", "", "name of the kubeconfig context to use") + rootCmd.AddCommand(version.VersionCmd, apievent.EventCmd) +} + +var rootCmd = &cobra.Command{ + Use: "sfctl", + Short: "Manage SentryFlow API events.", + Long: `SentryFlow command-line utility for managing captured API events.`, + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + return cmd.Help() + }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} diff --git a/sfctl/go.mod b/sfctl/go.mod new file mode 100644 index 0000000..502d967 --- /dev/null +++ b/sfctl/go.mod @@ -0,0 +1,88 @@ +module github.com/5GSEC/SentryFlow/sfctl + +go 1.23.0 + +toolchain go1.23.4 + +require ( + github.com/5GSEC/SentryFlow/protobuf/golang v0.0.0-20241211042917-1aaef73db0bc + github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.66.2 + k8s.io/apimachinery v0.32.0 + k8s.io/cli-runtime v0.32.0 + k8s.io/client-go v0.32.0 + sigs.k8s.io/controller-runtime v0.19.3 +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.0.1 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect + golang.org/x/net v0.30.0 // indirect + golang.org/x/oauth2 v0.23.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/term v0.25.0 // indirect + golang.org/x/text v0.19.0 // indirect + golang.org/x/time v0.7.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect + google.golang.org/protobuf v1.35.1 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.32.0 // indirect + k8s.io/apiextensions-apiserver v0.31.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/kustomize/api v0.18.0 // indirect + sigs.k8s.io/kustomize/kyaml v0.18.1 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/sfctl/go.sum b/sfctl/go.sum new file mode 100644 index 0000000..1ce3e23 --- /dev/null +++ b/sfctl/go.sum @@ -0,0 +1,244 @@ +github.com/5GSEC/SentryFlow/protobuf/golang v0.0.0-20241211042917-1aaef73db0bc h1:awz60hAFypgXbsKbvEc98lz8YhW6oKcB3EfFTHxvn14= +github.com/5GSEC/SentryFlow/protobuf/golang v0.0.0-20241211042917-1aaef73db0bc/go.mod h1:v9R/YchtBKv84wouvHwgdEDXsX1/Yq8EKFMNhVYs/gE= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= +github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= +github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU= +golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= +golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs= +golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= +golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= +google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= +google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= +google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= +k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= +k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk= +k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk= +k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= +k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/cli-runtime v0.32.0 h1:dP+OZqs7zHPpGQMCGAhectbHU2SNCuZtIimRKTv2T1c= +k8s.io/cli-runtime v0.32.0/go.mod h1:Mai8ht2+esoDRK5hr861KRy6z0zHsSTYttNVJXgP3YQ= +k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= +k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8bX1sPw= +sigs.k8s.io/controller-runtime v0.19.3/go.mod h1:j4j87DqtsThvwTv5/Tc5NFRyyF/RF0ip4+62tbTSIUM= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/kustomize/api v0.18.0 h1:hTzp67k+3NEVInwz5BHyzc9rGxIauoXferXyjv5lWPo= +sigs.k8s.io/kustomize/api v0.18.0/go.mod h1:f8isXnX+8b+SGLHQ6yO4JG1rdkZlvhaCf/uZbLVMb0U= +sigs.k8s.io/kustomize/kyaml v0.18.1 h1:WvBo56Wzw3fjS+7vBjN6TeivvpbW9GmRaWZ9CIVmt4E= +sigs.k8s.io/kustomize/kyaml v0.18.1/go.mod h1:C3L2BFVU1jgcddNBE1TxuVLgS46TjObMwW5FT9FcjYo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/sfctl/main.go b/sfctl/main.go new file mode 100644 index 0000000..de0af7a --- /dev/null +++ b/sfctl/main.go @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Authors of SentryFlow + +package main + +import ( + "github.com/5GSEC/SentryFlow/sfctl/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/sfctl/pkg/apievent/apievent.go b/sfctl/pkg/apievent/apievent.go new file mode 100644 index 0000000..f965b11 --- /dev/null +++ b/sfctl/pkg/apievent/apievent.go @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Authors of SentryFlow + +package apievent + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "k8s.io/client-go/kubernetes" + ctrl "sigs.k8s.io/controller-runtime" + + "github.com/5GSEC/SentryFlow/sfctl/pkg/client" + "github.com/5GSEC/SentryFlow/sfctl/pkg/util" +) + +var ( + prettyPrint bool + sentryflowPort string + sentryflowNamespace string + k8sClientset kubernetes.Interface + kubeConfigFilePath string + logger = util.GetLogger() +) + +func init() { + EventCmd.PersistentFlags().BoolVar(&prettyPrint, "pretty", false, "pretty print API Events in JSON format") + EventCmd.PersistentFlags().StringVar(&sentryflowPort, "port", "8888", "port to connect to SentryFlow") + EventCmd.Flags().StringVar(&sentryflowNamespace, "namespace", "sentryflow", "namespace to connect to SentryFlow") + EventCmd.AddCommand(filterCmd) +} + +var EventCmd = &cobra.Command{ + Use: "event", + Short: "Print the captured API Events", + Long: `Print captured API events from SentryFlow. + +This prints API events captured by SentryFlow. By default, events are printed in standard JSON format.`, + RunE: func(cmd *cobra.Command, args []string) error { + return printEvents(cmd.Flags()) + }, + Example: `# Print API events in standard JSON format +sfctl event + +# Pretty print API events in JSON format +sfctl event --pretty`, + SilenceUsage: true, +} + +func printEvents(flags *pflag.FlagSet) error { + kubeCtx, err := flags.GetString("context") + if err != nil { + return err + } + + kubeConfigFilePath, err = flags.GetString("kubeconfig") + if err != nil { + logger.Errorf("failed to get kubeconfig file path: %v", err) + return err + } + + k8sClientset, err = client.NewClientset(kubeConfigFilePath, &kubeCtx) + if err != nil { + logger.Errorf("failed to create Kubernetes clientset: %v", err) + return err + } + + logger.Debug("starting events stream") + return startEventsStreaming(ctrl.SetupSignalHandler(), kubeConfigFilePath, k8sClientset) +} diff --git a/sfctl/pkg/apievent/common.go b/sfctl/pkg/apievent/common.go new file mode 100644 index 0000000..68600d5 --- /dev/null +++ b/sfctl/pkg/apievent/common.go @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Authors of SentryFlow + +package apievent + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "io" + "math/big" + "net" + "net/http" + "net/url" + "os" + "strconv" + + pb "github.com/5GSEC/SentryFlow/protobuf/golang" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/portforward" + "k8s.io/client-go/transport/spdy" + + "github.com/5GSEC/SentryFlow/sfctl/pkg/client" +) + +func startEventsStreaming(ctx context.Context, config string, k8sClientset kubernetes.Interface) error { + logger.Debug("starting port-forwarding") + localPort, err := getFreeLocalPort() + if err != nil { + return err + } + logger.Debug("local port: ", localPort) + if err := setupPortForwarding(ctx, config, k8sClientset, localPort); err != nil { + return err + } + logger.Debug("started port-forwarding") + + return startStreaming(ctx, localPort) +} + +func setupPortForwarding(ctx context.Context, config string, k8sClientset kubernetes.Interface, localPort int64) error { + podName, err := getPodName(ctx, k8sClientset) + if err != nil { + return err + } + logger.Debug("pod name: ", podName) + + return startPortForwarding(config, podName, sentryflowNamespace, sentryflowPort, localPort) +} + +func startPortForwarding(config, podName, namespace, sentryFlowPort string, localPort int64) error { + restCfg, err := client.GetConfig(config, nil) + if err != nil { + return err + } + roundTripper, upgrader, err := spdy.RoundTripperFor(restCfg) + if err != nil { + return fmt.Errorf("unable to create round tripper and upgrader, error=%s", err.Error()) + } + + serverURL, err := url.Parse(restCfg.Host) + if err != nil { + return fmt.Errorf("failed to parse apiserver URL from kubeconfig. error=%s", err.Error()) + } + serverURL.Path = fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward", namespace, podName) + + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: roundTripper}, http.MethodPost, serverURL) + + StopChan, readyChan := make(chan struct{}, 1), make(chan struct{}, 1) + out, errOut := new(bytes.Buffer), new(bytes.Buffer) + + forwarder, err := portforward.New(dialer, []string{fmt.Sprintf("%d:%v", localPort, sentryFlowPort)}, + StopChan, readyChan, out, errOut) + if err != nil { + return fmt.Errorf("unable to portforward. error=%s", err.Error()) + } + + errChan := make(chan error, 1) + go func() { + errChan <- forwarder.ForwardPorts() + }() + + select { + case err = <-errChan: + close(errChan) + forwarder.Close() + return fmt.Errorf("could not create port forward %s", err) + case <-readyChan: + return nil + } +} + +func getFreeLocalPort() (int64, error) { + for attempts := 0; attempts < 100; attempts++ { + port, err := getRandomPort() + if err != nil { + continue + } + + listener, err := net.Listen("tcp", fmt.Sprintf("localhost:%v", strconv.FormatInt(port, 10))) + if err != nil { + return -1, err + } + if err := listener.Close(); err != nil { + return -1, err + } + return port, nil + } + + return -1, fmt.Errorf("failed to find a free local port") +} + +func getRandomPort() (int64, error) { + // Return a port number > 32767 + n, err := rand.Int(rand.Reader, big.NewInt(32900-32768)) + if err != nil { + return -1, fmt.Errorf("failed to generate random integer for port") + } + + portNo := n.Int64() + 32768 + return portNo, nil +} + +func getPodName(ctx context.Context, k8sClientset kubernetes.Interface) (string, error) { + logger.Debug("getting pod name") + + if k8sClientset == nil { + logger.Warn("k8sClientset is nil") + return "", errors.New("k8sClientset is nil") + } + pods, err := k8sClientset.CoreV1().Pods(sentryflowNamespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return "", err + } + if len(pods.Items) == 0 { + return "", fmt.Errorf("no sentryflow pods found in %s namespace", sentryflowNamespace) + } + return pods.Items[0].Name, nil +} + +func startStreaming(ctx context.Context, localPort int64) error { + logger.Debug("creating SentryFlow client") + + sfClient, closer := client.NewSentryFlowClient(localPort) + defer closer() + + hostname, err := os.Hostname() + if err != nil { + logger.Warnf("failed to get hostname: %v", err) + } + + ips, err := net.LookupIP(hostname) + if err != nil { + logger.Warnf("failed to get current host IP address: %v", err) + } + + clientInfo := &pb.ClientInfo{ + HostName: hostname, + IPAddress: ips[0].String(), + } + + logger.Info("starting API Events streaming") + stream, err := sfClient.GetAPIEvent(ctx, clientInfo) + if err != nil { + return err + } + + logger.Info("started API Events streaming") + for { + event, err := stream.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + return nil + } + if status.Code(err) != codes.Canceled { + return err + } + } + + select { + case <-ctx.Done(): + logger.Info("Shutting down API Events streaming") + return nil + default: + if statusCode != "" { + printFilteredEvents(event, prettyPrint) + continue + } + + if prettyPrint { + body, err := json.MarshalIndent(event, "", " ") + if err != nil { + return err + } + fmt.Println(string(body)) + } else { + body, err := json.Marshal(event) + if err != nil { + return err + } + fmt.Println(string(body)) + } + } + } +} diff --git a/sfctl/pkg/apievent/filter.go b/sfctl/pkg/apievent/filter.go new file mode 100644 index 0000000..6688ae2 --- /dev/null +++ b/sfctl/pkg/apievent/filter.go @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Authors of SentryFlow + +package apievent + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + pb "github.com/5GSEC/SentryFlow/protobuf/golang" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + statusCode string +) + +var filterCmd = &cobra.Command{ + Use: "filter", + Short: "Filter events by response status code (specific or family)", + Long: `Filter events streamed from SentryFlow by response status code. + +You can filter by specific status codes (e.g., 200, 404) to match only those codes, +or by status code families (e.g., 2xx, 4xx) to match a range of codes +(e.g., 2xx matches all codes between 200 and 299).`, + Example: ` +# Print filtered API Events based on some Response Status code +sfctl event filter --status "200" + +# Print filtered API Events based on family of Response Status code +sfctl event filter --status "2xx" +sfctl event filter --status "3xx" +sfctl event filter --status "4xx" +sfctl event filter --status "5xx"`, + RunE: func(cmd *cobra.Command, args []string) error { + val, err := cmd.Flags().GetString("status") + if err != nil { + return err + } + if val == "" || len(val) < 3 || + !strings.HasPrefix(val, "1") && + !strings.HasPrefix(val, "2") && + !strings.HasPrefix(val, "3") && + !strings.HasPrefix(val, "4") && + !strings.HasPrefix(val, "5") { + return fmt.Errorf("invalid status code, %v", val) + } + return filterEvents(cmd.Flags()) + }, + SilenceUsage: true, +} + +func filterEvents(flags *pflag.FlagSet) error { + statusCode, _ = flags.GetString("status") + return printEvents(flags) +} + +func printFilteredEvents(event *pb.APIEvent, prettyPrint bool) { + var err error + + apiResStatusCodeStr, exists := event.Response.Headers[":status"] + if !exists { + return + } + + apiResStatusCode, err := strconv.Atoi(apiResStatusCodeStr) + if err != nil { + logger.Warnf("failed to parse status code: %v", err) + return + } + + if !matches(apiResStatusCode, statusCode) { + return + } + + var jsonBytes []byte + if prettyPrint { + jsonBytes, err = json.MarshalIndent(event, "", " ") + if err != nil { + logger.Warnf("failed to marshal event: %v", err) + return + } + } else { + jsonBytes, err = json.Marshal(event) + if err != nil { + logger.Warnf("failed to marshal event: %v", err) + } + } + + fmt.Println(string(jsonBytes)) +} + +func matches(curr int, target string) bool { + switch target { + case "2xx": + return curr >= 200 && curr < 300 + case "3xx": + return curr >= 300 && curr < 400 + case "4xx": + return curr >= 400 && curr < 500 + case "5xx": + return curr >= 500 && curr < 600 + default: + if specificCode, err := strconv.Atoi(target); err == nil { + return curr == specificCode + } + return false + } +} + +func init() { + filterCmd.Flags().StringVar(&statusCode, "status", "", "response status code") +} diff --git a/sfctl/pkg/client/k8sclient.go b/sfctl/pkg/client/k8sclient.go new file mode 100644 index 0000000..3091ce3 --- /dev/null +++ b/sfctl/pkg/client/k8sclient.go @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Authors of SentryFlow + +package client + +import ( + "fmt" + "os" + "path/filepath" + + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +// NewClientset returns a new Kubernetes Clientset. +func NewClientset(kubeConfig string, kubeCtx *string) (kubernetes.Interface, error) { + config, err := GetConfig(kubeConfig, kubeCtx) + if err != nil { + return nil, fmt.Errorf("failed to get config: %v", err) + } + return kubernetes.NewForConfig(config) +} + +func GetConfig(kubeConfig string, kubeCtx *string) (*rest.Config, error) { + if kubeConfig == "" { + kubeConfig = filepath.Join(os.Getenv("HOME"), ".kube", "config") + } + + restClienGetter := genericclioptions.ConfigFlags{ + KubeConfig: &kubeConfig, + Context: kubeCtx, + } + rawKubeConfigLoader := restClienGetter.ToRawKubeConfigLoader() + + config, err := rawKubeConfigLoader.ClientConfig() + if err != nil { + return nil, err + } + + return config, nil +} diff --git a/sfctl/pkg/client/sentryflow.go b/sfctl/pkg/client/sentryflow.go new file mode 100644 index 0000000..8fd8648 --- /dev/null +++ b/sfctl/pkg/client/sentryflow.go @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Authors of SentryFlow + +package client + +import ( + "fmt" + + pb "github.com/5GSEC/SentryFlow/protobuf/golang" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + "github.com/5GSEC/SentryFlow/sfctl/pkg/util" +) + +func NewSentryFlowClient(port int64) (pb.SentryFlowClient, func()) { + logger := util.GetLogger() + + conn, err := grpc.NewClient(fmt.Sprintf("localhost:%d", port), + grpc.WithTransportCredentials( + insecure.NewCredentials(), + ), + ) + + if err != nil { + logger.Errorf("failed to connect to SentryFlow: %v", err) + return nil, nil + } + + return pb.NewSentryFlowClient(conn), func() { + if err := conn.Close(); err != nil { + logger.Warnf("failed to close connection to SentryFlow: %v", err) + } + } +} diff --git a/sfctl/pkg/util/logger.go b/sfctl/pkg/util/logger.go new file mode 100644 index 0000000..a91871f --- /dev/null +++ b/sfctl/pkg/util/logger.go @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Authors of SentryFlow + +package util + +import ( + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var logger *zap.SugaredLogger + +func InitLogger() { + cfg := zap.NewProductionConfig() + cfg.EncoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder + cfg.EncoderConfig.TimeKey = "timestamp" + cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder + coreLogger, _ := cfg.Build() + logger = coreLogger.Sugar() +} + +func GetLogger() *zap.SugaredLogger { + if logger == nil { + InitLogger() + } + return logger +} diff --git a/sfctl/pkg/version/version.go b/sfctl/pkg/version/version.go new file mode 100644 index 0000000..4659557 --- /dev/null +++ b/sfctl/pkg/version/version.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2021 Authors of KubeArmor + +package version + +import ( + "fmt" + "runtime" + "runtime/debug" + + "github.com/spf13/cobra" +) + +var VersionCmd = &cobra.Command{ + Use: "version", + Short: "Display version information", + Long: `Display version information`, + Run: func(cmd *cobra.Command, args []string) { + printVersion() + }, +} + +func printVersion() { + info, _ := debug.ReadBuildInfo() + vcsTime := "" + for _, s := range info.Settings { + if s.Key == "vcs.time" { + vcsTime = s.Value + } + } + fmt.Printf("sfctl version: %s %s/%s buildtime: %s\n", + info.Main.Version, + runtime.GOOS, + runtime.GOARCH, + vcsTime, + ) +}