cli: link to rule names to capa rules website (#2338)

* web: rules: redirect from various rule names to canonical rule URL

closes #2319

Update index.html

Co-authored-by: Moritz <mr-tz@users.noreply.github.com>

* cli: link to rule names to capa rules website

* just: make `just lint` run all steps, not fail on first error

---------

Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
This commit is contained in:
Willi Ballenthin
2024-08-29 08:56:14 -06:00
committed by GitHub
parent db4798aaf6
commit 729a1a85b7
10 changed files with 217 additions and 111 deletions

View File

@@ -235,7 +235,7 @@ const contextMenuItems = computed(() => [
label: "View rule in capa-rules",
icon: "pi pi-external-link",
target: "_blank",
url: createCapaRulesUrl(selectedNode.value, props.data.meta.version)
url: createCapaRulesUrl(selectedNode.value)
},
{
label: "Lookup rule in VirusTotal",

View File

@@ -62,9 +62,8 @@ export function createATTACKHref(attack) {
*/
export function createCapaRulesUrl(node, tag) {
if (!node || !node.data || !tag) return null;
const namespace = node.data.namespace || "lib";
const ruleName = node.data.name.toLowerCase().replace(/\s+/g, "-");
return `https://github.com/mandiant/capa-rules/blob/v${tag}/${namespace}/${ruleName}.yml`;
return `https://mandiant.github.io/capa/rules/${ruleName}/`;
}
/**

View File

@@ -200,14 +200,14 @@
<!-- TODO(williballenthin): add date -->
<li>
added:
<a href="./rules/overwrite-dll-text-section-to-remove-hooks.html">
<a href="./rules/overwrite DLL .text section to remove hooks/">
overwrite DLL .text section to remove hooks
</a>
</li>
<li>
added:
<a href="./rules/attach-bpf-to-socket-on-linux.html">
<a href="./rules/attach BPF to socket on Linux/">
attach BPF to socket on Linux
</a>
</li>

View File

@@ -10,3 +10,4 @@ file_modification_dates.txt
public/*.html
public/pagefind/
public/index.html
public/

View File

@@ -259,7 +259,6 @@ def generate_html(categories_data, color_map):
for card in cards_data:
first_word = get_first_word(card["namespace"])
rectangle_color = color_map[first_word]
file_name = card["filename"].rpartition(".yml")[0]
card_html = f"""
<div class="card-wrapper">
@@ -267,7 +266,7 @@ def generate_html(categories_data, color_map):
<div class="thin-rectangle" style="background-color: {rectangle_color};"></div>
<div class="card-body">
<div class="namespace">{card['namespace']}</div>
<div class="rule-name"><a href="./{file_name}.html">{card['name']}</a></div>
<div class="rule-name"><a href="./{card['name']}/">{card['name']}</a></div>
<div class="authors">{', '.join(card['authors'])}</div>
</div>
</div>

View File

@@ -10,6 +10,7 @@ See the License for the specific language governing permissions and limitations
import os
import sys
import logging
import urllib.parse
from glob import glob
from pathlib import Path
@@ -20,6 +21,9 @@ from pygments.formatters import HtmlFormatter
import capa.rules
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)
input_directory = Path(sys.argv[1])
txt_file_path = Path(sys.argv[2])
output_directory = Path(sys.argv[3])
@@ -29,13 +33,13 @@ assert txt_file_path.exists(), "file-modification txt file must exist"
assert output_directory.exists(), "output directory must exist"
def convert_yaml_to_html(timestamps, yaml_file: Path, output_dir: Path):
rule_content = yaml_file.read_text(encoding="utf-8")
def render_rule(timestamps, path: Path) -> str:
rule_content = path.read_text(encoding="utf-8")
rule = capa.rules.Rule.from_yaml(rule_content, use_ruamel=True)
filename = os.path.basename(yaml_file).rpartition(".yml")[0]
filename = path.with_suffix("").name
namespace = rule.meta.get("namespace", "")
timestamp = timestamps[yaml_file.as_posix()]
timestamp = timestamps[path.as_posix()]
rendered_rule = pygments.highlight(
rule_content,
@@ -53,7 +57,7 @@ def convert_yaml_to_html(timestamps, yaml_file: Path, output_dir: Path):
vt_fragment = urllib.parse.quote(urllib.parse.quote(vt_query))
vt_link = f"https://www.virustotal.com/gui/search/{vt_fragment}/files"
ns_query = f'"namespace: {namespace} "'
ns_link = f"./?{urllib.parse.urlencode({'q': ns_query})}"
ns_link = f"../?{urllib.parse.urlencode({'q': ns_query})}"
html_content = f"""
<!DOCTYPE html>
@@ -62,12 +66,12 @@ def convert_yaml_to_html(timestamps, yaml_file: Path, output_dir: Path):
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{rule.name}</title>
<link rel="icon" href="./img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="./css/bootstrap-5.3.3.min.css">
<script src="./js/jquery-3.5.1.slim.min.js"></script>
<script src="./js/bootstrap-5.3.3.bundle.min.js"></script>
<script defer src="https://cloud.umami.is/script.js" data-website-id="0bb8ff9e-fbcc-4ee2-9f9f-b337a2e8cc7f"></script>
<link rel="stylesheet" type="text/css" href="./css/style.css">
<link rel="icon" href="../img/favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../css/bootstrap-5.3.3.min.css">
<script src="../js/jquery-3.5.1.slim.min.js"></script>
<script src="../js/bootstrap-5.3.3.bundle.min.js"></script>
<script src="https://cloud.umami.is/script.js" defer data-website-id="0bb8ff9e-fbcc-4ee2-9f9f-b337a2e8cc7f"></script>
<link rel="stylesheet" type="text/css" href="../css/style.css">
<style>
.rule-content .highlight pre {{
overflow: visible;
@@ -81,7 +85,7 @@ def convert_yaml_to_html(timestamps, yaml_file: Path, output_dir: Path):
box-shadow: 0 0.5rem 1rem rgba(0,0,0,0.05),inset 0 -1px 0 rgba(0,0,0,0.15);"
>
<a href="/capa/" class="d-flex align-items-center mb-3 mb-md-0 me-md-auto">
<img src="./img/logo.png" height=48 />
<img src="../img/logo.png" height=48 />
</a>
<ul class="nav nav-pills">
@@ -115,9 +119,7 @@ def convert_yaml_to_html(timestamps, yaml_file: Path, output_dir: Path):
</html>
"""
output_dir.mkdir(parents=True, exist_ok=True)
output_file_path = output_dir / (filename + ".html")
output_file_path.write_text(html_content, encoding="utf-8")
return html_content
yaml_files = glob(os.path.join(input_directory, "**/*.yml"), recursive=True)
@@ -129,8 +131,41 @@ for line in txt_file_path.read_text(encoding="utf-8").splitlines():
if line.startswith("==="):
continue
path, _, timestamp = line.partition(" ")
timestamps[path] = timestamp
filepath, _, timestamp = line.partition(" ")
timestamps[filepath] = timestamp
for yaml_file in yaml_files:
convert_yaml_to_html(timestamps, Path(yaml_file), output_directory)
path = Path(yaml_file)
rule_content = path.read_text(encoding="utf-8")
html_content = render_rule(timestamps, path)
rule = capa.rules.Rule.from_yaml(path.read_text(encoding="utf-8"), use_ruamel=True)
# like: rules/create file/index.html
#
# which looks like the URL fragments:
#
# rules/create%20file/index.html
# rules/create%20file/
# rules/create file/
html_path = output_directory / rule.name / "index.html"
html_path.parent.mkdir(parents=True, exist_ok=True)
html_path.write_text(html_content, encoding="utf-8")
logger.info("wrote: %s", html_path)
# like: create-file
rule_id = path.with_suffix("").name
# like: rules/create-file/index.html
#
# which looks like the URL fragments:
#
# rules/create-file/index.html
# rules/create-file/
#
# and redirects, via meta refresh, to the canonical path above.
# since we don't control the GH Pages web server, we can't use HTTP redirects.
id_path = output_directory / rule_id / "index.html"
id_path.parent.mkdir(parents=True, exist_ok=True)
redirect = f"""<html><head><meta http-equiv="refresh" content="0; url=../{rule.name}/"></head></html>"""
id_path.write_text(redirect, encoding="utf-8")
logger.info("wrote: %s", id_path)