diff --git a/.ci/check b/.ci/check index a778d1a2..f84378fb 100755 --- a/.ci/check +++ b/.ci/check @@ -19,4 +19,9 @@ source "${SOURCE_PATH}/.ci/common.sh" echo "> Check..." make check -echo "All checks are passing" \ No newline at end of file + +# Run Static Application Security Testing (SAST) using gosec +echo "> SAST..." +make sast-report + +echo -e "\nAll checks are passing" diff --git a/.ci/pipeline_definitions b/.ci/pipeline_definitions index 2762f599..eb598bdb 100644 --- a/.ci/pipeline_definitions +++ b/.ci/pipeline_definitions @@ -1,5 +1,12 @@ etcd-wrapper: base_definition: + repo: + source_labels: + - name: cloud.gardener.cnudie/dso/scanning-hints/source_analysis/v1 + value: + policy: skip + comment: | + we use gosec for sast scanning. See attached log. traits: version: preprocess: @@ -61,6 +68,16 @@ etcd-wrapper: image: europe-docker.pkg.dev/gardener-project/releases/gardener/etcd-wrapper release: nextversion: 'bump_minor' + assets: + - type: build-step-log + step_name: check + purposes: + - lint + - sast + - gosec + comment: | + we use gosec (linter) for SAST scans + see: https://github.com/securego/gosec slack: default_channel: 'internal_scp_workspace' channel_cfgs: diff --git a/.gitignore b/.gitignore index 7de589c3..501b9e0c 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,6 @@ go.work # Ignore downloaded binaries /bin /hack/tools/bin + +# gosec +gosec-report.sarif diff --git a/Makefile b/Makefile index bdc5bf1f..bd43ff3a 100644 --- a/Makefile +++ b/Makefile @@ -29,4 +29,12 @@ revendor: .PHONY: check check: $(GOLANGCI_LINT) - @./hack/check.sh --golangci-lint-config=./.golangci.yaml ./internal/... \ No newline at end of file + @./hack/check.sh --golangci-lint-config=./.golangci.yaml ./internal/... + +.PHONY: sast +sast: $(GOSEC) + @./hack/sast.sh + +.PHONY: sast-report +sast-report: $(GOSEC) + @./hack/sast.sh --gosec-report true diff --git a/hack/sast.sh b/hack/sast.sh new file mode 100755 index 00000000..23e84a75 --- /dev/null +++ b/hack/sast.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +# +# SPDX-License-Identifier: Apache-2.0 + +set -e + +root_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )/.." &> /dev/null && pwd )" + +gosec_report="false" +gosec_report_parse_flags="" + +parse_flags() { + while test $# -gt 1; do + case "$1" in + --gosec-report) + shift; gosec_report="$1" + ;; + *) + echo "Unknown argument: $1" + exit 1 + ;; + esac + shift + done +} + +parse_flags "$@" + +echo "> Running gosec" +gosec --version +if [[ "$gosec_report" != "false" ]]; then + echo "Exporting report to $root_dir/gosec-report.sarif" + gosec_report_parse_flags="-track-suppressions -fmt=sarif -out=gosec-report.sarif -stdout" +fi + +# exclude generated code, hack directory (where hack scripts reside) +# and tmp directory (where temporary mod files are downloaded) +# shellcheck disable=SC2086 +gosec -exclude-generated -exclude-dir=hack -exclude-dir=tmp $gosec_report_parse_flags ./... diff --git a/hack/tools.mk b/hack/tools.mk index 294c890e..620e2fe8 100644 --- a/hack/tools.mk +++ b/hack/tools.mk @@ -5,10 +5,12 @@ TOOLS_DIR := hack/tools TOOLS_BIN_DIR := $(TOOLS_DIR)/bin GOLANGCI_LINT := $(TOOLS_BIN_DIR)/golangci-lint +GOSEC := $(TOOLS_BIN_DIR)/gosec GO_ADD_LICENSE := $(TOOLS_BIN_DIR)/addlicense # default tool versions GOLANGCI_LINT_VERSION ?= v1.61.0 +GOSEC_VERSION ?= v2.21.4 GO_ADD_LICENSE_VERSION ?= latest export TOOLS_BIN_DIR := $(TOOLS_BIN_DIR) @@ -23,5 +25,8 @@ $(GOLANGCI_LINT): $(call tool_version_file,$(GOLANGCI_LINT),$(GOLANGCI_LINT_VERS @# see https://github.com/golangci/golangci-lint/issues/1276 GOBIN=$(abspath $(TOOLS_BIN_DIR)) CGO_ENABLED=1 go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) +$(GOSEC): $(call tool_version_file,$(GOSEC),$(GOSEC_VERSION)) + @GOSEC_VERSION=$(GOSEC_VERSION) $(TOOLS_DIR)/install-gosec.sh + $(GO_ADD_LICENSE): GOBIN=$(abspath $(TOOLS_BIN_DIR)) go install github.com/google/addlicense@$(GO_ADD_LICENSE_VERSION) \ No newline at end of file diff --git a/hack/tools/install-gosec.sh b/hack/tools/install-gosec.sh new file mode 100755 index 00000000..079dc91e --- /dev/null +++ b/hack/tools/install-gosec.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +# +# SPDX-License-Identifier: Apache-2.0 + +set -e + +echo "> Installing gosec" + +TOOLS_BIN_DIR=${TOOLS_BIN_DIR:-$(dirname $0)/bin} + +platform=$(uname -s | tr '[:upper:]' '[:lower:]') +version=$GOSEC_VERSION +case $(uname -m) in + aarch64 | arm64) + arch="arm64" + ;; + x86_64) + arch="amd64" + ;; + *) + echo "Unknown architecture" + exit 1 + ;; +esac + +archive_name="gosec_${version#v}_${platform}_${arch}" +file_name="${archive_name}.tar.gz" + +temp_dir="$(mktemp -d)" +function cleanup { + rm -rf "${temp_dir}" +} +trap cleanup EXIT ERR INT TERM + +curl -L -o "${temp_dir}/${file_name}" "https://github.com/securego/gosec/releases/download/${version}/${file_name}" + +tar -xzm -C "${temp_dir}" -f "${temp_dir}/${file_name}" +mv "${temp_dir}/gosec" $TOOLS_BIN_DIR +chmod +x "${TOOLS_BIN_DIR}/gosec" diff --git a/internal/app/readycheck.go b/internal/app/readycheck.go index efea11c6..f966a646 100644 --- a/internal/app/readycheck.go +++ b/internal/app/readycheck.go @@ -21,11 +21,12 @@ import ( const ( // ServerPort is the port number for the http server of etcd wrapper - ServerPort = int64(9095) - etcdConnectionTimeout = 5 * time.Second - etcdGetTimeout = 5 * time.Second - etcdQueryInterval = 2 * time.Second - etcdEndpointPort = "2379" + ServerPort = int64(9095) + etcdEndpointPort = "2379" + etcdWrapperReadHeaderTimeout = 5 * time.Second + etcdConnectionTimeout = 5 * time.Second + etcdGetTimeout = 5 * time.Second + etcdQueryInterval = 2 * time.Second ) // queryAndUpdateEtcdReadiness periodically queries the etcd DB to check its readiness and updates the status @@ -146,7 +147,8 @@ func (a *Application) RegisterHandler() { mux.HandleFunc("/stop", a.stopEtcdHandler) a.server = &http.Server{ - Addr: fmt.Sprintf(":%d", ServerPort), - Handler: mux, + Addr: fmt.Sprintf(":%d", ServerPort), + Handler: mux, + ReadHeaderTimeout: etcdWrapperReadHeaderTimeout, } } diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 81371536..8e8ea170 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -88,7 +88,7 @@ func CaptureExitCode(signal os.Signal, exitCodeFilePath string) error { return nil } interruptSignal := []byte(signal.String()) - return os.WriteFile(exitCodeFilePath, interruptSignal, 0644) + return os.WriteFile(exitCodeFilePath, interruptSignal, 0600) } // CleanupExitCode removes the `exit_code` file @@ -116,7 +116,7 @@ func (i *initializer) tryGetEtcdConfig(ctx context.Context, maxRetries int, inte func determineValidationMode(exitCodeFilePath string, logger *zap.Logger) brclient.ValidationType { var err error if _, err = os.Stat(exitCodeFilePath); err == nil { - data, err := os.ReadFile(exitCodeFilePath) + data, err := os.ReadFile(exitCodeFilePath) // #nosec G304 -- only path passed is `DefaultExitCodeFilePath`, no user input is used. if err != nil { logger.Error("error in reading exitCodeFile, assuming full-validation to be done.", zap.String("exitCodeFilePath", exitCodeFilePath), zap.Error(err)) return brclient.FullValidation diff --git a/internal/brclient/brclient.go b/internal/brclient/brclient.go index 5b6f99cb..1a4b7b5b 100644 --- a/internal/brclient/brclient.go +++ b/internal/brclient/brclient.go @@ -145,7 +145,7 @@ func (c *brClient) GetEtcdConfig(ctx context.Context) (string, error) { if err != nil { return "", err } - if err = os.WriteFile(c.etcdConfigFilePath, etcdConfigBytes, 0644); err != nil { + if err = os.WriteFile(c.etcdConfigFilePath, etcdConfigBytes, 0600); err != nil { return "", err } return c.etcdConfigFilePath, nil diff --git a/internal/testutil/tls.go b/internal/testutil/tls.go index a16188a6..599c68c8 100644 --- a/internal/testutil/tls.go +++ b/internal/testutil/tls.go @@ -52,13 +52,13 @@ func (c *CertKeyPair) EncodeAndWrite(dir string, certFileName, keyFileName strin key := x509.MarshalPKCS1PrivateKey(&c.PrivateKey) pemKeyBytes := pemEncode(key, "RSA PRIVATE KEY") privateKeyPath := filepath.Join(dir, keyFileName) - err := os.WriteFile(privateKeyPath, pemKeyBytes, os.ModePerm) + err := os.WriteFile(privateKeyPath, pemKeyBytes, 0600) if err != nil { return fmt.Errorf("failed to write private key to dir: %s: err: %v", dir, err) } pemCertBytes := pemEncode(c.CertBytes, "CERTIFICATE") certPath := filepath.Join(dir, certFileName) - err = os.WriteFile(certPath, pemCertBytes, os.ModePerm) + err = os.WriteFile(certPath, pemCertBytes, 0600) if err != nil { return fmt.Errorf("failed to write certificate to dir: %s: err: %v", dir, err) } diff --git a/internal/util/tls.go b/internal/util/tls.go index 0991a5b2..f64bdd9d 100644 --- a/internal/util/tls.go +++ b/internal/util/tls.go @@ -12,7 +12,7 @@ import ( // CreateCACertPool creates a CA cert pool gives a CA cert bundle func CreateCACertPool(caCertBundlePath string) (*x509.CertPool, error) { - caCertBundle, err := os.ReadFile(caCertBundlePath) + caCertBundle, err := os.ReadFile(caCertBundlePath) // #nosec G304 -- path is generated by etcd-backup-restore server's /config handler. if err != nil { return nil, err } @@ -35,9 +35,9 @@ type KeyPair struct { // CreateTLSConfig creates a TLS Config to be used for TLS communication. func CreateTLSConfig(tlsEnabledFn IsTLSEnabledFn, serverName, caCertPath string, keyPair *KeyPair) (*tls.Config, error) { - tlsConf := tls.Config{} + tlsConf := tls.Config{} // #nosec G402 -- tlsConf.MinVersion=1.2 by default. if !tlsEnabledFn() { - tlsConf.InsecureSkipVerify = true + tlsConf.InsecureSkipVerify = true // #nosec G402 -- InsecureSkipVerify is set to true only when TLS is disabled. return &tlsConf, nil }