From d5209d5fb745118e045f7d912edea95265e8a0a5 Mon Sep 17 00:00:00 2001 From: alex <53851759+alxxjohn@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:11:09 -0400 Subject: [PATCH 1/3] fix(security): contain slop history writes --- .../checks/quality/quality_ai_history.go | 6 +++++ internal/codeguard/config/io.go | 14 +++++++--- tests/checks/quality_ai_history_test.go | 27 +++++++++++++++++++ .../security/config_containment_paths_test.go | 21 +++++++++++++++ 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/internal/codeguard/checks/quality/quality_ai_history.go b/internal/codeguard/checks/quality/quality_ai_history.go index 130585f..49367fe 100644 --- a/internal/codeguard/checks/quality/quality_ai_history.go +++ b/internal/codeguard/checks/quality/quality_ai_history.go @@ -1,6 +1,7 @@ package quality import ( + "strings" "time" "github.com/devr-tools/codeguard/internal/codeguard/checks/support" @@ -15,6 +16,11 @@ func recordSlopHistory(env support.Context, artifact *core.Artifact) { if artifact == nil || artifact.SlopScore == nil { return } + // Patch validation evaluates repository-controlled content in a temporary + // workspace and must not persist any state to repository-configured paths. + if strings.TrimSpace(env.DiffText) != "" { + return + } cfg := env.Config.Checks.QualityRules.AIChecks if !aiCheckEnabled(cfg.SlopHistory) { return diff --git a/internal/codeguard/config/io.go b/internal/codeguard/config/io.go index 210a868..5952f6b 100644 --- a/internal/codeguard/config/io.go +++ b/internal/codeguard/config/io.go @@ -91,9 +91,17 @@ func containConfigArtifactPaths(cfg *core.Config, baseDir string) error { } // History files are separate write targets and must be checked independently: // validating the cache path does not detect a symlink at a derived filename. - legibilityHistoryPath := cachefile.DerivedPath(cfg.Cache.Path, ".legibility-history") - if _, err := containedPath(baseDir, legibilityHistoryPath); err != nil { - return fmt.Errorf("context_rules.legibility_history: %w", err) + for _, history := range []struct { + label string + suffix string + }{ + {"context_rules.legibility_history", ".legibility-history"}, + {"quality_rules.ai_checks.slop_history", ".slop-history"}, + } { + historyPath := cachefile.DerivedPath(cfg.Cache.Path, history.suffix) + if _, err := containedPath(baseDir, historyPath); err != nil { + return fmt.Errorf("%s: %w", history.label, err) + } } for i := range cfg.ExternalReports { resolved, err := containedPath(baseDir, cfg.ExternalReports[i].Path) diff --git a/tests/checks/quality_ai_history_test.go b/tests/checks/quality_ai_history_test.go index 4eb6cce..33fe4af 100644 --- a/tests/checks/quality_ai_history_test.go +++ b/tests/checks/quality_ai_history_test.go @@ -4,6 +4,7 @@ import ( "context" "os" "path/filepath" + "strings" "testing" "github.com/devr-tools/codeguard/pkg/codeguard" @@ -107,6 +108,32 @@ func TestSlopScoreHistoryHonorsToggle(t *testing.T) { } } +func TestSlopScoreHistoryDoesNotWriteDuringPatchValidation(t *testing.T) { + dir := t.TempDir() + writeSlopFixture(t, dir) + cfg := slopHistoryTestConfig(dir, "quality-ai-history-patch") + diff := strings.Join([]string{ + "diff --git a/service.go b/service.go", + "index 4adc33a..59b9f08 100644", + "--- a/service.go", + "+++ b/service.go", + "@@ -1,5 +1,6 @@", + " package sample", + " ", + " func Run() error {", + "+\t// create error object", + " \terr := doThing()", + " \t_ = err", + }, "\n") + + if _, err := codeguard.RunPatch(context.Background(), cfg, diff); err != nil { + t.Fatalf("run patch: %v", err) + } + if _, err := os.Stat(codeguard.SlopHistoryPath(cfg)); !os.IsNotExist(err) { + t.Fatalf("expected no history file during patch validation, stat err = %v", err) + } +} + func TestSlopScoreHistoryCapsEntries(t *testing.T) { dir := t.TempDir() writeSlopFixture(t, dir) diff --git a/tests/security/config_containment_paths_test.go b/tests/security/config_containment_paths_test.go index 5895ed3..12e076e 100644 --- a/tests/security/config_containment_paths_test.go +++ b/tests/security/config_containment_paths_test.go @@ -100,3 +100,24 @@ func TestLoadConfigRejectsLegibilityHistorySymlinkEscape(t *testing.T) { t.Fatalf("expected a legibility-history symlink containment error, got %v", err) } } + +func TestLoadConfigRejectsSlopHistorySymlinkEscape(t *testing.T) { + dir := t.TempDir() + outside := filepath.Join(t.TempDir(), "victim.json") + cacheDir := filepath.Join(dir, ".codeguard") + if err := os.Mkdir(cacheDir, 0o750); err != nil { + t.Fatalf("create cache directory: %v", err) + } + if err := os.Symlink(outside, filepath.Join(cacheDir, "cache.slop-history.json")); err != nil { + t.Skipf("symlinks unsupported on this platform: %v", err) + } + path := writeConfig(t, dir, ".codeguard/cache.json", "") + + _, err := service.LoadConfigFile(path) + if err == nil { + t.Fatal("expected a derived slop-history symlink escape to be rejected") + } + if !strings.Contains(err.Error(), "slop_history") || !strings.Contains(err.Error(), "symlink") { + t.Fatalf("expected a slop-history symlink containment error, got %v", err) + } +} From 2ac4d2d3bda869b5eab1dff8586f3c00901692b3 Mon Sep 17 00:00:00 2001 From: alex <53851759+alxxjohn@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:28:04 -0400 Subject: [PATCH 2/3] test: import strings for slop history patch fixture --- tests/checks/quality_ai_history_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/checks/quality_ai_history_test.go b/tests/checks/quality_ai_history_test.go index dc03569..bcf71cf 100644 --- a/tests/checks/quality_ai_history_test.go +++ b/tests/checks/quality_ai_history_test.go @@ -1,6 +1,7 @@ package checks_test import ( + "strings" "context" "os" "path/filepath" From 256c01435c85526f879f6c37803a4a6c306e61fd Mon Sep 17 00:00:00 2001 From: alex <53851759+alxxjohn@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:32:00 -0400 Subject: [PATCH 3/3] test: format slop history imports --- tests/checks/quality_ai_history_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/checks/quality_ai_history_test.go b/tests/checks/quality_ai_history_test.go index bcf71cf..33fe4af 100644 --- a/tests/checks/quality_ai_history_test.go +++ b/tests/checks/quality_ai_history_test.go @@ -1,10 +1,10 @@ package checks_test import ( - "strings" "context" "os" "path/filepath" + "strings" "testing" "github.com/devr-tools/codeguard/pkg/codeguard"