| service | securityhub | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| sdk_module | aws-sdk-go-v2/service/securityhub@v1.75.4 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_audit_commit | 1659d616 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_audit_date | 2026-07-25 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overall | A | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ops |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| families |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gaps |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deferred | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| leaks |
|
The Go SDK module was bumped, revealing 7 operations added to
aws-sdk-go-v2/service/securityhub since the previous audit: CreateConnector,
GetConnector, UpdateConnector, DeleteConnector, ListConnectors (a new
CSPM third-party cloud-provider connector family -- see the "Traps" note
above for why this is not the same as the existing ConnectorV2 family),
and EnableSecurityHubFeatureV2/DisableSecurityHubFeatureV2 (opt-in feature
toggles scoped to the existing SecurityHub V2 hub state). All 7 were
implemented for real (routing, backend state, request parsing, response wire
shapes field-diffed against the SDK's own types/serializers.go/
deserializers.go, error codes, HTTP status, Snapshot/Restore persistence)
and added to GetSupportedOperations() -- none went into the
TestSDKCompleteness notImplemented list (which stayed empty).
Key design decisions:
EnableSecurityHubFeatureV2/DisableSecurityHubFeatureV2are wired to the existingHubV2state, not an orphan boolean. The real API's/hubv2/feature/{FeatureName}path and its documented "the service must be enabled before you can enable a feature" precondition both point at the existing V2 hub. Features are stored asHubV2.Features map[string]*HubV2Feature(new field on the existing struct) rather than a separate backend field, so they persist/reset with the V2 hub's own lifecycle for free (no new Snapshot/Restore wiring needed) andDescribeSecurityHubV2-- the existing op -- now reports them, matching the realDescribeSecurityHubV2Output.Featuresfield that also arrived in this SDK bump.- CSPM Connectors' authorization lifecycle is modeled honestly, not
auto-completed. Unlike Connectors V2 (which has
RegisterConnectorV2to complete an out-of-band OAuth handshake), the real CSPM Connector surface has no such operation at all -- see thegapsentry above. A connector created viaCreateConnectoris left atEnablementStatus=PENDING_ENABLEMENT/ healthConnectorStatus=UNKNOWNpermanently, since no real client action this backend can observe would legitimately advance it further. - Bonus fix, found while wiring
FeaturesintoDescribeSecurityHubV2: its response previously returned inventedCreatedAt/UpdatedAtfields; the realDescribeSecurityHubV2Output(confirmed in both v1.71.2 and v1.75.0, so this predates the SDK bump) is{Features, HubV2Arn, SubscribedAt}. Fixed in the same handler function this pass touched anyway to addFeatures.
Fresh audit (this service had no PARITY.md before the 2026-07-23 pass). Persistence (Handler.Snapshot/Restore delegating to InMemoryBackend) was added recently and verified intact -- no changes needed there.
-
handler_configpolicy.go-- ConfigurationPolicyAssociationTargetTypealways empty.GetConfigurationPolicyAssociation,StartConfigurationPolicyAssociation, andStartConfigurationPolicyDisassociationall read a"TargetType"key out of the request'sTargetobject. The real wire shape (types.Targetis a Smithy tagged union -- seeserializers.go:34632 awsRestjson1_serializeDocumentTarget) never sends that field; the request is one of{"AccountId":...}/{"OrganizationalUnitId":...}/{"RootId":...}andTargetType(ACCOUNT/ORGANIZATIONAL_UNIT/ROOT) must be derived from which key is present. Every association response'sTargetTypefield was silently empty for every real SDK client. Fixed by addingextractConfigPolicyTarget(derives ID + type from the union) and using it at all three call sites. Covered byTestParity_ConfigurationPolicyAssociation_TargetTypeDerived(parity_d_test.go). -
backend_members.go--InviteMembersnever validated the account exists. AWS requiresCreateMembersbeforeInviteMembers; inviting an account that was never created must land inUnprocessedAccounts. The previous implementation unconditionally created anInvitationfor every account ID with no existence check, soUnprocessedAccountswas always empty regardless of input validity -- a disguised no-op on the validation path. Fixed to checkb.members.Get(id)first and populateUnprocessedAccounts(ResourceNotFoundException) for unknown accounts, matching the same pattern already used byDeleteMembers/GetMembers. Covered byTestParity_InviteMembers_UnknownAccountUnprocessed. -
backend_v2.go--UpdateAutomationRuleV2silently droppedActionsupdates. The handler passes the raw decoded JSON request body straight through asupdates map[string]any. A JSON array decodes into[]any(each elementmap[string]any), but the backend assertedupdates["Actions"].([]map[string]any)directly -- an assertion that can never succeed against[]any, so everyActionsupdate was silently dropped while every other field updated fine. Fixed to convert[]any->[]map[string]anyelement-by-element, mirroring the pattern already used correctly inBatchUpdateAutomationRules(V1) and the V2 create handler. Covered byTestParity_UpdateAutomationRuleV2_ActionsApplied.
-
findings.go--GetFindings/GetFindingsV2acceptedSortCriteriabut silently discarded it (results returned in map-iteration order, effectively random). AddedsortFindings(stable multi-key sort overtypes.SortCriterion'sField/SortOrder"asc"/"desc" wire shape), wired into bothGetFindingsand the newGetFindingsV2. Covered byTestGetFindings_SortCriteria(findings_test.go). -
findings.go--BatchImportFindingsre-import overwroteNote/UserDefinedFields/VerificationState/Workflowinstead of preserving them. AWS documents ("After a finding is created,BatchImportFindingscannot be used to update the following finding fields...") that these four fields are retained from the finding's previous version regardless of what a re-import request supplies.ImportFindingspreviously did a flatmaps.Copythat let any subsequent import silently reset a customer's investigation Note/Workflow/etc. Fixed withpreserveCustomerManagedFields, which restores (or deletes, if never set) these fields from the prior stored version after every re-import. Covered byTestBatchImportFindings_PreservesCustomerManagedFields. -
findings.go--GetFindingHistorywas a hardcoded stub returning{Records: []}always; no finding-update history was ever recorded. Added afindingHistory map[string][]map[string]anystore field (snapshot-persisted alongsidefindings, same plain-map pattern) andrecordFindingHistory/diffFindingFieldshelpers.ImportFindingsnow records aFindingCreated: trueentry for new findings and a field-diff entry for re-imports;BatchUpdateFindingsandUpdateFindingseach record a field-diff entry per mutated finding (excluding theCreatedAt/UpdatedAt/FirstObservedAt/LastObservedAttimestamp fields AWS documents as excluded from history).GetFindingHistorynow filters the recorded log byStartTime/EndTimeand paginates it (100 per page, matching AWS's documented cap). Covered byTestGetFindingHistory_RecordsChangesandTestGetFindingHistory_UnknownFinding. -
handler_findings.go--BatchUpdateFindingsV2read a nonexistent"FindingFieldsUpdate"wrapper key. The realBatchUpdateFindingsV2Inputwire shape (aws-sdk-go-v2/service/securityhub/api_op_BatchUpdateFindingsV2.go) is flat:Comment,FindingIdentifiers([]types.OcsfFindingIdentifier),MetadataUids,SeverityId,StatusId-- there is no wrapper object, so every real client request was silently a no-op. Additionally,FindingIdentifiersusesCloudAccountUid/FindingInfoUid/MetadataProductUid(types.OcsfFindingIdentifier), not V1'sProductArn/Id, so even after fixing the wrapper-key bug the old delegation to V1BatchUpdateFindingscould never match a stored finding. Rewrote as a dedicatedBatchUpdateFindingsV2backend method (findings_v2.go) that parses the flat request fields and resolvesCloudAccountUid/FindingInfoUid/MetadataProductUidagainst the stored finding'sAwsAccountId/Id/ProductArn-- the only viable mapping since this mock has no separate OCSF ingestion API (findings only ever enter via V1BatchImportFindings). Covered byTestBatchUpdateFindingsV2_WireShapeandTestBatchUpdateFindingsV2_UnmatchedIdentifiers(findings_v2_test.go). -
handler_findings.go--GetFindingsV2Filterswas passed straight to the V1matchesFindingFilters, which looks for top-levelId/ProductArn/etc. keys. The realGetFindingsV2Filterswire shape istypes.OcsfFindingFilters:{CompositeFilters: [...], CompositeOperator: "AND"|"OR"}, eachCompositeFilterholdingStringFilters/NumberFilters/etc. keyed by an OCSF field name (types.OcsfStringField/OcsfNumberField) plus its ownOperator. None of those keys exist in the V1 filter shape, so every real V2 client'sFilterswas a complete no-op (matched everything) rather than merely "unsorted" -- worse than the PARITY.md entry previously on file suggested. AddedmatchesFindingFiltersV2+matchesCompositeFilter/matchesOcsfStringFilter/matchesOcsfNumberFilter(findings_v2.go), which evaluate the real nested shape against a field-name-mapped subset of the stored ASFF finding (seeocsfStringFieldMap/ocsfNumberFieldMapand the residual-gap entry above).severity_id/status_idNumberFilters round-trip theSeverityId/StatusIdfieldsBatchUpdateFindingsV2itself writes (fix #7), giving V2 update + V2 filter a coherent, testable round trip. Covered byTestGetFindingsV2_CompositeFilters.
The previous pass (fix #8 above) evaluated only StringFilters/NumberFilters
within each CompositeFilter; DateFilters, MapFilters, IpFilters,
BooleanFilters, and NestedCompositeFilters were accepted on the wire and
silently ignored -- worse than an error, since a caller got HTTP 200 and an
unfiltered result set with no indication their filter did nothing. Field-diffed
the full real taxonomy (types.CompositeFilter, types.Ocsf*Filter,
types.Ocsf*Field enums, types.StringFilter/MapFilter/DateFilter/
IpFilter/BooleanFilter/NumberFilter/DateRange,
types.AllowedOperators/StringFilterComparison/MapFilterComparison/
DateRangeComparison/DateRangeUnit) against aws-sdk-go-v2/service/ securityhub@v1.75.0's types/types.go and types/enums.go directly (not
against this handler's own prior output).
Filter types implemented this pass, each restructured into its own small
result-collector (stringFilterResults/numberFilterResults/
dateFilterResults/mapFilterResults/ipFilterResults/
booleanFilterResults/nestedCompositeFilterResults) feeding a single
matchesCompositeFilterDepth combinator (decomposed to keep CodeFactor's
Complex Method check quiet -- no nolint):
- DateFilters (
ocsfDateFieldMap):finding_info.created_time_dt->CreatedAt,finding_info.first_seen_time_dt->FirstObservedAt,finding_info.last_seen_time_dt->LastObservedAt,finding_info.modified_time_dt->UpdatedAt-- all genuine ASFF finding-level timestamps. Both comparator shapes are implemented: absoluteStart/Endbounds (matchesDateStartEnd), and relativeDateRange{Comparison: WITHIN|OLDER_THAN, Unit: DAYS, Value}(matchesDateRange) --WITHINmatches at-or-afternow - Value days,OLDER_THANits strict complement.resources.image.*/resources.modified_time_dthave no ASFF equivalent (ASFF'sResourcecarries no image/per-resource-modified timestamp) and are unmapped. - MapFilters (
mapFilterCandidates):resources.tags-> per-resourceResources[].Tags,finding_info.tags-> the finding-levelUserDefinedFieldsmap (the closest real ASFF analog to a finding-level "tag"),compliance.control_parameters->Compliance. SecurityControlParameters[]{Name,Value[]}. All fourMapFilterComparisonvalues implemented (EQUALS/NOT_EQUALS/CONTAINS/NOT_CONTAINS) viacompareMapFilter, with positive comparisons OR'd and negative ones AND'd across multiple candidate values for the same key (mirrors the documented same-field combination rule).databucket.tagshas no ASFF concept at all and is unmapped. - IpFilters (
ipFieldNetworkKeys):evidences.src_endpoint.ip->Network.SourceIpV4/SourceIpV6,evidences.dst_endpoint.ip->Network.DestinationIpV4/DestinationIpV6-- ASFF has no "evidences" concept, butNetwork's source/destination IP fields are the only genuinely analogous data this store carries.IpFilterhas only aCidrfield (no comparator) -- CIDR containment vianet.ParseCIDR/IPNet.Contains, with a bare IP address normalized to an exact-match/32or/128per AWS's documented "CIDR block or single IP" input. - BooleanFilters: only
vulnerabilities.is_exploit_availableis evaluated --Vulnerability.ExploitAvailableis a genuine two-valued ASFF enum (YES/NO), so it round-trips to bool cleanly; a finding matches if ANY entry in itsVulnerabilitiesarray has a matching value.vulnerabilities.is_fix_availableis deliberately NOT evaluated:Vulnerability.FixAvailableis three-valued (YES/NO/PARTIAL), and collapsingPARTIALinto eithertrueorfalsewould silently misclassify findings -- worse than leaving it unfiltered.compliance.assessments.meets_criteriahas no ASFF backing at all (no "assessments" concept onCompliance) and is also unmapped. - NumberFilters bonus: added
confidence_score-> ASFF's own top-levelConfidence(int 0-100) toocsfNumberFieldMap-- a clean scalar match found while auditing the taxonomy, not part of the original gap list.
NestedCompositeFilters: recurses fully via matchesCompositeFilterDepth
-- each nested CompositeFilter is evaluated as its own sub-tree (including
its own further NestedCompositeFilters) and the resulting bool joins its
parent's result list, combined by the parent's own Operator. This was
chosen over half-evaluating (e.g. only reading direct filters and ignoring
nesting) because a partially-evaluated boolean tree returns wrong
results, not merely unfiltered ones -- see the task's own warning, confirmed
by a regression-style test case
(NestedCompositeFilters_AND_recurses_and_requires_both_branches): a single
finding can't have two different AwsAccountId values, so ANDing two
mutually-exclusive nested branches must match zero findings; before this
fix (NestedCompositeFilters unevaluated -> empty result list -> vacuous
match-all), that same request would have wrongly matched both seeded
findings. Recursion depth is capped at maxNestedCompositeDepth = 5 (AWS
documents the real structure as capped at 3 layers; 5 is a defensive margin
against a pathological/hand-crafted request, not a limit real traffic
should approach). Note types.AllowedOperators has only AND/OR -- there
is no logical NOT combinator in the real API; negation is expressed at the
leaf via NOT_* comparators (NOT_EQUALS/NOT_CONTAINS/
PREFIX_NOT_EQUALS), not a boolean-tree NOT node, so AND/OR recursion is
the complete real semantics.
Comparator verification: StringFilterComparison
(EQUALS/PREFIX/NOT_EQUALS/PREFIX_NOT_EQUALS/CONTAINS/
NOT_CONTAINS/CONTAINS_WORD) was already correctly implemented by
compareStringFilter (reused unchanged) -- confirmed against types.go's
enum values and StringFilter's doc comments describing each comparator's
exact semantics (including the CONTAINS_WORD-only-in-V2-APIs note).
MapFilterComparison (EQUALS/NOT_EQUALS/CONTAINS/NOT_CONTAINS, no
PREFIX variant -- confirmed the enum has no PREFIX member) implemented fresh
in compareMapFilter following the same positive-OR/negative-AND doc
pattern. DateRangeComparison (WITHIN/OLDER_THAN, default WITHIN per
doc) and the fact DateRangeUnit has only DAYS as of this SDK version
were both confirmed directly against enums.go. NumberFilter was
reconfirmed to have no Comparison field at all (Eq/Gt/Gte/Lt/Lte
only) -- unchanged from the prior pass.
Tests: extended TestGetFindingsV2_CompositeFilters (existing table) with
two confidence_score cases, and added a new table test
TestGetFindingsV2_CompositeFilters_DateMapIPBooleanNested covering every
implemented filter type with paired cases that each narrow to exactly one of
two seeded findings with deliberately divergent field values (proving actual
discrimination, not a "matches everything" false pass), plus the
AND/OR nested-recursion pair described above.
Extracted every op's HTTP method + URI template directly from
aws-sdk-go-v2/service/securityhub@v1.71.2/serializers.go
(awsRestjson1_serializeOpHttpBindings* / SplitURI calls) for all ~105
operations and cross-checked against classifyPath's per-family
classify*Path functions in handler.go, handler_members.go,
handler_configpolicy.go, and handler_v2.go. All method+path pairs match.
RouteMatcher (handler.go) was separately checked to confirm every prefix
classifyPath switches on is also covered by RouteMatcher's
unambiguous-prefix OR-chain, so no routed op is reachable by Handler()
directly (bypassing the matcher, as unit tests do) but unreachable through
the real Echo route registration. No route-matcher bugs found in this
service.
/automationrulesv2is astrings.HasPrefixsuperset of/automationrules(both share the/automationrulessubstring) --classifyPath's switch correctly orders the V2 case before the V1 case. Don't "simplify" that ordering.- (parity-4) Same trap, new pair:
/connectorsv2is astrings.HasPrefixsuperset of the new plain/connectors(CSPM connectors).pathClassifiersinhandler.goordershasPathPrefix(pathConnectorsV2)beforehasPathPrefix(pathConnectors)-- don't reorder or collapse them. Also note:CreateConnector/GetConnector/etc. (this pass) andCreateConnectorV2/GetConnectorV2/etc. are two entirely unrelated real AWS features that happen to share the word "connector" -- CSPM connectors link to third-party cloud providers (Azure), Connectors V2 link to third-party ticketing systems (Jira/ServiceNow). Modeled as distinct Go types (CspmConnectorvsConnectorV2) and distinct backend/handler files (connectors.go/handler_connectors.govsconnectors_v2.go/handler_connectors_v2.go) specifically to avoid conflating them. classifyConfigPolicyPath's PATCH/DELETE cases match/configurationPolicy/with explicit exclusions forcreate/get/listsuffixes rather than a positive{Identifier}pattern -- this is intentional (mirrors the real flat-path-segment routing) and correct as long as no realConfigurationPolicyIdentifiervalue is literally"create","get", or"list".BatchUpdateFindings/ImportFindings/GetFindingsdo not checkhubEnabled(unlikeUpdateFindings/insights/action-targets). This was investigated and left as-is: AWS's own docs don't clearly state these ops require the hub to be enabled, and no existing test asserts either behavior, so flipping it risks breaking passing integrations without clear spec backing. Revisit if a concrete AWS error transcript surfaces.
Part of the gopherstack-us9u/g479 map-literal scanner's 526-key unknown-key
bucket triage. Fixed items proven via real aws-sdk-go-v2/service/securityhub
client round trips or raw-body assertion (wire_field_fixes_y1zn_test.go),
hand-reverted, confirmed failing, restored, md5sum-verified byte-identical.
ListAggregatorsV2: {wire: fixed} -- wrapped the list under "Aggregators"; real member (deserializers.go's awsRestjson1_deserializeOpDocumentListAggregatorsV2Output) is "AggregatorsV2".DeclineInvitations/DeleteInvitations: {wire: fixed} -- each emitted an extra "ProcessedAccounts" key alongside the real "UnprocessedAccounts"; neither DeclineInvitationsOutput nor DeleteInvitationsOutput has a ProcessedAccounts member -- success is implied by an account's absence from UnprocessedAccounts, not a separate echo.GenerateRecommendedPolicyV2/GetRecommendedPolicyV2: {wire: fixed, note: "confirmed real bug, then deferred to gopherstack-tp8x, now fixed (2026-08-21) -- see the ops entries above for the full fix. The deferral note's claim that GenerateRecommendedPolicyV2 'is not a real operation at all' was itself wrong (verified: it is real, POST /recommendedPolicyV2/{MetadataUid}, matching this handler's existing route exactly) -- a reminder that a prior pass's rejection reasoning needs re-verification against the serializer, same as any other claim."}
gopherstack-wlo1 (2026-08-22): dispatch-miss error path was the one call site gopherstack-aitg left untyped
gopherstack-aitg (2026-08-11, commit 695aa1c20) added a central error path
(typedErrorResponse, handler.go) and audited every named call site against
securityhub's real per-operation exception lists. handleREST's own
dispatch-miss fallback -- reached when classifyPath returns opUnknown,
i.e. no classify*Path function recognises the request's method+path --
was not one of the sites that pass touched, and unlike every genuinely
ambiguous ErrHubNotEnabled site in this file (each carries a comment
explaining why it's deliberately left unheadered), this one had no such
note. It wrote {"Message": "unknown operation"} with no
X-Amzn-Errortype header and no body code/__type field, so
restjson.GetErrorInfo (aws-sdk-go-v2's
aws/protocol/restjson/decoder_util.go) had nothing to read and the error
deserialized client-side as UnknownError regardless of the underlying
cause.
Reachability: cross-checked every op constant this package wires into
opHandlerGroups()'s dispatch tables (116 distinct map[string]func()
entries) against every api_op_*.go file in the pinned
securityhub@v1.75.4 module (116 real operations) -- exact 1:1 match, zero
missing. So this fallback is structurally unreachable for any
legitimately-constructed SDK request; it can only be reached by rewriting
the request after signing (proven below), the same white-box category as
medialive/mediatailor's analogous fixes in ea67f34cf.
Fixed: handleREST now calls typedErrorResponse(c, http.StatusNotFound, "ResourceNotFoundException", "unknown operation") -- the same helper (and
the same code) GetSecurityControlDefinition's unknown-control path
already uses (handler_error_type_test.go's existing
TestGetSecurityControlDefinition_UnknownControlSurfacesResourceNotFoundException),
so no new exception vocabulary was introduced.
Proof: TestGetInsightResults_UnrecognisedRouteSurfacesResourceNotFoundException
(handler_error_type_test.go) drives a real securityhubsdk.Client's
GetInsightResults through a Finalize-stage middleware that rewrites the
signed request's path from /insights/results/{InsightArn+} down to bare
/insights -- still inside RouteMatcher's /insights prefix (so the
request still reaches this package's Handler) but matching none of
classifyInsightsPath's method/path cases for a GET, landing in
handleREST's fallback. Hand-reverted handler.go to git show HEAD (the
pre-fix state, still carrying the bare map literal), confirmed the test
fails with apiErr.ErrorCode() == "UnknownError", restored the fix,
md5sum-confirmed byte-identical to the pre-revert file.
Not a repeat of the ErrHubNotEnabled ambiguity: those sites are ambiguous
between two real, named exceptions a specific operation models. This site
doesn't know the operation at all (routing itself failed), so there is no
per-operation vocabulary to disambiguate between -- a generic
ResourceNotFoundException (already the modeled 404 shape used elsewhere
in this file, e.g. GetSecurityControlDefinition) is the closest fit, not
a guess among named alternatives.
Confirmed still-deliberate and left untouched: every ErrHubNotEnabled
bare-message site (handler_hub.go, handler_insights.go, handler_findings.go,
handler_products.go, handler_action_targets.go) -- each carries its own
comment citing the specific operation's real error list from
securityhub@v1.75.4 deserializers.go and the reason no single exception
can be chosen without guessing. Re-spot-checked DisableSecurityHubV2
(deserializers.go:7744) directly: its real error list is
AccessDeniedException/InternalServerException/ThrottlingException/
ValidationException -- no ResourceNotFoundException, confirming the
comment's claim -- and left as documented rather than "resolved by
elimination", since the real "not enabled" AWS status for this call is not
independently verified here.
CI's unit-tests (3) job flagged -race failures in
TestExtractOperation_SDKRouteTable on describesecurityhubv2 and the
enable/disable-feature subtests. Root cause: DescribeSecurityHubV2
(hub.go) did cp := *b.hubV2 under RLock and returned &cp --
HubV2.Features is map[string]*HubV2Feature, so the copy's Features
field is the same map as the live b.hubV2.Features.
handleDescribeSecurityHubV2 (handler_hub.go) ranges over that map after
RUnlock has already run, racing against EnableSecurityHubFeatureV2/
DisableSecurityHubFeatureV2's b.hubV2.Features[name] = &HubV2Feature{...}
writes under Lock. Hub (v1) has no reference fields, so DescribeHub's
identical-looking cp := *b.hub is genuinely safe and was left alone.
Reproduced directly (not just via the flaky parallel-subtest ordering CI
hit): TestSecurityHubV2FeatureDescribeRace (hub_test.go) drives
DescribeSecurityHubV2 + Enable/DisableSecurityHubFeatureV2 concurrently
against one backend. Confirmed failing pre-fix (runtime.mapassign_faststr
write vs. runtime.mapIterStart/mapIterNext read), hand-reverted hub.go
to the shallow-copy version, re-confirmed the same failure, restored,
md5sum-confirmed byte-identical. Fixed with HubV2.clone(), which
deep-copies Features (new map, new *HubV2Feature per entry).
Audited the rest of services/securityhub/ for the same two shapes:
- A struct with a map/slice field is shallow-copied (
cp := *x) while that field is mutated in place (indexed assignment) elsewhere under lock, or is aliased with a map that's mutated in place elsewhere (Tagsfields are assigned the exact same map object passed tob.tags[ARN] = tagsat creation time, andTagResource/UntagResourcemutateb.tags[ARN]in place viamaps.Copy/delete). - A live, stored
*T(or one of its map/slice fields) is returned directly with no copy at all, and that same object is later mutated in place (by anUpdate*, or by the sameGet-style op itself, e.g.GetEnabledStandards's poll-to-READY advance) under a subsequent lock acquisition.
Fixed (added a .clone() deep-copy method per type, used at every point the
value crosses the lock boundary -- Create/Get/List/Batch/Update returns):
ConfigurationPolicy(configuration_policies.go):Tagsaliasesb.tags[Arn];ConfigurationPolicymap cloned too for consistency.CreateConfigurationPolicyalso returned the live stored pointer.CspmConnector(connectors.go):Tagsaliasesb.tags[ConnectorArn](Providercloned too).CreateConnectorreturned the live pointer.ConnectorV2(connectors_v2.go): sameTags/Providershape.CreateConnectorV2returned the live pointer; so didUpdateConnectorV2andRegisterConnectorV2before theircp := *targetwas replaced with.clone().AutomationRule/AutomationRuleV2(automation_rules.go):BatchGetAutomationRulesreturned live*AutomationRulepointers with no copy at all --BatchUpdateAutomationRulesmutatesRuleName/RuleStatus/Criteria/Actions/etc. on that same object in place.CreateAutomationRuleV2likewise returned the live pointer, later mutated byUpdateAutomationRuleV2.StandardsSubscription(standards.go):BatchEnableStandardsandBatchDisableStandardsreturned the live, stored pointer;GetEnabledStandardsreturns the exact objects it just mutated in place (pollCount,StandardsStatus) with no copy, and those same objects can be mutated again later byBatchDisableStandards.StandardsControl(standards.go):DescribeStandardsControls's override branch (controls[i] = override) assigned the live*StandardsControlstored inb.controlOverridesdirectly;UpdateStandardsControlmutates an existing override's fields in place.AggregatorV2/FindingAggregator:Regions []stringis only ever wholesale-reassigned (never indexed into), so the existing shallow copies on Get/List/Update were already safe -- butCreateAggregatorV2/CreateFindingAggregatorreturned the live pointer, later mutated by their respectiveUpdate*. Fixed by copying at the Create return only.Member(members.go):CreateMembersappended the live pointer;InviteMembers/DisassociateMembersmutateMemberStatus/InvitedAton that same object in place.GetMembers/ListMembersalready copied correctly.
Confirmed safe, left unchanged, with reason:
Hub(hub.go),Invitation/AdminAccount(invitations.go),OrgConfig(organizations.go),ConfigurationPolicyAssociation(configuration_policies.go),RecommendedPolicyV2/TicketV2: all-scalar structs, or (RecommendedPolicyV2/TicketV2) have noUpdate*that ever mutates an existing instance after creation.knownStandards/knownSecurityControls/knownProducts: package-level read-only lookup tables, never mutated afterinit; everycp := knownX[i]copy is safe regardless of field shape.BatchGetSecurityControls'sParametersfield (controls.go): hands outb.controlParams[id]'s map directly with no copy, but the only writer (UpdateSecurityControl) always replaces the whole map entry (b.controlParams[id] = parameters), never indexes into an existing one -- a previously-handed-out map is never touched again.BatchGetStandardsControlAssociations's override branch (standards.go): hands out the live*StandardsControlAssociationfromb.controlAssocOverridesdirectly, but the only writer (BatchUpdateStandardsControlAssociations) alwaysPuts a brand-new struct rather than mutating an existing one in place.Snapshot(store.go): marshals every live field (includingb.tags,b.hubV2,b.controlParams, ...) while still holdingRLockfor the entire call -- unlike the handler-side bugs above, the read never escapes the lock.
Proof: go test -race -count=20 ./services/securityhub/... clean after all
fixes; TestSecurityHubV2FeatureDescribeRace is the new permanent
regression test for the flagged bug specifically.
Audited securityhub's failure path -- what a real typed aws-sdk-go-v2
client sees when a request fails -- as part of a four-service sweep
(securityhub, kafka, elbv2, stepfunctions) hunting the class of bug where
gopherstack's error-handling call site picks a sentinel/wire code the real
operation's own deserializeOpError<Op> switch does not model. All 116
operations' switches extracted from deserializers.go (securityhub@v1.75.4)
and diffed against every typedErrorResponse(...) call site (125 sites
across all handler_*.go files) and the ErrHubNotEnabled/ErrNotFound/
ErrAlreadyExists/etc. sentinels feeding them.
Every literal errType string used at a typedErrorResponse call site names
a real type in this SDK's types/errors.go (AccessDeniedException,
ConflictException, InternalException, InternalServerException,
InvalidAccessException, InvalidInputException, ResourceConflictException,
ResourceNotFoundException, ValidationException) -- no fabricated code exists
anywhere in this service. Every ResourceNotFoundException/
ResourceConflictException/InvalidAccessException/ValidationException/
InvalidInputException call site was cross-checked against its own
operation's modeled set (not a sibling's) and matches exactly; the classic
REST vocabulary (InvalidInputException/InternalException/
ResourceConflictException) and the newer V2-style vocabulary
(ValidationException/InternalServerException/ConflictException) are never
crossed at a call site, including the several non-"V2"-suffixed operations
(Connectors, ConnectorsV2, AutomationRulesV2, AggregatorsV2) that use the
newer vocabulary -- this distinction was already called out and correctly
handled by a prior pass (see typedErrorResponse's doc comment,
handler.go:507-514), and this pass re-verified it rather than trusting the
comment.
Two call sites (handleStartConfigurationPolicyDisassociation,
handleUpdateStandardsControl) have an unreachable 500 fallback: their
backend methods never actually return an error (both silently accept any
identifier, including one that was never created, rather than validating
against a known-resource set) even though their operations model
ResourceNotFoundException. This is a missing-validation / structural gap,
not a wrong-sentinel-at-a-call-site bug -- fixing it would mean building a
"does this identifier correspond to a real resource" check neither op has
today, not swapping which existing sentinel a call site already picks -- so
it is reported here rather than fixed under this sweep's narrower scope.
No test changes; no source changes. Recorded as genuinely clean for this bug class, matching several other services in this campaign.
Distinct class from the error-path sweep above: not which sentinel a call
site picks, but whether a call's own return value carrying failure
information is thrown away (x, _ := b.Something(...)). ~195 , _ :=/
, _ =/bare _ = sites across all non-test .go files, triaged
individually.
The large majority are legitimate: JSON-body type assertions
(body["Field"].(string)) where a missing/wrong-typed value correctly
becomes the zero value; x, _ := b.<store>.Get(id) calls that follow a
resolve*/existence check in the same function (the miss case already
returned); and strconv.Atoi(v) best-effort query-param parses that fall
back to 0 ("use default").
All 12 Batch* operations checked against their backend implementations --
BatchImportFindings, BatchUpdateFindings, BatchUpdateFindingsV2,
BatchGetSecurityControls, BatchGetAutomationRules,
BatchDeleteAutomationRules, BatchUpdateAutomationRules,
BatchEnableStandards, BatchDisableStandards,
BatchGetStandardsControlAssociations,
BatchUpdateStandardsControlAssociations,
BatchGetConfigurationPolicyAssociations -- each correctly threads its
per-item unprocessed/failed list (or an err return) into the response.
Two things worth recording, neither a bug:
handleBatchEnableStandards/handleBatchDisableStandards(handler_standards.go:57,76) discardBatchEnableStandards/BatchDisableStandards's second return (a[]map[string]anyof failures). Left as-is:BatchEnableStandardsOutput/BatchDisableStandardsOutput(securityhub@v1.75.4 api_op_BatchEnableStandards.go / api_op_BatchDisableStandards.go) carry onlyStandardsSubscriptions-- there is no per-item failure field on the real wire shape to put it in.BatchEnableStandards's own failure branch (emptyStandardsArn) is additionally unreachable via a real typed client:StandardsArnis// This member is requiredontypes.StandardsSubscriptionRequestand enforced byvalidateStandardsSubscriptionRequest/validateOpBatchEnableStandardsInput(validators.go) before the request leaves the client.handleCreateAggregatorV2's_ = h.Backend.TagResource(...)(handler_aggregators_v2.go:46):TagResource(tags.go:5) unconditionally returns nil, so no real error is being suppressed.handleCreateMembers's_ = created(handler_members.go:74): correct per wire shape --CreateMembersOutput(api_op_CreateMembers.go) has onlyUnprocessedAccounts, no created-members field to populate.
No test changes; no source changes. Recorded as genuinely clean for this bug class.
Audited every paginated listing for the five known gopherstack
pagination-arithmetic bug classes (panic on stale offset, infinite loop on
stale equality-matched cursor, guarded-but-unused index, encoder/decoder
disagreement, unsorted collection). Census: one shared offset-token helper
(store.go's paginateSlice, 15 call sites) plus two supporting helpers
(filterOrAll, sortFindings) feed every List/Describe/Get* op in this
service; no inline for i, x := range all { if x.ID == token { start = i } }
site exists outside store.go. paginateSlice itself was already correct
(clamped offset decode, no equality search — all seven checks pass).
This service came back with a real, repo-wide Class E problem, not clean.
11 of the 15 paginateSlice call sites fed it a collection read straight
from a map or a store.Table.All() (explicitly documented as unspecified
iteration order) with no sort in between:
filterOrAll's "return everything" branch (arnsempty) calledt.All()— affectsDescribeActionTargetsandGetEnabledStandards.sortFindingswas a no-op whensortCriteriawas empty (if len(criteria) == 0 { return }) — affectsGetFindingsandGetFindingsV2, whose backing store (b.findings) is itself amap[string]map[string]any, so the common no-sort-criteria call shape hit this on every listing.- 8 more
.All()-straight-into-paginateSlicesites with zero sort:ListAutomationRulesV2,ListAggregatorsV2,ListInvitations,ListConnectors(CSPM),ListConnectorsV2,ListFindingAggregators,ListConfigurationPolicies,ListConfigurationPolicyAssociations,ListMembers. - 2 sites ranging a raw (non-
store.Table) map with zero sort:ListOrganizationAdminAccounts(b.orgAdminAccounts),GetResourcesV2(a locally-builtmap[string]map[string]anykeyed by resource Id).
All are Class E: a plain two-page walk with no deletion or tampering drops or duplicates results whenever Go's map iteration reorders between the two calls (confirmed empirically — reverting one fix and rerunning its regression test failed 5/5 times).
Fixed 9 of the store.Table-backed sites by swapping .All() for
.Snapshot() (same package, sorted by the table's own key, already the
established idiom in this repo for exactly this purpose). Fixed the 2
raw-map sites with an explicit sort.Slice by account ID / resource Id.
Fixed filterOrAll the same way (.Snapshot()). Fixed sortFindings by
removing the empty-criteria early return and adding a final deterministic
tiebreak (ProductArn|Id, both ASFF-required fields) that always runs,
whether or not the caller supplied real sort criteria — this also make the
existing sort well-defined on ties within real criteria, which previously
had no tiebreak either.
Safe-by-construction pattern applied throughout: default a miss/no-sort
case to a genuinely sorted read (Table.Snapshot(), or an explicit
sort.Slice for the two raw-map sites) — the same pattern already used
correctly elsewhere in this repo. No threshold-search or found-flag pattern
was applicable here since none of these sites use an equality-matched
cursor (offset tokens throughout).
7 checks run against paginateSlice directly (all pass, both before and
after — it was never the bug) plus a stale-cursor probe on filterOrAll and
a tied-order probe on sortFindings, both of which failed against the
pre-fix code and pass post-fix. 10 end-to-end boundary-walk regression tests
drive the real exported backend methods (23 items, page size 5, non-dividing
count) for a representative sample: ListAggregatorsV2,
ListAutomationRulesV2, ListFindingAggregators,
ListConfigurationPolicies, ListMembers, ListOrganizationAdminAccounts,
ListConnectorsV2, ListConnectors, DescribeActionTargets, GetFindings
(no SortCriteria). ListInvitations, ListConfigurationPolicyAssociations,
and GetResourcesV2 got the identical, already-proven .Snapshot()/explicit-sort
fix but no bespoke end-to-end test — lower priority given the pattern was
independently verified nine other times in this same sweep; flagged here for
anyone auditing this note.
New tests: services/securityhub/pagination_arithmetic_test.go (internal,
unexported-helper unit tests), services/securityhub/pagination_arithmetic_e2e_test.go
(external, real-API boundary walks).
Gates: go build ./services/securityhub/... (clean), go vet ./services/securityhub/... (clean, no signature changes), go test -race -count=1 ./services/securityhub/... (pass). Work left uncommitted per this
pass's instructions.
2026-08-30 (negative-continuation-token sweep): store.go's decodeToken used a bare
fmt.Sscanf(token, "%d", &offset) with no bounds check at all; paginateSlice's start >= len(results) guard does not catch a negative start, so results[start:end] panicked given
"-5" as a NextToken, across all 15 call sites (action_targets.go, aggregators_v2.go,
connectors.go, finding_aggregators.go, configuration_policies.go x2, connectors_v2.go,
automation_rules.go, findings.go, findings_v2.go, invitations.go, resources_v2.go,
organizations.go, members.go, standards.go). Fixed at the decode site: decodeToken now
returns 0 for a negative offset, so all 15 callers inherit the fix. The existing
TestPaginateSlice_SevenChecks table in pagination_arithmetic_test.go exercised stale/
past-end/malformed-non-numeric tokens but never a negative one.
Proof: the added negative offset token subtest of TestPaginateSlice_SevenChecks
(pagination_arithmetic_test.go) confirmed panicking pre-fix, passes now. Gates: go build ./services/securityhub/..., go vet ./services/securityhub/..., go test -race -count=1 ./services/securityhub/..., golangci-lint run ./services/securityhub/... (0 issues). Work
left uncommitted per this pass's instructions.
2026-08-30 (gopherstack-r3pr fabricated-error-code re-audit, no code change):
store.go:31's errCodeInvalidInput ("InvalidInput") re-checked against
cmd/errcodeaudit. All three call sites (standards.go:95,149,
findings.go:458) set it as a free-form ErrorCode map value inside a
Failures/UnprocessedFindings array on an ordinary 200 response
(BatchEnableStandards/BatchDisableStandards/BatchUpdateFindings), never
as an HTTP error envelope's __type — same shape as the already-known
false-positive class (glue/macie2/ce/xray free-form success-response
ErrorCode fields), confirmed not a wire-error-envelope bug. Aside, not
fixed here (out of scope for this class): the SDK doc comment on
BatchUpdateFindingsUnprocessedFinding.Code (types.go) lists
FindingNotFound as the specific documented value for the not-found case
findings.go:458 covers, which differs from the InvalidInput used there —
a real inaccuracy, but a different bug class with no errors.As ground
truth, deliberately not chased this pass per campaign scope.
2026-08-30 (gopherstack-uox6 value-semantics sweep, one bug fixed):
Audited every finding filter/matcher in this service against its SDK doc
comment (V1 matchesFindingFilters/matchesStringFilter/compareStringFilter
in findings.go; V2's matchesFindingFiltersV2/matchesCompositeFilter*/
matchesOcsf*Filter family in findings_v2.go; filterOrAll in store.go).
Bug found and fixed: matchesStringFilter (findings.go) combined every
entry of a field's []StringFilter list with a strict AND. types.StringFilter's
doc comment (securityhub@v1.75.4 types.go:19655) documents the opposite for
same-field entries: CONTAINS/EQUALS/PREFIX are joined by OR ("a finding
matches if it matches any one of those filters" — the doc's own worked
example is Title CONTAINS CloudFront OR Title CONTAINS CloudWatch),
NOT_CONTAINS/NOT_EQUALS/PREFIX_NOT_EQUALS are joined by AND, and the two
groups then combine by AND ("Security Hub CSPM first processes the PREFIX
filters, and then the NOT_EQUALS ... filters" — the doc's second worked
example, ResourceType PREFIX AwsIam + PREFIX AwsEc2 +
NOT_EQUALS AwsIamPolicy + NOT_EQUALS AwsEc2NetworkInterface). Under the
old AND-everything code, either worked example returned zero results against
a real matching finding: an under-match, invisible to any shape-based sweep
since the field is read and the comparator values are legal enum members —
only the combination across multiple entries was wrong. Affects GetFindings
and, via the shared matchesFindingFilters, BatchUpdateFindings.
No prior test passed a multi-entry filter on the same field (existing
TestBackend_MatchesStringFilter/TestGetFindings_FiltersApplied cases all
use exactly one StringFilter entry per field), so the bug was invisible to
the existing suite — "a filter test passing a single value cannot see a
multi-value bug."
Fixed by splitting entries into positive/negative groups (isNegativeStringComparison)
and combining !hasPositive || positiveMatched (OR over positives, defaulting
to "no restriction" when there are none) AND'd with every negative entry
passing. Both of the SDK doc's own worked examples now pass as tests.
Also checked and confirmed correct: matchesFindingFiltersV2's composite
AND/OR (CompositeOperator) and matchesCompositeFilterDepth's per-filter
Operator (both against types.OcsfFindingFilters/types.CompositeFilter's
doc comments, matched field-for-field: NestedCompositeFilters three-layer
structure, AllowedOperators AND/OR with no NOT combinator since negation is
expressed at the leaf comparator); matchesOcsfNumberFilter's
Eq/Gt/Gte/Lt/Lte against types.NumberFilter; matchesDateRange's
WITHIN/OLDER_THAN against types.DateRange (default WITHIN); compareMapFilter's
EQUALS/NOT_EQUALS/CONTAINS/NOT_CONTAINS against types.MapFilter; ipInCIDR's
bare-address-normalizes-to-/32-or-/128 against types.IpFilter's documented
"CIDR block or IP address" acceptance; matchesWholeWord's word-boundary
regex for CONTAINS_WORD (documented V2-only); the lifecycle-rule-style AND
combination across different filter fields in both V1 and V2 (correct in
both — the bug was specifically the same-field, multi-entry case).
One gap recorded, not fixed: whether OcsfMapFilter's same-field
CONTAINS/EQUALS-OR / NOT_CONTAINS/NOT_EQUALS-AND rule (documented on the
shared MapFilter type) still applies underneath a CompositeFilter's
explicit Operator, or is superseded by it, is not stated by either doc
comment — left open rather than guessed (see gaps).
GetResourcesV2's filters parameter (resources_v2.go) is read nowhere
(//nolint:revive // existing issue already marks it) and GetInsightResults
never evaluates insight.Filters at all (documented in-code: "no real
aggregation in mock") — both are pre-existing, already-flagged completeness
gaps (an unread field, not a wrong algorithm on a read one), not new findings
of this class, so left as-is.
No AWS web pages were fetched this pass — every comparator/operator set
needed was fully specified in the pinned SDK's Go doc comments
(securityhub@v1.75.4), unlike the SNS/EventBridge instances of this bug
class from the prior pass.
Tests: added TestGetFindings_MultiValueSameFieldCombination (2 subtests,
findings_test.go) driving the real filter shape through the HTTP handler
end to end and asserting the exact ID set returned (not just a count), using
the SDK doc's own two worked examples. Both subtests confirmed failing
(0 results each) against the unmodified matchesStringFilter before the fix,
passing after. No existing test was weakened; assertion count increased by 2
new subtests, 0 removed.
Gates: go build ./services/securityhub/..., go vet ./services/securityhub/...,
go test -race -count=1 ./services/securityhub/..., golangci-lint run ./services/securityhub/.... Work left uncommitted per this pass's
instructions.