mirror of
https://github.com/mandiant/capa.git
synced 2025-12-05 20:40:05 -08:00
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
This commit is contained in:
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