| 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.