Compare commits

..

2 Commits

Author SHA1 Message Date
Alon Girmonsky
c1dbbd2386 Replace HTML embeds with image placeholders in security-audit README 2026-05-19 22:37:24 -07:00
Alon Girmonsky
35dea1bc9a Add security-audit skill README with demo session and sample report 2026-05-19 22:13:09 -07:00
55 changed files with 354 additions and 923 deletions

View File

@@ -1,6 +1,5 @@
{
"name": "kubeshark",
"owner": { "name": "kubeshark" },
"description": "Kubeshark network observability skills for Kubernetes",
"plugins": [
{

View File

@@ -1,101 +0,0 @@
# 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}}"

View File

@@ -27,7 +27,7 @@ jobs:
go-version-file: 'go.mod'
- name: Go lint
uses: golangci/golangci-lint-action@v7
uses: golangci/golangci-lint-action@v3
with:
version: v2.8.0
version: latest
args: --timeout=10m

24
.github/workflows/release-tag.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
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"

View File

@@ -1,36 +0,0 @@
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

View File

@@ -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, create the release tag:"
@echo " make release-tag VERSION=x.y.z"
@echo " After both PRs merge: tag is created automatically,"
@echo " or run: make release-tag VERSION=x.y.z"
# Internal: validate VERSION before any release-* target runs.
_release-check-version:
@@ -362,18 +362,14 @@ 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: _release-check-version ## Step 2: Tag master after release PR is merged. Idempotent; re-run to retrigger the release build.
release-tag: ## Step 2 (fallback): Tag master after release PR is merged.
@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
@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)"
@git tag -d v$(VERSION) 2>/dev/null; git tag v$(VERSION) && git push origin --tags
@echo ""
@echo "Tagged v$(VERSION) on master. GitHub Actions will build the release."

View File

@@ -4,13 +4,12 @@ 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{

View File

@@ -8,14 +8,13 @@ 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) {

View File

@@ -4,13 +4,12 @@ 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{

View File

@@ -1,7 +1,6 @@
package cmd
import (
"context"
"fmt"
"net/http"
"net/url"
@@ -12,26 +11,19 @@ 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 (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.`,
Short: "Stream the scripting console logs into shell",
RunE: func(cmd *cobra.Command, args []string) error {
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."))
runConsole()
return nil
},
}
@@ -49,22 +41,9 @@ 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 {
@@ -87,11 +66,7 @@ func runConsoleWithoutProxy() {
}
headers := http.Header{}
headers.Set(utils.X_KUBESHARK_CAPTURE_HEADER_KEY, utils.X_KUBESHARK_CAPTURE_HEADER_IGNORE_VALUE)
if saToken != "" {
headers.Set(utils.CLI_AUTH_HEADER, saToken)
} else {
headers.Set(utils.LICENSE_KEY_HEADER, config.Config.License)
}
headers.Set("License-Key", config.Config.License)
c, _, err := websocket.DefaultDialer.Dial(u.String(), headers)
if err != nil {
@@ -146,10 +121,7 @@ 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

View File

@@ -3,9 +3,8 @@ package cmd
import (
"fmt"
"github.com/spf13/cobra"
"github.com/kubeshark/kubeshark/config"
"github.com/spf13/cobra"
)
var licenseCmd = &cobra.Command{

View File

@@ -5,14 +5,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/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{

View File

@@ -1,9 +1,8 @@
package cmd
import (
"github.com/spf13/cobra"
"github.com/kubeshark/kubeshark/config"
"github.com/spf13/cobra"
)
var mcpURL string
@@ -11,7 +10,6 @@ var mcpKubeconfig string
var mcpListTools bool
var mcpConfig bool
var mcpAllowDestructive bool
var mcpToken string
var mcpCmd = &cobra.Command{
Use: "mcp",
@@ -120,7 +118,6 @@ 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")

View File

@@ -15,14 +15,12 @@ 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/kubeshark/kubeshark/utils"
"github.com/rs/zerolog"
)
// MCP Protocol types (JSON-RPC 2.0)
@@ -160,52 +158,21 @@ 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: utils.NewHubHTTPClientWithTokenSource(30*time.Second, tokenSource, config.Config.License),
tokenSource: tokenSource,
httpClient: &http.Client{Timeout: 30 * time.Second},
stdin: os.Stdin,
stdout: os.Stdout,
setFlags: setFlags,
directURL: directURL,
urlMode: urlMode,
urlMode: directURL != "",
allowDestructive: allowDestructive,
}
@@ -228,7 +195,7 @@ func (s *mcpServer) validateDirectURL() error {
s.directURL = urlStr
// Use a short timeout for validation
client := utils.NewHubHTTPClientWithTokenSource(10*time.Second, s.tokenSource, config.Config.License)
client := &http.Client{Timeout: 10 * time.Second}
// Try to reach the MCP API base endpoint which returns tool definitions
testURL := fmt.Sprintf("%s/api/mcp", urlStr)
@@ -238,10 +205,6 @@ 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 {
@@ -319,20 +282,6 @@ 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()
@@ -355,10 +304,6 @@ 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
}
@@ -815,9 +760,6 @@ 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
}
@@ -878,11 +820,10 @@ 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{
CheckRedirect: utils.StopOnSSORedirect,
Transport: utils.HubAuthTransportWithTokenSource(s.tokenSource, config.Config.License, &http.Transport{
Transport: &http.Transport{
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
}),
},
}
resp, err := downloadClient.Get(fullURL)
@@ -891,11 +832,6 @@ 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
}
@@ -1123,10 +1059,6 @@ 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)
@@ -1139,7 +1071,7 @@ func listMCPTools(directURL string) {
fmt.Println()
hubURL := strings.TrimSuffix(directURL, "/") + "/api/mcp"
fetchAndDisplayTools(hubURL, 30*time.Second, tokenSource)
fetchAndDisplayTools(hubURL, 30*time.Second)
return
}
@@ -1163,7 +1095,7 @@ func listMCPTools(directURL string) {
}
fmt.Printf("Connected to: %s\n\n", hubURL)
fetchAndDisplayTools(hubURL, 30*time.Second, tokenSource)
fetchAndDisplayTools(hubURL, 30*time.Second)
}
// establishProxyConnection sets up proxy to Kubeshark and returns the hub URL
@@ -1212,8 +1144,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, tokenSource func() string) {
client := utils.NewHubHTTPClientWithTokenSource(timeout, tokenSource, config.Config.License)
func fetchAndDisplayTools(hubURL string, timeout time.Duration) {
client := &http.Client{Timeout: timeout}
// Fetch tools list from /api/mcp endpoint
resp, err := client.Get(strings.TrimSuffix(hubURL, "/mcp") + "/mcp")
@@ -1223,11 +1155,6 @@ func fetchAndDisplayTools(hubURL string, timeout time.Duration, tokenSource func
}
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 {

View File

@@ -658,10 +658,9 @@ 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) {
switch r.URL.Path {
case "/":
if r.URL.Path == "/" {
_, _ = w.Write([]byte(`{"name":"hub","tools":[{"name":"list_workloads","description":"","inputSchema":{}}]}`))
case "/tools/call":
} else if r.URL.Path == "/tools/call" {
_, _ = w.Write([]byte(`{"data":"ok"}`))
}
}))

View File

@@ -8,14 +8,13 @@ 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
@@ -46,12 +45,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
@@ -60,7 +59,7 @@ var pcapDumpCmd = &cobra.Command{
if timeIntervalStr != "" {
duration, err := time.ParseDuration(timeIntervalStr)
if err != nil {
return fmt.Errorf("invalid --time value %q: %w", timeIntervalStr, err)
return fmt.Errorf("Invalid format %w", err)
}
tempCutoffTime := time.Now().Add(-duration)
cutoffTime = &tempCutoffTime
@@ -71,17 +70,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())
}

View File

@@ -2,10 +2,9 @@ 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{

View File

@@ -5,13 +5,12 @@ import (
"fmt"
"github.com/go-cmd/cmd"
"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"
"github.com/rivo/tview"
"github.com/rs/zerolog/log"
v1 "k8s.io/api/core/v1"
)
func runPprof() {

View File

@@ -2,10 +2,9 @@ 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{

View File

@@ -5,14 +5,13 @@ 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) {

View File

@@ -4,11 +4,10 @@ import (
"fmt"
"github.com/creasty/defaults"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/misc"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{

View File

@@ -12,16 +12,15 @@ 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{

View File

@@ -4,12 +4,11 @@ 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{

View File

@@ -16,12 +16,11 @@ 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
@@ -446,6 +445,7 @@ 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)
}

View File

@@ -5,11 +5,10 @@ import (
"strconv"
"time"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
"github.com/kubeshark/kubeshark/config"
"github.com/kubeshark/kubeshark/misc"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
var versionCmd = &cobra.Command{

View File

@@ -13,14 +13,13 @@ 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 (

View File

@@ -4,10 +4,9 @@ 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 (
@@ -103,10 +102,23 @@ func CreateDefaultConfig() ConfigStruct {
},
},
Auth: configStructs.AuthConfig{
RolesClaim: "groups",
DefaultRole: "kubeshark-viewer",
GroupMapping: map[string]string{},
Roles: map[string]configStructs.RoleConfig{},
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,
},
},
},
EnabledDissectors: []string{
"amqp",
@@ -133,15 +145,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,

View File

@@ -7,9 +7,8 @@ import (
"path/filepath"
"strings"
"github.com/rs/zerolog/log"
"github.com/kubeshark/kubeshark/misc"
"github.com/rs/zerolog/log"
)
type ScriptingConfig struct {

View File

@@ -155,6 +155,23 @@ 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"`
@@ -173,70 +190,12 @@ 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"`
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 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 IngressConfig struct {
@@ -333,15 +292,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 {

View File

@@ -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
View File

@@ -1,6 +1,8 @@
module github.com/kubeshark/kubeshark
go 1.25.0
go 1.24.0
toolchain go1.24.5
require (
github.com/creasty/defaults v1.5.2
@@ -36,11 +38,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.34 // indirect
github.com/containerd/containerd v1.7.27 // 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.5.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.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
@@ -51,7 +53,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.3 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
@@ -87,7 +89,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.1 // indirect
github.com/moby/spdystream v0.5.0 // 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
@@ -98,30 +100,28 @@ 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.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/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/xerrors v0.0.0-20220907171357-04be3eba64a2 // 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
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
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.2 // indirect
oras.land/oras-go/v2 v2.6.0 // 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
View File

@@ -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.34 h1:Q35B4FUECxcoaMz9QrOlqp+0s72w8/0NWawVMhVdf5g=
github.com/containerd/containerd v1.7.34/go.mod h1:ozI//0TomTCLPhQREnx0IXDIQMg+Fk7yTtg9fNvU8EQ=
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/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.5.1 h1:eYgfMq5yryL4fbWfkLpFFy2ukSELzaJOTaUTuh+oF48=
github.com/cyphar/filepath-securejoin v0.5.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
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/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.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
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/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.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y=
github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/moby/term v0.5.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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
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/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
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/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.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
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/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.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/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/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.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/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/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.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/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/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.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
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/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.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
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/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.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
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/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
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/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
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.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
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.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
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/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.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/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/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.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
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-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=
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=
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.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo=
oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4=
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=
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=

View File

@@ -218,8 +218,6 @@ 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`) | `` |

View File

@@ -146,10 +146,6 @@ 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" }}
@@ -239,11 +235,6 @@ 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
@@ -284,10 +275,6 @@ 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" }}
@@ -363,11 +350,6 @@ 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 }}
@@ -448,12 +430,3 @@ 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 }}

View File

@@ -18,18 +18,20 @@ data:
INGRESS_ENABLED: '{{ .Values.tap.ingress.enabled }}'
INGRESS_HOST: '{{ .Values.tap.ingress.host }}'
PROXY_FRONT_PORT: '{{ .Values.tap.proxy.front.port }}'
AUTH_ENABLED: '{{ include "kubeshark.authEnabled" . }}'
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_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_CLI_SERVICE_ACCOUNTS: '{{ if (((.Values.tap).auth).cli).enabled }}{{ .Release.Namespace }}:kubeshark-cli{{ end }}'
AUTH_ROLES: '{{ .Values.tap.auth.roles | toJson }}'
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 }}'

View File

@@ -1,44 +0,0 @@
{{- 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 }}

View File

@@ -110,17 +110,3 @@ 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 -}}

View File

@@ -153,13 +153,22 @@ tap:
auth:
enabled: false
type: saml
rolesClaim: groups
defaultRole: kubeshark-viewer
groupMapping: {}
roles: {}
cli:
enabled: false
subjects: []
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: ""
saml:
idpMetadataUrl: ""
x509crt: ""

View File

@@ -230,9 +230,7 @@ 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)
@@ -264,9 +262,7 @@ 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)
@@ -300,9 +296,7 @@ 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)
@@ -347,10 +341,8 @@ 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)

View File

@@ -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
}

View File

@@ -6,12 +6,11 @@ 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 (

View File

@@ -8,6 +8,8 @@ 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"
@@ -20,9 +22,6 @@ 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"
@@ -80,7 +79,7 @@ func (h *Helm) Install() (rel *release.Release, err error) {
}
var cp string
cp, err = client.LocateChart(chartURL, settings)
cp, err = client.ChartPathOptions.LocateChart(chartURL, settings)
if err != nil {
return
}
@@ -88,7 +87,7 @@ func (h *Helm) Install() (rel *release.Release, err error) {
m := &downloader.Manager{
Out: os.Stdout,
ChartPath: cp,
Keyring: client.Keyring,
Keyring: client.ChartPathOptions.Keyring,
SkipUpdate: false,
Getters: getter.All(settings),
RepositoryConfig: settings.RepositoryConfig,

View File

@@ -1,112 +0,0 @@
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
}

View File

@@ -16,7 +16,7 @@ type PodWatchHelper struct {
func NewPodWatchHelper(kubernetesProvider *Provider, NameRegexFilter *regexp.Regexp) *PodWatchHelper {
return &PodWatchHelper{
kubernetesProvider: kubernetesProvider,
NameRegexFilter: NameRegexFilter,
NameRegexFilter: NameRegexFilter,
}
}

View File

@@ -11,6 +11,10 @@ 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"
@@ -21,11 +25,6 @@ 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 {

View File

@@ -11,13 +11,12 @@ 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 = "/"
@@ -156,7 +155,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)

View File

@@ -7,10 +7,9 @@ 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 {
@@ -48,6 +47,7 @@ func FilteredWatch(ctx context.Context, watcherCreator WatchCreator, targetNames
case <-ctx.Done():
return
default:
break
}
if err != nil {

View File

@@ -5,10 +5,9 @@ import (
"strconv"
"time"
"github.com/kubeshark/kubeshark/cmd"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/kubeshark/kubeshark/cmd"
)
func main() {

View File

@@ -88,30 +88,6 @@ 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
@@ -203,7 +179,6 @@ 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 |

View File

@@ -7,11 +7,10 @@ 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 {
@@ -22,7 +21,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)

View File

@@ -8,10 +8,9 @@ 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"
)

View File

@@ -0,0 +1,26 @@
# Security Audit Skill
A Kubeshark MCP skill that teaches AI agents to perform systematic Kubernetes
network security audits using the MITRE ATT&CK framework. It examines DNS
queries, HTTP requests, L4 flows, and protocol-level payloads to detect
compromised workloads, C2 communication, data exfiltration, cryptomining,
lateral movement, and credential theft.
See [SKILL.md](SKILL.md) for the full methodology.
## Demo
The demo below shows a real security audit session against a compromised
`k8s-mule` namespace containing 21 workloads, 6 of which were actively
compromised with C2, cryptomining, secret theft, S3 exfiltration, port
scanning, and Redis reconnaissance.
### Claude Code Session
<!-- TODO: replace with animated GIF once recorded -->
![Security Audit Demo](https://raw.githubusercontent.com/kubeshark/assets/master/png/security-audit-demo.gif)
### Sample Audit Report
<!-- TODO: replace with animated GIF once recorded -->
![Security Audit Report](https://raw.githubusercontent.com/kubeshark/assets/master/png/security-audit-report.gif)

View File

@@ -113,15 +113,25 @@ 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
**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.
In parallel, 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.
@@ -145,12 +155,6 @@ 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:
@@ -160,7 +164,8 @@ 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 `get_data_boundaries` to calculate how many snapshots are needed:
Use the data boundaries from Step 1 (`get_data_boundaries`) to calculate
how many snapshots are needed:
```
total_range_ms = newest_timestamp - oldest_timestamp
@@ -242,6 +247,7 @@ 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 |
@@ -255,12 +261,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
@@ -390,14 +396,32 @@ 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**: L7 dissection (after indexing).
**Data source**: Immediate (no dissection needed). Use L4 flows first,
then enrich with L7 data from dissection when available.
**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.
### 3a: L4 External Flows
### 3a: HTTP External Requests
**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
**Tool**: `list_api_calls` with KFL: `http && !dst.pod.namespace.startsWith("kube")`
@@ -416,11 +440,8 @@ 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**: 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.
**Data source**: L4 flows (immediate) for port scanning detection. L7
dissection (after indexing) for cross-namespace HTTP and API server analysis.
### 4a: Cross-Namespace Traffic
@@ -447,7 +468,19 @@ 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: Service Fingerprinting
### 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
**Tool**: `list_api_calls` with KFL: `http && (path == "/.env" || path == "/actuator/info" || path == "/server-info" || path == "/version")`
@@ -455,7 +488,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.
### 4d: Service Account Permission Audit via Traffic
### 4e: 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.
@@ -482,7 +515,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.
### 4e: Cross-Namespace Threat Correlation
### 4f: 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
@@ -570,6 +603,8 @@ 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:

View File

@@ -2,151 +2,17 @@ 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) {

View File

@@ -8,7 +8,7 @@ import (
)
func UnescapeUnicodeCharacters(raw string) string {
str, err := strconv.Unquote(strings.ReplaceAll(strconv.Quote(raw), `\\u`, `\u`))
str, err := strconv.Unquote(strings.Replace(strconv.Quote(raw), `\\u`, `\u`, -1))
if err != nil {
log.Error().Err(err).Send()
return raw