Compare commits

...

2 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
ygasparis
1975b6455c extract import / export symbols from stripped elf binaries (#2142) 2024-06-18 12:38:02 -06:00
6 changed files with 913 additions and 40 deletions

View File

@@ -12,6 +12,8 @@
### Bug Fixes
- elf: extract import / export symbols from stripped binaries #2096 @ygasparis
### capa explorer IDA Pro plugin
### Development

View File

@@ -10,8 +10,7 @@ import logging
from typing import Tuple, Iterator
from pathlib import Path
from elftools.elf.elffile import ELFFile, SymbolTableSection
from elftools.elf.relocation import RelocationSection
from elftools.elf.elffile import ELFFile, DynamicSegment, SymbolTableSection
import capa.features.extractors.common
from capa.features.file import Export, Import, Section
@@ -47,17 +46,37 @@ def extract_file_export_names(elf: ELFFile, **kwargs):
yield Export(symbol.name), AbsoluteVirtualAddress(symbol.entry.st_value)
for segment in elf.iter_segments():
if not isinstance(segment, DynamicSegment):
continue
logger.debug("Dynamic Segment contains %s symbols: ", segment.num_symbols())
for symbol in segment.iter_symbols():
# The following conditions are based on the following article
# http://www.m4b.io/elf/export/binary/analysis/2015/05/25/what-is-an-elf-export.html
if not symbol.name:
continue
if symbol.entry.st_info.type not in ["STT_FUNC", "STT_OBJECT", "STT_IFUNC"]:
continue
if symbol.entry.st_value == 0:
continue
if symbol.entry.st_shndx == "SHN_UNDEF":
continue
yield Export(symbol.name), AbsoluteVirtualAddress(symbol.entry.st_value)
def extract_file_import_names(elf: ELFFile, **kwargs):
# Create a dictionary to store symbol names by their index
symbol_names = {}
# Extract symbol names and store them in the dictionary
for section in elf.iter_sections():
if not isinstance(section, SymbolTableSection):
for segment in elf.iter_segments():
if not isinstance(segment, DynamicSegment):
continue
for _, symbol in enumerate(section.iter_symbols()):
for _, symbol in enumerate(segment.iter_symbols()):
# The following conditions are based on the following article
# http://www.m4b.io/elf/export/binary/analysis/2015/05/25/what-is-an-elf-export.html
if not symbol.name:
@@ -73,21 +92,19 @@ def extract_file_import_names(elf: ELFFile, **kwargs):
symbol_names[_] = symbol.name
for section in elf.iter_sections():
if not isinstance(section, RelocationSection):
for segment in elf.iter_segments():
if not isinstance(segment, DynamicSegment):
continue
if section["sh_entsize"] == 0:
logger.debug("Symbol table '%s' has a sh_entsize of zero!", section.name)
continue
relocation_tables = segment.get_relocation_tables()
logger.debug("Dynamic Segment contains %s relocation tables:", len(relocation_tables))
logger.debug("Symbol table '%s' contains %s entries:", section.name, section.num_relocations())
for relocation in section.iter_relocations():
# Extract the symbol name from the symbol table using the symbol index in the relocation
if relocation["r_info_sym"] not in symbol_names:
continue
yield Import(symbol_names[relocation["r_info_sym"]]), FileOffsetAddress(relocation["r_offset"])
for relocation_table in relocation_tables.values():
for relocation in relocation_table.iter_relocations():
# Extract the symbol name from the symbol table using the symbol index in the relocation
if relocation["r_info_sym"] not in symbol_names:
continue
yield Import(symbol_names[relocation["r_info_sym"]]), FileOffsetAddress(relocation["r_offset"])
def extract_file_section_names(elf: ELFFile, **kwargs):

View File

@@ -14,17 +14,11 @@ from capa.features.extractors.elffile import extract_file_export_names, extract_
CD = Path(__file__).resolve().parent
SAMPLE_PATH = CD / "data" / "055da8e6ccfe5a9380231ea04b850e18.elf_"
STRIPPED_SAMPLE_PATH = CD / "data" / "bb38149ff4b5c95722b83f24ca27a42b.elf_"
def test_elffile_import_features():
expected_imports = [
"memfrob",
"puts",
"__libc_start_main",
"malloc",
"__cxa_finalize",
]
path = Path(SAMPLE_PATH)
def check_import_features(sample_path, expected_imports):
path = Path(sample_path)
elf = ELFFile(io.BytesIO(path.read_bytes()))
# Extract imports
imports = list(extract_file_import_names(elf))
@@ -40,6 +34,52 @@ def test_elffile_import_features():
assert symbol_name in extracted_symbol_names, f"Symbol '{symbol_name}' not found in imports."
def check_export_features(sample_path, expected_exports):
path = Path(sample_path)
elf = ELFFile(io.BytesIO(path.read_bytes()))
# Extract imports
exports = list(extract_file_export_names(elf))
# Verify that at least one export was found
assert len(exports) > 0, "No exports were found."
# Extract the symbol names from the extracted imports
extracted_symbol_names = [exported[0].value for exported in exports]
# Check if all expected symbol names are found
for symbol_name in expected_exports:
assert symbol_name in extracted_symbol_names, f"Symbol '{symbol_name}' not found in exports."
def test_stripped_elffile_import_features():
expected_imports = ["__cxa_atexit", "__cxa_finalize", "__stack_chk_fail", "fclose", "fopen", "__android_log_print"]
check_import_features(STRIPPED_SAMPLE_PATH, expected_imports)
def test_stripped_elffile_export_features():
expected_exports = [
"_ZN7_JNIEnv14GetArrayLengthEP7_jarray",
"Java_o_ac_a",
"Java_o_ac_b",
"_Z6existsPKc",
"_ZN7_JNIEnv17GetStringUTFCharsEP8_jstringPh",
"_ZN7_JNIEnv21GetObjectArrayElementEP13_jobjectArrayi",
"_ZN7_JNIEnv21ReleaseStringUTFCharsEP8_jstringPKc",
]
check_export_features(STRIPPED_SAMPLE_PATH, expected_exports)
def test_elffile_import_features():
expected_imports = [
"memfrob",
"puts",
"__libc_start_main",
"malloc",
"__cxa_finalize",
]
check_import_features(SAMPLE_PATH, expected_imports)
def test_elffile_export_features():
expected_exports = [
"deregister_tm_clones",
@@ -55,17 +95,4 @@ def test_elffile_export_features():
"_IO_stdin_used",
"__libc_csu_init",
]
path = Path(SAMPLE_PATH)
elf = ELFFile(io.BytesIO(path.read_bytes()))
# Extract imports
exports = list(extract_file_export_names(elf))
# Verify that at least one export was found
assert len(exports) > 0, "No exports were found."
# Extract the symbol names from the extracted imports
extracted_symbol_names = [exported[0].value for exported in exports]
# Check if all expected symbol names are found
for symbol_name in expected_exports:
assert symbol_name in extracted_symbol_names, f"Symbol '{symbol_name}' not found in exports."
check_export_features(SAMPLE_PATH, expected_exports)

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
});