2019-11-11 23:35:16 +01:00
#!/usr/bin/env python3
# Assign UUIDs to Sigma rules and verify UUID assignment for a Sigma rule repository
2021-09-05 10:43:42 +02:00
# Copyright 2016-2021 SigmaHQ
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2019-11-11 23:35:16 +01:00
from argparse import ArgumentParser
from pathlib import Path
from uuid import uuid4 , UUID
2021-09-05 10:43:42 +02:00
import ruamel . yaml
2021-09-02 20:07:03 +02:00
2019-11-11 23:35:16 +01:00
def print_verbose ( * arg , * * kwarg ) :
2020-03-31 11:35:21 +02:00
print ( * arg , * * kwarg )
2019-11-11 23:35:16 +01:00
2021-09-05 10:43:42 +02:00
def valid_id ( rule , i , path ) :
2021-09-02 20:07:03 +02:00
try :
UUID ( rule [ " id " ] )
except ValueError : # id is not a valid UUID
print ( " Rule {} in file {} has a malformed UUID ' {} ' . " . format ( i , str ( path ) , rule [ " id " ] ) )
return False
except KeyError : # rule has no id
print ( " Rule {} in file {} has no UUID. " . format ( i , str ( path ) ) )
return False
return True
2021-09-05 10:43:42 +02:00
def is_global ( rule ) :
if ' action ' in rule :
if rule [ ' action ' ] == ' global ' :
return True
return False
2020-03-31 11:30:14 +02:00
def main ( ) :
2021-08-18 19:00:57 +00:00
argparser = ArgumentParser ( description = " Assign and verify UUIDs of Sigma rules " )
2020-03-31 11:30:14 +02:00
argparser . add_argument ( " --verify " , " -V " , action = " store_true " , help = " Verify existence and uniqueness of UUID assignments. Exits with error code if verification fails. " )
argparser . add_argument ( " --verbose " , " -v " , action = " store_true " , help = " Be verbose. " )
argparser . add_argument ( " --recursive " , " -r " , action = " store_true " , help = " Recurse into directories. " )
argparser . add_argument ( " --error " , " -e " , action = " store_true " , help = " Exit with error code 10 on verification failures. " )
argparser . add_argument ( " inputs " , nargs = " + " , help = " Sigma rule files or repository directories " )
args = argparser . parse_args ( )
2019-11-11 23:35:16 +01:00
2020-03-31 11:35:21 +02:00
if args . verbose :
print_verbose ( )
2020-03-31 11:30:14 +02:00
if args . recursive :
paths = [ p for pathname in args . inputs for p in Path ( pathname ) . glob ( " **/* " ) if p . is_file ( ) ]
2019-11-11 23:35:16 +01:00
else :
2020-03-31 11:30:14 +02:00
paths = [ Path ( pathname ) for pathname in args . inputs ]
passed = True
for path in paths :
print_verbose ( " Rule {} " . format ( str ( path ) ) )
2021-09-02 20:07:03 +02:00
with path . open ( " r " , encoding = " UTF-8 " ) as f :
2021-09-05 10:43:42 +02:00
rules = list ( ruamel . yaml . load_all ( f , Loader = ruamel . yaml . RoundTripLoader ) )
2020-03-31 11:30:14 +02:00
if args . verify :
2021-09-05 10:43:42 +02:00
i = 0
2020-03-31 11:30:14 +02:00
for rule in rules :
2021-09-05 10:43:42 +02:00
if is_global ( rule ) : # No id in global section
if ' id ' in rule :
2020-03-31 11:30:14 +02:00
passed = False
2021-09-05 10:43:42 +02:00
print ( " Rule {} in file {} has ID in global section. " . format ( i , str ( path ) ) )
else :
if not valid_id ( rule , i , path ) :
2020-03-31 11:30:14 +02:00
passed = False
2021-09-05 10:43:42 +02:00
i + = 1
2020-03-31 11:30:14 +02:00
else :
changed = False
i = 1
for rule in rules :
2021-09-05 17:50:54 +02:00
if is_global ( rule ) :
if ' id ' in rule :
uuid = rule [ ' id ' ]
del rule [ ' id ' ]
print ( " Remove Global UUID ' {} ' to rule {} in file {} . " . format ( str ( uuid ) , i , str ( path ) ) )
changed = True
2020-03-31 11:30:14 +02:00
else :
2021-09-05 17:50:54 +02:00
if ' id ' in rule :
if not valid_id ( rule , i , path ) :
uuid = uuid4 ( )
rule [ ' id ' ] = str ( uuid )
changed = True
print ( " Change bad UUID ' {} ' to rule {} in file {} . " . format ( str ( uuid ) , i , str ( path ) ) )
else :
pos = 1 if ' title ' in rule else 0 #put id in after title is need
uuid = uuid4 ( )
rule . insert ( pos , " id " , str ( uuid ) )
changed = True
print ( " Assigned UUID ' {} ' to rule {} in file {} . " . format ( str ( uuid ) , i , str ( path ) ) )
2020-03-31 11:30:14 +02:00
i + = 1
if changed :
with path . open ( " w " ) as f :
2021-09-05 17:50:54 +02:00
for rule in rules :
start = False if is_global ( rule ) else True
if len ( rules ) == 1 : start = False # avoid --- if only one rule
ruamel . yaml . round_trip_dump ( rule , stream = f , indent = 4 , block_seq_indent = 4 , explicit_start = start )
2019-11-11 23:35:16 +01:00
2020-03-31 11:30:14 +02:00
if not passed :
print ( " The Sigma rules listed above don ' t have an ID. The ID must be: " )
print ( " * Contained in the ' id ' attribute " )
print ( " * a valid UUIDv4 (randomly generated) " )
print ( " * Unique in this repository " )
2020-03-31 16:29:58 +02:00
print ( " Please generate one with the sigma_uuid tool or here: https://www.uuidgenerator.net/version4 " )
2020-03-31 11:30:14 +02:00
if args . error :
exit ( 10 )
2019-11-11 23:35:16 +01:00
2020-03-31 11:30:14 +02:00
if __name__ == " __main__ " :
main ( )