Repository navigation
chore(pkg/cmd): medium/low findings from the 2026-07-28 full review #1613
Description
Reviewed commit: be11bdcb5. Note: origin/main moved to 3e9660d06 during the review; re-verify against current main before changing code, since a finding may have been fixed or moved.
Medium and low findings from the 2026-07-28 full review of cmd/ (all 40 files, non-test read in full) and the assigned pkg/ packages (common, concurrency, config, errors, logging, provider, retry, scorer). Everything was read via git show origin/main:<path> and git grep origin/main; the shared working tree was not touched.
The critical and high findings from the same review are filed separately. Two further medium findings were posted as comments on existing issues instead: CSV numeric parsing on #1327, and CSV Term / PaymentOption validation on #379.
Scope note: there is no MCP server in this repository at origin/main (git ls-tree -r --name-only origin/main | grep -i mcp is empty), so the MCP stdout-purity item from the review brief had no target here and was not performed.
Each item below is independently actionable; they are grouped only to keep the backlog readable.
-
MEDIUM:
determineCSVCoveragetreats an explicit--coverage 80as "not set" and silently buys 100%Where:
cmd/multi_service_csv.go:18-26, called fromcmd/multi_service.go:315.What: the function compares
cfg.Coverage == 80.0against the flag's default literal to infer "the user did not override". A user who explicitly types the default value is indistinguishable from one who typed nothing. The codebase already knows the correct idiom and uses it a few files over:validateTargetCoveragedetects an explicit--coverageviacmd.Flags().Changed("coverage"), and its comment atcmd/validators.go:113-116spells out this exact hazard ("so a user who happens to set--coverage 80(which equals the default) still sees the notice").Failure scenario:
cudly --input-csv approved.csv --coverage 80 --purchase. The operator intends to buy 80% of each CSV row's count.determineCSVCoveragereturns100.0,filterAndAdjustRecommendationsskips sizing entirely (cmd/multi_service.go:463, sinceTargetCoverage == 0andcsvModeCoverage < 100is false), and 100% of every row is purchased. The log line atcmd/multi_service.go:469never prints, so there is no on-screen hint that the flag was ignored.Fix direction: thread the
*cobra.Command, or a precomputedcoverageExplicitlySet bool, intodetermineCSVCoverageand branch onFlags().Changed("coverage").
-
MEDIUM: on the CSV path, account filters are applied before
AccountNameis populatedWhere: ordering inside
runToolFromCSV:filterAndAdjustRecommendations(which callsapplyFilters) atcmd/multi_service.go:328,populateAccountNamesatcmd/multi_service.go:350. The filter isshouldIncludeAccount(rec.AccountName, cfg)(cmd/multi_service_filters.go:98,:194-213). The non-CSV path gets this right:populateRecommendationAccountNamesruns atcmd/multi_service_helpers.go:597, one line beforeapplyRegionFiltersat:598.What: in CSV mode
AccountNameis only whatever the file'sAccountNamecolumn held (cmd/multi_service_csv.go:110). The Organizations lookup that would fill it in from theAccountID runs 20 lines too late to matter.Failure scenario: a CSV exported with
Account(12-digit ID) but noAccountNamecolumn, so every rec hasAccountName == "". The operator runs--input-csv recs.csv --exclude-accounts sandbox --purchase.shouldIncludeAccountreturnslen(Include) == 0 && len(Exclude) == 0(cmd/multi_service_filters.go:196-198), which isfalsefor every row, so all recommendations are dropped and the run prints "No recommendations to process after filtering". The symmetric case is worse: with--include-accounts prodthe same all-empty rows are also dropped, so an operator who believes they scoped the run to prod silently buys nothing and may re-run without the filter.Fix direction: move
populateAccountNames(ctx, recs, accountCache)abovefilterAndAdjustRecommendations, which requires loading the AWS config first, or filter onrec.Accountwhenrec.AccountNameis empty.
-
MEDIUM:
shouldIncludeAccountdrops every unnamed recommendation as soon as any account filter is setWhere:
cmd/multi_service_filters.go:194-198.What:
if accountName == "" { return len(cfg.IncludeAccounts) == 0 && len(cfg.ExcludeAccounts) == 0 }. For an exclude-only filter the correct answer is "keep it", since an unnamed account cannot match an exclusion, but the code returnsfalseand drops it.Failure scenario: a payer account's own Cost Explorer recommendations come back with an empty
Accountfield, sopopulateRecommendationAccountNamesleavesAccountNameempty (cmd/multi_service_helpers.go:452-456only fills it whenAccount != ""). The operator runs--exclude-accounts sandboxto skip one linked account. Every payer-level recommendation is silently dropped alongside it, and because dimension-filter drops are deliberately not counted in the drop summary (cmd/multi_service_filters.go:62-66), nothing in the output says so.Fix direction: for an empty
accountName, returnlen(cfg.IncludeAccounts) == 0, since exclusions cannot match an unnamed account, and count these as a named drop reason.
-
MEDIUM: account filters do substring matching, but the flag help and the conflict validator assume exact names
Where:
accountMatchesFilteratcmd/multi_service_filters.go:241-244(filterLower == accountLower || strings.Contains(accountLower, filterLower)); flag help atcmd/main.go:122-123("Only include recommendations for these account names"); conflict check atcmd/validators.go:208->validateNoConflicts(cmd/validators.go:234-248), which compares with==only.What: substring matching is undocumented and asymmetric with the include/exclude conflict validation, which only rejects identical strings.
Failure scenario: an org with accounts
prod-eu,prod-us,non-prod-sandbox. The operator runs--include-accounts prod --exclude-accounts non-prod.validateNoConflictspasses, since "prod" is not equal to "non-prod". ThencheckIncludeListmatchesnon-prod-sandboxbecause it contains "prod", andcheckExcludeListalso matches it, so the exclusion happens to win. But--exclude-accounts prodalone would silently excludenon-prod-sandboxtoo, an account the operator never named. On a purchase run that is a silent scope change in either direction.Fix direction: switch to exact match. Delete the
strings.Containsarm fromaccountMatchesFilter, leaving the==compare thatvalidateNoConflictsalready uses, so the two agree by construction rather than by a second matcher kept in step.Not documenting the substring semantics instead: that blesses substring-for-equality on an account filter, which is the defect class this repo has shipped six times, and it is the more dangerous direction on
--exclude-accountsspecifically (excludingprodwould also excludenonprod-staging). Not adding glob or prefix syntax either: no reported use needs partial account matching, so that is new surface for a case nobody has demonstrated. Add it if someone asks.Remedy simplified (2026-08-03): the "document the substring semantics" option and the glob/prefix syntax were dropped, leaving the exact match. Making the validator match a substring matcher keeps both in the codebase; deleting the substring arm removes the asymmetry and the defect class together.
-
MEDIUM:
cmd/cleanup-lambdacan never delete a purchase execution, and has no status guard if it ever doesWhere:
cmd/cleanup-lambda/main.go:63(dry-run count) and:91(delete), bothDELETE FROM purchase_executions WHERE expires_at < $1.What:
PurchaseExecution.ExpiresAtis*time.Time(internal/config/types.go:888), andgit grep -n "\.ExpiresAt = \|ExpiresAt: *&" origin/main -- internal/finds no non-test writer for it; the hits areApprovalTokenExpiresAt, API-key expiry, andPurchaseSuppression.ExpiresAt, a different struct. The column is therefore always NULL, andNULL < $1is NULL, so the predicate never matches. Separately, the delete has nostatusfilter, so ifExpiresAtis ever populated it would hard-deletepending/scheduled/runningrows alongside terminal ones.Failure scenario (today): the scheduled cleanup Lambda runs on its cron, logs "Deleted 0 expired executions" forever, and reports
executionsDeleted: 0in its response. Operators reading that see a healthy job whilepurchase_executionsgrows without bound. Nothing alerts, because 0-deleted is indistinguishable from "nothing was due".Failure scenario (once wired): an execution stuck in
runningpast its grace window is deleted. The per-recommendation idempotency token is derived from the execution ID (DeriveIdempotencyToken(executionID, recIndex),pkg/common/tokens.go:64-67), so a re-drive after the row is gone necessarily uses a new execution ID and a new token. The AWS-side dedupe (ClientToken/IdempotencyTagKey) no longer recognises the earlier attempt, and the purchase can be made twice.Fix direction: decide the intended retention rule and implement it explicitly, either by setting
ExpiresAtat execution creation or by changing the predicate to acompleted_at < $1 AND status IN (<terminal statuses>)form. Either way, add a terminal-status guard before any DELETE.
-
MEDIUM:
configure-azure/configure-gcpwrite credentials intoListSecrets(...)[0], a prefix match over an unordered, unpaginated listWhere:
cmd/configure_azure.go:145-166(storeAzureCredentials) andcmd/configure_gcp.go:116-131(storeGCPCredentials); the lookup isAWSSecretsStore.ListSecrets(cmd/secrets_store.go:32-56), which builds aFilterNameStringTypeNamefilter and does no pagination.What: Secrets Manager's name filter is a prefix/contains match, not an exact match, and the returned order is unspecified. The code takes
arns[0]unconditionally when the list is non-empty and thenUpdateSecrets that ARN with the full credential payload. AListSecretserror is swallowed by theerr == nil &&guard (cmd/configure_azure.go:152) and falls back to writing by bare name.Failure scenario: an account has
cudly-AzureCredentialsand a leftovercudly-AzureCredentials-2026-rotation-backupfrom a prior rotation. Both match the filter"cudly-AzureCredentials". If AWS returns the backup first,cudly configure-azurewrites the live service-principal client secret into the backup secret and leaves the real one stale. The operator sees "Azure credentials stored successfully" and CUDly keeps authenticating with the old secret, while a live credential now sits in a secret nobody is watching.Fix direction: select the ARN whose
NameequalssecretNameexactly, paginating withNextToken; error out when more than one exact match or zero matches is found; and surface theListSecretserror instead of discarding it.
-
MEDIUM:
BuildReservationNamedrops its uniqueness suffix first, so long-SKU services get fully deterministic reservation namesWhere:
pkg/common/reservation_name.go:105-125.tail := []string{paymt, ts, randHex}and the loopfor keep := len(tail); keep >= 0; keep--drops from the right: the random suffix first, then the timestamp, then the payment option. Live at six call sites (providers/aws/services/{ec2,elasticache,memorydb,opensearch,rds,redshift}/client.go).What: the random suffix is the only unconditional uniqueness source, and the 60-character cap (
pkg/common/reservation_name.go:18) removes it before anything else. The function's own docstring example ("opensearch-us-east-1-r6gd-large-search-3x-1yr-allup-...") is 76 characters unabbreviated, so the documented example is itself a case where both the random suffix and the timestamp are dropped.Failure scenario: OpenSearch,
us-east-1, SKUr6gd.large.search, all-upfront, 1yr. The required-plus-payment prefix isopensearch-us-east-1-r6gd-large-search-3x-1yr-allupat 50 characters; adding the 16-character-20260728T120000timestamp exceeds 60, so both the timestamp and the random suffix are dropped. Every purchase of that pool, in any run on any day, produces the identicalReservationName. For services where the customer-supplied ID must be unique (RDSReservedDBInstanceId), a legitimate second purchase of the same pool then fails with an already-exists fault. Where it need not be unique, two distinct commitments become indistinguishable in the console and in any downstream reconciliation that keys on the name.Fix direction: treat
randHexas required rather than optional: droppaymtandtsbefore it, or shorten the timestamp or the SKU so the 8-character suffix always survives.
-
MEDIUM:
engineNameMapis duplicated verbatim betweencmd/helpers.goandpkg/common/engine.go, and thepkg/commoncopy is deadWhere:
cmd/helpers.go:727-760(engineNameMap,normalizeEngineName) andpkg/common/engine.go:8-41(engineNameMap,NormalizeEngineName), byte-for-byte identical map contents and fallback.pkg/common/matches.go:20is the only consumer of the exported copy, andgit grep -n "common\.Matches(" origin/mainfinds no non-test caller ofMatches, so the entirepkg/commoncopy is unreachable.What: the live logic is the private
cmdcopy, used to build the dedupe key inbuildExistingCommitmentsMapandgetEngineFromRecommendation(cmd/helpers.go:637,:702-722). Thepkg/commonexported version exists as the "shared" one but nothing shares it.Failure scenario: someone adds
"Db2": "db2"topkg/common/engine.go, the obvious exported package-documented location, while the CLI dedupe keeps using the un-updatedcmdmap. The recommendation-side key and the commitment-side key then diverge for any mapping that is not a pure lowercase, for example adding"Aurora Serverless v2"to only one map.adjustSingleRecommendation(cmd/helpers.go:667-690) finds no existing commitment for a pool that is covered and re-purchases it.Fix direction: delete the
cmd/helpers.gocopy and callcommon.NormalizeEngineName/common.EngineFromDetails; then either wirecommon.Matchesinto the dedupe path or delete it.
-
MEDIUM:
common.MatchesignoresDeployment,TermandPaymentOption, a latent duplicate-suppression bug for whoever wires it upWhere:
pkg/common/matches.go:7-21.What: the doc claims "OfferingClass and Term are not compared because Commitment has no such fields", but
Commitment.Deploymentdoes exist (pkg/common/types.go:399) and carries the same "single-az" / "multi-az" vocabulary asDatabaseDetails.AZConfig. Its field comment (pkg/common/types.go:383-389) says it was added specifically so "pool-key matching can collapse both sides", yetMatchesnever reads it.TermandPaymentOptionare likewise absent from the comparison.Failure scenario: if
Matchesis adopted for dedupe, which is the shape it is written for, a Single-AZdb.r7g.xlarge/postgresrecommendation would match an existing Multi-AZ commitment for the same instance class and engine and be suppressed as a duplicate. Multi-AZ list price is roughly 2x Single-AZ, so the two are entirely different commitments. The same applies to a 1yr recommendation matching a 3yr commitment.Fix direction: compare
Deployment(normalised) andTerminMatches, or delete the function, since it has no callers.
-
MEDIUM:
--max-instancesand--min-countare unit-mismatched for Savings Plans, which always reportCount: 1Where: SP recommendations are constructed with
Count: 1(providers/aws/recommendations/parser_sp.go:373); the count-based gates areApplyInstanceLimit(cmd/helpers.go:505-526) andscorer.filterReason'sMinCountcheck (pkg/scorer/scorer.go:88-90).What: an SP commitment is dollar-denominated (
SavingsPlanDetails.HourlyCommitment,pkg/common/types.go:578).pkg/common/types.go:237-239even documents thatRecommendedCountis "zero for SPs since the SP commitment is dollar-denominated rather than count-denominated". The instance-count cap therefore counts a $500/hr Compute SP as "1 instance".Failure scenario:
cudly --services savingsplans --purchase --max-instances 5reads as a conservative first run. It permits 5 Savings Plans of arbitrary hourly commitment, potentially millions of dollars over a 3-year term, while the same flag would cap an RI run at 5 instances. Symmetrically,--min-count 2silently filters out every Savings Plans recommendation, with the reason rendered as "count 1 below minimum 2".Fix direction: make
ApplyInstanceLimitskip or separately budgetIsSavingsPlanrecs, since a dollar cap is the right unit, and havescorerskipMinCountfor SPs rather than filtering them all out.
-
MEDIUM:
ApplyCoveragepasses Savings Plans through unscaled when theDetailstype assertion failsWhere:
cmd/helpers.go:151-162, theelsebranch, which logs a warning and then doesresult = append(result, adjusted)withadjusted == rec, meaning the full unscaled commitment.What: the comment argues that "a missing-Details record is a logged anomaly, not a reason to erase coverage from the run", but the chosen alternative is to buy 100% of a commitment during a run the operator sized to a fraction. The project's standing rule is to fail loud rather than degrade silently on money paths.
Failure scenario: a Compute SP recommendation arrives with
Detailsas a valueSavingsPlanDetailsrather than a pointer, or nil, for example after a JSONB round-trip whereDecodeServiceDetailsForreturned(nil, nil)for an unrecognised service slug (pkg/common/service_details_codec.go:94-97). The operator runs--coverage 25 --purchase. Every other rec is sized to a quarter; this one is purchased at AWS's full recommended hourly commitment. The only signal is aWARNING:line in the middle of stdout, and the CSV row shows the unscaled cost as if that were the intended purchase.Fix direction: drop the rec into the
DropSummaryand exclude it from the purchase set, or return an error that aborts the run, rather than silently promoting it to 100%.
-
MEDIUM: the Savings-Plan summary silently discards unknown plan types and every canonical (non-legacy) service type
Where:
categorizeSPRecommendations(cmd/multi_service_stats_helpers.go:20-46) andcollectSPSavings(:100-120) switch on the bare string literals"Compute"/"EC2Instance"/"SageMaker"/"Database"with nodefault.collectRISavings(:129-146) enumerates onlyServiceEC2,ServiceRDS,ServiceElastiCache,ServiceMemoryDBandServiceRedshift, not the canonicalServiceCompute/ServiceRelationalDB/ServiceCache/ServiceDataWarehouseconstants those same recs can carry (pkg/common/types.go:73-86).What: both are lookup tables that return the zero value for unrecognised input, the silent-default anti-pattern the project's own guidance calls out. The consequence here is not a wrong purchase but a wrong recommendation to the operator.
Failure scenario:
calculateComparisonOptions(cmd/multi_service_stats_helpers.go:159-183) computesOption2Savings = riSavings - risByService.EC2RISavings + BestComputeSP. If the EC2 recs carryServiceComputeinstead of the legacyServiceEC2,EC2RISavingsis 0 whileriSavingsincludes them, so Option 2 adds the Compute SP savings on top of the EC2 RI savings it is meant to replace, double-counting them.determineBestOption(:209-237) then prints "RECOMMENDATION: Use Option 2" on inflated arithmetic. Likewise a new AWS plan type, or aPlanTypeof"", contributes to no bucket and vanishes from the printed totals with no warning.Fix direction: add the canonical constants (
ServiceCompute,ServiceRelationalDB,ServiceCache,ServiceDataWarehouse) alongside the legacy ones incollectRISavings, replace the bare string literals with the typed constants, and give each switch adefault:arm that errors on an unrecognised value rather than logging and counting it. This is a money-path total; a value that reaches no bucket must not be summarised, and an unknownPlanTypeincluding""is exactly the case that produced the double-count.Not adding a canonical-versus-legacy normaliser: it would make
ServiceComputeandServiceEC2compare equal, and the double-count in the failure scenario above is arithmetic over two buckets that must stay distinct. Enumerating both sets is explicit, is the same number of lines, and fails visibly when a new constant is added instead of quietly folding it into an existing bucket.Remedy simplified (2026-08-03): the normaliser was dropped in favour of enumerating the canonical constants, and the
default:arm was changed from log-and-count to error, matching the repo's fail-loud rule on money paths.
-
LOW:
configure-azure's help text advertises passing the client secret on the command lineWhere:
cmd/configure_azure.go:101(theLonghelp string) and the flag atcmd/configure_azure.go:117.What: the first documented usage example is
cudly configure-azure --stack-name my-cudly --tenant-id xxx --client-id xxx --client-secret xxx .... Command-line arguments are visible to any local user viapsand/proc/<pid>/cmdline, and are written to shell history. The code already has the safe path:--interactivereads the secret withterm.ReadPassword(cmd/configure_azure.go:278) and zeroes the in-memory copy afterwards (cmd/configure_azure.go:204-205), which shows the secret is treated as sensitive everywhere except in its own help text.Failure scenario: an operator copy-pastes the documented command on a shared bastion. The Azure service-principal client secret lands in
~/.bash_historyand is readable frompsfor the duration of the run.Fix direction: lead the help with the
--interactiveform, add anAZURE_CLIENT_SECRET-style env-var path, and mark--client-secretas discouraged in its usage string.
-
LOW:
queryRDSInstancesInRegionpaginates without a page cap or a context check, unlike its fix(aws/recommendations): paginate CE recommendation + RDS engine-version describe (single-page truncation) #692-hardened siblingWhere:
cmd/multi_service_engine_versions.go:132-157. ComparefetchMajorEngineVersionsForEngine(:232-267), which checksctx.Err()at the top of every page and enforcesmaxEngineVersionPages(:94, added for fix(aws/recommendations): paginate CE recommendation + RDS engine-version describe (single-page truncation) #692).What: the RDS-instance paginator loops on
output.Markerwith no bound and never consultsctx.Failure scenario: a region whose
DescribeDBInstancesreturns a non-advancingMarker, or simply a very large fleet, spins that region's goroutine indefinitely. BecausequeryRDSInstancesInRegionsblocks onwg.Wait()(:127), the whole CLI hangs before it fetches a single recommendation, and Ctrl-C is the only exit since the goroutine ignores context cancellation.Fix direction: apply the same
ctx.Err()check and page cap the sibling paginator already has.Related: ctx.Canceled silently swallowed in queryMajorEngineVersionsWithClient (surfaced by PR #1225) #1325 covers context cancellation being swallowed in
queryMajorEngineVersionsWithClientand asks for a sweep of sibling fan-out loops. This is a different function and a different shape (an unbounded paginator rather than a swallowed error), so it will not be caught by that sweep's grep, but it is the same file and cheapest to fix in the same change.
-
LOW:
CheckAuditLogWritablecreates the audit log with 0600, defeating the 0644 the writer documents as requiredWhere:
cmd/helpers.go:558opens withos.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600;pkg/common/audit.go:28opens the same path with0644, and the comment atpkg/common/audit.go:23-26states 0644 is intentional because "the audit log is consumed by ops tooling and reconciled against purchase_history; restricting to 0600 would break that workflow".What: the writability pre-check runs first (
cmd/multi_service.go:108) and creates the file, soO_CREATinWriteAuditRecordis a no-op and its mode argument never applies. Every audit log created by the CLI is 0600.Failure scenario: the ops reconciliation job runs as a different service user and gets
permission deniedreadingcudly-audit.jsonl, the exact outcome the 0644 comment says it is avoiding.Fix direction: use the same mode in both places, picking one deliberately, or have the pre-check open without
O_CREATEand treat a missing file as writable if the directory is.
-
LOW:
RATE_LIMIT_DOMAINis interpolated into a SQLLIKEpattern without escaping wildcardsWhere:
cmd/lambda/clear-rate-limit/main.go:40-42, with the value fromgetDomain()(:15-20).What: the query is properly parameterised, so this is not injection, but
%and_inside the env var are LIKE metacharacters, so the deletion scope is wider than the variable name implies.Failure scenario:
RATE_LIMIT_DOMAIN=%makes the patternEMAIL#%@%#ENDPOINT#forgot_password, which deletes the forgot-password rate-limit rows for every domain rather than one, removing brute-force protection across all tenants. A typo containing_, for exampleleaner_cloud.com, silently widens the match by one character position.Fix direction: reject or escape
%and_ingetDomain(), or match on an exact suffix column instead of a constructed LIKE pattern.Related: sec(auth): forgot-password rate limit comment mismatch and no per-IP cap cloud-commitments-platform#13 covers the forgot-password rate limit's configuration and missing per-IP cap. This is a different mechanism (an operational tool that can clear the limits wholesale), but the two share a blast radius.
-
LOW:
retry.Do's doc claims the per-attempt context is "independent of ctx"; it is derived from itWhere:
pkg/retry/exponential.go:36-38and:96-98both say the per-attempt context is "independent of the outer ctx".runAttempt(:145) builds it withcontext.WithTimeout(ctx, cfg.PerAttemptTimeout), which is a child ofctx.What: the behaviour (fail fast per attempt, keep the retry budget) is fine; the doc overstates it. A caller reading it would expect an outer cancellation not to abort an in-flight attempt, when in fact it does.
Failure scenario: a future caller relies on the documented independence to let an in-flight money-path attempt finish after the parent context is cancelled, for example to observe whether a purchase landed. The attempt is cancelled instead, and the caller cannot tell a cancelled-before-send from a cancelled-after-send.
Fix direction: reword to "bounded independently of the outer deadline but still cancelled with it", or use
context.WithoutCancel(ctx)if independence is genuinely wanted.
-
LOW:
logging.SetOutputwritesdefaultLogger.outputunsynchronised whileWithreads itWhere:
pkg/logging/logger.go:272-277assignsdefaultLogger.output = w;(*Logger).Withreadsl.outputat:129. OnlyLogger.levelis atomic (:37);outputis a plain field with no mutex.What: the struct doc at
:31-35explains thatlevelwas made atomic because the logger is used "from many goroutines". The same reasoning applies tooutput, which was left unprotected. The underlyinglog.Logger.SetOutputis itself mutex-guarded, so only theLogger.outputmirror races.Failure scenario: a test calls
logging.SetOutput(buf)int.Cleanupwhile a background goroutine from the code under test callslogging.With("k", v).go test -racereports a data race onLogger.output: an intermittent CI failure unrelated to the code being tested, which is the kind that gets retried rather than diagnosed.Fix direction: store
outputin anatomic.Value, guard it with the same mutex pattern, or drop the mirror field and read it back from thelog.Logger.
-
LOW:
pkg/errorsandpkg/confighave no importersWhere:
pkg/errors/errors.go(300 lines, 7 error types) andpkg/config/{load,types}.go(about 490 lines).What:
git grep -ln "CUDly/pkg/errors" origin/mainandgit grep -n "pkg/config" origin/main, excluding the packages themselves, both return nothing. Both are fully tested and look production-ready, which makes them attractive to a reader looking for the project's error and config conventions.Failure scenario: a contributor adopts
pkg/config.Loadfor a new entry point and inherits theDryRun-inversion bug (filed separately from this review as its own issue) without any existing caller having exercised it. Similarly,pkg/errors' documented type-levelIssemantics (field-insensitive matching,pkg/errors/errors.go:5-11) are surprising enough that first adoption is likely to misuse them.Fix direction: either wire them in, fixing
applyYAMLBasefirst, or delete them so the tree reflects what actually runs.
Verified-clean areas (no findings)
Recorded so a later pass does not re-derive them: pkg/common/tokens.go (MaskToken fully redacts inputs of 8 characters or fewer rather than echoing a prefix that would be the whole value, :82-84; DeriveIdempotencyToken fits the 64-character AWS ClientToken limit exactly; IdempotencyGUID validates hex before formatting and returns "" below 32 characters; DeriveMuteToken / VerifyMuteToken fail closed on an empty key and compare with hmac.Equal); pkg/common/drop_summary.go (Add / Total / IsEmpty are all nil-receiver-safe, which matters because several call sites pass a nil *DropSummary); pkg/concurrency (leaf-only acquisition correctly documented and implemented; Acquire returns ctx.Err() on cancellation and no-ops with no semaphore attached); pkg/provider/registry.go (factories snapshotted under RLock and invoked lock-free, so a factory doing network I/O cannot block the registry); cmd/rekey/main.go (env-gated, refuses to run when the real key equals the dev key, per-row transactions, plaintext never logged); and cmd/gen-permissions/main.go (deterministic sorted output, no injection surface).
Findings from the 2026-09-02 codebase audit
Added by an automated audit of 3c0f8ac94048a2c36fce5ccddee54e6c4849a5cd (tip of origin/main). Each item below was reported by one reviewer and independently confirmed by a second that did not write it. Full report: docs/audits/codebase-audit-2026-09-02.md.
A10-004 (high)
A fourth account-filter item, upstream of the three already listed. GetAccountAlias (cmd/helpers.go:91-94) caches and returns the numeric account ID as the account name when organizations:DescribeAccount fails, and shouldIncludeAccount then filters on that stand-in (cmd/multi_service_filters.go:105, :156-175). So cudly --purchase --exclude-accounts production against account 123456789012 finds no substring match and buys in the account the operator explicitly ruled out; --include-accounts silently narrows the same way. The bad value is cached for the process lifetime, so one transient Organizations throttle poisons every rec for that account. This is the same class as the 'unnamed recommendation' item but a different cause: the name is present and wrong rather than absent. Fix direction: return (string, error) and fail the run when an account filter is in force and the alias cannot be resolved, and do not cache the failure. Finding A10-004.
A09-014 (medium)
Two facts to fold into the pkg/errors / pkg/config bullet, since they change the cost of leaving pkg/config in place. It is the only importer of github.com/spf13/pflag and gopkg.in/yaml.v3 anywhere in the pkg module (load.go:12-13 plus its test), and both sit in pkg/go.mod's direct require block at lines 13 and 16, so every consumer of the shared module inherits two dependencies for unreachable code. Deleting the package lets pflag go entirely; yaml.v3 becomes indirect rather than droppable, since testify pulls it in. Separately, pkg/config defines its own DefaultScorerConfig that no caller can reconcile against the live scorer configuration, which is a second trap for the first adopter alongside the DryRun inversion already filed as LeanerCloud/cloud-commitments-go#56. (audit finding A09-014)
A09-018 (medium)
The (nil, nil) return that the ApplyCoverage bullet uses as its failure scenario is worth listing as its own defect. DecodeServiceDetailsFor (pkg/common/service_details_codec.go:95) returns (nil, nil) before ever looking at raw whenever newDetailsForService has no case for the service slug (:121-167), so a row persisted by a newer deployment with a fully populated Details blob decodes to nothing on an older reader and the purchase proceeds with nil details instead of failing. The in-code comment calls this 'suspicious but tolerated'. Returning (nil, nil) only when raw is empty, and an error naming the service when a payload exists for a slug with no typed shape, would surface version skew instead of dropping purchase parameters. No current slug loses data this way (memorydb's nil details are deliberate at :154-161), so this is low severity today. Audit finding A09-018.
A07-029 (low)
A second latent mismatch on the same matcher. providers/aws/services/ec2/client.go:59-61 returns common.ServiceCompute ("compute") from GetServiceType, while GetExistingCommitments at :98 stamps Service: common.ServiceEC2 ("ec2") on every commitment it builds. common.Matches compares rec.Service to c.Service at pkg/common/matches.go:14, so an EC2 recommendation tagged compute can never match an EC2 commitment tagged ec2. Same inconsistency at ladder/commitments.go:85. Like the Deployment/Term/PaymentOption gap, this is latent because Matches has no caller, but whoever wires it up inherits both. Building the commitment from c.GetServiceType() removes the ability for the client to describe itself two ways. Finding A07-029.
A09-030 (low)
There is a second defect in this file worth pairing with the doc fix, and this one is behavioural rather than documentation. backoffFor (pkg/retry/exponential.go:168-186) applies the plus-or-minus 25 percent jitter factor after both MaxDelay clamps, so the documented cap does not hold: with MaxDelay = 30s the actual sleep reaches 37.5s. Clamping the post-jitter delay to MaxDelay is a one-line change and makes the documented bound true, which matters for the same class of caller the context wording misleads, one sizing a retry budget against the stated numbers. Audit finding A09-030.
A10-030 (low)
One more configure-wizard item for this tracker, on the same two files. The prompt at cmd/configure_azure.go:481 reads "[R]un, [S]kip?" with no default marked, but the guard skips only when the choice is none of "r", "run" or "", so pressing Enter falls through to createAzureServicePrincipal at :513: an Entra ID application, a password credential and a "Reservations Administrator" assignment at /subscriptions/. Undoing it means deleting the application by hand, since the wizard's rollback fires only on failure. The GCP wizard shares the shape with case "r", "run", "": at cmd/configure_gcp.go:666 and :700, so the same Enter creates a service account and grants it compute.admin. Making Skip the empty-input default on the resource-creating steps, and leaving Run-on-Enter only for the read-only listing prompts, fits alongside the help-text and ListSecrets items already here. Finding A10-030.
Scope after the split
Per the monorepo split's path mapping: root-level cmd/*.go files stay with this repo, pkg/ and providers/ moved to cloud-commitments-go, and cmd/ subdirectories (cleanup-lambda, lambda, server, rekey, gen-permissions) moved to cloud-commitments-platform.
- The
cmd/cleanup-lambdaandcmd/lambda/clear-rate-limitfindings went to chore(pkg/cmd): medium/low findings from the 2026-07-28 full review [platform part] cloud-commitments-platform#357 (cmd/subdirectories). - The
pkg/common,pkg/scorer,pkg/retry,pkg/logging,pkg/errors,pkg/config, andproviders/awsfindings went to chore(pkg/cmd): medium/low findings from the 2026-07-28 full review [go part] cloud-commitments-go#123. - Two findings span both repos (
engineNameMapduplication between this repo'scmd/helpers.goand the go repo'spkg/common/engine.go; the Savings-Plans count-unit mismatch between this repo'sApplyInstanceLimitand the go repo'sscorer.filterReason). Both are quoted in full in both issues with a cross-reference, so fixing either repo's half in isolation is visible as incomplete.
This issue is now scoped to only the findings whose fix location is root-level cmd/*.go in this repo, re-verified against the current tree (2026-09-28: all referenced files still present at their original paths, content not individually re-diffed given the volume - re-verify line numbers before changing code per the issue's own standing disclaimer):
MEDIUM (remaining in this repo)
determineCSVCoveragetreats an explicit--coverage 80as "not set" and silently buys 100% (cmd/multi_service_csv.go:18-26,cmd/multi_service.go:315).- On the CSV path, account filters are applied before
AccountNameis populated (cmd/multi_service.go:328,350,cmd/multi_service_filters.go:98,194-213). shouldIncludeAccountdrops every unnamed recommendation as soon as any account filter is set (cmd/multi_service_filters.go:194-198).- Account filters do substring matching, but the flag help and the conflict validator assume exact names (
cmd/multi_service_filters.go:241-244,cmd/main.go:122-123,cmd/validators.go:208,234-248). configure-azure/configure-gcpwrite credentials intoListSecrets(...)[0], a prefix match over an unordered, unpaginated list (cmd/configure_azure.go:145-166,cmd/configure_gcp.go:116-131,cmd/secrets_store.go:32-56).engineNameMapis duplicated verbatim between this repo'scmd/helpers.go:727-760andcloud-commitments-go'spkg/common/engine.go:8-41(cross-repo item, see above).--max-instancesand--min-countare unit-mismatched for Savings Plans on this repo's side:ApplyInstanceLimit(cmd/helpers.go:505-526) treats every SP as "1 instance" (cross-repo item, see above).ApplyCoveragepasses Savings Plans through unscaled when theDetailstype assertion fails (cmd/helpers.go:151-162).- The Savings-Plan summary silently discards unknown plan types and every canonical (non-legacy) service type (
cmd/multi_service_stats_helpers.go:20-46,100-146,159-183,209-237).
LOW (remaining in this repo)
configure-azure's help text advertises passing the client secret on the command line (cmd/configure_azure.go:101,117).queryRDSInstancesInRegionpaginates without a page cap or a context check, unlike its fix(aws/recommendations): paginate CE recommendation + RDS engine-version describe (single-page truncation) #692-hardened sibling (cmd/multi_service_engine_versions.go:132-157).CheckAuditLogWritablecreates the audit log with 0600, defeating the 0644pkg/common/audit.go(go repo) documents as required (cmd/helpers.go:558).
From the 2026-09-02 audit (remaining in this repo)
- A10-004 (high):
GetAccountAlias(cmd/helpers.go:91-94) caches and returns the numeric account ID as the account name whenorganizations:DescribeAccountfails, andshouldIncludeAccountthen filters on that stand-in.cudly --purchase --exclude-accounts productionagainst an account whose alias failed to resolve finds no substring match and buys in the account the operator explicitly ruled out. The bad value is cached for the process lifetime. Fix: return(string, error)and fail the run when an account filter is in force and the alias cannot be resolved; do not cache the failure. - A10-030 (low): the configure wizards'
"[R]un, [S]kip?"prompts have no default marked, but pressing Enter falls through to the resource-creating branch (cmd/configure_azure.go:481,513;cmd/configure_gcp.go:666,700), creating an Entra ID application / GCP service account with elevated grants on a blank input. Fix: make Skip the empty-input default on resource-creating steps.
All other findings from the original review body above are superseded by the sibling issues linked here.
Activity
cristim commentedon Sep 27, 2026
Split as part of the monorepo split (cloud-commitments-cli / -go / -mcp / -platform). This repo is one of the genuinely affected repos (most of the original findings are root-level
cmd/*.go), so this issue stays open, scoped down to that remainder. See the "Scope after the split" section added to the issue body.Sibling issues for the parts that moved elsewhere:
cmd/cleanup-lambda,cmd/lambda/clear-rate-limit)pkg/common,pkg/scorer,pkg/retry,pkg/logging,pkg/errors,pkg/config,providers/aws)Two findings (the
engineNameMapduplication, and the Savings-Plans count-unit mismatch) genuinely span this repo and the go repo; both are quoted in full in both issues with a cross-reference to the sibling half, so fixing only one side leaves the pair inconsistent.cristim commentedon Oct 16, 2026
Re-verified the remaining scope against origin/main fd0af9f [claude-cli-impl]:
--coverage 80on a CSV run buys 100%) is now fix(csv): explicit --coverage 80 is treated as unset on --input-csv runs and buys 100% #2236 (p1). M4 (substring account matching) is now fix(filters): account filters match substrings, so --include-accounts dev also selects devops-prod #2237 (p2).engineNameMapduplicate is gone from cmd), L3 (the audit log is now in the go repo), and A10-004 (fixed by fix(cmd): fail the run when an account name cannot be resolved for an account filter #2199). M8 now lives in the go repo'srecfilter/sizing.go:53-60.ListSecrets()[0], which overlaps fix(configure-azure): stop printing the new service principal's client secret (A10-028) #2212), M7 (cross-repo), M9 (the SP summary skips unknown or canonical types), L1 (the--client-secretflag advertised in help, which overlaps fix(configure-azure): stop printing the new service principal's client secret (A10-028) #2212), L2 (RDS pagination has no page cap, which overlaps fix(cmd): skip extended-support lifecycle entries with no start date #2210), and A10-030 (Enter runs resource creation in the Azure/GCP wizards).fix(cli): count every Savings Plan and RI type in the run summary (#2239
fix(cli): make Enter skip resource-creating configure-gcp steps (#2249)