From 07b4e1f8a2e73bd721ed57766ae255fb1b012bee Mon Sep 17 00:00:00 2001 From: Soufiane Fariss Date: Fri, 2 Aug 2024 01:26:36 +0200 Subject: [PATCH] implement unit test --- webui/src/components/RuleMatchesTable.vue | 2 +- webui/src/tests/rdocParser.test.js | 299 ++++++++++++++++++++++ webui/src/utils/rdocParser.js | 80 +----- webui/vitest.config.js | 25 +- 4 files changed, 314 insertions(+), 92 deletions(-) create mode 100644 webui/src/tests/rdocParser.test.js diff --git a/webui/src/components/RuleMatchesTable.vue b/webui/src/components/RuleMatchesTable.vue index 9f82039a..514b3ab0 100644 --- a/webui/src/components/RuleMatchesTable.vue +++ b/webui/src/components/RuleMatchesTable.vue @@ -55,7 +55,7 @@ diff --git a/webui/src/tests/rdocParser.test.js b/webui/src/tests/rdocParser.test.js new file mode 100644 index 00000000..51fa9e21 --- /dev/null +++ b/webui/src/tests/rdocParser.test.js @@ -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') + }) +}) diff --git a/webui/src/utils/rdocParser.js b/webui/src/utils/rdocParser.js index f6352d63..51ef6414 100644 --- a/webui/src/utils/rdocParser.js +++ b/webui/src/utils/rdocParser.js @@ -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) { diff --git a/webui/vitest.config.js b/webui/vitest.config.js index b1083123..abe5bba0 100644 --- a/webui/vitest.config.js +++ b/webui/vitest.config.js @@ -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}'], + } +}) \ No newline at end of file