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)