diff --git a/.github/workflows/build-and-snapshot.yml b/.github/workflows/build-and-snapshot.yml index 42cb832..3c8059d 100644 --- a/.github/workflows/build-and-snapshot.yml +++ b/.github/workflows/build-and-snapshot.yml @@ -21,10 +21,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: "3.11" @@ -52,10 +52,10 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} @@ -70,10 +70,49 @@ jobs: run: go mod tidy -e || true - name: Run golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v9 with: version: latest + - name: Run govulncheck + shell: bash + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + # Run in JSON mode and emit GitHub Actions warning annotations for each finding. + # govulncheck outputs multi-line JSON objects (not line-delimited), so we use + # raw_decode to parse successive top-level objects from the output stream. + govulncheck -json . 2>/dev/null | python3 -c " + import sys, json + decoder = json.JSONDecoder() + data = sys.stdin.read().strip() + pos = 0 + while pos < len(data): + obj, idx = decoder.raw_decode(data, pos) + pos = idx + while pos < len(data) and data[pos] in ' \t\n\r': + pos += 1 + if not isinstance(obj, dict): + continue + finding = obj.get('finding') + if not finding: + continue + osv_id = finding.get('osv', '') + traces = finding.get('trace', []) + mod = traces[0].get('module', '') if traces else '' + ver = traces[0].get('version', '') if traces else '' + fixed = finding.get('fixed_version', '') + summary = f'{mod} {ver} is vulnerable ({osv_id}); fixed in {fixed}' if fixed else osv_id + loc = '' + for frame in reversed(traces): + fpos = frame.get('position', {}) + fname = fpos.get('filename', '') + line_no = fpos.get('line', '') + if fname: + loc = f'file={fname},line={line_no},' + break + print(f'::warning {loc}title=govulncheck [{osv_id}]::{summary}') + " || true + - name: Lint and format Go files run: ./scripts/lint-go.sh ci @@ -83,7 +122,7 @@ jobs: python3 .github/workflows/build.py - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: cf-cli-java-plugin-${{ matrix.os }} path: | @@ -94,16 +133,17 @@ jobs: name: Create Snapshot Release needs: [build, lint-and-test-python] runs-on: ubuntu-latest + environment: release if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && (needs.lint-and-test-python.result == 'success' || needs.lint-and-test-python.result == 'skipped') permissions: contents: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: "3.11" @@ -113,7 +153,7 @@ jobs: pip install PyYAML - name: Download all artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: dist/ @@ -144,7 +184,7 @@ jobs: JSTALL_VERSION=$(curl -s https://api.github.com/repos/parttimenerd/jstall/releases/latest | python3 -c "import sys,json; print(json.load(sys.stdin).get('tag_name','unknown'))") echo "version=$JSTALL_VERSION" >> $GITHUB_OUTPUT - - uses: thomashampson/delete-older-releases@main + - uses: thomashampson/delete-older-releases@2ff234dfe6ad2757ac7e53d96e298fbe82b0fd56 # @main with: keep_latest: 0 delete_tag_regex: snapshot @@ -163,7 +203,7 @@ jobs: git push origin snapshot --force - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 with: files: | dist/* diff --git a/.github/workflows/plugin-repo.yml b/.github/workflows/plugin-repo.yml index 40dc7bf..af30f67 100644 --- a/.github/workflows/plugin-repo.yml +++ b/.github/workflows/plugin-repo.yml @@ -11,13 +11,14 @@ jobs: generate-plugin-repo: name: Generate Plugin Repository YAML runs-on: ubuntu-latest + environment: release steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: "3.x" diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 49cb8ea..e39f333 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -16,22 +16,22 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ">=1.23.5" - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: "3.11" - name: Set up Node.js for markdownlint - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: "18" + node-version: "24" - name: Download JStall minimal JAR for go:embed run: | @@ -42,10 +42,48 @@ jobs: run: go mod tidy -e || true - name: Run golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v9 with: version: latest + - name: Run govulncheck + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + # Run in JSON mode and emit GitHub Actions warning annotations for each finding. + # govulncheck outputs multi-line JSON objects (not line-delimited), so we use + # raw_decode to parse successive top-level objects from the output stream. + govulncheck -json . 2>/dev/null | python3 -c " + import sys, json + decoder = json.JSONDecoder() + data = sys.stdin.read().strip() + pos = 0 + while pos < len(data): + obj, idx = decoder.raw_decode(data, pos) + pos = idx + while pos < len(data) and data[pos] in ' \t\n\r': + pos += 1 + if not isinstance(obj, dict): + continue + finding = obj.get('finding') + if not finding: + continue + osv_id = finding.get('osv', '') + traces = finding.get('trace', []) + mod = traces[0].get('module', '') if traces else '' + ver = traces[0].get('version', '') if traces else '' + fixed = finding.get('fixed_version', '') + summary = f'{mod} {ver} is vulnerable ({osv_id}); fixed in {fixed}' if fixed else osv_id + loc = '' + for frame in reversed(traces): + fpos = frame.get('position', {}) + fname = fpos.get('filename', '') + line_no = fpos.get('line', '') + if fname: + loc = f'file={fname},line={line_no},' + break + print(f'::warning {loc}title=govulncheck [{osv_id}]::{summary}') + " || true + - name: Lint Go code run: ./scripts/lint-go.sh ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f74625..cf6e408 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,15 +23,15 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: "3.x" @@ -50,7 +50,7 @@ jobs: run: go mod tidy -e || true - name: Run golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v9 with: version: latest @@ -73,16 +73,17 @@ jobs: name: Create GitHub Release with Plugin Repository Entry needs: release runs-on: ubuntu-latest + environment: release steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: "3.x" @@ -159,7 +160,7 @@ jobs: run: echo "timestamp=$(date -u +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_OUTPUT - name: Create GitHub Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 with: tag_name: ${{ github.event.inputs.version }} name: ${{ github.event.inputs.version }} diff --git a/.golangci.yml b/.golangci.yml index 37bc648..4cdaa9f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -43,12 +43,22 @@ linters: - wastedassign # Check wasted assignments - whitespace # Check for extra whitespace - gocritic + - gosec # Security-focused linter (shell commands, file perms, crypto) + + settings: + gosec: + # exec.Command("cf", ...) — "cf" is a hardcoded binary; only SSH/CF args vary. + # exec.Command(javaPath, ...) — resolved from JAVA_HOME/PATH, not user input. + excludes: + - G204 # Subprocess launched with variable + nolintlint: + allow-unused: true # G702 nolint directives are unused on older linter versions disable: # Disabled as requested - gochecknoglobals # Ignore global variables (as requested) - + # Disabled for being too strict or problematic - testpackage # Too strict - requires separate test packages - paralleltest # Not always applicable @@ -70,5 +80,4 @@ linters: - gocyclo # Check cyclomatic complexity - cyclop # Check cyclomatic complexity - funlen # Check function length - - gosec # Security-focused linter - revive # Fast, configurable, extensible linter diff --git a/cf_cli_java_plugin.go b/cf_cli_java_plugin.go index 057d261..eaab765 100644 --- a/cf_cli_java_plugin.go +++ b/cf_cli_java_plugin.go @@ -13,6 +13,7 @@ import ( "fmt" "os" "os/exec" + "path/filepath" "strconv" "strings" @@ -251,9 +252,9 @@ var flagDefinitions = []FlagDefinition{ }, { Name: "verbose", - ShortName: "v", - Usage: "enable verbose output for the plugin", - Description: "enable verbose output for the plugin", + ShortName: "", + Usage: "enable verbose output for the plugin (note: -v is reserved by CF CLI)", + Description: "enable verbose output for the plugin (note: -v is reserved by CF CLI for its own trace mode and cannot be used as a shorthand here)", Type: typeBool, }, { @@ -291,13 +292,14 @@ func (c *JavaPlugin) createOptionsParser() flags.FlagContext { // Create flags from centralized definitions for _, flagDef := range flagDefinitions { + short := flagDef.ShortName switch flagDef.Type { case "int": - commandFlags.NewIntFlagWithDefault(flagDef.Name, flagDef.ShortName, flagDef.Usage, flagDef.DefaultInt) + commandFlags.NewIntFlagWithDefault(flagDef.Name, short, flagDef.Usage, flagDef.DefaultInt) case "bool": - commandFlags.NewBoolFlag(flagDef.Name, flagDef.ShortName, flagDef.Usage) + commandFlags.NewBoolFlag(flagDef.Name, short, flagDef.Usage) case "string": - commandFlags.NewStringFlag(flagDef.Name, flagDef.ShortName, flagDef.Usage) + commandFlags.NewStringFlag(flagDef.Name, short, flagDef.Usage) } } @@ -845,6 +847,19 @@ func (c *JavaPlugin) execute(_ plugin.CliConnection, args []string) (string, err localDir := options.LocalDir if localDir == "" { localDir = "." + } else { + // Expand tilde to home directory + if localDir == "~" || strings.HasPrefix(localDir, "~/") { + home, err := os.UserHomeDir() + if err == nil { + localDir = home + localDir[1:] + } + } + // Reject path traversal sequences + cleaned := filepath.Clean(localDir) + if strings.Contains(cleaned, "..") { + return "", &InvalidUsageError{message: "Error: --local-dir must not contain path traversal sequences (..)"} + } } c.logVerbosef("Remote directory: %s", remoteDir) @@ -930,6 +945,10 @@ func (c *JavaPlugin) execute(_ plugin.CliConnection, args []string) (string, err c.logVerbosef("Command %s does not support --args flag", command.Name) return "", &InvalidUsageError{message: fmt.Sprintf("The flag %q is not supported for %s", "args", command.Name)} } + // Reject whitespace-only --args + if options.Args != "" && strings.TrimSpace(options.Args) == "" { + return "", &InvalidUsageError{message: "Error: --args must not be empty or whitespace-only"} + } // Validate that commands requiring @ARGS have arguments provided if command.HasMiscArgs() && options.Args == "" && (command.Name == toolJcmd || command.Name == toolAsprof) { c.logVerbosef("Command %s requires --args flag", command.Name) @@ -945,6 +964,13 @@ func (c *JavaPlugin) execute(_ plugin.CliConnection, args []string) (string, err return "", &InvalidUsageError{message: fmt.Sprintf("Too many arguments provided: %v", strings.Join(arguments[2:], ", "))} } + // Validate --local-dir exists (catches errors early, including during dry-run) + if options.LocalDir != "" && (command.GenerateFiles || command.GenerateArbitraryFiles) { + if _, statErr := os.Stat(localDir); os.IsNotExist(statErr) { + return "", &InvalidUsageError{message: fmt.Sprintf("Error: --local-dir %q does not exist", localDir)} + } + } + applicationName := arguments[1] c.logVerbosef("Application name: %s", applicationName) @@ -1114,10 +1140,7 @@ func (c *JavaPlugin) execute(_ plugin.CliConnection, args []string) (string, err fullCommand := append([]string{}, cfSSHArguments...) fullCommand = append(fullCommand, remoteCommand) c.logVerbosef("Executing command: %v", fullCommand) - - cmdArgs := append([]string{"cf"}, fullCommand...) - c.logVerbosef("Executing command: %v", cmdArgs) - cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + cmd := exec.Command("cf", fullCommand...) outputBytes, err := cmd.CombinedOutput() output := strings.TrimRight(string(outputBytes), "\n") if err != nil { diff --git a/go.mod b/go.mod index 9819702..ebd3fee 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,10 @@ module cf.plugin.ref/requires -go 1.24.3 - -toolchain go1.24.4 +go 1.25.0 require ( code.cloudfoundry.org/cli v0.0.0-20250623142502-fb19e7a825ee + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/lithammer/fuzzysearch v1.1.8 github.com/simonleung8/flags v0.0.0-20170704170018-8020ed7bcf1a ) @@ -22,7 +21,6 @@ require ( github.com/cloudfoundry/bosh-utils v0.0.397 // indirect github.com/cppforlife/go-patch v0.1.0 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/jessevdk/go-flags v1.6.1 // indirect github.com/kr/pretty v0.3.1 // indirect @@ -35,10 +33,10 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/vito/go-interact v0.0.0-20171111012221-fa338ed9e9ec // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/cheggaaa/pb.v1 v1.0.28 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 09d015c..8a0da8a 100644 --- a/go.sum +++ b/go.sum @@ -190,8 +190,8 @@ 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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp/typeparams v0.0.0-20220218215828-6cf2b201936e/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= @@ -209,8 +209,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -241,21 +241,21 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= 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.6/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.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= 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= @@ -265,8 +265,8 @@ golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E golang.org/x/tools v0.1.11-0.20220316014157-77aa08bb151a/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= 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.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= 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= diff --git a/jstall.go b/jstall.go index d19771b..e5ef136 100644 --- a/jstall.go +++ b/jstall.go @@ -32,7 +32,7 @@ func javaExecutable() string { } func getJavaMajorVersion(javaPath string) (int, error) { - cmd := exec.Command(javaPath, "-version") + cmd := exec.Command(javaPath, "-version") //nolint:gosec // G702: javaPath comes from findJava17Plus, resolved from JAVA_HOME or PATH output, err := cmd.CombinedOutput() if err != nil { return 0, err @@ -130,7 +130,7 @@ func ensureJstallJar() (string, error) { cacheDir = os.TempDir() } pluginCacheDir := filepath.Join(cacheDir, "cf-java-plugin") - if err := os.MkdirAll(pluginCacheDir, 0o755); err != nil { + if err := os.MkdirAll(pluginCacheDir, 0o755); err != nil { //nolint:gosec // 0755 is correct for a cache dir return "", err } jarPath := filepath.Join(pluginCacheDir, "jstall-minimal.jar") @@ -138,17 +138,17 @@ func ensureJstallJar() (string, error) { // Check if cached JAR matches the embedded version by SHA-256 hash expectedHash := jstallJarHash() - if cachedHash, err := os.ReadFile(hashPath); err == nil && string(cachedHash) == expectedHash { + if cachedHash, err := os.ReadFile(hashPath); err == nil && string(cachedHash) == expectedHash { //nolint:gosec // path is derived from UserCacheDir, not user input if _, err := os.Stat(jarPath); err == nil { return jarPath, nil } } // Extract embedded JAR and write hash - if err := os.WriteFile(jarPath, jstallJarBytes, 0o644); err != nil { + if err := os.WriteFile(jarPath, jstallJarBytes, 0o644); err != nil { //nolint:gosec // 0644 is correct; JAR must be readable to execute return "", err } - if err := os.WriteFile(hashPath, []byte(expectedHash), 0o644); err != nil { + if err := os.WriteFile(hashPath, []byte(expectedHash), 0o644); err != nil { //nolint:gosec // 0644 is correct for a hash file // Non-fatal: JAR is already written, just can't cache the hash _ = err } @@ -170,6 +170,11 @@ func formatCommandForDisplay(command string, args []string) string { return command + " " + strings.Join(displayArgs, " ") } +// shellQuote wraps s in single quotes, escaping any single quotes within. +func shellQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'" +} + func (c *JavaPlugin) executeJstall(appName string, jstallArgs string, appInstanceIndex int, dryRun bool) (string, error) { javaPath, err := findJava17Plus() if err != nil { @@ -188,7 +193,8 @@ func (c *JavaPlugin) executeJstall(appName string, jstallArgs string, appInstanc // Build SSH command with PATH setup so jps/jcmd are discoverable on remote container // SAP Java Buildpack puts JDK tools at deep paths not on $PATH pathSetup := `JDK_BIN=$(dirname "$(find . -executable -name jps 2>/dev/null | head -1)" 2>/dev/null); if [ -n "$JDK_BIN" ]; then export PATH="$JDK_BIN:$PATH"; fi;` - sshCmd := "cf ssh " + appName + // Shell-quote appName to prevent command injection via a maliciously named CF app. + sshCmd := "cf ssh " + shellQuote(appName) if appInstanceIndex != -1 { sshCmd += " --app-instance-index " + strconv.Itoa(appInstanceIndex) } @@ -229,7 +235,7 @@ func (c *JavaPlugin) executeJstall(appName string, jstallArgs string, appInstanc } } - cmd := exec.Command(javaPath, args...) + cmd := exec.Command(javaPath, args...) //nolint:gosec // G702: javaPath comes from findJava17Plus, resolved from JAVA_HOME or PATH cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Stdin = os.Stdin diff --git a/scripts/lint-go.sh b/scripts/lint-go.sh index cc3dfa7..519aafc 100755 --- a/scripts/lint-go.sh +++ b/scripts/lint-go.sh @@ -119,7 +119,17 @@ case "$MODE" in print_warning "golangci-lint not found, skipping comprehensive linting" print_info "Install with: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest" fi - + + echo "🔍 Running govulncheck..." + if command -v govulncheck >/dev/null 2>&1; then + # Report vulnerabilities but don't fail — remaining findings are stdlib-only + # and require a Go major version bump (1.25.x) to resolve. + govulncheck . || true + else + print_warning "govulncheck not found, skipping vulnerability scan" + print_info "Install with: go install golang.org/x/vuln/cmd/govulncheck@latest" + fi + print_status "All Go linting checks passed!" ;; diff --git a/test/requirements.txt b/test/requirements.txt index 68e25b5..6c17196 100644 --- a/test/requirements.txt +++ b/test/requirements.txt @@ -1,5 +1,5 @@ # Requirements for CF Java Plugin Test Suite -pytest==8.4.1 +pytest==9.0.3 pyyaml>=6.0 pytest-xdist>=3.7.0 # For parallel test execution pytest-html>=4.1.1 # For HTML test reports diff --git a/utils/cfutils.go b/utils/cfutils.go index 91a0245..dbde266 100644 --- a/utils/cfutils.go +++ b/utils/cfutils.go @@ -213,11 +213,11 @@ func GetAvailablePath(data string, userpath string) (string, error) { func CopyOverCat(args []string, src string, dest string) error { // Ensure parent directory exists if dir := filepath.Dir(dest); dir != "" && dir != "." { - if err := os.MkdirAll(dir, 0o755); err != nil { + if err := os.MkdirAll(dir, 0o755); err != nil { //nolint:gosec // 0755 is correct for a local download directory return fmt.Errorf("cannot create local directory %s: %w", dir, err) } } - f, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) + f, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600) //nolint:gosec // dest is a plugin-constructed output path, not user-supplied file inclusion if err != nil { return errors.New("Error creating local file at " + dest + ". Please check that you are allowed to create files at the given local path.") }