@oneuptime/common 13.0.4 → 13.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/RumApplication.ts +1 -1
- package/Models/DatabaseModels/SecurityEventConnection.ts +457 -0
- package/Models/DatabaseModels/SecurityEventConnectionRun.ts +263 -0
- package/Models/DatabaseModels/TelemetryIngestionKey.ts +2 -2
- package/Server/API/AIInvestigationAPI.ts +484 -24
- package/Server/API/GoogleSecOpsConnectionAPI.ts +249 -8
- package/Server/API/SecurityEventConnectionAPI.ts +250 -0
- package/Server/API/TelemetryAPI.ts +402 -265
- package/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.ts +99 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Middleware/TelemetryIngest.ts +101 -6
- package/Server/Services/ExceptionAggregationService.ts +7 -16
- package/Server/Services/GoogleSecOpsConnectionService.ts +15 -0
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/InventoryItemService.ts +28 -0
- package/Server/Services/LogAggregationService.ts +17 -23
- package/Server/Services/MetricAggregationService.ts +4 -14
- package/Server/Services/SecurityEventConnectionRunService.ts +10 -0
- package/Server/Services/SecurityEventConnectionService.ts +538 -0
- package/Server/Services/TraceAggregationService.ts +77 -28
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
- package/Server/Utils/AI/Chat/ObservabilityAssistant.ts +13 -1
- package/Server/Utils/AI/SRE/AIInvestigationEngine.ts +15 -1
- package/Server/Utils/AI/SRE/InvestigationEvidence.ts +658 -0
- package/Server/Utils/AI/SRE/InvestigationReferences.ts +324 -0
- package/Server/Utils/AI/Toolbox/Index.ts +38 -0
- package/Server/Utils/AI/Toolbox/TimelineTools.ts +9 -2
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +80 -0
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +5 -3
- package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.ts +1108 -0
- package/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.ts +732 -0
- package/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.ts +495 -0
- package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.ts +689 -0
- package/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.ts +767 -0
- package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.ts +571 -0
- package/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.ts +827 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.ts +610 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.ts +549 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.ts +830 -0
- package/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.ts +684 -0
- package/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.ts +487 -0
- package/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.ts +778 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.ts +1002 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.ts +556 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.ts +275 -0
- package/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.ts +53 -0
- package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.ts +709 -0
- package/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.ts +596 -0
- package/Server/Utils/SecurityEvent/Connectors/Types.ts +191 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.ts +353 -65
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.ts +777 -0
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.ts +747 -148
- package/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.ts +48 -2
- package/Server/Utils/SecurityEvent/SecurityEventDedupe.ts +81 -0
- package/Server/Utils/SessionReplay/SessionReplayReadService.ts +741 -40
- package/Server/Utils/StartServer.ts +10 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +8 -0
- package/Server/Utils/Telemetry/KubernetesResourceAttributes.ts +108 -0
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +174 -93
- package/Server/Utils/Telemetry/ResourceFacetPlanner.ts +251 -0
- package/Server/Utils/Telemetry/ResourceFacetResolver.ts +248 -413
- package/Server/Utils/Telemetry/ServiceDependencyDiscovery.ts +668 -0
- package/Server/Utils/Telemetry/TraceContextPropagation.ts +348 -0
- package/Server/Utils/Telemetry.ts +18 -0
- package/Tests/App/Dashboard/AIInvestigationHeaderStatus.test.tsx +413 -0
- package/Tests/App/Dashboard/AIRootCauseFeedItem.test.tsx +285 -0
- package/Tests/App/Dashboard/AffectedResourcesDisplay.test.tsx +242 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +601 -15
- package/Tests/App/Dashboard/ChangeAlertStateAIHeader.test.tsx +835 -0
- package/Tests/App/Dashboard/CloudResourceTelemetryScope.test.ts +41 -0
- package/Tests/App/Dashboard/ConnectionTestModal.test.tsx +604 -0
- package/Tests/App/Dashboard/ConnectorTestReportView.test.tsx +738 -0
- package/Tests/App/Dashboard/DashboardLazyPages.test.tsx +153 -6
- package/Tests/App/Dashboard/EntityDetailPanel.test.tsx +165 -0
- package/Tests/App/Dashboard/EpisodeChangeState.test.tsx +1069 -0
- package/Tests/App/Dashboard/EpisodeMembersCard.test.tsx +772 -0
- package/Tests/App/Dashboard/EventOverviewPages.test.tsx +2354 -0
- package/Tests/App/Dashboard/EventOverviewSkeleton.test.tsx +210 -0
- package/Tests/App/Dashboard/EventStatBar.test.tsx +392 -0
- package/Tests/App/Dashboard/EventStatusPanel.test.tsx +247 -0
- package/Tests/App/Dashboard/ExceptionAIAssistance.test.tsx +606 -0
- package/Tests/App/Dashboard/ExceptionBreadcrumbTimeline.test.tsx +273 -0
- package/Tests/App/Dashboard/ExceptionDetailSideMenu.test.tsx +176 -0
- package/Tests/App/Dashboard/ExceptionOverviewCards.test.tsx +366 -0
- package/Tests/App/Dashboard/ExceptionSignalPages.test.tsx +371 -0
- package/Tests/App/Dashboard/ExceptionStackFrameViewer.test.tsx +440 -0
- package/Tests/App/Dashboard/ExceptionSummary.test.tsx +381 -0
- package/Tests/App/Dashboard/ExceptionTriage.test.tsx +306 -0
- package/Tests/App/Dashboard/ExceptionsResourceFacets.test.tsx +604 -0
- package/Tests/App/Dashboard/ExceptionsViewerEntityChips.test.tsx +706 -0
- package/Tests/App/Dashboard/FeedOrdering.test.ts +32 -0
- package/Tests/App/Dashboard/GoogleSecOpsConnectionForm.test.tsx +838 -0
- package/Tests/App/Dashboard/GoogleSecOpsConnectionsErrors.test.tsx +18 -0
- package/Tests/App/Dashboard/GoogleSecOpsDiagnostics.test.tsx +297 -20
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +618 -13
- package/Tests/App/Dashboard/InfrastructureExplorer.test.tsx +254 -207
- package/Tests/App/Dashboard/InfrastructureGraph.test.tsx +303 -135
- package/Tests/App/Dashboard/InvestigationEvidenceList.test.tsx +1251 -0
- package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +303 -0
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +876 -8
- package/Tests/App/Dashboard/InvestigationPanelStatus.test.tsx +35 -0
- package/Tests/App/Dashboard/InvestigationReferenceLink.test.tsx +177 -0
- package/Tests/App/Dashboard/InvestigationReportView.test.tsx +1295 -0
- package/Tests/App/Dashboard/KubernetesImageReferenceView.test.tsx +114 -0
- package/Tests/App/Dashboard/KubernetesLogsTabChips.test.tsx +285 -0
- package/Tests/App/Dashboard/LogsEntityChipNames.test.tsx +316 -0
- package/Tests/App/Dashboard/LogsResourceFacetChips.test.tsx +361 -0
- package/Tests/App/Dashboard/ProfileTableEntityNames.test.tsx +432 -0
- package/Tests/App/Dashboard/ProfileViewEntityName.test.tsx +250 -0
- package/Tests/App/Dashboard/ResourceArchiveSettings.test.tsx +10 -0
- package/Tests/App/Dashboard/ResourceLogsTabsBareLayout.test.tsx +700 -0
- package/Tests/App/Dashboard/ResourcePageEntityNameProps.test.tsx +803 -0
- package/Tests/App/Dashboard/RumSessionReplaySideMenu.test.tsx +11 -1
- package/Tests/App/Dashboard/ScheduledMaintenanceChangeState.test.tsx +1026 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +436 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceOverviewPage.test.tsx +1195 -0
- package/Tests/App/Dashboard/SecurityEventConnectionFormModal.test.tsx +1022 -0
- package/Tests/App/Dashboard/SecurityEventConnectionsTable.test.tsx +905 -0
- package/Tests/App/Dashboard/SecurityEventsSideMenu.test.tsx +247 -0
- package/Tests/App/Dashboard/ServiceMapGraph.test.tsx +375 -274
- package/Tests/App/Dashboard/SessionReplayAuditTable.test.tsx +410 -0
- package/Tests/App/Dashboard/TelemetryResourceRetentionSettings.test.tsx +774 -0
- package/Tests/App/Dashboard/TopErrorsPanelResourceNames.test.tsx +316 -0
- package/Tests/App/Dashboard/TopologyDataLoading.test.tsx +30 -0
- package/Tests/App/Dashboard/TopologyPageNavigation.test.tsx +88 -24
- package/Tests/App/Dashboard/TraceExplorer.test.tsx +1590 -0
- package/Tests/App/Dashboard/TraceWaterfall.test.tsx +488 -0
- package/Tests/App/Dashboard/TracesAnalyticsSeriesLabels.test.tsx +367 -0
- package/Tests/App/Dashboard/TracesEntityNames.test.tsx +585 -0
- package/Tests/App/Dashboard/TracesResourceFacets.test.tsx +613 -0
- package/Tests/App/Dashboard/UseServiceNames.test.tsx +359 -0
- package/Tests/Models/DatabaseModels/DomainRoleTierCoverage.test.ts +7 -0
- package/Tests/Models/InventoryItemNaming.test.ts +132 -16
- package/Tests/Models/SecurityDomainAccessControl.test.ts +61 -17
- package/Tests/Server/API/AIInvestigationAPI.test.ts +611 -2
- package/Tests/Server/API/AIInvestigationEvidenceAPI.test.ts +1098 -0
- package/Tests/Server/API/GoogleSecOpsConnectionAPI.test.ts +409 -0
- package/Tests/Server/API/SecurityEventConnectionAPI.test.ts +856 -0
- package/Tests/Server/API/SessionReplayAPI.test.ts +528 -1
- package/Tests/Server/API/TelemetryFacetsAPI.test.ts +810 -0
- package/Tests/Server/Infrastructure/E2EClickHouseFixtureAccess.test.ts +240 -0
- package/Tests/Server/Middleware/TelemetryIngestBrowserKey.test.ts +229 -0
- package/Tests/Server/Services/InventoryItemDisplayName.test.ts +66 -0
- package/Tests/Server/Services/OpenTelemetryResourceRetentionMemo.test.ts +28 -1
- package/Tests/Server/Services/SecurityEventConnectionService.test.ts +1233 -0
- package/Tests/Server/Services/TelemetryIngestionKeyValidation.test.ts +53 -0
- package/Tests/Server/Services/TelemetryResourceFacetCounting.test.ts +218 -0
- package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +155 -1
- package/Tests/Server/Services/TraceAggregationExceptionScope.test.ts +142 -0
- package/Tests/Server/Services/TraceAnalyticsResourceDisplayNames.test.ts +85 -24
- package/Tests/Server/Types/Database/Permissions/SecurityRoleAccess.test.ts +33 -0
- package/Tests/Server/Utils/AI/InvestigationEvidence.test.ts +1395 -0
- package/Tests/Server/Utils/AI/InvestigationReferences.test.ts +616 -0
- package/Tests/Server/Utils/AI/TimelineTools.test.ts +119 -0
- package/Tests/Server/Utils/AI/Toolbox/AIToolbox.test.ts +151 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +65 -0
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGeneratorExceptionScope.test.ts +224 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorPerSeriesFanout.test.ts +55 -2
- package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.test.ts +1240 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.test.ts +1853 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.test.ts +1129 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.test.ts +1066 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.test.ts +1568 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.test.ts +930 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.test.ts +1409 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.test.ts +763 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.test.ts +1296 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.test.ts +1102 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.test.ts +1174 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.test.ts +1032 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.test.ts +1558 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.test.ts +3041 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.test.ts +1168 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.test.ts +906 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.test.ts +1068 -0
- package/Tests/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.test.ts +1004 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.test.ts +784 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.test.ts +113 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsClient.test.ts +5 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsDiagnostics.test.ts +93 -13
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsHttpIntegration.test.ts +23 -1
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPoller.test.ts +25 -3
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerFailureTaxonomy.test.ts +22 -8
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsPollerHardening.test.ts +82 -16
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsSearchDetections.test.ts +730 -0
- package/Tests/Server/Utils/SecurityEvent/GoogleSecOpsThreePassPoller.test.ts +1577 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayOriginAllowlist.test.ts +41 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayReadServiceQueries.test.ts +1453 -18
- package/Tests/Server/Utils/Telemetry/KubernetesResourceAttributes.test.ts +162 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +460 -13
- package/Tests/Server/Utils/Telemetry/ResourceFacetPlanner.test.ts +678 -0
- package/Tests/Server/Utils/Telemetry/ResourceFacetResolver.test.ts +348 -2
- package/Tests/Server/Utils/Telemetry/ServiceDependencyDiscovery.test.ts +525 -0
- package/Tests/Server/Utils/Telemetry/TraceContextPropagation.test.ts +420 -0
- package/Tests/Types/Rum/SessionReplayApiContracts.test.ts +73 -0
- package/Tests/Types/Rum/SessionReplayTransportContracts.test.ts +95 -0
- package/Tests/Types/SecurityEvent/Connectors/ConnectorDiagnostics.test.ts +82 -0
- package/Tests/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.test.ts +314 -0
- package/Tests/Types/Telemetry/ExceptionSpanScope.test.ts +89 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +96 -0
- package/Tests/Types/Telemetry/ResourceFacetCatalog.test.ts +304 -0
- package/Tests/UI/Components/ActiveFilterChipsLockedFilters.test.tsx +357 -0
- package/Tests/UI/Components/Card.test.tsx +312 -1
- package/Tests/UI/Components/CardModelDetailHeaderLayout.test.tsx +253 -0
- package/Tests/UI/Components/CardModelDetailRefresher.test.tsx +342 -0
- package/Tests/UI/Components/Checkbox.test.tsx +79 -0
- package/Tests/UI/Components/CopyTextButton.test.tsx +142 -0
- package/Tests/UI/Components/DetailCompactStyle.test.tsx +621 -0
- package/Tests/UI/Components/ErrorBoundary.test.tsx +60 -0
- package/Tests/UI/Components/FacetSections.test.tsx +581 -0
- package/Tests/UI/Components/FacetSidebarControls.test.tsx +812 -0
- package/Tests/UI/Components/FacetVisibility.test.ts +429 -0
- package/Tests/UI/Components/LockedFilterActions.test.tsx +396 -0
- package/Tests/UI/Components/LockedFilterChip.test.tsx +651 -0
- package/Tests/UI/Components/LogsEntityNames.test.ts +1769 -0
- package/Tests/UI/Components/LogsEntityNamesComponents.test.tsx +696 -0
- package/Tests/UI/Components/LogsFacetSidebarHiddenFacets.test.tsx +600 -0
- package/Tests/UI/Components/LogsViewerEntityNames.test.tsx +713 -0
- package/Tests/UI/Components/MarkdownInlineReferences.test.tsx +2079 -0
- package/Tests/UI/Components/MemberRoleAssignment.test.tsx +446 -0
- package/Tests/UI/Components/ModelDetailRefetch.test.tsx +397 -0
- package/Tests/UI/Components/ModelListRefetch.test.tsx +233 -0
- package/Tests/UI/Components/ModelPageModelChange.test.tsx +409 -0
- package/Tests/UI/Components/PaginationIntegration.test.tsx +25 -0
- package/Tests/UI/Components/ResourceFacetConfigs.test.ts +172 -0
- package/Tests/UI/Components/SavedViewsSidebarIntegration.test.tsx +32 -14
- package/Tests/UI/Components/TableSortAndMobileColumns.test.tsx +44 -1
- package/Tests/UI/Components/TelemetryActiveFilterChipsLockedFilters.test.tsx +304 -0
- package/Tests/UI/Components/TelemetryFacetSidebarHiddenFacets.test.tsx +718 -0
- package/Tests/UI/Monitor/EvaluationLogList.test.tsx +303 -0
- package/Tests/UI/Monitor/MonitorSummarySnapshotRender.test.tsx +55 -1
- package/Tests/UI/Rum/FidelityNotices.test.ts +22 -0
- package/Tests/UI/Rum/RecordingHealthCard.test.tsx +120 -136
- package/Tests/UI/Rum/RecordingHealthDashboard.test.tsx +988 -0
- package/Tests/UI/Rum/ReplayCard.test.tsx +32 -0
- package/Tests/UI/Rum/ReplayCorrelationPanel.test.tsx +709 -12
- package/Tests/UI/Rum/ReplayHeader.test.tsx +50 -1
- package/Tests/UI/Rum/ReplayPinControl.test.tsx +67 -10
- package/Tests/UI/Rum/ReplayPlayerChrome.test.tsx +41 -12
- package/Tests/UI/Rum/ReplayStage.test.tsx +663 -1
- package/Tests/UI/Rum/ReplayStageOverlays.test.tsx +173 -0
- package/Tests/UI/Rum/ReplayUi.test.tsx +189 -2
- package/Tests/UI/Rum/ReplayUserSessionsMenu.test.tsx +91 -0
- package/Tests/UI/Rum/SessionReplayEmptyState.test.tsx +1 -1
- package/Tests/UI/Rum/SessionReplaySettingsPolicyLoading.test.tsx +585 -0
- package/Tests/UI/Rum/SessionReplayTable.test.tsx +844 -51
- package/Tests/UI/Rum/SessionReplayUsersTable.test.tsx +29 -1
- package/Tests/UI/Telemetry/TelemetryDetailPanel.test.tsx +288 -0
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealBreadcrumbTrails.ts +31 -1
- package/Tests/UI/Utils/Breadcrumb/fixtures/RealRoutePatterns.ts +6 -0
- package/Tests/UI/Utils/Clipboard.test.ts +154 -0
- package/Tests/UI/Utils/Navigation.test.ts +24 -0
- package/Tests/UI/Utils/Telemetry/TelemetryEntityNames.test.ts +2205 -0
- package/Tests/UI/Utils/Telemetry/UseTelemetryEntityNames.test.tsx +844 -0
- package/Tests/Utils/AI/InvestigationReport.test.ts +2430 -0
- package/Tests/Utils/APIOutgoingRequestTracer.test.ts +137 -0
- package/Tests/Utils/Rum/SessionReplayHealthDiagnosis.test.ts +40 -0
- package/Tests/Utils/Rum/SessionReplayRecordingEnded.test.ts +619 -0
- package/Tests/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.test.ts +631 -0
- package/Tests/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.test.ts +479 -0
- package/Tests/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.test.ts +507 -0
- package/Tests/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.test.ts +686 -0
- package/Tests/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.test.ts +474 -0
- package/Tests/Utils/SecurityEvent/Connectors/OktaNormalizer.test.ts +736 -0
- package/Tests/Utils/SecurityEvent/Connectors/SplunkNormalizer.test.ts +407 -0
- package/Tests/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.test.ts +129 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +57 -1
- package/Tests/Utils/Telemetry/EntityRelationship.test.ts +36 -0
- package/Tests/Utils/Telemetry/LockedFilterSearch.test.ts +600 -0
- package/Tests/Utils/Telemetry/NetworkHost.test.ts +48 -0
- package/Tests/Utils/Telemetry/OriginAllowList.test.ts +154 -0
- package/Tests/Utils/Traces/CriticalPath.test.ts +25 -0
- package/Types/AI/AIChatTypes.ts +8 -0
- package/Types/AI/InvestigationEvidence.ts +96 -0
- package/Types/Monitor/MonitorEvaluationSummary.ts +5 -0
- package/Types/Permission.ts +2 -2
- package/Types/Rum/SessionReplay.ts +109 -0
- package/Types/Rum/SessionReplayApi.ts +31 -0
- package/Types/SecurityEvent/Connectors/ConnectorDiagnostics.ts +111 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.ts +87 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.ts +492 -0
- package/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.ts +41 -0
- package/Types/SecurityEvent/GoogleSecOpsDiagnostics.ts +56 -1
- package/Types/SecurityEvent/GoogleSecOpsRegion.ts +34 -0
- package/Types/Telemetry/EntityType.ts +14 -0
- package/Types/Telemetry/ExceptionSpanScope.ts +83 -0
- package/Types/Telemetry/LockedFilterDetail.ts +46 -0
- package/Types/Telemetry/ResourceEntityFacet.ts +17 -10
- package/Types/Telemetry/ResourceFacetCatalog.ts +171 -0
- package/UI/Components/Card/Card.tsx +118 -83
- package/UI/Components/Checkbox/Checkbox.tsx +8 -5
- package/UI/Components/CopyTextButton/CopyTextButton.tsx +7 -1
- package/UI/Components/CustomFields/CustomFieldsDetail.tsx +4 -1
- package/UI/Components/Detail/Detail.tsx +137 -22
- package/UI/Components/ErrorBoundary.tsx +38 -0
- package/UI/Components/LogsViewer/LogsEntityNames.ts +912 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +156 -77
- package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +28 -11
- package/UI/Components/LogsViewer/components/FacetSection.tsx +66 -79
- package/UI/Components/LogsViewer/components/FacetValueRow.tsx +17 -2
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +45 -5
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +100 -19
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +156 -39
- package/UI/Components/LogsViewer/components/LogsTable.tsx +30 -8
- package/UI/Components/LogsViewer/types.ts +6 -0
- package/UI/Components/Markdown.tsx/InlineReferences.tsx +628 -0
- package/UI/Components/Markdown.tsx/MarkdownViewer.tsx +385 -312
- package/UI/Components/MemberRoleAssignment/MemberRoleAssignment.tsx +50 -22
- package/UI/Components/ModelDetail/CardModelDetail.tsx +15 -0
- package/UI/Components/ModelDetail/ModelDetail.tsx +64 -19
- package/UI/Components/ModelList/ModelList.tsx +33 -8
- package/UI/Components/Page/ModelPage.tsx +119 -54
- package/UI/Components/Table/Table.tsx +8 -0
- package/UI/Components/Table/TableRow.tsx +13 -5
- package/UI/Components/TelemetryViewer/FacetVisibility.ts +229 -0
- package/UI/Components/TelemetryViewer/ResourceFacetConfigs.ts +46 -0
- package/UI/Components/TelemetryViewer/TelemetryViewer.tsx +11 -0
- package/UI/Components/TelemetryViewer/components/FacetSearchInput.tsx +79 -0
- package/UI/Components/TelemetryViewer/components/FacetSectionHeader.tsx +69 -0
- package/UI/Components/TelemetryViewer/components/FacetShowMoreButton.tsx +36 -0
- package/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.tsx +88 -0
- package/UI/Components/TelemetryViewer/components/LockedFilterActions.tsx +205 -0
- package/UI/Components/TelemetryViewer/components/LockedFilterChip.tsx +496 -0
- package/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.tsx +30 -10
- package/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.tsx +203 -10
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSection.tsx +60 -73
- package/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.tsx +94 -7
- package/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.tsx +17 -2
- package/UI/Components/TelemetryViewer/types.ts +19 -0
- package/UI/Components/TelemetryViewer/useFacetSearchExemptions.ts +158 -0
- package/UI/Components/TelemetryViewer/useFacetSectionSearch.ts +102 -0
- package/UI/Utils/Clipboard.ts +68 -2
- package/UI/Utils/Navigation.ts +17 -2
- package/UI/Utils/Telemetry/TelemetryEntityNames.ts +620 -0
- package/UI/Utils/Telemetry/UseTelemetryEntityNames.ts +151 -0
- package/Utils/AI/InvestigationReport.ts +1789 -0
- package/Utils/API.ts +90 -1
- package/Utils/Rum/SessionReplayHealth.ts +11 -0
- package/Utils/Rum/SessionReplayRecordingEnded.ts +134 -0
- package/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.ts +427 -0
- package/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.ts +197 -0
- package/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.ts +464 -0
- package/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.ts +567 -0
- package/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.ts +299 -0
- package/Utils/SecurityEvent/Connectors/OktaNormalizer.ts +467 -0
- package/Utils/SecurityEvent/Connectors/SplunkNormalizer.ts +439 -0
- package/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.ts +124 -12
- package/Utils/Telemetry/CrossSignalScope.ts +22 -8
- package/Utils/Telemetry/EntityRelationship.ts +8 -0
- package/Utils/Telemetry/LockedFilterSearch.ts +412 -0
- package/Utils/Telemetry/NetworkHost.ts +43 -0
- package/Utils/Telemetry/OriginAllowList.ts +139 -4
- package/Utils/Traces/CriticalPath.ts +10 -4
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js +1 -1
- package/build/dist/Models/DatabaseModels/RumApplication.js.map +1 -1
- package/build/dist/Models/DatabaseModels/SecurityEventConnection.js +490 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnection.js.map +1 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js +287 -0
- package/build/dist/Models/DatabaseModels/SecurityEventConnectionRun.js.map +1 -0
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js +2 -2
- package/build/dist/Models/DatabaseModels/TelemetryIngestionKey.js.map +1 -1
- package/build/dist/Server/API/AIInvestigationAPI.js +336 -27
- package/build/dist/Server/API/AIInvestigationAPI.js.map +1 -1
- package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js +182 -7
- package/build/dist/Server/API/GoogleSecOpsConnectionAPI.js.map +1 -1
- package/build/dist/Server/API/SecurityEventConnectionAPI.js +171 -0
- package/build/dist/Server/API/SecurityEventConnectionAPI.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js +248 -170
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js +40 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1792700000000-AddSecurityEventConnections.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/TelemetryIngest.js +57 -4
- package/build/dist/Server/Middleware/TelemetryIngest.js.map +1 -1
- package/build/dist/Server/Services/ExceptionAggregationService.js +7 -16
- package/build/dist/Server/Services/ExceptionAggregationService.js.map +1 -1
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js +8 -0
- package/build/dist/Server/Services/GoogleSecOpsConnectionService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/InventoryItemService.js +22 -0
- package/build/dist/Server/Services/InventoryItemService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +8 -17
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/MetricAggregationService.js +4 -14
- package/build/dist/Server/Services/MetricAggregationService.js.map +1 -1
- package/build/dist/Server/Services/SecurityEventConnectionRunService.js +9 -0
- package/build/dist/Server/Services/SecurityEventConnectionRunService.js.map +1 -0
- package/build/dist/Server/Services/SecurityEventConnectionService.js +356 -0
- package/build/dist/Server/Services/SecurityEventConnectionService.js.map +1 -0
- package/build/dist/Server/Services/TraceAggregationService.js +51 -25
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js +3 -0
- package/build/dist/Server/Utils/AI/Chat/ObservabilityAssistant.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js +14 -7
- package/build/dist/Server/Utils/AI/SRE/AIInvestigationEngine.js.map +1 -1
- package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js +468 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationEvidence.js.map +1 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js +230 -0
- package/build/dist/Server/Utils/AI/SRE/InvestigationReferences.js.map +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js +32 -0
- package/build/dist/Server/Utils/AI/Toolbox/Index.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js +6 -2
- package/build/dist/Server/Utils/AI/Toolbox/TimelineTools.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +59 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +5 -3
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js +752 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js +493 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js +352 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ConnectorPlatformHealth.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js +405 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js +466 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/CrowdStrikeFalcon/CrowdStrikeFalconConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js +344 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js +511 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/ElasticSecurity/ElasticSecurityConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js +406 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js +336 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdr/MicrosoftDefenderXdrConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js +474 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js +417 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/MicrosoftSentinel/MicrosoftSentinelConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js +314 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js +494 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Okta/OktaConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js +719 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionPoller.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js +404 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionRunExecutor.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js +209 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectionTester.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js +40 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/SecurityEventConnectorRegistry.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js +494 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkClient.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js +379 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Splunk/SplunkConnector.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js +27 -0
- package/build/dist/Server/Utils/SecurityEvent/Connectors/Types.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js +234 -61
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsClient.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js +537 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsConnectionTester.js.map +1 -0
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js +572 -115
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsPoller.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js +35 -1
- package/build/dist/Server/Utils/SecurityEvent/GoogleSecOps/GoogleSecOpsRunExecutor.js.map +1 -1
- package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js +52 -0
- package/build/dist/Server/Utils/SecurityEvent/SecurityEventDedupe.js.map +1 -0
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js +521 -32
- package/build/dist/Server/Utils/SessionReplay/SessionReplayReadService.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +10 -0
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +8 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js +80 -0
- package/build/dist/Server/Utils/Telemetry/KubernetesResourceAttributes.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +122 -67
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js +146 -0
- package/build/dist/Server/Utils/Telemetry/ResourceFacetPlanner.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js +174 -248
- package/build/dist/Server/Utils/Telemetry/ResourceFacetResolver.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js +492 -0
- package/build/dist/Server/Utils/Telemetry/ServiceDependencyDiscovery.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js +259 -0
- package/build/dist/Server/Utils/Telemetry/TraceContextPropagation.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry.js +11 -5
- package/build/dist/Server/Utils/Telemetry.js.map +1 -1
- package/build/dist/Types/AI/InvestigationEvidence.js +2 -0
- package/build/dist/Types/AI/InvestigationEvidence.js.map +1 -0
- package/build/dist/Types/Permission.js +2 -2
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplay.js +95 -0
- package/build/dist/Types/Rum/SessionReplay.js.map +1 -1
- package/build/dist/Types/Rum/SessionReplayApi.js.map +1 -1
- package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js +18 -0
- package/build/dist/Types/SecurityEvent/Connectors/ConnectorDiagnostics.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js +2 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectionDiagnostics.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js +374 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog.js.map +1 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js +35 -0
- package/build/dist/Types/SecurityEvent/Connectors/SecurityEventConnectorProvider.js.map +1 -0
- package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js +35 -0
- package/build/dist/Types/SecurityEvent/GoogleSecOpsRegion.js.map +1 -0
- package/build/dist/Types/Telemetry/EntityType.js +14 -0
- package/build/dist/Types/Telemetry/EntityType.js.map +1 -1
- package/build/dist/Types/Telemetry/ExceptionSpanScope.js +59 -0
- package/build/dist/Types/Telemetry/ExceptionSpanScope.js.map +1 -0
- package/build/dist/Types/Telemetry/LockedFilterDetail.js +16 -0
- package/build/dist/Types/Telemetry/LockedFilterDetail.js.map +1 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js +16 -10
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -1
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js +119 -0
- package/build/dist/Types/Telemetry/ResourceFacetCatalog.js.map +1 -0
- package/build/dist/UI/Components/Card/Card.js +34 -17
- package/build/dist/UI/Components/Card/Card.js.map +1 -1
- package/build/dist/UI/Components/Checkbox/Checkbox.js +6 -4
- package/build/dist/UI/Components/Checkbox/Checkbox.js.map +1 -1
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +5 -1
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js +1 -1
- package/build/dist/UI/Components/CustomFields/CustomFieldsDetail.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +63 -13
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/UI/Components/ErrorBoundary.js +34 -0
- package/build/dist/UI/Components/ErrorBoundary.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js +535 -0
- package/build/dist/UI/Components/LogsViewer/LogsEntityNames.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +117 -57
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +5 -7
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +31 -44
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +17 -3
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -3
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +64 -18
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +100 -41
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +13 -5
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/types.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js +403 -0
- package/build/dist/UI/Components/Markdown.tsx/InlineReferences.js.map +1 -0
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js +197 -179
- package/build/dist/UI/Components/Markdown.tsx/MarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js +30 -21
- package/build/dist/UI/Components/MemberRoleAssignment/MemberRoleAssignment.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +12 -0
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js +46 -12
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelList/ModelList.js +26 -7
- package/build/dist/UI/Components/ModelList/ModelList.js.map +1 -1
- package/build/dist/UI/Components/Page/ModelPage.js +62 -34
- package/build/dist/UI/Components/Page/ModelPage.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +3 -1
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +8 -1
- package/build/dist/UI/Components/Table/TableRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js +111 -0
- package/build/dist/UI/Components/TelemetryViewer/FacetVisibility.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js +22 -0
- package/build/dist/UI/Components/TelemetryViewer/ResourceFacetConfigs.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js +1 -1
- package/build/dist/UI/Components/TelemetryViewer/TelemetryViewer.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js +36 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSearchInput.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js +21 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetSectionHeader.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js +14 -0
- package/build/dist/UI/Components/TelemetryViewer/components/FacetShowMoreButton.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js +27 -0
- package/build/dist/UI/Components/TelemetryViewer/components/HiddenFacetsFooter.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js +88 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterActions.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js +228 -0
- package/build/dist/UI/Components/TelemetryViewer/components/LockedFilterChip.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js +6 -6
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryActiveFilterChips.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js +111 -12
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryDetailPanel.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js +27 -40
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSection.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js +70 -10
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js +17 -3
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryFacetValueRow.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/types.js +0 -4
- package/build/dist/UI/Components/TelemetryViewer/types.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js +70 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSearchExemptions.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js +52 -0
- package/build/dist/UI/Components/TelemetryViewer/useFacetSectionSearch.js.map +1 -0
- package/build/dist/UI/Utils/Clipboard.js +59 -2
- package/build/dist/UI/Utils/Clipboard.js.map +1 -1
- package/build/dist/UI/Utils/Navigation.js +11 -2
- package/build/dist/UI/Utils/Navigation.js.map +1 -1
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js +471 -0
- package/build/dist/UI/Utils/Telemetry/TelemetryEntityNames.js.map +1 -0
- package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js +113 -0
- package/build/dist/UI/Utils/Telemetry/UseTelemetryEntityNames.js.map +1 -0
- package/build/dist/Utils/AI/InvestigationReport.js +1241 -0
- package/build/dist/Utils/AI/InvestigationReport.js.map +1 -0
- package/build/dist/Utils/API.js +25 -0
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayHealth.js +10 -0
- package/build/dist/Utils/Rum/SessionReplayHealth.js.map +1 -1
- package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js +54 -0
- package/build/dist/Utils/Rum/SessionReplayRecordingEnded.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js +311 -0
- package/build/dist/Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js +150 -0
- package/build/dist/Utils/SecurityEvent/Connectors/CrowdStrikeFalconNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js +328 -0
- package/build/dist/Utils/SecurityEvent/Connectors/ElasticSecurityNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js +405 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftDefenderXdrNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js +212 -0
- package/build/dist/Utils/SecurityEvent/Connectors/MicrosoftSentinelNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js +351 -0
- package/build/dist/Utils/SecurityEvent/Connectors/OktaNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js +348 -0
- package/build/dist/Utils/SecurityEvent/Connectors/SplunkNormalizer.js.map +1 -0
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js +103 -10
- package/build/dist/Utils/SecurityEvent/GoogleSecOpsAlertNormalizer.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +16 -5
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/build/dist/Utils/Telemetry/EntityRelationship.js +7 -0
- package/build/dist/Utils/Telemetry/EntityRelationship.js.map +1 -1
- package/build/dist/Utils/Telemetry/LockedFilterSearch.js +277 -0
- package/build/dist/Utils/Telemetry/LockedFilterSearch.js.map +1 -0
- package/build/dist/Utils/Telemetry/NetworkHost.js +39 -0
- package/build/dist/Utils/Telemetry/NetworkHost.js.map +1 -0
- package/build/dist/Utils/Telemetry/OriginAllowList.js +105 -4
- package/build/dist/Utils/Telemetry/OriginAllowList.js.map +1 -1
- package/build/dist/Utils/Traces/CriticalPath.js +9 -4
- package/build/dist/Utils/Traces/CriticalPath.js.map +1 -1
- package/package.json +1 -1
- package/Tests/UI/Rum/RecordingHealthStrip.test.tsx +0 -600
- package/UI/Components/GanttChart/Bar/BarLabel.tsx +0 -17
- package/UI/Components/GanttChart/Bar/Index.tsx +0 -149
- package/UI/Components/GanttChart/ChartContainer.tsx +0 -50
- package/UI/Components/GanttChart/Index.tsx +0 -90
- package/UI/Components/GanttChart/Row/Index.tsx +0 -22
- package/UI/Components/GanttChart/Row/Row.tsx +0 -231
- package/UI/Components/GanttChart/Row/RowLabel.tsx +0 -30
- package/UI/Components/GanttChart/Rows.tsx +0 -59
- package/UI/Components/GanttChart/Timeline/Index.tsx +0 -46
- package/UI/Components/GanttChart/Timeline/TimelineInterval.tsx +0 -33
- package/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.tsx +0 -43
- package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js +0 -8
- package/build/dist/UI/Components/GanttChart/Bar/BarLabel.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Bar/Index.js +0 -77
- package/build/dist/UI/Components/GanttChart/Bar/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/ChartContainer.js +0 -33
- package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Index.js +0 -39
- package/build/dist/UI/Components/GanttChart/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/Index.js +0 -7
- package/build/dist/UI/Components/GanttChart/Row/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/Row.js +0 -106
- package/build/dist/UI/Components/GanttChart/Row/Row.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Row/RowLabel.js +0 -10
- package/build/dist/UI/Components/GanttChart/Row/RowLabel.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Rows.js +0 -19
- package/build/dist/UI/Components/GanttChart/Rows.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/Index.js +0 -16
- package/build/dist/UI/Components/GanttChart/Timeline/Index.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js +0 -13
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineInterval.js.map +0 -1
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js +0 -13
- package/build/dist/UI/Components/GanttChart/Timeline/TimelineIntervalMarks.js.map +0 -1
package/Tests/Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector.test.ts
ADDED
|
@@ -0,0 +1,1853 @@
|
|
|
1
|
+
import { describe, expect, jest, test } from "@jest/globals";
|
|
2
|
+
import AwsSecurityHubConnector from "../../../../../../Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubConnector";
|
|
3
|
+
import AwsSecurityHubClient, {
|
|
4
|
+
AWS_SECURITY_HUB_SERVICE,
|
|
5
|
+
} from "../../../../../../Server/Utils/SecurityEvent/Connectors/AwsSecurityHub/AwsSecurityHubClient";
|
|
6
|
+
import {
|
|
7
|
+
ConnectorFetchResult,
|
|
8
|
+
SecurityConnectorSettings,
|
|
9
|
+
} from "../../../../../../Server/Utils/SecurityEvent/Connectors/Types";
|
|
10
|
+
import {
|
|
11
|
+
DataSourceHttpRequest,
|
|
12
|
+
DataSourceHttpResponse,
|
|
13
|
+
} from "../../../../../../Server/Utils/DataSource/HttpFetch";
|
|
14
|
+
import APIException from "../../../../../../Types/Exception/ApiException";
|
|
15
|
+
import BadDataException from "../../../../../../Types/Exception/BadDataException";
|
|
16
|
+
import Dictionary from "../../../../../../Types/Dictionary";
|
|
17
|
+
import { JSONObject } from "../../../../../../Types/JSON";
|
|
18
|
+
import { SecurityConnectorCheck } from "../../../../../../Types/SecurityEvent/Connectors/ConnectorDiagnostics";
|
|
19
|
+
import SecurityEventConnectorProvider from "../../../../../../Types/SecurityEvent/Connectors/SecurityEventConnectorProvider";
|
|
20
|
+
import {
|
|
21
|
+
SecurityEventConnectorDefinition,
|
|
22
|
+
getSecurityEventConnectorDefinition,
|
|
23
|
+
} from "../../../../../../Types/SecurityEvent/Connectors/SecurityEventConnectorCatalog";
|
|
24
|
+
import AwsSecurityHubNormalizer from "../../../../../../Utils/SecurityEvent/Connectors/AwsSecurityHubNormalizer";
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* The AWS Security Hub connector as the framework calls it: settings
|
|
28
|
+
* validation keyed by the catalog's field titles, the three test checks
|
|
29
|
+
* and their remediation per AWS failure mode (a signature rejection is an
|
|
30
|
+
* authentication failure, an AccessDenied after a verified signature is a
|
|
31
|
+
* permission failure), and a creation-time fetch that follows NextToken,
|
|
32
|
+
* respects the request and event bounds, accounts rejected and failed
|
|
33
|
+
* records, signs every request with Signature Version 4 over the exact
|
|
34
|
+
* body it sends, and never leaks the secret access key or session token.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const REGION: string = "us-east-1";
|
|
38
|
+
const ACCESS_KEY_ID: string = "AKIAIOSFODNN7EXAMPLE";
|
|
39
|
+
const TEMPORARY_ACCESS_KEY_ID: string = "ASIAIOSFODNN7EXAMPLE";
|
|
40
|
+
const SECRET_ACCESS_KEY: string = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
|
|
41
|
+
const SESSION_TOKEN: string =
|
|
42
|
+
"FwoGZXIvYXdzEBYaDHNlc3Npb24tdG9rZW4tZm9yLXRlc3RzLW9ubHktbm90LXJlYWwtdmFsdWU=";
|
|
43
|
+
const START: Date = new Date("2026-09-12T10:00:00.000Z");
|
|
44
|
+
const END: Date = new Date("2026-09-13T10:00:00.000Z");
|
|
45
|
+
|
|
46
|
+
type Responder = (
|
|
47
|
+
request: DataSourceHttpRequest,
|
|
48
|
+
) => DataSourceHttpResponse | Promise<DataSourceHttpResponse>;
|
|
49
|
+
|
|
50
|
+
interface Harness {
|
|
51
|
+
requests: Array<DataSourceHttpRequest>;
|
|
52
|
+
connector: AwsSecurityHubConnector;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function settings(
|
|
56
|
+
overrides: Partial<SecurityConnectorSettings> = {},
|
|
57
|
+
): SecurityConnectorSettings {
|
|
58
|
+
return {
|
|
59
|
+
provider: SecurityEventConnectorProvider.AwsSecurityHub,
|
|
60
|
+
config: { region: REGION, accessKeyId: ACCESS_KEY_ID },
|
|
61
|
+
secrets: { secretAccessKey: SECRET_ACCESS_KEY },
|
|
62
|
+
alertingOnly: false,
|
|
63
|
+
...overrides,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function temporarySettings(): SecurityConnectorSettings {
|
|
68
|
+
return settings({
|
|
69
|
+
config: { region: REGION, accessKeyId: TEMPORARY_ACCESS_KEY_ID },
|
|
70
|
+
secrets: {
|
|
71
|
+
secretAccessKey: SECRET_ACCESS_KEY,
|
|
72
|
+
sessionToken: SESSION_TOKEN,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function status(code: number, body: string): DataSourceHttpResponse {
|
|
78
|
+
let bodyJson: unknown = undefined;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
bodyJson = JSON.parse(body);
|
|
82
|
+
} catch {
|
|
83
|
+
bodyJson = undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { statusCode: code, bodyText: body, bodyJson, headers: {} };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function awsError(type: string, message: string): string {
|
|
90
|
+
return JSON.stringify({ __type: type, Message: message });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function page(
|
|
94
|
+
findings: Array<JSONObject>,
|
|
95
|
+
nextToken?: string | undefined,
|
|
96
|
+
): DataSourceHttpResponse {
|
|
97
|
+
return status(
|
|
98
|
+
200,
|
|
99
|
+
JSON.stringify({
|
|
100
|
+
Findings: findings,
|
|
101
|
+
...(nextToken ? { NextToken: nextToken } : {}),
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// A GuardDuty finding as GetFindings returns it (ASFF), keyed by suffix.
|
|
107
|
+
function guardDutyFinding(suffix: string): JSONObject {
|
|
108
|
+
return {
|
|
109
|
+
SchemaVersion: "2018-10-08",
|
|
110
|
+
Id: `arn:aws:guardduty:us-east-1:193043430472:detector/d4b040365221be2b54a6264dc9a4bc64/finding/${suffix}`,
|
|
111
|
+
ProductArn: "arn:aws:securityhub:us-east-1::product/aws/guardduty",
|
|
112
|
+
ProductName: "GuardDuty",
|
|
113
|
+
CompanyName: "Amazon",
|
|
114
|
+
GeneratorId:
|
|
115
|
+
"arn:aws:guardduty:us-east-1:193043430472:detector/d4b040365221be2b54a6264dc9a4bc64",
|
|
116
|
+
AwsAccountId: "193043430472",
|
|
117
|
+
Region: "us-east-1",
|
|
118
|
+
Types: ["TTPs/Initial Access/UnauthorizedAccess:EC2-SSHBruteForce"],
|
|
119
|
+
FirstObservedAt: "2026-09-12T12:20:00Z",
|
|
120
|
+
LastObservedAt: "2026-09-12T12:33:49Z",
|
|
121
|
+
CreatedAt: "2026-09-12T12:34:34.146Z",
|
|
122
|
+
UpdatedAt: "2026-09-12T12:34:34.146Z",
|
|
123
|
+
Severity: { Product: 2, Label: "MEDIUM", Normalized: 40 },
|
|
124
|
+
Title: `199.241.229.197 is performing SSH brute force attacks against i-${suffix}.`,
|
|
125
|
+
Description:
|
|
126
|
+
"Brute force attacks are used to gain unauthorized access to your instance by guessing the SSH password.",
|
|
127
|
+
Resources: [
|
|
128
|
+
{
|
|
129
|
+
Type: "AwsEc2Instance",
|
|
130
|
+
Id: `arn:aws:ec2:us-east-1:193043430472:instance/i-${suffix}`,
|
|
131
|
+
Partition: "aws",
|
|
132
|
+
Region: "us-east-1",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
WorkflowState: "NEW",
|
|
136
|
+
Workflow: { Status: "NEW" },
|
|
137
|
+
RecordState: "ACTIVE",
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// A GuardDuty finding created at a chosen moment, for resume-point tests.
|
|
142
|
+
function findingCreatedAt(suffix: string, createdAt: string): JSONObject {
|
|
143
|
+
return { ...guardDutyFinding(suffix), CreatedAt: createdAt };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function withoutCreatedAt(finding: JSONObject): JSONObject {
|
|
147
|
+
const copy: JSONObject = { ...finding };
|
|
148
|
+
delete copy["CreatedAt"];
|
|
149
|
+
return copy;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/*
|
|
153
|
+
* What DataSourceHttpFetch throws for a non-2xx answer in production: the
|
|
154
|
+
* status and a body excerpt, with every response header (including
|
|
155
|
+
* x-amzn-errortype) dropped.
|
|
156
|
+
*/
|
|
157
|
+
function productionHttpError(code: number, body: string): BadDataException {
|
|
158
|
+
return new BadDataException(
|
|
159
|
+
`Data source responded with HTTP ${code}: ${body.substring(0, 500)}`,
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// A Security Hub control finding: Compliance present, so class 2003.
|
|
164
|
+
function controlFinding(): JSONObject {
|
|
165
|
+
return {
|
|
166
|
+
SchemaVersion: "2018-10-08",
|
|
167
|
+
Id: "arn:aws:securityhub:eu-central-1:123456789012:security-control/IAM.9/finding/5a3c1b0e-9b7c-4b1e-8d4a-2b3c4d5e6f70",
|
|
168
|
+
ProductArn: "arn:aws:securityhub:eu-central-1::product/aws/securityhub",
|
|
169
|
+
ProductName: "Security Hub",
|
|
170
|
+
CompanyName: "AWS",
|
|
171
|
+
GeneratorId: "security-control/IAM.9",
|
|
172
|
+
AwsAccountId: "123456789012",
|
|
173
|
+
Region: "eu-central-1",
|
|
174
|
+
Types: [
|
|
175
|
+
"Software and Configuration Checks/Industry and Regulatory Standards",
|
|
176
|
+
],
|
|
177
|
+
FirstObservedAt: "2026-09-12T14:11:09.501Z",
|
|
178
|
+
CreatedAt: "2026-09-12T14:11:09.501Z",
|
|
179
|
+
UpdatedAt: "2026-09-12T14:11:09.501Z",
|
|
180
|
+
Severity: { Label: "CRITICAL", Normalized: 90, Original: "CRITICAL" },
|
|
181
|
+
Title: "IAM.9 MFA should be enabled for the root user",
|
|
182
|
+
Description:
|
|
183
|
+
"This AWS control checks whether your AWS account is enabled to use a multi-factor authentication (MFA) device to sign in with root user credentials.",
|
|
184
|
+
Resources: [
|
|
185
|
+
{
|
|
186
|
+
Type: "AwsAccount",
|
|
187
|
+
Id: "AWS::::Account:123456789012",
|
|
188
|
+
Partition: "aws",
|
|
189
|
+
Region: "eu-central-1",
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
Compliance: {
|
|
193
|
+
Status: "FAILED",
|
|
194
|
+
SecurityControlId: "IAM.9",
|
|
195
|
+
AssociatedStandards: [
|
|
196
|
+
{
|
|
197
|
+
StandardsId:
|
|
198
|
+
"standards/aws-foundational-security-best-practices/v/1.0.0",
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
},
|
|
202
|
+
Workflow: { Status: "NOTIFIED" },
|
|
203
|
+
RecordState: "ACTIVE",
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/*
|
|
208
|
+
* Routes every findings request to scripted responders, consumed in
|
|
209
|
+
* order, so a test can script pages, counts and failures in sequence.
|
|
210
|
+
*/
|
|
211
|
+
function buildHarness(responders: Array<Responder> = []): Harness {
|
|
212
|
+
const requests: Array<DataSourceHttpRequest> = [];
|
|
213
|
+
const queue: Array<Responder> = [...responders];
|
|
214
|
+
|
|
215
|
+
const connector: AwsSecurityHubConnector = new AwsSecurityHubConnector(
|
|
216
|
+
async (request: DataSourceHttpRequest): Promise<DataSourceHttpResponse> => {
|
|
217
|
+
requests.push(request);
|
|
218
|
+
|
|
219
|
+
const responder: Responder | undefined = queue.shift();
|
|
220
|
+
|
|
221
|
+
if (!responder) {
|
|
222
|
+
throw new Error(`Unexpected request to ${request.url}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return responder(request);
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
return { requests, connector };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function checkByKey(
|
|
233
|
+
checks: Array<SecurityConnectorCheck>,
|
|
234
|
+
key: string,
|
|
235
|
+
): SecurityConnectorCheck {
|
|
236
|
+
const check: SecurityConnectorCheck | undefined = checks.find(
|
|
237
|
+
(candidate: SecurityConnectorCheck): boolean => {
|
|
238
|
+
return candidate.key === key;
|
|
239
|
+
},
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (!check) {
|
|
243
|
+
throw new Error(`Missing check ${key}`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return check;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function statuses(checks: Array<SecurityConnectorCheck>): Array<string> {
|
|
250
|
+
return checks.map((check: SecurityConnectorCheck): string => {
|
|
251
|
+
return `${check.key}:${check.status}`;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function parsedBody(request: DataSourceHttpRequest): JSONObject {
|
|
256
|
+
expect(typeof request.body).toBe("string");
|
|
257
|
+
|
|
258
|
+
return JSON.parse(request.body as string) as JSONObject;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function createdAtFilter(body: JSONObject): JSONObject {
|
|
262
|
+
const filters: JSONObject = body["Filters"] as JSONObject;
|
|
263
|
+
const createdAt: Array<JSONObject> = filters[
|
|
264
|
+
"CreatedAt"
|
|
265
|
+
] as Array<JSONObject>;
|
|
266
|
+
|
|
267
|
+
expect(createdAt).toHaveLength(1);
|
|
268
|
+
|
|
269
|
+
return createdAt[0]!;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// YYYYMMDDTHHMMSSZ (x-amz-date) back to a Date.
|
|
273
|
+
function dateFromAmzDate(amzDate: string): Date {
|
|
274
|
+
expect(amzDate).toMatch(/^\d{8}T\d{6}Z$/);
|
|
275
|
+
|
|
276
|
+
return new Date(
|
|
277
|
+
`${amzDate.substring(0, 4)}-${amzDate.substring(4, 6)}-${amzDate.substring(
|
|
278
|
+
6,
|
|
279
|
+
8,
|
|
280
|
+
)}T${amzDate.substring(9, 11)}:${amzDate.substring(11, 13)}:${amzDate.substring(
|
|
281
|
+
13,
|
|
282
|
+
15,
|
|
283
|
+
)}Z`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/*
|
|
288
|
+
* A request is correctly signed when re-signing the exact body and
|
|
289
|
+
* headers it carries, for the date it declares, reproduces its
|
|
290
|
+
* Authorization header byte for byte. This checks the connector wired the
|
|
291
|
+
* region, service, key, secret and token into the signer, and that the
|
|
292
|
+
* transport receives the same string that was hashed.
|
|
293
|
+
*/
|
|
294
|
+
function expectSigned(
|
|
295
|
+
request: DataSourceHttpRequest,
|
|
296
|
+
options: {
|
|
297
|
+
region?: string;
|
|
298
|
+
accessKeyId?: string;
|
|
299
|
+
sessionToken?: string;
|
|
300
|
+
} = {},
|
|
301
|
+
): void {
|
|
302
|
+
const headers: Dictionary<string> = request.headers || {};
|
|
303
|
+
const url: URL = new URL(request.url);
|
|
304
|
+
const region: string = options.region || REGION;
|
|
305
|
+
const accessKeyId: string = options.accessKeyId || ACCESS_KEY_ID;
|
|
306
|
+
|
|
307
|
+
expect(request.method).toBe("POST");
|
|
308
|
+
expect(url.protocol).toBe("https:");
|
|
309
|
+
expect(url.host).toBe(`securityhub.${region}.amazonaws.com`);
|
|
310
|
+
expect(url.pathname).toBe("/findings");
|
|
311
|
+
expect([...url.searchParams.keys()].sort()).toEqual([]);
|
|
312
|
+
expect(headers["Content-Type"]).toBe("application/json");
|
|
313
|
+
|
|
314
|
+
const amzDate: string = headers["X-Amz-Date"] || "";
|
|
315
|
+
const expected: string = AwsSecurityHubClient.signRequest({
|
|
316
|
+
method: "POST",
|
|
317
|
+
host: url.host,
|
|
318
|
+
path: url.pathname,
|
|
319
|
+
headers: { "Content-Type": "application/json" },
|
|
320
|
+
body: request.body as string,
|
|
321
|
+
accessKeyId,
|
|
322
|
+
secretAccessKey: SECRET_ACCESS_KEY,
|
|
323
|
+
sessionToken: options.sessionToken,
|
|
324
|
+
region,
|
|
325
|
+
service: AWS_SECURITY_HUB_SERVICE,
|
|
326
|
+
date: dateFromAmzDate(amzDate),
|
|
327
|
+
}).headers["Authorization"]!;
|
|
328
|
+
|
|
329
|
+
expect(headers["Authorization"]).toBe(expected);
|
|
330
|
+
expect(headers["Authorization"]).toMatch(
|
|
331
|
+
new RegExp(
|
|
332
|
+
`^AWS4-HMAC-SHA256 Credential=${accessKeyId}/\\d{8}/${region}/securityhub/aws4_request, SignedHeaders=content-type;host;x-amz-date${
|
|
333
|
+
options.sessionToken ? ";x-amz-security-token" : ""
|
|
334
|
+
}, Signature=[0-9a-f]{64}$`,
|
|
335
|
+
),
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
if (options.sessionToken) {
|
|
339
|
+
expect(headers["X-Amz-Security-Token"]).toBe(options.sessionToken);
|
|
340
|
+
} else {
|
|
341
|
+
expect(headers["X-Amz-Security-Token"]).toBeUndefined();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function fetchOptions(
|
|
346
|
+
overrides: Partial<{
|
|
347
|
+
maxRequests: number;
|
|
348
|
+
maxEvents: number;
|
|
349
|
+
requestTimeoutInMs: number;
|
|
350
|
+
sampleLimit: number;
|
|
351
|
+
}> = {},
|
|
352
|
+
): {
|
|
353
|
+
maxRequests: number;
|
|
354
|
+
maxEvents: number;
|
|
355
|
+
requestTimeoutInMs: number;
|
|
356
|
+
sampleLimit: number;
|
|
357
|
+
} {
|
|
358
|
+
return {
|
|
359
|
+
maxRequests: 20,
|
|
360
|
+
maxEvents: 10000,
|
|
361
|
+
requestTimeoutInMs: 30000,
|
|
362
|
+
sampleLimit: 5,
|
|
363
|
+
...overrides,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function expectRejection(promise: Promise<unknown>): Promise<Error> {
|
|
368
|
+
try {
|
|
369
|
+
await promise;
|
|
370
|
+
} catch (error) {
|
|
371
|
+
expect(error).toBeInstanceOf(Error);
|
|
372
|
+
return error as Error;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
throw new Error("Expected the call to reject.");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
describe("AwsSecurityHubConnector", () => {
|
|
379
|
+
const definition: SecurityEventConnectorDefinition =
|
|
380
|
+
getSecurityEventConnectorDefinition(
|
|
381
|
+
SecurityEventConnectorProvider.AwsSecurityHub,
|
|
382
|
+
)!;
|
|
383
|
+
|
|
384
|
+
test("registers under the catalog's provider key", () => {
|
|
385
|
+
expect(new AwsSecurityHubConnector().provider).toBe("aws-security-hub");
|
|
386
|
+
expect(definition.provider).toBe(new AwsSecurityHubConnector().provider);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("the catalog's field keys are the ones the connector reads", () => {
|
|
390
|
+
expect(
|
|
391
|
+
definition.configFields.map((field: { key: string }): string => {
|
|
392
|
+
return field.key;
|
|
393
|
+
}),
|
|
394
|
+
).toEqual(["region", "accessKeyId"]);
|
|
395
|
+
expect(
|
|
396
|
+
definition.secretFields.map((field: { key: string }): string => {
|
|
397
|
+
return field.key;
|
|
398
|
+
}),
|
|
399
|
+
).toEqual(["secretAccessKey", "sessionToken"]);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
describe("validateSettings", () => {
|
|
403
|
+
test("accepts a long-lived access key without a session token", () => {
|
|
404
|
+
expect(() => {
|
|
405
|
+
return new AwsSecurityHubConnector().validateSettings(settings());
|
|
406
|
+
}).not.toThrow();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("accepts a temporary access key with its session token", () => {
|
|
410
|
+
expect(() => {
|
|
411
|
+
return new AwsSecurityHubConnector().validateSettings(
|
|
412
|
+
temporarySettings(),
|
|
413
|
+
);
|
|
414
|
+
}).not.toThrow();
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test("accepts a Region in any letter case", () => {
|
|
418
|
+
expect(() => {
|
|
419
|
+
return new AwsSecurityHubConnector().validateSettings(
|
|
420
|
+
settings({
|
|
421
|
+
config: { region: "EU-Central-1", accessKeyId: ACCESS_KEY_ID },
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
}).not.toThrow();
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("refuses settings for another provider", () => {
|
|
428
|
+
expect(() => {
|
|
429
|
+
return new AwsSecurityHubConnector().validateSettings(
|
|
430
|
+
settings({
|
|
431
|
+
provider: SecurityEventConnectorProvider.SplunkEnterpriseSecurity,
|
|
432
|
+
}),
|
|
433
|
+
);
|
|
434
|
+
}).toThrow(
|
|
435
|
+
/^Settings are for provider "splunk", not AWS Security Hub\.$/,
|
|
436
|
+
);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
test("names the catalog field titles in every rejection", () => {
|
|
440
|
+
const connector: AwsSecurityHubConnector = new AwsSecurityHubConnector();
|
|
441
|
+
|
|
442
|
+
expect(() => {
|
|
443
|
+
return connector.validateSettings(
|
|
444
|
+
settings({ config: { accessKeyId: ACCESS_KEY_ID } }),
|
|
445
|
+
);
|
|
446
|
+
}).toThrow(/^Region must be an AWS Region code/);
|
|
447
|
+
|
|
448
|
+
expect(() => {
|
|
449
|
+
return connector.validateSettings(
|
|
450
|
+
settings({
|
|
451
|
+
config: {
|
|
452
|
+
region: "securityhub.us-east-1.amazonaws.com",
|
|
453
|
+
accessKeyId: ACCESS_KEY_ID,
|
|
454
|
+
},
|
|
455
|
+
}),
|
|
456
|
+
);
|
|
457
|
+
}).toThrow(BadDataException);
|
|
458
|
+
|
|
459
|
+
expect(() => {
|
|
460
|
+
return connector.validateSettings(
|
|
461
|
+
settings({ config: { region: REGION } }),
|
|
462
|
+
);
|
|
463
|
+
}).toThrow(/^Access key ID must be 16-128 letters or digits/);
|
|
464
|
+
|
|
465
|
+
expect(() => {
|
|
466
|
+
return connector.validateSettings(
|
|
467
|
+
settings({ config: { region: REGION, accessKeyId: "AKIA/short" } }),
|
|
468
|
+
);
|
|
469
|
+
}).toThrow(BadDataException);
|
|
470
|
+
|
|
471
|
+
expect(() => {
|
|
472
|
+
return connector.validateSettings(settings({ secrets: {} }));
|
|
473
|
+
}).toThrow("Secret access key is required.");
|
|
474
|
+
|
|
475
|
+
expect(() => {
|
|
476
|
+
return connector.validateSettings(
|
|
477
|
+
settings({
|
|
478
|
+
config: { region: REGION, accessKeyId: TEMPORARY_ACCESS_KEY_ID },
|
|
479
|
+
}),
|
|
480
|
+
);
|
|
481
|
+
}).toThrow(
|
|
482
|
+
"Session token is required for a temporary access key (an Access key ID starting with ASIA).",
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
/*
|
|
487
|
+
* Review finding optional-secret-cannot-be-cleared: a connection moved
|
|
488
|
+
* from temporary credentials to a long-lived key keeps its old session
|
|
489
|
+
* token unless someone clears it, and AWS then rejects every request
|
|
490
|
+
* as an invalid security token. The rejection names both fields.
|
|
491
|
+
*/
|
|
492
|
+
test("refuses a long-lived AKIA key combined with a session token, naming both fields", () => {
|
|
493
|
+
const connector: AwsSecurityHubConnector = new AwsSecurityHubConnector();
|
|
494
|
+
let message: string = "";
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
connector.validateSettings(
|
|
498
|
+
settings({
|
|
499
|
+
secrets: {
|
|
500
|
+
secretAccessKey: SECRET_ACCESS_KEY,
|
|
501
|
+
sessionToken: SESSION_TOKEN,
|
|
502
|
+
},
|
|
503
|
+
}),
|
|
504
|
+
);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
expect(error).toBeInstanceOf(BadDataException);
|
|
507
|
+
message = (error as Error).message;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
expect(message).toBe(
|
|
511
|
+
"Session token must be empty for a long-lived access key (an Access key ID starting with AKIA). Remove the stored Session token, or enter the temporary Access key ID (starting with ASIA) that the token was issued with.",
|
|
512
|
+
);
|
|
513
|
+
expect(message).toContain(
|
|
514
|
+
definition.secretFields.find((field: { key: string }): boolean => {
|
|
515
|
+
return field.key === "sessionToken";
|
|
516
|
+
})!.title,
|
|
517
|
+
);
|
|
518
|
+
expect(message).toContain(
|
|
519
|
+
definition.configFields.find((field: { key: string }): boolean => {
|
|
520
|
+
return field.key === "accessKeyId";
|
|
521
|
+
})!.title,
|
|
522
|
+
);
|
|
523
|
+
expect(message).not.toContain(SESSION_TOKEN);
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("accepts a long-lived key whose cleared session token arrives as null or blank", () => {
|
|
527
|
+
const connector: AwsSecurityHubConnector = new AwsSecurityHubConnector();
|
|
528
|
+
|
|
529
|
+
const clearedValues: Array<string | null> = [null, "", " "];
|
|
530
|
+
|
|
531
|
+
for (const cleared of clearedValues) {
|
|
532
|
+
expect(() => {
|
|
533
|
+
return connector.validateSettings(
|
|
534
|
+
settings({
|
|
535
|
+
secrets: {
|
|
536
|
+
secretAccessKey: SECRET_ACCESS_KEY,
|
|
537
|
+
sessionToken: cleared,
|
|
538
|
+
},
|
|
539
|
+
}),
|
|
540
|
+
);
|
|
541
|
+
}).not.toThrow();
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
describe("testConnection", () => {
|
|
547
|
+
test("passes all three checks with one signed probe and two bounded counts", async () => {
|
|
548
|
+
const harness: Harness = buildHarness([
|
|
549
|
+
(): DataSourceHttpResponse => {
|
|
550
|
+
return page([guardDutyFinding("0c10c2c7863d1a356")]);
|
|
551
|
+
},
|
|
552
|
+
(): DataSourceHttpResponse => {
|
|
553
|
+
return page([
|
|
554
|
+
guardDutyFinding("a"),
|
|
555
|
+
guardDutyFinding("b"),
|
|
556
|
+
controlFinding(),
|
|
557
|
+
]);
|
|
558
|
+
},
|
|
559
|
+
(): DataSourceHttpResponse => {
|
|
560
|
+
return page(
|
|
561
|
+
Array.from({ length: 100 }, (_value: unknown, index: number) => {
|
|
562
|
+
return guardDutyFinding(`week-${index}`);
|
|
563
|
+
}),
|
|
564
|
+
"AQICAHhNextPageToken",
|
|
565
|
+
);
|
|
566
|
+
},
|
|
567
|
+
]);
|
|
568
|
+
|
|
569
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
570
|
+
await harness.connector.testConnection(settings(), {
|
|
571
|
+
requestTimeoutInMs: 15000,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
expect(statuses(checks)).toEqual([
|
|
575
|
+
"authentication:pass",
|
|
576
|
+
"read-permission:pass",
|
|
577
|
+
"detections-available:pass",
|
|
578
|
+
]);
|
|
579
|
+
|
|
580
|
+
const authentication: SecurityConnectorCheck = checkByKey(
|
|
581
|
+
checks,
|
|
582
|
+
"authentication",
|
|
583
|
+
);
|
|
584
|
+
expect(authentication.name).toBe("Authenticate with AWS");
|
|
585
|
+
expect(authentication.message).toContain("Signature Version 4");
|
|
586
|
+
expect(authentication.message).toContain(REGION);
|
|
587
|
+
|
|
588
|
+
const read: SecurityConnectorCheck = checkByKey(
|
|
589
|
+
checks,
|
|
590
|
+
"read-permission",
|
|
591
|
+
);
|
|
592
|
+
expect(read.name).toBe("Read findings");
|
|
593
|
+
expect(read.message).toContain("securityhub:GetFindings");
|
|
594
|
+
expect(read.details).toEqual({ region: REGION, sampleFindingCount: 1 });
|
|
595
|
+
|
|
596
|
+
const available: SecurityConnectorCheck = checkByKey(
|
|
597
|
+
checks,
|
|
598
|
+
"detections-available",
|
|
599
|
+
);
|
|
600
|
+
expect(available.name).toBe("Findings available to import");
|
|
601
|
+
expect(available.message).toBe(
|
|
602
|
+
"3 findings created in the last 24 hours and 100+ in the last 7 days.",
|
|
603
|
+
);
|
|
604
|
+
expect(available.details).toEqual({
|
|
605
|
+
createdLast24h: 3,
|
|
606
|
+
hasMoreLast24h: false,
|
|
607
|
+
createdLast7d: 100,
|
|
608
|
+
hasMoreLast7d: true,
|
|
609
|
+
});
|
|
610
|
+
expect(available.remediation).toBeUndefined();
|
|
611
|
+
|
|
612
|
+
// One probe for a single finding, then the two counts at a full page.
|
|
613
|
+
expect(harness.requests).toHaveLength(3);
|
|
614
|
+
expect(parsedBody(harness.requests[0]!)["MaxResults"]).toBe(1);
|
|
615
|
+
expect(parsedBody(harness.requests[1]!)["MaxResults"]).toBe(100);
|
|
616
|
+
expect(parsedBody(harness.requests[2]!)["MaxResults"]).toBe(100);
|
|
617
|
+
|
|
618
|
+
for (const request of harness.requests) {
|
|
619
|
+
expectSigned(request);
|
|
620
|
+
expect(request.timeoutInMs).toBe(15000);
|
|
621
|
+
|
|
622
|
+
const body: JSONObject = parsedBody(request);
|
|
623
|
+
expect(Object.keys(body).sort()).toEqual([
|
|
624
|
+
"Filters",
|
|
625
|
+
"MaxResults",
|
|
626
|
+
"SortCriteria",
|
|
627
|
+
]);
|
|
628
|
+
expect(body["SortCriteria"]).toEqual([
|
|
629
|
+
{ Field: "CreatedAt", SortOrder: "asc" },
|
|
630
|
+
]);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// The probe and the first count cover a day; the second count a week.
|
|
634
|
+
const probeWindow: JSONObject = createdAtFilter(
|
|
635
|
+
parsedBody(harness.requests[0]!),
|
|
636
|
+
);
|
|
637
|
+
const weekWindow: JSONObject = createdAtFilter(
|
|
638
|
+
parsedBody(harness.requests[2]!),
|
|
639
|
+
);
|
|
640
|
+
const dayInMs: number = 24 * 60 * 60 * 1000;
|
|
641
|
+
expect(
|
|
642
|
+
new Date(String(probeWindow["End"])).getTime() -
|
|
643
|
+
new Date(String(probeWindow["Start"])).getTime(),
|
|
644
|
+
).toBe(dayInMs);
|
|
645
|
+
expect(
|
|
646
|
+
new Date(String(weekWindow["End"])).getTime() -
|
|
647
|
+
new Date(String(weekWindow["Start"])).getTime(),
|
|
648
|
+
).toBe(7 * dayInMs);
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
test("uses the singular when exactly one finding was created", async () => {
|
|
652
|
+
const harness: Harness = buildHarness([
|
|
653
|
+
(): DataSourceHttpResponse => {
|
|
654
|
+
return page([]);
|
|
655
|
+
},
|
|
656
|
+
(): DataSourceHttpResponse => {
|
|
657
|
+
return page([guardDutyFinding("only")]);
|
|
658
|
+
},
|
|
659
|
+
(): DataSourceHttpResponse => {
|
|
660
|
+
return page([guardDutyFinding("only")]);
|
|
661
|
+
},
|
|
662
|
+
]);
|
|
663
|
+
|
|
664
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
665
|
+
await harness.connector.testConnection(settings(), {
|
|
666
|
+
requestTimeoutInMs: 1000,
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
expect(checkByKey(checks, "read-permission").message).toContain(
|
|
670
|
+
"no finding was created in the last 24 hours",
|
|
671
|
+
);
|
|
672
|
+
expect(checkByKey(checks, "detections-available").message).toBe(
|
|
673
|
+
"1 finding created in the last 24 hours and 1 in the last 7 days.",
|
|
674
|
+
);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
test("warns instead of passing when nothing was created in the last 7 days", async () => {
|
|
678
|
+
const harness: Harness = buildHarness([
|
|
679
|
+
(): DataSourceHttpResponse => {
|
|
680
|
+
return page([]);
|
|
681
|
+
},
|
|
682
|
+
(): DataSourceHttpResponse => {
|
|
683
|
+
return page([]);
|
|
684
|
+
},
|
|
685
|
+
(): DataSourceHttpResponse => {
|
|
686
|
+
return page([]);
|
|
687
|
+
},
|
|
688
|
+
]);
|
|
689
|
+
|
|
690
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
691
|
+
await harness.connector.testConnection(settings(), {
|
|
692
|
+
requestTimeoutInMs: 1000,
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
expect(statuses(checks)).toEqual([
|
|
696
|
+
"authentication:pass",
|
|
697
|
+
"read-permission:pass",
|
|
698
|
+
"detections-available:warn",
|
|
699
|
+
]);
|
|
700
|
+
|
|
701
|
+
const available: SecurityConnectorCheck = checkByKey(
|
|
702
|
+
checks,
|
|
703
|
+
"detections-available",
|
|
704
|
+
);
|
|
705
|
+
expect(available.message).toContain(
|
|
706
|
+
"No findings were created in the last 7 days",
|
|
707
|
+
);
|
|
708
|
+
expect(available.remediation).toContain("GuardDuty");
|
|
709
|
+
expect(available.remediation).toContain("administrator");
|
|
710
|
+
});
|
|
711
|
+
|
|
712
|
+
test("fails authentication on an invalid setting without contacting AWS", async () => {
|
|
713
|
+
const harness: Harness = buildHarness();
|
|
714
|
+
|
|
715
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
716
|
+
await harness.connector.testConnection(settings({ secrets: {} }), {
|
|
717
|
+
requestTimeoutInMs: 1000,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
expect(statuses(checks)).toEqual([
|
|
721
|
+
"authentication:fail",
|
|
722
|
+
"read-permission:skip",
|
|
723
|
+
"detections-available:skip",
|
|
724
|
+
]);
|
|
725
|
+
expect(checks[0]!.message).toBe("Secret access key is required.");
|
|
726
|
+
expect(checks[0]!.remediation).toContain("Nothing was contacted");
|
|
727
|
+
expect(checks[1]!.message).toContain("configuration is not usable");
|
|
728
|
+
expect(harness.requests).toHaveLength(0);
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
test("fails authentication for a temporary key without its session token, naming the field", async () => {
|
|
732
|
+
const harness: Harness = buildHarness();
|
|
733
|
+
|
|
734
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
735
|
+
await harness.connector.testConnection(
|
|
736
|
+
settings({
|
|
737
|
+
config: { region: REGION, accessKeyId: TEMPORARY_ACCESS_KEY_ID },
|
|
738
|
+
}),
|
|
739
|
+
{ requestTimeoutInMs: 1000 },
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
expect(checks[0]!.status).toBe("fail");
|
|
743
|
+
expect(checks[0]!.message).toContain("Session token is required");
|
|
744
|
+
expect(harness.requests).toHaveLength(0);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test("fails authentication on a rejected signature, skips the rest, and points at IAM security credentials", async () => {
|
|
748
|
+
const harness: Harness = buildHarness([
|
|
749
|
+
(): DataSourceHttpResponse => {
|
|
750
|
+
return status(
|
|
751
|
+
403,
|
|
752
|
+
awsError(
|
|
753
|
+
"InvalidSignatureException",
|
|
754
|
+
`The request signature we calculated does not match the signature you provided. SecretAccessKey=${SECRET_ACCESS_KEY}`,
|
|
755
|
+
),
|
|
756
|
+
);
|
|
757
|
+
},
|
|
758
|
+
]);
|
|
759
|
+
|
|
760
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
761
|
+
await harness.connector.testConnection(settings(), {
|
|
762
|
+
requestTimeoutInMs: 1000,
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
expect(statuses(checks)).toEqual([
|
|
766
|
+
"authentication:fail",
|
|
767
|
+
"read-permission:skip",
|
|
768
|
+
"detections-available:skip",
|
|
769
|
+
]);
|
|
770
|
+
expect(checks[0]!.message).toMatch(
|
|
771
|
+
/^AWS Security Hub findings request failed \(HTTP 403\): /,
|
|
772
|
+
);
|
|
773
|
+
expect(checks[0]!.message).toContain("InvalidSignatureException");
|
|
774
|
+
expect(checks[0]!.message).toContain("Secret access key does not match");
|
|
775
|
+
expect(checks[0]!.message).not.toContain(SECRET_ACCESS_KEY);
|
|
776
|
+
expect(checks[0]!.remediation).toContain("Security credentials");
|
|
777
|
+
expect(checks[1]!.message).toContain("authentication failed");
|
|
778
|
+
expect(harness.requests).toHaveLength(1);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test("fails authentication on an unrecognized access key with the key hint", async () => {
|
|
782
|
+
const harness: Harness = buildHarness([
|
|
783
|
+
(): DataSourceHttpResponse => {
|
|
784
|
+
return status(
|
|
785
|
+
403,
|
|
786
|
+
awsError(
|
|
787
|
+
"UnrecognizedClientException",
|
|
788
|
+
"The security token included in the request is invalid.",
|
|
789
|
+
),
|
|
790
|
+
);
|
|
791
|
+
},
|
|
792
|
+
]);
|
|
793
|
+
|
|
794
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
795
|
+
await harness.connector.testConnection(settings(), {
|
|
796
|
+
requestTimeoutInMs: 1000,
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
expect(statuses(checks)[0]).toBe("authentication:fail");
|
|
800
|
+
expect(checks[0]!.message).toContain(
|
|
801
|
+
"AWS does not recognize the Access key ID",
|
|
802
|
+
);
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
test("fails authentication on expired temporary credentials with the STS remediation", async () => {
|
|
806
|
+
const harness: Harness = buildHarness([
|
|
807
|
+
(): DataSourceHttpResponse => {
|
|
808
|
+
return status(
|
|
809
|
+
403,
|
|
810
|
+
awsError(
|
|
811
|
+
"ExpiredTokenException",
|
|
812
|
+
"The security token included in the request is expired",
|
|
813
|
+
),
|
|
814
|
+
);
|
|
815
|
+
},
|
|
816
|
+
]);
|
|
817
|
+
|
|
818
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
819
|
+
await harness.connector.testConnection(temporarySettings(), {
|
|
820
|
+
requestTimeoutInMs: 1000,
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
expect(statuses(checks)[0]).toBe("authentication:fail");
|
|
824
|
+
expect(checks[0]!.remediation).toContain("expired");
|
|
825
|
+
expect(checks[0]!.remediation).toContain("STS");
|
|
826
|
+
expect(JSON.stringify(checks)).not.toContain(SESSION_TOKEN);
|
|
827
|
+
expect(JSON.stringify(checks)).not.toContain(SECRET_ACCESS_KEY);
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
test("fails authentication on a skewed request time with the NTP remediation", async () => {
|
|
831
|
+
const harness: Harness = buildHarness([
|
|
832
|
+
(): DataSourceHttpResponse => {
|
|
833
|
+
return status(
|
|
834
|
+
403,
|
|
835
|
+
awsError(
|
|
836
|
+
"RequestTimeTooSkewed",
|
|
837
|
+
"The difference between the request time and the current time is too large.",
|
|
838
|
+
),
|
|
839
|
+
);
|
|
840
|
+
},
|
|
841
|
+
]);
|
|
842
|
+
|
|
843
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
844
|
+
await harness.connector.testConnection(settings(), {
|
|
845
|
+
requestTimeoutInMs: 1000,
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
expect(statuses(checks)[0]).toBe("authentication:fail");
|
|
849
|
+
expect(checks[0]!.remediation).toContain("NTP");
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test("passes authentication but fails read-permission on AccessDeniedException, naming securityhub:GetFindings", async () => {
|
|
853
|
+
const harness: Harness = buildHarness([
|
|
854
|
+
(): DataSourceHttpResponse => {
|
|
855
|
+
return status(
|
|
856
|
+
403,
|
|
857
|
+
awsError(
|
|
858
|
+
"AccessDeniedException",
|
|
859
|
+
"User: arn:aws:iam::123456789012:user/oneuptime is not authorized to perform: securityhub:GetFindings on resource: arn:aws:securityhub:us-east-1:123456789012:hub/default because no identity-based policy allows the securityhub:GetFindings action",
|
|
860
|
+
),
|
|
861
|
+
);
|
|
862
|
+
},
|
|
863
|
+
]);
|
|
864
|
+
|
|
865
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
866
|
+
await harness.connector.testConnection(settings(), {
|
|
867
|
+
requestTimeoutInMs: 1000,
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
expect(statuses(checks)).toEqual([
|
|
871
|
+
"authentication:pass",
|
|
872
|
+
"read-permission:fail",
|
|
873
|
+
"detections-available:skip",
|
|
874
|
+
]);
|
|
875
|
+
expect(checkByKey(checks, "authentication").message).toContain(
|
|
876
|
+
"verified the request signature",
|
|
877
|
+
);
|
|
878
|
+
|
|
879
|
+
const read: SecurityConnectorCheck = checkByKey(
|
|
880
|
+
checks,
|
|
881
|
+
"read-permission",
|
|
882
|
+
);
|
|
883
|
+
expect(read.message).toMatch(
|
|
884
|
+
/^AWS Security Hub findings request failed \(HTTP 403\): /,
|
|
885
|
+
);
|
|
886
|
+
expect(read.message).toContain("AccessDeniedException");
|
|
887
|
+
expect(read.remediation).toContain("securityhub:GetFindings");
|
|
888
|
+
expect(read.remediation).toContain("AWSSecurityHubReadOnlyAccess");
|
|
889
|
+
expect(read.remediation).toContain("delegated administrator");
|
|
890
|
+
expect(checkByKey(checks, "detections-available").message).toContain(
|
|
891
|
+
"findings could not be read",
|
|
892
|
+
);
|
|
893
|
+
expect(harness.requests).toHaveLength(1);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
test("fails read-permission on InvalidAccessException with the enable-Security-Hub remediation", async () => {
|
|
897
|
+
const harness: Harness = buildHarness([
|
|
898
|
+
(): DataSourceHttpResponse => {
|
|
899
|
+
return status(
|
|
900
|
+
403,
|
|
901
|
+
awsError(
|
|
902
|
+
"InvalidAccessException",
|
|
903
|
+
"Account 123456789012 is not subscribed to AWS Security Hub",
|
|
904
|
+
),
|
|
905
|
+
);
|
|
906
|
+
},
|
|
907
|
+
]);
|
|
908
|
+
|
|
909
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
910
|
+
await harness.connector.testConnection(settings(), {
|
|
911
|
+
requestTimeoutInMs: 1000,
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
expect(statuses(checks)).toEqual([
|
|
915
|
+
"authentication:pass",
|
|
916
|
+
"read-permission:fail",
|
|
917
|
+
"detections-available:skip",
|
|
918
|
+
]);
|
|
919
|
+
const read: SecurityConnectorCheck = checkByKey(
|
|
920
|
+
checks,
|
|
921
|
+
"read-permission",
|
|
922
|
+
);
|
|
923
|
+
expect(read.remediation).toContain("Enable Security Hub in this Region");
|
|
924
|
+
expect(read.remediation).toContain("delegated administrator");
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test("fails authentication with the Region remediation when no endpoint answers (404)", async () => {
|
|
928
|
+
const harness: Harness = buildHarness([
|
|
929
|
+
(): DataSourceHttpResponse => {
|
|
930
|
+
return status(404, "<html><title>404 Not Found</title></html>");
|
|
931
|
+
},
|
|
932
|
+
]);
|
|
933
|
+
|
|
934
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
935
|
+
await harness.connector.testConnection(settings(), {
|
|
936
|
+
requestTimeoutInMs: 1000,
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
expect(statuses(checks)[0]).toBe("authentication:fail");
|
|
940
|
+
expect(checks[0]!.message).toMatch(
|
|
941
|
+
/^AWS Security Hub findings request failed \(HTTP 404\): /,
|
|
942
|
+
);
|
|
943
|
+
expect(checks[0]!.remediation).toContain("Region");
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
test("fails authentication with the throttling remediation when the probe is throttled (429)", async () => {
|
|
947
|
+
const harness: Harness = buildHarness([
|
|
948
|
+
(): DataSourceHttpResponse => {
|
|
949
|
+
return status(
|
|
950
|
+
429,
|
|
951
|
+
awsError("LimitExceededException", "Rate exceeded"),
|
|
952
|
+
);
|
|
953
|
+
},
|
|
954
|
+
]);
|
|
955
|
+
|
|
956
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
957
|
+
await harness.connector.testConnection(settings(), {
|
|
958
|
+
requestTimeoutInMs: 1000,
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
expect(statuses(checks)[0]).toBe("authentication:fail");
|
|
962
|
+
expect(checks[0]!.remediation).toContain("throttling");
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
test("fails detections-available on a throttled count and keeps the earlier passes", async () => {
|
|
966
|
+
const harness: Harness = buildHarness([
|
|
967
|
+
(): DataSourceHttpResponse => {
|
|
968
|
+
return page([guardDutyFinding("probe")]);
|
|
969
|
+
},
|
|
970
|
+
(): DataSourceHttpResponse => {
|
|
971
|
+
return status(
|
|
972
|
+
429,
|
|
973
|
+
awsError("LimitExceededException", "Rate exceeded"),
|
|
974
|
+
);
|
|
975
|
+
},
|
|
976
|
+
]);
|
|
977
|
+
|
|
978
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
979
|
+
await harness.connector.testConnection(settings(), {
|
|
980
|
+
requestTimeoutInMs: 1000,
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
expect(statuses(checks)).toEqual([
|
|
984
|
+
"authentication:pass",
|
|
985
|
+
"read-permission:pass",
|
|
986
|
+
"detections-available:fail",
|
|
987
|
+
]);
|
|
988
|
+
const available: SecurityConnectorCheck = checkByKey(
|
|
989
|
+
checks,
|
|
990
|
+
"detections-available",
|
|
991
|
+
);
|
|
992
|
+
expect(available.message).toMatch(
|
|
993
|
+
/^AWS Security Hub findings request failed \(HTTP 429\): /,
|
|
994
|
+
);
|
|
995
|
+
expect(available.remediation).toContain("throttling");
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
test("fails detections-available on a server-side failure with the retry remediation", async () => {
|
|
999
|
+
const harness: Harness = buildHarness([
|
|
1000
|
+
(): DataSourceHttpResponse => {
|
|
1001
|
+
return page([]);
|
|
1002
|
+
},
|
|
1003
|
+
(): DataSourceHttpResponse => {
|
|
1004
|
+
return page([]);
|
|
1005
|
+
},
|
|
1006
|
+
(): DataSourceHttpResponse => {
|
|
1007
|
+
return status(
|
|
1008
|
+
500,
|
|
1009
|
+
awsError("InternalException", "Internal service error"),
|
|
1010
|
+
);
|
|
1011
|
+
},
|
|
1012
|
+
]);
|
|
1013
|
+
|
|
1014
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1015
|
+
await harness.connector.testConnection(settings(), {
|
|
1016
|
+
requestTimeoutInMs: 1000,
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
const available: SecurityConnectorCheck = checkByKey(
|
|
1020
|
+
checks,
|
|
1021
|
+
"detections-available",
|
|
1022
|
+
);
|
|
1023
|
+
expect(available.status).toBe("fail");
|
|
1024
|
+
expect(available.message).toMatch(
|
|
1025
|
+
/^AWS Security Hub findings request failed \(HTTP 500\): /,
|
|
1026
|
+
);
|
|
1027
|
+
expect(available.remediation).toContain("server-side");
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
test("fails authentication with the connectivity remediation when the transport never answers", async () => {
|
|
1031
|
+
const harness: Harness = buildHarness([
|
|
1032
|
+
(): Promise<DataSourceHttpResponse> => {
|
|
1033
|
+
return new Promise<DataSourceHttpResponse>((): void => {
|
|
1034
|
+
// Never settles; the client's own deadline must fire.
|
|
1035
|
+
});
|
|
1036
|
+
},
|
|
1037
|
+
]);
|
|
1038
|
+
|
|
1039
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1040
|
+
await harness.connector.testConnection(settings(), {
|
|
1041
|
+
requestTimeoutInMs: 50,
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
expect(statuses(checks)).toEqual([
|
|
1045
|
+
"authentication:fail",
|
|
1046
|
+
"read-permission:skip",
|
|
1047
|
+
"detections-available:skip",
|
|
1048
|
+
]);
|
|
1049
|
+
expect(checks[0]!.message).toBe(
|
|
1050
|
+
"AWS Security Hub findings request timed out after 1 seconds with no response.",
|
|
1051
|
+
);
|
|
1052
|
+
expect(checks[0]!.remediation).toContain("outbound HTTPS");
|
|
1053
|
+
expect(checks[0]!.remediation).toContain(
|
|
1054
|
+
"securityhub.<region>.amazonaws.com",
|
|
1055
|
+
);
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
test("fails authentication with the connectivity remediation when the production transport cannot reach AWS", async () => {
|
|
1059
|
+
const harness: Harness = buildHarness([
|
|
1060
|
+
(): DataSourceHttpResponse => {
|
|
1061
|
+
throw new BadDataException(
|
|
1062
|
+
"Could not reach data source: timeout of 1000ms exceeded",
|
|
1063
|
+
);
|
|
1064
|
+
},
|
|
1065
|
+
]);
|
|
1066
|
+
|
|
1067
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1068
|
+
await harness.connector.testConnection(settings(), {
|
|
1069
|
+
requestTimeoutInMs: 1000,
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
expect(statuses(checks)[0]).toBe("authentication:fail");
|
|
1073
|
+
expect(checks[0]!.message).toBe(
|
|
1074
|
+
"AWS Security Hub findings request failed: Could not reach data source: timeout of 1000ms exceeded",
|
|
1075
|
+
);
|
|
1076
|
+
expect(checks[0]!.remediation).toContain("outbound HTTPS");
|
|
1077
|
+
expect(checks[0]!.remediation).not.toContain("IAM > Users");
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
test("fails authentication when the endpoint answers something other than JSON", async () => {
|
|
1081
|
+
const harness: Harness = buildHarness([
|
|
1082
|
+
(): DataSourceHttpResponse => {
|
|
1083
|
+
return status(
|
|
1084
|
+
200,
|
|
1085
|
+
"<!DOCTYPE html><html><title>Proxy</title></html>",
|
|
1086
|
+
);
|
|
1087
|
+
},
|
|
1088
|
+
]);
|
|
1089
|
+
|
|
1090
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1091
|
+
await harness.connector.testConnection(settings(), {
|
|
1092
|
+
requestTimeoutInMs: 1000,
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
expect(statuses(checks)[0]).toBe("authentication:fail");
|
|
1096
|
+
expect(checks[0]!.message).toMatch(
|
|
1097
|
+
/^AWS Security Hub findings request returned a non-JSON body: /,
|
|
1098
|
+
);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
test("fails before contacting AWS for a long-lived key saved with a session token", async () => {
|
|
1102
|
+
const harness: Harness = buildHarness();
|
|
1103
|
+
|
|
1104
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1105
|
+
await harness.connector.testConnection(
|
|
1106
|
+
settings({
|
|
1107
|
+
secrets: {
|
|
1108
|
+
secretAccessKey: SECRET_ACCESS_KEY,
|
|
1109
|
+
sessionToken: SESSION_TOKEN,
|
|
1110
|
+
},
|
|
1111
|
+
}),
|
|
1112
|
+
{ requestTimeoutInMs: 1000 },
|
|
1113
|
+
);
|
|
1114
|
+
|
|
1115
|
+
expect(statuses(checks)).toEqual([
|
|
1116
|
+
"authentication:fail",
|
|
1117
|
+
"read-permission:skip",
|
|
1118
|
+
"detections-available:skip",
|
|
1119
|
+
]);
|
|
1120
|
+
expect(checks[0]!.message).toContain("Session token must be empty");
|
|
1121
|
+
expect(JSON.stringify(checks)).not.toContain(SESSION_TOKEN);
|
|
1122
|
+
expect(harness.requests).toHaveLength(0);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
/*
|
|
1126
|
+
* Review finding aws-signature-errors-reported-as-iam-permission. The
|
|
1127
|
+
* production transport keeps only the status and body of a failed
|
|
1128
|
+
* answer, so AWS's x-amzn-errortype header never arrives and a
|
|
1129
|
+
* signature rejection's body is just {"message": ...}. Those used to
|
|
1130
|
+
* pass authentication ("the access key and secret are valid") and fail
|
|
1131
|
+
* read-permission with IAM guidance. They are authentication failures.
|
|
1132
|
+
*/
|
|
1133
|
+
describe("with the production transport's header-less HTTP errors", () => {
|
|
1134
|
+
function probeFailsWith(code: number, body: JSONObject): Harness {
|
|
1135
|
+
return buildHarness([
|
|
1136
|
+
(): DataSourceHttpResponse => {
|
|
1137
|
+
throw productionHttpError(code, JSON.stringify(body));
|
|
1138
|
+
},
|
|
1139
|
+
]);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
test.each<[string, string]>([
|
|
1143
|
+
[
|
|
1144
|
+
"a wrong secret",
|
|
1145
|
+
"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.",
|
|
1146
|
+
],
|
|
1147
|
+
[
|
|
1148
|
+
"an unknown access key or a stray session token",
|
|
1149
|
+
"The security token included in the request is invalid.",
|
|
1150
|
+
],
|
|
1151
|
+
])(
|
|
1152
|
+
"fails authentication for %s and never calls the credentials valid",
|
|
1153
|
+
async (_label: string, awsMessage: string) => {
|
|
1154
|
+
const harness: Harness = probeFailsWith(403, { message: awsMessage });
|
|
1155
|
+
|
|
1156
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1157
|
+
await harness.connector.testConnection(settings(), {
|
|
1158
|
+
requestTimeoutInMs: 1000,
|
|
1159
|
+
});
|
|
1160
|
+
|
|
1161
|
+
expect(statuses(checks)).toEqual([
|
|
1162
|
+
"authentication:fail",
|
|
1163
|
+
"read-permission:skip",
|
|
1164
|
+
"detections-available:skip",
|
|
1165
|
+
]);
|
|
1166
|
+
|
|
1167
|
+
const authentication: SecurityConnectorCheck = checks[0]!;
|
|
1168
|
+
expect(authentication.message).toMatch(
|
|
1169
|
+
/^AWS Security Hub findings request failed \(HTTP 403\): /,
|
|
1170
|
+
);
|
|
1171
|
+
expect(authentication.message).toContain(awsMessage);
|
|
1172
|
+
expect(authentication.message).not.toContain("credentials are valid");
|
|
1173
|
+
expect(authentication.message).not.toContain(
|
|
1174
|
+
"securityhub:GetFindings",
|
|
1175
|
+
);
|
|
1176
|
+
expect(authentication.remediation).toContain("Security credentials");
|
|
1177
|
+
expect(authentication.remediation).not.toContain(
|
|
1178
|
+
"AWSSecurityHubReadOnlyAccess",
|
|
1179
|
+
);
|
|
1180
|
+
expect(JSON.stringify(checks)).not.toContain(
|
|
1181
|
+
"verified the request signature",
|
|
1182
|
+
);
|
|
1183
|
+
expect(checks[1]!.message).toContain("authentication failed");
|
|
1184
|
+
},
|
|
1185
|
+
);
|
|
1186
|
+
|
|
1187
|
+
test("fails authentication with the clock remediation for an expired signature", async () => {
|
|
1188
|
+
const harness: Harness = probeFailsWith(403, {
|
|
1189
|
+
message:
|
|
1190
|
+
"Signature expired: 20260913T100507Z is now earlier than 20260913T101000Z (20260913T101500Z - 5 min.)",
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1194
|
+
await harness.connector.testConnection(settings(), {
|
|
1195
|
+
requestTimeoutInMs: 1000,
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
expect(statuses(checks)[0]).toBe("authentication:fail");
|
|
1199
|
+
expect(checks[0]!.message).toContain("Fix NTP");
|
|
1200
|
+
expect(checks[0]!.remediation).toContain("NTP");
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
test("fails authentication with the STS remediation for an expired session token", async () => {
|
|
1204
|
+
const harness: Harness = probeFailsWith(403, {
|
|
1205
|
+
message: "The security token included in the request is expired",
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1209
|
+
await harness.connector.testConnection(temporarySettings(), {
|
|
1210
|
+
requestTimeoutInMs: 1000,
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
expect(statuses(checks)[0]).toBe("authentication:fail");
|
|
1214
|
+
expect(checks[0]!.remediation).toContain("STS");
|
|
1215
|
+
expect(JSON.stringify(checks)).not.toContain(SESSION_TOKEN);
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
test("fails authentication on a 403 that names no error code, pointing at the credentials and then IAM", async () => {
|
|
1219
|
+
const harness: Harness = probeFailsWith(403, {
|
|
1220
|
+
message: "Forbidden",
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1224
|
+
await harness.connector.testConnection(settings(), {
|
|
1225
|
+
requestTimeoutInMs: 1000,
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
expect(statuses(checks)).toEqual([
|
|
1229
|
+
"authentication:fail",
|
|
1230
|
+
"read-permission:skip",
|
|
1231
|
+
"detections-available:skip",
|
|
1232
|
+
]);
|
|
1233
|
+
expect(checks[0]!.message).toContain("without naming an error code");
|
|
1234
|
+
expect(checks[0]!.message).not.toContain("credentials are valid");
|
|
1235
|
+
expect(checks[0]!.remediation).toContain("without an error code");
|
|
1236
|
+
expect(checks[0]!.remediation).toContain("Secret access key");
|
|
1237
|
+
expect(checks[0]!.remediation).toContain("securityhub:GetFindings");
|
|
1238
|
+
});
|
|
1239
|
+
|
|
1240
|
+
test("still passes authentication and fails read-permission when AWS's wording names an IAM denial", async () => {
|
|
1241
|
+
const harness: Harness = probeFailsWith(403, {
|
|
1242
|
+
Message:
|
|
1243
|
+
"User: arn:aws:iam::123456789012:user/oneuptime is not authorized to perform: securityhub:GetFindings on resource: arn:aws:securityhub:us-east-1:123456789012:hub/default",
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1247
|
+
await harness.connector.testConnection(settings(), {
|
|
1248
|
+
requestTimeoutInMs: 1000,
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1251
|
+
expect(statuses(checks)).toEqual([
|
|
1252
|
+
"authentication:pass",
|
|
1253
|
+
"read-permission:fail",
|
|
1254
|
+
"detections-available:skip",
|
|
1255
|
+
]);
|
|
1256
|
+
expect(checkByKey(checks, "read-permission").remediation).toContain(
|
|
1257
|
+
"AWSSecurityHubReadOnlyAccess",
|
|
1258
|
+
);
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
test("passes authentication and fails read-permission on InvalidAccessException, which the API reference lists as HTTP 401", async () => {
|
|
1262
|
+
const harness: Harness = probeFailsWith(401, {
|
|
1263
|
+
Code: "InvalidAccessException",
|
|
1264
|
+
Message: "Account 123456789012 is not subscribed to AWS Security Hub",
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1268
|
+
await harness.connector.testConnection(settings(), {
|
|
1269
|
+
requestTimeoutInMs: 1000,
|
|
1270
|
+
});
|
|
1271
|
+
|
|
1272
|
+
expect(statuses(checks)).toEqual([
|
|
1273
|
+
"authentication:pass",
|
|
1274
|
+
"read-permission:fail",
|
|
1275
|
+
"detections-available:skip",
|
|
1276
|
+
]);
|
|
1277
|
+
expect(checkByKey(checks, "read-permission").remediation).toContain(
|
|
1278
|
+
"Enable Security Hub in this Region",
|
|
1279
|
+
);
|
|
1280
|
+
});
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
test("signs and sends the session token on every request for temporary credentials, and never prints it", async () => {
|
|
1284
|
+
const harness: Harness = buildHarness([
|
|
1285
|
+
(): DataSourceHttpResponse => {
|
|
1286
|
+
return page([guardDutyFinding("probe")]);
|
|
1287
|
+
},
|
|
1288
|
+
(): DataSourceHttpResponse => {
|
|
1289
|
+
return page([guardDutyFinding("a")]);
|
|
1290
|
+
},
|
|
1291
|
+
(): DataSourceHttpResponse => {
|
|
1292
|
+
return page([guardDutyFinding("a"), guardDutyFinding("b")]);
|
|
1293
|
+
},
|
|
1294
|
+
]);
|
|
1295
|
+
|
|
1296
|
+
const checks: Array<SecurityConnectorCheck> =
|
|
1297
|
+
await harness.connector.testConnection(temporarySettings(), {
|
|
1298
|
+
requestTimeoutInMs: 1000,
|
|
1299
|
+
});
|
|
1300
|
+
|
|
1301
|
+
expect(statuses(checks)).toEqual([
|
|
1302
|
+
"authentication:pass",
|
|
1303
|
+
"read-permission:pass",
|
|
1304
|
+
"detections-available:pass",
|
|
1305
|
+
]);
|
|
1306
|
+
expect(harness.requests).toHaveLength(3);
|
|
1307
|
+
|
|
1308
|
+
for (const request of harness.requests) {
|
|
1309
|
+
expectSigned(request, {
|
|
1310
|
+
accessKeyId: TEMPORARY_ACCESS_KEY_ID,
|
|
1311
|
+
sessionToken: SESSION_TOKEN,
|
|
1312
|
+
});
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
for (const check of checks) {
|
|
1316
|
+
expect(JSON.stringify(check)).not.toContain(SESSION_TOKEN);
|
|
1317
|
+
expect(JSON.stringify(check)).not.toContain(SECRET_ACCESS_KEY);
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
describe("fetchEvents", () => {
|
|
1323
|
+
test("reads the window by creation time, normalizes with the catalog's vendor and product, and builds samples", async () => {
|
|
1324
|
+
const harness: Harness = buildHarness([
|
|
1325
|
+
(): DataSourceHttpResponse => {
|
|
1326
|
+
return page([
|
|
1327
|
+
guardDutyFinding("0c10c2c7863d1a356"),
|
|
1328
|
+
controlFinding(),
|
|
1329
|
+
guardDutyFinding("0fedcba9876543210"),
|
|
1330
|
+
]);
|
|
1331
|
+
},
|
|
1332
|
+
]);
|
|
1333
|
+
|
|
1334
|
+
const result: ConnectorFetchResult = await harness.connector.fetchEvents(
|
|
1335
|
+
settings(),
|
|
1336
|
+
{ startTime: START, endTime: END },
|
|
1337
|
+
fetchOptions({ sampleLimit: 2 }),
|
|
1338
|
+
);
|
|
1339
|
+
|
|
1340
|
+
expect(result.complete).toBe(true);
|
|
1341
|
+
// A complete read names no resume point; the poller uses the window end.
|
|
1342
|
+
expect(result.resumeAfter).toBeUndefined();
|
|
1343
|
+
expect(result.warnings).toEqual([]);
|
|
1344
|
+
expect(result.requestCount).toBe(1);
|
|
1345
|
+
expect(result.fetchedCount).toBe(3);
|
|
1346
|
+
expect(result.rejectedCount).toBe(0);
|
|
1347
|
+
expect(result.failedCount).toBe(0);
|
|
1348
|
+
expect(result.events).toHaveLength(3);
|
|
1349
|
+
|
|
1350
|
+
for (const event of result.events) {
|
|
1351
|
+
expect(event.vendorName).toBe(definition.vendorName);
|
|
1352
|
+
expect(event.productName).toBe(definition.productName);
|
|
1353
|
+
expect(event.vendorName).toBe("Amazon Web Services");
|
|
1354
|
+
expect(event.productName).toBe("AWS Security Hub");
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
expect(
|
|
1358
|
+
result.events.map((event: { classUid: number }): number => {
|
|
1359
|
+
return event.classUid;
|
|
1360
|
+
}),
|
|
1361
|
+
).toEqual([2004, 2003, 2004]);
|
|
1362
|
+
expect(result.events[1]!.className).toBe("Compliance Finding");
|
|
1363
|
+
expect(result.events[0]!.className).toBe("Detection Finding");
|
|
1364
|
+
|
|
1365
|
+
expect(
|
|
1366
|
+
result.events.map((event: { eventUid: string }): string => {
|
|
1367
|
+
return event.eventUid;
|
|
1368
|
+
}),
|
|
1369
|
+
).toEqual([
|
|
1370
|
+
"arn:aws:guardduty:us-east-1:193043430472:detector/d4b040365221be2b54a6264dc9a4bc64/finding/0c10c2c7863d1a356",
|
|
1371
|
+
"arn:aws:securityhub:eu-central-1:123456789012:security-control/IAM.9/finding/5a3c1b0e-9b7c-4b1e-8d4a-2b3c4d5e6f70",
|
|
1372
|
+
"arn:aws:guardduty:us-east-1:193043430472:detector/d4b040365221be2b54a6264dc9a4bc64/finding/0fedcba9876543210",
|
|
1373
|
+
]);
|
|
1374
|
+
|
|
1375
|
+
expect(result.samples).toHaveLength(2);
|
|
1376
|
+
expect(result.samples[0]).toEqual({
|
|
1377
|
+
id: "arn:aws:guardduty:us-east-1:193043430472:detector/d4b040365221be2b54a6264dc9a4bc64/finding/0c10c2c7863d1a356",
|
|
1378
|
+
title:
|
|
1379
|
+
"199.241.229.197 is performing SSH brute force attacks against i-0c10c2c7863d1a356.",
|
|
1380
|
+
severity: "Medium",
|
|
1381
|
+
createdTime: "2026-09-12T12:34:34.146Z",
|
|
1382
|
+
eventTime: "2026-09-12T12:20:00Z",
|
|
1383
|
+
});
|
|
1384
|
+
expect(result.samples[1]!.severity).toBe("Critical");
|
|
1385
|
+
|
|
1386
|
+
const request: DataSourceHttpRequest = harness.requests[0]!;
|
|
1387
|
+
expectSigned(request);
|
|
1388
|
+
expect(request.timeoutInMs).toBe(30000);
|
|
1389
|
+
|
|
1390
|
+
const body: JSONObject = parsedBody(request);
|
|
1391
|
+
expect(Object.keys(body).sort()).toEqual([
|
|
1392
|
+
"Filters",
|
|
1393
|
+
"MaxResults",
|
|
1394
|
+
"SortCriteria",
|
|
1395
|
+
]);
|
|
1396
|
+
expect(createdAtFilter(body)).toEqual({
|
|
1397
|
+
Start: START.toISOString(),
|
|
1398
|
+
End: END.toISOString(),
|
|
1399
|
+
});
|
|
1400
|
+
expect(body["MaxResults"]).toBe(100);
|
|
1401
|
+
expect(body["SortCriteria"]).toEqual([
|
|
1402
|
+
{ Field: "CreatedAt", SortOrder: "asc" },
|
|
1403
|
+
]);
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
test("follows NextToken with the same filter and sort until the last page, signing every request", async () => {
|
|
1407
|
+
const harness: Harness = buildHarness([
|
|
1408
|
+
(): DataSourceHttpResponse => {
|
|
1409
|
+
return page(
|
|
1410
|
+
[guardDutyFinding("p1a"), guardDutyFinding("p1b")],
|
|
1411
|
+
"tok-1",
|
|
1412
|
+
);
|
|
1413
|
+
},
|
|
1414
|
+
(): DataSourceHttpResponse => {
|
|
1415
|
+
return page(
|
|
1416
|
+
[guardDutyFinding("p2a"), guardDutyFinding("p2b")],
|
|
1417
|
+
"tok-2",
|
|
1418
|
+
);
|
|
1419
|
+
},
|
|
1420
|
+
(): DataSourceHttpResponse => {
|
|
1421
|
+
return page([guardDutyFinding("p3a")]);
|
|
1422
|
+
},
|
|
1423
|
+
]);
|
|
1424
|
+
|
|
1425
|
+
const result: ConnectorFetchResult = await harness.connector.fetchEvents(
|
|
1426
|
+
settings(),
|
|
1427
|
+
{ startTime: START, endTime: END },
|
|
1428
|
+
fetchOptions(),
|
|
1429
|
+
);
|
|
1430
|
+
|
|
1431
|
+
expect(result.complete).toBe(true);
|
|
1432
|
+
expect(result.resumeAfter).toBeUndefined();
|
|
1433
|
+
expect(result.requestCount).toBe(3);
|
|
1434
|
+
expect(result.fetchedCount).toBe(5);
|
|
1435
|
+
expect(result.events).toHaveLength(5);
|
|
1436
|
+
expect(result.samples).toHaveLength(5);
|
|
1437
|
+
expect(harness.requests).toHaveLength(3);
|
|
1438
|
+
|
|
1439
|
+
const bodies: Array<JSONObject> = harness.requests.map(parsedBody);
|
|
1440
|
+
expect(Object.keys(bodies[0]!).sort()).toEqual([
|
|
1441
|
+
"Filters",
|
|
1442
|
+
"MaxResults",
|
|
1443
|
+
"SortCriteria",
|
|
1444
|
+
]);
|
|
1445
|
+
expect(Object.keys(bodies[1]!).sort()).toEqual([
|
|
1446
|
+
"Filters",
|
|
1447
|
+
"MaxResults",
|
|
1448
|
+
"NextToken",
|
|
1449
|
+
"SortCriteria",
|
|
1450
|
+
]);
|
|
1451
|
+
expect(bodies[1]!["NextToken"]).toBe("tok-1");
|
|
1452
|
+
expect(bodies[2]!["NextToken"]).toBe("tok-2");
|
|
1453
|
+
|
|
1454
|
+
for (const body of bodies) {
|
|
1455
|
+
expect(body["Filters"]).toEqual(bodies[0]!["Filters"]);
|
|
1456
|
+
expect(body["SortCriteria"]).toEqual(bodies[0]!["SortCriteria"]);
|
|
1457
|
+
expect(body["MaxResults"]).toBe(100);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
for (const request of harness.requests) {
|
|
1461
|
+
expectSigned(request);
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
/*
|
|
1466
|
+
* Review finding bound-hit-window-never-advances: these used to assert
|
|
1467
|
+
* "cursor is held", and the held cursor re-read the same oldest
|
|
1468
|
+
* findings on every poll so nothing newer was ever imported. A bound
|
|
1469
|
+
* now reports the CreatedAt of the last finding read (GetFindings is
|
|
1470
|
+
* sorted by CreatedAt ascending) so the poller can move past it.
|
|
1471
|
+
*/
|
|
1472
|
+
test("stops at the request bound, marks the fetch incomplete and reports the last CreatedAt read as the resume point", async () => {
|
|
1473
|
+
const harness: Harness = buildHarness([
|
|
1474
|
+
(): DataSourceHttpResponse => {
|
|
1475
|
+
return page(
|
|
1476
|
+
[
|
|
1477
|
+
findingCreatedAt("p1a", "2026-09-12T10:05:00.000Z"),
|
|
1478
|
+
findingCreatedAt("p1b", "2026-09-12T10:06:30.250Z"),
|
|
1479
|
+
],
|
|
1480
|
+
"tok-1",
|
|
1481
|
+
);
|
|
1482
|
+
},
|
|
1483
|
+
(): DataSourceHttpResponse => {
|
|
1484
|
+
return page(
|
|
1485
|
+
[findingCreatedAt("p2a", "2026-09-12T11:40:12.345678901Z")],
|
|
1486
|
+
"tok-2",
|
|
1487
|
+
);
|
|
1488
|
+
},
|
|
1489
|
+
(): DataSourceHttpResponse => {
|
|
1490
|
+
return page([findingCreatedAt("p3a", "2026-09-12T12:00:00.000Z")]);
|
|
1491
|
+
},
|
|
1492
|
+
]);
|
|
1493
|
+
|
|
1494
|
+
const result: ConnectorFetchResult = await harness.connector.fetchEvents(
|
|
1495
|
+
settings(),
|
|
1496
|
+
{ startTime: START, endTime: END },
|
|
1497
|
+
fetchOptions({ maxRequests: 2 }),
|
|
1498
|
+
);
|
|
1499
|
+
|
|
1500
|
+
expect(result.complete).toBe(false);
|
|
1501
|
+
expect(result.requestCount).toBe(2);
|
|
1502
|
+
expect(result.events).toHaveLength(3);
|
|
1503
|
+
// Nine fractional digits truncate to milliseconds: never later than the finding.
|
|
1504
|
+
expect(result.resumeAfter).toEqual(new Date("2026-09-12T11:40:12.345Z"));
|
|
1505
|
+
expect(result.warnings).toEqual([
|
|
1506
|
+
"Stopped after 2 findings requests (the per-run request limit) before the window was fully read. The last finding read was created at 2026-09-12T11:40:12.345Z.",
|
|
1507
|
+
]);
|
|
1508
|
+
expect(result.warnings[0]).not.toContain("cursor is held");
|
|
1509
|
+
expect(harness.requests).toHaveLength(2);
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
test("stops at the event bound inside a page and resumes after the last finding kept, not the one dropped", async () => {
|
|
1513
|
+
const harness: Harness = buildHarness([
|
|
1514
|
+
(): DataSourceHttpResponse => {
|
|
1515
|
+
return page([
|
|
1516
|
+
findingCreatedAt("a", "2026-09-12T10:01:00.000Z"),
|
|
1517
|
+
findingCreatedAt("b", "2026-09-12T10:02:00.000Z"),
|
|
1518
|
+
findingCreatedAt("c", "2026-09-12T10:03:00.000Z"),
|
|
1519
|
+
]);
|
|
1520
|
+
},
|
|
1521
|
+
]);
|
|
1522
|
+
|
|
1523
|
+
const result: ConnectorFetchResult = await harness.connector.fetchEvents(
|
|
1524
|
+
settings(),
|
|
1525
|
+
{ startTime: START, endTime: END },
|
|
1526
|
+
fetchOptions({ maxEvents: 2 }),
|
|
1527
|
+
);
|
|
1528
|
+
|
|
1529
|
+
expect(result.complete).toBe(false);
|
|
1530
|
+
expect(result.events).toHaveLength(2);
|
|
1531
|
+
expect(result.fetchedCount).toBe(2);
|
|
1532
|
+
expect(result.resumeAfter).toEqual(new Date("2026-09-12T10:02:00.000Z"));
|
|
1533
|
+
expect(result.warnings).toEqual([
|
|
1534
|
+
"Stopped after 2 findings (the per-run record limit) before the window was fully read. The last finding read was created at 2026-09-12T10:02:00.000Z.",
|
|
1535
|
+
]);
|
|
1536
|
+
expect(harness.requests).toHaveLength(1);
|
|
1537
|
+
});
|
|
1538
|
+
|
|
1539
|
+
test("does not request the next page once the event bound is reached exactly at a page end", async () => {
|
|
1540
|
+
const harness: Harness = buildHarness([
|
|
1541
|
+
(): DataSourceHttpResponse => {
|
|
1542
|
+
return page(
|
|
1543
|
+
[
|
|
1544
|
+
findingCreatedAt("a", "2026-09-12T10:01:00.000Z"),
|
|
1545
|
+
findingCreatedAt("b", "2026-09-12T10:04:00.000Z"),
|
|
1546
|
+
],
|
|
1547
|
+
"tok-1",
|
|
1548
|
+
);
|
|
1549
|
+
},
|
|
1550
|
+
]);
|
|
1551
|
+
|
|
1552
|
+
const result: ConnectorFetchResult = await harness.connector.fetchEvents(
|
|
1553
|
+
settings(),
|
|
1554
|
+
{ startTime: START, endTime: END },
|
|
1555
|
+
fetchOptions({ maxEvents: 2 }),
|
|
1556
|
+
);
|
|
1557
|
+
|
|
1558
|
+
expect(result.complete).toBe(false);
|
|
1559
|
+
expect(result.events).toHaveLength(2);
|
|
1560
|
+
expect(result.warnings[0]).toContain("per-run record limit");
|
|
1561
|
+
expect(result.resumeAfter).toEqual(new Date("2026-09-12T10:04:00.000Z"));
|
|
1562
|
+
expect(harness.requests).toHaveLength(1);
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
test("counts a finding the normalizer rejects toward the resume point and skips a missing or unparseable CreatedAt", async () => {
|
|
1566
|
+
const harness: Harness = buildHarness([
|
|
1567
|
+
(): DataSourceHttpResponse => {
|
|
1568
|
+
return page(
|
|
1569
|
+
[
|
|
1570
|
+
findingCreatedAt("a", "2026-09-12T10:01:00.000Z"),
|
|
1571
|
+
// Not a finding the normalizer recognizes (no Id), but it was read.
|
|
1572
|
+
{
|
|
1573
|
+
Message: "not a finding",
|
|
1574
|
+
CreatedAt: "2026-09-12T10:07:00.000Z",
|
|
1575
|
+
},
|
|
1576
|
+
findingCreatedAt("bad-date", "not a timestamp"),
|
|
1577
|
+
withoutCreatedAt(guardDutyFinding("no-created-at")),
|
|
1578
|
+
],
|
|
1579
|
+
"tok-1",
|
|
1580
|
+
);
|
|
1581
|
+
},
|
|
1582
|
+
]);
|
|
1583
|
+
|
|
1584
|
+
const result: ConnectorFetchResult = await harness.connector.fetchEvents(
|
|
1585
|
+
settings(),
|
|
1586
|
+
{ startTime: START, endTime: END },
|
|
1587
|
+
fetchOptions({ maxRequests: 1 }),
|
|
1588
|
+
);
|
|
1589
|
+
|
|
1590
|
+
expect(result.complete).toBe(false);
|
|
1591
|
+
expect(result.fetchedCount).toBe(4);
|
|
1592
|
+
expect(result.rejectedCount).toBe(1);
|
|
1593
|
+
expect(result.resumeAfter).toEqual(new Date("2026-09-12T10:07:00.000Z"));
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
test("reports no resume point when no finding was read before the bound", async () => {
|
|
1597
|
+
const harness: Harness = buildHarness([
|
|
1598
|
+
(): DataSourceHttpResponse => {
|
|
1599
|
+
return page([], "tok-1");
|
|
1600
|
+
},
|
|
1601
|
+
]);
|
|
1602
|
+
|
|
1603
|
+
const result: ConnectorFetchResult = await harness.connector.fetchEvents(
|
|
1604
|
+
settings(),
|
|
1605
|
+
{ startTime: START, endTime: END },
|
|
1606
|
+
fetchOptions({ maxRequests: 1 }),
|
|
1607
|
+
);
|
|
1608
|
+
|
|
1609
|
+
expect(result.complete).toBe(false);
|
|
1610
|
+
expect(result.resumeAfter).toBeUndefined();
|
|
1611
|
+
expect(result.warnings).toEqual([
|
|
1612
|
+
"Stopped after 1 findings requests (the per-run request limit) before the window was fully read. No finding read carried a readable CreatedAt, so no resume point is reported.",
|
|
1613
|
+
]);
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
test("counts findings the normalizer does not recognize as rejected and ones that throw as failed", async () => {
|
|
1617
|
+
const harness: Harness = buildHarness([
|
|
1618
|
+
(): DataSourceHttpResponse => {
|
|
1619
|
+
return page([
|
|
1620
|
+
guardDutyFinding("good"),
|
|
1621
|
+
{ Message: "not a finding", Code: "Whatever" },
|
|
1622
|
+
{ Id: "id-without-any-required-attribute" },
|
|
1623
|
+
guardDutyFinding("boom"),
|
|
1624
|
+
]);
|
|
1625
|
+
},
|
|
1626
|
+
]);
|
|
1627
|
+
|
|
1628
|
+
const original: typeof AwsSecurityHubNormalizer.normalize =
|
|
1629
|
+
AwsSecurityHubNormalizer.normalize.bind(AwsSecurityHubNormalizer);
|
|
1630
|
+
const spy: ReturnType<typeof jest.spyOn> = jest
|
|
1631
|
+
.spyOn(AwsSecurityHubNormalizer, "normalize")
|
|
1632
|
+
.mockImplementation((raw: JSONObject) => {
|
|
1633
|
+
if (String(raw["Id"]).endsWith("/finding/boom")) {
|
|
1634
|
+
throw new Error("synthetic normalization failure");
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
return original(raw);
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
try {
|
|
1641
|
+
const result: ConnectorFetchResult =
|
|
1642
|
+
await harness.connector.fetchEvents(
|
|
1643
|
+
settings(),
|
|
1644
|
+
{ startTime: START, endTime: END },
|
|
1645
|
+
fetchOptions(),
|
|
1646
|
+
);
|
|
1647
|
+
|
|
1648
|
+
expect(result.complete).toBe(true);
|
|
1649
|
+
expect(result.resumeAfter).toBeUndefined();
|
|
1650
|
+
expect(result.fetchedCount).toBe(4);
|
|
1651
|
+
expect(result.rejectedCount).toBe(2);
|
|
1652
|
+
expect(result.failedCount).toBe(1);
|
|
1653
|
+
expect(result.events).toHaveLength(1);
|
|
1654
|
+
expect(result.samples).toHaveLength(1);
|
|
1655
|
+
} finally {
|
|
1656
|
+
spy.mockRestore();
|
|
1657
|
+
}
|
|
1658
|
+
});
|
|
1659
|
+
|
|
1660
|
+
test("ignores the alertingOnly flag, which Security Hub has no notion of", async () => {
|
|
1661
|
+
const harness: Harness = buildHarness([
|
|
1662
|
+
(): DataSourceHttpResponse => {
|
|
1663
|
+
return page([guardDutyFinding("a")]);
|
|
1664
|
+
},
|
|
1665
|
+
]);
|
|
1666
|
+
|
|
1667
|
+
await harness.connector.fetchEvents(
|
|
1668
|
+
settings({ alertingOnly: true }),
|
|
1669
|
+
{ startTime: START, endTime: END },
|
|
1670
|
+
fetchOptions(),
|
|
1671
|
+
);
|
|
1672
|
+
|
|
1673
|
+
expect(Object.keys(parsedBody(harness.requests[0]!)).sort()).toEqual([
|
|
1674
|
+
"Filters",
|
|
1675
|
+
"MaxResults",
|
|
1676
|
+
"SortCriteria",
|
|
1677
|
+
]);
|
|
1678
|
+
expect(
|
|
1679
|
+
Object.keys(parsedBody(harness.requests[0]!)["Filters"] as JSONObject),
|
|
1680
|
+
).toEqual(["CreatedAt"]);
|
|
1681
|
+
});
|
|
1682
|
+
|
|
1683
|
+
test("normalizes the Region's case into the endpoint host and the credential scope", async () => {
|
|
1684
|
+
const harness: Harness = buildHarness([
|
|
1685
|
+
(): DataSourceHttpResponse => {
|
|
1686
|
+
return page([]);
|
|
1687
|
+
},
|
|
1688
|
+
]);
|
|
1689
|
+
|
|
1690
|
+
const result: ConnectorFetchResult = await harness.connector.fetchEvents(
|
|
1691
|
+
settings({
|
|
1692
|
+
config: { region: "EU-West-2", accessKeyId: ` ${ACCESS_KEY_ID} ` },
|
|
1693
|
+
}),
|
|
1694
|
+
{ startTime: START, endTime: END },
|
|
1695
|
+
fetchOptions(),
|
|
1696
|
+
);
|
|
1697
|
+
|
|
1698
|
+
expect(result.complete).toBe(true);
|
|
1699
|
+
expect(result.events).toEqual([]);
|
|
1700
|
+
expect(harness.requests[0]!.url).toBe(
|
|
1701
|
+
"https://securityhub.eu-west-2.amazonaws.com/findings",
|
|
1702
|
+
);
|
|
1703
|
+
expectSigned(harness.requests[0]!, { region: "eu-west-2" });
|
|
1704
|
+
});
|
|
1705
|
+
|
|
1706
|
+
test("throws with the step prefix for 401, 403, 429 and 500 and never prints a credential", async () => {
|
|
1707
|
+
for (const code of [401, 403, 429, 500]) {
|
|
1708
|
+
const harness: Harness = buildHarness([
|
|
1709
|
+
(): DataSourceHttpResponse => {
|
|
1710
|
+
return status(
|
|
1711
|
+
code,
|
|
1712
|
+
JSON.stringify({
|
|
1713
|
+
__type: "SomeException",
|
|
1714
|
+
Message: `rejected ${SECRET_ACCESS_KEY} with token ${SESSION_TOKEN}`,
|
|
1715
|
+
SecretAccessKey: SECRET_ACCESS_KEY,
|
|
1716
|
+
SessionToken: SESSION_TOKEN,
|
|
1717
|
+
}),
|
|
1718
|
+
);
|
|
1719
|
+
},
|
|
1720
|
+
]);
|
|
1721
|
+
|
|
1722
|
+
const error: Error = await expectRejection(
|
|
1723
|
+
harness.connector.fetchEvents(
|
|
1724
|
+
temporarySettings(),
|
|
1725
|
+
{ startTime: START, endTime: END },
|
|
1726
|
+
fetchOptions(),
|
|
1727
|
+
),
|
|
1728
|
+
);
|
|
1729
|
+
|
|
1730
|
+
expect(error).toBeInstanceOf(APIException);
|
|
1731
|
+
expect(error.message).toMatch(
|
|
1732
|
+
new RegExp(
|
|
1733
|
+
`^AWS Security Hub findings request failed \\(HTTP ${code}\\): `,
|
|
1734
|
+
),
|
|
1735
|
+
);
|
|
1736
|
+
expect(error.message).not.toContain(SECRET_ACCESS_KEY);
|
|
1737
|
+
expect(error.message).not.toContain(SESSION_TOKEN);
|
|
1738
|
+
expect(harness.requests).toHaveLength(1);
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
|
|
1742
|
+
test("throws with the step prefix when a page is not JSON", async () => {
|
|
1743
|
+
const harness: Harness = buildHarness([
|
|
1744
|
+
(): DataSourceHttpResponse => {
|
|
1745
|
+
return status(200, `<html>login ${SECRET_ACCESS_KEY}</html>`);
|
|
1746
|
+
},
|
|
1747
|
+
]);
|
|
1748
|
+
|
|
1749
|
+
const error: Error = await expectRejection(
|
|
1750
|
+
harness.connector.fetchEvents(
|
|
1751
|
+
settings(),
|
|
1752
|
+
{ startTime: START, endTime: END },
|
|
1753
|
+
fetchOptions(),
|
|
1754
|
+
),
|
|
1755
|
+
);
|
|
1756
|
+
|
|
1757
|
+
expect(error).toBeInstanceOf(APIException);
|
|
1758
|
+
expect(error.message).toMatch(
|
|
1759
|
+
/^AWS Security Hub findings request returned a non-JSON body: /,
|
|
1760
|
+
);
|
|
1761
|
+
expect(error.message).not.toContain(SECRET_ACCESS_KEY);
|
|
1762
|
+
});
|
|
1763
|
+
|
|
1764
|
+
test("throws with the step prefix when the transport never settles within the request timeout", async () => {
|
|
1765
|
+
const harness: Harness = buildHarness([
|
|
1766
|
+
(): Promise<DataSourceHttpResponse> => {
|
|
1767
|
+
return new Promise<DataSourceHttpResponse>((): void => {
|
|
1768
|
+
// Never settles.
|
|
1769
|
+
});
|
|
1770
|
+
},
|
|
1771
|
+
]);
|
|
1772
|
+
|
|
1773
|
+
await expect(
|
|
1774
|
+
harness.connector.fetchEvents(
|
|
1775
|
+
settings(),
|
|
1776
|
+
{ startTime: START, endTime: END },
|
|
1777
|
+
fetchOptions({ requestTimeoutInMs: 50 }),
|
|
1778
|
+
),
|
|
1779
|
+
).rejects.toThrow(
|
|
1780
|
+
"AWS Security Hub findings request timed out after 1 seconds with no response.",
|
|
1781
|
+
);
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
test("throws with the step prefix when the production transport reports a timeout", async () => {
|
|
1785
|
+
const harness: Harness = buildHarness([
|
|
1786
|
+
(): DataSourceHttpResponse => {
|
|
1787
|
+
throw new BadDataException(
|
|
1788
|
+
`Could not reach data source: timeout of 1000ms exceeded (x-amz-security-token: ${SESSION_TOKEN})`,
|
|
1789
|
+
);
|
|
1790
|
+
},
|
|
1791
|
+
]);
|
|
1792
|
+
|
|
1793
|
+
const error: Error = await expectRejection(
|
|
1794
|
+
harness.connector.fetchEvents(
|
|
1795
|
+
temporarySettings(),
|
|
1796
|
+
{ startTime: START, endTime: END },
|
|
1797
|
+
fetchOptions({ requestTimeoutInMs: 1000 }),
|
|
1798
|
+
),
|
|
1799
|
+
);
|
|
1800
|
+
|
|
1801
|
+
expect(error).toBeInstanceOf(APIException);
|
|
1802
|
+
expect(error.message).toMatch(
|
|
1803
|
+
/^AWS Security Hub findings request failed: Could not reach data source: timeout of 1000ms exceeded/,
|
|
1804
|
+
);
|
|
1805
|
+
expect(error.message).not.toContain(SESSION_TOKEN);
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
test("throws a BadDataException for invalid settings before any request", async () => {
|
|
1809
|
+
const harness: Harness = buildHarness();
|
|
1810
|
+
|
|
1811
|
+
await expect(
|
|
1812
|
+
harness.connector.fetchEvents(
|
|
1813
|
+
settings({ secrets: {} }),
|
|
1814
|
+
{ startTime: START, endTime: END },
|
|
1815
|
+
fetchOptions(),
|
|
1816
|
+
),
|
|
1817
|
+
).rejects.toBeInstanceOf(BadDataException);
|
|
1818
|
+
|
|
1819
|
+
await expect(
|
|
1820
|
+
harness.connector.fetchEvents(
|
|
1821
|
+
settings({
|
|
1822
|
+
config: { region: REGION, accessKeyId: TEMPORARY_ACCESS_KEY_ID },
|
|
1823
|
+
}),
|
|
1824
|
+
{ startTime: START, endTime: END },
|
|
1825
|
+
fetchOptions(),
|
|
1826
|
+
),
|
|
1827
|
+
).rejects.toThrow(/^Session token is required/);
|
|
1828
|
+
|
|
1829
|
+
await expect(
|
|
1830
|
+
harness.connector.fetchEvents(
|
|
1831
|
+
settings({
|
|
1832
|
+
secrets: {
|
|
1833
|
+
secretAccessKey: SECRET_ACCESS_KEY,
|
|
1834
|
+
sessionToken: SESSION_TOKEN,
|
|
1835
|
+
},
|
|
1836
|
+
}),
|
|
1837
|
+
{ startTime: START, endTime: END },
|
|
1838
|
+
fetchOptions(),
|
|
1839
|
+
),
|
|
1840
|
+
).rejects.toThrow(/^Session token must be empty/);
|
|
1841
|
+
|
|
1842
|
+
await expect(
|
|
1843
|
+
harness.connector.fetchEvents(
|
|
1844
|
+
settings({ provider: SecurityEventConnectorProvider.OktaSystemLog }),
|
|
1845
|
+
{ startTime: START, endTime: END },
|
|
1846
|
+
fetchOptions(),
|
|
1847
|
+
),
|
|
1848
|
+
).rejects.toThrow(/^Settings are for provider/);
|
|
1849
|
+
|
|
1850
|
+
expect(harness.requests).toHaveLength(0);
|
|
1851
|
+
});
|
|
1852
|
+
});
|
|
1853
|
+
});
|