mirror of
https://github.com/mandiant/capa.git
synced 2026-07-09 13:49:15 -07:00
reformat function capabilities into a rowspan table instead of tree table
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
@@ -1,91 +1,72 @@
|
||||
<template>
|
||||
<TreeTable
|
||||
:value="treeData"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
size="small"
|
||||
:filters="filters"
|
||||
:filterMode="filterMode.value"
|
||||
sortField="funcaddr"
|
||||
:sortOrder="1"
|
||||
removableSort
|
||||
:indentation="1.3"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-content-between align-items-center mb-4 mx-2">
|
||||
<Button icon="pi pi-expand" @click="toggleAll" label="Toggle All" class="mr-3" />
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="filters['global']" placeholder="Global search" />
|
||||
</IconField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="funcaddr" sortable header="Function Address" expander filterMatchMode="contains">
|
||||
<template #filter>
|
||||
<InputText
|
||||
style="width: 70%"
|
||||
v-model="filters['funcaddr']"
|
||||
type="text"
|
||||
placeholder="Filter by function address"
|
||||
/>
|
||||
<div>
|
||||
<DataTable
|
||||
:value="tableData"
|
||||
rowGroupMode="rowspan"
|
||||
groupRowsBy="funcaddr"
|
||||
sortMode="single"
|
||||
removableSort
|
||||
size="small"
|
||||
:sortOrder="1"
|
||||
:filters="filters"
|
||||
:rowHover="true"
|
||||
:filterMode="filterMode"
|
||||
filterDisplay="menu"
|
||||
:globalFilterFields="['funcaddr', 'ruleName', 'namespace']"
|
||||
>
|
||||
<template #header>
|
||||
<InputText v-model="filters['global'].value" placeholder="Global Search" />
|
||||
</template>
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.node.data.funcaddr }}
|
||||
<span v-if="slotProps.node.data.matchcount > 1" class="font-italic"
|
||||
>({{ slotProps.node.data.matchcount }} matches)</span
|
||||
>
|
||||
<Tag
|
||||
v-if="slotProps.node.data.lib"
|
||||
class="ml-2"
|
||||
style="scale: 0.8"
|
||||
v-tooltip.top="{
|
||||
value: 'Library rules capture common logic',
|
||||
showDelay: 100,
|
||||
hideDelay: 100
|
||||
}"
|
||||
value="lib"
|
||||
severity="info"
|
||||
></Tag>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="namespace" sortable header="Namespace" filterMatchMode="contains">
|
||||
<template #filter>
|
||||
<InputText v-model="filters['namespace']" type="text" placeholder="Filter by namespace" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="funcaddr" sortable header="Function Address" :rowspan="3" class="w-min">
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.funcaddr }}
|
||||
<span v-if="slotProps.data.matchcount > 1" class="font-italic">
|
||||
({{ slotProps.data.matchcount }} matches)
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="source" header="Source">
|
||||
<template #body="slotProps">
|
||||
<Button
|
||||
v-if="slotProps.node.data.source"
|
||||
rounded
|
||||
icon="pi pi-external-link"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
style="height: 1.5rem; width: 1.5rem"
|
||||
@click="showSource(slotProps.node.data.source)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</TreeTable>
|
||||
<Column field="ruleName" sortable header="Matches" class="w-min">
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.ruleName }}
|
||||
<LibraryTag
|
||||
v-if="slotProps.data.lib"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Dialog v-model:visible="sourceDialogVisible" :style="{ width: '50vw' }">
|
||||
<highlightjs lang="yml" :code="currentSource" />
|
||||
</Dialog>
|
||||
<Column field="namespace" sortable header="Namespace"></Column>
|
||||
|
||||
<Column field="source" header="Source">
|
||||
<template #body="slotProps">
|
||||
<Button
|
||||
v-if="slotProps.data.source"
|
||||
rounded
|
||||
icon="pi pi-external-link"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
style="height: 1.5rem; width: 1.5rem"
|
||||
@click="showSource(slotProps.data.source)"
|
||||
/>
|
||||
</template>
|
||||
</Column>
|
||||
</DataTable>
|
||||
|
||||
<Dialog v-model:visible="sourceDialogVisible" :style="{ width: '50vw' }">
|
||||
<highlightjs lang="yml" :code="currentSource" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import TreeTable from 'primevue/treetable'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Button from 'primevue/button'
|
||||
import Badge from 'primevue/badge'
|
||||
import Tag from 'primevue/tag'
|
||||
import LibraryTag from './misc/LibraryTag.vue'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@@ -98,35 +79,26 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
const filters = ref({})
|
||||
const filterMode = ref({ value: 'lenient' })
|
||||
const filters = ref({
|
||||
'global': { value: null, matchMode: 'contains' },
|
||||
})
|
||||
const filterMode = ref('lenient')
|
||||
const sourceDialogVisible = ref(false)
|
||||
const currentSource = ref('')
|
||||
const expandedKeys = ref({})
|
||||
|
||||
const showSource = (source) => {
|
||||
currentSource.value = source
|
||||
sourceDialogVisible.value = true
|
||||
}
|
||||
|
||||
const toggleAll = () => {
|
||||
let _expandedKeys = {}
|
||||
|
||||
if (Object.keys(expandedKeys.value).length === 0) {
|
||||
const expandAll = (node) => {
|
||||
if (node.children && node.children.length) {
|
||||
_expandedKeys[node.key] = true
|
||||
node.children.forEach(expandAll)
|
||||
}
|
||||
}
|
||||
|
||||
treeData.value.forEach(expandAll)
|
||||
}
|
||||
|
||||
expandedKeys.value = _expandedKeys
|
||||
}
|
||||
|
||||
import { parseFunctionCapabilities } from '../utils/rdocParser'
|
||||
|
||||
const treeData = computed(() => parseFunctionCapabilities(props.data, props.showLibraryRules))
|
||||
const tableData = computed(() => parseFunctionCapabilities(props.data, props.showLibraryRules))
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* tighten up the spacing between rows */
|
||||
:deep(.p-datatable.p-datatable-sm .p-datatable-tbody > tr > td) {
|
||||
padding: 0.1rem 0.5rem !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -150,7 +150,8 @@ const processTree = computed(() => {
|
||||
})
|
||||
// build the final tree structure
|
||||
const rootProcesses = []
|
||||
processMap.forEach((processNode, pid) => {
|
||||
processMap.forEach((processNode) => {
|
||||
console.log(processNode)
|
||||
processNode.data.uniqueRules = Array.from(processNode.data.uniqueRules.values())
|
||||
const parentProcess = processMap.get(processNode.data.ppid)
|
||||
if (parentProcess) {
|
||||
|
||||
@@ -301,25 +301,21 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
/**
|
||||
* Formats an MBC (Malware Behavior Catalog) object into a string representation.
|
||||
* Creates an MBC (Malware Behavior Catalog) URL from an MBC object.
|
||||
*
|
||||
* @param {Object} mbc - The MBC object to format.
|
||||
* @returns {string} A string representation of the MBC object.
|
||||
*
|
||||
* e.g. "Anti-Behavioral Analysis::Virtual Machine Detection::Human User Check [B0009.012]"
|
||||
* @returns {string} The MBC URL.
|
||||
*/
|
||||
|
||||
const formatMBC = (mbc) => {
|
||||
return `${mbc.parts.join('::')} [${mbc.id}]`
|
||||
}
|
||||
|
||||
function createMBCHref(mbc) {
|
||||
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';
|
||||
} else if (mbc.id.startsWith('C')) {
|
||||
// Micro-Behavior
|
||||
baseUrl = 'https://github.com/MBCProject/mbc-markdown/blob/main/micro-behaviors';
|
||||
} else {
|
||||
return null
|
||||
@@ -345,7 +341,7 @@ function createMBCHref(mbc) {
|
||||
const idParts = attack.id.split('.');
|
||||
|
||||
if (idParts.length === 1) {
|
||||
// It's a main technique
|
||||
// It's a technique
|
||||
return `${baseUrl}${idParts[0]}`;
|
||||
} else if (idParts.length === 2) {
|
||||
// It's a sub-technique
|
||||
@@ -366,7 +362,7 @@ function createMBCHref(mbc) {
|
||||
|
||||
/* Make all matches nodes (i.e. not rule names) slightly smaller */
|
||||
.p-treetable-tbody > tr:not(:is([aria-level='1'])) > td {
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Optional: Add a subtle background to root-level rows for better distinction */
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<span>- {{ node.data.typeValue }}: <span class="text-green-700" style="font-family: monospace;">{{ node.data.name }}</span></span>
|
||||
|
||||
</template>
|
||||
<span v-if="node.data.description" class="text-gray-500" style="font-size: 90%;">
|
||||
<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">
|
||||
|
||||
@@ -71,64 +71,59 @@ export function parseRules(rules, flavor) {
|
||||
* Parses rules data for the CapasByFunction component
|
||||
* @param {Object} data - The full JSON data object containing analysis results
|
||||
* @param {boolean} showLibraryRules - Whether to include library rules in the output
|
||||
* @returns {Array} - Parsed tree data for the CapasByFunction component
|
||||
* @returns {Array} - Parsed data for the CapasByFunction DataTable component
|
||||
*/
|
||||
export function parseFunctionCapabilities(data, showLibraryRules) {
|
||||
const result = []
|
||||
const result = [];
|
||||
let id = 0;
|
||||
|
||||
// Iterate through each function in the metadata
|
||||
for (const functionInfo of data.meta.analysis.layout.functions) {
|
||||
// Convert function address to uppercase hexadecimal string
|
||||
const functionAddress = functionInfo.address.value.toString(16).toUpperCase()
|
||||
const matchingRules = []
|
||||
const functionAddress = functionInfo.address.value.toString(16).toUpperCase();
|
||||
const matchingRules = [];
|
||||
|
||||
// Iterate through all rules in the data
|
||||
for (const ruleId in data.rules) {
|
||||
const rule = data.rules[ruleId]
|
||||
const rule = data.rules[ruleId];
|
||||
|
||||
// Skip library rules if showLibraryRules is false
|
||||
if (!showLibraryRules && rule.meta.lib) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find matches for this rule within the current function
|
||||
const matches = rule.matches.filter((match) =>
|
||||
// Check if any of the function's basic blocks match the rule
|
||||
functionInfo.matched_basic_blocks.some((block) => block.address.value === match[0].value)
|
||||
)
|
||||
);
|
||||
|
||||
// If there are matches, add this rule to the matchingRules array
|
||||
if (matches.length > 0) {
|
||||
matchingRules.push({
|
||||
key: `${functionAddress}-${matchingRules.length}`, // Unique key for each rule
|
||||
data: {
|
||||
funcaddr: `${rule.meta.name}`,
|
||||
lib: rule.meta.lib,
|
||||
matchcount: null,
|
||||
namespace: rule.meta.namespace,
|
||||
source: rule.source
|
||||
}
|
||||
})
|
||||
ruleName: rule.meta.name,
|
||||
lib: rule.meta.lib,
|
||||
namespace: rule.meta.namespace,
|
||||
source: rule.source
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If there are matching rules for this function, add it to the result
|
||||
if (matchingRules.length > 0) {
|
||||
result.push({
|
||||
key: functionAddress, // Use function address as key
|
||||
data: {
|
||||
funcaddr: `function: 0x${functionAddress}`,
|
||||
lib: false, // Functions are not library rules
|
||||
matchcount: matchingRules.length, // Number of matching rules for this function
|
||||
namespace: null, // Functions don't have a namespace
|
||||
source: null // Functions don't have source code in this context
|
||||
},
|
||||
children: matchingRules // Add matching rules as children
|
||||
})
|
||||
// Add each matching rule as a separate row
|
||||
matchingRules.forEach(rule => {
|
||||
result.push({
|
||||
id: id++,
|
||||
funcaddr: `0x${functionAddress}`,
|
||||
matchcount: matchingRules.length,
|
||||
...rule
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user