Compare commits

..

1 Commits

Author SHA1 Message Date
Soufiane Fariss
413047a7e8 webui: initial mockup of capa-webui
The following commits introduces capa-webui, a new web-based tool
to render capa existing output format - the result document.

The current project structure is as follows -
- webui/index.html: is the main HTML file, that serves as the entry
point for the web application.
- scripts/main.js: includes event handlers, DOM manipulation code
- webui/assets/css/styles.css: contains the styles for the UI

Webui is meant to be used a standalone static site, though for now
we are splitting this into multiple souce files for ease of use.
We can release a standalone static index.html in the releases.

This initial draft is subject to major structrual changes

Webui is meant to be deployed using github-pages
2024-06-19 23:33:06 +02:00
13 changed files with 854 additions and 56 deletions

View File

@@ -32,7 +32,7 @@ jobs:
artifact_name: capa.exe
asset_name: windows
python_version: 3.8
- os: macos-12
- os: macos-11
# use older macOS for assumed better portability
artifact_name: capa
asset_name: macos

View File

@@ -76,7 +76,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-20.04, windows-2019, macos-12]
os: [ubuntu-20.04, windows-2019, macos-11]
# across all operating systems
python-version: ["3.8", "3.11"]
include:

View File

@@ -17,7 +17,6 @@
### capa explorer IDA Pro plugin
### Development
- CI: use macos-12 since macos-11 is deprecated and will be removed on June 28th, 2024 #2173 @mr-tz
### Raw diffs
- [capa v7.1.0...master](https://github.com/mandiant/capa/compare/v7.1.0...master)

View File

@@ -28,7 +28,7 @@ from capa.features.extractors.base_extractor import (
class BinjaFeatureExtractor(StaticFeatureExtractor):
def __init__(self, bv: binja.BinaryView):
super().__init__(hashes=SampleHashes.from_bytes(bv.file.raw.read(0, bv.file.raw.length)))
super().__init__(hashes=SampleHashes.from_bytes(bv.file.raw.read(0, len(bv.file.raw))))
self.bv = bv
self.global_features: List[Tuple[Feature, Address]] = []
self.global_features.extend(capa.features.extractors.binja.file.extract_file_format(self.bv))

View File

@@ -48,7 +48,7 @@ def extract_format(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
else:
logger.warning("unknown file format, file command output: %s", report.target.file.type)
raise ValueError(
f"unrecognized file format from the CAPE report; output of file command: {report.target.file.type}"
"unrecognized file format from the CAPE report; output of file command: {report.target.file.type}"
)
@@ -73,7 +73,7 @@ def extract_os(report: CapeReport) -> Iterator[Tuple[Feature, Address]]:
else:
# if the operating system information is missing from the cape report, it's likely a bug
logger.warning("unrecognized OS: %s", file_output)
raise ValueError(f"unrecognized OS from the CAPE report; output of file command: {file_output}")
raise ValueError("unrecognized OS from the CAPE report; output of file command: {file_output}")
else:
# the sample is shellcode
logger.debug("unsupported file format, file command output: %s", file_output)

View File

@@ -123,10 +123,10 @@ dev = [
"pytest-sugar==1.0.0",
"pytest-instafail==0.5.0",
"pytest-cov==5.0.0",
"flake8==7.1.0",
"flake8==7.0.0",
"flake8-bugbear==24.4.26",
"flake8-encodings==0.5.1",
"flake8-comprehensions==3.15.0",
"flake8-comprehensions==3.14.0",
"flake8-logging-format==0.9.0",
"flake8-no-implicit-concat==0.3.5",
"flake8-print==5.0.0",
@@ -134,7 +134,7 @@ dev = [
"flake8-simplify==0.21.0",
"flake8-use-pathlib==0.3.0",
"flake8-copyright==0.2.4",
"ruff==0.5.0",
"ruff==0.4.8",
"black==24.4.2",
"isort==5.13.2",
"mypy==1.10.0",
@@ -162,10 +162,10 @@ build = [
]
scripts = [
"jschema_to_python==1.2.3",
"psutil==6.0.0",
"psutil==5.9.2",
"stix2==3.0.1",
"sarif_om==1.0.4",
"requests==2.32.3",
"requests==2.31.0",
]
[tool.deptry]

View File

@@ -69,8 +69,7 @@ def load_analysis(bv):
return 0
binaryninja.log_info(f"Using capa file {path}")
with Path(path).open("r", encoding="utf-8") as file:
doc = json.load(file)
doc = json.loads(path.read_bytes().decode("utf-8"))
if "meta" not in doc or "rules" not in doc:
binaryninja.log_error("doesn't appear to be a capa report")
@@ -84,35 +83,20 @@ def load_analysis(bv):
binaryninja.log_error("sample mismatch")
return -2
# Retreive base address
capa_base_address = 0
if "analysis" in doc["meta"] and "base_address" in doc["meta"]["analysis"]:
if doc["meta"]["analysis"]["base_address"]["type"] == "absolute":
capa_base_address = int(doc["meta"]["analysis"]["base_address"]["value"])
rows = []
for rule in doc["rules"].values():
if rule["meta"].get("lib"):
continue
if rule["meta"].get("capa/subscope"):
continue
if rule["meta"]["scopes"].get("static") != "function":
if rule["meta"]["scope"] != "function":
continue
name = rule["meta"]["name"]
ns = rule["meta"].get("namespace", "")
for matches in rule["matches"]:
for match in matches:
if "type" not in match.keys():
continue
if "value" not in match.keys():
continue
va = match["value"]
# Substract va and CAPA base_address
va = int(va) - capa_base_address
# Add binja base address
va = va + bv.start
rows.append((ns, name, va))
for va in rule["matches"].keys():
va = int(va)
rows.append((ns, name, va))
# order by (namespace, name) so that like things show up together
rows = sorted(rows)

View File

@@ -171,8 +171,8 @@ def print_dynamic_analysis(extractor: DynamicFeatureExtractor, args):
process_handles = tuple(extractor.get_processes())
if args.process:
process_handles = tuple(filter(lambda ph: extractor.get_process_name(ph) == args.process, process_handles))
if args.process not in [extractor.get_process_name(ph) for ph in process_handles]:
process_handles = tuple(filter(lambda ph: ph.inner["name"] == args.process, process_handles))
if args.process not in [ph.inner["name"] for ph in args.process]:
print(f"{args.process} not a process")
return -1
@@ -227,13 +227,13 @@ def print_static_features(functions, extractor: StaticFeatureExtractor):
def print_dynamic_features(processes, extractor: DynamicFeatureExtractor):
for p in processes:
print(f"proc: {extractor.get_process_name(p)} (ppid={p.address.ppid}, pid={p.address.pid})")
print(f"proc: {p.inner.process_name} (ppid={p.address.ppid}, pid={p.address.pid})")
for feature, addr in extractor.extract_process_features(p):
if is_global_feature(feature):
continue
print(f" proc: {extractor.get_process_name(p)}: {feature}")
print(f" proc: {p.inner.process_name}: {feature}")
for t in extractor.get_threads(p):
print(f" thread: {t.address.tid}")

View File

@@ -23,21 +23,10 @@ def get_script_path(s: str):
return str(CD / ".." / "scripts" / s)
def get_binary_file_path():
def get_file_path():
return str(CD / "data" / "9324d1a8ae37a36ae560c37448c9705a.exe_")
def get_report_file_path():
return str(
CD
/ "data"
/ "dynamic"
/ "cape"
/ "v2.4"
/ "fb7ade52dc5a1d6128b9c217114a46d0089147610f99f5122face29e429a1e74.json.gz"
)
def get_rules_path():
return str(CD / ".." / "rules")
@@ -59,13 +48,12 @@ def get_rule_path():
pytest.param("lint.py", ["-t", "create directory", get_rules_path()]),
# `create directory` rule has native and .NET example PEs
pytest.param("lint.py", ["--thorough", "-t", "create directory", get_rules_path()]),
pytest.param("match-function-id.py", [get_binary_file_path()]),
pytest.param("show-capabilities-by-function.py", [get_binary_file_path()]),
pytest.param("show-features.py", [get_binary_file_path()]),
pytest.param("show-features.py", ["-F", "0x407970", get_binary_file_path()]),
pytest.param("show-features.py", ["-P", "MicrosoftEdgeUpdate.exe", get_report_file_path()]),
pytest.param("show-unused-features.py", [get_binary_file_path()]),
pytest.param("capa_as_library.py", [get_binary_file_path()]),
pytest.param("match-function-id.py", [get_file_path()]),
pytest.param("show-capabilities-by-function.py", [get_file_path()]),
pytest.param("show-features.py", [get_file_path()]),
pytest.param("show-features.py", ["-F", "0x407970", get_file_path()]),
pytest.param("show-unused-features.py", [get_file_path()]),
pytest.param("capa_as_library.py", [get_file_path()]),
],
)
def test_scripts(script, args):

355
webui/assets/css/style.css Executable file
View File

@@ -0,0 +1,355 @@
/*
* Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at: [package root]/LICENSE.txt
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/
/* TODO(s-ff): simplfy and refactor the CSS styling */
/* TODO(s-ff): change font, do not use Product Sans */
@font-face {
font-family: 'Product Sans';
font-style: normal;
src: local('Open Sans'), local('OpenSans'), url(https://fonts.gstatic.com/s/productsans/v5/HYvgU2fE2nRJvZ5JFAumwegdm0LZdjqr5-oayXSOefg.woff2) format('woff2');
}
/* Reset and general styles */
*,
*::before,
*::after {
box-sizing: border-box;
}
input {
font: inherit;
}
fieldset {
padding: 16px;
display: grid;
gap: 16px;
}
legend {
font-weight: 700;
}
html {
font-family: 'Product Sans', sans-serif;
}
body {
max-width: 95%;
margin: 0 auto;
padding: 8px;
}
/* Tree styles */
.tree {
--font-size: 1rem;
--line-height: 1.75;
--spacing: calc(var(--line-height) * 1em);
--thickness: 0.35px;
--radius: 8px;
line-height: var(--line-height);
font-size: var(--font-size);
}
.tree li {
display: block;
position: relative;
padding-left: calc(2 * var(--spacing) - var(--radius) - var(--thickness));
}
.tree ul {
margin-left: calc(var(--radius) - var(--spacing));
padding-left: 0;
}
.tree ul li {
border-left: var(--thickness) solid grey;
}
.tree ul li:last-child {
border-color: transparent;
}
.tree ul li::before {
content: "";
display: block;
position: absolute;
top: calc(var(--spacing) / -2);
left: calc(-1 * var(--thickness));
width: calc(var(--spacing) + var(--thickness));
height: calc(var(--spacing) + var(--thickness) / 2);
border: solid grey;
border-width: 0 0 var(--thickness) var(--thickness);
}
.tree summary {
display: block;
cursor: pointer;
}
.tree summary::marker,
.tree summary::-webkit-details-marker {
display: none;
}
.tree summary:focus {
outline: none;
}
.tree summary:focus-visible {
outline: var(--thickness) dotted #000;
}
.tree li::after,
.tree summary::before {
content: "";
display: block;
position: absolute;
top: calc(var(--spacing) / 2 - var(--radius));
left: calc(var(--spacing) - var(--radius) - var(--thickness) / 2);
width: calc(2 * var(--radius));
height: calc(2 * var(--radius));
border-radius: 50%;
background: #ddd;
}
.tree summary::before {
content: "+";
z-index: 1;
background: #7fa0bb;
color: white;
line-height: calc(2 * var(--radius));
text-align: center;
}
.tree summary .summary-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.tree summary .summary-content .statement-type,
.feature-type {
background-color: #f0f0f0;
border-radius: 4px;
color: #000;
display: inline-block;
font-size: 0.7em;
padding: 1px 6px;
cursor: pointer;
}
.tree summary .summary-content .rule-title,
.feature-value {
margin-left: 1rem;
flex-grow: 1;
}
.feature-value {
color: green;
}
.feature-location {
margin-left: 5px;
font-size: 0.8em;
color: #000;
}
.tree summary .summary-content .namespace {
background-color: #f0f0f0;
border-radius: 4px;
color: #000;
display: inline-block;
font-size: 0.75em;
padding: 2px 6px;
cursor: pointer;
content: "";
}
.tree details[open] > summary::before {
content: "";
}
/* Rule source tooltip styles */
.tree .rule-title {
position: relative;
}
.tree .rule-title:hover::after {
content: attr(source);
font-family: 'Courier New', monospace;
font-size: 0.7em;
line-height: 1.2em;
color: black;
width: max-content;
padding: 8px;
border-radius: 6px;
background: #eee;
position: absolute;
left: 350px;
z-index: 1000;
white-space: pre-wrap;
opacity: 0;
}
.tree .rule-title:hover::after {
opacity: 1;
}
.tree .rule-title:hover::before {
display: none;
}
/* Controls styles */
.controls {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.hover-toggle-label {
margin-right: 20px;
}
#searchContainer {
flex-grow: 1;
}
#searchContainer input {
width: 100%;
padding: 5px;
}
/* Metadata table styles */
.metadata-container {
margin-bottom: 20px;
}
.metadata-table {
border-collapse: collapse;
table-layout: fixed;
width: 100%;
}
.metadata-table td {
border: 1px solid #ddd;
padding: 8px;
}
.metadata-table tr:nth-child(even) {
background-color: #f2f2f2;
}
.metadata-table tr:hover {
background-color: #ddd;
}
.metadata-table td:first-child {
font-weight: bold;
width: 140px;
}
/* Banner styles */
.banner {
background-color: #007bff;
margin: 7px 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 15px;
border-radius: 5px;
text-align: left;
color: #fff;
font-size: 14px;
font-family: 'Product-Sans', sans-serif;
cursor: initial;
}
.bannerContent {
padding-right: 15px;
}
.bannerLinks {
display: flex;
align-items: center;
border-left: 2px solid #ffffffad;
padding-left: 15px;
cursor: initial;
}
.banner a {
display: inline;
color: #fff;
text-decoration-color: #fff;
}
.banner a:hover {
color: #343a40;
opacity: 0.8;
}
/* Matched rule styles */
.matched-rule li {
position: relative;
}
.matched-rule .feature-location {
position: absolute;
right: 580px;
top: 50%;
transform: translateY(-50%);
font-size: 0.8em;
color: #000;
}
/* Tree table styles */
.tree-table {
width: 100%;
border-collapse: collapse;
}
.tree-table th {
padding: 8px;
text-align: left;
vertical-align: top;
}
.tree-table th {
background-color: #f0f0f0;
font-weight: bold;
}
.tree-table th:nth-child(1) {
width: 60%;
}
.tree-table th:nth-child(2),
.tree-table th:nth-child(3) {
width: 20%;
border-left: 1px solid #ddd;
}
.tree-table .tree {
margin-top: 0;
}
/* Button styles */
button {
background-color: #f0f0f0;
border: none;
border-radius: 4px;
color: #000;
cursor: pointer;
font-size: 0.9em;
margin-left: 10px;
padding: 5px 10px;
}

142
webui/index.html Executable file
View File

@@ -0,0 +1,142 @@
<!--
Copyright (C) 2024 Mandiant, Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at: [package root]/LICENSE.txt
Unless required by applicable law or agreed to in writing, software distributed under the License
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and limitations under the License.
-->
<html>
<head>
<!-- TODO(s-ff): include a favicon -->
<title>CAPA WebUI</title>
<meta charset="utf-8" />
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="assets/css/style.css">
<script src="scripts/main.js"> </script>
<script>
// TODO(s-ff): the search now only search for feature value, fix it to
// to also search for the rule titles.
// unstable
function searchTree(node, keyword) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.classList.contains('rule-title') || node.classList.contains('feature-value')) {
const textContent = node.textContent.toLowerCase();
if (textContent.includes(keyword.toLowerCase())) {
node.closest('.matched-rule').style.display = 'block';
expandParentDetails(node);
} else {
node.closest('.matched-rule').style.display = 'none';
}
}
for (const child of node.childNodes) {
searchTree(child, keyword);
}
}
}
function expandParentDetails(node) {
const parentDetails = node.closest('details');
if (parentDetails) {
parentDetails.open = true;
expandParentDetails(parentDetails.parentNode);
}
}
function collapseAllDetails() {
const detailsElements = document.querySelectorAll('details');
detailsElements.forEach(details => {
details.open = false;
});
}
function expandAllDetails() {
const detailsElements = document.querySelectorAll('details');
detailsElements.forEach(details => {
details.open = true;
});
}
function toggleExpandCollapse() {
const togglebutton = document.getElementById('toggleExpand');
const tree = document.getElementById('tree');
if (togglebutton.textContent === 'Expand All') {
expandAllDetails();
togglebutton.textContent = 'Collapse All';
} else {
collapseAllDetails();
togglebutton.textContent = 'Expand All';
}
}
// unstable
function search(value) {
const tree = document.getElementById('tree');
if (value.trim() === '') {
// If the search bar is cleared, reset the tree to its initial state
const matchedRules = document.querySelectorAll('.matched-rule');
matchedRules.forEach(rule => {
rule.style.display = 'block';
});
collapseAllDetails();
} else {
searchTree(tree, value);
}
}
document.addEventListener('DOMContentLoaded', function() {
document.getElementById('toggleExpand').addEventListener('click', toggleExpandCollapse);
});
</script>
</head>
<body>
<header class="header">
<div class="banner">
<span class="bannerContent" role="alert">This is a pre-alpha release of capa-webui. Please report any bugs, enhacements, or features in the Github issues</span>
<div class="bannerLinks">
<div>
<a href="https://github.com/mandiant/capa">CAPA v7.0.1 on Github</a>
</div>
</div>
</div>
</header>
<h1></h1>
<!-- TODO(s-ff): these controls are not prefectly aligned -->
<div class="controls">
<button id="toggleExpand">Expand All</button>
<!-- TODO(s-ff): allow users to disable showing rule logic on hover -->
<label class="hover-toggle-label">
<input type="checkbox" id="enableHover" checked>
Show rule logic on hover
</label>
<div id="searchContainer">
<input onkeyup="search(this.value)" type="text" placeholder="Type to search... (unstable)" />
</div>
</div>
<table class="tree-table">
<thead>
<tr>
<th>Rule Title</th>
<th>Feature Address</th>
<th>Namespace</th>
</tr>
</thead>
</table>
<ul class="tree" id="tree">
<!-- This will contain the rendered JSON capa result document -->
</ul>
</body>
</html>

