Skip to content

Commit 327e63f

Browse files
committed
Sieve bot gets :before and :after keywords.
1 parent 6d4d889 commit 327e63f

File tree

8 files changed

+142
-13
lines changed

8 files changed

+142
-13
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ CHANGELOG
4444
- `intelmq.bots.experts.cymru_whois`:
4545
- Ignore AS names with unexpected unicode characters (PR#2352, fixes #2132)
4646
- Avoid extraneous search domain-based queries on NXDOMAIN result (PR#2352)
47+
- `intelmq.bots.experts.sieve`:
48+
- Added :before and :after keywords (PR#2374)
4749

4850
#### Outputs
4951
- `intelmq.bots.outputs.cif3.output`: Added (PR#2244 by Michael Davis).

docs/user/bots.rst

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2190,7 +2190,7 @@ Both parameters accept string values describing absolute or relative time:
21902190

21912191
* absolute
21922192

2193-
* basically anything parseable by datetime parser, eg. "2015-09-012T06:22:11+00:00"
2193+
* basically anything parseable by datetime parser, eg. "2015-09-12T06:22:11+00:00"
21942194
* `time.source` taken from the event will be compared to this value to decide the filter behavior
21952195

21962196
* relative
@@ -2200,7 +2200,7 @@ Both parameters accept string values describing absolute or relative time:
22002200

22012201
*Examples of time filter definition*
22022202

2203-
* ```"not_before" : "2015-09-012T06:22:11+00:00"``` events older than the specified time will be dropped
2203+
* ```"not_before" : "2015-09-12T06:22:11+00:00"``` events older than the specified time will be dropped
22042204
* ```"not_after" : "6 months"``` just events older than 6 months will be passed through the pipeline
22052205

22062206
**Possible paths**
@@ -2999,6 +2999,12 @@ The following operators may be used to match events:
29992999
* `:supersetof` tests if the list of values from the given key is a superset of the values specified as the argument. Example for matching hosts with at least the IoT and vulnerable tags:
30003000
``if extra.tags :supersetof ['iot', 'vulnerable'] { ... }``
30013001
3002+
* `:before` tests if the date value occurred before given time ago. The time might be absolute (basically anything parseable by pendulum parser, eg. “2015-09-12T06:22:11+00:00”) or relative (accepted string formatted like this “<integer> <epoch>”, where epoch could be any of following strings (could optionally end with trailing ‘s’): hour, day, week, month, year)
3003+
``if time.observation :before '1 week' { ... }``
3004+
3005+
* `:after` tests if the date value occurred after given time ago; see `:before`
3006+
``if time.observation :after '2015-09-12' { ... } # happened after midnight the 12th Sep``
3007+
30023008
* Boolean values can be matched with `==` or `!=` followed by `true` or `false`. Example:
30033009
``if extra.has_known_vulns == true { ... }``
30043010
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# SPDX-FileCopyrightText: 2017 Antoine Neuenschwander
22
# SPDX-License-Identifier: AGPL-3.0-or-later
33

4+
pendulum
45
textX>=1.5.1

intelmq/bots/experts/sieve/expert.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,28 +13,35 @@
1313
import os
1414
import re
1515
import traceback
16-
import datetime
1716
import operator
1817

19-
from typing import Optional
18+
from datetime import datetime, timedelta, timezone
19+
from typing import Callable, Dict, Optional, Union
2020
from enum import Enum, auto
21-
from functools import partial
2221

2322
import intelmq.lib.exceptions as exceptions
2423
from intelmq import HARMONIZATION_CONF_FILE
2524
from intelmq.lib import utils
2625
from intelmq.lib.bot import ExpertBot
2726
from intelmq.lib.exceptions import MissingDependencyError
28-
from intelmq.lib.utils import parse_relative
27+
from intelmq.lib.message import Event
28+
from intelmq.lib.utils import parse_relative, TIMESPANS
2929
from intelmq.lib.harmonization import DateTime
3030

31+
try:
32+
from pendulum import parse
33+
except:
34+
parse = None
35+
3136
try:
3237
import textx.model
3338
from textx.metamodel import metamodel_from_file
3439
from textx.exceptions import TextXError, TextXSemanticError
3540
except ImportError:
3641
metamodel_from_file = None
3742

43+
CondMap = Dict[str, Callable[['SieveExpertBot', object, Event], bool]]
44+
3845

3946
class Procedure(Enum):
4047
CONTINUE = auto() # continue processing subsequent statements (default)
@@ -93,19 +100,28 @@ class SieveExpertBot(ExpertBot):
93100
'!=': operator.ne,
94101
}
95102

96-
_cond_map = {
103+
_date_op_map = {
104+
':before': operator.lt,
105+
':after': operator.gt
106+
}
107+
108+
_cond_map: CondMap = {
97109
'ExistMatch': lambda self, match, event: self.process_exist_match(match.key, match.op, event),
98110
'SingleStringMatch': lambda self, match, event: self.process_single_string_match(match.key, match.op, match.value, event),
99111
'MultiStringMatch': lambda self, match, event: self.process_multi_string_match(match.key, match.op, match.value, event),
100112
'SingleNumericMatch': lambda self, match, event: self.process_single_numeric_match(match.key, match.op, match.value, event),
101113
'MultiNumericMatch': lambda self, match, event: self.process_multi_numeric_match(match.key, match.op, match.value, event),
102114
'IpRangeMatch': lambda self, match, event: self.process_ip_range_match(match.key, match.range, event),
115+
'DateMatch': lambda self, match, event: self.process_date_match(match.key, match.op, match.date, event),
103116
'ListMatch': lambda self, match, event: self.process_list_match(match.key, match.op, match.value, event),
104117
'BoolMatch': lambda self, match, event: self.process_bool_match(match.key, match.op, match.value, event),
105118
'Expression': lambda self, match, event: self.match_expression(match, event),
106119
}
107120

108121
def init(self) -> None:
122+
if parse is None:
123+
raise MissingDependencyError("pendulum")
124+
109125
if not SieveExpertBot._harmonization:
110126
harmonization_config = utils.load_configuration(HARMONIZATION_CONF_FILE)
111127
SieveExpertBot._harmonization = harmonization_config['event']
@@ -300,6 +316,30 @@ def process_ip_range_match(self, key, ip_range, event) -> bool:
300316
return any(addr in ipaddress.ip_network(val.value, strict=False) for val in ip_range.values)
301317
raise TextXSemanticError(f'Unhandled type: {name}')
302318

319+
def parse_timeattr(self, time_attr) -> Union[datetime, timedelta]:
320+
""" Parses relative or absolute time specification. """
321+
try:
322+
return parse(time_attr)
323+
except ValueError:
324+
return timedelta(minutes=parse_relative(time_attr))
325+
326+
def process_date_match(self, key, op, value, event) -> bool:
327+
if key not in event:
328+
return False
329+
330+
op = self._date_op_map[op]
331+
332+
base_time = self.parse_timeattr(value.value)
333+
if isinstance(base_time, timedelta):
334+
base_time = datetime.now(tz=timezone.utc) - base_time
335+
try:
336+
event_time = parse(str(event[key]))
337+
except ValueError:
338+
self.logger.warning("Could not parse time.source %s=%s at %s.", key, event[key], event)
339+
return False
340+
else:
341+
return op(event_time, base_time)
342+
303343
def process_list_match(self, key, op, value, event) -> bool:
304344
if not (key in event and isinstance(event[key], list)):
305345
return False
@@ -316,7 +356,7 @@ def process_bool_match(self, key, op, value, event):
316356

317357
def compute_basic_math(self, action, event) -> str:
318358
date = DateTime.from_isoformat(event[action.key], True)
319-
delta = datetime.timedelta(minutes=parse_relative(action.value))
359+
delta = timedelta(minutes=parse_relative(action.value))
320360

321361
return self._basic_math_op_map[action.operator](date, delta).isoformat()
322362

intelmq/bots/experts/sieve/sieve.tx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Condition:
1818
match=StringMatch
1919
| match=NumericMatch
2020
| match=IpRangeMatch
21+
| match=DateMatch
2122
| match=ExistMatch
2223
| match=ListMatch
2324
| match=BoolMatch
@@ -68,6 +69,9 @@ IpRange: SingleIpRange | IpRangeList;
6869
SingleIpRange: value=STRING;
6970
IpRangeList: '[' values+=SingleIpRange[','] ']' ;
7071

72+
DateMatch: key=Key op=DateOperator date=SingleStringValue;
73+
DateOperator: ':before' | ':after';
74+
7175
ExistMatch: op=ExistOperator key=Key;
7276
ExistOperator: ':exists' | ':notexists';
7377

intelmq/tests/bots/experts/sieve/test_expert.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
# -*- coding: utf-8 -*-
66

7+
from pathlib import Path
78
import unittest
89
import os
10+
from datetime import timedelta, datetime
911
import intelmq.lib.test as test
1012
from intelmq.bots.experts.sieve.expert import SieveExpertBot
1113

@@ -949,6 +951,55 @@ def test_network_host_bits_list_match(self):
949951
self.run_bot()
950952
self.assertMessageEqual(0, event)
951953

954+
def test_date_match(self):
955+
""" Test comparing absolute and relative to now dates. """
956+
self.sysconfig['file'] = Path(__file__).parent / 'test_sieve_files/test_date_match.sieve'
957+
958+
def check(event, expected):
959+
self.input_message = event
960+
self.run_bot()
961+
self.assertMessageEqual(0, expected)
962+
963+
event = EXAMPLE_INPUT.copy()
964+
expected = event.copy()
965+
966+
event["time.observation"] = "2017-01-01T00:00:00+00:00" # past event with tz
967+
expected['extra.list'] = ['before 1 week', 'before 2023-06-01', 'before 2023-06-01 15:00']
968+
check(event, expected)
969+
970+
event["time.observation"] = "2017-01-01T00:00:00" # past event without tz
971+
check(event, expected)
972+
973+
event["time.observation"] = "2023-06-01" # just date, neither before nor after the date's midnight
974+
expected['extra.list'] = ['before 1 week', 'before 2023-06-01 15:00']
975+
check(event, expected)
976+
977+
event["time.observation"] = "2023-06-01 10:00" # time given
978+
expected['extra.list'] = ['before 1 week', 'after 2023-06-01', 'before 2023-06-01 15:00']
979+
check(event, expected)
980+
981+
event["time.observation"] = "2023-06-01T10:00+00:00" # time including tz
982+
check(event, expected)
983+
984+
event["time.observation"] = "2023-06-01T10:00-06:00" # tz changes
985+
expected['extra.list'] = ['before 1 week', 'after 2023-06-01', 'after 2023-06-01 15:00']
986+
check(event, expected)
987+
988+
event["time.observation"] = str(datetime.now())
989+
expected['extra.list'] = ['after 1 week', 'after 2023-06-01', 'after 2023-06-01 15:00']
990+
check(event, expected)
991+
992+
event["time.observation"] = str(datetime.now() - timedelta(days=3))
993+
check(event, expected)
994+
995+
event["time.observation"] = str(datetime.now() - timedelta(days=8))
996+
expected['extra.list'] = ['before 1 week', 'after 2023-06-01', 'after 2023-06-01 15:00']
997+
check(event, expected)
998+
999+
event["time.observation"] = str(datetime.now() - timedelta(days=8))
1000+
expected['extra.list'] = ['before 1 week', 'after 2023-06-01', 'after 2023-06-01 15:00']
1001+
check(event, expected)
1002+
9521003
def test_comments(self):
9531004
""" Test comments in sieve file."""
9541005
self.sysconfig['file'] = os.path.join(os.path.dirname(__file__), 'test_sieve_files/test_comments.sieve')
@@ -972,7 +1023,6 @@ def test_named_queues(self):
9721023
self.run_bot()
9731024
self.assertOutputQueueLen(0)
9741025

975-
9761026
# if doesn't match keep
9771027
numeric_match_false = EXAMPLE_INPUT.copy()
9781028
numeric_match_false['comment'] = "keep without path"
@@ -1301,7 +1351,6 @@ def test_bool_match(self):
13011351
self.run_bot()
13021352
self.assertMessageEqual(0, expected)
13031353

1304-
13051354
# negative test with true == false
13061355
event = base.copy()
13071356
event['comment'] = 'match5'
@@ -1394,9 +1443,6 @@ def test_typed_values(self):
13941443
self.run_bot()
13951444
self.assertMessageEqual(0, expected)
13961445

1397-
1398-
1399-
14001446
def test_append(self):
14011447
''' Test append action '''
14021448
self.sysconfig['file'] = os.path.join(os.path.dirname(__file__), 'test_sieve_files/test_append.sieve')
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
if time.observation :before '1 week' {
2+
append extra.list 'before 1 week'
3+
}
4+
5+
if time.observation :after '1 week' {
6+
append extra.list 'after 1 week'
7+
}
8+
9+
if time.observation :before '2023-06-01' {
10+
append extra.list 'before 2023-06-01'
11+
}
12+
13+
if time.observation :after '2023-06-01' {
14+
# when time is not set, '2023-06-01 00:01' resolves here
15+
append extra.list 'after 2023-06-01'
16+
}
17+
18+
if time.observation :before '2023-06-01 15:00' {
19+
append extra.list 'before 2023-06-01 15:00'
20+
}
21+
22+
if time.observation :after '2023-06-01 15:00' {
23+
append extra.list 'after 2023-06-01 15:00'
24+
}
25+
26+
if extra.str == 'few-hours' && time.observation :after '10 h' {
27+
append extra.list 'after 10 h'
28+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SPDX-FileCopyrightText: 2023 Edvard Rejthar CSIRT.cz
2+
SPDX-License-Identifier: AGPL-3.0-or-later

0 commit comments

Comments
 (0)