mirror of
https://github.com/mandiant/capa.git
synced 2025-12-05 20:40:05 -08:00
highlight links, use monospace for feature values
This commit is contained in:
@@ -9,6 +9,11 @@ body {
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* Disable the toggle button for statement and features */
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
</h1>
|
||||
<p class="text-xs mt-1 mb-0 text-white-alpha-70">SHA256:
|
||||
<a :href="`https://www.virustotal.com/gui/file/${sha256}`" target="_blank">{{ sha256 }}
|
||||
<i class="pi pi-external-link ml-1 text-xs"></i>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -58,46 +58,8 @@
|
||||
placeholder="Filter by Rule or Feature"
|
||||
/>
|
||||
</template>
|
||||
<template #body="slotProps">
|
||||
<div
|
||||
:style="{
|
||||
color:
|
||||
!slotProps.node.children || slotProps.node.children.length === 0
|
||||
? 'green'
|
||||
: 'black',
|
||||
fontWeight:
|
||||
slotProps.node.children &&
|
||||
slotProps.node.children.length > 0 &&
|
||||
slotProps.node.key.includes('-') &&
|
||||
!slotProps.node.data.isMatchLocation
|
||||
? 'bold'
|
||||
: 'normal'
|
||||
}"
|
||||
>
|
||||
{{ slotProps.node.data.name }}
|
||||
<span
|
||||
v-if="slotProps.node.data.description"
|
||||
style="font-style: none; font-size: 90%; font-weight: normal; color: grey"
|
||||
>
|
||||
{{ ' ' + slotProps.node.data.description }}
|
||||
</span>
|
||||
<span v-if="slotProps.node.data.matchCount > 1" style="font-style: italic">{{
|
||||
`(${slotProps.node.data.matchCount} matches)`
|
||||
}}</span>
|
||||
|
||||
<Tag
|
||||
v-if="slotProps.node.data.lib && slotProps.node.data.matchCount"
|
||||
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>
|
||||
<template #body="{ node }">
|
||||
<RuleColumn :node="node" />
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
@@ -130,7 +92,7 @@
|
||||
<div v-if="slotProps.node.data.attack">
|
||||
<div v-for="(attack, index) in slotProps.node.data.attack" :key="index">
|
||||
<a :href="createATTACKHref(attack)" target="_blank">
|
||||
{{ attack.technique }} ({{ attack.id }})
|
||||
{{ attack.technique }} <span class="text-500 text-sm font-normal ml-1">({{ attack.id }})</span>
|
||||
</a>
|
||||
<div
|
||||
v-for="(technique, techIndex) in attack.techniques"
|
||||
@@ -138,7 +100,7 @@
|
||||
style="font-size: 0.8em; margin-left: 1em"
|
||||
>
|
||||
<a :href="createATTACKHref(technique)" target="_blank">
|
||||
↳ {{ technique.technique }} ({{ technique.id }})
|
||||
↳ {{ technique.technique }} <span class="text-500 text-xs font-normal ml-1">({{ technique.id }})</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,7 +112,7 @@
|
||||
<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">
|
||||
{{ formatMBC(mbc) }}
|
||||
{{ mbc.parts.join('::') }} <span class="text-500 text-sm font-normal opacity-80 ml-1">[{{ mbc.id }}]</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -188,7 +150,6 @@
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import TreeTable from 'primevue/treetable'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Tag from 'primevue/tag'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Column from 'primevue/column'
|
||||
import Button from 'primevue/button'
|
||||
@@ -196,6 +157,8 @@ import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import MultiSelect from 'primevue/multiselect'
|
||||
|
||||
import RuleColumn from './columns/RuleColumn.vue';
|
||||
|
||||
import { parseRules } from '../utils/rdocParser'
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
39
webui/src/components/columns/RuleColumn.vue
Normal file
39
webui/src/components/columns/RuleColumn.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="node.data.type === 'rule'">
|
||||
{{ node.data.name }}
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'match location'">
|
||||
<span class="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 v-if="node.data.typeValue === 'number' || node.data.typeValue === 'mnemonic' || node.data.typeValue === 'bytes' || node.data.typeValue === 'api' || node.data.typeValue === 'offset' || node.data.typeValue === 'operand offset'">- {{ node.data.typeValue }}: <span class="text-green-700" style="font-family: monospace;">{{ node.data.name }}</span></span>-->
|
||||
<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%;">
|
||||
= {{ 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';
|
||||
|
||||
defineProps({
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
13
webui/src/components/misc/LibraryTag.vue
Normal file
13
webui/src/components/misc/LibraryTag.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<Tag
|
||||
class="ml-2"
|
||||
style="scale: 0.8"
|
||||
value="lib"
|
||||
severity="info"
|
||||
v-tooltip.right="'Library rules capture common logic'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Tag from 'primevue/tag';
|
||||
</script>
|
||||
@@ -8,6 +8,7 @@ export function parseRules(rules, flavor) {
|
||||
const ruleNode = {
|
||||
key: index.toString(),
|
||||
data: {
|
||||
type: 'rule',
|
||||
name: rule.meta.name,
|
||||
lib: rule.meta.lib,
|
||||
matchCount: rule.matches.length,
|
||||
@@ -45,15 +46,15 @@ export function parseRules(rules, flavor) {
|
||||
const matchNode = {
|
||||
key: matchKey,
|
||||
data: {
|
||||
type: 'match location',
|
||||
name:
|
||||
flavor === 'static'
|
||||
? `${rule.meta.scopes.static} @ ${formatAddress(match[0].value)}`
|
||||
? `${rule.meta.scopes.static} @ ${formatStaticAddress(match[0].value)}`
|
||||
: `${formatDynamicAddress(match[0].value)}`,
|
||||
address:
|
||||
flavor === 'static'
|
||||
? `${formatAddress(match[0].value)}`
|
||||
? `${formatStaticAddress(match[0].value)}`
|
||||
: formatDynamicAddress(match[0].value),
|
||||
isMatchLocation: true
|
||||
},
|
||||
children: [parseNode(match[1], `${matchKey}-0`, rules, rule.meta.lib)]
|
||||
}
|
||||
@@ -101,11 +102,11 @@ export function parseFunctionCapabilities(data, showLibraryRules) {
|
||||
matchingRules.push({
|
||||
key: `${functionAddress}-${matchingRules.length}`, // Unique key for each rule
|
||||
data: {
|
||||
funcaddr: `${rule.meta.name}`, // Display rule name
|
||||
lib: rule.meta.lib, // Indicate if it's a library rule
|
||||
matchcount: null, // Matchcount is not used for individual rules
|
||||
namespace: rule.meta.namespace, // Rule namespace
|
||||
source: rule.source // Rule source code
|
||||
funcaddr: `${rule.meta.name}`,
|
||||
lib: rule.meta.lib,
|
||||
matchcount: null,
|
||||
namespace: rule.meta.namespace,
|
||||
source: rule.source
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -116,7 +117,7 @@ export function parseFunctionCapabilities(data, showLibraryRules) {
|
||||
result.push({
|
||||
key: functionAddress, // Use function address as key
|
||||
data: {
|
||||
funcaddr: `function: 0x${functionAddress}`, // Display function address
|
||||
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
|
||||
@@ -228,6 +229,8 @@ function parseNode(node, key, rules, lib) {
|
||||
const result = {
|
||||
key: key,
|
||||
data: {
|
||||
type: processedNode.node.type, // statement or feature
|
||||
typeValue: processedNode.node.statement ? processedNode.node.statement.type : processedNode.node.feature.type, // type value (eg. number, regex, api, or, and, optional ... etc)
|
||||
success: processedNode.success,
|
||||
name: getNodeName(processedNode),
|
||||
lib: lib,
|
||||
@@ -333,7 +336,8 @@ function getStatementName(statement) {
|
||||
case 'some':
|
||||
return `${statement.count} or more`
|
||||
default:
|
||||
return `${statement.type}: `
|
||||
// statement (e.g. "and: ", "or: ", "optional:", ... etc)
|
||||
return `${statement.type}:`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,11 +351,14 @@ function getFeatureName(feature) {
|
||||
case 'number':
|
||||
case 'offset':
|
||||
// example: "number: 0x1234", "offset: 0x3C"
|
||||
return `${feature.type}: 0x${feature[feature.type].toString(16).toUpperCase()}`
|
||||
// return `${feature.type}: 0x${feature[feature.type].toString(16).toUpperCase()}`
|
||||
return `0x${feature[feature.type].toString(16).toUpperCase()}`
|
||||
case 'bytes':
|
||||
return formatBytes(feature.bytes)
|
||||
case 'operand offset':
|
||||
return `operand[${feature.index}].offset: 0x${feature.operand_offset.toString(16).toUpperCase()}`
|
||||
default:
|
||||
return `${feature.type}: ${feature[feature.type]}`
|
||||
return `${feature[feature.type]}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,6 +398,19 @@ function getNodeAddress(node) {
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats bytes string for display
|
||||
* @param {Array} value - The bytes string
|
||||
* @returns {string} - Formatted bytes string
|
||||
*/
|
||||
|
||||
function formatBytes(byteString) {
|
||||
// Use a regular expression to insert a space after every two characters
|
||||
const formattedString = byteString.replace(/(.{2})/g, '$1 ').trim();
|
||||
// convert to uppercase
|
||||
return formattedString.toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the address for dynamic flavor
|
||||
* @param {Array} value - The address value array
|
||||
@@ -404,6 +424,6 @@ function formatDynamicAddress(value) {
|
||||
.join(' ← ')
|
||||
}
|
||||
|
||||
function formatAddress(address) {
|
||||
function formatStaticAddress(address) {
|
||||
return `0x${address.toString(16).toUpperCase()}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user