330
webui/scripts/main.js Executable file
View File

@@ -0,0 +1,330 @@
/*
* Copyright (C) 2023 Mandiant, Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at: [package root]/LICENSE.txt
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and limitations under the License.
*/
// TODO(s-ff): simply all the functions and introduce smaller helper functions
// for creating elements, i.e. createOrStatement, isRangeStatement, .. etc.
/**
* Renders the JSON data representing the CAPA results into an HTML tree structure.
* @param {Object} json - The JSON data containing the CAPA results.
*/
function renderJSON(json) {
const tree = document.getElementById('tree');
// Iterate over each rule in the JSON
for (const ruleName in json.rules) {
const rule = json.rules[ruleName];
// Create a list item for the matched rule
const ruleElement = document.createElement('li');
ruleElement.className = 'matched-rule';
ruleElement.setAttribute('name', ruleName);
// Create a details element for the rule
const ruleDetails = document.createElement('details');
// Create a summary element for the rule
const ruleSummary = document.createElement('summary');
// Create a div for the summary content
const ruleSummaryContent = document.createElement('div');
ruleSummaryContent.className = 'summary-content';
// Add the statement type, rule title, and namespace to the summary content
const statementType = document.createElement('span');
statementType.className = 'statement-type';
statementType.textContent = 'rule';
ruleSummaryContent.appendChild(statementType);
const ruleTitle = document.createElement('span');
ruleTitle.className = 'rule-title';
ruleTitle.setAttribute('source', rule.source);
ruleTitle.textContent = rule.meta.name;
ruleSummaryContent.appendChild(ruleTitle);
const namespace = document.createElement('span');
namespace.className = 'namespace';
//namespace.textContent = rule.meta.namespace;
if (rule.meta.namespace) {
namespace.textContent = rule.meta.namespace;
} else {
namespace.style.display = 'none';
}
ruleSummaryContent.appendChild(namespace);
// Append the summary content to the summary element
ruleSummary.appendChild(ruleSummaryContent);
// Append the summary element to the details element
ruleDetails.appendChild(ruleSummary);
// Create a list for the rule's matches
const matchesList = document.createElement('ul');
// Iterate over each match in the rule
for (const match of rule.matches) {
// Render the match and its children recursively
const matchElement = renderMatch(match);
matchesList.appendChild(matchElement);
}
// Append the matches list to the details element
ruleDetails.appendChild(matchesList);
// Append the rule details to the rule list item
ruleElement.appendChild(ruleDetails);
// Append the rule list item to the tree
tree.appendChild(ruleElement);
}
}
/**
* Recursively renders a match and its children into an HTML structure.
* @param {Object} match - The match object to be rendered.
* @returns {HTMLElement} The rendered match element.
*/
function renderMatch(match) {
// Create a list item for the match
const matchElement = document.createElement('li');
matchElement.className = 'match-container';
// Check if the match is a range statement
if (match[1].node.type === 'statement' && match[1].node.statement.type === 'range') {
const rangeElement = document.createElement('li');
rangeElement.className = 'match-container';
const rangeType = document.createElement('span');
rangeType.className = 'feature-type';
rangeType.textContent = `count(${match[1].node.statement.child.type}(${match[1].node.statement.child[match[1].node.statement.child.type]}))`;
rangeElement.appendChild(rangeType);
const rangeValue = document.createElement('span');
rangeValue.className = 'feature-value';
const minima = match[1].node.statement.min
const maxima = match[1].node.statement.max
if (minima == maxima) {
rangeValue.textContent = `${minima}`;
rangeType.textContent = `count(${match[1].node.statement.child.type})`;
} else {
rangeValue.textContent = `${minima} or more`;
}
rangeElement.appendChild(rangeValue);
const rangeLocation = document.createElement('span');
rangeLocation.className = 'feature-location';
if (match[1].locations[0].value) {
const locations = match[1].locations.map(loc => '0x' + loc.value.toString(16).toUpperCase());
rangeLocation.textContent = locations.join(', ');
}
rangeElement.appendChild(rangeLocation);
return rangeElement;
}
// If the match is a feature, render its type and value
if (match[1].node.type === 'feature') {
const featureType = document.createElement('span');
featureType.className = 'feature-type';
featureType.textContent = match[1].node.feature.type;
matchElement.appendChild(featureType);
const featureValue = document.createElement('span');
featureValue.className = 'feature-value';
featureValue.textContent = match[1].node.feature[match[1].node.feature.type];
matchElement.appendChild(featureValue);
const featureLocation = document.createElement('span');
featureLocation.className = 'feature-location';
if (match[1].locations[0].value) {
const locations = match[1].locations.map(loc => '0x' + loc.value.toString(16).toUpperCase());
featureLocation.textContent = locations.join(', ');
}
matchElement.appendChild(featureLocation);
} else {
// Check if the match is an optional statement - these always have `success: true`.
const isOptional = match[1].node.statement.type === 'optional';
// If it's an optional statement, check if any of its children have success set to true
if (isOptional) {
const hasSuccessfulChild = match[1].children.some(child => child.success);
// If none of the children have success set to true, don't render the optional statement
if (!hasSuccessfulChild) {
return null;
}
}
// Create a details element for the match
const matchDetails = document.createElement('details');
// Create a summary element for the match
const matchSummary = document.createElement('summary');
// Create a div for the summary content
const matchSummaryContent = document.createElement('div');
matchSummaryContent.className = 'summary-content';
// Add the statement type to the summary content
const statementType = document.createElement('span');
statementType.className = 'statement-type';
statementType.textContent = match[1].node.statement.type;
matchSummaryContent.appendChild(statementType);
// Append the summary content to the summary element
matchSummary.appendChild(matchSummaryContent);
// Append the summary element to the details element
matchDetails.appendChild(matchSummary);
// Create a list for the match's children
const childrenList = document.createElement('ul');
// Iterate over each child in the match
for (const child of match[1].children) {
// Recursively render the child if it is successful
if (child.success) {
const childElement = renderMatch([null, child]);
if (childElement !== null) {
childrenList.appendChild(childElement);
}
}
}
// Append the children list to the details element
matchDetails.appendChild(childrenList);
// Append the match details to the match list item
matchElement.appendChild(matchDetails);
}
return matchElement;
}
/**
* Determines the statement type based on the feature object.
* @param {Object} feature - The feature object.
* @returns {string} The statement type.
*/
function getStatementType(feature) {
if (feature.type === 'and' || feature.type === 'or' || feature.type === 'not') {
return feature.type;
} else if (feature.match) {
return 'match';
} else {
return 'feature';
}
}
/**
* Checks if the given feature is a leaf feature (i.e., not a compound feature).
* @param {Object} feature - The feature object.
* @returns {boolean} True if the feature is a leaf feature, false otherwise.
*/
function isLeafFeature(feature) {
return feature.type !== 'and' && feature.type !== 'or' && feature.type !== 'not' && !feature.match;
}
/**
* Adds a metadata table to the document body.
* @param {Object} metadata - The metadata object containing sample and analysis information.
*/
function addMetadataTable(metadata) {
const metadataContainer = document.createElement("div");
metadataContainer.className = "metadata-container";
const table = document.createElement("table");
table.className = "metadata-table";
const rows = [{
key: "MD5",
value: metadata.sample.md5
},
{
key: "SHA1",
value: metadata.sample.sha1
},
{
key: "SHA256",
value: metadata.sample.sha256
},
{
key: "Extractor",
value: metadata.analysis.extractor
},
{
key: "Analysis",
value: metadata.flavor
},
{
key: "OS",
value: metadata.analysis.os
},
{
key: "Format",
value: metadata.analysis.format
},
{
key: "Arch",
value: metadata.analysis.arch
},
{
key: "Path",
value: metadata.sample.path
},
{
key: "Base Address",
value: "0x" + metadata.analysis.base_address.value.toString(16)
},
{
key: "Version",
value: metadata.version
},
{
key: "Timestamp",
value: metadata.timestamp
},
{
key: "Function Count",
value: Object.keys(metadata.analysis.feature_counts).length
}
];
rows.forEach((row) => {
const tr = document.createElement("tr");
const keyTd = document.createElement("td");
keyTd.textContent = row.key;
tr.appendChild(keyTd);
const valueTd = document.createElement("td");
valueTd.textContent = row.value;
tr.appendChild(valueTd);
table.appendChild(tr);
});
metadataContainer.appendChild(table);
document.body.insertBefore(metadataContainer, document.querySelector("h1"));
}
// TODO(s-ff): introduce a "Upload from local" and "Load from URL" options
/* For now we are using a static al-khaser_64.exe rdoc for testing */
let url = 'https://raw.githubusercontent.com/mandiant/capa-testfiles/master/rd/al-khaser_x64.exe_.json';
fetch(url)
.then(res => res.json())
.then(result_document => {
addMetadataTable(result_document.meta);
renderJSON(result_document);
})
.catch(err => {
throw err
});