Skip to content

Commit cdccb82

Browse files
openshift-cherrypick-robotjotakjpinsonneau
authored
[release-1.11] NETOBSERV-2598: NETOBSERV-2517: Remove dependency on oc/kubectl (#461)
* Remove dependency on oc/kubectl * 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. * Use go-client to delete daemonset * fix options array * fix namespace not propagated to go runtime * separate foreground and background cases * 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 --------- Co-authored-by: Joel Takvorian <jtakvori@redhat.com> Co-authored-by: Julien Pinsonneau <jpinsonn@redhat.com>
1 parent 06a5b1f commit cdccb82

File tree

10 files changed

+106
-79
lines changed

10 files changed

+106
-79
lines changed

Dockerfile

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ARG LDFLAGS
1010
WORKDIR /opt/app-root
1111

1212
COPY cmd cmd
13+
COPY internal internal
1314
COPY main.go main.go
1415
COPY go.mod go.mod
1516
COPY go.sum go.sum
@@ -25,12 +26,6 @@ COPY scripts/ scripts/
2526
COPY Makefile Makefile
2627
COPY .mk/ .mk/
2728

28-
# Install oc to allow collector to run commands
29-
RUN set -x; \
30-
OC_TAR_URL="https://mirror.openshift.com/pub/openshift-v4/$(uname -m)/clients/ocp/latest/openshift-client-linux.tar.gz" && \
31-
curl -L -q -o /tmp/oc.tar.gz "$OC_TAR_URL" && \
32-
tar -C /tmp -xvf /tmp/oc.tar.gz oc kubectl
33-
3429
# Embed commands in case users want to pull it from collector image
3530
RUN USER=netobserv VERSION=main make oc-commands
3631

@@ -42,8 +37,6 @@ FROM --platform=linux/$TARGETARCH registry.access.redhat.com/ubi9/ubi:9.7-177023
4237
WORKDIR /
4338

4439
COPY --from=builder /opt/app-root/build .
45-
COPY --from=builder /tmp/oc /usr/bin/oc
46-
COPY --from=builder /tmp/kubectl /usr/bin/kubectl
4740
COPY --from=builder --chown=65532:65532 /opt/app-root/output /output
4841
USER 65532:65532
4942

Dockerfile.downstream

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
ARG BUILDVERSION
22
ARG BUILDVERSION_Y
33

4-
# Make kubectl & oc scripts available for copy
5-
FROM registry.redhat.io/openshift4/ose-cli-rhel9:v4.18.0-202502040032.p0.ga50d4c0.assembly.stream.el9 as ose-cli
6-
74
# Build the manager binary
85
FROM brew.registry.redhat.io/rh-osbs/openshift-golang-builder:v1.25 as builder
96
ARG BUILDVERSION
@@ -13,6 +10,7 @@ ARG AGENT_IMAGE=registry.redhat.io/network-observability/network-observability-e
1310
WORKDIR /opt/app-root
1411

1512
COPY cmd cmd
13+
COPY internal internal
1614
COPY main.go main.go
1715
COPY go.mod go.mod
1816
COPY go.sum go.sum
@@ -46,8 +44,6 @@ COPY --from=builder /opt/app-root/build .
4644
COPY --from=builder --chown=65532:65532 /opt/app-root/output /output
4745
COPY LICENSE /licenses/
4846
COPY README.downstream ./README
49-
COPY --from=ose-cli /usr/bin/kubectl /usr/bin/kubectl
50-
COPY --from=ose-cli /usr/bin/oc /usr/bin/oc
5147

5248
USER 65532:65532
5349

cmd/flow_capture.go

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cmd
22

33
import (
44
"encoding/json"
5-
"os"
65
"strings"
76
"time"
87

@@ -44,21 +43,13 @@ func startFlowCollector() {
4443
":", "") // get rid of offensive colons
4544
}
4645

47-
var f *os.File
48-
err := os.MkdirAll("./output/flow/", 0700)
46+
// Create a text file to receive json chunks; the file will be fixed and renamed as json later, when pulled in shell.
47+
f, err := createOutputFile("flow", filename+".txt")
4948
if err != nil {
50-
log.Errorf("Create directory failed: %v", err.Error())
51-
log.Fatal(err)
52-
}
53-
log.Debug("Created flow folder")
54-
55-
f, err = os.Create("./output/flow/" + filename + ".txt")
56-
if err != nil {
57-
log.Errorf("Create file %s failed: %v", filename, err.Error())
58-
log.Fatal(err)
49+
log.Fatalf("Creating output file failed: %v", err)
5950
}
6051
defer f.Close()
61-
log.Debug("Created flow logs txt file")
52+
log.Debugf("Created flow logs txt file: %s", f.Name())
6253

6354
// Initialize sqlite DB
6455
db := initFlowDB(filename)
@@ -126,7 +117,7 @@ func startFlowCollector() {
126117
// terminate capture if max time reached
127118
now := currentTime()
128119
duration := now.Sub(startupTime)
129-
if int(duration) > int(maxTime) {
120+
if duration > maxTime {
130121
if exit := onLimitReached(); exit {
131122
log.Infof("Capture reached %s, exiting now...", maxTime)
132123
return

cmd/flow_db.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"database/sql"
55
"encoding/json"
66
"fmt"
7-
"os"
87

98
"github.com/netobserv/flowlogs-pipeline/pkg/config"
109
// need to import the sqlite3 driver
@@ -13,16 +12,15 @@ import (
1312

1413
func initFlowDB(filename string) *sql.DB {
1514
// SQLite is a file based database.
16-
flowsDB := "./output/flow/" + filename + ".db"
17-
1815
log.Println("Creating database...")
19-
file, err := os.Create(flowsDB) // Create SQLite file
16+
f, err := createOutputFile("flow", filename+".db")
2017
if err != nil {
21-
log.Errorf("Failed to create flows db file: %v", err.Error())
22-
log.Fatal(err)
18+
log.Fatalf("Creating output db file failed: %v", err)
2319
}
24-
file.Close()
25-
log.Println("flows.db created")
20+
flowsDB := f.Name()
21+
f.Close()
22+
23+
log.Println("flows db created")
2624
// Open SQLite database
2725
db, err := sql.Open("sqlite3", flowsDB)
2826
if err != nil {

cmd/packet_capture.go

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"encoding/base64"
55
"encoding/json"
66
"fmt"
7-
"os"
87
"sort"
98
"strings"
109
"time"
@@ -55,14 +54,7 @@ func startPacketCollector() {
5554
":", "") // get rid of offensive colons
5655
}
5756

58-
err := os.MkdirAll("./output/pcap/", 0700)
59-
if err != nil {
60-
log.Error("Create directory failed", err)
61-
return
62-
}
63-
log.Debug("Created pcap folder")
64-
65-
f, err := os.Create("./output/pcap/" + filename + ".pcapng")
57+
f, err := createOutputFile("pcap", filename+".pcapng")
6658
if err != nil {
6759
log.Fatal(err)
6860
}

cmd/root.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package cmd
22

33
import (
4+
"context"
45
"fmt"
56
"os"
67
"os/exec"
@@ -10,6 +11,7 @@ import (
1011
"syscall"
1112
"time"
1213

14+
"github.com/netobserv/network-observability-cli/internal/pkg/kubernetes"
1315
"github.com/sirupsen/logrus"
1416
"github.com/spf13/cobra"
1517
)
@@ -23,13 +25,14 @@ const (
2325
)
2426

2527
var (
26-
log = logrus.New()
27-
logLevel string
28-
port int
29-
filename string
30-
options string
31-
maxTime time.Duration
32-
maxBytes int64
28+
log = logrus.New()
29+
logLevel string
30+
port int
31+
filename string
32+
namespace string
33+
options string
34+
maxTime time.Duration
35+
maxBytes int64
3336

3437
currentTime = time.Now
3538
startupTime = currentTime()
@@ -69,6 +72,7 @@ func init() {
6972
rootCmd.PersistentFlags().StringVarP(&options, "options", "", "", "Options(s)")
7073
rootCmd.PersistentFlags().DurationVarP(&maxTime, "maxtime", "", 5*time.Minute, "Maximum capture time")
7174
rootCmd.PersistentFlags().Int64VarP(&maxBytes, "maxbytes", "", 50000000, "Maximum capture bytes")
75+
rootCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "netobserv-cli", "Namespace where agent pods are running")
7276
rootCmd.PersistentFlags().BoolVarP(&useMocks, "mock", "", false, "Use mock")
7377

7478
c := make(chan os.Signal, 1)
@@ -148,11 +152,10 @@ func onLimitReached() bool {
148152
app.Stop()
149153
}
150154
if isBackground {
151-
out, err := exec.Command("/oc-netobserv", "stop").Output()
155+
err := kubernetes.DeleteDaemonSet(context.Background(), namespace)
152156
if err != nil {
153-
log.Fatal(err)
157+
log.Error(err)
154158
}
155-
fmt.Printf("%s", out)
156159
fmt.Print(`Thank you for using...`)
157160
printBanner()
158161

@@ -187,3 +190,17 @@ func onLimitReached() bool {
187190

188191
return shouldExit
189192
}
193+
194+
// Create output file, preventing path traversal
195+
func createOutputFile(kind, filename string) (*os.File, error) {
196+
base := "./output/" + kind + "/"
197+
if err := os.MkdirAll(base, 0700); err != nil {
198+
return nil, err
199+
}
200+
root, err := os.OpenRoot(base)
201+
if err != nil {
202+
return nil, err
203+
}
204+
defer root.Close()
205+
return root.Create(filename)
206+
}

commands/netobserv

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -189,24 +189,43 @@ trap onExit EXIT
189189
setup
190190

191191
if [[ "$command" == "flows" || "$command" == "packets" || "$command" == "metrics" ]]; then
192-
# convert options to string
193-
optionStr="${options//--/}"
194-
optionStr="${optionStr// /|}"
192+
# convert options array to pipe-separated string
193+
optionStr=""
194+
for opt in "${options[@]}"; do
195+
opt="${opt#--}" # remove leading --
196+
if [ -n "$optionStr" ]; then
197+
optionStr="$optionStr|$opt"
198+
else
199+
optionStr="$opt"
200+
fi
201+
done
195202

196203
# prepare commands & args
197204
runCommand="sleep infinity"
198-
execCommand="/network-observability-cli get-$command${optionStr:+" --options" "${optionStr}"} --loglevel $logLevel --maxtime $maxTime"
199-
if [[ "$command" == "flows" || "$command" == "packets" ]]; then
200-
execCommand="$execCommand --maxbytes $maxBytes"
201-
else
202-
copy="false"
203-
fi
204-
205205
if [[ "$runBackground" == "true" || "$outputYAML" == "true" ]]; then
206+
# For background mode: wrap in bash -c with proper escaping for pod command
207+
execCommand="/network-observability-cli get-$command${optionStr:+" --options \\\"${optionStr}\\\""} --loglevel $logLevel --maxtime $maxTime --namespace $namespace"
208+
if [[ "$command" == "flows" || "$command" == "packets" ]]; then
209+
execCommand="$execCommand --maxbytes $maxBytes"
210+
fi
206211
runCommand="bash -c \"$execCommand && $runCommand\""
207212
execCommand=""
213+
else
214+
# For foreground mode: will be used in kubectl exec
215+
# Build as array to avoid quoting issues
216+
execCommandBase="/network-observability-cli get-$command"
217+
execCommandArgs="--loglevel $logLevel --maxtime $maxTime --namespace $namespace"
218+
if [[ "$command" == "flows" || "$command" == "packets" ]]; then
219+
execCommandArgs="$execCommandArgs --maxbytes $maxBytes"
220+
fi
221+
if [ -n "$optionStr" ]; then
222+
# Store options for later use
223+
execOptions="$optionStr"
224+
else
225+
execOptions=""
226+
fi
208227
fi
209-
228+
210229
# override extra configs
211230
overrides="'{\"spec\":{\"serviceAccount\": \"netobserv-cli\"}}'"
212231

@@ -251,13 +270,13 @@ if [[ "$command" == "flows" || "$command" == "packets" || "$command" == "metrics
251270

252271
captureStarted=true
253272

254-
if [ -n "${execCommand}" ]; then
273+
if [[ "$runBackground" != "true" && "$outputYAML" != "true" ]]; then
255274
echo "Executing collector command... "
256-
cmd="${K8S_CLI_BIN} exec -i --tty \
257-
-n $namespace \
258-
collector \
259-
-- $execCommand"
260-
eval "$cmd"
275+
if [ -n "$execOptions" ]; then
276+
eval "${K8S_CLI_BIN} exec -i --tty -n $namespace collector -- $execCommandBase --options \"$execOptions\" $execCommandArgs"
277+
else
278+
eval "${K8S_CLI_BIN} exec -i --tty -n $namespace collector -- $execCommandBase $execCommandArgs"
279+
fi
261280
elif [[ "$command" == "flows" || "$command" == "packets" ]]; then
262281
echo "Background capture started. Use:"
263282
echo " - '${K8S_CLI_BIN} netobserv follow' to see the capture progress"

go.mod

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,6 @@ require (
1919
sigs.k8s.io/e2e-framework v0.6.0
2020
)
2121

22-
require (
23-
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
24-
golang.org/x/mod v0.30.0 // indirect
25-
golang.org/x/sync v0.19.0 // indirect
26-
)
27-
2822
require (
2923
github.com/Masterminds/semver/v3 v3.4.0 // indirect
3024
github.com/beorn7/perks v1.0.1 // indirect
@@ -41,6 +35,7 @@ require (
4135
github.com/go-openapi/jsonreference v0.21.0 // indirect
4236
github.com/go-openapi/swag v0.23.1 // indirect
4337
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
38+
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
4439
github.com/google/gnostic-models v0.7.0 // indirect
4540
github.com/google/go-cmp v0.7.0 // indirect
4641
github.com/google/pprof v0.0.0-20250423184734-337e5dd93bb4 // indirect
@@ -79,8 +74,10 @@ require (
7974
go.opentelemetry.io/otel/trace v1.39.0 // indirect
8075
go.yaml.in/yaml/v2 v2.4.3 // indirect
8176
go.yaml.in/yaml/v3 v3.0.4 // indirect
77+
golang.org/x/mod v0.30.0 // indirect
8278
golang.org/x/net v0.48.0 // indirect
8379
golang.org/x/oauth2 v0.34.0 // indirect
80+
golang.org/x/sync v0.19.0 // indirect
8481
golang.org/x/sys v0.40.0 // indirect
8582
golang.org/x/term v0.38.0 // indirect
8683
golang.org/x/text v0.32.0 // indirect
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package kubernetes
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
8+
"k8s.io/client-go/kubernetes"
9+
"k8s.io/client-go/rest"
10+
)
11+
12+
func DeleteDaemonSet(ctx context.Context, namespace string) error {
13+
config, err := rest.InClusterConfig()
14+
if err != nil {
15+
return fmt.Errorf("cannot get Kubernetes InClusterConfig: %w", err)
16+
}
17+
18+
clientset, err := kubernetes.NewForConfig(config)
19+
if err != nil {
20+
return fmt.Errorf("cannot create Kubernetes client from InClusterConfig: %w", err)
21+
}
22+
23+
return clientset.AppsV1().DaemonSets(namespace).Delete(ctx, "netobserv-cli", v1.DeleteOptions{})
24+
}

scripts/functions.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ function follow() {
386386
}
387387

388388
function copyOutput() {
389-
echo "Copying collector output files..."
389+
echo "Copying collector files to ${OUTPUT_PATH}..."
390390
if [[ ! -d ${OUTPUT_PATH} ]]; then
391391
mkdir -p ${OUTPUT_PATH} >/dev/null
392392
fi
@@ -1108,7 +1108,7 @@ function check_args_and_apply() {
11081108
logLevel=$value
11091109
filter=${filter/$key=$logLevel/}
11101110
else
1111-
echo "invalid value for --action"
1111+
echo "invalid value for --log-level"
11121112
fi
11131113
;;
11141114
*max-time) # Max time

0 commit comments

Comments
 (0)