diff --git a/.github/workflows/benchmark_cmd_app.yml b/.github/workflows/benchmark_cmd_app.yml index c9c3b73..d58af1c 100644 --- a/.github/workflows/benchmark_cmd_app.yml +++ b/.github/workflows/benchmark_cmd_app.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version-file: 'go.mod' - name: Download dependencies run: go mod download diff --git a/.github/workflows/build_cmd_app.yml b/.github/workflows/build_cmd_app.yml index d7e5768..2a5e99a 100644 --- a/.github/workflows/build_cmd_app.yml +++ b/.github/workflows/build_cmd_app.yml @@ -8,6 +8,8 @@ jobs: build: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read steps: - uses: actions/checkout@v5 @@ -15,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version-file: 'go.mod' - name: Download dependencies run: go mod download diff --git a/.github/workflows/scan_cmd_app.yml b/.github/workflows/scan_cmd_app.yml new file mode 100644 index 0000000..8859679 --- /dev/null +++ b/.github/workflows/scan_cmd_app.yml @@ -0,0 +1,39 @@ +name: Security Scan +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + security: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Checkout Source + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Run Gosec Security Scanner + uses: securego/gosec@master + with: + args: '-fmt sarif -out gosec.sarif ./...' + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: gosec.sarif + + - name: Run govulncheck + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + govulncheck ./... diff --git a/.github/workflows/test_cmd_app.yml b/.github/workflows/test_cmd_app.yml index e21ee9f..4e5f9a3 100644 --- a/.github/workflows/test_cmd_app.yml +++ b/.github/workflows/test_cmd_app.yml @@ -8,6 +8,8 @@ jobs: test: runs-on: ubuntu-latest timeout-minutes: 5 + permissions: + contents: read steps: - uses: actions/checkout@v5 @@ -15,7 +17,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.25" + go-version-file: 'go.mod' - name: Download dependencies run: go mod download diff --git a/cmd/app/handlers.go b/cmd/app/handlers.go index c8d5c11..c6f985f 100644 --- a/cmd/app/handlers.go +++ b/cmd/app/handlers.go @@ -168,7 +168,7 @@ func (app *app) postSignIn(w http.ResponseWriter, r *http.Request) { // Prevent timing attacks - use pre-computed bcrypt hash dummyUser := &users.User{} dummyUser.Password = users.Password{Hash: []byte(users.PreComputedHash)} - dummyUser.Password.Matches(form.Password) + _, _ = dummyUser.Password.Matches(form.Password) // Intentionally ignored to prevent timing attacks match = false app.logger.WarnContext(r.Context(), "error getting user by email", slog.String("msg", err.Error())) diff --git a/cmd/app/helpers.go b/cmd/app/helpers.go index 54ae6cf..f795025 100644 --- a/cmd/app/helpers.go +++ b/cmd/app/helpers.go @@ -37,7 +37,9 @@ func (app *app) render(w http.ResponseWriter, r *http.Request, status int, templ w.WriteHeader(status) - buf.WriteTo(w) + if _, err := buf.WriteTo(w); err != nil { + app.logger.ErrorContext(r.Context(), "failed to write response", slog.String("msg", err.Error())) + } } func (app *app) renderError(w http.ResponseWriter, r *http.Request, err error, userMessage string) { @@ -144,7 +146,9 @@ func (h *TraceHandler) Handle(ctx context.Context, r slog.Record) error { func generateTraceID() string { bytes := make([]byte, 4) - rand.Read(bytes) + if _, err := rand.Read(bytes); err != nil { + panic(err) // crypto/rand should never fail + } return hex.EncodeToString(bytes) } diff --git a/cmd/app/main.go b/cmd/app/main.go index 26aa63f..f194ee6 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -82,13 +82,7 @@ func main() { logger.Info("database version", slog.Int64("version", *dbVersion)) - htmxFuncs := template.FuncMap{ - "preload": func(event string) template.HTMLAttr { - return template.HTMLAttr(fmt.Sprintf(`preload="%s"`, event)) - }, - } - - templateCache, err := newTemplateCache(WithFunctions(htmxFuncs)) + templateCache, err := newTemplateCache() if err != nil { logger.Error("error creating template cache", slog.String("msg", err.Error())) os.Exit(1) diff --git a/cmd/app/testutils_test.go b/cmd/app/testutils_test.go index 7e5f342..1d44066 100644 --- a/cmd/app/testutils_test.go +++ b/cmd/app/testutils_test.go @@ -2,8 +2,6 @@ package main import ( "bytes" - "fmt" - "html/template" "io" "log/slog" "net/http" @@ -36,12 +34,7 @@ func newTestApplication(tb testing.TB) *app { tb.Fatal(err) } - // Create an instance of the template cache with HTMX functions. - htmxFuncs := template.FuncMap{ - "get": func(url string) template.HTMLAttr { return template.HTMLAttr(fmt.Sprintf(`hx-get="%s"`, url)) }, - "preload": func(event string) template.HTMLAttr { return template.HTMLAttr(fmt.Sprintf(`preload="%s"`, event)) }, - } - templateCache, err := newTemplateCache(WithFunctions(htmxFuncs)) + templateCache, err := newTemplateCache() if err != nil { tb.Fatal(err) } diff --git a/go.mod b/go.mod index 9c5f155..592a280 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/bit8bytes/goalkeepr -go 1.25.0 +go 1.25.3 require ( github.com/alexedwards/scs/v2 v2.9.0 diff --git a/internal/data/sqlite.go b/internal/data/sqlite.go index c2b1c52..a1e9253 100644 --- a/internal/data/sqlite.go +++ b/internal/data/sqlite.go @@ -24,7 +24,7 @@ func (p *SQLiteProvider) Open(path string) (*sql.DB, error) { defer cancel() err = db.PingContext(ctx) if err != nil { - db.Close() + _ = db.Close() // Ignore close error, ping error takes precedence return nil, err } diff --git a/ui/html/app/goals.html b/ui/html/app/goals.html index 0a270ed..7b3715e 100644 --- a/ui/html/app/goals.html +++ b/ui/html/app/goals.html @@ -23,7 +23,7 @@

{{ if eq (mod $index 2) 0 }} {{ $goal.Due.Format "2006" }}: @@ -67,7 +67,7 @@

{{ $goal.Due.Format "2006" }}: {{ $goal.Goal }} @@ -85,7 +85,7 @@