mirror of
https://github.com/mandiant/capa.git
synced 2026-02-04 11:07:53 -08:00
implement unit test
This commit is contained in:
@@ -55,7 +55,7 @@
|
||||
<!-- Address column body template -->
|
||||
<template v-if="col.field === 'address'" #body="slotProps">
|
||||
<span class="text-sm" style="font-family: monospace">
|
||||
{{ slotProps.node.data.type === 'match location' ? '' : slotProps.node.data.address }}
|
||||
{{ slotProps.node.data.address }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
299
webui/src/tests/rdocParser.test.js
Normal file
299
webui/src/tests/rdocParser.test.js
Normal file
@@ -0,0 +1,299 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { parseRules, parseFunctionCapabilities, parseProcessCapabilities } from '../utils/rdocParser'
|
||||
|
||||
describe('parseRules', () => {
|
||||
it('should return an empty array for empty rules', () => {
|
||||
const rules = {}
|
||||
const flavor = 'static'
|
||||
const layout = {}
|
||||
const result = parseRules(rules, flavor, layout)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should correctly parse a simple rule with static scope', () => {
|
||||
const rules = {
|
||||
'test rule': {
|
||||
meta: {
|
||||
name: 'test rule',
|
||||
namespace: 'test',
|
||||
lib: false,
|
||||
scopes: {
|
||||
static: 'function',
|
||||
dynamic: 'process'
|
||||
}
|
||||
},
|
||||
source: 'test rule source',
|
||||
matches: [
|
||||
[
|
||||
{ type: 'absolute', value: 0x1000 },
|
||||
{
|
||||
success: true,
|
||||
node: { type: 'feature', feature: { type: 'api', api: 'TestAPI' } },
|
||||
children: [],
|
||||
locations: [{ type: 'absolute', value: 0x1000 }],
|
||||
captures: {}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
const result = parseRules(rules, 'static', {})
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].key).toBe('0')
|
||||
expect(result[0].data.type).toBe('rule')
|
||||
expect(result[0].data.name).toBe('test rule')
|
||||
expect(result[0].data.lib).toBe(false)
|
||||
expect(result[0].data.namespace).toBe('test')
|
||||
expect(result[0].data.source).toBe('test rule source')
|
||||
expect(result[0].children).toHaveLength(1)
|
||||
expect(result[0].children[0].key).toBe('0-0')
|
||||
expect(result[0].children[0].data.type).toBe('match location')
|
||||
expect(result[0].children[0].children[0].data.type).toBe('feature')
|
||||
expect(result[0].children[0].children[0].data.typeValue).toBe('api')
|
||||
expect(result[0].children[0].children[0].data.name).toBe('TestAPI')
|
||||
})
|
||||
|
||||
it('should handle rule with "not" statements correctly', () => {
|
||||
const rules = {
|
||||
'test rule': {
|
||||
meta: {
|
||||
name: 'test rule',
|
||||
namespace: 'test',
|
||||
lib: false,
|
||||
scopes: {
|
||||
static: 'function',
|
||||
dynamic: 'process'
|
||||
}
|
||||
},
|
||||
source: 'test rule source',
|
||||
matches: [
|
||||
[
|
||||
{ type: 'absolute', value: 0x1000 },
|
||||
{
|
||||
success: true,
|
||||
node: { type: 'statement', statement: { type: 'not' } },
|
||||
children: [{ success: false, node: { type: 'feature', feature: { type: 'api', api: 'TestAPI' } } }]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
const result = parseRules(rules, 'static', {})
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].children[0].children[0].data.type).toBe('statement')
|
||||
expect(result[0].children[0].children[0].data.name).toBe('not:')
|
||||
expect(result[0].children[0].children[0].children[0].data.type).toBe('feature')
|
||||
expect(result[0].children[0].children[0].children[0].data.typeValue).toBe('api')
|
||||
expect(result[0].children[0].children[0].children[0].data.name).toBe('TestAPI')
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseFunctionCapabilities', () => {
|
||||
it('should return an empty array when no functions match', () => {
|
||||
const mockData = {
|
||||
meta: {
|
||||
analysis: {
|
||||
layout: {
|
||||
functions: []
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {}
|
||||
}
|
||||
const result = parseFunctionCapabilities(mockData, false)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('should parse a single function with one rule match', () => {
|
||||
const mockData = {
|
||||
meta: {
|
||||
analysis: {
|
||||
layout: {
|
||||
functions: [
|
||||
{
|
||||
address: {
|
||||
type: 'absolute',
|
||||
value: 0x1000
|
||||
},
|
||||
matched_basic_blocks: [
|
||||
{
|
||||
address: {
|
||||
type: 'absolute',
|
||||
value: 0x1000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
rule1: {
|
||||
meta: {
|
||||
name: 'Test Rule',
|
||||
namespace: 'test',
|
||||
lib: false,
|
||||
scopes: { static: 'function' }
|
||||
},
|
||||
matches: [[{ value: 0x1000 }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = parseFunctionCapabilities(mockData, false)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0]).toEqual({
|
||||
funcaddr: '0x1000',
|
||||
matchCount: 1,
|
||||
ruleName: 'Test Rule',
|
||||
ruleMatchCount: 1,
|
||||
namespace: 'test',
|
||||
lib: false
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle multiple rules matching a single function', () => {
|
||||
const mockData = {
|
||||
meta: {
|
||||
analysis: {
|
||||
layout: {
|
||||
functions: [
|
||||
{
|
||||
address: {
|
||||
type: 'absolute',
|
||||
value: 0x1000
|
||||
},
|
||||
matched_basic_blocks: []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
rule1: {
|
||||
meta: {
|
||||
name: 'Rule 1',
|
||||
namespace: 'test1',
|
||||
lib: false,
|
||||
scopes: { static: 'function' }
|
||||
},
|
||||
matches: [[{ value: 0x1000 }]]
|
||||
},
|
||||
rule2: {
|
||||
meta: {
|
||||
name: 'Rule 2',
|
||||
namespace: 'test2',
|
||||
lib: false,
|
||||
scopes: { static: 'function' }
|
||||
},
|
||||
matches: [[{ value: 0x1000 }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = parseFunctionCapabilities(mockData, false)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].funcaddr).toBe('0x1000')
|
||||
expect(result[1].funcaddr).toBe('0x1000')
|
||||
expect(result.map((r) => r.ruleName)).toEqual(['Rule 1', 'Rule 2'])
|
||||
})
|
||||
|
||||
it('should handle library rules correctly', () => {
|
||||
const mockData = {
|
||||
meta: {
|
||||
analysis: {
|
||||
layout: {
|
||||
functions: [
|
||||
{
|
||||
address: { type: 'absolute', value: 0x1000 },
|
||||
matched_basic_blocks: []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
libRule: {
|
||||
meta: {
|
||||
name: 'Lib Rule',
|
||||
namespace: 'lib',
|
||||
lib: true,
|
||||
scopes: { static: 'function' }
|
||||
},
|
||||
matches: [[{ value: 0x1000 }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
const resultWithLib = parseFunctionCapabilities(mockData, true)
|
||||
expect(resultWithLib).toHaveLength(1)
|
||||
expect(resultWithLib[0].lib).toBe(true)
|
||||
|
||||
const resultWithoutLib = parseFunctionCapabilities(mockData, false)
|
||||
expect(resultWithoutLib).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should handle a single rule matching in multiple functions', () => {
|
||||
const mockData = {
|
||||
meta: {
|
||||
analysis: {
|
||||
layout: {
|
||||
functions: [
|
||||
{ address: { value: 0x1000 }, matched_basic_blocks: [] },
|
||||
{ address: { value: 0x2000 }, matched_basic_blocks: [] }
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
rule1: {
|
||||
meta: {
|
||||
name: 'Multi-function Rule',
|
||||
namespace: 'test',
|
||||
lib: false,
|
||||
scopes: { static: 'function' }
|
||||
},
|
||||
matches: [[{ value: 0x1000 }], [{ value: 0x2000 }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = parseFunctionCapabilities(mockData, false)
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result[0].funcaddr).toBe('0x1000')
|
||||
expect(result[0].ruleName).toBe('Multi-function Rule')
|
||||
expect(result[0].ruleMatchCount).toBe(1)
|
||||
expect(result[1].funcaddr).toBe('0x2000')
|
||||
expect(result[1].ruleName).toBe('Multi-function Rule')
|
||||
expect(result[1].ruleMatchCount).toBe(1)
|
||||
})
|
||||
|
||||
it('should handle basic block scoped rules', () => {
|
||||
const mockData = {
|
||||
meta: {
|
||||
analysis: {
|
||||
layout: {
|
||||
functions: [
|
||||
{
|
||||
address: { value: 0x1000 },
|
||||
matched_basic_blocks: [{ address: { value: 0x1010 } }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
bbRule: {
|
||||
meta: {
|
||||
name: 'Basic Block Rule',
|
||||
namespace: 'test',
|
||||
lib: false,
|
||||
scopes: { static: 'basic block' }
|
||||
},
|
||||
matches: [[{ value: 0x1010 }]]
|
||||
}
|
||||
}
|
||||
}
|
||||
const result = parseFunctionCapabilities(mockData, false)
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].funcaddr).toBe('0x1000')
|
||||
expect(result[0].ruleName).toBe('Basic Block Rule')
|
||||
})
|
||||
})
|
||||
@@ -52,9 +52,8 @@ export function parseRules(rules, flavor, layout, maxMatches = 1) {
|
||||
type: 'match location',
|
||||
name:
|
||||
flavor === 'static'
|
||||
? `${rule.meta.scopes.static} @ ${formatHex(match[0].value)}`
|
||||
: `${formatDynamicAddress(match[0].value)}`,
|
||||
address: flavor === 'static' ? `${formatHex(match[0].value)}` : formatDynamicAddress(match[0].value)
|
||||
? `${rule.meta.scopes.static} @ ` + formatAddress(match[0])
|
||||
: getProcessName(layout, match[0])
|
||||
},
|
||||
children: [parseNode(match[1], `${matchKey}`, rules, rule.meta.lib, layout)]
|
||||
}
|
||||
@@ -172,79 +171,6 @@ export function parseFunctionCapabilities(data, showLibraryRules) {
|
||||
return finalResult
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses rules data for the CapasByProcess 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 CapasByProcess component
|
||||
*/
|
||||
export function parseProcessCapabilities(data, showLibraryRules) {
|
||||
const result = []
|
||||
const processes = data.meta.analysis.layout.processes
|
||||
|
||||
let processKey = 1
|
||||
|
||||
// Iterate through each process in the rdoc
|
||||
for (const processInfo of processes) {
|
||||
const processName = processInfo.name
|
||||
const matchingRules = []
|
||||
|
||||
// Iterate through all rules in the data
|
||||
for (const ruleId in data.rules) {
|
||||
const rule = data.rules[ruleId]
|
||||
|
||||
// Skip library rules if showLibraryRules is false
|
||||
if (!showLibraryRules && rule.meta.lib) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if the rule's scope is 'process'
|
||||
if (rule.meta.scopes.dynamic === 'process') {
|
||||
// Find matches for this rule within the current process
|
||||
const matches = rule.matches.filter(
|
||||
(match) =>
|
||||
match[0].type === 'process' &&
|
||||
// Ensure all addresses in the match are included in the process's address
|
||||
match[0].value.every((addr) => processInfo.address.value.includes(addr))
|
||||
)
|
||||
|
||||
// If there are matches, add this rule to the matchingRules array
|
||||
if (matches.length > 0) {
|
||||
matchingRules.push({
|
||||
key: `${processName}-${matchingRules.length}`, // Unique key for each rule
|
||||
data: {
|
||||
processname: `rule: ${rule.meta.name}`, // Display rule name
|
||||
type: 'rule',
|
||||
matchcount: null, // Matchcount is not relevant here
|
||||
namespace: rule.meta.namespace,
|
||||
procID: processInfo.address.value.join(', '), // PID, PPID
|
||||
source: rule.source
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there are matching rules for this process, add it to the result
|
||||
if (matchingRules.length > 0) {
|
||||
result.push({
|
||||
key: `process-${processKey++}`, // Unique key for each process
|
||||
data: {
|
||||
processname: processName, // Process name
|
||||
type: 'process',
|
||||
matchcount: matchingRules.length, // Number of matching rules for this process
|
||||
namespace: null, // Processes don't have a namespace
|
||||
procID: processInfo.address.value.join(', '), // PID, PPID
|
||||
source: null // Processes don't have source code in this context
|
||||
},
|
||||
children: matchingRules // Add matching rules as children
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
/**
|
||||
@@ -690,7 +616,7 @@ function formatDynamicAddress(value) {
|
||||
return value
|
||||
.map((item, index) => `${parts[index]}:${item}`)
|
||||
.reverse()
|
||||
.join(' ← ')
|
||||
.join(',')
|
||||
}
|
||||
|
||||
function formatHex(address) {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
base: '/capa/',
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||
root: fileURLToPath(new URL('./', import.meta.url))
|
||||
}
|
||||
})
|
||||
)
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
exclude: ['node_modules', 'dist', '.idea', '.git', '.cache'],
|
||||
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user