From c35671f64290efd90ea5fe79a59c3e7e4add6edd Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 16 Jul 2026 19:45:59 +0300 Subject: [PATCH 1/2] ci: bump gosec pin from v2.26.1 to v2.28.0 The latest securego/gosec release is v2.28.0. Update the self-install in the Security Scanning job so CI runs the current version. PR #1376 (pre-commit gosec hook) is still open; a comment has been posted there asking to align its pin to v2.28.0 when it lands. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2a7fbb34..dd69b2065 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -325,7 +325,7 @@ jobs: # Install pinned gosec using the job's existing setup-go. # The securego/gosec Docker action bundles its own Go toolchain which # cannot satisfy the module's go directive, causing a toolchain mismatch. - go install github.com/securego/gosec/v2/cmd/gosec@v2.26.1 + go install github.com/securego/gosec/v2/cmd/gosec@v2.28.0 # Multi-module repo: each ./... only walks the current module so scanning root # alone silently misses pkg/ and providers/*. Mirror the govulncheck per-module # loop, collect per-module SARIF, then merge for the upload step. From e1287009ee8eea8733184b93ce3d25eea2dd6d1e Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Thu, 16 Jul 2026 19:46:37 +0300 Subject: [PATCH 2/2] ci: retire 10 #nosec annotations obsolete in gosec v2.28.0 Tested each annotation by temporarily removing it and running gosec v2.28.0 on the owning module. Findings that no longer appear indicate the rule was removed or no longer triggers on the pattern. Retired (finding gone in v2.28.0): - G201 x6: postgres_analytics.go (x4) and analytics_postgres.go (x2) -- gosec v2.28.0 removed the G201 SQL-format-string rule entirely - G101 x2: email/templates.go -- gosec no longer flags email copy text containing the word "password" as a hardcoded credential - G101 x1: credentials/resolver.go:27 (azure_client_secret constant) -- gosec no longer flags this string; sibling GCP constants still flagged - G122 x1 (partial): deploy/frontend.go annotation updated from G304,G122 to G304 -- G122 is not a recognized rule in v2.28.0 Kept (finding still returned after removing the annotation): - G302,G304: pkg/common/audit.go -- 0644 file permission + path variable - G404: pkg/retry/exponential.go and providers/aws/recommendations/ratelimiter.go - G115, G101, G704, G703, G304, G204, G117, G505, G706, G104, G703, G705: all remaining annotations (verified per-module) Full verification: gosec v2.28.0 on all 6 modules -> 0 findings; go build ./... and go vet ./... clean; tests pass on all touched packages. --- internal/analytics/postgres_analytics.go | 5 ----- internal/api/analytics_postgres.go | 6 ------ internal/credentials/resolver.go | 2 +- internal/deploy/frontend.go | 2 +- internal/email/templates.go | 4 ++-- 5 files changed, 4 insertions(+), 15 deletions(-) diff --git a/internal/analytics/postgres_analytics.go b/internal/analytics/postgres_analytics.go index 5b117bdd4..a6a1cc97b 100644 --- a/internal/analytics/postgres_analytics.go +++ b/internal/analytics/postgres_analytics.go @@ -221,8 +221,6 @@ func (s *PostgresAnalyticsStore) BulkInsertSnapshots(ctx context.Context, snapsh func (s *PostgresAnalyticsStore) QuerySavings(ctx context.Context, req QueryRequest) ([]SavingsSnapshot, error) { accountClause, args := accountFilterClause(req.AccountUUIDs, req.AccountExternalIDsByProvider, []any{req.StartDate, req.EndDate}) - // #nosec G201 — accountClause references only parameter placeholders built - // internally; the optional provider/service filters below are also bound. query := ` SELECT id, account_id, cloud_account_id, timestamp, provider, service, region, commitment_type, total_commitment, total_usage, total_savings, @@ -304,7 +302,6 @@ func (s *PostgresAnalyticsStore) QueryMonthlyTotals(ctx context.Context, account // the INTERVAL '1 month' * N off-by-one (M1). accountClause, args := accountFilterClause(accountUUIDs, accountExternalIDsByProvider, []any{months}) - // #nosec G201 — accountClause uses only internally-built placeholders. query := ` SELECT month, account_id, cloud_account_id, provider, service, total_savings, avg_coverage, snapshot_count FROM monthly_savings_summary @@ -352,7 +349,6 @@ func (s *PostgresAnalyticsStore) QueryByProvider(ctx context.Context, accountUUI // timestamp and the outer query averages the instant totals over time; a // flat AVG would report the mean per-row run-rate instead of the bucket // total (COR-02). - // #nosec G201 — accountClause uses only internally-built placeholders. query := ` SELECT provider, service, AVG(ts_savings) as total_savings, AVG(ts_coverage) as avg_coverage FROM ( @@ -408,7 +404,6 @@ func (s *PostgresAnalyticsStore) QueryByService(ctx context.Context, accountUUID // Same H5 nested rollup as QueryByProvider: a (service, region) bucket // spans accounts and commitment types at the same timestamp, so SUM the // rows per timestamp first, then AVG the instant totals over time (COR-02). - // #nosec G201 — accountClause / providerClause use only internally-built placeholders. query := fmt.Sprintf(` SELECT service, region, AVG(ts_savings) as total_savings, AVG(ts_coverage) as avg_coverage FROM ( diff --git a/internal/api/analytics_postgres.go b/internal/api/analytics_postgres.go index a66619cf7..81fdbf4b1 100644 --- a/internal/api/analytics_postgres.go +++ b/internal/api/analytics_postgres.go @@ -165,9 +165,6 @@ func (c *PostgresAnalyticsClient) QueryHistory( // breakdowns without a second trip to the DB. The account predicate is the // shared dual-column clause (see accountFilterClause); the optional provider // predicate mirrors QueryByService (parameter-bound, "" = no filter). - // - // #nosec G201 — `unit` is allowlisted by intervalToTruncUnit above and the - // account / provider clauses are parameter-bound (no user input interpolated). accountClause, args := accountFilterClause(accountUUIDs, accountExternalIDsByProvider, []any{start, end}) providerClause := "" if provider != "" { @@ -269,9 +266,6 @@ func (c *PostgresAnalyticsClient) QueryBreakdown( // Dual-column account predicate: see accountFilterClause / QueryHistory for // rationale (issue #701/#498/#866). - // - // #nosec G201 — `column` is allowlisted by dimensionToColumn above and the - // account clause is parameter-bound (no user input interpolated). accountClause, args := accountFilterClause(accountUUIDs, accountExternalIDsByProvider, []any{start, end}) query := fmt.Sprintf(` SELECT %s AS bucket, diff --git a/internal/credentials/resolver.go b/internal/credentials/resolver.go index a9d6d1196..316c33e39 100644 --- a/internal/credentials/resolver.go +++ b/internal/credentials/resolver.go @@ -24,7 +24,7 @@ import ( // Credential type constants used as credType in CredentialStore. const ( CredTypeAWSAccessKeys = "aws_access_keys" - CredTypeAzureClientSecret = "azure_client_secret" // #nosec G101 -- credential type name constant; not a credential value + CredTypeAzureClientSecret = "azure_client_secret" CredTypeGCPServiceAccount = "gcp_service_account" // #nosec G101 -- credential type name constant; not a credential value CredTypeGCPWIFConfig = "gcp_workload_identity_config" // #nosec G101 -- credential type name constant; not a credential value ) diff --git a/internal/deploy/frontend.go b/internal/deploy/frontend.go index 4089c3e94..bfb69dfb9 100644 --- a/internal/deploy/frontend.go +++ b/internal/deploy/frontend.go @@ -102,7 +102,7 @@ func (s *FrontendService) uploadFile(ctx context.Context, distDir, bucketName, p } key := strings.ReplaceAll(relPath, string(filepath.Separator), "/") - content, err := os.ReadFile(path) // #nosec G304,G122 -- path is from WalkDir callback, symlinks rejected by caller; always a regular file descendant of distDir (npm build output under operator control) + content, err := os.ReadFile(path) // #nosec G304 -- path is from WalkDir callback, symlinks rejected by caller; always a regular file descendant of distDir (npm build output under operator control) if err != nil { return fmt.Errorf("failed to read %s: %w", path, err) } diff --git a/internal/email/templates.go b/internal/email/templates.go index 1328fbc62..89e0a6cfe 100644 --- a/internal/email/templates.go +++ b/internal/email/templates.go @@ -137,7 +137,7 @@ Review failed purchases: This is an automated message from CUDly. ` -const passwordResetTemplate = "" + // #nosec G101 -- email template; "password" in body text is email copy, not a hardcoded credential +const passwordResetTemplate = "" + `CUDly - Password Reset Request ============================== @@ -161,7 +161,7 @@ This is an automated message from CUDly. // Modeled on purchaseApprovalRequestHTMLTemplate (line 367) — inline styles // because most email clients (Outlook, mobile Gmail) ignore class-based CSS. // Issue #355. -const passwordResetHTMLTemplate = "" + // #nosec G101 -- HTML email template; "password" in body text is email copy, not a hardcoded credential +const passwordResetHTMLTemplate = "" + ` CUDly - Password Reset Request