Update endpoint event and alert types (#69292)

* start redoing types

* finish redoing types

* fix bad test

* rework tests

* fix more types

* fix test

* Fix endpoints test and render error

* add deletePolicyStream to alerts api tests

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Co-authored-by: Paul Tavares <paul.tavares@elastic.co>
This commit is contained in:
Marshall Main 2020-06-18 13:50:53 -04:00 committed by GitHub
parent f4b0d5cbbc
commit 4a26f56f31
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 456 additions and 300 deletions

View file

@ -47,7 +47,7 @@ describe('data generator', () => {
const metadata = generator.generateHostMetadata(timestamp);
expect(metadata['@timestamp']).toEqual(timestamp);
expect(metadata.event.created).toEqual(timestamp);
expect(metadata.endpoint).not.toBeNull();
expect(metadata.Endpoint).not.toBeNull();
expect(metadata.agent).not.toBeNull();
expect(metadata.host).not.toBeNull();
});
@ -57,10 +57,10 @@ describe('data generator', () => {
const hostPolicyResponse = generator.generatePolicyResponse(timestamp);
expect(hostPolicyResponse['@timestamp']).toEqual(timestamp);
expect(hostPolicyResponse.event.created).toEqual(timestamp);
expect(hostPolicyResponse.endpoint).not.toBeNull();
expect(hostPolicyResponse.Endpoint).not.toBeNull();
expect(hostPolicyResponse.agent).not.toBeNull();
expect(hostPolicyResponse.host).not.toBeNull();
expect(hostPolicyResponse.endpoint.policy.applied).not.toBeNull();
expect(hostPolicyResponse.Endpoint.policy.applied).not.toBeNull();
});
it('creates alert event documents', () => {
@ -68,7 +68,7 @@ describe('data generator', () => {
const alert = generator.generateAlert(timestamp);
expect(alert['@timestamp']).toEqual(timestamp);
expect(alert.event.action).not.toBeNull();
expect(alert.endpoint).not.toBeNull();
expect(alert.Endpoint).not.toBeNull();
expect(alert.agent).not.toBeNull();
expect(alert.host).not.toBeNull();
expect(alert.process.entity_id).not.toBeNull();
@ -364,7 +364,9 @@ describe('data generator', () => {
it('creates full resolver tree', () => {
const alertAncestors = 3;
const generations = 2;
const events = [...generator.fullResolverTreeGenerator(alertAncestors, generations)];
const events = [
...generator.fullResolverTreeGenerator({ ancestors: alertAncestors, generations }),
];
const rootNode = buildResolverTree(events);
const visitedEvents = countResolverEvents(rootNode, alertAncestors + generations);
expect(visitedEvents).toEqual(events.length);

View file

@ -10,7 +10,7 @@ import {
EndpointEvent,
Host,
HostMetadata,
HostOS,
OSFields,
HostPolicyResponse,
HostPolicyResponseActionStatus,
PolicyData,
@ -28,38 +28,46 @@ interface EventOptions {
processName?: string;
}
const Windows: HostOS[] = [
const Windows: OSFields[] = [
{
name: 'windows 10.0',
full: 'Windows 10',
version: '10.0',
variant: 'Windows Pro',
Ext: {
variant: 'Windows Pro',
},
},
{
name: 'windows 10.0',
full: 'Windows Server 2016',
version: '10.0',
variant: 'Windows Server',
Ext: {
variant: 'Windows Server',
},
},
{
name: 'windows 6.2',
full: 'Windows Server 2012',
version: '6.2',
variant: 'Windows Server',
Ext: {
variant: 'Windows Server',
},
},
{
name: 'windows 6.3',
full: 'Windows Server 2012R2',
version: '6.3',
variant: 'Windows Server Release 2',
Ext: {
variant: 'Windows Server Release 2',
},
},
];
const Linux: HostOS[] = [];
const Linux: OSFields[] = [];
const Mac: HostOS[] = [];
const Mac: OSFields[] = [];
const OS: HostOS[] = [...Windows, ...Mac, ...Linux];
const OS: OSFields[] = [...Windows, ...Mac, ...Linux];
const APPLIED_POLICIES: Array<{
name: string;
@ -186,7 +194,7 @@ interface HostInfo {
type: string;
};
host: Host;
endpoint: {
Endpoint: {
policy: {
applied: {
id: string;
@ -283,8 +291,8 @@ export class EndpointDocGenerator {
* Creates new random policy id for the host to simulate new policy application
*/
public updatePolicyId() {
this.commonInfo.endpoint.policy.applied.id = this.randomChoice(APPLIED_POLICIES).id;
this.commonInfo.endpoint.policy.applied.status = this.randomChoice([
this.commonInfo.Endpoint.policy.applied = this.randomChoice(APPLIED_POLICIES);
this.commonInfo.Endpoint.policy.applied.status = this.randomChoice([
HostPolicyResponseActionStatus.success,
HostPolicyResponseActionStatus.failure,
HostPolicyResponseActionStatus.warning,
@ -310,7 +318,7 @@ export class EndpointDocGenerator {
mac: this.randomArray(3, () => this.randomMac()),
os: this.randomChoice(OS),
},
endpoint: {
Endpoint: {
policy: {
applied: this.randomChoice(APPLIED_POLICIES),
},
@ -371,77 +379,88 @@ export class EndpointDocGenerator {
sha1: 'fake file sha1',
sha256: 'fake file sha256',
},
code_signature: {
trusted: false,
subject_name: 'bad signer',
Ext: {
code_signature: [
{
trusted: false,
subject_name: 'bad signer',
},
],
malware_classification: {
identifier: 'endpointpe',
score: 1,
threshold: 0.66,
version: '3.0.33',
},
temp_file_path: 'C:/temp/fake_malware.exe',
},
malware_classification: {
identifier: 'endpointpe',
score: 1,
threshold: 0.66,
version: '3.0.33',
},
temp_file_path: 'C:/temp/fake_malware.exe',
},
process: {
pid: 2,
name: 'malware writer',
start: ts,
uptime: 0,
user: 'SYSTEM',
entity_id: entityID,
executable: 'C:/malware.exe',
parent: parentEntityID ? { entity_id: parentEntityID, pid: 1 } : undefined,
token: {
domain: 'NT AUTHORITY',
integrity_level: 16384,
integrity_level_name: 'system',
privileges: [
{
description: 'Replace a process level token',
enabled: false,
name: 'SeAssignPrimaryTokenPrivilege',
},
],
sid: 'S-1-5-18',
type: 'tokenPrimary',
user: 'SYSTEM',
},
code_signature: {
trusted: false,
subject_name: 'bad signer',
},
hash: {
md5: 'fake md5',
sha1: 'fake sha1',
sha256: 'fake sha256',
},
Ext: {
code_signature: [
{
trusted: false,
subject_name: 'bad signer',
},
],
user: 'SYSTEM',
token: {
domain: 'NT AUTHORITY',
integrity_level: 16384,
integrity_level_name: 'system',
privileges: [
{
description: 'Replace a process level token',
enabled: false,
name: 'SeAssignPrimaryTokenPrivilege',
},
],
sid: 'S-1-5-18',
type: 'tokenPrimary',
user: 'SYSTEM',
},
},
},
dll: [
{
pe: {
architecture: 'x64',
imphash: 'c30d230b81c734e82e86e2e2fe01cd01',
},
code_signature: {
subject_name: 'Cybereason Inc',
trusted: true,
},
compile_time: 1534424710,
hash: {
md5: '1f2d082566b0fc5f2c238a5180db7451',
sha1: 'ca85243c0af6a6471bdaa560685c51eefd6dbc0d',
sha256: '8ad40c90a611d36eb8f9eb24fa04f7dbca713db383ff55a03aa0f382e92061a2',
},
malware_classification: {
identifier: 'Whitelisted',
score: 0,
threshold: 0,
version: '3.0.0',
},
mapped_address: 5362483200,
mapped_size: 0,
path: 'C:\\Program Files\\Cybereason ActiveProbe\\AmSvc.exe',
Ext: {
compile_time: 1534424710,
mapped_address: 5362483200,
mapped_size: 0,
malware_classification: {
identifier: 'Whitelisted',
score: 0,
threshold: 0,
version: '3.0.0',
},
},
},
],
};
@ -561,28 +580,9 @@ export class EndpointDocGenerator {
* @param percentTerminated - percent of nodes which will have process termination events
* @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children
*/
public *alertsGenerator(
numAlerts: number,
alertAncestors?: number,
childGenerations?: number,
maxChildrenPerNode?: number,
relatedEventsPerNode?: number,
relatedAlertsPerNode?: number,
percentNodesWithRelated?: number,
percentTerminated?: number,
alwaysGenMaxChildrenPerNode?: boolean
) {
public *alertsGenerator(numAlerts: number, options: TreeOptions = {}) {
for (let i = 0; i < numAlerts; i++) {
yield* this.fullResolverTreeGenerator(
alertAncestors,
childGenerations,
maxChildrenPerNode,
relatedEventsPerNode,
relatedAlertsPerNode,
percentNodesWithRelated,
percentTerminated,
alwaysGenMaxChildrenPerNode
);
yield* this.fullResolverTreeGenerator(options);
}
}
@ -600,21 +600,12 @@ export class EndpointDocGenerator {
* @param percentTerminated - percent of nodes which will have process termination events
* @param alwaysGenMaxChildrenPerNode - flag to always return the max children per node instead of it being a random number of children
*/
public *fullResolverTreeGenerator(
alertAncestors?: number,
childGenerations?: number,
maxChildrenPerNode?: number,
relatedEventsPerNode?: RelatedEventInfo[] | number,
relatedAlertsPerNode?: number,
percentNodesWithRelated?: number,
percentTerminated?: number,
alwaysGenMaxChildrenPerNode?: boolean
) {
public *fullResolverTreeGenerator(options: TreeOptions = {}) {
const ancestry = this.createAlertEventAncestry(
alertAncestors,
relatedEventsPerNode,
percentNodesWithRelated,
percentTerminated
options.ancestors,
options.relatedEvents,
options.percentWithRelated,
options.percentTerminated
);
for (let i = 0; i < ancestry.length; i++) {
yield ancestry[i];
@ -622,13 +613,13 @@ export class EndpointDocGenerator {
// ancestry will always have at least 2 elements, and the last element will be the alert
yield* this.descendantsTreeGenerator(
ancestry[ancestry.length - 1],
childGenerations,
maxChildrenPerNode,
relatedEventsPerNode,
relatedAlertsPerNode,
percentNodesWithRelated,
percentTerminated,
alwaysGenMaxChildrenPerNode
options.generations,
options.children,
options.relatedEvents,
options.relatedAlerts,
options.percentWithRelated,
options.percentTerminated,
options.alwaysGenMaxChildrenPerNode
);
}
@ -940,7 +931,7 @@ export class EndpointDocGenerator {
host: {
id: this.commonInfo.host.id,
},
endpoint: {
Endpoint: {
policy: {
applied: {
actions: [
@ -1045,7 +1036,7 @@ export class EndpointDocGenerator {
status: HostPolicyResponseActionStatus.success,
},
],
id: this.commonInfo.endpoint.policy.applied.id,
id: this.commonInfo.Endpoint.policy.applied.id,
response: {
configurations: {
events: {
@ -1086,9 +1077,9 @@ export class EndpointDocGenerator {
],
},
},
status: this.commonInfo.endpoint.policy.applied.status,
status: this.commonInfo.Endpoint.policy.applied.status,
version: policyVersion,
name: this.commonInfo.endpoint.policy.applied.name,
name: this.commonInfo.Endpoint.policy.applied.name,
},
},
},

View file

@ -0,0 +1,92 @@
/*
* 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 { Client } from '@elastic/elasticsearch';
import seedrandom from 'seedrandom';
import { EndpointDocGenerator, TreeOptions, Event } from './generate_data';
export async function indexHostsAndAlerts(
client: Client,
seed: string,
numHosts: number,
numDocs: number,
metadataIndex: string,
policyIndex: string,
eventIndex: string,
alertsPerHost: number,
options: TreeOptions = {}
) {
const random = seedrandom(seed);
for (let i = 0; i < numHosts; i++) {
const generator = new EndpointDocGenerator(random);
await indexHostDocs(numDocs, client, metadataIndex, policyIndex, generator);
await indexAlerts(client, eventIndex, generator, alertsPerHost, options);
}
await client.indices.refresh({
index: eventIndex,
});
// TODO: Unclear why the documents are not showing up after the call to refresh.
// Waiting 5 seconds allows the indices to refresh automatically and
// the documents become available in API/integration tests.
await delay(5000);
}
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function indexHostDocs(
numDocs: number,
client: Client,
metadataIndex: string,
policyIndex: string,
generator: EndpointDocGenerator
) {
const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents
const timestamp = new Date().getTime();
for (let j = 0; j < numDocs; j++) {
generator.updateHostData();
generator.updatePolicyId();
await client.index({
index: metadataIndex,
body: generator.generateHostMetadata(timestamp - timeBetweenDocs * (numDocs - j - 1)),
op_type: 'create',
});
await client.index({
index: policyIndex,
body: generator.generatePolicyResponse(timestamp - timeBetweenDocs * (numDocs - j - 1)),
op_type: 'create',
});
}
}
async function indexAlerts(
client: Client,
index: string,
generator: EndpointDocGenerator,
numAlerts: number,
options: TreeOptions = {}
) {
const alertGenerator = generator.alertsGenerator(numAlerts, options);
let result = alertGenerator.next();
while (!result.done) {
let k = 0;
const resolverDocs: Event[] = [];
while (k < 1000 && !result.done) {
resolverDocs.push(result.value);
result = alertGenerator.next();
k++;
}
const body = resolverDocs.reduce(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(array: Array<Record<string, any>>, doc) => (
array.push({ create: { _index: index } }, doc), array
),
[]
);
await client.bulk({ body, refresh: 'true' });
}
}

View file

@ -173,12 +173,19 @@ export interface HostResultList {
}
/**
* Operating System metadata for a host.
* Operating System metadata.
*/
export interface HostOS {
export interface OSFields {
full: string;
name: string;
version: string;
Ext: OSFieldsExt;
}
/**
* Extended Operating System metadata.
*/
export interface OSFieldsExt {
variant: string;
}
@ -190,7 +197,7 @@ export interface Host {
hostname: string;
ip: string[];
mac: string[];
os: HostOS;
os: OSFields;
}
/**
@ -220,27 +227,30 @@ interface MalwareClassification {
interface ThreadFields {
id: number;
service_name: string;
start: number;
start_address: number;
start_address_module: string;
Ext: {
service_name: string;
start: number;
start_address: number;
start_address_module: string;
};
}
interface DllFields {
hash: Hashes;
path: string;
pe: {
architecture: string;
imphash: string;
};
code_signature: {
subject_name: string;
trusted: boolean;
};
compile_time: number;
hash: Hashes;
malware_classification: MalwareClassification;
mapped_address: number;
mapped_size: number;
path: string;
Ext: {
compile_time: number;
malware_classification: MalwareClassification;
mapped_address: number;
mapped_size: number;
};
}
/**
@ -265,7 +275,7 @@ export interface AlertEvent {
module: string;
type: string;
};
endpoint: {
Endpoint: {
policy: {
applied: {
id: string;
@ -275,12 +285,7 @@ export interface AlertEvent {
};
};
process: {
code_signature: {
subject_name: string;
trusted: boolean;
};
command_line?: string;
domain?: string;
pid: number;
ppid?: number;
entity_id: string;
@ -290,29 +295,31 @@ export interface AlertEvent {
};
name: string;
hash: Hashes;
pe?: {
imphash: string;
};
executable: string;
sid?: string;
start: number;
malware_classification?: MalwareClassification;
token: {
domain: string;
type: string;
user: string;
sid: string;
integrity_level: number;
integrity_level_name: string;
privileges?: Array<{
description: string;
name: string;
enabled: boolean;
}>;
};
thread?: ThreadFields[];
uptime: number;
user: string;
Ext: {
code_signature: Array<{
subject_name: string;
trusted: boolean;
}>;
malware_classification?: MalwareClassification;
token: {
domain: string;
type: string;
user: string;
sid: string;
integrity_level: number;
integrity_level_name: string;
privileges?: Array<{
description: string;
name: string;
enabled: boolean;
}>;
};
user: string;
};
};
file: {
owner: string;
@ -323,15 +330,14 @@ export interface AlertEvent {
created: number;
size: number;
hash: Hashes;
pe?: {
imphash: string;
Ext: {
malware_classification: MalwareClassification;
temp_file_path: string;
code_signature: Array<{
trusted: boolean;
subject_name: string;
}>;
};
code_signature: {
trusted: boolean;
subject_name: string;
};
malware_classification: MalwareClassification;
temp_file_path: string;
};
host: Host;
dll?: DllFields[];
@ -373,7 +379,7 @@ export type HostMetadata = Immutable<{
id: string;
};
};
endpoint: {
Endpoint: {
policy: {
applied: {
id: string;
@ -666,7 +672,7 @@ export interface HostPolicyResponseAppliedAction {
message: string;
}
export type HostPolicyResponseConfiguration = HostPolicyResponse['endpoint']['policy']['applied']['response']['configurations'];
export type HostPolicyResponseConfiguration = HostPolicyResponse['Endpoint']['policy']['applied']['response']['configurations'];
interface HostPolicyResponseConfigurationStatus {
status: HostPolicyResponseActionStatus;
@ -711,7 +717,7 @@ export interface HostPolicyResponse {
version: string;
id: string;
};
endpoint: {
Endpoint: {
policy: {
applied: {
version: string;

View file

@ -74,7 +74,7 @@ export const FileAccordion = memo(({ alertData }: { alertData: Immutable<AlertDa
defaultMessage: 'Signer',
}
),
description: alertData.file.code_signature.subject_name,
description: alertData.file.Ext.code_signature[0].subject_name,
},
{
title: i18n.translate(

View file

@ -56,7 +56,7 @@ export const GeneralAccordion = memo(({ alertData }: { alertData: Immutable<Aler
defaultMessage: 'MalwareScore',
}
),
description: alertData.file.malware_classification.score,
description: alertData.file.Ext.malware_classification.score,
},
{
title: i18n.translate(

View file

@ -73,7 +73,7 @@ export const SourceProcessAccordion = memo(({ alertData }: { alertData: Immutabl
defaultMessage: 'MalwareScore',
}
),
description: alertData.process.malware_classification?.score || '-',
description: alertData.process.Ext.malware_classification?.score || '-',
},
{
title: i18n.translate(
@ -91,7 +91,7 @@ export const SourceProcessAccordion = memo(({ alertData }: { alertData: Immutabl
defaultMessage: 'Signer',
}
),
description: alertData.process.code_signature.subject_name,
description: alertData.process.Ext.code_signature[0].subject_name,
},
{
title: i18n.translate(
@ -100,7 +100,7 @@ export const SourceProcessAccordion = memo(({ alertData }: { alertData: Immutabl
defaultMessage: 'Username',
}
),
description: alertData.process.token.user,
description: alertData.process.Ext.token.user,
},
{
title: i18n.translate(
@ -109,7 +109,7 @@ export const SourceProcessAccordion = memo(({ alertData }: { alertData: Immutabl
defaultMessage: 'Domain',
}
),
description: alertData.process.token.domain,
description: alertData.process.Ext.token.domain,
},
];
}, [alertData]);

View file

@ -20,7 +20,7 @@ export const SourceProcessTokenAccordion = memo(
defaultMessage: 'SID',
}
),
description: alertData.process.token.sid,
description: alertData.process.Ext.token.sid,
},
{
title: i18n.translate(
@ -29,7 +29,7 @@ export const SourceProcessTokenAccordion = memo(
defaultMessage: 'Integrity Level',
}
),
description: alertData.process.token.integrity_level,
description: alertData.process.Ext.token.integrity_level,
},
];
}, [alertData]);

View file

@ -204,7 +204,7 @@ export const AlertIndex = memo(() => {
} else if (columnId === 'archived') {
return null;
} else if (columnId === 'malware_score') {
return row.file.malware_classification.score;
return row.file.Ext.malware_classification.score;
}
return null;
},

View file

@ -41,7 +41,7 @@ export const detailsError = (state: Immutable<HostState>) => state.detailsError;
* Returns the full policy response from the endpoint after a user modifies a policy.
*/
const detailsPolicyAppliedResponse = (state: Immutable<HostState>) =>
state.policyResponse && state.policyResponse.endpoint.policy.applied;
state.policyResponse && state.policyResponse.Endpoint.policy.applied;
/**
* Returns the response configurations from the endpoint after a user modifies a policy.
@ -179,6 +179,6 @@ export const showView: (state: HostState) => 'policy_response' | 'details' = cre
export const policyResponseStatus: (state: Immutable<HostState>) => string = createSelector(
(state) => state.policyResponse,
(policyResponse) => {
return (policyResponse && policyResponse?.endpoint?.policy?.applied?.status) || '';
return (policyResponse && policyResponse?.Endpoint?.policy?.applied?.status) || '';
}
);

View file

@ -97,15 +97,15 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
return [
getManagementUrl({
name: 'policyDetails',
policyId: details.endpoint.policy.applied.id,
policyId: details.Endpoint.policy.applied.id,
excludePrefix: true,
}),
getManagementUrl({
name: 'policyDetails',
policyId: details.endpoint.policy.applied.id,
policyId: details.Endpoint.policy.applied.id,
}),
];
}, [details.endpoint.policy.applied.id]);
}, [details.Endpoint.policy.applied.id]);
const policyDetailsClickHandler = useNavigateByRouterEventHandler(policyDetailsRoutePath);
@ -123,7 +123,7 @@ export const HostDetails = memo(({ details }: { details: HostMetadata }) => {
href={policyDetailsRouteUrl}
onClick={policyDetailsClickHandler}
>
{details.endpoint.policy.applied.name}
{details.Endpoint.policy.applied.name}
</EuiLink>
</>
),

View file

@ -50,13 +50,13 @@ describe('when on the hosts page', () => {
});
describe('when list data loads', () => {
const generatedPolicyStatuses: Array<
HostInfo['metadata']['endpoint']['policy']['applied']['status']
HostInfo['metadata']['Endpoint']['policy']['applied']['status']
> = [];
let firstPolicyID: string;
beforeEach(() => {
reactTestingLibrary.act(() => {
const hostListData = mockHostResultList({ total: 3 });
firstPolicyID = hostListData.hosts[0].metadata.endpoint.policy.applied.id;
firstPolicyID = hostListData.hosts[0].metadata.Endpoint.policy.applied.id;
[HostStatus.ERROR, HostStatus.ONLINE, HostStatus.OFFLINE].forEach((status, index) => {
hostListData.hosts[index] = {
metadata: hostListData.hosts[index].metadata,
@ -64,7 +64,7 @@ describe('when on the hosts page', () => {
};
});
hostListData.hosts.forEach((item, index) => {
generatedPolicyStatuses[index] = item.metadata.endpoint.policy.applied.status;
generatedPolicyStatuses[index] = item.metadata.Endpoint.policy.applied.status;
});
const action: AppAction = {
type: 'serverReturnedHostList',
@ -160,9 +160,11 @@ describe('when on the hosts page', () => {
overallStatus: HostPolicyResponseActionStatus = HostPolicyResponseActionStatus.success
) => {
const policyResponse = docGenerator.generatePolicyResponse();
policyResponse.endpoint.policy.applied.status = overallStatus;
policyResponse.endpoint.policy.applied.response.configurations.malware.status = overallStatus;
let downloadModelAction = policyResponse.endpoint.policy.applied.actions.find(
const malwareResponseConfigurations =
policyResponse.Endpoint.policy.applied.response.configurations.malware;
policyResponse.Endpoint.policy.applied.status = overallStatus;
malwareResponseConfigurations.status = overallStatus;
let downloadModelAction = policyResponse.Endpoint.policy.applied.actions.find(
(action) => action.name === 'download_model'
);
@ -172,19 +174,30 @@ describe('when on the hosts page', () => {
message: 'Failed to apply a portion of the configuration (kernel)',
status: overallStatus,
};
policyResponse.endpoint.policy.applied.actions.push(downloadModelAction);
policyResponse.Endpoint.policy.applied.actions.push(downloadModelAction);
}
if (
overallStatus === HostPolicyResponseActionStatus.failure ||
overallStatus === HostPolicyResponseActionStatus.warning
) {
downloadModelAction.message = 'no action taken';
}
store.dispatch({
type: 'serverReturnedHostPolicyResponse',
payload: {
policy_response: policyResponse,
},
// Make sure that at least one configuration has the above action, else
// we get into an out-of-sync condition
if (
malwareResponseConfigurations.concerned_actions.indexOf(downloadModelAction.name) === -1
) {
malwareResponseConfigurations.concerned_actions.push(downloadModelAction.name);
}
reactTestingLibrary.act(() => {
store.dispatch({
type: 'serverReturnedHostPolicyResponse',
payload: {
policy_response: policyResponse,
},
});
});
};
@ -236,7 +249,7 @@ describe('when on the hosts page', () => {
const policyDetailsLink = await renderResult.findByTestId('policyDetailsValue');
expect(policyDetailsLink).not.toBeNull();
expect(policyDetailsLink.getAttribute('href')).toEqual(
`#/management/policy/${hostDetails.metadata.endpoint.policy.applied.id}`
`#/management/policy/${hostDetails.metadata.Endpoint.policy.applied.id}`
);
});
@ -252,7 +265,7 @@ describe('when on the hosts page', () => {
});
const changedUrlAction = await userChangedUrlChecker;
expect(changedUrlAction.payload.pathname).toEqual(
`/management/policy/${hostDetails.metadata.endpoint.policy.applied.id}`
`/management/policy/${hostDetails.metadata.Endpoint.policy.applied.id}`
);
});
@ -361,6 +374,12 @@ describe('when on the hosts page', () => {
describe('when showing host Policy Response panel', () => {
let renderResult: ReturnType<typeof render>;
beforeEach(async () => {
coreStart.http.post.mockImplementation(async (requestOptions) => {
if (requestOptions.path === '/api/endpoint/metadata') {
return mockHostResultList({ total: 0 });
}
throw new Error(`POST to '${requestOptions.path}' does not have a mock response!`);
});
renderResult = render();
const policyStatusLink = await renderResult.findByTestId('policyStatusValue');
const userChangedUrlChecker = middlewareSpy.waitForAction('userChangedUrl');
@ -373,6 +392,8 @@ describe('when on the hosts page', () => {
});
});
afterEach(reactTestingLibrary.cleanup);
it('should hide the host details panel', async () => {
const hostDetailsFlyout = await renderResult.queryByTestId('hostDetailsFlyoutBody');
expect(hostDetailsFlyout).toBeNull();
@ -433,21 +454,29 @@ describe('when on the hosts page', () => {
});
});
it('should show a numbered badge if at least one action failed', () => {
it('should show a numbered badge if at least one action failed', async () => {
const policyResponseActionDispatched = middlewareSpy.waitForAction(
'serverReturnedHostPolicyResponse'
);
reactTestingLibrary.act(() => {
dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.failure);
});
const attentionBadge = renderResult.findAllByTestId(
await policyResponseActionDispatched;
const attentionBadge = await renderResult.findAllByTestId(
'hostDetailsPolicyResponseAttentionBadge'
);
expect(attentionBadge).not.toBeNull();
});
it('should show a numbered badge if at least one action has a warning', () => {
it('should show a numbered badge if at least one action has a warning', async () => {
const policyResponseActionDispatched = middlewareSpy.waitForAction(
'serverReturnedHostPolicyResponse'
);
reactTestingLibrary.act(() => {
dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.warning);
});
const attentionBadge = renderResult.findAllByTestId(
await policyResponseActionDispatched;
const attentionBadge = await renderResult.findAllByTestId(
'hostDetailsPolicyResponseAttentionBadge'
);
expect(attentionBadge).not.toBeNull();

View file

@ -154,13 +154,13 @@ export const HostList = () => {
},
},
{
field: 'metadata.endpoint.policy.applied',
field: 'metadata.Endpoint.policy.applied',
name: i18n.translate('xpack.securitySolution.endpointList.policy', {
defaultMessage: 'Policy',
}),
truncateText: true,
// eslint-disable-next-line react/display-name
render: (policy: HostInfo['metadata']['endpoint']['policy']['applied']) => {
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied']) => {
const toRoutePath = getManagementUrl({
name: 'policyDetails',
policyId: policy.id,
@ -181,12 +181,12 @@ export const HostList = () => {
},
},
{
field: 'metadata.endpoint.policy.applied',
field: 'metadata.Endpoint.policy.applied',
name: i18n.translate('xpack.securitySolution.endpointList.policyStatus', {
defaultMessage: 'Policy Status',
}),
// eslint-disable-next-line react/display-name
render: (policy: HostInfo['metadata']['endpoint']['policy']['applied'], item: HostInfo) => {
render: (policy: HostInfo['metadata']['Endpoint']['policy']['applied'], item: HostInfo) => {
const toRoutePath = getManagementUrl({
name: 'endpointPolicyResponse',
selected_host: item.metadata.host.id,

View file

@ -8,10 +8,9 @@ import * as path from 'path';
import yargs from 'yargs';
import * as url from 'url';
import fetch from 'node-fetch';
import seedrandom from 'seedrandom';
import { Client, ClientOptions } from '@elastic/elasticsearch';
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
import { EndpointDocGenerator, Event } from '../../common/endpoint/generate_data';
import { indexHostsAndAlerts } from '../../common/endpoint/index_data';
main();
@ -201,59 +200,26 @@ async function main() {
seed = Math.random().toString();
console.log(`No seed supplied, using random seed: ${seed}`);
}
const random = seedrandom(seed);
const startTime = new Date().getTime();
for (let i = 0; i < argv.numHosts; i++) {
const generator = new EndpointDocGenerator(random);
const timeBetweenDocs = 6 * 3600 * 1000; // 6 hours between metadata documents
const timestamp = new Date().getTime();
for (let j = 0; j < argv.numDocs; j++) {
generator.updateHostData();
generator.updatePolicyId();
await client.index({
index: argv.metadataIndex,
body: generator.generateHostMetadata(timestamp - timeBetweenDocs * (argv.numDocs - j - 1)),
op_type: 'create',
});
await client.index({
index: argv.policyIndex,
body: generator.generatePolicyResponse(
timestamp - timeBetweenDocs * (argv.numDocs - j - 1)
),
op_type: 'create',
});
await indexHostsAndAlerts(
client,
seed,
argv.numHosts,
argv.numDocs,
argv.metadataIndex,
argv.policyIndex,
argv.eventIndex,
argv.alertsPerHost,
{
ancestors: argv.ancestors,
generations: argv.generations,
children: argv.children,
relatedEvents: argv.relatedEvents,
relatedAlerts: argv.relatedAlerts,
percentWithRelated: argv.percentWithRelated,
percentTerminated: argv.percentTerminated,
alwaysGenMaxChildrenPerNode: argv.maxChildrenPerNode,
}
const alertGenerator = generator.alertsGenerator(
argv.alertsPerHost,
argv.ancestors,
argv.generations,
argv.children,
argv.relatedEvents,
argv.relatedAlerts,
argv.percentWithRelated,
argv.percentTerminated,
argv.maxChildrenPerNode
);
let result = alertGenerator.next();
while (!result.done) {
let k = 0;
const resolverDocs: Event[] = [];
while (k < 1000 && !result.done) {
resolverDocs.push(result.value);
result = alertGenerator.next();
k++;
}
const body = resolverDocs.reduce(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(array: Array<Record<string, any>>, doc) => (
array.push({ create: { _index: argv.eventIndex } }, doc), array
),
[]
);
await client.bulk({ body, refresh: 'true' });
}
}
);
console.log(`Creating and indexing documents took: ${new Date().getTime() - startTime}ms`);
}

View file

@ -237,7 +237,7 @@ describe('test endpoint route', () => {
expect(routeConfig.options).toEqual({ authRequired: true });
expect(mockResponse.ok).toBeCalled();
const result = mockResponse.ok.mock.calls[0][0]?.body as HostInfo;
expect(result).toHaveProperty('metadata.endpoint');
expect(result).toHaveProperty('metadata.Endpoint');
expect(result.host_status).toEqual(HostStatus.ONLINE);
});

View file

@ -7,12 +7,19 @@ import expect from '@kbn/expect/expect.js';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { AlertData } from '../../../../../plugins/security_solution/common/endpoint_alerts/types';
import { eventsIndexPattern } from '../../../../../plugins/security_solution/common/endpoint/constants';
import { deleteEventsStream, deleteMetadataStream } from '../data_stream_helper';
import {
deleteEventsStream,
deleteMetadataStream,
deletePolicyStream,
} from '../data_stream_helper';
import { indexHostsAndAlerts } from '../../../../../plugins/security_solution/common/endpoint/index_data';
/**
* The number of alert documents in the es archive.
*/
const numberOfAlertsInFixture = 12;
const numberOfHosts = 3;
const numberOfAlertsPerHost = 4;
const numberOfAlertsInFixture = numberOfHosts * numberOfAlertsPerHost;
/**
* The default number of entries returned when no page_size is specified.
@ -57,10 +64,9 @@ const ES_QUERY_MISSING = {
};
export default function ({ getService }: FtrProviderContext) {
const esArchiver = getService('esArchiver');
const supertest = getService('supertest');
const es = getService('legacyEs');
const client = getService('es');
const nextPrevPrefixQuery = "query=(language:kuery,query:'')";
const nextPrevPrefixDateRange = "date_range=(from:'2018-01-10T00:00:00.000Z',to:now)";
const nextPrevPrefixSort = 'sort=@timestamp';
@ -73,8 +79,17 @@ export default function ({ getService }: FtrProviderContext) {
describe('Endpoint alert API', () => {
describe('when data is in elasticsearch', () => {
before(async () => {
await esArchiver.load('endpoint/alerts/api_feature', { useCreate: true });
await esArchiver.load('endpoint/alerts/host_api_feature', { useCreate: true });
await indexHostsAndAlerts(
client,
'alerts-seed',
numberOfHosts,
1,
'metrics-endpoint.metadata-default-1',
'metrics-endpoint.policy-default-1',
'events-endpoint-1',
numberOfAlertsPerHost
);
const res = await es.search({
index: eventsIndexPattern,
body: ES_QUERY_MISSING,
@ -85,7 +100,11 @@ export default function ({ getService }: FtrProviderContext) {
after(async () => {
// the endpoint uses data streams and es archiver does not support deleting them at the moment so we need
// to do it manually
await Promise.all([deleteEventsStream(getService), deleteMetadataStream(getService)]);
await Promise.all([
deleteEventsStream(getService),
deleteMetadataStream(getService),
deletePolicyStream(getService),
]);
});
it('should not support POST requests', async () => {
@ -159,7 +178,7 @@ export default function ({ getService }: FtrProviderContext) {
expect(body.message).to.contain('Value must be equal to or greater than [1]');
});
it('should return links to the next and previous pages using cursor-based pagination', async () => {
it('should return working link to the next page using cursor-based pagination', async () => {
const { body } = await supertest
.get('/api/endpoint/alerts?page_index=0')
.set('kbn-xsrf', 'xxx')
@ -170,51 +189,75 @@ export default function ({ getService }: FtrProviderContext) {
expect(body.next).to.eql(
`/api/endpoint/alerts?${nextPrevPrefix}&after=${lastTimestampFirstPage}&after=${lastEventIdFirstPage}`
);
});
it('should return data using `next` link', async () => {
const { body } = await supertest
.get(
`/api/endpoint/alerts?${nextPrevPrefix}&after=1584044338719&after=66008e21-2493-4b15-a937-939ea228064a`
)
const { body: nextBody } = await supertest
.get(body.next)
.set('kbn-xsrf', 'xxx')
.expect(200);
expect(body.alerts.length).to.eql(defaultPageSize);
const firstTimestampNextPage = body.alerts[0]['@timestamp'];
const firstEventIdNextPage = body.alerts[0].event.id;
expect(body.prev).to.eql(
expect(nextBody.alerts.length).to.eql(2);
const firstTimestampNextPage = nextBody.alerts[0]['@timestamp'];
const firstEventIdNextPage = nextBody.alerts[0].event.id;
expect(nextBody.prev).to.eql(
`/api/endpoint/alerts?${nextPrevPrefix}&before=${firstTimestampNextPage}&before=${firstEventIdNextPage}`
);
});
it('should return data using `prev` link', async () => {
it('should return working link to the prev page using cursor-based pagination', async () => {
const { body } = await supertest
.get(
`/api/endpoint/alerts?${nextPrevPrefix}&before=1542789412000&before=823d814d-fa0c-4e53-a94c-f6b296bb965b`
)
.get('/api/endpoint/alerts?page_index=1')
.set('kbn-xsrf', 'xxx')
.expect(200);
expect(body.alerts.length).to.eql(10);
expect(body.alerts.length).to.eql(2);
const firstTimestamp = body.alerts[0]['@timestamp'];
const firstEventId = body.alerts[0].event.id;
expect(body.prev).to.eql(
`/api/endpoint/alerts?${nextPrevPrefix}&before=${firstTimestamp}&before=${firstEventId}`
);
const { body: prevBody } = await supertest
.get(body.prev)
.set('kbn-xsrf', 'xxx')
.expect(200);
expect(prevBody.alerts.length).to.eql(10);
const lastTimestampFirstPage = prevBody.alerts[9]['@timestamp'];
const lastEventIdFirstPage = prevBody.alerts[9].event.id;
expect(prevBody.next).to.eql(
`/api/endpoint/alerts?${nextPrevPrefix}&after=${lastTimestampFirstPage}&after=${lastEventIdFirstPage}`
);
});
it('should return no results when `before` is requested past beginning of first page', async () => {
const { body } = await supertest
.get('/api/endpoint/alerts')
.set('kbn-xsrf', 'xxx')
.expect(200);
const { body: emptyBody } = await supertest
.get(
`/api/endpoint/alerts?${nextPrevPrefix}&before=1584044338726&before=5ff1a4ec-758e-49e7-89aa-2c6821fe6b54`
`/api/endpoint/alerts?${nextPrevPrefix}&before=${
body.alerts[0]['@timestamp'] + 1
}&before=5ff1a4ec-758e-49e7-89aa-2c6821fe6b54`
)
.set('kbn-xsrf', 'xxx')
.expect(200);
expect(body.alerts.length).to.eql(0);
expect(emptyBody.alerts.length).to.eql(0);
});
it('should return no results when `after` is requested past end of last page, descending', async () => {
const { body } = await supertest
.get('/api/endpoint/alerts?page_index=1')
.set('kbn-xsrf', 'xxx')
.expect(200);
const { body: emptyBody } = await supertest
.get(
`/api/endpoint/alerts?${nextPrevPrefix}&after=1584044338612&after=6d75d498-3cca-45ad-a304-525b95ae0412`
`/api/endpoint/alerts?${nextPrevPrefix}&after=${
body.alerts[1]['@timestamp'] - 1
}&after=6d75d498-3cca-45ad-a304-525b95ae0412`
)
.set('kbn-xsrf', 'xxx')
.expect(200);
expect(body.alerts.length).to.eql(0);
expect(emptyBody.alerts.length).to.eql(0);
});
it('alerts api should return data using `before` by custom sort parameter, descending', async () => {
@ -346,7 +389,12 @@ export default function ({ getService }: FtrProviderContext) {
});
it('should filter results of alert data using rison-encoded filters', async () => {
const hostname = 'Host-abmfhmc5ku';
const { body: firstBody } = await supertest
.get('/api/endpoint/alerts?page_index=0')
.set('kbn-xsrf', 'xxx')
.expect(200);
const hostname = firstBody.alerts[0].host.hostname;
const { body } = await supertest
.get(
`/api/endpoint/alerts?filters=!((%27%24state%27%3A(store%3AappState)%2Cmeta%3A(alias%3A!n%2Cdisabled%3A!f%2Ckey%3Ahost.hostname%2Cnegate%3A!f%2Cparams%3A(query%3A${hostname})%2Ctype%3Aphrase)%2Cquery%3A(match_phrase%3A(host.hostname%3A${hostname}))))`
@ -361,7 +409,12 @@ export default function ({ getService }: FtrProviderContext) {
});
it('should filter results of alert data using KQL', async () => {
const agentID = '7cf9f7a3-28a6-4d1e-bb45-005aa28f18d0';
const { body: firstBody } = await supertest
.get('/api/endpoint/alerts?page_index=0')
.set('kbn-xsrf', 'xxx')
.expect(200);
const agentID = firstBody.alerts[0].agent.id;
const { body } = await supertest
.get(
`/api/endpoint/alerts?query=(language%3Akuery%2Cquery%3A%27agent.id%20%3A%20"${agentID}"%27)`

View file

@ -160,18 +160,18 @@ export default function ({ getService }: FtrProviderContext) {
expect(body.request_page_index).to.eql(0);
});
it('metadata api should return page based on host.os.variant filter.', async () => {
it('metadata api should return page based on host.os.Ext.variant filter.', async () => {
const variantValue = 'Windows Pro';
const { body } = await supertest
.post('/api/endpoint/metadata')
.set('kbn-xsrf', 'xxx')
.send({
filter: `host.os.variant:${variantValue}`,
filter: `host.os.Ext.variant:${variantValue}`,
})
.expect(200);
expect(body.total).to.eql(2);
const resultOsVariantValue: Set<string> = new Set(
body.hosts.map((hostInfo: Record<string, any>) => hostInfo.metadata.host.os.variant)
body.hosts.map((hostInfo: Record<string, any>) => hostInfo.metadata.host.os.Ext.variant)
);
expect(Array.from(resultOsVariantValue)).to.eql([variantValue]);
expect(body.hosts.length).to.eql(2);
@ -204,15 +204,14 @@ export default function ({ getService }: FtrProviderContext) {
.post('/api/endpoint/metadata')
.set('kbn-xsrf', 'xxx')
.send({
filter: `not endpoint.policy.applied.status:success`,
filter: `not Endpoint.policy.applied.status:success`,
})
.expect(200);
const statuses: Set<string> = new Set(
body.hosts.map(
(hostInfo: Record<string, any>) => hostInfo.metadata.endpoint.policy.applied.status
(hostInfo: Record<string, any>) => hostInfo.metadata.Endpoint.policy.applied.status
)
);
expect(statuses.size).to.eql(1);
expect(Array.from(statuses)).to.eql(['failure']);
});

View file

@ -27,7 +27,7 @@ export default function ({ getService }: FtrProviderContext) {
.expect(200);
expect(body.policy_response.host.id).to.eql(expectedHostId);
expect(body.policy_response.endpoint.policy).to.not.be(undefined);
expect(body.policy_response.Endpoint.policy).to.not.be(undefined);
});
it('should return not found if host has no policy response', async () => {

View file

@ -15,7 +15,7 @@
"id": "11488bae-880b-4e7b-8d28-aac2aa9de816"
}
},
"endpoint": {
"Endpoint": {
"policy": {
"applied": {
"name": "Default",
@ -44,7 +44,9 @@
"full": "Windows 10",
"name": "windows 10.0",
"version": "10.0",
"variant" : "Windows Pro"
"Ext": {
"variant" : "Windows Pro"
}
}
}
}
@ -68,7 +70,7 @@
"id": "92ac1ce0-e1f7-409e-8af6-f17e97b1fc71"
}
},
"endpoint": {
"Endpoint": {
"policy": {
"applied": {
"name": "Default",
@ -96,7 +98,9 @@
"full": "Windows Server 2016",
"name": "windows 10.0",
"version": "10.0",
"variant" : "Windows Server"
"Ext": {
"variant" : "Windows Server"
}
}
}
}
@ -120,7 +124,7 @@
"id": "023fa40c-411d-4188-a941-4147bfadd095"
}
},
"endpoint": {
"Endpoint": {
"policy": {
"applied": {
"name": "Default",
@ -146,7 +150,9 @@
"full": "Windows 10",
"name": "windows 10.0",
"version": "10.0",
"variant" : "Windows Pro"
"Ext": {
"variant" : "Windows Pro"
}
}
}
}
@ -170,7 +176,7 @@
"id": "11488bae-880b-4e7b-8d28-aac2aa9de816"
}
},
"endpoint": {
"Endpoint": {
"policy": {
"applied": {
"name": "Default",
@ -199,7 +205,9 @@
"full": "Windows Server 2016",
"name": "windows 10.0",
"version": "10.0",
"variant" : "Windows Server 2016"
"Ext": {
"variant" : "Windows Server 2016"
}
}
}
}
@ -223,7 +231,7 @@
"id": "92ac1ce0-e1f7-409e-8af6-f17e97b1fc71"
}
},
"endpoint": {
"Endpoint": {
"policy": {
"applied": {
"name": "Default",
@ -250,7 +258,9 @@
"full": "Windows Server 2012",
"name": "windows 6.2",
"version": "6.2",
"variant" : "Windows Server 2012"
"Ext": {
"variant" : "Windows Server 2012"
}
}
}
}
@ -274,7 +284,7 @@
"id": "023fa40c-411d-4188-a941-4147bfadd095"
}
},
"endpoint": {
"Endpoint": {
"policy": {
"applied": {
"name": "With Eventing",
@ -301,7 +311,9 @@
"full": "Windows Server 2012",
"name": "windows 6.2",
"version": "6.2",
"variant" : "Windows Server 2012"
"Ext": {
"variant" : "Windows Server 2012"
}
}
}
}
@ -325,7 +337,7 @@
"id": "11488bae-880b-4e7b-8d28-aac2aa9de816"
}
},
"endpoint": {
"Endpoint": {
"policy": {
"applied": {
"name": "With Eventing",
@ -353,7 +365,9 @@
"full": "Windows Server 2012R2",
"name": "windows 6.3",
"version": "6.3",
"variant" : "Windows Server 2012 R2"
"Ext": {
"variant" : "Windows Server 2012 R2"
}
}
}
}
@ -377,7 +391,7 @@
"id": "92ac1ce0-e1f7-409e-8af6-f17e97b1fc71"
}
},
"endpoint": {
"Endpoint": {
"policy": {
"applied": {
"name": "Default",
@ -404,7 +418,9 @@
"full": "Windows Server 2012R2",
"name": "windows 6.3",
"version": "6.3",
"variant" : "Windows Server 2012 R2"
"Ext": {
"variant" : "Windows Server 2012 R2"
}
}
}
}
@ -428,7 +444,7 @@
"id": "023fa40c-411d-4188-a941-4147bfadd095"
}
},
"endpoint": {
"Endpoint": {
"policy": {
"applied": {
"name": "With Eventing",
@ -455,7 +471,9 @@
"full": "Windows Server 2012",
"name": "windows 6.2",
"version": "6.2",
"variant" : "Windows Server 2012"
"Ext": {
"variant" : "Windows Server 2012"
}
}
}
}