From 8e95804bbabcb3e374b36371c521480e9da96e0e Mon Sep 17 00:00:00 2001 From: alex <53851759+alxxjohn@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:54:21 -0400 Subject: [PATCH 1/3] fix(quality): cap git provenance output --- .../checks/quality/quality_ai_resolution.go | 33 +++++++++++++-- .../quality/quality_ai_resolution_test.go | 42 +++++++++++++++++++ 2 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 internal/codeguard/checks/quality/quality_ai_resolution_test.go diff --git a/internal/codeguard/checks/quality/quality_ai_resolution.go b/internal/codeguard/checks/quality/quality_ai_resolution.go index e016ea0..fde8de7 100644 --- a/internal/codeguard/checks/quality/quality_ai_resolution.go +++ b/internal/codeguard/checks/quality/quality_ai_resolution.go @@ -1,8 +1,10 @@ package quality import ( + "bytes" "context" "encoding/json" + "errors" "os" "os/exec" "path/filepath" @@ -11,6 +13,10 @@ import ( "time" ) +const maxGitHeadMessageBytes = 1 << 20 + +var errGitHeadMessageTooLarge = errors.New("git HEAD message exceeds size limit") + type packageManifest struct { Name string `json:"name"` Dependencies map[string]string `json:"dependencies"` @@ -59,11 +65,32 @@ func readGitHeadMessage(dir string) string { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() cmd := exec.CommandContext(ctx, "git", "-C", dir, "log", "-1", "--format=%B") //nolint:gosec // fixed git subcommand; dir is a config-supplied scan target path - out, err := cmd.Output() - if err != nil { + out := limitedBuffer{limit: maxGitHeadMessageBytes} + cmd.Stdout = &out + if err := cmd.Run(); err != nil { return "" } - return string(out) + return out.String() +} + +// limitedBuffer prevents os/exec from buffering attacker-controlled command +// output without bound. Returning an error also stops the stdout copy once the +// limit is reached. +type limitedBuffer struct { + bytes.Buffer + limit int +} + +func (b *limitedBuffer) Write(p []byte) (int, error) { + remaining := b.limit - b.Len() + if remaining <= 0 { + return 0, errGitHeadMessageTooLarge + } + if len(p) > remaining { + n, _ := b.Buffer.Write(p[:remaining]) + return n, errGitHeadMessageTooLarge + } + return b.Buffer.Write(p) } func envFlagEnabled(keys []string) bool { diff --git a/internal/codeguard/checks/quality/quality_ai_resolution_test.go b/internal/codeguard/checks/quality/quality_ai_resolution_test.go new file mode 100644 index 0000000..792e5dc --- /dev/null +++ b/internal/codeguard/checks/quality/quality_ai_resolution_test.go @@ -0,0 +1,42 @@ +package quality + +import ( + "errors" + "testing" +) + +func TestLimitedBufferRejectsOutputPastLimit(t *testing.T) { + t.Parallel() + + var output limitedBuffer + output.limit = 4 + + n, err := output.Write([]byte("abcdef")) + if n != output.limit { + t.Fatalf("Write() wrote %d bytes, want %d", n, output.limit) + } + if !errors.Is(err, errGitHeadMessageTooLarge) { + t.Fatalf("Write() error = %v, want %v", err, errGitHeadMessageTooLarge) + } + if got := output.String(); got != "abcd" { + t.Fatalf("buffer contents = %q, want %q", got, "abcd") + } + + n, err = output.Write([]byte("more")) + if n != 0 || !errors.Is(err, errGitHeadMessageTooLarge) { + t.Fatalf("second Write() = (%d, %v), want (0, %v)", n, err, errGitHeadMessageTooLarge) + } +} + +func TestLimitedBufferAcceptsOutputWithinLimit(t *testing.T) { + t.Parallel() + + output := limitedBuffer{limit: 4} + n, err := output.Write([]byte("abcd")) + if err != nil || n != 4 { + t.Fatalf("Write() = (%d, %v), want (4, nil)", n, err) + } + if got := output.String(); got != "abcd" { + t.Fatalf("buffer contents = %q, want %q", got, "abcd") + } +} From e2d2722908d44f7f59f8cdab4d6dc68c7a617875 Mon Sep 17 00:00:00 2001 From: alex <53851759+alxxjohn@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:31:01 -0400 Subject: [PATCH 2/3] ci: pin CodeGuard action to immutable SHA --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74c423f..5e8df59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,7 +85,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 # make codeguard-ci: CI intentionally runs the stable Marketplace action instead of the in-repo binary. - - uses: devr-tools/codeguard@v1.2.0 + - uses: devr-tools/codeguard@a1f8eb3aed6b6b645d42be2a8279b3f578328c40 with: config: .codeguard/codeguard.yaml From 36e2e43ac80253e71a72f816043890bd04183d87 Mon Sep 17 00:00:00 2001 From: alex <53851759+alxxjohn@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:31:09 -0400 Subject: [PATCH 3/3] ci: pin SLSA generator to immutable SHA --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9d4b197..704ac54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -232,7 +232,7 @@ jobs: # the SLSA generator verifies its own provenance against the tagged ref, and # a SHA pin breaks that check. This is the one intentional exception to the # SHA-pinning policy. - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a with: base64-subjects: ${{ needs.build-release.outputs.hashes }} upload-assets: true