Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIEM] ML Rules Details #61182

Merged
merged 15 commits into from
Mar 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { useState, useEffect, useContext } from 'react';
import { useState, useEffect } from 'react';
import { anomaliesTableData } from '../api/anomalies_table_data';
import { InfluencerInput, Anomalies, CriteriaFields } from '../types';
import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions';
import { MlCapabilitiesContext } from '../permissions/ml_capabilities_provider';
import { useSiemJobs } from '../../ml_popover/hooks/use_siem_jobs';
import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities';
import { useStateToaster, errorToToaster } from '../../toasters';

import * as i18n from './translations';
Expand Down Expand Up @@ -59,7 +59,7 @@ export const useAnomaliesTableData = ({
const [tableData, setTableData] = useState<Anomalies | null>(null);
const [, siemJobs] = useSiemJobs(true);
const [loading, setLoading] = useState(true);
const capabilities = useContext(MlCapabilitiesContext);
const capabilities = useMlCapabilities();
const userPermissions = hasMlUserPermissions(capabilities);
const [, dispatchToaster] = useStateToaster();
const timeZone = useTimeZone();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { isJobStarted, isJobLoading, isJobFailed } from './';

describe('isJobStarted', () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yaaay! Tests for verifying job states -- thanks @rylnd! 🎉🥇

test('returns false if only jobState is enabled', () => {
expect(isJobStarted('started', 'closing')).toBe(false);
});

test('returns false if only datafeedState is enabled', () => {
expect(isJobStarted('stopping', 'opened')).toBe(false);
});

test('returns true if both enabled states are provided', () => {
expect(isJobStarted('started', 'opened')).toBe(true);
});
});

describe('isJobLoading', () => {
test('returns true if both loading states are not provided', () => {
expect(isJobLoading('started', 'closing')).toBe(true);
});

test('returns true if only jobState is loading', () => {
expect(isJobLoading('starting', 'opened')).toBe(true);
});

test('returns true if only datafeedState is loading', () => {
expect(isJobLoading('started', 'opening')).toBe(true);
});

test('returns false if both disabling states are provided', () => {
expect(isJobLoading('stopping', 'closing')).toBe(true);
});
});

describe('isJobFailed', () => {
test('returns true if only jobState is failure/deleted', () => {
expect(isJobFailed('failed', 'stopping')).toBe(true);
});

test('returns true if only dataFeed is failure/deleted', () => {
expect(isJobFailed('started', 'deleted')).toBe(true);
});

test('returns true if both enabled states are failure/deleted', () => {
expect(isJobFailed('failed', 'deleted')).toBe(true);
});

test('returns false only if both states are not failure/deleted', () => {
expect(isJobFailed('opened', 'stopping')).toBe(false);
});
});
22 changes: 22 additions & 0 deletions x-pack/legacy/plugins/siem/public/components/ml/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

// Based on ML Job/Datafeed States from x-pack/legacy/plugins/ml/common/constants/states.js
const enabledStates = ['started', 'opened'];
const loadingStates = ['starting', 'stopping', 'opening', 'closing'];
const failureStates = ['deleted', 'failed'];

export const isJobStarted = (jobState: string, datafeedState: string): boolean => {
return enabledStates.includes(jobState) && enabledStates.includes(datafeedState);
};

export const isJobLoading = (jobState: string, datafeedState: string): boolean => {
return loadingStates.includes(jobState) || loadingStates.includes(datafeedState);
};

export const isJobFailed = (jobState: string, datafeedState: string): boolean => {
return failureStates.includes(jobState) || failureStates.includes(datafeedState);
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useContext } from 'react';
import React from 'react';

import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
import { HeaderSection } from '../../header_section';
Expand All @@ -16,7 +16,7 @@ import { Loader } from '../../loader';
import { getIntervalFromAnomalies } from '../anomaly/get_interval_from_anomalies';
import { AnomaliesHostTableProps } from '../types';
import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions';
import { MlCapabilitiesContext } from '../permissions/ml_capabilities_provider';
import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities';
import { BasicTable } from './basic_table';
import { hostEquality } from './host_equality';
import { getCriteriaFromHostType } from '../criteria/get_criteria_from_host_type';
Expand All @@ -37,7 +37,7 @@ const AnomaliesHostTableComponent: React.FC<AnomaliesHostTableProps> = ({
skip,
type,
}) => {
const capabilities = useContext(MlCapabilitiesContext);
const capabilities = useMlCapabilities();
const [loading, tableData] = useAnomaliesTableData({
startDate,
endDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/

import React, { useContext } from 'react';
import React from 'react';
import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data';
import { HeaderSection } from '../../header_section';

Expand All @@ -13,8 +13,8 @@ import { convertAnomaliesToNetwork } from './convert_anomalies_to_network';
import { Loader } from '../../loader';
import { AnomaliesNetworkTableProps } from '../types';
import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns';
import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities';
import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions';
import { MlCapabilitiesContext } from '../permissions/ml_capabilities_provider';
import { BasicTable } from './basic_table';
import { networkEquality } from './network_equality';
import { getCriteriaFromNetworkType } from '../criteria/get_criteria_from_network_type';
Expand All @@ -35,7 +35,7 @@ const AnomaliesNetworkTableComponent: React.FC<AnomaliesNetworkTableProps> = ({
type,
flowTarget,
}) => {
const capabilities = useContext(MlCapabilitiesContext);
const capabilities = useMlCapabilities();
const [loading, tableData] = useAnomaliesTableData({
startDate,
endDate,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { useContext } from 'react';

import { MlCapabilitiesContext } from '../../ml/permissions/ml_capabilities_provider';

export const useMlCapabilities = () => useContext(MlCapabilitiesContext);
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { useContext, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';

import { checkRecognizer, getJobsSummary, getModules } from '../api';
import { SiemJob } from '../types';
import { hasMlUserPermissions } from '../../ml/permissions/has_ml_user_permissions';
import { MlCapabilitiesContext } from '../../ml/permissions/ml_capabilities_provider';
import { errorToToaster, useStateToaster } from '../../toasters';
import { useUiSetting$ } from '../../../lib/kibana';
import { DEFAULT_INDEX_KEY } from '../../../../common/constants';

import * as i18n from './translations';
import { createSiemJobs } from './use_siem_jobs_helpers';
import { useMlCapabilities } from './use_ml_capabilities';

type Return = [boolean, SiemJob[]];

Expand All @@ -30,8 +30,8 @@ type Return = [boolean, SiemJob[]];
export const useSiemJobs = (refetchData: boolean): Return => {
const [siemJobs, setSiemJobs] = useState<SiemJob[]>([]);
const [loading, setLoading] = useState(true);
const capabilities = useContext(MlCapabilitiesContext);
const userPermissions = hasMlUserPermissions(capabilities);
const mlCapabilities = useMlCapabilities();
const userPermissions = hasMlUserPermissions(mlCapabilities);
const [siemDefaultIndex] = useUiSetting$<string[]>(DEFAULT_INDEX_KEY);
const [, dispatchToaster] = useStateToaster();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import { shallow, mount } from 'enzyme';
import React from 'react';

import { isChecked, isFailure, isJobLoading, JobSwitchComponent } from './job_switch';
import { JobSwitchComponent } from './job_switch';
import { cloneDeep } from 'lodash/fp';
import { mockSiemJobs } from '../__mocks__/api';
import { SiemJob } from '../types';
Expand Down Expand Up @@ -75,54 +75,4 @@ describe('JobSwitch', () => {
);
expect(wrapper.find('[data-test-subj="job-switch"]').exists()).toBe(false);
});

describe('isChecked', () => {
test('returns false if only jobState is enabled', () => {
expect(isChecked('started', 'closing')).toBe(false);
});

test('returns false if only datafeedState is enabled', () => {
expect(isChecked('stopping', 'opened')).toBe(false);
});

test('returns true if both enabled states are provided', () => {
expect(isChecked('started', 'opened')).toBe(true);
});
});

describe('isJobLoading', () => {
test('returns true if both loading states are not provided', () => {
expect(isJobLoading('started', 'closing')).toBe(true);
});

test('returns true if only jobState is loading', () => {
expect(isJobLoading('starting', 'opened')).toBe(true);
});

test('returns true if only datafeedState is loading', () => {
expect(isJobLoading('started', 'opening')).toBe(true);
});

test('returns false if both disabling states are provided', () => {
expect(isJobLoading('stopping', 'closing')).toBe(true);
});
});

describe('isFailure', () => {
test('returns true if only jobState is failure/deleted', () => {
expect(isFailure('failed', 'stopping')).toBe(true);
});

test('returns true if only dataFeed is failure/deleted', () => {
expect(isFailure('started', 'deleted')).toBe(true);
});

test('returns true if both enabled states are failure/deleted', () => {
expect(isFailure('failed', 'deleted')).toBe(true);
});

test('returns false only if both states are not failure/deleted', () => {
expect(isFailure('opened', 'stopping')).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import styled from 'styled-components';
import React, { useState, useCallback } from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, EuiSwitch } from '@elastic/eui';
import { SiemJob } from '../types';
import { isJobLoading, isJobStarted, isJobFailed } from '../../ml/helpers';

const StaticSwitch = styled(EuiSwitch)`
.euiSwitch__thumb,
Expand All @@ -24,23 +25,6 @@ export interface JobSwitchProps {
onJobStateChange: (job: SiemJob, latestTimestampMs: number, enable: boolean) => Promise<void>;
}

// Based on ML Job/Datafeed States from x-pack/legacy/plugins/ml/common/constants/states.js
const enabledStates = ['started', 'opened'];
const loadingStates = ['starting', 'stopping', 'opening', 'closing'];
const failureStates = ['deleted', 'failed'];

export const isChecked = (jobState: string, datafeedState: string): boolean => {
return enabledStates.includes(jobState) && enabledStates.includes(datafeedState);
};

export const isJobLoading = (jobState: string, datafeedState: string): boolean => {
return loadingStates.includes(jobState) || loadingStates.includes(datafeedState);
};

export const isFailure = (jobState: string, datafeedState: string): boolean => {
return failureStates.includes(jobState) || failureStates.includes(datafeedState);
};

export const JobSwitchComponent = ({
job,
isSiemJobsLoading,
Expand All @@ -64,8 +48,8 @@ export const JobSwitchComponent = ({
) : (
<StaticSwitch
data-test-subj="job-switch"
disabled={isFailure(job.jobState, job.datafeedState)}
checked={isChecked(job.jobState, job.datafeedState)}
disabled={isJobFailed(job.jobState, job.datafeedState)}
checked={isJobStarted(job.jobState, job.datafeedState)}
onChange={handleChange}
showLabel={false}
label=""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@
import { EuiButtonEmpty, EuiCallOut, EuiPopover, EuiPopoverTitle, EuiSpacer } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import moment from 'moment';
import React, { useContext, useReducer, useState } from 'react';
import React, { useReducer, useState } from 'react';
import styled from 'styled-components';

import { useKibana } from '../../lib/kibana';
import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../lib/telemetry';
import { hasMlAdminPermissions } from '../ml/permissions/has_ml_admin_permissions';
import { MlCapabilitiesContext } from '../ml/permissions/ml_capabilities_provider';
import { errorToToaster, useStateToaster } from '../toasters';
import { setupMlJob, startDatafeeds, stopDatafeeds } from './api';
import { filterJobs } from './helpers';
Expand All @@ -25,6 +24,7 @@ import { PopoverDescription } from './popover_description';
import * as i18n from './translations';
import { JobsFilters, JobSummary, SiemJob } from './types';
import { UpgradeContents } from './upgrade_contents';
import { useMlCapabilities } from './hooks/use_ml_capabilities';

const PopoverContentsDiv = styled.div`
max-width: 684px;
Expand Down Expand Up @@ -97,7 +97,7 @@ export const MlPopover = React.memo(() => {
const [filterProperties, setFilterProperties] = useState(defaultFilterProps);
const [isLoadingSiemJobs, siemJobs] = useSiemJobs(refreshToggle);
const [, dispatchToaster] = useStateToaster();
const capabilities = useContext(MlCapabilitiesContext);
const capabilities = useMlCapabilities();
const docLinks = useKibana().services.docLinks;

// Enable/Disable Job & Datafeed -- passed to JobsTable for use as callback on JobSwitch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/

import { MlError } from '../ml/types';
import { AuditMessageBase } from '../../../../../../plugins/ml/common/types/audit_message';

export interface Group {
id: string;
Expand Down Expand Up @@ -101,6 +102,7 @@ export interface MlSetupArgs {
* Representation of an ML Job as returned from the `ml/jobs/jobs_summary` API
*/
export interface JobSummary {
auditMessage?: AuditMessageBase;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice that we can use the explicit ML types! ++ 🎉

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was a little wary of this one... I'm getting no linter complaints, and that file is just types, so I think we should be fine.

datafeedId: string;
datafeedIndices: string[];
datafeedState: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { EuiFlexItem } from '@elastic/eui';
import darkTheme from '@elastic/eui/dist/eui_theme_dark.json';
import lightTheme from '@elastic/eui/dist/eui_theme_light.json';
import { getOr } from 'lodash/fp';
import React, { useContext } from 'react';
import React from 'react';

import { DEFAULT_DARK_MODE } from '../../../../../common/constants';
import { DescriptionList } from '../../../../../common/utility_types';
Expand All @@ -19,8 +19,8 @@ import { InspectButton, InspectButtonContainer } from '../../../inspect';
import { HostItem } from '../../../../graphql/types';
import { Loader } from '../../../loader';
import { IPDetailsLink } from '../../../links';
import { MlCapabilitiesContext } from '../../../ml/permissions/ml_capabilities_provider';
import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions';
import { useMlCapabilities } from '../../../ml_popover/hooks/use_ml_capabilities';
import { AnomalyScores } from '../../../ml/score/anomaly_scores';
import { Anomalies, NarrowDateRange } from '../../../ml/types';
import { DescriptionListStyled, OverviewWrapper } from '../../index';
Expand Down Expand Up @@ -56,7 +56,7 @@ export const HostOverview = React.memo<HostSummaryProps>(
anomaliesData,
narrowDateRange,
}) => {
const capabilities = useContext(MlCapabilitiesContext);
const capabilities = useMlCapabilities();
const userPermissions = hasMlUserPermissions(capabilities);
const [darkMode] = useUiSetting$<boolean>(DEFAULT_DARK_MODE);

Expand Down
Loading