prebuilt-rules-scripts/create_documentation.py (238 lines of code) (raw):

import json import textwrap from pathlib import Path import re # Creates the asciidoc files for the documentation. All prebuilt rule doc files # are generated, even those that have not been changed, so you can just copy and # paste the updated files to the documentation folders. releaseVersion = "7.11.0" # Security app release version - update as required ROOT = Path(__file__).resolve().parent.parent PREBUILT_RULES = ROOT.joinpath('prebuilt-rules-scripts') GENERATED_ASCII = ROOT.joinpath('generated-ascii-files') def sort_by_name(_rule): """Helper to sort rule by name""" return _rule['name'] def sort_tag_by_name(_rule): """Helper to sort tags by name""" return _rule['tag'] def translate_interval_period(interval): units = "" length = "" runtime = re.match(r"([0-9]+)([a-z]+)", interval, re.I) if runtime: runtime = runtime.groups() if len(runtime) == 2: if runtime[1] == 'm': units = "minutes" elif runtime[1] == 's': units = "seconds" elif runtime[1] == 'h': units = "hours" elif runtime[1] == 'H': units = "hours" elif runtime[1] == 'd': units = "days" elif runtime[1] == 'w': units = "weeks" elif runtime[1] == 'M': units = "months" elif runtime[1] == 'y': units = "years" else: units = "" length = runtime[0] if length == "1": units = units[:-1] return str(length + " " + units) # Formats text using asciidoc syntax def format_text(text): return text.replace('\\n', '\n') # Path to the generated JSON file final_diff = str(PREBUILT_RULES.joinpath('diff-files', 'final-files', f'final-rule-file-{releaseVersion}.json')) with open(final_diff, 'r') as source: rules_dict = json.load(source) sorted_rules = sorted(rules_dict, key=sort_by_name) newText = """[[prebuilt-rules]] [role="xpack"] == Prebuilt rule reference beta[] This section lists all available prebuilt rules. IMPORTANT: To run {ml} prebuilt rules, you must have the https://www.elastic.co/subscriptions[appropriate license] or use a {ess-trial}[Cloud] deployment. All {ml} prebuilt rules are tagged with `ML`, and their rule type is `machine_learning`. [width="100%",options="header"] |============================================== |Rule |Description |Tags |Added |Version """ # Creates overview table for rule in sorted_rules: tagStrings = "" versionText = "" linkString = re.sub(' ', '-', rule['name'].lower()) linkString = re.sub('[():]', '', linkString) linkString = re.sub('-+', '-', linkString) linkString = re.sub('/', '-', linkString) newText = newText + "|<<" + linkString + ", " + rule['name'] + ">> |" + re.sub(' +', ' ', rule['description'].replace('\n', ' ')) for i in rule['tags']: tagStrings = tagStrings + "[" + i + "] " if rule['version'] == 1: versionText = str(rule['version']) if rule['version'] > 1: versionText = str(rule['version']) + " <<" + linkString + "-history, Version history>>" newText = newText + " |" + tagStrings + " |" + rule['added'] + " |" + versionText + "\n\n" tagStrings = "" newText = newText + "|==============================================" GENERATED_ASCII.mkdir(exist_ok=True) file_write = str(GENERATED_ASCII.joinpath('prebuilt-rules-reference.asciidoc')) with open(file_write, "w+") as writeFile: writeFile.write(newText) # End overview table # Create files for each rule and the index (ToC) file fileText = "" rules_index_file = [] ruleNameChanged = False filesWithUpdatedRuleName = set() for rule in sorted_rules: rule_link = re.sub(' ', '-', rule['name'].lower()) rule_link = re.sub('[():]', '', rule_link) rule_link = re.sub('-+', '-', rule_link) rule_link = re.sub('/', '-', rule_link) fileText = "[[" + rule_link + "]]\n=== " + rule['name'] + "\n\n" fileText = fileText + format_text(rule['description']) + "\n\n" fileText = fileText + "*Rule type*: " + rule['type'] + "\n\n" if 'machine_learning_job_id' in rule: fileText = fileText + "*Machine learning job*: " + rule['machine_learning_job_id'] + "\n\n" fileText = fileText + "*Machine learning anomaly threshold*: " + str(rule['anomaly_threshold']) + "\n\n" if 'index' in rule: if len(rule['index']) != 0: fileText = fileText + "*Rule indices*:" + "\n\n" for i in rule['index']: fileText = fileText + "* " + i + "\n" else: fileText = fileText + "*Rule index*: " + rule['index'] + "\n\n" fileText = fileText + "\n*Severity*: " + rule['severity'] + "\n\n" fileText = fileText + "*Risk score*: " + str(rule['risk_score']) + "\n\n" if 'interval' in rule: fileText = fileText + "*Runs every*: " + translate_interval_period(rule['interval']) + "\n\n" if 'interval' not in rule: fileText = fileText + "*Runs every*: 5 minutes" + "\n\n" if 'from' in rule: fileText = fileText + "*Searches indices from*: " + rule['from'] + " ({ref}/common-options.html#date-math[Date Math format], see also <<rule-schedule, `Additional look-back time`>>)" + "\n\n" if 'from' not in rule: fileText = fileText + "*Searches indices from*: now-6m" + " ({ref}/common-options.html#date-math[Date Math format], see also <<rule-schedule, `Additional look-back time`>>)" + "\n\n" if 'max_signals' in rule: fileText = fileText + "*Maximum alerts per execution*: " + str(rule['max_signals']) + "\n\n" if 'max_signals' not in rule: fileText = fileText + "*Maximum alerts per execution*: 100" + "\n\n" if 'references' in rule: if len(rule['references']) != 0: fileText = fileText + "*References*:\n\n" for i in rule['references']: fileText = fileText + "* " + i + "\n" if len(rule['references']) != 0: fileText = fileText + "\n" fileText = fileText + "*Tags*:\n\n" for i in rule['tags']: fileText = fileText + "* " + i + "\n" if rule['version'] == 1: fileText = fileText + "\n*Version*: " + str(rule['version']) + "\n\n" if rule['version'] > 1: fileText = fileText + "\n*Version*: " + str(rule['version']) + " (<<" + rule_link + "-history, version history>>)" + "\n\n" fileText = fileText + "*Added ({stack} release)*: " + rule['added'] + "\n\n" if rule['version'] > 1: fileText = fileText + "*Last modified ({stack} release)*: " + rule['last_update'] + "\n\n" fileText = fileText + "*Rule authors*: " for count, i in enumerate(rule['author']): if count > 0: fileText = fileText + ", " fileText = fileText + i fileText = fileText + "\n\n" fileText = fileText + "*Rule license*: " + rule['license'] + "\n" if 'false_positives' in rule: if len(rule['false_positives']) != 0: fileText = fileText + "\n==== Potential false positives" + "\n\n" for i in rule['false_positives']: fileText = fileText + format_text(i) + "\n" if 'note' in rule: fileText = fileText + "\n==== Investigation guide" + "\n\n" fileText = fileText + format_text(rule['note']) + "\n" if 'query' in rule: fileText = fileText + "\n==== Rule query\n\n" fileText = fileText + "\n[source,js]\n" fileText = fileText + "----------------------------------" + "\n" fileText = fileText + re.sub(' +', ' ', textwrap.fill(rule['query'], width=70)) + "\n" fileText = fileText + "----------------------------------" + "\n\n" if 'filters' in rule: if len(rule['filters']) != 0: fileText = fileText + "==== Rule filters" + "\n\n" fileText = fileText + "[source,js]\n" fileText = fileText + "----------------------------------" + "\n" for i in rule['filters']: fileText = fileText + json.dumps(i, sort_keys=True, indent=4) + "\n" fileText = fileText + "----------------------------------" + "\n\n" if 'threat' in rule: if len(rule['threat']) != 0: fileText = fileText + "==== Threat mapping" + "\n\n" isFirstLoop = True for i in rule['threat']: if isFirstLoop: fileText = fileText + "*Framework*: " + i['framework'] isFirstLoop = False if i['framework'] == "MITRE ATT&CK": fileText = fileText + "^TM^" fileText = fileText + "\n\n* Tactic:\n" fileText = fileText + "** Name: " + i['tactic']['name'] + "\n" fileText = fileText + "** ID: " + i['tactic']['id'] + "\n" fileText = fileText + "** Reference URL: " + i['tactic']['reference'] + "\n" if i.get('technique'): fileText = fileText + "* Technique:\n" fileText = fileText + "** Name: " + i['technique'][0]['name'] + "\n" fileText = fileText + "** ID: " + i['technique'][0]['id'] + "\n" fileText = fileText + "** Reference URL: " + i['technique'][0]['reference'] + "\n" if 'changelog' in rule: identifier = rule_link + "-history" fileText = fileText + "\n[[" + identifier + "]]\n" fileText = fileText + "==== Rule version history" + "\n\n" for i in reversed(rule['changelog']['changes']): fileText = fileText + "Version " + str(i['version']) + " (" + i['updated'] + " release)" + "::\n" if 'pre_name' in i: if i['pre_name'] != None: fileText = fileText + "* Rule name changed from: " + i['pre_name'] + "\n" ruleNameChanged = True if i['updated'] == releaseVersion: filesWithUpdatedRuleName.add(rule_link + ".asciidoc") if i['doc_text'] == "Updated query.": if 'pre_name' in i: if i['pre_name'] != None: fileText = fileText + "+\n" fileText = fileText + "* Updated query, changed from:\n+\n" fileText = fileText + "[source, js]\n" fileText = fileText + "----------------------------------" + "\n" fileText = fileText + re.sub(' +', ' ', textwrap.fill(i['pre_query'], width=70)) + "\n" fileText = fileText + "----------------------------------" + "\n\n" if i['doc_text'] != "Updated query." and ruleNameChanged == False: fileText = fileText + "* " + i['doc_text'] + "\n\n" ruleNameChanged = False rule_details_dir = GENERATED_ASCII.joinpath('rule-details') rule_details_dir.mkdir(exist_ok=True) asciidoc_file = str(rule_details_dir.joinpath(f'{rule_link}.asciidoc')) with open(asciidoc_file, "w+") as asciiWrite: asciiWrite.write(fileText) rules_index_file.append("include::rule-details/" + rule_link + ".asciidoc[]") # Create index file index_file_text = "" for index_link in rules_index_file: index_file_text += index_link + "\n" index_file_write = str(GENERATED_ASCII.joinpath('rule-desc-index.asciidoc')) with open(index_file_write, "w+") as indexFileWrite: indexFileWrite.write(index_file_text) # Print files of rules with changed names to terminal filesWithUpdatedRuleName = sorted(filesWithUpdatedRuleName) print("Rule names in these files have been changed:\n") for cn in filesWithUpdatedRuleName: print(cn) print("\n") # END: Create files for each rule # START: Create rule changelog file. This needs updating each release to add # rules changed for the new release. versionHistoryPage = """[[prebuilt-rules-changelog]] == Prebuilt rule changes per release beta[] The following lists prebuilt rule updates per release. Only rules with significant modifications to their query or scope are listed. For detailed information about a rule's changes, see the rule's description page. """ # Rules that have been deleted so there is no need to add them manually after # generating the docs deletedRules = """ These prebuilt rules have been removed: * Execution via Signed Binary * Suspicious Process spawning from Script Interpreter * Suspicious Script Object Execution These prebuilt rules have been updated: """ def addVersionUpdates(updated): global versionHistoryPage versionHistoryPage = versionHistoryPage + "[float]\n" versionHistoryPage = versionHistoryPage + "=== " + updated + "\n\n" if updated == "7.7.0": versionHistoryPage = versionHistoryPage + deletedRules for rule in sorted_rules: if 'changelog' in rule: for i in (rule['changelog']['changes']): if i['updated'] == updated and i['doc_text'] != "Formatting only": linkString = re.sub(' ', '-', rule['name'].lower()) linkString = re.sub('[():]', '', linkString) linkString = re.sub('-+', '-', linkString) linkString = re.sub('/', '-', linkString) versionHistoryPage = versionHistoryPage + "<<" + linkString + ">>\n\n" addVersionUpdates("7.11.0") addVersionUpdates("7.10.0") addVersionUpdates("7.9.0") addVersionUpdates("7.8.0") addVersionUpdates("7.7.0") addVersionUpdates("7.6.2") addVersionUpdates("7.6.1") file_write = str(GENERATED_ASCII.joinpath('prebuilt-rules-changelog.asciidoc')) with open(file_write, "w+") as writeFile: writeFile.write(versionHistoryPage)