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




![image](https://user-images.githubusercontent.com/2946766/55267152-2268e980-5246-11e9-9c3e-592cb5c211d2.png)
This commit is contained in:
Garrett Spong 2019-04-02 11:34:51 -06:00 committed by GitHub
parent d82b6e9427
commit 41c2ab38a3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
49 changed files with 4295 additions and 303 deletions

View file

@ -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"
);
});
});
});

View file

@ -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>
)
);

View file

@ -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';

View file

@ -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>
`;

View file

@ -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>
`;

View file

@ -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"
/>
`;

View file

@ -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();
});
});
});

View file

@ -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 />
</>
);

View file

@ -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');
});
});
});

View file

@ -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);

View file

@ -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',
},
},
},
};

View file

@ -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']);
});
});
});

View file

@ -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}
/>
));

View file

@ -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',
}
);

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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>

View file

@ -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
}
}
}
}
}
`;

View file

@ -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>
)
);

View file

@ -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;

View file

@ -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;
};

View file

@ -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;
};

View file

@ -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": [

View file

@ -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;

View file

@ -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: {

View file

@ -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) || '',
});
};

View file

@ -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;
};

View file

@ -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');

View file

@ -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;
}

View file

@ -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();

View file

@ -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
);

View file

@ -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
}

View file

@ -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

View file

@ -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';

View file

@ -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: [],
},
],
},
};

View file

@ -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));
});
});

View file

@ -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);
},
},
});

View file

@ -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
}
`;

View 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);
});
});
});

View file

@ -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>;
}

View file

@ -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,

View file

@ -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 = {

View file

@ -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);
});
});
});

View file

@ -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,
},
},
};
};

View 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);
}
}

View 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;
};

View 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;
}

View file

@ -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;
}

View 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;