add query to check auditbeat indexes/indices/alias + indexfield query… (#26234)

* add query to check auditbeat indexes/indices/alias + indexfield query for KQL

* Add api integration test for secops with esArchiver

* restructure esArchiver folder for auditbeat

* mock is not a page

* add EmptyPage component + container to detect if audibeat indexes is set up or not + test

* fix type issues

* review re-working

* remove any for ALiases

* remove duplicate interface

* resolve PR review

* remove flicky transistion between data and no data
This commit is contained in:
Xavier Mouligneau 2018-11-28 16:00:14 -05:00 committed by Andrew Goldstein
parent 6bdcd88a63
commit afe82e2054
No known key found for this signature in database
GPG key ID: 42995DC9117D52CE
59 changed files with 3125 additions and 117 deletions

View file

@ -255,6 +255,7 @@
"@types/bluebird": "^3.1.1",
"@types/boom": "^7.2.0",
"@types/chance": "^1.0.0",
"@types/cheerio": "^0.22.10",
"@types/classnames": "^2.2.3",
"@types/d3": "^3.5.41",
"@types/dedent": "^0.7.0",
@ -381,9 +382,9 @@
"sinon": "^5.0.7",
"strip-ansi": "^3.0.1",
"stylelint": "^9.7.1",
"stylelint-processor-styled-components": "^1.5.0",
"stylelint-config-styled-components": "^0.1.1",
"stylelint-config-standard": "^18.2.0",
"stylelint-config-styled-components": "^0.1.1",
"stylelint-processor-styled-components": "^1.5.0",
"supertest": "^3.1.0",
"supertest-as-promised": "^4.0.2",
"tree-kill": "^1.1.0",

View file

@ -97,6 +97,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "status",
"description": "The status of the source",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "OBJECT", "name": "SourceStatus", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "getEvents",
"description": "Gets Suricata events based on timerange and specified criteria, or all events in the timerange if no criteria is specified",
@ -142,7 +154,7 @@
"description": "A set of configuration options for a security data source",
"fields": [
{
"name": "fileAlias",
"name": "logAlias",
"description": "The alias to read file data from",
"args": [],
"type": {
@ -153,6 +165,18 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "auditbeatAlias",
"description": "The alias to read auditbeat data from",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "fields",
"description": "The field mapping to use for this source",
@ -272,6 +296,176 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SourceStatus",
"description": "The status of an infrastructure data source",
"fields": [
{
"name": "auditbeatAliasExists",
"description": "Whether the configured auditbeat alias exists",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "auditbeatIndicesExist",
"description": "Whether the configured alias or wildcard pattern resolve to any auditbeat indices",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "auditbeatIndices",
"description": "The list of indices in the auditbeat alias",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "indexFields",
"description": "The list of fields defined in the index mappings",
"args": [
{
"name": "indexType",
"description": "",
"type": { "kind": "ENUM", "name": "IndexType", "ofType": null },
"defaultValue": "ANY"
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "OBJECT", "name": "IndexField", "ofType": null }
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "Boolean",
"description": "The `Boolean` scalar type represents `true` or `false`.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "IndexType",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{ "name": "ANY", "description": "", "isDeprecated": false, "deprecationReason": null },
{ "name": "LOGS", "description": "", "isDeprecated": false, "deprecationReason": null },
{
"name": "AUDITBEAT",
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "IndexField",
"description": "A descriptor of a field in an index",
"fields": [
{
"name": "name",
"description": "The name of the field",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "type",
"description": "The type of the field's values as recognized by Kibana",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "String", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "searchable",
"description": "Whether the field's values can be efficiently searched for",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "aggregatable",
"description": "Whether the field's values can be aggregated",
"args": [],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": { "kind": "SCALAR", "name": "Boolean", "ofType": null }
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "TimerangeInput",
@ -1026,16 +1220,6 @@
],
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "Boolean",
"description": "The `Boolean` scalar type represents `true` or `false`.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "__Field",

View file

@ -0,0 +1,7 @@
/*
* 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 * from './schema.gql';

View file

@ -0,0 +1,15 @@
/*
* 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 sharedSchema = gql`
enum IndexType {
ANY
LOGS
AUDITBEAT
}
`;

View file

@ -37,12 +37,14 @@ export interface Query {
export interface Source {
id: string /** The id of the source */;
configuration: SourceConfiguration /** The raw configuration of the source */;
status: SourceStatus /** The status of the source */;
getEvents?: EventsData | null /** Gets Suricata events based on timerange and specified criteria, or all events in the timerange if no criteria is specified */;
whoAmI?: SayMyName | null /** Just a simple example to get the app name */;
}
/** A set of configuration options for a security data source */
export interface SourceConfiguration {
fileAlias: string /** The alias to read file data from */;
logAlias: string /** The alias to read file data from */;
auditbeatAlias: string /** The alias to read auditbeat data from */;
fields: SourceFields /** The field mapping to use for this source */;
}
/** A mapping of semantic fields to their document counterparts */
@ -54,6 +56,20 @@ export interface SourceFields {
tiebreaker: string /** The field to use as a tiebreaker for log events that have identical timestamps */;
timestamp: string /** The field to use as a timestamp for metrics and logs */;
}
/** The status of an infrastructure data source */
export interface SourceStatus {
auditbeatAliasExists: boolean /** Whether the configured auditbeat alias exists */;
auditbeatIndicesExist: boolean /** Whether the configured alias or wildcard pattern resolve to any auditbeat indices */;
auditbeatIndices: string[] /** The list of indices in the auditbeat alias */;
indexFields: IndexField[] /** The list of fields defined in the index mappings */;
}
/** A descriptor of a field in an index */
export interface IndexField {
name: string /** The name of the field */;
type: string /** The type of the field's values as recognized by Kibana */;
searchable: boolean /** Whether the field's values can be efficiently searched for */;
aggregatable: boolean /** Whether the field's values can be aggregated */;
}
export interface EventsData {
kpiEventType: KpiItem[];
@ -135,6 +151,15 @@ export interface GetEventsSourceArgs {
timerange: TimerangeInput;
filterQuery?: string | null;
}
export interface IndexFieldsSourceStatusArgs {
indexType?: IndexType | null;
}
export enum IndexType {
ANY = 'ANY',
LOGS = 'LOGS',
AUDITBEAT = 'AUDITBEAT',
}
export namespace QueryResolvers {
export interface Resolvers<Context = any> {
@ -171,6 +196,7 @@ export namespace SourceResolvers {
any,
Context
> /** The raw configuration of the source */;
status?: StatusResolver<SourceStatus, any, Context> /** The status of the source */;
getEvents?: GetEventsResolver<
EventsData | null,
any,
@ -189,6 +215,11 @@ export namespace SourceResolvers {
Parent = any,
Context = any
> = Resolver<R, Parent, Context>;
export type StatusResolver<R = SourceStatus, Parent = any, Context = any> = Resolver<
R,
Parent,
Context
>;
export type GetEventsResolver<R = EventsData | null, Parent = any, Context = any> = Resolver<
R,
Parent,
@ -209,7 +240,12 @@ export namespace SourceResolvers {
/** A set of configuration options for a security data source */
export namespace SourceConfigurationResolvers {
export interface Resolvers<Context = any> {
fileAlias?: FileAliasResolver<string, any, Context> /** The alias to read file data from */;
logAlias?: LogAliasResolver<string, any, Context> /** The alias to read file data from */;
auditbeatAlias?: AuditbeatAliasResolver<
string,
any,
Context
> /** The alias to read auditbeat data from */;
fields?: FieldsResolver<
SourceFields,
any,
@ -217,7 +253,12 @@ export namespace SourceConfigurationResolvers {
> /** The field mapping to use for this source */;
}
export type FileAliasResolver<R = string, Parent = any, Context = any> = Resolver<
export type LogAliasResolver<R = string, Parent = any, Context = any> = Resolver<
R,
Parent,
Context
>;
export type AuditbeatAliasResolver<R = string, Parent = any, Context = any> = Resolver<
R,
Parent,
Context
@ -274,6 +315,90 @@ export namespace SourceFieldsResolvers {
Context
>;
}
/** The status of an infrastructure data source */
export namespace SourceStatusResolvers {
export interface Resolvers<Context = any> {
auditbeatAliasExists?: AuditbeatAliasExistsResolver<
boolean,
any,
Context
> /** Whether the configured auditbeat alias exists */;
auditbeatIndicesExist?: AuditbeatIndicesExistResolver<
boolean,
any,
Context
> /** Whether the configured alias or wildcard pattern resolve to any auditbeat indices */;
auditbeatIndices?: AuditbeatIndicesResolver<
string[],
any,
Context
> /** The list of indices in the auditbeat alias */;
indexFields?: IndexFieldsResolver<
IndexField[],
any,
Context
> /** The list of fields defined in the index mappings */;
}
export type AuditbeatAliasExistsResolver<R = boolean, Parent = any, Context = any> = Resolver<
R,
Parent,
Context
>;
export type AuditbeatIndicesExistResolver<R = boolean, Parent = any, Context = any> = Resolver<
R,
Parent,
Context
>;
export type AuditbeatIndicesResolver<R = string[], Parent = any, Context = any> = Resolver<
R,
Parent,
Context
>;
export type IndexFieldsResolver<R = IndexField[], Parent = any, Context = any> = Resolver<
R,
Parent,
Context,
IndexFieldsArgs
>;
export interface IndexFieldsArgs {
indexType?: IndexType | null;
}
}
/** A descriptor of a field in an index */
export namespace IndexFieldResolvers {
export interface Resolvers<Context = any> {
name?: NameResolver<string, any, Context> /** The name of the field */;
type?: TypeResolver<
string,
any,
Context
> /** The type of the field's values as recognized by Kibana */;
searchable?: SearchableResolver<
boolean,
any,
Context
> /** Whether the field's values can be efficiently searched for */;
aggregatable?: AggregatableResolver<
boolean,
any,
Context
> /** Whether the field's values can be aggregated */;
}
export type NameResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
export type TypeResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
export type SearchableResolver<R = boolean, Parent = any, Context = any> = Resolver<
R,
Parent,
Context
>;
export type AggregatableResolver<R = boolean, Parent = any, Context = any> = Resolver<
R,
Parent,
Context
>;
}
export namespace EventsDataResolvers {
export interface Resolvers<Context = any> {
@ -624,6 +749,46 @@ export namespace GetEventsQuery {
};
}
export namespace SourceQuery {
export type Variables = {
sourceId?: string | null;
};
export type Query = {
__typename?: 'Query';
source: Source;
};
export type Source = {
__typename?: 'Source';
id: string;
configuration: Configuration;
status: Status;
};
export type Configuration = {
__typename?: 'SourceConfiguration';
auditbeatAlias: string;
logAlias: string;
};
export type Status = {
__typename?: 'SourceStatus';
auditbeatIndicesExist: boolean;
auditbeatAliasExists: boolean;
auditbeatIndices: string[];
indexFields: IndexFields[];
};
export type IndexFields = {
__typename?: 'IndexField';
name: string;
searchable: boolean;
type: string;
aggregatable: boolean;
};
}
export namespace WhoAmIQuery {
export type Variables = {
sourceId: string;

View file

@ -0,0 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders correctly 1`] = `
<Component
actionLabel="Do Something"
actionUrl="my/url/from/nowwhere"
message="My awesome message"
title="My Super Title"
/>
`;

View file

@ -0,0 +1,22 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import React from 'react';
import { EmptyPage } from './index';
it('renders correctly', () => {
const EmptyComponent = shallow(
<EmptyPage
title="My Super Title"
message="My awesome message"
actionLabel="Do Something"
actionUrl="my/url/from/nowwhere"
/>
);
expect(toJson(EmptyComponent)).toMatchSnapshot();
});

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 { EuiButton, EuiEmptyPrompt } from '@elastic/eui';
import React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
interface EmptyPageProps {
message: string;
title: string;
actionLabel: string;
actionUrl: string;
'data-test-subj'?: string;
}
export const EmptyPage = pure<EmptyPageProps>(
({ actionLabel, actionUrl, message, title, ...rest }) => (
<CenteredEmptyPrompt
title={<h2>{title}</h2>}
body={<p>{message}</p>}
actions={
<EuiButton href={actionUrl} color="primary" fill>
{actionLabel}
</EuiButton>
}
{...rest}
/>
)
);
const CenteredEmptyPrompt = styled(EuiEmptyPrompt)`
align-self: center;
`;

View file

@ -8,7 +8,7 @@ import { mount } from 'enzyme';
import { noop } from 'lodash/fp';
import * as React from 'react';
import { Body } from '.';
import { mockECSData } from '../../../pages/mock/mock_ecs';
import { mockECSData } from '../../../mock/mock_ecs';
import { headers } from './column_headers/headers';
import { columnRenderers, rowRenderers } from './renderers';
import { Sort } from './sort';

View file

@ -9,7 +9,7 @@ import { cloneDeep, omit } from 'lodash/fp';
import React from 'react';
import { EMPTY_VALUE, emptyColumnRenderer } from '.';
import { mockECSData } from '../../../../pages/mock/mock_ecs';
import { mockECSData } from '../../../../mock/mock_ecs';
import { ECS } from '../../ecs';
describe('empty_column_renderer', () => {

View file

@ -10,7 +10,7 @@ import React from 'react';
import { EMPTY_VALUE } from '.';
import { columnRenderers } from '.';
import { mockECSData } from '../../../../pages/mock/mock_ecs';
import { mockECSData } from '../../../../mock/mock_ecs';
import { ECS } from '../../ecs';
import { getColumnRenderer } from './get_column_renderer';

View file

@ -9,7 +9,7 @@ import React from 'react';
import { cloneDeep } from 'lodash';
import { rowRenderers } from '.';
import { mockECSData } from '../../../../pages/mock/mock_ecs';
import { mockECSData } from '../../../../mock/mock_ecs';
import { ECS } from '../../ecs';
import { getRowRenderer } from './get_row_renderer';

View file

@ -9,7 +9,7 @@ import { cloneDeep, omit } from 'lodash/fp';
import React from 'react';
import { EMPTY_VALUE, plainColumnRenderer } from '.';
import { mockECSData } from '../../../../pages/mock/mock_ecs';
import { mockECSData } from '../../../../mock/mock_ecs';
import { ECS } from '../../ecs';
describe('plain_column_renderer', () => {

View file

@ -9,7 +9,7 @@ import React from 'react';
import { cloneDeep } from 'lodash';
import { plainRowRenderer } from '.';
import { mockECSData } from '../../../../pages/mock/mock_ecs';
import { mockECSData } from '../../../../mock/mock_ecs';
import { ECS } from '../../ecs';
describe('plain_row_renderer', () => {

View file

@ -9,7 +9,7 @@ import { cloneDeep, omit, set } from 'lodash/fp';
import React from 'react';
import { EMPTY_VALUE, suricataColumnRenderer } from '.';
import { mockECSData } from '../../../../pages/mock/mock_ecs';
import { mockECSData } from '../../../../mock/mock_ecs';
import { ECS } from '../../ecs';
describe('suricata_column_renderer', () => {

View file

@ -9,7 +9,7 @@ import { cloneDeep, omit } from 'lodash/fp';
import React from 'react';
import { suricataRowRenderer } from '.';
import { mockECSData } from '../../../../pages/mock/mock_ecs';
import { mockECSData } from '../../../../mock/mock_ecs';
import { ECS } from '../../ecs';
describe('plain_row_renderer', () => {

View file

@ -9,7 +9,7 @@ import { cloneDeep } from 'lodash';
import React from 'react';
import { EMPTY_VALUE } from '.';
import { mockECSData } from '../../../../pages/mock/mock_ecs';
import { mockECSData } from '../../../../mock/mock_ecs';
import { ECS } from '../../ecs';
import { unknownColumnRenderer } from './unknown_column_renderer';

View file

@ -10,7 +10,7 @@ import * as React from 'react';
import { MockedProvider } from 'react-apollo/test-utils';
import { eventsQuery } from '../../containers/events/events.gql_query';
import { mockECSData } from '../../pages/mock/mock_ecs';
import { mockECSData } from '../../mock/mock_ecs';
import { ColumnHeaderType } from './body/column_headers/column_header';
import { headers } from './body/column_headers/headers';
import { columnRenderers, rowRenderers } from './body/renderers';

View file

@ -0,0 +1,36 @@
/*
* 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 { get } from 'lodash/fp';
import React from 'react';
import { Query } from 'react-apollo';
import { SourceQuery } from '../../../common/graphql/types';
import { sourceQuery } from './source.gql_query';
interface WithSourceArgs {
auditbeatIndicesExist: boolean;
}
interface WithSourceProps {
children: (args: WithSourceArgs) => React.ReactNode;
sourceId: string;
}
export const WithSource = ({ children, sourceId }: WithSourceProps) => (
<Query<SourceQuery.Query, SourceQuery.Variables>
query={sourceQuery}
fetchPolicy="no-cache"
notifyOnNetworkStatusChange
variables={{ sourceId }}
>
{({ data }) =>
children({
auditbeatIndicesExist: get('source.status.auditbeatIndicesExist', data),
})
}
</Query>
);

View file

@ -0,0 +1,30 @@
/*
* 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 sourceQuery = gql`
query SourceQuery($sourceId: ID = "default") {
source(id: $sourceId) {
id
configuration {
auditbeatAlias
logAlias
}
status {
auditbeatIndicesExist
auditbeatAliasExists
auditbeatIndices
indexFields {
name
searchable
type
aggregatable
}
}
}
}
`;

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ECS } from '../../components/timeline/ecs';
import { ECS } from '../components/timeline/ecs';
export const mockECSData: ECS[] = [
{

View file

@ -5,21 +5,26 @@
*/
import { EuiBadge, EuiText } from '@elastic/eui';
import { getOr } from 'lodash/fp';
import { getOr, isUndefined } from 'lodash/fp';
import React from 'react';
import { connect } from 'react-redux';
import { pure } from 'recompose';
import { Dispatch } from 'redux';
import styled from 'styled-components';
import chrome from 'ui/chrome';
import { EventItem, KpiItem } from '../../../common/graphql/types';
import { BasicTable } from '../../components/basic_table';
import { EmptyPage } from '../../components/empty_page';
import { HorizontalBarChart, HorizontalBarChartData } from '../../components/horizontal_bar_chart';
import { Pane1FlexContent } from '../../components/page';
import { Placeholders, VisualizationPlaceholder } from '../../components/visualization_placeholder';
import { EventsQuery } from '../../containers/events';
import { WithSource } from '../../containers/source';
import { timelineActions } from '../../store';
const basePath = chrome.getBasePath();
// start/end date to show good alert in the timeline
const startDate = 1521830963132;
const endDate = 1521862432253;
@ -34,36 +39,49 @@ interface Props {
export const Hosts = connect()(
pure<Props>(({ dispatch }) => (
<EventsQuery sourceId="default" startDate={startDate} endDate={endDate}>
{({ events, kpiEventType, loading }) => (
<Pane1FlexContent data-test-subj="pane1FlexContent">
<VisualizationPlaceholder>
<HorizontalBarChart
loading={loading}
title="KPI event types"
width={490}
height={279}
barChartdata={
kpiEventType.map((i: KpiItem) => ({
x: i.count,
y: i.value,
})) as HorizontalBarChartData[]
}
/>
</VisualizationPlaceholder>
<VisualizationPlaceholder>
<BasicTable
columns={getEventsColumns(dispatch)}
loading={loading}
pageOfItems={events}
sortField="host.hostname"
title="Events"
/>
</VisualizationPlaceholder>
<Placeholders timelineId="pane2-timeline" count={8} myRoute="Hosts" />
</Pane1FlexContent>
)}
</EventsQuery>
<WithSource sourceId="default">
{({ auditbeatIndicesExist }) =>
auditbeatIndicesExist || isUndefined(auditbeatIndicesExist) ? (
<EventsQuery sourceId="default" startDate={startDate} endDate={endDate}>
{({ events, kpiEventType, loading }) => (
<Pane1FlexContent data-test-subj="pane1FlexContent">
<VisualizationPlaceholder>
<HorizontalBarChart
loading={loading}
title="KPI event types"
width={490}
height={279}
barChartdata={
kpiEventType.map((i: KpiItem) => ({
x: i.count,
y: i.value,
})) as HorizontalBarChartData[]
}
/>
</VisualizationPlaceholder>
<VisualizationPlaceholder>
<BasicTable
columns={getEventsColumns(dispatch)}
loading={loading}
pageOfItems={events}
sortField="host.hostname"
title="Events"
/>
</VisualizationPlaceholder>
<Placeholders timelineId="pane2-timeline" count={8} myRoute="Hosts" />
</Pane1FlexContent>
)}
</EventsQuery>
) : (
<EmptyPage
title="Looks like you don't have any auditbeat indices."
message="Let's add some!"
actionLabel="Setup Instructions"
actionUrl={`${basePath}/app/kibana#/home/tutorial_directory/security`}
/>
)
}
</WithSource>
))
);

View file

@ -8,7 +8,7 @@ import { Range } from '../../../components/timeline/body/column_headers/range_pi
import { Sort } from '../../../components/timeline/body/sort';
import { DataProvider } from '../../../components/timeline/data_providers/data_provider';
import { ECS } from '../../../components/timeline/ecs';
import { mockECSData } from '../../../pages/mock/mock_ecs';
import { mockECSData } from '../../../mock/mock_ecs';
export interface TimelineModel {
id: string;

View file

@ -4,15 +4,24 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { rootSchema } from '../../common/graphql/root/schema.gql';
import { rootSchema } from '../../common/graphql/root';
import { sharedSchema } from '../../common/graphql/shared';
import { getSourceQueryMock } from '../graphql/sources/source.mock';
import { getAllSourcesQueryMock } from '../graphql/sources/sources.mock';
import { Logger } from '../utils/logger';
import { eventsSchema } from './events/schema.gql';
import { sourceStatusSchema } from './source_status/schema.gql';
import { sourcesSchema } from './sources/schema.gql';
import { whoAmISchema } from './who_am_i/schema.gql';
export const schemas = [rootSchema, sourcesSchema, eventsSchema, whoAmISchema];
export const schemas = [
eventsSchema,
rootSchema,
sourcesSchema,
sourceStatusSchema,
sharedSchema,
whoAmISchema,
];
// The types from graphql-tools/src/mock.ts 'any' based. I add slightly
// stricter types here, but these should go away when graphql-tools using something

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 { createSourceStatusResolvers } from './resolvers';
export { sourceStatusSchema } from './schema.gql';

View file

@ -0,0 +1,63 @@
/*
* 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 { IndexType, SourceStatusResolvers } from '../../../common/graphql/types';
import { AppResolvedResult, AppResolverOf } from '../../lib/framework';
import { IndexFields } from '../../lib/index_fields';
import { SourceStatus } from '../../lib/source_status';
import { Context } from '../../lib/types';
import { QuerySourceResolver } from '../sources/resolvers';
export type SourceStatusAuditbeatAliasExistsResolver = AppResolverOf<
SourceStatusResolvers.AuditbeatAliasExistsResolver,
AppResolvedResult<QuerySourceResolver>,
Context
>;
export type SourceStatusAuditbeatIndicesExistResolver = AppResolverOf<
SourceStatusResolvers.AuditbeatIndicesExistResolver,
AppResolvedResult<QuerySourceResolver>,
Context
>;
export type SourceStatusAuditbeatIndicesResolver = AppResolverOf<
SourceStatusResolvers.AuditbeatIndicesResolver,
AppResolvedResult<QuerySourceResolver>,
Context
>;
export type SourceStatusIndexFieldsResolver = AppResolverOf<
SourceStatusResolvers.IndexFieldsResolver,
AppResolvedResult<QuerySourceResolver>,
Context
>;
export const createSourceStatusResolvers = (libs: {
sourceStatus: SourceStatus;
fields: IndexFields;
}): {
SourceStatus: {
auditbeatAliasExists: SourceStatusAuditbeatAliasExistsResolver;
auditbeatIndicesExist: SourceStatusAuditbeatIndicesExistResolver;
auditbeatIndices: SourceStatusAuditbeatIndicesResolver;
indexFields: SourceStatusIndexFieldsResolver;
};
} => ({
SourceStatus: {
async auditbeatAliasExists(source, args, { req }) {
return await libs.sourceStatus.hasAlias(req, source.id, 'auditbeatAlias');
},
async auditbeatIndicesExist(source, args, { req }) {
return await libs.sourceStatus.hasIndices(req, source.id, 'auditbeatAlias');
},
async auditbeatIndices(source, args, { req }) {
return await libs.sourceStatus.getIndexNames(req, source.id, 'auditbeatAlias');
},
async indexFields(source, args, { req }) {
return await libs.fields.getFields(req, source.id, args.indexType || IndexType.ANY);
},
},
});

View file

@ -0,0 +1,32 @@
/*
* 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 sourceStatusSchema = gql`
"A descriptor of a field in an index"
type IndexField {
"The name of the field"
name: String!
"The type of the field's values as recognized by Kibana"
type: String!
"Whether the field's values can be efficiently searched for"
searchable: Boolean!
"Whether the field's values can be aggregated"
aggregatable: Boolean!
}
extend type SourceStatus {
"Whether the configured auditbeat alias exists"
auditbeatAliasExists: Boolean!
"Whether the configured alias or wildcard pattern resolve to any auditbeat indices"
auditbeatIndicesExist: Boolean!
"The list of indices in the auditbeat alias"
auditbeatIndices: [String!]!
"The list of fields defined in the index mappings"
indexFields(indexType: IndexType = ANY): [IndexField!]!
}
`;

View file

@ -4,7 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { GraphQLResolveInfo } from 'graphql';
import { omit } from 'lodash/fp';
import { FrameworkRequest, internalFrameworkRequest } from '../../lib/framework';
import { SourceStatus, SourceStatusAdapter } from '../../lib/source_status';
import { Sources, SourcesAdapter } from '../../lib/sources';
import { createSourcesResolvers, SourcesResolversDeps } from './resolvers';
import { mockSourceData } from './source.mock';
@ -18,8 +20,22 @@ mockGetAll.mockResolvedValue({
const mockSourcesAdapter: SourcesAdapter = {
getAll: mockGetAll,
};
const mockGetIndexNames = jest.fn();
mockGetIndexNames.mockResolvedValue([]);
const mockHasAlias = jest.fn();
mockHasAlias.mockResolvedValue(false);
const mockHasIndices = jest.fn();
mockHasIndices.mockResolvedValue(false);
const mockSourceStatusAdapter: SourceStatusAdapter = {
getIndexNames: mockGetIndexNames,
hasAlias: mockHasAlias,
hasIndices: mockHasIndices,
};
const mockLibs: SourcesResolversDeps = {
sources: new Sources(mockSourcesAdapter),
sourceStatus: new SourceStatus(mockSourceStatusAdapter, new Sources(mockSourcesAdapter)),
};
const req: FrameworkRequest = {
@ -40,7 +56,7 @@ const req: FrameworkRequest = {
const context = { req };
describe('Test Source Resolvers', () => {
test(`Make sure that getConfiguration have been called`, async () => {
test('Make sure that getConfiguration have been called', async () => {
const data = await createSourcesResolvers(mockLibs).Query.source(
null,
{ id: 'default' },
@ -48,6 +64,6 @@ describe('Test Source Resolvers', () => {
{} as GraphQLResolveInfo
);
expect(mockSourcesAdapter.getAll).toHaveBeenCalled();
expect(data).toEqual(mockSourceData);
expect(data).toEqual(omit('status', mockSourceData));
});
});

View file

@ -4,8 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { QueryResolvers } from '../../../common/graphql/types';
import { AppResolverWithFields } from '../../lib/framework';
import { QueryResolvers, SourceResolvers } from '../../../common/graphql/types';
import { AppResolvedResult, AppResolverWithFields } from '../../lib/framework';
import { SourceStatus } from '../../lib/source_status';
import { Sources } from '../../lib/sources';
import { Context } from '../../lib/types';
@ -23,8 +24,16 @@ export type QueryAllSourcesResolver = AppResolverWithFields<
'id' | 'configuration'
>;
export type SourceStatusResolver = AppResolverWithFields<
SourceResolvers.StatusResolver,
AppResolvedResult<QuerySourceResolver>,
Context,
never
>;
export interface SourcesResolversDeps {
sources: Sources;
sourceStatus: SourceStatus;
}
export const createSourcesResolvers = (
@ -34,6 +43,9 @@ export const createSourcesResolvers = (
source: QuerySourceResolver;
allSources: QueryAllSourcesResolver;
};
Source: {
status: SourceStatusResolver;
};
} => ({
Query: {
async source(root, args) {
@ -53,4 +65,9 @@ export const createSourcesResolvers = (
}));
},
},
Source: {
async status(source) {
return source;
},
},
});

View file

@ -19,12 +19,19 @@ export const sourcesSchema = gql`
id: ID!
"The raw configuration of the source"
configuration: SourceConfiguration!
"The status of the source"
status: SourceStatus!
}
"The status of an infrastructure data source"
type SourceStatus
"A set of configuration options for a security data source"
type SourceConfiguration {
"The alias to read file data from"
fileAlias: String!
logAlias: String!
"The alias to read auditbeat data from"
auditbeatAlias: String!
"The field mapping to use for this source"
fields: SourceFields!
}

View file

@ -8,7 +8,9 @@ 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 { sourceStatusSchema } from '../source_status/schema.gql';
import { sourcesSchema } from './schema.gql';
import { getSourceQueryMock, mockSourceData } from './source.mock';
@ -22,7 +24,18 @@ const testCaseSource = {
fields {
host
}
}
}
status {
auditbeatIndicesExist
auditbeatAliasExists
auditbeatIndices
indexFields {
name
searchable
type
aggregatable
}
}
}
}
`,
@ -46,7 +59,7 @@ const testCaseSource = {
describe('Test Source Schema', () => {
// Array of case types
const cases = [testCaseSource];
const typeDefs = [rootSchema, sourcesSchema];
const typeDefs = [rootSchema, sharedSchema, sourcesSchema, sourceStatusSchema];
const mockSchema = makeExecutableSchema({ typeDefs });
// Here we specify the return payloads of mocked types

View file

@ -15,6 +15,28 @@ export const mockSourceData = {
host: 'beat.hostname',
},
},
status: {
auditbeatIndicesExist: true,
auditbeatAliasExists: true,
auditbeatIndices: [
"auditbeat-7.0.0-alpha1-2018.10.03",
"auditbeat-7.0.0-alpha1-2018.10.04",
],
indexFields: [
{
name: "@timestamp",
searchable: true,
type: "date",
aggregatable: true,
},
{
name: "apache2.access.agent",
searchable: true,
type: "string",
aggregatable: false,
},
],
},
};
/* tslint:enable */

View file

@ -6,23 +6,12 @@
import { Logger } from '../../utils/logger';
import { Context } from '../index';
import { mockSourceData } from './source.mock';
/* tslint:disable */
export const sourcesDataMock =
[
{
"id": "default",
"configuration": {
"fields": {
"container": "docker.container.name",
"host": "beat.hostname",
"message": [
"message",
"@message"
]
}
}
}
mockSourceData,
];
/* tslint:enable */

View file

@ -8,6 +8,7 @@ import { addMockFunctionsToSchema, IResolvers, makeExecutableSchema } from 'grap
import { createMocks, schemas } from './graphql';
import { createEventsResolvers } from './graphql/events';
import { createSourceStatusResolvers } from './graphql/source_status';
import { createSourcesResolvers } from './graphql/sources';
import { createWhoAmIResolvers } from './graphql/who_am_i';
import { AppBackendLibs } from './lib/types';
@ -22,6 +23,7 @@ export const initServer = (libs: AppBackendLibs, config: Config) => {
const { logger, mocking } = config;
const schema = makeExecutableSchema({
resolvers: [
createSourceStatusResolvers(libs) as IResolvers,
createSourcesResolvers(libs) as IResolvers,
createEventsResolvers(libs) as IResolvers,
createWhoAmIResolvers() as IResolvers,

View file

@ -9,22 +9,26 @@ import { Server } from 'hapi';
import { KibanaConfigurationAdapter } from '../configuration/kibana_configuration_adapter';
import { ElasticsearchEventsAdapter, Events } from '../events';
import { KibanaBackendFrameworkAdapter } from '../framework/kibana_framework_adapter';
import { Sources } from '../sources';
import { ConfigurationSourcesAdapter } from '../sources/configuration_sources_adapter';
import { ElasticsearchIndexFieldAdapter, IndexFields } from '../index_fields';
import { ElasticsearchSourceStatusAdapter, SourceStatus } from '../source_status';
import { ConfigurationSourcesAdapter, Sources } from '../sources';
import { AppBackendLibs, AppDomainLibs, Configuration } from '../types';
export function compose(server: Server): AppBackendLibs {
const configuration = new KibanaConfigurationAdapter<Configuration>(server);
const framework = new KibanaBackendFrameworkAdapter(server);
const sources = new Sources(new ConfigurationSourcesAdapter(configuration));
const sourceStatus = new SourceStatus(new ElasticsearchSourceStatusAdapter(framework), sources);
const domainLibs: AppDomainLibs = {
events: new Events(new ElasticsearchEventsAdapter(framework)),
fields: new IndexFields(new ElasticsearchIndexFieldAdapter(framework), sources),
};
const libs: AppBackendLibs = {
configuration,
framework,
sourceStatus,
sources,
...domainLibs,
};

View file

@ -90,7 +90,7 @@ export class ElasticsearchEventsAdapter implements EventsAdapter {
const query = {
allowNoIndices: true,
index: options.sourceConfiguration.fileAlias,
index: options.sourceConfiguration.logAlias,
ignoreUnavailable: true,
body: {
aggregations: agg,

View file

@ -4,4 +4,4 @@
* you may not use this file except in compliance with the Elastic License.
*/
export * from './adapter_types';
export * from './types';

View file

@ -7,18 +7,19 @@
import { GraphQLSchema } from 'graphql';
import { Request, Server } from 'hapi';
import {
FrameworkAdapter,
FrameworkRequest,
internalFrameworkRequest,
WrappableRequest,
} from './adapter_types';
import {
graphiqlHapi,
graphqlHapi,
HapiGraphiQLPluginOptions,
HapiGraphQLPluginOptions,
} from './apollo_server_hapi';
import {
FrameworkAdapter,
FrameworkIndexPatternsService,
FrameworkRequest,
internalFrameworkRequest,
WrappableRequest,
} from './types';
declare module 'hapi' {
interface PluginProperties {
@ -82,6 +83,26 @@ export class KibanaBackendFrameworkAdapter implements FrameworkAdapter {
plugin: graphiqlHapi,
});
}
public getIndexPatternsService(
request: FrameworkRequest<Request>
): FrameworkIndexPatternsService {
if (!isServerWithIndexPatternsServiceFactory(this.server)) {
throw new Error('Failed to access indexPatternsService for the request');
}
return this.server.indexPatternsServiceFactory({
// tslint:disable-next-line:no-any
callCluster: async (method: string, args: [object], ...rest: any[]) => {
const fieldCaps = await this.callWithRequest(
request,
method,
{ ...args, allowNoIndices: true },
...rest
);
return fieldCaps;
},
});
}
}
export function wrapRequest<InternalRequest extends WrappableRequest>(
@ -96,3 +117,16 @@ export function wrapRequest<InternalRequest extends WrappableRequest>(
query,
};
}
interface ServerWithIndexPatternsServiceFactory extends Server {
indexPatternsServiceFactory(options: {
// tslint:disable-next-line:no-any
callCluster: (...args: any[]) => any;
}): FrameworkIndexPatternsService;
}
const isServerWithIndexPatternsServiceFactory = (
server: Server
): server is ServerWithIndexPatternsServiceFactory =>
// tslint:disable-next-line:no-any
typeof (server as any).indexPatternsServiceFactory === 'function';

View file

@ -23,7 +23,19 @@ export interface FrameworkAdapter {
req: FrameworkRequest,
method: 'msearch',
options?: object
): Promise<InfraDatabaseMultiResponse<Hit, Aggregation>>;
): Promise<DatabaseMultiResponse<Hit, Aggregation>>;
callWithRequest(
req: FrameworkRequest,
method: 'indices.existsAlias',
options?: object
): Promise<boolean>;
callWithRequest(
req: FrameworkRequest,
method: 'indices.getAlias' | 'indices.get',
options?: object
): Promise<DatabaseGetIndicesResponse>;
// tslint:disable-next-line:no-any
getIndexPatternsService(req: FrameworkRequest<any>): FrameworkIndexPatternsService;
}
export interface FrameworkRequest<InternalRequest extends WrappableRequest = WrappableRequest> {
@ -54,6 +66,36 @@ export interface DatabaseSearchResponse<Hit = {}, Aggregations = undefined>
};
}
export interface InfraDatabaseMultiResponse<Hit, Aggregation> extends DatabaseResponse {
export interface DatabaseMultiResponse<Hit, Aggregation> extends DatabaseResponse {
responses: Array<DatabaseSearchResponse<Hit, Aggregation>>;
}
interface FrameworkIndexFieldDescriptor {
name: string;
type: string;
searchable: boolean;
aggregatable: boolean;
readFromDocValues: boolean;
}
export interface FrameworkIndexPatternsService {
getFieldsForWildcard(options: {
pattern: string | string[];
}): Promise<FrameworkIndexFieldDescriptor[]>;
}
interface Alias {
settings: {
index: {
uuid: string;
};
};
}
export interface DatabaseGetIndicesResponse {
[indexName: string]: {
aliases: {
[aliasName: string]: Alias;
};
};
}

View file

@ -0,0 +1,20 @@
/*
* 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 { IndexField } from '../../../common/graphql/types';
import { FrameworkAdapter, FrameworkRequest } from '../framework';
import { FieldsAdapter } from './types';
export class ElasticsearchIndexFieldAdapter implements FieldsAdapter {
constructor(private readonly framework: FrameworkAdapter) {}
public async getIndexFields(request: FrameworkRequest, indices: string[]): Promise<IndexField[]> {
const indexPatternsService = this.framework.getIndexPatternsService(request);
return await indexPatternsService.getFieldsForWildcard({
pattern: indices,
});
}
}

View file

@ -0,0 +1,45 @@
/*
* 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 { IndexField, IndexType } from '../../../common/graphql/types';
import { FrameworkRequest } from '../framework';
import { Sources } from '../sources';
import { FieldsAdapter } from './types';
export { ElasticsearchIndexFieldAdapter } from './elasticsearch_adapter';
export class IndexFields implements FieldsAdapter {
private adapter: FieldsAdapter;
private sources: Sources;
constructor(adapter: FieldsAdapter, sources: Sources) {
this.adapter = adapter;
this.sources = sources;
}
public async getFields(
request: FrameworkRequest,
sourceId: string,
indexType: IndexType
): Promise<IndexField[]> {
const sourceConfiguration = await this.sources.getConfiguration(sourceId);
const includeAuditBeatIndices = [IndexType.ANY, IndexType.AUDITBEAT].includes(indexType);
const includeLogIndices = [IndexType.ANY, IndexType.LOGS].includes(indexType);
const indices = [
...(includeAuditBeatIndices ? [sourceConfiguration.auditbeatAlias] : []),
...(includeLogIndices ? [sourceConfiguration.logAlias] : []),
];
return this.getIndexFields(request, indices);
}
public async getIndexFields(
request: FrameworkRequest,
indices: string[] = []
): Promise<IndexField[]> {
return await this.adapter.getIndexFields(request, indices as string[]);
}
}

View file

@ -0,0 +1,20 @@
/*
* 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 { IndexField, IndexType } from '../../../common/graphql/types';
import { FrameworkRequest } from '../framework';
export interface FieldsAdapter {
getFields?(req: FrameworkRequest, sourceId: string, indexType: IndexType): Promise<IndexField[]>;
getIndexFields(req: FrameworkRequest, indices: string[]): Promise<IndexField[]>;
}
export interface IndexFieldDescriptor {
name: string;
type: string;
searchable: boolean;
aggregatable: boolean;
}

View file

@ -0,0 +1,53 @@
/*
* 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 { DatabaseGetIndicesResponse, FrameworkAdapter, FrameworkRequest } from '../framework';
import { SourceStatusAdapter } from './index';
export class ElasticsearchSourceStatusAdapter implements SourceStatusAdapter {
constructor(private readonly framework: FrameworkAdapter) {}
public async getIndexNames(request: FrameworkRequest, aliasName: string) {
const indexMaps = await Promise.all([
this.framework
.callWithRequest(request, 'indices.getAlias', {
name: aliasName,
filterPath: '*.settings.index.uuid', // to keep the response size as small as possible
})
.catch(withDefaultIfNotFound<DatabaseGetIndicesResponse>({})),
this.framework
.callWithRequest(request, 'indices.get', {
index: aliasName,
filterPath: '*.settings.index.uuid', // to keep the response size as small as possible
})
.catch(withDefaultIfNotFound<DatabaseGetIndicesResponse>({})),
]);
return indexMaps.reduce(
(indexNames, indexMap) => [...indexNames, ...Object.keys(indexMap)],
[] as string[]
);
}
public async hasAlias(request: FrameworkRequest, aliasName: string) {
return await this.framework.callWithRequest(request, 'indices.existsAlias', {
name: aliasName,
});
}
public async hasIndices(request: FrameworkRequest, indexNames: string) {
return (await this.getIndexNames(request, indexNames)).length > 0;
}
}
const withDefaultIfNotFound = <DefaultValue>(defaultValue: DefaultValue) => (
// tslint:disable-next-line:no-any
error: any
): DefaultValue => {
if (error && error.status === 404) {
return defaultValue;
}
throw error;
};

View file

@ -0,0 +1,50 @@
/*
* 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 { FrameworkRequest } from '../framework';
import { AliasConfiguration, SourceConfiguration, Sources } from '../sources';
export { ElasticsearchSourceStatusAdapter } from './elasticsearch_adapter';
export class SourceStatus {
constructor(private readonly adapter: SourceStatusAdapter, private readonly sources: Sources) {}
public async getIndexNames(
request: FrameworkRequest,
sourceId: string,
aliasName: keyof AliasConfiguration
): Promise<string[]> {
return await this.adapter.getIndexNames(request, await this.getAliasName(sourceId, aliasName));
}
public async hasAlias(
request: FrameworkRequest,
sourceId: string,
aliasName: keyof AliasConfiguration
): Promise<boolean> {
return await this.adapter.hasAlias(request, await this.getAliasName(sourceId, aliasName));
}
public async hasIndices(
request: FrameworkRequest,
sourceId: string,
aliasName: keyof AliasConfiguration
): Promise<boolean> {
return await this.adapter.hasIndices(request, await this.getAliasName(sourceId, aliasName));
}
private getAliasName = async (sourceId: string, aliasName: keyof AliasConfiguration) => {
const sourceConfiguration: SourceConfiguration = await this.sources.getConfiguration(sourceId);
return prop(sourceConfiguration, aliasName);
};
}
export interface SourceStatusAdapter {
getIndexNames(request: FrameworkRequest, aliasName: string): Promise<string[]>;
hasAlias(request: FrameworkRequest, aliasName: string): Promise<boolean>;
hasIndices(request: FrameworkRequest, indexNames: string): Promise<boolean>;
}
function prop<T, K extends keyof SourceConfiguration>(obj: SourceConfiguration, key: K) {
return obj[key];
}

View file

@ -5,8 +5,8 @@
*/
import { InmemoryConfigurationAdapter } from '../configuration/inmemory_configuration_adapter';
import { PartialSourceConfiguration } from './adapter_types';
import { ConfigurationSourcesAdapter } from './configuration_sources_adapter';
import { ConfigurationSourcesAdapter } from './configuration';
import { PartialSourceConfiguration } from './types';
describe('the ConfigurationSourcesAdapter', () => {
test('adds the default source when no sources are configured', async () => {
@ -17,7 +17,8 @@ describe('the ConfigurationSourcesAdapter', () => {
expect(await sourcesAdapter.getAll()).toMatchObject({
default: {
metricAlias: expect.any(String),
fileAlias: expect.any(String),
logAlias: expect.any(String),
auditbeatAlias: expect.any(String),
fields: {
container: expect.any(String),
host: expect.any(String),
@ -42,7 +43,7 @@ describe('the ConfigurationSourcesAdapter', () => {
expect(await sourcesAdapter.getAll()).toMatchObject({
default: {
metricAlias: expect.any(String),
fileAlias: expect.any(String),
logAlias: expect.any(String),
},
});
});
@ -53,7 +54,8 @@ describe('the ConfigurationSourcesAdapter', () => {
sources: {
default: {
metricAlias: 'METRIC_ALIAS',
fileAlias: 'FILE_ALIAS',
logAlias: 'LOG_ALIAS',
auditbeatAlias: 'AUDITBEAT_ALIAS',
fields: {
container: 'DIFFERENT_CONTAINER_FIELD',
},
@ -65,7 +67,8 @@ describe('the ConfigurationSourcesAdapter', () => {
expect(await sourcesAdapter.getAll()).toMatchObject({
default: {
metricAlias: 'METRIC_ALIAS',
fileAlias: 'FILE_ALIAS',
logAlias: 'LOG_ALIAS',
auditbeatAlias: 'AUDITBEAT_ALIAS',
fields: {
container: 'DIFFERENT_CONTAINER_FIELD',
host: expect.any(String),
@ -84,7 +87,8 @@ describe('the ConfigurationSourcesAdapter', () => {
sources: {
sourceOne: {
metricAlias: 'METRIC_ALIAS',
fileAlias: 'FILE_ALIAS',
logAlias: 'LOG_ALIAS',
auditbeatAlias: 'AUDITBEAT_ALIAS',
fields: {
container: 'DIFFERENT_CONTAINER_FIELD',
},
@ -96,7 +100,8 @@ describe('the ConfigurationSourcesAdapter', () => {
expect(await sourcesAdapter.getAll()).toMatchObject({
sourceOne: {
metricAlias: 'METRIC_ALIAS',
fileAlias: 'FILE_ALIAS',
logAlias: 'LOG_ALIAS',
auditbeatAlias: 'AUDITBEAT_ALIAS',
fields: {
container: 'DIFFERENT_CONTAINER_FIELD',
host: expect.any(String),

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ConfigurationAdapter } from '../configuration';
import { PartialSourceConfigurations } from './adapter_types';
import { SourceConfigurations, SourcesAdapter } from './index';
import { PartialSourceConfigurations } from './types';
interface ConfigurationWithSources {
sources?: PartialSourceConfigurations;
@ -58,6 +58,7 @@ const DEFAULT_FIELDS = {
const DEFAULT_SOURCE = {
metricAlias: 'metricbeat-*',
fileAlias: 'filebeat-*',
logAlias: 'filebeat-*',
auditbeatAlias: 'auditbeat-*',
fields: DEFAULT_FIELDS,
};

View file

@ -4,15 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
export { ConfigurationSourcesAdapter } from './configuration_sources_adapter';
export { ConfigurationSourcesAdapter } from './configuration';
export class Sources {
constructor(private readonly adapter: SourcesAdapter) {}
public async getConfiguration(sourceId: string) {
public async getConfiguration(sourceId: string): Promise<SourceConfiguration> {
const sourceConfigurations = await this.getAllConfigurations();
const requestedSourceConfiguration = sourceConfigurations[sourceId];
if (!requestedSourceConfiguration) {
throw new Error(`Failed to find source '${sourceId}'`);
}
@ -33,8 +32,13 @@ export interface SourceConfigurations {
[sourceId: string]: SourceConfiguration;
}
export interface SourceConfiguration {
fileAlias: string;
export interface AliasConfiguration {
metricAlias: string;
logAlias: string;
auditbeatAlias: string;
}
export interface SourceConfiguration extends AliasConfiguration {
fields: {
container: string;
host: string;

View file

@ -7,16 +7,20 @@
import { ConfigurationAdapter } from './configuration';
import { Events } from './events';
import { FrameworkAdapter, FrameworkRequest } from './framework';
import { IndexFields } from './index_fields';
import { SourceStatus } from './source_status';
import { SourceConfigurations, Sources } from './sources';
export interface AppDomainLibs {
events: Events;
fields: IndexFields;
}
export interface AppBackendLibs extends AppDomainLibs {
configuration: ConfigurationAdapter<Configuration>;
framework: FrameworkAdapter;
sources: Sources;
sourceStatus: SourceStatus;
}
export interface Configuration {

View file

@ -17,13 +17,8 @@ export default function ({ loadTestFile }) {
// loadTestFile(require.resolve('./logstash'));
// loadTestFile(require.resolve('./kibana'));
// TODO: I am only running infra at the moment
// but in reality I should not be running infra and
// should instead be running secops which still needs
// to be built. I kept this api integration test running for right now
// as an example. -- Frank H.
// See completion of issue: https://github.com/elastic/ingest-dev/issues/56
loadTestFile(require.resolve('./infra'));
//Only running our secops test for now since we are working in our own branch
loadTestFile(require.resolve('./secops'));
// loadTestFile(require.resolve('./beats'));
});

View file

@ -0,0 +1,12 @@
/*
* 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 default function ({ loadTestFile }) {
describe('SecOps GraphQL Endpoints', () => {
loadTestFile(require.resolve('./sources'));
});
}

View file

@ -0,0 +1,48 @@
/*
* 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 'expect.js';
import { SourceQuery } from '../../../../plugins/secops/common/graphql/types';
import { sourceQuery } from '../../../../plugins/secops/public/containers/source/source.gql_query';
import { KbnTestProvider } from './types';
const sourcesTests: KbnTestProvider = ({ getService }) => {
const esArchiver = getService('esArchiver');
const client = getService('secOpsGraphQLClient');
describe('sources', () => {
before(() => esArchiver.load('auditbeat/default'));
after(() => esArchiver.unload('auditbeat/default'));
it('Make sure that we get source information when auditbeat indices is there', () => {
return client
.query<SourceQuery.Query>({
query: sourceQuery,
variables: {
sourceId: 'default',
},
})
.then(resp => {
const sourceConfiguration = resp.data.source.configuration;
const sourceStatus = resp.data.source.status;
// shipped default values
expect(sourceConfiguration.auditbeatAlias).to.be('auditbeat-*');
expect(sourceConfiguration.logAlias).to.be('filebeat-*');
// test data in x-pack/test/functional/es_archives/auditbeat_test_data/data.json.gz
expect(sourceStatus.indexFields.length).to.be(345);
expect(sourceStatus.auditbeatIndices.length).to.be(1);
expect(sourceStatus.auditbeatIndicesExist).to.be(true);
expect(sourceStatus.auditbeatAliasExists).to.be(false);
});
});
});
};
// tslint:disable-next-line no-default-export
export default sourcesTests;

View file

@ -0,0 +1,21 @@
/*
* 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 { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client';
export interface EsArchiver {
load(name: string): void;
unload(name: string): void;
}
export interface KbnTestProviderOptions {
getService(name: string): any;
getService(name: 'esArchiver'): EsArchiver;
getService(name: 'secOpsGraphQLClient'): ApolloClient<InMemoryCache>;
}
export type KbnTestProvider = (options: KbnTestProviderOptions) => void;

View file

@ -9,7 +9,8 @@ import {
EsSupertestWithoutAuthProvider,
SupertestWithoutAuthProvider,
UsageAPIProvider,
InfraOpsGraphQLProvider
InfraOpsGraphQLProvider,
SecOpsGraphQLProvider,
} from './services';
export default async function ({ readConfigFile }) {
@ -27,6 +28,7 @@ export default async function ({ readConfigFile }) {
supertestWithoutAuth: SupertestWithoutAuthProvider,
esSupertestWithoutAuth: EsSupertestWithoutAuthProvider,
infraOpsGraphQLClient: InfraOpsGraphQLProvider,
secOpsGraphQLClient: SecOpsGraphQLProvider,
es: EsProvider,
esArchiver: kibanaCommonConfig.get('services.esArchiver'),
usageAPI: UsageAPIProvider,

View file

@ -8,4 +8,5 @@ export { EsProvider } from './es';
export { EsSupertestWithoutAuthProvider } from './es_supertest_without_auth';
export { SupertestWithoutAuthProvider } from './supertest_without_auth';
export { UsageAPIProvider } from './usage_api';
export { InfraOpsGraphQLProvider } from './infraops_graphql_client';
export { InfraOpsGraphQLProvider } from './infraops_graphql_client';
export { SecOpsGraphQLProvider } from './secops_graphql_client';

View file

@ -0,0 +1,34 @@
/*
* 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 { format as formatUrl } from 'url';
import fetch from 'node-fetch';
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client';
import { HttpLink } from 'apollo-link-http';
import introspectionQueryResultData from '../../../plugins/secops/common/graphql/introspection.json';
export function SecOpsGraphQLProvider({ getService }) {
const config = getService('config');
const kbnURL = formatUrl(config.get('servers.kibana'));
return new ApolloClient({
cache: new InMemoryCache({
fragmentMatcher: new IntrospectionFragmentMatcher({
introspectionQueryResultData,
}),
}),
link: new HttpLink({
credentials: 'same-origin',
fetch,
headers: {
'kbn-xsrf': 'xxx',
},
uri: `${kbnURL}/api/secops/graphql`,
}),
});
}

File diff suppressed because it is too large Load diff

View file

@ -1074,6 +1074,11 @@
resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.8.tgz#5702f74f78b73e13f1eb1bd435c2c9de61a250d4"
integrity sha512-LzF540VOFabhS2TR2yYFz2Mu/fTfkA+5AwYddtJbOJGwnYrr2e7fHadT7/Z3jNGJJdCRlO3ySxmW26NgRdwhNA==
"@types/cheerio@^0.22.10":
version "0.22.10"
resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.10.tgz#780d552467824be4a241b29510a7873a7432c4a6"
integrity sha512-fOM/Jhv51iyugY7KOBZz2ThfT1gwvsGCfWxpLpZDgkGjpEO4Le9cld07OdskikLjDUQJ43dzDaVRSFwQlpdqVg==
"@types/classnames@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.3.tgz#3f0ff6873da793870e20a260cada55982f38a9e5"