diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7eeaec9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + push: + branches: ["main"] + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Run tests with coverage + run: go test ./... -coverprofile=coverage.out + + - name: Upload coverage to Codecov + if: env.CODECOV_TOKEN != '' + uses: codecov/codecov-action@v4 + with: + token: ${{ env.CODECOV_TOKEN }} + files: coverage.out + fail_ci_if_error: true + verbose: true diff --git a/.gitignore b/.gitignore index 4bca4bc..5c3f85a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Binaries -docker-lint +/docker-lint docker-lint.exe *.exe *.exe~ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d6fd46..0b60ce0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ Thank you for your interest in contributing to docker-lint! ### Prerequisites -- Go 1.21 or later +- Go 1.22 or later (matches the `go` directive in `go.mod`) - Git ### Getting Started diff --git a/README.md b/README.md index d4d6cb3..0a6895e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # docker-lint -[![CI](https://img.shields.io/github/actions/workflow/status/devblac/docker-lint/ci.yml?branch=main&logo=github)](https://github.com/devblac/docker-lint/actions/workflows/ci.yml) [![Codecov](https://codecov.io/gh/devblac/docker-lint/graph/badge.svg)](https://codecov.io/gh/devblac/docker-lint) [![Go Report Card](https://goreportcard.com/badge/github.com/devblac/docker-lint)](https://goreportcard.com/report/github.com/devblac/docker-lint) [![Go Reference](https://pkg.go.dev/badge/github.com/devblac/docker-lint.svg)](https://pkg.go.dev/github.com/devblac/docker-lint) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Release](https://img.shields.io/github/v/release/devblac/docker-lint?display_name=release&sort=semver)](https://github.com/devblac/docker-lint/releases) +[![CI](https://img.shields.io/github/actions/workflow/status/devblac/docker-lint/ci.yml?branch=main&logo=github)](https://github.com/devblac/docker-lint/actions/workflows/ci.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/devblac/docker-lint)](https://goreportcard.com/report/github.com/devblac/docker-lint) [![Go Reference](https://pkg.go.dev/badge/github.com/devblac/docker-lint.svg)](https://pkg.go.dev/github.com/devblac/docker-lint) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Release](https://img.shields.io/github/v/release/devblac/docker-lint?display_name=release&sort=semver)](https://github.com/devblac/docker-lint/releases) A minimal, production-grade CLI tool for statically analyzing Dockerfiles to detect common inefficiencies, anti-patterns, and security issues. @@ -18,21 +18,23 @@ A minimal, production-grade CLI tool for statically analyzing Dockerfiles to det ### From Source +Requires Go 1.22 or later. + ```bash -go install github.com/docker-lint/docker-lint/cmd/docker-lint@latest +go install github.com/devblac/docker-lint/cmd/docker-lint@latest ``` ### Build from Repository ```bash -git clone https://github.com/docker-lint/docker-lint.git +git clone https://github.com/devblac/docker-lint.git cd docker-lint go build -o docker-lint ./cmd/docker-lint ``` ### Binary Download -Download the latest release from the [Releases](https://github.com/docker-lint/docker-lint/releases) page. +Download the latest release from the [Releases](https://github.com/devblac/docker-lint/releases) page. ## Usage @@ -179,6 +181,8 @@ Machine-readable JSON output for CI/CD integration: ## CI/CD Integration +The repository's CI workflow runs `go test ./... -cover`. Coverage uploads to Codecov are attempted only when a `CODECOV_TOKEN` secret is configured; otherwise the upload step is skipped while tests still gate the build. + ### GitHub Actions ```yaml @@ -218,7 +222,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, testing guidelines ```bash # Clone the repository -git clone https://github.com/docker-lint/docker-lint.git +git clone https://github.com/devblac/docker-lint.git cd docker-lint # Install dependencies diff --git a/cmd/docker-lint/main.go b/cmd/docker-lint/main.go new file mode 100644 index 0000000..36cfd0a --- /dev/null +++ b/cmd/docker-lint/main.go @@ -0,0 +1,143 @@ +package main + +import ( + "flag" + "fmt" + "io" + "os" + "strings" + + "github.com/devblac/docker-lint/internal/analyzer" + "github.com/devblac/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/formatter" + "github.com/devblac/docker-lint/internal/parser" + "github.com/devblac/docker-lint/internal/rules" +) + +// version is set at build time using -ldflags. Defaults to "dev" when not set. +var version = "dev" + +func main() { + var ( + jsonOutput bool + quiet bool + strict bool + versionFlg bool + rulesFlag bool + ignoreCSV string + ) + + flag.BoolVar(&jsonOutput, "json", false, "Output findings as JSON") + flag.BoolVar(&jsonOutput, "j", false, "Output findings as JSON") + + flag.BoolVar(&quiet, "quiet", false, "Suppress informational messages (show only warnings and errors)") + flag.BoolVar(&quiet, "q", false, "Suppress informational messages (show only warnings and errors)") + + flag.BoolVar(&strict, "strict", false, "Treat warnings as errors") + flag.BoolVar(&strict, "s", false, "Treat warnings as errors") + + flag.BoolVar(&versionFlg, "version", false, "Show version information") + flag.BoolVar(&versionFlg, "v", false, "Show version information") + + flag.BoolVar(&rulesFlag, "rules", false, "List all available rules with descriptions") + + flag.StringVar(&ignoreCSV, "ignore", "", "Comma-separated list of rule IDs to ignore") + + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage: %s [flags] [file]\n", os.Args[0]) + flag.PrintDefaults() + } + + flag.Parse() + + if versionFlg { + fmt.Println(version) + return + } + + if rulesFlag { + listRules() + return + } + + args := flag.Args() + if len(args) > 1 { + fmt.Fprintln(os.Stderr, "too many arguments: only one Dockerfile path is supported") + os.Exit(2) + } + + filename := "stdin" + var reader io.Reader = os.Stdin + + if len(args) == 1 { + filename = args[0] + file, err := os.Open(filename) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open file: %v\n", err) + os.Exit(2) + } + defer file.Close() + reader = file + } + + dockerfile, err := parser.ParseReader(reader) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to parse Dockerfile: %v\n", err) + os.Exit(2) + } + + ignoreRules := parseIgnoreList(ignoreCSV) + + anlzr := analyzer.NewWithDefaults(analyzer.Config{IgnoreRules: ignoreRules}) + findings := anlzr.Analyze(dockerfile) + + var errorsCount, warningsCount int + for _, finding := range findings { + switch finding.Severity { + case ast.SeverityError: + errorsCount++ + case ast.SeverityWarning: + warningsCount++ + } + } + + if jsonOutput { + jsonFormatter := formatter.NewJSONFormatter(filename, quiet) + if err := jsonFormatter.Format(findings, os.Stdout); err != nil { + fmt.Fprintf(os.Stderr, "failed to format JSON output: %v\n", err) + os.Exit(2) + } + } else { + textFormatter := formatter.NewTextFormatter(filename, quiet) + if err := textFormatter.Format(findings, os.Stdout); err != nil { + fmt.Fprintf(os.Stderr, "failed to format text output: %v\n", err) + os.Exit(2) + } + } + + if errorsCount > 0 || (strict && warningsCount > 0) { + os.Exit(1) + } +} + +func parseIgnoreList(csv string) []string { + if csv == "" { + return nil + } + + parts := strings.Split(csv, ",") + result := make([]string, 0, len(parts)) + for _, part := range parts { + trimmed := strings.TrimSpace(part) + if trimmed != "" { + result = append(result, trimmed) + } + } + return result +} + +func listRules() { + for _, rule := range rules.DefaultRegistry.All() { + fmt.Printf("%s\t[%s]\t%s - %s\n", rule.ID(), rule.Severity().String(), rule.Name(), rule.Description()) + } +} diff --git a/go.mod b/go.mod index 3f88f0e..3701039 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/docker-lint/docker-lint +module github.com/devblac/docker-lint go 1.22.0 diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 17ad674..370b702 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -4,8 +4,8 @@ package analyzer import ( "sort" - "github.com/docker-lint/docker-lint/internal/ast" - "github.com/docker-lint/docker-lint/internal/rules" + "github.com/devblac/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/rules" ) // Config holds configuration options for the analyzer. diff --git a/internal/analyzer/analyzer_property_test.go b/internal/analyzer/analyzer_property_test.go index d9a9685..a43d542 100644 --- a/internal/analyzer/analyzer_property_test.go +++ b/internal/analyzer/analyzer_property_test.go @@ -5,8 +5,8 @@ import ( "reflect" "testing" - "github.com/docker-lint/docker-lint/internal/ast" - "github.com/docker-lint/docker-lint/internal/rules" + "github.com/devblac/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/rules" "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen" "github.com/leanovate/gopter/prop" diff --git a/internal/analyzer/analyzer_test.go b/internal/analyzer/analyzer_test.go index 4b2474b..b184a63 100644 --- a/internal/analyzer/analyzer_test.go +++ b/internal/analyzer/analyzer_test.go @@ -4,9 +4,9 @@ import ( "strings" "testing" - "github.com/docker-lint/docker-lint/internal/ast" - "github.com/docker-lint/docker-lint/internal/parser" - "github.com/docker-lint/docker-lint/internal/rules" + "github.com/devblac/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/parser" + "github.com/devblac/docker-lint/internal/rules" ) func TestAnalyzer_Analyze_BasicFindings(t *testing.T) { diff --git a/internal/formatter/formatter_property_test.go b/internal/formatter/formatter_property_test.go index 2fce1bc..947dab1 100644 --- a/internal/formatter/formatter_property_test.go +++ b/internal/formatter/formatter_property_test.go @@ -6,7 +6,7 @@ import ( "encoding/json" "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen" "github.com/leanovate/gopter/prop" diff --git a/internal/formatter/formatter_test.go b/internal/formatter/formatter_test.go index 83ada77..f6b77a9 100644 --- a/internal/formatter/formatter_test.go +++ b/internal/formatter/formatter_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) func TestTextFormatter_Format(t *testing.T) { diff --git a/internal/formatter/json.go b/internal/formatter/json.go index 387727d..481f760 100644 --- a/internal/formatter/json.go +++ b/internal/formatter/json.go @@ -4,7 +4,7 @@ import ( "encoding/json" "io" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) // JSONFinding represents a single finding in JSON output format. diff --git a/internal/formatter/text.go b/internal/formatter/text.go index 932b035..90928ad 100644 --- a/internal/formatter/text.go +++ b/internal/formatter/text.go @@ -5,7 +5,7 @@ import ( "fmt" "io" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) // TextFormatter formats findings as human-readable text. diff --git a/internal/parser/formatter.go b/internal/parser/formatter.go index 824d414..904eea6 100644 --- a/internal/parser/formatter.go +++ b/internal/parser/formatter.go @@ -7,7 +7,7 @@ import ( "sort" "strings" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) // Format converts a Dockerfile AST back to text representation. diff --git a/internal/parser/formatter_test.go b/internal/parser/formatter_test.go index acb3a54..da56c7a 100644 --- a/internal/parser/formatter_test.go +++ b/internal/parser/formatter_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) func TestFormatNilDockerfile(t *testing.T) { diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 6cfb709..a38903d 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -8,7 +8,7 @@ import ( "regexp" "strings" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) // ParseError represents a parsing error with location information. diff --git a/internal/parser/parser_property_test.go b/internal/parser/parser_property_test.go index 913b8e9..db4b9eb 100644 --- a/internal/parser/parser_property_test.go +++ b/internal/parser/parser_property_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen" "github.com/leanovate/gopter/prop" diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 3662268..c347670 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) // TestParseAllInstructionTypes tests parsing of valid Dockerfiles with all instruction types. diff --git a/internal/rules/base_image.go b/internal/rules/base_image.go index 6d54707..f0de486 100644 --- a/internal/rules/base_image.go +++ b/internal/rules/base_image.go @@ -4,7 +4,7 @@ package rules import ( "strings" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) // largeBaseImages is a set of known large base images that have smaller alternatives. diff --git a/internal/rules/base_image_property_test.go b/internal/rules/base_image_property_test.go index 92895c9..28cf4f4 100644 --- a/internal/rules/base_image_property_test.go +++ b/internal/rules/base_image_property_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen" "github.com/leanovate/gopter/prop" diff --git a/internal/rules/base_image_test.go b/internal/rules/base_image_test.go index 99551c7..867e57f 100644 --- a/internal/rules/base_image_test.go +++ b/internal/rules/base_image_test.go @@ -3,7 +3,7 @@ package rules import ( "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) func TestBaseImageRulesRegistered(t *testing.T) { diff --git a/internal/rules/bestpractice.go b/internal/rules/bestpractice.go index db45f98..9b6e271 100644 --- a/internal/rules/bestpractice.go +++ b/internal/rules/bestpractice.go @@ -5,7 +5,7 @@ import ( "path/filepath" "strings" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) // wildcardChars contains characters that indicate wildcard patterns diff --git a/internal/rules/bestpractice_test.go b/internal/rules/bestpractice_test.go index e47c04f..d498a8a 100644 --- a/internal/rules/bestpractice_test.go +++ b/internal/rules/bestpractice_test.go @@ -3,7 +3,7 @@ package rules import ( "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) func TestBestPracticeRulesRegistered(t *testing.T) { diff --git a/internal/rules/layer.go b/internal/rules/layer.go index 8c6478d..de3af96 100644 --- a/internal/rules/layer.go +++ b/internal/rules/layer.go @@ -5,7 +5,7 @@ import ( "regexp" "strings" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) // Package manager patterns for cache cleanup detection diff --git a/internal/rules/layer_property_test.go b/internal/rules/layer_property_test.go index 545dc6a..d04b497 100644 --- a/internal/rules/layer_property_test.go +++ b/internal/rules/layer_property_test.go @@ -5,7 +5,7 @@ import ( "reflect" "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen" "github.com/leanovate/gopter/prop" diff --git a/internal/rules/layer_test.go b/internal/rules/layer_test.go index 36c7906..45d98c0 100644 --- a/internal/rules/layer_test.go +++ b/internal/rules/layer_test.go @@ -3,7 +3,7 @@ package rules import ( "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) func TestLayerRulesRegistered(t *testing.T) { diff --git a/internal/rules/registry.go b/internal/rules/registry.go index c25b2e9..ff1bdbe 100644 --- a/internal/rules/registry.go +++ b/internal/rules/registry.go @@ -5,7 +5,7 @@ import ( "sort" "sync" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) // Rule IDs for base image rules (DL3xxx) diff --git a/internal/rules/security.go b/internal/rules/security.go index 311a877..bca2563 100644 --- a/internal/rules/security.go +++ b/internal/rules/security.go @@ -5,7 +5,7 @@ import ( "regexp" "strings" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) // secretPatterns contains regex patterns for detecting potential secrets in ENV/ARG keys. diff --git a/internal/rules/security_property_test.go b/internal/rules/security_property_test.go index af557d9..25e3585 100644 --- a/internal/rules/security_property_test.go +++ b/internal/rules/security_property_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" "github.com/leanovate/gopter" "github.com/leanovate/gopter/gen" "github.com/leanovate/gopter/prop" diff --git a/internal/rules/security_test.go b/internal/rules/security_test.go index 32796fd..484980a 100644 --- a/internal/rules/security_test.go +++ b/internal/rules/security_test.go @@ -3,7 +3,7 @@ package rules import ( "testing" - "github.com/docker-lint/docker-lint/internal/ast" + "github.com/devblac/docker-lint/internal/ast" ) func TestSecurityRulesRegistered(t *testing.T) {