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
2022-11-29 13:47:09 +01:00
import collections
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 ) :
2023-01-04 15:58:35 +01:00
@classmethod
def setUpClass ( cls ) :
print ( " Calling get_mitre_data() " )
# Get Current Data from MITRE ATT&CK®
cls . MITRE_ALL = get_mitre_data ( )
print ( " Catched data - starting tests... " )
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
2023-01-04 16:25:07 +01:00
path_to_rules = " ../rules "
path_to_rules = os . path . join ( os . path . dirname ( os . path . realpath ( __file__ ) ) , path_to_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 ) :
2022-12-22 08:46:25 +01:00
# See Issue # https://github.com/SigmaHQ/sigma/issues/1028
2021-05-15 13:02:49 +02:00
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 (
2022-11-11 10:07:57 +01:00
r " cve \ . \ d+ \ . \ d+|attack \ .(t \ d {4} \ . \ d {3} |[gts] \ d {4} )$|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 :
2023-01-04 15:58:35 +01:00
if tag not in self . 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-11-29 13:47:09 +01:00
def check_list_or_recurse_on_dict ( item , depth : int , special : bool ) - > None :
2019-01-25 12:22:28 +03:00
if type ( item ) == list :
2022-11-29 13:47:09 +01:00
check_if_list_contain_duplicates ( item , depth , special )
2019-01-25 12:22:28 +03:00
elif type ( item ) == dict and depth < = MAX_DEPTH :
2022-11-29 13:47:09 +01:00
for keys , sub_item in item . items ( ) :
if " |base64 " in keys : # Covers both "base64" and "base64offset" modifiers
check_list_or_recurse_on_dict ( sub_item , depth + 1 , True )
else :
check_list_or_recurse_on_dict ( sub_item , depth + 1 , special )
2019-01-25 12:22:28 +03:00
2022-11-29 13:47:09 +01:00
def check_if_list_contain_duplicates ( item : list , depth : int , special : bool ) - > None :
2019-01-25 12:22:28 +03:00
try :
2022-11-29 13:47:09 +01:00
# We use a list comprehension to convert all the element to lowercase. Since we don't care about casing in SIGMA except for the following modifiers
# - "base64offset"
# - "base64"
if special :
item_ = item
else :
item_ = [ i . lower ( ) for i in item ]
if len ( item_ ) != len ( set ( item_ ) ) :
# We find the duplicates and then print them to the user
duplicates = [ i for i , count in collections . Counter ( item_ ) . items ( ) if count > 1 ]
print ( Fore . RED + " Rule {} has duplicate filters {} " . format ( file , duplicates ) )
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 :
2022-11-29 13:47:09 +01:00
check_list_or_recurse_on_dict ( sub_item , depth + 1 , special )
2019-01-25 12:22:28 +03:00
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 " )
2022-11-29 13:47:09 +01:00
check_list_or_recurse_on_dict ( detection , 1 , False )
2019-01-25 12:22:28 +03:00
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 :
2022-10-21 17:29:22 +02:00
detection = self . get_rule_part ( file_path = file , part_name = " detection " )
if detection :
for search_identifier in detection :
if isinstance ( detection [ search_identifier ] , dict ) :
for field in detection [ search_identifier ] :
if " Provider_Name " in field :
if isinstance ( detection [ search_identifier ] [ " Provider_Name " ] , list ) :
for value in detection [ search_identifier ] [ " Provider_Name " ] :
if " Microsoft-Windows-Security-Auditing " in value or " Microsoft-Windows-Sysmon " in value :
if file not in faulty_detections :
faulty_detections . append ( file )
else :
if " Microsoft-Windows-Security-Auditing " in detection [ search_identifier ] [ " Provider_Name " ] or " Microsoft-Windows-Sysmon " in detection [ search_identifier ] [ " Provider_Name " ] :
if file not in faulty_detections :
faulty_detections . append ( file )
2019-03-02 20:51:49 +03:00
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 )
2022-12-05 00:31:51 +01:00
elif id . lower ( ) 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 :
2022-12-05 00:31:51 +01:00
dict_id [ id . lower ( ) ] = 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) " )
2022-11-29 23:29:38 +01:00
def test_references_in_description ( self ) :
# This test checks for the presence of a links and special keywords in the "description" field while there is no "references" field.
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
references = self . get_rule_part (
file_path = file , part_name = " references " )
# Reference field doesn't exist
if not references :
descriptionfield = self . get_rule_part (
file_path = file , part_name = " description " )
if descriptionfield :
2022-11-30 10:06:10 +01:00
for i in [ " http:// " , " https:// " , " internal research " ] : # Extends the list with other common references starters
if i in descriptionfield . lower ( ) :
2022-11-30 10:21:24 +01:00
print ( Fore . RED + " Rule {} has a field that contains references to external links but no references set. Add a ' references ' key and add URLs as list items. " . format ( file ) )
2022-11-29 23:29:38 +01:00
faulty_rules . append ( file )
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules with malformed ' description ' fields. (links and external references have to be in a seperate field named ' references ' . see specification https://github.com/SigmaHQ/sigma-specification) " )
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 )
2022-12-23 09:25:16 +01:00
else :
# This test make sure that every rules has a filename that corresponds to
# It's specific logsource.
# Fix Issue #1381 (https://github.com/SigmaHQ/sigma/issues/1381)
logsource = self . get_rule_part ( file_path = file , part_name = " logsource " )
if logsource :
pattern_prefix = " "
os_infix = " "
os_bool = False
for key , value in logsource . items ( ) :
if key == " definition " :
pass
else :
if key == " product " :
# This is to get the OS for certain categories
if value == " windows " :
os_infix = " win_ "
elif value == " macos " :
os_infix = " macos_ "
elif value == " linux " :
os_infix = " lnx_ "
# For other stuff
elif value == " aws " :
pattern_prefix = " aws_ "
elif value == " azure " :
pattern_prefix = " azure_ "
elif value == " gcp " :
pattern_prefix = " gcp_ "
elif value == " gworkspace " :
pattern_prefix = " gworkspace_ "
elif value == " m365 " :
pattern_prefix = " microsoft365_ "
elif value == " okta " :
pattern_prefix = " okta_ "
elif value == " onelogin " :
pattern_prefix = " onelogin_ "
elif key == " category " :
if value == " process_creation " :
pattern_prefix = " proc_creation_ "
os_bool = True
elif value == " image_load " :
pattern_prefix = " image_load_ "
elif value == " file_event " :
pattern_prefix = " file_event_ "
os_bool = True
elif value == " registry_set " :
pattern_prefix = " registry_set_ "
elif value == " registry_add " :
pattern_prefix = " registry_add_ "
elif value == " registry_event " :
pattern_prefix = " registry_event_ "
elif value == " registry_delete " :
pattern_prefix = " registry_delete_ "
elif value == " registry_rename " :
pattern_prefix = " registry_rename_ "
elif value == " process_access " :
pattern_prefix = " proc_access_ "
os_bool = True
elif value == " driver_load " :
pattern_prefix = " driver_load_ "
os_bool = True
elif value == " dns_query " :
pattern_prefix = " dns_query_ "
os_bool = True
elif value == " ps_script " :
pattern_prefix = " posh_ps_ "
elif value == " ps_module " :
pattern_prefix = " posh_pm_ "
elif value == " ps_classic_start " :
pattern_prefix = " posh_pc_ "
elif value == " pipe_created " :
pattern_prefix = " pipe_created_ "
elif value == " network_connection " :
pattern_prefix = " net_connection_ "
os_bool = True
elif value == " file_rename " :
pattern_prefix = " file_rename_ "
os_bool = True
elif value == " file_delete " :
pattern_prefix = " file_delete_ "
os_bool = True
elif value == " file_change " :
pattern_prefix = " file_change_ "
os_bool = True
elif value == " file_access " :
pattern_prefix = " file_access_ "
os_bool = True
elif value == " create_stream_hash " :
pattern_prefix = " create_stream_hash_ "
elif value == " create_remote_thread " :
pattern_prefix = " create_remote_thread_win_ "
elif value == " dns " :
pattern_prefix = " net_dns_ "
elif value == " firewall " :
pattern_prefix = " net_firewall_ "
elif value == " webserver " :
pattern_prefix = " web_ "
elif key == " service " :
if value == " auditd " :
pattern_prefix = " lnx_auditd_ "
elif value == " modsecurity " :
pattern_prefix = " modsec_ "
elif value == " diagnosis-scripted " :
pattern_prefix = " win_diagnosis_scripted_ "
elif value == " firewall-as " :
pattern_prefix = " win_firewall_as_ "
elif value == " msexchange-management " :
pattern_prefix = " win_exchange_ "
elif value == " security " :
pattern_prefix = " win_security_ "
elif value == " system " :
pattern_prefix = " win_system_ "
elif value == " taskscheduler " :
pattern_prefix = " win_taskscheduler_ "
elif value == " terminalservices-localsessionmanager " :
pattern_prefix = " win_terminalservices_ "
elif value == " windefend " :
pattern_prefix = " win_defender_ "
elif value == " wmi " :
pattern_prefix = " win_wmi_ "
elif value == " codeintegrity-operational " :
pattern_prefix = " win_codeintegrity_ "
elif value == " bits-client " :
pattern_prefix = " win_bits_client_ "
elif value == " applocker " :
pattern_prefix = " win_applocker_ "
2023-01-02 12:16:09 +01:00
elif value == " dns-server-analytic " :
pattern_prefix = " win_dns_analytic_ "
2023-01-02 22:19:32 +01:00
elif value == " bitlocker " :
pattern_prefix = " win_bitlocker_ "
2022-12-23 09:25:16 +01:00
# This value is used to test if we should add the OS infix for certain categories
if os_bool :
pattern_prefix + = os_infix
if pattern_prefix != " " :
if not filename . startswith ( pattern_prefix ) :
print (
Fore . YELLOW + " Rule {} has a file name that doesn ' t match our standard naming convention. " . format ( file ) )
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-12-23 09:25:16 +01: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. It also has to follow the following naming convention https://github.com/SigmaHQ/sigma-specification/blob/main/sigmahq/Sigmahq_filename_rule.md ' )
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-12-22 08:46:25 +01: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-12-22 08:46:25 +01: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
2022-10-21 17:29:22 +02:00
# this probably is not the best way to check whether
2022-05-09 16:05:19 +02:00
# 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
2022-12-22 08:46:25 +01:00
def test_duplicate_titles ( self ) :
# This test ensure that every rule has a unique title
faulty_rules = [ ]
titles_dict = { }
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
title = self . get_rule_part ( file_path = file , part_name = " title " ) . lower ( ) . rstrip ( )
duplicate = False
for rule , title_ in titles_dict . items ( ) :
if title == title_ :
print ( Fore . RED + " Rule {} has an already used title in {} . " . format ( file , rule ) )
duplicate = True
faulty_rules . append ( file )
continue
if not duplicate :
titles_dict [ file ] = title
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules that share the same ' title ' . Please check: https://github.com/SigmaHQ/sigma/wiki/Rule-Creation-Guide#title " )
2022-12-30 16:31:41 +01:00
# def test_invalid_logsource_attributes(self):
# faulty_rules = []
# valid_logsource = [
# '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")
# if not logsource:
# print(Fore.RED + "Rule {} has no 'logsource'.".format(file))
# faulty_rules.append(file)
# continue
# valid = True
# for key in logsource:
# if key.lower() not in valid_logsource:
# print(
# Fore.RED + "Rule {} has a logsource with an invalid field ({})".format(file, key))
# valid = False
# elif not isinstance(logsource[key], str):
# print(
# Fore.RED + "Rule {} has a logsource with an invalid field type ({})".format(file, key))
# valid = False
# if not valid:
# faulty_rules.append(file)
# self.assertEqual(faulty_rules, [], Fore.RED +
# "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 ) :
2022-12-08 16:23:48 +01:00
def treat_list ( file , values , valid_ , selection_name ) :
# rule with only list of Keywords term
if len ( values ) == 1 and not isinstance ( values [ 0 ] , str ) :
print (
Fore . RED + " Rule {} has the selection ( {} ) with a list of only 1 element in detection " . format ( file , key )
)
valid_ = False
elif isinstance ( values [ 0 ] , dict ) :
valid_ = treat_dict ( file , values , valid_ , selection_name )
return valid_
def treat_dict ( file , values , valid_ , selection_name ) :
if isinstance ( values , list ) :
for dict_ in values :
for key_ in dict_ . keys ( ) :
if isinstance ( dict_ [ key_ ] , list ) :
if len ( dict_ [ key_ ] ) == 1 :
print (
Fore . RED + " Rule {} has the selection ( {} / {} ) with a list of only 1 value in detection " . format ( file , selection_name , key_ )
)
valid_ = False
else :
dict_ = values
for key_ in dict_ . keys ( ) :
if isinstance ( dict_ [ key_ ] , list ) :
if len ( dict_ [ key_ ] ) == 1 :
print (
Fore . RED + " Rule {} has the selection ( {} / {} ) with a list of only 1 value in detection " . format ( file , selection_name , key_ )
)
valid_ = False
return valid_
2021-08-14 09:54:27 +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
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
2022-05-10 17:12:43 +02:00
if detection :
2022-12-08 16:23:48 +01:00
2022-05-10 17:12:43 +02:00
valid = True
for key in detection :
2022-12-08 16:23:48 +01:00
values = detection [ key ]
2022-08-12 14:19:08 +02:00
if isinstance ( detection [ key ] , list ) :
2022-12-08 16:23:48 +01:00
valid = treat_list ( file , values , valid , key )
2022-08-12 14:19:08 +02:00
if isinstance ( detection [ key ] , dict ) :
2022-12-08 16:23:48 +01:00
valid = treat_dict ( file , values , valid , key )
2022-05-10 17:12:43 +02:00
if not valid :
faulty_rules . append ( file )
2022-12-08 16:23:48 +01:00
2022-08-12 14:19:08 +02:00
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
2022-12-08 16:23:48 +01:00
" There are rules using list with only 1 element " )
2021-05-15 13:09:08 +02:00
2022-12-22 08:41:37 +01:00
def test_selection_start_or_and ( self ) :
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
detection = self . get_rule_part (
file_path = file , part_name = " detection " )
if detection :
# This test is a best effort to avoid breaking SIGMAC parser. You could do more testing and try to fix this once and for all by modifiying the token regular expressions https://github.com/SigmaHQ/sigma/blob/b9ae5303f12cda8eb6b5b90a32fd7f11ad65645d/tools/sigma/parser/condition.py#L107-L127
for key in detection :
if key [ : 3 ] . lower ( ) == " sel " :
continue
elif key [ : 2 ] . lower ( ) == " or " :
print ( Fore . RED + " Rule {} has a selection ' {} ' that starts with the string ' or ' " . format ( file , key ) )
faulty_rules . append ( file )
elif key [ : 3 ] . lower ( ) == " and " :
print ( Fore . RED + " Rule {} has a selection ' {} ' that starts with the string ' and ' " . format ( file , key ) )
faulty_rules . append ( file )
elif key [ : 3 ] . lower ( ) == " not " :
print ( Fore . RED + " Rule {} has a selection ' {} ' that starts with the string ' not ' " . format ( file , key ) )
faulty_rules . append ( file )
self . assertEqual ( faulty_rules , [ ] , Fore . RED +
" There are rules with bad selection names. Can ' t start a selection name with an ' or* ' or an ' and* ' or a ' not* ' " )
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
2022-12-08 11:57:26 +01:00
# remove special keywords
condition_list = condition . replace ( " not " , ' ' ) . replace ( " 1 of " , ' ' ) . replace ( " all of " , ' ' ) . replace ( ' or ' , ' ' ) . replace ( ' and ' , ' ' ) . replace ( ' ( ' , ' ' ) . replace ( ' ) ' , ' ' ) . split ( " " )
if selection in condition_list :
2022-05-10 11:07:40 +02:00
continue
2022-12-08 11:57:26 +01:00
2022-05-10 11:07:40 +02:00
# 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-12-30 16:31:41 +01:00
# def test_field_name_typo(self):
# # add "OriginalFilename" after Aurora switched to SourceFilename
# # add "ProviderName" after special case powershell classic is resolved
# faulty_rules = []
# for file in self.yield_next_rule_file_path(self.path_to_rules):
# # typos is a list of tuples where each tuple contains ("The typo", "The correct version")
# typos = [("ServiceFilename", "ServiceFileName"), ("TargetFileName", "TargetFilename"), ("SourceFileName", "OriginalFileName"), ("Commandline", "CommandLine"), ("Targetobject", "TargetObject"), ("OriginalName", "OriginalFileName"), ("ImageFileName", "OriginalFileName"), ("details", "Details")]
# # Some fields exists in certain log sources in different forms than other log sources. We need to handle these as special cases
# # We check first the logsource to handle special cases
# logsource = self.get_rule_part(file_path=file, part_name="logsource").values()
# # add more typos in specific logsources below
# if "windefend" in logsource:
# typos += [("New_Value", "NewValue"), ("Old_Value", "OldValue"), ('Source_Name', 'SourceName'), ("Newvalue", "NewValue"), ("Oldvalue", "OldValue"), ('Sourcename', 'SourceName')]
# elif "registry_set" in logsource or "registry_add" in logsource or "registry_event" in logsource:
# typos += [("Targetobject", "TargetObject"), ("Eventtype", "EventType"), ("Newname", "NewName")]
# elif "process_creation" in logsource:
# typos += [("Parentimage", "ParentImage"), ("Integritylevel", "IntegrityLevel"), ("IntegritiLevel", "IntegrityLevel")]
# elif "file_access" in logsource:
# del(typos[typos.index(("TargetFileName", "TargetFilename"))]) # We remove the entry to "TargetFileName" to avoid confusion
# typos += [("TargetFileName", "FileName"), ("TargetFilename","FileName")]
# detection = self.get_rule_part(file_path=file, part_name="detection")
# if detection:
# for search_identifier in detection:
# if isinstance(detection[search_identifier], dict):
# for field in detection[search_identifier]:
# for typo in typos:
# if typo[0] in field:
# print(Fore.RED + "Rule {} has a common typo ({}) which should be ({}) in selection ({}/{})".format(file, typo[0], typo[1], search_identifier, field))
# faulty_rules.append(file)
# self.assertEqual(faulty_rules, [], Fore.RED + "There are rules with common typos in field names.")
2022-10-21 17:29:22 +02:00
2022-10-11 09:39:06 +02:00
def test_unknown_value_modifier ( self ) :
known_modifiers = [ " contains " , " startswith " , " endswith " , " all " , " base64offset " , " base64 " , " utf16le " , " utf16be " , " wide " , " utf16 " , " windash " , " re " ]
faulty_rules = [ ]
for file in self . yield_next_rule_file_path ( self . path_to_rules ) :
detection = self . get_rule_part ( file_path = file , part_name = " detection " )
if detection :
for search_identifier in detection :
if isinstance ( detection [ search_identifier ] , dict ) :
for field in detection [ search_identifier ] :
if " | " in field :
for current_modifier in field . split ( ' | ' ) [ 1 : ] :
found = False
for target_modifier in known_modifiers :
if current_modifier == target_modifier :
found = True
if not found :
print ( Fore . RED + " Rule {} uses an unknown field modifier ( {} / {} ) " . format ( file , search_identifier , field ) )
faulty_rules . append ( file )
self . assertEqual ( faulty_rules , [ ] , Fore . RED + " There are rules with unknown value modifiers. Most often it is just a typo. " )
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
"""
2023-01-04 16:02:57 +01:00
cti_path = " cti/ "
cti_path = os . path . join ( os . path . dirname ( os . path . realpath ( __file__ ) ) , cti_path )
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-07-14 17:54:02 +02:00
# Run the tests
2019-01-23 23:31:36 +01:00
unittest . main ( )