| service | securityhub | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| sdk_module | aws-sdk-go-v2/service/securityhub@v1.75.4 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_audit_commit | b7c35baea | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| last_audit_date | 2026-09-19 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overall | A | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ops |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| families |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| gaps | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| items_still_open |
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deferred | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| leaks |
|
Configuration policy IDs were sequential "policy-N" (fails the provider's UUID validation); StandardsSubscriptionArn/StandardsControlArn used a fabricated shape breaking control lookup.
Checked every op with >=1 SDK-required output member (30 ops, 47 members;
cmd/requiredoutputfields) against handler code, prioritizing idempotent
deletes and V2 CSPM Connector/Ticket paths. All already always-populated —
GetConnector/GetConnectorV2 (Health/ProviderDetail), BatchImportFindings
(SuccessCount/FailedCount), CreateTicketV2 (TicketId), DescribeProducts/
DescribeProductsV2, insight/resource/trend wrappers all set the required
key on every response path, none behind a conditional or omitempty. No
fixes needed.
Worked all 13 tier-1 findings. 4 real fixes: EnableOrganizationAdminAccount/
DisableOrganizationAdminAccount.Feature (undeclared; this backend tracked
one flat, feature-unaware admin-account set -- added orgAdminAccountFeatures map[string]string alongside the existing orgAdminAccounts map so
ListOrganizationAdminAccounts's existing Feature query-param filter,
previously accepted-and-ignored per its own comment, now actually narrows
results, and disabling under the wrong feature is a no-op rather than
removing an admin scoped to a different feature). EnableSecurityHub.ControlFindingGenerator
(undeclared; EnableHub hardcoded "SECURITY_CONTROL" regardless of what
was requested -- now honored, validated, defaults to SECURITY_CONTROL).
GetFindingStatisticsV2.SortOrder (undeclared; GroupByValues were
returned in first-seen order regardless of count, ignoring the documented
"descending is the default" -- groupByResults, shared with
GetResourcesStatisticsV2, now sorts by count; both ops' SortOrder
threaded through, though only GetFindingStatisticsV2.SortOrder was the
flagged tier-1 finding). 5 false positives: CreateAutomationRule.IsTerminal
and UpdateOrganizationConfiguration.AutoEnable/AutoEnableStandards/
UpdateSecurityHubConfiguration.AutoEnableControls were all already read by
name. BatchUpdateFindings.VerificationState is a different, notable
blind-spot shape: handleBatchUpdateFindings (handler_findings.go) collects
every body field except FindingIdentifiers into a generic updates map
applied wholesale via maps.Copy onto the stored ASFF finding
(findings.go:510) -- the field is genuinely honored (confirmed both by a
pre-existing test, TestBatchImportFindings_PreservesCustomerManagedFields,
and a new real-SDK-client test), but no per-field declaration exists
anywhere for the tool to find. 4 recorded gaps (see items_still_open):
GetFindingsV2/GetFindingStatisticsV2/GetResourcesV2/
GetResourcesStatisticsV2.Scopes (AwsOrganizations-OU filtering; this
backend has no organizational-unit tree to filter against). Proven via
realclient_control_finding_generator_and_misc_fields_test.go driving the real securityhub client.
go build/vet/test -race, golangci-lint, and cmd/paritylint all clean;
no persistence-schema version bump (new orgAdminAccountFeatures field is
additive with omitempty, old fields unchanged; 1 inventory row added by
hand: snapshot.OrgAdminAccountFeatures).
The Go SDK module was bumped, revealing 7 operations added to
aws-sdk-go-v2/service/securityhub since the previous audit: CreateConnector,
GetConnector, UpdateConnector, DeleteConnector, ListConnectors (a new
CSPM third-party cloud-provider connector family -- see the "Traps" note
above for why this is not the same as the existing ConnectorV2 family),
and EnableSecurityHubFeatureV2/DisableSecurityHubFeatureV2 (opt-in feature
toggles scoped to the existing SecurityHub V2 hub state). All 7 were
implemented for real (routing, backend state, request parsing, response wire
shapes field-diffed against the SDK's own types/serializers.go/
deserializers.go, error codes, HTTP status, Snapshot/Restore persistence)
and added to GetSupportedOperations() -- none went into the
TestSDKCompleteness notImplemented list (which stayed empty).
Key design decisions:
EnableSecurityHubFeatureV2/DisableSecurityHubFeatureV2are wired to the existingHubV2state, not an orphan boolean. The real API's/hubv2/feature/{FeatureName}path and its documented "the service must be enabled before you can enable a feature" precondition both point at the existing V2 hub. Features are stored asHubV2.Features map[string]*HubV2Feature(new field on the existing struct) rather than a separate backend field, so they persist/reset with the V2 hub's own lifecycle for free (no new Snapshot/Restore wiring needed) andDescribeSecurityHubV2-- the existing op -- now reports them, matching the realDescribeSecurityHubV2Output.Featuresfield that also arrived in this SDK bump.- CSPM Connectors' authorization lifecycle is modeled honestly, not
auto-completed. Unlike Connectors V2 (which has
RegisterConnectorV2to complete an out-of-band OAuth handshake), the real CSPM Connector surface has no such operation at all -- see thegapsentry above. A connector created viaCreateConnectoris left atEnablementStatus=PENDING_ENABLEMENT/ healthConnectorStatus=UNKNOWNpermanently, since no real client action this backend can observe would legitimately advance it further. - Bonus fix, found while wiring
FeaturesintoDescribeSecurityHubV2: its response previously returned inventedCreatedAt/UpdatedAtfields; the realDescribeSecurityHubV2Output(confirmed in both v1.71.2 and v1.75.0, so this predates the SDK bump) is{Features, HubV2Arn, SubscribedAt}. Fixed in the same handler function this pass touched anyway to addFeatures.
Fresh audit (this service had no PARITY.md before the 2026-07-23 pass). Persistence (Handler.Snapshot/Restore delegating to InMemoryBackend) was added recently and verified intact -- no changes needed there.
-
handler_configpolicy.go-- ConfigurationPolicyAssociationTargetTypealways empty.GetConfigurationPolicyAssociation,StartConfigurationPolicyAssociation, andStartConfigurationPolicyDisassociationall read a"TargetType"key out of the request'sTargetobject. The real wire shape (types.Targetis a Smithy tagged union -- seeserializers.go:34632 awsRestjson1_serializeDocumentTarget) never sends that field; the request is one of{"AccountId":...}/{"OrganizationalUnitId":...}/{"RootId":...}andTargetType(ACCOUNT/ORGANIZATIONAL_UNIT/ROOT) must be derived from which key is present. Every association response'sTargetTypefield was silently empty for every real SDK client. Fixed by addingextractConfigPolicyTarget(derives ID + type from the union) and using it at all three call sites. Covered byTestParity_ConfigurationPolicyAssociation_TargetTypeDerived(parity_d_test.go). -
backend_members.go--InviteMembersnever validated the account exists. AWS requiresCreateMembersbeforeInviteMembers; inviting an account that was never created must land inUnprocessedAccounts. The previous implementation unconditionally created anInvitationfor every account ID with no existence check, soUnprocessedAccountswas always empty regardless of input validity -- a disguised no-op on the validation path. Fixed to checkb.members.Get(id)first and populateUnprocessedAccounts(ResourceNotFoundException) for unknown accounts, matching the same pattern already used byDeleteMembers/GetMembers. Covered byTestParity_InviteMembers_UnknownAccountUnprocessed. -
backend_v2.go--UpdateAutomationRuleV2silently droppedActionsupdates. The handler passes the raw decoded JSON request body straight through asupdates map[string]any. A JSON array decodes into[]any(each elementmap[string]any), but the backend assertedupdates["Actions"].([]map[string]any)directly -- an assertion that can never succeed against[]any, so everyActionsupdate was silently dropped while every other field updated fine. Fixed to convert[]any->[]map[string]anyelement-by-element, mirroring the pattern already used correctly inBatchUpdateAutomationRules(V1) and the V2 create handler. Covered byTestParity_UpdateAutomationRuleV2_ActionsApplied.
-
findings.go--GetFindings/GetFindingsV2acceptedSortCriteriabut silently discarded it (results returned in map-iteration order, effectively random). AddedsortFindings(stable multi-key sort overtypes.SortCriterion'sField/SortOrder"asc"/"desc" wire shape), wired into bothGetFindingsand the newGetFindingsV2. Covered byTestGetFindings_SortCriteria(findings_test.go). -
findings.go--BatchImportFindingsre-import overwroteNote/UserDefinedFields/VerificationState/Workflowinstead of preserving them. AWS documents ("After a finding is created,BatchImportFindingscannot be used to update the following finding fields...") that these four fields are retained from the finding's previous version regardless of what a re-import request supplies.ImportFindingspreviously did a flatmaps.Copythat let any subsequent import silently reset a customer's investigation Note/Workflow/etc. Fixed withpreserveCustomerManagedFields, which restores (or deletes, if never set) these fields from the prior stored version after every re-import. Covered byTestBatchImportFindings_PreservesCustomerManagedFields. -
findings.go--GetFindingHistorywas a hardcoded stub returning{Records: []}always; no finding-update history was ever recorded. Added afindingHistory map[string][]map[string]anystore field (snapshot-persisted alongsidefindings, same plain-map pattern) andrecordFindingHistory/diffFindingFieldshelpers.ImportFindingsnow records aFindingCreated: trueentry for new findings and a field-diff entry for re-imports;BatchUpdateFindingsandUpdateFindingseach record a field-diff entry per mutated finding (excluding theCreatedAt/UpdatedAt/FirstObservedAt/LastObservedAttimestamp fields AWS documents as excluded from history).GetFindingHistorynow filters the recorded log byStartTime/EndTimeand paginates it (100 per page, matching AWS's documented cap). Covered byTestGetFindingHistory_RecordsChangesandTestGetFindingHistory_UnknownFinding. -
handler_findings.go--BatchUpdateFindingsV2read a nonexistent"FindingFieldsUpdate"wrapper key. The realBatchUpdateFindingsV2Inputwire shape (aws-sdk-go-v2/service/securityhub/api_op_BatchUpdateFindingsV2.go) is flat:Comment,FindingIdentifiers([]types.OcsfFindingIdentifier),MetadataUids,SeverityId,StatusId-- there is no wrapper object, so every real client request was silently a no-op. Additionally,FindingIdentifiersusesCloudAccountUid/FindingInfoUid/MetadataProductUid(types.OcsfFindingIdentifier), not V1'sProductArn/Id, so even after fixing the wrapper-key bug the old delegation to V1BatchUpdateFindingscould never match a stored finding. Rewrote as a dedicatedBatchUpdateFindingsV2backend method (findings_v2.go) that parses the flat request fields and resolvesCloudAccountUid/FindingInfoUid/MetadataProductUidagainst the stored finding'sAwsAccountId/Id/ProductArn-- the only viable mapping since this mock has no separate OCSF ingestion API (findings only ever enter via V1BatchImportFindings). Covered byTestBatchUpdateFindingsV2_WireShapeandTestBatchUpdateFindingsV2_UnmatchedIdentifiers(findings_v2_test.go). -
handler_findings.go--GetFindingsV2Filterswas passed straight to the V1matchesFindingFilters, which looks for top-levelId/ProductArn/etc. keys. The realGetFindingsV2Filterswire shape istypes.OcsfFindingFilters:{CompositeFilters: [...], CompositeOperator: "AND"|"OR"}, eachCompositeFilterholdingStringFilters/NumberFilters/etc. keyed by an OCSF field name (types.OcsfStringField/OcsfNumberField) plus its ownOperator. None of those keys exist in the V1 filter shape, so every real V2 client'sFilterswas a complete no-op (matched everything) rather than merely "unsorted" -- worse than the PARITY.md entry previously on file suggested. AddedmatchesFindingFiltersV2+matchesCompositeFilter/matchesOcsfStringFilter/matchesOcsfNumberFilter(findings_v2.go), which evaluate the real nested shape against a field-name-mapped subset of the stored ASFF finding (seeocsfStringFieldMap/ocsfNumberFieldMapand the residual-gap entry above).severity_id/status_idNumberFilters round-trip theSeverityId/StatusIdfieldsBatchUpdateFindingsV2itself writes (fix #7), giving V2 update + V2 filter a coherent, testable round trip. Covered byTestGetFindingsV2_CompositeFilters.
The previous pass (fix #8 above) evaluated only StringFilters/NumberFilters
within each CompositeFilter; DateFilters, MapFilters, IpFilters,
BooleanFilters, and NestedCompositeFilters were accepted on the wire and
silently ignored -- worse than an error, since a caller got HTTP 200 and an
unfiltered result set with no indication their filter did nothing. Field-diffed
the full real taxonomy (types.CompositeFilter, types.Ocsf*Filter,
types.Ocsf*Field enums, types.StringFilter/MapFilter/DateFilter/
IpFilter/BooleanFilter/NumberFilter/DateRange,
types.AllowedOperators/StringFilterComparison/MapFilterComparison/
DateRangeComparison/DateRangeUnit) against aws-sdk-go-v2/service/ securityhub@v1.75.0's types/types.go and types/enums.go directly (not
against this handler's own prior output).
Filter types implemented this pass, each restructured into its own small
result-collector (stringFilterResults/numberFilterResults/
dateFilterResults/mapFilterResults/ipFilterResults/
booleanFilterResults/nestedCompositeFilterResults) feeding a single
matchesCompositeFilterDepth combinator (decomposed to keep CodeFactor's
Complex Method check quiet -- no nolint):
- DateFilters (
ocsfDateFieldMap):finding_info.created_time_dt->CreatedAt,finding_info.first_seen_time_dt->FirstObservedAt,finding_info.last_seen_time_dt->LastObservedAt,finding_info.modified_time_dt->UpdatedAt-- all genuine ASFF finding-level timestamps. Both comparator shapes are implemented: absoluteStart/Endbounds (matchesDateStartEnd), and relativeDateRange{Comparison: WITHIN|OLDER_THAN, Unit: DAYS, Value}(matchesDateRange) --WITHINmatches at-or-afternow - Value days,OLDER_THANits strict complement.resources.image.*/resources.modified_time_dthave no ASFF equivalent (ASFF'sResourcecarries no image/per-resource-modified timestamp) and are unmapped. - MapFilters (
mapFilterCandidates):resources.tags-> per-resourceResources[].Tags,finding_info.tags-> the finding-levelUserDefinedFieldsmap (the closest real ASFF analog to a finding-level "tag"),compliance.control_parameters->Compliance. SecurityControlParameters[]{Name,Value[]}. All fourMapFilterComparisonvalues implemented (EQUALS/NOT_EQUALS/CONTAINS/NOT_CONTAINS) viacompareMapFilter, with positive comparisons OR'd and negative ones AND'd across multiple candidate values for the same key (mirrors the documented same-field combination rule).databucket.tagshas no ASFF concept at all and is unmapped. - IpFilters (
ipFieldNetworkKeys):evidences.src_endpoint.ip->Network.SourceIpV4/SourceIpV6,evidences.dst_endpoint.ip->Network.DestinationIpV4/DestinationIpV6-- ASFF has no "evidences" concept, butNetwork's source/destination IP fields are the only genuinely analogous data this store carries.IpFilterhas only aCidrfield (no comparator) -- CIDR containment vianet.ParseCIDR/IPNet.Contains, with a bare IP address normalized to an exact-match/32or/128per AWS's documented "CIDR block or single IP" input. - BooleanFilters: only
vulnerabilities.is_exploit_availableis evaluated --Vulnerability.ExploitAvailableis a genuine two-valued ASFF enum (YES/NO), so it round-trips to bool cleanly; a finding matches if ANY entry in itsVulnerabilitiesarray has a matching value.vulnerabilities.is_fix_availableis deliberately NOT evaluated:Vulnerability.FixAvailableis three-valued (YES/NO/PARTIAL), and collapsingPARTIALinto eithertrueorfalsewould silently misclassify findings -- worse than leaving it unfiltered.compliance.assessments.meets_criteriahas no ASFF backing at all (no "assessments" concept onCompliance) and is also unmapped. - NumberFilters bonus: added
confidence_score-> ASFF's own top-levelConfidence(int 0-100) toocsfNumberFieldMap-- a clean scalar match found while auditing the taxonomy, not part of the original gap list.
NestedCompositeFilters: recurses fully via matchesCompositeFilterDepth
-- each nested CompositeFilter is evaluated as its own sub-tree (including
its own further NestedCompositeFilters) and the resulting bool joins its
parent's result list, combined by the parent's own Operator. This was
chosen over half-evaluating (e.g. only reading direct filters and ignoring
nesting) because a partially-evaluated boolean tree returns wrong
results, not merely unfiltered ones -- see the task's own warning, confirmed
by a regression-style test case
(NestedCompositeFilters_AND_recurses_and_requires_both_branches): a single
finding can't have two different AwsAccountId values, so ANDing two
mutually-exclusive nested branches must match zero findings; before this
fix (NestedCompositeFilters unevaluated -> empty result list -> vacuous
match-all), that same request would have wrongly matched both seeded
findings. Recursion depth is capped at maxNestedCompositeDepth = 5 (AWS
documents the real structure as capped at 3 layers; 5 is a defensive margin
against a pathological/hand-crafted request, not a limit real traffic
should approach). Note types.AllowedOperators has only AND/OR -- there
is no logical NOT combinator in the real API; negation is expressed at the
leaf via NOT_* comparators (NOT_EQUALS/NOT_CONTAINS/
PREFIX_NOT_EQUALS), not a boolean-tree NOT node, so AND/OR recursion is
the complete real semantics.
Comparator verification: StringFilterComparison
(EQUALS/PREFIX/NOT_EQUALS/PREFIX_NOT_EQUALS/CONTAINS/
NOT_CONTAINS/CONTAINS_WORD) was already correctly implemented by
compareStringFilter (reused unchanged) -- confirmed against types.go's
enum values and StringFilter's doc comments describing each comparator's
exact semantics (including the CONTAINS_WORD-only-in-V2-APIs note).
MapFilterComparison (EQUALS/NOT_EQUALS/CONTAINS/NOT_CONTAINS, no
PREFIX variant -- confirmed the enum has no PREFIX member) implemented fresh
in compareMapFilter following the same positive-OR/negative-AND doc
pattern. DateRangeComparison (WITHIN/OLDER_THAN, default WITHIN per
doc) and the fact DateRangeUnit has only DAYS as of this SDK version
were both confirmed directly against enums.go. NumberFilter was
reconfirmed to have no Comparison field at all (Eq/Gt/Gte/Lt/Lte
only) -- unchanged from the prior pass.
Tests: extended TestGetFindingsV2_CompositeFilters (existing table) with
two confidence_score cases, and added a new table test
TestGetFindingsV2_CompositeFilters_DateMapIPBooleanNested covering every
implemented filter type with paired cases that each narrow to exactly one of
two seeded findings with deliberately divergent field values (proving actual
discrimination, not a "matches everything" false pass), plus the
AND/OR nested-recursion pair described above.
Extracted every op's HTTP method + URI template directly from
aws-sdk-go-v2/service/securityhub@v1.71.2/serializers.go
(awsRestjson1_serializeOpHttpBindings* / SplitURI calls) for all ~105
operations and cross-checked against classifyPath's per-family
classify*Path functions in handler.go, handler_members.go,
handler_configpolicy.go, and handler_v2.go. All method+path pairs match.
RouteMatcher (handler.go) was separately checked to confirm every prefix
classifyPath switches on is also covered by RouteMatcher's
unambiguous-prefix OR-chain, so no routed op is reachable by Handler()
directly (bypassing the matcher, as unit tests do) but unreachable through
the real Echo route registration. No route-matcher bugs found in this
service.
/automationrulesv2is astrings.HasPrefixsuperset of/automationrules(both share the/automationrulessubstring) --classifyPath's switch correctly orders the V2 case before the V1 case. Don't "simplify" that ordering.- (parity-4) Same trap, new pair:
/connectorsv2is astrings.HasPrefixsuperset of the new plain/connectors(CSPM connectors).pathClassifiersinhandler.goordershasPathPrefix(pathConnectorsV2)beforehasPathPrefix(pathConnectors)-- don't reorder or collapse them. Also note:CreateConnector/GetConnector/etc. (this pass) andCreateConnectorV2/GetConnectorV2/etc. are two entirely unrelated real AWS features that happen to share the word "connector" -- CSPM connectors link to third-party cloud providers (Azure), Connectors V2 link to third-party ticketing systems (Jira/ServiceNow). Modeled as distinct Go types (CspmConnectorvsConnectorV2) and distinct backend/handler files (connectors.go/handler_connectors.govsconnectors_v2.go/handler_connectors_v2.go) specifically to avoid conflating them. classifyConfigPolicyPath's PATCH/DELETE cases match/configurationPolicy/with explicit exclusions forcreate/get/listsuffixes rather than a positive{Identifier}pattern -- this is intentional (mirrors the real flat-path-segment routing) and correct as long as no realConfigurationPolicyIdentifiervalue is literally"create","get", or"list".BatchUpdateFindings/ImportFindings/GetFindingsdo not checkhubEnabled(unlikeUpdateFindings/insights/action-targets). This was investigated and left as-is: AWS's own docs don't clearly state these ops require the hub to be enabled, and no existing test asserts either behavior, so flipping it risks breaking passing integrations without clear spec backing. Revisit if a concrete AWS error transcript surfaces.
Part of the gopherstack-us9u/g479 map-literal scanner's 526-key unknown-key
bucket triage. Fixed items proven via real aws-sdk-go-v2/service/securityhub
client round trips or raw-body assertion (wire_field_fixes_y1zn_test.go),
hand-reverted, confirmed failing, restored, md5sum-verified byte-identical.
ListAggregatorsV2: {wire: fixed} -- wrapped the list under "Aggregators"; real member (deserializers.go's awsRestjson1_deserializeOpDocumentListAggregatorsV2Output) is "AggregatorsV2".DeclineInvitations/DeleteInvitations: {wire: fixed} -- each emitted an extra "ProcessedAccounts" key alongside the real "UnprocessedAccounts"; neither DeclineInvitationsOutput nor DeleteInvitationsOutput has a ProcessedAccounts member -- success is implied by an account's absence from UnprocessedAccounts, not a separate echo.GenerateRecommendedPolicyV2/GetRecommendedPolicyV2: {wire: fixed, note: "confirmed real bug, then deferred to gopherstack-tp8x, now fixed (2026-08-21) -- see the ops entries above for the full fix. The deferral note's claim that GenerateRecommendedPolicyV2 'is not a real operation at all' was itself wrong (verified: it is real, POST /recommendedPolicyV2/{MetadataUid}, matching this handler's existing route exactly) -- a reminder that a prior pass's rejection reasoning needs re-verification against the serializer, same as any other claim."}
gopherstack-wlo1 (2026-08-22): dispatch-miss error path was the one call site gopherstack-aitg left untyped
gopherstack-aitg (2026-08-11, commit 695aa1c20) added a central error path
(typedErrorResponse, handler.go) and audited every named call site against
securityhub's real per-operation exception lists. handleREST's own
dispatch-miss fallback -- reached when classifyPath returns opUnknown,
i.e. no classify*Path function recognises the request's method+path --
was not one of the sites that pass touched, and unlike every genuinely
ambiguous ErrHubNotEnabled site in this file (each carries a comment
explaining why it's deliberately left unheadered), this one had no such
note. It wrote {"Message": "unknown operation"} with no
X-Amzn-Errortype header and no body code/__type field, so
restjson.GetErrorInfo (aws-sdk-go-v2's
aws/protocol/restjson/decoder_util.go) had nothing to read and the error
deserialized client-side as UnknownError regardless of the underlying
cause.
Reachability: cross-checked every op constant this package wires into
opHandlerGroups()'s dispatch tables (116 distinct map[string]func()
entries) against every api_op_*.go file in the pinned
securityhub@v1.75.4 module (116 real operations) -- exact 1:1 match, zero
missing. So this fallback is structurally unreachable for any
legitimately-constructed SDK request; it can only be reached by rewriting
the request after signing (proven below), the same white-box category as
medialive/mediatailor's analogous fixes in ea67f34cf.
Fixed: handleREST now calls typedErrorResponse(c, http.StatusNotFound, "ResourceNotFoundException", "unknown operation") -- the same helper (and
the same code) GetSecurityControlDefinition's unknown-control path
already uses (handler_error_type_test.go's existing
TestGetSecurityControlDefinition_UnknownControlSurfacesResourceNotFoundException),
so no new exception vocabulary was introduced.
Proof: TestGetInsightResults_UnrecognisedRouteSurfacesResourceNotFoundException
(handler_error_type_test.go) drives a real securityhubsdk.Client's
GetInsightResults through a Finalize-stage middleware that rewrites the
signed request's path from /insights/results/{InsightArn+} down to bare
/insights -- still inside RouteMatcher's /insights prefix (so the
request still reaches this package's Handler) but matching none of
classifyInsightsPath's method/path cases for a GET, landing in
handleREST's fallback. Hand-reverted handler.go to git show HEAD (the
pre-fix state, still carrying the bare map literal), confirmed the test
fails with apiErr.ErrorCode() == "UnknownError", restored the fix,
md5sum-confirmed byte-identical to the pre-revert file.
Not a repeat of the ErrHubNotEnabled ambiguity: those sites are ambiguous
between two real, named exceptions a specific operation models. This site
doesn't know the operation at all (routing itself failed), so there is no
per-operation vocabulary to disambiguate between -- a generic
ResourceNotFoundException (already the modeled 404 shape used elsewhere
in this file, e.g. GetSecurityControlDefinition) is the closest fit, not
a guess among named alternatives.
Confirmed still-deliberate and left untouched: every ErrHubNotEnabled
bare-message site (handler_hub.go, handler_insights.go, handler_findings.go,
handler_products.go, handler_action_targets.go) -- each carries its own
comment citing the specific operation's real error list from
securityhub@v1.75.4 deserializers.go and the reason no single exception
can be chosen without guessing. Re-spot-checked DisableSecurityHubV2
(deserializers.go:7744) directly: its real error list is
AccessDeniedException/InternalServerException/ThrottlingException/
ValidationException -- no ResourceNotFoundException, confirming the
comment's claim -- and left as documented rather than "resolved by
elimination", since the real "not enabled" AWS status for this call is not
independently verified here.
CI's unit-tests (3) job flagged -race failures in
TestExtractOperation_SDKRouteTable on describesecurityhubv2 and the
enable/disable-feature subtests. Root cause: DescribeSecurityHubV2
(hub.go) did cp := *b.hubV2 under RLock and returned &cp --
HubV2.Features is map[string]*HubV2Feature, so the copy's Features
field is the same map as the live b.hubV2.Features.
handleDescribeSecurityHubV2 (handler_hub.go) ranges over that map after
RUnlock has already run, racing against EnableSecurityHubFeatureV2/
DisableSecurityHubFeatureV2's b.hubV2.Features[name] = &HubV2Feature{...}
writes under Lock. Hub (v1) has no reference fields, so DescribeHub's
identical-looking cp := *b.hub is genuinely safe and was left alone.
Reproduced directly (not just via the flaky parallel-subtest ordering CI
hit): TestSecurityHubV2FeatureDescribeRace (hub_test.go) drives
DescribeSecurityHubV2 + Enable/DisableSecurityHubFeatureV2 concurrently
against one backend. Confirmed failing pre-fix (runtime.mapassign_faststr
write vs. runtime.mapIterStart/mapIterNext read), hand-reverted hub.go
to the shallow-copy version, re-confirmed the same failure, restored,
md5sum-confirmed byte-identical. Fixed with HubV2.clone(), which
deep-copies Features (new map, new *HubV2Feature per entry).
Audited the rest of services/securityhub/ for the same two shapes:
- A struct with a map/slice field is shallow-copied (
cp := *x) while that field is mutated in place (indexed assignment) elsewhere under lock, or is aliased with a map that's mutated in place elsewhere (Tagsfields are assigned the exact same map object passed tob.tags[ARN] = tagsat creation time, andTagResource/UntagResourcemutateb.tags[ARN]in place viamaps.Copy/delete). - A live, stored
*T(or one of its map/slice fields) is returned directly with no copy at all, and that same object is later mutated in place (by anUpdate*, or by the sameGet-style op itself, e.g.GetEnabledStandards's poll-to-READY advance) under a subsequent lock acquisition.
Fixed (added a .clone() deep-copy method per type, used at every point the
value crosses the lock boundary -- Create/Get/List/Batch/Update returns):
ConfigurationPolicy(configuration_policies.go):Tagsaliasesb.tags[Arn];ConfigurationPolicymap cloned too for consistency.CreateConfigurationPolicyalso returned the live stored pointer.CspmConnector(connectors.go):Tagsaliasesb.tags[ConnectorArn](Providercloned too).CreateConnectorreturned the live pointer.ConnectorV2(connectors_v2.go): sameTags/Providershape.CreateConnectorV2returned the live pointer; so didUpdateConnectorV2andRegisterConnectorV2before theircp := *targetwas replaced with.clone().AutomationRule/AutomationRuleV2(automation_rules.go):BatchGetAutomationRulesreturned live*AutomationRulepointers with no copy at all --BatchUpdateAutomationRulesmutatesRuleName/RuleStatus/Criteria/Actions/etc. on that same object in place.CreateAutomationRuleV2likewise returned the live pointer, later mutated byUpdateAutomationRuleV2.StandardsSubscription(standards.go):BatchEnableStandardsandBatchDisableStandardsreturned the live, stored pointer;GetEnabledStandardsreturns the exact objects it just mutated in place (pollCount,StandardsStatus) with no copy, and those same objects can be mutated again later byBatchDisableStandards.StandardsControl(standards.go):DescribeStandardsControls's override branch (controls[i] = override) assigned the live*StandardsControlstored inb.controlOverridesdirectly;UpdateStandardsControlmutates an existing override's fields in place.AggregatorV2/FindingAggregator:Regions []stringis only ever wholesale-reassigned (never indexed into), so the existing shallow copies on Get/List/Update were already safe -- butCreateAggregatorV2/CreateFindingAggregatorreturned the live pointer, later mutated by their respectiveUpdate*. Fixed by copying at the Create return only.Member(members.go):CreateMembersappended the live pointer;InviteMembers/DisassociateMembersmutateMemberStatus/InvitedAton that same object in place.GetMembers/ListMembersalready copied correctly.
Confirmed safe, left unchanged, with reason:
Hub(hub.go),Invitation/AdminAccount(invitations.go),OrgConfig(organizations.go),ConfigurationPolicyAssociation(configuration_policies.go),RecommendedPolicyV2/TicketV2: all-scalar structs, or (RecommendedPolicyV2/TicketV2) have noUpdate*that ever mutates an existing instance after creation.knownStandards/knownSecurityControls/knownProducts: package-level read-only lookup tables, never mutated afterinit; everycp := knownX[i]copy is safe regardless of field shape.BatchGetSecurityControls'sParametersfield (controls.go): hands outb.controlParams[id]'s map directly with no copy, but the only writer (UpdateSecurityControl) always replaces the whole map entry (b.controlParams[id] = parameters), never indexes into an existing one -- a previously-handed-out map is never touched again.BatchGetStandardsControlAssociations's override branch (standards.go): hands out the live*StandardsControlAssociationfromb.controlAssocOverridesdirectly, but the only writer (BatchUpdateStandardsControlAssociations) alwaysPuts a brand-new struct rather than mutating an existing one in place.Snapshot(store.go): marshals every live field (includingb.tags,b.hubV2,b.controlParams, ...) while still holdingRLockfor the entire call -- unlike the handler-side bugs above, the read never escapes the lock.
Proof: go test -race -count=20 ./services/securityhub/... clean after all
fixes; TestSecurityHubV2FeatureDescribeRace is the new permanent
regression test for the flagged bug specifically.
Audited securityhub's failure path -- what a real typed aws-sdk-go-v2
client sees when a request fails -- as part of a four-service sweep
(securityhub, kafka, elbv2, stepfunctions) hunting the class of bug where
gopherstack's error-handling call site picks a sentinel/wire code the real
operation's own deserializeOpError<Op> switch does not model. All 116
operations' switches extracted from deserializers.go (securityhub@v1.75.4)
and diffed against every typedErrorResponse(...) call site (125 sites
across all handler_*.go files) and the ErrHubNotEnabled/ErrNotFound/
ErrAlreadyExists/etc. sentinels feeding them.
Every literal errType string used at a typedErrorResponse call site names
a real type in this SDK's types/errors.go (AccessDeniedException,
ConflictException, InternalException, InternalServerException,
InvalidAccessException, InvalidInputException, ResourceConflictException,
ResourceNotFoundException, ValidationException) -- no fabricated code exists
anywhere in this service. Every ResourceNotFoundException/
ResourceConflictException/InvalidAccessException/ValidationException/
InvalidInputException call site was cross-checked against its own
operation's modeled set (not a sibling's) and matches exactly; the classic
REST vocabulary (InvalidInputException/InternalException/
ResourceConflictException) and the newer V2-style vocabulary
(ValidationException/InternalServerException/ConflictException) are never
crossed at a call site, including the several non-"V2"-suffixed operations
(Connectors, ConnectorsV2, AutomationRulesV2, AggregatorsV2) that use the
newer vocabulary -- this distinction was already called out and correctly
handled by a prior pass (see typedErrorResponse's doc comment,
handler.go:507-514), and this pass re-verified it rather than trusting the
comment.
Two call sites (handleStartConfigurationPolicyDisassociation,
handleUpdateStandardsControl) have an unreachable 500 fallback: their
backend methods never actually return an error (both silently accept any
identifier, including one that was never created, rather than validating
against a known-resource set) even though their operations model
ResourceNotFoundException. This is a missing-validation / structural gap,
not a wrong-sentinel-at-a-call-site bug -- fixing it would mean building a
"does this identifier correspond to a real resource" check neither op has
today, not swapping which existing sentinel a call site already picks -- so
it is reported here rather than fixed under this sweep's narrower scope.
No test changes; no source changes. Recorded as genuinely clean for this bug class, matching several other services in this campaign.
Distinct class from the error-path sweep above: not which sentinel a call
site picks, but whether a call's own return value carrying failure
information is thrown away (x, _ := b.Something(...)). ~195 , _ :=/
, _ =/bare _ = sites across all non-test .go files, triaged
individually.
The large majority are legitimate: JSON-body type assertions
(body["Field"].(string)) where a missing/wrong-typed value correctly
becomes the zero value; x, _ := b.<store>.Get(id) calls that follow a
resolve*/existence check in the same function (the miss case already
returned); and strconv.Atoi(v) best-effort query-param parses that fall
back to 0 ("use default").
All 12 Batch* operations checked against their backend implementations --
BatchImportFindings, BatchUpdateFindings, BatchUpdateFindingsV2,
BatchGetSecurityControls, BatchGetAutomationRules,
BatchDeleteAutomationRules, BatchUpdateAutomationRules,
BatchEnableStandards, BatchDisableStandards,
BatchGetStandardsControlAssociations,
BatchUpdateStandardsControlAssociations,
BatchGetConfigurationPolicyAssociations -- each correctly threads its
per-item unprocessed/failed list (or an err return) into the response.
Two things worth recording, neither a bug:
handleBatchEnableStandards/handleBatchDisableStandards(handler_standards.go:57,76) discardBatchEnableStandards/BatchDisableStandards's second return (a[]map[string]anyof failures). Left as-is:BatchEnableStandardsOutput/BatchDisableStandardsOutput(securityhub@v1.75.4 api_op_BatchEnableStandards.go / api_op_BatchDisableStandards.go) carry onlyStandardsSubscriptions-- there is no per-item failure field on the real wire shape to put it in.BatchEnableStandards's own failure branch (emptyStandardsArn) is additionally unreachable via a real typed client:StandardsArnis// This member is requiredontypes.StandardsSubscriptionRequestand enforced byvalidateStandardsSubscriptionRequest/validateOpBatchEnableStandardsInput(validators.go) before the request leaves the client.handleCreateAggregatorV2's_ = h.Backend.TagResource(...)(handler_aggregators_v2.go:46):TagResource(tags.go:5) unconditionally returns nil, so no real error is being suppressed.handleCreateMembers's_ = created(handler_members.go:74): correct per wire shape --CreateMembersOutput(api_op_CreateMembers.go) has onlyUnprocessedAccounts, no created-members field to populate.
No test changes; no source changes. Recorded as genuinely clean for this bug class.
Audited every paginated listing for the five known gopherstack
pagination-arithmetic bug classes (panic on stale offset, infinite loop on
stale equality-matched cursor, guarded-but-unused index, encoder/decoder
disagreement, unsorted collection). Census: one shared offset-token helper
(store.go's paginateSlice, 15 call sites) plus two supporting helpers
(filterOrAll, sortFindings) feed every List/Describe/Get* op in this
service; no inline for i, x := range all { if x.ID == token { start = i } }
site exists outside store.go. paginateSlice itself was already correct
(clamped offset decode, no equality search — all seven checks pass).
This service came back with a real, repo-wide Class E problem, not clean.
11 of the 15 paginateSlice call sites fed it a collection read straight
from a map or a store.Table.All() (explicitly documented as unspecified
iteration order) with no sort in between:
filterOrAll's "return everything" branch (arnsempty) calledt.All()— affectsDescribeActionTargetsandGetEnabledStandards.sortFindingswas a no-op whensortCriteriawas empty (if len(criteria) == 0 { return }) — affectsGetFindingsandGetFindingsV2, whose backing store (b.findings) is itself amap[string]map[string]any, so the common no-sort-criteria call shape hit this on every listing.- 8 more
.All()-straight-into-paginateSlicesites with zero sort:ListAutomationRulesV2,ListAggregatorsV2,ListInvitations,ListConnectors(CSPM),ListConnectorsV2,ListFindingAggregators,ListConfigurationPolicies,ListConfigurationPolicyAssociations,ListMembers. - 2 sites ranging a raw (non-
store.Table) map with zero sort:ListOrganizationAdminAccounts(b.orgAdminAccounts),GetResourcesV2(a locally-builtmap[string]map[string]anykeyed by resource Id).
All are Class E: a plain two-page walk with no deletion or tampering drops or duplicates results whenever Go's map iteration reorders between the two calls (confirmed empirically — reverting one fix and rerunning its regression test failed 5/5 times).
Fixed 9 of the store.Table-backed sites by swapping .All() for
.Snapshot() (same package, sorted by the table's own key, already the
established idiom in this repo for exactly this purpose). Fixed the 2
raw-map sites with an explicit sort.Slice by account ID / resource Id.
Fixed filterOrAll the same way (.Snapshot()). Fixed sortFindings by
removing the empty-criteria early return and adding a final deterministic
tiebreak (ProductArn|Id, both ASFF-required fields) that always runs,
whether or not the caller supplied real sort criteria — this also make the
existing sort well-defined on ties within real criteria, which previously
had no tiebreak either.
Safe-by-construction pattern applied throughout: default a miss/no-sort
case to a genuinely sorted read (Table.Snapshot(), or an explicit
sort.Slice for the two raw-map sites) — the same pattern already used
correctly elsewhere in this repo. No threshold-search or found-flag pattern
was applicable here since none of these sites use an equality-matched
cursor (offset tokens throughout).
7 checks run against paginateSlice directly (all pass, both before and
after — it was never the bug) plus a stale-cursor probe on filterOrAll and
a tied-order probe on sortFindings, both of which failed against the
pre-fix code and pass post-fix. 10 end-to-end boundary-walk regression tests
drive the real exported backend methods (23 items, page size 5, non-dividing
count) for a representative sample: ListAggregatorsV2,
ListAutomationRulesV2, ListFindingAggregators,
ListConfigurationPolicies, ListMembers, ListOrganizationAdminAccounts,
ListConnectorsV2, ListConnectors, DescribeActionTargets, GetFindings
(no SortCriteria). ListInvitations, ListConfigurationPolicyAssociations,
and GetResourcesV2 got the identical, already-proven .Snapshot()/explicit-sort
fix but no bespoke end-to-end test — lower priority given the pattern was
independently verified nine other times in this same sweep; flagged here for
anyone auditing this note.
New tests: services/securityhub/pagination_arithmetic_test.go (internal,
unexported-helper unit tests), services/securityhub/pagination_arithmetic_e2e_test.go
(external, real-API boundary walks).
Gates: go build ./services/securityhub/... (clean), go vet ./services/securityhub/... (clean, no signature changes), go test -race -count=1 ./services/securityhub/... (pass). Work left uncommitted per this
pass's instructions.
2026-08-30 (negative-continuation-token sweep): store.go's decodeToken used a bare
fmt.Sscanf(token, "%d", &offset) with no bounds check at all; paginateSlice's start >= len(results) guard does not catch a negative start, so results[start:end] panicked given
"-5" as a NextToken, across all 15 call sites (action_targets.go, aggregators_v2.go,
connectors.go, finding_aggregators.go, configuration_policies.go x2, connectors_v2.go,
automation_rules.go, findings.go, findings_v2.go, invitations.go, resources_v2.go,
organizations.go, members.go, standards.go). Fixed at the decode site: decodeToken now
returns 0 for a negative offset, so all 15 callers inherit the fix. The existing
TestPaginateSlice_SevenChecks table in pagination_arithmetic_test.go exercised stale/
past-end/malformed-non-numeric tokens but never a negative one.
Proof: the added negative offset token subtest of TestPaginateSlice_SevenChecks
(pagination_arithmetic_test.go) confirmed panicking pre-fix, passes now. Gates: go build ./services/securityhub/..., go vet ./services/securityhub/..., go test -race -count=1 ./services/securityhub/..., golangci-lint run ./services/securityhub/... (0 issues). Work
left uncommitted per this pass's instructions.
2026-08-30 (gopherstack-r3pr fabricated-error-code re-audit, no code change):
store.go:31's errCodeInvalidInput ("InvalidInput") re-checked against
cmd/errcodeaudit. All three call sites (standards.go:95,149,
findings.go:458) set it as a free-form ErrorCode map value inside a
Failures/UnprocessedFindings array on an ordinary 200 response
(BatchEnableStandards/BatchDisableStandards/BatchUpdateFindings), never
as an HTTP error envelope's __type — same shape as the already-known
false-positive class (glue/macie2/ce/xray free-form success-response
ErrorCode fields), confirmed not a wire-error-envelope bug. Aside, not
fixed here (out of scope for this class): the SDK doc comment on
BatchUpdateFindingsUnprocessedFinding.Code (types.go) lists
FindingNotFound as the specific documented value for the not-found case
findings.go:458 covers, which differs from the InvalidInput used there —
a real inaccuracy, but a different bug class with no errors.As ground
truth, deliberately not chased this pass per campaign scope.
2026-08-30 (gopherstack-uox6 value-semantics sweep, one bug fixed):
Audited every finding filter/matcher in this service against its SDK doc
comment (V1 matchesFindingFilters/matchesStringFilter/compareStringFilter
in findings.go; V2's matchesFindingFiltersV2/matchesCompositeFilter*/
matchesOcsf*Filter family in findings_v2.go; filterOrAll in store.go).
Bug found and fixed: matchesStringFilter (findings.go) combined every
entry of a field's []StringFilter list with a strict AND. types.StringFilter's
doc comment (securityhub@v1.75.4 types.go:19655) documents the opposite for
same-field entries: CONTAINS/EQUALS/PREFIX are joined by OR ("a finding
matches if it matches any one of those filters" — the doc's own worked
example is Title CONTAINS CloudFront OR Title CONTAINS CloudWatch),
NOT_CONTAINS/NOT_EQUALS/PREFIX_NOT_EQUALS are joined by AND, and the two
groups then combine by AND ("Security Hub CSPM first processes the PREFIX
filters, and then the NOT_EQUALS ... filters" — the doc's second worked
example, ResourceType PREFIX AwsIam + PREFIX AwsEc2 +
NOT_EQUALS AwsIamPolicy + NOT_EQUALS AwsEc2NetworkInterface). Under the
old AND-everything code, either worked example returned zero results against
a real matching finding: an under-match, invisible to any shape-based sweep
since the field is read and the comparator values are legal enum members —
only the combination across multiple entries was wrong. Affects GetFindings
and, via the shared matchesFindingFilters, BatchUpdateFindings.
No prior test passed a multi-entry filter on the same field (existing
TestBackend_MatchesStringFilter/TestGetFindings_FiltersApplied cases all
use exactly one StringFilter entry per field), so the bug was invisible to
the existing suite — "a filter test passing a single value cannot see a
multi-value bug."
Fixed by splitting entries into positive/negative groups (isNegativeStringComparison)
and combining !hasPositive || positiveMatched (OR over positives, defaulting
to "no restriction" when there are none) AND'd with every negative entry
passing. Both of the SDK doc's own worked examples now pass as tests.
Also checked and confirmed correct: matchesFindingFiltersV2's composite
AND/OR (CompositeOperator) and matchesCompositeFilterDepth's per-filter
Operator (both against types.OcsfFindingFilters/types.CompositeFilter's
doc comments, matched field-for-field: NestedCompositeFilters three-layer
structure, AllowedOperators AND/OR with no NOT combinator since negation is
expressed at the leaf comparator); matchesOcsfNumberFilter's
Eq/Gt/Gte/Lt/Lte against types.NumberFilter; matchesDateRange's
WITHIN/OLDER_THAN against types.DateRange (default WITHIN); compareMapFilter's
EQUALS/NOT_EQUALS/CONTAINS/NOT_CONTAINS against types.MapFilter; ipInCIDR's
bare-address-normalizes-to-/32-or-/128 against types.IpFilter's documented
"CIDR block or IP address" acceptance; matchesWholeWord's word-boundary
regex for CONTAINS_WORD (documented V2-only); the lifecycle-rule-style AND
combination across different filter fields in both V1 and V2 (correct in
both — the bug was specifically the same-field, multi-entry case).
One gap recorded, not fixed: whether OcsfMapFilter's same-field
CONTAINS/EQUALS-OR / NOT_CONTAINS/NOT_EQUALS-AND rule (documented on the
shared MapFilter type) still applies underneath a CompositeFilter's
explicit Operator, or is superseded by it, is not stated by either doc
comment — left open rather than guessed (see gaps).
GetResourcesV2's filters parameter (resources_v2.go) is read nowhere
(//nolint:revive // existing issue already marks it) and GetInsightResults
never evaluates insight.Filters at all (documented in-code: "no real
aggregation in mock") — both are pre-existing, already-flagged completeness
gaps (an unread field, not a wrong algorithm on a read one), not new findings
of this class, so left as-is.
No AWS web pages were fetched this pass — every comparator/operator set
needed was fully specified in the pinned SDK's Go doc comments
(securityhub@v1.75.4), unlike the SNS/EventBridge instances of this bug
class from the prior pass.
Tests: added TestGetFindings_MultiValueSameFieldCombination (2 subtests,
findings_test.go) driving the real filter shape through the HTTP handler
end to end and asserting the exact ID set returned (not just a count), using
the SDK doc's own two worked examples. Both subtests confirmed failing
(0 results each) against the unmodified matchesStringFilter before the fix,
passing after. No existing test was weakened; assertion count increased by 2
new subtests, 0 removed.
Gates: go build ./services/securityhub/..., go vet ./services/securityhub/...,
go test -race -count=1 ./services/securityhub/..., golangci-lint run ./services/securityhub/.... Work left uncommitted per this pass's
instructions.
Full-service audit for AWS parity/correctness. Independently re-verified
PARITY.md's own claims per the campaign's "don't treat prior audits as
ground truth" instruction rather than trusting them; found three genuine
"accepted but never done" bugs (pattern class (b)) plus one missing delete
precondition (class (f)), none previously flagged as bugs in this file's
gaps/ops (two were explicitly noted as deliberate, acceptable mock
limitations, which this pass disagrees with -- see each entry).
-
Automation rules were pure CRUD --
Criteria/Actionswere stored and echoed back but zero call sites in the package ever evaluated them against a finding (automation_rules.go/findings.go). Same shape as guardduty's dead filter Action. Strong evidence this was a real gap, not a deliberate omission:findings.go's ownfindingCustomerManagedFieldsdoc comment already statesBatchImportFindingscannot setNote/UserDefinedFields/VerificationState/Workflow"since they're managed by Security Hub customers/automation rules" -- automation rules are the only mechanism that manages those fields, so the comment's own premise was false until this fix. AddedapplyAutomationRules(automation_rules.go), called fromImportFindingsafterpreserveCustomerManagedFields: evaluates everyRuleStatus=="ENABLED"rule in ascendingRuleOrder(ties broken byRuleArn) viamatchesFindingFiltersagainstrule.Criteria(the same field-name-mapped subset already used forGetFindings/GetInsightsfilters -- AWS'sAutomationRulesFindingFilters, securityhub@v1.75.4 types.go:575, has additionalNumberFilter/DateFilter/MapFiltermembers this file has no evaluator for; left unevaluated per the no-fabrication rule, same as the existing V2 filter gaps), and applies each match'sFINDING_FIELDS_UPDATEaction (the only realAutomationRulesActionType, enums.go:119) viamaps.Copy-- the same mechanismBatchUpdateFindingsalready uses, sinceAutomationRulesFindingFieldsUpdate(types.go:524) has the identical field set. Stops at the firstIsTerminalmatch. Proof:TestBatchImportFindings_AutomationRuleFires(2 subtests,automation_rules_test.go) -- confirmed failing (finding's Severity unchanged) againstgit show HEAD, passing after, restored. -
DisableSecurityHubnever checked AWS's documented precondition.api_op_DisableSecurityHub.go's doc comment: "You can't disable Security Hub CSPM in an account that is currently the Security Hub CSPM administrator."DisableHub(hub.go) only checkedhubEnabled. Fixed: refuses (newErrHubIsAdministratorsentinel, mapped toInvalidAccessException/400 -- one ofDisableSecurityHub's five modeled error types, deserializers.go:7544, and the same code this file already uses for analogoushubEnabled-gap fixes onUpdateActionTarget/DeleteActionTarget/DisableImportFindingsForProduct) while any member (CreateMembersis this backend's only path to the administrator relationship -- Organizations delegated admin never createsMemberrecords, seeorganizations.go) hasMemberStatus != "Removed". Proof:TestDisableSecurityHub_RefusedWhileAdministrator(2 subtests,hub_test.go) -- confirmed failing (200 instead of 400) againstgit show HEAD, passing after, restored, md5sum-confirmed byte-identical. -
GetInsightResultsalways returned emptyResultValues. PARITY.md previously called this "acceptable mock behavior, not a stub since Insight itself is real" -- this pass disagrees:Insight.GroupByAttribute/Filtersare real, stored, client-supplied values with a well-specified real aggregation (InsightResults/InsightResultValue, types.go:15875-15912, is just{GroupByAttributeValue, Count}per distinct value), and the infrastructure to compute it (matchesFindingFilters,findingFieldString) already existed in this same package forGetFindings/GetInsights-- returning it unconditionally empty is functionally indistinguishable from a stub to any real client. Fixed via newaggregateInsightResults(insights.go): filtersb.findingsbyinsight.Filters(matchesFindingFilters, deliberately the same mapped-subset limitation as every other reuse of that function in this file -- not a new gap), groups byfindingFieldString(f, insight.GroupByAttribute)(already resolves theSeverityLabel/WorkflowStatus/ComplianceStatusnested-field cases), counts, and returns values sorted for determinism. Proof:TestBackend_GetInsightResults_AggregatesFindings(insights_test.go) -- confirmed failing (all counts 0) againstgit show HEAD, passing after, restored, md5sum-confirmed byte-identical.
Checked and confirmed correct (no bug), contra this pass's own speculation before reading the code:
BatchUpdateFindings/handleBatchUpdateFindings's "copy every body key except FindingIdentifiers into updates" (handler_findings.go) looked like an unbounded-field-write risk at first glance, butBatchUpdateFindingsInput(api_op_BatchUpdateFindings.go) only ever defines nine possible fields besidesFindingIdentifiers(Confidence/Criticality/Note/RelatedFindings/Severity/Types/UserDefinedFields/VerificationState/Workflow) -- a real typed client is structurally incapable of sending anything else, so this is CLEAN for the client-observability bar this campaign uses, not a bug.DeleteMembers's missing check for AWS's documented "can't delete Organizations-org members" restriction is moot in this backend:organizations.gonever createsMemberrecords (delegated-admin enable/disable is separate bookkeeping with no member-creation side effect), so no org-managed member can ever exist here to violate the restriction. Not a bug; architecturally inapplicable.DeleteInsight/DeleteActionTarget/DeleteFindingAggregator/BatchDeleteAutomationRules/DisassociateMembersall correctly validate existence (or, forDisassociateMembers, correctly have nothing to validate against --DisassociateMembersOutputhas zero members besidesResultMetadata, so silently skipping an unknown id matches the real wire shape exactly).CreateFindingAggregator/cross-Region aggregation andBatchEnableStandards/BatchUpdateStandardsControlAssociationscontrol status are bookkeeping, not derived from real cross-region replication or finding-vs-control evaluation -- structural (single-backend-instance mock has no second region to replicate into, and no config-rule-evaluation engine), same category asDescribeStandards'/DescribeProducts' static catalogs, not a new finding. This does NOT extend to finding-levelCompliance.Status: that field is never fabricated by this backend (see gopherstack-cf4j triage below) -- it's not part of this bullet's claim, only the control/standard bookkeeping is. Prior wording here read "...compliance status are bookkeeping" as one run-on list, which is ambiguous about which "compliance" it means; corrected.
Structural/absent, checked rather than assumed, not fixed this pass:
- No cross-service integration. Grepped
services/guardduty,services/inspector*,services/macie*for any call intoservices/securityhub-- none exists. GuardDuty/Inspector/Macie appear in this service only as staticDescribeProductscatalog entries (products.go); a real finding provider (or gopherstack's own emulated GuardDuty/Inspector/Macie) must callBatchImportFindingsitself for findings to appear here. No EventBridge event publication on finding create/update either (only one unrelated code comment mentions "change events"; zeroevents./Publishcall sites in non-test files). Organizations delegated admin (organizations.go) is standalone bookkeeping with no cross-check against gopherstack's own Organizations service state. All matches this file's existing "findings only ever enter via BatchImportFindings" structural note -- reported here as the cross-service-integration angle this audit brief specifically asked to verify rather than assume. - Findings generation is import-only (
BatchImportFindings/ImportFindingsis the only path that creates a finding) -- structural, already documented elsewhere in this file, re-confirmed.
Performance: read GetFindings/GetFindingsV2's filter+sort path and
paginateSlice (already the subject of a dedicated 2026-08-30 sweep in this
file). No quadratic loops found; every filter/aggregate op here (including
the two new ones this pass added) is a single O(n) pass under the backend's
one coarse lock, consistent with every other listing op in this service.
Not independently re-benchmarked.
LocalStack parity: NOT CHECKED -- no LocalStack instance available this pass.
Resource leaks: re-confirmed the existing findingHistory ghost-row
finding from a prior pass is a deliberate append-only audit log, not a leak
(per this task's brief, not re-litigated). No new maps were added by this
pass's fixes (automation-rule application mutates the finding map already
being written; insight aggregation reads b.findings transiently, storing
nothing new).
Gates: GOTOOLCHAIN=go1.26.6 go build ./services/securityhub/...,
go vet ./services/securityhub/..., go test -race -count=1 ./services/securityhub/... (all pass), golangci-lint run ./services/securityhub/... (0 issues), gofmt -l services/securityhub/
(clean).
gopherstack-cf4j (2026-09-07): triage -- standards/control associations ARE bookkeeping (correctly); compliance status is NOT synthesized
Filed title-only, empty description: "securityhub: standards and control associations are bookkeeping; compliance status is synthesized." Re-derived both claims from the code since none of the specifics existed in the issue. Verdict: structural for claim 1, factually wrong for claim 2 -- no code change. This section is the missing triage note plus the correction.
BatchEnableStandards/BatchDisableStandards (standards.go) create/delete a
StandardsSubscription record. DescribeStandardsControls returns a static
defaultControls() list overridable per-arn via UpdateStandardsControl
(b.controlOverrides). BatchGetStandardsControlAssociations/
BatchUpdateStandardsControlAssociations/ListStandardsControlAssociations
read/write b.controlAssocOverrides the same way. All four are pure
CRUD-on-a-map: stored on write, echoed on read, consulted by nothing else.
Confirmed by grep, not assumed: ImportFindings (the only function that
creates a finding -- interfaces.go:14, called from exactly one call site,
handler_findings.go:71 handleBatchImportFindings) is never called from
standards.go, controls.go, handler_standards.go, or handler_controls.go.
Enabling a standard, disabling a control, or updating a control association
never produces, withdraws, or touches a single finding.
That is the honest behavior, not a gap, because the real AWS semantics this
mirrors require a check-evaluation engine gopherstack doesn't have.
StandardsControl.ControlStatus's doc comment (types.go:19299-19301,
securityhub@v1.75.4) says outright:
The current status of the security standard control. Indicates whether the control is enabled or disabled. Security Hub CSPM does not check against disabled controls.
"Does not check against disabled controls" presupposes Security Hub CSPM
does check against enabled ones -- a continuous compliance engine that
inspects real resource state per control and emits/withdraws findings as
ControlStatus/AssociationStatus change. Gopherstack's securityhub
package has no such engine (per the 2026-08-29 error-path sweep, confirmed
again this pass: zero cross-service call sites from guardduty/inspector/
macie into securityhub, and the only finding-creation path is client-driven
BatchImportFindings). Given that, ControlStatus/AssociationStatus have
exactly one honest implementation available: store what the caller set and
echo it back. Building a real per-control resource evaluator is out of
reach without picking a source of truth for "what does S3.1 check" across
every emulated service and re-running it on every toggle -- a project-wide
feature, not a securityhub fix.
What would have to exist first: a resource-evaluation engine that maps
each SecurityControlId (controls.go's knownSecurityControls, e.g.
S3.1 "S3 Block Public Access setting should be enabled") to a real check
against the corresponding emulated service's stored state (e.g. query
services/s3's bucket public-access-block config), runs it when a control's
AssociationStatus/ControlStatus is ENABLED, and creates/updates
Compliance.Status findings via the existing ImportFindings path when the
check result changes. That's new cross-service infrastructure, not a
securityhub-local fix. Not building it; echo-only bookkeeping is correct
until it exists.
types.ComplianceStatus (enums.go:237,241-244) has four values: PASSED,
WARNING, FAILED, NOT_AVAILABLE.
Grepped every non-test .go file in this package for all four literals and
for any math/rand import: zero hits. Nothing in this backend ever writes
a ComplianceStatus value. The only two places Compliance/Compliance.Status
appear in non-test code are reads: findings.go:347 (nestedFindingString,
used by GetFindings/GetFindingsV2's ComplianceStatus filter) and
findings_v2.go:570 (GetFindingsV2 composite-filter evaluation). Neither
writes a value; both return "" when the field is absent on the stored
finding (nestedFindingString, findings.go:357-360) -- absence stays
absent, it is never defaulted to an enum member.
The only place a finding (and therefore any Compliance object) is created
is ImportFindings (findings.go:104-148), which copies the caller's ASFF
map verbatim (maps.Copy(stored, f), findings.go:133) into storage. The one
list of fields explicitly protected from being overwritten by a client's
re-import -- findingCustomerManagedFields (findings.go:21-23): Note,
UserDefinedFields, VerificationState, Workflow -- does not include
Compliance, which is correct: AWS's own docs place Compliance with the
finding-provider-owned fields a re-import is expected to refresh, not the
customer-managed set. So on every BatchImportFindings call, whatever
Compliance.Status the caller supplies is exactly what gets stored,
overwriting the prior value -- matching real AWS, where the finding
provider (a real CSPM check, GuardDuty, a third-party integration) is the
only party that ever sets Compliance.Status; Security Hub itself doesn't
invent one.
BatchUpdateFindings (findings.go:480-527) could in principle be a second
write path -- handleBatchUpdateFindings (handler_findings.go:80-98)
collects updates from every raw JSON body key except
FindingIdentifiers (handler_findings.go:91-98), with no field allowlist,
and maps.Copy(f, updates) applies it verbatim. This was already investigated by the 2026-08-29
error-discard sweep (PARITY.md, "Checked and confirmed correct... contra
this pass's own speculation") and found clean: BatchUpdateFindingsInput
(api_op_BatchUpdateFindings.go, securityhub@v1.75.4) defines exactly nine
fields besides FindingIdentifiers -- Confidence, Criticality, Note,
RelatedFindings, Severity, Types, UserDefinedFields,
VerificationState, Workflow -- and has no Compliance member at all. A
real typed aws-sdk-go-v2 client is structurally incapable of sending
Compliance through BatchUpdateFindings; only a hand-crafted raw HTTP
request could exploit the missing allowlist, and even then it would be
replaying attacker-supplied input, not the backend inventing a status.
Re-confirmed this pass, not just cited: still true, not treating it as new
scope for gopherstack-cf4j.
Conclusion: Compliance.Status falls in the "copied from client input
on BatchImportFindings" category the audit brief calls out as legitimate,
not the "invented" category. Nothing here resembles the accessanalyzer/
personalize undisclosed-confident-answer bug class (gopherstack-xyu4/h3th)
-- there is no code path that fabricates a value the caller never supplied.
The issue title's second half does not hold up against the code as written.
services/securityhub/PARITY.md: addednote:fields to theDescribeStandardsControls/UpdateStandardsControl/ListStandardsControlAssociations/BatchGetStandardsControlAssociations/BatchUpdateStandardsControlAssociationstable rows pointing here; tightened the pre-existing but ambiguous "...compliance status are bookkeeping" sentence (2026-08-29 sweep section) that conflated control-association bookkeeping with finding-levelCompliance.Statusin one run-on clause, and added a forward pointer to this section. No.gofiles touched -- both claims resolve to "no code defect," not "no code reviewed."
Close gopherstack-cf4j as not a bug / documentation-only, or re-file if the maintainer wants the "what would have to exist first" cross-service check-evaluation engine tracked separately (it would be a new, large, multi-service feature, not a securityhub-local fix). Suggested bd close text below.
gopherstack-3t96 (2026-09-08, P2): malformed JSON body reached the matched operation with body == nil -- found and fixed
Part of the sweep following elasticache (gopherstack-8haq, P1), pinpoint (gopherstack-246v),
and apigatewayv2 (gopherstack-wsvb, P1). decodeJSONBody (handler.go:531, called only from
handleREST at handler.go:577 -- confirmed the single call site, so no contract-change fallout
elsewhere) rejected malformed JSON by writing the 400 via c.JSON and returning that call's
result, which is nil after a successful write. handleREST stored that nil in err and tested
if err != nil, which never fired, so classifyPath's matched operation ran anyway with
body == nil, on top of the already-committed 400.
What a nil body actually does downstream is worse than a second write. A nil map[string]any
reads safely in Go (body["Field"].(string) returns "", false), so every op handler that reads
required fields out of body (e.g. handleCreateActionTarget, action_targets.go: name, _ := body["Name"].(string)) sees them as empty and rejects with its own 400 -- a real second write,
corrupting the wire body, but no state change. The dangerous case is any op with no required
fields: handleEnableSecurityHubV2 (handler_hub.go) reads only the optional Tags map, so a nil
body is indistinguishable from a valid empty request -- h.Backend.EnableSecurityHubV2(nil) ran
and actually enabled SecurityHub V2, a real, unintended state mutation, even though the client had
already received a 400 for the malformed body that triggered it. handleEnableHub (V1, same
family) has the same shape.
Tests first, new handler_malformed_body_test.go (no pre-existing test sent malformed JSON to
this package at all, so nothing to strengthen -- both new tests assert observable state, not just
status):
TestMalformedJSONBody_DoesNotEnableHubV2: POST/hubv2with{"Tags":(malformed), then GET/hubv2must still be 404ResourceNotFoundException(not enabled), not 200.TestMalformedJSONBody_DoesNotDoubleWrite: POST/actionTargetswith{"Name":(malformed) must produce one well-formed JSON body, not two concatenatedMessageobjects.
Confirmed both FAIL against unmodified code (verbatim, go test ./services/securityhub/... -run TestMalformedJSONBody):
=== NAME TestMalformedJSONBody_DoesNotEnableHubV2
handler_malformed_body_test.go:57:
Error: Not equal:
expected: 404
actual : 200
Messages: SecurityHub V2 must not be enabled after a malformed EnableSecurityHubV2 request
--- FAIL: TestMalformedJSONBody_DoesNotEnableHubV2 (0.00s)
=== NAME TestMalformedJSONBody_DoesNotDoubleWrite
handler_malformed_body_test.go:81:
Error: Received unexpected error:
invalid character '{' after top-level value
Messages: a single write must produce one well-formed JSON body, got:
{"Message":"invalid JSON body"}{"Message":"Name is required"}
--- FAIL: TestMalformedJSONBody_DoesNotDoubleWrite (0.00s)
each paired with a logger line "echo: response already written to client".
Fixed with the pinpoint raw-unwritten-error pattern: decodeJSONBody no longer writes; it
returns a new unexported static error (errInvalidJSONBody, handler.go), and handleREST maps
any non-nil error to a 400 via c.JSON(http.StatusBadRequest, map[string]any{keyMessage: err.Error()}) and writes exactly once -- left unheadered (no X-Amzn-Errortype), same as before
the fix and for the same reason: decodeJSONBody runs before the request is classified to an
operation, so it can't know which exception vocabulary (classic vs. V2-style) applies.
errname/err113 (this repo's golangci-lint config) require this as a static package-level
sentinel, not inline errors.New at the call site.
Neuter-verified two ways at handler.go's handleREST: (1) reverting the call site to bare
return err still compiles and fails require.NoError in both new tests, surfacing the raw
"invalid JSON body" text since nothing ever wrote a response; (2) restoring the entire original
decodeJSONBody/call-site pair verbatim (write-then-return-nil) still compiles and reproduces
the exact failures above, including the concatenated-body text.
go test -race ./services/securityhub/... and golangci-lint run ./services/securityhub/...
both clean after the fix. Full go test ./services/... also green (see gopherstack-3t96's
cross-service report for the combined blast-radius run covering lambda, securityhub, and
organizations).
Added realclient_hub_standards_and_automation_test.go (12 subtests) driving hub v1/v2
lifecycle, standards/controls, security control definitions, organization
admin, invitations/members, automation rules (v1+v2), configuration
policies, finding aggregators, connectors (v1+v2), aggregators v2,
products, and misc findings-v2 ops through the real aws-sdk-go-v2
securityhub client. Typed-client coverage (cmd/opcensus + cmd/
clientcoverage): 45/116 (38.8%) -> 114/116 (98.3%); uncovered dropped from
71 to 2 (AcceptInvitation -- exercised via direct backend call for this
pass's invitation-acceptance setup rather than the typed client;
UpdateConnectorV2 -- not attempted this pass).
One real wire-shape bug found and fixed: BatchGetConfigurationPolicy- Associations read each request-list item's TargetId field directly, but
the real wire shape (securityhub@v1.75.4 serializers.go's
awsRestjson1_serializeDocumentConfigurationPolicyAssociation) has no flat
TargetId member at all -- each identifier is {"Target": {"AccountId"|"OrganizationalUnitId"|"RootId": ...}}, the same tagged union
StartConfigurationPolicyAssociation/GetConfigurationPolicyAssociation
already parse correctly via extractConfigPolicyTarget. A real client's
request therefore always decoded TargetId as empty, so every association
was reported unprocessed regardless of backend state. Fixed by normalizing
each raw request item through extractConfigPolicyTarget before handing it
to the backend (batchGetConfigPolicyAssocRequests, handler_configuration_
policies.go). TestConfigurationPolicy/ConfigurationPolicy_association_ lifecycle's "batch get" step previously asserted the old flat shape as
correct (it only passed because the pre-fix handler expected that same
wrong shape); corrected to the real nested Target shape.
No persisted struct fields changed; no version bump. Gates: go build ./..., go vet ./services/securityhub/..., go test -race -count=1 ./services/securityhub/... (pass), golangci-lint run --new-from-rev=HEAD ./services/securityhub/... (0 issues). cmd/paritylint stays at 0 FAIL.
Added realclient_accept_invitation_and_connector_test.go covering securityhub's last two
typed-client-uncovered ops: the deprecated AcceptInvitation (an alias
for AcceptAdministratorInvitation, invitations.go) and
UpdateConnectorV2.
One real bug found and fixed: ConnectorV2 (models.go) had no
EnablementStatus field at all, so UpdateConnectorV2Output.EnablementStatus
(securityhub@v1.75.4 api_op_UpdateConnectorV2.go, deserializers.go's
case "EnablementStatus") always decoded as the zero value regardless of
backend state -- and, sharing the same connectorV2ToResponse render
function, so did CreateConnectorV2Output.EnablementStatus. Fixed by
adding the field to ConnectorV2, setting it to "ENABLED" on Create
(connectors_v2.go), and echoing it from connectorV2ToResponse
(handler_connectors_v2.go). pkgs/persistence/testdata/snapshot_inventory.json
updated (one additive row, ConnectorV2.EnablementStatus); no version
bump (TestSnapshotVersionGuard confirms pure addition).
Accept-and-drop finding, NOT fixed (out of scope for this pass, left
disclosed): ConnectorV2.ConnectorStatus is set to the literal "ACTIVE"
at creation, but the real types.ConnectorStatus enum for this API family
is CONNECTED/DEGRADED/FAILED_TO_CONNECT/PENDING_AUTHORIZATION/
PENDING_CONFIGURATION/UNKNOWN -- "ACTIVE" isn't a member of it. Since
ConnectorStatus is a plain Go string type, this never causes a real
client's decode to fail (only a semantically wrong status string), so it
was left as-is rather than risk-adjusting ConnectorStatus's value across
the whole connectors_v2.go file (would also touch
connectors_v2_test.go's existing "ACTIVE" assertions).
Typed-client coverage: 114/116 -> 116/116 (100%).
Every real client call to AcceptInvitation/GetMasterAccount carries an
expected SA1019 deprecation notice; the whole file is exempted from
staticcheck in .golangci.yml (same pattern as iotanalytics/
opsworks's existing deprecated-op exemptions).
Gates: go build ./..., go vet ./services/securityhub/..., go test -race -count=1 ./services/securityhub/... (pass), golangci-lint run --new-from-rev=HEAD ./services/securityhub/... (0 issues), go test -race -count=1 ./pkgs/persistence/... (pass, includes TestSnapshotVersionGuard).
cmd/paritylint stays at 0 FAIL.