From d35f2ac6f1975c18ba64d2a0bdc2370df7b9d724 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Mon, 25 Mar 2024 15:49:57 -0600 Subject: [PATCH 1/3] Update scorecard github action --- .github/workflows/scorecard.yml | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 156a2b5..e9e0fb6 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -10,7 +10,7 @@ on: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained #schedule: - #- cron: '38 15 * * 5' + #- cron: '28 21 * * 1' push: branches: [ "main" ] @@ -26,40 +26,32 @@ jobs: security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write - # Uncomment the permissions below if installing in a private repository. - # contents: read - # actions: read steps: - name: "Checkout code" - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 # v3.1.0 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: persist-credentials: false - name: "Run analysis" - uses: ossf/scorecard-action@e38b1902ae4f44df626f11ba0734b14fb91f8f86 # v2.1.2 + uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: - # - you want to enable the Branch-Protection check on a *public* repository, or - # - you are installing Scorecard on a *private* repository + # you want to enable the Branch-Protection check on a *public* repository, or # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. # repo_token: ${{ secrets.SCORECARD_TOKEN }} - # Public repositories: - # - Publish results to OpenSSF REST API for easy access by consumers - # - Allows the repository to include the Scorecard badge. - # - See https://github.com/ossf/scorecard-action#publishing-results. - # For private repositories: - # - `publish_results` will always be set to `false`, regardless - # of the value entered here. + # - Publish results to OpenSSF REST API for easy access by consumers + # - Allows the repository to include the Scorecard badge. + # - See https://github.com/ossf/scorecard-action#publishing-results. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 # v3.1.0 + uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1 with: name: SARIF file path: results.sarif @@ -67,6 +59,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@17573ee1cc1b9d061760f3a006fc4aac4f944fd5 # v2.2.4 + uses: github/codeql-action/upload-sarif@8a470fddafa5cbb6266ee11b37ef4d8aae19c571 # v3.24.6 with: sarif_file: results.sarif From 8c52ec87d6b270c2d9855f1ffcec0a9eb3ed49d1 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Mon, 25 Mar 2024 15:57:16 -0600 Subject: [PATCH 2/3] Use upstream sanitization since my PR was merged --- json_tags.go | 43 ------------- json_tags_test.go | 34 ---------- log_to_bug.go | 15 +++-- metadata.go | 161 ---------------------------------------------- metadata_test.go | 134 -------------------------------------- 5 files changed, 11 insertions(+), 376 deletions(-) delete mode 100644 json_tags.go delete mode 100644 json_tags_test.go delete mode 100644 metadata.go delete mode 100644 metadata_test.go diff --git a/json_tags.go b/json_tags.go deleted file mode 100644 index 5a41c68..0000000 --- a/json_tags.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package slogbugsnag - -import ( - "strings" -) - -/* -The code is taken from: -http://golang.org/src/pkg/encoding/json/tags.go -*/ - -// tagOptions is the string following a comma in a struct field's "json" -// tag, or the empty string. It does not include the leading comma. -type tagOptions string - -// parseTag splits a struct field's json tag into its name and -// comma-separated options. -func parseTag(tag string) (string, tagOptions) { - tag, opt, _ := strings.Cut(tag, ",") - return tag, tagOptions(opt) -} - -// Contains reports whether a comma-separated list of options -// contains a particular substr flag. substr must be surrounded by a -// string boundary or commas. -func (o tagOptions) Contains(optionName string) bool { - if len(o) == 0 { - return false - } - s := string(o) - for s != "" { - var name string - name, s, _ = strings.Cut(s, ",") - if name == optionName { - return true - } - } - return false -} diff --git a/json_tags_test.go b/json_tags_test.go deleted file mode 100644 index cac76cf..0000000 --- a/json_tags_test.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2011 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package slogbugsnag - -import ( - "testing" -) - -/* -The code is taken from: -http://golang.org/src/pkg/encoding/json/tags_test.go -*/ - -func TestTagParsing(t *testing.T) { - t.Parallel() - name, opts := parseTag("field,foobar,foo") - if name != "field" { - t.Fatalf("name = %q, want field", name) - } - for _, tt := range []struct { - opt string - want bool - }{ - {"foobar", true}, - {"foo", true}, - {"bar", false}, - } { - if opts.Contains(tt.opt) != tt.want { - t.Errorf("Contains(%q) = %v", tt.opt, !tt.want) - } - } -} diff --git a/log_to_bug.go b/log_to_bug.go index 2830c5b..321ec43 100644 --- a/log_to_bug.go +++ b/log_to_bug.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "runtime" + "strings" "time" "github.com/bugsnag/bugsnag-go/v2" @@ -119,8 +120,6 @@ func (h *Handler) logToBug(ctx context.Context, t time.Time, lvl slog.Level, msg // Attribute values are redacted based on the notifier config ParamsFilters. // accumulateRawData also finds the latest [error] and [bugsnag.User]. func (h *Handler) accumulateRawData(errForBugsnag *error, user *bugsnag.User, md bugsnag.MetaData, tab string, attrs []slog.Attr) { - san := sanitizer{Filters: h.notifiers.notifier.Config.ParamsFilters} - for _, attr := range attrs { if attr.Value.Kind() == slog.KindGroup { h.accumulateRawData(errForBugsnag, user, md, attr.Key, attr.Value.Group()) @@ -157,9 +156,17 @@ func (h *Handler) accumulateRawData(errForBugsnag *error, user *bugsnag.User, md // Always resolve log attribute values attr.Value = attr.Value.Resolve() - val := san.Sanitize(attr.Value.Any()) - md.Add(tab, attr.Key, val) + md.Add(tab, attr.Key, attr.Value.Any()) + } +} + +func shouldRedact(key string, filters []string) bool { + for _, filter := range filters { + if strings.Contains(strings.ToLower(key), strings.ToLower(filter)) { + return true + } } + return false } // bsSeverity converts a [slog.Level] to a [bugsnag.severity] diff --git a/metadata.go b/metadata.go deleted file mode 100644 index 30d071e..0000000 --- a/metadata.go +++ /dev/null @@ -1,161 +0,0 @@ -package slogbugsnag - -import ( - "encoding" - "fmt" - "reflect" - "strings" - "time" -) - -/* -This code is copied from github.com/bugsnag/bugsnag-go because it is private and we need it. -It has been modified to support well known types like error, time, and stringers. -*/ - -// Sanitizer is used to remove filtered params and recursion from meta-data. -type sanitizer struct { - Filters []string - Seen []any -} - -// Sanitize resolves any interface into a value that bugsnag can display, -// as well as removing filtered params and recursion from meta-data. -func (s sanitizer) Sanitize(data any) any { - for _, s := range s.Seen { - // TODO: we don't need deep equal here, just type-ignoring equality - if reflect.DeepEqual(data, s) { - return "[RECURSION]" - } - } - - // Sanitizers are passed by value, so we can modify s and it only affects - // s.Seen for nested calls. - s.Seen = append(s.Seen, data) - - // Handle certain well known interfaces and types - switch data := data.(type) { - case error: - return data.Error() - - case time.Time: - return data.Format(time.RFC3339Nano) - - case fmt.Stringer: - // This also covers time.Duration - return data.String() - - case encoding.TextMarshaler: - if b, err := data.MarshalText(); err == nil { - return string(b) - } - } - - t := reflect.TypeOf(data) - v := reflect.ValueOf(data) - - if t == nil { - return "" - } - - switch t.Kind() { - case reflect.Bool, - reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, - reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, - reflect.Float32, reflect.Float64: - return data - - case reflect.String: - return data - - case reflect.Interface, reflect.Ptr: - if v.IsNil() { - return "" - } - return s.Sanitize(v.Elem().Interface()) - - case reflect.Array, reflect.Slice: - ret := make([]any, v.Len()) - for i := 0; i < v.Len(); i++ { - ret[i] = s.Sanitize(v.Index(i).Interface()) - } - return ret - - case reflect.Map: - return s.sanitizeMap(v) - - case reflect.Struct: - return s.sanitizeStruct(v, t) - - // Things JSON can't serialize: - // case t.Chan, t.Func, reflect.Complex64, reflect.Complex128, reflect.UnsafePointer: - default: - return "[" + t.String() + "]" - } -} - -func (s sanitizer) sanitizeMap(v reflect.Value) any { - ret := make(map[string]any) - - for _, key := range v.MapKeys() { - val := s.Sanitize(v.MapIndex(key).Interface()) - newKey := fmt.Sprintf("%v", key.Interface()) - - if s.shouldRedact(newKey) { - val = "[FILTERED]" - } - - ret[newKey] = val - } - - return ret -} - -func (s sanitizer) sanitizeStruct(v reflect.Value, t reflect.Type) any { - ret := make(map[string]any) - - for i := 0; i < v.NumField(); i++ { - - val := v.Field(i) - // Don't export private fields - if !val.CanInterface() { - continue - } - - name := t.Field(i).Name - var opts tagOptions - - // Parse JSON tags. Supports name and "omitempty" - if jsonTag := t.Field(i).Tag.Get("json"); len(jsonTag) != 0 { - name, opts = parseTag(jsonTag) - } - - if s.shouldRedact(name) { - ret[name] = "[FILTERED]" - } else { - sanitized := s.Sanitize(val.Interface()) - if str, ok := sanitized.(string); ok { - if !(opts.Contains("omitempty") && len(str) == 0) { - ret[name] = str - } - } else { - ret[name] = sanitized - } - } - } - - return ret -} - -func (s sanitizer) shouldRedact(key string) bool { - return shouldRedact(key, s.Filters) -} - -func shouldRedact(key string, filters []string) bool { - for _, filter := range filters { - if strings.Contains(strings.ToLower(key), strings.ToLower(filter)) { - return true - } - } - return false -} diff --git a/metadata_test.go b/metadata_test.go deleted file mode 100644 index 83b0db5..0000000 --- a/metadata_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package slogbugsnag - -import ( - "errors" - "reflect" - "testing" - "time" -) - -type _account struct { - ID string - Name string - Plan struct { - Premium bool - } - Password string - secret string - Email string `json:"email"` - EmptyEmail string `json:"emptyemail,omitempty"` - NotEmptyEmail string `json:"not_empty_email,omitempty"` -} - -type _broken struct { - Me *_broken - Data string -} - -type _textMarshaller struct{} - -func (_textMarshaller) MarshalText() ([]byte, error) { - return []byte("marshalled text"), nil -} - -func TestSanitize(t *testing.T) { - t.Parallel() - var broken = _broken{} - broken.Me = &broken - broken.Data = "ohai" - - account := _account{} - account.Name = "test" - account.ID = "test" - account.secret = "hush" - account.Email = "example@example.com" - account.EmptyEmail = "" - account.NotEmptyEmail = "not_empty_email@example.com" - - data := map[string]map[string]any{ - "one": { - "bool": true, - "int": 7, - "float": 7.1, - "complex": complex(1, 1), - "func": func() {}, - "string": "string", - "password": "secret", - "error": errors.New("some error"), - "time": time.Date(2023, 12, 5, 23, 59, 59, 123456789, time.UTC), - "duration": 105567462 * time.Millisecond, - "text": _textMarshaller{}, - "array": []map[string]any{{ - "creditcard": "1234567812345678", - "broken": broken, - }}, - "broken": broken, - "account": account, - }, - } - - san := sanitizer{Filters: []string{"password", "creditcard"}} - actual := san.Sanitize(data) - - if !reflect.DeepEqual(actual, map[string]any{ - "one": map[string]any{ - "bool": true, - "int": 7, - "float": 7.1, - "complex": "[complex128]", - "string": "string", - "func": "[func()]", - "password": "[FILTERED]", - "error": "some error", - "time": "2023-12-05T23:59:59.123456789Z", - "duration": "29h19m27.462s", - "text": "marshalled text", - "array": []any{map[string]any{ - "creditcard": "[FILTERED]", - "broken": map[string]any{ - "Me": "[RECURSION]", - "Data": "ohai", - }, - }}, - "broken": map[string]any{ - "Me": "[RECURSION]", - "Data": "ohai", - }, - "account": map[string]any{ - "ID": "test", - "Name": "test", - "Plan": map[string]any{ - "Premium": false, - }, - "Password": "[FILTERED]", - "email": "example@example.com", - "not_empty_email": "not_empty_email@example.com", - }, - }, - }) { - t.Errorf("metadata.Sanitize didn't work: %#v", actual) - } -} - -func TestSanitizerSanitize(t *testing.T) { - t.Parallel() - var ( - nilPointer *int - nilInterface = any(nil) - ) - - for n, tc := range []struct { - input any - want any - }{ - {nilPointer, ""}, - {nilInterface, ""}, - } { - s := &sanitizer{} - gotValue := s.Sanitize(tc.input) - - if got, want := gotValue, tc.want; got != want { - t.Errorf("[%d] got %v, want %v", n, got, want) - } - } -} From 88de94642948e8421f71ce355401bb09d66280eb Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Mon, 25 Mar 2024 18:00:10 -0600 Subject: [PATCH 3/3] Update project references --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6a68c13..f1fa573 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,9 @@ Never forget to snag another bug again. ### Other Great SLOG Utilities - [slogctx](https://github.com/veqryn/slog-context): Add attributes to context and have them automatically added to all log lines. Work with a logger stored in context. - [slogotel](https://github.com/veqryn/slog-context/tree/main/otel): Automatically extract and add [OpenTelemetry](https://opentelemetry.io/) TraceID's to all log lines. -- [slogdedup](https://github.com/veqryn/slog-dedup): Middleware that deduplicates and sorts attributes. Particularly useful for JSON logging. +- [slogdedup](https://github.com/veqryn/slog-dedup): Middleware that deduplicates and sorts attributes. Particularly useful for JSON logging. Format logs for aggregators (Graylog, GCP/Stackdriver, etc). - [slogbugsnag](https://github.com/veqryn/slog-bugsnag): Middleware that pipes Errors to [Bugsnag](https://www.bugsnag.com/). +- [slogjson](https://github.com/veqryn/slog-json): Formatter that uses the [JSON v2](https://github.com/golang/go/discussions/63397) [library](https://github.com/go-json-experiment/json), with optional single-line pretty-printing. ## Install `go get github.com/veqryn/slog-bugsnag`