Skip to content

Commit

Permalink
Merge/restyle nodes table (#69098)
Browse files Browse the repository at this point in the history
Adds panel views and drilldowns to Resolver
  • Loading branch information
bkimmel authored Jun 18, 2020
1 parent bdb6592 commit 700f53d
Show file tree
Hide file tree
Showing 29 changed files with 2,557 additions and 553 deletions.
56 changes: 53 additions & 3 deletions x-pack/plugins/security_solution/common/endpoint/generate_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ interface EventOptions {
eventType?: string;
eventCategory?: string | string[];
processName?: string;
pid?: number;
parentPid?: number;
extensions?: object;
}

const Windows: OSFields[] = [
Expand Down Expand Up @@ -471,12 +474,36 @@ export class EndpointDocGenerator {
* @param options - Allows event field values to be specified
*/
public generateEvent(options: EventOptions = {}): EndpointEvent {
const processName = options.processName ? options.processName : randomProcessName();
const detailRecordForEventType =
options.extensions ||
((eventCategory) => {
if (eventCategory === 'registry') {
return { registry: { key: `HKLM/Windows/Software/${this.randomString(5)}` } };
}
if (eventCategory === 'network') {
return {
network: {
direction: this.randomChoice(['inbound', 'outbound']),
forwarded_ip: `${this.randomIP()}`,
},
};
}
if (eventCategory === 'file') {
return { file: { path: 'C:\\My Documents\\business\\January\\processName' } };
}
if (eventCategory === 'dns') {
return { dns: { question: { name: `${this.randomIP()}` } } };
}
return {};
})(options.eventCategory);
return {
'@timestamp': options.timestamp ? options.timestamp : new Date().getTime(),
agent: { ...this.commonInfo.agent, type: 'endpoint' },
ecs: {
version: '1.4.0',
},
...detailRecordForEventType,
event: {
category: options.eventCategory ? options.eventCategory : 'process',
kind: 'event',
Expand All @@ -485,9 +512,30 @@ export class EndpointDocGenerator {
},
host: this.commonInfo.host,
process: {
pid:
'pid' in options && typeof options.pid !== 'undefined' ? options.pid : this.randomN(5000),
executable: `C:\\${processName}`,
args: `"C:\\${processName}" \\${this.randomString(3)}`,
code_signature: {
status: 'trusted',
subject_name: 'Microsoft',
},
hash: { md5: this.seededUUIDv4() },
entity_id: options.entityID ? options.entityID : this.randomString(10),
parent: options.parentEntityID ? { entity_id: options.parentEntityID } : undefined,
name: options.processName ? options.processName : randomProcessName(),
parent: options.parentEntityID
? {
entity_id: options.parentEntityID,
pid:
'parentPid' in options && typeof options.parentPid !== 'undefined'
? options.parentPid
: this.randomN(5000),
}
: undefined,
name: processName,
},
user: {
domain: this.randomString(10),
name: this.randomString(10),
},
};
}
Expand Down Expand Up @@ -692,6 +740,8 @@ export class EndpointDocGenerator {
ancestor = this.generateEvent({
timestamp,
parentEntityID: ancestor.process.entity_id,
parentPid: ancestor.process.pid,
pid: this.randomN(5000),
});
events.push(ancestor);
timestamp = timestamp + 1000;
Expand Down Expand Up @@ -1117,7 +1167,7 @@ export class EndpointDocGenerator {
return [...this.randomNGenerator(255, 6)].map((x) => x.toString(16)).join('-');
}

private randomIP(): string {
public randomIP(): string {
return [10, ...this.randomNGenerator(255, 3)].map((x) => x.toString()).join('.');
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 { EndpointDocGenerator } from '../generate_data';
import { descriptiveName } from './event';

describe('Event descriptive names', () => {
let generator: EndpointDocGenerator;
beforeEach(() => {
generator = new EndpointDocGenerator('seed');
});

it('returns the right name for a registry event', () => {
const extensions = { registry: { key: `HKLM/Windows/Software/abc` } };
const event = generator.generateEvent({ eventCategory: 'registry', extensions });
expect(descriptiveName(event)).toEqual({ subject: `HKLM/Windows/Software/abc` });
});

it('returns the right name for a network event', () => {
const randomIP = `${generator.randomIP()}`;
const extensions = { network: { direction: 'outbound', forwarded_ip: randomIP } };
const event = generator.generateEvent({ eventCategory: 'network', extensions });
expect(descriptiveName(event)).toEqual({ subject: `${randomIP}`, descriptor: 'outbound' });
});

it('returns the right name for a file event', () => {
const extensions = { file: { path: 'C:\\My Documents\\business\\January\\processName' } };
const event = generator.generateEvent({ eventCategory: 'file', extensions });
expect(descriptiveName(event)).toEqual({
subject: 'C:\\My Documents\\business\\January\\processName',
});
});

it('returns the right name for a dns event', () => {
const extensions = { dns: { question: { name: `${generator.randomIP()}` } } };
const event = generator.generateEvent({ eventCategory: 'dns', extensions });
expect(descriptiveName(event)).toEqual({ subject: extensions.dns.question.name });
});
});
92 changes: 92 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/models/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,95 @@ export function parentEntityId(event: ResolverEvent): string | undefined {
}
return event.process.parent?.entity_id;
}

/**
* @param event The event to get the category for
*/
export function primaryEventCategory(event: ResolverEvent): string | undefined {
// Returning "Process" as a catch-all here because it seems pretty general
if (isLegacyEvent(event)) {
const legacyFullType = event.endgame.event_type_full;
if (legacyFullType) {
return legacyFullType;
}
} else {
const eventCategories = event.event.category;
const category = typeof eventCategories === 'string' ? eventCategories : eventCategories[0];

return category;
}
}

/**
* ECS event type will be things like 'creation', 'deletion', 'access', etc.
* see: https://www.elastic.co/guide/en/ecs/current/ecs-event.html
* @param event The ResolverEvent to get the ecs type for
*/
export function ecsEventType(event: ResolverEvent): Array<string | undefined> {
if (isLegacyEvent(event)) {
return [event.endgame.event_subtype_full];
}
return typeof event.event.type === 'string' ? [event.event.type] : event.event.type;
}

/**
* #Descriptive Names For Related Events:
*
* The following section provides facilities for deriving **Descriptive Names** for ECS-compliant event data.
* There are drawbacks to trying to do this: It *will* require ongoing maintenance. It presents temptations to overarticulate.
* On balance, however, it seems that the benefit of giving the user some form of information they can recognize & scan outweighs the drawbacks.
*/
type DeepPartial<T> = T extends object ? { [K in keyof T]?: DeepPartial<T[K]> } : T;
/**
* Based on the ECS category of the event, attempt to provide a more descriptive name
* (e.g. the `event.registry.key` for `registry` or the `dns.question.name` for `dns`, etc.).
* This function returns the data in the form of `{subject, descriptor}` where `subject` will
* tend to be the more distinctive term (e.g. 137.213.212.7 for a network event) and the
* `descriptor` can be used to present more useful/meaningful view (e.g. `inbound 137.213.212.7`
* in the example above).
* see: https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html
* @param event The ResolverEvent to get the descriptive name for
* @returns { descriptiveName } An attempt at providing a readable name to the user
*/
export function descriptiveName(event: ResolverEvent): { subject: string; descriptor?: string } {
if (isLegacyEvent(event)) {
return { subject: eventName(event) };
}

// To be somewhat defensive, we'll check for the presence of these.
const partialEvent: DeepPartial<ResolverEvent> = event;

/**
* This list of attempts can be expanded/adjusted as the underlying model changes over time:
*/

// Stable, per ECS 1.5: https://www.elastic.co/guide/en/ecs/current/ecs-allowed-values-event-category.html

if (partialEvent.network?.forwarded_ip) {
return {
subject: String(partialEvent.network?.forwarded_ip),
descriptor: String(partialEvent.network?.direction),
};
}

if (partialEvent.file?.path) {
return {
subject: String(partialEvent.file?.path),
};
}

// Extended categories (per ECS 1.5):
const pathOrKey = partialEvent.registry?.path || partialEvent.registry?.key;
if (pathOrKey) {
return {
subject: String(pathOrKey),
};
}

if (partialEvent.dns?.question?.name) {
return { subject: String(partialEvent.dns?.question?.name) };
}

// Fall back on entityId if we can't fish a more descriptive name out.
return { subject: entityId(event) };
}
24 changes: 24 additions & 0 deletions x-pack/plugins/security_solution/common/endpoint/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,14 +444,38 @@ export interface EndpointEvent {
kind: string;
};
host: Host;
network?: {
direction: unknown;
forwarded_ip: unknown;
};
dns?: {
question: { name: unknown };
};
process: {
entity_id: string;
name: string;
executable?: string;
args?: string;
code_signature?: {
status?: string;
subject_name: string;
};
pid?: number;
hash?: {
md5: string;
};
parent?: {
entity_id: string;
name?: string;
pid?: number;
};
};
user?: {
domain?: string;
name: string;
};
file?: { path: unknown };
registry?: { path: unknown; key: unknown };
}

export type ResolverEvent = EndpointEvent | LegacyEndpointEvent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,39 @@ import {
} from '../../../common/endpoint_alerts/types';
import { ImmutableMiddlewareFactory } from '../../common/store';
import { cloneHttpFetchQuery } from '../../common/utils/clone_http_fetch_query';

import {
isOnAlertPage,
apiQueryParams,
hasSelectedAlert,
uiQueryParams,
hasSelectedAlert,
isAlertPageTabChange,
} from './selectors';

export const alertMiddlewareFactory: ImmutableMiddlewareFactory<AlertListState> = (
coreStart,
depsStart
) => {
let lastSelectedAlert: string | null = null;
/**
* @returns <boolean> true once per change of `selectedAlert` in query params.
*
* As opposed to `hasSelectedAlert` which always returns true if the alert is present
* query params, which can cause unnecessary requests and re-renders in some cases.
*/
const selectedAlertHasChanged = (params: ReturnType<typeof uiQueryParams>): boolean => {
const { selected_alert: selectedAlert } = params;
const shouldNotChange = selectedAlert === lastSelectedAlert;
if (shouldNotChange) {
return false;
}
if (typeof selectedAlert !== 'string') {
return false;
}
lastSelectedAlert = selectedAlert;
return true;
};

async function fetchIndexPatterns(): Promise<IIndexPattern[]> {
const { indexPatterns } = depsStart.data;
const fields = await indexPatterns.getFieldsForWildcard({
Expand All @@ -50,7 +71,7 @@ export const alertMiddlewareFactory: ImmutableMiddlewareFactory<AlertListState>
});
api.dispatch({ type: 'serverReturnedAlertsData', payload: listResponse });

if (hasSelectedAlert(state)) {
if (hasSelectedAlert(state) && selectedAlertHasChanged(uiQueryParams(state))) {
const uiParams = uiQueryParams(state);
const detailsResponse: AlertDetails = await coreStart.http.get(
`/api/endpoint/alerts/${uiParams.selected_alert}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@ export function factory(processes: ResolverEvent[]): IndexedProcessTree {
currentProcessAdjacencyMap.parent = uniqueParentPid;
}
} else {
idToChildren.set(uniqueParentPid, [process]);

if (uniqueParentPid) {
idToChildren.set(uniqueParentPid, [process]);
/**
* Get the parent's map, otherwise set an empty one
*/
Expand Down
Loading

0 comments on commit 700f53d

Please sign in to comment.