2019-01-22 21:25:13 +03:00
#!/usr/bin/env python3
"""
Checks for noncompliance or common errors on all rules
2019-02-18 21:05:58 +03:00
Run using the command
2021-07-28 15:13:55 +02:00
# python test_rules.py
2019-01-22 21:25:13 +03:00
"""
import os
import unittest
import yaml
2019-03-02 20:51:49 +03:00
import re
2020-07-14 17:54:02 +02:00
from attackcti import attack_client
2020-01-30 08:37:47 +01:00
from colorama import init
from colorama import Fore
2019-01-22 21:25:13 +03:00
2022-08-12 14:19:08 +02:00
2019-01-22 21:25:13 +03:00
class TestRules ( unittest . TestCase ) :
2022-08-12 14:19:08 +02:00
MITRE_TECHNIQUE_NAMES = [
" process_injection " , " signed_binary_proxy_execution " , " process_injection " ] # incomplete list
MITRE_TACTICS = [ " initial_access " , " execution " , " persistence " , " privilege_escalation " , " defense_evasion " , " credential_access " ,
" discovery " , " lateral_movement " , " collection " , " exfiltration " , " command_and_control " , " impact " , " launch " ]
2021-05-15 13:02:49 +02:00
# Don't use trademarks in rules - they require non-ASCII characters to be used on we don't want them in our rules
TRADE_MARKS = { " MITRE ATT&CK " , " ATT&CK " }
2019-01-22 21:25:13 +03:00
2019-01-25 12:25:51 +03:00
path_to_rules = " rules "
2019-01-22 21:25:13 +03:00
# Helper functions
2022-08-12 14:19:08 +02:00
def yield_next_rule_file_path ( self , path_to_rules : str ) - > str :
2019-01-22 21:25:13 +03:00
for root , _ , files in os . walk ( path_to_rules ) :
for file in files :
yield os . path . join ( root , file )
2022-08-12 14:19:08 +02:00
def get_rule_part ( self , file_path : str , part_name : str ) :
2019-01-22 21:25:13 +03:00
yaml_dicts = self . get_rule_yaml ( file_path )
for yaml_part in yaml_dicts :
if part_name in yaml_part . keys ( ) :
return yaml_part [ part_name ]
return None
2022-08-12 14:19:08 +02:00
def get_rule_yaml ( self , file_path : str ) - > dict :
2019-01-22 21:25:13 +03:00
data = [ ]
2022-08-12 14:19:08 +02:00
with open ( file_path , encoding = ' utf-8 ' ) as f :
2019-04-22 23:21:08 +02:00
yaml_parts = yaml . safe_load_all ( f )
2019-01-22 21:25:13 +03:00
for part in yaml_parts :
data . append ( part )
return data
# Tests
2021-09-22 19:02:44 +02:00
# def test_confirm_extension_is_yml(self):
# files_with_incorrect_extensions = []
2019-01-22 21:25:13 +03:00
2021-09-22 19:02:44 +02:00
# for file in self.yield_next_rule_file_path(self.path_to_rules):
2022-08-12 14:19:08 +02:00
# file_name_and_extension = os.path.splitext(file)
# if len(file_name_and_extension) == 2:
# extension = file_name_and_extension[1]
# if extension != ".yml":
# files_with_incorrect_extensions.append(file)
2019-01-22 21:25:13 +03:00
2022-05-09 10:23:38 +02:00
# self.assertEqual(files_with_incorrect_extensions, [], Fore.RED +
2022-08-12 14:19:08 +02:00
# "There are rule files with extensions other than .yml")
2019-01-22 21:25:13 +03:00
2021-05-15 13:02:49 +02:00
def test_legal_trademark_violations ( self ) :
files_with_legal_issues = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
with open ( file , ' r ' , encoding = ' utf-8 ' ) as fh :
2021-05-15 13:02:49 +02:00
file_data = fh . read ( )
for tm in self . TRADE_MARKS :
if tm in file_data :
files_with_legal_issues . append ( file )
2022-05-09 10:23:38 +02:00
self . assertEqual ( files_with_legal_issues , [ ] , Fore . RED +
2022-08-12 14:19:08 +02:00
" There are rule files which contains a trademark or reference that doesn ' t comply with the respective trademark requirements - please remove the trademark to avoid legal issues " )
2022-05-09 10:23:38 +02:00
2021-08-24 10:10:45 +02:00
def test_optional_tags ( self ) :
files_with_incorrect_tags = [ ]
2022-08-12 14:19:08 +02:00
tags_pattern = re . compile (
r " cve \ . \ d+ \ . \ d+|attack \ .t \ d+ \ .* \ d*|attack \ .[a-z_]+|car \ . \ d {4} - \ d {2} - \ d {3} " )
2021-08-24 10:10:45 +02:00
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
tags = self . get_rule_part ( file_path = file , part_name = " tags " )
if tags :
2021-08-25 09:15:57 +02:00
for tag in tags :
2021-10-25 18:14:03 +02:00
if tags_pattern . match ( tag ) == None :
2022-08-12 14:19:08 +02:00
print (
Fore . RED + " Rule {} has the invalid tag < {} > " . format ( file , tag ) )
2021-08-25 09:15:57 +02:00
files_with_incorrect_tags . append ( file )
2021-08-24 10:10:45 +02:00
2022-05-09 10:23:38 +02:00
self . assertEqual ( files_with_incorrect_tags , [ ] , Fore . RED +
2021-08-24 10:10:45 +02:00
" There are rules with incorrect/unknown MITRE Tags. (please inform us about new tags that are not yet supported in our tests) and check the correct tags here: https://attack.mitre.org/ " )
2021-05-15 13:02:49 +02:00
2019-01-22 21:25:13 +03:00
def test_confirm_correct_mitre_tags ( self ) :
files_with_incorrect_mitre_tags = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
tags = self . get_rule_part ( file_path = file , part_name = " tags " )
if tags :
for tag in tags :
2020-07-14 17:54:02 +02:00
if tag not in MITRE_ALL and tag . startswith ( " attack. " ) :
2022-08-12 14:19:08 +02:00
print (
Fore . RED + " Rule {} has the following incorrect tag {} " . format ( file , tag ) )
2019-01-22 21:25:13 +03:00
files_with_incorrect_mitre_tags . append ( file )
2019-02-18 21:05:58 +03:00
2022-05-09 10:23:38 +02:00
self . assertEqual ( files_with_incorrect_mitre_tags , [ ] , Fore . RED +
2020-07-14 11:56:28 +02:00
" There are rules with incorrect/unknown MITRE Tags. (please inform us about new tags that are not yet supported in our tests) and check the correct tags here: https://attack.mitre.org/ " )
2019-01-22 21:25:13 +03:00
2020-07-27 11:37:58 +02:00
def test_duplicate_tags ( self ) :
files_with_incorrect_mitre_tags = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
tags = self . get_rule_part ( file_path = file , part_name = " tags " )
if tags :
known_tags = [ ]
for tag in tags :
if tag in known_tags :
2022-08-12 14:19:08 +02:00
print (
Fore . RED + " Rule {} has the duplicate tag {} " . format ( file , tag ) )
2020-07-27 11:37:58 +02:00
files_with_incorrect_mitre_tags . append ( file )
2022-05-09 10:23:38 +02:00
else :
2020-07-27 11:37:58 +02:00
known_tags . append ( tag )
2022-05-09 10:23:38 +02:00
self . assertEqual ( files_with_incorrect_mitre_tags , [ ] , Fore . RED +
2020-07-27 11:37:58 +02:00
" There are rules with duplicate tags " )
2019-01-25 12:22:28 +03:00
def test_look_for_duplicate_filters ( self ) :
2022-08-12 14:19:08 +02:00
def check_list_or_recurse_on_dict ( item , depth : int ) - > None :
2019-01-25 12:22:28 +03:00
if type ( item ) == list :
check_if_list_contain_duplicates ( item , depth )
elif type ( item ) == dict and depth < = MAX_DEPTH :
for sub_item in item . values ( ) :
check_list_or_recurse_on_dict ( sub_item , depth + 1 )
2022-08-12 14:19:08 +02:00
def check_if_list_contain_duplicates ( item : list , depth : int ) - > None :
2019-01-25 12:22:28 +03:00
try :
if len ( item ) != len ( set ( item ) ) :
2020-01-30 08:50:22 +01:00
print ( Fore . RED + " Rule {} has duplicate filters " . format ( file ) )
2019-01-25 12:22:28 +03:00
files_with_duplicate_filters . append ( file )
except :
# unhashable types like dictionaries
for sub_item in item :
if type ( sub_item ) == dict and depth < = MAX_DEPTH :
check_list_or_recurse_on_dict ( sub_item , depth + 1 )
MAX_DEPTH = 3
files_with_duplicate_filters = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
2019-01-25 12:22:28 +03:00
check_list_or_recurse_on_dict ( detection , 1 )
2022-05-09 10:23:38 +02:00
self . assertEqual ( files_with_duplicate_filters , [ ] , Fore . RED +
2019-01-25 12:22:28 +03:00
" There are rules with duplicate filters " )
2021-10-13 14:21:23 +02:00
def test_field_name_with_space ( self ) :
def key_iterator ( fields , faulty ) :
for key , value in fields . items ( ) :
if " " in key :
faulty . append ( key )
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a space in field name ( {} ). " . format ( file , key ) )
2021-10-13 14:21:23 +02:00
if type ( value ) == dict :
key_iterator ( value , faulty )
faulty_fieldnames = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
2021-10-13 14:21:23 +02:00
key_iterator ( detection , faulty_fieldnames )
self . assertEqual ( faulty_fieldnames , [ ] , Fore . RED +
2022-08-12 14:19:08 +02:00
" There are rules with an unsupported field name. Spaces are not allowed. (Replace space with an underscore character ' _ ' ) " )
2021-10-13 14:21:23 +02:00
2019-02-13 21:27:27 +03:00
def test_single_named_condition_with_x_of_them ( self ) :
faulty_detections = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
yaml = self . get_rule_yaml ( file_path = file )
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
2019-02-18 21:05:58 +03:00
2019-02-13 21:27:27 +03:00
has_them_in_condition = " them " in detection [ " condition " ]
has_only_one_named_condition = len ( detection ) == 2
not_multipart_yaml_file = len ( yaml ) == 1
if has_them_in_condition and \
has_only_one_named_condition and \
not_multipart_yaml_file :
faulty_detections . append ( file )
2020-01-30 08:50:22 +01:00
self . assertEqual ( faulty_detections , [ ] , Fore . RED +
2019-02-13 21:27:27 +03:00
" There are rules using ' 1/all of them ' style conditions but only have one condition " )
2021-12-02 14:30:09 +01:00
def test_all_of_them_condition ( self ) :
faulty_detections = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
2021-12-02 14:30:09 +01:00
if " all of them " in detection [ " condition " ] :
faulty_detections . append ( file )
self . assertEqual ( faulty_detections , [ ] , Fore . RED +
" There are rules using ' all of them ' . Better use e.g. ' all of selection* ' instead (and use the ' selection_ ' prefix as search-identifier). " )
2021-10-26 12:22:18 +02:00
def test_duplicate_detections ( self ) :
2022-08-12 14:19:08 +02:00
def compare_detections ( detection1 : dict , detection2 : dict ) - > bool :
2019-02-18 21:05:58 +03:00
2021-10-26 12:22:18 +02:00
# detections not the same count can't be the same
2019-02-18 21:05:58 +03:00
if len ( detection1 ) != len ( detection2 ) :
2022-05-09 10:23:38 +02:00
return False
2019-02-18 21:05:58 +03:00
for named_condition in detection1 :
2022-08-12 14:19:08 +02:00
# don't check timeframes
2021-09-21 22:54:45 +02:00
if named_condition == " timeframe " :
continue
2022-05-09 10:23:38 +02:00
# condition clause must be the same too
2019-02-18 21:05:58 +03:00
if named_condition == " condition " :
if detection1 [ " condition " ] != detection2 [ " condition " ] :
return False
else :
continue
2022-05-09 10:23:38 +02:00
2019-02-18 21:05:58 +03:00
# Named condition must exist in both rule files
if named_condition not in detection2 :
return False
2022-05-09 10:23:38 +02:00
2022-08-12 14:19:08 +02:00
# can not be the same if len is not equal
2019-02-18 21:05:58 +03:00
if len ( detection1 [ named_condition ] ) != len ( detection2 [ named_condition ] ) :
return False
2022-05-09 10:23:38 +02:00
2019-02-18 21:05:58 +03:00
for condition in detection1 [ named_condition ] :
if type ( condition ) != str :
return False
if condition not in detection2 [ named_condition ] :
return False
2022-05-09 10:23:38 +02:00
2019-02-18 21:05:58 +03:00
condition_value1 = detection1 [ named_condition ] [ condition ]
condition_value2 = detection2 [ named_condition ] [ condition ]
if condition_value1 != condition_value2 :
return False
return True
faulty_detections = [ ]
files_and_their_detections = { }
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
logsource = self . get_rule_part (
file_path = file , part_name = " logsource " )
2021-10-26 12:22:18 +02:00
detection [ " logsource " ] = { }
detection [ " logsource " ] . update ( logsource )
2022-08-12 14:19:08 +02:00
yaml = self . get_rule_yaml ( file_path = file )
2019-02-18 21:05:58 +03:00
is_multipart_yaml_file = len ( yaml ) != 1
if is_multipart_yaml_file :
continue
for key in files_and_their_detections :
if compare_detections ( detection , files_and_their_detections [ key ] ) :
faulty_detections . append ( ( key , file ) )
files_and_their_detections [ file ] = detection
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_detections , [ ] , Fore . YELLOW +
2019-02-18 21:05:58 +03:00
" There are rule files with exactly the same detection logic. " )
def test_source_eventlog ( self ) :
faulty_detections = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
2019-02-18 21:05:58 +03:00
detection_str = str ( detection ) . lower ( )
if " ' source ' : ' eventlog ' " in detection_str :
faulty_detections . append ( file )
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_detections , [ ] , Fore . YELLOW +
2019-02-18 21:05:58 +03:00
" There are detections with ' Source: Eventlog ' . This does not add value to the detection. " )
2019-03-09 21:00:11 +03:00
def test_event_id_instead_of_process_creation ( self ) :
2019-03-02 20:51:49 +03:00
faulty_detections = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
with open ( file , encoding = ' utf-8 ' ) as f :
2019-03-02 20:51:49 +03:00
for line in f :
2019-03-09 19:23:50 +03:00
if re . search ( r ' .*EventID: (?:1|4688) \ s*$ ' , line ) and file not in faulty_detections :
2019-03-02 20:51:49 +03:00
faulty_detections . append ( file )
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_detections , [ ] , Fore . YELLOW +
2019-03-09 21:00:11 +03:00
" There are rules still using Sysmon 1 or Event ID 4688. Please migrate to the process_creation category. " )
2019-03-02 20:51:49 +03:00
2020-01-30 16:08:24 +01:00
def test_missing_id ( self ) :
faulty_rules = [ ]
2021-08-16 18:12:17 +02:00
dict_id = { }
2020-01-30 16:08:24 +01:00
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
id = self . get_rule_part ( file_path = file , part_name = " id " )
if not id :
print ( Fore . YELLOW + " Rule {} has no field ' id ' . " . format ( file ) )
faulty_rules . append ( file )
elif len ( id ) != 36 :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a malformed ' id ' (not 36 chars). " . format ( file ) )
2022-05-09 10:23:38 +02:00
faulty_rules . append ( file )
2021-08-16 18:12:17 +02:00
elif id in dict_id . keys ( ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has the same ' id ' than {} must be unique. " . format ( file , dict_id [ id ] ) )
2021-07-17 10:32:29 +02:00
faulty_rules . append ( file )
else :
2021-08-16 18:12:17 +02:00
dict_id [ id ] = file
2020-01-30 16:08:24 +01:00
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2020-01-30 16:08:24 +01:00
" There are rules with missing or malformed ' id ' fields. Create an id (e.g. here: https://www.uuidgenerator.net/version4) and add it to the reported rule(s). " )
2021-08-11 14:26:20 +02:00
2021-07-24 09:41:04 +02:00
def test_optional_related ( self ) :
faulty_rules = [ ]
valid_type = [
" derived " ,
" obsoletes " ,
" merged " ,
" renamed " ,
2022-05-24 09:51:48 +02:00
" similar "
2022-08-12 14:19:08 +02:00
]
2021-07-24 09:41:04 +02:00
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
related_lst = self . get_rule_part (
file_path = file , part_name = " related " )
2021-07-24 09:41:04 +02:00
if related_lst :
# it exists but isn't a list
if not isinstance ( related_lst , list ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a ' related ' field that isn ' t a list. " . format ( file ) )
2021-07-24 09:41:04 +02:00
faulty_rules . append ( file )
else :
2022-05-09 10:23:38 +02:00
# should probably test if we have only 'id' and 'type' ...
2021-07-24 09:41:04 +02:00
type_ok = True
for ref in related_lst :
id_str = ref [ ' id ' ]
type_str = ref [ ' type ' ]
if not type_str in valid_type :
2022-08-12 14:19:08 +02:00
type_ok = False
# Only add one time if many bad type in the same file
2021-07-24 09:41:04 +02:00
if type_ok == False :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a ' related/type ' invalid value. " . format ( file ) )
2021-08-11 14:26:20 +02:00
faulty_rules . append ( file )
2021-07-24 09:41:04 +02:00
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2021-08-11 14:26:20 +02:00
" There are rules with malformed optional ' related ' fields. (check https://github.com/SigmaHQ/sigma/wiki/Specification) " )
2020-05-23 10:25:37 -04:00
def test_sysmon_rule_without_eventid ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
logsource = self . get_rule_part (
file_path = file , part_name = " logsource " )
2021-09-12 20:13:58 +02:00
if logsource :
service = logsource . get ( ' service ' , ' ' )
if service . lower ( ) == ' sysmon ' :
2022-08-12 14:19:08 +02:00
with open ( file , encoding = ' utf-8 ' ) as f :
2021-09-12 20:13:58 +02:00
found = False
for line in f :
2022-08-12 14:19:08 +02:00
# might be on a single line or in multiple lines
if re . search ( r ' .*EventID:.*$ ' , line ) :
2021-09-12 20:13:58 +02:00
found = True
break
if not found :
faulty_rules . append ( file )
2020-05-23 10:25:37 -04:00
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2020-05-23 10:25:37 -04:00
" There are rules using sysmon events but with no EventID specified " )
2020-01-30 16:08:34 +01:00
def test_missing_date ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
datefield = self . get_rule_part ( file_path = file , part_name = " date " )
if not datefield :
print ( Fore . YELLOW + " Rule {} has no field ' date ' . " . format ( file ) )
faulty_rules . append ( file )
2021-07-21 18:28:47 +02:00
elif not isinstance ( datefield , str ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a malformed ' date ' (should be YYYY/MM/DD). " . format ( file ) )
2021-08-11 14:26:20 +02:00
faulty_rules . append ( file )
2020-01-30 16:08:34 +01:00
elif len ( datefield ) != 10 :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a malformed ' date ' (not 10 chars, should be YYYY/MM/DD). " . format ( file ) )
2021-08-11 14:26:20 +02:00
faulty_rules . append ( file )
2021-08-26 06:51:37 +02:00
elif datefield [ 4 ] != ' / ' or datefield [ 7 ] != ' / ' :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a malformed ' date ' (should be YYYY/MM/DD). " . format ( file ) )
2021-08-26 06:51:37 +02:00
faulty_rules . append ( file )
2020-01-30 16:08:34 +01:00
2020-11-27 10:17:45 +01:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2020-01-30 16:08:34 +01:00
" There are rules with missing or malformed ' date ' fields. (create one, e.g. date: 2019/01/14) " )
2019-03-02 20:51:49 +03:00
2022-01-12 12:55:49 +01:00
def test_missing_description ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
descriptionfield = self . get_rule_part (
file_path = file , part_name = " description " )
2022-01-12 12:55:49 +01:00
if not descriptionfield :
print ( Fore . YELLOW + " Rule {} has no field ' description ' . " . format ( file ) )
faulty_rules . append ( file )
elif not isinstance ( descriptionfield , str ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a ' description ' field that isn ' t a string. " . format ( file ) )
2022-01-12 12:55:49 +01:00
faulty_rules . append ( file )
elif len ( descriptionfield ) < 16 :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a really short description. Please elaborate. " . format ( file ) )
2022-01-12 12:55:49 +01:00
faulty_rules . append ( file )
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules with missing or malformed ' description ' field. (create one, e.g. description: Detects the suspicious behaviour of process XY doing YZ) " )
2021-07-24 09:41:04 +02:00
def test_optional_date_modified ( self ) :
2021-07-21 18:28:47 +02:00
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
modifiedfield = self . get_rule_part (
file_path = file , part_name = " modified " )
2021-07-21 18:28:47 +02:00
if modifiedfield :
if not isinstance ( modifiedfield , str ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a malformed ' modified ' (should be YYYY/MM/DD). " . format ( file ) )
2021-08-11 14:26:20 +02:00
faulty_rules . append ( file )
2021-07-21 18:28:47 +02:00
elif len ( modifiedfield ) != 10 :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a malformed ' modified ' (not 10 chars, should be YYYY/MM/DD). " . format ( file ) )
2021-08-11 14:26:20 +02:00
faulty_rules . append ( file )
2021-08-26 06:51:37 +02:00
elif modifiedfield [ 4 ] != ' / ' or modifiedfield [ 7 ] != ' / ' :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a malformed ' modified ' (should be YYYY/MM/DD). " . format ( file ) )
2022-05-09 10:23:38 +02:00
faulty_rules . append ( file )
2021-07-21 18:28:47 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules with malformed ' modified ' fields. (create one, e.g. date: 2019/01/14) " )
2021-07-22 19:25:51 +02:00
def test_optional_status ( self ) :
faulty_rules = [ ]
2021-07-24 09:41:04 +02:00
valid_status = [
" stable " ,
" test " ,
" experimental " ,
2021-10-28 20:08:27 +02:00
" deprecated " ,
" unsupported "
2022-08-12 14:19:08 +02:00
]
2021-07-22 19:25:51 +02:00
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
status_str = self . get_rule_part ( file_path = file , part_name = " status " )
if status_str :
if not status_str in valid_status :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a invalid ' status ' (check wiki). " . format ( file ) )
2022-05-09 10:23:38 +02:00
faulty_rules . append ( file )
2021-10-28 20:08:27 +02:00
elif status_str == " unsupported " :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has the unsupported ' status ' , can not be in rules directory " . format ( file ) )
2021-10-28 20:08:27 +02:00
faulty_rules . append ( file )
2022-05-09 10:23:38 +02:00
2021-07-22 19:25:51 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2021-08-11 14:26:20 +02:00
" There are rules with malformed ' status ' fields. (check https://github.com/SigmaHQ/sigma/wiki/Specification) " )
2021-07-22 19:25:51 +02:00
def test_level ( self ) :
faulty_rules = [ ]
2021-07-24 09:41:04 +02:00
valid_level = [
" informational " ,
" low " ,
" medium " ,
" high " ,
" critical " ,
2022-08-12 14:19:08 +02:00
]
2021-07-22 19:25:51 +02:00
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
level_str = self . get_rule_part ( file_path = file , part_name = " level " )
if not level_str :
print ( Fore . YELLOW + " Rule {} has no field ' level ' . " . format ( file ) )
2021-08-11 14:26:20 +02:00
faulty_rules . append ( file )
2021-07-22 19:25:51 +02:00
elif not level_str in valid_level :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a invalid ' level ' (check wiki). " . format ( file ) )
faulty_rules . append ( file )
2021-08-11 14:26:20 +02:00
2021-07-22 19:25:51 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2021-08-11 14:26:20 +02:00
" There are rules with missing or malformed ' level ' fields. (check https://github.com/SigmaHQ/sigma/wiki/Specification) " )
2021-07-22 19:25:51 +02:00
2021-07-24 09:41:04 +02:00
def test_optional_fields ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
fields_str = self . get_rule_part ( file_path = file , part_name = " fields " )
if fields_str :
# it exists but isn't a list
if not isinstance ( fields_str , list ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a ' fields ' field that isn ' t a list. " . format ( file ) )
2021-07-24 09:41:04 +02:00
faulty_rules . append ( file )
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2021-07-24 09:41:04 +02:00
" There are rules with malformed optional ' fields ' fields. (has to be a list of values even if it contains only a single value) " )
2022-05-09 13:37:43 +02:00
def test_optional_falsepositives_listtype ( self ) :
2021-07-24 09:41:04 +02:00
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
falsepositives_str = self . get_rule_part (
file_path = file , part_name = " falsepositives " )
2021-07-24 09:41:04 +02:00
if falsepositives_str :
# it exists but isn't a list
if not isinstance ( falsepositives_str , list ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a ' falsepositives ' field that isn ' t a list. " . format ( file ) )
2021-07-24 09:41:04 +02:00
faulty_rules . append ( file )
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2021-07-24 09:41:04 +02:00
" There are rules with malformed optional ' falsepositives ' fields. (has to be a list of values even if it contains only a single value) " )
2022-05-09 13:37:43 +02:00
def test_optional_falsepositives_capital ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
fps = self . get_rule_part (
file_path = file , part_name = " falsepositives " )
2022-05-09 13:37:43 +02:00
if fps :
for fp in fps :
# first letter should be capital
try :
if fp [ 0 ] . upper ( ) != fp [ 0 ] :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} defines a falsepositive that does not start with a capital letter: ' {} ' . " . format ( file , fp ) )
2022-05-09 13:37:43 +02:00
faulty_rules . append ( file )
except TypeError as err :
print ( " TypeError Exception for rule {} " . format ( file ) )
print ( " Error: {} " . format ( err ) )
print ( " Maybe you created an empty falsepositive item? " )
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules with false positives that don ' t start with a capital letter (e.g. ' unknown ' should be ' Unknown ' ) " )
2022-05-09 14:43:49 +02:00
def test_optional_falsepositives_blocked_content ( self ) :
faulty_rules = [ ]
banned_words = [ " none " , " pentest " , " penetration test " ]
common_typos = [ " unkown " , " ligitimate " , " legitim " , " legitimeate " ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
fps = self . get_rule_part (
file_path = file , part_name = " falsepositives " )
2022-05-09 14:43:49 +02:00
if fps :
for fp in fps :
for typo in common_typos :
if fp == " Unknow " or typo in fp . lower ( ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} defines a falsepositive with a common typo: ' {} ' . " . format ( file , typo ) )
2022-05-09 14:43:49 +02:00
faulty_rules . append ( file )
for banned_word in banned_words :
if banned_word in fp . lower ( ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} defines a falsepositive with an invalid reason: ' {} ' . " . format ( file , banned_word ) )
2022-05-09 14:43:49 +02:00
faulty_rules . append ( file )
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules with invalid false positive definitions (e.g. Pentest, None or common typos) " )
2021-08-14 19:16:36 +02:00
# Upgrade Detection Rule License 1.1
2021-10-25 18:14:03 +02:00
def test_optional_author ( self ) :
2021-07-27 19:14:00 +02:00
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
author_str = self . get_rule_part ( file_path = file , part_name = " author " )
if author_str :
# it exists but isn't a string
if not isinstance ( author_str , str ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a ' author ' field that isn ' t a string. " . format ( file ) )
2021-07-27 19:14:00 +02:00
faulty_rules . append ( file )
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2021-08-14 19:16:36 +02:00
" There are rules with malformed ' author ' fields. (has to be a string even if it contains many author) " )
2021-07-27 19:14:00 +02:00
2021-08-14 19:42:29 +02:00
def test_optional_license ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
license_str = self . get_rule_part (
file_path = file , part_name = " license " )
2021-08-14 19:42:29 +02:00
if license_str :
if not isinstance ( license_str , str ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a malformed ' license ' (has to be a string). " . format ( file ) )
2021-08-14 19:42:29 +02:00
faulty_rules . append ( file )
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules with malformed ' license ' fields. (has to be a string ) " )
2021-08-11 14:26:20 +02:00
def test_optional_tlp ( self ) :
faulty_rules = [ ]
valid_tlp = [
" WHITE " ,
" GREEN " ,
" AMBER " ,
" RED " ,
2022-08-12 14:19:08 +02:00
]
2021-08-11 14:26:20 +02:00
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
tlp_str = self . get_rule_part ( file_path = file , part_name = " tlp " )
if tlp_str :
# it exists but isn't a string
if not isinstance ( tlp_str , str ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a ' tlp ' field that isn ' t a string. " . format ( file ) )
2021-08-11 14:26:20 +02:00
faulty_rules . append ( file )
elif not tlp_str . upper ( ) in valid_tlp :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a ' tlp ' field with not valid value. " . format ( file ) )
2021-08-11 14:26:20 +02:00
faulty_rules . append ( file )
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2021-08-11 14:26:20 +02:00
" There are rules with malformed optional ' tlp ' fields. (https://www.cisa.gov/tlp) " )
def test_optional_target ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
target = self . get_rule_part ( file_path = file , part_name = " target " )
if target :
# it exists but isn't a list
if not isinstance ( target , list ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a ' target ' field that isn ' t a list. " . format ( file ) )
2021-08-11 14:26:20 +02:00
faulty_rules . append ( file )
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2021-08-11 14:26:20 +02:00
" There are rules with malformed ' target ' fields. (has to be a list of values even if it contains only a single value) " )
2020-07-13 18:07:19 +02:00
def test_references ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
references = self . get_rule_part (
file_path = file , part_name = " references " )
2020-11-27 10:17:45 +01:00
# Reference field doesn't exist
# if not references:
2022-08-12 14:19:08 +02:00
# print(Fore.YELLOW + "Rule {} has no field 'references'.".format(file))
# faulty_rules.append(file)
2020-07-14 12:33:02 +02:00
if references :
2020-07-13 18:49:00 +02:00
# it exists but isn't a list
if not isinstance ( references , list ) :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a references field that isn ' t a list. " . format ( file ) )
2020-11-27 10:17:45 +01:00
faulty_rules . append ( file )
2020-07-13 18:07:19 +02:00
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2020-07-13 18:07:19 +02:00
" There are rules with malformed ' references ' fields. (has to be a list of values even if it contains only a single value) " )
2020-11-27 10:17:45 +01:00
def test_references_plural ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
reference = self . get_rule_part (
file_path = file , part_name = " reference " )
2020-11-27 10:17:45 +01:00
if reference :
# it exists but in singular form
faulty_rules . append ( file )
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules with malformed ' references ' fields. (has to be ' references ' in plural form, not singular) " )
2020-07-14 12:33:16 +02:00
def test_file_names ( self ) :
faulty_rules = [ ]
2021-09-23 06:50:18 +02:00
name_lst = [ ]
2022-05-09 15:41:47 +02:00
filename_pattern = re . compile ( r ' [a-z0-9_] { 10,70} \ .yml ' )
2020-07-14 12:33:16 +02:00
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
filename = os . path . basename ( file )
2021-09-23 06:50:18 +02:00
if filename in name_lst :
print ( Fore . YELLOW + " Rule {} is a duplicate file name. " . format ( file ) )
2022-05-09 10:23:38 +02:00
faulty_rules . append ( file )
2021-09-23 06:50:18 +02:00
elif filename [ - 4 : ] != " .yml " :
2022-08-12 14:19:08 +02:00
print ( Fore . YELLOW +
" Rule {} has a invalid extension (.yml). " . format ( file ) )
2021-09-22 19:02:44 +02:00
faulty_rules . append ( file )
elif len ( filename ) > 74 :
2022-08-12 14:19:08 +02:00
print ( Fore . YELLOW +
" Rule {} has a file name too long >70. " . format ( file ) )
2021-09-22 19:02:44 +02:00
faulty_rules . append ( file )
elif len ( filename ) < 14 :
2022-08-12 14:19:08 +02:00
print ( Fore . YELLOW +
" Rule {} has a file name too short <10. " . format ( file ) )
2021-09-22 19:02:44 +02:00
faulty_rules . append ( file )
elif filename_pattern . match ( filename ) == None or not ' _ ' in filename :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a file name that doesn ' t match our standard. " . format ( file ) )
2021-08-11 14:26:20 +02:00
faulty_rules . append ( file )
2021-09-23 06:50:18 +02:00
name_lst . append ( filename )
2020-07-14 12:33:16 +02:00
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2022-05-09 15:41:47 +02:00
r ' There are rules with malformed file names (too short, too long, uppercase letters, a minus sign etc.). Please see the file names used in our repository and adjust your file names accordingly. The pattern for a valid file name is \' [a-z0-9_] { 10,70} \ .yml \' and it has to contain at least an underline character. ' )
2020-07-14 12:33:16 +02:00
2020-01-30 17:26:21 +01:00
def test_title ( self ) :
faulty_rules = [ ]
2020-02-20 23:00:16 +01:00
allowed_lowercase_words = [
2022-08-12 14:19:08 +02:00
' the ' ,
' for ' ,
' in ' ,
' with ' ,
' via ' ,
' on ' ,
' to ' ,
' without ' ,
' of ' ,
' through ' ,
' from ' ,
' by ' ,
' as ' ,
' a ' ,
' or ' ,
' at ' ,
' and ' ,
' an ' ,
' over ' ,
' new ' ,
]
2020-01-30 17:26:21 +01:00
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
title = self . get_rule_part ( file_path = file , part_name = " title " )
if not title :
print ( Fore . RED + " Rule {} has no field ' title ' . " . format ( file ) )
faulty_rules . append ( file )
continue
elif len ( title ) > 70 :
2022-08-12 14:19:08 +02:00
print (
Fore . YELLOW + " Rule {} has a title field with too many characters (>70) " . format ( file ) )
2020-01-30 17:26:21 +01:00
faulty_rules . append ( file )
if title . startswith ( " Detects " ) :
2022-08-12 14:19:08 +02:00
print (
Fore . RED + " Rule {} has a title that starts with ' Detects ' " . format ( file ) )
2020-01-30 17:26:21 +01:00
faulty_rules . append ( file )
2022-05-10 11:25:09 +02:00
if title . endswith ( " . " ) :
print ( Fore . RED + " Rule {} has a title that ends with ' . ' " . format ( file ) )
faulty_rules . append ( file )
2020-01-30 17:26:21 +01:00
wrong_casing = [ ]
for word in title . split ( " " ) :
2020-05-23 10:03:13 -04:00
if word . islower ( ) and not word . lower ( ) in allowed_lowercase_words and not " . " in word and not " / " in word and not word [ 0 ] . isdigit ( ) :
2020-01-30 17:26:21 +01:00
wrong_casing . append ( word )
if len ( wrong_casing ) > 0 :
2022-08-12 14:19:08 +02:00
print ( Fore . RED + " Rule {} has a title that has not title capitalization. Words: ' {} ' " . format (
file , " , " . join ( wrong_casing ) ) )
2020-01-30 17:26:21 +01:00
faulty_rules . append ( file )
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2021-08-31 12:50:11 +02:00
" There are rules with non-conform ' title ' fields. Please check: https://github.com/SigmaHQ/sigma/wiki/Rule-Creation-Guide#title " )
2019-03-02 20:51:49 +03:00
2022-05-09 16:05:19 +02:00
def test_title_in_first_line ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
yaml = self . get_rule_yaml ( file )
# skip multi-part yaml
if len ( yaml ) > 1 :
continue
# this propably is not the best way to check whether
# title is the attribute given in the 1st line
# (also assumes dict keeps the order from the input file)
if list ( yaml [ 0 ] . keys ( ) ) [ 0 ] != " title " :
2022-08-12 14:19:08 +02:00
print (
Fore . RED + " Rule {} does not have its ' title ' attribute in the first line " . format ( file ) )
2022-05-09 16:05:19 +02:00
faulty_rules . append ( file )
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2022-08-12 14:19:08 +02:00
" There are rules without the ' title ' attribute in their first line. " )
2022-05-09 16:05:19 +02:00
2020-07-13 17:02:28 -04:00
def test_invalid_logsource_attributes ( self ) :
faulty_rules = [ ]
2021-08-10 10:21:22 +02:00
valid_logsource = [
2022-08-12 14:19:08 +02:00
' category ' ,
' product ' ,
' service ' ,
' definition ' ,
]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
logsource = self . get_rule_part (
file_path = file , part_name = " logsource " )
2021-09-12 20:04:58 +02:00
if not logsource :
print ( Fore . RED + " Rule {} has no ' logsource ' . " . format ( file ) )
faulty_rules . append ( file )
2022-05-09 10:23:38 +02:00
continue
2021-08-10 10:21:22 +02:00
valid = True
2020-07-13 17:02:28 -04:00
for key in logsource :
2021-08-10 10:21:22 +02:00
if key . lower ( ) not in valid_logsource :
2022-08-12 14:19:08 +02:00
print (
Fore . RED + " Rule {} has a logsource with an invalid field ( {} ) " . format ( file , key ) )
2021-08-18 19:00:57 +00:00
valid = False
2022-08-12 14:19:08 +02:00
elif not isinstance ( logsource [ key ] , str ) :
print (
Fore . RED + " Rule {} has a logsource with an invalid field type ( {} ) " . format ( file , key ) )
2021-09-27 18:59:05 +02:00
valid = False
2021-08-10 10:21:22 +02:00
if not valid :
2022-08-12 14:19:08 +02:00
faulty_rules . append ( file )
2021-08-11 14:26:20 +02:00
2022-05-09 10:23:38 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2021-08-10 11:07:28 +02:00
" There are rules with non-conform ' logsource ' fields. Please check: https://github.com/SigmaHQ/sigma/wiki/Rule-Creation-Guide#log-source " )
2022-05-09 10:23:38 +02:00
2021-08-14 09:54:27 +02:00
def test_selection_list_one_value ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
2022-05-10 17:12:43 +02:00
if detection :
valid = True
for key in detection :
2022-08-12 14:19:08 +02:00
if isinstance ( detection [ key ] , list ) :
# rule with only list of Keywords term
if len ( detection [ key ] ) == 1 and not isinstance ( detection [ key ] [ 0 ] , str ) :
print (
Fore . RED + " Rule {} has the selection ( {} ) with a list of only 1 element in detection " . format ( file , key ) )
2021-08-14 09:54:27 +02:00
valid = False
2022-08-12 14:19:08 +02:00
if isinstance ( detection [ key ] , dict ) :
2022-05-10 17:12:43 +02:00
for sub_key in detection [ key ] :
2022-08-12 14:19:08 +02:00
# split in 2 if as get a error "int has not len()"
if isinstance ( detection [ key ] [ sub_key ] , list ) :
2022-05-10 17:12:43 +02:00
if len ( detection [ key ] [ sub_key ] ) == 1 :
2022-08-12 14:19:08 +02:00
print (
Fore . RED + " Rule {} has the selection ( {} / {} ) with a list of only 1 value in detection " . format ( file , key , sub_key ) )
2022-05-10 17:12:43 +02:00
valid = False
if not valid :
faulty_rules . append ( file )
2022-05-09 10:23:38 +02:00
2022-08-12 14:19:08 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules using list with only 1 element " )
2021-05-15 13:09:08 +02:00
2022-05-10 11:07:40 +02:00
def test_unused_selection ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
2022-05-10 11:07:40 +02:00
condition = detection [ " condition " ]
wildcard_selections = re . compile ( r " \ sof \ s([ \ w \ *]+)(?:$| \ s| \ )) " )
# skip rules containing aggregations
if type ( condition ) == list :
continue
for selection in detection :
if selection == " condition " :
continue
if selection == " timeframe " :
continue
if selection in condition :
continue
# find all wildcards in condition
found = False
for wildcard_selection in wildcard_selections . findall ( condition ) :
# wildcard matches selection
if re . search ( wildcard_selection . replace ( r " * " , r " .* " ) , selection ) is not None :
found = True
break
# selection was not found in condition
if not found :
2022-08-12 14:19:08 +02:00
print (
Fore . RED + " Rule {} has an unused selection ' {} ' " . format ( file , selection ) )
2022-05-10 11:07:40 +02:00
faulty_rules . append ( file )
2022-08-12 14:19:08 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules with unused selections " )
2022-05-10 11:07:40 +02:00
2022-05-11 11:06:09 +02:00
def test_all_value_modifier_single_item ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
2022-05-11 11:06:09 +02:00
if detection :
for search_identifier in detection :
2022-08-12 14:19:08 +02:00
if isinstance ( detection [ search_identifier ] , dict ) :
2022-05-11 11:06:09 +02:00
for field in detection [ search_identifier ] :
2022-08-12 14:19:08 +02:00
if " |all " in field and not isinstance ( detection [ search_identifier ] [ field ] , list ) :
print ( Fore . RED + " Rule {} uses the ' all ' modifier on a single item in selection ( {} / {} ) " . format (
file , search_identifier , field ) )
2022-05-11 11:06:09 +02:00
faulty_rules . append ( file )
self . assertEqual ( faulty_rules , [ ] , Fore . RED + " There are rules with |all modifier only having one item. " +
2022-08-12 14:19:08 +02:00
" Single item values are not allowed to have an all modifier as some back-ends cannot support it. " +
" If you use it as a workaround to duplicate a field in a selection, use a new selection instead. " )
2022-05-11 11:06:09 +02:00
2022-05-27 15:13:26 +02:00
def test_field_user_localization ( self ) :
def checkUser ( faulty_rules , dict ) :
for key , value in dict . items ( ) :
if " User " in key :
if type ( value ) == str :
if " AUTORI " in value or " AUTHORI " in value :
print ( " Localized user name ' {} ' . " . format ( value ) )
faulty_rules . append ( file )
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
2022-05-27 15:13:26 +02:00
for sel_key , sel_value in detection . items ( ) :
if sel_key == " condition " or sel_key == " timeframe " :
continue
# single item selection
if type ( sel_value ) == dict :
checkUser ( faulty_rules , sel_value )
if type ( sel_value ) == list :
# skip keyword selection
if type ( sel_value [ 0 ] ) != dict :
continue
# multiple item selection
for item in sel_value :
checkUser ( faulty_rules , item )
self . assertEqual ( faulty_rules , [ ] , Fore . RED + " There are rules that match using localized user accounts. Better employ a generic version such as: \n " +
2022-08-12 14:19:08 +02:00
" User|contains: # covers many language settings \n " +
" - ' AUTHORI ' \n " +
" - ' AUTORI ' " )
2022-05-27 15:13:26 +02:00
2021-09-10 13:33:16 +02:00
def test_condition_operator_casesensitive ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
2022-08-12 14:19:08 +02:00
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
if detection :
valid = True
if isinstance ( detection [ " condition " ] , str ) :
param = detection [ " condition " ] . split ( ' ' )
for item in param :
2021-09-10 13:33:16 +02:00
if item . lower ( ) == ' or ' and not item == ' or ' :
valid = False
elif item . lower ( ) == ' and ' and not item == ' and ' :
valid = False
elif item . lower ( ) == ' not ' and not item == ' not ' :
2022-05-09 10:23:38 +02:00
valid = False
2021-09-10 13:33:16 +02:00
elif item . lower ( ) == ' of ' and not item == ' of ' :
2022-05-09 10:23:38 +02:00
valid = False
2022-08-12 14:19:08 +02:00
if not valid :
print ( Fore . RED + " Rule {} has a invalid condition ' {} ' : ' or ' , ' and ' , ' not ' , ' of ' are lowercase " . format (
file , detection [ " condition " ] ) )
faulty_rules . append ( file )
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules using condition without lowercase operator " )
2022-05-09 10:23:38 +02:00
2021-09-10 13:33:16 +02:00
2020-07-14 17:54:02 +02:00
def get_mitre_data ( ) :
"""
2022-08-12 14:19:08 +02:00
Use Tags from CTI subrepo to get consitant data
2020-07-14 17:54:02 +02:00
"""
2022-08-12 14:19:08 +02:00
cti_path = " tests/cti/ "
2020-09-30 08:53:52 +02:00
# Get ATT&CK information
2022-08-12 14:19:08 +02:00
lift = attack_client ( local_path = cti_path )
2020-07-14 17:54:02 +02:00
# Techniques
MITRE_TECHNIQUES = [ ]
MITRE_TECHNIQUE_NAMES = [ ]
MITRE_PHASE_NAMES = set ( )
MITRE_TOOLS = [ ]
MITRE_GROUPS = [ ]
2022-05-09 10:23:38 +02:00
# Techniques
2020-07-14 17:54:02 +02:00
enterprise_techniques = lift . get_enterprise_techniques ( )
for t in enterprise_techniques :
2022-08-12 14:19:08 +02:00
MITRE_TECHNIQUE_NAMES . append (
t [ ' name ' ] . lower ( ) . replace ( ' ' , ' _ ' ) . replace ( ' - ' , ' _ ' ) )
2020-07-14 17:54:02 +02:00
for r in t . external_references :
if ' external_id ' in r :
MITRE_TECHNIQUES . append ( r [ ' external_id ' ] . lower ( ) )
if ' kill_chain_phases ' in t :
for kc in t [ ' kill_chain_phases ' ] :
if ' phase_name ' in kc :
2022-08-12 14:19:08 +02:00
MITRE_PHASE_NAMES . add ( kc [ ' phase_name ' ] . replace ( ' - ' , ' _ ' ) )
2020-07-14 17:54:02 +02:00
# Tools / Malware
enterprise_tools = lift . get_enterprise_tools ( )
for t in enterprise_tools :
for r in t . external_references :
if ' external_id ' in r :
MITRE_TOOLS . append ( r [ ' external_id ' ] . lower ( ) )
enterprise_malware = lift . get_enterprise_malware ( )
for m in enterprise_malware :
for r in m . external_references :
if ' external_id ' in r :
MITRE_TOOLS . append ( r [ ' external_id ' ] . lower ( ) )
# Groups
enterprise_groups = lift . get_enterprise_groups ( )
for g in enterprise_groups :
for r in g . external_references :
if ' external_id ' in r :
MITRE_GROUPS . append ( r [ ' external_id ' ] . lower ( ) )
2022-01-19 15:21:50 +01:00
2022-05-09 10:23:38 +02:00
# Debugging
2022-08-12 14:19:08 +02:00
print ( " MITRE ATT&CK LIST LENGTHS: %d %d %d %d %d " % ( len ( MITRE_TECHNIQUES ) , len (
MITRE_TECHNIQUE_NAMES ) , len ( list ( MITRE_PHASE_NAMES ) ) , len ( MITRE_GROUPS ) , len ( MITRE_TOOLS ) ) )
2022-01-19 15:21:50 +01:00
2020-07-14 17:54:02 +02:00
# Combine all IDs to a big tag list
return [ " attack. " + item for item in MITRE_TECHNIQUES + MITRE_TECHNIQUE_NAMES + list ( MITRE_PHASE_NAMES ) + MITRE_GROUPS + MITRE_TOOLS ]
2020-07-13 17:02:28 -04:00
2019-01-22 21:25:13 +03:00
if __name__ == " __main__ " :
2020-01-30 08:37:47 +01:00
init ( autoreset = True )
2020-09-30 08:53:52 +02:00
# Get Current Data from MITRE ATT&CK®
2020-07-14 17:54:02 +02:00
MITRE_ALL = get_mitre_data ( )
# Run the tests
2019-01-23 23:31:36 +01:00
unittest . main ( )