mirror of
https://github.com/mandiant/capa.git
synced 2025-12-12 15:49:46 -08:00
Merge pull request #2301 from s-ff/use-gzipped-preview
web: don't bundle preview data in build and release
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
<script setup>
|
||||
import Menubar from "primevue/menubar";
|
||||
import { RouterLink } from "vue-router";
|
||||
|
||||
const isBundle = import.meta.env.MODE === "bundle";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -13,8 +15,9 @@ import { RouterLink } from "vue-router";
|
||||
<template #end>
|
||||
<div class="flex align-items-center gap-3">
|
||||
<a
|
||||
v-if="!isBundle"
|
||||
v-ripple
|
||||
v-tooltip.right="'Download capa Explorer Web for offline usage'"
|
||||
v-tooltip.bottom="'Download capa Explorer Web for offline usage'"
|
||||
href="./capa-explorer-web.zip"
|
||||
download="capa-explorer-web.zip"
|
||||
aria-label="Download capa Explorer Web release"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
:value="filteredTreeData"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
size="small"
|
||||
scrollable
|
||||
:scrollable="true"
|
||||
:filters="filters"
|
||||
:filterMode="filterMode"
|
||||
sortField="namespace"
|
||||
@@ -49,10 +49,11 @@
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<!-- Address/Process column -->
|
||||
<!-- Address column (only shown for static flavor) -->
|
||||
<Column
|
||||
v-if="props.data.meta.flavor === 'static'"
|
||||
field="address"
|
||||
:header="props.data.meta.flavor === 'dynamic' ? 'Process' : 'Address'"
|
||||
header="Address"
|
||||
filterMatchMode="contains"
|
||||
style="width: 8.5%"
|
||||
class="cursor-default"
|
||||
@@ -252,7 +253,6 @@ const onRightClick = (event, instance) => {
|
||||
selectedNode.value = instance.node;
|
||||
|
||||
// show the context menu
|
||||
console.log(menu);
|
||||
menu.value.show(event);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -29,28 +29,34 @@
|
||||
</FloatLabel>
|
||||
<Button icon="pi pi-arrow-right" @click="$emit('load-from-url', loadURL)" :disabled="!loadURL" />
|
||||
</div>
|
||||
<template v-if="!isBundle">
|
||||
<Divider layout="vertical" class="hidden-mobile">
|
||||
<b>OR</b>
|
||||
</Divider>
|
||||
<Divider layout="horizontal" class="visible-mobile" align="center">
|
||||
<b>OR</b>
|
||||
</Divider>
|
||||
|
||||
<Divider layout="vertical" class="hidden-mobile">
|
||||
<b>OR</b>
|
||||
</Divider>
|
||||
<Divider layout="horizontal" class="visible-mobile" align="center">
|
||||
<b>OR</b>
|
||||
</Divider>
|
||||
<div class="flex-grow-1 flex align-items-center justify-content-center">
|
||||
<Button
|
||||
label="Preview Static"
|
||||
@click="router.push({ path: '/', query: { rdoc: staticURL } })"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-grow-1 flex align-items-center justify-content-center">
|
||||
<Button label="Preview Static" @click="$emit('load-demo-static')" class="p-button" />
|
||||
</div>
|
||||
|
||||
<Divider layout="vertical" class="hidden-mobile">
|
||||
<b>OR</b>
|
||||
</Divider>
|
||||
<Divider layout="horizontal" class="visible-mobile" align="center">
|
||||
<b>OR</b>
|
||||
</Divider>
|
||||
|
||||
<div class="flex-grow-1 flex align-items-center justify-content-center">
|
||||
<Button label="Preview Dynamic" @click="$emit('load-demo-dynamic')" class="p-button" />
|
||||
</div>
|
||||
<Divider layout="vertical" class="hidden-mobile">
|
||||
<b>OR</b>
|
||||
</Divider>
|
||||
<Divider layout="horizontal" class="visible-mobile" align="center">
|
||||
<b>OR</b>
|
||||
</Divider>
|
||||
<div class="flex-grow-1 flex align-items-center justify-content-center">
|
||||
<Button
|
||||
label="Preview Dynamic"
|
||||
@click="router.push({ path: '/', query: { rdoc: dynamicURL } })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
@@ -65,9 +71,17 @@ import FloatLabel from "primevue/floatlabel";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Button from "primevue/button";
|
||||
|
||||
const loadURL = ref("");
|
||||
import { useRouter } from "vue-router";
|
||||
const router = useRouter();
|
||||
|
||||
defineEmits(["load-from-local", "load-from-url", "load-demo-static", "load-demo-dynamic"]);
|
||||
const loadURL = ref("");
|
||||
const isBundle = import.meta.env.MODE === "bundle";
|
||||
|
||||
defineEmits(["load-from-local", "load-from-url"]);
|
||||
|
||||
const dynamicURL =
|
||||
"https://raw.githubusercontent.com/mandiant/capa-testfiles/master/rd/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz";
|
||||
const staticURL = "https://raw.githubusercontent.com/mandiant/capa-testfiles/master/rd/al-khaser_x64.exe_.json";
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<!-- example node: "basic block @ 0x401000" or "explorer.exe" -->
|
||||
<template v-else-if="node.data.type === 'match location'">
|
||||
<span class="text-sm font-italic">{{ node.data.name }}</span>
|
||||
<span class="text-sm font-monospace text-xs">{{ node.data.name }}</span>
|
||||
</template>
|
||||
|
||||
<!-- example node: "- or", "- and" -->
|
||||
@@ -52,7 +52,7 @@
|
||||
|
||||
<!-- example node: "exit(0) -> 0" (if the node type is call-info, we highlight node.data.name.callInfo) -->
|
||||
<template v-else-if="node.data.type === 'call-info'">
|
||||
<highlightjs lang="c" :code="node.data.name.callInfo" />
|
||||
<highlightjs lang="c" :code="node.data.name.callInfo" class="text-xs" />
|
||||
</template>
|
||||
|
||||
<!-- example node: " = IMAGE_NT_SIGNATURE (PE)" -->
|
||||
|
||||
@@ -6,32 +6,63 @@ export function useRdocLoader() {
|
||||
const MIN_SUPPORTED_VERSION = "7.0.0";
|
||||
|
||||
/**
|
||||
* Checks if the loaded rdoc version is supported
|
||||
* @param {Object} rdoc - The loaded JSON rdoc data
|
||||
* @returns {boolean} - True if version is supported, false otherwise
|
||||
* Displays a toast notification.
|
||||
* @param {string} severity - The severity level of the notification
|
||||
* @param {string} summary - The title of the notification.
|
||||
* @param {string} detail - The detailed message of the notification.
|
||||
* @returns {void}
|
||||
*/
|
||||
const showToast = (severity, summary, detail) => {
|
||||
toast.add({ severity, summary, detail, life: 3000, group: "bc" }); // bc: bottom-center
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the version of the loaded data is supported.
|
||||
* @param {Object} rdoc - The loaded JSON data containing version information.
|
||||
* @returns {boolean} True if the version is supported, false otherwise.
|
||||
*/
|
||||
const checkVersion = (rdoc) => {
|
||||
const version = rdoc.meta.version;
|
||||
if (version < MIN_SUPPORTED_VERSION) {
|
||||
console.error(
|
||||
showToast(
|
||||
"error",
|
||||
"Unsupported Version",
|
||||
`Version ${version} is not supported. Please use version ${MIN_SUPPORTED_VERSION} or higher.`
|
||||
);
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "Unsupported Version",
|
||||
detail: `Version ${version} is not supported. Please use version ${MIN_SUPPORTED_VERSION} or higher.`,
|
||||
life: 5000,
|
||||
group: "bc" // bottom-center
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads JSON rdoc data from various sources
|
||||
* @param {File|string|Object} source - File object, URL string, or JSON object
|
||||
* @returns {Promise<void>}
|
||||
* Processes the content of a file or blob.
|
||||
* @param {File|Blob} blob - The file or blob to process.
|
||||
* @returns {Promise<Object>} A promise that resolves to the parsed JSON data.
|
||||
* @throws {Error} If the content cannot be parsed as JSON.
|
||||
*/
|
||||
const processBlob = async (blob) => {
|
||||
const content = (await isGzipped(blob)) ? await decompressGzip(blob) : await readFileAsText(blob);
|
||||
return JSON.parse(content);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches data from a URL.
|
||||
* @param {string} url - The URL to fetch data from.
|
||||
* @returns {Promise<Blob>} A promise that resolves to the fetched data as a Blob.
|
||||
* @throws {Error} If the fetch request fails.
|
||||
*/
|
||||
const fetchFromUrl = async (url) => {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return response.blob();
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads and processes RDOC data from various sources.
|
||||
* @param {string|File|Object} source - The source of the RDOC data. Can be a URL string, a File object, or a JSON object.
|
||||
* @returns {Promise<Object|null>} A promise that resolves to the processed RDOC data, or null if processing fails.
|
||||
*/
|
||||
const loadRdoc = async (source) => {
|
||||
try {
|
||||
@@ -39,50 +70,25 @@ export function useRdocLoader() {
|
||||
|
||||
if (typeof source === "string") {
|
||||
// Load from URL
|
||||
const response = await fetch(source);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
data = await response.json();
|
||||
const blob = await fetchFromUrl(source);
|
||||
data = await processBlob(blob);
|
||||
} else if (source instanceof File) {
|
||||
let fileContent;
|
||||
if (await isGzipped(source)) {
|
||||
fileContent = await decompressGzip(source);
|
||||
} else {
|
||||
fileContent = await readFileAsText(source);
|
||||
}
|
||||
data = JSON.parse(fileContent);
|
||||
} else if (typeof source === "object") {
|
||||
// Direct JSON object (Preview options)
|
||||
data = source;
|
||||
// Load from local
|
||||
data = await processBlob(source);
|
||||
} else {
|
||||
throw new Error("Invalid source type");
|
||||
}
|
||||
|
||||
if (checkVersion(data)) {
|
||||
toast.add({
|
||||
severity: "success",
|
||||
summary: "Success",
|
||||
detail: "JSON data loaded successfully",
|
||||
life: 3000,
|
||||
group: "bc" // bottom-center
|
||||
});
|
||||
showToast("success", "Success", "JSON data loaded successfully");
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading JSON:", error);
|
||||
toast.add({
|
||||
severity: "error",
|
||||
summary: "Error",
|
||||
detail: "Failed to process the file. Please ensure it's a valid JSON or gzipped JSON file.",
|
||||
life: 3000,
|
||||
group: "bc" // bottom-center
|
||||
});
|
||||
showToast("error", "Failed to process the file", error.message);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return {
|
||||
loadRdoc
|
||||
};
|
||||
return { loadRdoc };
|
||||
}
|
||||
|
||||
@@ -3,69 +3,219 @@
|
||||
* @param {Object} rules - The rules object from the rodc JSON data
|
||||
* @param {string} flavor - The flavor of the analysis (static or dynamic)
|
||||
* @param {Object} layout - The layout object from the rdoc JSON data
|
||||
* @param {number} [maxMatches=1] - Maximum number of matches to parse per rule
|
||||
* @param {number} [maxMatches=300] - Maximum number of matches to parse per rule (used for optimized rendering in dynamic analysis)
|
||||
* @returns {Array} - Parsed tree data for the TreeTable component
|
||||
*/
|
||||
export function parseRules(rules, flavor, layout, maxMatches = 1) {
|
||||
return Object.entries(rules).map(([, rule], index) => {
|
||||
const ruleNode = {
|
||||
key: `${index}`,
|
||||
data: {
|
||||
type: "rule",
|
||||
name: rule.meta.name,
|
||||
lib: rule.meta.lib,
|
||||
matchCount: rule.matches.length,
|
||||
namespace: rule.meta.namespace,
|
||||
mbc: rule.meta.mbc,
|
||||
source: rule.source,
|
||||
attack: rule.meta.attack
|
||||
}
|
||||
};
|
||||
export function parseRules(rules, flavor, _layout, maxMatches = 300) {
|
||||
const layout = preprocessLayout(_layout);
|
||||
const treeData = [];
|
||||
let index = 0;
|
||||
|
||||
for (const [, rule] of Object.entries(rules)) {
|
||||
const ruleNode = createRuleNode(rule, index, flavor);
|
||||
|
||||
// Limit the number of matches to process
|
||||
// Dynamic matches can have thousands of matches, only show `maxMatches` for performance reasons
|
||||
const limitedMatches = flavor === "dynamic" ? rule.matches.slice(0, maxMatches) : rule.matches;
|
||||
// Dynamic matches can have thousands of matches, only show `maxMatches` for rendering optimization
|
||||
const matchesToProcess = flavor === "dynamic" ? rule.matches.slice(0, maxMatches) : rule.matches;
|
||||
|
||||
// Is this a static rule with a file-level scope?
|
||||
const isFileScope = rule.meta.scopes && rule.meta.scopes.static === "file";
|
||||
for (let matchIndex = 0; matchIndex < matchesToProcess.length; matchIndex++) {
|
||||
const match = matchesToProcess[matchIndex];
|
||||
const matchKey = `${index}-${matchIndex}`;
|
||||
|
||||
if (isFileScope) {
|
||||
// The scope for the rule is a file, so we don't need to show the match location address
|
||||
ruleNode.children = limitedMatches.map((match, matchIndex) => {
|
||||
return parseNode(match[1], `${index}-${matchIndex}`, rules, rule.meta.lib, layout);
|
||||
});
|
||||
} else {
|
||||
// This is not a file-level match scope, we need to create intermediate nodes for each match
|
||||
ruleNode.children = limitedMatches.map((match, matchIndex) => {
|
||||
const matchKey = `${index}-${matchIndex}`;
|
||||
const matchNode = {
|
||||
key: matchKey,
|
||||
data: {
|
||||
type: "match location",
|
||||
name:
|
||||
flavor === "static"
|
||||
? `${rule.meta.scopes.static} @ ` + formatAddress(match[0])
|
||||
: getProcessName(layout, match[0])
|
||||
},
|
||||
children: [parseNode(match[1], `${matchKey}`, rules, rule.meta.lib, layout)]
|
||||
};
|
||||
return matchNode;
|
||||
});
|
||||
// Check if the rule has a file-level scope
|
||||
if (rule.meta.scopes && rule.meta.scopes.static === "file") {
|
||||
// The scope for the rule is a file, so we don't need to show the match location address
|
||||
ruleNode.children.push(parseNode(match[1], matchKey, rules, rule.meta.lib, layout));
|
||||
} else {
|
||||
// This is not a file-level match scope, we need to create an intermediate node for each match
|
||||
const matchNode = createMatchNode(rule.meta.scopes.static, match, matchKey, flavor, layout);
|
||||
matchNode.children.push(parseNode(match[1], matchKey, rules, rule.meta.lib, layout));
|
||||
ruleNode.children.push(matchNode);
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, add a note if there are more matches than the limit (only applicable in dynamic mode)
|
||||
if (rule.matches.length > limitedMatches.length) {
|
||||
ruleNode.children.push({
|
||||
key: `${index}`,
|
||||
data: {
|
||||
type: "match location",
|
||||
name: `... and ${rule.matches.length - maxMatches} more matches`
|
||||
// Add note for additional non-covered matches in dynamic mode
|
||||
if (flavor === "dynamic" && rule.matches.length > maxMatches) {
|
||||
ruleNode.children.push(createAdditionalMatchesNode(index, rule.matches.length - maxMatches));
|
||||
}
|
||||
|
||||
treeData.push(ruleNode);
|
||||
index++;
|
||||
}
|
||||
return treeData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Preprocesses the layout to create efficient lookup maps
|
||||
* @param {Object} layout - The layout object from rdoc JSON data
|
||||
* @returns {Object} An object containing lookup maps for calls, threads, and processes
|
||||
*/
|
||||
function preprocessLayout(layout) {
|
||||
const processMap = new Map();
|
||||
const threadMap = new Map();
|
||||
const callMap = new Map();
|
||||
|
||||
if (layout && layout.processes) {
|
||||
for (const process of layout.processes) {
|
||||
if (process.address && process.address.type === "process" && process.address.value) {
|
||||
const [ppid, pid] = process.address.value;
|
||||
processMap.set(`${ppid}-${pid}`, process);
|
||||
|
||||
if (process.matched_threads) {
|
||||
for (const thread of process.matched_threads) {
|
||||
if (thread.address && thread.address.type === "thread" && thread.address.value) {
|
||||
const [, , tid] = thread.address.value;
|
||||
threadMap.set(`${ppid}-${pid}-${tid}`, thread);
|
||||
|
||||
if (thread.matched_calls) {
|
||||
for (const call of thread.matched_calls) {
|
||||
if (call.address && call.address.type === "call" && call.address.value) {
|
||||
const [, , , callId] = call.address.value;
|
||||
callMap.set(`${ppid}-${pid}-${tid}-${callId}`, call);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { processMap, threadMap, callMap };
|
||||
}
|
||||
// Creates a node for a rule
|
||||
function createRuleNode(rule, index) {
|
||||
return {
|
||||
key: `${index}`,
|
||||
data: {
|
||||
type: "rule",
|
||||
name: rule.meta.name,
|
||||
lib: rule.meta.lib,
|
||||
matchCount: rule.matches.length,
|
||||
namespace: rule.meta.namespace,
|
||||
mbc: rule.meta.mbc,
|
||||
source: rule.source,
|
||||
attack: rule.meta.attack
|
||||
},
|
||||
children: []
|
||||
};
|
||||
}
|
||||
|
||||
// Creates a match location (e.g. basic block @ 0x1000 or explorer.exe (ppid: 1234, pid: 5678)) node
|
||||
function createMatchNode(scope, match, matchKey, flavor, layout) {
|
||||
const [location] = match;
|
||||
const name = flavor === "static" ? `${scope} @ ${formatAddress(location)}` : getProcessName(layout, location);
|
||||
|
||||
return {
|
||||
key: matchKey,
|
||||
data: {
|
||||
type: "match location",
|
||||
name: name
|
||||
},
|
||||
children: []
|
||||
};
|
||||
}
|
||||
|
||||
// Creates a note node for additional non-covered matches in dynamic mode
|
||||
function createAdditionalMatchesNode(index, additionalMatchCount) {
|
||||
return {
|
||||
key: `${index}`,
|
||||
data: {
|
||||
type: "match location",
|
||||
name: `... and ${additionalMatchCount} more matches`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a single `node` object (i.e. statement or feature) in each rule
|
||||
* @param {Object} node - The node to parse
|
||||
* @param {string} key - The key for this node
|
||||
* @param {Object} rules - The full rules object
|
||||
* @param {boolean} lib - Whether this is a library rule
|
||||
* @returns {Object} - Parsed node data
|
||||
**/
|
||||
|
||||
function parseNode(node, key, rules, lib, layout) {
|
||||
if (!node) return null;
|
||||
|
||||
const isNotStatement = node.node.statement && node.node.statement.type === "not";
|
||||
const processedNode = isNotStatement ? invertNotStatementSuccess(node) : node;
|
||||
|
||||
if (!processedNode.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = {
|
||||
key: key,
|
||||
data: {
|
||||
type: processedNode.node.type, // feature or statement
|
||||
typeValue: processedNode.node.statement?.type || processedNode.node.feature?.type,
|
||||
success: processedNode.success,
|
||||
name: getNodeName(processedNode),
|
||||
lib: lib,
|
||||
address: getNodeAddress(processedNode),
|
||||
description: getNodeDescription(processedNode)
|
||||
},
|
||||
children: []
|
||||
};
|
||||
|
||||
if (processedNode.children && Array.isArray(processedNode.children)) {
|
||||
result.children = processedNode.children
|
||||
.map((child) => parseNode(child, `${key}`, rules, lib, layout))
|
||||
.filter((child) => child !== null);
|
||||
}
|
||||
|
||||
if (processedNode.node.feature && processedNode.node.feature.type === "match") {
|
||||
const ruleName = processedNode.node.feature.match;
|
||||
const rule = rules[ruleName];
|
||||
if (rule) {
|
||||
result.data.source = rule.source;
|
||||
}
|
||||
result.children = [];
|
||||
}
|
||||
|
||||
if (
|
||||
processedNode.node.statement &&
|
||||
processedNode.node.statement.type === "optional" &&
|
||||
result.children.length === 0
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (processedNode.node.feature && processedNode.node.feature.type === "regex") {
|
||||
result.children = processRegexCaptures(processedNode, key);
|
||||
}
|
||||
|
||||
if (processedNode.node.feature && processedNode.node.feature.type === "api") {
|
||||
const callInfo = getCallInfo(node, layout);
|
||||
if (callInfo) {
|
||||
result.children.push({
|
||||
key: key,
|
||||
data: {
|
||||
type: "call-info",
|
||||
name: callInfo
|
||||
},
|
||||
children: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return ruleNode;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the process name using the optimized processNames Map
|
||||
* @param {Map} layout - The layout object containing maps
|
||||
* @param {Object} address - The address object containing process information
|
||||
* @returns {string} The process name
|
||||
*/
|
||||
function getProcessName(layout, address) {
|
||||
const [ppid, pid] = address.value;
|
||||
const processKey = `${ppid}-${pid}`;
|
||||
const process = layout.processMap.get(processKey);
|
||||
return process.name + ` (ppid:${ppid}, pid:${pid})`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -175,249 +325,34 @@ export function parseFunctionCapabilities(doc) {
|
||||
|
||||
// Helper functions
|
||||
|
||||
/**
|
||||
* Parses a single `node` object (i.e. statement or feature) in each rule
|
||||
* @param {Object} node - The node to parse
|
||||
* @param {string} key - The key for this node
|
||||
* @param {Object} rules - The full rules object
|
||||
* @param {boolean} lib - Whether this is a library rule
|
||||
* @returns {Object} - Parsed node data
|
||||
*/
|
||||
function parseNode(node, key, rules, lib, layout) {
|
||||
if (!node) return null;
|
||||
|
||||
const isNotStatement = node.node.statement && node.node.statement.type === "not";
|
||||
const processedNode = isNotStatement ? invertNotStatementSuccess(node) : node;
|
||||
|
||||
if (!processedNode.success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = {
|
||||
key: key,
|
||||
data: {
|
||||
type: processedNode.node.type, // statement or feature
|
||||
typeValue: processedNode.node.statement?.type || processedNode.node.feature?.type, // e.g., number, regex, api, or, and, optional ... etc
|
||||
success: processedNode.success,
|
||||
name: getNodeName(processedNode),
|
||||
lib: lib,
|
||||
address: getNodeAddress(processedNode),
|
||||
description: getNodeDescription(processedNode)
|
||||
},
|
||||
children: []
|
||||
};
|
||||
// Recursively parse node children (i.e., nested statements or features)
|
||||
if (processedNode.children && Array.isArray(processedNode.children)) {
|
||||
result.children = processedNode.children
|
||||
.map((child) => {
|
||||
const childNode = parseNode(child, `${key}`, rules, lib, layout);
|
||||
return childNode;
|
||||
})
|
||||
.filter((child) => child !== null);
|
||||
}
|
||||
// If this is a match node, add the rule's source code to the result.data.source object
|
||||
if (processedNode.node.feature && processedNode.node.feature.type === "match") {
|
||||
const ruleName = processedNode.node.feature.match;
|
||||
const rule = rules[ruleName];
|
||||
if (rule) {
|
||||
result.data.source = rule.source;
|
||||
}
|
||||
result.children = [];
|
||||
}
|
||||
// If this is an optional node, check if it has children. If not, return null (optional statement always evaluate to true)
|
||||
// we only render them, if they have at least one child node where node.success is true.
|
||||
if (processedNode.node.statement && processedNode.node.statement.type === "optional") {
|
||||
if (result.children.length === 0) return null;
|
||||
}
|
||||
|
||||
// regex features have captures, which we need to process and add as children
|
||||
if (processedNode.node.feature && processedNode.node.feature.type === "regex") {
|
||||
result.children = processRegexCaptures(processedNode, key);
|
||||
}
|
||||
|
||||
// Add call information for dynamic sandbox traces when the feature is `api`
|
||||
if (processedNode.node.feature && processedNode.node.feature.type === "api") {
|
||||
const callInfo = getCallInfo(node, layout);
|
||||
if (callInfo) {
|
||||
result.children.push({
|
||||
key: key,
|
||||
data: {
|
||||
type: "call-info",
|
||||
name: callInfo
|
||||
},
|
||||
children: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function getCallInfo(node, layout) {
|
||||
if (!node.locations || node.locations.length === 0) return null;
|
||||
|
||||
const location = node.locations[0];
|
||||
if (location.type !== "call") return null;
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [ppid, pid, tid, callId] = location.value;
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const callName = node.node.feature.api;
|
||||
|
||||
const pname = getProcessName(layout, location);
|
||||
const cname = getCallName(layout, location);
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [fname, separator, restWithArgs] = partition(cname, "(");
|
||||
const [args, , returnValueWithParen] = rpartition(restWithArgs, ")");
|
||||
|
||||
const s = [];
|
||||
s.push(`${fname}(`);
|
||||
for (const arg of args.split(", ")) {
|
||||
s.push(` ${arg},`);
|
||||
}
|
||||
s.push(`)${returnValueWithParen}`);
|
||||
|
||||
//const callInfo = `${pname}{pid:${pid},tid:${tid},call:${callId}}\n${s.join('\n')}`;
|
||||
|
||||
return { processName: pname, callInfo: s.join("\n") };
|
||||
return { processName: pname, callInfo: cname };
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a string into three parts based on the first occurrence of a separator.
|
||||
* This function mimics Python's str.partition() method.
|
||||
*
|
||||
* @param {string} str - The input string to be partitioned.
|
||||
* @param {string} separator - The separator to use for partitioning.
|
||||
* @returns {Array<string>} An array containing three elements:
|
||||
* 1. The part of the string before the separator.
|
||||
* 2. The separator itself.
|
||||
* 3. The part of the string after the separator.
|
||||
* If the separator is not found, returns [str, '', ''].
|
||||
*
|
||||
* @example
|
||||
* // Returns ["hello", ",", "world"]
|
||||
* partition("hello,world", ",");
|
||||
*
|
||||
* @example
|
||||
* // Returns ["hello world", "", ""]
|
||||
* partition("hello world", ":");
|
||||
*/
|
||||
function partition(str, separator) {
|
||||
const index = str.indexOf(separator);
|
||||
if (index === -1) {
|
||||
// Separator not found, return original string and two empty strings
|
||||
return [str, "", ""];
|
||||
}
|
||||
return [str.slice(0, index), separator, str.slice(index + separator.length)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the process name from the layout
|
||||
* @param {Object} layout - The layout object
|
||||
* @param {Object} address - The address object containing process information
|
||||
* @returns {string} The process name
|
||||
*/
|
||||
function getProcessName(layout, address) {
|
||||
if (!layout || !layout.processes || !Array.isArray(layout.processes)) {
|
||||
console.error("Invalid layout structure");
|
||||
return "Unknown Process";
|
||||
}
|
||||
|
||||
const [ppid, pid] = address.value;
|
||||
|
||||
for (const process of layout.processes) {
|
||||
if (
|
||||
process.address &&
|
||||
process.address.type === "process" &&
|
||||
process.address.value &&
|
||||
process.address.value[0] === ppid &&
|
||||
process.address.value[1] === pid
|
||||
) {
|
||||
return process.name || "Unnamed Process";
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown Process";
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits a string into three parts based on the last occurrence of a separator.
|
||||
* This function mimics Python's str.rpartition() method.
|
||||
*
|
||||
* @param {string} str - The input string to be partitioned.
|
||||
* @param {string} separator - The separator to use for partitioning.
|
||||
* @returns {Array<string>} An array containing three elements:
|
||||
* 1. The part of the string before the last occurrence of the separator.
|
||||
* 2. The separator itself.
|
||||
* 3. The part of the string after the last occurrence of the separator.
|
||||
* If the separator is not found, returns ['', '', str].
|
||||
*
|
||||
* @example
|
||||
* // Returns ["hello,", ",", "world"]
|
||||
* rpartition("hello,world,", ",");
|
||||
*
|
||||
* @example
|
||||
* // Returns ["", "", "hello world"]
|
||||
* rpartition("hello world", ":");
|
||||
*/
|
||||
function rpartition(str, separator) {
|
||||
const index = str.lastIndexOf(separator);
|
||||
if (index === -1) {
|
||||
// Separator not found, return two empty strings and the original string
|
||||
return ["", "", str];
|
||||
}
|
||||
return [
|
||||
str.slice(0, index), // Part before the last separator
|
||||
separator, // The separator itself
|
||||
str.slice(index + separator.length) // Part after the last separator
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the call name from the layout
|
||||
* @param {Object} layout - The layout object
|
||||
* Get the call name from the preprocessed layout maps
|
||||
* @param {Object} layoutMaps - The preprocessed layout maps
|
||||
* @param {Object} address - The address object containing call information
|
||||
* @returns {string} The call name with arguments
|
||||
* @returns {string} The call name or "Unknown Call" if not found
|
||||
*/
|
||||
function getCallName(layout, address) {
|
||||
if (!layout || !layout.processes || !Array.isArray(layout.processes)) {
|
||||
console.error("Invalid layout structure");
|
||||
function getCallName(layoutMaps, address) {
|
||||
if (!address || !address.value || address.value.length < 4) {
|
||||
return "Unknown Call";
|
||||
}
|
||||
|
||||
const [ppid, pid, tid, callId] = address.value;
|
||||
const callKey = `${ppid}-${pid}-${tid}-${callId}`;
|
||||
|
||||
for (const process of layout.processes) {
|
||||
if (
|
||||
process.address &&
|
||||
process.address.type === "process" &&
|
||||
process.address.value &&
|
||||
process.address.value[0] === ppid &&
|
||||
process.address.value[1] === pid
|
||||
) {
|
||||
for (const thread of process.matched_threads) {
|
||||
if (
|
||||
thread.address &&
|
||||
thread.address.type === "thread" &&
|
||||
thread.address.value &&
|
||||
thread.address.value[2] === tid
|
||||
) {
|
||||
for (const call of thread.matched_calls) {
|
||||
if (
|
||||
call.address &&
|
||||
call.address.type === "call" &&
|
||||
call.address.value &&
|
||||
call.address.value[3] === callId
|
||||
) {
|
||||
return call.name || "Unnamed Call";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "Unknown Call";
|
||||
const call = layoutMaps.callMap.get(callKey);
|
||||
return call.name;
|
||||
}
|
||||
|
||||
function processRegexCaptures(node, key) {
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<template>
|
||||
<DescriptionPanel />
|
||||
<UploadOptions
|
||||
@load-from-local="loadFromLocal"
|
||||
@load-from-url="loadFromURL"
|
||||
@load-demo-static="loadDemoDataStatic"
|
||||
@load-demo-dynamic="loadDemoDataDynamic"
|
||||
/>
|
||||
<UploadOptions @load-from-local="loadFromLocal" @load-from-url="loadFromURL" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -15,10 +10,6 @@ import { watch } from "vue";
|
||||
import DescriptionPanel from "@/components/DescriptionPanel.vue";
|
||||
import UploadOptions from "@/components/UploadOptions.vue";
|
||||
|
||||
// import demo data
|
||||
import demoRdocStatic from "@testfiles/rd/al-khaser_x64.exe_.json";
|
||||
import demoRdocDynamic from "@testfiles/rd/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json";
|
||||
|
||||
// import router utils
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
const router = useRouter();
|
||||
@@ -47,22 +38,6 @@ const loadFromURL = async (url) => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadDemoDataStatic = async () => {
|
||||
const result = await loadRdoc(demoRdocStatic);
|
||||
if (result) {
|
||||
rdocStore.setData(demoRdocStatic);
|
||||
router.push("/analysis");
|
||||
}
|
||||
};
|
||||
|
||||
const loadDemoDataDynamic = async () => {
|
||||
const result = await loadRdoc(demoRdocDynamic);
|
||||
if (result) {
|
||||
rdocStore.setData(demoRdocDynamic);
|
||||
router.push("/analysis");
|
||||
}
|
||||
};
|
||||
|
||||
// Watch for changes in the rdoc query parameter
|
||||
watch(
|
||||
() => route.query.rdoc,
|
||||
|
||||
@@ -14,6 +14,7 @@ export default defineConfig(({ mode }) => {
|
||||
"@": fileURLToPath(new URL("src", import.meta.url)),
|
||||
"@testfiles": fileURLToPath(new URL("../../tests/data", import.meta.url))
|
||||
}
|
||||
}
|
||||
},
|
||||
assetsInclude: ["**/*.gz"]
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user