Skip to content

Commit

Permalink
reformat function capabilities into a rowspan table instead of tree t…
Browse files Browse the repository at this point in the history
…able
  • Loading branch information
fariss committed Jul 26, 2024
1 parent 4aad53c commit d25c86c
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 137 deletions.
2 changes: 1 addition & 1 deletion webui/.prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"semi": false,
"tabWidth": 2,
"singleQuote": true,
"printWidth": 100,
"printWidth": 120,
"trailingComma": "none"
}
164 changes: 68 additions & 96 deletions webui/src/components/FunctionCapabilities.vue
Original file line number Diff line number Diff line change
@@ -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"
/>
</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>
<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>
</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: {
Expand All @@ -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>
3 changes: 2 additions & 1 deletion webui/src/components/ProcessCapabilities.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 6 additions & 10 deletions webui/src/components/RuleMatchesTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 */
Expand Down
2 changes: 1 addition & 1 deletion webui/src/components/columns/RuleColumn.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
51 changes: 23 additions & 28 deletions webui/src/utils/rdocParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down

0 comments on commit d25c86c

Please sign in to comment.