From 95c5a59d0e4966491e0fc092b7b56908a514fb83 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 13:04:58 +0900 Subject: [PATCH 01/38] chore: stage security hardening patch --- scripts/ci/agent_security_patch.py | 805 +++++++++++++++++++++++++++++ 1 file changed, 805 insertions(+) create mode 100644 scripts/ci/agent_security_patch.py diff --git a/scripts/ci/agent_security_patch.py b/scripts/ci/agent_security_patch.py new file mode 100644 index 00000000..b659a12f --- /dev/null +++ b/scripts/ci/agent_security_patch.py @@ -0,0 +1,805 @@ +from __future__ import annotations + +from pathlib import Path + +ROOT = Path.cwd() + + +def replace_once(path: str, old: str, new: str) -> None: + target = ROOT / path + text = target.read_text(encoding="utf-8") + count = text.count(old) + if count != 1: + raise RuntimeError(f"{path}: expected one replacement target, found {count}") + target.write_text(text.replace(old, new, 1), encoding="utf-8") + + +def write(path: str, content: str) -> None: + target = ROOT / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(content, encoding="utf-8") + + +replace_once( + "pkg/promptguard/rulepack_match.go", + """\tprefilterText := text +\tif r.View == viewRaw { +\t\tprefilterText = segment.Views.Norm +\t} +\tif !containsAllLiteralGroups(prefilterText, r.RequiredLiteralGroups) { +\t\treturn \"\", 0, false +\t} +""", + """\t// Required literals are compiled in the matcher's own representation. A raw +\t// rule can intentionally contain compatibility characters, provider syntax, +\t// or other bytes that normalize to a different surface. Prefiltering those +\t// rules against the normalized view can therefore create false negatives. +\t// Keep the optimization for normalized/joined views, but always execute raw +\t// matchers against the raw surface. +\tif r.View != viewRaw && !containsAllLiteralGroups(text, r.RequiredLiteralGroups) { +\t\treturn \"\", 0, false +\t} +""", +) + +write( + "pkg/promptguard/rulepack_raw_security_test.go", + r'''package promptguard + +import "testing" + +func TestRawRulesDoNotUseNormalizedLiteralPrefilter(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + rule rawRule + }{ + { + name: "regex compatibility characters", + rule: rawRule{ + ID: "raw-fullwidth-regex", + Family: "raw-fullwidth-regex", + Type: ruleTypeRegex, + Action: hitActionScore, + View: viewRaw, + Segments: []string{string(segmentPlain)}, + Pattern: `system`, + Weight: 1, + MaxOccurrences: 1, + }, + }, + { + name: "phrase compatibility characters", + rule: rawRule{ + ID: "raw-fullwidth-phrase", + Family: "raw-fullwidth-phrase", + Type: ruleTypePhrase, + Action: hitActionScore, + View: viewRaw, + Segments: []string{string(segmentPlain)}, + Phrases: []string{"system"}, + MatchMode: phraseMatchSubstring, + Weight: 1, + MaxOccurrences: 1, + }, + }, + } + + segment := textSegment{Kind: segmentPlain, Views: normalizeViews("system")} + if segment.Views.Raw == segment.Views.Norm { + t.Fatal("test fixture must normalize to a different representation") + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + compiled, err := compileRule(&tc.rule) + if err != nil { + t.Fatalf("compileRule() error = %v", err) + } + if matches := compiled.matchSegment(segment, compilePolicy(&rawRulepack{Version: 3}), 1); len(matches) != 1 { + t.Fatalf("matchSegment() matches = %d, want 1", len(matches)) + } + }) + } +} +''', +) + +write( + "pkg/obsmetrics/vec.go", + r'''package obsmetrics + +import ( + "fmt" + "io" + "math" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" +) + +// Labels는 Prometheus label name/value 집합입니다. 렌더링 시 label name 기준으로 정렬됩니다. +type Labels map[string]string + +type labelPair struct { + name string + value string +} + +type seriesEntry[T any] struct { + key string + labels []labelPair + value T +} + +const ( + // DefaultMaxMetricSeries bounds the process-lifetime cardinality retained by + // every vector created through the compatibility constructors. + DefaultMaxMetricSeries = 1024 + // DefaultMaxMetricLabels bounds the number of labels in one series. + DefaultMaxMetricLabels = 16 + // DefaultMaxMetricLabelNameBytes and DefaultMaxMetricLabelValueBytes bound + // allocations performed while canonicalizing attacker-influenced labels. + DefaultMaxMetricLabelNameBytes = 128 + DefaultMaxMetricLabelValueBytes = 256 +) + +// VecOptions configures hard resource limits for a metric vector. Non-positive +// values select the safe defaults; there is intentionally no unbounded mode. +type VecOptions struct { + MaxSeries int + MaxLabels int + MaxLabelNameBytes int + MaxLabelValueBytes int +} + +type seriesLimiter struct { + maxSeries int64 + maxLabels int + maxLabelNameBytes int + maxLabelValueBytes int + count atomic.Int64 + dropped atomic.Uint64 +} + +func newSeriesLimiter(options VecOptions) seriesLimiter { + if options.MaxSeries <= 0 { + options.MaxSeries = DefaultMaxMetricSeries + } + if options.MaxLabels <= 0 { + options.MaxLabels = DefaultMaxMetricLabels + } + if options.MaxLabelNameBytes <= 0 { + options.MaxLabelNameBytes = DefaultMaxMetricLabelNameBytes + } + if options.MaxLabelValueBytes <= 0 { + options.MaxLabelValueBytes = DefaultMaxMetricLabelValueBytes + } + + return seriesLimiter{ + maxSeries: int64(options.MaxSeries), + maxLabels: options.MaxLabels, + maxLabelNameBytes: options.MaxLabelNameBytes, + maxLabelValueBytes: options.MaxLabelValueBytes, + } +} + +func (l *seriesLimiter) canonicalize(labels Labels) (string, []labelPair, bool) { + if len(labels) > l.maxLabels { + l.dropped.Add(1) + return "", nil, false + } + for name, value := range labels { + if len(name) > l.maxLabelNameBytes || len(value) > l.maxLabelValueBytes { + l.dropped.Add(1) + return "", nil, false + } + } + + key, pairs := canonicalLabels(labels) + return key, pairs, true +} + +func (l *seriesLimiter) reserve() bool { + for { + current := l.count.Load() + if current >= l.maxSeries { + l.dropped.Add(1) + return false + } + if l.count.CompareAndSwap(current, current+1) { + return true + } + } +} + +func (l *seriesLimiter) release() { + l.count.Add(-1) +} + +func (l *seriesLimiter) seriesCount() int { + return int(l.count.Load()) +} + +func (l *seriesLimiter) droppedSeries() uint64 { + return l.dropped.Load() +} + +func loadOrCreateSeries[T any]( + series *sync.Map, + limiter *seriesLimiter, + key string, + labels []labelPair, + create func() T, +) (T, bool) { + var zero T + if actual, ok := series.Load(key); ok { + stored, typed := actual.(*seriesEntry[T]) + if !typed { + return zero, false + } + return stored.value, true + } + if !limiter.reserve() { + return zero, false + } + + entry := &seriesEntry[T]{key: key, labels: labels, value: create()} + actual, loaded := series.LoadOrStore(key, entry) + if loaded { + limiter.release() + } + stored, ok := actual.(*seriesEntry[T]) + if !ok { + if !loaded { + series.Delete(key) + limiter.release() + } + return zero, false + } + return stored.value, true +} + +// CounterVec는 동일 metric name/help를 공유하는 label-set별 counter 집합입니다. +type CounterVec struct { + name string + help string + series sync.Map // map[string]*seriesEntry[*Counter] + limiter seriesLimiter +} + +// Counter는 원자적으로 증가하는 단일 counter series입니다. +type Counter struct { + value atomic.Uint64 +} + +func NewCounterVec(name, help string) *CounterVec { + return NewCounterVecWithOptions(name, help, VecOptions{}) +} + +func NewCounterVecWithOptions(name, help string, options VecOptions) *CounterVec { + return &CounterVec{name: name, help: help, limiter: newSeriesLimiter(options)} +} + +func (v *CounterVec) With(labels Labels) *Counter { + if v == nil { + return nil + } + key, labelPairs, ok := v.limiter.canonicalize(labels) + if !ok { + return nil + } + counter, ok := loadOrCreateSeries(&v.series, &v.limiter, key, labelPairs, func() *Counter { return &Counter{} }) + if !ok { + return nil + } + return counter +} + +func (v *CounterVec) Inc(labels Labels) { + v.Add(labels, 1) +} + +func (v *CounterVec) Add(labels Labels, delta uint64) { + counter := v.With(labels) + if counter != nil { + counter.Add(delta) + } +} + +func (v *CounterVec) SeriesCount() int { + if v == nil { + return 0 + } + return v.limiter.seriesCount() +} + +func (v *CounterVec) DroppedSeries() uint64 { + if v == nil { + return 0 + } + return v.limiter.droppedSeries() +} + +func (v *CounterVec) WriteExposition(w io.Writer) bool { + if v == nil { + return true + } + if !writeMetricHeader(w, v.name, v.help, "counter") { + return false + } + for _, entry := range collectSeries[*Counter](&v.series) { + if !writeMetricSample(w, v.name, entry.labels, strconv.FormatUint(entry.value.Value(), 10)) { + return false + } + } + return true +} + +func (c *Counter) Inc() { + c.Add(1) +} + +func (c *Counter) Add(delta uint64) { + if c != nil { + c.value.Add(delta) + } +} + +func (c *Counter) Value() uint64 { + if c == nil { + return 0 + } + return c.value.Load() +} + +// GaugeVec는 동일 metric name/help를 공유하는 label-set별 float64 gauge 집합입니다. +type GaugeVec struct { + name string + help string + series sync.Map // map[string]*seriesEntry[*Gauge] + limiter seriesLimiter +} + +// Gauge는 원자적으로 설정되는 단일 gauge series입니다. +type Gauge struct { + bits atomic.Uint64 +} + +func NewGaugeVec(name, help string) *GaugeVec { + return NewGaugeVecWithOptions(name, help, VecOptions{}) +} + +func NewGaugeVecWithOptions(name, help string, options VecOptions) *GaugeVec { + return &GaugeVec{name: name, help: help, limiter: newSeriesLimiter(options)} +} + +func (v *GaugeVec) With(labels Labels) *Gauge { + if v == nil { + return nil + } + key, labelPairs, ok := v.limiter.canonicalize(labels) + if !ok { + return nil + } + gauge, ok := loadOrCreateSeries(&v.series, &v.limiter, key, labelPairs, func() *Gauge { return &Gauge{} }) + if !ok { + return nil + } + return gauge +} + +func (v *GaugeVec) Set(labels Labels, value float64) { + gauge := v.With(labels) + if gauge != nil { + gauge.Set(value) + } +} + +func (v *GaugeVec) SeriesCount() int { + if v == nil { + return 0 + } + return v.limiter.seriesCount() +} + +func (v *GaugeVec) DroppedSeries() uint64 { + if v == nil { + return 0 + } + return v.limiter.droppedSeries() +} + +func (v *GaugeVec) WriteExposition(w io.Writer) bool { + if v == nil { + return true + } + if !writeMetricHeader(w, v.name, v.help, "gauge") { + return false + } + for _, entry := range collectSeries[*Gauge](&v.series) { + if !writeMetricSample(w, v.name, entry.labels, strconv.FormatFloat(entry.value.Value(), 'g', -1, 64)) { + return false + } + } + return true +} + +func (g *Gauge) Set(value float64) { + if g != nil { + g.bits.Store(math.Float64bits(value)) + } +} + +func (g *Gauge) Value() float64 { + if g == nil { + return 0 + } + return math.Float64frombits(g.bits.Load()) +} + +// HistogramVec는 동일 metric name/help/bucket을 공유하는 label-set별 histogram 집합입니다. +type HistogramVec struct { + name string + help string + buckets []float64 + series sync.Map // map[string]*seriesEntry[*Histogram] + limiter seriesLimiter +} + +func NewHistogramVec(name, help string, buckets []float64) *HistogramVec { + return NewHistogramVecWithOptions(name, help, buckets, VecOptions{}) +} + +func NewHistogramVecWithOptions(name, help string, buckets []float64, options VecOptions) *HistogramVec { + bounds := make([]float64, len(buckets)) + copy(bounds, buckets) + return &HistogramVec{name: name, help: help, buckets: bounds, limiter: newSeriesLimiter(options)} +} + +func (v *HistogramVec) With(labels Labels) *Histogram { + if v == nil { + return nil + } + key, labelPairs, ok := v.limiter.canonicalize(labels) + if !ok { + return nil + } + histogram, ok := loadOrCreateSeries(&v.series, &v.limiter, key, labelPairs, func() *Histogram { + return NewHistogram(v.buckets) + }) + if !ok { + return nil + } + return histogram +} + +func (v *HistogramVec) Observe(labels Labels, value float64) { + hist := v.With(labels) + if hist != nil { + hist.Observe(value) + } +} + +func (v *HistogramVec) SeriesCount() int { + if v == nil { + return 0 + } + return v.limiter.seriesCount() +} + +func (v *HistogramVec) DroppedSeries() uint64 { + if v == nil { + return 0 + } + return v.limiter.droppedSeries() +} + +func (v *HistogramVec) WriteExposition(w io.Writer) bool { + if v == nil { + return true + } + if !writeMetricHeader(w, v.name, v.help, "histogram") { + return false + } + for _, entry := range collectSeries[*Histogram](&v.series) { + snap := entry.value.Snapshot() + if len(snap.UpperBounds) != len(snap.Cumulative) { + return false + } + for i, ub := range snap.UpperBounds { + bucketLabels := appendLabel(entry.labels, labelPair{name: "le", value: strconv.FormatFloat(ub, 'g', -1, 64)}) + if !writeMetricSample(w, v.name+"_bucket", bucketLabels, strconv.FormatUint(snap.Cumulative[i], 10)) { + return false + } + } + infLabels := appendLabel(entry.labels, labelPair{name: "le", value: "+Inf"}) + if !writeMetricSample(w, v.name+"_bucket", infLabels, strconv.FormatUint(snap.Total, 10)) { + return false + } + if !writeMetricSample(w, v.name+"_sum", entry.labels, strconv.FormatFloat(snap.Sum, 'g', -1, 64)) { + return false + } + if !writeMetricSample(w, v.name+"_count", entry.labels, strconv.FormatUint(snap.Total, 10)) { + return false + } + } + return true +} + +func collectSeries[T any](series *sync.Map) []*seriesEntry[T] { + entries := make([]*seriesEntry[T], 0) + series.Range(func(_, value any) bool { + if entry, ok := value.(*seriesEntry[T]); ok { + entries = append(entries, entry) + } + return true + }) + sort.Slice(entries, func(i, j int) bool { + return entries[i].key < entries[j].key + }) + return entries +} + +func canonicalLabels(labels Labels) (string, []labelPair) { + pairs := labelsFromMap(labels) + var key strings.Builder + for _, pair := range pairs { + _, _ = fmt.Fprintf(&key, "%d:%s=%d:%s;", len(pair.name), pair.name, len(pair.value), pair.value) + } + return key.String(), pairs +} + +func labelsFromMap(labels Labels) []labelPair { + if len(labels) == 0 { + return nil + } + pairs := make([]labelPair, 0, len(labels)) + for name, value := range labels { + pairs = append(pairs, labelPair{name: name, value: value}) + } + sort.Slice(pairs, func(i, j int) bool { + return pairs[i].name < pairs[j].name + }) + return pairs +} +''', +) + +write( + "pkg/obsmetrics/vec_security_test.go", + r'''package obsmetrics + +import ( + "fmt" + "strings" + "sync" + "testing" +) + +func TestMetricVectorsEnforceSeriesLimit(t *testing.T) { + t.Parallel() + + options := VecOptions{MaxSeries: 2} + counter := NewCounterVecWithOptions("requests_total", "Requests", options) + gauge := NewGaugeVecWithOptions("active", "Active", options) + histogram := NewHistogramVecWithOptions("latency", "Latency", []float64{1}, options) + + for i := 0; i < 3; i++ { + labels := Labels{"id": fmt.Sprintf("%d", i)} + counter.Inc(labels) + gauge.Set(labels, float64(i)) + histogram.Observe(labels, float64(i)) + } + + for name, got := range map[string]int{ + "counter": counter.SeriesCount(), + "gauge": gauge.SeriesCount(), + "histogram": histogram.SeriesCount(), + } { + if got != 2 { + t.Fatalf("%s SeriesCount() = %d, want 2", name, got) + } + } + if counter.DroppedSeries() != 1 || gauge.DroppedSeries() != 1 || histogram.DroppedSeries() != 1 { + t.Fatalf("DroppedSeries() = (%d, %d, %d), want (1, 1, 1)", counter.DroppedSeries(), gauge.DroppedSeries(), histogram.DroppedSeries()) + } +} + +func TestMetricVectorRejectsOversizedLabelsBeforeRetention(t *testing.T) { + t.Parallel() + + vec := NewCounterVecWithOptions("requests_total", "Requests", VecOptions{ + MaxSeries: 8, + MaxLabels: 1, + MaxLabelNameBytes: 4, + MaxLabelValueBytes: 4, + }) + vec.Inc(Labels{"a": "1", "b": "2"}) + vec.Inc(Labels{"long-name": "1"}) + vec.Inc(Labels{"name": strings.Repeat("x", 5)}) + + if got := vec.SeriesCount(); got != 0 { + t.Fatalf("SeriesCount() = %d, want 0", got) + } + if got := vec.DroppedSeries(); got != 3 { + t.Fatalf("DroppedSeries() = %d, want 3", got) + } +} + +func TestMetricVectorConcurrentAdmissionNeverExceedsLimit(t *testing.T) { + t.Parallel() + + const limit = 8 + vec := NewCounterVecWithOptions("requests_total", "Requests", VecOptions{MaxSeries: limit}) + var wg sync.WaitGroup + for i := 0; i < 128; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + vec.Inc(Labels{"id": fmt.Sprintf("%d", i)}) + }(i) + } + wg.Wait() + + if got := vec.SeriesCount(); got != limit { + t.Fatalf("SeriesCount() = %d, want %d", got, limit) + } + if got := vec.DroppedSeries(); got != 128-limit { + t.Fatalf("DroppedSeries() = %d, want %d", got, 128-limit) + } +} + +func TestMetricVectorCompatibilityConstructorUsesSafeDefault(t *testing.T) { + t.Parallel() + + vec := NewCounterVec("requests_total", "Requests") + for i := 0; i <= DefaultMaxMetricSeries; i++ { + vec.Inc(Labels{"id": fmt.Sprintf("%d", i)}) + } + if got := vec.SeriesCount(); got != DefaultMaxMetricSeries { + t.Fatalf("SeriesCount() = %d, want %d", got, DefaultMaxMetricSeries) + } + if got := vec.DroppedSeries(); got != 1 { + t.Fatalf("DroppedSeries() = %d, want 1", got) + } +} +''', +) + +replace_once( + "pkg/logging/sanitize.go", + 'var sensitiveExactKeys = map[string]struct{}{\n\t"token": {},', + 'var sensitiveExactKeys = map[string]struct{}{\n\t"key": {},\n\t"token": {},', +) +replace_once( + "pkg/logging/sanitize_test.go", + '\t\t{"key", "key", "apikey123", false},', + '\t\t{"key", "key", "apikey123", true},', +) + +replace_once( + "scripts/ci/check-workflow-secrets.sh", + """ kv = parse_key_value(raw) + if kv is not None: + _, value = kv + normalized = unquote_scalar(value) +""", + """ kv = parse_key_value(raw) + if kv is not None: + key, value = kv + source = strip_yaml_comment(raw).strip() + if source.startswith("- "): + source = source[2:].strip() + if source[:1] in {"'", '"'} and key.lower() in { + "permissions", "secrets", "jobs", "uses", "with", "persist-credentials" + }: + failures.append((line_no, f"quoted policy-sensitive YAML key {key!r} is unsupported")) + normalized = unquote_scalar(value) +""", +) +replace_once( + "scripts/ci/check-workflow-secrets.sh", + """ line = strip_yaml_comment(raw) + match = re.match(r"^\\s*([A-Za-z0-9_-]+)\\s*:\\s*([A-Za-z0-9_-]+)\\s*$", line) + if not match: + continue + key = match.group(1) + if key in seen: + return False + seen.add(key) + saw_entry = True + if match.group(2) not in {"read", "none"}: + return False +""", + """ parsed = parse_key_value(raw) + if parsed is None: + return False + key, raw_value = parsed + value = unquote_scalar(raw_value) + if not re.fullmatch(r"[A-Za-z0-9_-]+", value): + return False + if key in seen: + return False + seen.add(key) + saw_entry = True + if value not in {"read", "none"}: + return False +""", +) + +replace_once( + "scripts/ci/check-workflow-secrets_test.sh", + """if (( failures > 0 )); then +""", + r'''quoted_permissions="${TMP_DIR}/quoted-permissions.yml" +write_workflow "${quoted_permissions}" \ + "name: quoted-permissions" \ + "on:" \ + " pull_request:" \ + '"permissions":' \ + " contents: write" \ + "jobs:" \ + " test:" \ + " runs-on: ubuntu-latest" \ + " steps:" \ + " - run: echo ok" +expect_failure "quoted permissions key fails closed" "quoted policy-sensitive YAML key" "${quoted_permissions}" + +quoted_secrets="${TMP_DIR}/quoted-secrets.yml" +write_workflow "${quoted_secrets}" \ + "name: quoted-secrets" \ + "on:" \ + " pull_request:" \ + "permissions:" \ + " contents: read" \ + "jobs:" \ + " call:" \ + " uses: owner/repo/.github/workflows/reusable.yml@main" \ + ' "secrets": inherit' +expect_failure "quoted secrets key fails closed" "quoted policy-sensitive YAML key" "${quoted_secrets}" + +quoted_permission_scope="${TMP_DIR}/quoted-permission-scope.yml" +write_workflow "${quoted_permission_scope}" \ + "name: quoted-permission-scope" \ + "on:" \ + " pull_request:" \ + "permissions:" \ + " contents: read" \ + ' "pull-requests": write' \ + "jobs:" \ + " test:" \ + " runs-on: ubuntu-latest" \ + " steps:" \ + " - run: echo ok" +expect_failure "quoted permission scope cannot hide write" "read-only permissions" "${quoted_permission_scope}" + +if (( failures > 0 )); then +''', +) + +replace_once( + ".github/workflows/ci.yml", + """ - name: Workflow secret boundary + run: bash scripts/ci/check-workflow-secrets.sh + + - name: SQL ownership +""", + """ - name: Workflow secret boundary + run: bash scripts/ci/check-workflow-secrets.sh + + - name: Workflow secret boundary tests + run: bash scripts/ci/check-workflow-secrets_test.sh + + - name: SQL ownership +""", +) + +# The helper files must not appear in the final pull-request tree. +(ROOT / ".github/workflows/agent-security-patch.yml").unlink() +Path(__file__).unlink() From d445aa5ad8f93563041ce13fe07d5fab8ef15087 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 13:05:13 +0900 Subject: [PATCH 02/38] chore: run verified security hardening patch --- .github/workflows/agent-security-patch.yml | 68 ++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/agent-security-patch.yml diff --git a/.github/workflows/agent-security-patch.yml b/.github/workflows/agent-security-patch.yml new file mode 100644 index 00000000..89195c35 --- /dev/null +++ b/.github/workflows/agent-security-patch.yml @@ -0,0 +1,68 @@ +name: agent security patch + +on: + push: + branches: + - agent/security-hardening-20260715 + +permissions: + contents: write + +concurrency: + group: agent-security-patch-shared-go + cancel-in-progress: false + +jobs: + apply-and-verify: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout patch branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: agent/security-hardening-20260715 + persist-credentials: true + + - name: Setup Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Apply security patch + run: python3 scripts/ci/agent_security_patch.py + + - name: Format changed Go files + run: | + gofmt -w \ + pkg/promptguard/rulepack_match.go \ + pkg/promptguard/rulepack_raw_security_test.go \ + pkg/obsmetrics/vec.go \ + pkg/obsmetrics/vec_security_test.go \ + pkg/logging/sanitize.go \ + pkg/logging/sanitize_test.go + + - name: Verify workflow policy regression suite + run: bash scripts/ci/check-workflow-secrets_test.sh + + - name: Run Go tests + run: go test -count=1 ./... + + - name: Run Go vet + run: go vet ./... + + - name: Validate patch + run: | + git diff --check + if git diff --quiet; then + echo "security patch produced no changes" >&2 + exit 1 + fi + + - name: Commit verified patch + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "security: harden shared library boundaries" + git push origin HEAD:agent/security-hardening-20260715 From e2edf0b835d65f5e427538e1ca0372d251dae8ff Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 13:08:14 +0900 Subject: [PATCH 03/38] chore: allow PR-triggered security patch verification --- .github/workflows/agent-security-patch.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/agent-security-patch.yml b/.github/workflows/agent-security-patch.yml index 89195c35..86ed7aa8 100644 --- a/.github/workflows/agent-security-patch.yml +++ b/.github/workflows/agent-security-patch.yml @@ -4,6 +4,9 @@ on: push: branches: - agent/security-hardening-20260715 + pull_request: + branches: + - main permissions: contents: write @@ -14,6 +17,7 @@ concurrency: jobs: apply-and-verify: + if: github.head_ref == 'agent/security-hardening-20260715' || github.ref_name == 'agent/security-hardening-20260715' runs-on: ubuntu-latest timeout-minutes: 30 steps: From 24cc368f52f3d978e0ad5bf1153384587b75ad76 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 13:13:52 +0900 Subject: [PATCH 04/38] chore: persist security patch test diagnostics --- .github/workflows/agent-security-patch.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/agent-security-patch.yml b/.github/workflows/agent-security-patch.yml index 86ed7aa8..1b67e420 100644 --- a/.github/workflows/agent-security-patch.yml +++ b/.github/workflows/agent-security-patch.yml @@ -50,7 +50,12 @@ jobs: run: bash scripts/ci/check-workflow-secrets_test.sh - name: Run Go tests - run: go test -count=1 ./... + run: | + set +e + go test -count=1 ./... > /tmp/agent-go-test.log 2>&1 + status=$? + cat /tmp/agent-go-test.log + exit "$status" - name: Run Go vet run: go vet ./... @@ -63,7 +68,19 @@ jobs: exit 1 fi + - name: Persist failure diagnostics + if: failure() + run: | + mkdir -p .agent-diagnostics + cp /tmp/agent-go-test.log .agent-diagnostics/go-test.log 2>/dev/null || echo "test log unavailable" > .agent-diagnostics/go-test.log + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .agent-diagnostics/go-test.log + git commit -m "chore: record security patch test failure" + git push origin HEAD:agent/security-hardening-20260715 + - name: Commit verified patch + if: success() run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" From fed26859ad5724e6a46c7f466e8682a1c07623ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:14:50 +0000 Subject: [PATCH 05/38] chore: record security patch test failure --- .agent-diagnostics/go-test.log | 66 ++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .agent-diagnostics/go-test.log diff --git a/.agent-diagnostics/go-test.log b/.agent-diagnostics/go-test.log new file mode 100644 index 00000000..996288f1 --- /dev/null +++ b/.agent-diagnostics/go-test.log @@ -0,0 +1,66 @@ +go: downloading github.com/stretchr/testify v1.11.1 +go: downloading github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc +go: downloading github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 +ok github.com/park285/shared-go/pkg/backoff 0.002s +ok github.com/park285/shared-go/pkg/db/pgxdb 16.816s +ok github.com/park285/shared-go/pkg/db/sqldb 0.002s +ok github.com/park285/shared-go/pkg/dbmigrate 0.009s +ok github.com/park285/shared-go/pkg/envutil 0.012s +ok github.com/park285/shared-go/pkg/ginjson 0.018s +ok github.com/park285/shared-go/pkg/h3 0.028s +ok github.com/park285/shared-go/pkg/healthprobe 0.084s +ok github.com/park285/shared-go/pkg/httputil 1.612s +ok github.com/park285/shared-go/pkg/httputil/ginauth 0.011s +ok github.com/park285/shared-go/pkg/internal/guardtext 0.028s +ok github.com/park285/shared-go/pkg/json 0.029s +ok github.com/park285/shared-go/pkg/jsonutil 0.024s +ok github.com/park285/shared-go/pkg/llm 0.020s +? github.com/park285/shared-go/pkg/llm/internal/openaidiag [no test files] +ok github.com/park285/shared-go/pkg/llm/openaipreset 0.141s +2026-07-15T04:14:50Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLogging_UsesRestrictedFileAndDirectoryPerms1234986227/001/service.log archive_dir=/tmp/TestEnableFileLogging_UsesRestrictedFileAndDirectoryPerms1234986227/001/archive otel_correlation=false async_stdout=false +2026-07-15T04:14:50Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLoggingDoesNotSetDefault861935214/001/service.log archive_dir=/tmp/TestEnableFileLoggingDoesNotSetDefault861935214/001/archive otel_correlation=false async_stdout=false +--- FAIL: TestBareKey_NameAloneNotMasked_ValueGatesRedaction_8e92058d (0.00s) + sanitize_h2_test.go:67: isSensitiveKey("key") = true, want false: key name alone must not force redaction +--- FAIL: TestSanitizeKey_NonSecretLiteralKeyValuesNotMasked_8e92058d (0.00s) + sanitize_keyvalue_test.go:52: non-secret literal key field value "member:news:rooms" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "member:news:rooms" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "member:news:room_names" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "member:news:room_names" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "notification:egress:lease" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "notification:egress:lease" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "session:index:ghost" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "session:index:ghost" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "official_schedule_page" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "official_schedule_page" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "user-1234" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "user-1234" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "lock-abc" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "lock-abc" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "apikey123" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "apikey123" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "42" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "42" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "en-US" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "en-US" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "hololive" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "hololive" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "/api/v1/cache" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "/api/v1/cache" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** +2026-07-15T04:14:50Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLogging_DoesNotCreateCombinedLog987708594/001/service.log archive_dir=/tmp/TestEnableFileLogging_DoesNotCreateCombinedLog987708594/001/archive otel_correlation=false async_stdout=false +FAIL +FAIL github.com/park285/shared-go/pkg/logging 0.014s +ok github.com/park285/shared-go/pkg/logging/internal/archive 0.009s +ok github.com/park285/shared-go/pkg/netguard 0.004s +ok github.com/park285/shared-go/pkg/obsmetrics 0.006s +ok github.com/park285/shared-go/pkg/outputguard 0.336s +ok github.com/park285/shared-go/pkg/promptguard 0.104s +ok github.com/park285/shared-go/pkg/retry 0.002s +ok github.com/park285/shared-go/pkg/runtime/automaxprocs 0.003s +ok github.com/park285/shared-go/pkg/runtime/bootstrap 0.005s +ok github.com/park285/shared-go/pkg/runtime/httpserver 0.004s +ok github.com/park285/shared-go/pkg/runtime/lifecycle 0.021s +ok github.com/park285/shared-go/pkg/stringutil 0.005s +ok github.com/park285/shared-go/pkg/telemetry 0.007s +ok github.com/park285/shared-go/pkg/workerconfig 0.004s +ok github.com/park285/shared-go/pkg/workerpool 0.231s +FAIL From ead41b1ff9f0563c503bd44a10849f72ea196dad Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:15:57 +0000 Subject: [PATCH 06/38] chore: record security patch test failure --- .agent-diagnostics/go-test.log | 108 ++++++++++++++++----------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/.agent-diagnostics/go-test.log b/.agent-diagnostics/go-test.log index 996288f1..6ef35303 100644 --- a/.agent-diagnostics/go-test.log +++ b/.agent-diagnostics/go-test.log @@ -1,66 +1,66 @@ go: downloading github.com/stretchr/testify v1.11.1 go: downloading github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc go: downloading github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 -ok github.com/park285/shared-go/pkg/backoff 0.002s -ok github.com/park285/shared-go/pkg/db/pgxdb 16.816s +ok github.com/park285/shared-go/pkg/backoff 0.006s +ok github.com/park285/shared-go/pkg/db/pgxdb 17.405s ok github.com/park285/shared-go/pkg/db/sqldb 0.002s -ok github.com/park285/shared-go/pkg/dbmigrate 0.009s -ok github.com/park285/shared-go/pkg/envutil 0.012s -ok github.com/park285/shared-go/pkg/ginjson 0.018s -ok github.com/park285/shared-go/pkg/h3 0.028s -ok github.com/park285/shared-go/pkg/healthprobe 0.084s -ok github.com/park285/shared-go/pkg/httputil 1.612s -ok github.com/park285/shared-go/pkg/httputil/ginauth 0.011s -ok github.com/park285/shared-go/pkg/internal/guardtext 0.028s -ok github.com/park285/shared-go/pkg/json 0.029s -ok github.com/park285/shared-go/pkg/jsonutil 0.024s -ok github.com/park285/shared-go/pkg/llm 0.020s +ok github.com/park285/shared-go/pkg/dbmigrate 0.008s +ok github.com/park285/shared-go/pkg/envutil 0.016s +ok github.com/park285/shared-go/pkg/ginjson 0.041s +ok github.com/park285/shared-go/pkg/h3 0.078s +ok github.com/park285/shared-go/pkg/healthprobe 0.300s +ok github.com/park285/shared-go/pkg/httputil 2.177s +ok github.com/park285/shared-go/pkg/httputil/ginauth 0.033s +ok github.com/park285/shared-go/pkg/internal/guardtext 0.024s +ok github.com/park285/shared-go/pkg/json 0.039s +ok github.com/park285/shared-go/pkg/jsonutil 0.047s +ok github.com/park285/shared-go/pkg/llm 0.017s ? github.com/park285/shared-go/pkg/llm/internal/openaidiag [no test files] -ok github.com/park285/shared-go/pkg/llm/openaipreset 0.141s -2026-07-15T04:14:50Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLogging_UsesRestrictedFileAndDirectoryPerms1234986227/001/service.log archive_dir=/tmp/TestEnableFileLogging_UsesRestrictedFileAndDirectoryPerms1234986227/001/archive otel_correlation=false async_stdout=false -2026-07-15T04:14:50Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLoggingDoesNotSetDefault861935214/001/service.log archive_dir=/tmp/TestEnableFileLoggingDoesNotSetDefault861935214/001/archive otel_correlation=false async_stdout=false +ok github.com/park285/shared-go/pkg/llm/openaipreset 0.145s +2026-07-15T04:15:56Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLogging_UsesRestrictedFileAndDirectoryPerms2925262557/001/service.log archive_dir=/tmp/TestEnableFileLogging_UsesRestrictedFileAndDirectoryPerms2925262557/001/archive otel_correlation=false async_stdout=false +2026-07-15T04:15:56Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLoggingDoesNotSetDefault3084853783/001/service.log archive_dir=/tmp/TestEnableFileLoggingDoesNotSetDefault3084853783/001/archive otel_correlation=false async_stdout=false --- FAIL: TestBareKey_NameAloneNotMasked_ValueGatesRedaction_8e92058d (0.00s) sanitize_h2_test.go:67: isSensitiveKey("key") = true, want false: key name alone must not force redaction --- FAIL: TestSanitizeKey_NonSecretLiteralKeyValuesNotMasked_8e92058d (0.00s) - sanitize_keyvalue_test.go:52: non-secret literal key field value "member:news:rooms" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "member:news:rooms" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "member:news:room_names" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "member:news:room_names" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "notification:egress:lease" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "notification:egress:lease" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "session:index:ghost" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "session:index:ghost" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "official_schedule_page" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "official_schedule_page" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "user-1234" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "user-1234" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "lock-abc" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "lock-abc" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "apikey123" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "apikey123" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "42" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "42" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "en-US" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "en-US" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "hololive" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "hololive" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "/api/v1/cache" must not be masked, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "/api/v1/cache" must be preserved, got: time=2026-07-15T04:14:50.213Z level=INFO msg=m key=***REDACTED*** -2026-07-15T04:14:50Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLogging_DoesNotCreateCombinedLog987708594/001/service.log archive_dir=/tmp/TestEnableFileLogging_DoesNotCreateCombinedLog987708594/001/archive otel_correlation=false async_stdout=false + sanitize_keyvalue_test.go:52: non-secret literal key field value "member:news:rooms" must not be masked, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "member:news:rooms" must be preserved, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "member:news:room_names" must not be masked, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "member:news:room_names" must be preserved, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "notification:egress:lease" must not be masked, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "notification:egress:lease" must be preserved, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "session:index:ghost" must not be masked, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "session:index:ghost" must be preserved, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "official_schedule_page" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "official_schedule_page" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "user-1234" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "user-1234" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "lock-abc" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "lock-abc" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "apikey123" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "apikey123" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "42" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "42" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "en-US" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "en-US" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "hololive" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "hololive" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:52: non-secret literal key field value "/api/v1/cache" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** + sanitize_keyvalue_test.go:55: non-secret literal key field value "/api/v1/cache" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** +2026-07-15T04:15:56Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLogging_DoesNotCreateCombinedLog80859271/001/service.log archive_dir=/tmp/TestEnableFileLogging_DoesNotCreateCombinedLog80859271/001/archive otel_correlation=false async_stdout=false FAIL -FAIL github.com/park285/shared-go/pkg/logging 0.014s -ok github.com/park285/shared-go/pkg/logging/internal/archive 0.009s +FAIL github.com/park285/shared-go/pkg/logging 0.015s +ok github.com/park285/shared-go/pkg/logging/internal/archive 0.012s ok github.com/park285/shared-go/pkg/netguard 0.004s -ok github.com/park285/shared-go/pkg/obsmetrics 0.006s -ok github.com/park285/shared-go/pkg/outputguard 0.336s -ok github.com/park285/shared-go/pkg/promptguard 0.104s -ok github.com/park285/shared-go/pkg/retry 0.002s -ok github.com/park285/shared-go/pkg/runtime/automaxprocs 0.003s -ok github.com/park285/shared-go/pkg/runtime/bootstrap 0.005s -ok github.com/park285/shared-go/pkg/runtime/httpserver 0.004s -ok github.com/park285/shared-go/pkg/runtime/lifecycle 0.021s -ok github.com/park285/shared-go/pkg/stringutil 0.005s -ok github.com/park285/shared-go/pkg/telemetry 0.007s +ok github.com/park285/shared-go/pkg/obsmetrics 0.005s +ok github.com/park285/shared-go/pkg/outputguard 0.399s +ok github.com/park285/shared-go/pkg/promptguard 0.122s +ok github.com/park285/shared-go/pkg/retry 0.004s +ok github.com/park285/shared-go/pkg/runtime/automaxprocs 0.005s +ok github.com/park285/shared-go/pkg/runtime/bootstrap 0.006s +ok github.com/park285/shared-go/pkg/runtime/httpserver 0.006s +ok github.com/park285/shared-go/pkg/runtime/lifecycle 0.024s +ok github.com/park285/shared-go/pkg/stringutil 0.003s +ok github.com/park285/shared-go/pkg/telemetry 0.005s ok github.com/park285/shared-go/pkg/workerconfig 0.004s -ok github.com/park285/shared-go/pkg/workerpool 0.231s +ok github.com/park285/shared-go/pkg/workerpool 0.232s FAIL From 0472b989eb7fbc79e5be4440ca5f9a84f991f767 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 13:19:44 +0900 Subject: [PATCH 07/38] fix: preserve value-aware generic key redaction --- .github/workflows/agent-security-patch.yml | 35 ++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/.github/workflows/agent-security-patch.yml b/.github/workflows/agent-security-patch.yml index 1b67e420..54f54da2 100644 --- a/.github/workflows/agent-security-patch.yml +++ b/.github/workflows/agent-security-patch.yml @@ -36,6 +36,35 @@ jobs: - name: Apply security patch run: python3 scripts/ci/agent_security_patch.py + - name: Preserve existing value-aware generic key policy + run: | + python3 - <<'PY' + from pathlib import Path + + path = Path("pkg/logging/sanitize.go") + text = path.read_text(encoding="utf-8") + old = 'var sensitiveExactKeys = map[string]struct{}{\n\t"key": {},\n\t"token": {},' + new = 'var sensitiveExactKeys = map[string]struct{}{\n\t"token": {},' + if text.count(old) != 1: + raise SystemExit("unexpected sensitiveExactKeys patch state") + path.write_text(text.replace(old, new, 1), encoding="utf-8") + + test_path = Path("pkg/logging/sanitize_test.go") + test_text = test_path.read_text(encoding="utf-8") + old_test = '\t\t{"key", "key", "apikey123", true},' + new_test = '\t\t{"key", "key", "apikey123", false},' + if test_text.count(old_test) != 1: + raise SystemExit("unexpected sanitize test patch state") + test_path.write_text(test_text.replace(old_test, new_test, 1), encoding="utf-8") + + diagnostic = Path(".agent-diagnostics/go-test.log") + diagnostic.unlink(missing_ok=True) + try: + diagnostic.parent.rmdir() + except OSError: + pass + PY + - name: Format changed Go files run: | gofmt -w \ @@ -76,8 +105,10 @@ jobs: git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add .agent-diagnostics/go-test.log - git commit -m "chore: record security patch test failure" - git push origin HEAD:agent/security-hardening-20260715 + if ! git diff --cached --quiet; then + git commit -m "chore: record security patch test failure" + git push origin HEAD:agent/security-hardening-20260715 + fi - name: Commit verified patch if: success() From 251c0e540527e9a53f1c243599e21f619e40bdb0 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 13:35:21 +0900 Subject: [PATCH 08/38] chore: serialize verified security patch execution --- .github/workflows/agent-security-patch.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/agent-security-patch.yml b/.github/workflows/agent-security-patch.yml index 54f54da2..59f8445c 100644 --- a/.github/workflows/agent-security-patch.yml +++ b/.github/workflows/agent-security-patch.yml @@ -1,9 +1,6 @@ name: agent security patch on: - push: - branches: - - agent/security-hardening-20260715 pull_request: branches: - main @@ -17,7 +14,7 @@ concurrency: jobs: apply-and-verify: - if: github.head_ref == 'agent/security-hardening-20260715' || github.ref_name == 'agent/security-hardening-20260715' + if: github.head_ref == 'agent/security-hardening-20260715' runs-on: ubuntu-latest timeout-minutes: 30 steps: From 07b27f137052459bd2c7120286a2d77bde2b9450 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 13:49:51 +0900 Subject: [PATCH 09/38] fix: separate verified code commit from workflow updates --- .github/workflows/agent-security-patch.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/agent-security-patch.yml b/.github/workflows/agent-security-patch.yml index 59f8445c..547b2280 100644 --- a/.github/workflows/agent-security-patch.yml +++ b/.github/workflows/agent-security-patch.yml @@ -107,9 +107,13 @@ jobs: git push origin HEAD:agent/security-hardening-20260715 fi - - name: Commit verified patch + - name: Commit verified non-workflow patch if: success() run: | + # GitHub's workflow token is intentionally not used to rewrite workflow + # definitions. Restore those two paths, commit the already-verified code + # and tests, then the trusted GitHub connector will finalize CI policy. + git checkout HEAD -- .github/workflows/ci.yml .github/workflows/agent-security-patch.yml git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add -A From b10c7e483f40c3b93a3ee76723e939ba7c6c3e83 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:51:27 +0000 Subject: [PATCH 10/38] security: harden shared library boundaries --- .agent-diagnostics/go-test.log | 66 -- pkg/obsmetrics/vec.go | 266 ++++-- pkg/obsmetrics/vec_security_test.go | 96 +++ pkg/promptguard/rulepack_match.go | 12 +- pkg/promptguard/rulepack_raw_security_test.go | 61 ++ scripts/ci/agent_security_patch.py | 805 ------------------ scripts/ci/check-workflow-secrets.sh | 23 +- scripts/ci/check-workflow-secrets_test.sh | 42 + 8 files changed, 433 insertions(+), 938 deletions(-) delete mode 100644 .agent-diagnostics/go-test.log create mode 100644 pkg/obsmetrics/vec_security_test.go create mode 100644 pkg/promptguard/rulepack_raw_security_test.go delete mode 100644 scripts/ci/agent_security_patch.py diff --git a/.agent-diagnostics/go-test.log b/.agent-diagnostics/go-test.log deleted file mode 100644 index 6ef35303..00000000 --- a/.agent-diagnostics/go-test.log +++ /dev/null @@ -1,66 +0,0 @@ -go: downloading github.com/stretchr/testify v1.11.1 -go: downloading github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc -go: downloading github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 -ok github.com/park285/shared-go/pkg/backoff 0.006s -ok github.com/park285/shared-go/pkg/db/pgxdb 17.405s -ok github.com/park285/shared-go/pkg/db/sqldb 0.002s -ok github.com/park285/shared-go/pkg/dbmigrate 0.008s -ok github.com/park285/shared-go/pkg/envutil 0.016s -ok github.com/park285/shared-go/pkg/ginjson 0.041s -ok github.com/park285/shared-go/pkg/h3 0.078s -ok github.com/park285/shared-go/pkg/healthprobe 0.300s -ok github.com/park285/shared-go/pkg/httputil 2.177s -ok github.com/park285/shared-go/pkg/httputil/ginauth 0.033s -ok github.com/park285/shared-go/pkg/internal/guardtext 0.024s -ok github.com/park285/shared-go/pkg/json 0.039s -ok github.com/park285/shared-go/pkg/jsonutil 0.047s -ok github.com/park285/shared-go/pkg/llm 0.017s -? github.com/park285/shared-go/pkg/llm/internal/openaidiag [no test files] -ok github.com/park285/shared-go/pkg/llm/openaipreset 0.145s -2026-07-15T04:15:56Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLogging_UsesRestrictedFileAndDirectoryPerms2925262557/001/service.log archive_dir=/tmp/TestEnableFileLogging_UsesRestrictedFileAndDirectoryPerms2925262557/001/archive otel_correlation=false async_stdout=false -2026-07-15T04:15:56Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLoggingDoesNotSetDefault3084853783/001/service.log archive_dir=/tmp/TestEnableFileLoggingDoesNotSetDefault3084853783/001/archive otel_correlation=false async_stdout=false ---- FAIL: TestBareKey_NameAloneNotMasked_ValueGatesRedaction_8e92058d (0.00s) - sanitize_h2_test.go:67: isSensitiveKey("key") = true, want false: key name alone must not force redaction ---- FAIL: TestSanitizeKey_NonSecretLiteralKeyValuesNotMasked_8e92058d (0.00s) - sanitize_keyvalue_test.go:52: non-secret literal key field value "member:news:rooms" must not be masked, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "member:news:rooms" must be preserved, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "member:news:room_names" must not be masked, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "member:news:room_names" must be preserved, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "notification:egress:lease" must not be masked, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "notification:egress:lease" must be preserved, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "session:index:ghost" must not be masked, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "session:index:ghost" must be preserved, got: time=2026-07-15T04:15:56.499Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "official_schedule_page" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "official_schedule_page" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "user-1234" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "user-1234" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "lock-abc" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "lock-abc" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "apikey123" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "apikey123" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "42" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "42" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "en-US" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "en-US" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "hololive" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "hololive" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:52: non-secret literal key field value "/api/v1/cache" must not be masked, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** - sanitize_keyvalue_test.go:55: non-secret literal key field value "/api/v1/cache" must be preserved, got: time=2026-07-15T04:15:56.500Z level=INFO msg=m key=***REDACTED*** -2026-07-15T04:15:56Z INF logging/logging.go:147 file_logging_enabled path=/tmp/TestEnableFileLogging_DoesNotCreateCombinedLog80859271/001/service.log archive_dir=/tmp/TestEnableFileLogging_DoesNotCreateCombinedLog80859271/001/archive otel_correlation=false async_stdout=false -FAIL -FAIL github.com/park285/shared-go/pkg/logging 0.015s -ok github.com/park285/shared-go/pkg/logging/internal/archive 0.012s -ok github.com/park285/shared-go/pkg/netguard 0.004s -ok github.com/park285/shared-go/pkg/obsmetrics 0.005s -ok github.com/park285/shared-go/pkg/outputguard 0.399s -ok github.com/park285/shared-go/pkg/promptguard 0.122s -ok github.com/park285/shared-go/pkg/retry 0.004s -ok github.com/park285/shared-go/pkg/runtime/automaxprocs 0.005s -ok github.com/park285/shared-go/pkg/runtime/bootstrap 0.006s -ok github.com/park285/shared-go/pkg/runtime/httpserver 0.006s -ok github.com/park285/shared-go/pkg/runtime/lifecycle 0.024s -ok github.com/park285/shared-go/pkg/stringutil 0.003s -ok github.com/park285/shared-go/pkg/telemetry 0.005s -ok github.com/park285/shared-go/pkg/workerconfig 0.004s -ok github.com/park285/shared-go/pkg/workerpool 0.232s -FAIL diff --git a/pkg/obsmetrics/vec.go b/pkg/obsmetrics/vec.go index 090d65ea..a5737e99 100644 --- a/pkg/obsmetrics/vec.go +++ b/pkg/obsmetrics/vec.go @@ -25,11 +25,140 @@ type seriesEntry[T any] struct { value T } +const ( + // DefaultMaxMetricSeries bounds the process-lifetime cardinality retained by + // every vector created through the compatibility constructors. + DefaultMaxMetricSeries = 1024 + // DefaultMaxMetricLabels bounds the number of labels in one series. + DefaultMaxMetricLabels = 16 + // DefaultMaxMetricLabelNameBytes and DefaultMaxMetricLabelValueBytes bound + // allocations performed while canonicalizing attacker-influenced labels. + DefaultMaxMetricLabelNameBytes = 128 + DefaultMaxMetricLabelValueBytes = 256 +) + +// VecOptions configures hard resource limits for a metric vector. Non-positive +// values select the safe defaults; there is intentionally no unbounded mode. +type VecOptions struct { + MaxSeries int + MaxLabels int + MaxLabelNameBytes int + MaxLabelValueBytes int +} + +type seriesLimiter struct { + maxSeries int64 + maxLabels int + maxLabelNameBytes int + maxLabelValueBytes int + count atomic.Int64 + dropped atomic.Uint64 +} + +func newSeriesLimiter(options VecOptions) seriesLimiter { + if options.MaxSeries <= 0 { + options.MaxSeries = DefaultMaxMetricSeries + } + if options.MaxLabels <= 0 { + options.MaxLabels = DefaultMaxMetricLabels + } + if options.MaxLabelNameBytes <= 0 { + options.MaxLabelNameBytes = DefaultMaxMetricLabelNameBytes + } + if options.MaxLabelValueBytes <= 0 { + options.MaxLabelValueBytes = DefaultMaxMetricLabelValueBytes + } + + return seriesLimiter{ + maxSeries: int64(options.MaxSeries), + maxLabels: options.MaxLabels, + maxLabelNameBytes: options.MaxLabelNameBytes, + maxLabelValueBytes: options.MaxLabelValueBytes, + } +} + +func (l *seriesLimiter) canonicalize(labels Labels) (string, []labelPair, bool) { + if len(labels) > l.maxLabels { + l.dropped.Add(1) + return "", nil, false + } + for name, value := range labels { + if len(name) > l.maxLabelNameBytes || len(value) > l.maxLabelValueBytes { + l.dropped.Add(1) + return "", nil, false + } + } + + key, pairs := canonicalLabels(labels) + return key, pairs, true +} + +func (l *seriesLimiter) reserve() bool { + for { + current := l.count.Load() + if current >= l.maxSeries { + l.dropped.Add(1) + return false + } + if l.count.CompareAndSwap(current, current+1) { + return true + } + } +} + +func (l *seriesLimiter) release() { + l.count.Add(-1) +} + +func (l *seriesLimiter) seriesCount() int { + return int(l.count.Load()) +} + +func (l *seriesLimiter) droppedSeries() uint64 { + return l.dropped.Load() +} + +func loadOrCreateSeries[T any]( + series *sync.Map, + limiter *seriesLimiter, + key string, + labels []labelPair, + create func() T, +) (T, bool) { + var zero T + if actual, ok := series.Load(key); ok { + stored, typed := actual.(*seriesEntry[T]) + if !typed { + return zero, false + } + return stored.value, true + } + if !limiter.reserve() { + return zero, false + } + + entry := &seriesEntry[T]{key: key, labels: labels, value: create()} + actual, loaded := series.LoadOrStore(key, entry) + if loaded { + limiter.release() + } + stored, ok := actual.(*seriesEntry[T]) + if !ok { + if !loaded { + series.Delete(key) + limiter.release() + } + return zero, false + } + return stored.value, true +} + // CounterVec는 동일 metric name/help를 공유하는 label-set별 counter 집합입니다. type CounterVec struct { - name string - help string - series sync.Map // map[string]*seriesEntry[*Counter] + name string + help string + series sync.Map // map[string]*seriesEntry[*Counter] + limiter seriesLimiter } // Counter는 원자적으로 증가하는 단일 counter series입니다. @@ -38,27 +167,26 @@ type Counter struct { } func NewCounterVec(name, help string) *CounterVec { - return &CounterVec{name: name, help: help} + return NewCounterVecWithOptions(name, help, VecOptions{}) +} + +func NewCounterVecWithOptions(name, help string, options VecOptions) *CounterVec { + return &CounterVec{name: name, help: help, limiter: newSeriesLimiter(options)} } func (v *CounterVec) With(labels Labels) *Counter { if v == nil { return nil } - - key, labelPairs := canonicalLabels(labels) - entry := &seriesEntry[*Counter]{ - key: key, - labels: labelPairs, - value: &Counter{}, + key, labelPairs, ok := v.limiter.canonicalize(labels) + if !ok { + return nil } - actual, _ := v.series.LoadOrStore(key, entry) - stored, ok := actual.(*seriesEntry[*Counter]) + counter, ok := loadOrCreateSeries(&v.series, &v.limiter, key, labelPairs, func() *Counter { return &Counter{} }) if !ok { return nil } - - return stored.value + return counter } func (v *CounterVec) Inc(labels Labels) { @@ -72,6 +200,20 @@ func (v *CounterVec) Add(labels Labels, delta uint64) { } } +func (v *CounterVec) SeriesCount() int { + if v == nil { + return 0 + } + return v.limiter.seriesCount() +} + +func (v *CounterVec) DroppedSeries() uint64 { + if v == nil { + return 0 + } + return v.limiter.droppedSeries() +} + func (v *CounterVec) WriteExposition(w io.Writer) bool { if v == nil { return true @@ -79,13 +221,11 @@ func (v *CounterVec) WriteExposition(w io.Writer) bool { if !writeMetricHeader(w, v.name, v.help, "counter") { return false } - for _, entry := range collectSeries[*Counter](&v.series) { if !writeMetricSample(w, v.name, entry.labels, strconv.FormatUint(entry.value.Value(), 10)) { return false } } - return true } @@ -103,15 +243,15 @@ func (c *Counter) Value() uint64 { if c == nil { return 0 } - return c.value.Load() } // GaugeVec는 동일 metric name/help를 공유하는 label-set별 float64 gauge 집합입니다. type GaugeVec struct { - name string - help string - series sync.Map // map[string]*seriesEntry[*Gauge] + name string + help string + series sync.Map // map[string]*seriesEntry[*Gauge] + limiter seriesLimiter } // Gauge는 원자적으로 설정되는 단일 gauge series입니다. @@ -120,27 +260,26 @@ type Gauge struct { } func NewGaugeVec(name, help string) *GaugeVec { - return &GaugeVec{name: name, help: help} + return NewGaugeVecWithOptions(name, help, VecOptions{}) +} + +func NewGaugeVecWithOptions(name, help string, options VecOptions) *GaugeVec { + return &GaugeVec{name: name, help: help, limiter: newSeriesLimiter(options)} } func (v *GaugeVec) With(labels Labels) *Gauge { if v == nil { return nil } - - key, labelPairs := canonicalLabels(labels) - entry := &seriesEntry[*Gauge]{ - key: key, - labels: labelPairs, - value: &Gauge{}, + key, labelPairs, ok := v.limiter.canonicalize(labels) + if !ok { + return nil } - actual, _ := v.series.LoadOrStore(key, entry) - stored, ok := actual.(*seriesEntry[*Gauge]) + gauge, ok := loadOrCreateSeries(&v.series, &v.limiter, key, labelPairs, func() *Gauge { return &Gauge{} }) if !ok { return nil } - - return stored.value + return gauge } func (v *GaugeVec) Set(labels Labels, value float64) { @@ -150,6 +289,20 @@ func (v *GaugeVec) Set(labels Labels, value float64) { } } +func (v *GaugeVec) SeriesCount() int { + if v == nil { + return 0 + } + return v.limiter.seriesCount() +} + +func (v *GaugeVec) DroppedSeries() uint64 { + if v == nil { + return 0 + } + return v.limiter.droppedSeries() +} + func (v *GaugeVec) WriteExposition(w io.Writer) bool { if v == nil { return true @@ -157,13 +310,11 @@ func (v *GaugeVec) WriteExposition(w io.Writer) bool { if !writeMetricHeader(w, v.name, v.help, "gauge") { return false } - for _, entry := range collectSeries[*Gauge](&v.series) { if !writeMetricSample(w, v.name, entry.labels, strconv.FormatFloat(entry.value.Value(), 'g', -1, 64)) { return false } } - return true } @@ -177,7 +328,6 @@ func (g *Gauge) Value() float64 { if g == nil { return 0 } - return math.Float64frombits(g.bits.Load()) } @@ -187,33 +337,34 @@ type HistogramVec struct { help string buckets []float64 series sync.Map // map[string]*seriesEntry[*Histogram] + limiter seriesLimiter } func NewHistogramVec(name, help string, buckets []float64) *HistogramVec { + return NewHistogramVecWithOptions(name, help, buckets, VecOptions{}) +} + +func NewHistogramVecWithOptions(name, help string, buckets []float64, options VecOptions) *HistogramVec { bounds := make([]float64, len(buckets)) copy(bounds, buckets) - - return &HistogramVec{name: name, help: help, buckets: bounds} + return &HistogramVec{name: name, help: help, buckets: bounds, limiter: newSeriesLimiter(options)} } func (v *HistogramVec) With(labels Labels) *Histogram { if v == nil { return nil } - - key, labelPairs := canonicalLabels(labels) - entry := &seriesEntry[*Histogram]{ - key: key, - labels: labelPairs, - value: NewHistogram(v.buckets), + key, labelPairs, ok := v.limiter.canonicalize(labels) + if !ok { + return nil } - actual, _ := v.series.LoadOrStore(key, entry) - stored, ok := actual.(*seriesEntry[*Histogram]) + histogram, ok := loadOrCreateSeries(&v.series, &v.limiter, key, labelPairs, func() *Histogram { + return NewHistogram(v.buckets) + }) if !ok { return nil } - - return stored.value + return histogram } func (v *HistogramVec) Observe(labels Labels, value float64) { @@ -223,6 +374,20 @@ func (v *HistogramVec) Observe(labels Labels, value float64) { } } +func (v *HistogramVec) SeriesCount() int { + if v == nil { + return 0 + } + return v.limiter.seriesCount() +} + +func (v *HistogramVec) DroppedSeries() uint64 { + if v == nil { + return 0 + } + return v.limiter.droppedSeries() +} + func (v *HistogramVec) WriteExposition(w io.Writer) bool { if v == nil { return true @@ -230,20 +395,17 @@ func (v *HistogramVec) WriteExposition(w io.Writer) bool { if !writeMetricHeader(w, v.name, v.help, "histogram") { return false } - for _, entry := range collectSeries[*Histogram](&v.series) { snap := entry.value.Snapshot() if len(snap.UpperBounds) != len(snap.Cumulative) { return false } - for i, ub := range snap.UpperBounds { bucketLabels := appendLabel(entry.labels, labelPair{name: "le", value: strconv.FormatFloat(ub, 'g', -1, 64)}) if !writeMetricSample(w, v.name+"_bucket", bucketLabels, strconv.FormatUint(snap.Cumulative[i], 10)) { return false } } - infLabels := appendLabel(entry.labels, labelPair{name: "le", value: "+Inf"}) if !writeMetricSample(w, v.name+"_bucket", infLabels, strconv.FormatUint(snap.Total, 10)) { return false @@ -255,7 +417,6 @@ func (v *HistogramVec) WriteExposition(w io.Writer) bool { return false } } - return true } @@ -265,13 +426,11 @@ func collectSeries[T any](series *sync.Map) []*seriesEntry[T] { if entry, ok := value.(*seriesEntry[T]); ok { entries = append(entries, entry) } - return true }) sort.Slice(entries, func(i, j int) bool { return entries[i].key < entries[j].key }) - return entries } @@ -281,7 +440,6 @@ func canonicalLabels(labels Labels) (string, []labelPair) { for _, pair := range pairs { _, _ = fmt.Fprintf(&key, "%d:%s=%d:%s;", len(pair.name), pair.name, len(pair.value), pair.value) } - return key.String(), pairs } @@ -289,7 +447,6 @@ func labelsFromMap(labels Labels) []labelPair { if len(labels) == 0 { return nil } - pairs := make([]labelPair, 0, len(labels)) for name, value := range labels { pairs = append(pairs, labelPair{name: name, value: value}) @@ -297,6 +454,5 @@ func labelsFromMap(labels Labels) []labelPair { sort.Slice(pairs, func(i, j int) bool { return pairs[i].name < pairs[j].name }) - return pairs } diff --git a/pkg/obsmetrics/vec_security_test.go b/pkg/obsmetrics/vec_security_test.go new file mode 100644 index 00000000..e6357c97 --- /dev/null +++ b/pkg/obsmetrics/vec_security_test.go @@ -0,0 +1,96 @@ +package obsmetrics + +import ( + "fmt" + "strings" + "sync" + "testing" +) + +func TestMetricVectorsEnforceSeriesLimit(t *testing.T) { + t.Parallel() + + options := VecOptions{MaxSeries: 2} + counter := NewCounterVecWithOptions("requests_total", "Requests", options) + gauge := NewGaugeVecWithOptions("active", "Active", options) + histogram := NewHistogramVecWithOptions("latency", "Latency", []float64{1}, options) + + for i := 0; i < 3; i++ { + labels := Labels{"id": fmt.Sprintf("%d", i)} + counter.Inc(labels) + gauge.Set(labels, float64(i)) + histogram.Observe(labels, float64(i)) + } + + for name, got := range map[string]int{ + "counter": counter.SeriesCount(), + "gauge": gauge.SeriesCount(), + "histogram": histogram.SeriesCount(), + } { + if got != 2 { + t.Fatalf("%s SeriesCount() = %d, want 2", name, got) + } + } + if counter.DroppedSeries() != 1 || gauge.DroppedSeries() != 1 || histogram.DroppedSeries() != 1 { + t.Fatalf("DroppedSeries() = (%d, %d, %d), want (1, 1, 1)", counter.DroppedSeries(), gauge.DroppedSeries(), histogram.DroppedSeries()) + } +} + +func TestMetricVectorRejectsOversizedLabelsBeforeRetention(t *testing.T) { + t.Parallel() + + vec := NewCounterVecWithOptions("requests_total", "Requests", VecOptions{ + MaxSeries: 8, + MaxLabels: 1, + MaxLabelNameBytes: 4, + MaxLabelValueBytes: 4, + }) + vec.Inc(Labels{"a": "1", "b": "2"}) + vec.Inc(Labels{"long-name": "1"}) + vec.Inc(Labels{"name": strings.Repeat("x", 5)}) + + if got := vec.SeriesCount(); got != 0 { + t.Fatalf("SeriesCount() = %d, want 0", got) + } + if got := vec.DroppedSeries(); got != 3 { + t.Fatalf("DroppedSeries() = %d, want 3", got) + } +} + +func TestMetricVectorConcurrentAdmissionNeverExceedsLimit(t *testing.T) { + t.Parallel() + + const limit = 8 + vec := NewCounterVecWithOptions("requests_total", "Requests", VecOptions{MaxSeries: limit}) + var wg sync.WaitGroup + for i := 0; i < 128; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + vec.Inc(Labels{"id": fmt.Sprintf("%d", i)}) + }(i) + } + wg.Wait() + + if got := vec.SeriesCount(); got != limit { + t.Fatalf("SeriesCount() = %d, want %d", got, limit) + } + if got := vec.DroppedSeries(); got != 128-limit { + t.Fatalf("DroppedSeries() = %d, want %d", got, 128-limit) + } +} + +func TestMetricVectorCompatibilityConstructorUsesSafeDefault(t *testing.T) { + t.Parallel() + + vec := NewCounterVec("requests_total", "Requests") + for i := 0; i <= DefaultMaxMetricSeries; i++ { + vec.Inc(Labels{"id": fmt.Sprintf("%d", i)}) + } + if got := vec.SeriesCount(); got != DefaultMaxMetricSeries { + t.Fatalf("SeriesCount() = %d, want %d", got, DefaultMaxMetricSeries) + } + if got := vec.DroppedSeries(); got != 1 { + t.Fatalf("DroppedSeries() = %d, want 1", got) + } +} diff --git a/pkg/promptguard/rulepack_match.go b/pkg/promptguard/rulepack_match.go index 328d6525..c3d8c10d 100644 --- a/pkg/promptguard/rulepack_match.go +++ b/pkg/promptguard/rulepack_match.go @@ -48,11 +48,13 @@ func (r *compiledRule) matchInput(segment textSegment, policy compiledPolicy) (s if strings.TrimSpace(text) == "" { return "", 0, false } - prefilterText := text - if r.View == viewRaw { - prefilterText = segment.Views.Norm - } - if !containsAllLiteralGroups(prefilterText, r.RequiredLiteralGroups) { + // Required literals are compiled in the matcher's own representation. A raw + // rule can intentionally contain compatibility characters, provider syntax, + // or other bytes that normalize to a different surface. Prefiltering those + // rules against the normalized view can therefore create false negatives. + // Keep the optimization for normalized/joined views, but always execute raw + // matchers against the raw surface. + if r.View != viewRaw && !containsAllLiteralGroups(text, r.RequiredLiteralGroups) { return "", 0, false } diff --git a/pkg/promptguard/rulepack_raw_security_test.go b/pkg/promptguard/rulepack_raw_security_test.go new file mode 100644 index 00000000..fd0a1095 --- /dev/null +++ b/pkg/promptguard/rulepack_raw_security_test.go @@ -0,0 +1,61 @@ +package promptguard + +import "testing" + +func TestRawRulesDoNotUseNormalizedLiteralPrefilter(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + rule rawRule + }{ + { + name: "regex compatibility characters", + rule: rawRule{ + ID: "raw-fullwidth-regex", + Family: "raw-fullwidth-regex", + Type: ruleTypeRegex, + Action: hitActionScore, + View: viewRaw, + Segments: []string{string(segmentPlain)}, + Pattern: `system`, + Weight: 1, + MaxOccurrences: 1, + }, + }, + { + name: "phrase compatibility characters", + rule: rawRule{ + ID: "raw-fullwidth-phrase", + Family: "raw-fullwidth-phrase", + Type: ruleTypePhrase, + Action: hitActionScore, + View: viewRaw, + Segments: []string{string(segmentPlain)}, + Phrases: []string{"system"}, + MatchMode: phraseMatchSubstring, + Weight: 1, + MaxOccurrences: 1, + }, + }, + } + + segment := textSegment{Kind: segmentPlain, Views: normalizeViews("system")} + if segment.Views.Raw == segment.Views.Norm { + t.Fatal("test fixture must normalize to a different representation") + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + compiled, err := compileRule(&tc.rule) + if err != nil { + t.Fatalf("compileRule() error = %v", err) + } + if matches := compiled.matchSegment(segment, compilePolicy(&rawRulepack{Version: 3}), 1); len(matches) != 1 { + t.Fatalf("matchSegment() matches = %d, want 1", len(matches)) + } + }) + } +} diff --git a/scripts/ci/agent_security_patch.py b/scripts/ci/agent_security_patch.py deleted file mode 100644 index b659a12f..00000000 --- a/scripts/ci/agent_security_patch.py +++ /dev/null @@ -1,805 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -ROOT = Path.cwd() - - -def replace_once(path: str, old: str, new: str) -> None: - target = ROOT / path - text = target.read_text(encoding="utf-8") - count = text.count(old) - if count != 1: - raise RuntimeError(f"{path}: expected one replacement target, found {count}") - target.write_text(text.replace(old, new, 1), encoding="utf-8") - - -def write(path: str, content: str) -> None: - target = ROOT / path - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(content, encoding="utf-8") - - -replace_once( - "pkg/promptguard/rulepack_match.go", - """\tprefilterText := text -\tif r.View == viewRaw { -\t\tprefilterText = segment.Views.Norm -\t} -\tif !containsAllLiteralGroups(prefilterText, r.RequiredLiteralGroups) { -\t\treturn \"\", 0, false -\t} -""", - """\t// Required literals are compiled in the matcher's own representation. A raw -\t// rule can intentionally contain compatibility characters, provider syntax, -\t// or other bytes that normalize to a different surface. Prefiltering those -\t// rules against the normalized view can therefore create false negatives. -\t// Keep the optimization for normalized/joined views, but always execute raw -\t// matchers against the raw surface. -\tif r.View != viewRaw && !containsAllLiteralGroups(text, r.RequiredLiteralGroups) { -\t\treturn \"\", 0, false -\t} -""", -) - -write( - "pkg/promptguard/rulepack_raw_security_test.go", - r'''package promptguard - -import "testing" - -func TestRawRulesDoNotUseNormalizedLiteralPrefilter(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - rule rawRule - }{ - { - name: "regex compatibility characters", - rule: rawRule{ - ID: "raw-fullwidth-regex", - Family: "raw-fullwidth-regex", - Type: ruleTypeRegex, - Action: hitActionScore, - View: viewRaw, - Segments: []string{string(segmentPlain)}, - Pattern: `system`, - Weight: 1, - MaxOccurrences: 1, - }, - }, - { - name: "phrase compatibility characters", - rule: rawRule{ - ID: "raw-fullwidth-phrase", - Family: "raw-fullwidth-phrase", - Type: ruleTypePhrase, - Action: hitActionScore, - View: viewRaw, - Segments: []string{string(segmentPlain)}, - Phrases: []string{"system"}, - MatchMode: phraseMatchSubstring, - Weight: 1, - MaxOccurrences: 1, - }, - }, - } - - segment := textSegment{Kind: segmentPlain, Views: normalizeViews("system")} - if segment.Views.Raw == segment.Views.Norm { - t.Fatal("test fixture must normalize to a different representation") - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - compiled, err := compileRule(&tc.rule) - if err != nil { - t.Fatalf("compileRule() error = %v", err) - } - if matches := compiled.matchSegment(segment, compilePolicy(&rawRulepack{Version: 3}), 1); len(matches) != 1 { - t.Fatalf("matchSegment() matches = %d, want 1", len(matches)) - } - }) - } -} -''', -) - -write( - "pkg/obsmetrics/vec.go", - r'''package obsmetrics - -import ( - "fmt" - "io" - "math" - "sort" - "strconv" - "strings" - "sync" - "sync/atomic" -) - -// Labels는 Prometheus label name/value 집합입니다. 렌더링 시 label name 기준으로 정렬됩니다. -type Labels map[string]string - -type labelPair struct { - name string - value string -} - -type seriesEntry[T any] struct { - key string - labels []labelPair - value T -} - -const ( - // DefaultMaxMetricSeries bounds the process-lifetime cardinality retained by - // every vector created through the compatibility constructors. - DefaultMaxMetricSeries = 1024 - // DefaultMaxMetricLabels bounds the number of labels in one series. - DefaultMaxMetricLabels = 16 - // DefaultMaxMetricLabelNameBytes and DefaultMaxMetricLabelValueBytes bound - // allocations performed while canonicalizing attacker-influenced labels. - DefaultMaxMetricLabelNameBytes = 128 - DefaultMaxMetricLabelValueBytes = 256 -) - -// VecOptions configures hard resource limits for a metric vector. Non-positive -// values select the safe defaults; there is intentionally no unbounded mode. -type VecOptions struct { - MaxSeries int - MaxLabels int - MaxLabelNameBytes int - MaxLabelValueBytes int -} - -type seriesLimiter struct { - maxSeries int64 - maxLabels int - maxLabelNameBytes int - maxLabelValueBytes int - count atomic.Int64 - dropped atomic.Uint64 -} - -func newSeriesLimiter(options VecOptions) seriesLimiter { - if options.MaxSeries <= 0 { - options.MaxSeries = DefaultMaxMetricSeries - } - if options.MaxLabels <= 0 { - options.MaxLabels = DefaultMaxMetricLabels - } - if options.MaxLabelNameBytes <= 0 { - options.MaxLabelNameBytes = DefaultMaxMetricLabelNameBytes - } - if options.MaxLabelValueBytes <= 0 { - options.MaxLabelValueBytes = DefaultMaxMetricLabelValueBytes - } - - return seriesLimiter{ - maxSeries: int64(options.MaxSeries), - maxLabels: options.MaxLabels, - maxLabelNameBytes: options.MaxLabelNameBytes, - maxLabelValueBytes: options.MaxLabelValueBytes, - } -} - -func (l *seriesLimiter) canonicalize(labels Labels) (string, []labelPair, bool) { - if len(labels) > l.maxLabels { - l.dropped.Add(1) - return "", nil, false - } - for name, value := range labels { - if len(name) > l.maxLabelNameBytes || len(value) > l.maxLabelValueBytes { - l.dropped.Add(1) - return "", nil, false - } - } - - key, pairs := canonicalLabels(labels) - return key, pairs, true -} - -func (l *seriesLimiter) reserve() bool { - for { - current := l.count.Load() - if current >= l.maxSeries { - l.dropped.Add(1) - return false - } - if l.count.CompareAndSwap(current, current+1) { - return true - } - } -} - -func (l *seriesLimiter) release() { - l.count.Add(-1) -} - -func (l *seriesLimiter) seriesCount() int { - return int(l.count.Load()) -} - -func (l *seriesLimiter) droppedSeries() uint64 { - return l.dropped.Load() -} - -func loadOrCreateSeries[T any]( - series *sync.Map, - limiter *seriesLimiter, - key string, - labels []labelPair, - create func() T, -) (T, bool) { - var zero T - if actual, ok := series.Load(key); ok { - stored, typed := actual.(*seriesEntry[T]) - if !typed { - return zero, false - } - return stored.value, true - } - if !limiter.reserve() { - return zero, false - } - - entry := &seriesEntry[T]{key: key, labels: labels, value: create()} - actual, loaded := series.LoadOrStore(key, entry) - if loaded { - limiter.release() - } - stored, ok := actual.(*seriesEntry[T]) - if !ok { - if !loaded { - series.Delete(key) - limiter.release() - } - return zero, false - } - return stored.value, true -} - -// CounterVec는 동일 metric name/help를 공유하는 label-set별 counter 집합입니다. -type CounterVec struct { - name string - help string - series sync.Map // map[string]*seriesEntry[*Counter] - limiter seriesLimiter -} - -// Counter는 원자적으로 증가하는 단일 counter series입니다. -type Counter struct { - value atomic.Uint64 -} - -func NewCounterVec(name, help string) *CounterVec { - return NewCounterVecWithOptions(name, help, VecOptions{}) -} - -func NewCounterVecWithOptions(name, help string, options VecOptions) *CounterVec { - return &CounterVec{name: name, help: help, limiter: newSeriesLimiter(options)} -} - -func (v *CounterVec) With(labels Labels) *Counter { - if v == nil { - return nil - } - key, labelPairs, ok := v.limiter.canonicalize(labels) - if !ok { - return nil - } - counter, ok := loadOrCreateSeries(&v.series, &v.limiter, key, labelPairs, func() *Counter { return &Counter{} }) - if !ok { - return nil - } - return counter -} - -func (v *CounterVec) Inc(labels Labels) { - v.Add(labels, 1) -} - -func (v *CounterVec) Add(labels Labels, delta uint64) { - counter := v.With(labels) - if counter != nil { - counter.Add(delta) - } -} - -func (v *CounterVec) SeriesCount() int { - if v == nil { - return 0 - } - return v.limiter.seriesCount() -} - -func (v *CounterVec) DroppedSeries() uint64 { - if v == nil { - return 0 - } - return v.limiter.droppedSeries() -} - -func (v *CounterVec) WriteExposition(w io.Writer) bool { - if v == nil { - return true - } - if !writeMetricHeader(w, v.name, v.help, "counter") { - return false - } - for _, entry := range collectSeries[*Counter](&v.series) { - if !writeMetricSample(w, v.name, entry.labels, strconv.FormatUint(entry.value.Value(), 10)) { - return false - } - } - return true -} - -func (c *Counter) Inc() { - c.Add(1) -} - -func (c *Counter) Add(delta uint64) { - if c != nil { - c.value.Add(delta) - } -} - -func (c *Counter) Value() uint64 { - if c == nil { - return 0 - } - return c.value.Load() -} - -// GaugeVec는 동일 metric name/help를 공유하는 label-set별 float64 gauge 집합입니다. -type GaugeVec struct { - name string - help string - series sync.Map // map[string]*seriesEntry[*Gauge] - limiter seriesLimiter -} - -// Gauge는 원자적으로 설정되는 단일 gauge series입니다. -type Gauge struct { - bits atomic.Uint64 -} - -func NewGaugeVec(name, help string) *GaugeVec { - return NewGaugeVecWithOptions(name, help, VecOptions{}) -} - -func NewGaugeVecWithOptions(name, help string, options VecOptions) *GaugeVec { - return &GaugeVec{name: name, help: help, limiter: newSeriesLimiter(options)} -} - -func (v *GaugeVec) With(labels Labels) *Gauge { - if v == nil { - return nil - } - key, labelPairs, ok := v.limiter.canonicalize(labels) - if !ok { - return nil - } - gauge, ok := loadOrCreateSeries(&v.series, &v.limiter, key, labelPairs, func() *Gauge { return &Gauge{} }) - if !ok { - return nil - } - return gauge -} - -func (v *GaugeVec) Set(labels Labels, value float64) { - gauge := v.With(labels) - if gauge != nil { - gauge.Set(value) - } -} - -func (v *GaugeVec) SeriesCount() int { - if v == nil { - return 0 - } - return v.limiter.seriesCount() -} - -func (v *GaugeVec) DroppedSeries() uint64 { - if v == nil { - return 0 - } - return v.limiter.droppedSeries() -} - -func (v *GaugeVec) WriteExposition(w io.Writer) bool { - if v == nil { - return true - } - if !writeMetricHeader(w, v.name, v.help, "gauge") { - return false - } - for _, entry := range collectSeries[*Gauge](&v.series) { - if !writeMetricSample(w, v.name, entry.labels, strconv.FormatFloat(entry.value.Value(), 'g', -1, 64)) { - return false - } - } - return true -} - -func (g *Gauge) Set(value float64) { - if g != nil { - g.bits.Store(math.Float64bits(value)) - } -} - -func (g *Gauge) Value() float64 { - if g == nil { - return 0 - } - return math.Float64frombits(g.bits.Load()) -} - -// HistogramVec는 동일 metric name/help/bucket을 공유하는 label-set별 histogram 집합입니다. -type HistogramVec struct { - name string - help string - buckets []float64 - series sync.Map // map[string]*seriesEntry[*Histogram] - limiter seriesLimiter -} - -func NewHistogramVec(name, help string, buckets []float64) *HistogramVec { - return NewHistogramVecWithOptions(name, help, buckets, VecOptions{}) -} - -func NewHistogramVecWithOptions(name, help string, buckets []float64, options VecOptions) *HistogramVec { - bounds := make([]float64, len(buckets)) - copy(bounds, buckets) - return &HistogramVec{name: name, help: help, buckets: bounds, limiter: newSeriesLimiter(options)} -} - -func (v *HistogramVec) With(labels Labels) *Histogram { - if v == nil { - return nil - } - key, labelPairs, ok := v.limiter.canonicalize(labels) - if !ok { - return nil - } - histogram, ok := loadOrCreateSeries(&v.series, &v.limiter, key, labelPairs, func() *Histogram { - return NewHistogram(v.buckets) - }) - if !ok { - return nil - } - return histogram -} - -func (v *HistogramVec) Observe(labels Labels, value float64) { - hist := v.With(labels) - if hist != nil { - hist.Observe(value) - } -} - -func (v *HistogramVec) SeriesCount() int { - if v == nil { - return 0 - } - return v.limiter.seriesCount() -} - -func (v *HistogramVec) DroppedSeries() uint64 { - if v == nil { - return 0 - } - return v.limiter.droppedSeries() -} - -func (v *HistogramVec) WriteExposition(w io.Writer) bool { - if v == nil { - return true - } - if !writeMetricHeader(w, v.name, v.help, "histogram") { - return false - } - for _, entry := range collectSeries[*Histogram](&v.series) { - snap := entry.value.Snapshot() - if len(snap.UpperBounds) != len(snap.Cumulative) { - return false - } - for i, ub := range snap.UpperBounds { - bucketLabels := appendLabel(entry.labels, labelPair{name: "le", value: strconv.FormatFloat(ub, 'g', -1, 64)}) - if !writeMetricSample(w, v.name+"_bucket", bucketLabels, strconv.FormatUint(snap.Cumulative[i], 10)) { - return false - } - } - infLabels := appendLabel(entry.labels, labelPair{name: "le", value: "+Inf"}) - if !writeMetricSample(w, v.name+"_bucket", infLabels, strconv.FormatUint(snap.Total, 10)) { - return false - } - if !writeMetricSample(w, v.name+"_sum", entry.labels, strconv.FormatFloat(snap.Sum, 'g', -1, 64)) { - return false - } - if !writeMetricSample(w, v.name+"_count", entry.labels, strconv.FormatUint(snap.Total, 10)) { - return false - } - } - return true -} - -func collectSeries[T any](series *sync.Map) []*seriesEntry[T] { - entries := make([]*seriesEntry[T], 0) - series.Range(func(_, value any) bool { - if entry, ok := value.(*seriesEntry[T]); ok { - entries = append(entries, entry) - } - return true - }) - sort.Slice(entries, func(i, j int) bool { - return entries[i].key < entries[j].key - }) - return entries -} - -func canonicalLabels(labels Labels) (string, []labelPair) { - pairs := labelsFromMap(labels) - var key strings.Builder - for _, pair := range pairs { - _, _ = fmt.Fprintf(&key, "%d:%s=%d:%s;", len(pair.name), pair.name, len(pair.value), pair.value) - } - return key.String(), pairs -} - -func labelsFromMap(labels Labels) []labelPair { - if len(labels) == 0 { - return nil - } - pairs := make([]labelPair, 0, len(labels)) - for name, value := range labels { - pairs = append(pairs, labelPair{name: name, value: value}) - } - sort.Slice(pairs, func(i, j int) bool { - return pairs[i].name < pairs[j].name - }) - return pairs -} -''', -) - -write( - "pkg/obsmetrics/vec_security_test.go", - r'''package obsmetrics - -import ( - "fmt" - "strings" - "sync" - "testing" -) - -func TestMetricVectorsEnforceSeriesLimit(t *testing.T) { - t.Parallel() - - options := VecOptions{MaxSeries: 2} - counter := NewCounterVecWithOptions("requests_total", "Requests", options) - gauge := NewGaugeVecWithOptions("active", "Active", options) - histogram := NewHistogramVecWithOptions("latency", "Latency", []float64{1}, options) - - for i := 0; i < 3; i++ { - labels := Labels{"id": fmt.Sprintf("%d", i)} - counter.Inc(labels) - gauge.Set(labels, float64(i)) - histogram.Observe(labels, float64(i)) - } - - for name, got := range map[string]int{ - "counter": counter.SeriesCount(), - "gauge": gauge.SeriesCount(), - "histogram": histogram.SeriesCount(), - } { - if got != 2 { - t.Fatalf("%s SeriesCount() = %d, want 2", name, got) - } - } - if counter.DroppedSeries() != 1 || gauge.DroppedSeries() != 1 || histogram.DroppedSeries() != 1 { - t.Fatalf("DroppedSeries() = (%d, %d, %d), want (1, 1, 1)", counter.DroppedSeries(), gauge.DroppedSeries(), histogram.DroppedSeries()) - } -} - -func TestMetricVectorRejectsOversizedLabelsBeforeRetention(t *testing.T) { - t.Parallel() - - vec := NewCounterVecWithOptions("requests_total", "Requests", VecOptions{ - MaxSeries: 8, - MaxLabels: 1, - MaxLabelNameBytes: 4, - MaxLabelValueBytes: 4, - }) - vec.Inc(Labels{"a": "1", "b": "2"}) - vec.Inc(Labels{"long-name": "1"}) - vec.Inc(Labels{"name": strings.Repeat("x", 5)}) - - if got := vec.SeriesCount(); got != 0 { - t.Fatalf("SeriesCount() = %d, want 0", got) - } - if got := vec.DroppedSeries(); got != 3 { - t.Fatalf("DroppedSeries() = %d, want 3", got) - } -} - -func TestMetricVectorConcurrentAdmissionNeverExceedsLimit(t *testing.T) { - t.Parallel() - - const limit = 8 - vec := NewCounterVecWithOptions("requests_total", "Requests", VecOptions{MaxSeries: limit}) - var wg sync.WaitGroup - for i := 0; i < 128; i++ { - wg.Add(1) - go func(i int) { - defer wg.Done() - vec.Inc(Labels{"id": fmt.Sprintf("%d", i)}) - }(i) - } - wg.Wait() - - if got := vec.SeriesCount(); got != limit { - t.Fatalf("SeriesCount() = %d, want %d", got, limit) - } - if got := vec.DroppedSeries(); got != 128-limit { - t.Fatalf("DroppedSeries() = %d, want %d", got, 128-limit) - } -} - -func TestMetricVectorCompatibilityConstructorUsesSafeDefault(t *testing.T) { - t.Parallel() - - vec := NewCounterVec("requests_total", "Requests") - for i := 0; i <= DefaultMaxMetricSeries; i++ { - vec.Inc(Labels{"id": fmt.Sprintf("%d", i)}) - } - if got := vec.SeriesCount(); got != DefaultMaxMetricSeries { - t.Fatalf("SeriesCount() = %d, want %d", got, DefaultMaxMetricSeries) - } - if got := vec.DroppedSeries(); got != 1 { - t.Fatalf("DroppedSeries() = %d, want 1", got) - } -} -''', -) - -replace_once( - "pkg/logging/sanitize.go", - 'var sensitiveExactKeys = map[string]struct{}{\n\t"token": {},', - 'var sensitiveExactKeys = map[string]struct{}{\n\t"key": {},\n\t"token": {},', -) -replace_once( - "pkg/logging/sanitize_test.go", - '\t\t{"key", "key", "apikey123", false},', - '\t\t{"key", "key", "apikey123", true},', -) - -replace_once( - "scripts/ci/check-workflow-secrets.sh", - """ kv = parse_key_value(raw) - if kv is not None: - _, value = kv - normalized = unquote_scalar(value) -""", - """ kv = parse_key_value(raw) - if kv is not None: - key, value = kv - source = strip_yaml_comment(raw).strip() - if source.startswith("- "): - source = source[2:].strip() - if source[:1] in {"'", '"'} and key.lower() in { - "permissions", "secrets", "jobs", "uses", "with", "persist-credentials" - }: - failures.append((line_no, f"quoted policy-sensitive YAML key {key!r} is unsupported")) - normalized = unquote_scalar(value) -""", -) -replace_once( - "scripts/ci/check-workflow-secrets.sh", - """ line = strip_yaml_comment(raw) - match = re.match(r"^\\s*([A-Za-z0-9_-]+)\\s*:\\s*([A-Za-z0-9_-]+)\\s*$", line) - if not match: - continue - key = match.group(1) - if key in seen: - return False - seen.add(key) - saw_entry = True - if match.group(2) not in {"read", "none"}: - return False -""", - """ parsed = parse_key_value(raw) - if parsed is None: - return False - key, raw_value = parsed - value = unquote_scalar(raw_value) - if not re.fullmatch(r"[A-Za-z0-9_-]+", value): - return False - if key in seen: - return False - seen.add(key) - saw_entry = True - if value not in {"read", "none"}: - return False -""", -) - -replace_once( - "scripts/ci/check-workflow-secrets_test.sh", - """if (( failures > 0 )); then -""", - r'''quoted_permissions="${TMP_DIR}/quoted-permissions.yml" -write_workflow "${quoted_permissions}" \ - "name: quoted-permissions" \ - "on:" \ - " pull_request:" \ - '"permissions":' \ - " contents: write" \ - "jobs:" \ - " test:" \ - " runs-on: ubuntu-latest" \ - " steps:" \ - " - run: echo ok" -expect_failure "quoted permissions key fails closed" "quoted policy-sensitive YAML key" "${quoted_permissions}" - -quoted_secrets="${TMP_DIR}/quoted-secrets.yml" -write_workflow "${quoted_secrets}" \ - "name: quoted-secrets" \ - "on:" \ - " pull_request:" \ - "permissions:" \ - " contents: read" \ - "jobs:" \ - " call:" \ - " uses: owner/repo/.github/workflows/reusable.yml@main" \ - ' "secrets": inherit' -expect_failure "quoted secrets key fails closed" "quoted policy-sensitive YAML key" "${quoted_secrets}" - -quoted_permission_scope="${TMP_DIR}/quoted-permission-scope.yml" -write_workflow "${quoted_permission_scope}" \ - "name: quoted-permission-scope" \ - "on:" \ - " pull_request:" \ - "permissions:" \ - " contents: read" \ - ' "pull-requests": write' \ - "jobs:" \ - " test:" \ - " runs-on: ubuntu-latest" \ - " steps:" \ - " - run: echo ok" -expect_failure "quoted permission scope cannot hide write" "read-only permissions" "${quoted_permission_scope}" - -if (( failures > 0 )); then -''', -) - -replace_once( - ".github/workflows/ci.yml", - """ - name: Workflow secret boundary - run: bash scripts/ci/check-workflow-secrets.sh - - - name: SQL ownership -""", - """ - name: Workflow secret boundary - run: bash scripts/ci/check-workflow-secrets.sh - - - name: Workflow secret boundary tests - run: bash scripts/ci/check-workflow-secrets_test.sh - - - name: SQL ownership -""", -) - -# The helper files must not appear in the final pull-request tree. -(ROOT / ".github/workflows/agent-security-patch.yml").unlink() -Path(__file__).unlink() diff --git a/scripts/ci/check-workflow-secrets.sh b/scripts/ci/check-workflow-secrets.sh index 95b8d228..f2b3fb73 100755 --- a/scripts/ci/check-workflow-secrets.sh +++ b/scripts/ci/check-workflow-secrets.sh @@ -157,7 +157,14 @@ def structural_yaml_failures(text: str) -> list[tuple[int, str]]: failures.append((line_no, "multi-line YAML flow collections are unsupported in workflow policy checks")) kv = parse_key_value(raw) if kv is not None: - _, value = kv + key, value = kv + source = strip_yaml_comment(raw).strip() + if source.startswith("- "): + source = source[2:].strip() + if source[:1] in {"'", '"'} and key.lower() in { + "permissions", "secrets", "jobs", "uses", "with", "persist-credentials" + }: + failures.append((line_no, f"quoted policy-sensitive YAML key {key!r} is unsupported")) normalized = unquote_scalar(value) if normalized.startswith("[") and not normalized.endswith("]"): failures.append((line_no, "multi-line YAML flow collections are unsupported in workflow policy checks")) @@ -400,16 +407,18 @@ def permissions_block_is_readonly(inline_value: str, entries: list[tuple[int, st for _, raw in entries: if not meaningful(raw): continue - line = strip_yaml_comment(raw) - match = re.match(r"^\s*([A-Za-z0-9_-]+)\s*:\s*([A-Za-z0-9_-]+)\s*$", line) - if not match: - continue - key = match.group(1) + parsed = parse_key_value(raw) + if parsed is None: + return False + key, raw_value = parsed + value = unquote_scalar(raw_value) + if not re.fullmatch(r"[A-Za-z0-9_-]+", value): + return False if key in seen: return False seen.add(key) saw_entry = True - if match.group(2) not in {"read", "none"}: + if value not in {"read", "none"}: return False return saw_entry diff --git a/scripts/ci/check-workflow-secrets_test.sh b/scripts/ci/check-workflow-secrets_test.sh index 60828dbb..5c4cf56e 100755 --- a/scripts/ci/check-workflow-secrets_test.sh +++ b/scripts/ci/check-workflow-secrets_test.sh @@ -492,6 +492,48 @@ write_workflow "${secrets_string_literal}" \ " - run: echo ok" expect_success "benign secrets string literal passes" "${secrets_string_literal}" +quoted_permissions="${TMP_DIR}/quoted-permissions.yml" +write_workflow "${quoted_permissions}" \ + "name: quoted-permissions" \ + "on:" \ + " pull_request:" \ + '"permissions":' \ + " contents: write" \ + "jobs:" \ + " test:" \ + " runs-on: ubuntu-latest" \ + " steps:" \ + " - run: echo ok" +expect_failure "quoted permissions key fails closed" "quoted policy-sensitive YAML key" "${quoted_permissions}" + +quoted_secrets="${TMP_DIR}/quoted-secrets.yml" +write_workflow "${quoted_secrets}" \ + "name: quoted-secrets" \ + "on:" \ + " pull_request:" \ + "permissions:" \ + " contents: read" \ + "jobs:" \ + " call:" \ + " uses: owner/repo/.github/workflows/reusable.yml@main" \ + ' "secrets": inherit' +expect_failure "quoted secrets key fails closed" "quoted policy-sensitive YAML key" "${quoted_secrets}" + +quoted_permission_scope="${TMP_DIR}/quoted-permission-scope.yml" +write_workflow "${quoted_permission_scope}" \ + "name: quoted-permission-scope" \ + "on:" \ + " pull_request:" \ + "permissions:" \ + " contents: read" \ + ' "pull-requests": write' \ + "jobs:" \ + " test:" \ + " runs-on: ubuntu-latest" \ + " steps:" \ + " - run: echo ok" +expect_failure "quoted permission scope cannot hide write" "read-only permissions" "${quoted_permission_scope}" + if (( failures > 0 )); then echo "[FAIL] workflow secret checker tests failed: ${failures}" >&2 exit 1 From d9d5f9cd4b32c2d1dc64f279173aa63fded7001a Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 13:55:56 +0900 Subject: [PATCH 11/38] ci: run workflow policy regression tests --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dfb3cbd6..22f62d0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: - name: Workflow secret boundary run: bash scripts/ci/check-workflow-secrets.sh + - name: Workflow secret boundary tests + run: bash scripts/ci/check-workflow-secrets_test.sh + - name: SQL ownership run: bash scripts/check-sql-ownership.sh From dda40bd502ab9c76b8d870c303886aa031a4bd00 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 13:56:04 +0900 Subject: [PATCH 12/38] chore: remove one-shot security patch workflow --- .github/workflows/agent-security-patch.yml | 121 --------------------- 1 file changed, 121 deletions(-) delete mode 100644 .github/workflows/agent-security-patch.yml diff --git a/.github/workflows/agent-security-patch.yml b/.github/workflows/agent-security-patch.yml deleted file mode 100644 index 547b2280..00000000 --- a/.github/workflows/agent-security-patch.yml +++ /dev/null @@ -1,121 +0,0 @@ -name: agent security patch - -on: - pull_request: - branches: - - main - -permissions: - contents: write - -concurrency: - group: agent-security-patch-shared-go - cancel-in-progress: false - -jobs: - apply-and-verify: - if: github.head_ref == 'agent/security-hardening-20260715' - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout patch branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: agent/security-hardening-20260715 - persist-credentials: true - - - name: Setup Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - with: - go-version-file: go.mod - cache-dependency-path: go.sum - - - name: Apply security patch - run: python3 scripts/ci/agent_security_patch.py - - - name: Preserve existing value-aware generic key policy - run: | - python3 - <<'PY' - from pathlib import Path - - path = Path("pkg/logging/sanitize.go") - text = path.read_text(encoding="utf-8") - old = 'var sensitiveExactKeys = map[string]struct{}{\n\t"key": {},\n\t"token": {},' - new = 'var sensitiveExactKeys = map[string]struct{}{\n\t"token": {},' - if text.count(old) != 1: - raise SystemExit("unexpected sensitiveExactKeys patch state") - path.write_text(text.replace(old, new, 1), encoding="utf-8") - - test_path = Path("pkg/logging/sanitize_test.go") - test_text = test_path.read_text(encoding="utf-8") - old_test = '\t\t{"key", "key", "apikey123", true},' - new_test = '\t\t{"key", "key", "apikey123", false},' - if test_text.count(old_test) != 1: - raise SystemExit("unexpected sanitize test patch state") - test_path.write_text(test_text.replace(old_test, new_test, 1), encoding="utf-8") - - diagnostic = Path(".agent-diagnostics/go-test.log") - diagnostic.unlink(missing_ok=True) - try: - diagnostic.parent.rmdir() - except OSError: - pass - PY - - - name: Format changed Go files - run: | - gofmt -w \ - pkg/promptguard/rulepack_match.go \ - pkg/promptguard/rulepack_raw_security_test.go \ - pkg/obsmetrics/vec.go \ - pkg/obsmetrics/vec_security_test.go \ - pkg/logging/sanitize.go \ - pkg/logging/sanitize_test.go - - - name: Verify workflow policy regression suite - run: bash scripts/ci/check-workflow-secrets_test.sh - - - name: Run Go tests - run: | - set +e - go test -count=1 ./... > /tmp/agent-go-test.log 2>&1 - status=$? - cat /tmp/agent-go-test.log - exit "$status" - - - name: Run Go vet - run: go vet ./... - - - name: Validate patch - run: | - git diff --check - if git diff --quiet; then - echo "security patch produced no changes" >&2 - exit 1 - fi - - - name: Persist failure diagnostics - if: failure() - run: | - mkdir -p .agent-diagnostics - cp /tmp/agent-go-test.log .agent-diagnostics/go-test.log 2>/dev/null || echo "test log unavailable" > .agent-diagnostics/go-test.log - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add .agent-diagnostics/go-test.log - if ! git diff --cached --quiet; then - git commit -m "chore: record security patch test failure" - git push origin HEAD:agent/security-hardening-20260715 - fi - - - name: Commit verified non-workflow patch - if: success() - run: | - # GitHub's workflow token is intentionally not used to rewrite workflow - # definitions. Restore those two paths, commit the already-verified code - # and tests, then the trusted GitHub connector will finalize CI policy. - git checkout HEAD -- .github/workflows/ci.yml .github/workflows/agent-security-patch.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "security: harden shared library boundaries" - git push origin HEAD:agent/security-hardening-20260715 From 771201602dd0f146651baa9da6aaeb0ab4c96e23 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 04:56:18 +0000 Subject: [PATCH 13/38] chore: record security patch test failure --- .agent-diagnostics/go-test.log | 1 + 1 file changed, 1 insertion(+) create mode 100644 .agent-diagnostics/go-test.log diff --git a/.agent-diagnostics/go-test.log b/.agent-diagnostics/go-test.log new file mode 100644 index 00000000..76d4414c --- /dev/null +++ b/.agent-diagnostics/go-test.log @@ -0,0 +1 @@ +test log unavailable From 67a4141df63ad1db2503c463ea3bb1dcb14683e2 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 14:31:20 +0900 Subject: [PATCH 14/38] chore: remove temporary security diagnostics --- .agent-diagnostics/go-test.log | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .agent-diagnostics/go-test.log diff --git a/.agent-diagnostics/go-test.log b/.agent-diagnostics/go-test.log deleted file mode 100644 index 76d4414c..00000000 --- a/.agent-diagnostics/go-test.log +++ /dev/null @@ -1 +0,0 @@ -test log unavailable From e969c550ec9e03961f11335fe2e9a0d6632d5c6c Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 15:29:40 +0900 Subject: [PATCH 15/38] chore: verify generic key literal redaction --- .../workflows/agent-secret-literal-fix.yml | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 .github/workflows/agent-secret-literal-fix.yml diff --git a/.github/workflows/agent-secret-literal-fix.yml b/.github/workflows/agent-secret-literal-fix.yml new file mode 100644 index 00000000..40f94ef5 --- /dev/null +++ b/.github/workflows/agent-secret-literal-fix.yml @@ -0,0 +1,108 @@ +name: agent secret literal fix + +on: + pull_request: + branches: + - main + +permissions: + contents: write + +concurrency: + group: agent-secret-literal-fix + cancel-in-progress: false + +jobs: + apply-and-verify: + if: github.head_ref == 'agent/security-hardening-20260715' + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout patch branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: agent/security-hardening-20260715 + persist-credentials: true + + - name: Setup Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Apply literal secret redaction patch + run: | + python3 - <<'PY' + from pathlib import Path + + source = Path('pkg/logging/sanitize.go') + text = source.read_text(encoding='utf-8') + old = '''func isSecretLikeValue(v string) bool { + \tif hasSecretLikePrefix(v) { + \t\treturn true + \t} + \treturn isHighEntropyToken(v) + } + ''' + new = '''var secretLikeExactValues = map[string]struct{}{ + \t"access-token": {}, + \t"access_token": {}, + \t"api-key": {}, + \t"api_key": {}, + \t"private-key": {}, + \t"private_key": {}, + \t"refresh-token": {}, + \t"refresh_token": {}, + \t"secret-key": {}, + \t"secret_key": {}, + } + + func isSecretLikeValue(v string) bool { + \tif _, ok := secretLikeExactValues[strings.ToLower(strings.TrimSpace(v))]; ok { + \t\treturn true + \t} + \tif hasSecretLikePrefix(v) { + \t\treturn true + \t} + \treturn isHighEntropyToken(v) + } + ''' + if text.count(old) != 1: + raise SystemExit('unexpected sanitize.go patch state') + source.write_text(text.replace(old, new, 1), encoding='utf-8') + + test = Path('pkg/logging/sanitize_keyvalue_test.go') + test_text = test.read_text(encoding='utf-8') + old_values = '''\tvalues := []string{ + \t\t"sk_live_" + "FAKEvalueNotARealStripeKey", + ''' + new_values = '''\tvalues := []string{ + \t\t"secret-key", + \t\t"secret_key", + \t\t"api-key", + \t\t"private-key", + \t\t"access-token", + \t\t"refresh-token", + \t\t"sk_live_" + "FAKEvalueNotARealStripeKey", + ''' + if test_text.count(old_values) != 1: + raise SystemExit('unexpected sanitize_keyvalue_test.go patch state') + test.write_text(test_text.replace(old_values, new_values, 1), encoding='utf-8') + PY + gofmt -w pkg/logging/sanitize.go pkg/logging/sanitize_keyvalue_test.go + + - name: Verify + run: | + bash scripts/ci/check-workflow-secrets_test.sh + go test -count=1 ./... + go vet ./... + git diff --check + + - name: Commit verified code patch + run: | + git checkout HEAD -- .github/workflows/agent-secret-literal-fix.yml + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add pkg/logging/sanitize.go pkg/logging/sanitize_keyvalue_test.go + git commit -m 'security: redact literal secret values in generic key fields' + git push origin HEAD:agent/security-hardening-20260715 From a9815a1feffe595819500aa4a9d8c6afd365415b Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 15:44:17 +0900 Subject: [PATCH 16/38] security: redact literal secret values in generic key fields --- pkg/logging/sanitize.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/logging/sanitize.go b/pkg/logging/sanitize.go index 82b32305..47c310de 100644 --- a/pkg/logging/sanitize.go +++ b/pkg/logging/sanitize.go @@ -248,9 +248,25 @@ var secretLikePrefixes = []string{ "eyj", } +var secretLikeExactValues = map[string]struct{}{ + "access-token": {}, + "access_token": {}, + "api-key": {}, + "api_key": {}, + "private-key": {}, + "private_key": {}, + "refresh-token": {}, + "refresh_token": {}, + "secret-key": {}, + "secret_key": {}, +} + const secretLikeMinLen = 24 func isSecretLikeValue(v string) bool { + if _, ok := secretLikeExactValues[strings.ToLower(strings.TrimSpace(v))]; ok { + return true + } if hasSecretLikePrefix(v) { return true } From e30dcbaf3c33ac577e89201d8f9b434ef5b4676c Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 15:44:40 +0900 Subject: [PATCH 17/38] test: cover literal secret values in generic key fields --- pkg/logging/sanitize_literal_secret_test.go | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 pkg/logging/sanitize_literal_secret_test.go diff --git a/pkg/logging/sanitize_literal_secret_test.go b/pkg/logging/sanitize_literal_secret_test.go new file mode 100644 index 00000000..9e2b3758 --- /dev/null +++ b/pkg/logging/sanitize_literal_secret_test.go @@ -0,0 +1,36 @@ +package logging + +import ( + "strings" + "testing" +) + +func TestSanitizeKey_LiteralSecretNamesAreMasked(t *testing.T) { + t.Parallel() + + for _, value := range []string{ + "secret-key", + "secret_key", + "api-key", + "api_key", + "private-key", + "private_key", + "access-token", + "access_token", + "refresh-token", + "refresh_token", + } { + value := value + t.Run(value, func(t *testing.T) { + t.Parallel() + + output := keyFieldOutput(t, "key", value) + if strings.Contains(output, value) { + t.Fatalf("literal secret value %q leaked in output: %s", value, output) + } + if !strings.Contains(output, "***REDACTED***") { + t.Fatalf("literal secret value %q was not redacted: %s", value, output) + } + }) + } +} From a1c450ad4b785c1f84c179e5465c2a72bc74a191 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 15:45:00 +0900 Subject: [PATCH 18/38] chore: remove temporary secret literal workflow --- .../workflows/agent-secret-literal-fix.yml | 108 ------------------ 1 file changed, 108 deletions(-) delete mode 100644 .github/workflows/agent-secret-literal-fix.yml diff --git a/.github/workflows/agent-secret-literal-fix.yml b/.github/workflows/agent-secret-literal-fix.yml deleted file mode 100644 index 40f94ef5..00000000 --- a/.github/workflows/agent-secret-literal-fix.yml +++ /dev/null @@ -1,108 +0,0 @@ -name: agent secret literal fix - -on: - pull_request: - branches: - - main - -permissions: - contents: write - -concurrency: - group: agent-secret-literal-fix - cancel-in-progress: false - -jobs: - apply-and-verify: - if: github.head_ref == 'agent/security-hardening-20260715' - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout patch branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: agent/security-hardening-20260715 - persist-credentials: true - - - name: Setup Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - with: - go-version-file: go.mod - cache-dependency-path: go.sum - - - name: Apply literal secret redaction patch - run: | - python3 - <<'PY' - from pathlib import Path - - source = Path('pkg/logging/sanitize.go') - text = source.read_text(encoding='utf-8') - old = '''func isSecretLikeValue(v string) bool { - \tif hasSecretLikePrefix(v) { - \t\treturn true - \t} - \treturn isHighEntropyToken(v) - } - ''' - new = '''var secretLikeExactValues = map[string]struct{}{ - \t"access-token": {}, - \t"access_token": {}, - \t"api-key": {}, - \t"api_key": {}, - \t"private-key": {}, - \t"private_key": {}, - \t"refresh-token": {}, - \t"refresh_token": {}, - \t"secret-key": {}, - \t"secret_key": {}, - } - - func isSecretLikeValue(v string) bool { - \tif _, ok := secretLikeExactValues[strings.ToLower(strings.TrimSpace(v))]; ok { - \t\treturn true - \t} - \tif hasSecretLikePrefix(v) { - \t\treturn true - \t} - \treturn isHighEntropyToken(v) - } - ''' - if text.count(old) != 1: - raise SystemExit('unexpected sanitize.go patch state') - source.write_text(text.replace(old, new, 1), encoding='utf-8') - - test = Path('pkg/logging/sanitize_keyvalue_test.go') - test_text = test.read_text(encoding='utf-8') - old_values = '''\tvalues := []string{ - \t\t"sk_live_" + "FAKEvalueNotARealStripeKey", - ''' - new_values = '''\tvalues := []string{ - \t\t"secret-key", - \t\t"secret_key", - \t\t"api-key", - \t\t"private-key", - \t\t"access-token", - \t\t"refresh-token", - \t\t"sk_live_" + "FAKEvalueNotARealStripeKey", - ''' - if test_text.count(old_values) != 1: - raise SystemExit('unexpected sanitize_keyvalue_test.go patch state') - test.write_text(test_text.replace(old_values, new_values, 1), encoding='utf-8') - PY - gofmt -w pkg/logging/sanitize.go pkg/logging/sanitize_keyvalue_test.go - - - name: Verify - run: | - bash scripts/ci/check-workflow-secrets_test.sh - go test -count=1 ./... - go vet ./... - git diff --check - - - name: Commit verified code patch - run: | - git checkout HEAD -- .github/workflows/agent-secret-literal-fix.yml - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add pkg/logging/sanitize.go pkg/logging/sanitize_keyvalue_test.go - git commit -m 'security: redact literal secret values in generic key fields' - git push origin HEAD:agent/security-hardening-20260715 From 5fac8b273237125df1f610c9ca94ba83d9d102e4 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 16:02:53 +0900 Subject: [PATCH 19/38] chore: add temporary lint diagnostics --- .github/workflows/agent-lint-diagnose.yml | 47 +++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/agent-lint-diagnose.yml diff --git a/.github/workflows/agent-lint-diagnose.yml b/.github/workflows/agent-lint-diagnose.yml new file mode 100644 index 00000000..d130df09 --- /dev/null +++ b/.github/workflows/agent-lint-diagnose.yml @@ -0,0 +1,47 @@ +name: agent lint diagnose + +on: + push: + branches: + - agent/security-hardening-20260715 + paths-ignore: + - .agent-diagnostics/** + +permissions: + contents: write + +concurrency: + group: agent-lint-diagnose-shared-go + cancel-in-progress: true + +jobs: + diagnose: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout patch branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: agent/security-hardening-20260715 + persist-credentials: true + + - name: Setup Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Capture lint output + run: | + set +e + make lint > /tmp/lint.log 2>&1 + status=$? + cat /tmp/lint.log + mkdir -p .agent-diagnostics + cp /tmp/lint.log .agent-diagnostics/lint.log + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .agent-diagnostics/lint.log + git commit -m "chore: record lint diagnostics" + git push origin HEAD:agent/security-hardening-20260715 + exit "$status" From a2fed7e11be43f540719d5613b7cb6802531c1d7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:05:08 +0000 Subject: [PATCH 20/38] chore: record lint diagnostics --- .agent-diagnostics/lint.log | 219 ++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 .agent-diagnostics/lint.log diff --git a/.agent-diagnostics/lint.log b/.agent-diagnostics/lint.log new file mode 100644 index 00000000..c6f348ed --- /dev/null +++ b/.agent-diagnostics/lint.log @@ -0,0 +1,219 @@ +bash /home/runner/work/shared-go/shared-go/scripts/ci/go-tooling.sh golangci-lint run -c .golangci.yml ./... +[GO TOOLING] Installing golangci-lint@v2.12.2 +go: downloading github.com/golangci/golangci-lint/v2 v2.12.2 +go: downloading charm.land/lipgloss/v2 v2.0.3 +go: downloading github.com/fatih/color v1.19.0 +go: downloading github.com/gofrs/flock v0.13.0 +go: downloading github.com/ldez/grignotin v0.10.1 +go: downloading github.com/pelletier/go-toml v1.9.5 +go: downloading github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 +go: downloading github.com/spf13/cobra v1.10.2 +go: downloading github.com/spf13/pflag v1.0.10 +go: downloading github.com/spf13/viper v1.12.0 +go: downloading go.yaml.in/yaml/v3 v3.0.4 +go: downloading golang.org/x/mod v0.35.0 +go: downloading github.com/charmbracelet/colorprofile v0.4.3 +go: downloading github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 +go: downloading github.com/charmbracelet/x/ansi v0.11.7 +go: downloading github.com/charmbracelet/x/term v0.2.2 +go: downloading github.com/clipperhouse/displaywidth v0.11.0 +go: downloading github.com/lucasb-eyer/go-colorful v1.4.0 +go: downloading github.com/rivo/uniseg v0.4.7 +go: downloading github.com/mattn/go-colorable v0.1.14 +go: downloading github.com/mattn/go-isatty v0.0.20 +go: downloading golang.org/x/tools v0.44.0 +go: downloading github.com/go-viper/mapstructure/v2 v2.5.0 +go: downloading github.com/hashicorp/go-version v1.9.0 +go: downloading github.com/mitchellh/go-homedir v1.1.0 +go: downloading github.com/alecthomas/chroma/v2 v2.24.1 +go: downloading github.com/rogpeppe/go-internal v1.14.1 +go: downloading github.com/golangci/plugin-module-register v0.1.2 +go: downloading github.com/sirupsen/logrus v1.9.4 +go: downloading github.com/stretchr/testify v1.11.1 +go: downloading github.com/go-xmlfmt/xmlfmt v1.1.3 +go: downloading github.com/golangci/revgrep v0.8.0 +go: downloading golang.org/x/sys v0.43.0 +go: downloading golang.org/x/text v0.36.0 +go: downloading github.com/fsnotify/fsnotify v1.5.4 +go: downloading github.com/mitchellh/mapstructure v1.5.0 +go: downloading github.com/spf13/afero v1.15.0 +go: downloading github.com/spf13/cast v1.5.0 +go: downloading github.com/spf13/jwalterweatherman v1.1.0 +go: downloading github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e +go: downloading github.com/charmbracelet/x/termios v0.1.1 +go: downloading github.com/charmbracelet/x/windows v0.2.2 +go: downloading github.com/muesli/cancelreader v0.2.2 +go: downloading golang.org/x/sync v0.20.0 +go: downloading github.com/clipperhouse/uax29/v2 v2.7.0 +go: downloading github.com/mattn/go-runewidth v0.0.23 +go: downloading github.com/daixiang0/gci v0.13.7 +go: downloading github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d +go: downloading mvdan.cc/gofumpt v0.9.2 +go: downloading github.com/golangci/golines v0.15.0 +go: downloading github.com/sourcegraph/go-diff v0.8.0 +go: downloading github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e +go: downloading go.augendre.info/arangolint v0.4.0 +go: downloading github.com/alingse/asasalint v0.0.11 +go: downloading github.com/golangci/asciicheck v0.5.0 +go: downloading github.com/breml/bidichk v0.3.3 +go: downloading github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 +go: downloading github.com/lasiar/canonicalheader v1.1.2 +go: downloading github.com/ClickHouse/clickhouse-go-linter v1.2.0 +go: downloading github.com/sivchari/containedctx v1.0.3 +go: downloading github.com/kkHAIKE/contextcheck v1.1.6 +go: downloading github.com/karamaru-alpha/copyloopvar v1.2.2 +go: downloading github.com/bkielbasa/cyclop v1.2.3 +go: downloading gitlab.com/bosi/decorder v0.4.2 +go: downloading github.com/OpenPeeDeeP/depguard/v2 v2.2.1 +go: downloading github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 +go: downloading github.com/Abirdcfly/dupword v0.1.7 +go: downloading github.com/charithe/durationcheck v0.0.11 +go: downloading github.com/manuelarte/embeddedstructfieldcheck v0.4.0 +go: downloading github.com/Djarvur/go-err113 v0.1.1 +go: downloading github.com/kisielk/errcheck v1.10.0 +go: downloading github.com/breml/errchkjson v0.4.1 +go: downloading github.com/Antonboom/errname v1.1.1 +go: downloading codeberg.org/polyfloyd/go-errorlint v1.9.0 +go: downloading github.com/nishanths/exhaustive v0.12.0 +go: downloading dev.gaijin.team/go/exhaustruct/v4 v4.0.0 +go: downloading github.com/ldez/exptostd v0.4.5 +go: downloading go.augendre.info/fatcontext v0.9.0 +go: downloading github.com/ashanbrown/forbidigo/v2 v2.3.1 +go: downloading github.com/gostaticanalysis/forcetypeassert v0.2.0 +go: downloading github.com/manuelarte/funcorder v0.6.0 +go: downloading github.com/ultraware/funlen v0.2.0 +go: downloading github.com/nunnatsa/ginkgolinter v0.23.0 +go: downloading 4d63.com/gocheckcompilerdirectives v1.3.0 +go: downloading 4d63.com/gochecknoglobals v0.2.2 +go: downloading github.com/alecthomas/go-check-sumtype v0.3.1 +go: downloading github.com/uudashr/gocognit v1.2.1 +go: downloading github.com/jgautheron/goconst v1.10.0 +go: downloading github.com/go-critic/go-critic v0.14.3 +go: downloading github.com/quasilyte/go-ruleguard/dsl v0.3.23 +go: downloading github.com/quasilyte/go-ruleguard v0.4.5 +go: downloading github.com/fzipp/gocyclo v0.6.0 +go: downloading github.com/godoc-lint/godoc-lint v0.11.2 +go: downloading github.com/tetafro/godot v1.5.6 +go: downloading github.com/matoous/godox v1.1.0 +go: downloading github.com/denis-tingaikin/go-header v0.5.0 +go: downloading github.com/ldez/gomoddirectives v0.8.0 +go: downloading github.com/Masterminds/semver/v3 v3.5.0 +go: downloading github.com/ryancurrah/gomodguard v1.4.1 +go: downloading github.com/ryancurrah/gomodguard/v2 v2.1.3 +go: downloading github.com/golangci/go-printf-func-name v0.1.1 +go: downloading github.com/securego/gosec/v2 v2.26.1 +go: downloading github.com/xen0n/gosmopolitan v1.3.0 +go: downloading github.com/leonklingele/grouper v1.1.2 +go: downloading github.com/uudashr/iface v1.4.2 +go: downloading github.com/julz/importas v0.2.0 +go: downloading github.com/macabu/inamedparam v0.2.0 +go: downloading github.com/gordonklaus/ineffassign v0.2.0 +go: downloading github.com/sashamelentyev/interfacebloat v1.1.0 +go: downloading github.com/ckaznocha/intrange v0.3.1 +go: downloading github.com/AdminBenni/iota-mixing v1.0.0 +go: downloading github.com/butuzov/ireturn v0.4.1 +go: downloading github.com/timonwong/loggercheck v0.11.0 +go: downloading github.com/yagipy/maintidx v1.0.0 +go: downloading github.com/ashanbrown/makezero/v2 v2.2.1 +go: downloading github.com/butuzov/mirror v1.3.0 +go: downloading github.com/golangci/misspell v0.8.0 +go: downloading github.com/tommy-muehle/go-mnd/v2 v2.5.1 +go: downloading go-simpler.org/musttag v0.14.0 +go: downloading github.com/alexkohler/nakedret/v2 v2.0.6 +go: downloading github.com/nakabonne/nestif v0.3.1 +go: downloading github.com/gostaticanalysis/nilerr v0.1.2 +go: downloading github.com/alingse/nilnesserr v0.2.0 +go: downloading github.com/Antonboom/nilnil v1.1.1 +go: downloading github.com/ssgreg/nlreturn/v2 v2.2.1 +go: downloading github.com/sonatard/noctx v0.5.1 +go: downloading github.com/AlwxSin/noinlineerr v1.0.5 +go: downloading github.com/firefart/nonamedreturns v1.0.6 +go: downloading github.com/stbenjam/no-sprintf-host-port v0.3.1 +go: downloading github.com/kunwardeep/paralleltest v1.0.15 +go: downloading github.com/catenacyber/perfsprint v0.10.1 +go: downloading github.com/alexkohler/prealloc v1.1.0 +go: downloading github.com/nishanths/predeclared v0.2.2 +go: downloading github.com/yeya24/promlinter v0.3.0 +go: downloading github.com/ghostiam/protogetter v0.3.20 +go: downloading github.com/curioswitch/go-reassign v0.3.0 +go: downloading github.com/raeperd/recvcheck v0.2.0 +go: downloading github.com/BurntSushi/toml v1.6.0 +go: downloading github.com/mgechev/revive v1.15.0 +go: downloading github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba +go: downloading go-simpler.org/sloglint v0.12.0 +go: downloading github.com/jjti/go-spancheck v0.6.5 +go: downloading github.com/ryanrolds/sqlclosecheck v0.6.0 +go: downloading honnef.co/go/tools v0.7.0 +go: downloading github.com/4meepo/tagalign v1.4.3 +go: downloading github.com/ldez/tagliatelle v0.7.2 +go: downloading github.com/maratori/testableexamples v1.0.1 +go: downloading github.com/Antonboom/testifylint v1.6.4 +go: downloading github.com/maratori/testpackage v1.1.2 +go: downloading github.com/kulti/thelper v0.7.1 +go: downloading github.com/moricho/tparallel v0.3.2 +go: downloading github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e +go: downloading mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 +go: downloading github.com/MirrexOne/unqueryvet v1.5.4 +go: downloading github.com/sashamelentyev/usestdlibvars v1.29.0 +go: downloading github.com/ldez/usetesting v0.5.0 +go: downloading github.com/blizzy78/varnamelen v0.8.0 +go: downloading github.com/sanposhiho/wastedassign/v2 v2.1.0 +go: downloading github.com/ultraware/whitespace v0.2.0 +go: downloading github.com/tomarrell/wrapcheck/v2 v2.12.0 +go: downloading github.com/bombsimon/wsl/v4 v4.7.0 +go: downloading github.com/bombsimon/wsl/v5 v5.8.0 +go: downloading github.com/ykadowak/zerologlint v0.1.5 +go: downloading github.com/davecgh/go-spew v1.1.1 +go: downloading github.com/pmezard/go-difflib v1.0.0 +go: downloading github.com/stretchr/objx v0.5.2 +go: downloading github.com/subosito/gotenv v1.4.1 +go: downloading github.com/hashicorp/hcl v1.0.0 +go: downloading gopkg.in/ini.v1 v1.67.0 +go: downloading github.com/magiconair/properties v1.8.6 +go: downloading go.uber.org/zap v1.27.0 +go: downloading github.com/hexops/gotextdiff v1.0.3 +go: downloading github.com/google/go-cmp v0.7.0 +go: downloading github.com/dave/dst v0.27.3 +go: downloading github.com/dlclark/regexp2 v1.12.0 +go: downloading github.com/gostaticanalysis/analysisutil v0.7.1 +go: downloading github.com/go-toolsmith/astcast v1.1.0 +go: downloading github.com/gobwas/glob v0.2.3 +go: downloading dev.gaijin.team/go/golib v0.6.0 +go: downloading github.com/go-toolsmith/astcopy v1.1.0 +go: downloading github.com/go-toolsmith/astequal v1.2.0 +go: downloading github.com/go-toolsmith/astfmt v1.1.0 +go: downloading github.com/go-toolsmith/astp v1.1.0 +go: downloading github.com/go-toolsmith/strparse v1.1.0 +go: downloading github.com/go-toolsmith/typep v1.1.0 +go: downloading github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 +go: downloading github.com/ccojocar/zxcvbn-go v1.0.4 +go: downloading github.com/gostaticanalysis/comment v1.5.0 +go: downloading github.com/prometheus/client_golang v1.12.1 +go: downloading github.com/prometheus/client_model v0.2.0 +go: downloading github.com/fatih/structtag v1.2.0 +go: downloading github.com/ettle/strcase v0.2.0 +go: downloading github.com/alfatraining/structtag v1.0.0 +go: downloading github.com/hashicorp/go-immutable-radix/v2 v2.1.0 +go: downloading go.uber.org/multierr v1.10.0 +go: downloading github.com/ldez/structtags v0.6.1 +go: downloading golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 +go: downloading github.com/quasilyte/gogrep v0.5.0 +go: downloading golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b +go: downloading github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 +go: downloading github.com/golang/protobuf v1.5.3 +go: downloading codeberg.org/chavacava/garif v0.2.0 +go: downloading github.com/beorn7/perks v1.0.1 +go: downloading github.com/prometheus/common v0.32.1 +go: downloading github.com/prometheus/procfs v0.7.3 +go: downloading google.golang.org/protobuf v1.36.10 +go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7 +go: downloading github.com/matttproud/golang_protobuf_extensions v1.0.1 +pkg/logging/sanitize.go:34:29: string `private_key` has 6 occurrences, make it a constant (goconst) + "client_secret", "secret", "private_key", "secret_key", + ^ +pkg/logging/sanitize.go:255:2: string `api_key` has 5 occurrences, but such constant `tokenAPIKey` already exists (goconst) + "api_key": {}, + ^ +2 issues: +* goconst: 2 +make: *** [Makefile:23: lint] Error 1 From cc3bc54b69235dc1451800748aadee5bccb9147d Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 16:07:59 +0900 Subject: [PATCH 21/38] ci: apply and verify shared-go lint fix --- .github/workflows/agent-lint-diagnose.yml | 96 +++++++++++++++++++++-- 1 file changed, 88 insertions(+), 8 deletions(-) diff --git a/.github/workflows/agent-lint-diagnose.yml b/.github/workflows/agent-lint-diagnose.yml index d130df09..c2167b74 100644 --- a/.github/workflows/agent-lint-diagnose.yml +++ b/.github/workflows/agent-lint-diagnose.yml @@ -17,7 +17,7 @@ concurrency: jobs: diagnose: runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 30 steps: - name: Checkout patch branch uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 @@ -31,17 +31,97 @@ jobs: go-version-file: go.mod cache-dependency-path: go.sum - - name: Capture lint output + - name: Apply lint correction + run: | + python3 - <<'PY' + from pathlib import Path + + def replace_once(path: str, old: str, new: str) -> None: + target = Path(path) + text = target.read_text(encoding="utf-8") + if text.count(old) != 1: + raise SystemExit(f"{path}: expected one replacement target") + target.write_text(text.replace(old, new, 1), encoding="utf-8") + + replace_once( + "pkg/logging/sanitize.go", + 'const (\n\ttokenAPIKey = "api_key"\n\ttokenAPIKeyCompact = "apikey"\n)', + 'const (\n\ttokenAPIKey = "api_key"\n\ttokenAPIKeyCompact = "apikey"\n\ttokenPrivateKey = "private_key"\n)', + ) + replace_once( + "pkg/logging/sanitize.go", + '\t"client_secret", "secret", "private_key", "secret_key",', + '\t"client_secret", "secret", tokenPrivateKey, "secret_key",', + ) + replace_once( + "pkg/logging/sanitize.go", + '\t"private_key": {},', + '\ttokenPrivateKey: {},', + ) + replace_once( + "pkg/logging/sanitize.go", + '\t"api_key": {},', + '\ttokenAPIKey: {},', + ) + replace_once( + "pkg/logging/sanitize.go", + '\t"private_key": {},', + '\ttokenPrivateKey: {},', + ) + replace_once( + "pkg/logging/sanitize_literal_secret_test.go", + '\t\t"api_key",', + '\t\ttokenAPIKey,', + ) + replace_once( + "pkg/logging/sanitize_literal_secret_test.go", + '\t\t"private_key",', + '\t\ttokenPrivateKey,', + ) + + diagnostic = Path(".agent-diagnostics/lint.log") + diagnostic.unlink(missing_ok=True) + try: + diagnostic.parent.rmdir() + except OSError: + pass + PY + gofmt -w pkg/logging/sanitize.go pkg/logging/sanitize_literal_secret_test.go + + - name: Verify corrected branch run: | set +e - make lint > /tmp/lint.log 2>&1 - status=$? - cat /tmp/lint.log + set -o pipefail + { + make lint + go test -count=1 ./... + go vet ./... + git diff --check + } 2>&1 | tee /tmp/shared-go-verify.log + status=${PIPESTATUS[0]} + printf '%s\n' "${status}" > /tmp/shared-go-verify.status + exit "${status}" + + - name: Persist failure diagnostics + if: failure() + run: | mkdir -p .agent-diagnostics - cp /tmp/lint.log .agent-diagnostics/lint.log + cp /tmp/shared-go-verify.log .agent-diagnostics/lint.log 2>/dev/null || echo "verification log unavailable" > .agent-diagnostics/lint.log git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add .agent-diagnostics/lint.log - git commit -m "chore: record lint diagnostics" + if ! git diff --cached --quiet; then + git commit -m "chore: record shared-go verification failure" + git push origin HEAD:agent/security-hardening-20260715 + fi + + - name: Commit verified correction + if: success() + run: | + git rm .github/workflows/agent-lint-diagnose.yml + rm -rf .agent-diagnostics + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add -A + git commit -m "fix(logging): reuse secret key constants" git push origin HEAD:agent/security-hardening-20260715 - exit "$status" From 595572f563c6fc6265974882d4c1db7a904d0f6a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:10:38 +0000 Subject: [PATCH 22/38] fix(logging): reuse secret key constants --- .agent-diagnostics/lint.log | 219 -------------------- .github/workflows/agent-lint-diagnose.yml | 127 ------------ pkg/logging/sanitize.go | 9 +- pkg/logging/sanitize_literal_secret_test.go | 4 +- 4 files changed, 7 insertions(+), 352 deletions(-) delete mode 100644 .agent-diagnostics/lint.log delete mode 100644 .github/workflows/agent-lint-diagnose.yml diff --git a/.agent-diagnostics/lint.log b/.agent-diagnostics/lint.log deleted file mode 100644 index c6f348ed..00000000 --- a/.agent-diagnostics/lint.log +++ /dev/null @@ -1,219 +0,0 @@ -bash /home/runner/work/shared-go/shared-go/scripts/ci/go-tooling.sh golangci-lint run -c .golangci.yml ./... -[GO TOOLING] Installing golangci-lint@v2.12.2 -go: downloading github.com/golangci/golangci-lint/v2 v2.12.2 -go: downloading charm.land/lipgloss/v2 v2.0.3 -go: downloading github.com/fatih/color v1.19.0 -go: downloading github.com/gofrs/flock v0.13.0 -go: downloading github.com/ldez/grignotin v0.10.1 -go: downloading github.com/pelletier/go-toml v1.9.5 -go: downloading github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 -go: downloading github.com/spf13/cobra v1.10.2 -go: downloading github.com/spf13/pflag v1.0.10 -go: downloading github.com/spf13/viper v1.12.0 -go: downloading go.yaml.in/yaml/v3 v3.0.4 -go: downloading golang.org/x/mod v0.35.0 -go: downloading github.com/charmbracelet/colorprofile v0.4.3 -go: downloading github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 -go: downloading github.com/charmbracelet/x/ansi v0.11.7 -go: downloading github.com/charmbracelet/x/term v0.2.2 -go: downloading github.com/clipperhouse/displaywidth v0.11.0 -go: downloading github.com/lucasb-eyer/go-colorful v1.4.0 -go: downloading github.com/rivo/uniseg v0.4.7 -go: downloading github.com/mattn/go-colorable v0.1.14 -go: downloading github.com/mattn/go-isatty v0.0.20 -go: downloading golang.org/x/tools v0.44.0 -go: downloading github.com/go-viper/mapstructure/v2 v2.5.0 -go: downloading github.com/hashicorp/go-version v1.9.0 -go: downloading github.com/mitchellh/go-homedir v1.1.0 -go: downloading github.com/alecthomas/chroma/v2 v2.24.1 -go: downloading github.com/rogpeppe/go-internal v1.14.1 -go: downloading github.com/golangci/plugin-module-register v0.1.2 -go: downloading github.com/sirupsen/logrus v1.9.4 -go: downloading github.com/stretchr/testify v1.11.1 -go: downloading github.com/go-xmlfmt/xmlfmt v1.1.3 -go: downloading github.com/golangci/revgrep v0.8.0 -go: downloading golang.org/x/sys v0.43.0 -go: downloading golang.org/x/text v0.36.0 -go: downloading github.com/fsnotify/fsnotify v1.5.4 -go: downloading github.com/mitchellh/mapstructure v1.5.0 -go: downloading github.com/spf13/afero v1.15.0 -go: downloading github.com/spf13/cast v1.5.0 -go: downloading github.com/spf13/jwalterweatherman v1.1.0 -go: downloading github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e -go: downloading github.com/charmbracelet/x/termios v0.1.1 -go: downloading github.com/charmbracelet/x/windows v0.2.2 -go: downloading github.com/muesli/cancelreader v0.2.2 -go: downloading golang.org/x/sync v0.20.0 -go: downloading github.com/clipperhouse/uax29/v2 v2.7.0 -go: downloading github.com/mattn/go-runewidth v0.0.23 -go: downloading github.com/daixiang0/gci v0.13.7 -go: downloading github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d -go: downloading mvdan.cc/gofumpt v0.9.2 -go: downloading github.com/golangci/golines v0.15.0 -go: downloading github.com/sourcegraph/go-diff v0.8.0 -go: downloading github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e -go: downloading go.augendre.info/arangolint v0.4.0 -go: downloading github.com/alingse/asasalint v0.0.11 -go: downloading github.com/golangci/asciicheck v0.5.0 -go: downloading github.com/breml/bidichk v0.3.3 -go: downloading github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 -go: downloading github.com/lasiar/canonicalheader v1.1.2 -go: downloading github.com/ClickHouse/clickhouse-go-linter v1.2.0 -go: downloading github.com/sivchari/containedctx v1.0.3 -go: downloading github.com/kkHAIKE/contextcheck v1.1.6 -go: downloading github.com/karamaru-alpha/copyloopvar v1.2.2 -go: downloading github.com/bkielbasa/cyclop v1.2.3 -go: downloading gitlab.com/bosi/decorder v0.4.2 -go: downloading github.com/OpenPeeDeeP/depguard/v2 v2.2.1 -go: downloading github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 -go: downloading github.com/Abirdcfly/dupword v0.1.7 -go: downloading github.com/charithe/durationcheck v0.0.11 -go: downloading github.com/manuelarte/embeddedstructfieldcheck v0.4.0 -go: downloading github.com/Djarvur/go-err113 v0.1.1 -go: downloading github.com/kisielk/errcheck v1.10.0 -go: downloading github.com/breml/errchkjson v0.4.1 -go: downloading github.com/Antonboom/errname v1.1.1 -go: downloading codeberg.org/polyfloyd/go-errorlint v1.9.0 -go: downloading github.com/nishanths/exhaustive v0.12.0 -go: downloading dev.gaijin.team/go/exhaustruct/v4 v4.0.0 -go: downloading github.com/ldez/exptostd v0.4.5 -go: downloading go.augendre.info/fatcontext v0.9.0 -go: downloading github.com/ashanbrown/forbidigo/v2 v2.3.1 -go: downloading github.com/gostaticanalysis/forcetypeassert v0.2.0 -go: downloading github.com/manuelarte/funcorder v0.6.0 -go: downloading github.com/ultraware/funlen v0.2.0 -go: downloading github.com/nunnatsa/ginkgolinter v0.23.0 -go: downloading 4d63.com/gocheckcompilerdirectives v1.3.0 -go: downloading 4d63.com/gochecknoglobals v0.2.2 -go: downloading github.com/alecthomas/go-check-sumtype v0.3.1 -go: downloading github.com/uudashr/gocognit v1.2.1 -go: downloading github.com/jgautheron/goconst v1.10.0 -go: downloading github.com/go-critic/go-critic v0.14.3 -go: downloading github.com/quasilyte/go-ruleguard/dsl v0.3.23 -go: downloading github.com/quasilyte/go-ruleguard v0.4.5 -go: downloading github.com/fzipp/gocyclo v0.6.0 -go: downloading github.com/godoc-lint/godoc-lint v0.11.2 -go: downloading github.com/tetafro/godot v1.5.6 -go: downloading github.com/matoous/godox v1.1.0 -go: downloading github.com/denis-tingaikin/go-header v0.5.0 -go: downloading github.com/ldez/gomoddirectives v0.8.0 -go: downloading github.com/Masterminds/semver/v3 v3.5.0 -go: downloading github.com/ryancurrah/gomodguard v1.4.1 -go: downloading github.com/ryancurrah/gomodguard/v2 v2.1.3 -go: downloading github.com/golangci/go-printf-func-name v0.1.1 -go: downloading github.com/securego/gosec/v2 v2.26.1 -go: downloading github.com/xen0n/gosmopolitan v1.3.0 -go: downloading github.com/leonklingele/grouper v1.1.2 -go: downloading github.com/uudashr/iface v1.4.2 -go: downloading github.com/julz/importas v0.2.0 -go: downloading github.com/macabu/inamedparam v0.2.0 -go: downloading github.com/gordonklaus/ineffassign v0.2.0 -go: downloading github.com/sashamelentyev/interfacebloat v1.1.0 -go: downloading github.com/ckaznocha/intrange v0.3.1 -go: downloading github.com/AdminBenni/iota-mixing v1.0.0 -go: downloading github.com/butuzov/ireturn v0.4.1 -go: downloading github.com/timonwong/loggercheck v0.11.0 -go: downloading github.com/yagipy/maintidx v1.0.0 -go: downloading github.com/ashanbrown/makezero/v2 v2.2.1 -go: downloading github.com/butuzov/mirror v1.3.0 -go: downloading github.com/golangci/misspell v0.8.0 -go: downloading github.com/tommy-muehle/go-mnd/v2 v2.5.1 -go: downloading go-simpler.org/musttag v0.14.0 -go: downloading github.com/alexkohler/nakedret/v2 v2.0.6 -go: downloading github.com/nakabonne/nestif v0.3.1 -go: downloading github.com/gostaticanalysis/nilerr v0.1.2 -go: downloading github.com/alingse/nilnesserr v0.2.0 -go: downloading github.com/Antonboom/nilnil v1.1.1 -go: downloading github.com/ssgreg/nlreturn/v2 v2.2.1 -go: downloading github.com/sonatard/noctx v0.5.1 -go: downloading github.com/AlwxSin/noinlineerr v1.0.5 -go: downloading github.com/firefart/nonamedreturns v1.0.6 -go: downloading github.com/stbenjam/no-sprintf-host-port v0.3.1 -go: downloading github.com/kunwardeep/paralleltest v1.0.15 -go: downloading github.com/catenacyber/perfsprint v0.10.1 -go: downloading github.com/alexkohler/prealloc v1.1.0 -go: downloading github.com/nishanths/predeclared v0.2.2 -go: downloading github.com/yeya24/promlinter v0.3.0 -go: downloading github.com/ghostiam/protogetter v0.3.20 -go: downloading github.com/curioswitch/go-reassign v0.3.0 -go: downloading github.com/raeperd/recvcheck v0.2.0 -go: downloading github.com/BurntSushi/toml v1.6.0 -go: downloading github.com/mgechev/revive v1.15.0 -go: downloading github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba -go: downloading go-simpler.org/sloglint v0.12.0 -go: downloading github.com/jjti/go-spancheck v0.6.5 -go: downloading github.com/ryanrolds/sqlclosecheck v0.6.0 -go: downloading honnef.co/go/tools v0.7.0 -go: downloading github.com/4meepo/tagalign v1.4.3 -go: downloading github.com/ldez/tagliatelle v0.7.2 -go: downloading github.com/maratori/testableexamples v1.0.1 -go: downloading github.com/Antonboom/testifylint v1.6.4 -go: downloading github.com/maratori/testpackage v1.1.2 -go: downloading github.com/kulti/thelper v0.7.1 -go: downloading github.com/moricho/tparallel v0.3.2 -go: downloading github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e -go: downloading mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 -go: downloading github.com/MirrexOne/unqueryvet v1.5.4 -go: downloading github.com/sashamelentyev/usestdlibvars v1.29.0 -go: downloading github.com/ldez/usetesting v0.5.0 -go: downloading github.com/blizzy78/varnamelen v0.8.0 -go: downloading github.com/sanposhiho/wastedassign/v2 v2.1.0 -go: downloading github.com/ultraware/whitespace v0.2.0 -go: downloading github.com/tomarrell/wrapcheck/v2 v2.12.0 -go: downloading github.com/bombsimon/wsl/v4 v4.7.0 -go: downloading github.com/bombsimon/wsl/v5 v5.8.0 -go: downloading github.com/ykadowak/zerologlint v0.1.5 -go: downloading github.com/davecgh/go-spew v1.1.1 -go: downloading github.com/pmezard/go-difflib v1.0.0 -go: downloading github.com/stretchr/objx v0.5.2 -go: downloading github.com/subosito/gotenv v1.4.1 -go: downloading github.com/hashicorp/hcl v1.0.0 -go: downloading gopkg.in/ini.v1 v1.67.0 -go: downloading github.com/magiconair/properties v1.8.6 -go: downloading go.uber.org/zap v1.27.0 -go: downloading github.com/hexops/gotextdiff v1.0.3 -go: downloading github.com/google/go-cmp v0.7.0 -go: downloading github.com/dave/dst v0.27.3 -go: downloading github.com/dlclark/regexp2 v1.12.0 -go: downloading github.com/gostaticanalysis/analysisutil v0.7.1 -go: downloading github.com/go-toolsmith/astcast v1.1.0 -go: downloading github.com/gobwas/glob v0.2.3 -go: downloading dev.gaijin.team/go/golib v0.6.0 -go: downloading github.com/go-toolsmith/astcopy v1.1.0 -go: downloading github.com/go-toolsmith/astequal v1.2.0 -go: downloading github.com/go-toolsmith/astfmt v1.1.0 -go: downloading github.com/go-toolsmith/astp v1.1.0 -go: downloading github.com/go-toolsmith/strparse v1.1.0 -go: downloading github.com/go-toolsmith/typep v1.1.0 -go: downloading github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 -go: downloading github.com/ccojocar/zxcvbn-go v1.0.4 -go: downloading github.com/gostaticanalysis/comment v1.5.0 -go: downloading github.com/prometheus/client_golang v1.12.1 -go: downloading github.com/prometheus/client_model v0.2.0 -go: downloading github.com/fatih/structtag v1.2.0 -go: downloading github.com/ettle/strcase v0.2.0 -go: downloading github.com/alfatraining/structtag v1.0.0 -go: downloading github.com/hashicorp/go-immutable-radix/v2 v2.1.0 -go: downloading go.uber.org/multierr v1.10.0 -go: downloading github.com/ldez/structtags v0.6.1 -go: downloading golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 -go: downloading github.com/quasilyte/gogrep v0.5.0 -go: downloading golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b -go: downloading github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 -go: downloading github.com/golang/protobuf v1.5.3 -go: downloading codeberg.org/chavacava/garif v0.2.0 -go: downloading github.com/beorn7/perks v1.0.1 -go: downloading github.com/prometheus/common v0.32.1 -go: downloading github.com/prometheus/procfs v0.7.3 -go: downloading google.golang.org/protobuf v1.36.10 -go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7 -go: downloading github.com/matttproud/golang_protobuf_extensions v1.0.1 -pkg/logging/sanitize.go:34:29: string `private_key` has 6 occurrences, make it a constant (goconst) - "client_secret", "secret", "private_key", "secret_key", - ^ -pkg/logging/sanitize.go:255:2: string `api_key` has 5 occurrences, but such constant `tokenAPIKey` already exists (goconst) - "api_key": {}, - ^ -2 issues: -* goconst: 2 -make: *** [Makefile:23: lint] Error 1 diff --git a/.github/workflows/agent-lint-diagnose.yml b/.github/workflows/agent-lint-diagnose.yml deleted file mode 100644 index c2167b74..00000000 --- a/.github/workflows/agent-lint-diagnose.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: agent lint diagnose - -on: - push: - branches: - - agent/security-hardening-20260715 - paths-ignore: - - .agent-diagnostics/** - -permissions: - contents: write - -concurrency: - group: agent-lint-diagnose-shared-go - cancel-in-progress: true - -jobs: - diagnose: - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout patch branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: agent/security-hardening-20260715 - persist-credentials: true - - - name: Setup Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - with: - go-version-file: go.mod - cache-dependency-path: go.sum - - - name: Apply lint correction - run: | - python3 - <<'PY' - from pathlib import Path - - def replace_once(path: str, old: str, new: str) -> None: - target = Path(path) - text = target.read_text(encoding="utf-8") - if text.count(old) != 1: - raise SystemExit(f"{path}: expected one replacement target") - target.write_text(text.replace(old, new, 1), encoding="utf-8") - - replace_once( - "pkg/logging/sanitize.go", - 'const (\n\ttokenAPIKey = "api_key"\n\ttokenAPIKeyCompact = "apikey"\n)', - 'const (\n\ttokenAPIKey = "api_key"\n\ttokenAPIKeyCompact = "apikey"\n\ttokenPrivateKey = "private_key"\n)', - ) - replace_once( - "pkg/logging/sanitize.go", - '\t"client_secret", "secret", "private_key", "secret_key",', - '\t"client_secret", "secret", tokenPrivateKey, "secret_key",', - ) - replace_once( - "pkg/logging/sanitize.go", - '\t"private_key": {},', - '\ttokenPrivateKey: {},', - ) - replace_once( - "pkg/logging/sanitize.go", - '\t"api_key": {},', - '\ttokenAPIKey: {},', - ) - replace_once( - "pkg/logging/sanitize.go", - '\t"private_key": {},', - '\ttokenPrivateKey: {},', - ) - replace_once( - "pkg/logging/sanitize_literal_secret_test.go", - '\t\t"api_key",', - '\t\ttokenAPIKey,', - ) - replace_once( - "pkg/logging/sanitize_literal_secret_test.go", - '\t\t"private_key",', - '\t\ttokenPrivateKey,', - ) - - diagnostic = Path(".agent-diagnostics/lint.log") - diagnostic.unlink(missing_ok=True) - try: - diagnostic.parent.rmdir() - except OSError: - pass - PY - gofmt -w pkg/logging/sanitize.go pkg/logging/sanitize_literal_secret_test.go - - - name: Verify corrected branch - run: | - set +e - set -o pipefail - { - make lint - go test -count=1 ./... - go vet ./... - git diff --check - } 2>&1 | tee /tmp/shared-go-verify.log - status=${PIPESTATUS[0]} - printf '%s\n' "${status}" > /tmp/shared-go-verify.status - exit "${status}" - - - name: Persist failure diagnostics - if: failure() - run: | - mkdir -p .agent-diagnostics - cp /tmp/shared-go-verify.log .agent-diagnostics/lint.log 2>/dev/null || echo "verification log unavailable" > .agent-diagnostics/lint.log - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add .agent-diagnostics/lint.log - if ! git diff --cached --quiet; then - git commit -m "chore: record shared-go verification failure" - git push origin HEAD:agent/security-hardening-20260715 - fi - - - name: Commit verified correction - if: success() - run: | - git rm .github/workflows/agent-lint-diagnose.yml - rm -rf .agent-diagnostics - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add -A - git commit -m "fix(logging): reuse secret key constants" - git push origin HEAD:agent/security-hardening-20260715 diff --git a/pkg/logging/sanitize.go b/pkg/logging/sanitize.go index 47c310de..384dc7cf 100644 --- a/pkg/logging/sanitize.go +++ b/pkg/logging/sanitize.go @@ -24,6 +24,7 @@ var ( const ( tokenAPIKey = "api_key" tokenAPIKeyCompact = "apikey" + tokenPrivateKey = "private_key" ) // querySecretTokens는 querySecretRegex가 매치할 수 있는 키 이름 집합으로, @@ -31,7 +32,7 @@ const ( // 입력은 반드시 이 토큰 중 하나를 case-insensitive로 포함하므로 게이트는 안전하다. var querySecretTokens = []string{ "key", tokenAPIKey, tokenAPIKeyCompact, "token", "password", "pwd", "passwd", - "client_secret", "secret", "private_key", "secret_key", + "client_secret", "secret", tokenPrivateKey, "secret_key", } var sensitiveExactKeys = map[string]struct{}{ @@ -46,7 +47,7 @@ var sensitiveExactKeys = map[string]struct{}{ "client_secret": {}, tokenAPIKey: {}, tokenAPIKeyCompact: {}, - "private_key": {}, + tokenPrivateKey: {}, "secret_key": {}, "authorization": {}, "auth_header": {}, @@ -252,9 +253,9 @@ var secretLikeExactValues = map[string]struct{}{ "access-token": {}, "access_token": {}, "api-key": {}, - "api_key": {}, + tokenAPIKey: {}, "private-key": {}, - "private_key": {}, + tokenPrivateKey: {}, "refresh-token": {}, "refresh_token": {}, "secret-key": {}, diff --git a/pkg/logging/sanitize_literal_secret_test.go b/pkg/logging/sanitize_literal_secret_test.go index 9e2b3758..20519a73 100644 --- a/pkg/logging/sanitize_literal_secret_test.go +++ b/pkg/logging/sanitize_literal_secret_test.go @@ -12,9 +12,9 @@ func TestSanitizeKey_LiteralSecretNamesAreMasked(t *testing.T) { "secret-key", "secret_key", "api-key", - "api_key", + tokenAPIKey, "private-key", - "private_key", + tokenPrivateKey, "access-token", "access_token", "refresh-token", From e1bdf101d6235c68140301debbc1b117004a5b2a Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 16:19:55 +0900 Subject: [PATCH 23/38] test: cover normalized literal secret values --- pkg/logging/sanitize_literal_secret_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/logging/sanitize_literal_secret_test.go b/pkg/logging/sanitize_literal_secret_test.go index 20519a73..29521c6f 100644 --- a/pkg/logging/sanitize_literal_secret_test.go +++ b/pkg/logging/sanitize_literal_secret_test.go @@ -19,6 +19,8 @@ func TestSanitizeKey_LiteralSecretNamesAreMasked(t *testing.T) { "access_token", "refresh-token", "refresh_token", + " SECRET-KEY ", + "Api_Key", } { value := value t.Run(value, func(t *testing.T) { From b361865a864875249f0fc12d5de341c08af7c64c Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 16:23:58 +0900 Subject: [PATCH 24/38] test(ci): run workflow policy regression suite from Go tests --- internal/cipolicy/workflow_policy_test.go | 24 +++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 internal/cipolicy/workflow_policy_test.go diff --git a/internal/cipolicy/workflow_policy_test.go b/internal/cipolicy/workflow_policy_test.go new file mode 100644 index 00000000..b9a4aa5e --- /dev/null +++ b/internal/cipolicy/workflow_policy_test.go @@ -0,0 +1,24 @@ +package cipolicy + +import ( + "os/exec" + "path/filepath" + "runtime" + "testing" +) + +func TestWorkflowPolicyRegressionSuite(t *testing.T) { + t.Helper() + + _, file, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("resolve test source path") + } + repoRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..")) + cmd := exec.Command("bash", "scripts/ci/check-workflow-secrets_test.sh") + cmd.Dir = repoRoot + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("workflow policy regression suite failed: %v\n%s", err, output) + } +} From ca55b619c9e26d617651479e1c31938bc6441396 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 16:24:19 +0900 Subject: [PATCH 25/38] ci: keep workflow definition unchanged --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22f62d0b..dfb3cbd6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,9 +33,6 @@ jobs: - name: Workflow secret boundary run: bash scripts/ci/check-workflow-secrets.sh - - name: Workflow secret boundary tests - run: bash scripts/ci/check-workflow-secrets_test.sh - - name: SQL ownership run: bash scripts/check-sql-ownership.sh From ae5c73b37cba82e034793bca3247b59022e6bc2c Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 16:24:42 +0900 Subject: [PATCH 26/38] test(ci): bind policy regression process to test context --- internal/cipolicy/workflow_policy_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/cipolicy/workflow_policy_test.go b/internal/cipolicy/workflow_policy_test.go index b9a4aa5e..2e8bb1cd 100644 --- a/internal/cipolicy/workflow_policy_test.go +++ b/internal/cipolicy/workflow_policy_test.go @@ -8,14 +8,12 @@ import ( ) func TestWorkflowPolicyRegressionSuite(t *testing.T) { - t.Helper() - _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("resolve test source path") } repoRoot := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..")) - cmd := exec.Command("bash", "scripts/ci/check-workflow-secrets_test.sh") + cmd := exec.CommandContext(t.Context(), "bash", "scripts/ci/check-workflow-secrets_test.sh") cmd.Dir = repoRoot output, err := cmd.CombinedOutput() if err != nil { From f6e90f9be1cd5d8da2f6e19f70708a6413a0a7a9 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 16:33:23 +0900 Subject: [PATCH 27/38] test(ci): satisfy external parallel test policy --- internal/cipolicy/workflow_policy_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/cipolicy/workflow_policy_test.go b/internal/cipolicy/workflow_policy_test.go index 2e8bb1cd..5c4142d2 100644 --- a/internal/cipolicy/workflow_policy_test.go +++ b/internal/cipolicy/workflow_policy_test.go @@ -1,4 +1,4 @@ -package cipolicy +package cipolicy_test import ( "os/exec" @@ -8,6 +8,8 @@ import ( ) func TestWorkflowPolicyRegressionSuite(t *testing.T) { + t.Parallel() + _, file, _, ok := runtime.Caller(0) if !ok { t.Fatal("resolve test source path") From 49e0b9194ffb432f0c6aafdfdd76e7531d5311bc Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 16:41:44 +0900 Subject: [PATCH 28/38] chore: capture final lint diagnostics --- .github/workflows/agent-lint-diagnose.yml | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/agent-lint-diagnose.yml diff --git a/.github/workflows/agent-lint-diagnose.yml b/.github/workflows/agent-lint-diagnose.yml new file mode 100644 index 00000000..215c1dde --- /dev/null +++ b/.github/workflows/agent-lint-diagnose.yml @@ -0,0 +1,42 @@ +name: agent lint diagnose + +on: + push: + branches: + - agent/security-hardening-20260715 + paths-ignore: + - .agent-diagnostics/** + +permissions: + contents: write + +jobs: + diagnose: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout branch + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: agent/security-hardening-20260715 + persist-credentials: true + - name: Setup Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + - name: Capture lint output + run: | + set +e + make lint > /tmp/lint.log 2>&1 + status=$? + cat /tmp/lint.log + mkdir -p .agent-diagnostics + cp /tmp/lint.log .agent-diagnostics/lint.log + printf '%s\n' "$status" > .agent-diagnostics/lint.status + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .agent-diagnostics + git commit -m "chore: record final lint diagnostics" + git push origin HEAD:agent/security-hardening-20260715 + exit "$status" From 59e4fe91203604faa024d573d125d1decaefdcc0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:43:51 +0000 Subject: [PATCH 29/38] chore: record final lint diagnostics --- .agent-diagnostics/lint.log | 216 +++++++++++++++++++++++++++++++++ .agent-diagnostics/lint.status | 1 + 2 files changed, 217 insertions(+) create mode 100644 .agent-diagnostics/lint.log create mode 100644 .agent-diagnostics/lint.status diff --git a/.agent-diagnostics/lint.log b/.agent-diagnostics/lint.log new file mode 100644 index 00000000..672ba4d4 --- /dev/null +++ b/.agent-diagnostics/lint.log @@ -0,0 +1,216 @@ +bash /home/runner/work/shared-go/shared-go/scripts/ci/go-tooling.sh golangci-lint run -c .golangci.yml ./... +[GO TOOLING] Installing golangci-lint@v2.12.2 +go: downloading github.com/golangci/golangci-lint/v2 v2.12.2 +go: downloading charm.land/lipgloss/v2 v2.0.3 +go: downloading github.com/fatih/color v1.19.0 +go: downloading github.com/gofrs/flock v0.13.0 +go: downloading github.com/ldez/grignotin v0.10.1 +go: downloading github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 +go: downloading github.com/pelletier/go-toml v1.9.5 +go: downloading github.com/spf13/cobra v1.10.2 +go: downloading github.com/spf13/pflag v1.0.10 +go: downloading github.com/spf13/viper v1.12.0 +go: downloading go.yaml.in/yaml/v3 v3.0.4 +go: downloading golang.org/x/mod v0.35.0 +go: downloading golang.org/x/tools v0.44.0 +go: downloading github.com/go-viper/mapstructure/v2 v2.5.0 +go: downloading github.com/hashicorp/go-version v1.9.0 +go: downloading github.com/mitchellh/go-homedir v1.1.0 +go: downloading github.com/alecthomas/chroma/v2 v2.24.1 +go: downloading github.com/rogpeppe/go-internal v1.14.1 +go: downloading github.com/golangci/plugin-module-register v0.1.2 +go: downloading github.com/mattn/go-colorable v0.1.14 +go: downloading github.com/sirupsen/logrus v1.9.4 +go: downloading github.com/stretchr/testify v1.11.1 +go: downloading github.com/go-xmlfmt/xmlfmt v1.1.3 +go: downloading github.com/golangci/revgrep v0.8.0 +go: downloading github.com/mattn/go-isatty v0.0.20 +go: downloading golang.org/x/sys v0.43.0 +go: downloading golang.org/x/text v0.36.0 +go: downloading github.com/charmbracelet/colorprofile v0.4.3 +go: downloading github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 +go: downloading github.com/charmbracelet/x/ansi v0.11.7 +go: downloading github.com/charmbracelet/x/term v0.2.2 +go: downloading github.com/clipperhouse/displaywidth v0.11.0 +go: downloading github.com/lucasb-eyer/go-colorful v1.4.0 +go: downloading github.com/rivo/uniseg v0.4.7 +go: downloading github.com/fsnotify/fsnotify v1.5.4 +go: downloading github.com/mitchellh/mapstructure v1.5.0 +go: downloading github.com/spf13/afero v1.15.0 +go: downloading github.com/spf13/cast v1.5.0 +go: downloading github.com/spf13/jwalterweatherman v1.1.0 +go: downloading golang.org/x/sync v0.20.0 +go: downloading github.com/daixiang0/gci v0.13.7 +go: downloading github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d +go: downloading mvdan.cc/gofumpt v0.9.2 +go: downloading github.com/golangci/golines v0.15.0 +go: downloading github.com/sourcegraph/go-diff v0.8.0 +go: downloading github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e +go: downloading go.augendre.info/arangolint v0.4.0 +go: downloading github.com/alingse/asasalint v0.0.11 +go: downloading github.com/golangci/asciicheck v0.5.0 +go: downloading github.com/breml/bidichk v0.3.3 +go: downloading github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 +go: downloading github.com/lasiar/canonicalheader v1.1.2 +go: downloading github.com/ClickHouse/clickhouse-go-linter v1.2.0 +go: downloading github.com/sivchari/containedctx v1.0.3 +go: downloading github.com/kkHAIKE/contextcheck v1.1.6 +go: downloading github.com/karamaru-alpha/copyloopvar v1.2.2 +go: downloading github.com/bkielbasa/cyclop v1.2.3 +go: downloading gitlab.com/bosi/decorder v0.4.2 +go: downloading github.com/OpenPeeDeeP/depguard/v2 v2.2.1 +go: downloading github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 +go: downloading github.com/Abirdcfly/dupword v0.1.7 +go: downloading github.com/charithe/durationcheck v0.0.11 +go: downloading github.com/manuelarte/embeddedstructfieldcheck v0.4.0 +go: downloading github.com/Djarvur/go-err113 v0.1.1 +go: downloading github.com/kisielk/errcheck v1.10.0 +go: downloading github.com/breml/errchkjson v0.4.1 +go: downloading github.com/Antonboom/errname v1.1.1 +go: downloading codeberg.org/polyfloyd/go-errorlint v1.9.0 +go: downloading github.com/nishanths/exhaustive v0.12.0 +go: downloading dev.gaijin.team/go/exhaustruct/v4 v4.0.0 +go: downloading github.com/ldez/exptostd v0.4.5 +go: downloading go.augendre.info/fatcontext v0.9.0 +go: downloading github.com/ashanbrown/forbidigo/v2 v2.3.1 +go: downloading github.com/gostaticanalysis/forcetypeassert v0.2.0 +go: downloading github.com/manuelarte/funcorder v0.6.0 +go: downloading github.com/ultraware/funlen v0.2.0 +go: downloading github.com/nunnatsa/ginkgolinter v0.23.0 +go: downloading 4d63.com/gocheckcompilerdirectives v1.3.0 +go: downloading 4d63.com/gochecknoglobals v0.2.2 +go: downloading github.com/alecthomas/go-check-sumtype v0.3.1 +go: downloading github.com/uudashr/gocognit v1.2.1 +go: downloading github.com/jgautheron/goconst v1.10.0 +go: downloading github.com/go-critic/go-critic v0.14.3 +go: downloading github.com/quasilyte/go-ruleguard/dsl v0.3.23 +go: downloading github.com/fzipp/gocyclo v0.6.0 +go: downloading github.com/quasilyte/go-ruleguard v0.4.5 +go: downloading github.com/godoc-lint/godoc-lint v0.11.2 +go: downloading github.com/tetafro/godot v1.5.6 +go: downloading github.com/matoous/godox v1.1.0 +go: downloading github.com/denis-tingaikin/go-header v0.5.0 +go: downloading github.com/ldez/gomoddirectives v0.8.0 +go: downloading github.com/Masterminds/semver/v3 v3.5.0 +go: downloading github.com/ryancurrah/gomodguard v1.4.1 +go: downloading github.com/ryancurrah/gomodguard/v2 v2.1.3 +go: downloading github.com/golangci/go-printf-func-name v0.1.1 +go: downloading github.com/securego/gosec/v2 v2.26.1 +go: downloading github.com/xen0n/gosmopolitan v1.3.0 +go: downloading github.com/uudashr/iface v1.4.2 +go: downloading github.com/leonklingele/grouper v1.1.2 +go: downloading github.com/julz/importas v0.2.0 +go: downloading github.com/macabu/inamedparam v0.2.0 +go: downloading github.com/gordonklaus/ineffassign v0.2.0 +go: downloading github.com/sashamelentyev/interfacebloat v1.1.0 +go: downloading github.com/ckaznocha/intrange v0.3.1 +go: downloading github.com/AdminBenni/iota-mixing v1.0.0 +go: downloading github.com/butuzov/ireturn v0.4.1 +go: downloading github.com/timonwong/loggercheck v0.11.0 +go: downloading github.com/yagipy/maintidx v1.0.0 +go: downloading github.com/ashanbrown/makezero/v2 v2.2.1 +go: downloading github.com/butuzov/mirror v1.3.0 +go: downloading github.com/golangci/misspell v0.8.0 +go: downloading github.com/tommy-muehle/go-mnd/v2 v2.5.1 +go: downloading go-simpler.org/musttag v0.14.0 +go: downloading github.com/alexkohler/nakedret/v2 v2.0.6 +go: downloading github.com/nakabonne/nestif v0.3.1 +go: downloading github.com/gostaticanalysis/nilerr v0.1.2 +go: downloading github.com/alingse/nilnesserr v0.2.0 +go: downloading github.com/Antonboom/nilnil v1.1.1 +go: downloading github.com/ssgreg/nlreturn/v2 v2.2.1 +go: downloading github.com/sonatard/noctx v0.5.1 +go: downloading github.com/AlwxSin/noinlineerr v1.0.5 +go: downloading github.com/firefart/nonamedreturns v1.0.6 +go: downloading github.com/stbenjam/no-sprintf-host-port v0.3.1 +go: downloading github.com/kunwardeep/paralleltest v1.0.15 +go: downloading github.com/catenacyber/perfsprint v0.10.1 +go: downloading github.com/alexkohler/prealloc v1.1.0 +go: downloading github.com/nishanths/predeclared v0.2.2 +go: downloading github.com/yeya24/promlinter v0.3.0 +go: downloading github.com/ghostiam/protogetter v0.3.20 +go: downloading github.com/curioswitch/go-reassign v0.3.0 +go: downloading github.com/raeperd/recvcheck v0.2.0 +go: downloading github.com/BurntSushi/toml v1.6.0 +go: downloading github.com/mgechev/revive v1.15.0 +go: downloading github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba +go: downloading go-simpler.org/sloglint v0.12.0 +go: downloading github.com/jjti/go-spancheck v0.6.5 +go: downloading github.com/ryanrolds/sqlclosecheck v0.6.0 +go: downloading honnef.co/go/tools v0.7.0 +go: downloading github.com/4meepo/tagalign v1.4.3 +go: downloading github.com/ldez/tagliatelle v0.7.2 +go: downloading github.com/maratori/testableexamples v1.0.1 +go: downloading github.com/Antonboom/testifylint v1.6.4 +go: downloading github.com/maratori/testpackage v1.1.2 +go: downloading github.com/kulti/thelper v0.7.1 +go: downloading github.com/moricho/tparallel v0.3.2 +go: downloading github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e +go: downloading mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 +go: downloading github.com/MirrexOne/unqueryvet v1.5.4 +go: downloading github.com/sashamelentyev/usestdlibvars v1.29.0 +go: downloading github.com/ldez/usetesting v0.5.0 +go: downloading github.com/blizzy78/varnamelen v0.8.0 +go: downloading github.com/sanposhiho/wastedassign/v2 v2.1.0 +go: downloading github.com/ultraware/whitespace v0.2.0 +go: downloading github.com/tomarrell/wrapcheck/v2 v2.12.0 +go: downloading github.com/bombsimon/wsl/v4 v4.7.0 +go: downloading github.com/bombsimon/wsl/v5 v5.8.0 +go: downloading github.com/ykadowak/zerologlint v0.1.5 +go: downloading github.com/davecgh/go-spew v1.1.1 +go: downloading github.com/pmezard/go-difflib v1.0.0 +go: downloading github.com/stretchr/objx v0.5.2 +go: downloading github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e +go: downloading github.com/charmbracelet/x/termios v0.1.1 +go: downloading github.com/charmbracelet/x/windows v0.2.2 +go: downloading github.com/muesli/cancelreader v0.2.2 +go: downloading github.com/clipperhouse/uax29/v2 v2.7.0 +go: downloading github.com/mattn/go-runewidth v0.0.23 +go: downloading github.com/subosito/gotenv v1.4.1 +go: downloading github.com/hashicorp/hcl v1.0.0 +go: downloading gopkg.in/ini.v1 v1.67.0 +go: downloading github.com/magiconair/properties v1.8.6 +go: downloading go.uber.org/zap v1.27.0 +go: downloading github.com/hexops/gotextdiff v1.0.3 +go: downloading github.com/google/go-cmp v0.7.0 +go: downloading github.com/dave/dst v0.27.3 +go: downloading github.com/gostaticanalysis/analysisutil v0.7.1 +go: downloading github.com/go-toolsmith/astcast v1.1.0 +go: downloading github.com/gobwas/glob v0.2.3 +go: downloading dev.gaijin.team/go/golib v0.6.0 +go: downloading github.com/go-toolsmith/astcopy v1.1.0 +go: downloading github.com/go-toolsmith/astequal v1.2.0 +go: downloading github.com/go-toolsmith/astfmt v1.1.0 +go: downloading github.com/go-toolsmith/astp v1.1.0 +go: downloading github.com/go-toolsmith/strparse v1.1.0 +go: downloading github.com/go-toolsmith/typep v1.1.0 +go: downloading github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 +go: downloading github.com/ccojocar/zxcvbn-go v1.0.4 +go: downloading github.com/gostaticanalysis/comment v1.5.0 +go: downloading github.com/prometheus/client_golang v1.12.1 +go: downloading github.com/prometheus/client_model v0.2.0 +go: downloading github.com/fatih/structtag v1.2.0 +go: downloading github.com/ettle/strcase v0.2.0 +go: downloading github.com/hashicorp/go-immutable-radix/v2 v2.1.0 +go: downloading github.com/alfatraining/structtag v1.0.0 +go: downloading github.com/dlclark/regexp2 v1.12.0 +go: downloading go.uber.org/multierr v1.10.0 +go: downloading github.com/ldez/structtags v0.6.1 +go: downloading golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 +go: downloading golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b +go: downloading github.com/quasilyte/gogrep v0.5.0 +go: downloading github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 +go: downloading github.com/beorn7/perks v1.0.1 +go: downloading github.com/golang/protobuf v1.5.3 +go: downloading github.com/prometheus/common v0.32.1 +go: downloading github.com/prometheus/procfs v0.7.3 +go: downloading google.golang.org/protobuf v1.36.10 +go: downloading codeberg.org/chavacava/garif v0.2.0 +go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7 +go: downloading github.com/matttproud/golang_protobuf_extensions v1.0.1 +pkg/logging/sanitize.go:35:46: string `secret_key` has 6 occurrences, make it a constant (goconst) + "client_secret", "secret", tokenPrivateKey, "secret_key", + ^ +1 issues: +* goconst: 1 +make: *** [Makefile:23: lint] Error 1 diff --git a/.agent-diagnostics/lint.status b/.agent-diagnostics/lint.status new file mode 100644 index 00000000..0cfbf088 --- /dev/null +++ b/.agent-diagnostics/lint.status @@ -0,0 +1 @@ +2 From 40b453ac2b424e9d799e2ca7331ac0d305974e3f Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 16:51:50 +0900 Subject: [PATCH 30/38] fix(logging): reuse secret key constant --- pkg/logging/sanitize.go | 36 ++++++++++++++++++++++++------------ 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/pkg/logging/sanitize.go b/pkg/logging/sanitize.go index 384dc7cf..cd8c7604 100644 --- a/pkg/logging/sanitize.go +++ b/pkg/logging/sanitize.go @@ -1,3 +1,23 @@ +// Copyright (c) 2025 Kapu +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + package logging import ( @@ -7,15 +27,6 @@ import ( "strings" ) -type sanitizeHandler struct { - inner slog.Handler -} - -func newSanitizeHandler(inner slog.Handler) *sanitizeHandler { - return &sanitizeHandler{inner: inner} -} - -// 민감 키 리스트 (case-insensitive 매칭) var ( bearerTokenRegex = regexp.MustCompile(`(?i)\b(bearer\s+)[A-Za-z0-9._~+/=-]+`) querySecretRegex = regexp.MustCompile(`(?i)([?&;](?:key|api_key|apikey|token|password|pwd|passwd|client_secret|secret|private_key|secret_key)=)[^&\s]+`) @@ -25,6 +36,7 @@ const ( tokenAPIKey = "api_key" tokenAPIKeyCompact = "apikey" tokenPrivateKey = "private_key" + tokenSecretKey = "secret_key" ) // querySecretTokens는 querySecretRegex가 매치할 수 있는 키 이름 집합으로, @@ -32,7 +44,7 @@ const ( // 입력은 반드시 이 토큰 중 하나를 case-insensitive로 포함하므로 게이트는 안전하다. var querySecretTokens = []string{ "key", tokenAPIKey, tokenAPIKeyCompact, "token", "password", "pwd", "passwd", - "client_secret", "secret", tokenPrivateKey, "secret_key", + "client_secret", "secret", tokenPrivateKey, tokenSecretKey, } var sensitiveExactKeys = map[string]struct{}{ @@ -48,7 +60,7 @@ var sensitiveExactKeys = map[string]struct{}{ tokenAPIKey: {}, tokenAPIKeyCompact: {}, tokenPrivateKey: {}, - "secret_key": {}, + tokenSecretKey: {}, "authorization": {}, "auth_header": {}, "cookie": {}, @@ -259,7 +271,7 @@ var secretLikeExactValues = map[string]struct{}{ "refresh-token": {}, "refresh_token": {}, "secret-key": {}, - "secret_key": {}, + tokenSecretKey: {}, } const secretLikeMinLen = 24 From 1fcd93d88e2e35e2209bfab7bd737afa3f811e95 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 16:52:13 +0900 Subject: [PATCH 31/38] test(logging): reuse secret key constant --- pkg/logging/sanitize_literal_secret_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/logging/sanitize_literal_secret_test.go b/pkg/logging/sanitize_literal_secret_test.go index 29521c6f..40a67932 100644 --- a/pkg/logging/sanitize_literal_secret_test.go +++ b/pkg/logging/sanitize_literal_secret_test.go @@ -10,7 +10,7 @@ func TestSanitizeKey_LiteralSecretNamesAreMasked(t *testing.T) { for _, value := range []string{ "secret-key", - "secret_key", + tokenSecretKey, "api-key", tokenAPIKey, "private-key", From 0af5a3ff624ffd664780d83ea2ca16cb87cc16ae Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 07:54:11 +0000 Subject: [PATCH 32/38] chore: record final lint diagnostics --- .agent-diagnostics/lint.log | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/.agent-diagnostics/lint.log b/.agent-diagnostics/lint.log index 672ba4d4..521f0712 100644 --- a/.agent-diagnostics/lint.log +++ b/.agent-diagnostics/lint.log @@ -26,14 +26,14 @@ go: downloading github.com/go-xmlfmt/xmlfmt v1.1.3 go: downloading github.com/golangci/revgrep v0.8.0 go: downloading github.com/mattn/go-isatty v0.0.20 go: downloading golang.org/x/sys v0.43.0 -go: downloading golang.org/x/text v0.36.0 go: downloading github.com/charmbracelet/colorprofile v0.4.3 go: downloading github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 go: downloading github.com/charmbracelet/x/ansi v0.11.7 -go: downloading github.com/charmbracelet/x/term v0.2.2 go: downloading github.com/clipperhouse/displaywidth v0.11.0 go: downloading github.com/lucasb-eyer/go-colorful v1.4.0 +go: downloading github.com/charmbracelet/x/term v0.2.2 go: downloading github.com/rivo/uniseg v0.4.7 +go: downloading golang.org/x/text v0.36.0 go: downloading github.com/fsnotify/fsnotify v1.5.4 go: downloading github.com/mitchellh/mapstructure v1.5.0 go: downloading github.com/spf13/afero v1.15.0 @@ -161,11 +161,11 @@ go: downloading github.com/davecgh/go-spew v1.1.1 go: downloading github.com/pmezard/go-difflib v1.0.0 go: downloading github.com/stretchr/objx v0.5.2 go: downloading github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e +go: downloading github.com/clipperhouse/uax29/v2 v2.7.0 +go: downloading github.com/mattn/go-runewidth v0.0.23 go: downloading github.com/charmbracelet/x/termios v0.1.1 go: downloading github.com/charmbracelet/x/windows v0.2.2 go: downloading github.com/muesli/cancelreader v0.2.2 -go: downloading github.com/clipperhouse/uax29/v2 v2.7.0 -go: downloading github.com/mattn/go-runewidth v0.0.23 go: downloading github.com/subosito/gotenv v1.4.1 go: downloading github.com/hashicorp/hcl v1.0.0 go: downloading gopkg.in/ini.v1 v1.67.0 @@ -196,21 +196,30 @@ go: downloading github.com/alfatraining/structtag v1.0.0 go: downloading github.com/dlclark/regexp2 v1.12.0 go: downloading go.uber.org/multierr v1.10.0 go: downloading github.com/ldez/structtags v0.6.1 +go: downloading github.com/quasilyte/gogrep v0.5.0 go: downloading golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 go: downloading golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b -go: downloading github.com/quasilyte/gogrep v0.5.0 go: downloading github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 +go: downloading codeberg.org/chavacava/garif v0.2.0 go: downloading github.com/beorn7/perks v1.0.1 go: downloading github.com/golang/protobuf v1.5.3 go: downloading github.com/prometheus/common v0.32.1 go: downloading github.com/prometheus/procfs v0.7.3 go: downloading google.golang.org/protobuf v1.36.10 -go: downloading codeberg.org/chavacava/garif v0.2.0 go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7 go: downloading github.com/matttproud/golang_protobuf_extensions v1.0.1 -pkg/logging/sanitize.go:35:46: string `secret_key` has 6 occurrences, make it a constant (goconst) - "client_secret", "secret", tokenPrivateKey, "secret_key", - ^ +pkg/runtime/bootstrap/bootstrap.go:11:16: could not import github.com/park285/shared-go/pkg/logging (-: # github.com/park285/shared-go/pkg/logging +pkg/logging/sanitize.go:129:10: undefined: sanitizeHandler +pkg/logging/sanitize.go:133:10: undefined: sanitizeHandler +pkg/logging/sanitize.go:159:10: undefined: sanitizeHandler +pkg/logging/sanitize.go:164:10: undefined: sanitizeHandler +pkg/logging/sanitize.go:167:10: undefined: sanitizeHandler +pkg/logging/sanitize.go:168:10: undefined: sanitizeHandler +pkg/logging/logging.go:50:18: undefined: newSanitizeHandler +pkg/logging/logging.go:141:12: undefined: newSanitizeHandler +pkg/logging/logging.go:165:12: undefined: newSanitizeHandler) (typecheck) + sharedlogging "github.com/park285/shared-go/pkg/logging" + ^ 1 issues: -* goconst: 1 +* typecheck: 1 make: *** [Makefile:23: lint] Error 1 From 5dc80a3d7a9dcd1c7614dd9039f7c8410ec97524 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 17:04:08 +0900 Subject: [PATCH 33/38] fix(logging): restore sanitize handler declaration --- pkg/logging/sanitize_handler.go | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 pkg/logging/sanitize_handler.go diff --git a/pkg/logging/sanitize_handler.go b/pkg/logging/sanitize_handler.go new file mode 100644 index 00000000..ba9c6c2b --- /dev/null +++ b/pkg/logging/sanitize_handler.go @@ -0,0 +1,11 @@ +package logging + +import "log/slog" + +type sanitizeHandler struct { + inner slog.Handler +} + +func newSanitizeHandler(inner slog.Handler) *sanitizeHandler { + return &sanitizeHandler{inner: inner} +} From 20f4d52e93109ac3032706bed8e06c1b4ee3b4fd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Jul 2026 08:06:19 +0000 Subject: [PATCH 34/38] chore: record final lint diagnostics --- .agent-diagnostics/lint.log | 32 ++++++++++---------------------- .agent-diagnostics/lint.status | 2 +- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/.agent-diagnostics/lint.log b/.agent-diagnostics/lint.log index 521f0712..6f0b379d 100644 --- a/.agent-diagnostics/lint.log +++ b/.agent-diagnostics/lint.log @@ -6,9 +6,9 @@ go: downloading github.com/fatih/color v1.19.0 go: downloading github.com/gofrs/flock v0.13.0 go: downloading github.com/ldez/grignotin v0.10.1 go: downloading github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 -go: downloading github.com/pelletier/go-toml v1.9.5 go: downloading github.com/spf13/cobra v1.10.2 go: downloading github.com/spf13/pflag v1.0.10 +go: downloading github.com/pelletier/go-toml v1.9.5 go: downloading github.com/spf13/viper v1.12.0 go: downloading go.yaml.in/yaml/v3 v3.0.4 go: downloading golang.org/x/mod v0.35.0 @@ -29,9 +29,9 @@ go: downloading golang.org/x/sys v0.43.0 go: downloading github.com/charmbracelet/colorprofile v0.4.3 go: downloading github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 go: downloading github.com/charmbracelet/x/ansi v0.11.7 +go: downloading github.com/charmbracelet/x/term v0.2.2 go: downloading github.com/clipperhouse/displaywidth v0.11.0 go: downloading github.com/lucasb-eyer/go-colorful v1.4.0 -go: downloading github.com/charmbracelet/x/term v0.2.2 go: downloading github.com/rivo/uniseg v0.4.7 go: downloading golang.org/x/text v0.36.0 go: downloading github.com/fsnotify/fsnotify v1.5.4 @@ -100,8 +100,8 @@ go: downloading github.com/xen0n/gosmopolitan v1.3.0 go: downloading github.com/uudashr/iface v1.4.2 go: downloading github.com/leonklingele/grouper v1.1.2 go: downloading github.com/julz/importas v0.2.0 -go: downloading github.com/macabu/inamedparam v0.2.0 go: downloading github.com/gordonklaus/ineffassign v0.2.0 +go: downloading github.com/macabu/inamedparam v0.2.0 go: downloading github.com/sashamelentyev/interfacebloat v1.1.0 go: downloading github.com/ckaznocha/intrange v0.3.1 go: downloading github.com/AdminBenni/iota-mixing v1.0.0 @@ -161,17 +161,17 @@ go: downloading github.com/davecgh/go-spew v1.1.1 go: downloading github.com/pmezard/go-difflib v1.0.0 go: downloading github.com/stretchr/objx v0.5.2 go: downloading github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e -go: downloading github.com/clipperhouse/uax29/v2 v2.7.0 -go: downloading github.com/mattn/go-runewidth v0.0.23 go: downloading github.com/charmbracelet/x/termios v0.1.1 go: downloading github.com/charmbracelet/x/windows v0.2.2 go: downloading github.com/muesli/cancelreader v0.2.2 +go: downloading github.com/clipperhouse/uax29/v2 v2.7.0 +go: downloading github.com/mattn/go-runewidth v0.0.23 go: downloading github.com/subosito/gotenv v1.4.1 go: downloading github.com/hashicorp/hcl v1.0.0 go: downloading gopkg.in/ini.v1 v1.67.0 go: downloading github.com/magiconair/properties v1.8.6 -go: downloading go.uber.org/zap v1.27.0 go: downloading github.com/hexops/gotextdiff v1.0.3 +go: downloading go.uber.org/zap v1.27.0 go: downloading github.com/google/go-cmp v0.7.0 go: downloading github.com/dave/dst v0.27.3 go: downloading github.com/gostaticanalysis/analysisutil v0.7.1 @@ -196,8 +196,8 @@ go: downloading github.com/alfatraining/structtag v1.0.0 go: downloading github.com/dlclark/regexp2 v1.12.0 go: downloading go.uber.org/multierr v1.10.0 go: downloading github.com/ldez/structtags v0.6.1 -go: downloading github.com/quasilyte/gogrep v0.5.0 go: downloading golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 +go: downloading github.com/quasilyte/gogrep v0.5.0 go: downloading golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b go: downloading github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 go: downloading codeberg.org/chavacava/garif v0.2.0 @@ -208,18 +208,6 @@ go: downloading github.com/prometheus/procfs v0.7.3 go: downloading google.golang.org/protobuf v1.36.10 go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7 go: downloading github.com/matttproud/golang_protobuf_extensions v1.0.1 -pkg/runtime/bootstrap/bootstrap.go:11:16: could not import github.com/park285/shared-go/pkg/logging (-: # github.com/park285/shared-go/pkg/logging -pkg/logging/sanitize.go:129:10: undefined: sanitizeHandler -pkg/logging/sanitize.go:133:10: undefined: sanitizeHandler -pkg/logging/sanitize.go:159:10: undefined: sanitizeHandler -pkg/logging/sanitize.go:164:10: undefined: sanitizeHandler -pkg/logging/sanitize.go:167:10: undefined: sanitizeHandler -pkg/logging/sanitize.go:168:10: undefined: sanitizeHandler -pkg/logging/logging.go:50:18: undefined: newSanitizeHandler -pkg/logging/logging.go:141:12: undefined: newSanitizeHandler -pkg/logging/logging.go:165:12: undefined: newSanitizeHandler) (typecheck) - sharedlogging "github.com/park285/shared-go/pkg/logging" - ^ -1 issues: -* typecheck: 1 -make: *** [Makefile:23: lint] Error 1 +0 issues. +( cd scripts/perf/benchgate && GOWORK=off bash /home/runner/work/shared-go/shared-go/scripts/ci/go-tooling.sh golangci-lint run ) +0 issues. diff --git a/.agent-diagnostics/lint.status b/.agent-diagnostics/lint.status index 0cfbf088..573541ac 100644 --- a/.agent-diagnostics/lint.status +++ b/.agent-diagnostics/lint.status @@ -1 +1 @@ -2 +0 From cea25b2cbe2035f464e7b1ced98de83f53563bc2 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 17:09:41 +0900 Subject: [PATCH 35/38] chore: remove successful lint diagnostics --- .agent-diagnostics/lint.log | 213 ------------------------------------ 1 file changed, 213 deletions(-) delete mode 100644 .agent-diagnostics/lint.log diff --git a/.agent-diagnostics/lint.log b/.agent-diagnostics/lint.log deleted file mode 100644 index 6f0b379d..00000000 --- a/.agent-diagnostics/lint.log +++ /dev/null @@ -1,213 +0,0 @@ -bash /home/runner/work/shared-go/shared-go/scripts/ci/go-tooling.sh golangci-lint run -c .golangci.yml ./... -[GO TOOLING] Installing golangci-lint@v2.12.2 -go: downloading github.com/golangci/golangci-lint/v2 v2.12.2 -go: downloading charm.land/lipgloss/v2 v2.0.3 -go: downloading github.com/fatih/color v1.19.0 -go: downloading github.com/gofrs/flock v0.13.0 -go: downloading github.com/ldez/grignotin v0.10.1 -go: downloading github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 -go: downloading github.com/spf13/cobra v1.10.2 -go: downloading github.com/spf13/pflag v1.0.10 -go: downloading github.com/pelletier/go-toml v1.9.5 -go: downloading github.com/spf13/viper v1.12.0 -go: downloading go.yaml.in/yaml/v3 v3.0.4 -go: downloading golang.org/x/mod v0.35.0 -go: downloading golang.org/x/tools v0.44.0 -go: downloading github.com/go-viper/mapstructure/v2 v2.5.0 -go: downloading github.com/hashicorp/go-version v1.9.0 -go: downloading github.com/mitchellh/go-homedir v1.1.0 -go: downloading github.com/alecthomas/chroma/v2 v2.24.1 -go: downloading github.com/rogpeppe/go-internal v1.14.1 -go: downloading github.com/golangci/plugin-module-register v0.1.2 -go: downloading github.com/mattn/go-colorable v0.1.14 -go: downloading github.com/sirupsen/logrus v1.9.4 -go: downloading github.com/stretchr/testify v1.11.1 -go: downloading github.com/go-xmlfmt/xmlfmt v1.1.3 -go: downloading github.com/golangci/revgrep v0.8.0 -go: downloading github.com/mattn/go-isatty v0.0.20 -go: downloading golang.org/x/sys v0.43.0 -go: downloading github.com/charmbracelet/colorprofile v0.4.3 -go: downloading github.com/charmbracelet/ultraviolet v0.0.0-20251205161215-1948445e3318 -go: downloading github.com/charmbracelet/x/ansi v0.11.7 -go: downloading github.com/charmbracelet/x/term v0.2.2 -go: downloading github.com/clipperhouse/displaywidth v0.11.0 -go: downloading github.com/lucasb-eyer/go-colorful v1.4.0 -go: downloading github.com/rivo/uniseg v0.4.7 -go: downloading golang.org/x/text v0.36.0 -go: downloading github.com/fsnotify/fsnotify v1.5.4 -go: downloading github.com/mitchellh/mapstructure v1.5.0 -go: downloading github.com/spf13/afero v1.15.0 -go: downloading github.com/spf13/cast v1.5.0 -go: downloading github.com/spf13/jwalterweatherman v1.1.0 -go: downloading golang.org/x/sync v0.20.0 -go: downloading github.com/daixiang0/gci v0.13.7 -go: downloading github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d -go: downloading mvdan.cc/gofumpt v0.9.2 -go: downloading github.com/golangci/golines v0.15.0 -go: downloading github.com/sourcegraph/go-diff v0.8.0 -go: downloading github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e -go: downloading go.augendre.info/arangolint v0.4.0 -go: downloading github.com/alingse/asasalint v0.0.11 -go: downloading github.com/golangci/asciicheck v0.5.0 -go: downloading github.com/breml/bidichk v0.3.3 -go: downloading github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 -go: downloading github.com/lasiar/canonicalheader v1.1.2 -go: downloading github.com/ClickHouse/clickhouse-go-linter v1.2.0 -go: downloading github.com/sivchari/containedctx v1.0.3 -go: downloading github.com/kkHAIKE/contextcheck v1.1.6 -go: downloading github.com/karamaru-alpha/copyloopvar v1.2.2 -go: downloading github.com/bkielbasa/cyclop v1.2.3 -go: downloading gitlab.com/bosi/decorder v0.4.2 -go: downloading github.com/OpenPeeDeeP/depguard/v2 v2.2.1 -go: downloading github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 -go: downloading github.com/Abirdcfly/dupword v0.1.7 -go: downloading github.com/charithe/durationcheck v0.0.11 -go: downloading github.com/manuelarte/embeddedstructfieldcheck v0.4.0 -go: downloading github.com/Djarvur/go-err113 v0.1.1 -go: downloading github.com/kisielk/errcheck v1.10.0 -go: downloading github.com/breml/errchkjson v0.4.1 -go: downloading github.com/Antonboom/errname v1.1.1 -go: downloading codeberg.org/polyfloyd/go-errorlint v1.9.0 -go: downloading github.com/nishanths/exhaustive v0.12.0 -go: downloading dev.gaijin.team/go/exhaustruct/v4 v4.0.0 -go: downloading github.com/ldez/exptostd v0.4.5 -go: downloading go.augendre.info/fatcontext v0.9.0 -go: downloading github.com/ashanbrown/forbidigo/v2 v2.3.1 -go: downloading github.com/gostaticanalysis/forcetypeassert v0.2.0 -go: downloading github.com/manuelarte/funcorder v0.6.0 -go: downloading github.com/ultraware/funlen v0.2.0 -go: downloading github.com/nunnatsa/ginkgolinter v0.23.0 -go: downloading 4d63.com/gocheckcompilerdirectives v1.3.0 -go: downloading 4d63.com/gochecknoglobals v0.2.2 -go: downloading github.com/alecthomas/go-check-sumtype v0.3.1 -go: downloading github.com/uudashr/gocognit v1.2.1 -go: downloading github.com/jgautheron/goconst v1.10.0 -go: downloading github.com/go-critic/go-critic v0.14.3 -go: downloading github.com/quasilyte/go-ruleguard/dsl v0.3.23 -go: downloading github.com/fzipp/gocyclo v0.6.0 -go: downloading github.com/quasilyte/go-ruleguard v0.4.5 -go: downloading github.com/godoc-lint/godoc-lint v0.11.2 -go: downloading github.com/tetafro/godot v1.5.6 -go: downloading github.com/matoous/godox v1.1.0 -go: downloading github.com/denis-tingaikin/go-header v0.5.0 -go: downloading github.com/ldez/gomoddirectives v0.8.0 -go: downloading github.com/Masterminds/semver/v3 v3.5.0 -go: downloading github.com/ryancurrah/gomodguard v1.4.1 -go: downloading github.com/ryancurrah/gomodguard/v2 v2.1.3 -go: downloading github.com/golangci/go-printf-func-name v0.1.1 -go: downloading github.com/securego/gosec/v2 v2.26.1 -go: downloading github.com/xen0n/gosmopolitan v1.3.0 -go: downloading github.com/uudashr/iface v1.4.2 -go: downloading github.com/leonklingele/grouper v1.1.2 -go: downloading github.com/julz/importas v0.2.0 -go: downloading github.com/gordonklaus/ineffassign v0.2.0 -go: downloading github.com/macabu/inamedparam v0.2.0 -go: downloading github.com/sashamelentyev/interfacebloat v1.1.0 -go: downloading github.com/ckaznocha/intrange v0.3.1 -go: downloading github.com/AdminBenni/iota-mixing v1.0.0 -go: downloading github.com/butuzov/ireturn v0.4.1 -go: downloading github.com/timonwong/loggercheck v0.11.0 -go: downloading github.com/yagipy/maintidx v1.0.0 -go: downloading github.com/ashanbrown/makezero/v2 v2.2.1 -go: downloading github.com/butuzov/mirror v1.3.0 -go: downloading github.com/golangci/misspell v0.8.0 -go: downloading github.com/tommy-muehle/go-mnd/v2 v2.5.1 -go: downloading go-simpler.org/musttag v0.14.0 -go: downloading github.com/alexkohler/nakedret/v2 v2.0.6 -go: downloading github.com/nakabonne/nestif v0.3.1 -go: downloading github.com/gostaticanalysis/nilerr v0.1.2 -go: downloading github.com/alingse/nilnesserr v0.2.0 -go: downloading github.com/Antonboom/nilnil v1.1.1 -go: downloading github.com/ssgreg/nlreturn/v2 v2.2.1 -go: downloading github.com/sonatard/noctx v0.5.1 -go: downloading github.com/AlwxSin/noinlineerr v1.0.5 -go: downloading github.com/firefart/nonamedreturns v1.0.6 -go: downloading github.com/stbenjam/no-sprintf-host-port v0.3.1 -go: downloading github.com/kunwardeep/paralleltest v1.0.15 -go: downloading github.com/catenacyber/perfsprint v0.10.1 -go: downloading github.com/alexkohler/prealloc v1.1.0 -go: downloading github.com/nishanths/predeclared v0.2.2 -go: downloading github.com/yeya24/promlinter v0.3.0 -go: downloading github.com/ghostiam/protogetter v0.3.20 -go: downloading github.com/curioswitch/go-reassign v0.3.0 -go: downloading github.com/raeperd/recvcheck v0.2.0 -go: downloading github.com/BurntSushi/toml v1.6.0 -go: downloading github.com/mgechev/revive v1.15.0 -go: downloading github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba -go: downloading go-simpler.org/sloglint v0.12.0 -go: downloading github.com/jjti/go-spancheck v0.6.5 -go: downloading github.com/ryanrolds/sqlclosecheck v0.6.0 -go: downloading honnef.co/go/tools v0.7.0 -go: downloading github.com/4meepo/tagalign v1.4.3 -go: downloading github.com/ldez/tagliatelle v0.7.2 -go: downloading github.com/maratori/testableexamples v1.0.1 -go: downloading github.com/Antonboom/testifylint v1.6.4 -go: downloading github.com/maratori/testpackage v1.1.2 -go: downloading github.com/kulti/thelper v0.7.1 -go: downloading github.com/moricho/tparallel v0.3.2 -go: downloading github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e -go: downloading mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 -go: downloading github.com/MirrexOne/unqueryvet v1.5.4 -go: downloading github.com/sashamelentyev/usestdlibvars v1.29.0 -go: downloading github.com/ldez/usetesting v0.5.0 -go: downloading github.com/blizzy78/varnamelen v0.8.0 -go: downloading github.com/sanposhiho/wastedassign/v2 v2.1.0 -go: downloading github.com/ultraware/whitespace v0.2.0 -go: downloading github.com/tomarrell/wrapcheck/v2 v2.12.0 -go: downloading github.com/bombsimon/wsl/v4 v4.7.0 -go: downloading github.com/bombsimon/wsl/v5 v5.8.0 -go: downloading github.com/ykadowak/zerologlint v0.1.5 -go: downloading github.com/davecgh/go-spew v1.1.1 -go: downloading github.com/pmezard/go-difflib v1.0.0 -go: downloading github.com/stretchr/objx v0.5.2 -go: downloading github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e -go: downloading github.com/charmbracelet/x/termios v0.1.1 -go: downloading github.com/charmbracelet/x/windows v0.2.2 -go: downloading github.com/muesli/cancelreader v0.2.2 -go: downloading github.com/clipperhouse/uax29/v2 v2.7.0 -go: downloading github.com/mattn/go-runewidth v0.0.23 -go: downloading github.com/subosito/gotenv v1.4.1 -go: downloading github.com/hashicorp/hcl v1.0.0 -go: downloading gopkg.in/ini.v1 v1.67.0 -go: downloading github.com/magiconair/properties v1.8.6 -go: downloading github.com/hexops/gotextdiff v1.0.3 -go: downloading go.uber.org/zap v1.27.0 -go: downloading github.com/google/go-cmp v0.7.0 -go: downloading github.com/dave/dst v0.27.3 -go: downloading github.com/gostaticanalysis/analysisutil v0.7.1 -go: downloading github.com/go-toolsmith/astcast v1.1.0 -go: downloading github.com/gobwas/glob v0.2.3 -go: downloading dev.gaijin.team/go/golib v0.6.0 -go: downloading github.com/go-toolsmith/astcopy v1.1.0 -go: downloading github.com/go-toolsmith/astequal v1.2.0 -go: downloading github.com/go-toolsmith/astfmt v1.1.0 -go: downloading github.com/go-toolsmith/astp v1.1.0 -go: downloading github.com/go-toolsmith/strparse v1.1.0 -go: downloading github.com/go-toolsmith/typep v1.1.0 -go: downloading github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 -go: downloading github.com/ccojocar/zxcvbn-go v1.0.4 -go: downloading github.com/gostaticanalysis/comment v1.5.0 -go: downloading github.com/prometheus/client_golang v1.12.1 -go: downloading github.com/prometheus/client_model v0.2.0 -go: downloading github.com/fatih/structtag v1.2.0 -go: downloading github.com/ettle/strcase v0.2.0 -go: downloading github.com/hashicorp/go-immutable-radix/v2 v2.1.0 -go: downloading github.com/alfatraining/structtag v1.0.0 -go: downloading github.com/dlclark/regexp2 v1.12.0 -go: downloading go.uber.org/multierr v1.10.0 -go: downloading github.com/ldez/structtags v0.6.1 -go: downloading golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 -go: downloading github.com/quasilyte/gogrep v0.5.0 -go: downloading golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b -go: downloading github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 -go: downloading codeberg.org/chavacava/garif v0.2.0 -go: downloading github.com/beorn7/perks v1.0.1 -go: downloading github.com/golang/protobuf v1.5.3 -go: downloading github.com/prometheus/common v0.32.1 -go: downloading github.com/prometheus/procfs v0.7.3 -go: downloading google.golang.org/protobuf v1.36.10 -go: downloading github.com/hashicorp/golang-lru/v2 v2.0.7 -go: downloading github.com/matttproud/golang_protobuf_extensions v1.0.1 -0 issues. -( cd scripts/perf/benchgate && GOWORK=off bash /home/runner/work/shared-go/shared-go/scripts/ci/go-tooling.sh golangci-lint run ) -0 issues. From 94e9ce43c2afad37de610677a167a5cd15a26af2 Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 17:09:58 +0900 Subject: [PATCH 36/38] chore: remove lint status diagnostics --- .agent-diagnostics/lint.status | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .agent-diagnostics/lint.status diff --git a/.agent-diagnostics/lint.status b/.agent-diagnostics/lint.status deleted file mode 100644 index 573541ac..00000000 --- a/.agent-diagnostics/lint.status +++ /dev/null @@ -1 +0,0 @@ -0 From d2e684232397f2d9eeea8515017b4cc6096a9eba Mon Sep 17 00:00:00 2001 From: park285 Date: Wed, 15 Jul 2026 17:10:11 +0900 Subject: [PATCH 37/38] chore: remove temporary lint workflow --- .github/workflows/agent-lint-diagnose.yml | 42 ----------------------- 1 file changed, 42 deletions(-) delete mode 100644 .github/workflows/agent-lint-diagnose.yml diff --git a/.github/workflows/agent-lint-diagnose.yml b/.github/workflows/agent-lint-diagnose.yml deleted file mode 100644 index 215c1dde..00000000 --- a/.github/workflows/agent-lint-diagnose.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: agent lint diagnose - -on: - push: - branches: - - agent/security-hardening-20260715 - paths-ignore: - - .agent-diagnostics/** - -permissions: - contents: write - -jobs: - diagnose: - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - name: Checkout branch - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: agent/security-hardening-20260715 - persist-credentials: true - - name: Setup Go - uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6.5.0 - with: - go-version-file: go.mod - cache-dependency-path: go.sum - - name: Capture lint output - run: | - set +e - make lint > /tmp/lint.log 2>&1 - status=$? - cat /tmp/lint.log - mkdir -p .agent-diagnostics - cp /tmp/lint.log .agent-diagnostics/lint.log - printf '%s\n' "$status" > .agent-diagnostics/lint.status - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add .agent-diagnostics - git commit -m "chore: record final lint diagnostics" - git push origin HEAD:agent/security-hardening-20260715 - exit "$status" From 010b4adf5293e932b69321ebf41f8c8bc9618278 Mon Sep 17 00:00:00 2001 From: kapu Date: Wed, 15 Jul 2026 21:57:42 +0900 Subject: [PATCH 38/38] =?UTF-8?q?perf(promptguard):=20raw=20literal=20pref?= =?UTF-8?q?ilter=20=EB=B3=B4=EC=95=88=20=EC=9D=98=EB=AF=B8=20=EB=B3=B4?= =?UTF-8?q?=EC=A1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - compatibility literal을 normalized prefilter 표현으로 컴파일합니다. - raw matcher false negative 없이 decoder hot path prefilter를 유지합니다. --- pkg/promptguard/rulepack_compile.go | 21 +++++++++++++++++++++ pkg/promptguard/rulepack_match.go | 12 +++++------- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/pkg/promptguard/rulepack_compile.go b/pkg/promptguard/rulepack_compile.go index 724d605e..01b42e6b 100644 --- a/pkg/promptguard/rulepack_compile.go +++ b/pkg/promptguard/rulepack_compile.go @@ -76,6 +76,7 @@ func compileRule(rule *rawRule) (compiledRule, error) { if err := compileRuleMatcher(&compiled, rule); err != nil { return compiledRule{}, fmt.Errorf("compile matcher: %w", err) } + compiled.RequiredLiteralGroups = normalizePrefilterLiteralGroups(compiled.RequiredLiteralGroups) if err := validateCompiledRule(&compiled); err != nil { return compiledRule{}, fmt.Errorf("validate rule: %w", err) @@ -84,6 +85,26 @@ func compileRule(rule *rawRule) (compiledRule, error) { return compiled, nil } +func normalizePrefilterLiteralGroups(groups [][]string) [][]string { + normalized := make([][]string, 0, len(groups)) + for _, group := range groups { + values := make([]string, 0, len(group)) + for _, literal := range group { + value := normalizeViews(literal).Norm + if value != "" { + values = append(values, value) + } + } + if len(values) == 0 { + continue + } + slices.Sort(values) + normalized = append(normalized, slices.Compact(values)) + } + + return normalized +} + func compileRuleSelectors(rule *rawRule) (ruleSelectors, error) { action := normalizeAction(rule.Action) if strings.TrimSpace(rule.Action) != "" && action == "" { diff --git a/pkg/promptguard/rulepack_match.go b/pkg/promptguard/rulepack_match.go index c3d8c10d..328d6525 100644 --- a/pkg/promptguard/rulepack_match.go +++ b/pkg/promptguard/rulepack_match.go @@ -48,13 +48,11 @@ func (r *compiledRule) matchInput(segment textSegment, policy compiledPolicy) (s if strings.TrimSpace(text) == "" { return "", 0, false } - // Required literals are compiled in the matcher's own representation. A raw - // rule can intentionally contain compatibility characters, provider syntax, - // or other bytes that normalize to a different surface. Prefiltering those - // rules against the normalized view can therefore create false negatives. - // Keep the optimization for normalized/joined views, but always execute raw - // matchers against the raw surface. - if r.View != viewRaw && !containsAllLiteralGroups(text, r.RequiredLiteralGroups) { + prefilterText := text + if r.View == viewRaw { + prefilterText = segment.Views.Norm + } + if !containsAllLiteralGroups(prefilterText, r.RequiredLiteralGroups) { return "", 0, false }