Skip to content

Latest commit

 

History

History
668 lines (621 loc) · 59.9 KB

File metadata and controls

668 lines (621 loc) · 59.9 KB
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
EnableSecurityHub DisableSecurityHub DescribeHub UpdateSecurityHubConfiguration GetFindings BatchImportFindings BatchUpdateFindings UpdateFindings GetFindingHistory CreateInsight GetInsights GetInsightResults UpdateInsight DeleteInsight BatchEnableStandards BatchDisableStandards GetEnabledStandards DescribeStandards DescribeStandardsControls UpdateStandardsControl ListStandardsControlAssociations BatchGetStandardsControlAssociations BatchUpdateStandardsControlAssociations CreateActionTarget DescribeActionTargets UpdateActionTarget DeleteActionTarget DescribeProducts ListEnabledProductsForImport EnableImportFindingsForProduct DisableImportFindingsForProduct GetSecurityControlDefinition ListSecurityControlDefinitions BatchGetSecurityControls UpdateSecurityControl ListAutomationRules CreateAutomationRule BatchGetAutomationRules BatchDeleteAutomationRules BatchUpdateAutomationRules ListTagsForResource TagResource UntagResource CreateMembers DeleteMembers GetMembers InviteMembers ListMembers DisassociateMembers AcceptAdministratorInvitation AcceptInvitation DeclineInvitations DeleteInvitations GetInvitationsCount ListInvitations GetAdministratorAccount GetMasterAccount DisassociateFromAdministratorAccount DisassociateFromMasterAccount DescribeOrganizationConfiguration UpdateOrganizationConfiguration EnableOrganizationAdminAccount DisableOrganizationAdminAccount ListOrganizationAdminAccounts CreateFindingAggregator GetFindingAggregator ListFindingAggregators UpdateFindingAggregator DeleteFindingAggregator CreateConfigurationPolicy GetConfigurationPolicy UpdateConfigurationPolicy DeleteConfigurationPolicy ListConfigurationPolicies GetConfigurationPolicyAssociation ListConfigurationPolicyAssociations StartConfigurationPolicyAssociation StartConfigurationPolicyDisassociation BatchGetConfigurationPolicyAssociations EnableSecurityHubV2 DisableSecurityHubV2 DescribeSecurityHubV2 EnableSecurityHubFeatureV2 DisableSecurityHubFeatureV2 CreateAggregatorV2 GetAggregatorV2 ListAggregatorsV2 UpdateAggregatorV2 DeleteAggregatorV2 CreateAutomationRuleV2 GetAutomationRuleV2 ListAutomationRulesV2 UpdateAutomationRuleV2 DeleteAutomationRuleV2 CreateConnectorV2 GetConnectorV2 ListConnectorsV2 UpdateConnectorV2 DeleteConnectorV2 RegisterConnectorV2 CreateTicketV2 GetFindingsV2 BatchUpdateFindingsV2 GetFindingStatisticsV2 GetFindingsTrendsV2 GetResourcesV2 GetResourcesStatisticsV2 GetResourcesTrendsV2 DescribeProductsV2 GenerateRecommendedPolicyV2 GetRecommendedPolicyV2 CreateConnector GetConnector UpdateConnector DeleteConnector ListConnectors
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass -- SortCriteria is now applied (sortFindings), see Notes
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass -- re-import now preserves Note/UserDefinedFields/VerificationState/Workflow per AWS's documented semantics, see Notes
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
fixed
ok
fixed
ok
FIXED this pass -- BatchImportFindings/BatchUpdateFindings/UpdateFindings now record real FindingHistoryRecord entries (findingHistory map, snapshot-persisted); GetFindingHistory returns them filtered by StartTime/EndTime and paginated. See Notes.
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
ok
ok
ok
ok
ResultValues always empty (no real aggregation) -- acceptable mock behavior, not a stub since Insight itself is real
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
ok
ok
fixed
ok
gopherstack-muzq (2026-08-21): stamped StandardsStatus PENDING and nothing else in this backend ever advanced it -- EnableHub's own default-standards subscriptions are stamped the terminal READY directly at creation (no async work modeled for those either), which is exactly the sibling-resource contrast this bug class hides behind. Confirmed no async mechanism anywhere in the package (no ticker/goroutine/janitor/work.After/runDelayed/reconciler; grepped all non-test .go files). BatchDisableStandards's DELETING stamp is NOT this bug: it deletes the record synchronously and returns the transitional value on the removed copy, so a later GetEnabledStandards correctly omits it -- the ephemeral-response-literal shape, not a stall. Fixed via GetEnabledStandards, see below.
wire errors state persist note
ok
ok
ok
ok
DELETING is an ephemeral response literal returned after a synchronous delete (record is removed from the table in the same call) -- a later GetEnabledStandards correctly no longer returns it. Not the gopherstack-muzq stall pattern; left as-is.
wire errors state persist note
ok
ok
fixed
ok
gopherstack-muzq (2026-08-21): now advances any PENDING subscription to READY on first poll (new unexported StandardsSubscription.pollCount field), mirroring the reap-on-read pattern services/omics uses for Get*-advances-Creating resources -- no generated Get*Waiter ships for this op in this SDK version, but that only means a real caller must hand-roll its own poll loop, not that an unadvancing status is correct. TestBatchEnableStandardsPath (standards_test.go) previously asserted only the initial PENDING status and stopped; strengthened with a GetEnabledStandards follow-up asserting READY. New real-SDK-client proof: TestBatchEnableStandards_ReachesReady (wire_field_fixes_test.go). Hand-reverted standards.go+models.go to git show HEAD, confirmed both tests fail with StandardsStatus stuck at PENDING, restored, md5sum byte-identical.
wire errors state persist note
ok
ok
ok
n/a
static known-standards catalog, matches AWS ARNs/names
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
n/a
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
ok
ok
ok
n/a
static known-products catalog
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
ok
ok
ok
n/a
static known-controls catalog
wire errors state persist
ok
ok
ok
n/a
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
ok
ok
fixed
ok
FIXED this pass -- see Notes
wire errors state persist note
ok
ok
ok
ok
onlyAssociated=true filters on MemberStatus==Enabled, which no code path ever sets (member acceptance is a cross-account operation this single-account backend can't model) -- see gaps
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
n/a
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass -- see Notes
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass -- see Notes
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass -- see Notes
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass -- see Notes (parity-4): real DescribeSecurityHubV2Output is {Features, HubV2Arn, SubscribedAt}, not {HubV2Arn, CreatedAt, UpdatedAt} as previously returned; now also reports the Features map (new in v1.75.0).
wire errors state persist note
ok
ok
ok
ok
NEW op (parity-4). Gated on SecurityHub V2 being enabled (matches the real API's documented "the service must be enabled before you can enable a feature"); features live in HubV2.Features (map[string]*HubV2Feature), so they persist/reset with the V2 hub itself -- no separate state. Idempotent: re-enabling an already-ENABLED feature is a no-op.
wire errors state persist note
ok
ok
ok
ok
NEW op (parity-4). Same gating as EnableSecurityHubFeatureV2. Idempotent: disabling a never-enabled or already-DISABLED feature is a no-op that leaves the Features map unchanged (matches the real API's documented no-op semantics rather than fabricating a DISABLED entry for a feature never touched).
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
ok
ok
fixed
ok
FIXED this pass -- see Notes
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass (gopherstack-jo2r) -- handler dropped required Health/LastUpdatedAt/ProviderDetail entirely and emitted a fabricated "UpdatedAt" key where the real required key is "LastUpdatedAt" (securityhub@v1.75.4 api_op_GetConnectorV2.go:39-79), so a real client decoded a zero-value output. Now uses a dedicated connectorV2ToGetResponse mirroring the V1 CSPM connectorToGetResponse shape: Health.ConnectorStatus/LastCheckedAt and LastUpdatedAt all reuse ConnectorV2.UpdatedAt (this backend tracks one timestamp, not separate health-check/update times); ProviderDetail echoes Provider verbatim since ProviderConfiguration and ProviderDetail share the same union member tags (Azure/JiraCloud/ServiceNow).
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist
ok
ok
ok
ok
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass (gopherstack-4ggy) -- handler read a fabricated body["ConnectorId"]; the real RegisterConnectorV2Input carries only AuthCode and AuthState, no ConnectorId at all (securityhub@v1.75.4 api_op_RegisterConnectorV2.go:26-40). AuthState's content is opaque to any real client (minted server-side, only round-tripped verbatim); this backend's documented convention is that AuthState IS the connector ID it was minted for. AuthCode is required and validated but not persisted -- no ConnectorV2 field or RegisterConnectorV2Output member models it.
wire errors state persist note
fixed
ok
fixed
ok
FIXED this pass (gopherstack-u90v) -- handler previously read a fabricated TicketConfiguration/Tags request shape and returned a fabricated TicketConfigurationArn, neither of which exist on the real wire (securityhub@v1.75.4 api_op_CreateTicketV2.go:31-63: Input is ConnectorId/FindingMetadataUid[required]/ClientToken/Mode, Output is TicketId[required]/TicketSrcUrl). Now requires ConnectorId+FindingMetadataUid (400 ValidationException if absent), validates ConnectorId against the ConnectorV2 store (404 ResourceNotFoundException if unknown), rejects any Mode other than DRYRUN, and returns a generated TicketId. TicketSrcUrl is modeled but left permanently empty -- this backend has no real ITSM integration to source a URL from. FindingMetadataUid is required and stored but not validated against a real finding, matching BatchUpdateFindingsV2's documented metadataUids gap (no OCSF ingestion path hands out real metadata.uid values here). real SDK exposes only Create for TicketV2 -- no Get/List/Update/Delete to implement.
wire errors state persist note
partial
ok
ok
ok
FIXED this pass (gopherstack-8j08) -- DateFilters/MapFilters/IpFilters/BooleanFilters/NestedCompositeFilters, previously accepted on the wire and silently ignored (worse than unsupported: a caller got zero errors and unfiltered results), are now evaluated for the field subset genuinely backed by ASFF data this store carries. NestedCompositeFilters recurses fully (AND/OR, depth-capped) rather than being half-evaluated. See Notes for the full field-by-field crosswalk and what remains unmapped (documented, not fabricated).
wire errors state persist note
fixed
ok
fixed
ok
FIXED this pass -- request now parses the real flat wire shape (Comment/SeverityId/StatusId/FindingIdentifiers/MetadataUids, not the nonexistent "FindingFieldsUpdate" wrapper); FindingIdentifiers now resolve via CloudAccountUid/FindingInfoUid/MetadataProductUid mapped onto the stored finding's AwsAccountId/Id/ProductArn. MetadataUids entries always report ResourceNotFoundException (documented gap -- this mock has no OCSF ingestion path that would ever hand a caller a real metadata.uid). See Notes.
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass (gopherstack-4ggy request side, gopherstack-jo2r sweep response side) -- request read a fabricated body["GroupByAttributes"] ([]string); the real required input member is GroupByRules ([]types.GroupByRule, {GroupByField,Filters} objects -- types.go:15710-15722). Response emitted "FindingStatistics"; the real key is "GroupByResults" ([]types.GroupByResult: GroupByField+GroupByValues[{FieldValue,Count}] -- types.go:15698-15707). GroupByField now echoes the client's requested OCSF name verbatim (e.g. "severity") while lookups translate it via the existing ocsfStringFieldMap onto the backend's ASFF storage key (e.g. "SeverityLabel"). Per-rule Filters accepted but not applied (matches GetResourcesV2's pre-existing filters-ignored convention).
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass (gopherstack-jo2r) -- handler emitted "FindingsTrends" and dropped required Granularity/TrendsMetrics (securityhub@v1.75.4 api_op_GetFindingsTrendsV2.go:22-58); the backend already computed real trend data, just under the wrong key. Also found by reading the full real input: GetFindingsTrendsV2Input has no GroupByAttribute member at all, so that request-side read (always empty against a real client) was removed. Now returns one TrendsMetricsResult (Timestamp+TrendsValues.SeverityTrends) aggregating every stored finding's ASFF SeverityLabel into the 8 real bucket names; ASFF has no FATAL severity so that bucket is always 0 (documented, not fabricated). Granularity is derived from the requested time span via a documented heuristic, since the real API takes no Granularity input either -- it derives it server-side.
wire errors state persist note
ok
ok
ok
ok
resources derived live from V1 findings' Resources arrays -- reasonable given no separate resource ingestion API exists
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass (gopherstack-4ggy request side, gopherstack-jo2r sweep response side) -- request read a fabricated body["GroupByAttributes"]; real required input member is GroupByRules ([]types.ResourceGroupByRule -- types.go:17851-17862). Response emitted "ResourceStatistics"; real required key is "GroupByResults" (types.go:15698-15707). Same GroupByField-echo/lookup-translation shape as GetFindingStatisticsV2, but no OCSF->internal field map exists for resources (GetResourcesV2 has never honored Filters either), so lookups use the client's field name verbatim against this backend's ASFF Resource keys.
wire errors state persist note
fixed
ok
ok
ok
FIXED this pass (gopherstack-jo2r) -- handler emitted "ResourcesTrends" and dropped required Granularity/TrendsMetrics (securityhub@v1.75.4 api_op_GetResourcesTrendsV2.go:22-58). Also found by reading the full real input: GetResourcesTrendsV2Input has no GroupByAttribute member either, so that request-side read was removed. Now returns one ResourcesTrendsMetricsResult (Timestamp+TrendsValues.ResourcesCount.AllResources); Granularity uses the same time-span heuristic as GetFindingsTrendsV2.
wire errors state persist note
fixed
ok
ok
n/a
FIXED this pass (gopherstack-jo2r) -- handler emitted "Products"; the real required key is "ProductsV2" ([]types.ProductV2 -- api_op_DescribeProductsV2.go:36-51), so a real client decoded a nil slice regardless of catalog content. Also renamed the per-item fields ProductV2 actually has (ProductV2Name, IntegrationV2Types) and dropped ProductArn, which ProductV2 has no member for at all (types.go:17113-17141); MarketplaceProductId left absent, no backing field on the shared Product model.
wire errors state persist note
fixed
ok
ok
ok
gopherstack-tp8x (2026-08-21): returned {MetadataUid,Policy,GenerationTime}, a fabricated shape. GenerateRecommendedPolicyV2Output (securityhub@v1.75.4 api_op_GenerateRecommendedPolicyV2.go) has NO members at all besides ResultMetadata -- it only starts async generation. Fixed to return {}. NOTE: the y1zn filing's claim that this op 'is not a real operation at all' and the POST route is 'unreachable by any real client' is wrong -- verified against awsRestjson1_serializeOpGenerateRecommendedPolicyV2 (serializers.go), which sends POST /recommendedPolicyV2/{MetadataUid}, exactly this handler's route. A prior 'confirmed' verdict is not evidence; re-verify against the serializer before trusting a rejection note. Locked by TestGenerateGetRecommendedPolicyV2_RealClient.
wire errors state persist note
fixed
ok
ok
ok
gopherstack-tp8x (2026-08-21): returned {MetadataUid,Policy,GenerationTime}; real GetRecommendedPolicyV2Output is an async-retrieval-status shape (Status/RecommendationType/ResourceArn/RecommendationSteps/Error/NextToken, deserializers.go), not a returned policy document. RecommendationSteps is a union tagged "UnusedPermissions" (types.RecommendationStepMemberUnusedPermissions), each step carrying RecommendedAction/RecommendedPolicy/ExistingPolicy/ExistingPolicyId/PolicyUpdatedAt (types.UnusedPermissionsRecommendationStep). This backend generates synchronously so Status is always SUCCEEDED and Error/NextToken are never populated; ResourceArn is omitted (not tracked -- no Finding-to-metadataUid linkage exists in this backend, honest gap rather than a fabricated ARN). Locked by TestGenerateGetRecommendedPolicyV2_RealClient (real-client decode of the RecommendationStepMemberUnusedPermissions union member).
wire errors state persist note
ok
ok
ok
ok
NEW op (parity-4). Field-diffed against api_op_CreateConnector.go + types.CspmProviderConfiguration/AzureProviderConfiguration. Required Name/Provider return 400 if missing (client-side validation middleware in the real SDK, modeled defensively here). See gaps for the connector-status lifecycle limitation.
wire errors state persist note
ok
ok
ok
ok
NEW op (parity-4). Field-diffed against api_op_GetConnector.go + types.CspmHealthCheck/CspmProviderDetail/AzureDetail. Response nests health under Health{ConnectorStatus,LastCheckedAt,Message,Issues} and provider detail under ProviderDetail{Azure:{...}}, matching the real tagged-union wire shape exactly (input Provider and output ProviderDetail share the same {"Azure": AzureDetail} shape, confirmed via serializers.go/deserializers.go).
wire errors state persist note
ok
ok
ok
ok
NEW op (parity-4). Field-diffed against api_op_UpdateConnector.go + types.AzureUpdateConfiguration -- UpdateConnectorInput has NO Name field (only ConnectorId/Description/Provider), and AzureUpdateConfiguration has no AWSConfigConnectorArn (immutable after create); both are honored here (Name update silently ignored per the real shape's absence of the field; AWSConfigConnectorArn merged forward from the original CreateConnector call rather than dropped).
wire errors state persist note
ok
ok
ok
ok
NEW op (parity-4). Field-diffed against api_op_DeleteConnector.go -- output is EnablementStatus only (PENDING_DELETION), no ConnectorId/Arn. This mock removes the record synchronously (no background worker to model AWS's async teardown window) but still reports PENDING_DELETION on the delete response itself for wire fidelity.
wire errors state persist note
ok
ok
ok
ok
NEW op (parity-4). Field-diffed against api_op_ListConnectors.go + types.CspmConnectorSummary/CspmProviderSummary -- ConnectorStatus/EnablementStatus/ProviderName filters are query params (SetQuery, confirmed via serializers.go), not a JSON body, applied as exact-match filters against the stored connector fields.
families
RouteMatcher Persistence
status note
ok
every classifyPath prefix cross-checked against real serializers.go SetURI paths for all ~105 ops in aws-sdk-go-v2 v1.71.2 as of the last full audit; the parity-4 pass additionally verified the 7 new ops' SetURI paths (/connectors, /connectors/{ConnectorId+}, /hubv2/feature/{FeatureName}) against v1.75.0 and confirmed pathClassifiers orders the /connectorsv2 prefix before the new plain /connectors prefix (same ordering trap as /automationrulesv2 vs /automationrules -- see Traps below). RouteMatcher's unambiguous-prefix list covers every prefix classifyPath switches on; /findings and /tags/{arn} correctly disambiguated (Authorization signing-service header / ARN service segment) from other services sharing those prefixes (e.g. Macie2). No unreachable-op bugs found.
status note
ok
Handler.Snapshot/Restore (persistence.go) delegate to InMemoryBackend.Snapshot/Restore (backend.go), which round-trips every store.Table via registry.SnapshotAll/RestoreAll (store_setup.go) plus the 5 plain-map fields (tags, findings, controlParams, productSubscriptions, orgAdminAccounts) and all scalar/pointer fields. Verified store_setup.go registers exactly the set of *store.Table fields declared on InMemoryBackend -- no orphaned or unregistered table.
gaps
ListMembers(onlyAssociated=true) can never return members: filters on MemberStatus=="Enabled", but nothing transitions a member to Enabled because member-invitation acceptance is a cross-account action this single-account in-memory backend doesn't model (the member's own account would call AcceptInvitation against ITS OWN backend instance, not the administrator's). Not attempted this pass -- architectural, not a bug-fix-sized change; would need a multi-backend cross-account simulation this service doesn't have.
GetFindingsV2 Filters.CompositeFilters evaluates String/Number/Date/Map/Ip/Boolean filters and NestedCompositeFilters (gopherstack-8j08), but only for the field-name subset in ocsfStringFieldMap/ocsfNumberFieldMap/ocsfDateFieldMap/ipFieldNetworkKeys/mapFilterCandidates (findings_v2.go) that has a genuine ASFF-backed equivalent. Any OcsfStringField/OcsfNumberField/OcsfDateField/OcsfMapField/OcsfIpField/OcsfBooleanField outside those mapped subsets is accepted on the wire but not evaluated -- deliberately, per the no-fabrication rule, rather than guessed at. Remaining unmapped, with reasons: (a) fields with no ASFF concept at all -- OcsfBooleanField compliance.assessments.meets_criteria (ASFF Compliance has no 'assessments'), OcsfMapField databucket.tags (ASFF has no databucket concept), most 'evidences.*'/vendor_attributes.*' string+number fields (ASFF has no evidences/vendor_attributes objects); (b) fields whose only ASFF analog is lossy/ambiguous -- OcsfBooleanField vulnerabilities.is_fix_available (ASFF Vulnerability.FixAvailable is three-valued YES/NO/PARTIAL; collapsing PARTIAL into a bool would misclassify findings); (c) fields that exist in ASFF only nested inside arrays this pass didn't reach -- e.g. vulnerabilities.cve.cvss.base_score (Vulnerabilities[].Cvss[].BaseScore), resources.image.*/resources.modified_time_dt (ASFF Resource has no image/per-resource-modified timestamp). class_name (its closest analog, Types, is a string array, not scalar) remains unmapped from the prior pass. A complete OCSF taxonomy crosswalk is ~70 string + ~14 number fields; this pass closed the DateFilters/MapFilters/IpFilters/BooleanFilters/NestedCompositeFilters gap specifically (the issue's stated priority) plus one bonus NumberFilter field (confidence_score -> ASFF Confidence).
BatchUpdateFindingsV2 MetadataUids-based finding identification can never resolve (always ResourceNotFoundException): this backend has no OCSF ingestion path that would ever hand a real client a metadata.uid to reference back. Only FindingIdentifiers (CloudAccountUid/FindingInfoUid/MetadataProductUid, mapped onto AwsAccountId/Id/ProductArn) can resolve a finding.
(parity-4) CSPM Connector health ConnectorStatus can never leave UNKNOWN, and EnablementStatus can never reach ENABLED: unlike Connectors V2 (which has a dedicated RegisterConnectorV2 to complete an out-of-band OAuth handshake), the real CreateConnector/GetConnector/UpdateConnector/DeleteConnector/ListConnectors surface has NO companion 'complete authorization' operation at all -- establishing connectivity to the Azure account requires a purely external, provider-side step (granting the AWSConfigConnectorArn role access in the Azure portal) that this mock has no API-observable signal for. Auto-advancing a connector to CONNECTED/ENABLED without any real client action causing it would be a fabricated transition, so CreateConnector leaves it at PENDING_ENABLEMENT/UNKNOWN and UpdateConnector leaves it at PENDING_UPDATE permanently. Not attempted this pass -- architectural (no out-of-band signal exists to model), not a bug-fix-sized change.
deferred
leaks
status note
clean
no goroutines, tickers, or background loops in services/securityhub -- pure request-response over an in-memory store.Registry guarded by one lockmetrics.RWMutex. New findingHistory map (findings.go/store.go) follows the same plain-map + coarse-lock pattern as findings/tags -- every read/write path holds b.mu for the duration, no separate lock, no goroutines.

Notes

parity-4 pass (2026-07-25): 7 new SDK ops from the v1.71.2 -> v1.75.0 bump

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/DisableSecurityHubFeatureV2 are wired to the existing HubV2 state, 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 as HubV2.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) and DescribeSecurityHubV2 -- the existing op -- now reports them, matching the real DescribeSecurityHubV2Output.Features field that also arrived in this SDK bump.
  • CSPM Connectors' authorization lifecycle is modeled honestly, not auto-completed. Unlike Connectors V2 (which has RegisterConnectorV2 to complete an out-of-band OAuth handshake), the real CSPM Connector surface has no such operation at all -- see the gaps entry above. A connector created via CreateConnector is left at EnablementStatus=PENDING_ENABLEMENT / health ConnectorStatus=UNKNOWN permanently, since no real client action this backend can observe would legitimately advance it further.
  • Bonus fix, found while wiring Features into DescribeSecurityHubV2: its response previously returned invented CreatedAt/UpdatedAt fields; the real DescribeSecurityHubV2Output (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 add Features.

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.

Bugs fixed this pass

  1. handler_configpolicy.go -- ConfigurationPolicyAssociation TargetType always empty. GetConfigurationPolicyAssociation, StartConfigurationPolicyAssociation, and StartConfigurationPolicyDisassociation all read a "TargetType" key out of the request's Target object. The real wire shape (types.Target is a Smithy tagged union -- see serializers.go:34632 awsRestjson1_serializeDocumentTarget) never sends that field; the request is one of {"AccountId":...} / {"OrganizationalUnitId":...} / {"RootId":...} and TargetType (ACCOUNT/ORGANIZATIONAL_UNIT/ROOT) must be derived from which key is present. Every association response's TargetType field was silently empty for every real SDK client. Fixed by adding extractConfigPolicyTarget (derives ID + type from the union) and using it at all three call sites. Covered by TestParity_ConfigurationPolicyAssociation_TargetTypeDerived (parity_d_test.go).

  2. backend_members.go -- InviteMembers never validated the account exists. AWS requires CreateMembers before InviteMembers; inviting an account that was never created must land in UnprocessedAccounts. The previous implementation unconditionally created an Invitation for every account ID with no existence check, so UnprocessedAccounts was always empty regardless of input validity -- a disguised no-op on the validation path. Fixed to check b.members.Get(id) first and populate UnprocessedAccounts (ResourceNotFoundException) for unknown accounts, matching the same pattern already used by DeleteMembers/GetMembers. Covered by TestParity_InviteMembers_UnknownAccountUnprocessed.

  3. backend_v2.go -- UpdateAutomationRuleV2 silently dropped Actions updates. The handler passes the raw decoded JSON request body straight through as updates map[string]any. A JSON array decodes into []any (each element map[string]any), but the backend asserted updates["Actions"].([]map[string]any) directly -- an assertion that can never succeed against []any, so every Actions update was silently dropped while every other field updated fine. Fixed to convert []any -> []map[string]any element-by-element, mirroring the pattern already used correctly in BatchUpdateAutomationRules (V1) and the V2 create handler. Covered by TestParity_UpdateAutomationRuleV2_ActionsApplied.

Bugs fixed this pass (2026-07-23 gaps sweep)

  1. findings.go -- GetFindings/GetFindingsV2 accepted SortCriteria but silently discarded it (results returned in map-iteration order, effectively random). Added sortFindings (stable multi-key sort over types.SortCriterion's Field/SortOrder "asc"/"desc" wire shape), wired into both GetFindings and the new GetFindingsV2. Covered by TestGetFindings_SortCriteria (findings_test.go).

  2. findings.go -- BatchImportFindings re-import overwrote Note/UserDefinedFields/VerificationState/Workflow instead of preserving them. AWS documents ("After a finding is created, BatchImportFindings cannot 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. ImportFindings previously did a flat maps.Copy that let any subsequent import silently reset a customer's investigation Note/Workflow/etc. Fixed with preserveCustomerManagedFields, which restores (or deletes, if never set) these fields from the prior stored version after every re-import. Covered by TestBatchImportFindings_PreservesCustomerManagedFields.

  3. findings.go -- GetFindingHistory was a hardcoded stub returning {Records: []} always; no finding-update history was ever recorded. Added a findingHistory map[string][]map[string]any store field (snapshot-persisted alongside findings, same plain-map pattern) and recordFindingHistory/diffFindingFields helpers. ImportFindings now records a FindingCreated: true entry for new findings and a field-diff entry for re-imports; BatchUpdateFindings and UpdateFindings each record a field-diff entry per mutated finding (excluding the CreatedAt/UpdatedAt/FirstObservedAt/LastObservedAt timestamp fields AWS documents as excluded from history). GetFindingHistory now filters the recorded log by StartTime/EndTime and paginates it (100 per page, matching AWS's documented cap). Covered by TestGetFindingHistory_RecordsChanges and TestGetFindingHistory_UnknownFinding.

  4. handler_findings.go -- BatchUpdateFindingsV2 read a nonexistent "FindingFieldsUpdate" wrapper key. The real BatchUpdateFindingsV2Input wire 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, FindingIdentifiers uses CloudAccountUid/FindingInfoUid/MetadataProductUid (types.OcsfFindingIdentifier), not V1's ProductArn/Id, so even after fixing the wrapper-key bug the old delegation to V1 BatchUpdateFindings could never match a stored finding. Rewrote as a dedicated BatchUpdateFindingsV2 backend method (findings_v2.go) that parses the flat request fields and resolves CloudAccountUid/FindingInfoUid/MetadataProductUid against the stored finding's AwsAccountId/Id/ProductArn -- the only viable mapping since this mock has no separate OCSF ingestion API (findings only ever enter via V1 BatchImportFindings). Covered by TestBatchUpdateFindingsV2_WireShape and TestBatchUpdateFindingsV2_UnmatchedIdentifiers (findings_v2_test.go).

  5. handler_findings.go -- GetFindingsV2 Filters was passed straight to the V1 matchesFindingFilters, which looks for top-level Id/ProductArn/etc. keys. The real GetFindingsV2 Filters wire shape is types.OcsfFindingFilters: {CompositeFilters: [...], CompositeOperator: "AND"|"OR"}, each CompositeFilter holding StringFilters/NumberFilters/etc. keyed by an OCSF field name (types.OcsfStringField/OcsfNumberField) plus its own Operator. None of those keys exist in the V1 filter shape, so every real V2 client's Filters was a complete no-op (matched everything) rather than merely "unsorted" -- worse than the PARITY.md entry previously on file suggested. Added matchesFindingFiltersV2 + matchesCompositeFilter/matchesOcsfStringFilter/matchesOcsfNumberFilter (findings_v2.go), which evaluate the real nested shape against a field-name-mapped subset of the stored ASFF finding (see ocsfStringFieldMap/ocsfNumberFieldMap and the residual-gap entry above). severity_id/status_id NumberFilters round-trip the SeverityId/StatusId fields BatchUpdateFindingsV2 itself writes (fix #7), giving V2 update + V2 filter a coherent, testable round trip. Covered by TestGetFindingsV2_CompositeFilters.

GetFindingsV2 composite filter taxonomy (gopherstack-8j08, this pass)

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: absolute Start/End bounds (matchesDateStartEnd), and relative DateRange{Comparison: WITHIN|OLDER_THAN, Unit: DAYS, Value} (matchesDateRange) -- WITHIN matches at-or-after now - Value days, OLDER_THAN its strict complement. resources.image.*/ resources.modified_time_dt have no ASFF equivalent (ASFF's Resource carries no image/per-resource-modified timestamp) and are unmapped.
  • MapFilters (mapFilterCandidates): resources.tags -> per-resource Resources[].Tags, finding_info.tags -> the finding-level UserDefinedFields map (the closest real ASFF analog to a finding-level "tag"), compliance.control_parameters -> Compliance. SecurityControlParameters[]{Name,Value[]}. All four MapFilterComparison values implemented (EQUALS/NOT_EQUALS/CONTAINS/NOT_CONTAINS) via compareMapFilter, 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.tags has 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, but Network's source/destination IP fields are the only genuinely analogous data this store carries. IpFilter has only a Cidr field (no comparator) -- CIDR containment via net.ParseCIDR/ IPNet.Contains, with a bare IP address normalized to an exact-match /32 or /128 per AWS's documented "CIDR block or single IP" input.
  • BooleanFilters: only vulnerabilities.is_exploit_available is evaluated -- Vulnerability.ExploitAvailable is a genuine two-valued ASFF enum (YES/NO), so it round-trips to bool cleanly; a finding matches if ANY entry in its Vulnerabilities array has a matching value. vulnerabilities.is_fix_available is deliberately NOT evaluated: Vulnerability.FixAvailable is three-valued (YES/NO/PARTIAL), and collapsing PARTIAL into either true or false would silently misclassify findings -- worse than leaving it unfiltered. compliance.assessments.meets_criteria has no ASFF backing at all (no "assessments" concept on Compliance) and is also unmapped.
  • NumberFilters bonus: added confidence_score -> ASFF's own top-level Confidence (int 0-100) to ocsfNumberFieldMap -- 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.

Route-matcher check

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.

Traps for the next auditor

  • /automationrulesv2 is a strings.HasPrefix superset of /automationrules (both share the /automationrules substring) -- classifyPath's switch correctly orders the V2 case before the V1 case. Don't "simplify" that ordering.
  • (parity-4) Same trap, new pair: /connectorsv2 is a strings.HasPrefix superset of the new plain /connectors (CSPM connectors). pathClassifiers in handler.go orders hasPathPrefix(pathConnectorsV2) before hasPathPrefix(pathConnectors) -- don't reorder or collapse them. Also note: CreateConnector/GetConnector/etc. (this pass) and CreateConnectorV2/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 (CspmConnector vs ConnectorV2) and distinct backend/handler files (connectors.go/handler_connectors.go vs connectors_v2.go/handler_connectors_v2.go) specifically to avoid conflating them.
  • classifyConfigPolicyPath's PATCH/DELETE cases match /configurationPolicy/ with explicit exclusions for create/get/list suffixes rather than a positive {Identifier} pattern -- this is intentional (mirrors the real flat-path-segment routing) and correct as long as no real ConfigurationPolicyIdentifier value is literally "create", "get", or "list".
  • BatchUpdateFindings/ImportFindings/GetFindings do not check hubEnabled (unlike UpdateFindings/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.

gopherstack-y1zn (2026-08-21): unknown-key sweep, 3 fixed, 2 deferred

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.

Race-safety sweep (2026-08-22): shallow struct copies and live-pointer returns escaping the lock

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:

  1. 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 (Tags fields are assigned the exact same map object passed to b.tags[ARN] = tags at creation time, and TagResource/UntagResource mutate b.tags[ARN] in place via maps.Copy/delete).
  2. 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 an Update*, or by the same Get-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): Tags aliases b.tags[Arn]; ConfigurationPolicy map cloned too for consistency. CreateConfigurationPolicy also returned the live stored pointer.
  • CspmConnector (connectors.go): Tags aliases b.tags[ConnectorArn] (Provider cloned too). CreateConnector returned the live pointer.
  • ConnectorV2 (connectors_v2.go): same Tags/Provider shape. CreateConnectorV2 returned the live pointer; so did UpdateConnectorV2 and RegisterConnectorV2 before their cp := *target was replaced with .clone().
  • AutomationRule/AutomationRuleV2 (automation_rules.go): BatchGetAutomationRules returned live *AutomationRule pointers with no copy at all -- BatchUpdateAutomationRules mutates RuleName/ RuleStatus/Criteria/Actions/etc. on that same object in place. CreateAutomationRuleV2 likewise returned the live pointer, later mutated by UpdateAutomationRuleV2.
  • StandardsSubscription (standards.go): BatchEnableStandards and BatchDisableStandards returned the live, stored pointer; GetEnabledStandards returns the exact objects it just mutated in place (pollCount, StandardsStatus) with no copy, and those same objects can be mutated again later by BatchDisableStandards.
  • StandardsControl (standards.go): DescribeStandardsControls's override branch (controls[i] = override) assigned the live *StandardsControl stored in b.controlOverrides directly; UpdateStandardsControl mutates an existing override's fields in place.
  • AggregatorV2/FindingAggregator: Regions []string is only ever wholesale-reassigned (never indexed into), so the existing shallow copies on Get/List/Update were already safe -- but CreateAggregatorV2/ CreateFindingAggregator returned the live pointer, later mutated by their respective Update*. Fixed by copying at the Create return only.
  • Member (members.go): CreateMembers appended the live pointer; InviteMembers/DisassociateMembers mutate MemberStatus/InvitedAt on that same object in place. GetMembers/ListMembers already 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 no Update* that ever mutates an existing instance after creation.
  • knownStandards/knownSecurityControls/knownProducts: package-level read-only lookup tables, never mutated after init; every cp := knownX[i] copy is safe regardless of field shape.
  • BatchGetSecurityControls's Parameters field (controls.go): hands out b.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 *StandardsControlAssociation from b.controlAssocOverrides directly, but the only writer (BatchUpdateStandardsControlAssociations) always Puts a brand-new struct rather than mutating an existing one in place.
  • Snapshot (store.go): marshals every live field (including b.tags, b.hubV2, b.controlParams, ...) while still holding RLock for 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.