Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SIEM][Detection Engine] Modified gap detection util to accept all dateMath formats #56055

Merged
merged 10 commits into from
Jan 31, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,29 @@
*/

import moment from 'moment';
import sinon from 'sinon';

import { generateId, parseInterval, getDriftTolerance, getGapBetweenRuns } from './utils';
import {
generateId,
parseInterval,
parseScheduleDates,
getDriftTolerance,
getGapBetweenRuns,
} from './utils';

describe('utils', () => {
const anchor = '2020-01-01T06:06:06.666Z';
const unix = moment(anchor).valueOf();
let nowDate = moment('2020-01-01T00:00:00.000Z');
let clock: sinon.SinonFakeTimers;

beforeEach(() => {
nowDate = moment('2020-01-01T00:00:00.000Z');
clock = sinon.useFakeTimers(unix);
});

afterEach(() => {
clock.restore();
});

describe('generateId', () => {
Expand All @@ -27,7 +42,7 @@ describe('utils', () => {
});
});

describe('getIntervalMilliseconds', () => {
describe('parseInterval', () => {
test('it returns a duration when given one that is valid', () => {
const duration = parseInterval('5m');
expect(duration).not.toBeNull();
Expand All @@ -40,8 +55,36 @@ describe('utils', () => {
});
});

describe('getDriftToleranceMilliseconds', () => {
test('it returns a drift tolerance in milliseconds of 1 minute when from overlaps to by 1 minute and the interval is 5 minutes', () => {
describe('parseScheduleDates', () => {
test('it returns a moment when given an ISO string', () => {
const result = parseScheduleDates('2020-01-01T00:00:00.000Z');
expect(result).not.toBeNull();
expect(result).toEqual(moment('2020-01-01T00:00:00.000Z'));
});

test('it returns a moment when given `now`', () => {
const result = parseScheduleDates('now');

expect(result).not.toBeNull();
expect(moment.isMoment(result)).toBeTruthy();
});

test('it returns a moment when given `now-x`', () => {
const result = parseScheduleDates('now-6m');

expect(result).not.toBeNull();
expect(moment.isMoment(result)).toBeTruthy();
});

test('it returns null when given a string that is not an ISO string, `now` or `now-x`', () => {
const result = parseScheduleDates('invalid');

expect(result).toBeNull();
});
});

describe('getDriftTolerance', () => {
test('it returns a drift tolerance in milliseconds of 1 minute when "from" overlaps "to" by 1 minute and the interval is 5 minutes', () => {
const drift = getDriftTolerance({
from: 'now-6m',
to: 'now',
Expand All @@ -51,7 +94,7 @@ describe('utils', () => {
expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds());
});

test('it returns a drift tolerance of 0 when from equals the interval', () => {
test('it returns a drift tolerance of 0 when "from" equals the interval', () => {
const drift = getDriftTolerance({
from: 'now-5m',
to: 'now',
Expand All @@ -60,7 +103,7 @@ describe('utils', () => {
expect(drift?.asMilliseconds()).toEqual(0);
});

test('it returns a drift tolerance of 5 minutes when from is 10 minutes but the interval is 5 minutes', () => {
test('it returns a drift tolerance of 5 minutes when "from" is 10 minutes but the interval is 5 minutes', () => {
const drift = getDriftTolerance({
from: 'now-10m',
to: 'now',
Expand All @@ -70,7 +113,7 @@ describe('utils', () => {
expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds());
});

test('it returns a drift tolerance of 10 minutes when from is 10 minutes ago and the interval is 0', () => {
test('it returns a drift tolerance of 10 minutes when "from" is 10 minutes ago and the interval is 0', () => {
const drift = getDriftTolerance({
from: 'now-10m',
to: 'now',
Expand All @@ -80,36 +123,61 @@ describe('utils', () => {
expect(drift?.asMilliseconds()).toEqual(moment.duration(10, 'minutes').asMilliseconds());
});

test('returns null if the "to" is not "now" since we have limited support for date math', () => {
test('returns a drift tolerance of 1 minute when "from" is invalid and defaults to "now-6m" and interval is 5 minutes', () => {
const drift = getDriftTolerance({
from: 'now-6m',
to: 'invalid', // if not set to "now" this function returns null
interval: moment.duration(1000, 'milliseconds'),
from: 'invalid',
to: 'now',
interval: moment.duration(5, 'minutes'),
});
expect(drift).toBeNull();
expect(drift).not.toBeNull();
expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds());
});

test('returns null if the "from" does not start with "now-" since we have limited support for date math', () => {
test('returns a drift tolerance of 1 minute when "from" does not include `now` and defaults to "now-6m" and interval is 5 minutes', () => {
const drift = getDriftTolerance({
from: 'valid', // if not set to "now-x" where x is an interval such as 6m
from: '10m',
to: 'now',
interval: moment.duration(1000, 'milliseconds'),
interval: moment.duration(5, 'minutes'),
});
expect(drift).not.toBeNull();
expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds());
});

test('returns a drift tolerance of 4 minutes when "to" is "now-x", from is a valid input and interval is 5 minute', () => {
const drift = getDriftTolerance({
from: 'now-10m',
to: 'now-1m',
interval: moment.duration(5, 'minutes'),
});
expect(drift).toBeNull();
expect(drift).not.toBeNull();
expect(drift?.asMilliseconds()).toEqual(moment.duration(4, 'minutes').asMilliseconds());
});

test('returns null if the "from" starts with "now-" but has a string instead of an integer', () => {
test('it returns expected drift tolerance when "from" is an ISO string', () => {
const drift = getDriftTolerance({
from: 'now-dfdf', // if not set to "now-x" where x is an interval such as 6m
from: moment()
.subtract(10, 'minutes')
.toISOString(),
to: 'now',
interval: moment.duration(1000, 'milliseconds'),
interval: moment.duration(5, 'minutes'),
});
expect(drift).toBeNull();
expect(drift).not.toBeNull();
expect(drift?.asMilliseconds()).toEqual(moment.duration(5, 'minutes').asMilliseconds());
});

test('it returns expected drift tolerance when "to" is an ISO string', () => {
const drift = getDriftTolerance({
from: 'now-6m',
to: moment().toISOString(),
interval: moment.duration(5, 'minutes'),
});
expect(drift).not.toBeNull();
expect(drift?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds());
});
});

describe('getGapBetweenRuns', () => {
test('it returns a gap of 0 when from and interval match each other and the previous started was from the previous interval time', () => {
test('it returns a gap of 0 when "from" and interval match each other and the previous started was from the previous interval time', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(5, 'minutes'),
interval: '5m',
Expand All @@ -121,7 +189,7 @@ describe('utils', () => {
expect(gap?.asMilliseconds()).toEqual(0);
});

test('it returns a negative gap of 1 minute when from overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => {
test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(5, 'minutes'),
interval: '5m',
Expand All @@ -133,7 +201,7 @@ describe('utils', () => {
expect(gap?.asMilliseconds()).toEqual(moment.duration(-1, 'minute').asMilliseconds());
});

test('it returns a negative gap of 5 minutes when from overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => {
test('it returns a negative gap of 5 minutes when "from" overlaps to by 1 minute and the previousStartedAt was 5 minutes ago', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(5, 'minutes'),
interval: '5m',
Expand All @@ -145,7 +213,7 @@ describe('utils', () => {
expect(gap?.asMilliseconds()).toEqual(moment.duration(-5, 'minute').asMilliseconds());
});

test('it returns a negative gap of 1 minute when from overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => {
test('it returns a negative gap of 1 minute when "from" overlaps to by 1 minute and the previousStartedAt was 10 minutes ago and so was the interval', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone().subtract(10, 'minutes'),
interval: '10m',
Expand Down Expand Up @@ -233,26 +301,28 @@ describe('utils', () => {
expect(gap).toBeNull();
});

test('it returns null if from is an invalid string such as "invalid"', () => {
test('it returns the expected result when "from" is an invalid string such as "invalid"', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone(),
previousStartedAt: nowDate.clone().subtract(7, 'minutes'),
interval: '5m',
from: 'invalid', // if not set to "now-x" where x is an interval such as 6m
from: 'invalid',
to: 'now',
now: nowDate.clone(),
});
expect(gap).toBeNull();
expect(gap?.asMilliseconds()).not.toBeNull();
expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds());
});

test('it returns null if to is an invalid string such as "invalid"', () => {
test('it returns the expected result when "to" is an invalid string such as "invalid"', () => {
const gap = getGapBetweenRuns({
previousStartedAt: nowDate.clone(),
previousStartedAt: nowDate.clone().subtract(7, 'minutes'),
interval: '5m',
from: 'now-5m',
to: 'invalid', // if not set to "now" this function returns null
from: 'now-6m',
to: 'invalid',
now: nowDate.clone(),
});
expect(gap).toBeNull();
expect(gap?.asMilliseconds()).not.toBeNull();
expect(gap?.asMilliseconds()).toEqual(moment.duration(1, 'minute').asMilliseconds());
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
import { createHash } from 'crypto';
import moment from 'moment';
import dateMath from '@elastic/datemath';

import { parseDuration } from '../../../../../alerting/server/lib';

Expand All @@ -26,25 +27,34 @@ export const parseInterval = (intervalString: string): moment.Duration | null =>
}
};

export const parseScheduleDates = (time: string): moment.Moment | null => {
const isValidDateString = !isNaN(Date.parse(time));
const isValidInput = isValidDateString || time.trim().startsWith('now');
const formattedDate = isValidDateString
? moment(time)
: isValidInput
? dateMath.parse(time)
: null;

return formattedDate ?? null;
};

export const getDriftTolerance = ({
from,
to,
interval,
now = moment(),
}: {
from: string;
to: string;
interval: moment.Duration;
now?: moment.Moment;
}): moment.Duration | null => {
if (to.trim() !== 'now') {
// we only support 'now' for drift detection
return null;
}
if (!from.trim().startsWith('now-')) {
// we only support from tha starts with now for drift detection
return null;
}
const split = from.split('-');
const duration = parseInterval(split[1]);
const toDate = parseScheduleDates(to) ?? now;
const fromDate = parseScheduleDates(from) ?? dateMath.parse('now-6m');
const timeSegment = toDate.diff(fromDate);
const duration = moment.duration(timeSegment);

if (duration !== null) {
return duration.subtract(interval);
} else {
Expand Down