mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Create IP Overview Component (#33756)
Create `IP Overview` component as outlined in https://github.com/elastic/ingest-dev/issues/321 #### New Features 🎉 For a specified `ipv4` or `ipv6`, the `IP Details` page now has an `IP Overview` widget which displays the most recent values across all time for the following fields as either a `Source` or `Destination`: * Location, Autonomous System, First Seen, Last Seen, Host ID & Host Name It also includes a `Whois` link, and links for checking the reputation of the IP. #### New Components * `<WhoIsLink>` & `<ReputationLink>` which accept with an IP or domain * `Field Renderers` for all the above fields for easily displaying `ECS` fields #### Notable Refactorings: * Redux `Network Model` `queries` are no longer `null`, and the details page now has a specific `NetworkDetailsModel` * `Network Selectors` now no longer require the `NetworkType` parameter 
This commit is contained in:
parent
d82b6e9427
commit
41c2ab38a3
49 changed files with 4295 additions and 303 deletions
|
@ -10,7 +10,14 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
|||
|
||||
import { encodeIpv6 } from '../../lib/helpers';
|
||||
|
||||
import { GoogleLink, HostDetailsLink, IPDetailsLink, TotalVirusLink } from '.';
|
||||
import {
|
||||
GoogleLink,
|
||||
HostDetailsLink,
|
||||
IPDetailsLink,
|
||||
ReputationLink,
|
||||
VirusTotalLink,
|
||||
WhoIsLink,
|
||||
} from '.';
|
||||
|
||||
describe('Custom Links', () => {
|
||||
const hostId = '133fd7715f1d47979ce817ba0df10c6e';
|
||||
|
@ -92,26 +99,74 @@ describe('Custom Links', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('TotalVirusLink', () => {
|
||||
describe('ReputationLink', () => {
|
||||
test('it renders link text', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ReputationLink domain={'192.0.2.0'}>{'Example Link'}</ReputationLink>
|
||||
);
|
||||
expect(wrapper.text()).toEqual('Example Link');
|
||||
});
|
||||
|
||||
test('it renders correct href', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ReputationLink domain={'192.0.2.0'}>{'Example Link'} </ReputationLink>
|
||||
);
|
||||
expect(wrapper.find('a').prop('href')).toEqual(
|
||||
'https://www.talosintelligence.com/reputation_center/lookup?search=192.0.2.0'
|
||||
);
|
||||
});
|
||||
|
||||
test("it encodes <script>alert('XSS')</script>", () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<ReputationLink domain={"<script>alert('XSS')</script>"}>{'Example Link'}</ReputationLink>
|
||||
);
|
||||
expect(wrapper.find('a').prop('href')).toEqual(
|
||||
"https://www.talosintelligence.com/reputation_center/lookup?search=%3Cscript%3Ealert('XSS')%3C/script%3E"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('VirusTotalLink', () => {
|
||||
test('it renders sha passed in as value', () => {
|
||||
const wrapper = mountWithIntl(<TotalVirusLink link={'abc'}>{'Example Link'}</TotalVirusLink>);
|
||||
const wrapper = mountWithIntl(<VirusTotalLink link={'abc'}>{'Example Link'}</VirusTotalLink>);
|
||||
expect(wrapper.text()).toEqual('Example Link');
|
||||
});
|
||||
|
||||
test('it renders sha passed in as link', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TotalVirusLink link={'abc'}>{'Example Link'} </TotalVirusLink>
|
||||
<VirusTotalLink link={'abc'}>{'Example Link'} </VirusTotalLink>
|
||||
);
|
||||
expect(wrapper.find('a').prop('href')).toEqual('https://www.virustotal.com/#/search/abc');
|
||||
});
|
||||
|
||||
test("it encodes <script>alert('XSS')</script>", () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<TotalVirusLink link={"<script>alert('XSS')</script>"}>{'Example Link'}</TotalVirusLink>
|
||||
<VirusTotalLink link={"<script>alert('XSS')</script>"}>{'Example Link'}</VirusTotalLink>
|
||||
);
|
||||
expect(wrapper.find('a').prop('href')).toEqual(
|
||||
"https://www.virustotal.com/#/search/%3Cscript%3Ealert('XSS')%3C/script%3E"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhoisLink', () => {
|
||||
test('it renders ip passed in as domain', () => {
|
||||
const wrapper = mountWithIntl(<WhoIsLink domain={'192.0.2.0'}>{'Example Link'}</WhoIsLink>);
|
||||
expect(wrapper.text()).toEqual('Example Link');
|
||||
});
|
||||
|
||||
test('it renders correct href', () => {
|
||||
const wrapper = mountWithIntl(<WhoIsLink domain={'192.0.2.0'}>{'Example Link'} </WhoIsLink>);
|
||||
expect(wrapper.find('a').prop('href')).toEqual('https://www.iana.org/whois?q=192.0.2.0');
|
||||
});
|
||||
|
||||
test("it encodes <script>alert('XSS')</script>", () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<WhoIsLink domain={"<script>alert('XSS')</script>"}>{'Example Link'}</WhoIsLink>
|
||||
);
|
||||
expect(wrapper.find('a').prop('href')).toEqual(
|
||||
"https://www.iana.org/whois?q=%3Cscript%3Ealert('XSS')%3C/script%3E"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,7 @@ import { pure } from 'recompose';
|
|||
|
||||
import { encodeIpv6 } from '../../lib/helpers';
|
||||
|
||||
// Internal Links
|
||||
export const HostDetailsLink = pure<{ children?: React.ReactNode; hostId: string }>(
|
||||
({ children, hostId }) => (
|
||||
<EuiLink href={`#/link-to/hosts/${encodeURIComponent(hostId)}`}>
|
||||
|
@ -26,6 +27,7 @@ export const IPDetailsLink = pure<{ children?: React.ReactNode; ip: string }>(
|
|||
)
|
||||
);
|
||||
|
||||
// External Links
|
||||
export const GoogleLink = pure<{ children?: React.ReactNode; link: string }>(
|
||||
({ children, link }) => (
|
||||
<EuiLink href={`https://www.google.com/search?q=${encodeURI(link)}`} target="_blank">
|
||||
|
@ -72,10 +74,31 @@ export const CertificateFingerprintLink = pure<{
|
|||
</EuiLink>
|
||||
));
|
||||
|
||||
export const TotalVirusLink = pure<{ children?: React.ReactNode; link: string }>(
|
||||
export const ReputationLink = pure<{ children?: React.ReactNode; domain: string }>(
|
||||
({ children, domain }) => (
|
||||
<EuiLink
|
||||
href={`https://www.talosintelligence.com/reputation_center/lookup?search=${encodeURI(
|
||||
domain
|
||||
)}`}
|
||||
target="_blank"
|
||||
>
|
||||
{children ? children : domain}
|
||||
</EuiLink>
|
||||
)
|
||||
);
|
||||
|
||||
export const VirusTotalLink = pure<{ children?: React.ReactNode; link: string }>(
|
||||
({ children, link }) => (
|
||||
<EuiLink href={`https://www.virustotal.com/#/search/${encodeURI(link)}`} target="_blank">
|
||||
{children ? children : link}
|
||||
</EuiLink>
|
||||
)
|
||||
);
|
||||
|
||||
export const WhoIsLink = pure<{ children?: React.ReactNode; domain: string }>(
|
||||
({ children, domain }) => (
|
||||
<EuiLink href={`https://www.iana.org/whois?q=${encodeURI(domain)}`} target="_blank">
|
||||
{children ? children : domain}
|
||||
</EuiLink>
|
||||
)
|
||||
);
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { NetworkTopNFlowTable } from './network_top_n_flow_table';
|
||||
export { IpOverview } from './ip_overview';
|
||||
export { KpiNetworkComponent } from './kpi_network';
|
||||
export { NetworkTopNFlowTable } from './network_top_n_flow_table';
|
||||
|
|
|
@ -0,0 +1,344 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Field Renderers #autonomousSystemRenderer it renders correctly against snapshot 1`] = `
|
||||
<Component>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<pure(Component)
|
||||
field="source.autonomous_system.as_org"
|
||||
id="ip-overview-source.autonomous_system.as_org"
|
||||
value="Test Org"
|
||||
/>
|
||||
|
||||
/
|
||||
<pure(Component)
|
||||
field="source.autonomous_system.asn"
|
||||
id="ip-overview-source.autonomous_system.asn"
|
||||
value="Test ASN"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`Field Renderers #dateRenderer it renders correctly against snapshot 1`] = `
|
||||
<Component>
|
||||
<pure(Component)
|
||||
fieldName="firstSeen"
|
||||
value="2019-02-07T17:19:41.636Z"
|
||||
/>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`Field Renderers #hostIdRenderer it renders correctly against snapshot 1`] = `
|
||||
<Component>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<pure(Component)
|
||||
field="host.name"
|
||||
id="ip-overview-host-name"
|
||||
value="b19a781f683541a7a25ee345133aa399"
|
||||
>
|
||||
<pure(Component)
|
||||
hostId="b19a781f683541a7a25ee345133aa399"
|
||||
>
|
||||
raspberrypi
|
||||
</pure(Component)>
|
||||
</pure(Component)>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot 1`] = `
|
||||
<Component>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<pure(Component)
|
||||
field="host.name"
|
||||
id="ip-overview-host-name"
|
||||
value="b19a781f683541a7a25ee345133aa399"
|
||||
>
|
||||
<pure(Component)
|
||||
hostId="b19a781f683541a7a25ee345133aa399"
|
||||
>
|
||||
raspberrypi
|
||||
</pure(Component)>
|
||||
</pure(Component)>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`Field Renderers #locationRenderer it renders correctly against snapshot 1`] = `
|
||||
<Component>
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
data-test-subj="location-field"
|
||||
gutterSize="none"
|
||||
>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<pure(Component)
|
||||
field="source.geo.city_name"
|
||||
id="ip-overview-source.geo.city_name"
|
||||
value="New York"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
,
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
>
|
||||
<pure(Component)
|
||||
field="source.geo.region_name"
|
||||
id="ip-overview-source.geo.region_name"
|
||||
value="New York"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`Field Renderers #reputationRenderer it renders correctly against snapshot 1`] = `
|
||||
<Component
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {
|
||||
"date": Object {
|
||||
"full": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"weekday": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"long": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"medium": Object {
|
||||
"day": "numeric",
|
||||
"month": "short",
|
||||
"year": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"day": "numeric",
|
||||
"month": "numeric",
|
||||
"year": "2-digit",
|
||||
},
|
||||
},
|
||||
"number": Object {
|
||||
"currency": Object {
|
||||
"style": "currency",
|
||||
},
|
||||
"percent": Object {
|
||||
"style": "percent",
|
||||
},
|
||||
},
|
||||
"relative": Object {
|
||||
"days": Object {
|
||||
"units": "day",
|
||||
},
|
||||
"hours": Object {
|
||||
"units": "hour",
|
||||
},
|
||||
"minutes": Object {
|
||||
"units": "minute",
|
||||
},
|
||||
"months": Object {
|
||||
"units": "month",
|
||||
},
|
||||
"seconds": Object {
|
||||
"units": "second",
|
||||
},
|
||||
"years": Object {
|
||||
"units": "year",
|
||||
},
|
||||
},
|
||||
"time": Object {
|
||||
"full": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"long": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"medium": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
},
|
||||
},
|
||||
},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<pure(Component)
|
||||
domain="10.10.10.10"
|
||||
>
|
||||
View at iana.org
|
||||
</pure(Component)>
|
||||
<pure(Component) />
|
||||
</Component>
|
||||
`;
|
||||
|
||||
exports[`Field Renderers #whoisRenderer it renders correctly against snapshot 1`] = `
|
||||
<Component
|
||||
intl={
|
||||
Object {
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"formatDate": [Function],
|
||||
"formatHTMLMessage": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelative": [Function],
|
||||
"formatTime": [Function],
|
||||
"formats": Object {
|
||||
"date": Object {
|
||||
"full": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"weekday": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"long": Object {
|
||||
"day": "numeric",
|
||||
"month": "long",
|
||||
"year": "numeric",
|
||||
},
|
||||
"medium": Object {
|
||||
"day": "numeric",
|
||||
"month": "short",
|
||||
"year": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"day": "numeric",
|
||||
"month": "numeric",
|
||||
"year": "2-digit",
|
||||
},
|
||||
},
|
||||
"number": Object {
|
||||
"currency": Object {
|
||||
"style": "currency",
|
||||
},
|
||||
"percent": Object {
|
||||
"style": "percent",
|
||||
},
|
||||
},
|
||||
"relative": Object {
|
||||
"days": Object {
|
||||
"units": "day",
|
||||
},
|
||||
"hours": Object {
|
||||
"units": "hour",
|
||||
},
|
||||
"minutes": Object {
|
||||
"units": "minute",
|
||||
},
|
||||
"months": Object {
|
||||
"units": "month",
|
||||
},
|
||||
"seconds": Object {
|
||||
"units": "second",
|
||||
},
|
||||
"years": Object {
|
||||
"units": "year",
|
||||
},
|
||||
},
|
||||
"time": Object {
|
||||
"full": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"long": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short",
|
||||
},
|
||||
"medium": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
},
|
||||
"short": Object {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
},
|
||||
},
|
||||
},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralFormat": [Function],
|
||||
"getRelativeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"now": [Function],
|
||||
"onError": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<pure(Component)
|
||||
domain="10.10.10.10"
|
||||
>
|
||||
View at iana.org
|
||||
</pure(Component)>
|
||||
<pure(Component) />
|
||||
</Component>
|
||||
`;
|
|
@ -0,0 +1,11 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`IP Overview Component rendering it renders the default IP Overview 1`] = `
|
||||
<Component>
|
||||
<Connect(IpOverviewComponent)
|
||||
ip="10.10.10.10"
|
||||
loading={false}
|
||||
type="details"
|
||||
/>
|
||||
</Component>
|
||||
`;
|
|
@ -0,0 +1,10 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`IP Overview Select direction rendering it renders the select type for IP Overview 1`] = `
|
||||
<Component
|
||||
id="ip-overview-select-type"
|
||||
isLoading={false}
|
||||
onChangeType={[MockFunction]}
|
||||
selectedType="source"
|
||||
/>
|
||||
`;
|
|
@ -0,0 +1,224 @@
|
|||
/*
|
||||
* 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 { mount, shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import * as React from 'react';
|
||||
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
import { GetIpOverviewQuery, HostEcsFields, IpOverviewType } from '../../../../graphql/types';
|
||||
import { TestProviders } from '../../../../mock';
|
||||
import { getEmptyValue } from '../../../empty_value';
|
||||
|
||||
import {
|
||||
autonomousSystemRenderer,
|
||||
dateRenderer,
|
||||
hostNameRenderer,
|
||||
locationRenderer,
|
||||
whoisRenderer,
|
||||
} from './field_renderers';
|
||||
import { mockData } from './mock';
|
||||
import AutonomousSystem = GetIpOverviewQuery.AutonomousSystem;
|
||||
|
||||
describe('Field Renderers', () => {
|
||||
describe('#locationRenderer', () => {
|
||||
test('it renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
{locationRenderer(['source.geo.city_name', 'source.geo.region_name'], mockData.complete)}
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders emptyTagValue when no fields provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>{locationRenderer([], mockData.complete)}</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
|
||||
test('it renders emptyTagValue when invalid fields provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
{locationRenderer(['source.geo.my_house'], mockData.complete)}
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#dateRenderer', () => {
|
||||
test('it renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>{dateRenderer('firstSeen', mockData.complete.source!)}</TestProviders>
|
||||
);
|
||||
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders emptyTagValue when invalid field provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>{dateRenderer('geo.spark_plug', mockData.complete.source!)}</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#autonomousSystemRenderer', () => {
|
||||
const emptyMock: AutonomousSystem = { as_org: null, asn: null, ip: '10.10.10.10' };
|
||||
const halfEmptyMock: AutonomousSystem = { as_org: null, asn: 'Test ASN', ip: '10.10.10.10' };
|
||||
|
||||
test('it renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
{autonomousSystemRenderer(
|
||||
mockData.complete.source!.autonomousSystem!,
|
||||
IpOverviewType.source
|
||||
)}
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders emptyTagValue when non-string field provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
{autonomousSystemRenderer(halfEmptyMock, IpOverviewType.source)}
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
|
||||
test('it renders emptyTagValue when invalid field provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>{autonomousSystemRenderer(emptyMock, IpOverviewType.source)}</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#hostIdRenderer', () => {
|
||||
const emptyIdHost: Partial<HostEcsFields> = {
|
||||
name: 'test',
|
||||
id: null,
|
||||
ip: ['10.10.10.10'],
|
||||
};
|
||||
const emptyIpHost: Partial<HostEcsFields> = {
|
||||
name: 'test',
|
||||
id: 'test',
|
||||
ip: null,
|
||||
};
|
||||
test('it renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
{hostNameRenderer(mockData.complete.source!.host!, '10.10.10.10')}
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders emptyTagValue when non-matching IP is provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
{hostNameRenderer(mockData.complete.source!.host!, '10.10.10.11')}
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
|
||||
test('it renders emptyTagValue when no host.id is provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>{hostNameRenderer(emptyIdHost, IpOverviewType.source)}</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
test('it renders emptyTagValue when no host.ip is provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>{hostNameRenderer(emptyIpHost, IpOverviewType.source)}</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#hostNameRenderer', () => {
|
||||
const emptyIdHost: Partial<HostEcsFields> = {
|
||||
name: 'test',
|
||||
id: null,
|
||||
ip: ['10.10.10.10'],
|
||||
};
|
||||
const emptyIpHost: Partial<HostEcsFields> = {
|
||||
name: 'test',
|
||||
id: 'test',
|
||||
ip: null,
|
||||
};
|
||||
const emptyNameHost: Partial<HostEcsFields> = {
|
||||
name: null,
|
||||
id: 'test',
|
||||
ip: ['10.10.10.10'],
|
||||
};
|
||||
test('it renders correctly against snapshot', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
{hostNameRenderer(mockData.complete.source!.host!, '10.10.10.10')}
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('it renders emptyTagValue when non-matching IP is provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>
|
||||
{hostNameRenderer(mockData.complete.source!.host!, '10.10.10.11')}
|
||||
</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
|
||||
test('it renders emptyTagValue when no host.id is provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>{hostNameRenderer(emptyIdHost, IpOverviewType.source)}</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
test('it renders emptyTagValue when no host.ip is provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>{hostNameRenderer(emptyIpHost, IpOverviewType.source)}</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
test('it renders emptyTagValue when no host.name is provided', () => {
|
||||
const wrapper = mount(
|
||||
<TestProviders>{hostNameRenderer(emptyNameHost, IpOverviewType.source)}</TestProviders>
|
||||
);
|
||||
expect(wrapper.text()).toEqual(getEmptyValue());
|
||||
});
|
||||
});
|
||||
|
||||
describe('#whoisRenderer', () => {
|
||||
test('it renders correctly against snapshot', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<TestProviders>{whoisRenderer('10.10.10.10')}</TestProviders>
|
||||
);
|
||||
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#reputationRenderer', () => {
|
||||
test('it renders correctly against snapshot', () => {
|
||||
const wrapper = shallowWithIntl(
|
||||
<TestProviders>{whoisRenderer('10.10.10.10')}</TestProviders>
|
||||
);
|
||||
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { getOr } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
AutonomousSystem,
|
||||
HostEcsFields,
|
||||
IpOverviewData,
|
||||
IpOverviewType,
|
||||
Overview,
|
||||
} from '../../../../graphql/types';
|
||||
import { DefaultDraggable } from '../../../draggables';
|
||||
import { getEmptyTagValue } from '../../../empty_value';
|
||||
import { ExternalLinkIcon } from '../../../external_link_icon';
|
||||
import { FormattedDate } from '../../../formatted_date';
|
||||
import { HostDetailsLink, ReputationLink, VirusTotalLink, WhoIsLink } from '../../../links';
|
||||
|
||||
import { IpOverviewId } from './index';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const locationRenderer = (fieldNames: string[], data: IpOverviewData): React.ReactElement =>
|
||||
fieldNames.length > 0 && fieldNames.every(fieldName => getOr(null, fieldName, data)) ? (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none" data-test-subj="location-field">
|
||||
{fieldNames.map((fieldName, index) => {
|
||||
const locationValue = getOr('', fieldName, data);
|
||||
return (
|
||||
<React.Fragment key={`${IpOverviewId}-${fieldName}`}>
|
||||
{index ? ',\u00A0' : ''}
|
||||
<EuiFlexItem grow={false}>
|
||||
<DefaultDraggable
|
||||
id={`${IpOverviewId}-${fieldName}`}
|
||||
field={fieldName}
|
||||
value={locationValue}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
);
|
||||
|
||||
export const dateRenderer = (fieldName: string, data: Overview): React.ReactElement => (
|
||||
<FormattedDate value={getOr(null, fieldName, data)} fieldName={fieldName} />
|
||||
);
|
||||
|
||||
export const autonomousSystemRenderer = (
|
||||
as: AutonomousSystem,
|
||||
flowType: IpOverviewType
|
||||
): React.ReactElement =>
|
||||
as && as.as_org && as.asn ? (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<DefaultDraggable
|
||||
id={`${IpOverviewId}-${flowType}.autonomous_system.as_org`}
|
||||
field={`${flowType}.autonomous_system.as_org`}
|
||||
value={as.as_org}
|
||||
/>{' '}
|
||||
/
|
||||
<DefaultDraggable
|
||||
id={`${IpOverviewId}-${flowType}.autonomous_system.asn`}
|
||||
field={`${flowType}.autonomous_system.asn`}
|
||||
value={as.asn}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
);
|
||||
|
||||
export const hostIdRenderer = (host: HostEcsFields, ipFilter?: string): React.ReactElement =>
|
||||
host.id && host.ip && (!(ipFilter != null) || host.ip.includes(ipFilter)) ? (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<DefaultDraggable id={`${IpOverviewId}-host-id`} field={'host.id'} value={host.id}>
|
||||
<HostDetailsLink hostId={host.id} />
|
||||
</DefaultDraggable>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
);
|
||||
|
||||
export const hostNameRenderer = (host: HostEcsFields, ipFilter?: string): React.ReactElement =>
|
||||
host.id && host.ip && (!(ipFilter != null) || host.ip.includes(ipFilter)) ? (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="none">
|
||||
<EuiFlexItem grow={false}>
|
||||
<DefaultDraggable id={`${IpOverviewId}-host-name`} field={'host.name'} value={host.id}>
|
||||
<HostDetailsLink hostId={host.id}>
|
||||
{host.name ? host.name : getEmptyTagValue()}
|
||||
</HostDetailsLink>
|
||||
</DefaultDraggable>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
);
|
||||
|
||||
export const whoisRenderer = (ip: string) => (
|
||||
<>
|
||||
<WhoIsLink domain={ip}>{i18n.VIEW_WHOIS}</WhoIsLink>
|
||||
<ExternalLinkIcon />
|
||||
</>
|
||||
);
|
||||
|
||||
export const reputationRenderer = (ip: string): React.ReactElement => (
|
||||
<>
|
||||
<VirusTotalLink link={ip}>{i18n.VIEW_VIRUS_TOTAL}</VirusTotalLink>
|
||||
<ExternalLinkIcon />
|
||||
<br />
|
||||
<ReputationLink domain={ip}>{i18n.VIEW_TALOS_INTELLIGENCE}</ReputationLink>
|
||||
<ExternalLinkIcon />
|
||||
</>
|
||||
);
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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 { shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import * as React from 'react';
|
||||
import { MockedProvider } from 'react-apollo/test-utils';
|
||||
import { mountWithIntl } from 'test_utils/enzyme_helpers';
|
||||
|
||||
import { mockGlobalState, TestProviders } from '../../../../mock';
|
||||
import { createStore, State } from '../../../../store';
|
||||
import { networkModel } from '../../../../store/local/network';
|
||||
|
||||
import { IpOverview, IpOverviewId } from './index';
|
||||
import { mockData } from './mock';
|
||||
|
||||
describe('IP Overview Component', () => {
|
||||
const state: State = mockGlobalState;
|
||||
|
||||
let store = createStore(state);
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore(state);
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
test('it renders the default IP Overview', () => {
|
||||
const wrapper = shallow(
|
||||
<TestProviders>
|
||||
<IpOverview
|
||||
loading={false}
|
||||
ip="10.10.10.10"
|
||||
data={mockData.IpOverview}
|
||||
type={networkModel.NetworkType.details}
|
||||
/>
|
||||
</TestProviders>
|
||||
);
|
||||
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('changing selected type', () => {
|
||||
test('selecting destination from the type drop down', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
<MockedProvider>
|
||||
<TestProviders store={store}>
|
||||
<IpOverview
|
||||
loading={false}
|
||||
ip="10.10.10.10"
|
||||
data={mockData.complete}
|
||||
type={networkModel.NetworkType.details}
|
||||
/>
|
||||
</TestProviders>
|
||||
</MockedProvider>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find(`[data-test-subj="${IpOverviewId}-select-type"] button`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find(`button#${IpOverviewId}-select-type-destination`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(
|
||||
wrapper
|
||||
.find(`[data-test-subj="${IpOverviewId}-select-type"] button`)
|
||||
.first()
|
||||
.text()
|
||||
.toLocaleLowerCase()
|
||||
).toEqual('as destination');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiDescriptionList,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiHorizontalRule,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedRelative } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import styled from 'styled-components';
|
||||
import { ActionCreator } from 'typescript-fsa';
|
||||
|
||||
import { IpOverviewData, IpOverviewType, Overview } from '../../../../graphql/types';
|
||||
import { networkActions, networkModel, networkSelectors, State } from '../../../../store';
|
||||
import { getEmptyTagValue } from '../../../empty_value';
|
||||
|
||||
import {
|
||||
autonomousSystemRenderer,
|
||||
dateRenderer,
|
||||
hostIdRenderer,
|
||||
hostNameRenderer,
|
||||
locationRenderer,
|
||||
reputationRenderer,
|
||||
whoisRenderer,
|
||||
} from './field_renderers';
|
||||
import { SelectType } from './select_type';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const IpOverviewId = 'ip-overview';
|
||||
|
||||
const SelectTypeItem = styled(EuiFlexItem)`
|
||||
min-width: 180px;
|
||||
`;
|
||||
|
||||
interface DescriptionList {
|
||||
title: string;
|
||||
description: JSX.Element;
|
||||
}
|
||||
|
||||
interface OwnProps {
|
||||
ip: string;
|
||||
data: IpOverviewData;
|
||||
loading: boolean;
|
||||
type: networkModel.NetworkType;
|
||||
}
|
||||
|
||||
interface IpOverviewReduxProps {
|
||||
flowType: IpOverviewType;
|
||||
}
|
||||
|
||||
interface IpOverViewDispatchProps {
|
||||
updateIpOverviewFlowType: ActionCreator<{
|
||||
flowType: IpOverviewType;
|
||||
}>;
|
||||
}
|
||||
|
||||
type IpOverviewProps = OwnProps & IpOverviewReduxProps & IpOverViewDispatchProps;
|
||||
|
||||
class IpOverviewComponent extends React.PureComponent<IpOverviewProps> {
|
||||
public render() {
|
||||
const { ip, data, loading, flowType } = this.props;
|
||||
const typeData: Overview = data[flowType]!;
|
||||
|
||||
const descriptionLists: Readonly<DescriptionList[][]> = [
|
||||
[
|
||||
{
|
||||
title: i18n.LOCATION,
|
||||
description: locationRenderer(
|
||||
[`${flowType}.geo.city_name`, `${flowType}.geo.region_name`],
|
||||
data
|
||||
),
|
||||
},
|
||||
{
|
||||
title: i18n.AUTONOMOUS_SYSTEM,
|
||||
description: typeData
|
||||
? autonomousSystemRenderer(typeData.autonomousSystem, flowType)
|
||||
: getEmptyTagValue(),
|
||||
},
|
||||
],
|
||||
[
|
||||
{ title: i18n.FIRST_SEEN, description: dateRenderer('firstSeen', typeData) },
|
||||
{ title: i18n.LAST_SEEN, description: dateRenderer('lastSeen', typeData) },
|
||||
],
|
||||
[
|
||||
{
|
||||
title: i18n.HOST_ID,
|
||||
description: typeData ? hostIdRenderer(typeData.host, ip) : getEmptyTagValue(),
|
||||
},
|
||||
{
|
||||
title: i18n.HOST_NAME,
|
||||
description: typeData ? hostNameRenderer(typeData.host, ip) : getEmptyTagValue(),
|
||||
},
|
||||
],
|
||||
[
|
||||
{ title: i18n.WHOIS, description: whoisRenderer(ip) },
|
||||
{ title: i18n.REPUTATION, description: reputationRenderer(ip) },
|
||||
],
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiText>
|
||||
<h1>{ip}</h1>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<SelectTypeItem grow={false} data-test-subj={`${IpOverviewId}-select-type`}>
|
||||
<SelectType
|
||||
id={`${IpOverviewId}-select-type`}
|
||||
selectedType={flowType}
|
||||
onChangeType={this.onChangeType}
|
||||
isLoading={loading}
|
||||
/>
|
||||
</SelectTypeItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiText>
|
||||
{i18n.LAST_BEAT}:{' '}
|
||||
{typeData && typeData.lastSeen != null ? (
|
||||
<EuiToolTip position="bottom" content={typeData.lastSeen}>
|
||||
<FormattedRelative value={new Date(typeData.lastSeen)} />
|
||||
</EuiToolTip>
|
||||
) : (
|
||||
getEmptyTagValue()
|
||||
)}
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
<EuiHorizontalRule margin="xs" />
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFlexGroup>
|
||||
{descriptionLists.map((descriptionList, index) =>
|
||||
this.getDescriptionList(descriptionList, index)
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private getDescriptionList = (descriptionList: DescriptionList[], key: number) => {
|
||||
return (
|
||||
<EuiFlexItem key={key}>
|
||||
<EuiDescriptionList listItems={descriptionList} />
|
||||
</EuiFlexItem>
|
||||
);
|
||||
};
|
||||
|
||||
private onChangeType = (flowType: IpOverviewType) => {
|
||||
this.props.updateIpOverviewFlowType({ flowType });
|
||||
};
|
||||
}
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getIpOverviewSelector = networkSelectors.ipOverviewSelector();
|
||||
const mapStateToProps = (state: State) => getIpOverviewSelector(state);
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export const IpOverview = connect(
|
||||
makeMapStateToProps,
|
||||
{
|
||||
updateIpOverviewFlowType: networkActions.updateIpOverviewFlowType,
|
||||
}
|
||||
)(IpOverviewComponent);
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { IpOverviewData } from '../../../../graphql/types';
|
||||
|
||||
export const mockData: Readonly<Record<string, IpOverviewData>> = {
|
||||
complete: {
|
||||
source: {
|
||||
firstSeen: '2019-02-07T17:19:41.636Z',
|
||||
lastSeen: '2019-02-07T17:19:41.636Z',
|
||||
autonomousSystem: { as_org: 'Test Org', asn: 'Test ASN', ip: '10.10.10.10' },
|
||||
geo: {
|
||||
continent_name: 'North America',
|
||||
city_name: 'New York',
|
||||
country_iso_code: 'US',
|
||||
country_name: null,
|
||||
location: {
|
||||
lat: 40.7214,
|
||||
lon: -74.0052,
|
||||
},
|
||||
region_iso_code: 'US-NY',
|
||||
region_name: 'New York',
|
||||
},
|
||||
host: {
|
||||
os: {
|
||||
kernel: '4.14.50-v7+',
|
||||
name: 'Raspbian GNU/Linux',
|
||||
family: '',
|
||||
version: '9 (stretch)',
|
||||
platform: 'raspbian',
|
||||
},
|
||||
name: 'raspberrypi',
|
||||
id: 'b19a781f683541a7a25ee345133aa399',
|
||||
ip: ['10.10.10.10'],
|
||||
architecture: 'armv7l',
|
||||
},
|
||||
},
|
||||
destination: {
|
||||
firstSeen: '2019-02-07T17:19:41.648Z',
|
||||
lastSeen: '2019-02-07T17:19:41.648Z',
|
||||
autonomousSystem: { as_org: 'Test Org', asn: 'Test ASN', ip: '10.10.10.10' },
|
||||
geo: {
|
||||
continent_name: 'North America',
|
||||
city_name: 'New York',
|
||||
country_iso_code: 'US',
|
||||
country_name: null,
|
||||
location: {
|
||||
lat: 40.7214,
|
||||
lon: -74.0052,
|
||||
},
|
||||
region_iso_code: 'US-NY',
|
||||
region_name: 'New York',
|
||||
},
|
||||
host: {
|
||||
os: {
|
||||
kernel: '4.14.50-v7+',
|
||||
name: 'Raspbian GNU/Linux',
|
||||
family: '',
|
||||
version: '9 (stretch)',
|
||||
platform: 'raspbian',
|
||||
},
|
||||
name: 'raspberrypi',
|
||||
id: 'b19a781f683541a7a25ee345133aa399',
|
||||
ip: ['10.10.10.10'],
|
||||
architecture: 'armv7l',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 { mount, shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
import * as React from 'react';
|
||||
|
||||
import { IpOverviewType } from '../../../../graphql/types';
|
||||
|
||||
import { IpOverviewId } from '.';
|
||||
import { SelectType } from './select_type';
|
||||
|
||||
describe('IP Overview Select direction', () => {
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
describe('rendering', () => {
|
||||
test('it renders the select type for IP Overview', () => {
|
||||
const wrapper = shallow(
|
||||
<SelectType
|
||||
id={`${IpOverviewId}-select-type`}
|
||||
selectedType={IpOverviewType.source}
|
||||
onChangeType={mockOnChange}
|
||||
isLoading={false}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(toJson(wrapper)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Functionality works as expected', () => {
|
||||
test('when you click on destination, you trigger onChange function', () => {
|
||||
const wrapper = mount(
|
||||
<SelectType
|
||||
id={`${IpOverviewId}-select-type`}
|
||||
selectedType={IpOverviewType.source}
|
||||
onChangeType={mockOnChange}
|
||||
isLoading={false}
|
||||
/>
|
||||
);
|
||||
|
||||
wrapper
|
||||
.find('button')
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
wrapper.update();
|
||||
|
||||
wrapper
|
||||
.find(`button#${IpOverviewId}-select-type-destination`)
|
||||
.first()
|
||||
.simulate('click');
|
||||
|
||||
expect(mockOnChange.mock.calls[0]).toEqual(['destination']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 { EuiSuperSelect } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
|
||||
import { IpOverviewType } from '../../../../graphql/types';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
||||
const toggleTypeOptions = (id: string) => [
|
||||
{
|
||||
id: `${id}-${IpOverviewType.source}`,
|
||||
value: IpOverviewType.source,
|
||||
inputDisplay: i18n.AS_SOURCE,
|
||||
},
|
||||
{
|
||||
id: `${id}-${IpOverviewType.destination}`,
|
||||
value: IpOverviewType.destination,
|
||||
inputDisplay: i18n.AS_DESTINATION,
|
||||
},
|
||||
];
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
selectedType: IpOverviewType;
|
||||
onChangeType: (value: IpOverviewType) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const SelectType = pure<Props>(({ id, isLoading = false, onChangeType, selectedType }) => (
|
||||
<EuiSuperSelect
|
||||
options={toggleTypeOptions(id)}
|
||||
valueOfSelected={selectedType}
|
||||
onChange={onChangeType}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
));
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const LAST_BEAT = i18n.translate('xpack.secops.network.ipDetails.ipOverview.lastBeatTitle', {
|
||||
defaultMessage: 'Last Beat',
|
||||
});
|
||||
|
||||
export const LOCATION = i18n.translate('xpack.secops.network.ipDetails.ipOverview.locationTitle', {
|
||||
defaultMessage: 'Location',
|
||||
});
|
||||
|
||||
export const AUTONOMOUS_SYSTEM = i18n.translate(
|
||||
'xpack.secops.network.ipDetails.ipOverview.autonomousSystemTitle',
|
||||
{
|
||||
defaultMessage: 'Autonomous System',
|
||||
}
|
||||
);
|
||||
|
||||
export const FIRST_SEEN = i18n.translate(
|
||||
'xpack.secops.network.ipDetails.ipOverview.firstSeenTitle',
|
||||
{
|
||||
defaultMessage: 'First Seen',
|
||||
}
|
||||
);
|
||||
|
||||
export const LAST_SEEN = i18n.translate('xpack.secops.network.ipDetails.ipOverview.lastSeenTitle', {
|
||||
defaultMessage: 'Last Seen',
|
||||
});
|
||||
|
||||
export const HOST_ID = i18n.translate('xpack.secops.network.ipDetails.ipOverview.hostIdTitle', {
|
||||
defaultMessage: 'Host ID',
|
||||
});
|
||||
|
||||
export const HOST_NAME = i18n.translate('xpack.secops.network.ipDetails.ipOverview.hostNameTitle', {
|
||||
defaultMessage: 'Host Name',
|
||||
});
|
||||
|
||||
export const WHOIS = i18n.translate('xpack.secops.network.ipDetails.ipOverview.whoIsTitle', {
|
||||
defaultMessage: 'WhoIs',
|
||||
});
|
||||
|
||||
export const VIEW_WHOIS = i18n.translate(
|
||||
'xpack.secops.network.ipDetails.ipOverview.viewWhoisTitle',
|
||||
{
|
||||
defaultMessage: 'View at iana.org',
|
||||
}
|
||||
);
|
||||
export const VIEW_VIRUS_TOTAL = i18n.translate(
|
||||
'xpack.secops.network.ipDetails.ipOverview.viewVirusTotalTitle.',
|
||||
{
|
||||
defaultMessage: 'View at virustotal.com',
|
||||
}
|
||||
);
|
||||
export const VIEW_TALOS_INTELLIGENCE = i18n.translate(
|
||||
'xpack.secops.network.ipDetails.ipOverview.viewTalosIntelligenceTitle',
|
||||
{
|
||||
defaultMessage: 'View at talosIntelligence.com',
|
||||
}
|
||||
);
|
||||
|
||||
export const REPUTATION = i18n.translate(
|
||||
'xpack.secops.network.ipDetails.ipOverview.ipReputationTitle',
|
||||
{
|
||||
defaultMessage: 'Reputation',
|
||||
}
|
||||
);
|
||||
|
||||
export const AS_SOURCE = i18n.translate(
|
||||
'xpack.secops.network.ipDetails.ipOverview.asSourceDropDownOptionLabel',
|
||||
{
|
||||
defaultMessage: 'As Source',
|
||||
}
|
||||
);
|
||||
export const AS_DESTINATION = i18n.translate(
|
||||
'xpack.secops.network.ipDetails.ipOverview.asDestinationDropDownOptionLabel',
|
||||
{
|
||||
defaultMessage: 'As Destination',
|
||||
}
|
||||
);
|
|
@ -154,7 +154,7 @@ class NetworkDnsTableComponent extends React.PureComponent<NetworkDnsTableProps>
|
|||
|
||||
const makeMapStateToProps = () => {
|
||||
const getNetworkDnsSelector = networkSelectors.dnsSelector();
|
||||
const mapStateToProps = (state: State, { type }: OwnProps) => getNetworkDnsSelector(state, type);
|
||||
const mapStateToProps = (state: State) => getNetworkDnsSelector(state);
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
|
|
|
@ -199,8 +199,7 @@ class NetworkTopNFlowTableComponent extends React.PureComponent<NetworkTopNFlowT
|
|||
|
||||
const makeMapStateToProps = () => {
|
||||
const getNetworkTopNFlowSelector = networkSelectors.topNFlowSelector();
|
||||
const mapStateToProps = (state: State, { type }: OwnProps) =>
|
||||
getNetworkTopNFlowSelector(state, type);
|
||||
const mapStateToProps = (state: State) => getNetworkTopNFlowSelector(state);
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import { escapeQueryValue } from '../../../../lib/keury';
|
|||
import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper';
|
||||
import { escapeDataProviderId } from '../../../drag_and_drop/helpers';
|
||||
import { ExternalLinkIcon } from '../../../external_link_icon';
|
||||
import { GoogleLink, TotalVirusLink } from '../../../links';
|
||||
import { GoogleLink, VirusTotalLink } from '../../../links';
|
||||
import { Provider } from '../../../timeline/data_providers/provider';
|
||||
|
||||
import * as i18n from './translations';
|
||||
|
@ -125,7 +125,7 @@ export const TotalVirusLinkSha = pure<{ value: string | null }>(({ value }) =>
|
|||
value != null ? (
|
||||
<LinkFlexItem grow={false}>
|
||||
<div>
|
||||
<TotalVirusLink link={value}>{value}</TotalVirusLink>
|
||||
<VirusTotalLink link={value}>{value}</VirusTotalLink>
|
||||
<ExternalLinkIcon />
|
||||
</div>
|
||||
</LinkFlexItem>
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
|
||||
export const ipOverviewQuery = gql`
|
||||
query GetIpOverviewQuery($sourceId: ID!, $filterQuery: String, $ip: String!) {
|
||||
source(id: $sourceId) {
|
||||
id
|
||||
IpOverview(filterQuery: $filterQuery, ip: $ip) {
|
||||
source {
|
||||
firstSeen
|
||||
lastSeen
|
||||
autonomousSystem {
|
||||
as_org
|
||||
asn
|
||||
ip
|
||||
}
|
||||
geo {
|
||||
continent_name
|
||||
city_name
|
||||
country_iso_code
|
||||
country_name
|
||||
location {
|
||||
lat
|
||||
lon
|
||||
}
|
||||
region_iso_code
|
||||
region_name
|
||||
}
|
||||
host {
|
||||
architecture
|
||||
id
|
||||
ip
|
||||
mac
|
||||
name
|
||||
os {
|
||||
family
|
||||
name
|
||||
platform
|
||||
version
|
||||
}
|
||||
type
|
||||
}
|
||||
}
|
||||
destination {
|
||||
firstSeen
|
||||
lastSeen
|
||||
autonomousSystem {
|
||||
as_org
|
||||
asn
|
||||
ip
|
||||
}
|
||||
geo {
|
||||
continent_name
|
||||
city_name
|
||||
country_iso_code
|
||||
country_name
|
||||
location {
|
||||
lat
|
||||
lon
|
||||
}
|
||||
region_iso_code
|
||||
region_name
|
||||
}
|
||||
host {
|
||||
architecture
|
||||
id
|
||||
ip
|
||||
mac
|
||||
name
|
||||
os {
|
||||
family
|
||||
name
|
||||
platform
|
||||
version
|
||||
}
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { getOr } from 'lodash/fp';
|
||||
import React from 'react';
|
||||
import { Query } from 'react-apollo';
|
||||
import { pure } from 'recompose';
|
||||
|
||||
import { GetIpOverviewQuery, IpOverviewData } from '../../graphql/types';
|
||||
import { networkModel } from '../../store/local';
|
||||
import { createFilter } from '../helpers';
|
||||
import { QueryTemplateProps } from '../query_template';
|
||||
|
||||
import { ipOverviewQuery } from './index.gql_query';
|
||||
|
||||
export interface IpOverviewArgs {
|
||||
id: string;
|
||||
ipOverviewData: IpOverviewData;
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export interface IpOverviewProps extends QueryTemplateProps {
|
||||
children: (args: IpOverviewArgs) => React.ReactNode;
|
||||
type: networkModel.NetworkType;
|
||||
ip: string;
|
||||
}
|
||||
|
||||
export const IpOverviewQuery = pure<IpOverviewProps>(
|
||||
({ id = 'ipOverviewQuery', children, filterQuery, sourceId, ip }) => (
|
||||
<Query<GetIpOverviewQuery.Query, GetIpOverviewQuery.Variables>
|
||||
query={ipOverviewQuery}
|
||||
fetchPolicy="cache-and-network"
|
||||
notifyOnNetworkStatusChange
|
||||
variables={{
|
||||
sourceId,
|
||||
filterQuery: createFilter(filterQuery),
|
||||
ip,
|
||||
}}
|
||||
>
|
||||
{({ data, loading }) => {
|
||||
const init: IpOverviewData = {};
|
||||
const ipOverviewData: IpOverviewData = getOr(init, 'source.IpOverview', data);
|
||||
return children({
|
||||
id,
|
||||
ipOverviewData,
|
||||
loading,
|
||||
});
|
||||
}}
|
||||
</Query>
|
||||
)
|
||||
);
|
|
@ -92,10 +92,10 @@ const NetworkFilterComponent = pure<NetworkFilterProps>(
|
|||
const makeMapStateToProps = () => {
|
||||
const getNetworkFilterQueryDraft = networkSelectors.networkFilterQueryDraft();
|
||||
const getIsNetworkFilterQueryDraftValid = networkSelectors.isNetworkFilterQueryDraftValid();
|
||||
const mapStateToProps = (state: State, { type }: OwnProps) => {
|
||||
const mapStateToProps = (state: State) => {
|
||||
return {
|
||||
networkFilterQueryDraft: getNetworkFilterQueryDraft(state, type),
|
||||
isNetworkFilterQueryDraftValid: getIsNetworkFilterQueryDraftValid(state, type),
|
||||
networkFilterQueryDraft: getNetworkFilterQueryDraft(state),
|
||||
isNetworkFilterQueryDraftValid: getIsNetworkFilterQueryDraftValid(state),
|
||||
};
|
||||
};
|
||||
return mapStateToProps;
|
||||
|
|
|
@ -130,9 +130,8 @@ class NetworkDnsComponentQuery extends QueryTemplate<
|
|||
}
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getNetworkDnsSelectorSelector = networkSelectors.dnsSelector();
|
||||
const mapStateToProps = (state: State, { type }: OwnProps) =>
|
||||
getNetworkDnsSelectorSelector(state, type);
|
||||
const getNetworkDnsSelector = networkSelectors.dnsSelector();
|
||||
const mapStateToProps = (state: State) => getNetworkDnsSelector(state);
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
|
|
@ -135,9 +135,8 @@ class NetworkTopNFlowComponentQuery extends QueryTemplate<
|
|||
}
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getNetworktopNFlowSelectorSelector = networkSelectors.topNFlowSelector();
|
||||
const mapStateToProps = (state: State, { type }: OwnProps) =>
|
||||
getNetworktopNFlowSelectorSelector(state, type);
|
||||
const getNetworkTopNFlowSelector = networkSelectors.topNFlowSelector();
|
||||
const mapStateToProps = (state: State) => getNetworkTopNFlowSelector(state);
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
|
|
@ -334,6 +334,68 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "IpOverview",
|
||||
"description": "",
|
||||
"args": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "filterQuery",
|
||||
"description": "",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "ip",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": { "kind": "OBJECT", "name": "IpOverviewData", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "KpiNetwork",
|
||||
"description": "",
|
||||
"args": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "timerange",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null }
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "filterQuery",
|
||||
"description": "",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": { "kind": "OBJECT", "name": "KpiNetworkData", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "NetworkTopNFlow",
|
||||
"description": "Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified",
|
||||
|
@ -528,37 +590,6 @@
|
|||
"type": { "kind": "OBJECT", "name": "SayMyName", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "KpiNetwork",
|
||||
"description": "",
|
||||
"args": [
|
||||
{
|
||||
"name": "id",
|
||||
"description": "",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "timerange",
|
||||
"description": "",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "INPUT_OBJECT", "name": "TimerangeInput", "ofType": null }
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "filterQuery",
|
||||
"description": "",
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": { "kind": "OBJECT", "name": "KpiNetworkData", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
@ -1473,7 +1504,7 @@
|
|||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "continent_name",
|
||||
"name": "city_name",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
|
@ -1481,7 +1512,7 @@
|
|||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "country_name",
|
||||
"name": "continent_name",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
|
@ -1497,13 +1528,21 @@
|
|||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "city_name",
|
||||
"name": "country_name",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "location",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "OBJECT", "name": "Location", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "region_iso_code",
|
||||
"description": "",
|
||||
|
@ -1526,6 +1565,33 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "Location",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "lon",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "lat",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "HostEcsFields",
|
||||
|
@ -4102,6 +4168,198 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "IpOverviewData",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "source",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "OBJECT", "name": "Overview", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "destination",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "OBJECT", "name": "Overview", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "Overview",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "firstSeen",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Date", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "lastSeen",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Date", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "autonomousSystem",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "OBJECT", "name": "AutonomousSystem", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "host",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "OBJECT", "name": "HostEcsFields", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "geo",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "OBJECT", "name": "GeoEcsFields", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "AutonomousSystem",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "as_org",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "asn",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "ip",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "KpiNetworkData",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "networkEvents",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "uniqueFlowId",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "activeAgents",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "uniqueSourcePrivateIps",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "uniqueDestinationPrivateIps",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "dnsQueries",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "tlsHandshakes",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "NetworkTopNFlowDirection",
|
||||
|
@ -4917,73 +5175,6 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "KpiNetworkData",
|
||||
"description": "",
|
||||
"fields": [
|
||||
{
|
||||
"name": "networkEvents",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "uniqueFlowId",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "activeAgents",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "uniqueSourcePrivateIps",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "uniqueDestinationPrivateIps",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "dnsQueries",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "tlsHandshakes",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": { "kind": "SCALAR", "name": "Float", "ofType": null },
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "__Schema",
|
||||
|
@ -5673,6 +5864,24 @@
|
|||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "IpOverviewType",
|
||||
"description": "",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "destination",
|
||||
"description": "",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{ "name": "source", "description": "", "isDeprecated": false, "deprecationReason": null }
|
||||
],
|
||||
"possibleTypes": null
|
||||
}
|
||||
],
|
||||
"directives": [
|
||||
|
|
|
@ -47,6 +47,10 @@ export interface Source {
|
|||
TimelineDetails: TimelineDetailsData;
|
||||
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
|
||||
Hosts: HostsData;
|
||||
|
||||
IpOverview?: IpOverviewData | null;
|
||||
|
||||
KpiNetwork?: KpiNetworkData | null;
|
||||
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
|
||||
NetworkTopNFlow: NetworkTopNFlowData;
|
||||
|
||||
|
@ -55,8 +59,6 @@ export interface Source {
|
|||
UncommonProcesses: UncommonProcessesData;
|
||||
/** Just a simple example to get the app name */
|
||||
whoAmI?: SayMyName | null;
|
||||
|
||||
KpiNetwork?: KpiNetworkData | null;
|
||||
}
|
||||
/** A set of configuration options for a security data source */
|
||||
export interface SourceConfiguration {
|
||||
|
@ -192,19 +194,27 @@ export interface SourceEcsFields {
|
|||
}
|
||||
|
||||
export interface GeoEcsFields {
|
||||
continent_name?: string | null;
|
||||
city_name?: string | null;
|
||||
|
||||
country_name?: string | null;
|
||||
continent_name?: string | null;
|
||||
|
||||
country_iso_code?: string | null;
|
||||
|
||||
city_name?: string | null;
|
||||
country_name?: string | null;
|
||||
|
||||
location?: Location | null;
|
||||
|
||||
region_iso_code?: string | null;
|
||||
|
||||
region_name?: string | null;
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
lon?: number | null;
|
||||
|
||||
lat?: number | null;
|
||||
}
|
||||
|
||||
export interface HostEcsFields {
|
||||
architecture?: string | null;
|
||||
|
||||
|
@ -743,6 +753,48 @@ export interface HostItem {
|
|||
lastBeat?: Date | null;
|
||||
}
|
||||
|
||||
export interface IpOverviewData {
|
||||
source?: Overview | null;
|
||||
|
||||
destination?: Overview | null;
|
||||
}
|
||||
|
||||
export interface Overview {
|
||||
firstSeen?: Date | null;
|
||||
|
||||
lastSeen?: Date | null;
|
||||
|
||||
autonomousSystem: AutonomousSystem;
|
||||
|
||||
host: HostEcsFields;
|
||||
|
||||
geo: GeoEcsFields;
|
||||
}
|
||||
|
||||
export interface AutonomousSystem {
|
||||
as_org?: string | null;
|
||||
|
||||
asn?: string | null;
|
||||
|
||||
ip?: string | null;
|
||||
}
|
||||
|
||||
export interface KpiNetworkData {
|
||||
networkEvents?: number | null;
|
||||
|
||||
uniqueFlowId?: number | null;
|
||||
|
||||
activeAgents?: number | null;
|
||||
|
||||
uniqueSourcePrivateIps?: number | null;
|
||||
|
||||
uniqueDestinationPrivateIps?: number | null;
|
||||
|
||||
dnsQueries?: number | null;
|
||||
|
||||
tlsHandshakes?: number | null;
|
||||
}
|
||||
|
||||
export interface NetworkTopNFlowData {
|
||||
edges: NetworkTopNFlowEdges[];
|
||||
|
||||
|
@ -852,22 +904,6 @@ export interface SayMyName {
|
|||
appName: string;
|
||||
}
|
||||
|
||||
export interface KpiNetworkData {
|
||||
networkEvents?: number | null;
|
||||
|
||||
uniqueFlowId?: number | null;
|
||||
|
||||
activeAgents?: number | null;
|
||||
|
||||
uniqueSourcePrivateIps?: number | null;
|
||||
|
||||
uniqueDestinationPrivateIps?: number | null;
|
||||
|
||||
dnsQueries?: number | null;
|
||||
|
||||
tlsHandshakes?: number | null;
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// InputTypes
|
||||
// ====================================================
|
||||
|
@ -957,6 +993,20 @@ export interface HostsSourceArgs {
|
|||
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
export interface IpOverviewSourceArgs {
|
||||
id?: string | null;
|
||||
|
||||
filterQuery?: string | null;
|
||||
|
||||
ip: string;
|
||||
}
|
||||
export interface KpiNetworkSourceArgs {
|
||||
id?: string | null;
|
||||
|
||||
timerange: TimerangeInput;
|
||||
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
export interface NetworkTopNFlowSourceArgs {
|
||||
direction: NetworkTopNFlowDirection;
|
||||
|
||||
|
@ -992,13 +1042,6 @@ export interface UncommonProcessesSourceArgs {
|
|||
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
export interface KpiNetworkSourceArgs {
|
||||
id?: string | null;
|
||||
|
||||
timerange: TimerangeInput;
|
||||
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
export interface IndexFieldsSourceStatusArgs {
|
||||
indexTypes?: IndexType[] | null;
|
||||
}
|
||||
|
@ -1056,6 +1099,11 @@ export enum NetworkDnsFields {
|
|||
dnsBytesOut = 'dnsBytesOut',
|
||||
}
|
||||
|
||||
export enum IpOverviewType {
|
||||
destination = 'destination',
|
||||
source = 'source',
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// END: Typescript template
|
||||
// ====================================================
|
||||
|
@ -1547,6 +1595,196 @@ export namespace GetHostsTableQuery {
|
|||
};
|
||||
}
|
||||
|
||||
export namespace GetIpOverviewQuery {
|
||||
export type Variables = {
|
||||
sourceId: string;
|
||||
filterQuery?: string | null;
|
||||
ip: string;
|
||||
};
|
||||
|
||||
export type Query = {
|
||||
__typename?: 'Query';
|
||||
|
||||
source: Source;
|
||||
};
|
||||
|
||||
export type Source = {
|
||||
__typename?: 'Source';
|
||||
|
||||
id: string;
|
||||
|
||||
IpOverview?: IpOverview | null;
|
||||
};
|
||||
|
||||
export type IpOverview = {
|
||||
__typename?: 'IpOverviewData';
|
||||
|
||||
source?: _Source | null;
|
||||
|
||||
destination?: Destination | null;
|
||||
};
|
||||
|
||||
export type _Source = {
|
||||
__typename?: 'Overview';
|
||||
|
||||
firstSeen?: Date | null;
|
||||
|
||||
lastSeen?: Date | null;
|
||||
|
||||
autonomousSystem: AutonomousSystem;
|
||||
|
||||
geo: Geo;
|
||||
|
||||
host: Host;
|
||||
};
|
||||
|
||||
export type AutonomousSystem = {
|
||||
__typename?: 'AutonomousSystem';
|
||||
|
||||
as_org?: string | null;
|
||||
|
||||
asn?: string | null;
|
||||
|
||||
ip?: string | null;
|
||||
};
|
||||
|
||||
export type Geo = {
|
||||
__typename?: 'GeoEcsFields';
|
||||
|
||||
continent_name?: string | null;
|
||||
|
||||
city_name?: string | null;
|
||||
|
||||
country_iso_code?: string | null;
|
||||
|
||||
country_name?: string | null;
|
||||
|
||||
location?: Location | null;
|
||||
|
||||
region_iso_code?: string | null;
|
||||
|
||||
region_name?: string | null;
|
||||
};
|
||||
|
||||
export type Location = {
|
||||
__typename?: 'Location';
|
||||
|
||||
lat?: number | null;
|
||||
|
||||
lon?: number | null;
|
||||
};
|
||||
|
||||
export type Host = {
|
||||
__typename?: 'HostEcsFields';
|
||||
|
||||
architecture?: string | null;
|
||||
|
||||
id?: string | null;
|
||||
|
||||
ip?: (string | null)[] | null;
|
||||
|
||||
mac?: (string | null)[] | null;
|
||||
|
||||
name?: string | null;
|
||||
|
||||
os?: Os | null;
|
||||
|
||||
type?: string | null;
|
||||
};
|
||||
|
||||
export type Os = {
|
||||
__typename?: 'OsEcsFields';
|
||||
|
||||
family?: string | null;
|
||||
|
||||
name?: string | null;
|
||||
|
||||
platform?: string | null;
|
||||
|
||||
version?: string | null;
|
||||
};
|
||||
|
||||
export type Destination = {
|
||||
__typename?: 'Overview';
|
||||
|
||||
firstSeen?: Date | null;
|
||||
|
||||
lastSeen?: Date | null;
|
||||
|
||||
autonomousSystem: _AutonomousSystem;
|
||||
|
||||
geo: _Geo;
|
||||
|
||||
host: _Host;
|
||||
};
|
||||
|
||||
export type _AutonomousSystem = {
|
||||
__typename?: 'AutonomousSystem';
|
||||
|
||||
as_org?: string | null;
|
||||
|
||||
asn?: string | null;
|
||||
|
||||
ip?: string | null;
|
||||
};
|
||||
|
||||
export type _Geo = {
|
||||
__typename?: 'GeoEcsFields';
|
||||
|
||||
continent_name?: string | null;
|
||||
|
||||
city_name?: string | null;
|
||||
|
||||
country_iso_code?: string | null;
|
||||
|
||||
country_name?: string | null;
|
||||
|
||||
location?: _Location | null;
|
||||
|
||||
region_iso_code?: string | null;
|
||||
|
||||
region_name?: string | null;
|
||||
};
|
||||
|
||||
export type _Location = {
|
||||
__typename?: 'Location';
|
||||
|
||||
lat?: number | null;
|
||||
|
||||
lon?: number | null;
|
||||
};
|
||||
|
||||
export type _Host = {
|
||||
__typename?: 'HostEcsFields';
|
||||
|
||||
architecture?: string | null;
|
||||
|
||||
id?: string | null;
|
||||
|
||||
ip?: (string | null)[] | null;
|
||||
|
||||
mac?: (string | null)[] | null;
|
||||
|
||||
name?: string | null;
|
||||
|
||||
os?: _Os | null;
|
||||
|
||||
type?: string | null;
|
||||
};
|
||||
|
||||
export type _Os = {
|
||||
__typename?: 'OsEcsFields';
|
||||
|
||||
family?: string | null;
|
||||
|
||||
name?: string | null;
|
||||
|
||||
platform?: string | null;
|
||||
|
||||
version?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export namespace GetKpiEventsQuery {
|
||||
export type Variables = {
|
||||
sourceId: string;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import { defaultWidth } from '../components/timeline/body';
|
||||
import {
|
||||
Direction,
|
||||
IpOverviewType,
|
||||
NetworkDnsFields,
|
||||
NetworkTopNFlowDirection,
|
||||
NetworkTopNFlowFields,
|
||||
|
@ -65,7 +66,15 @@ export const mockGlobalState: State = {
|
|||
filterQuery: null,
|
||||
filterQueryDraft: null,
|
||||
},
|
||||
details: { filterQuery: null, filterQueryDraft: null, queries: null },
|
||||
details: {
|
||||
filterQuery: null,
|
||||
filterQueryDraft: null,
|
||||
queries: {
|
||||
ipOverview: {
|
||||
flowType: IpOverviewType.source,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
global: {
|
||||
|
|
|
@ -12,7 +12,9 @@ import chrome from 'ui/chrome';
|
|||
import { EmptyPage } from '../../components/empty_page';
|
||||
import { getNetworkUrl, NetworkComponentProps } from '../../components/link_to/redirect_to_network';
|
||||
import { BreadcrumbItem } from '../../components/page/navigation/breadcrumb';
|
||||
import { IpOverview } from '../../components/page/network/ip_overview';
|
||||
import { GlobalTime } from '../../containers/global_time';
|
||||
import { IpOverviewQuery } from '../../containers/ip_overview';
|
||||
import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source';
|
||||
import { IndexType } from '../../graphql/types';
|
||||
import { decodeIpv6 } from '../../lib/helpers';
|
||||
|
@ -23,10 +25,9 @@ import { NetworkKql } from './kql';
|
|||
import * as i18n from './translations';
|
||||
|
||||
const basePath = chrome.getBasePath();
|
||||
const type = networkModel.NetworkType.details;
|
||||
|
||||
interface IPDetailsComponentReduxProps {
|
||||
filterQueryExpression: string;
|
||||
filterQuery: string;
|
||||
}
|
||||
|
||||
type IPDetailsComponentProps = IPDetailsComponentReduxProps & NetworkComponentProps;
|
||||
|
@ -36,7 +37,7 @@ const IPDetailsComponent = pure<IPDetailsComponentProps>(
|
|||
match: {
|
||||
params: { ip },
|
||||
},
|
||||
filterQueryExpression,
|
||||
filterQuery,
|
||||
}) => (
|
||||
<WithSource sourceId="default" indexTypes={[IndexType.FILEBEAT, IndexType.PACKETBEAT]}>
|
||||
{({ filebeatIndicesExist, indexPattern }) =>
|
||||
|
@ -46,7 +47,23 @@ const IPDetailsComponent = pure<IPDetailsComponentProps>(
|
|||
<PageContent data-test-subj="pageContent" panelPaddingSize="none">
|
||||
<PageContentBody data-test-subj="pane1ScrollContainer">
|
||||
<GlobalTime>
|
||||
{({ poll, to, from, setQuery }) => <>{`Hello ${decodeIpv6(ip)}!`}</>}
|
||||
{({ poll, to, from, setQuery }) => (
|
||||
<IpOverviewQuery
|
||||
sourceId="default"
|
||||
filterQuery={filterQuery}
|
||||
type={networkModel.NetworkType.details}
|
||||
ip={decodeIpv6(ip)}
|
||||
>
|
||||
{({ ipOverviewData, loading }) => (
|
||||
<IpOverview
|
||||
ip={decodeIpv6(ip)}
|
||||
data={ipOverviewData}
|
||||
loading={loading}
|
||||
type={networkModel.NetworkType.details}
|
||||
/>
|
||||
)}
|
||||
</IpOverviewQuery>
|
||||
)}
|
||||
</GlobalTime>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
|
@ -67,7 +84,7 @@ const IPDetailsComponent = pure<IPDetailsComponentProps>(
|
|||
const makeMapStateToProps = () => {
|
||||
const getNetworkFilterQuery = networkSelectors.networkFilterQueryExpression();
|
||||
return (state: State) => ({
|
||||
filterQueryExpression: getNetworkFilterQuery(state, type) || '',
|
||||
filterQueryExpression: getNetworkFilterQuery(state) || '',
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -144,7 +144,7 @@ const NetworkComponent = pure<NetworkComponentProps>(({ filterQuery }) => (
|
|||
const makeMapStateToProps = () => {
|
||||
const getNetworkFilterQueryAsJson = networkSelectors.networkFilterQueryAsJson();
|
||||
const mapStateToProps = (state: State) => ({
|
||||
filterQuery: getNetworkFilterQueryAsJson(state, networkModel.NetworkType.page) || '',
|
||||
filterQuery: getNetworkFilterQueryAsJson(state) || '',
|
||||
});
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
import actionCreatorFactory from 'typescript-fsa';
|
||||
|
||||
import {
|
||||
IpOverviewType,
|
||||
NetworkDnsSortField,
|
||||
NetworkTopNFlowDirection,
|
||||
NetworkTopNFlowSortField,
|
||||
|
@ -18,9 +19,10 @@ import { NetworkType } from './model';
|
|||
|
||||
const actionCreator = actionCreatorFactory('x-pack/secops/local/network');
|
||||
|
||||
export const updateDnsLimit = actionCreator<{ limit: number; networkType: NetworkType }>(
|
||||
'UPDATE_DNS_LIMIT'
|
||||
);
|
||||
export const updateDnsLimit = actionCreator<{
|
||||
limit: number;
|
||||
networkType: NetworkType;
|
||||
}>('UPDATE_DNS_LIMIT');
|
||||
|
||||
export const updateDnsSort = actionCreator<{
|
||||
dnsSortField: NetworkDnsSortField;
|
||||
|
@ -30,11 +32,12 @@ export const updateDnsSort = actionCreator<{
|
|||
export const updateIsPtrIncluded = actionCreator<{
|
||||
isPtrIncluded: boolean;
|
||||
networkType: NetworkType;
|
||||
}>('UPDATE_DNS_iS_PTR_INCLUDED');
|
||||
}>('UPDATE_DNS_IS_PTR_INCLUDED');
|
||||
|
||||
export const updateTopNFlowLimit = actionCreator<{ limit: number; networkType: NetworkType }>(
|
||||
'UPDATE_TOP_N_FLOW_LIMIT'
|
||||
);
|
||||
export const updateTopNFlowLimit = actionCreator<{
|
||||
limit: number;
|
||||
networkType: NetworkType;
|
||||
}>('UPDATE_TOP_N_FLOW_LIMIT');
|
||||
|
||||
export const updateTopNFlowSort = actionCreator<{
|
||||
topNFlowSort: NetworkTopNFlowSortField;
|
||||
|
@ -60,3 +63,8 @@ export const applyNetworkFilterQuery = actionCreator<{
|
|||
filterQuery: SerializedFilterQuery;
|
||||
networkType: NetworkType;
|
||||
}>('APPLY_NETWORK_FILTER_QUERY');
|
||||
|
||||
// IP Overview Actions
|
||||
export const updateIpOverviewFlowType = actionCreator<{
|
||||
flowType: IpOverviewType;
|
||||
}>('UPDATE_IP_OVERVIEW_FLOW_TYPE');
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
IpOverviewType,
|
||||
NetworkDnsSortField,
|
||||
NetworkTopNFlowDirection,
|
||||
NetworkTopNFlowSortField,
|
||||
|
@ -21,6 +22,7 @@ export interface BasicQuery {
|
|||
limit: number;
|
||||
}
|
||||
|
||||
// Network Page Models
|
||||
export interface TopNFlowQuery extends BasicQuery {
|
||||
topNFlowType: NetworkTopNFlowType;
|
||||
topNFlowSort: NetworkTopNFlowSortField;
|
||||
|
@ -37,13 +39,29 @@ interface NetworkQueries {
|
|||
dns: DnsQuery;
|
||||
}
|
||||
|
||||
export interface GenericNetworkModel {
|
||||
export interface NetworkPageModel {
|
||||
filterQuery: SerializedFilterQuery | null;
|
||||
filterQueryDraft: KueryFilterQuery | null;
|
||||
queries: NetworkQueries | null;
|
||||
queries: NetworkQueries;
|
||||
}
|
||||
|
||||
export interface NetworkModel {
|
||||
page: GenericNetworkModel;
|
||||
details: GenericNetworkModel;
|
||||
// IP Details Models
|
||||
export interface IpOverviewQuery {
|
||||
flowType: IpOverviewType;
|
||||
}
|
||||
|
||||
interface IpOverviewQueries {
|
||||
ipOverview: IpOverviewQuery;
|
||||
}
|
||||
|
||||
export interface NetworkDetailsModel {
|
||||
filterQuery: SerializedFilterQuery | null;
|
||||
filterQueryDraft: KueryFilterQuery | null;
|
||||
queries: IpOverviewQueries;
|
||||
}
|
||||
|
||||
// Network Model
|
||||
export interface NetworkModel {
|
||||
[NetworkType.page]: NetworkPageModel;
|
||||
[NetworkType.details]: NetworkDetailsModel;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { reducerWithInitialState } from 'typescript-fsa-reducers';
|
|||
|
||||
import {
|
||||
Direction,
|
||||
IpOverviewType,
|
||||
NetworkDnsFields,
|
||||
NetworkTopNFlowDirection,
|
||||
NetworkTopNFlowFields,
|
||||
|
@ -20,6 +21,7 @@ import {
|
|||
setNetworkFilterQueryDraft,
|
||||
updateDnsLimit,
|
||||
updateDnsSort,
|
||||
updateIpOverviewFlowType,
|
||||
updateIsPtrIncluded,
|
||||
updateTopNFlowDirection,
|
||||
updateTopNFlowLimit,
|
||||
|
@ -27,7 +29,7 @@ import {
|
|||
updateTopNFlowType,
|
||||
} from './actions';
|
||||
import { helperUpdateTopNFlowDirection } from './helper';
|
||||
import { NetworkModel } from './model';
|
||||
import { NetworkModel, NetworkType } from './model';
|
||||
|
||||
export type NetworkState = NetworkModel;
|
||||
|
||||
|
@ -56,7 +58,11 @@ export const initialNetworkState: NetworkState = {
|
|||
filterQueryDraft: null,
|
||||
},
|
||||
details: {
|
||||
queries: null,
|
||||
queries: {
|
||||
ipOverview: {
|
||||
flowType: IpOverviewType.source,
|
||||
},
|
||||
},
|
||||
filterQuery: null,
|
||||
filterQueryDraft: null,
|
||||
},
|
||||
|
@ -70,7 +76,7 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
|
|||
queries: {
|
||||
...state[networkType].queries,
|
||||
dns: {
|
||||
...state[networkType].queries!.dns,
|
||||
...state[NetworkType.page].queries.dns,
|
||||
limit,
|
||||
},
|
||||
},
|
||||
|
@ -83,7 +89,7 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
|
|||
queries: {
|
||||
...state[networkType].queries,
|
||||
dns: {
|
||||
...state[networkType].queries!.dns,
|
||||
...state[NetworkType.page].queries.dns,
|
||||
dnsSortField,
|
||||
},
|
||||
},
|
||||
|
@ -96,7 +102,7 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
|
|||
queries: {
|
||||
...state[networkType].queries,
|
||||
dns: {
|
||||
...state[networkType].queries!.dns,
|
||||
...state[NetworkType.page].queries.dns,
|
||||
isPtrIncluded,
|
||||
},
|
||||
},
|
||||
|
@ -109,7 +115,7 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
|
|||
queries: {
|
||||
...state[networkType].queries,
|
||||
topNFlow: {
|
||||
...state[networkType].queries!.topNFlow,
|
||||
...state[NetworkType.page].queries.topNFlow,
|
||||
limit,
|
||||
},
|
||||
},
|
||||
|
@ -122,9 +128,9 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
|
|||
queries: {
|
||||
...state[networkType].queries,
|
||||
topNFlow: {
|
||||
...state[networkType].queries!.topNFlow,
|
||||
...state[NetworkType.page].queries.topNFlow,
|
||||
...helperUpdateTopNFlowDirection(
|
||||
state[networkType].queries!.topNFlow.topNFlowType,
|
||||
state[NetworkType.page].queries.topNFlow.topNFlowType,
|
||||
topNFlowDirection
|
||||
),
|
||||
},
|
||||
|
@ -138,7 +144,7 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
|
|||
queries: {
|
||||
...state[networkType].queries,
|
||||
topNFlow: {
|
||||
...state[networkType].queries!.topNFlow,
|
||||
...state[NetworkType.page].queries.topNFlow,
|
||||
topNFlowSort,
|
||||
},
|
||||
},
|
||||
|
@ -151,7 +157,7 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
|
|||
queries: {
|
||||
...state[networkType].queries,
|
||||
topNFlow: {
|
||||
...state[networkType].queries!.topNFlow,
|
||||
...state[NetworkType.page].queries.topNFlow,
|
||||
topNFlowType,
|
||||
topNFlowSort: {
|
||||
field: NetworkTopNFlowFields.bytes,
|
||||
|
@ -176,4 +182,17 @@ export const networkReducer = reducerWithInitialState(initialNetworkState)
|
|||
filterQuery,
|
||||
},
|
||||
}))
|
||||
.case(updateIpOverviewFlowType, (state, { flowType }) => ({
|
||||
...state,
|
||||
[NetworkType.details]: {
|
||||
...state[NetworkType.details],
|
||||
queries: {
|
||||
...state[NetworkType.details].queries,
|
||||
ipOverview: {
|
||||
...state[NetworkType.details].queries.ipOverview,
|
||||
flowType,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
.build();
|
||||
|
|
|
@ -4,49 +4,56 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash/fp';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { isFromKueryExpressionValid } from '../../../lib/keury';
|
||||
import { State } from '../../reducer';
|
||||
|
||||
import { GenericNetworkModel, NetworkType } from './model';
|
||||
import { NetworkDetailsModel, NetworkPageModel } from './model';
|
||||
|
||||
const selectNetwork = (state: State, networkType: NetworkType): GenericNetworkModel =>
|
||||
get(networkType, state.local.network);
|
||||
const selectNetworkPage = (state: State): NetworkPageModel => state.local.network.page;
|
||||
|
||||
const selectNetworkDetails = (state: State): NetworkDetailsModel => state.local.network.details;
|
||||
|
||||
export const dnsSelector = () =>
|
||||
createSelector(
|
||||
selectNetwork,
|
||||
network => network.queries!.dns
|
||||
selectNetworkPage,
|
||||
network => network.queries.dns
|
||||
);
|
||||
|
||||
export const topNFlowSelector = () =>
|
||||
createSelector(
|
||||
selectNetwork,
|
||||
network => network.queries!.topNFlow
|
||||
selectNetworkPage,
|
||||
network => network.queries.topNFlow
|
||||
);
|
||||
|
||||
export const networkFilterQueryExpression = () =>
|
||||
createSelector(
|
||||
selectNetwork,
|
||||
selectNetworkPage,
|
||||
network => (network.filterQuery ? network.filterQuery.query.expression : null)
|
||||
);
|
||||
|
||||
export const networkFilterQueryAsJson = () =>
|
||||
createSelector(
|
||||
selectNetwork,
|
||||
selectNetworkPage,
|
||||
network => (network.filterQuery ? network.filterQuery.serializedQuery : null)
|
||||
);
|
||||
|
||||
export const networkFilterQueryDraft = () =>
|
||||
createSelector(
|
||||
selectNetwork,
|
||||
selectNetworkPage,
|
||||
network => network.filterQueryDraft
|
||||
);
|
||||
|
||||
export const isNetworkFilterQueryDraftValid = () =>
|
||||
createSelector(
|
||||
selectNetwork,
|
||||
selectNetworkPage,
|
||||
network => isFromKueryExpressionValid(network.filterQueryDraft)
|
||||
);
|
||||
|
||||
// IP Details Selectors
|
||||
export const ipOverviewSelector = () =>
|
||||
createSelector(
|
||||
selectNetworkDetails,
|
||||
network => network.queries.ipOverview
|
||||
);
|
||||
|
|
|
@ -22,11 +22,17 @@ export const ecsSchema = gql`
|
|||
dataset: String
|
||||
}
|
||||
|
||||
type Location {
|
||||
lon: Float
|
||||
lat: Float
|
||||
}
|
||||
|
||||
type GeoEcsFields {
|
||||
continent_name: String
|
||||
country_name: String
|
||||
country_iso_code: String
|
||||
city_name: String
|
||||
continent_name: String
|
||||
country_iso_code: String
|
||||
country_name: String
|
||||
location: Location
|
||||
region_iso_code: String
|
||||
region_name: String
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { authenticationsSchema } from './authentications';
|
|||
import { ecsSchema } from './ecs';
|
||||
import { eventsSchema } from './events';
|
||||
import { hostsSchema } from './hosts';
|
||||
import { ipOverviewSchema } from './ip_overview';
|
||||
import { kpiNetworkSchema } from './kpi_network';
|
||||
import { networkSchema } from './network';
|
||||
import { dateSchema } from './scalar_date';
|
||||
|
@ -28,6 +29,8 @@ export const schemas = [
|
|||
eventsSchema,
|
||||
dateSchema,
|
||||
hostsSchema,
|
||||
ipOverviewSchema,
|
||||
kpiNetworkSchema,
|
||||
networkSchema,
|
||||
rootSchema,
|
||||
sourcesSchema,
|
||||
|
@ -35,7 +38,6 @@ export const schemas = [
|
|||
sharedSchema,
|
||||
uncommonProcessesSchema,
|
||||
whoAmISchema,
|
||||
kpiNetworkSchema,
|
||||
];
|
||||
|
||||
// The types from graphql-tools/src/mock.ts 'any' based. I add slightly
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { createIpOverviewResolvers } from './resolvers';
|
||||
export { ipOverviewSchema } from './schema.gql';
|
|
@ -0,0 +1,807 @@
|
|||
/*
|
||||
* 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 { FieldNode } from 'graphql';
|
||||
|
||||
import { Logger } from '../../utils/logger';
|
||||
import { SecOpsContext } from '../index';
|
||||
import { IpOverviewData } from '../types';
|
||||
|
||||
export const mockIpOverviewData: { IpOverview: IpOverviewData } = {
|
||||
IpOverview: {
|
||||
source: {
|
||||
firstSeen: null,
|
||||
lastSeen: '2019-02-07T17:19:41.636Z',
|
||||
autonomousSystem: { as_org: 'Hello World', asn: 'Hello World', ip: 'Hello World' },
|
||||
geo: {
|
||||
continent_name: 'Hello World',
|
||||
city_name: 'Hello World',
|
||||
country_iso_code: 'Hello World',
|
||||
country_name: null,
|
||||
location: {
|
||||
lat: 40.7214,
|
||||
lon: -74.0052,
|
||||
},
|
||||
region_iso_code: 'Hello World',
|
||||
region_name: 'Hello World',
|
||||
},
|
||||
host: {
|
||||
os: {
|
||||
name: 'Raspbian GNU/Linux',
|
||||
family: '',
|
||||
kernel: '4.14.50-v7+',
|
||||
version: '9 (stretch)',
|
||||
platform: 'raspbian',
|
||||
},
|
||||
name: 'raspberrypi',
|
||||
id: 'b19a781f683541a7a25ee345133aa399',
|
||||
ip: ['Hello World', 'Hello World'],
|
||||
mac: ['Hello World', 'Hello World'],
|
||||
architecture: 'armv7l',
|
||||
type: 'Hello World',
|
||||
},
|
||||
},
|
||||
destination: {
|
||||
firstSeen: '2019-02-07T17:19:41.648Z',
|
||||
lastSeen: '2019-02-07T17:19:41.648Z',
|
||||
autonomousSystem: { as_org: 'Hello World', asn: 'Hello World', ip: 'Hello World' },
|
||||
geo: {
|
||||
continent_name: 'Hello World',
|
||||
city_name: 'Hello World',
|
||||
country_iso_code: 'Hello World',
|
||||
country_name: null,
|
||||
location: {
|
||||
lat: 40.7214,
|
||||
lon: -74.0052,
|
||||
},
|
||||
region_iso_code: 'Hello World',
|
||||
region_name: 'Hello World',
|
||||
},
|
||||
host: {
|
||||
os: {
|
||||
name: 'Raspbian GNU/Linux',
|
||||
family: '',
|
||||
kernel: '4.14.50-v7+',
|
||||
version: '9 (stretch)',
|
||||
platform: 'raspbian',
|
||||
},
|
||||
ip: ['Hello World', 'Hello World'],
|
||||
mac: ['Hello World', 'Hello World'],
|
||||
name: 'raspberrypi',
|
||||
id: 'b19a781f683541a7a25ee345133aa399',
|
||||
architecture: 'armv7l',
|
||||
type: 'Hello World',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const getIpOverviewQueryMock = (logger: Logger) => ({
|
||||
source: (root: unknown, args: unknown, context: SecOpsContext) => {
|
||||
logger.info('Mock source');
|
||||
const operationName = context.req.payload.operationName.toLowerCase();
|
||||
switch (operationName) {
|
||||
case 'ip-overview': {
|
||||
return mockIpOverviewData;
|
||||
}
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const mockIpOverviewFields: FieldNode = {
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'IpOverview',
|
||||
},
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'source',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'firstSeen',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'lastSeen',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'autonomousSystem',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'as_org',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'asn',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'ip',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'geo',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'continent_name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'city_name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'country_iso_code',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'country_name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'location',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'lat',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'lon',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'region_iso_code',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'region_name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'host',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'architecture',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'id',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'ip',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'mac',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'os',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'family',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'platform',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'version',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'type',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'destination',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'firstSeen',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'lastSeen',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'autonomousSystem',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'as_org',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'asn',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'ip',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'geo',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'continent_name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'city_name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'country_iso_code',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'country_name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'location',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'lat',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'lon',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'region_iso_code',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'region_name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'host',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'architecture',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'id',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'ip',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'mac',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'os',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
selectionSet: {
|
||||
kind: 'SelectionSet',
|
||||
selections: [
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'family',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'name',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'platform',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'version',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: 'type',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'Field',
|
||||
name: {
|
||||
kind: 'Name',
|
||||
value: '__typename',
|
||||
},
|
||||
arguments: [],
|
||||
directives: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { GraphQLResolveInfo } from 'graphql';
|
||||
import { omit } from 'lodash/fp';
|
||||
|
||||
import { Source } from '../../graphql/types';
|
||||
import { FrameworkRequest, internalFrameworkRequest } from '../../lib/framework';
|
||||
import { IpOverview } from '../../lib/ip_overview';
|
||||
import { IpOverviewAdapter } from '../../lib/ip_overview/types';
|
||||
import { SourceStatus } from '../../lib/source_status';
|
||||
import { Sources } from '../../lib/sources';
|
||||
import { createSourcesResolvers } from '../sources';
|
||||
import { SourcesResolversDeps } from '../sources/resolvers';
|
||||
import { mockSourcesAdapter, mockSourceStatusAdapter } from '../sources/resolvers.test';
|
||||
|
||||
import { mockIpOverviewData, mockIpOverviewFields } from './ip_overview.mock';
|
||||
import { createIpOverviewResolvers, IpOverviewResolversDeps } from './resolvers';
|
||||
|
||||
const mockGetFields = jest.fn();
|
||||
mockGetFields.mockResolvedValue({ fieldNodes: [mockIpOverviewFields] });
|
||||
jest.mock('../../utils/build_query/fields', () => ({
|
||||
getFields: mockGetFields,
|
||||
}));
|
||||
|
||||
const mockIpOverview = jest.fn();
|
||||
mockIpOverview.mockResolvedValue({
|
||||
IpOverview: {
|
||||
...mockIpOverviewData.IpOverview,
|
||||
},
|
||||
});
|
||||
const mockIpOverviewAdapter: IpOverviewAdapter = {
|
||||
getIpOverview: mockIpOverview,
|
||||
};
|
||||
|
||||
const mockIpOverviewLibs: IpOverviewResolversDeps = {
|
||||
ipOverview: new IpOverview(mockIpOverviewAdapter),
|
||||
};
|
||||
|
||||
const mockSrcLibs: SourcesResolversDeps = {
|
||||
sources: new Sources(mockSourcesAdapter),
|
||||
sourceStatus: new SourceStatus(mockSourceStatusAdapter, new Sources(mockSourcesAdapter)),
|
||||
};
|
||||
|
||||
const req: FrameworkRequest = {
|
||||
[internalFrameworkRequest]: {
|
||||
params: {},
|
||||
query: {},
|
||||
payload: {
|
||||
operationName: 'test',
|
||||
},
|
||||
},
|
||||
params: {},
|
||||
query: {},
|
||||
payload: {
|
||||
operationName: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const context = { req };
|
||||
|
||||
describe('Test Source Resolvers', () => {
|
||||
test('Make sure that getIpOverview have been called', async () => {
|
||||
const source = await createSourcesResolvers(mockSrcLibs).Query.source(
|
||||
{},
|
||||
{ id: 'default' },
|
||||
context,
|
||||
{} as GraphQLResolveInfo
|
||||
);
|
||||
const data = await createIpOverviewResolvers(mockIpOverviewLibs).Source.IpOverview(
|
||||
source as Source,
|
||||
{
|
||||
ip: '10.10.10.10',
|
||||
},
|
||||
context,
|
||||
{} as GraphQLResolveInfo
|
||||
);
|
||||
expect(mockIpOverviewAdapter.getIpOverview).toHaveBeenCalled();
|
||||
expect(data).toEqual(omit('status', mockIpOverviewData));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { SourceResolvers } from '../../graphql/types';
|
||||
import { AppResolverOf, ChildResolverOf } from '../../lib/framework';
|
||||
import { IpOverview } from '../../lib/ip_overview';
|
||||
import { createOptions } from '../../utils/build_query/create_options';
|
||||
import { QuerySourceResolver } from '../sources/resolvers';
|
||||
|
||||
export type QueryIpOverviewResolver = ChildResolverOf<
|
||||
AppResolverOf<SourceResolvers.IpOverviewResolver>,
|
||||
QuerySourceResolver
|
||||
>;
|
||||
|
||||
export interface IpOverviewResolversDeps {
|
||||
ipOverview: IpOverview;
|
||||
}
|
||||
|
||||
export const createIpOverviewResolvers = (
|
||||
libs: IpOverviewResolversDeps
|
||||
): {
|
||||
Source: {
|
||||
IpOverview: QueryIpOverviewResolver;
|
||||
};
|
||||
} => ({
|
||||
Source: {
|
||||
async IpOverview(source, args, { req }, info) {
|
||||
const options = { ...createOptions(source, args, info), ip: args.ip };
|
||||
return libs.ipOverview.getIpOverview(req, options);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 gql from 'graphql-tag';
|
||||
|
||||
export const ipOverviewSchema = gql`
|
||||
enum IpOverviewType {
|
||||
destination
|
||||
source
|
||||
}
|
||||
|
||||
type AutonomousSystem {
|
||||
as_org: String
|
||||
asn: String
|
||||
ip: String
|
||||
}
|
||||
|
||||
type Overview {
|
||||
firstSeen: Date
|
||||
lastSeen: Date
|
||||
autonomousSystem: AutonomousSystem!
|
||||
host: HostEcsFields!
|
||||
geo: GeoEcsFields!
|
||||
}
|
||||
|
||||
type IpOverviewData {
|
||||
source: Overview
|
||||
destination: Overview
|
||||
}
|
||||
|
||||
extend type Source {
|
||||
IpOverview(id: String, filterQuery: String, ip: String!): IpOverviewData
|
||||
}
|
||||
`;
|
166
x-pack/plugins/secops/server/graphql/ip_overview/schema.test.ts
Normal file
166
x-pack/plugins/secops/server/graphql/ip_overview/schema.test.ts
Normal file
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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 { graphql } from 'graphql';
|
||||
import { addMockFunctionsToSchema, makeExecutableSchema } from 'graphql-tools';
|
||||
|
||||
import { rootSchema } from '../../../common/graphql/root/schema.gql';
|
||||
import { sharedSchema } from '../../../common/graphql/shared';
|
||||
import { Logger } from '../../utils/logger';
|
||||
import { ecsSchema } from '../ecs';
|
||||
import { dateSchema } from '../scalar_date';
|
||||
import { sourceStatusSchema } from '../source_status/schema.gql';
|
||||
import { sourcesSchema } from '../sources/schema.gql';
|
||||
|
||||
import { getIpOverviewQueryMock, mockIpOverviewData } from './ip_overview.mock';
|
||||
|
||||
import { ipOverviewSchema } from './schema.gql';
|
||||
|
||||
const testCaseSource = {
|
||||
id: 'Test case to query IpOverview',
|
||||
query: `
|
||||
query GetIpOverviewQuery(
|
||||
$filterQuery: String
|
||||
$ip: String!
|
||||
) {
|
||||
source(id: "default") {
|
||||
IpOverview(filterQuery: $filterQuery, ip: $ip) {
|
||||
source {
|
||||
firstSeen
|
||||
lastSeen
|
||||
autonomousSystem {
|
||||
as_org
|
||||
asn
|
||||
ip
|
||||
}
|
||||
geo {
|
||||
continent_name
|
||||
city_name
|
||||
country_iso_code
|
||||
country_name
|
||||
location {
|
||||
lat
|
||||
lon
|
||||
}
|
||||
region_iso_code
|
||||
region_name
|
||||
}
|
||||
host {
|
||||
architecture
|
||||
id
|
||||
ip
|
||||
mac
|
||||
name
|
||||
os {
|
||||
kernel
|
||||
family
|
||||
name
|
||||
platform
|
||||
version
|
||||
}
|
||||
type
|
||||
}
|
||||
}
|
||||
destination {
|
||||
firstSeen
|
||||
lastSeen
|
||||
autonomousSystem {
|
||||
as_org
|
||||
asn
|
||||
ip
|
||||
}
|
||||
geo {
|
||||
continent_name
|
||||
city_name
|
||||
country_iso_code
|
||||
country_name
|
||||
location {
|
||||
lat
|
||||
lon
|
||||
}
|
||||
region_iso_code
|
||||
region_name
|
||||
}
|
||||
host {
|
||||
architecture
|
||||
id
|
||||
ip
|
||||
mac
|
||||
name
|
||||
os {
|
||||
kernel
|
||||
family
|
||||
name
|
||||
platform
|
||||
version
|
||||
}
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
ip: '10.10.10.10',
|
||||
},
|
||||
context: {
|
||||
req: {
|
||||
payload: {
|
||||
operationName: 'ip-overview',
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: {
|
||||
data: {
|
||||
source: {
|
||||
...mockIpOverviewData,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('Test Source Schema', () => {
|
||||
// Array of case types
|
||||
const cases = [testCaseSource];
|
||||
const typeDefs = [
|
||||
rootSchema,
|
||||
sharedSchema,
|
||||
sourcesSchema,
|
||||
sourceStatusSchema,
|
||||
ecsSchema,
|
||||
ipOverviewSchema,
|
||||
dateSchema,
|
||||
];
|
||||
const mockSchema = makeExecutableSchema({ typeDefs });
|
||||
|
||||
// Here we specify the return payloads of mocked types
|
||||
const logger: Logger = {
|
||||
debug: jest.fn(),
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
};
|
||||
const mocks = {
|
||||
Query: () => ({
|
||||
...getIpOverviewQueryMock(logger),
|
||||
}),
|
||||
};
|
||||
|
||||
addMockFunctionsToSchema({
|
||||
schema: mockSchema,
|
||||
mocks,
|
||||
});
|
||||
|
||||
cases.forEach(obj => {
|
||||
const { id, query, variables, context, expected } = obj;
|
||||
|
||||
test(`${id}`, async () => {
|
||||
const result = await graphql(mockSchema, query, null, context, variables);
|
||||
return await expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -76,6 +76,10 @@ export interface Source {
|
|||
TimelineDetails: TimelineDetailsData;
|
||||
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
|
||||
Hosts: HostsData;
|
||||
|
||||
IpOverview?: IpOverviewData | null;
|
||||
|
||||
KpiNetwork?: KpiNetworkData | null;
|
||||
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
|
||||
NetworkTopNFlow: NetworkTopNFlowData;
|
||||
|
||||
|
@ -84,8 +88,6 @@ export interface Source {
|
|||
UncommonProcesses: UncommonProcessesData;
|
||||
/** Just a simple example to get the app name */
|
||||
whoAmI?: SayMyName | null;
|
||||
|
||||
KpiNetwork?: KpiNetworkData | null;
|
||||
}
|
||||
/** A set of configuration options for a security data source */
|
||||
export interface SourceConfiguration {
|
||||
|
@ -221,19 +223,27 @@ export interface SourceEcsFields {
|
|||
}
|
||||
|
||||
export interface GeoEcsFields {
|
||||
continent_name?: string | null;
|
||||
city_name?: string | null;
|
||||
|
||||
country_name?: string | null;
|
||||
continent_name?: string | null;
|
||||
|
||||
country_iso_code?: string | null;
|
||||
|
||||
city_name?: string | null;
|
||||
country_name?: string | null;
|
||||
|
||||
location?: Location | null;
|
||||
|
||||
region_iso_code?: string | null;
|
||||
|
||||
region_name?: string | null;
|
||||
}
|
||||
|
||||
export interface Location {
|
||||
lon?: number | null;
|
||||
|
||||
lat?: number | null;
|
||||
}
|
||||
|
||||
export interface HostEcsFields {
|
||||
architecture?: string | null;
|
||||
|
||||
|
@ -772,6 +782,48 @@ export interface HostItem {
|
|||
lastBeat?: Date | null;
|
||||
}
|
||||
|
||||
export interface IpOverviewData {
|
||||
source?: Overview | null;
|
||||
|
||||
destination?: Overview | null;
|
||||
}
|
||||
|
||||
export interface Overview {
|
||||
firstSeen?: Date | null;
|
||||
|
||||
lastSeen?: Date | null;
|
||||
|
||||
autonomousSystem: AutonomousSystem;
|
||||
|
||||
host: HostEcsFields;
|
||||
|
||||
geo: GeoEcsFields;
|
||||
}
|
||||
|
||||
export interface AutonomousSystem {
|
||||
as_org?: string | null;
|
||||
|
||||
asn?: string | null;
|
||||
|
||||
ip?: string | null;
|
||||
}
|
||||
|
||||
export interface KpiNetworkData {
|
||||
networkEvents?: number | null;
|
||||
|
||||
uniqueFlowId?: number | null;
|
||||
|
||||
activeAgents?: number | null;
|
||||
|
||||
uniqueSourcePrivateIps?: number | null;
|
||||
|
||||
uniqueDestinationPrivateIps?: number | null;
|
||||
|
||||
dnsQueries?: number | null;
|
||||
|
||||
tlsHandshakes?: number | null;
|
||||
}
|
||||
|
||||
export interface NetworkTopNFlowData {
|
||||
edges: NetworkTopNFlowEdges[];
|
||||
|
||||
|
@ -881,22 +933,6 @@ export interface SayMyName {
|
|||
appName: string;
|
||||
}
|
||||
|
||||
export interface KpiNetworkData {
|
||||
networkEvents?: number | null;
|
||||
|
||||
uniqueFlowId?: number | null;
|
||||
|
||||
activeAgents?: number | null;
|
||||
|
||||
uniqueSourcePrivateIps?: number | null;
|
||||
|
||||
uniqueDestinationPrivateIps?: number | null;
|
||||
|
||||
dnsQueries?: number | null;
|
||||
|
||||
tlsHandshakes?: number | null;
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// InputTypes
|
||||
// ====================================================
|
||||
|
@ -986,6 +1022,20 @@ export interface HostsSourceArgs {
|
|||
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
export interface IpOverviewSourceArgs {
|
||||
id?: string | null;
|
||||
|
||||
filterQuery?: string | null;
|
||||
|
||||
ip: string;
|
||||
}
|
||||
export interface KpiNetworkSourceArgs {
|
||||
id?: string | null;
|
||||
|
||||
timerange: TimerangeInput;
|
||||
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
export interface NetworkTopNFlowSourceArgs {
|
||||
direction: NetworkTopNFlowDirection;
|
||||
|
||||
|
@ -1021,13 +1071,6 @@ export interface UncommonProcessesSourceArgs {
|
|||
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
export interface KpiNetworkSourceArgs {
|
||||
id?: string | null;
|
||||
|
||||
timerange: TimerangeInput;
|
||||
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
export interface IndexFieldsSourceStatusArgs {
|
||||
indexTypes?: IndexType[] | null;
|
||||
}
|
||||
|
@ -1085,6 +1128,11 @@ export enum NetworkDnsFields {
|
|||
dnsBytesOut = 'dnsBytesOut',
|
||||
}
|
||||
|
||||
export enum IpOverviewType {
|
||||
destination = 'destination',
|
||||
source = 'source',
|
||||
}
|
||||
|
||||
// ====================================================
|
||||
// END: Typescript template
|
||||
// ====================================================
|
||||
|
@ -1137,6 +1185,10 @@ export namespace SourceResolvers {
|
|||
TimelineDetails?: TimelineDetailsResolver<TimelineDetailsData, TypeParent, Context>;
|
||||
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
|
||||
Hosts?: HostsResolver<HostsData, TypeParent, Context>;
|
||||
|
||||
IpOverview?: IpOverviewResolver<IpOverviewData | null, TypeParent, Context>;
|
||||
|
||||
KpiNetwork?: KpiNetworkResolver<KpiNetworkData | null, TypeParent, Context>;
|
||||
/** Gets Hosts based on timerange and specified criteria, or all events in the timerange if no criteria is specified */
|
||||
NetworkTopNFlow?: NetworkTopNFlowResolver<NetworkTopNFlowData, TypeParent, Context>;
|
||||
|
||||
|
@ -1145,8 +1197,6 @@ export namespace SourceResolvers {
|
|||
UncommonProcesses?: UncommonProcessesResolver<UncommonProcessesData, TypeParent, Context>;
|
||||
/** Just a simple example to get the app name */
|
||||
whoAmI?: WhoAmIResolver<SayMyName | null, TypeParent, Context>;
|
||||
|
||||
KpiNetwork?: KpiNetworkResolver<KpiNetworkData | null, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type IdResolver<R = string, Parent = Source, Context = SecOpsContext> = Resolver<
|
||||
|
@ -1237,6 +1287,32 @@ export namespace SourceResolvers {
|
|||
filterQuery?: string | null;
|
||||
}
|
||||
|
||||
export type IpOverviewResolver<
|
||||
R = IpOverviewData | null,
|
||||
Parent = Source,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context, IpOverviewArgs>;
|
||||
export interface IpOverviewArgs {
|
||||
id?: string | null;
|
||||
|
||||
filterQuery?: string | null;
|
||||
|
||||
ip: string;
|
||||
}
|
||||
|
||||
export type KpiNetworkResolver<
|
||||
R = KpiNetworkData | null,
|
||||
Parent = Source,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context, KpiNetworkArgs>;
|
||||
export interface KpiNetworkArgs {
|
||||
id?: string | null;
|
||||
|
||||
timerange: TimerangeInput;
|
||||
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
|
||||
export type NetworkTopNFlowResolver<
|
||||
R = NetworkTopNFlowData,
|
||||
Parent = Source,
|
||||
|
@ -1295,18 +1371,6 @@ export namespace SourceResolvers {
|
|||
Parent = Source,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type KpiNetworkResolver<
|
||||
R = KpiNetworkData | null,
|
||||
Parent = Source,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context, KpiNetworkArgs>;
|
||||
export interface KpiNetworkArgs {
|
||||
id?: string | null;
|
||||
|
||||
timerange: TimerangeInput;
|
||||
|
||||
filterQuery?: string | null;
|
||||
}
|
||||
}
|
||||
/** A set of configuration options for a security data source */
|
||||
export namespace SourceConfigurationResolvers {
|
||||
|
@ -1746,25 +1810,27 @@ export namespace SourceEcsFieldsResolvers {
|
|||
|
||||
export namespace GeoEcsFieldsResolvers {
|
||||
export interface Resolvers<Context = SecOpsContext, TypeParent = GeoEcsFields> {
|
||||
continent_name?: ContinentNameResolver<string | null, TypeParent, Context>;
|
||||
city_name?: CityNameResolver<string | null, TypeParent, Context>;
|
||||
|
||||
country_name?: CountryNameResolver<string | null, TypeParent, Context>;
|
||||
continent_name?: ContinentNameResolver<string | null, TypeParent, Context>;
|
||||
|
||||
country_iso_code?: CountryIsoCodeResolver<string | null, TypeParent, Context>;
|
||||
|
||||
city_name?: CityNameResolver<string | null, TypeParent, Context>;
|
||||
country_name?: CountryNameResolver<string | null, TypeParent, Context>;
|
||||
|
||||
location?: LocationResolver<Location | null, TypeParent, Context>;
|
||||
|
||||
region_iso_code?: RegionIsoCodeResolver<string | null, TypeParent, Context>;
|
||||
|
||||
region_name?: RegionNameResolver<string | null, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type ContinentNameResolver<
|
||||
export type CityNameResolver<
|
||||
R = string | null,
|
||||
Parent = GeoEcsFields,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type CountryNameResolver<
|
||||
export type ContinentNameResolver<
|
||||
R = string | null,
|
||||
Parent = GeoEcsFields,
|
||||
Context = SecOpsContext
|
||||
|
@ -1774,11 +1840,16 @@ export namespace GeoEcsFieldsResolvers {
|
|||
Parent = GeoEcsFields,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type CityNameResolver<
|
||||
export type CountryNameResolver<
|
||||
R = string | null,
|
||||
Parent = GeoEcsFields,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type LocationResolver<
|
||||
R = Location | null,
|
||||
Parent = GeoEcsFields,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type RegionIsoCodeResolver<
|
||||
R = string | null,
|
||||
Parent = GeoEcsFields,
|
||||
|
@ -1791,6 +1862,25 @@ export namespace GeoEcsFieldsResolvers {
|
|||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace LocationResolvers {
|
||||
export interface Resolvers<Context = SecOpsContext, TypeParent = Location> {
|
||||
lon?: LonResolver<number | null, TypeParent, Context>;
|
||||
|
||||
lat?: LatResolver<number | null, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type LonResolver<R = number | null, Parent = Location, Context = SecOpsContext> = Resolver<
|
||||
R,
|
||||
Parent,
|
||||
Context
|
||||
>;
|
||||
export type LatResolver<R = number | null, Parent = Location, Context = SecOpsContext> = Resolver<
|
||||
R,
|
||||
Parent,
|
||||
Context
|
||||
>;
|
||||
}
|
||||
|
||||
export namespace HostEcsFieldsResolvers {
|
||||
export interface Resolvers<Context = SecOpsContext, TypeParent = HostEcsFields> {
|
||||
architecture?: ArchitectureResolver<string | null, TypeParent, Context>;
|
||||
|
@ -3586,6 +3676,149 @@ export namespace HostItemResolvers {
|
|||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace IpOverviewDataResolvers {
|
||||
export interface Resolvers<Context = SecOpsContext, TypeParent = IpOverviewData> {
|
||||
source?: SourceResolver<Overview | null, TypeParent, Context>;
|
||||
|
||||
destination?: DestinationResolver<Overview | null, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type SourceResolver<
|
||||
R = Overview | null,
|
||||
Parent = IpOverviewData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type DestinationResolver<
|
||||
R = Overview | null,
|
||||
Parent = IpOverviewData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace OverviewResolvers {
|
||||
export interface Resolvers<Context = SecOpsContext, TypeParent = Overview> {
|
||||
firstSeen?: FirstSeenResolver<Date | null, TypeParent, Context>;
|
||||
|
||||
lastSeen?: LastSeenResolver<Date | null, TypeParent, Context>;
|
||||
|
||||
autonomousSystem?: AutonomousSystemResolver<AutonomousSystem, TypeParent, Context>;
|
||||
|
||||
host?: HostResolver<HostEcsFields, TypeParent, Context>;
|
||||
|
||||
geo?: GeoResolver<GeoEcsFields, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type FirstSeenResolver<
|
||||
R = Date | null,
|
||||
Parent = Overview,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type LastSeenResolver<
|
||||
R = Date | null,
|
||||
Parent = Overview,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type AutonomousSystemResolver<
|
||||
R = AutonomousSystem,
|
||||
Parent = Overview,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type HostResolver<
|
||||
R = HostEcsFields,
|
||||
Parent = Overview,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type GeoResolver<R = GeoEcsFields, Parent = Overview, Context = SecOpsContext> = Resolver<
|
||||
R,
|
||||
Parent,
|
||||
Context
|
||||
>;
|
||||
}
|
||||
|
||||
export namespace AutonomousSystemResolvers {
|
||||
export interface Resolvers<Context = SecOpsContext, TypeParent = AutonomousSystem> {
|
||||
as_org?: AsOrgResolver<string | null, TypeParent, Context>;
|
||||
|
||||
asn?: AsnResolver<string | null, TypeParent, Context>;
|
||||
|
||||
ip?: IpResolver<string | null, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type AsOrgResolver<
|
||||
R = string | null,
|
||||
Parent = AutonomousSystem,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type AsnResolver<
|
||||
R = string | null,
|
||||
Parent = AutonomousSystem,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type IpResolver<
|
||||
R = string | null,
|
||||
Parent = AutonomousSystem,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace KpiNetworkDataResolvers {
|
||||
export interface Resolvers<Context = SecOpsContext, TypeParent = KpiNetworkData> {
|
||||
networkEvents?: NetworkEventsResolver<number | null, TypeParent, Context>;
|
||||
|
||||
uniqueFlowId?: UniqueFlowIdResolver<number | null, TypeParent, Context>;
|
||||
|
||||
activeAgents?: ActiveAgentsResolver<number | null, TypeParent, Context>;
|
||||
|
||||
uniqueSourcePrivateIps?: UniqueSourcePrivateIpsResolver<number | null, TypeParent, Context>;
|
||||
|
||||
uniqueDestinationPrivateIps?: UniqueDestinationPrivateIpsResolver<
|
||||
number | null,
|
||||
TypeParent,
|
||||
Context
|
||||
>;
|
||||
|
||||
dnsQueries?: DnsQueriesResolver<number | null, TypeParent, Context>;
|
||||
|
||||
tlsHandshakes?: TlsHandshakesResolver<number | null, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type NetworkEventsResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type UniqueFlowIdResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type ActiveAgentsResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type UniqueSourcePrivateIpsResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type UniqueDestinationPrivateIpsResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type DnsQueriesResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type TlsHandshakesResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
||||
export namespace NetworkTopNFlowDataResolvers {
|
||||
export interface Resolvers<Context = SecOpsContext, TypeParent = NetworkTopNFlowData> {
|
||||
edges?: EdgesResolver<NetworkTopNFlowEdges[], TypeParent, Context>;
|
||||
|
@ -3940,61 +4173,3 @@ export namespace SayMyNameResolvers {
|
|||
Context
|
||||
>;
|
||||
}
|
||||
|
||||
export namespace KpiNetworkDataResolvers {
|
||||
export interface Resolvers<Context = SecOpsContext, TypeParent = KpiNetworkData> {
|
||||
networkEvents?: NetworkEventsResolver<number | null, TypeParent, Context>;
|
||||
|
||||
uniqueFlowId?: UniqueFlowIdResolver<number | null, TypeParent, Context>;
|
||||
|
||||
activeAgents?: ActiveAgentsResolver<number | null, TypeParent, Context>;
|
||||
|
||||
uniqueSourcePrivateIps?: UniqueSourcePrivateIpsResolver<number | null, TypeParent, Context>;
|
||||
|
||||
uniqueDestinationPrivateIps?: UniqueDestinationPrivateIpsResolver<
|
||||
number | null,
|
||||
TypeParent,
|
||||
Context
|
||||
>;
|
||||
|
||||
dnsQueries?: DnsQueriesResolver<number | null, TypeParent, Context>;
|
||||
|
||||
tlsHandshakes?: TlsHandshakesResolver<number | null, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type NetworkEventsResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type UniqueFlowIdResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type ActiveAgentsResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type UniqueSourcePrivateIpsResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type UniqueDestinationPrivateIpsResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type DnsQueriesResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type TlsHandshakesResolver<
|
||||
R = number | null,
|
||||
Parent = KpiNetworkData,
|
||||
Context = SecOpsContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import { createAuthenticationsResolvers } from './graphql/authentications';
|
|||
import { createScalarToStringArrayValueResolvers } from './graphql/ecs';
|
||||
import { createEsValueResolvers, createEventsResolvers } from './graphql/events';
|
||||
import { createHostsResolvers } from './graphql/hosts';
|
||||
import { createIpOverviewResolvers } from './graphql/ip_overview';
|
||||
import { createKpiNetworkResolvers } from './graphql/kpi_network';
|
||||
import { createNetworkResolvers } from './graphql/network';
|
||||
import { createScalarDateResolvers } from './graphql/scalar_date';
|
||||
|
@ -33,6 +34,7 @@ export const initServer = (libs: AppBackendLibs, config: Config) => {
|
|||
createEsValueResolvers() as IResolvers,
|
||||
createEventsResolvers(libs) as IResolvers,
|
||||
createHostsResolvers(libs) as IResolvers,
|
||||
createIpOverviewResolvers(libs) as IResolvers,
|
||||
createSourcesResolvers(libs) as IResolvers,
|
||||
createScalarToStringArrayValueResolvers() as IResolvers,
|
||||
createNetworkResolvers(libs) as IResolvers,
|
||||
|
|
|
@ -13,6 +13,7 @@ import { ElasticsearchEventsAdapter, Events } from '../events';
|
|||
import { KibanaBackendFrameworkAdapter } from '../framework/kibana_framework_adapter';
|
||||
import { ElasticsearchHostsAdapter, Hosts } from '../hosts';
|
||||
import { ElasticsearchIndexFieldAdapter, IndexFields } from '../index_fields';
|
||||
import { ElasticsearchIpOverviewAdapter, IpOverview } from '../ip_overview';
|
||||
import { KpiNetwork } from '../kpi_network';
|
||||
import { ElasticsearchKpiNetworkAdapter } from '../kpi_network/elasticsearch_adapter';
|
||||
import { ElasticsearchNetworkAdapter, Network } from '../network';
|
||||
|
@ -32,9 +33,10 @@ export function compose(server: Server): AppBackendLibs {
|
|||
events: new Events(new ElasticsearchEventsAdapter(framework)),
|
||||
fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework), sources),
|
||||
hosts: new Hosts(new ElasticsearchHostsAdapter(framework)),
|
||||
ipOverview: new IpOverview(new ElasticsearchIpOverviewAdapter(framework)),
|
||||
kpiNetwork: new KpiNetwork(new ElasticsearchKpiNetworkAdapter(framework)),
|
||||
network: new Network(new ElasticsearchNetworkAdapter(framework)),
|
||||
uncommonProcesses: new UncommonProcesses(new ElasticsearchUncommonProcessesAdapter(framework)),
|
||||
kpiNetwork: new KpiNetwork(new ElasticsearchKpiNetworkAdapter(framework)),
|
||||
};
|
||||
|
||||
const libs: AppBackendLibs = {
|
||||
|
|
|
@ -0,0 +1,333 @@
|
|||
/*
|
||||
* 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 { IpOverviewType } from '../../graphql/types';
|
||||
|
||||
import { getIpOverviewAgg } from './elasticsearch_adapter';
|
||||
import { IpOverviewHit } from './types';
|
||||
|
||||
describe('elasticsearch_adapter', () => {
|
||||
describe('#getIpOverview', () => {
|
||||
const responseAggs: IpOverviewHit = {
|
||||
aggregations: {
|
||||
destination: {
|
||||
doc_count: 882307,
|
||||
geo: {
|
||||
doc_count: 62089,
|
||||
results: {
|
||||
hits: {
|
||||
total: {
|
||||
value: 62089,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: null,
|
||||
hits: [
|
||||
{
|
||||
_source: {
|
||||
destination: {
|
||||
geo: {
|
||||
continent_name: 'Asia',
|
||||
region_iso_code: 'IN-KA',
|
||||
city_name: 'Bengaluru',
|
||||
country_iso_code: 'IN',
|
||||
region_name: 'Karnataka',
|
||||
location: {
|
||||
lon: 77.5833,
|
||||
lat: 12.9833,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: [1553894176003],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
lastSeen: {
|
||||
value: 1553900180003,
|
||||
value_as_string: '2019-03-29T22:56:20.003Z',
|
||||
},
|
||||
firstSeen: {
|
||||
value: 1551388820000,
|
||||
value_as_string: '2019-02-28T21:20:20.000Z',
|
||||
},
|
||||
host: {
|
||||
doc_count: 882307,
|
||||
results: {
|
||||
hits: {
|
||||
total: {
|
||||
value: 882307,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: null,
|
||||
hits: [
|
||||
{
|
||||
_index: 'packetbeat-8.0.0-2019.02.19-000001',
|
||||
_type: '_doc',
|
||||
_id: 'vX5Py2kBCQofM5eX2OEu',
|
||||
_score: null,
|
||||
_source: {
|
||||
host: {
|
||||
hostname: 'suricata-bangalore',
|
||||
os: {
|
||||
kernel: '4.15.0-45-generic',
|
||||
codename: 'bionic',
|
||||
name: 'Ubuntu',
|
||||
family: 'debian',
|
||||
version: '18.04.2 LTS (Bionic Beaver)',
|
||||
platform: 'ubuntu',
|
||||
},
|
||||
containerized: false,
|
||||
ip: ['139.59.11.147', '10.47.0.5', 'fe80::ec0b:1bff:fe29:80bd'],
|
||||
name: 'suricata-bangalore',
|
||||
id: '0a63559c1acf4c419d979c4b4d8b83ff',
|
||||
mac: ['ee:0b:1b:29:80:bd'],
|
||||
architecture: 'x86_64',
|
||||
},
|
||||
},
|
||||
sort: [1553894200003],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
autonomous_system: {
|
||||
doc_count: 0,
|
||||
results: {
|
||||
hits: {
|
||||
total: {
|
||||
value: 0,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: null,
|
||||
hits: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
source: {
|
||||
doc_count: 1002234,
|
||||
geo: {
|
||||
doc_count: 1507,
|
||||
results: {
|
||||
hits: {
|
||||
total: {
|
||||
value: 1507,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: null,
|
||||
hits: [
|
||||
{
|
||||
_index: 'filebeat-8.0.0-2019.03.21-000002',
|
||||
_type: '_doc',
|
||||
_id: 'dHQ6y2kBCQofM5eXi5OE',
|
||||
_score: null,
|
||||
_source: {
|
||||
source: {
|
||||
geo: {
|
||||
continent_name: 'Asia',
|
||||
region_iso_code: 'IN-KA',
|
||||
city_name: 'Bengaluru',
|
||||
country_iso_code: 'IN',
|
||||
region_name: 'Karnataka',
|
||||
location: {
|
||||
lon: 77.5833,
|
||||
lat: 12.9833,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
sort: [1553892804003],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
lastSeen: {
|
||||
value: 1553900180003,
|
||||
value_as_string: '2019-03-29T22:56:20.003Z',
|
||||
},
|
||||
firstSeen: {
|
||||
value: 1551388804322,
|
||||
value_as_string: '2019-02-28T21:20:04.322Z',
|
||||
},
|
||||
host: {
|
||||
doc_count: 1002234,
|
||||
results: {
|
||||
hits: {
|
||||
total: {
|
||||
value: 1002234,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: null,
|
||||
hits: [
|
||||
{
|
||||
_index: 'packetbeat-8.0.0-2019.02.19-000001',
|
||||
_type: '_doc',
|
||||
_id: 'vn5Py2kBCQofM5eX2OEu',
|
||||
_score: null,
|
||||
_source: {
|
||||
host: {
|
||||
hostname: 'suricata-bangalore',
|
||||
os: {
|
||||
kernel: '4.15.0-45-generic',
|
||||
codename: 'bionic',
|
||||
name: 'Ubuntu',
|
||||
family: 'debian',
|
||||
version: '18.04.2 LTS (Bionic Beaver)',
|
||||
platform: 'ubuntu',
|
||||
},
|
||||
ip: ['139.59.11.147', '10.47.0.5', 'fe80::ec0b:1bff:fe29:80bd'],
|
||||
containerized: false,
|
||||
name: 'suricata-bangalore',
|
||||
id: '0a63559c1acf4c419d979c4b4d8b83ff',
|
||||
mac: ['ee:0b:1b:29:80:bd'],
|
||||
architecture: 'x86_64',
|
||||
},
|
||||
},
|
||||
sort: [1553894200003],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
autonomous_system: {
|
||||
doc_count: 0,
|
||||
results: {
|
||||
hits: {
|
||||
total: {
|
||||
value: 0,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: null,
|
||||
hits: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
_shards: {
|
||||
total: 42,
|
||||
successful: 42,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
hits: {
|
||||
total: {
|
||||
value: 71358841,
|
||||
relation: 'eq',
|
||||
},
|
||||
max_score: null,
|
||||
hits: [],
|
||||
},
|
||||
took: 392,
|
||||
timeout: 500,
|
||||
};
|
||||
|
||||
const formattedDestination = {
|
||||
destination: {
|
||||
firstSeen: '2019-02-28T21:20:20.000Z',
|
||||
lastSeen: '2019-03-29T22:56:20.003Z',
|
||||
autonomousSystem: {},
|
||||
host: {
|
||||
hostname: 'suricata-bangalore',
|
||||
os: {
|
||||
kernel: '4.15.0-45-generic',
|
||||
codename: 'bionic',
|
||||
name: 'Ubuntu',
|
||||
family: 'debian',
|
||||
version: '18.04.2 LTS (Bionic Beaver)',
|
||||
platform: 'ubuntu',
|
||||
},
|
||||
containerized: false,
|
||||
ip: ['139.59.11.147', '10.47.0.5', 'fe80::ec0b:1bff:fe29:80bd'],
|
||||
name: 'suricata-bangalore',
|
||||
id: '0a63559c1acf4c419d979c4b4d8b83ff',
|
||||
mac: ['ee:0b:1b:29:80:bd'],
|
||||
architecture: 'x86_64',
|
||||
},
|
||||
geo: {
|
||||
continent_name: 'Asia',
|
||||
region_iso_code: 'IN-KA',
|
||||
city_name: 'Bengaluru',
|
||||
country_iso_code: 'IN',
|
||||
region_name: 'Karnataka',
|
||||
location: {
|
||||
lon: 77.5833,
|
||||
lat: 12.9833,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const formattedSource = {
|
||||
source: {
|
||||
firstSeen: '2019-02-28T21:20:04.322Z',
|
||||
lastSeen: '2019-03-29T22:56:20.003Z',
|
||||
autonomousSystem: {},
|
||||
host: {
|
||||
hostname: 'suricata-bangalore',
|
||||
os: {
|
||||
kernel: '4.15.0-45-generic',
|
||||
codename: 'bionic',
|
||||
name: 'Ubuntu',
|
||||
family: 'debian',
|
||||
version: '18.04.2 LTS (Bionic Beaver)',
|
||||
platform: 'ubuntu',
|
||||
},
|
||||
containerized: false,
|
||||
ip: ['139.59.11.147', '10.47.0.5', 'fe80::ec0b:1bff:fe29:80bd'],
|
||||
name: 'suricata-bangalore',
|
||||
id: '0a63559c1acf4c419d979c4b4d8b83ff',
|
||||
mac: ['ee:0b:1b:29:80:bd'],
|
||||
architecture: 'x86_64',
|
||||
},
|
||||
geo: {
|
||||
continent_name: 'Asia',
|
||||
region_iso_code: 'IN-KA',
|
||||
city_name: 'Bengaluru',
|
||||
country_iso_code: 'IN',
|
||||
region_name: 'Karnataka',
|
||||
location: {
|
||||
lon: 77.5833,
|
||||
lat: 12.9833,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const formattedEmptySource = {
|
||||
source: {
|
||||
firstSeen: null,
|
||||
lastSeen: null,
|
||||
autonomousSystem: {},
|
||||
host: {},
|
||||
geo: {},
|
||||
},
|
||||
};
|
||||
|
||||
test('will return a destination correctly', () => {
|
||||
const destination = getIpOverviewAgg(
|
||||
IpOverviewType.destination,
|
||||
responseAggs.aggregations.destination!
|
||||
);
|
||||
expect(destination).toEqual(formattedDestination);
|
||||
});
|
||||
|
||||
test('will return a source correctly', () => {
|
||||
const destination = getIpOverviewAgg(
|
||||
IpOverviewType.source,
|
||||
responseAggs.aggregations.source!
|
||||
);
|
||||
expect(destination).toEqual(formattedSource);
|
||||
});
|
||||
|
||||
test('will return an empty source correctly', () => {
|
||||
const destination = getIpOverviewAgg(IpOverviewType.source, {});
|
||||
expect(destination).toEqual(formattedEmptySource);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { getOr } from 'lodash/fp';
|
||||
|
||||
import { AutonomousSystem, GeoEcsFields, HostEcsFields, IpOverviewData } from '../../graphql/types';
|
||||
import { FrameworkAdapter, FrameworkRequest } from '../framework';
|
||||
import { TermAggregation } from '../types';
|
||||
|
||||
import { IpOverviewRequestOptions } from './index';
|
||||
import { buildQuery } from './query.dsl';
|
||||
import { IpOverviewAdapter, IpOverviewHit, OverviewHit } from './types';
|
||||
|
||||
export class ElasticsearchIpOverviewAdapter implements IpOverviewAdapter {
|
||||
constructor(private readonly framework: FrameworkAdapter) {}
|
||||
|
||||
public async getIpOverview(
|
||||
request: FrameworkRequest,
|
||||
options: IpOverviewRequestOptions
|
||||
): Promise<IpOverviewData> {
|
||||
const response = await this.framework.callWithRequest<IpOverviewHit, TermAggregation>(
|
||||
request,
|
||||
'search',
|
||||
buildQuery(options)
|
||||
);
|
||||
|
||||
return {
|
||||
...getIpOverviewAgg('source', getOr({}, 'aggregations.source', response)),
|
||||
...getIpOverviewAgg('destination', getOr({}, 'aggregations.destination', response)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const getIpOverviewAgg = (type: string, overviewHit: OverviewHit | {}) => {
|
||||
const firstSeen = getOr(null, `firstSeen.value_as_string`, overviewHit);
|
||||
const lastSeen = getOr(null, `lastSeen.value_as_string`, overviewHit);
|
||||
|
||||
const autonomousSystem: AutonomousSystem | null = getOr(
|
||||
null,
|
||||
`autonomousSystem.results.hits.hits[0]._source.autonomous_system`,
|
||||
overviewHit
|
||||
);
|
||||
const geoFields: GeoEcsFields | null = getOr(
|
||||
null,
|
||||
`geo.results.hits.hits[0]._source.${type}.geo`,
|
||||
overviewHit
|
||||
);
|
||||
const hostFields: HostEcsFields | null = getOr(
|
||||
null,
|
||||
`host.results.hits.hits[0]._source.host`,
|
||||
overviewHit
|
||||
);
|
||||
|
||||
return {
|
||||
[type]: {
|
||||
firstSeen,
|
||||
lastSeen,
|
||||
autonomousSystem: {
|
||||
...autonomousSystem,
|
||||
},
|
||||
host: {
|
||||
...hostFields,
|
||||
},
|
||||
geo: {
|
||||
...geoFields,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
27
x-pack/plugins/secops/server/lib/ip_overview/index.ts
Normal file
27
x-pack/plugins/secops/server/lib/ip_overview/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { IpOverviewData } from '../../graphql/types';
|
||||
import { FrameworkRequest, RequestOptions } from '../framework';
|
||||
|
||||
import { IpOverviewAdapter } from './types';
|
||||
|
||||
export * from './elasticsearch_adapter';
|
||||
|
||||
export interface IpOverviewRequestOptions extends RequestOptions {
|
||||
ip: string;
|
||||
}
|
||||
|
||||
export class IpOverview {
|
||||
constructor(private readonly adapter: IpOverviewAdapter) {}
|
||||
|
||||
public async getIpOverview(
|
||||
req: FrameworkRequest,
|
||||
options: IpOverviewRequestOptions
|
||||
): Promise<IpOverviewData> {
|
||||
return await this.adapter.getIpOverview(req, options);
|
||||
}
|
||||
}
|
122
x-pack/plugins/secops/server/lib/ip_overview/query.dsl.ts
Normal file
122
x-pack/plugins/secops/server/lib/ip_overview/query.dsl.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* 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 { IpOverviewRequestOptions } from './index';
|
||||
|
||||
const getAggs = (type: string, ip: string) => {
|
||||
return {
|
||||
[type]: {
|
||||
filter: {
|
||||
term: {
|
||||
[`${type}.ip`]: ip,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
firstSeen: {
|
||||
min: {
|
||||
field: '@timestamp',
|
||||
},
|
||||
},
|
||||
lastSeen: {
|
||||
max: {
|
||||
field: '@timestamp',
|
||||
},
|
||||
},
|
||||
autonomous_system: {
|
||||
filter: {
|
||||
exists: {
|
||||
field: 'autonomous_system',
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
results: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
_source: ['autonomous_system'],
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': 'desc',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
host: {
|
||||
filter: {
|
||||
exists: {
|
||||
field: 'host',
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
results: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
_source: ['host'],
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': 'desc',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
geo: {
|
||||
filter: {
|
||||
exists: {
|
||||
field: `${type}.geo`,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
results: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
_source: [`${type}.geo`],
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': 'desc',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const buildQuery = ({
|
||||
filterQuery,
|
||||
sourceConfiguration: {
|
||||
fields: { timestamp },
|
||||
logAlias,
|
||||
packetbeatAlias,
|
||||
},
|
||||
ip,
|
||||
}: IpOverviewRequestOptions) => {
|
||||
const dslQuery = {
|
||||
allowNoIndices: true,
|
||||
index: [logAlias, packetbeatAlias],
|
||||
ignoreUnavailable: true,
|
||||
body: {
|
||||
aggs: {
|
||||
...getAggs('source', ip),
|
||||
...getAggs('destination', ip),
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
should: [],
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
track_total_hits: true,
|
||||
},
|
||||
};
|
||||
|
||||
return dslQuery;
|
||||
};
|
78
x-pack/plugins/secops/server/lib/ip_overview/types.ts
Normal file
78
x-pack/plugins/secops/server/lib/ip_overview/types.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 { IpOverviewData } from '../../graphql/types';
|
||||
import { FrameworkRequest, RequestBasicOptions } from '../framework';
|
||||
import { Hit, ShardsResponse, TotalValue } from '../types';
|
||||
|
||||
export interface IpOverviewAdapter {
|
||||
getIpOverview(request: FrameworkRequest, options: RequestBasicOptions): Promise<IpOverviewData>;
|
||||
}
|
||||
|
||||
interface ResultHit<T> {
|
||||
doc_count: number;
|
||||
results: {
|
||||
hits: {
|
||||
total: TotalValue | number;
|
||||
max_score: number | null;
|
||||
hits: Array<{
|
||||
_source: T;
|
||||
sort?: [number];
|
||||
_index?: string;
|
||||
_type?: string;
|
||||
_id?: string;
|
||||
_score?: number | null;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface OverviewHit {
|
||||
took?: number;
|
||||
timed_out?: boolean;
|
||||
_scroll_id?: string;
|
||||
_shards?: ShardsResponse;
|
||||
timeout?: number;
|
||||
hits?: {
|
||||
total: number;
|
||||
hits: Hit[];
|
||||
};
|
||||
doc_count: number;
|
||||
geo: ResultHit<object>;
|
||||
host: ResultHit<object>;
|
||||
autonomous_system: ResultHit<object>;
|
||||
firstSeen: {
|
||||
value: number;
|
||||
value_as_string: string;
|
||||
};
|
||||
lastSeen: {
|
||||
value: number;
|
||||
value_as_string: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IpOverviewHit {
|
||||
aggregations: {
|
||||
destination?: OverviewHit;
|
||||
source?: OverviewHit;
|
||||
};
|
||||
_shards: {
|
||||
total: number;
|
||||
successful: number;
|
||||
skipped: number;
|
||||
failed: number;
|
||||
};
|
||||
hits: {
|
||||
total: {
|
||||
value: number;
|
||||
relation: string;
|
||||
};
|
||||
max_score: number | null;
|
||||
hits: [];
|
||||
};
|
||||
took: number;
|
||||
timeout: number;
|
||||
}
|
|
@ -10,6 +10,7 @@ import { Events } from './events';
|
|||
import { FrameworkAdapter, FrameworkRequest } from './framework';
|
||||
import { Hosts } from './hosts';
|
||||
import { IndexFields } from './index_fields';
|
||||
import { IpOverview } from './ip_overview';
|
||||
import { KpiNetwork } from './kpi_network';
|
||||
import { Network } from './network';
|
||||
import { SourceStatus } from './source_status';
|
||||
|
@ -23,6 +24,7 @@ export interface AppDomainLibs {
|
|||
events: Events;
|
||||
fields: IndexFields;
|
||||
hosts: Hosts;
|
||||
ipOverview: IpOverview;
|
||||
network: Network;
|
||||
kpiNetwork: KpiNetwork;
|
||||
uncommonProcesses: UncommonProcesses;
|
||||
|
@ -48,7 +50,7 @@ export interface SecOpsContext {
|
|||
req: FrameworkRequest;
|
||||
}
|
||||
|
||||
interface TotalValue {
|
||||
export interface TotalValue {
|
||||
value: number;
|
||||
relation: string;
|
||||
}
|
||||
|
|
66
x-pack/test/api_integration/apis/secops/ip_overview.ts
Normal file
66
x-pack/test/api_integration/apis/secops/ip_overview.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import { ipOverviewQuery } from '../../../../plugins/secops/public/containers/ip_overview/index.gql_query';
|
||||
import { GetIpOverviewQuery } from '../../../../plugins/secops/public/graphql/types';
|
||||
import { KbnTestProvider } from './types';
|
||||
|
||||
const ipOverviewTests: KbnTestProvider = ({ getService }) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const client = getService('secOpsGraphQLClient');
|
||||
describe('IP Overview', () => {
|
||||
describe('With filebeat', () => {
|
||||
before(() => esArchiver.load('filebeat/default'));
|
||||
after(() => esArchiver.unload('filebeat/default'));
|
||||
|
||||
it('Make sure that we get KpiNetwork data', () => {
|
||||
return client
|
||||
.query<GetIpOverviewQuery.Query>({
|
||||
query: ipOverviewQuery,
|
||||
variables: {
|
||||
sourceId: 'default',
|
||||
ip: '151.205.0.17',
|
||||
},
|
||||
})
|
||||
.then(resp => {
|
||||
const ipOverview = resp.data.source.IpOverview;
|
||||
expect(ipOverview!.source!.geo!.continent_name).to.be('North America');
|
||||
expect(ipOverview!.source!.geo!.location!.lat!).to.be(37.751);
|
||||
expect(ipOverview!.source!.host!.os!.platform!).to.be('raspbian');
|
||||
expect(ipOverview!.destination!.geo!.continent_name).to.be('North America');
|
||||
expect(ipOverview!.destination!.geo!.location!.lat!).to.be(37.751);
|
||||
expect(ipOverview!.destination!.host!.os!.platform!).to.be('raspbian');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('With packetbeat', () => {
|
||||
before(() => esArchiver.load('packetbeat/default'));
|
||||
after(() => esArchiver.unload('packetbeat/default'));
|
||||
|
||||
it('Make sure that we get KpiNetwork data', () => {
|
||||
return client
|
||||
.query<GetIpOverviewQuery.Query>({
|
||||
query: ipOverviewQuery,
|
||||
variables: {
|
||||
sourceId: 'default',
|
||||
ip: '185.53.91.88',
|
||||
},
|
||||
})
|
||||
.then(resp => {
|
||||
const ipOverview = resp.data.source.IpOverview;
|
||||
expect(ipOverview!.destination!.host!.id!).to.be('2ce8b1e7d69e4a1d9c6bcddc473da9d9');
|
||||
expect(ipOverview!.destination!.host!.name!).to.be('zeek-sensor-amsterdam');
|
||||
expect(ipOverview!.destination!.host!.os!.platform!).to.be('ubuntu');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// tslint:disable-next-line no-default-export
|
||||
export default ipOverviewTests;
|
Loading…
Add table
Add a link
Reference in a new issue