@cccsaurora/howler-ui 2.19.0-cases.863 → 2.19.0-dev.1000
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/api/index.d.ts +0 -4
- package/api/index.js +2 -10
- package/api/search/facet/hit.d.ts +3 -1
- package/api/search/facet/index.d.ts +1 -3
- package/api/search/index.d.ts +1 -2
- package/api/search/index.js +1 -2
- package/commons/components/leftnav/LeftNavDrawer.js +1 -1
- package/components/app/App.d.ts +1 -0
- package/components/app/App.js +20 -42
- package/components/app/hooks/useMatchers.d.ts +1 -1
- package/components/app/hooks/useMatchers.js +11 -23
- package/components/app/hooks/useMatchers.test.js +22 -22
- package/components/app/hooks/useTitle.js +5 -5
- package/components/app/providers/FavouritesProvider.js +2 -6
- package/components/app/providers/HitProvider.d.ts +22 -0
- package/components/app/providers/{RecordProvider.js → HitProvider.js} +42 -42
- package/components/app/providers/{RecordSearchProvider.d.ts → HitSearchProvider.d.ts} +6 -6
- package/components/app/providers/{RecordSearchProvider.js → HitSearchProvider.js} +17 -12
- package/components/app/providers/{RecordSearchProvider.test.js → HitSearchProvider.test.js} +71 -52
- package/components/app/providers/ModalProvider.d.ts +1 -1
- package/components/app/providers/ModalProvider.js +5 -1
- package/components/app/providers/ParameterProvider.d.ts +2 -9
- package/components/app/providers/ParameterProvider.js +240 -165
- package/components/app/providers/ParameterProvider.test.js +94 -346
- package/components/app/providers/SocketProvider.d.ts +2 -11
- package/components/app/providers/SocketProvider.js +5 -18
- package/components/app/providers/UserListProvider.js +8 -28
- package/components/elements/PluginTypography.d.ts +1 -2
- package/components/elements/PluginTypography.js +2 -3
- package/components/elements/UserList.d.ts +2 -5
- package/components/elements/UserList.js +8 -18
- package/components/elements/addons/buttons/CustomButton.test.js +106 -0
- package/components/elements/addons/layout/FlexOne.test.js +30 -0
- package/components/elements/addons/lists/TuiListBase.js +4 -0
- package/components/elements/addons/lists/TuiListElement.js +6 -3
- package/components/elements/addons/lists/index.d.ts +1 -0
- package/components/elements/addons/search/SearchPagination.test.js +80 -0
- package/components/elements/addons/search/SearchTotal.test.js +45 -0
- package/components/elements/addons/search/phrase/Phrase.js +1 -1
- package/components/elements/display/ChipPopper.d.ts +1 -1
- package/components/elements/display/ChipPopper.js +5 -5
- package/components/elements/display/DocumentationButton.test.js +84 -0
- package/components/elements/display/DynamicTabs.test.js +93 -0
- package/components/elements/display/HowlerAvatar.js +2 -1
- package/components/elements/display/HowlerCard.js +1 -1
- package/components/elements/display/HowlerCard.test.js +55 -0
- package/components/elements/display/Image.test.js +79 -0
- package/components/elements/display/Markdown.js +9 -2
- package/components/elements/display/Modal.js +0 -2
- package/components/elements/display/QueryResultText.test.js +48 -0
- package/components/elements/display/TextDivider.test.js +43 -0
- package/components/elements/display/TypingIndicator.test.js +33 -0
- package/components/elements/display/icons/BundleButton.d.ts +6 -0
- package/components/elements/display/icons/BundleButton.js +32 -0
- package/components/elements/display/modals/ConfirmDeleteModal.d.ts +4 -0
- package/components/elements/display/modals/ConfirmDeleteModal.js +4 -2
- package/components/elements/hit/HitActions.js +4 -4
- package/components/elements/hit/HitBanner.d.ts +0 -1
- package/components/elements/hit/HitBanner.js +49 -31
- package/components/elements/hit/HitCard.d.ts +0 -2
- package/components/elements/hit/HitCard.js +7 -7
- package/components/elements/{record/RecordComments.d.ts → hit/HitComments.d.ts} +4 -5
- package/components/elements/{record/RecordComments.js → hit/HitComments.js} +28 -29
- package/components/elements/{ObjectDetails.js → hit/HitDetails.js} +17 -17
- package/components/elements/hit/HitLabels.js +2 -2
- package/components/elements/hit/HitOutline.d.ts +0 -1
- package/components/elements/hit/HitOutline.js +3 -3
- package/components/elements/hit/{HitPreview.d.ts → HitQuickSearch.d.ts} +3 -3
- package/components/elements/hit/{HitPreview.js → HitQuickSearch.js} +4 -10
- package/components/elements/hit/HitRelated.d.ts +6 -0
- package/components/elements/hit/HitRelated.js +7 -0
- package/components/elements/hit/HitSummary.d.ts +1 -2
- package/components/elements/hit/HitSummary.js +8 -8
- package/components/elements/{record/RecordWorklog.d.ts → hit/HitWorklog.d.ts} +3 -4
- package/components/elements/{record/RecordWorklog.js → hit/HitWorklog.js} +13 -15
- package/components/elements/hit/aggregate/HitGraph.js +8 -8
- package/components/elements/hit/elements/{Assigned.d.ts → HitUsers.d.ts} +5 -2
- package/components/elements/hit/elements/HitUsers.js +33 -0
- package/components/elements/hit/outlines/DefaultOutline.js +1 -1
- package/components/elements/view/ViewTitle.d.ts +0 -1
- package/components/elements/view/ViewTitle.js +2 -9
- package/components/hooks/useHitActions.d.ts +1 -1
- package/components/hooks/useHitActions.js +4 -4
- package/components/hooks/{useRecordSelection.d.ts → useHitSelection.d.ts} +2 -2
- package/components/hooks/{useRecordSelection.js → useHitSelection.js} +33 -12
- package/components/hooks/useMyPreferences.js +3 -11
- package/components/hooks/useMySearch.js +2 -2
- package/components/hooks/useMySitemap.js +1 -4
- package/components/hooks/useMyTheme.js +2 -9
- package/components/routes/403.test.js +27 -0
- package/components/routes/404.test.js +26 -0
- package/components/routes/ErrorBoundary.test.js +43 -0
- package/components/routes/ErrorOccured.test.js +35 -0
- package/components/routes/Logout.test.js +59 -0
- package/components/routes/action/edit/ActionEditor.js +2 -2
- package/components/routes/action/view/ActionDetails.js +4 -2
- package/components/routes/action/view/ActionSearch.js +12 -7
- package/components/routes/advanced/QueryBuilder.js +1 -1
- package/components/routes/advanced/QueryEditor.js +3 -3
- package/components/routes/advanced/historyCompletionProvider.js +3 -3
- package/components/routes/analytics/AnalyticDetails.js +12 -8
- package/components/routes/analytics/AnalyticSearch.js +1 -1
- package/components/routes/dossiers/DossierEditor.js +2 -2
- package/components/routes/dossiers/DossierEditor.test.js +1 -1
- package/components/routes/dossiers/Dossiers.js +15 -11
- package/components/routes/help/ApiDocumentation.js +1 -1
- package/components/routes/help/BundleDocumentation.d.ts +3 -0
- package/components/routes/help/BundleDocumentation.js +12 -0
- package/components/routes/help/HitBannerDocumentation.js +0 -1
- package/components/routes/help/HitDocumentation.js +3 -1
- package/components/routes/help/markdown/en/bundles.md.js +1 -0
- package/components/routes/help/markdown/en/client.md.js +1 -1
- package/components/routes/help/markdown/fr/bundles.md.js +1 -0
- package/components/routes/help/markdown/fr/client.md.js +1 -1
- package/components/routes/hits/search/BundleParentMenu.d.ts +6 -0
- package/components/routes/hits/search/BundleParentMenu.js +32 -0
- package/components/routes/hits/search/BundleScroller.d.ts +2 -0
- package/components/routes/hits/search/BundleScroller.js +6 -0
- package/components/routes/hits/search/{RecordBrowser.js → HitBrowser.js} +9 -9
- package/components/{elements/record/RecordContextMenu.d.ts → routes/hits/search/HitContextMenu.d.ts} +3 -3
- package/components/routes/hits/search/HitContextMenu.js +239 -0
- package/components/{elements/record/RecordContextMenu.test.js → routes/hits/search/HitContextMenu.test.js} +43 -98
- package/components/routes/hits/search/{RecordQuery.d.ts → HitQuery.d.ts} +2 -2
- package/components/routes/hits/search/{RecordQuery.js → HitQuery.js} +6 -6
- package/components/routes/hits/search/InformationPane.d.ts +0 -1
- package/components/routes/hits/search/InformationPane.js +63 -50
- package/components/routes/hits/search/LayoutSettings.js +3 -3
- package/components/routes/hits/search/QuerySettings.js +1 -2
- package/components/routes/hits/search/QuerySettings.test.js +9 -14
- package/components/routes/hits/search/SearchPane.js +49 -26
- package/components/routes/hits/search/ViewLink.js +3 -3
- package/components/routes/hits/search/ViewLink.test.js +8 -8
- package/components/routes/hits/search/grid/AddColumnModal.js +4 -5
- package/components/routes/hits/search/grid/EnhancedCell.d.ts +1 -2
- package/components/routes/hits/search/grid/EnhancedCell.js +2 -2
- package/components/routes/hits/search/grid/HitGrid.js +18 -20
- package/components/routes/hits/search/grid/{RecordRow.d.ts → HitRow.d.ts} +2 -3
- package/components/routes/hits/search/grid/{RecordRow.js → HitRow.js} +9 -11
- package/components/routes/hits/search/shared/CustomSpan.js +7 -7
- package/components/routes/hits/search/shared/CustomSpan.test.js +170 -0
- package/components/routes/hits/view/HitViewer.js +13 -12
- package/components/routes/home/AddNewCard.test.js +68 -0
- package/components/routes/home/ViewCard.js +41 -47
- package/components/routes/home/ViewRefresh.test.js +79 -0
- package/components/routes/overviews/OverviewCard.test.js +108 -0
- package/components/{elements/MarkdownEditor.js → routes/overviews/OverviewEditor.js} +3 -3
- package/components/routes/overviews/OverviewViewer.js +14 -10
- package/components/routes/overviews/Overviews.js +15 -11
- package/components/routes/settings/LocalSection.js +2 -1
- package/components/routes/settings/SettingsSection.test.js +19 -0
- package/components/routes/templates/TemplateCard.d.ts +3 -1
- package/components/routes/templates/TemplateCard.js +8 -4
- package/components/routes/templates/TemplateCard.test.d.ts +1 -0
- package/components/routes/templates/TemplateCard.test.js +115 -0
- package/components/routes/templates/TemplateViewer.js +57 -16
- package/components/routes/templates/Templates.js +23 -6
- package/components/routes/views/ViewComposer.js +19 -46
- package/components/routes/views/Views.js +8 -4
- package/locales/en/translation.json +17 -117
- package/locales/fr/translation.json +19 -119
- package/models/WithMetadata.d.ts +1 -2
- package/models/entities/generated/{ThreatEnrichment.d.ts → Enrichment.d.ts} +1 -1
- package/models/entities/generated/Hit.d.ts +1 -1
- package/models/entities/generated/Howler.d.ts +6 -0
- package/models/entities/generated/Rule.d.ts +9 -6
- package/models/entities/generated/Threat.d.ts +2 -2
- package/models/entities/generated/UserUser.d.ts +2 -0
- package/models/entities/generated/View.d.ts +0 -1
- package/package.json +112 -132
- package/plugins/clue/components/ClueTypography.js +2 -2
- package/plugins/clue/utils.d.ts +1 -2
- package/rest/FetchClient.test.d.ts +1 -0
- package/rest/FetchClient.test.js +81 -0
- package/setupTests.js +5 -0
- package/tests/mocks.d.ts +1 -11
- package/tests/mocks.js +7 -12
- package/tests/server-handlers.js +1 -6
- package/tests/utils.d.ts +0 -4
- package/tests/utils.js +0 -20
- package/utils/Throttler.test.d.ts +1 -0
- package/utils/Throttler.test.js +135 -0
- package/utils/actionUtils.test.d.ts +1 -0
- package/utils/actionUtils.test.js +140 -0
- package/utils/constants.d.ts +4 -4
- package/utils/constants.js +1 -6
- package/utils/hitFunctions.d.ts +1 -2
- package/utils/hitFunctions.js +4 -4
- package/utils/hitFunctions.test.d.ts +1 -0
- package/utils/hitFunctions.test.js +52 -0
- package/utils/localStorage.test.d.ts +1 -0
- package/utils/localStorage.test.js +96 -0
- package/utils/menuUtils.test.d.ts +1 -0
- package/utils/menuUtils.test.js +198 -0
- package/utils/sessionStorage.test.d.ts +1 -0
- package/utils/sessionStorage.test.js +81 -0
- package/utils/socketUtils.d.ts +0 -14
- package/utils/socketUtils.js +1 -17
- package/utils/socketUtils.test.js +16 -49
- package/utils/stringUtils.test.d.ts +1 -0
- package/utils/stringUtils.test.js +159 -0
- package/utils/utils.test.d.ts +1 -0
- package/utils/utils.test.js +292 -0
- package/utils/viewUtils.js +0 -3
- package/utils/viewUtils.test.d.ts +1 -0
- package/utils/viewUtils.test.js +45 -0
- package/utils/xsrf.test.d.ts +1 -0
- package/utils/xsrf.test.js +41 -0
- package/api/search/case.d.ts +0 -4
- package/api/search/case.js +0 -8
- package/api/socket/index.d.ts +0 -3
- package/api/socket/index.js +0 -6
- package/api/socket/viewers.d.ts +0 -2
- package/api/socket/viewers.js +0 -8
- package/api/socket/viewers.test.js +0 -44
- package/api/v2/case/index.d.ts +0 -9
- package/api/v2/case/index.js +0 -21
- package/api/v2/case/items.d.ts +0 -6
- package/api/v2/case/items.js +0 -18
- package/api/v2/case/rules.d.ts +0 -6
- package/api/v2/case/rules.js +0 -18
- package/api/v2/index.d.ts +0 -4
- package/api/v2/index.js +0 -6
- package/api/v2/search/facet.d.ts +0 -3
- package/api/v2/search/facet.js +0 -12
- package/api/v2/search/index.d.ts +0 -5
- package/api/v2/search/index.js +0 -24
- package/components/app/providers/RecordProvider.d.ts +0 -23
- package/components/elements/ContextMenu.d.ts +0 -56
- package/components/elements/ContextMenu.js +0 -109
- package/components/elements/ContextMenu.test.js +0 -215
- package/components/elements/ObjectDetails.d.ts +0 -6
- package/components/elements/case/CaseCard.d.ts +0 -12
- package/components/elements/case/CaseCard.js +0 -42
- package/components/elements/case/CasePreview.d.ts +0 -6
- package/components/elements/case/CasePreview.js +0 -17
- package/components/elements/case/StatusIcon.d.ts +0 -5
- package/components/elements/case/StatusIcon.js +0 -13
- package/components/elements/hit/elements/AnalyticLink.d.ts +0 -9
- package/components/elements/hit/elements/AnalyticLink.js +0 -22
- package/components/elements/hit/elements/Assigned.js +0 -34
- package/components/elements/hit/elements/Assigned.test.js +0 -65
- package/components/elements/hit/related/RelatedRecords.js +0 -63
- package/components/elements/observable/ObservableCard.d.ts +0 -6
- package/components/elements/observable/ObservableCard.js +0 -22
- package/components/elements/observable/ObservablePreview.d.ts +0 -6
- package/components/elements/observable/ObservablePreview.js +0 -12
- package/components/elements/record/RecordContextMenu.js +0 -268
- package/components/elements/record/RecordRelated.d.ts +0 -7
- package/components/elements/record/RecordRelated.js +0 -34
- package/components/hooks/useRelatedRecords.d.ts +0 -13
- package/components/hooks/useRelatedRecords.js +0 -32
- package/components/routes/cases/CaseViewer.d.ts +0 -2
- package/components/routes/cases/CaseViewer.js +0 -44
- package/components/routes/cases/CaseViewer.test.js +0 -133
- package/components/routes/cases/Cases.d.ts +0 -2
- package/components/routes/cases/Cases.js +0 -148
- package/components/routes/cases/constants.d.ts +0 -6
- package/components/routes/cases/constants.js +0 -6
- package/components/routes/cases/detail/AlertPanel.d.ts +0 -6
- package/components/routes/cases/detail/AlertPanel.js +0 -33
- package/components/routes/cases/detail/CaseAssets.d.ts +0 -11
- package/components/routes/cases/detail/CaseAssets.js +0 -104
- package/components/routes/cases/detail/CaseAssets.test.js +0 -167
- package/components/routes/cases/detail/CaseDashboard.d.ts +0 -7
- package/components/routes/cases/detail/CaseDashboard.js +0 -66
- package/components/routes/cases/detail/CaseDetails.d.ts +0 -6
- package/components/routes/cases/detail/CaseDetails.js +0 -70
- package/components/routes/cases/detail/CaseOverview.d.ts +0 -7
- package/components/routes/cases/detail/CaseOverview.js +0 -43
- package/components/routes/cases/detail/CaseRules.d.ts +0 -7
- package/components/routes/cases/detail/CaseRules.js +0 -57
- package/components/routes/cases/detail/CaseRules.test.js +0 -221
- package/components/routes/cases/detail/CaseSidebar.d.ts +0 -8
- package/components/routes/cases/detail/CaseSidebar.js +0 -107
- package/components/routes/cases/detail/CaseSidebar.test.js +0 -266
- package/components/routes/cases/detail/CaseTask.d.ts +0 -11
- package/components/routes/cases/detail/CaseTask.js +0 -66
- package/components/routes/cases/detail/CaseTimeline.d.ts +0 -12
- package/components/routes/cases/detail/CaseTimeline.js +0 -106
- package/components/routes/cases/detail/CaseTimeline.test.js +0 -320
- package/components/routes/cases/detail/CreateRuleDialog.d.ts +0 -9
- package/components/routes/cases/detail/CreateRuleDialog.js +0 -163
- package/components/routes/cases/detail/CreateRuleDialog.test.js +0 -259
- package/components/routes/cases/detail/ItemPage.d.ts +0 -6
- package/components/routes/cases/detail/ItemPage.js +0 -95
- package/components/routes/cases/detail/RelatedCasePanel.d.ts +0 -6
- package/components/routes/cases/detail/RelatedCasePanel.js +0 -34
- package/components/routes/cases/detail/TaskPanel.d.ts +0 -7
- package/components/routes/cases/detail/TaskPanel.js +0 -52
- package/components/routes/cases/detail/aggregates/CaseAggregate.d.ts +0 -11
- package/components/routes/cases/detail/aggregates/CaseAggregate.js +0 -24
- package/components/routes/cases/detail/aggregates/SourceAggregate.d.ts +0 -6
- package/components/routes/cases/detail/aggregates/SourceAggregate.js +0 -26
- package/components/routes/cases/detail/assets/Asset.d.ts +0 -14
- package/components/routes/cases/detail/assets/Asset.js +0 -12
- package/components/routes/cases/detail/assets/Asset.test.js +0 -72
- package/components/routes/cases/detail/sidebar/CaseFolder.d.ts +0 -20
- package/components/routes/cases/detail/sidebar/CaseFolder.js +0 -83
- package/components/routes/cases/detail/sidebar/CaseFolder.test.js +0 -295
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.d.ts +0 -34
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.js +0 -103
- package/components/routes/cases/detail/sidebar/CaseFolderContextMenu.test.js +0 -363
- package/components/routes/cases/detail/sidebar/FolderEntry.d.ts +0 -25
- package/components/routes/cases/detail/sidebar/FolderEntry.js +0 -88
- package/components/routes/cases/detail/sidebar/FolderEntry.test.js +0 -206
- package/components/routes/cases/detail/sidebar/RootDropZone.d.ts +0 -5
- package/components/routes/cases/detail/sidebar/RootDropZone.js +0 -33
- package/components/routes/cases/detail/sidebar/types.d.ts +0 -9
- package/components/routes/cases/detail/sidebar/utils.d.ts +0 -3
- package/components/routes/cases/detail/sidebar/utils.js +0 -29
- package/components/routes/cases/detail/sidebar/utils.test.js +0 -82
- package/components/routes/cases/hooks/useCase.d.ts +0 -13
- package/components/routes/cases/hooks/useCase.js +0 -69
- package/components/routes/cases/hooks/useCase.test.js +0 -141
- package/components/routes/cases/modals/AddToCaseModal.d.ts +0 -7
- package/components/routes/cases/modals/AddToCaseModal.js +0 -59
- package/components/routes/cases/modals/AddToCaseModal.test.js +0 -313
- package/components/routes/cases/modals/CaseRecordRow.d.ts +0 -9
- package/components/routes/cases/modals/CaseRecordRow.js +0 -15
- package/components/routes/cases/modals/CreateCaseModal.d.ts +0 -7
- package/components/routes/cases/modals/CreateCaseModal.js +0 -55
- package/components/routes/cases/modals/CreateCaseModal.test.js +0 -358
- package/components/routes/cases/modals/RenameItemModal.d.ts +0 -9
- package/components/routes/cases/modals/RenameItemModal.js +0 -48
- package/components/routes/cases/modals/ResolveModal.d.ts +0 -7
- package/components/routes/cases/modals/ResolveModal.js +0 -115
- package/components/routes/cases/modals/ResolveModal.test.js +0 -394
- package/components/routes/cases/modals/hooks.d.ts +0 -7
- package/components/routes/cases/modals/hooks.js +0 -44
- package/components/routes/cases/modals/types.d.ts +0 -5
- package/components/routes/cases/search/CaseAssigneeFilter.d.ts +0 -6
- package/components/routes/cases/search/CaseAssigneeFilter.js +0 -33
- package/components/routes/cases/search/CaseAssigneeFilter.test.js +0 -127
- package/components/routes/cases/search/CaseDateFilter.d.ts +0 -13
- package/components/routes/cases/search/CaseDateFilter.js +0 -26
- package/components/routes/cases/search/CaseDateFilter.test.js +0 -115
- package/components/routes/cases/search/CaseStatusFilter.d.ts +0 -6
- package/components/routes/cases/search/CaseStatusFilter.js +0 -13
- package/components/routes/cases/search/CaseStatusFilter.test.js +0 -86
- package/components/routes/hits/search/shared/IndexPicker.d.ts +0 -2
- package/components/routes/hits/search/shared/IndexPicker.js +0 -20
- package/components/routes/observables/ObservableViewer.d.ts +0 -7
- package/components/routes/observables/ObservableViewer.js +0 -27
- package/models/entities/generated/AttachmentsFile.d.ts +0 -12
- package/models/entities/generated/Case.d.ts +0 -28
- package/models/entities/generated/DestinationOriginal.d.ts +0 -19
- package/models/entities/generated/EmailAttachment.d.ts +0 -8
- package/models/entities/generated/EmailParent.d.ts +0 -19
- package/models/entities/generated/Enrichments.d.ts +0 -7
- package/models/entities/generated/EnrichmentsIndicator.d.ts +0 -21
- package/models/entities/generated/HttpResponse.d.ts +0 -11
- package/models/entities/generated/Item.d.ts +0 -9
- package/models/entities/generated/Observable.d.ts +0 -85
- package/models/entities/generated/ObservableCloud.d.ts +0 -20
- package/models/entities/generated/ObservableDestination.d.ts +0 -23
- package/models/entities/generated/ObservableEmail.d.ts +0 -30
- package/models/entities/generated/ObservableFile.d.ts +0 -36
- package/models/entities/generated/ObservableHowler.d.ts +0 -42
- package/models/entities/generated/ObservableHttp.d.ts +0 -11
- package/models/entities/generated/ObservableObserver.d.ts +0 -21
- package/models/entities/generated/ObservableOrganization.d.ts +0 -7
- package/models/entities/generated/ObservableProcess.d.ts +0 -34
- package/models/entities/generated/ObservableSource.d.ts +0 -23
- package/models/entities/generated/ObservableThreat.d.ts +0 -21
- package/models/entities/generated/ObservableTls.d.ts +0 -12
- package/models/entities/generated/ObserverIngress.d.ts +0 -9
- package/models/entities/generated/Task.d.ts +0 -10
- package/models/socket/CaseUpdate.d.ts +0 -5
- package/models/socket/ViewersUpdate.d.ts +0 -4
- package/utils/typeUtils.d.ts +0 -7
- package/utils/typeUtils.js +0 -27
- /package/{api/socket/viewers.test.d.ts → components/app/providers/HitSearchProvider.test.d.ts} +0 -0
- /package/components/{app/providers/RecordSearchProvider.test.d.ts → elements/addons/buttons/CustomButton.test.d.ts} +0 -0
- /package/components/elements/{ContextMenu.test.d.ts → addons/layout/FlexOne.test.d.ts} +0 -0
- /package/components/elements/{hit/elements/Assigned.test.d.ts → addons/search/SearchPagination.test.d.ts} +0 -0
- /package/components/elements/{record/RecordContextMenu.test.d.ts → addons/search/SearchTotal.test.d.ts} +0 -0
- /package/components/{routes/cases/CaseViewer.test.d.ts → elements/display/DocumentationButton.test.d.ts} +0 -0
- /package/components/{routes/cases/detail/CaseAssets.test.d.ts → elements/display/DynamicTabs.test.d.ts} +0 -0
- /package/components/{routes/cases/detail/CaseRules.test.d.ts → elements/display/HowlerCard.test.d.ts} +0 -0
- /package/components/{routes/cases/detail/CaseSidebar.test.d.ts → elements/display/Image.test.d.ts} +0 -0
- /package/components/{routes/cases/detail/CaseTimeline.test.d.ts → elements/display/QueryResultText.test.d.ts} +0 -0
- /package/components/{routes/cases/detail/CreateRuleDialog.test.d.ts → elements/display/TextDivider.test.d.ts} +0 -0
- /package/components/{routes/cases/detail/assets/Asset.test.d.ts → elements/display/TypingIndicator.test.d.ts} +0 -0
- /package/components/elements/hit/{related/RelatedRecords.d.ts → HitDetails.d.ts} +0 -0
- /package/components/routes/{cases/detail/sidebar/CaseFolder.test.d.ts → 403.test.d.ts} +0 -0
- /package/components/routes/{cases/detail/sidebar/CaseFolderContextMenu.test.d.ts → 404.test.d.ts} +0 -0
- /package/components/routes/{cases/detail/sidebar/FolderEntry.test.d.ts → ErrorBoundary.test.d.ts} +0 -0
- /package/components/routes/{cases/detail/sidebar/utils.test.d.ts → ErrorOccured.test.d.ts} +0 -0
- /package/components/routes/{cases/hooks/useCase.test.d.ts → Logout.test.d.ts} +0 -0
- /package/components/routes/hits/search/{RecordBrowser.d.ts → HitBrowser.d.ts} +0 -0
- /package/components/routes/{cases/modals/AddToCaseModal.test.d.ts → hits/search/HitContextMenu.test.d.ts} +0 -0
- /package/components/routes/{cases/modals/CreateCaseModal.test.d.ts → hits/search/shared/CustomSpan.test.d.ts} +0 -0
- /package/components/routes/{cases/modals/ResolveModal.test.d.ts → home/AddNewCard.test.d.ts} +0 -0
- /package/components/routes/{cases/search/CaseAssigneeFilter.test.d.ts → home/ViewRefresh.test.d.ts} +0 -0
- /package/components/routes/{cases/search/CaseDateFilter.test.d.ts → overviews/OverviewCard.test.d.ts} +0 -0
- /package/components/{elements/MarkdownEditor.d.ts → routes/overviews/OverviewEditor.d.ts} +0 -0
- /package/components/routes/{cases/search/CaseStatusFilter.test.d.ts → settings/SettingsSection.test.d.ts} +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
5
|
+
import { I18nextProvider } from 'react-i18next';
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import ErrorOccured from './ErrorOccured';
|
|
8
|
+
vi.mock('commons/components/pages/PageCenter', () => ({
|
|
9
|
+
default: ({ children }) => _jsx("div", { id: "page-center", children: children })
|
|
10
|
+
}));
|
|
11
|
+
const mockExecuteFunction = vi.fn(() => null);
|
|
12
|
+
vi.mock('react-pluggable', () => ({
|
|
13
|
+
usePluginStore: () => ({
|
|
14
|
+
executeFunction: mockExecuteFunction
|
|
15
|
+
})
|
|
16
|
+
}));
|
|
17
|
+
vi.mock('plugins/store', () => ({
|
|
18
|
+
default: { plugins: ['TestPlugin'] }
|
|
19
|
+
}));
|
|
20
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
|
|
21
|
+
describe('ErrorOccured', () => {
|
|
22
|
+
it('should render the error title and description', () => {
|
|
23
|
+
render(_jsx(ErrorOccured, {}), { wrapper: Wrapper });
|
|
24
|
+
expect(screen.getByText('Application Stopped Working')).toBeInTheDocument();
|
|
25
|
+
expect(screen.getByText('The application stopped working suddenly. If the problem persists please reach out on teams.')).toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
it('should render inside PageCenter', () => {
|
|
28
|
+
render(_jsx(ErrorOccured, {}), { wrapper: Wrapper });
|
|
29
|
+
expect(screen.getByTestId('page-center')).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
it('should execute plugin support functions', () => {
|
|
32
|
+
render(_jsx(ErrorOccured, {}), { wrapper: Wrapper });
|
|
33
|
+
expect(mockExecuteFunction).toHaveBeenCalledWith('TestPlugin.support');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/// <reference types="vitest" />
|
|
3
|
+
import { render, screen } from '@testing-library/react';
|
|
4
|
+
import i18n from '@cccsaurora/howler-ui/i18n';
|
|
5
|
+
import { I18nextProvider } from 'react-i18next';
|
|
6
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import Logout from './Logout';
|
|
8
|
+
const mockHideMenus = vi.fn();
|
|
9
|
+
const mockClear = vi.fn();
|
|
10
|
+
vi.mock('commons/components/app/hooks', () => ({
|
|
11
|
+
useAppBanner: () => _jsx("div", { "data-testid": "app-banner", children: "Banner" }),
|
|
12
|
+
useAppLayout: () => ({
|
|
13
|
+
hideMenus: mockHideMenus
|
|
14
|
+
})
|
|
15
|
+
}));
|
|
16
|
+
vi.mock('components/hooks/useMyLocalStorage', () => ({
|
|
17
|
+
default: () => ({
|
|
18
|
+
clear: mockClear
|
|
19
|
+
})
|
|
20
|
+
}));
|
|
21
|
+
vi.mock('commons/components/pages/PageCardCentered', () => ({
|
|
22
|
+
default: ({ children }) => _jsx("div", { children: children })
|
|
23
|
+
}));
|
|
24
|
+
const Wrapper = ({ children }) => (_jsx(I18nextProvider, { i18n: i18n, children: children }));
|
|
25
|
+
describe('Logout', () => {
|
|
26
|
+
const originalLocation = window.location;
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.useFakeTimers();
|
|
29
|
+
// Mock window.location.replace
|
|
30
|
+
Object.defineProperty(window, 'location', {
|
|
31
|
+
value: { ...originalLocation, replace: vi.fn() },
|
|
32
|
+
writable: true
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.useRealTimers();
|
|
37
|
+
Object.defineProperty(window, 'location', { value: originalLocation, writable: true });
|
|
38
|
+
vi.clearAllMocks();
|
|
39
|
+
});
|
|
40
|
+
it('should render the logout message', () => {
|
|
41
|
+
render(_jsx(Logout, {}), { wrapper: Wrapper });
|
|
42
|
+
expect(screen.getByText(/Logging out current user/)).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
it('should render the app banner', () => {
|
|
45
|
+
render(_jsx(Logout, {}), { wrapper: Wrapper });
|
|
46
|
+
expect(screen.getByText('Banner')).toBeInTheDocument();
|
|
47
|
+
});
|
|
48
|
+
it('should hide menus on mount', () => {
|
|
49
|
+
render(_jsx(Logout, {}), { wrapper: Wrapper });
|
|
50
|
+
expect(mockHideMenus).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
it('should clear localStorage and redirect after 2 seconds', () => {
|
|
53
|
+
render(_jsx(Logout, {}), { wrapper: Wrapper });
|
|
54
|
+
expect(mockClear).not.toHaveBeenCalled();
|
|
55
|
+
vi.advanceTimersByTime(2000);
|
|
56
|
+
expect(mockClear).toHaveBeenCalled();
|
|
57
|
+
expect(window.location.replace).toHaveBeenCalledWith('/');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -7,7 +7,7 @@ import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCente
|
|
|
7
7
|
import { FieldContext } from '@cccsaurora/howler-ui/components/app/providers/FieldProvider';
|
|
8
8
|
import SocketBadge from '@cccsaurora/howler-ui/components/elements/display/icons/SocketBadge';
|
|
9
9
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
10
|
-
import
|
|
10
|
+
import HitQuery from '@cccsaurora/howler-ui/components/routes/hits/search/HitQuery';
|
|
11
11
|
import { difference, uniq } from 'lodash-es';
|
|
12
12
|
import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
|
|
13
13
|
import { useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
@@ -102,7 +102,7 @@ const ActionEditor = () => {
|
|
|
102
102
|
: disabled && userOperations.length > 0
|
|
103
103
|
? t('route.actions.trigger.disabled.explanation')
|
|
104
104
|
: null, children: component }, trigger));
|
|
105
|
-
}) }) }), _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "end", sx: { mb: -1 }, children: [_jsx(Typography, { sx: theme => ({ color: theme.palette.text.disabled, fontStyle: 'italic', mb: 0.5 }), variant: "body2", children: t('hit.search.prompt') }), _jsx(SocketBadge, { size: "small" })] }), _jsx(
|
|
105
|
+
}) }) }), _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "end", sx: { mb: -1 }, children: [_jsx(Typography, { sx: theme => ({ color: theme.palette.text.disabled, fontStyle: 'italic', mb: 0.5 }), variant: "body2", children: t('hit.search.prompt') }), _jsx(SocketBadge, { size: "small" })] }), _jsx(HitQuery, { triggerSearch: onSearch }), response ? (_jsx(QueryResultText, { count: response.total, query: responseQuery })) : (_jsx(Typography, { sx: theme => ({
|
|
106
106
|
color: theme.palette.text.secondary,
|
|
107
107
|
fontSize: '0.9em',
|
|
108
108
|
fontStyle: 'italic',
|
|
@@ -4,13 +4,14 @@ import { Button, Checkbox, FormControlLabel, FormGroup, IconButton, LinearProgre
|
|
|
4
4
|
import api from '@cccsaurora/howler-ui/api';
|
|
5
5
|
import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
|
|
6
6
|
import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
|
|
7
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
7
8
|
import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
|
|
8
9
|
import Phrase from '@cccsaurora/howler-ui/components/elements/addons/search/phrase/Phrase';
|
|
9
10
|
import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
|
|
10
11
|
import useMyApi from '@cccsaurora/howler-ui/components/hooks/useMyApi';
|
|
11
12
|
import OperationEntry from '@cccsaurora/howler-ui/components/routes/action/shared/OperationEntry';
|
|
12
13
|
import howlerPluginStore from '@cccsaurora/howler-ui/plugins/store';
|
|
13
|
-
import { useCallback, useEffect, useState } from 'react';
|
|
14
|
+
import { useCallback, useContext, useEffect, useState } from 'react';
|
|
14
15
|
import { useTranslation } from 'react-i18next';
|
|
15
16
|
import { usePluginStore } from 'react-pluggable';
|
|
16
17
|
import { Link, useParams } from 'react-router-dom';
|
|
@@ -26,6 +27,7 @@ const ActionDetails = () => {
|
|
|
26
27
|
const { response, onSearch, loading, setLoading, executeAction, deleteAction, progress, report } = useMyActionFunctions();
|
|
27
28
|
const [operations, setOperations] = useState([]);
|
|
28
29
|
const [action, setAction] = useState();
|
|
30
|
+
const { withConfirmDeleteModal } = useContext(ModalContext);
|
|
29
31
|
const onTriggerChange = useCallback(async (e) => {
|
|
30
32
|
let newTriggers = action.triggers ?? [];
|
|
31
33
|
if (e.target.checked && !newTriggers.includes(e.target.name)) {
|
|
@@ -63,7 +65,7 @@ const ActionDetails = () => {
|
|
|
63
65
|
user.roles.includes('admin') ||
|
|
64
66
|
user.roles.includes('actionrunner_basic') ||
|
|
65
67
|
user.roles.includes('actionrunner_advanced');
|
|
66
|
-
return (_jsx(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", justifyContent: "space-between", children: [_jsx(Typography, { variant: "h5", children: action?.name }), action?.owner_id && _jsx(HowlerAvatar, { sx: { width: 32, height: 32 }, userId: action.owner_id })] }), _jsx(Phrase, { fullWidth: true, value: action?.query, disabled: true, size: "small", onChange: () => { }, startAdornment: _jsx(IconButton, { onClick: () => onSearch(action?.query), children: _jsx(Search, { fontSize: "small" }) }) }), _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [response && _jsx(QueryResultText, { count: response.total, query: action?.query }), _jsx(FlexOne, {}), ((action?.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Delete, {}), size: "small", variant: "outlined", color: "error", onClick: () => deleteAction(action?.action_id), children: t('delete') })), execRoles && (_jsx(Button, { startIcon: _jsx(PlayCircleOutline, {}), size: "small", variant: "outlined", color: "success", onClick: () => executeAction(action?.action_id), children: t('route.actions.execute') })), ((action?.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Edit, {}), size: "small", variant: "outlined", component: Link, to: `/action/${params.id}/edit`, children: t('route.actions.edit') }))] }), user.roles.includes('automation_advanced') && (_jsx(FormGroup, { children: _jsx(Stack, { direction: "row", spacing: 1, children: action?.operations
|
|
68
|
+
return (_jsx(PageCenter, { maxWidth: "1500px", textAlign: "left", height: "100%", children: _jsxs(Stack, { spacing: 1, children: [_jsxs(Stack, { direction: "row", justifyContent: "space-between", children: [_jsx(Typography, { variant: "h5", children: action?.name }), action?.owner_id && _jsx(HowlerAvatar, { sx: { width: 32, height: 32 }, userId: action.owner_id })] }), _jsx(Phrase, { fullWidth: true, value: action?.query, disabled: true, size: "small", onChange: () => { }, startAdornment: _jsx(IconButton, { onClick: () => onSearch(action?.query), children: _jsx(Search, { fontSize: "small" }) }) }), _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [response && _jsx(QueryResultText, { count: response.total, query: action?.query }), _jsx(FlexOne, {}), ((action?.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Delete, {}), size: "small", variant: "outlined", color: "error", onClick: () => withConfirmDeleteModal(() => deleteAction(action?.action_id)), children: t('button.delete') })), execRoles && (_jsx(Button, { startIcon: _jsx(PlayCircleOutline, {}), size: "small", variant: "outlined", color: "success", onClick: () => executeAction(action?.action_id), children: t('route.actions.execute') })), ((action?.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(Button, { startIcon: _jsx(Edit, {}), size: "small", variant: "outlined", component: Link, to: `/action/${params.id}/edit`, children: t('route.actions.edit') }))] }), user.roles.includes('automation_advanced') && (_jsx(FormGroup, { children: _jsx(Stack, { direction: "row", spacing: 1, children: action?.operations
|
|
67
69
|
?.map(a => (operations ?? []).find(_action => _action.id === a.operation_id)?.triggers ?? [])
|
|
68
70
|
.reduce((acc, triggers) => acc.filter(_t => triggers.includes(_t)))
|
|
69
71
|
.map(trigger => (_jsx(FormControlLabel, { control: _jsx(Checkbox, { name: trigger, onChange: onTriggerChange, checked: action?.triggers?.includes(trigger) ?? false }), label: t(`route.actions.trigger.${trigger}`) }, trigger))) }) })), loading &&
|
|
@@ -3,6 +3,7 @@ import { Delete, Engineering, Terminal } from '@mui/icons-material';
|
|
|
3
3
|
import { Autocomplete, Card, CardContent, CardHeader, Chip, Grid, IconButton, Stack, TextField, Tooltip, Typography } from '@mui/material';
|
|
4
4
|
import api from '@cccsaurora/howler-ui/api';
|
|
5
5
|
import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
|
|
6
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
6
7
|
import FlexOne from '@cccsaurora/howler-ui/components/elements/addons/layout/FlexOne';
|
|
7
8
|
import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
|
|
8
9
|
import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
|
|
@@ -22,6 +23,7 @@ const ActionSearch = () => {
|
|
|
22
23
|
const { user } = useAppUser();
|
|
23
24
|
const { dispatchApi } = useMyApi();
|
|
24
25
|
const { load } = useContext(TuiListMethodContext);
|
|
26
|
+
const { withConfirmDeleteModal } = useContext(ModalContext);
|
|
25
27
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
26
28
|
const { deleteAction } = useMyActionFunctions();
|
|
27
29
|
const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
|
|
@@ -70,6 +72,14 @@ const ActionSearch = () => {
|
|
|
70
72
|
setOffset(_offset);
|
|
71
73
|
}
|
|
72
74
|
}, [offset, searchParams, setSearchParams]);
|
|
75
|
+
const onDelete = useCallback((e, actionId) => {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
e.stopPropagation();
|
|
78
|
+
withConfirmDeleteModal(async () => {
|
|
79
|
+
await deleteAction(actionId);
|
|
80
|
+
onSearch();
|
|
81
|
+
});
|
|
82
|
+
}, [deleteAction, onSearch, withConfirmDeleteModal]);
|
|
73
83
|
// Effect to initialize list of users.
|
|
74
84
|
useEffect(() => {
|
|
75
85
|
onSearch();
|
|
@@ -105,13 +115,8 @@ const ActionSearch = () => {
|
|
|
105
115
|
transitionProperty: 'border-color',
|
|
106
116
|
cursor: 'pointer',
|
|
107
117
|
mt: 1
|
|
108
|
-
}, children: [_jsx(CardHeader, { title: _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h5", children: item.item.name }), item.item.triggers.length > 0 && (_jsx(Tooltip, { title: _jsx(Trans, { i18nKey: "route.actions.trigger.description", values: { triggers: item.item.triggers.join(', ') }, components: { bold: _jsx("strong", {}) } }), children: _jsx(Engineering, {}) })), _jsx(FlexOne, {}), ((item.item.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(IconButton, { size: "small", onClick:
|
|
109
|
-
|
|
110
|
-
e.stopPropagation();
|
|
111
|
-
await deleteAction(item.item.action_id);
|
|
112
|
-
onSearch();
|
|
113
|
-
}, children: _jsx(Delete, {}) })), _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important' }, userId: item.item.owner_id })] }), subheader: item.item.query }), _jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsx(Grid, { container: true, spacing: 1, children: item.item.operations.map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { label: t(`operations.${d.operation_id}`) }) }, d.operation_id))) }) })] }, item.item.name));
|
|
114
|
-
}, [deleteAction, editRoles, navigate, onSearch, t, user.roles, user.username]);
|
|
118
|
+
}, children: [_jsx(CardHeader, { title: _jsxs(Stack, { direction: "row", spacing: 1, alignItems: "center", children: [_jsx(Typography, { variant: "h5", children: item.item.name }), item.item.triggers.length > 0 && (_jsx(Tooltip, { title: _jsx(Trans, { i18nKey: "route.actions.trigger.description", values: { triggers: item.item.triggers.join(', ') }, components: { bold: _jsx("strong", {}) } }), children: _jsx(Engineering, {}) })), _jsx(FlexOne, {}), ((item.item.owner_id === user.username && editRoles) || user.roles?.includes('admin')) && (_jsx(IconButton, { size: "small", onClick: e => onDelete(e, item.item.action_id), children: _jsx(Delete, {}) })), _jsx(HowlerAvatar, { sx: { width: 24, height: 24, marginRight: '8px !important' }, userId: item.item.owner_id })] }), subheader: item.item.query }), _jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsx(Grid, { container: true, spacing: 1, children: item.item.operations.map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { size: "small", label: t(`operations.${d.operation_id}`) }) }, d.operation_id))) }) })] }, item.item.name));
|
|
119
|
+
}, [editRoles, navigate, onDelete, t, user.roles, user.username]);
|
|
115
120
|
return (_jsx(ItemManager, { onSearch: onSearch, onCreate: editRoles ? () => navigate('/action/execute') : undefined, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, aboveSearch: _jsx(Typography, { sx: theme => ({ fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }), variant: "body2", children: t('route.actions.search.prompt') }), searchFilters: _jsx(Autocomplete, { multiple: true, size: "small", value: searchModifiers, onChange: (__, values) => setSearchModifiers(values), getOptionLabel: trigger => t(`route.actions.trigger.${trigger}`), options: VALID_ACTION_TRIGGERS, renderInput: params => (_jsx(TextField, { ...params, sx: { maxWidth: '500px' }, label: t('route.actions.trigger') })) }), renderer: renderer, response: response, createPrompt: "route.actions.create", searchPrompt: "route.actions.search", createIcon: _jsx(Terminal, { sx: { mr: 1 } }) }));
|
|
116
121
|
};
|
|
117
122
|
const ActionSearchProvider = () => {
|
|
@@ -233,7 +233,7 @@ const QueryBuilder = () => {
|
|
|
233
233
|
height: '100%',
|
|
234
234
|
'& .MuiFormControl-root': { height: '100%', '& > div': { height: '100%' } }
|
|
235
235
|
}
|
|
236
|
-
], slotProps: { paper: { sx: { minWidth: '600px' } } } }), _jsx(Card, { variant: "outlined", sx: { flex: 1, maxWidth: '350px', minWidth: '210px' }, children: _jsxs(Stack, { spacing: 0.5, sx: { px: 1, alignItems: 'start' }, children: [_jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: [t('route.advanced.rows'), ": ", STEPS[rows]] }), _jsx(Slider, { size: "small", valueLabelDisplay: "off", value: rows, onChange: (_, value) => setRows(value), min: 0, max: 9, step: 1, marks: true, track: false, sx: { py: 0.5 } })] }) })] }), type === 'lucene' && (_jsx(Autocomplete, { size: "small", getOptionLabel: opt => t(`route.advanced.query.type.${opt}`), options: LUCENE_QUERY_OPTIONS, value: queryType, onChange: (_event, value) => setQueryType(value), renderInput: params => (_jsx(TextField, { ...params, label: t('route.advanced.query.lucene.type'), sx: { minWidth: '230px' } })), renderOption: (props, option) => (_jsx(ListItemText, { ...props, sx: { flexDirection: 'column', alignItems: 'start !important' }, primary: t(`route.advanced.query.type.${option}`), secondary: t(`route.advanced.query.type.${option}.description`) })) })), queryType === 'groupby' && (_jsx(Autocomplete, { size: "small", options: fieldOptions, value: groupByField, onChange: (__, value) => setGroupByField(value), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.pivot.field') }), sx: { minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), allFields && queryType !== 'facet' ? (_jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: allFields, onChange: (__, checked) => setAllFields(checked) }), label: t('route.advanced.fields.all'), sx: { '& > span': { color: 'text.secondary' }, alignSelf: 'start' } })) : (_jsx(Autocomplete, { fullWidth: true, renderTags: values => values.length <= 3 ? (_jsx(Stack, { direction: "row", spacing: 0.5, children: values.map(_value => (_jsx(Chip, { label: _value }, _value))) })) : (_jsx(Tooltip, { title: _jsx(Stack, { spacing: 1, children: values.map(_value => (_jsx("span", { children: _value }, _value))) }), children: _jsx(Chip, { label: values.length }) })), multiple: true, size: "small", options: fieldOptions, value: fields, onChange: (__, values) => (values.length > 0 ? setFields(values) : setAllFields(true)), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.fields') }), sx: { maxWidth: '500px', width: '20vw', minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), _jsx(FlexOne, {}), type === 'lucene' &&
|
|
236
|
+
], slotProps: { paper: { sx: { minWidth: '600px' } } } }), _jsx(Card, { variant: "outlined", sx: { flex: 1, maxWidth: '350px', minWidth: '210px' }, children: _jsxs(Stack, { spacing: 0.5, sx: { px: 1, alignItems: 'start' }, children: [_jsxs(Typography, { variant: "caption", color: "text.secondary", sx: { whiteSpace: 'nowrap' }, children: [t('route.advanced.rows'), ": ", STEPS[rows]] }), _jsx(Slider, { size: "small", valueLabelDisplay: "off", value: rows, onChange: (_, value) => setRows(value), min: 0, max: 9, step: 1, marks: true, track: false, sx: { py: 0.5 } })] }) })] }), type === 'lucene' && (_jsx(Autocomplete, { size: "small", getOptionLabel: opt => t(`route.advanced.query.type.${opt}`), options: LUCENE_QUERY_OPTIONS, value: queryType, onChange: (_event, value) => setQueryType(value), renderInput: params => (_jsx(TextField, { ...params, label: t('route.advanced.query.lucene.type'), sx: { minWidth: '230px' } })), renderOption: (props, option) => (_jsx(ListItemText, { ...props, sx: { flexDirection: 'column', alignItems: 'start !important' }, primary: t(`route.advanced.query.type.${option}`), secondary: t(`route.advanced.query.type.${option}.description`) })) })), queryType === 'groupby' && (_jsx(Autocomplete, { size: "small", options: fieldOptions, value: groupByField, onChange: (__, value) => setGroupByField(value), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.pivot.field') }), sx: { minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), allFields && queryType !== 'facet' ? (_jsx(FormControlLabel, { control: _jsx(Checkbox, { size: "small", checked: allFields, onChange: (__, checked) => setAllFields(checked) }), label: t('route.advanced.fields.all'), sx: { '& > span': { color: 'text.secondary' }, alignSelf: 'start' } })) : (_jsx(Autocomplete, { fullWidth: true, renderTags: values => values.length <= 3 ? (_jsx(Stack, { direction: "row", spacing: 0.5, children: values.map(_value => (_jsx(Chip, { size: "small", label: _value }, _value))) })) : (_jsx(Tooltip, { title: _jsx(Stack, { spacing: 1, children: values.map(_value => (_jsx("span", { children: _value }, _value))) }), children: _jsx(Chip, { size: "small", label: values.length }) })), multiple: true, size: "small", options: fieldOptions, value: fields, onChange: (__, values) => (values.length > 0 ? setFields(values) : setAllFields(true)), renderInput: params => _jsx(TextField, { ...params, label: t('route.advanced.fields') }), sx: { maxWidth: '500px', width: '20vw', minWidth: '200px', '& label': { zIndex: 1200 } }, onKeyDown: onKeyDown, PopperComponent: CustomPopper })), _jsx(FlexOne, {}), type === 'lucene' &&
|
|
237
237
|
(smallButtons ? (_jsx(Tooltip, { title: t('route.advanced.open'), children: _jsx(IconButton, { color: "primary", sx: { alignSelf: 'center' }, component: Link, disabled: !response, to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}`, children: _jsx(OpenInNew, { fontSize: "small" }) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", startIcon: _jsx(OpenInNew, {}), component: Link, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, children: t('route.advanced.open') }))), smallButtons ? (_jsx(Tooltip, { title: response ? t('route.advanced.create.rule') : t('route.advanced.create.rule.disabled'), children: _jsx(IconButton, { size: "small", sx: { alignSelf: 'center' }, color: "info", onClick: onCreateRule, disabled: !response, children: _jsx(SsidChart, {}) }) })) : (_jsx(CustomButton, { size: "small", variant: "outlined", color: "info", startIcon: _jsx(SsidChart, {}), onClick: onCreateRule, disabled: !response, ...{ to: `/hits?query=${sanitizeMultilineLucene(query).replaceAll('\n', ' ').trim()}` }, tooltip: !response && t('route.advanced.create.rule.disabled'), children: t('route.advanced.create.rule') }))] }), _jsxs(Box, { width: "100%", height: "calc(100vh - 112px)", sx: { position: 'relative', overflow: 'hidden', borderTop: `thin solid ${theme.palette.divider}` }, children: [_jsx(Box, { sx: {
|
|
238
238
|
position: 'absolute',
|
|
239
239
|
top: 0,
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { useMonaco } from '@monaco-editor/react';
|
|
3
3
|
import { Box, useTheme } from '@mui/material';
|
|
4
4
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
5
|
-
import {
|
|
5
|
+
import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
|
|
6
6
|
import ThemedEditor from '@cccsaurora/howler-ui/components/elements/ThemedEditor';
|
|
7
7
|
import { memo, useCallback, useContext, useEffect, useMemo } from 'react';
|
|
8
8
|
import { useContextSelector } from 'use-context-selector';
|
|
@@ -20,8 +20,8 @@ const QueryEditor = ({ query, setQuery, onMount, language = 'lucene', fontSize =
|
|
|
20
20
|
const yamlCompletion = useYamlCompletionProvider();
|
|
21
21
|
const eqlCompletion = useEQLCompletionProvider();
|
|
22
22
|
const historyCompletion = useHistoryCompletionProvider();
|
|
23
|
-
const fzfSearch = useContextSelector(
|
|
24
|
-
const setFzfSearch = useContextSelector(
|
|
23
|
+
const fzfSearch = useContextSelector(HitSearchContext, ctx => ctx?.fzfSearch ?? false);
|
|
24
|
+
const setFzfSearch = useContextSelector(HitSearchContext, ctx => ctx?.setFzfSearch);
|
|
25
25
|
const beforeEditorMount = useCallback((_monaco) => {
|
|
26
26
|
_monaco.languages.register({ id: 'lucene' });
|
|
27
27
|
_monaco.languages.register({ id: 'eql' });
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useMonaco } from '@monaco-editor/react';
|
|
2
|
-
import {
|
|
2
|
+
import { HitSearchContext } from '@cccsaurora/howler-ui/components/app/providers/HitSearchProvider';
|
|
3
3
|
import Fuse from 'fuse.js';
|
|
4
4
|
import { useMemo } from 'react';
|
|
5
5
|
import { useContextSelector } from 'use-context-selector';
|
|
6
6
|
import { twitterShort } from '@cccsaurora/howler-ui/utils/utils';
|
|
7
7
|
const useHistoryCompletionProvider = () => {
|
|
8
8
|
const monaco = useMonaco();
|
|
9
|
-
const fzfSearch = useContextSelector(
|
|
10
|
-
const queryHistory = useContextSelector(
|
|
9
|
+
const fzfSearch = useContextSelector(HitSearchContext, ctx => ctx?.fzfSearch);
|
|
10
|
+
const queryHistory = useContextSelector(HitSearchContext, ctx => ctx?.queryHistory ?? {});
|
|
11
11
|
// Using fuse for fuzzy searching
|
|
12
12
|
const fuse = useMemo(() => new Fuse(Object.keys(queryHistory), { keys: ['key'], threshold: 0.4 }), [queryHistory]);
|
|
13
13
|
return {
|
|
@@ -5,6 +5,7 @@ import api from '@cccsaurora/howler-ui/api';
|
|
|
5
5
|
import { useAppUser } from '@cccsaurora/howler-ui/commons/components/app/hooks';
|
|
6
6
|
import PageCenter from '@cccsaurora/howler-ui/commons/components/pages/PageCenter';
|
|
7
7
|
import { ApiConfigContext } from '@cccsaurora/howler-ui/components/app/providers/ApiConfigProvider';
|
|
8
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
8
9
|
import { UserListContext } from '@cccsaurora/howler-ui/components/app/providers/UserListProvider';
|
|
9
10
|
import UserList from '@cccsaurora/howler-ui/components/elements/UserList';
|
|
10
11
|
import HowlerAvatar from '@cccsaurora/howler-ui/components/elements/display/HowlerAvatar';
|
|
@@ -31,6 +32,7 @@ const AnalyticDetails = () => {
|
|
|
31
32
|
const { dispatchApi } = useMyApi();
|
|
32
33
|
const theme = useTheme();
|
|
33
34
|
const { showSuccessMessage } = useMySnackbar();
|
|
35
|
+
const { withConfirmDeleteModal } = useContext(ModalContext);
|
|
34
36
|
const { users, searchUsers } = useContext(UserListContext);
|
|
35
37
|
const { config } = useContext(ApiConfigContext);
|
|
36
38
|
const [analytic, setAnalytic] = useState(null);
|
|
@@ -51,18 +53,20 @@ const AnalyticDetails = () => {
|
|
|
51
53
|
_setFilter(detection);
|
|
52
54
|
}
|
|
53
55
|
}, [filter]);
|
|
54
|
-
const onOwnerChange = useCallback(async (
|
|
56
|
+
const onOwnerChange = useCallback(async (ownerId) => {
|
|
55
57
|
const result = await dispatchApi(api.analytic.owner.post(analytic.analytic_id, { username: ownerId }), {
|
|
56
58
|
throwError: true,
|
|
57
59
|
showError: true
|
|
58
60
|
});
|
|
59
61
|
setAnalytic(result);
|
|
60
62
|
}, [analytic?.analytic_id, dispatchApi]);
|
|
61
|
-
const onDelete = useCallback(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const onDelete = useCallback(() => {
|
|
64
|
+
withConfirmDeleteModal(async () => {
|
|
65
|
+
await dispatchApi(api.analytic.del(analytic?.analytic_id));
|
|
66
|
+
showSuccessMessage(t('route.analytics.deleted'));
|
|
67
|
+
navigate('/analytics');
|
|
68
|
+
});
|
|
69
|
+
}, [analytic?.analytic_id, dispatchApi, navigate, withConfirmDeleteModal, showSuccessMessage, t]);
|
|
66
70
|
const onEdit = useCallback(async () => {
|
|
67
71
|
if (editingInterval) {
|
|
68
72
|
setIntervalLoading(true);
|
|
@@ -108,14 +112,14 @@ const AnalyticDetails = () => {
|
|
|
108
112
|
marginTop: '0 !important',
|
|
109
113
|
marginLeft: `${theme.spacing(-1)} !important`,
|
|
110
114
|
marginRight: `${theme.spacing(-1)} !important`
|
|
111
|
-
},
|
|
115
|
+
}, userId: analytic?.owner, onChange: onOwnerChange, i18nLabel: "route.analytics.set.owner" })) : (_jsx(HowlerAvatar, { userId: analytic?.owner })), _jsx(Stack, { children: users[analytic?.owner] ? (_jsxs(_Fragment, { children: [_jsx(Typography, { variant: "body1", children: users[analytic?.owner].name }), _jsx(Typography, { component: "a", href: `mailto:${users[analytic?.owner].email}`, variant: "caption", color: "text.secondary", children: users[analytic?.owner].email })] })) : (_jsxs(_Fragment, { children: [_jsx(Skeleton, { variant: "text", width: "70px" }), _jsx(Skeleton, { variant: "text", width: "60px" })] })) })] })] }), filteredContributors.length > 0 && (_jsxs(Stack, { spacing: 1, children: [_jsx(Typography, { variant: "body1", color: "text.secondary", children: t('route.analytics.contributors') }), _jsx(Stack, { direction: "row", alignItems: "center", spacing: 1, children: filteredContributors.map(_user => (_jsx(HowlerAvatar, { userId: _user }, _user))) })] })), analytic?.rule_crontab && (_jsxs(Stack, { direction: "row", spacing: 1, children: [_jsxs(Stack, { spacing: 1, justifyContent: "space-between", children: [_jsx(Typography, { variant: "body1", color: "text.secondary", children: t('rule.interval') }), editingInterval ? (_jsxs(FormControl, { sx: { minWidth: '200px' }, children: [_jsx(InputLabel, { children: t('rule.interval') }), _jsx(Select, { size: "small", label: t('rule.interval'), onChange: event => setCrontab(event.target.value), value: crontab, children: RULE_INTERVALS.map(interval => (_jsx(MenuItem, { value: interval.crontab, children: t(interval.key) }, interval.key))) })] })) : (_jsx("code", { style: {
|
|
112
116
|
backgroundColor: theme.palette.background.paper,
|
|
113
117
|
padding: theme.spacing(0.5),
|
|
114
118
|
alignSelf: 'start',
|
|
115
119
|
borderRadius: theme.shape.borderRadius,
|
|
116
120
|
border: `thin solid ${theme.palette.divider}`,
|
|
117
121
|
width: '100%'
|
|
118
|
-
}, children: analytic.rule_crontab }))] }), (analytic?.owner === user.username || user.roles.includes('admin')) && (_jsx(Tooltip, { title: editingInterval ? t('rule.interval.save') : t('rule.interval.edit'), children: _jsx(IconButton, { disabled: intervalLoading, sx: { alignSelf: 'end' }, onClick: onEdit, children: intervalLoading ? _jsx(CircularProgress, { size: 20 }) : editingInterval ? _jsx(Check, {}) : _jsx(Edit, {}) }) }))] }))] }), _jsxs(Grid, { container: true, children: [_jsx(Grid, { item: true, xs: 12, md: 9, children: _jsxs(Tabs, { value: tab, onChange: (_, _tab) => setTab(_tab), children: [_jsx(Tab, { label: t('route.analytics.tab.overview'), value: "overview" }), _jsx(Tab, { label: t('route.analytics.tab.comments'), value: "comments" }), _jsx(Tab, { label: t('route.analytics.tab.hit_comments'), value: "hit_comments" }), _jsx(Tab, { label: t('route.analytics.tab.templates'), value: "templates" }), _jsx(Tab, { label: t('route.analytics.tab.overviews'), value: "overviews" }), analytic?.rule && _jsx(Tab, { label: t('route.analytics.tab.rule'), value: "rule" }), config?.configuration.features.notebook && (_jsx(Tab, { label: t('route.analytics.tab.notebooks'), value: "notebooks" })), _jsx(Tab, { label: t('route.analytics.tab.triage'), value: "triage" })] }) }), ['comments', 'hit_comments'].includes(tab) && (_jsx(Grid, { item: true, xs: 12, md: 3, children: _jsx(Autocomplete, { options: analytic?.detections ?? [], renderInput: param => _jsx(TextField, { ...param, label:
|
|
122
|
+
}, children: analytic.rule_crontab }))] }), (analytic?.owner === user.username || user.roles.includes('admin')) && (_jsx(Tooltip, { title: editingInterval ? t('rule.interval.save') : t('rule.interval.edit'), children: _jsx(IconButton, { disabled: intervalLoading, sx: { alignSelf: 'end' }, onClick: onEdit, children: intervalLoading ? _jsx(CircularProgress, { size: 20 }) : editingInterval ? _jsx(Check, {}) : _jsx(Edit, {}) }) }))] }))] }), _jsxs(Grid, { container: true, children: [_jsx(Grid, { item: true, xs: 12, md: 9, children: _jsxs(Tabs, { value: tab, onChange: (_, _tab) => setTab(_tab), children: [_jsx(Tab, { label: t('route.analytics.tab.overview'), value: "overview" }), _jsx(Tab, { label: t('route.analytics.tab.comments'), value: "comments" }), _jsx(Tab, { label: t('route.analytics.tab.hit_comments'), value: "hit_comments" }), _jsx(Tab, { label: t('route.analytics.tab.templates'), value: "templates" }), _jsx(Tab, { label: t('route.analytics.tab.overviews'), value: "overviews" }), analytic?.rule && _jsx(Tab, { label: t('route.analytics.tab.rule'), value: "rule" }), config?.configuration.features.notebook && (_jsx(Tab, { label: t('route.analytics.tab.notebooks'), value: "notebooks" })), _jsx(Tab, { label: t('route.analytics.tab.triage'), value: "triage" })] }) }), ['comments', 'hit_comments'].includes(tab) && (_jsx(Grid, { item: true, xs: 12, md: 3, children: _jsx(Autocomplete, { options: analytic?.detections ?? [], renderInput: param => _jsx(TextField, { ...param, label: t('route.analytics.dropdown.detection') }), value: filter, onChange: (_, v) => setFilter(v) }) }))] }), {
|
|
119
123
|
comments: _jsx(AnalyticComments, { analytic: analytic, setAnalytic: setAnalytic }),
|
|
120
124
|
hit_comments: _jsx(AnalyticHitComments, { analytic: analytic }),
|
|
121
125
|
overview: _jsx(AnalyticOverview, { analytic: analytic, setAnalytic: setAnalytic }),
|
|
@@ -128,7 +128,7 @@ const AnalyticSearchBase = () => {
|
|
|
128
128
|
padding: theme.spacing(0.5),
|
|
129
129
|
borderRadius: theme.shape.borderRadius,
|
|
130
130
|
border: `thin solid ${theme.palette.divider}`
|
|
131
|
-
}, children: item.item.rule_type })] })), _jsx(FlexOne, {}), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mt: 1 }, children: [item.item.owner && _jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: item.item.owner }), filteredContributors.length > 0 && _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsx(AvatarGroup, { children: filteredContributors.map(contributor => (_jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: contributor }, contributor))) })] }), _jsx(Tooltip, { title: t('button.pin'), children: _jsx(IconButton, { size: "small", onClick: e => onFavourite(e, item.item), children: appUser.user?.favourite_analytics?.includes(item.item.analytic_id) ? _jsx(Star, {}) : _jsx(StarBorder, {}) }) })] }) }), item.item.detections?.length > 0 && (_jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsxs(Grid, { container: true, spacing: 0.5, sx: { marginTop: `${theme.spacing(-0.5)} !important` }, children: [item.item.detections.slice(0, 5).map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { variant: "outlined", label: d }) }, d))), item.item.detections.length > 5 && (_jsx(Grid, { item: true, children: _jsx(Tooltip, { title: _jsx(Stack, { children: item.item.detections.slice(5).map(d => (_jsx("span", { children: d }, d))) }), children: _jsx(Chip, { variant: "outlined", label: `+ ${item.item.detections.length - 5}` }) }) }))] }) }))] }, item.item.name));
|
|
131
|
+
}, children: item.item.rule_type })] })), _jsx(FlexOne, {}), _jsxs(Stack, { direction: "row", spacing: 1, sx: { mt: 1 }, children: [item.item.owner && _jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: item.item.owner }), filteredContributors.length > 0 && _jsx(Divider, { orientation: "vertical", flexItem: true }), _jsx(AvatarGroup, { children: filteredContributors.map(contributor => (_jsx(HowlerAvatar, { sx: { width: 24, height: 24 }, userId: contributor }, contributor))) })] }), _jsx(Tooltip, { title: t('button.pin'), children: _jsx(IconButton, { size: "small", onClick: e => onFavourite(e, item.item), children: appUser.user?.favourite_analytics?.includes(item.item.analytic_id) ? _jsx(Star, {}) : _jsx(StarBorder, {}) }) })] }) }), item.item.detections?.length > 0 && (_jsx(CardContent, { sx: { paddingTop: 0 }, children: _jsxs(Grid, { container: true, spacing: 0.5, sx: { marginTop: `${theme.spacing(-0.5)} !important` }, children: [item.item.detections.slice(0, 5).map(d => (_jsx(Grid, { item: true, children: _jsx(Chip, { size: "small", variant: "outlined", label: d }) }, d))), item.item.detections.length > 5 && (_jsx(Grid, { item: true, children: _jsx(Tooltip, { title: _jsx(Stack, { children: item.item.detections.slice(5).map(d => (_jsx("span", { children: d }, d))) }), children: _jsx(Chip, { size: "small", variant: "outlined", label: `+ ${item.item.detections.length - 5}` }) }) }))] }) }))] }, item.item.name));
|
|
132
132
|
}, [appUser.user?.favourite_analytics, navigate, onFavourite, t, theme]);
|
|
133
133
|
return (_jsx(ItemManager, { onSearch: onSearch, onPageChange: onPageChange, phrase: phrase, setPhrase: setPhrase, hasError: hasError, searching: searching, searchAdornment: _jsx(InputAdornment, { position: "end", children: _jsx(Tooltip, { title: t(`route.analytics.search.filter.rules.${onlyRules < 0 ? 'hide' : onlyRules > 0 ? 'show' : 'toggle'}`), children: _jsx(IconButton, { onClick: () => setOnlyRules((((onlyRules + 2) % 3) - 1)), children: _jsx(SsidChart, { color: onlyRules < 0 ? 'error' : onlyRules > 0 ? 'info' : 'inherit', sx: { transition: theme.transitions.create(['color']) } }) }) }) }), aboveSearch: _jsx(Typography, { sx: { fontStyle: 'italic', color: theme.palette.text.disabled, mb: 0.5 }, variant: "body2", children: t('route.analytics.search.prompt') }), renderer: renderer, response: response, searchPrompt: "route.analytics.manager.search" }));
|
|
134
134
|
};
|
|
@@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
13
13
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
14
14
|
import { useContextSelector } from 'use-context-selector';
|
|
15
15
|
import QueryResultText from '../../elements/display/QueryResultText';
|
|
16
|
-
import
|
|
16
|
+
import HitQuery from '../hits/search/HitQuery';
|
|
17
17
|
import LeadForm from './LeadForm';
|
|
18
18
|
import PivotForm from './PivotForm';
|
|
19
19
|
const DossierEditor = () => {
|
|
@@ -188,6 +188,6 @@ const DossierEditor = () => {
|
|
|
188
188
|
fontSize: '0.9em',
|
|
189
189
|
fontStyle: 'italic',
|
|
190
190
|
mb: 0.5
|
|
191
|
-
}), variant: "body2", children: t('hit.search.prompt') }), _jsx(
|
|
191
|
+
}), variant: "body2", children: t('hit.search.prompt') }), _jsx(HitQuery, { disabled: !dossier || loading, onChange: (_val, isDirty) => setSearchDirty(isDirty), triggerSearch: query => setDossier(_dossier => ({ ..._dossier, query })) }), searchTotal >= 0 && _jsx(QueryResultText, { count: searchTotal, query: dossier.query })] }) }), _jsxs(Tabs, { value: tab, onChange: (_ev, value) => setTab(value), children: [_jsx(Tab, { label: t('route.dossiers.manager.tabs.leads'), value: "leads" }), _jsx(Tab, { label: t('route.dossiers.manager.tabs.pivots'), value: "pivots" })] }), tab === 'leads' && _jsx(LeadForm, { dossier: dossier, setDossier: setDossier, loading: loading }), tab === 'pivots' && _jsx(PivotForm, { dossier: dossier, setDossier: setDossier, loading: loading })] })] }) }));
|
|
192
192
|
};
|
|
193
193
|
export default memo(DossierEditor);
|
|
@@ -61,7 +61,7 @@ vi.mock('commons/components/pages/PageCenter', () => ({
|
|
|
61
61
|
vi.mock('../../elements/display/QueryResultText', () => ({
|
|
62
62
|
default: ({ count, query }) => (_jsxs("div", { id: "query-result-text", children: [count, " ", ' results for ', " ", query] }))
|
|
63
63
|
}));
|
|
64
|
-
vi.mock('../hits/search/
|
|
64
|
+
vi.mock('../hits/search/HitQuery', () => ({
|
|
65
65
|
default: ({ onChange, triggerSearch, disabled }) => {
|
|
66
66
|
return (_jsxs("div", { id: "hit-query", children: [_jsx("input", { id: "query-input", disabled: disabled, onChange: e => {
|
|
67
67
|
onChange?.(e.target.value, false);
|
|
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Topic } from '@mui/icons-material';
|
|
3
3
|
import { Typography } from '@mui/material';
|
|
4
4
|
import api from '@cccsaurora/howler-ui/api';
|
|
5
|
+
import { ModalContext } from '@cccsaurora/howler-ui/components/app/providers/ModalProvider';
|
|
5
6
|
import { TuiListProvider } from '@cccsaurora/howler-ui/components/elements/addons/lists';
|
|
6
7
|
import { TuiListMethodContext } from '@cccsaurora/howler-ui/components/elements/addons/lists/TuiListProvider';
|
|
7
8
|
import ItemManager from '@cccsaurora/howler-ui/components/elements/display/ItemManager';
|
|
@@ -18,6 +19,7 @@ const DossiersBase = () => {
|
|
|
18
19
|
const navigate = useNavigate();
|
|
19
20
|
const { dispatchApi } = useMyApi();
|
|
20
21
|
const { showSuccessMessage } = useMySnackbar();
|
|
22
|
+
const { withConfirmDeleteModal } = useContext(ModalContext);
|
|
21
23
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
22
24
|
const { load } = useContext(TuiListMethodContext);
|
|
23
25
|
const pageCount = useMyLocalStorageItem(StorageKey.PAGE_COUNT, 25)[0];
|
|
@@ -73,19 +75,21 @@ const DossiersBase = () => {
|
|
|
73
75
|
setOffset(_offset);
|
|
74
76
|
}
|
|
75
77
|
}, [offset, searchParams, setSearchParams]);
|
|
76
|
-
const onDelete = useCallback(
|
|
78
|
+
const onDelete = useCallback((e, id) => {
|
|
77
79
|
e.preventDefault();
|
|
78
80
|
e.stopPropagation();
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
81
|
+
withConfirmDeleteModal(async () => {
|
|
82
|
+
try {
|
|
83
|
+
await dispatchApi(api.dossier.del(id), { throwError: false, showError: true });
|
|
84
|
+
await onSearch();
|
|
85
|
+
showSuccessMessage(t('route.dossiers.manager.delete.success'));
|
|
86
|
+
}
|
|
87
|
+
catch (_err) {
|
|
88
|
+
// eslint-disable-next-line no-console
|
|
89
|
+
console.warn(_err);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
}, [dispatchApi, onSearch, withConfirmDeleteModal, showSuccessMessage, t]);
|
|
89
93
|
useEffect(() => {
|
|
90
94
|
onSearch();
|
|
91
95
|
if (!searchParams.has('offset')) {
|
|
@@ -49,7 +49,7 @@ const ApiDocumentation = () => {
|
|
|
49
49
|
.replace(/(\S+)\s+=>\s+(.+)/g, '\n`$1`: $2\n')
|
|
50
50
|
.replace(/(Data Block:\n)([\s\S]+)(Result Example:)/, (__, p1, p2, p3) => `${p1}\`\`\`\n${p2.trim()}\n\`\`\`\n${p3}`)
|
|
51
51
|
.replace(/(Result Example:\n)([\s\S]+)$/, (__, p1, p2) => `${p1}\`\`\`\n${p2.trim()}\n\`\`\``) }));
|
|
52
|
-
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { style: { marginBottom: '1rem' }, sx: [isLg && { '& > td': { borderBottom: 0 } }], children: [_jsx(TableCell, { children: _jsxs(Stack, { direction: "column", spacing: 1, alignItems: "start", children: [_jsx(Typography, { children: endpoint.name }), _jsx("code", { children: endpoint.path }), _jsxs(Stack, { direction: "row", spacing: 1, children: [endpoint.complete ? (_jsx(Chip, { label: "Stable", color: "success" })) : (_jsx(Chip, { label: "Unstable", color: "error" })), endpoint.protected ? (_jsx(Chip, { label: "Protected", color: "warning" })) : (_jsx(Chip, { label: "Unprotected" }))] }), _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.methods.map(m => (_jsx(Chip, { size: "small", label: m }, m))) }), endpoint.ui_only && _jsx(Chip, { label: "UI Only" })] }) }), _jsx(TableCell, { children: _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.required_type.map(type => (_jsx(Chip, { size: "small", label: type, color: user.roles?.includes(type) ? 'success' : 'default' }, type))) }) }), _jsx(TableCell, { children: _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.required_priv.map((p) => (_jsx(Chip, { size: "small", label: t(APIKEY_LABELS[p]) }, p))) }) }), !isLg && _jsx(TableCell, { children: documentationCell })] }), isLg && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 3, sx: { '& pre': { whiteSpace: 'pre-wrap' } }, children: documentationCell }) }))] }, endpoint.id));
|
|
52
|
+
return (_jsxs(Fragment, { children: [_jsxs(TableRow, { style: { marginBottom: '1rem' }, sx: [isLg && { '& > td': { borderBottom: 0 } }], children: [_jsx(TableCell, { children: _jsxs(Stack, { direction: "column", spacing: 1, alignItems: "start", children: [_jsx(Typography, { children: endpoint.name }), _jsx("code", { children: endpoint.path }), _jsxs(Stack, { direction: "row", spacing: 1, children: [endpoint.complete ? (_jsx(Chip, { size: "small", label: "Stable", color: "success" })) : (_jsx(Chip, { size: "small", label: "Unstable", color: "error" })), endpoint.protected ? (_jsx(Chip, { size: "small", label: "Protected", color: "warning" })) : (_jsx(Chip, { size: "small", label: "Unprotected" }))] }), _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.methods.map(m => (_jsx(Chip, { size: "small", label: m }, m))) }), endpoint.ui_only && _jsx(Chip, { size: "small", label: "UI Only" })] }) }), _jsx(TableCell, { children: _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.required_type.map(type => (_jsx(Chip, { size: "small", label: type, color: user.roles?.includes(type) ? 'success' : 'default' }, type))) }) }), _jsx(TableCell, { children: _jsx(Stack, { spacing: 1, direction: "row", children: endpoint.required_priv.map((p) => (_jsx(Chip, { size: "small", label: t(APIKEY_LABELS[p]) }, p))) }) }), !isLg && _jsx(TableCell, { children: documentationCell })] }), isLg && (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: 3, sx: { '& pre': { whiteSpace: 'pre-wrap' } }, children: documentationCell }) }))] }, endpoint.id));
|
|
53
53
|
}) })] }) })] }) }));
|
|
54
54
|
};
|
|
55
55
|
export default ApiDocumentation;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import Markdown from '@cccsaurora/howler-ui/components/elements/display/Markdown';
|
|
3
|
+
import { useMemo } from 'react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import BUNDLES_EN from './markdown/en/bundles.md';
|
|
6
|
+
import BUNDLES_FR from './markdown/fr/bundles.md';
|
|
7
|
+
const BundleDocumentation = () => {
|
|
8
|
+
const { i18n } = useTranslation();
|
|
9
|
+
const md = useMemo(() => (i18n.language === 'en' ? BUNDLES_EN : BUNDLES_FR), [i18n.language]);
|
|
10
|
+
return _jsx(Markdown, { md: md });
|
|
11
|
+
};
|
|
12
|
+
export default BundleDocumentation;
|
|
@@ -5,6 +5,7 @@ import { useScrollRestoration } from '@cccsaurora/howler-ui/components/hooks/use
|
|
|
5
5
|
import { useCallback, useState } from 'react';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
import { useSearchParams } from 'react-router-dom';
|
|
8
|
+
import BundleDocumentation from './BundleDocumentation';
|
|
8
9
|
import HelpTabs from './components/HelpTabs';
|
|
9
10
|
import HitBannerDocumentation from './HitBannerDocumentation';
|
|
10
11
|
import HitLabelsDocumentation from './HitLabelsDocumentation';
|
|
@@ -22,7 +23,8 @@ const HitDocumentation = () => {
|
|
|
22
23
|
searchParams.set('tab', _tab);
|
|
23
24
|
setSearchParams(new URLSearchParams(searchParams));
|
|
24
25
|
}, [searchParams, setSearchParams]);
|
|
25
|
-
return (_jsx(PageCenter, { margin: 4, width: "100%", maxWidth: "1750px", textAlign: "left", children: _jsxs(Stack, { sx: { flexDirection: useHorizontal ? 'column' : 'row', '& h1': { mt: 0 } }, children: [_jsxs(HelpTabs, { value: tab, children: [_jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.schema.title') }), value: "schema", onClick: () => onChange('schema') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.banner.title') }), value: "header", onClick: () => onChange('header') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.links.title') }), value: "links", onClick: () => onChange('links') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.labels.title') }), value: "labels", onClick: () => onChange('labels') })] }), _jsx(Box, { children: {
|
|
26
|
+
return (_jsx(PageCenter, { margin: 4, width: "100%", maxWidth: "1750px", textAlign: "left", children: _jsxs(Stack, { sx: { flexDirection: useHorizontal ? 'column' : 'row', '& h1': { mt: 0 } }, children: [_jsxs(HelpTabs, { value: tab, children: [_jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.schema.title') }), value: "schema", onClick: () => onChange('schema') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.banner.title') }), value: "header", onClick: () => onChange('header') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.bundle.title') }), value: "bundle", onClick: () => onChange('bundle') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.links.title') }), value: "links", onClick: () => onChange('links') }), _jsx(Tab, { label: _jsx(Typography, { variant: "caption", children: t('help.hit.labels.title') }), value: "labels", onClick: () => onChange('labels') })] }), _jsx(Box, { children: {
|
|
27
|
+
bundle: () => _jsx(BundleDocumentation, {}),
|
|
26
28
|
header: () => _jsx(HitBannerDocumentation, {}),
|
|
27
29
|
links: () => _jsx(HitLinksDocumentation, {}),
|
|
28
30
|
labels: () => _jsx(HitLabelsDocumentation, {}),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default "<!-- docs/ingestion/bundles.md -->\n\n# Howler Hit Bundles\n\nHit bundles can be used to easily package together a large number of similar alerts, allowing analysts to easily triage them as a single incident. For example, consider a single computer that repeatedly makes a network call to `baddomain.ru` - while an alert may be generated for every instance of this computer hitting that domain, it makes sense for analysts to treat all these alerts as a single case.\n\n## Creating bundles through the Howler Client\n\nThere are a couple of ways to create a bundle through the howler client:\n\n```python\nfrom howler_client import get_client\n\nhowler = get_client(\"https://howler.dev.analysis.cyber.gc.ca\")\n\n\"\"\"Creating a howler bundle and the hits at the same time\"\"\"\nhowler.bundle.create(\n # First argument is the bundle hit\n {\n \"howler.analytic\": \"example-test\",\n \"howler.score\": 0\n },\n # Second argument is a hit or list of hits to include in the bundle\n [\n {\n \"howler.analytic\": \"example-test\",\n \"howler.score\": 0\n },\n {\n \"howler.analytic\": \"example-test\",\n \"howler.score\": 0\n }\n ]\n)\n\n\"\"\"Creating a howler bundle from existing hits\"\"\"\nhowler.bundle.create(\n {\n \"howler.analytic\": \"example-test\",\n \"howler.score\": 0,\n \"howler.hits\": [\"YcUsL8QsjmwwIdstieROk\", \"6s7MztwuSvz6tM0PgGJhvz\"]\n },\n # Note: In future releases, you won't need to include this argument\n []\n)\n\n\n\"\"\"Creating from a map\"\"\"\nbundle_hit = {\n \"score\": 0,\n \"bundle\": True\n}\n\nmap = {\n \"score\": [\"howler.score\"],\n \"bundle\": [\"howler.is_bundle\"]\n}\n\nhowler.bundle.create_from_map(\"example-test\", bundle_hit, map, [{\"score\": 0}])\n```\n\n## Viewing bundles on the Howler UI\n\nIn order to view created bundles on the Howler UI, you can use the query `howler.is_bundle:true`. This will provide a list of created bundles you can look through.\n\nClicking on a bundle will open up a slightly different search UI to normal. In this case, we automatically filter the search results to include only hits that are included in the bundle. To make this obvious, the header representing the bundle will appear above the search bar.\n\nYou can continue to filter through hits using the same queries as usual, and view them as usual. When triaging a bundle, assessing it will apply this assessment to all hits in the bundle, **except those that have already been triaged**. That is, if the bundle is open, all open hits will be assessed when you assess it.\n\nBundles also have a **Summary** tab not available for regular hits. This summary tab will aid you in aggregating data about all the hits in the bundle. Simply open the tab and click \"Create Summary\". Note that this may take some time, as a large number of queries are being run to aggregate the data.\n"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export default "# Howler Client Documentation\n\nThis documentation will outline how to interact with the howler API using the howler client in both Java and python development environments. We will outline the basic process of creating a new hit in each environment as well as searching howler for hits matching your query.\n\n## Getting started\n\n### Installation\n\nIn order to use the howler client, you need to list it as a dependency in your project.\n\n#### **Python**\n\nSimply install through pip:\n\n```bash\npip install howler-client\n```\n\nYou can also add it to your requirements.txt, or whatever dependency management system you use.\n\n### Authentication\n\nAs outlined in the [Authentication Documentation](/help/auth), there's a number of ways users can choose to authenticate. In order to interface with the howler client, however, the suggested flow is to use an API key. So before we start, let's generate a key.\n\n1. Open the Howler UI you'd like to interface with.\n2. Log in, then click your profile in the top right.\n3. Under user menu, click Settings.\n4. Under User Security, press the (+) icon on the API Keys row.\n5. Name your key, and give it the requisite permissions.\n6. Press Create, and copy the supplied string somewhere safe. **You will not see this string again.**\n\nThis API Key will be supplied to your code later on.\n\n## Python Client\n\nIn order to connect with howler using the python client, there is a fairly simple process to follow:\n\n```python\nfrom howler_client import get_client\n\nUSERNAME = 'user' # Obtain this from the user settings page of the Howler UI\nAPIKEY = 'apikey_name:apikey_data'\n\napikey = (USERNAME, APIKEY)\n\nhowler = get_client(\"$CURRENT_URL\", apikey=apikey)\n```\n\n```alert\nYou can skip generating an API Key and providing it if you're executing this code within HOGWARTS (i.e., on jupyterhub or airflow). OBO will handle authentication for you!\n```\n\nThat's it! You can now use the `howler` object to interact with the server. So what does that actually look like?\n\n### Creating hits in Python\n\nFor the python client, you can create hits using either the `howler.hit.create` or `howler.hit.create_from_map` functions.\n\n#### `create`\n\nThis function takes in a single argument - either a single hit, or a list of them, conforming to the [Howler Schema](/help/hit?tab=schema). Here is a simple example:\n\n```python\n# Some bogus data in the Howler Schema format\nexample_hit = {\n \"howler\": {\n \"analytic\": \"example\",\n \"score\": 10.0\n },\n \"event\": {\n \"reason\": \"Example hit\"\n }\n}\n\nhowler.hit.create(example_hit)\n```\n\nYou can also ingest data in a flat format:\n\n```python\nexample_hit = {\n \"howler.analytic\": \"example\",\n \"howler.score\": 10.0,\n \"event.reason\": \"Example hit\"\n}\n\nhowler.hit.create(example_hit)\n```\n\n#### `create_from_map`\n\nThis function takes in three arguments:\n\n- `tool name`: The name of the analytic creating the hit\n- `map`: A mapping between the raw data you have and the howler schema\n - The format is a dictionary where the keys are the flattened path of the raw data, and the values are a list of flattened paths for Howler's fields where the data will be copied into.\n- `documents`: The raw data you want to add to howler\n\nHere is a simple example:\n\n```python\n# The mapping from our data to howler's schema\nhwl_map = {\n \"file.sha256\": [\"file.hash.sha256\", \"howler.hash\"],\n \"file.name\": [\"file.name\"],\n \"src_ip\": [\"source.ip\", \"related.ip\"],\n \"dest_ip\": [\"destination.ip\", \"related.ip\"],\n \"time.created\": [\"event.start\"],\n}\n\n# Some bogus data in a custom format we want to add to howler\nexample_hit = {\n \"src_ip\": \"0.0.0.0\",\n \"dest_ip\": \"8.8.8.8\",\n \"file\": {\n \"name\": \"hello.exe\",\n \"sha256\": sha256(str(\"hello.exe\").encode()).hexdigest()\n },\n \"time\": {\n \"created\": datetime.now().isoformat()\n },\n}\n\n# Note that the third argument is of type list!\nhowler.hit.create_from_map(\"example_ingestor\", hwl_map, [example_hit])\n```\n\n### Querying Hits\n\nQuerying hits using the howler python client is done using the `howler.search.hit` function. It has a number of required and optional arguments:\n\n- Required:\n - `query`: lucene query (string)\n- Optional:\n - `filters`: Additional lucene queries used to filter the data (list of strings)\n - `fl`: List of fields to return (comma separated string of fields)\n - `offset`: Offset at which the query items should start (integer)\n - `rows`: Number of records to return (integer)\n - `sort`: Field used for sorting with direction (string: ex. 'id desc')\n - `timeout`: Max amount of milliseconds the query will run (integer)\n - `
|
|
1
|
+
export default "# Howler Client Documentation\n\nThis documentation will outline how to interact with the howler API using the howler client in both Java and python development environments. We will outline the basic process of creating a new hit in each environment as well as searching howler for hits matching your query.\n\n## Getting started\n\n### Installation\n\nIn order to use the howler client, you need to list it as a dependency in your project.\n\n#### **Python**\n\nSimply install through pip:\n\n```bash\npip install howler-client\n```\n\nYou can also add it to your requirements.txt, or whatever dependency management system you use.\n\n### Authentication\n\nAs outlined in the [Authentication Documentation](/help/auth), there's a number of ways users can choose to authenticate. In order to interface with the howler client, however, the suggested flow is to use an API key. So before we start, let's generate a key.\n\n1. Open the Howler UI you'd like to interface with.\n2. Log in, then click your profile in the top right.\n3. Under user menu, click Settings.\n4. Under User Security, press the (+) icon on the API Keys row.\n5. Name your key, and give it the requisite permissions.\n6. Press Create, and copy the supplied string somewhere safe. **You will not see this string again.**\n\nThis API Key will be supplied to your code later on.\n\n## Python Client\n\nIn order to connect with howler using the python client, there is a fairly simple process to follow:\n\n```python\nfrom howler_client import get_client\n\nUSERNAME = 'user' # Obtain this from the user settings page of the Howler UI\nAPIKEY = 'apikey_name:apikey_data'\n\napikey = (USERNAME, APIKEY)\n\nhowler = get_client(\"$CURRENT_URL\", apikey=apikey)\n```\n\n```alert\nYou can skip generating an API Key and providing it if you're executing this code within HOGWARTS (i.e., on jupyterhub or airflow). OBO will handle authentication for you!\n```\n\nThat's it! You can now use the `howler` object to interact with the server. So what does that actually look like?\n\n### Creating hits in Python\n\nFor the python client, you can create hits using either the `howler.hit.create` or `howler.hit.create_from_map` functions.\n\n#### `create`\n\nThis function takes in a single argument - either a single hit, or a list of them, conforming to the [Howler Schema](/help/hit?tab=schema). Here is a simple example:\n\n```python\n# Some bogus data in the Howler Schema format\nexample_hit = {\n \"howler\": {\n \"analytic\": \"example\",\n \"score\": 10.0\n },\n \"event\": {\n \"reason\": \"Example hit\"\n }\n}\n\nhowler.hit.create(example_hit)\n```\n\nYou can also ingest data in a flat format:\n\n```python\nexample_hit = {\n \"howler.analytic\": \"example\",\n \"howler.score\": 10.0,\n \"event.reason\": \"Example hit\"\n}\n\nhowler.hit.create(example_hit)\n```\n\n#### `create_from_map`\n\nThis function takes in three arguments:\n\n- `tool name`: The name of the analytic creating the hit\n- `map`: A mapping between the raw data you have and the howler schema\n - The format is a dictionary where the keys are the flattened path of the raw data, and the values are a list of flattened paths for Howler's fields where the data will be copied into.\n- `documents`: The raw data you want to add to howler\n\nHere is a simple example:\n\n```python\n# The mapping from our data to howler's schema\nhwl_map = {\n \"file.sha256\": [\"file.hash.sha256\", \"howler.hash\"],\n \"file.name\": [\"file.name\"],\n \"src_ip\": [\"source.ip\", \"related.ip\"],\n \"dest_ip\": [\"destination.ip\", \"related.ip\"],\n \"time.created\": [\"event.start\"],\n}\n\n# Some bogus data in a custom format we want to add to howler\nexample_hit = {\n \"src_ip\": \"0.0.0.0\",\n \"dest_ip\": \"8.8.8.8\",\n \"file\": {\n \"name\": \"hello.exe\",\n \"sha256\": sha256(str(\"hello.exe\").encode()).hexdigest()\n },\n \"time\": {\n \"created\": datetime.now().isoformat()\n },\n}\n\n# Note that the third argument is of type list!\nhowler.hit.create_from_map(\"example_ingestor\", hwl_map, [example_hit])\n```\n\n### Querying Hits\n\nQuerying hits using the howler python client is done using the `howler.search.hit` function. It has a number of required and optional arguments:\n\n- Required:\n - `query`: lucene query (string)\n- Optional:\n - `filters`: Additional lucene queries used to filter the data (list of strings)\n - `fl`: List of fields to return (comma separated string of fields)\n - `offset`: Offset at which the query items should start (integer)\n - `rows`: Number of records to return (integer)\n - `sort`: Field used for sorting with direction (string: ex. 'id desc')\n - `timeout`: Max amount of milliseconds the query will run (integer)\n - `track_total_hits`: Number of hits to track (default: 10k)\n\nHere are some example queries:\n\n```python\n# Search for all hits created by assemblyline, show the first 50, and return only their ids\nhowler.search.hit(\"howler.analytic:assemblyline\", fl=\"howler.id\", rows=50)\n\n# Search for all resolved hits created in the last five days, returning their id and the analytic that created them. Show only ten, offset by 40\nhowler.search.hit(\"howler.status:resolved\", filters=['event.created:[now-5d TO now]'] fl=\"howler.id,howler.analytic\", rows=10, offset=40)\n\n# Search for all hits, timeout if the query takes more than 100ms\nhowler.search.hit(\"howler.id:*\", track_total_hits=100000000, timeout=100)\n```\n\n### Updating Hits\n\nIn order to update hits, there are a number of supported functions:\n\n- `howler.hit.update(...)`\n- `howler.hit.update_by_query(...)`\n- `howler.hit.overwrite(...)`\n\n#### `update()`\n\nIf you want to update a hit in a transactional way, you can use the following code:\n\n```python\nhit_to_update = client.search.hit(\"howler.id:*\", rows=1, sort=\"event.created desc\")[\"items\"][0]\n\nresult = client.hit.update(hit_to_update[\"howler\"][\"id\"], [(UPDATE_SET, \"howler.score\", hit_to_update[\"howler\"][\"score\"] + 100)])\n```\n\nThe following operations can be run to update a hit.\n\n**List Operations:**\n\n- `UPDATE_APPEND`: Used to append a value to a given list\n- `UPDATE_APPEND_IF_MISSING`: Used to append a value to a given list if the value isn't already in the list\n- `UPDATE_REMOVE`: Will remove a given value from a list\n\n**Numeric Operations:**\n\n- `UPDATE_DEC`: Decrement a numeric value by the specified amount\n- `UPDATE_INC`: Increment a numeric value by the specified amount\n- `UPDATE_MAX`: Will set a numeric value to the maximum of the existing value and the specified value\n- `UPDATE_MIN`: Will set a numeric value to the minimum of the existing value and the specified value\n\n**Multipurpose Operations:**\n\n- `UPDATE_SET`: Set a field's value to the given value\n- `UPDATE_DELETE`: Will delete a given field's value\n\n#### `update_by_query()`\n\nThis function allows you to update a large number of hits by a query:\n\n```python\nclient.hit.update_by_query(f'howler.analytic:\"Example Alert\"', [(UPDATE_INC, \"howler.score\", 100)])\n```\n\nThe same operations as in `update()` can be used.\n\n### `overwrite()`\n\nThis function allows you to directly overwrite a hit with a partial hit object. This is the most easy to use, but loses some of the validation and additional processing of the update functions.\n\n```python\nhit_to_update = client.search.hit(\"howler.id:*\", rows=1, sort=\"event.created desc\")[\"items\"][0]\n\nresult = client.hit.overwrite(hit_to_update[\"howler\"][\"id\"], {\"source.ip\": \"127.0.0.1\", \"destination.ip\": \"8.8.8.8\"})\n```\n"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default "<!-- docs/ingestion/bundles.fr.md -->\n\n# Les groupes des hits Howler\n\nLes groupes des hits peuvent \u00eatre utilis\u00e9s pour regrouper facilement un grand nombre d'alertes similaires, ce qui permet aux analystes de les traiter comme un seul incident. Prenons l'exemple d'un ordinateur qui effectue \u00e0 plusieurs reprises un appel r\u00e9seau vers `baddomain.ru` - bien qu'une alerte puisse \u00eatre g\u00e9n\u00e9r\u00e9e pour chaque cas o\u00f9 cet ordinateur acc\u00e8de \u00e0 ce domaine, il est logique que les analystes traitent toutes ces alertes comme un seul et m\u00eame cas.\n\n## Cr\u00e9ation de groupes via le client Howler\n\nIl y a plusieurs fa\u00e7ons de cr\u00e9er un groupe via le client Howler:\n\n```python\nfrom howler_client import get_client\n\nhowler = get_client(\"https://howler.dev.analysis.cyber.gc.ca\")\n\n\"\"\"Cr\u00e9ation simultan\u00e9e d'un groupe howler et de hits\"\"\"\nhowler.bundle.create(\n # Le premier argument est le hit de l'offre group\u00e9e\n {\n \"howler.analytic\": \"example-test\",\n \"howler.score\": 0\n },\n # Le deuxi\u00e8me argument est un hit ou une liste de hits \u00e0 inclure dans l'offre group\u00e9e.\n [\n {\n \"howler.analytic\": \"example-test\",\n \"howler.score\": 0\n },\n {\n \"howler.analytic\": \"example-test\",\n \"howler.score\": 0\n }\n ]\n)\n\n\"\"\"Cr\u00e9ation d'un groupe howler \u00e0 partir de hits existants\"\"\"\nhowler.bundle.create(\n {\n \"howler.analytic\": \"example-test\",\n \"howler.score\": 0,\n \"howler.hits\": [\"YcUsL8QsjmwwIdstieROk\", \"6s7MztwuSvz6tM0PgGJhvz\"]\n },\n # Noter: Dans les prochaines versions, vous n'aurez plus besoin d'inclure cet argument.\n []\n)\n\n\n\"\"\"Cr\u00e9ation \u00e0 partir d'une carte\"\"\"\nbundle_hit = {\n \"score\": 0,\n \"bundle\": True\n}\n\nmap = {\n \"score\": [\"howler.score\"],\n \"bundle\": [\"howler.is_bundle\"]\n}\n\nhowler.bundle.create_from_map(\"example-test\", bundle_hit, map, [{\"score\": 0}])\n```\n\n## Visualiser les groupes sur l'interface utilisateur de Howler\n\nAfin de visualiser les groupes cr\u00e9\u00e9s sur l'interface utilisateur de Howler, vous pouvez utiliser la requ\u00eate `howler.is_bundle:true`. Cela fournira une liste de groupes cr\u00e9\u00e9s que vous pourrez consulter.\n\nEn cliquant sur un groupe, vous ouvrirez une interface de recherche l\u00e9g\u00e8rement diff\u00e9rente de l'interface normale. Dans ce cas, nous filtrons automatiquement les r\u00e9sultats de la recherche pour n'inclure que les r\u00e9sultats inclus dans le groupe. Pour que cela soit \u00e9vident, l'en-t\u00eate repr\u00e9sentant le groupe appara\u00eet au-dessus de la barre de recherche.\n\nVous pouvez continuer \u00e0 filtrer les r\u00e9sultats en utilisant les m\u00eames requ\u00eates que d'habitude et \u00e0 les visualiser comme d'habitude. Lors du triage d'un groupe, son \u00e9valuation s'appliquera \u00e0 tous les hits du groupe, **sauf ceux qui ont d\u00e9j\u00e0 \u00e9t\u00e9 tri\u00e9s**. En d'autres termes, si le groupe est ouvert, tous les hits ouverts seront \u00e9valu\u00e9s lorsque vous l'\u00e9valuerez.\n\nLes groupes disposent \u00e9galement d'un onglet **R\u00e9sum\u00e9** qui n'est pas disponible pour les hits ordinaires. Cet onglet vous aidera \u00e0 regrouper les donn\u00e9es relatives \u00e0 tous les r\u00e9sultats du groupe. Il suffit d'ouvrir l'onglet et de cliquer sur \"Cr\u00e9er un sommaire\". Notez que cette op\u00e9ration peut prendre un certain temps, car un grand nombre de requ\u00eates sont ex\u00e9cut\u00e9es pour agr\u00e9ger les donn\u00e9es.\n"
|