From e64cfa3e35aefe6ec3bfccf7e57d319718bb9a78 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 8 Aug 2026 19:23:03 -0300 Subject: [PATCH 1/2] refactor(auth): deepen audit-log verification behind a shared core --- CHANGELOG.md | 1 + cmd/app/commands/verify_audit_logs.go | 79 +++++++---- cmd/app/commands/verify_audit_logs_test.go | 39 +++++ docs/cli-commands.md | 2 + internal/auth/usecase/audit_log_usecase.go | 82 +++++++---- .../auth/usecase/audit_log_usecase_test.go | 134 ++++++++++++++++++ internal/auth/usecase/interface.go | 14 +- 7 files changed, 295 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca01cb3..2ba3f79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Deepened audit-log signature verification behind a single shared core used by both single-log and batch verification. Batch `verify-audit-logs` now reports KEK-missing logs in their own `kek_missing_count`/`kek_missing_logs` bucket instead of lumping them into `invalid_count`, so batch agrees with single-log verification (`ErrKekNotFoundForLog`). - Internal refactors with no API change: consolidated the six retention-sweep CLI commands (`purge-secrets`, `purge-transit-keys`, `purge-tokenization-keys`, `clean-expired-tokens`, `clean-audit-logs`, `purge-auth-tokens`) behind a single `RunRetentionSweep` module, and relocated the interactive policy-prompt helpers from `internal/ui` into the CLI commands package (#141). - Folded the master-key/KMS lifecycle into the `keyring` deep module. KEK loading (`bootstrapWith`) and the KMS decrypt path are now unit-testable without a database or live KMS; `KMSKeeper` gained an explicit `Encrypt` operation so the create/rotate-master-key CLI commands no longer type-assert the concrete keeper; added an in-memory `keyring.FakeKMSService` test double and removed the unused `internal/tokenization/testing` helper. diff --git a/cmd/app/commands/verify_audit_logs.go b/cmd/app/commands/verify_audit_logs.go index 748357f..26c2894 100644 --- a/cmd/app/commands/verify_audit_logs.go +++ b/cmd/app/commands/verify_audit_logs.go @@ -13,15 +13,17 @@ import ( // VerifyAuditLogsResult holds the result of the audit log verification operation. type VerifyAuditLogsResult struct { - TotalChecked int64 `json:"total_checked"` - SignedCount int64 `json:"signed_count"` - UnsignedCount int64 `json:"unsigned_count"` - ValidCount int64 `json:"valid_count"` - InvalidCount int64 `json:"invalid_count"` - InvalidLogs []string `json:"invalid_logs"` - Passed bool `json:"passed"` - StartDate time.Time `json:"start_date"` - EndDate time.Time `json:"end_date"` + TotalChecked int64 `json:"total_checked"` + SignedCount int64 `json:"signed_count"` + UnsignedCount int64 `json:"unsigned_count"` + ValidCount int64 `json:"valid_count"` + InvalidCount int64 `json:"invalid_count"` + InvalidLogs []string `json:"invalid_logs"` + KekMissingCount int64 `json:"kek_missing_count"` + KekMissingLogs []string `json:"kek_missing_logs"` + Passed bool `json:"passed"` + StartDate time.Time `json:"start_date"` + EndDate time.Time `json:"end_date"` } // ToText returns a human-readable representation of the verification result. @@ -38,14 +40,28 @@ func (r *VerifyAuditLogsResult) ToText() string { output += fmt.Sprintf("Signed: %d\n", r.SignedCount) output += fmt.Sprintf("Unsigned: %d (legacy)\n", r.UnsignedCount) output += fmt.Sprintf("Valid: %d\n", r.ValidCount) - output += fmt.Sprintf("Invalid: %d\n\n", r.InvalidCount) + output += fmt.Sprintf("Invalid: %d\n", r.InvalidCount) + output += fmt.Sprintf("Kek Missing: %d\n", r.KekMissingCount) switch { - case r.InvalidCount > 0: - output += fmt.Sprintf("WARNING: %d log(s) failed integrity check!\n\n", r.InvalidCount) - output += "Invalid Log IDs:\n" - for _, id := range r.InvalidLogs { - output += fmt.Sprintf(" - %s\n", id) + case r.InvalidCount > 0 || r.KekMissingCount > 0: + output += "\n" + if r.InvalidCount > 0 { + output += fmt.Sprintf("WARNING: %d log(s) failed integrity check!\n\n", r.InvalidCount) + output += "Invalid Log IDs:\n" + for _, id := range r.InvalidLogs { + output += fmt.Sprintf(" - %s\n", id) + } + } + if r.KekMissingCount > 0 { + output += fmt.Sprintf( + "WARNING: %d log(s) have an unavailable KEK and could not be verified.\n\n", + r.KekMissingCount, + ) + output += "Kek Missing Log IDs:\n" + for _, id := range r.KekMissingLogs { + output += fmt.Sprintf(" - %s\n", id) + } } output += "\nStatus: FAILED ❌" case r.TotalChecked == 0: @@ -103,18 +119,24 @@ func RunVerifyAuditLogs( for i, id := range report.InvalidLogs { invalidLogs[i] = id.String() } + kekMissingLogs := make([]string, len(report.KekMissingLogs)) + for i, id := range report.KekMissingLogs { + kekMissingLogs[i] = id.String() + } // Output result result := &VerifyAuditLogsResult{ - TotalChecked: report.TotalChecked, - SignedCount: report.SignedCount, - UnsignedCount: report.UnsignedCount, - ValidCount: report.ValidCount, - InvalidCount: report.InvalidCount, - InvalidLogs: invalidLogs, - Passed: report.InvalidCount == 0, - StartDate: start, - EndDate: end, + TotalChecked: report.TotalChecked, + SignedCount: report.SignedCount, + UnsignedCount: report.UnsignedCount, + ValidCount: report.ValidCount, + InvalidCount: report.InvalidCount, + InvalidLogs: invalidLogs, + KekMissingCount: report.KekMissingCount, + KekMissingLogs: kekMissingLogs, + Passed: report.InvalidCount == 0 && report.KekMissingCount == 0, + StartDate: start, + EndDate: end, } WriteOutput(writer, format, result) @@ -123,12 +145,17 @@ func RunVerifyAuditLogs( slog.Int64("total_checked", report.TotalChecked), slog.Int64("valid", report.ValidCount), slog.Int64("invalid", report.InvalidCount), + slog.Int64("kek_missing", report.KekMissingCount), slog.Int64("unsigned", report.UnsignedCount), ) // Exit with error code if integrity check failed - if report.InvalidCount > 0 { - return fmt.Errorf("integrity check failed: %d invalid signature(s)", report.InvalidCount) + if report.InvalidCount > 0 || report.KekMissingCount > 0 { + return fmt.Errorf( + "integrity check failed: %d invalid signature(s), %d KEK missing", + report.InvalidCount, + report.KekMissingCount, + ) } return nil diff --git a/cmd/app/commands/verify_audit_logs_test.go b/cmd/app/commands/verify_audit_logs_test.go index c3af6c0..f53ccd8 100644 --- a/cmd/app/commands/verify_audit_logs_test.go +++ b/cmd/app/commands/verify_audit_logs_test.go @@ -55,6 +55,45 @@ func TestRunVerifyAuditLogs(t *testing.T) { mockUseCase.AssertExpectations(t) }) + t.Run("text-output-kek-missing-fails", func(t *testing.T) { + mockUseCase := &authMocks.MockAuditLogUseCase{} + kekMissingLogs := []uuid.UUID{uuid.New()} + mockUseCase.On("VerifyBatch", ctx, mock.Anything, mock.Anything). + Return(&authUseCase.VerificationReport{ + TotalChecked: 5, + KekMissingCount: 1, + KekMissingLogs: kekMissingLogs, + }, nil) + + var out bytes.Buffer + err := RunVerifyAuditLogs(ctx, mockUseCase, logger, &out, startDate, endDate, "text") + + require.Error(t, err) + require.Contains(t, out.String(), "Status: FAILED") + require.Contains(t, out.String(), "Kek Missing") + require.Contains(t, out.String(), kekMissingLogs[0].String()) + mockUseCase.AssertExpectations(t) + }) + + t.Run("json-output-kek-missing-fails", func(t *testing.T) { + mockUseCase := &authMocks.MockAuditLogUseCase{} + mockUseCase.On("VerifyBatch", ctx, mock.Anything, mock.Anything). + Return(&authUseCase.VerificationReport{ + TotalChecked: 10, + ValidCount: 9, + KekMissingCount: 1, + KekMissingLogs: []uuid.UUID{uuid.New()}, + }, nil) + + var out bytes.Buffer + err := RunVerifyAuditLogs(ctx, mockUseCase, logger, &out, startDate, endDate, "json") + + require.Error(t, err) + require.Contains(t, out.String(), `"kek_missing_count": 1`) + require.Contains(t, out.String(), `"passed": false`) + mockUseCase.AssertExpectations(t) + }) + t.Run("json-output", func(t *testing.T) { mockUseCase := &authMocks.MockAuditLogUseCase{} mockUseCase.On("VerifyBatch", ctx, mock.Anything, mock.Anything). diff --git a/docs/cli-commands.md b/docs/cli-commands.md index a940c6b..497f26a 100644 --- a/docs/cli-commands.md +++ b/docs/cli-commands.md @@ -649,6 +649,8 @@ Output (JSON format): "valid_count": 120, "invalid_count": 0, "invalid_logs": [], + "kek_missing_count": 0, + "kek_missing_logs": [], "passed": true } ``` diff --git a/internal/auth/usecase/audit_log_usecase.go b/internal/auth/usecase/audit_log_usecase.go index de7082a..c4eb5e8 100644 --- a/internal/auth/usecase/audit_log_usecase.go +++ b/internal/auth/usecase/audit_log_usecase.go @@ -105,6 +105,42 @@ func (a *auditLogUseCase) DeleteOlderThan( return count, nil } +// signatureStatus classifies the outcome of verifying one audit log's HMAC +// signature. Shared by VerifyIntegrity (single) and VerifyBatch (batch) so the +// two paths never disagree about what a signature failure means. +type signatureStatus int + +const ( + sigValid signatureStatus = iota + sigMissing // legacy/unsigned: no signature data + sigKekMissing // KEK referenced by the log is not in the chain + sigInvalid // signature present but does not verify +) + +// verifyAuditSignature verifies one audit log's signature against the KEK it +// references. The status classifies the crypto outcome; err is non-nil only for +// canonicalization failures, which single-log verification surfaces distinctly +// from a tampered signature. +func (a *auditLogUseCase) verifyAuditSignature(log *authDomain.AuditLog) (signatureStatus, error) { + if !log.IsSigned || log.KekID == nil { + return sigMissing, nil + } + + canonical, err := log.Canonical() + if err != nil { + return sigInvalid, apperrors.Wrap(err, "failed to canonicalize audit log") + } + + if err := a.keySigner.VerifyWithKey(*log.KekID, canonical, log.Signature); err != nil { + if errors.Is(err, keyring.ErrKekNotFound) { + return sigKekMissing, nil + } + return sigInvalid, nil + } + + return sigValid, nil +} + // VerifyIntegrity verifies the cryptographic signature of a specific audit log. // Retrieves the log from the repository and validates its HMAC-SHA256 signature // using the KEK referenced by log.KekID. Returns nil if valid, error otherwise. @@ -115,23 +151,19 @@ func (a *auditLogUseCase) VerifyIntegrity(ctx context.Context, id uuid.UUID) (er return apperrors.Wrap(err, "failed to retrieve audit log") } - // Check if legacy unsigned log - if !auditLog.IsSigned || auditLog.KekID == nil { - return authDomain.ErrSignatureMissing - } - - canonical, err := auditLog.Canonical() + status, err := a.verifyAuditSignature(auditLog) if err != nil { - return apperrors.Wrap(err, "failed to canonicalize audit log") + return err } - if err = a.keySigner.VerifyWithKey(*auditLog.KekID, canonical, auditLog.Signature); err != nil { - if errors.Is(err, keyring.ErrKekNotFound) { - return authDomain.ErrKekNotFoundForLog - } + switch status { + case sigMissing: + return authDomain.ErrSignatureMissing + case sigKekMissing: + return authDomain.ErrKekNotFoundForLog + case sigInvalid: return apperrors.Wrap(authDomain.ErrSignatureInvalid, "audit log signature verification failed") } - return nil } @@ -143,7 +175,8 @@ func (a *auditLogUseCase) VerifyBatch( startTime, endTime time.Time, ) (result *VerificationReport, err error) { report := &VerificationReport{ - InvalidLogs: []uuid.UUID{}, + InvalidLogs: []uuid.UUID{}, + KekMissingLogs: []uuid.UUID{}, } // Paginate through logs in batches using cursor-based pagination @@ -165,28 +198,29 @@ func (a *auditLogUseCase) VerifyBatch( for _, log := range logs { report.TotalChecked++ - // Check if signed - if !log.IsSigned || log.KekID == nil { + status, err := a.verifyAuditSignature(log) + if status == sigMissing { report.UnsignedCount++ continue } report.SignedCount++ - - canonical, err := log.Canonical() - if err != nil { - report.InvalidCount++ - report.InvalidLogs = append(report.InvalidLogs, log.ID) + if err == nil && status == sigValid { + report.ValidCount++ continue } - if err := a.keySigner.VerifyWithKey(*log.KekID, canonical, log.Signature); err != nil { + // Tampered, KEK-missing, or unverifiable signature: distinct buckets so + // batch agrees with single-log verification (ErrKekNotFoundForLog is + // not reported as a tampered signature). + switch status { + case sigKekMissing: + report.KekMissingCount++ + report.KekMissingLogs = append(report.KekMissingLogs, log.ID) + default: // sigInvalid, or unverifiable (canonicalization) signature report.InvalidCount++ report.InvalidLogs = append(report.InvalidLogs, log.ID) - continue } - - report.ValidCount++ } // Check if we have more pages diff --git a/internal/auth/usecase/audit_log_usecase_test.go b/internal/auth/usecase/audit_log_usecase_test.go index 09fe022..d2cf0e7 100644 --- a/internal/auth/usecase/audit_log_usecase_test.go +++ b/internal/auth/usecase/audit_log_usecase_test.go @@ -515,3 +515,137 @@ func TestAuditLogUseCase_ListCursor(t *testing.T) { mockRepo.AssertExpectations(t) }) } + +// TestAuditLogUseCase_verifyAuditSignature covers the shared verification core +// used by both VerifyIntegrity and VerifyBatch, so the tamper-detection logic +// has a focused test surface that is independent of the repository. +func TestAuditLogUseCase_verifyAuditSignature(t *testing.T) { + base := &authDomain.AuditLog{ + ID: uuid.Must(uuid.NewV7()), + RequestID: uuid.Must(uuid.NewV7()), + ClientID: uuid.Must(uuid.NewV7()), + Capability: authDomain.ReadCapability, + Path: "/secrets/test", + CreatedAt: time.Now().UTC(), + } + + // signed returns a copy of base carrying a valid HMAC signature produced by + // keyring.NewFake (fixed zero key), so the same fake verifies it. + signed := func() *authDomain.AuditLog { + fake := keyring.NewFake() + log := *base + kekID := uuid.New() + log.KekID = &kekID + log.IsSigned = true + canonical, err := log.Canonical() + if err != nil { + t.Fatalf("canonicalize: %v", err) + } + sig, _, err := fake.SignWithKey(canonical) + if err != nil { + t.Fatalf("sign: %v", err) + } + log.Signature = sig + return &log + } + + tampered := func() *authDomain.AuditLog { + log := signed() + log.Signature[0] ^= 0xff + return log + } + + tests := []struct { + name string + log *authDomain.AuditLog + signer keyring.KeySigner + want signatureStatus + wantErr bool + }{ + { + name: "valid signature", + log: signed(), + signer: keyring.NewFake(), + want: sigValid, + }, + { + name: "tampered signature", + log: tampered(), + signer: keyring.NewFake(), + want: sigInvalid, + }, + { + name: "legacy unsigned log", + log: &authDomain.AuditLog{ID: base.ID, CreatedAt: base.CreatedAt}, + signer: keyring.NewFake(), + want: sigMissing, + }, + { + name: "kek missing from chain", + log: signed(), + signer: &keyring.Fake{FailSign: keyring.ErrKekNotFound}, + want: sigKekMissing, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uc := &auditLogUseCase{keySigner: tt.signer} + got, err := uc.verifyAuditSignature(tt.log) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +// TestAuditLogUseCase_VerifyBatch_KekMissing asserts that a KEK-missing log is +// reported in its own bucket rather than counted as an invalid (tampered) +// signature — the divergence fix that makes batch agree with single-log +// verification (ErrKekNotFoundForLog). +func TestAuditLogUseCase_VerifyBatch_KekMissing(t *testing.T) { + ctx := context.Background() + start := time.Now().Add(-time.Hour) + end := time.Now() + + logs := []*authDomain.AuditLog{ + signedAuditLog(t), + signedAuditLog(t), + } + + mockRepo := &mockAuditLogRepository{} + mockRepo.On("ListCursor", ctx, (*uuid.UUID)(nil), 1000, mock.Anything, mock.Anything, (*uuid.UUID)(nil)). + Return(logs, nil). + Once() + + uc := &auditLogUseCase{ + auditLogRepo: mockRepo, + keySigner: &keyring.Fake{FailSign: keyring.ErrKekNotFound}, + } + + report, err := uc.VerifyBatch(ctx, start, end) + + assert.NoError(t, err) + assert.Equal(t, int64(2), report.KekMissingCount) + assert.Equal(t, int64(0), report.InvalidCount) + assert.Len(t, report.KekMissingLogs, 2) + mockRepo.AssertExpectations(t) +} + +// signedAuditLog returns an AuditLog marked signed with a placeholder KEK, so a +// signing failure on VerifyWithKey is attributable to the signer, not to a +// missing signature. +func signedAuditLog(t *testing.T) *authDomain.AuditLog { + t.Helper() + kekID := uuid.New() + return &authDomain.AuditLog{ + ID: uuid.Must(uuid.NewV7()), + IsSigned: true, + KekID: &kekID, + Signature: make([]byte, 32), + CreatedAt: time.Now().UTC(), + } +} diff --git a/internal/auth/usecase/interface.go b/internal/auth/usecase/interface.go index 2d753a0..eda6864 100644 --- a/internal/auth/usecase/interface.go +++ b/internal/auth/usecase/interface.go @@ -164,10 +164,12 @@ type AuditLogUseCase interface { // VerificationReport summarizes batch audit log verification results. // Used by VerifyBatch to provide detailed integrity check statistics. type VerificationReport struct { - TotalChecked int64 // Total number of audit logs checked - SignedCount int64 // Number of signed logs with signatures - UnsignedCount int64 // Number of unsigned legacy logs - ValidCount int64 // Number of logs with valid signatures - InvalidCount int64 // Number of logs with invalid signatures - InvalidLogs []uuid.UUID // IDs of logs with invalid signatures (for investigation) + TotalChecked int64 // Total number of audit logs checked + SignedCount int64 // Number of signed logs with signatures + UnsignedCount int64 // Number of unsigned legacy logs + ValidCount int64 // Number of logs with valid signatures + InvalidCount int64 // Number of logs with invalid signatures (tampered) + InvalidLogs []uuid.UUID // IDs of logs with invalid signatures (for investigation) + KekMissingCount int64 // Number of signed logs whose KEK is not in the chain + KekMissingLogs []uuid.UUID // IDs of logs whose KEK is missing (for investigation) } From 388fa81faf8b3fae543673eb8cf51d6473744746 Mon Sep 17 00:00:00 2001 From: Allisson Azevedo Date: Sat, 8 Aug 2026 19:29:05 -0300 Subject: [PATCH 2/2] build(deps): bump grpc to v1.82.1 to fix GO-2026-6061 --- go.mod | 6 +++--- go.sum | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 2d9f39c..4025c47 100644 --- a/go.mod +++ b/go.mod @@ -131,9 +131,9 @@ require ( golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/api v0.272.0 // indirect google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect - google.golang.org/grpc v1.79.3 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/grpc v1.82.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 393c766..ee83f3c 100644 --- a/go.sum +++ b/go.sum @@ -344,18 +344,18 @@ golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= -gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE= google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw= -google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5 h1:CogIeEXn4qWYzzQU0QqvYBM8yDF9cFYzDq9ojSpv0Js= -google.golang.org/genproto/googleapis/api v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:EIQZ5bFCfRQDV4MhRle7+OgjNtZ6P1PiZBgAKuxXu/Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=