mirror of
https://github.com/mandiant/capa.git
synced 2025-12-25 04:14:55 -08:00
Compare commits
1 Commits
dependabot
...
testing/ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
413047a7e8 |
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