diff --git a/.ecrc b/.ecrc index a31c8e5..a644e75 100644 --- a/.ecrc +++ b/.ecrc @@ -11,6 +11,8 @@ "\\.svg$", "\\.terraform\\.lock\\.hcl$", "\\.txt$", + "docs/man", + "docs/markdown", "go\\.mod$", "go\\.sum$", "package-lock\\.json$", diff --git a/.golangci.yml b/.golangci.yml index 89a1948..ee734ed 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1335,6 +1335,11 @@ issues: - gosec source: lint:allow_unhandled + - text: (G304) + linters: + - gosec + source: lint:allow_include_file + - text: (G404) linters: - gosec diff --git a/.licensei.toml b/.licensei.toml index d3d2ff4..8adb131 100644 --- a/.licensei.toml +++ b/.licensei.toml @@ -1,14 +1,14 @@ [header] template = """// Copyright 2024, Ryan Parman // -// Licensed under the Apache License, Version 2.0 (the \"License\"); +// Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an \"AS IS\" BASIS, +// distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License.""" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7f3e67b..46a8d92 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -45,6 +45,8 @@ repos: - --ignore=node_modules - --ignore=.github - --ignore=.templates + - --ignore=docs/man + - --ignore=docs/markdown - --fix - "**/*.md" @@ -71,12 +73,12 @@ repos: language: system stages: [commit, push] - - id: go-consistent - name: "Go: Consistent Patterns" - description: Analyzes Go packages to identify unnecessary type conversions. - entry: bash -c 'go-consistent ./...' - language: system - stages: [commit, push] + # - id: go-consistent + # name: "Go: Consistent Patterns" + # description: Analyzes Go packages to identify unnecessary type conversions. + # entry: bash -c 'go-consistent ./...' + # language: system + # stages: [commit, push] - id: unconvert name: "Go: unconvert (current GOOS/GOARCH)" @@ -85,12 +87,12 @@ repos: language: system stages: [commit, push] - - id: smrcptr - name: "Go: Same Receiver Pointer" - description: Don't mix receiver types. Choose either pointers or struct types for all available methods. - entry: bash -c 'smrcptr -skip-std=true --constructor=true ./...' - language: system - stages: [commit, push] + # - id: smrcptr + # name: "Go: Same Receiver Pointer" + # description: Don't mix receiver types. Choose either pointers or struct types for all available methods. + # entry: bash -c 'smrcptr -skip-std=true --constructor=true ./...' + # language: system + # stages: [commit, push] - id: govulncheck name: "Go: Vulnerability check" diff --git a/Makefile b/Makefile index 2062d79..088d8ed 100644 --- a/Makefile +++ b/Makefile @@ -68,11 +68,12 @@ install-tools-go: $(GO) install github.com/pelletier/go-toml/v2/cmd/tomljson@latest $(GO) install github.com/quasilyte/go-consistent@latest $(GO) install github.com/securego/gosec/v2/cmd/gosec@latest + $(GO) install github.com/spf13/cobra-cli@latest $(GO) install golang.org/x/perf/cmd/benchstat@latest $(GO) install golang.org/x/tools/cmd/godoc@latest + $(GO) install golang.org/x/tools/go/analysis/passes/fieldalignment/cmd/fieldalignment@latest $(GO) install golang.org/x/vuln/cmd/govulncheck@latest $(GO) install gotest.tools/gotestsum@latest - $(GO) install github.com/spf13/cobra-cli@latest .PHONY: install-tools-mac ## install-tools-mac: [tools]* Install/upgrade the required tools for macOS, including Go packages. diff --git a/cmd/doc.go b/cmd/doc.go new file mode 100644 index 0000000..bb7b932 --- /dev/null +++ b/cmd/doc.go @@ -0,0 +1,20 @@ +// Copyright 2024, Ryan Parman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* +Package cmd is the entry points for the command-line interface (CLI) of the +`devsec-tools` application. Each package in this directory is a sub-command of +the CLI. +*/ +package cmd diff --git a/cmd/docs.go b/cmd/docs.go index 91cee17..4df7db6 100644 --- a/cmd/docs.go +++ b/cmd/docs.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -39,7 +39,10 @@ var ( logger.Fatal().Err(err).Msg("Failed to generate Manpage documentation.") } } else { - cmd.Help() + err := cmd.Help() + if err != nil { + logger.Fatal().Err(err).Msg("Failed to display help.") + } } }, } diff --git a/cmd/dockerfile_hasher.go b/cmd/hasher.go similarity index 82% rename from cmd/dockerfile_hasher.go rename to cmd/hasher.go index 3d24e31..1002134 100644 --- a/cmd/dockerfile_hasher.go +++ b/cmd/hasher.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -18,7 +18,7 @@ import ( "strings" "github.com/lithammer/dedent" - hasher "github.com/northwood-labs/devsec-tools/pkg/dockerfile-hasher" + "github.com/northwood-labs/devsec-tools/pkg/hasher" "github.com/spf13/cobra" ) @@ -44,7 +44,7 @@ var ( Run: func(cmd *cobra.Command, args []string) { dockerfile, rawParser, stageList, err := hasher.ReadFile(fDockerfile, logger) if err != nil { - panic(err) + logger.Fatal().Err(err).Msg("Failed to read/parse the Dockerfile.") } dockerfileLines, err := hasher.ModifyFromLines( @@ -54,7 +54,7 @@ var ( logger, ) if err != nil { - panic(err) + logger.Fatal().Err(err).Msg("Failed to modify the image references.") } outputStream := "" @@ -64,7 +64,7 @@ var ( bites, err := hasher.WriteFile(dockerfileLines, outputStream, logger) if err != nil { - panic(err) + logger.Fatal().Err(err).Msg("Failed to write the changes back to disk.") } logger.Info().Int("bytes", bites).Msgf("Wrote %d bytes.", bites) @@ -80,7 +80,10 @@ func init() { // lint:allow_init &fDockerfile, "dockerfile", "f", "Dockerfile", "Path to the Dockerfile to parse/rewrite.", ) - dockerfileHasherCmd.MarkFlagRequired("dockerfile") + err := dockerfileHasherCmd.MarkFlagRequired("dockerfile") + if err != nil { + logger.Fatal().Err(err).Msg("The --dockerfile flag was missing from the command.") + } rootCmd.AddCommand(dockerfileHasherCmd) } diff --git a/cmd/root.go b/cmd/root.go index 693843b..11efc97 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, @@ -15,7 +15,6 @@ package cmd import ( - "context" "fmt" "os" "strconv" @@ -30,7 +29,6 @@ import ( var ( // Color text. colorHeader = color.New(color.FgWhite, color.BgBlue, color.OpBold) - ctx = context.Background() logger zerolog.Logger fQuiet bool @@ -55,15 +53,6 @@ var ( } ) -// Execute adds all child commands to the root command and sets flags appropriately. -// This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute() { - err := rootCmd.Execute() - if err != nil { - os.Exit(1) - } -} - func init() { rootCmd.PersistentFlags().BoolVarP( &fVerbose, "verbose", "v", false, "Enable verbose output.", @@ -75,36 +64,39 @@ func init() { rootCmd.MarkFlagsMutuallyExclusive("verbose", "quiet") } +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + os.Exit(1) + } +} + func getLogger(useJSON bool) zerolog.Logger { var zlog zerolog.Logger zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string { short := file + for i := len(file) - 1; i > 0; i-- { if file[i] == '/' { short = file[i+1:] break } } + file = short return file + ":" + strconv.Itoa(line) } - // JSON output - if useJSON { - zlog = zerolog.New(os.Stderr).With(). - Caller(). - Timestamp(). - Logger() - } - output := zerolog.ConsoleWriter{ Out: os.Stderr, TimeFormat: zerolog.TimeFormatUnix, } - output.FormatLevel = func(i interface{}) string { + output.FormatLevel = func(i any) string { return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) } @@ -112,6 +104,14 @@ func getLogger(useJSON bool) zerolog.Logger { Timestamp(). Logger() + // JSON output + if useJSON { + zlog = zerolog.New(os.Stderr).With(). + Caller(). + Timestamp(). + Logger() + } + zlog = zlog.Level(zerolog.InfoLevel) if fQuiet { zlog = zlog.Level(zerolog.ErrorLevel) diff --git a/lambda/dockerfile-hasher/main.go b/lambda/dockerfile-hasher/main.go deleted file mode 100644 index fc50b95..0000000 --- a/lambda/dockerfile-hasher/main.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "context" - - "github.com/aws/aws-lambda-go/lambda" - "github.com/aws/aws-lambda-go/lambdacontext" - "github.com/northwood-labs/golang-utils/debug" -) - -func main() { - lambda.Start(HandleRequest) -} - -func HandleRequest(ctx context.Context, event interface{}) error { - lctx, _ := lambdacontext.FromContext(ctx) - - pp := debug.GetSpew() - pp.Dump(lctx) - - return nil -} diff --git a/lambda/hasher/main.go b/lambda/hasher/main.go new file mode 100644 index 0000000..62773e1 --- /dev/null +++ b/lambda/hasher/main.go @@ -0,0 +1,36 @@ +// Copyright 2024, Ryan Parman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + + "github.com/aws/aws-lambda-go/lambda" + "github.com/aws/aws-lambda-go/lambdacontext" + "github.com/northwood-labs/golang-utils/debug" +) + +func main() { + lambda.Start(HandleRequest) +} + +func HandleRequest(ctx context.Context, event any) error { + lctx, _ := lambdacontext.FromContext(ctx) + + pp := debug.GetSpew() + pp.Dump(lctx) + + return nil +} diff --git a/main.go b/main.go index c1e2c51..120b5e7 100644 --- a/main.go +++ b/main.go @@ -4,7 +4,7 @@ // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // -// http://www.apache.org/licenses/LICENSE-2.0 +// http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, diff --git a/pkg/dockerfile-hasher/dockerfile_hasher_test.go b/pkg/dockerfile-hasher/dockerfile_hasher_test.go deleted file mode 100644 index 9063fc8..0000000 --- a/pkg/dockerfile-hasher/dockerfile_hasher_test.go +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright 2024, Ryan Parman -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package dockerfile_hasher - -import ( - "testing" - - "github.com/google/go-cmp/cmp" -) - -var ( - // ImageRefTestTable is a table-driven test for the ImageRef.OriginalLine method. - // - ImageRefTestTable = map[string]struct { - Expected string - Name string - Flags []string - Args []string - }{ - "syntax=docker/dockerfile:1": { - Name: "#", - Args: []string{"syntax=docker/dockerfile:1"}, - Expected: "# syntax=docker/dockerfile:1", - }, - "golang:1.21-alpine": { - Name: "FROM", - Flags: []string{"--platform=$BUILDPLATFORM"}, - Args: []string{"golang:1.21-alpine", "AS", "base-builder"}, - Expected: "FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS base-builder", - }, - "golang@sha256:0ff68fa7b2177e8d68b4555621c2321c804bcff839fd512c2681de49026573b7": { - Name: "FROM", - Flags: []string{"--platform=$BUILDPLATFORM"}, - Args: []string{ - "golang@sha256:0ff68fa7b2177e8d68b4555621c2321c804bcff839fd512c2681de49026573b7", - "AS", - "fake-second-base", - }, - Expected: "FROM --platform=$BUILDPLATFORM golang@sha256:0ff68fa7b2177e8d68b4555621c2321c804bcff839fd512" + - "c2681de49026573b7 AS fake-second-base", - }, - "ghcr.io/adrianchifor/harpoon:latest": { - Name: "FROM", - Flags: []string{"--platform=$BUILDPLATFORM"}, - Args: []string{"ghcr.io/adrianchifor/harpoon:latest", "AS", "fake-third-base"}, - Expected: "FROM --platform=$BUILDPLATFORM ghcr.io/adrianchifor/harpoon:latest AS fake-third-base", - }, - } - - // ImageRefRewriteTestTable is a table-driven test for the ImageRef.RewriteLine method. - // - ImageRefRewriteTestTable = map[string]struct { - Expected string - Digest string - Name string - Flags []string - Args []string - }{ - "syntax=docker/dockerfile:1": { - Name: "#", - Args: []string{"syntax=docker/dockerfile:1"}, - Digest: "docker/dockerfile@sha256:ac85f380a63b13dfcefa89046420e1781752bab202122f8f50032edf31be0021", - Expected: "# syntax=docker/dockerfile@sha256:ac85f380a63b13dfcefa89046420e1781752bab202122f8f50032ed" + - "f31be0021", - }, - "golang:1.21-alpine": { - Name: "FROM", - Flags: []string{"--platform=$BUILDPLATFORM"}, - Args: []string{"golang:1.21-alpine", "AS", "base-builder"}, - Digest: "golang@sha256:0ff68fa7b2177e8d68b4555621c2321c804bcff839fd512c2681de49026573b7", - Expected: "FROM --platform=$BUILDPLATFORM golang@sha256:0ff68fa7b2177e8d68b4555621c2321c804bcff839fd5" + - "12c2681de49026573b7 AS base-builder", - }, - "golang@sha256:0ff68fa7b2177e8d68b4555621c2321c804bcff839fd512c2681de49026573b7": { - Name: "FROM", - Flags: []string{"--platform=$BUILDPLATFORM"}, - Args: []string{ - "golang@sha256:0ff68fa7b2177e8d68b4555621c2321c804bcff839fd512c2681de49026573b7", - "AS", - "fake-second-base", - }, - Digest: "golang@sha256:0ff68fa7b2177e8d68b4555621c2321c804bcff839fd512c2681de49026573b7", - Expected: "FROM --platform=$BUILDPLATFORM golang@sha256:0ff68fa7b2177e8d68b4555621c2321c804bcff839fd512" + - "c2681de49026573b7 AS fake-second-base", - }, - "ghcr.io/adrianchifor/harpoon:latest": { - Name: "FROM", - Flags: []string{"--platform=$BUILDPLATFORM"}, - Args: []string{"ghcr.io/adrianchifor/harpoon:latest", "AS", "fake-third-base"}, - Digest: "ghcr.io/adrianchifor/harpoon@sha256:842dc97a2ce0bd8b1c84ec0de999aab4349c5f5cecb942" + - "3341abd89b8e6903f1", - Expected: "FROM --platform=$BUILDPLATFORM ghcr.io/adrianchifor/harpoon@sha256:842dc97a2ce0bd8b1c84ec0d" + - "e999aab4349c5f5cecb9423341abd89b8e6903f1 AS fake-third-base", - }, - } -) - -func TestImageRefOriginal(t *testing.T) { - for name, tc := range ImageRefTestTable { - t.Run(name, func(t *testing.T) { - actual := ImageRef{ - CommandAST: Command{ - Name: tc.Name, - Flags: tc.Flags, - Args: tc.Args, - }, - } - - if actual.OriginalLine() != tc.Expected { - diff := cmp.Diff(tc.Expected, actual.OriginalLine()) - if diff != "" { - t.Errorf(diff) - } - } - }) - } -} - -func TestImageRefRewrite(t *testing.T) { - for name, tc := range ImageRefRewriteTestTable { - t.Run(name, func(t *testing.T) { - actual := ImageRef{ - ImageDigest: tc.Digest, - CommandAST: Command{ - Name: tc.Name, - Flags: tc.Flags, - Args: tc.Args, - }, - } - - if actual.RewriteLine() != tc.Expected { - diff := cmp.Diff(tc.Expected, actual.OriginalLine()) - if diff != "" { - t.Errorf(diff) - } - } - }) - } -} - -func BenchmarkImageRefOriginal(b *testing.B) { - b.ReportAllocs() - - for name, tc := range ImageRefTestTable { - b.Run(name, func(b *testing.B) { - b.ResetTimer() - - for i := 0; i < b.N; i++ { - actual := ImageRef{ - CommandAST: Command{ - Name: tc.Name, - Flags: tc.Flags, - Args: tc.Args, - }, - } - actual.OriginalLine() - } - }) - } -} - -func BenchmarkImageRefOriginalParallel(b *testing.B) { - b.ReportAllocs() - - for name, tc := range ImageRefTestTable { - b.Run(name, func(b *testing.B) { - b.ResetTimer() - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - actual := ImageRef{ - CommandAST: Command{ - Name: tc.Name, - Flags: tc.Flags, - Args: tc.Args, - }, - } - actual.OriginalLine() - } - }) - }) - } -} diff --git a/pkg/dockerfile-hasher/doc.go b/pkg/hasher/doc.go similarity index 72% rename from pkg/dockerfile-hasher/doc.go rename to pkg/hasher/doc.go index 5c482b5..8bf50b8 100644 --- a/pkg/dockerfile-hasher/doc.go +++ b/pkg/hasher/doc.go @@ -13,10 +13,10 @@ // limitations under the License. /* -Package dockerfile_hasher is a package that provides the ability to read a -Dockerfile from disk, parse it into an Abstract Syntax Tree (AST), and then -rewrite the lines in the Dockerfile with the SHA256 digest of the image. +Package hasher is a package that provides the ability to read a Dockerfile from +disk, parse it into an Abstract Syntax Tree (AST), and then rewrite the lines in +the Dockerfile with the SHA256 digest of the image. Supports logging with the https://github.com/rs/zerolog package. */ -package dockerfile_hasher +package hasher diff --git a/pkg/dockerfile-hasher/dockerfile_hasher.go b/pkg/hasher/hasher.go similarity index 96% rename from pkg/dockerfile-hasher/dockerfile_hasher.go rename to pkg/hasher/hasher.go index 37ccbef..f2f9e98 100644 --- a/pkg/dockerfile-hasher/dockerfile_hasher.go +++ b/pkg/hasher/hasher.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package dockerfile_hasher +package hasher import ( "fmt" @@ -68,7 +68,7 @@ func ReadFile( zlog = logger[0] } - f, err = os.Open(fsPath) + f, err = os.Open(fsPath) // lint:allow_include_file if err != nil { return emptyResult, wlParser.RawDockerfileParser{}, []instructions.Stage{}, errors.Wrap( err, @@ -242,9 +242,9 @@ func WriteFile(lines []string, outputPath string, logger ...zerolog.Logger) (int if outputPath != "" { zlog.Debug().Msgf("Writing updated Dockerfile to %s", outputPath) - fp, err = os.Create(outputPath) + fp, err = os.Create(outputPath) // lint:allow_include_file if err != nil { - return bites, err + return bites, errors.Wrap(err, "failed to open file pointer") } } @@ -253,13 +253,13 @@ func WriteFile(lines []string, outputPath string, logger ...zerolog.Logger) (int bites, err = fmt.Fprintln(fp, line) if err != nil { - return bites, err + return bites, errors.Wrap(err, "failed to write lines to the file pointer") } } err = fp.Close() if err != nil { - return bites, err + return bites, errors.Wrap(err, "failed to close file pointer") } return bites, nil diff --git a/pkg/hasher/hasher_test.go b/pkg/hasher/hasher_test.go new file mode 100644 index 0000000..c3f6230 --- /dev/null +++ b/pkg/hasher/hasher_test.go @@ -0,0 +1,17 @@ +// Copyright 2024, Ryan Parman +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hasher + +// "github.com/google/go-cmp/cmp"