From 8393883400b4b74bdb98dd167129faf5d5f2deb3 Mon Sep 17 00:00:00 2001 From: Joel Takvorian Date: Tue, 3 Feb 2026 09:23:28 +0100 Subject: [PATCH 1/7] Remove dependency on oc/kubectl --- Dockerfile | 8 -------- Dockerfile.downstream | 5 ----- 2 files changed, 13 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7c2500c6..97b75e38 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,12 +25,6 @@ COPY scripts/ scripts/ COPY Makefile Makefile COPY .mk/ .mk/ -# Install oc to allow collector to run commands -RUN set -x; \ - OC_TAR_URL="https://mirror.openshift.com/pub/openshift-v4/$(uname -m)/clients/ocp/latest/openshift-client-linux.tar.gz" && \ - curl -L -q -o /tmp/oc.tar.gz "$OC_TAR_URL" && \ - tar -C /tmp -xvf /tmp/oc.tar.gz oc kubectl - # Embed commands in case users want to pull it from collector image RUN USER=netobserv VERSION=main make oc-commands @@ -42,8 +36,6 @@ FROM --platform=linux/$TARGETARCH registry.access.redhat.com/ubi9/ubi:9.7-177023 WORKDIR / COPY --from=builder /opt/app-root/build . -COPY --from=builder /tmp/oc /usr/bin/oc -COPY --from=builder /tmp/kubectl /usr/bin/kubectl COPY --from=builder --chown=65532:65532 /opt/app-root/output /output USER 65532:65532 diff --git a/Dockerfile.downstream b/Dockerfile.downstream index 77426b81..c59bd2cb 100644 --- a/Dockerfile.downstream +++ b/Dockerfile.downstream @@ -1,9 +1,6 @@ ARG BUILDVERSION ARG BUILDVERSION_Y -# Make kubectl & oc scripts available for copy -FROM registry.redhat.io/openshift4/ose-cli-rhel9:v4.18.0-202502040032.p0.ga50d4c0.assembly.stream.el9 as ose-cli - # Build the manager binary FROM brew.registry.redhat.io/rh-osbs/openshift-golang-builder:v1.25 as builder ARG BUILDVERSION @@ -46,8 +43,6 @@ COPY --from=builder /opt/app-root/build . COPY --from=builder --chown=65532:65532 /opt/app-root/output /output COPY LICENSE /licenses/ COPY README.downstream ./README -COPY --from=ose-cli /usr/bin/kubectl /usr/bin/kubectl -COPY --from=ose-cli /usr/bin/oc /usr/bin/oc USER 65532:65532 From e0ec7f3813f5e961cd5780c5a8fdb8c536e5c3ba Mon Sep 17 00:00:00 2001 From: Joel Takvorian Date: Tue, 3 Feb 2026 11:23:52 +0100 Subject: [PATCH 2/7] Prevent future path traversal The --filename argument of the go binary, which isn't used at the moment, is not sanitized for path traversal. This could be an unnoticed vulnerability if we chose to leverage it later on. This change uses go 1.24 "os.Root" API to prevent path traversal. --- cmd/flow_capture.go | 17 ++++------------- cmd/flow_db.go | 14 ++++++-------- cmd/packet_capture.go | 10 +--------- cmd/root.go | 14 ++++++++++++++ scripts/functions.sh | 4 ++-- 5 files changed, 27 insertions(+), 32 deletions(-) diff --git a/cmd/flow_capture.go b/cmd/flow_capture.go index 935fd658..3c06635f 100644 --- a/cmd/flow_capture.go +++ b/cmd/flow_capture.go @@ -2,7 +2,6 @@ package cmd import ( "encoding/json" - "os" "strings" "time" @@ -44,21 +43,13 @@ func startFlowCollector() { ":", "") // get rid of offensive colons } - var f *os.File - err := os.MkdirAll("./output/flow/", 0700) + // Create a text file to receive json chunks; the file will be fixed and renamed as json later, when pulled in shell. + f, err := createOutputFile("flow", filename+".txt") if err != nil { - log.Errorf("Create directory failed: %v", err.Error()) - log.Fatal(err) - } - log.Debug("Created flow folder") - - f, err = os.Create("./output/flow/" + filename + ".txt") - if err != nil { - log.Errorf("Create file %s failed: %v", filename, err.Error()) - log.Fatal(err) + log.Fatalf("Creating output file failed: %v", err) } defer f.Close() - log.Debug("Created flow logs txt file") + log.Debugf("Created flow logs txt file: %s", f.Name()) // Initialize sqlite DB db := initFlowDB(filename) diff --git a/cmd/flow_db.go b/cmd/flow_db.go index 06a1ebe6..b1584fa4 100644 --- a/cmd/flow_db.go +++ b/cmd/flow_db.go @@ -4,7 +4,6 @@ import ( "database/sql" "encoding/json" "fmt" - "os" "github.com/netobserv/flowlogs-pipeline/pkg/config" // need to import the sqlite3 driver @@ -13,16 +12,15 @@ import ( func initFlowDB(filename string) *sql.DB { // SQLite is a file based database. - flowsDB := "./output/flow/" + filename + ".db" - log.Println("Creating database...") - file, err := os.Create(flowsDB) // Create SQLite file + f, err := createOutputFile("flow", filename+".db") if err != nil { - log.Errorf("Failed to create flows db file: %v", err.Error()) - log.Fatal(err) + log.Fatalf("Creating output db file failed: %v", err) } - file.Close() - log.Println("flows.db created") + flowsDB := f.Name() + f.Close() + + log.Println("flows db created") // Open SQLite database db, err := sql.Open("sqlite3", flowsDB) if err != nil { diff --git a/cmd/packet_capture.go b/cmd/packet_capture.go index 9f074ef5..3b1cca1c 100644 --- a/cmd/packet_capture.go +++ b/cmd/packet_capture.go @@ -4,7 +4,6 @@ import ( "encoding/base64" "encoding/json" "fmt" - "os" "sort" "strings" "time" @@ -55,14 +54,7 @@ func startPacketCollector() { ":", "") // get rid of offensive colons } - err := os.MkdirAll("./output/pcap/", 0700) - if err != nil { - log.Error("Create directory failed", err) - return - } - log.Debug("Created pcap folder") - - f, err := os.Create("./output/pcap/" + filename + ".pcapng") + f, err := createOutputFile("pcap", filename+".pcapng") if err != nil { log.Fatal(err) } diff --git a/cmd/root.go b/cmd/root.go index 1463c92f..e33a655d 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -187,3 +187,17 @@ func onLimitReached() bool { return shouldExit } + +// Create output file, preventing path traversal +func createOutputFile(kind, filename string) (*os.File, error) { + base := "./output/" + kind + "/" + if err := os.MkdirAll(base, 0700); err != nil { + return nil, err + } + root, err := os.OpenRoot(base) + if err != nil { + return nil, err + } + defer root.Close() + return root.Create(filename) +} diff --git a/scripts/functions.sh b/scripts/functions.sh index 08e84f86..90954834 100755 --- a/scripts/functions.sh +++ b/scripts/functions.sh @@ -386,7 +386,7 @@ function follow() { } function copyOutput() { - echo "Copying collector output files..." + echo "Copying collector files to ${OUTPUT_PATH}..." if [[ ! -d ${OUTPUT_PATH} ]]; then mkdir -p ${OUTPUT_PATH} >/dev/null fi @@ -1108,7 +1108,7 @@ function check_args_and_apply() { logLevel=$value filter=${filter/$key=$logLevel/} else - echo "invalid value for --action" + echo "invalid value for --log-level" fi ;; *max-time) # Max time From d8f68846565463c1ec1fa7328c2d4ece3d947699 Mon Sep 17 00:00:00 2001 From: Joel Takvorian Date: Tue, 3 Feb 2026 13:45:19 +0100 Subject: [PATCH 3/7] Use go-client to delete daemonset --- Dockerfile | 1 + Dockerfile.downstream | 1 + cmd/flow_capture.go | 2 +- cmd/root.go | 7 ++++--- go.mod | 9 +++------ internal/pkg/kubernetes/kubernetes.go | 24 ++++++++++++++++++++++++ 6 files changed, 34 insertions(+), 10 deletions(-) create mode 100644 internal/pkg/kubernetes/kubernetes.go diff --git a/Dockerfile b/Dockerfile index 97b75e38..50522dd4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,6 +10,7 @@ ARG LDFLAGS WORKDIR /opt/app-root COPY cmd cmd +COPY internal internal COPY main.go main.go COPY go.mod go.mod COPY go.sum go.sum diff --git a/Dockerfile.downstream b/Dockerfile.downstream index c59bd2cb..45fad721 100644 --- a/Dockerfile.downstream +++ b/Dockerfile.downstream @@ -10,6 +10,7 @@ ARG AGENT_IMAGE=registry.redhat.io/network-observability/network-observability-e WORKDIR /opt/app-root COPY cmd cmd +COPY internal internal COPY main.go main.go COPY go.mod go.mod COPY go.sum go.sum diff --git a/cmd/flow_capture.go b/cmd/flow_capture.go index 3c06635f..0b190378 100644 --- a/cmd/flow_capture.go +++ b/cmd/flow_capture.go @@ -117,7 +117,7 @@ func startFlowCollector() { // terminate capture if max time reached now := currentTime() duration := now.Sub(startupTime) - if int(duration) > int(maxTime) { + if duration > maxTime { if exit := onLimitReached(); exit { log.Infof("Capture reached %s, exiting now...", maxTime) return diff --git a/cmd/root.go b/cmd/root.go index e33a655d..7ea25d00 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,7 @@ package cmd import ( + "context" "fmt" "os" "os/exec" @@ -10,6 +11,7 @@ import ( "syscall" "time" + "github.com/netobserv/network-observability-cli/internal/pkg/kubernetes" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) @@ -148,11 +150,10 @@ func onLimitReached() bool { app.Stop() } if isBackground { - out, err := exec.Command("/oc-netobserv", "stop").Output() + err := kubernetes.DeleteDaemonSet(context.Background()) if err != nil { - log.Fatal(err) + log.Error(err) } - fmt.Printf("%s", out) fmt.Print(`Thank you for using...`) printBanner() diff --git a/go.mod b/go.mod index fbf1dc18..50d6491d 100644 --- a/go.mod +++ b/go.mod @@ -19,12 +19,6 @@ require ( sigs.k8s.io/e2e-framework v0.6.0 ) -require ( - github.com/golang-jwt/jwt/v5 v5.3.0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/sync v0.19.0 // indirect -) - require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/beorn7/perks v1.0.1 // indirect @@ -41,6 +35,7 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect @@ -79,8 +74,10 @@ require ( go.opentelemetry.io/otel/trace v1.39.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.40.0 // indirect golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect diff --git a/internal/pkg/kubernetes/kubernetes.go b/internal/pkg/kubernetes/kubernetes.go new file mode 100644 index 00000000..9187b648 --- /dev/null +++ b/internal/pkg/kubernetes/kubernetes.go @@ -0,0 +1,24 @@ +package kubernetes + +import ( + "context" + "fmt" + + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" +) + +func DeleteDaemonSet(ctx context.Context) error { + config, err := rest.InClusterConfig() + if err != nil { + return fmt.Errorf("cannot get Kubernetes InClusterConfig: %w", err) + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return fmt.Errorf("cannot create Kubernetes client from InClusterConfig: %w", err) + } + + return clientset.AppsV1().DaemonSets("netobserv-cli").Delete(ctx, "netobserv-cli", v1.DeleteOptions{}) +} From 933d581dfeccfec00d5ba7ef4bfd84bef61623be Mon Sep 17 00:00:00 2001 From: Julien Pinsonneau Date: Tue, 3 Feb 2026 14:36:46 +0100 Subject: [PATCH 4/7] fix options array --- commands/netobserv | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/commands/netobserv b/commands/netobserv index 280285ec..f847734b 100755 --- a/commands/netobserv +++ b/commands/netobserv @@ -189,13 +189,20 @@ trap onExit EXIT setup if [[ "$command" == "flows" || "$command" == "packets" || "$command" == "metrics" ]]; then - # convert options to string - optionStr="${options//--/}" - optionStr="${optionStr// /|}" + # convert options array to pipe-separated string + optionStr="" + for opt in "${options[@]}"; do + opt="${opt#--}" # remove leading -- + if [ -n "$optionStr" ]; then + optionStr="$optionStr|$opt" + else + optionStr="$opt" + fi + done # prepare commands & args runCommand="sleep infinity" - execCommand="/network-observability-cli get-$command${optionStr:+" --options" "${optionStr}"} --loglevel $logLevel --maxtime $maxTime" + execCommand="/network-observability-cli get-$command${optionStr:+" --options \\\"${optionStr}\\\""} --loglevel $logLevel --maxtime $maxTime" if [[ "$command" == "flows" || "$command" == "packets" ]]; then execCommand="$execCommand --maxbytes $maxBytes" else From 0e3d2fa80f4811588abd3098faace2d8df9ee3fe Mon Sep 17 00:00:00 2001 From: Joel Takvorian Date: Tue, 3 Feb 2026 15:30:20 +0100 Subject: [PATCH 5/7] fix namespace not propagated to go runtime --- cmd/root.go | 18 ++++++++++-------- commands/netobserv | 2 +- internal/pkg/kubernetes/kubernetes.go | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/cmd/root.go b/cmd/root.go index 7ea25d00..e1381256 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -25,13 +25,14 @@ const ( ) var ( - log = logrus.New() - logLevel string - port int - filename string - options string - maxTime time.Duration - maxBytes int64 + log = logrus.New() + logLevel string + port int + filename string + namespace string + options string + maxTime time.Duration + maxBytes int64 currentTime = time.Now startupTime = currentTime() @@ -71,6 +72,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&options, "options", "", "", "Options(s)") rootCmd.PersistentFlags().DurationVarP(&maxTime, "maxtime", "", 5*time.Minute, "Maximum capture time") rootCmd.PersistentFlags().Int64VarP(&maxBytes, "maxbytes", "", 50000000, "Maximum capture bytes") + rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "netobserv-cli", "Namespace where agent pods are running") rootCmd.PersistentFlags().BoolVarP(&useMocks, "mock", "", false, "Use mock") c := make(chan os.Signal, 1) @@ -150,7 +152,7 @@ func onLimitReached() bool { app.Stop() } if isBackground { - err := kubernetes.DeleteDaemonSet(context.Background()) + err := kubernetes.DeleteDaemonSet(context.Background(), namespace) if err != nil { log.Error(err) } diff --git a/commands/netobserv b/commands/netobserv index f847734b..90cfedff 100755 --- a/commands/netobserv +++ b/commands/netobserv @@ -202,7 +202,7 @@ if [[ "$command" == "flows" || "$command" == "packets" || "$command" == "metrics # prepare commands & args runCommand="sleep infinity" - execCommand="/network-observability-cli get-$command${optionStr:+" --options \\\"${optionStr}\\\""} --loglevel $logLevel --maxtime $maxTime" + execCommand="/network-observability-cli get-$command${optionStr:+" --options \\\"${optionStr}\\\""} --loglevel $logLevel --maxtime $maxTime --namespace $namespace" if [[ "$command" == "flows" || "$command" == "packets" ]]; then execCommand="$execCommand --maxbytes $maxBytes" else diff --git a/internal/pkg/kubernetes/kubernetes.go b/internal/pkg/kubernetes/kubernetes.go index 9187b648..e5fafc72 100644 --- a/internal/pkg/kubernetes/kubernetes.go +++ b/internal/pkg/kubernetes/kubernetes.go @@ -9,7 +9,7 @@ import ( "k8s.io/client-go/rest" ) -func DeleteDaemonSet(ctx context.Context) error { +func DeleteDaemonSet(ctx context.Context, namespace string) error { config, err := rest.InClusterConfig() if err != nil { return fmt.Errorf("cannot get Kubernetes InClusterConfig: %w", err) @@ -20,5 +20,5 @@ func DeleteDaemonSet(ctx context.Context) error { return fmt.Errorf("cannot create Kubernetes client from InClusterConfig: %w", err) } - return clientset.AppsV1().DaemonSets("netobserv-cli").Delete(ctx, "netobserv-cli", v1.DeleteOptions{}) + return clientset.AppsV1().DaemonSets(namespace).Delete(ctx, "netobserv-cli", v1.DeleteOptions{}) } From 60ad5704eab3ca2f4dff3c10aeca5c9345f7097e Mon Sep 17 00:00:00 2001 From: Julien Pinsonneau Date: Tue, 3 Feb 2026 16:07:45 +0100 Subject: [PATCH 6/7] separate foreground and background cases --- commands/netobserv | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/commands/netobserv b/commands/netobserv index 90cfedff..976d1a00 100755 --- a/commands/netobserv +++ b/commands/netobserv @@ -202,18 +202,30 @@ if [[ "$command" == "flows" || "$command" == "packets" || "$command" == "metrics # prepare commands & args runCommand="sleep infinity" - execCommand="/network-observability-cli get-$command${optionStr:+" --options \\\"${optionStr}\\\""} --loglevel $logLevel --maxtime $maxTime --namespace $namespace" - if [[ "$command" == "flows" || "$command" == "packets" ]]; then - execCommand="$execCommand --maxbytes $maxBytes" - else - copy="false" - fi - if [[ "$runBackground" == "true" || "$outputYAML" == "true" ]]; then + # For background mode: wrap in bash -c with proper escaping for pod command + execCommand="/network-observability-cli get-$command${optionStr:+" --options \\\"${optionStr}\\\""} --loglevel $logLevel --maxtime $maxTime --namespace $namespace" + if [[ "$command" == "flows" || "$command" == "packets" ]]; then + execCommand="$execCommand --maxbytes $maxBytes" + fi runCommand="bash -c \"$execCommand && $runCommand\"" execCommand="" + else + # For foreground mode: will be used in kubectl exec + # Build as array to avoid quoting issues + execCommandBase="/network-observability-cli get-$command" + execCommandArgs="--loglevel $logLevel --maxtime $maxTime --namespace $namespace" + if [[ "$command" == "flows" || "$command" == "packets" ]]; then + execCommandArgs="$execCommandArgs --maxbytes $maxBytes" + fi + if [ -n "$optionStr" ]; then + # Store options for later use + execOptions="$optionStr" + else + execOptions="" + fi fi - + # override extra configs overrides="'{\"spec\":{\"serviceAccount\": \"netobserv-cli\"}}'" @@ -258,13 +270,15 @@ if [[ "$command" == "flows" || "$command" == "packets" || "$command" == "metrics captureStarted=true - if [ -n "${execCommand}" ]; then + if [[ "$runBackground" != "true" && "$outputYAML" != "true" ]]; then echo "Executing collector command... " - cmd="${K8S_CLI_BIN} exec -i --tty \ - -n $namespace \ - collector \ - -- $execCommand" - eval "$cmd" + if [ -n "$execOptions" ]; then + ${K8S_CLI_BIN} exec -i --tty -n "$namespace" collector -- \ + $execCommandBase --options "$execOptions" $execCommandArgs + else + ${K8S_CLI_BIN} exec -i --tty -n "$namespace" collector -- \ + $execCommandBase $execCommandArgs + fi elif [[ "$command" == "flows" || "$command" == "packets" ]]; then echo "Background capture started. Use:" echo " - '${K8S_CLI_BIN} netobserv follow' to see the capture progress" From e195a3b7c5dd3be278eab5bc26b9b5c324f98de1 Mon Sep 17 00:00:00 2001 From: Joel Takvorian Date: Thu, 5 Feb 2026 14:21:48 +0100 Subject: [PATCH 7/7] Add "eval" on command exec For some reason (??) it worked for get-flows but not for get-metrics ... "eval" makes it work for metrics too --- commands/netobserv | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/commands/netobserv b/commands/netobserv index 976d1a00..4a19c8d4 100755 --- a/commands/netobserv +++ b/commands/netobserv @@ -273,11 +273,9 @@ if [[ "$command" == "flows" || "$command" == "packets" || "$command" == "metrics if [[ "$runBackground" != "true" && "$outputYAML" != "true" ]]; then echo "Executing collector command... " if [ -n "$execOptions" ]; then - ${K8S_CLI_BIN} exec -i --tty -n "$namespace" collector -- \ - $execCommandBase --options "$execOptions" $execCommandArgs + eval "${K8S_CLI_BIN} exec -i --tty -n $namespace collector -- $execCommandBase --options \"$execOptions\" $execCommandArgs" else - ${K8S_CLI_BIN} exec -i --tty -n "$namespace" collector -- \ - $execCommandBase $execCommandArgs + eval "${K8S_CLI_BIN} exec -i --tty -n $namespace collector -- $execCommandBase $execCommandArgs" fi elif [[ "$command" == "flows" || "$command" == "packets" ]]; then echo "Background capture started. Use:"