mirror of
https://github.com/mandiant/capa.git
synced 2026-03-14 14:07:17 -07:00
Compare commits
1 Commits
webui
...
testing/ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
413047a7e8 |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/tests.yml
vendored
2
.github/workflows/tests.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Submodule tests/data updated: 5c3b7a8da4...3a7690178b
@@ -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
355
webui/assets/css/style.css
Executable 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
142
webui/index.html
Executable 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
330
webui/scripts/main.js
Executable 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
|
||||
});
|
||||
Reference in New Issue
Block a user