misp-galaxy/tools/sigma/sigma-to-galaxy.py

268 lines
9.8 KiB
Python

"""
Author: Jose Luis Sanchez Martinez
Twitter: @Joseliyo_Jstnk
date: 2022/11/18
Modified: 2023/01/03
GitHub: https://github.com/jstnk9/MISP
Description: This script can create MISP Galaxies from Sigma Rules. It can be done setting the path
where you have stored your sigma rules in the system.
Examples: python sigma-to-galaxy -p "C:\lab\sigma\rules\" -r
MISP Galaxy: https://github.com/MISP/misp-galaxy
"""
import os, json, yaml, argparse, uuid, configparser, time
unique_uuid = '9cf7cd2e-d5f1-48c4-9909-7896ba1c96b2'
def main(args):
uuidGalaxy = create_galaxy_json()
galaxyCluster = create_cluster(uuidGalaxy=unique_uuid)
valuesData = create_cluster_value(args.inputPath, args.recursive, galaxyCluster)
galaxyCluster["values"].extend(valuesData)
galaxyCluster = createRelations(galaxyCluster)
create_cluster_json(galaxyCluster)
check_duplicates(galaxyCluster)
def createRelations(galaxyCluster):
"""
:param galaxyCluster: Content of the cluster with all the values related to the Sigma Rules
:return galaxyCluster: Content of the cluster adding the relation between sigma rule and MITRE technique
"""
for obj in galaxyCluster["values"]:
for attack in obj["meta"]["tags"]:
if attack.startswith("attack.t"):
with open(
config["MISP"]["cluster_path"]
+ config["MISP"]["mitre_attack_cluster"],
"r",
) as mitreCluster:
data = json.load(mitreCluster)
for technique in data["values"]:
if (
technique["meta"]["external_id"]
== attack.split(".", 1)[1].upper()
):
if obj.get("related"):
obj["related"].append(
{
"dest-uuid": "%s" % (technique["uuid"]),
"tags": [
"estimative-language:likelihood-probability=\"almost-certain\""
],
"type": "related-to",
}
)
else:
obj["related"] = []
obj["related"].append(
{
"dest-uuid": "%s" % (technique["uuid"]),
"tags": [
"estimative-language:likelihood-probability=\"almost-certain\""
],
"type": "related-to",
}
)
return galaxyCluster
def check_duplicates(galaxy):
"""
:param galaxy: Content of the cluster with all the values
:return res:
"""
galaxiesObj = {}
for val in galaxy["values"]:
obj = {}
if galaxiesObj.get(val["value"]):
galaxiesObj[val["value"]].append(val["uuid"])
else:
galaxiesObj[val["value"]] = []
galaxiesObj[val["value"]].append(val["uuid"])
for k, v in galaxiesObj.items():
if len(v) > 1:
print("[*] Title duplicated: %s " % (k))
for ids in v:
print(" %s" % (ids))
def create_cluster_json(galaxyCluster):
"""
:param galaxyCluster: Content of the cluster with all the values related to the Sigma Rules
This function finally creates the sigma-cluster.json file with all the information.
"""
with open("sigma-cluster.json", "w") as f:
json.dump(galaxyCluster, f)
def parseYaml(inputPath, yamlFile):
"""
:param inputPath: Path where is stored the Sigma Rule to parse.
:param yamlFile: Content of the Sigma Rule.
This function can convert a Sigma Rule to JSON (dict)
:return jsonData: Sigma rule converted to dict.
"""
fullPath = os.path.join(inputPath, yamlFile)
with open(fullPath, encoding='utf-8') as f:
jsonData = yaml.load(f, Loader=yaml.FullLoader)
return jsonData
def create_cluster(uuidGalaxy=unique_uuid):
"""
:param uuidGalaxy: Is the uuid4 generated for the galaxy JSON file previously.
This function creates the JSON file of the path /app/files/misp-galaxy/clusters without values.
:return cluster: Dict with the basic information needed for the JSON file.
"""
version = int(time.strftime("%Y%m%d"))
cluster = {
"authors": ["@Joseliyo_Jstnk"],
"category": "rules",
"description": "MISP galaxy cluster based on Sigma Rules.",
"name": "Sigma-Rules",
"source": "https://github.com/jstnk9/MISP/tree/main/misp-galaxy/sigma",
"type": "sigma-rules",
"uuid": uuidGalaxy,
"values": [],
"version": version
}
return cluster
def create_cluster_value(pathsigma, recursive, galaxyCluster):
"""
:param pathsigma: Is the path established with the -p parameter
:param recurisve: If true, it can recursively navigate through every subfolder of :pathsigma:
:param galaxyCluster: Dictionary with the information needed for the cluster JSON file
This function makes a loop in every subfolder to identify Sigma Rules and after that..
1. It parse the YAML file to dict
2. Once it's a dict, it call the function to parse the dict and start creating the
values of the cluster
IMPORTANT: Sigma rules must ends with .yml and not .yaml
:return valuesData: Array with every Sigma Rule parsed into a dict.
"""
valuesData = []
if recursive == True:
for dirpath, dirs, files in os.walk(pathsigma):
if os.name == 'nt':
path = dirpath.split('/')[0]
else:
path = dirpath
for f in files:
if f.endswith(".yml"):
jsonData = parseYaml(path, f)
valuesData.append(
parse_sigma_to_cluster(jsonData, f, path.split("rules")[1])
)
return valuesData
def parse_sigma_to_cluster(jsonData, sigmaFile, sigmaPath):
"""
:param jsonData: Is the Sigma Rule parsed to dict.
:param sigmaFile: Is the Sigma Rule filename.
:param sigmaPath: Is the path where are stored the Sigma Rules.
This function parse the dict of the Sigma Rule to fill all the fields needed for the MISP Galaxy.
:return valueData: Dict with all the fields filled ready to be added in the cluster JSON file.
"""
valueData = {}
valueData["description"] = jsonData.get("description", "No established description")
valueData["uuid"] = jsonData.get("id", "No established id")
valueData["value"] = jsonData.get("title", "No established title")
valueData["meta"] = {}
valueData["meta"]["refs"] = []
if jsonData.get("references"):
for rf in jsonData.get("references"):
valueData["meta"]["refs"].append(rf)
valueData["meta"]["refs"] = [
*set(valueData["meta"]["refs"])
] # Removing duplicated references
valueData["meta"]["tags"] = jsonData.get("tags", "No established tags")
valueData["meta"]["creation_date"] = jsonData.get("date", "No established date")
valueData["meta"]["filename"] = sigmaFile
valueData["meta"]["author"] = jsonData.get("author", "No established author")
valueData["meta"]["level"] = jsonData.get("level", "No established level")
valueData["meta"]["falsepositive"] = jsonData.get(
"falsepositives", "No established falsepositives"
)
valueData["meta"]["refs"].append(
"https://github.com/SigmaHQ/sigma/tree/master/rules%s/%s"
% (sigmaPath.replace("\\", "/"), sigmaFile)
) # this value only works if you set the path like it was cloned from github
valueData["meta"]["logsource.category"] = jsonData.get("logsource").get(
"category", "No established category"
)
valueData["meta"]["logsource.product"] = jsonData.get("logsource").get(
"product", "No established product"
)
return valueData
def create_galaxy_json():
"""
This method creates first the galaxy JSON stored in the path /app/files/misp-galaxy/galaxies
The information of this JSON is basic.
:return uuidGalaxy: Return the uuid needed for the cluster JSON File which is created after this.
"""
uuidGalaxy = unique_uuid
galaxy = {
"description": "Sigma Rules are used to detect suspicious behaviors related to threat actors, malware and tools",
"icon": "link",
"name": "Sigma-Rules",
"namespace": "misp",
"type": "sigma-rules",
"uuid": uuidGalaxy,
"version": 1,
}
with open("sigma-rules.json", "w") as f:
json.dump(galaxy, f)
return uuidGalaxy
if __name__ == '__main__':
config = configparser.ConfigParser()
config.read("config.ini")
parser = argparse.ArgumentParser(
description="This script can convert your sigma rules in MISP galaxies, generating both files needed for cluster and galaxies. If you need more information about how to import it, please, go to https://github.com/jstnk9/MISP/tree/main/misp-galaxy/sigma"
)
parser.add_argument(
"-p",
"--path",
dest="inputPath",
required=True,
default="None",
help="Path with your sigma rules.",
)
parser.add_argument(
"-r",
"--recursive",
dest="recursive",
action="store_true",
help="If you have subfolders on the initial path and you want to convert all of them, use -r to do it recursive.",
)
args = parser.parse_args()
main(args)