mirror of
https://github.com/mandiant/capa.git
synced 2026-02-04 11:07:53 -08:00
feature: add call information to api feature in dynamic mode (-vv)
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<TreeTable
|
||||
:value="filteredTreeData"
|
||||
:value="filteredTreeData"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
size="small"
|
||||
:filters="filters"
|
||||
:filterMode="filterMode.value"
|
||||
:filterMode="filterMode.value"
|
||||
sortField="namespace"
|
||||
:sortOrder="-1"
|
||||
removableSort
|
||||
@@ -16,18 +16,12 @@
|
||||
@nodeSelect="onNodeSelect"
|
||||
:pt="{
|
||||
row: ({ instance }) => ({
|
||||
oncontextmenu: (event) => onRightClick(event, instance),
|
||||
}),
|
||||
oncontextmenu: (event) => onRightClick(event, instance)
|
||||
})
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div style="display: flex; justify-content: end; align-items: center">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="filters['global']" placeholder="Global search" />
|
||||
@@ -36,19 +30,9 @@
|
||||
</template>
|
||||
|
||||
<!-- Name column (always visible) -->
|
||||
<Column
|
||||
field="name"
|
||||
header="Rule"
|
||||
:sortable="true"
|
||||
:expander="true"
|
||||
filterMatchMode="contains"
|
||||
>
|
||||
<Column field="name" header="Rule" :sortable="true" :expander="true" filterMatchMode="contains">
|
||||
<template #filter>
|
||||
<InputText
|
||||
v-model="filters['name']"
|
||||
type="text"
|
||||
placeholder="Filter by Rule or Feature"
|
||||
/>
|
||||
<InputText v-model="filters['name']" type="text" placeholder="Filter by Rule or Feature" />
|
||||
</template>
|
||||
<template #body="{ node }">
|
||||
<RuleColumn :node="node" />
|
||||
@@ -59,26 +43,20 @@
|
||||
v-for="col in visibleColumns"
|
||||
:key="col.field"
|
||||
:field="col.field"
|
||||
:header="
|
||||
props.data.meta.flavor === 'dynamic' && col.field === 'address' ? 'Process' : col.header
|
||||
"
|
||||
:header="props.data.meta.flavor === 'dynamic' && col.field === 'address' ? 'Process' : col.header"
|
||||
:sortable="col.field !== 'source'"
|
||||
:class="{ 'w-3': col.field === 'mbc', 'w-full': col.field === 'name' }"
|
||||
filterMatchMode="contains"
|
||||
>
|
||||
<!-- Filter template -->
|
||||
<template #filter>
|
||||
<InputText
|
||||
v-model="filters[col.field]"
|
||||
type="text"
|
||||
:placeholder="`Filter by ${col.header}`"
|
||||
/>
|
||||
<InputText v-model="filters[col.field]" type="text" :placeholder="`Filter by ${col.header}`" />
|
||||
</template>
|
||||
|
||||
<!-- Address column body template -->
|
||||
<template v-if="col.field === 'address'" #body="slotProps">
|
||||
<span style="font-family: monospace">
|
||||
{{ slotProps.node.data.type === 'match location' ? "" : slotProps.node.data.address}}
|
||||
{{ slotProps.node.data.type === 'match location' ? '' : slotProps.node.data.address }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -95,7 +73,8 @@
|
||||
style="font-size: 0.8em; margin-left: 1em"
|
||||
>
|
||||
<a :href="createATTACKHref(technique)" target="_blank">
|
||||
↳ {{ technique.technique }} <span class="text-500 text-xs font-normal ml-1">({{ technique.id }})</span>
|
||||
↳ {{ technique.technique }}
|
||||
<span class="text-500 text-xs font-normal ml-1">({{ technique.id }})</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,8 +86,9 @@
|
||||
<div v-if="slotProps.node.data.mbc">
|
||||
<div v-for="(mbc, index) in slotProps.node.data.mbc" :key="index">
|
||||
<a :href="createMBCHref(mbc)" target="_blank">
|
||||
{{ mbc.parts.join('::') }} <span class="text-500 text-sm font-normal opacity-80 ml-1">[{{ mbc.id }}]</span>
|
||||
</a>
|
||||
{{ mbc.parts.join('::') }}
|
||||
<span class="text-500 text-sm font-normal opacity-80 ml-1">[{{ mbc.id }}]</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -123,15 +103,15 @@
|
||||
</TreeTable>
|
||||
<ContextMenu ref="menu" :model="contextMenuItems">
|
||||
<template #item="{ item, props }">
|
||||
<a v-ripple v-bind="props.action" :href="item.url" :target="item.target">
|
||||
<span v-if="item.icon !== 'vt-icon'" :class="item.icon" />
|
||||
<VTIcon v-else-if="item.icon === 'vt-icon'" />
|
||||
<span>{{ item.label }}</span>
|
||||
</a>
|
||||
</template>
|
||||
<a v-ripple v-bind="props.action" :href="item.url" :target="item.target">
|
||||
<span v-if="item.icon !== 'vt-icon'" :class="item.icon" />
|
||||
<VTIcon v-else-if="item.icon === 'vt-icon'" />
|
||||
<span>{{ item.label }}</span>
|
||||
</a>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
|
||||
<Toast/>
|
||||
|
||||
<Toast />
|
||||
|
||||
<Dialog v-model:visible="sourceDialogVisible" :style="{ width: '50vw' }">
|
||||
<highlightjs autodetect :code="currentSource" />
|
||||
@@ -149,10 +129,8 @@ import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import ContextMenu from 'primevue/contextmenu'
|
||||
|
||||
|
||||
import RuleColumn from './columns/RuleColumn.vue';
|
||||
import VTIcon from './misc/VTIcon.vue';
|
||||
|
||||
import RuleColumn from './columns/RuleColumn.vue'
|
||||
import VTIcon from './misc/VTIcon.vue'
|
||||
|
||||
import { parseRules } from '../utils/rdocParser'
|
||||
|
||||
@@ -174,8 +152,8 @@ const sourceDialogVisible = ref(false)
|
||||
const currentSource = ref('')
|
||||
const expandedKeys = ref({})
|
||||
|
||||
const menu = ref();
|
||||
const selectedNode = ref({});
|
||||
const menu = ref()
|
||||
const selectedNode = ref({})
|
||||
|
||||
const contextMenuItems = computed(() => [
|
||||
{
|
||||
@@ -183,55 +161,55 @@ const contextMenuItems = computed(() => [
|
||||
icon: 'pi pi-eye',
|
||||
command: () => {
|
||||
showSource(selectedNode.value.data.source)
|
||||
},
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'View rule in capa-rules',
|
||||
icon: 'pi pi-external-link',
|
||||
target: '_blank',
|
||||
url: selectedNode.value.url,
|
||||
url: selectedNode.value.url
|
||||
},
|
||||
{
|
||||
label: 'Lookup rule in VirusTotal',
|
||||
icon: 'vt-icon',
|
||||
target: '_blank',
|
||||
url: selectedNode.value.vturl,
|
||||
},
|
||||
]);
|
||||
url: selectedNode.value.vturl
|
||||
}
|
||||
])
|
||||
|
||||
const onRightClick = (event, instance) => {
|
||||
if (instance.node.data.source) {
|
||||
selectedNode.value = instance.node;
|
||||
selectedNode.value = instance.node
|
||||
// contrust capa-rules url
|
||||
selectedNode.value.url = `https://github.com/mandiant/capa-rules/blob/master/${instance.node.data.namespace || 'lib'}/${instance.node.data.name.toLowerCase().replace(/\s+/g, '-')}.yml`
|
||||
// construct VirusTotal deep link
|
||||
const behaviourSignature = `behaviour_signature:"${instance.node.data.name}"`;
|
||||
const behaviourSignature = `behaviour_signature:"${instance.node.data.name}"`
|
||||
selectedNode.value.vturl = `https://www.virustotal.com/gui/search/${behaviourSignature}/files`
|
||||
|
||||
menu.value.show(event);
|
||||
menu.value.show(event)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Expand node on click
|
||||
/*
|
||||
* Expand node on click
|
||||
*/
|
||||
const onNodeSelect = (node) => {
|
||||
const nodeKey = node.key;
|
||||
const nodeType = node.data.type;
|
||||
const nodeKey = node.key
|
||||
const nodeType = node.data.type
|
||||
|
||||
if (nodeType === 'rule') {
|
||||
// For rule nodes, clear existing expanded keys and set the clicked rule as expanded
|
||||
// expand the first (child) match by default
|
||||
expandedKeys.value = { [nodeKey]: true, [`${nodeKey}-0`]: true };
|
||||
// expand the first (child) match by default
|
||||
expandedKeys.value = { [nodeKey]: true, [`${nodeKey}-0`]: true }
|
||||
} else if (nodeType === 'match location') {
|
||||
// For match location nodes, we need to keep the parent expanded
|
||||
// and toggle the clicked node while collapsing siblings
|
||||
const [parentKey, _] = nodeKey.split('-');
|
||||
expandedKeys.value = { [parentKey]: true, [`${nodeKey}`]: true};
|
||||
const [parentKey, _] = nodeKey.split('-')
|
||||
expandedKeys.value = { [parentKey]: true, [`${nodeKey}`]: true }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// All available columns
|
||||
const togglableColumns = ref([
|
||||
@@ -272,10 +250,9 @@ const showSource = (source) => {
|
||||
sourceDialogVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
if (props.data && props.data.rules) {
|
||||
treeData.value = parseRules(props.data.rules, props.data.meta.flavor)
|
||||
treeData.value = parseRules(props.data.rules, props.data.meta.flavor, props.data.meta.analysis.layout)
|
||||
} else {
|
||||
console.error('Invalid data prop:', props.data)
|
||||
}
|
||||
@@ -289,25 +266,25 @@ onMounted(() => {
|
||||
*/
|
||||
|
||||
function createMBCHref(mbc) {
|
||||
let baseUrl;
|
||||
let baseUrl
|
||||
|
||||
// Determine the base URL based on the id
|
||||
if (mbc.id.startsWith('B')) {
|
||||
// Behavior
|
||||
baseUrl = 'https://github.com/MBCProject/mbc-markdown/blob/main';
|
||||
baseUrl = 'https://github.com/MBCProject/mbc-markdown/blob/main'
|
||||
} else if (mbc.id.startsWith('C')) {
|
||||
// Micro-Behavior
|
||||
baseUrl = 'https://github.com/MBCProject/mbc-markdown/blob/main/micro-behaviors';
|
||||
baseUrl = 'https://github.com/MBCProject/mbc-markdown/blob/main/micro-behaviors'
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
// Convert the objective and behavior to lowercase and replace spaces with hyphens
|
||||
const objectivePath = mbc.objective.toLowerCase().replace(/\s+/g, '-');
|
||||
const behaviorPath = mbc.behavior.toLowerCase().replace(/\s+/g, '-');
|
||||
const objectivePath = mbc.objective.toLowerCase().replace(/\s+/g, '-')
|
||||
const behaviorPath = mbc.behavior.toLowerCase().replace(/\s+/g, '-')
|
||||
|
||||
// Construct the final URL
|
||||
return `${baseUrl}/${objectivePath}/${behaviorPath}.md`;
|
||||
return `${baseUrl}/${objectivePath}/${behaviorPath}.md`
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -317,33 +294,34 @@ function createMBCHref(mbc) {
|
||||
* @param {string} attack.id - The ID of the ATT&CK technique or sub-technique.
|
||||
* @returns {string} The formatted MITRE ATT&CK URL for the technique.
|
||||
*/
|
||||
function createATTACKHref(attack) {
|
||||
const baseUrl = 'https://attack.mitre.org/techniques/';
|
||||
const idParts = attack.id.split('.');
|
||||
function createATTACKHref(attack) {
|
||||
const baseUrl = 'https://attack.mitre.org/techniques/'
|
||||
const idParts = attack.id.split('.')
|
||||
|
||||
if (idParts.length === 1) {
|
||||
// It's a technique
|
||||
return `${baseUrl}${idParts[0]}`;
|
||||
return `${baseUrl}${idParts[0]}`
|
||||
} else if (idParts.length === 2) {
|
||||
// It's a sub-technique
|
||||
return `${baseUrl}${idParts[0]}/${idParts[1]}`;
|
||||
return `${baseUrl}${idParts[0]}/${idParts[1]}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Disable the toggle button for statement and features */
|
||||
:deep(.p-treetable-tbody > tr:not(:is([aria-level='1'], [aria-level='2'])) > td > div > .p-treetable-node-toggle-button) {
|
||||
visibility: hidden !important;
|
||||
height: 1.3rem;
|
||||
:deep(
|
||||
.p-treetable-tbody > tr:not(:is([aria-level='1'], [aria-level='2'])) > td > div > .p-treetable-node-toggle-button
|
||||
) {
|
||||
visibility: hidden !important;
|
||||
height: 1.3rem;
|
||||
}
|
||||
/* Disable the toggle button for rules */
|
||||
:deep(.p-treetable-tbody > tr:is([aria-level='1']) > td > div > .p-treetable-node-toggle-button) {
|
||||
visibility: collapse !important;
|
||||
height: 1.3rem;
|
||||
visibility: collapse !important;
|
||||
height: 1.3rem;
|
||||
}
|
||||
|
||||
/* Make all matches nodes (i.e. not rule names) slightly smaller,
|
||||
|
||||
@@ -1,40 +1,50 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="node.data.type === 'rule'">
|
||||
<div>
|
||||
<template v-if="node.data.type === 'rule'">
|
||||
{{ node.data.name }}
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'match location'">
|
||||
<span class="text-sm font-italic">{{ node.data.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'statement'"
|
||||
>-
|
||||
<span
|
||||
:class="{ 'text-green-700': node.data.typeValue === 'range', 'font-semibold': node.data.typeValue !== 'range' }"
|
||||
>
|
||||
{{ node.data.name }}
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'match location'">
|
||||
<span class="text-sm font-italic">{{ node.data.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'statement'">-
|
||||
<span :class="{ 'text-green-700': node.data.typeValue === 'range', 'font-semibold': node.data.typeValue !== 'range' }">
|
||||
{{ node.data.name }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'feature'">
|
||||
<span>- {{ node.data.typeValue }}: <span :class="{'text-green-700': node.data.typeValue !== 'regex'}" style="font-family: monospace;">{{ node.data.name }}</span></span>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'regex-capture'">
|
||||
- <span class="text-green-700" style="font-family: monospace;">{{ node.data.name }}</span>
|
||||
</template>
|
||||
<span v-if="node.data.description" class="text-gray-500 text-sm" style="font-size: 90%;">
|
||||
= {{ node.data.description }}
|
||||
</span>
|
||||
<span v-if="node.data.matchCount > 1" class="font-italic">
|
||||
({{ node.data.matchCount }} matches)
|
||||
</span>
|
||||
<LibraryTag v-if="node.data.lib && node.data.matchCount" />
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'feature'">
|
||||
<span
|
||||
>- {{ node.data.typeValue }}:
|
||||
<span :class="{ 'text-green-700': node.data.typeValue !== 'regex' }" style="font-family: monospace">{{
|
||||
node.data.name
|
||||
}}</span></span
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'regex-capture'">
|
||||
- <span class="text-green-700" style="font-family: monospace">{{ node.data.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'call-info'">
|
||||
<!-- <code class="text-gray-700">{{ node.data.name }}</code> -->
|
||||
<highlightjs lang="c" :code="node.data.name" />
|
||||
</template>
|
||||
<span v-if="node.data.description" class="text-gray-500 text-sm" style="font-size: 90%">
|
||||
= {{ node.data.description }}
|
||||
</span>
|
||||
<span v-if="node.data.matchCount > 1" class="font-italic"> ({{ node.data.matchCount }} matches) </span>
|
||||
<LibraryTag v-if="node.data.lib && node.data.matchCount" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from 'vue';
|
||||
import LibraryTag from '../misc/LibraryTag.vue';
|
||||
<script setup>
|
||||
import { defineProps } from 'vue'
|
||||
import LibraryTag from '../misc/LibraryTag.vue'
|
||||
|
||||
defineProps({
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
defineProps({
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @param {Object} rules - The rules object from the rodc JSON data
|
||||
* @returns {Array} - Parsed tree data for the TreeTable component
|
||||
*/
|
||||
export function parseRules(rules, flavor) {
|
||||
export function parseRules(rules, flavor, layout) {
|
||||
return Object.entries(rules).map(([ruleName, rule], index) => {
|
||||
const ruleNode = {
|
||||
key: index.toString(),
|
||||
@@ -34,7 +34,7 @@ export function parseRules(rules, flavor) {
|
||||
if (isFileScope) {
|
||||
// The scope for the rule is a file, so we don't need to show the match location address
|
||||
ruleNode.children = rule.matches.map((match, matchIndex) => {
|
||||
return parseNode(match[1], `${index}-${matchIndex}`, rules, rule.meta.lib)
|
||||
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
|
||||
@@ -56,7 +56,7 @@ export function parseRules(rules, flavor) {
|
||||
? `${formatHex(match[0].value)}`
|
||||
: formatDynamicAddress(match[0].value),
|
||||
},
|
||||
children: [parseNode(match[1], `${matchKey}`, rules, rule.meta.lib)]
|
||||
children: [parseNode(match[1], `${matchKey}`, rules, rule.meta.lib, layout)]
|
||||
}
|
||||
matchCounter++
|
||||
return matchNode
|
||||
@@ -245,7 +245,7 @@ export function parseProcessCapabilities(data, showLibraryRules) {
|
||||
* @param {boolean} lib - Whether this is a library rule
|
||||
* @returns {Object} - Parsed node data
|
||||
*/
|
||||
function parseNode(node, key, rules, lib) {
|
||||
function parseNode(node, key, rules, lib, layout) {
|
||||
if (!node) return null
|
||||
|
||||
const isNotStatement = node.node.statement && node.node.statement.type === 'not'
|
||||
@@ -275,7 +275,7 @@ function parseNode(node, key, rules, lib) {
|
||||
if (processedNode.children && Array.isArray(processedNode.children)) {
|
||||
result.children = processedNode.children
|
||||
.map((child) => {
|
||||
const childNode = parseNode(child, `${key}`, rules, lib)
|
||||
const childNode = parseNode(child, `${key}`, rules, lib, layout)
|
||||
return childNode
|
||||
})
|
||||
.filter((child) => child !== null)
|
||||
@@ -297,11 +297,190 @@ function parseNode(node, key, rules, lib) {
|
||||
|
||||
if (processedNode.node.feature && processedNode.node.feature.type === 'regex') {
|
||||
result.children = processRegexCaptures(processedNode, key);
|
||||
}
|
||||
}
|
||||
|
||||
// Add call information for dynamic sandbox traces
|
||||
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;
|
||||
|
||||
const [ppid, pid, tid, callId] = location.value;
|
||||
const callName = node.node.feature.api;
|
||||
|
||||
const pname = getProcessName(layout, location);
|
||||
const cname = getCallName(layout, location);
|
||||
|
||||
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 callInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 DynamicLayout 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 DynamicLayout object
|
||||
* @param {Object} address - The address object containing call information
|
||||
* @returns {string} The call name with arguments
|
||||
*/
|
||||
function getCallName(layout, address) {
|
||||
if (!layout || !layout.processes || !Array.isArray(layout.processes)) {
|
||||
console.error('Invalid layout structure');
|
||||
return 'Unknown Call';
|
||||
}
|
||||
|
||||
const [ppid, pid, tid, callId] = 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) {
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
function processRegexCaptures(node, key) {
|
||||
if (!node.captures) return [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user