implement unit test

This commit is contained in:
Soufiane Fariss
2024-08-02 01:26:36 +02:00
parent 4137923c2e
commit 07b4e1f8a2
4 changed files with 314 additions and 92 deletions

View File

@@ -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>

View 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')
})
})

View File

@@ -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) {

View File

@@ -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}'],
}
})