mirror of
https://github.com/kubeshark/kubeshark.git
synced 2026-08-07 14:38:13 +00:00
Compare commits
10 Commits
v53.3.0
...
fix-1954-w
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bad778d478 | ||
|
|
c8a9ba1196 | ||
|
|
f38980ea94 | ||
|
|
bf6e8271ee | ||
|
|
d3ac79a2f5 | ||
|
|
f3b2b205f1 | ||
|
|
ad5df664fc | ||
|
|
fcceed23ab | ||
|
|
9396e64b9b | ||
|
|
b5e59321e0 |
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"name": "kubeshark",
|
||||
"owner": { "name": "kubeshark" },
|
||||
"description": "Kubeshark network observability skills for Kubernetes",
|
||||
"plugins": [
|
||||
{
|
||||
|
||||
101
.github/workflows/codeql.yml
vendored
Normal file
101
.github/workflows/codeql.yml
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL Advanced"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '16 11 * * 5'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze (${{ matrix.language }})
|
||||
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||
# - https://gh.io/supported-runners-and-hardware-resources
|
||||
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
# required to fetch internal or private CodeQL packs
|
||||
packages: read
|
||||
|
||||
# only required for workflows in private repositories
|
||||
actions: read
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- language: actions
|
||||
build-mode: none
|
||||
- language: go
|
||||
build-mode: autobuild
|
||||
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
|
||||
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||
# or others). This is typically only required for manual builds.
|
||||
# - name: Setup runtime (example)
|
||||
# uses: actions/setup-example@v1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
# If the analyze step fails for one of the languages you are analyzing with
|
||||
# "We were unable to automatically build your code", modify the matrix above
|
||||
# to set the build mode to "manual" for that language. Then modify this step
|
||||
# to build your code.
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
- name: Run manual build steps
|
||||
if: matrix.build-mode == 'manual'
|
||||
shell: bash
|
||||
run: |
|
||||
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||
'languages you are analyzing, replace this with the commands to build' \
|
||||
'your code, for example:'
|
||||
echo ' make bootstrap'
|
||||
echo ' make release'
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
4
.github/workflows/linter.yml
vendored
4
.github/workflows/linter.yml
vendored
@@ -27,7 +27,7 @@ jobs:
|
||||
go-version-file: 'go.mod'
|
||||
|
||||
- name: Go lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
uses: golangci/golangci-lint-action@v7
|
||||
with:
|
||||
version: latest
|
||||
version: v2.8.0
|
||||
args: --timeout=10m
|
||||
|
||||
24
.github/workflows/release-tag.yml
vendored
24
.github/workflows/release-tag.yml
vendored
@@ -1,24 +0,0 @@
|
||||
name: Auto-tag release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
tag:
|
||||
if: github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/v')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Create and push tag
|
||||
run: |
|
||||
VERSION="${GITHUB_HEAD_REF#release/}"
|
||||
echo "Creating tag $VERSION on master"
|
||||
git tag "$VERSION"
|
||||
git push origin "$VERSION"
|
||||
36
.golangci.yml
Normal file
36
.golangci.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
version: "2"
|
||||
|
||||
run:
|
||||
timeout: 10m
|
||||
go: "1.25.0"
|
||||
modules-download-mode: readonly
|
||||
|
||||
linters:
|
||||
default: none
|
||||
enable:
|
||||
- govet
|
||||
- staticcheck
|
||||
- ineffassign
|
||||
- unused
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- goimports
|
||||
settings:
|
||||
gofmt:
|
||||
simplify: true
|
||||
goimports:
|
||||
local-prefixes:
|
||||
- github.com/kubeshark/kubeshark
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
output:
|
||||
formats:
|
||||
tab:
|
||||
path: stdout
|
||||
colors: true
|
||||
print-linter-name: true
|
||||
12
Makefile
12
Makefile
@@ -268,8 +268,8 @@ release: ## Print release workflow instructions.
|
||||
@echo ""
|
||||
@echo " Shortcut: make release-pr VERSION=x.y.z runs 1 → 2 → 3."
|
||||
@echo ""
|
||||
@echo " After both PRs merge: tag is created automatically,"
|
||||
@echo " or run: make release-tag VERSION=x.y.z"
|
||||
@echo " After both PRs merge, create the release tag:"
|
||||
@echo " make release-tag VERSION=x.y.z"
|
||||
|
||||
# Internal: validate VERSION before any release-* target runs.
|
||||
_release-check-version:
|
||||
@@ -362,14 +362,18 @@ release-pr: release-siblings release-pr-kubeshark release-pr-helm ## Run release
|
||||
@echo " - kubeshark.github.io: Review and merge the helm chart PR."
|
||||
@echo "Tag will be created automatically, or run: make release-tag VERSION=$(VERSION)"
|
||||
|
||||
release-tag: ## Step 2 (fallback): Tag master after release PR is merged.
|
||||
release-tag: _release-check-version ## Step 2: Tag master after release PR is merged. Idempotent; re-run to retrigger the release build.
|
||||
@echo "Verifying release PR was merged..."
|
||||
@if ! gh pr list --state merged --head release/v$(VERSION) --json number --jq '.[0].number' | grep -q .; then \
|
||||
echo "Error: No merged PR found for release/v$(VERSION). Merge the PR first."; \
|
||||
exit 1; \
|
||||
fi
|
||||
@git checkout master && git pull
|
||||
@git tag -d v$(VERSION) 2>/dev/null; git tag v$(VERSION) && git push origin --tags
|
||||
@if git ls-remote --tags origin "refs/tags/v$(VERSION)" | grep -q .; then \
|
||||
echo "Tag v$(VERSION) already exists on origin — deleting to retrigger release..."; \
|
||||
git push origin :refs/tags/v$(VERSION); \
|
||||
fi
|
||||
@git tag -d v$(VERSION) 2>/dev/null; git tag v$(VERSION) && git push origin "refs/tags/v$(VERSION)"
|
||||
@echo ""
|
||||
@echo "Tagged v$(VERSION) on master. GitHub Actions will build the release."
|
||||
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/kubeshark/kubeshark/kubernetes/helm"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var cleanCmd = &cobra.Command{
|
||||
|
||||
@@ -8,13 +8,14 @@ import (
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/errormessage"
|
||||
"github.com/kubeshark/kubeshark/internal/connect"
|
||||
"github.com/kubeshark/kubeshark/kubernetes"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/kubeshark/kubeshark/misc/fsUtils"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func startProxyReportErrorIfAny(kubernetesProvider *kubernetes.Provider, ctx context.Context, serviceName string, podName string, proxyPortLabel string, srcPort uint16, dstPort uint16, healthCheck string) {
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var configCmd = &cobra.Command{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -11,19 +12,26 @@ import (
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/kubeshark/kubeshark/kubernetes"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var consoleCmd = &cobra.Command{
|
||||
Use: "console",
|
||||
Short: "Stream the scripting console logs into shell",
|
||||
Short: "Stream the scripting console logs into shell (temporarily non-functional — under refactoring after hub API changes)",
|
||||
Long: `Stream the scripting console logs into shell.
|
||||
|
||||
NOTE: This command is currently non-functional and under refactoring. The hub
|
||||
moved scripting-console log streaming off the /scripts/logs WebSocket onto a
|
||||
Connect-RPC streaming service, and this client has not yet been updated to use
|
||||
it, so no logs will stream until the migration lands.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
runConsole()
|
||||
log.Warn().Msg(fmt.Sprintf(utils.Yellow, "The 'console' command is temporarily non-functional and under refactoring: the hub moved scripting-console log streaming off the /scripts/logs WebSocket onto a Connect-RPC service, and this client has not yet been updated. No logs will stream, so the command exits without doing anything."))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -41,9 +49,22 @@ func init() {
|
||||
consoleCmd.Flags().StringP(configStructs.ReleaseNamespaceLabel, "s", defaultTapConfig.Release.Namespace, "Release namespace of Kubeshark")
|
||||
}
|
||||
|
||||
//nolint:unused // retained for the in-progress console refactoring; re-wired once the Connect-RPC client lands
|
||||
func runConsoleWithoutProxy() {
|
||||
log.Info().Msg("Starting scripting console ...")
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// Best-effort: mint a scoped ServiceAccount token to authenticate to a
|
||||
// gated Hub as kubeshark-cli; fall back to License-Key when not possible.
|
||||
saToken := ""
|
||||
if provider, err := getKubernetesProviderForCli(true, true); err == nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
if tok, terr := provider.MintHubToken(ctx, config.Config.Tap.Release.Namespace); terr == nil {
|
||||
saToken = tok
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
|
||||
hubUrl := kubernetes.GetHubUrl()
|
||||
for {
|
||||
|
||||
@@ -66,7 +87,11 @@ func runConsoleWithoutProxy() {
|
||||
}
|
||||
headers := http.Header{}
|
||||
headers.Set(utils.X_KUBESHARK_CAPTURE_HEADER_KEY, utils.X_KUBESHARK_CAPTURE_HEADER_IGNORE_VALUE)
|
||||
headers.Set("License-Key", config.Config.License)
|
||||
if saToken != "" {
|
||||
headers.Set(utils.CLI_AUTH_HEADER, saToken)
|
||||
} else {
|
||||
headers.Set(utils.LICENSE_KEY_HEADER, config.Config.License)
|
||||
}
|
||||
|
||||
c, _, err := websocket.DefaultDialer.Dial(u.String(), headers)
|
||||
if err != nil {
|
||||
@@ -121,7 +146,10 @@ func runConsoleWithoutProxy() {
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:unused // retained for the in-progress console refactoring; re-wired once the Connect-RPC client lands
|
||||
func runConsole() {
|
||||
log.Warn().Msg(fmt.Sprintf(utils.Yellow, "The 'console' command is temporarily non-functional and under refactoring: the hub moved scripting-console log streaming off the /scripts/logs WebSocket onto a Connect-RPC service, and this client has not yet been updated. No logs will stream."))
|
||||
|
||||
go runConsoleWithoutProxy()
|
||||
|
||||
// Create interrupt channel and setup signal handling once
|
||||
|
||||
@@ -3,8 +3,9 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
)
|
||||
|
||||
var licenseCmd = &cobra.Command{
|
||||
|
||||
@@ -5,13 +5,14 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/kubeshark/kubeshark/errormessage"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/kubeshark/kubeshark/misc/fsUtils"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var logsCmd = &cobra.Command{
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
)
|
||||
|
||||
var mcpURL string
|
||||
@@ -10,6 +11,7 @@ var mcpKubeconfig string
|
||||
var mcpListTools bool
|
||||
var mcpConfig bool
|
||||
var mcpAllowDestructive bool
|
||||
var mcpToken string
|
||||
|
||||
var mcpCmd = &cobra.Command{
|
||||
Use: "mcp",
|
||||
@@ -118,6 +120,7 @@ func init() {
|
||||
rootCmd.AddCommand(mcpCmd)
|
||||
|
||||
mcpCmd.Flags().StringVar(&mcpURL, "url", "", "Direct URL to Kubeshark (e.g., https://kubeshark.example.com). When set, connects directly without kubectl/proxy and disables start/stop tools.")
|
||||
mcpCmd.Flags().StringVar(&mcpToken, "token", "", "ServiceAccount/bearer token for a gated Hub in --url mode (e.g. from 'kubectl create token kubeshark-cli --audience kubeshark-hub'); also read from KUBESHARK_HUB_TOKEN. Ignored without --url, where the token is minted and auto-renewed from kube access.")
|
||||
mcpCmd.Flags().StringVar(&mcpKubeconfig, "kubeconfig", "", "Path to kubeconfig file (e.g., /Users/me/.kube/config)")
|
||||
mcpCmd.Flags().BoolVar(&mcpListTools, "list-tools", false, "List available MCP tools and exit")
|
||||
mcpCmd.Flags().BoolVar(&mcpConfig, "mcp-config", false, "Print MCP client configuration JSON and exit")
|
||||
|
||||
@@ -15,12 +15,14 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/kubeshark/kubeshark/internal/connect"
|
||||
"github.com/kubeshark/kubeshark/kubernetes"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
)
|
||||
|
||||
// MCP Protocol types (JSON-RPC 2.0)
|
||||
@@ -158,21 +160,52 @@ type mcpServer struct {
|
||||
cachedHubMCP *hubMCPResponse // Cached tools/prompts from Hub
|
||||
cachedAt time.Time // When the cache was populated
|
||||
hubMCPMu sync.Mutex
|
||||
tokenSource func() string // hub SA token source; proxy mode auto-renews, URL mode is the static --token. nil → License-Key
|
||||
}
|
||||
|
||||
const hubMCPCacheTTL = 5 * time.Minute
|
||||
|
||||
// hubTokenSource builds the SA-token source used to authenticate to a gated
|
||||
// Hub. Proxy mode (kube access) returns an auto-renewing minter so long-lived
|
||||
// processes keep working past the ~1h token TTL; URL mode returns the static
|
||||
// --token / KUBESHARK_HUB_TOKEN (it can't mint without kube access). Returns
|
||||
// nil when no token is available, so callers fall back to the License-Key.
|
||||
func hubTokenSource(urlMode bool) func() string {
|
||||
if urlMode {
|
||||
staticTok := mcpToken
|
||||
if staticTok == "" {
|
||||
staticTok = os.Getenv("KUBESHARK_HUB_TOKEN")
|
||||
}
|
||||
if staticTok == "" {
|
||||
return nil
|
||||
}
|
||||
return func() string { return staticTok }
|
||||
}
|
||||
provider, err := getKubernetesProviderForCli(true, true)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return kubernetes.NewHubTokenRenewer(provider, config.Config.Tap.Release.Namespace).Token
|
||||
}
|
||||
|
||||
func runMCPWithConfig(setFlags []string, directURL string, allowDestructive bool) {
|
||||
// Disable zerolog output to stderr (MCP uses stdio)
|
||||
zerolog.SetGlobalLevel(zerolog.Disabled)
|
||||
|
||||
urlMode := directURL != ""
|
||||
|
||||
// Hub SA-token source: auto-renewing in proxy mode, the static --token in
|
||||
// URL mode; nil falls back to the License-Key. See hubTokenSource.
|
||||
tokenSource := hubTokenSource(urlMode)
|
||||
|
||||
server := &mcpServer{
|
||||
httpClient: &http.Client{Timeout: 30 * time.Second},
|
||||
httpClient: utils.NewHubHTTPClientWithTokenSource(30*time.Second, tokenSource, config.Config.License),
|
||||
tokenSource: tokenSource,
|
||||
stdin: os.Stdin,
|
||||
stdout: os.Stdout,
|
||||
setFlags: setFlags,
|
||||
directURL: directURL,
|
||||
urlMode: directURL != "",
|
||||
urlMode: urlMode,
|
||||
allowDestructive: allowDestructive,
|
||||
}
|
||||
|
||||
@@ -195,7 +228,7 @@ func (s *mcpServer) validateDirectURL() error {
|
||||
s.directURL = urlStr
|
||||
|
||||
// Use a short timeout for validation
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
client := utils.NewHubHTTPClientWithTokenSource(10*time.Second, s.tokenSource, config.Config.License)
|
||||
|
||||
// Try to reach the MCP API base endpoint which returns tool definitions
|
||||
testURL := fmt.Sprintf("%s/api/mcp", urlStr)
|
||||
@@ -205,6 +238,10 @@ func (s *mcpServer) validateDirectURL() error {
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if utils.IsAuthRequired(resp) {
|
||||
return fmt.Errorf("%s", s.hubAuthErrorMessage())
|
||||
}
|
||||
|
||||
// Try to parse the MCP response to validate it's a valid Kubeshark endpoint
|
||||
var mcpInfo hubMCPResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&mcpInfo); err != nil {
|
||||
@@ -282,6 +319,20 @@ func (s *mcpServer) ensureBackendConnection() string {
|
||||
|
||||
// fetchHubMCP fetches tools and prompts from the Hub's /api/mcp endpoint
|
||||
// Returns nil if Hub is not available or returns an error
|
||||
// hubAuthErrorMessage returns a clear, user-facing explanation for an
|
||||
// auth-required hub response (401, or an unfollowed SSO 302/303 redirect),
|
||||
// distinguishing a missing/expired credential from a generic API error. URL
|
||||
// mode can't auto-renew, so it points the user at re-minting.
|
||||
func (s *mcpServer) hubAuthErrorMessage() string {
|
||||
if s.urlMode {
|
||||
return "Hub requires authentication (401/SSO redirect) and no accepted credential was presented. " +
|
||||
"In URL mode pass a token via --token / KUBESHARK_HUB_TOKEN (mint with " +
|
||||
"`kubectl create token kubeshark-cli --audience kubeshark-hub`) — it can't auto-renew, so re-mint and " +
|
||||
"restart if it expired — or set a valid license (License-Key) if the deployment uses one."
|
||||
}
|
||||
return "Hub rejected the request (401 Unauthorized): the minted kubeshark-cli token was not accepted (check the Hub's AUTH_CLI_SERVICE_ACCOUNTS allowlist and the token audience), or no credential was available — if you lack RBAC to mint the token the CLI falls back to the License-Key, so set a valid license."
|
||||
}
|
||||
|
||||
func (s *mcpServer) fetchHubMCP() *hubMCPResponse {
|
||||
s.hubMCPMu.Lock()
|
||||
defer s.hubMCPMu.Unlock()
|
||||
@@ -304,6 +355,10 @@ func (s *mcpServer) fetchHubMCP() *hubMCPResponse {
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if utils.IsAuthRequired(resp) {
|
||||
fmt.Fprintf(os.Stderr, "[kubeshark-mcp] %s\n", s.hubAuthErrorMessage())
|
||||
return nil
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil
|
||||
}
|
||||
@@ -760,6 +815,9 @@ func (s *mcpServer) callHubTool(toolName string, args map[string]any) (string, b
|
||||
return fmt.Sprintf("Error reading response: %v", err), true
|
||||
}
|
||||
|
||||
if utils.IsAuthRequired(resp) {
|
||||
return s.hubAuthErrorMessage(), true
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Sprintf("Hub API error (%d): %s", resp.StatusCode, string(body)), true
|
||||
}
|
||||
@@ -820,10 +878,11 @@ func (s *mcpServer) callDownloadFile(args map[string]any) (string, bool) {
|
||||
// The default s.httpClient has a 30s total timeout which would fail for large files (up to 10GB).
|
||||
// This client sets only connection-level timeouts and lets the body stream without a deadline.
|
||||
downloadClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
CheckRedirect: utils.StopOnSSORedirect,
|
||||
Transport: utils.HubAuthTransportWithTokenSource(s.tokenSource, config.Config.License, &http.Transport{
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ResponseHeaderTimeout: 30 * time.Second,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
resp, err := downloadClient.Get(fullURL)
|
||||
@@ -832,6 +891,11 @@ func (s *mcpServer) callDownloadFile(args map[string]any) (string, bool) {
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
// Catch auth-required (401 / unfollowed SSO redirect) before treating the
|
||||
// body as a file, so we don't write an HTML login page to disk.
|
||||
if utils.IsAuthRequired(resp) {
|
||||
return s.hubAuthErrorMessage(), true
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Sprintf("Error downloading file: HTTP %d", resp.StatusCode), true
|
||||
}
|
||||
@@ -1059,6 +1123,10 @@ func listMCPTools(directURL string) {
|
||||
fmt.Println("=========")
|
||||
fmt.Println()
|
||||
|
||||
// Same hub credential as the running server: renewing minter in proxy
|
||||
// mode, static --token in URL mode (nil → License-Key).
|
||||
tokenSource := hubTokenSource(directURL != "")
|
||||
|
||||
// URL mode - no cluster management, connect directly
|
||||
if directURL != "" {
|
||||
fmt.Printf("URL Mode: %s\n\n", directURL)
|
||||
@@ -1071,7 +1139,7 @@ func listMCPTools(directURL string) {
|
||||
fmt.Println()
|
||||
|
||||
hubURL := strings.TrimSuffix(directURL, "/") + "/api/mcp"
|
||||
fetchAndDisplayTools(hubURL, 30*time.Second)
|
||||
fetchAndDisplayTools(hubURL, 30*time.Second, tokenSource)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1095,7 +1163,7 @@ func listMCPTools(directURL string) {
|
||||
}
|
||||
|
||||
fmt.Printf("Connected to: %s\n\n", hubURL)
|
||||
fetchAndDisplayTools(hubURL, 30*time.Second)
|
||||
fetchAndDisplayTools(hubURL, 30*time.Second, tokenSource)
|
||||
}
|
||||
|
||||
// establishProxyConnection sets up proxy to Kubeshark and returns the hub URL
|
||||
@@ -1144,8 +1212,8 @@ func establishProxyConnection(timeout time.Duration) (string, error) {
|
||||
}
|
||||
|
||||
// fetchAndDisplayTools fetches tools from the Kubeshark API and displays them
|
||||
func fetchAndDisplayTools(hubURL string, timeout time.Duration) {
|
||||
client := &http.Client{Timeout: timeout}
|
||||
func fetchAndDisplayTools(hubURL string, timeout time.Duration, tokenSource func() string) {
|
||||
client := utils.NewHubHTTPClientWithTokenSource(timeout, tokenSource, config.Config.License)
|
||||
|
||||
// Fetch tools list from /api/mcp endpoint
|
||||
resp, err := client.Get(strings.TrimSuffix(hubURL, "/mcp") + "/mcp")
|
||||
@@ -1155,6 +1223,11 @@ func fetchAndDisplayTools(hubURL string, timeout time.Duration) {
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
if utils.IsAuthRequired(resp) {
|
||||
fmt.Println("Kubeshark API: 401 Unauthorized — the Hub requires a valid credential. In --url mode pass --token (or KUBESHARK_HUB_TOKEN); in proxy mode ensure you have RBAC to mint the kubeshark-cli token.")
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the response using the Hub MCP response format
|
||||
var mcpInfo hubMCPResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&mcpInfo); err != nil {
|
||||
|
||||
@@ -658,9 +658,10 @@ func TestMCP_ToolsList_IncludesFileTools(t *testing.T) {
|
||||
|
||||
func TestMCP_FullConversation(t *testing.T) {
|
||||
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/" {
|
||||
switch r.URL.Path {
|
||||
case "/":
|
||||
_, _ = w.Write([]byte(`{"name":"hub","tools":[{"name":"list_workloads","description":"","inputSchema":{}}]}`))
|
||||
} else if r.URL.Path == "/tools/call" {
|
||||
case "/tools/call":
|
||||
_, _ = w.Write([]byte(`{"data":"ok"}`))
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -8,13 +8,14 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
)
|
||||
|
||||
// pcapDumpCmd represents the consolidated pcapdump command
|
||||
@@ -45,12 +46,12 @@ var pcapDumpCmd = &cobra.Command{
|
||||
// Use the current context in kubeconfig
|
||||
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error building kubeconfig: %w", err)
|
||||
return fmt.Errorf("error building kubeconfig: %w", err)
|
||||
}
|
||||
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error creating Kubernetes client: %w", err)
|
||||
return fmt.Errorf("error creating Kubernetes client: %w", err)
|
||||
}
|
||||
|
||||
// Parse the `--time` flag
|
||||
@@ -59,7 +60,7 @@ var pcapDumpCmd = &cobra.Command{
|
||||
if timeIntervalStr != "" {
|
||||
duration, err := time.ParseDuration(timeIntervalStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Invalid format %w", err)
|
||||
return fmt.Errorf("invalid --time value %q: %w", timeIntervalStr, err)
|
||||
}
|
||||
tempCutoffTime := time.Now().Add(-duration)
|
||||
cutoffTime = &tempCutoffTime
|
||||
@@ -70,17 +71,17 @@ var pcapDumpCmd = &cobra.Command{
|
||||
if destDir != "" {
|
||||
info, err := os.Stat(destDir)
|
||||
if os.IsNotExist(err) {
|
||||
return fmt.Errorf("Directory does not exist: %s", destDir)
|
||||
return fmt.Errorf("directory does not exist: %s", destDir)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error checking dest directory: %w", err)
|
||||
return fmt.Errorf("error checking dest directory: %w", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("Dest path is not a directory: %s", destDir)
|
||||
return fmt.Errorf("dest path is not a directory: %s", destDir)
|
||||
}
|
||||
tempFile, err := os.CreateTemp(destDir, "write-test-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Directory %s is not writable", destDir)
|
||||
return fmt.Errorf("directory %s is not writable", destDir)
|
||||
}
|
||||
_ = os.Remove(tempFile.Name())
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
)
|
||||
|
||||
var pprofCmd = &cobra.Command{
|
||||
|
||||
@@ -5,12 +5,13 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-cmd/cmd"
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/kubernetes"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
"github.com/rivo/tview"
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/kubernetes"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
)
|
||||
|
||||
func runPprof() {
|
||||
|
||||
@@ -2,9 +2,10 @@ package cmd
|
||||
|
||||
import (
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
)
|
||||
|
||||
var proxyCmd = &cobra.Command{
|
||||
|
||||
@@ -5,13 +5,14 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/kubeshark/kubeshark/internal/connect"
|
||||
"github.com/kubeshark/kubeshark/kubernetes"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func runProxy(block bool, noBrowser bool) {
|
||||
|
||||
@@ -4,10 +4,11 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
|
||||
@@ -12,15 +12,16 @@ import (
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/kubeshark/kubeshark/kubernetes"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/kubeshark/kubeshark/kubernetes"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
)
|
||||
|
||||
var scriptsCmd = &cobra.Command{
|
||||
|
||||
@@ -4,11 +4,12 @@ import (
|
||||
"errors"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/kubeshark/kubeshark/errormessage"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var tapCmd = &cobra.Command{
|
||||
|
||||
@@ -16,11 +16,12 @@ import (
|
||||
|
||||
core "k8s.io/api/core/v1"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
"github.com/kubeshark/kubeshark/errormessage"
|
||||
"github.com/kubeshark/kubeshark/kubernetes"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const cleanupTimeout = time.Minute
|
||||
@@ -445,7 +446,6 @@ func postFrontStarted(ctx context.Context, kubernetesProvider *kubernetes.Provid
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
|
||||
if (config.Config.Scripting.Source != "" || len(config.Config.Scripting.Sources) > 0) && config.Config.Scripting.WatchScripts {
|
||||
watchScripts(ctx, kubernetesProvider, false)
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
)
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
|
||||
@@ -13,13 +13,14 @@ import (
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"github.com/goccy/go-yaml"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/kubeshark/kubeshark/misc/version"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/kubeshark/kubeshark/misc/version"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -4,9 +4,10 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/util/homedir"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config/configStructs"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -102,23 +103,10 @@ func CreateDefaultConfig() ConfigStruct {
|
||||
},
|
||||
},
|
||||
Auth: configStructs.AuthConfig{
|
||||
RolesClaim: "role",
|
||||
Roles: map[string]configStructs.Role{
|
||||
"admin": {
|
||||
Filter: "",
|
||||
CanDownloadPCAP: true,
|
||||
CanUseScripting: true,
|
||||
ScriptingPermissions: configStructs.ScriptingPermissions{
|
||||
CanSave: true,
|
||||
CanActivate: true,
|
||||
CanDelete: true,
|
||||
},
|
||||
CanUpdateTargetedPods: true,
|
||||
CanStopTrafficCapturing: true,
|
||||
CanControlDissection: true,
|
||||
ShowAdminConsoleLink: true,
|
||||
},
|
||||
},
|
||||
RolesClaim: "groups",
|
||||
DefaultRole: "kubeshark-viewer",
|
||||
GroupMapping: map[string]string{},
|
||||
Roles: map[string]configStructs.RoleConfig{},
|
||||
},
|
||||
EnabledDissectors: []string{
|
||||
"amqp",
|
||||
@@ -145,15 +133,15 @@ func CreateDefaultConfig() ConfigStruct {
|
||||
"tcp-conn",
|
||||
},
|
||||
PortMapping: configStructs.PortMapping{
|
||||
HTTP: []uint16{80, 443, 8080},
|
||||
AMQP: []uint16{5671, 5672},
|
||||
KAFKA: []uint16{9092},
|
||||
MONGODB: []uint16{27017},
|
||||
HTTP: []uint16{80, 443, 8080},
|
||||
AMQP: []uint16{5671, 5672},
|
||||
KAFKA: []uint16{9092},
|
||||
MONGODB: []uint16{27017},
|
||||
MYSQL: []uint16{3306},
|
||||
POSTGRESQL: []uint16{5432},
|
||||
REDIS: []uint16{6379},
|
||||
LDAP: []uint16{389},
|
||||
DIAMETER: []uint16{3868},
|
||||
LDAP: []uint16{389},
|
||||
DIAMETER: []uint16{3868},
|
||||
},
|
||||
Dashboard: configStructs.DashboardConfig{
|
||||
CompleteStreamingEnabled: true,
|
||||
|
||||
@@ -7,8 +7,9 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
)
|
||||
|
||||
type ScriptingConfig struct {
|
||||
|
||||
@@ -155,23 +155,6 @@ type ProbeConfig struct {
|
||||
FailureThreshold int `yaml:"failureThreshold" json:"failureThreshold" default:"3"`
|
||||
}
|
||||
|
||||
type ScriptingPermissions struct {
|
||||
CanSave bool `yaml:"canSave" json:"canSave" default:"true"`
|
||||
CanActivate bool `yaml:"canActivate" json:"canActivate" default:"true"`
|
||||
CanDelete bool `yaml:"canDelete" json:"canDelete" default:"true"`
|
||||
}
|
||||
|
||||
type Role struct {
|
||||
Filter string `yaml:"filter" json:"filter" default:""`
|
||||
CanDownloadPCAP bool `yaml:"canDownloadPCAP" json:"canDownloadPCAP" default:"false"`
|
||||
CanUseScripting bool `yaml:"canUseScripting" json:"canUseScripting" default:"false"`
|
||||
ScriptingPermissions ScriptingPermissions `yaml:"scriptingPermissions" json:"scriptingPermissions"`
|
||||
CanUpdateTargetedPods bool `yaml:"canUpdateTargetedPods" json:"canUpdateTargetedPods" default:"false"`
|
||||
CanStopTrafficCapturing bool `yaml:"canStopTrafficCapturing" json:"canStopTrafficCapturing" default:"false"`
|
||||
CanControlDissection bool `yaml:"canControlDissection" json:"canControlDissection" default:"false"`
|
||||
ShowAdminConsoleLink bool `yaml:"showAdminConsoleLink" json:"showAdminConsoleLink" default:"false"`
|
||||
}
|
||||
|
||||
type SamlConfig struct {
|
||||
IdpMetadataUrl string `yaml:"idpMetadataUrl" json:"idpMetadataUrl"`
|
||||
X509crt string `yaml:"x509crt" json:"x509crt"`
|
||||
@@ -190,12 +173,70 @@ type AuthConfig struct {
|
||||
// NOTE: prior releases routed `oidc` to Descope. If you were using `oidc`
|
||||
// to mean Descope, switch to `descope` (or `default`). The rename is a
|
||||
// breaking change documented in the release notes.
|
||||
Type string `yaml:"type" json:"type" default:"saml"`
|
||||
Roles map[string]Role `yaml:"roles" json:"roles"`
|
||||
RolesClaim string `yaml:"rolesClaim" json:"rolesClaim"`
|
||||
DefaultRole string `yaml:"defaultRole" json:"defaultRole"`
|
||||
DefaultFilter string `yaml:"defaultFilter" json:"defaultFilter"`
|
||||
Saml SamlConfig `yaml:"saml" json:"saml"`
|
||||
Type string `yaml:"type" json:"type" default:"saml"`
|
||||
RolesClaim string `yaml:"rolesClaim" json:"rolesClaim"`
|
||||
// DefaultRole is applied when the authenticated user's SSO claim has no
|
||||
// recognized group. Must be one of the four built-in roles
|
||||
// (kubeshark-admin / kubeshark-realtime / kubeshark-snapshot /
|
||||
// kubeshark-viewer), the name of an operator-defined role under
|
||||
// `tap.auth.roles`, or empty for strict-deny.
|
||||
DefaultRole string `yaml:"defaultRole" json:"defaultRole"`
|
||||
// GroupMapping translates SSO group names into role names (built-in or
|
||||
// operator-defined). Optional — groups whose name already matches a
|
||||
// built-in role are identity-matched and don't need an entry here.
|
||||
// Operator-defined role names MUST appear here to participate in
|
||||
// resolution (identity-match is built-in-only).
|
||||
GroupMapping map[string]string `yaml:"groupMapping" json:"groupMapping"`
|
||||
// Roles is the operator-defined role catalogue, keyed by role name.
|
||||
// Each role has its own capability set + namespace scope. Names with
|
||||
// the `kubeshark-` prefix are reserved for built-ins and will be
|
||||
// rejected at hub startup. Unknown capability strings are dropped
|
||||
// with a warning; empty / "*" namespace specs mean deny-all-data and
|
||||
// allow-all respectively.
|
||||
Roles map[string]RoleConfig `yaml:"roles" json:"roles"`
|
||||
Cli CliAuthConfig `yaml:"cli" json:"cli"`
|
||||
Saml SamlConfig `yaml:"saml" json:"saml"`
|
||||
}
|
||||
|
||||
// CliAuthConfig gates ServiceAccount-token auth for the CLI. When enabled, the
|
||||
// chart creates a `kubeshark-cli` ServiceAccount plus a Role permitting
|
||||
// `create` on its token, and the hub allowlists it via the
|
||||
// AUTH_CLI_SERVICE_ACCOUNTS env var. The CLI mints a short-lived token for
|
||||
// that SA to authenticate to a gated hub. Map `kubeshark-cli` to a role via
|
||||
// GroupMapping (or DefaultRole); without a mapping it falls back to
|
||||
// DefaultRole.
|
||||
type CliAuthConfig struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled" default:"false"`
|
||||
// Subjects are the RBAC subjects permitted to mint the kubeshark-cli
|
||||
// token — i.e. who may use the CLI against a gated hub. Rendered verbatim
|
||||
// into the kubeshark-cli-token-minter RoleBinding; empty means the Role is
|
||||
// created but bound to nobody.
|
||||
Subjects []CliAuthSubject `yaml:"subjects" json:"subjects"`
|
||||
}
|
||||
|
||||
// CliAuthSubject is one entry under tap.auth.cli.subjects, mirroring
|
||||
// rbacv1.Subject: Kind is User, Group or ServiceAccount; ApiGroup is
|
||||
// rbac.authorization.k8s.io for User and Group and must be empty for
|
||||
// ServiceAccount; Namespace applies to ServiceAccount only. The empty-able
|
||||
// fields are omitted when unset so the rendered RoleBinding stays valid for
|
||||
// every kind.
|
||||
type CliAuthSubject struct {
|
||||
Kind string `yaml:"kind" json:"kind"`
|
||||
Name string `yaml:"name" json:"name"`
|
||||
ApiGroup string `yaml:"apiGroup,omitempty" json:"apiGroup,omitempty"`
|
||||
Namespace string `yaml:"namespace,omitempty" json:"namespace,omitempty"`
|
||||
}
|
||||
|
||||
// RoleConfig is an operator-defined role declared under tap.auth.roles.
|
||||
// Capabilities is the closed vocabulary documented in the hub project
|
||||
// (snapshot:read / snapshot:write / snapshot:dissection / dissection:live /
|
||||
// dissection:control / pods:target:write / settings:write); unknown
|
||||
// capability strings are warn-dropped at hub startup. Namespaces is a
|
||||
// comma-separated list with `*` (allow-all) and glob (`foo-*`, `*-bar`,
|
||||
// `*mid*`) support; empty string means deny-all-data.
|
||||
type RoleConfig struct {
|
||||
Capabilities []string `yaml:"capabilities" json:"capabilities"`
|
||||
Namespaces string `yaml:"namespaces" json:"namespaces"`
|
||||
}
|
||||
|
||||
type IngressConfig struct {
|
||||
@@ -292,15 +333,15 @@ type PcapDumpConfig struct {
|
||||
}
|
||||
|
||||
type PortMapping struct {
|
||||
HTTP []uint16 `yaml:"http" json:"http"`
|
||||
AMQP []uint16 `yaml:"amqp" json:"amqp"`
|
||||
KAFKA []uint16 `yaml:"kafka" json:"kafka"`
|
||||
MONGODB []uint16 `yaml:"mongodb" json:"mongodb"`
|
||||
HTTP []uint16 `yaml:"http" json:"http"`
|
||||
AMQP []uint16 `yaml:"amqp" json:"amqp"`
|
||||
KAFKA []uint16 `yaml:"kafka" json:"kafka"`
|
||||
MONGODB []uint16 `yaml:"mongodb" json:"mongodb"`
|
||||
MYSQL []uint16 `yaml:"mysql" json:"mysql"`
|
||||
POSTGRESQL []uint16 `yaml:"postgresql" json:"postgresql"`
|
||||
REDIS []uint16 `yaml:"redis" json:"redis"`
|
||||
LDAP []uint16 `yaml:"ldap" json:"ldap"`
|
||||
DIAMETER []uint16 `yaml:"diameter" json:"diameter"`
|
||||
LDAP []uint16 `yaml:"ldap" json:"ldap"`
|
||||
DIAMETER []uint16 `yaml:"diameter" json:"diameter"`
|
||||
}
|
||||
|
||||
type SecurityContextConfig struct {
|
||||
|
||||
@@ -148,7 +148,7 @@ func TestMergeSetFlagNotSliceValues(t *testing.T) {
|
||||
{Name: "int field", FieldsSetValues: []FieldSetValues{{SetValues: []string{"int-field=6"}, FieldName: "IntField", FieldValue: 6}}},
|
||||
{Name: "bool field", FieldsSetValues: []FieldSetValues{{SetValues: []string{"bool-field=true"}, FieldName: "BoolField", FieldValue: true}}},
|
||||
{Name: "uint field", FieldsSetValues: []FieldSetValues{{SetValues: []string{"uint-field=6"}, FieldName: "UintField", FieldValue: uint(6)}}},
|
||||
{Name: "four fields combined", FieldsSetValues: []FieldSetValues {
|
||||
{Name: "four fields combined", FieldsSetValues: []FieldSetValues{
|
||||
{SetValues: []string{"string-field=test"}, FieldName: "StringField", FieldValue: "test"},
|
||||
{SetValues: []string{"int-field=6"}, FieldName: "IntField", FieldValue: 6},
|
||||
{SetValues: []string{"bool-field=true"}, FieldName: "BoolField", FieldValue: true},
|
||||
|
||||
38
go.mod
38
go.mod
@@ -1,8 +1,6 @@
|
||||
module github.com/kubeshark/kubeshark
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.5
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/creasty/defaults v1.5.2
|
||||
@@ -38,11 +36,11 @@ require (
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||
github.com/chai2010/gettext-go v1.0.2 // indirect
|
||||
github.com/containerd/containerd v1.7.27 // indirect
|
||||
github.com/containerd/containerd v1.7.34 // indirect
|
||||
github.com/containerd/errdefs v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v0.2.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 // 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.9.11+incompatible // indirect
|
||||
@@ -53,7 +51,7 @@ require (
|
||||
github.com/gdamore/tcell/v2 v2.7.1 // indirect
|
||||
github.com/go-errors/errors v1.4.2 // indirect
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // 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
|
||||
@@ -89,7 +87,7 @@ require (
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/spdystream v0.5.0 // indirect
|
||||
github.com/moby/spdystream v0.5.1 // indirect
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -100,28 +98,30 @@ require (
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/rubenv/sql-migrate v1.8.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/shopspring/decimal v1.4.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
|
||||
github.com/xlab/treeprint v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.39.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/oauth2 v0.28.0 // indirect
|
||||
golang.org/x/sync v0.15.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.26.0 // indirect
|
||||
golang.org/x/time v0.9.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect
|
||||
google.golang.org/grpc v1.68.1 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/grpc v1.83.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/sourcemap.v1 v1.0.5 // indirect
|
||||
@@ -133,7 +133,7 @@ require (
|
||||
k8s.io/klog/v2 v2.130.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
|
||||
oras.land/oras-go/v2 v2.6.0 // indirect
|
||||
oras.land/oras-go/v2 v2.6.2 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.19.0 // indirect
|
||||
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
|
||||
|
||||
112
go.sum
112
go.sum
@@ -36,8 +36,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=
|
||||
github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=
|
||||
github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII=
|
||||
github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0=
|
||||
github.com/containerd/containerd v1.7.34 h1:Q35B4FUECxcoaMz9QrOlqp+0s72w8/0NWawVMhVdf5g=
|
||||
github.com/containerd/containerd v1.7.34/go.mod h1:ozI//0TomTCLPhQREnx0IXDIQMg+Fk7yTtg9fNvU8EQ=
|
||||
github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4=
|
||||
github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
@@ -53,8 +53,8 @@ github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
|
||||
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/creasty/defaults v1.5.2 h1:/VfB6uxpyp6h0fr7SPp7n8WJBoV8jfxQXPCnkVSjyls=
|
||||
github.com/creasty/defaults v1.5.2/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqLPvXUZGfY=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
|
||||
github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
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=
|
||||
@@ -101,8 +101,8 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=
|
||||
github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=
|
||||
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/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
|
||||
@@ -238,8 +238,8 @@ github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQ
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
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/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y=
|
||||
github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -294,8 +294,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/robertkrimen/otto v0.2.1 h1:FVP0PJ0AHIjC+N4pKCG9yCDz6LHNPCwi/GKID5pGGF0=
|
||||
github.com/robertkrimen/otto v0.2.1/go.mod h1:UPwtJ1Xu7JrLcZjNWN8orJaM5n5YEtqL//farB5FlRY=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
|
||||
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
|
||||
@@ -327,8 +327,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tanqiangyes/grep-go v0.0.0-20220515134556-b36bff9c3d8e h1:+qDZ81UqxfZsWK6Vq9wET3AsdQxHGbViYOqkNxZ9FnU=
|
||||
github.com/tanqiangyes/grep-go v0.0.0-20220515134556-b36bff9c3d8e/go.mod h1:ANZlXE3vfRYCYnkojePl2hJODYmOeCVD+XahuhDdTbI=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
@@ -348,16 +348,16 @@ github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w=
|
||||
go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4=
|
||||
go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q=
|
||||
go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw=
|
||||
go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8=
|
||||
@@ -382,16 +382,16 @@ go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsu
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s=
|
||||
go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=
|
||||
go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=
|
||||
go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ=
|
||||
go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM=
|
||||
go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=
|
||||
go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s=
|
||||
go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg=
|
||||
go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -400,14 +400,14 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
|
||||
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
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=
|
||||
@@ -416,18 +416,18 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -447,31 +447,31 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
|
||||
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
|
||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
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=
|
||||
@@ -480,14 +480,14 @@ golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3j
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
|
||||
google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0=
|
||||
google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.83.0 h1:JeNZEKJFbQxArAMl+hiytHauacDNqJUllNfmIMmpqnQ=
|
||||
google.golang.org/grpc v1.83.0/go.mod h1:kDyl6SKsiHKt0uylY5gtn5cEjkrIOhQOGDgIc4JGwzQ=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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=
|
||||
@@ -526,8 +526,8 @@ k8s.io/kubectl v0.33.2 h1:7XKZ6DYCklu5MZQzJe+CkCjoGZwD1wWl7t/FxzhMz7Y=
|
||||
k8s.io/kubectl v0.33.2/go.mod h1:8rC67FB8tVTYraovAGNi/idWIK90z2CHFNMmGJZJ3KI=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=
|
||||
oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=
|
||||
oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo=
|
||||
oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4=
|
||||
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.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ=
|
||||
|
||||
@@ -218,6 +218,8 @@ Example for overriding image names:
|
||||
| `tap.auth.rolesClaim` | Name of the JWT claim (OIDC) or SAML attribute carrying role memberships. | `role` |
|
||||
| `tap.auth.defaultRole` | Optional role name inside `tap.auth.roles` applied as fallback when an authenticated user has no matching role. Empty string = no fallback, zero-valued permissions. | `""` |
|
||||
| `tap.auth.roles` | Backend-neutral role map shared by SAML and OIDC. Each role's `namespaces` is a comma-separated list controlling which Kubernetes namespaces the role's users see traffic for: `""` = deny all, `"*"` = allow all, `"foo"` = literal namespace, `"foo,bar"` = OR over literals, `"foo-*"` = glob expansion against the cluster's known namespaces. Empty/unset `tap.auth.roles` grants nothing — admins opt into elevated access by populating this map. | `{"admin":{"namespaces":"*","canDownloadPCAP":true,"canUpdateTargetedPods":true,"canUseScripting":true,"scriptingPermissions":{"canSave":true,"canActivate":true,"canDelete":true},"canStopTrafficCapturing":true,"canControlDissection":true,"showAdminConsoleLink":true}}` |
|
||||
| `tap.auth.cli.enabled` | Enable ServiceAccount-token auth for the CLI. Creates a `kubeshark-cli` ServiceAccount plus a Role permitting `create` on its token, and allowlists it on the hub via `AUTH_CLI_SERVICE_ACCOUNTS`. The CLI mints a short-lived token for that SA to authenticate to a gated hub. Map `kubeshark-cli` to a role via `tap.auth.groupMapping`; without a mapping it falls back to `tap.auth.defaultRole`. | `false` |
|
||||
| `tap.auth.cli.subjects` | RBAC subjects permitted to mint the `kubeshark-cli` token — i.e. who may use the CLI against a gated hub. Each entry mirrors an `rbacv1.Subject`: `kind` (`User`, `Group` or `ServiceAccount`), `name`, `apiGroup` (`rbac.authorization.k8s.io` for `User`/`Group`, omitted for `ServiceAccount`) and `namespace` (`ServiceAccount` only). Empty means the Role is created but bound to nobody. | `[]` |
|
||||
| `tap.auth.saml.idpMetadataUrl` | SAML IDP metadata URL <br/>(effective, if `tap.auth.type = saml`) | `` |
|
||||
| `tap.auth.saml.x509crt` | A self-signed X.509 `.cert` contents <br/>(effective, if `tap.auth.type = saml`) | `` |
|
||||
| `tap.auth.saml.x509key` | A self-signed X.509 `.key` contents <br/>(effective, if `tap.auth.type = saml`) | `` |
|
||||
|
||||
@@ -146,6 +146,10 @@ spec:
|
||||
value: '{{ (include "sentry.enabled" .) }}'
|
||||
- name: SENTRY_ENVIRONMENT
|
||||
value: '{{ .Values.tap.sentry.environment }}'
|
||||
{{- if eq (include "kubeshark.authEnabled" .) "true" }}
|
||||
- name: HUB_INTERNAL_TOKEN_PATH
|
||||
value: /var/run/secrets/kubeshark/hub-token/token
|
||||
{{- end }}
|
||||
resources:
|
||||
limits:
|
||||
{{ if ne (toString .Values.tap.resources.sniffer.limits.cpu) "0" }}
|
||||
@@ -235,6 +239,11 @@ spec:
|
||||
{{- if .Values.tap.persistentStorage }}
|
||||
subPathExpr: $(NODE_NAME)
|
||||
{{- end }}
|
||||
{{- if eq (include "kubeshark.authEnabled" .) "true" }}
|
||||
- mountPath: /var/run/secrets/kubeshark/hub-token
|
||||
name: hub-internal-token
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- if .Values.tap.tls }}
|
||||
- command:
|
||||
- ./tracer
|
||||
@@ -275,6 +284,10 @@ spec:
|
||||
value: '{{ (include "sentry.enabled" .) }}'
|
||||
- name: SENTRY_ENVIRONMENT
|
||||
value: '{{ .Values.tap.sentry.environment }}'
|
||||
{{- if eq (include "kubeshark.authEnabled" .) "true" }}
|
||||
- name: HUB_INTERNAL_TOKEN_PATH
|
||||
value: /var/run/secrets/kubeshark/hub-token/token
|
||||
{{- end }}
|
||||
resources:
|
||||
limits:
|
||||
{{ if ne (toString .Values.tap.resources.tracer.limits.cpu) "0" }}
|
||||
@@ -350,6 +363,11 @@ spec:
|
||||
mountPropagation: HostToContainer
|
||||
name: root
|
||||
readOnly: true
|
||||
{{- if eq (include "kubeshark.authEnabled" .) "true" }}
|
||||
- mountPath: /var/run/secrets/kubeshark/hub-token
|
||||
name: hub-internal-token
|
||||
readOnly: true
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
hostNetwork: {{ .Values.tap.hostNetwork }}
|
||||
@@ -430,3 +448,12 @@ spec:
|
||||
emptyDir:
|
||||
sizeLimit: {{ .Values.tap.storageLimit }}
|
||||
{{- end }}
|
||||
{{- if eq (include "kubeshark.authEnabled" .) "true" }}
|
||||
- name: hub-internal-token
|
||||
projected:
|
||||
sources:
|
||||
- serviceAccountToken:
|
||||
path: token
|
||||
audience: kubeshark-hub
|
||||
expirationSeconds: 3600
|
||||
{{- end }}
|
||||
|
||||
@@ -18,20 +18,18 @@ data:
|
||||
INGRESS_ENABLED: '{{ .Values.tap.ingress.enabled }}'
|
||||
INGRESS_HOST: '{{ .Values.tap.ingress.host }}'
|
||||
PROXY_FRONT_PORT: '{{ .Values.tap.proxy.front.port }}'
|
||||
AUTH_ENABLED: '{{- if and .Values.cloudLicenseEnabled (not (empty .Values.license)) -}}
|
||||
{{ (default false .Values.demoModeEnabled) | ternary true ((and .Values.tap.auth.enabled (or (eq .Values.tap.auth.type "oidc") (eq .Values.tap.auth.type "dex"))) | ternary true false) }}
|
||||
{{- else -}}
|
||||
{{ .Values.cloudLicenseEnabled | ternary "true" ((default false .Values.demoModeEnabled) | ternary "true" .Values.tap.auth.enabled) }}
|
||||
{{- end }}'
|
||||
AUTH_ENABLED: '{{ include "kubeshark.authEnabled" . }}'
|
||||
AUTH_TYPE: '{{- if and .Values.cloudLicenseEnabled (not (or (eq .Values.tap.auth.type "oidc") (eq .Values.tap.auth.type "dex"))) -}}
|
||||
default
|
||||
{{- else -}}
|
||||
{{ (default false .Values.demoModeEnabled) | ternary "default" .Values.tap.auth.type }}
|
||||
{{- end }}'
|
||||
AUTH_SAML_IDP_METADATA_URL: '{{ .Values.tap.auth.saml.idpMetadataUrl }}'
|
||||
AUTH_ROLES: '{{ .Values.tap.auth.roles | toJson }}'
|
||||
AUTH_CLI_SERVICE_ACCOUNTS: '{{ if (((.Values.tap).auth).cli).enabled }}{{ .Release.Namespace }}:kubeshark-cli{{ end }}'
|
||||
AUTH_ROLES_CLAIM: '{{ .Values.tap.auth.rolesClaim }}'
|
||||
AUTH_DEFAULT_ROLE: '{{ default "" .Values.tap.auth.defaultRole }}'
|
||||
AUTH_GROUP_MAPPING: '{{ default (dict) .Values.tap.auth.groupMapping | toJson }}'
|
||||
AUTH_ROLES: '{{ default (dict) .Values.tap.auth.roles | toJson }}'
|
||||
AUTH_OIDC_ISSUER: '{{ default "not set" (((.Values.tap).auth).oidc).issuer }}'
|
||||
AUTH_OIDC_REFRESH_TOKEN_LIFETIME: '{{ default "3960h" (((.Values.tap).auth).oidc).refreshTokenLifetime }}'
|
||||
AUTH_OIDC_STATE_PARAM_EXPIRY: '{{ default "10m" (((.Values.tap).auth).oidc).oauth2StateParamExpiry }}'
|
||||
|
||||
44
helm-chart/templates/22-cli-auth.yaml
Normal file
44
helm-chart/templates/22-cli-auth.yaml
Normal file
@@ -0,0 +1,44 @@
|
||||
{{- if (((.Values.tap).auth).cli).enabled }}
|
||||
---
|
||||
# ServiceAccount the CLI mints a short-lived token for (TokenRequest API) to
|
||||
# authenticate to a gated Hub. Its name must match the Hub's
|
||||
# AUTH_CLI_SERVICE_ACCOUNTS allowlist and the CLI's kubeshark-cli constant.
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "kubeshark.labels" . | nindent 4 }}
|
||||
name: kubeshark-cli
|
||||
namespace: {{ .Release.Namespace }}
|
||||
---
|
||||
# Permission to mint a token for the kubeshark-cli SA. Binding this Role to a
|
||||
# subject is what grants that subject CLI access to a gated Hub.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "kubeshark.labels" . | nindent 4 }}
|
||||
name: kubeshark-cli-token-minter
|
||||
namespace: {{ .Release.Namespace }}
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["serviceaccounts/token"]
|
||||
resourceNames: ["kubeshark-cli"]
|
||||
verbs: ["create"]
|
||||
{{- with .Values.tap.auth.cli.subjects }}
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "kubeshark.labels" $ | nindent 4 }}
|
||||
name: kubeshark-cli-token-minter
|
||||
namespace: {{ $.Release.Namespace }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: kubeshark-cli-token-minter
|
||||
subjects:
|
||||
{{- toYaml . | nindent 2 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -110,3 +110,17 @@ Dex IdP: retrieve a secret for static client with a specific ID
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{/*
|
||||
Single source of truth for whether the hub enforces authentication.
|
||||
Consumed by the hub ConfigMap (AUTH_ENABLED) and by the worker DaemonSet, which
|
||||
must mount an internal hub token whenever the hub requires one. Keeping the two
|
||||
in sync prevents workers from being issued no token while the hub demands one.
|
||||
*/}}
|
||||
{{- define "kubeshark.authEnabled" -}}
|
||||
{{- if and .Values.cloudLicenseEnabled (not (empty .Values.license)) -}}
|
||||
{{ (default false .Values.demoModeEnabled) | ternary true ((and .Values.tap.auth.enabled (or (eq .Values.tap.auth.type "oidc") (eq .Values.tap.auth.type "dex"))) | ternary true false) }}
|
||||
{{- else -}}
|
||||
{{ .Values.cloudLicenseEnabled | ternary "true" ((default false .Values.demoModeEnabled) | ternary "true" .Values.tap.auth.enabled) }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
@@ -153,22 +153,13 @@ tap:
|
||||
auth:
|
||||
enabled: false
|
||||
type: saml
|
||||
roles:
|
||||
admin:
|
||||
filter: ""
|
||||
canDownloadPCAP: true
|
||||
canUseScripting: true
|
||||
scriptingPermissions:
|
||||
canSave: true
|
||||
canActivate: true
|
||||
canDelete: true
|
||||
canUpdateTargetedPods: true
|
||||
canStopTrafficCapturing: true
|
||||
canControlDissection: true
|
||||
showAdminConsoleLink: true
|
||||
rolesClaim: role
|
||||
defaultRole: ""
|
||||
defaultFilter: ""
|
||||
rolesClaim: groups
|
||||
defaultRole: kubeshark-viewer
|
||||
groupMapping: {}
|
||||
roles: {}
|
||||
cli:
|
||||
enabled: false
|
||||
subjects: []
|
||||
saml:
|
||||
idpMetadataUrl: ""
|
||||
x509crt: ""
|
||||
|
||||
@@ -230,7 +230,9 @@ func TestMCP_ToolsList_ReadOnly(t *testing.T) {
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Tools []struct{ Name string `json:"name"` } `json:"tools"`
|
||||
Tools []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Result, &result); err != nil {
|
||||
t.Fatalf("Failed to parse result: %v", err)
|
||||
@@ -262,7 +264,9 @@ func TestMCP_ToolsList_WithDestructive(t *testing.T) {
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Tools []struct{ Name string `json:"name"` } `json:"tools"`
|
||||
Tools []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"tools"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Result, &result); err != nil {
|
||||
t.Fatalf("Failed to parse result: %v", err)
|
||||
@@ -296,7 +300,9 @@ func TestMCP_CheckKubesharkStatus_NotRunning(t *testing.T) {
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Content []struct{ Text string `json:"text"` } `json:"content"`
|
||||
Content []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Result, &result); err != nil {
|
||||
t.Fatalf("Failed to parse result: %v", err)
|
||||
@@ -341,8 +347,10 @@ func TestMCP_StartKubeshark_WithoutFlag(t *testing.T) {
|
||||
resp := session.callTool(t, 2, "start_kubeshark", nil)
|
||||
|
||||
var result struct {
|
||||
Content []struct{ Text string `json:"text"` } `json:"content"`
|
||||
IsError bool `json:"isError"`
|
||||
Content []struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"content"`
|
||||
IsError bool `json:"isError"`
|
||||
}
|
||||
if err := json.Unmarshal(resp.Result, &result); err != nil {
|
||||
t.Fatalf("Failed to parse result: %v", err)
|
||||
|
||||
@@ -51,7 +51,7 @@ func (connector *Connector) TestConnection(path string) error {
|
||||
}
|
||||
|
||||
if retriesLeft == 0 {
|
||||
return fmt.Errorf("Couldn't reach the URL: %s after %d retries!", connector.url, connector.retries)
|
||||
return fmt.Errorf("couldn't reach the URL: %s after %d retries", connector.url, connector.retries)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/rs/zerolog/log"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
"helm.sh/helm/v3/pkg/action"
|
||||
@@ -22,6 +20,9 @@ import (
|
||||
"helm.sh/helm/v3/pkg/registry"
|
||||
"helm.sh/helm/v3/pkg/release"
|
||||
"helm.sh/helm/v3/pkg/repo"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
)
|
||||
|
||||
const ENV_HELM_DRIVER = "HELM_DRIVER"
|
||||
@@ -79,7 +80,7 @@ func (h *Helm) Install() (rel *release.Release, err error) {
|
||||
}
|
||||
|
||||
var cp string
|
||||
cp, err = client.ChartPathOptions.LocateChart(chartURL, settings)
|
||||
cp, err = client.LocateChart(chartURL, settings)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -87,7 +88,7 @@ func (h *Helm) Install() (rel *release.Release, err error) {
|
||||
m := &downloader.Manager{
|
||||
Out: os.Stdout,
|
||||
ChartPath: cp,
|
||||
Keyring: client.ChartPathOptions.Keyring,
|
||||
Keyring: client.Keyring,
|
||||
SkipUpdate: false,
|
||||
Getters: getter.All(settings),
|
||||
RepositoryConfig: settings.RepositoryConfig,
|
||||
|
||||
112
kubernetes/hubtoken.go
Normal file
112
kubernetes/hubtoken.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
// HubTokenAudience must match the hub's internalauth TokenAudience.
|
||||
HubTokenAudience = "kubeshark-hub"
|
||||
// CLIServiceAccountName is the ServiceAccount the CLI mints a token for;
|
||||
// must match the SA the helm chart creates and the hub's
|
||||
// AUTH_CLI_SERVICE_ACCOUNTS allowlist.
|
||||
CLIServiceAccountName = "kubeshark-cli"
|
||||
|
||||
hubTokenExpirySeconds = 3600
|
||||
// hubTokenRenewMargin re-mints this long before expiry so requests in
|
||||
// flight always carry a comfortably valid token.
|
||||
hubTokenRenewMargin = 5 * time.Minute
|
||||
// hubTokenMintRetryInterval throttles re-minting after a failure so a
|
||||
// cluster where minting can't succeed (no RBAC, SA missing) doesn't pay a
|
||||
// blocking CreateToken round-trip on every request.
|
||||
hubTokenMintRetryInterval = 30 * time.Second
|
||||
)
|
||||
|
||||
// MintHubToken requests a short-lived ServiceAccount token (audience
|
||||
// HubTokenAudience) for the CLI ServiceAccount via the TokenRequest API. The
|
||||
// hub validates it with TokenReview and maps the SA to a role. The caller's
|
||||
// kube RBAC must permit `create` on serviceaccounts/token for that SA — which
|
||||
// is what gates who may use the CLI against a gated Hub.
|
||||
func (provider *Provider) MintHubToken(ctx context.Context, namespace string) (string, error) {
|
||||
tok, _, err := provider.mintHubToken(ctx, namespace)
|
||||
return tok, err
|
||||
}
|
||||
|
||||
// mintHubToken is MintHubToken plus the token's expiry (from the TokenRequest
|
||||
// response, so a cluster that caps the duration below the request is respected;
|
||||
// falls back to the requested TTL if the API omits it).
|
||||
func (provider *Provider) mintHubToken(ctx context.Context, namespace string) (string, time.Time, error) {
|
||||
exp := int64(hubTokenExpirySeconds)
|
||||
tr, err := provider.clientSet.CoreV1().ServiceAccounts(namespace).CreateToken(
|
||||
ctx,
|
||||
CLIServiceAccountName,
|
||||
&authenticationv1.TokenRequest{
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
Audiences: []string{HubTokenAudience},
|
||||
ExpirationSeconds: &exp,
|
||||
},
|
||||
},
|
||||
metav1.CreateOptions{},
|
||||
)
|
||||
if err != nil {
|
||||
return "", time.Time{}, fmt.Errorf("minting hub token for serviceaccount %s/%s: %w", namespace, CLIServiceAccountName, err)
|
||||
}
|
||||
expireAt := tr.Status.ExpirationTimestamp.Time
|
||||
if expireAt.IsZero() {
|
||||
expireAt = time.Now().Add(time.Duration(hubTokenExpirySeconds) * time.Second)
|
||||
}
|
||||
return tr.Status.Token, expireAt, nil
|
||||
}
|
||||
|
||||
// HubTokenRenewer hands out a hub SA token, transparently re-minting it before
|
||||
// it expires. Safe for concurrent use; intended for long-lived CLI processes
|
||||
// (mcp/console proxy mode) that would otherwise 401 after the ~1h token TTL.
|
||||
type HubTokenRenewer struct {
|
||||
provider *Provider
|
||||
namespace string
|
||||
|
||||
mu sync.Mutex
|
||||
token string
|
||||
expireAt time.Time
|
||||
nextAttempt time.Time // earliest time to retry minting after a failure
|
||||
}
|
||||
|
||||
// NewHubTokenRenewer builds a renewer that mints kubeshark-cli tokens in the
|
||||
// given namespace.
|
||||
func NewHubTokenRenewer(provider *Provider, namespace string) *HubTokenRenewer {
|
||||
return &HubTokenRenewer{provider: provider, namespace: namespace}
|
||||
}
|
||||
|
||||
// Token returns a currently-valid hub token, minting or re-minting as needed.
|
||||
// On a mint failure it returns the last token (possibly "") so the caller can
|
||||
// fall back to the License-Key rather than hard-failing.
|
||||
func (r *HubTokenRenewer) Token() string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
needsMint := r.token == "" || time.Now().After(r.expireAt.Add(-hubTokenRenewMargin))
|
||||
if needsMint && time.Now().After(r.nextAttempt) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
tok, expireAt, err := r.provider.mintHubToken(ctx, r.namespace)
|
||||
cancel()
|
||||
if err != nil {
|
||||
// Throttle retries so a failing cluster doesn't block every request
|
||||
// on a doomed mint; fall through to the expiry check below.
|
||||
r.nextAttempt = time.Now().Add(hubTokenMintRetryInterval)
|
||||
} else {
|
||||
r.token, r.expireAt, r.nextAttempt = tok, expireAt, time.Time{}
|
||||
}
|
||||
}
|
||||
// Never hand out an already-expired token: return "" so the auth
|
||||
// round-tripper falls back to the License-Key instead of looping on 401s
|
||||
// (it prefers any non-empty SA token over the License-Key).
|
||||
if r.token != "" && !r.expireAt.IsZero() && time.Now().After(r.expireAt) {
|
||||
return ""
|
||||
}
|
||||
return r.token
|
||||
}
|
||||
@@ -16,7 +16,7 @@ type PodWatchHelper struct {
|
||||
func NewPodWatchHelper(kubernetesProvider *Provider, NameRegexFilter *regexp.Regexp) *PodWatchHelper {
|
||||
return &PodWatchHelper{
|
||||
kubernetesProvider: kubernetesProvider,
|
||||
NameRegexFilter: NameRegexFilter,
|
||||
NameRegexFilter: NameRegexFilter,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,10 +11,6 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/kubeshark/kubeshark/semver"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/tanqiangyes/grep-go/reader"
|
||||
core "k8s.io/api/core/v1"
|
||||
@@ -25,6 +21,11 @@ import (
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/kubeshark/kubeshark/semver"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
)
|
||||
|
||||
type Provider struct {
|
||||
|
||||
@@ -11,12 +11,13 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/rs/zerolog/log"
|
||||
"k8s.io/apimachinery/pkg/util/httpstream"
|
||||
"k8s.io/client-go/tools/portforward"
|
||||
"k8s.io/client-go/transport/spdy"
|
||||
"k8s.io/kubectl/pkg/proxy"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
)
|
||||
|
||||
const k8sProxyApiPrefix = "/"
|
||||
@@ -155,7 +156,7 @@ func getHttpDialer(kubernetesProvider *Provider, namespace string, podName strin
|
||||
|
||||
clientConfigHostUrl, err := url.Parse(kubernetesProvider.clientConfig.Host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed parsing client config host URL %s, error %w", kubernetesProvider.clientConfig.Host, err)
|
||||
return nil, fmt.Errorf("failed parsing client config host URL %s, error %w", kubernetesProvider.clientConfig.Host, err)
|
||||
}
|
||||
path := fmt.Sprintf("%s/api/v1/namespaces/%s/pods/%s/portforward", clientConfigHostUrl.Path, namespace, podName)
|
||||
|
||||
|
||||
@@ -7,9 +7,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kubeshark/kubeshark/debounce"
|
||||
"github.com/rs/zerolog/log"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
|
||||
"github.com/kubeshark/kubeshark/debounce"
|
||||
)
|
||||
|
||||
type EventFilterer interface {
|
||||
@@ -47,7 +48,6 @@ func FilteredWatch(ctx context.Context, watcherCreator WatchCreator, targetNames
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -5,9 +5,10 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/kubeshark/kubeshark/cmd"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/kubeshark/kubeshark/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -88,6 +88,30 @@ Connect directly to an existing Kubeshark deployment:
|
||||
}
|
||||
```
|
||||
|
||||
For a **gated Hub** (`AUTH_ENABLED=true`), URL mode can't mint a token (no kube
|
||||
access), so supply one explicitly via `--token` (or the `KUBESHARK_HUB_TOKEN`
|
||||
env var). Mint it from a machine that has cluster access:
|
||||
|
||||
```bash
|
||||
kubectl create token kubeshark-cli -n <release-namespace> --audience kubeshark-hub
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"kubeshark": {
|
||||
"command": "kubeshark",
|
||||
"args": ["mcp", "--url", "https://kubeshark.example.com", "--token", "<token>"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The token is short-lived (~1h) and URL mode **cannot auto-renew** it; when it
|
||||
expires the server reports a clear `401 ... token expired/invalid` message — re-mint
|
||||
and restart. Proxy mode (default, with kube access) mints the `kubeshark-cli`
|
||||
token automatically and **auto-renews** it, so long-running sessions don't expire.
|
||||
|
||||
#### With Destructive Operations
|
||||
|
||||
```json
|
||||
@@ -179,6 +203,7 @@ Found 5 services connecting to postgres:5432:
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `--url` | Direct URL to Kubeshark Hub |
|
||||
| `--token` | Hub SA/bearer token for `--url` mode against a gated Hub (also `KUBESHARK_HUB_TOKEN`); ignored in proxy mode, which mints and auto-renews the token |
|
||||
| `--kubeconfig` | Path to kubeconfig file |
|
||||
| `--allow-destructive` | Enable start/stop operations |
|
||||
| `--list-tools` | List available tools and exit |
|
||||
|
||||
@@ -7,10 +7,11 @@ import (
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/kubeshark/kubeshark/config"
|
||||
"github.com/kubeshark/kubeshark/kubernetes"
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func DumpLogs(ctx context.Context, provider *kubernetes.Provider, filePath string, grep string) error {
|
||||
@@ -21,7 +22,7 @@ func DumpLogs(ctx context.Context, provider *kubernetes.Provider, filePath strin
|
||||
}
|
||||
|
||||
if len(pods) == 0 {
|
||||
return fmt.Errorf("No %s pods found in namespace %s", misc.Software, config.Config.Tap.Release.Namespace)
|
||||
return fmt.Errorf("no %s pods found in namespace %s", misc.Software, config.Config.Tap.Release.Namespace)
|
||||
}
|
||||
|
||||
newZipFile, err := os.Create(filePath)
|
||||
|
||||
@@ -8,9 +8,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/kubeshark/kubeshark/misc"
|
||||
"github.com/kubeshark/kubeshark/utils"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/google/go-github/v37/github"
|
||||
)
|
||||
|
||||
@@ -113,25 +113,15 @@ waiting for snapshot creation or dissection.
|
||||
|
||||
Confirm Kubeshark is running and which tools are available.
|
||||
|
||||
**Tool**: `get_data_boundaries`
|
||||
|
||||
Check how far back raw capture data exists. You need this to plan snapshot
|
||||
creation in Step 3 — call it now so the data is ready when you need it.
|
||||
|
||||
**Tool**: `list_workloads` (no snapshot_id — queries live state)
|
||||
|
||||
Get the current workload inventory for the target namespace. This returns
|
||||
pod names, namespaces, and IP addresses. Save the IPs — you'll need them
|
||||
throughout the audit.
|
||||
|
||||
**Note**: `list_workloads` without a `snapshot_id` may fail with some
|
||||
Kubeshark versions (`snapshot_id is required for filtered listing`). If
|
||||
this happens, use individual lookups with `name` + `namespace` parameters,
|
||||
or skip to Step 3 and get the workload inventory from the first snapshot.
|
||||
|
||||
### Step 2: Query Live Traffic
|
||||
|
||||
In parallel, query the real-time dissected traffic across key dimensions.
|
||||
**Tool**: `get_l7_data_boundaries`
|
||||
|
||||
Check the time boundaries of dissected API calls in the real-time database.
|
||||
This tells you how far back L7 data is available — use it to understand
|
||||
the scope of your real-time queries before running them.
|
||||
|
||||
Then query the real-time dissected traffic across key dimensions.
|
||||
Use `list_api_calls` and `list_l4_flows` **without** a `snapshot_id` to
|
||||
hit the live data.
|
||||
|
||||
@@ -155,6 +145,12 @@ appear. Treat Section A findings as a fast first pass, not the final word.
|
||||
|
||||
While analyzing real-time data, begin creating snapshots for Section B.
|
||||
|
||||
**Tool**: `get_data_boundaries`
|
||||
|
||||
Check how far back raw capture data exists. Raw capture is the FIFO buffer
|
||||
that feeds snapshot creation — this tells you the time window available
|
||||
for snapshots (which is different from the L7 boundaries in Step 2).
|
||||
|
||||
**CRITICAL: Create snapshots ONE AT A TIME, sequentially.** Kubeshark only
|
||||
supports one concurrent snapshot download. Parallel creation will cause
|
||||
failures and data loss. The pattern is:
|
||||
@@ -164,8 +160,7 @@ failures and data loss. The pattern is:
|
||||
3. You do NOT need to wait for dissection before creating the next snapshot.
|
||||
Create the next snapshot while the previous one dissects.
|
||||
|
||||
Use the data boundaries from Step 1 (`get_data_boundaries`) to calculate
|
||||
how many snapshots are needed:
|
||||
Use `get_data_boundaries` to calculate how many snapshots are needed:
|
||||
|
||||
```
|
||||
total_range_ms = newest_timestamp - oldest_timestamp
|
||||
@@ -247,7 +242,6 @@ wait for dissection to use the first two:
|
||||
| Source | Available | Tool | What It Provides |
|
||||
|--------|-----------|------|-----------------|
|
||||
| **Workloads & IPs** | Immediately | `list_workloads` with `snapshot_id` | Pod names, namespaces, IPs at capture time |
|
||||
| **L4 Flows** | Immediately | `list_l4_flows` with `snapshot_id` | TCP/UDP connections: src/dst IPs, ports, bytes, duration |
|
||||
| **PCAP Export** | Immediately | `export_snapshot_pcap` | Raw packets filtered by BPF expression |
|
||||
| **L7 Dissection** | After indexing | `list_api_calls`, `get_api_call`, `get_api_stats` | DNS queries, HTTP requests, SQL statements, Redis commands, gRPC methods |
|
||||
|
||||
@@ -261,12 +255,12 @@ Snapshot ready
|
||||
├── Start dissection (background)
|
||||
├── Phase 1: list_workloads (immediate) — workload inventory + IPs
|
||||
│ export_snapshot_pcap (immediate) — raw packet evidence
|
||||
├── Phase 3: list_l4_flows (immediate) — external flows, port scanning
|
||||
├── Phase 4: list_l4_flows (immediate) — lateral movement, fan-out
|
||||
│
|
||||
├── [dissection completes]
|
||||
│
|
||||
├── Phase 2: list_api_calls — DNS threat analysis
|
||||
├── Phase 3: list_api_calls — external HTTP communication
|
||||
├── Phase 4: list_api_calls — lateral movement, K8s API access
|
||||
├── Phase 5: list_api_calls — protocol abuse (PG, Redis, gRPC)
|
||||
├── Phase 6: list_api_calls — credential access (IMDS, cloud APIs)
|
||||
└── Phase 7: correlate all findings
|
||||
@@ -396,32 +390,14 @@ Compare the count of failed queries to total queries per source pod.
|
||||
|
||||
**Goal**: Identify all traffic leaving the cluster. Any pod connecting to
|
||||
external IPs or domains needs justification.
|
||||
**Data source**: Immediate (no dissection needed). Use L4 flows first,
|
||||
then enrich with L7 data from dissection when available.
|
||||
**Data source**: L7 dissection (after indexing).
|
||||
|
||||
### 3a: L4 External Flows
|
||||
**Note**: L4 flow analysis for external communication is covered in
|
||||
Section A (Step 2) using `list_l4_flows` against real-time data. In
|
||||
Section B, use `list_api_calls` against dissected snapshot data for
|
||||
deeper L7 inspection of external traffic.
|
||||
|
||||
**Tool**: `list_l4_flows` with `snapshot_id`
|
||||
|
||||
This is available immediately — do not wait for dissection. Use the workload
|
||||
IPs from Phase 1 to map flows to pod identities.
|
||||
|
||||
Look for flows where the destination is NOT a cluster-internal IP (not RFC 1918:
|
||||
10.x.x.x, 172.16-31.x.x, 192.168.x.x). Every external flow is a potential
|
||||
exfiltration or C2 channel.
|
||||
|
||||
**What to flag**:
|
||||
|
||||
| Pattern | Threat | Severity |
|
||||
|---------|--------|----------|
|
||||
| Destination 169.254.169.254 | IMDS metadata credential theft | CRITICAL |
|
||||
| Destination port 3333, 14433, 45700 | Stratum mining protocol | CRITICAL |
|
||||
| Destination port 4444, 1337 | Reverse shell / backdoor | CRITICAL |
|
||||
| Persistent connections to single external IP | C2 beaconing | HIGH |
|
||||
| Large outbound data volume (>1MB) to external | Data exfiltration | HIGH |
|
||||
| Connections to cloud API endpoints (port 443) | Stolen credential usage | MEDIUM |
|
||||
|
||||
### 3b: HTTP External Requests
|
||||
### 3a: HTTP External Requests
|
||||
|
||||
**Tool**: `list_api_calls` with KFL: `http && !dst.pod.namespace.startsWith("kube")`
|
||||
|
||||
@@ -440,8 +416,11 @@ Inspect outbound HTTP requests for:
|
||||
|
||||
**Goal**: Identify pods communicating with services they shouldn't — crossing
|
||||
namespace boundaries, probing infrastructure, or scanning the network.
|
||||
**Data source**: L4 flows (immediate) for port scanning detection. L7
|
||||
dissection (after indexing) for cross-namespace HTTP and API server analysis.
|
||||
**Data source**: L7 dissection (after indexing) for cross-namespace HTTP
|
||||
and API server analysis.
|
||||
|
||||
**Note**: Port scanning detection via `list_l4_flows` is covered in
|
||||
Section A (Step 2) against real-time data.
|
||||
|
||||
### 4a: Cross-Namespace Traffic
|
||||
|
||||
@@ -468,19 +447,7 @@ A pod hitting **multiple** of these paths is performing systematic enumeration,
|
||||
not legitimate API access. Legitimate workloads typically access 1-2 specific
|
||||
resources, not sweep across resource types.
|
||||
|
||||
### 4c: Port Scanning Detection
|
||||
|
||||
**Tool**: `list_l4_flows` with `snapshot_id` (immediate — no dissection needed)
|
||||
|
||||
Use the workload IPs from Phase 1 to identify the source pod.
|
||||
Look for a single source IP with connections to:
|
||||
- Many distinct destination IPs (>10)
|
||||
- Many distinct destination ports (>5)
|
||||
- High connection failure rate (RST/timeout)
|
||||
|
||||
This is a textbook port scan pattern.
|
||||
|
||||
### 4d: Service Fingerprinting
|
||||
### 4c: Service Fingerprinting
|
||||
|
||||
**Tool**: `list_api_calls` with KFL: `http && (path == "/.env" || path == "/actuator/info" || path == "/server-info" || path == "/version")`
|
||||
|
||||
@@ -488,7 +455,7 @@ These paths are used for service fingerprinting — mapping what software is
|
||||
running on internal endpoints. A pod probing multiple services with these
|
||||
paths is performing reconnaissance.
|
||||
|
||||
### 4e: Service Account Permission Audit via Traffic
|
||||
### 4d: Service Account Permission Audit via Traffic
|
||||
|
||||
Cross-reference Phase 4b findings (K8s API traffic) with the source pod's
|
||||
actual service account to determine if permissions are excessive.
|
||||
@@ -515,7 +482,7 @@ For each pod making API server calls:
|
||||
This converts a network finding (API traffic volume) into an actionable RBAC
|
||||
recommendation — telling the user exactly which ClusterRoleBinding to revoke.
|
||||
|
||||
### 4f: Cross-Namespace Threat Correlation
|
||||
### 4e: Cross-Namespace Threat Correlation
|
||||
|
||||
When port scanning or lateral movement targets IPs outside the audited
|
||||
namespace (e.g., IPs in the pod CIDR `10.244.x.x` that don't belong to
|
||||
@@ -603,8 +570,6 @@ cloud API exploitation.
|
||||
|
||||
**Tool**: `list_api_calls` with KFL: `dst.ip == "169.254.169.254"`
|
||||
|
||||
Or use `list_l4_flows` to find connections to 169.254.169.254.
|
||||
|
||||
Any pod connecting to this IP is attempting to steal the node's cloud credentials.
|
||||
Check the HTTP paths:
|
||||
|
||||
|
||||
134
utils/http.go
134
utils/http.go
@@ -2,17 +2,151 @@ package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
X_KUBESHARK_CAPTURE_HEADER_KEY = "X-Kubeshark-Capture"
|
||||
X_KUBESHARK_CAPTURE_HEADER_IGNORE_VALUE = "ignore"
|
||||
LICENSE_KEY_HEADER = "License-Key"
|
||||
// CLI_AUTH_HEADER carries a ServiceAccount bearer token to the Hub.
|
||||
// A custom (non-Authorization) header so it survives the kube
|
||||
// API-server service proxy, which consumes Authorization.
|
||||
CLI_AUTH_HEADER = "X-Kubeshark-Authorization"
|
||||
)
|
||||
|
||||
// ErrHubAuthRequired indicates the Hub rejected the request because auth is
|
||||
// enabled but no valid credential was presented (missing/expired license).
|
||||
var ErrHubAuthRequired = errors.New("hub requires authentication: set a valid license (config 'license') or credentials")
|
||||
|
||||
// hubAuthRoundTripper attaches the CLI's Hub credential to every request so any
|
||||
// client built with it authenticates to a gated Hub. It prefers a scoped
|
||||
// ServiceAccount token (CLI_AUTH_HEADER) when present, falling back to the
|
||||
// License-Key (admin/transitional). Both are custom headers so they survive
|
||||
// the kube API-server service proxy, unlike an Authorization bearer.
|
||||
//
|
||||
// The SA token is sourced via saTokenFunc on every request rather than captured
|
||||
// once, so a caller with cluster access (proxy mode) can hand in a renewing
|
||||
// source and long-lived clients keep working past the token's ~1h expiry.
|
||||
type hubAuthRoundTripper struct {
|
||||
saTokenFunc func() string
|
||||
licenseKey string
|
||||
base http.RoundTripper
|
||||
}
|
||||
|
||||
func (rt *hubAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
base := rt.base
|
||||
if base == nil {
|
||||
base = http.DefaultTransport
|
||||
}
|
||||
saToken := ""
|
||||
if rt.saTokenFunc != nil {
|
||||
saToken = rt.saTokenFunc()
|
||||
}
|
||||
switch {
|
||||
case saToken != "":
|
||||
if req.Header.Get(CLI_AUTH_HEADER) == "" {
|
||||
req = req.Clone(req.Context())
|
||||
req.Header.Set(CLI_AUTH_HEADER, saToken)
|
||||
}
|
||||
case rt.licenseKey != "":
|
||||
if req.Header.Get(LICENSE_KEY_HEADER) == "" {
|
||||
req = req.Clone(req.Context())
|
||||
req.Header.Set(LICENSE_KEY_HEADER, rt.licenseKey)
|
||||
}
|
||||
}
|
||||
return base.RoundTrip(req)
|
||||
}
|
||||
|
||||
// staticToken adapts a fixed token string to the saTokenFunc source, returning
|
||||
// nil for an empty token so the round-tripper falls back to the License-Key.
|
||||
func staticToken(saToken string) func() string {
|
||||
if saToken == "" {
|
||||
return nil
|
||||
}
|
||||
return func() string { return saToken }
|
||||
}
|
||||
|
||||
// StopOnSSORedirect is an *http.Client CheckRedirect that does NOT follow
|
||||
// SSO-style auth redirects (302 Found / 303 See Other) — it returns the
|
||||
// redirect response so callers can detect auth-required via IsAuthRequired
|
||||
// instead of silently following it to an HTML login page (and, for downloads,
|
||||
// writing that page to disk). Other redirects (301/307/308) are still followed.
|
||||
func StopOnSSORedirect(req *http.Request, _ []*http.Request) error {
|
||||
if req.Response != nil {
|
||||
switch req.Response.StatusCode {
|
||||
case http.StatusFound, http.StatusSeeOther:
|
||||
return http.ErrUseLastResponse
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewHubHTTPClient returns an *http.Client that authenticates to the Hub with
|
||||
// the License-Key header.
|
||||
func NewHubHTTPClient(timeout time.Duration, licenseKey string) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: StopOnSSORedirect,
|
||||
Transport: &hubAuthRoundTripper{licenseKey: licenseKey},
|
||||
}
|
||||
}
|
||||
|
||||
// NewHubHTTPClientWithToken returns an *http.Client that authenticates to the
|
||||
// Hub with a fixed ServiceAccount token when saToken is set, otherwise the
|
||||
// License-Key.
|
||||
func NewHubHTTPClientWithToken(timeout time.Duration, saToken, licenseKey string) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: StopOnSSORedirect,
|
||||
Transport: &hubAuthRoundTripper{saTokenFunc: staticToken(saToken), licenseKey: licenseKey},
|
||||
}
|
||||
}
|
||||
|
||||
// NewHubHTTPClientWithTokenSource is NewHubHTTPClientWithToken with a token
|
||||
// source consulted per request, so a renewing source keeps the client
|
||||
// authenticated past the token's expiry. A nil source falls back to the
|
||||
// License-Key.
|
||||
func NewHubHTTPClientWithTokenSource(timeout time.Duration, saTokenFunc func() string, licenseKey string) *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: timeout,
|
||||
CheckRedirect: StopOnSSORedirect,
|
||||
Transport: &hubAuthRoundTripper{saTokenFunc: saTokenFunc, licenseKey: licenseKey},
|
||||
}
|
||||
}
|
||||
|
||||
// HubAuthTransport wraps base so requests carry the License-Key header. Use
|
||||
// when a client needs custom transport settings (e.g. streaming downloads)
|
||||
// but must still authenticate to the Hub.
|
||||
func HubAuthTransport(licenseKey string, base http.RoundTripper) http.RoundTripper {
|
||||
return &hubAuthRoundTripper{licenseKey: licenseKey, base: base}
|
||||
}
|
||||
|
||||
// HubAuthTransportWithToken is HubAuthTransport with a fixed ServiceAccount
|
||||
// token (preferred over the License-Key when set).
|
||||
func HubAuthTransportWithToken(saToken, licenseKey string, base http.RoundTripper) http.RoundTripper {
|
||||
return &hubAuthRoundTripper{saTokenFunc: staticToken(saToken), licenseKey: licenseKey, base: base}
|
||||
}
|
||||
|
||||
// HubAuthTransportWithTokenSource is HubAuthTransportWithToken with a token
|
||||
// source consulted per request (see NewHubHTTPClientWithTokenSource).
|
||||
func HubAuthTransportWithTokenSource(saTokenFunc func() string, licenseKey string, base http.RoundTripper) http.RoundTripper {
|
||||
return &hubAuthRoundTripper{saTokenFunc: saTokenFunc, licenseKey: licenseKey, base: base}
|
||||
}
|
||||
|
||||
// IsAuthRequired reports whether the response indicates the Hub demanded
|
||||
// authentication — a 401, or a 302/303 redirect to an SSO login page (the
|
||||
// hub clients are configured not to follow those; see StopOnSSORedirect).
|
||||
func IsAuthRequired(resp *http.Response) bool {
|
||||
return resp != nil && (resp.StatusCode == http.StatusUnauthorized ||
|
||||
resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// Get - When err is nil, resp always contains a non-nil resp.Body.
|
||||
// Caller should close resp.Body when done reading from it.
|
||||
func Get(url string, client *http.Client) (*http.Response, error) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func UnescapeUnicodeCharacters(raw string) string {
|
||||
str, err := strconv.Unquote(strings.Replace(strconv.Quote(raw), `\\u`, `\u`, -1))
|
||||
str, err := strconv.Unquote(strings.ReplaceAll(strconv.Quote(raw), `\\u`, `\u`))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Send()
|
||||
return raw
|
||||
|
||||
Reference in New Issue
Block a user