diff --git a/webui/.eslintrc.cjs b/webui/.eslintrc.cjs deleted file mode 100644 index 5ee7e2ac..00000000 --- a/webui/.eslintrc.cjs +++ /dev/null @@ -1,13 +0,0 @@ -/* eslint-env node */ -require('@rushstack/eslint-patch/modern-module-resolution') - -module.exports = { - root: true, - extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-prettier/skip-formatting'], - parserOptions: { - ecmaVersion: 'latest' - }, - rules: { - 'vue/multi-word-component-names': 'off' - } -} diff --git a/webui/.gitignore b/webui/.gitignore deleted file mode 100644 index 15918818..00000000 --- a/webui/.gitignore +++ /dev/null @@ -1,28 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -.DS_Store -dist -dist-ssr -coverage -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.vscode -.idea -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -*.tsbuildinfo diff --git a/webui/.prettierrc.json b/webui/.prettierrc.json deleted file mode 100644 index ecb550a4..00000000 --- a/webui/.prettierrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/prettierrc", - "semi": true, - "tabWidth": 4, - "singleQuote": false, - "printWidth": 120, - "trailingComma": "none" -} \ No newline at end of file diff --git a/webui/README.md b/webui/README.md deleted file mode 100644 index c7e09273..00000000 --- a/webui/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Capa Explorer WebUI - -Capa Explorer WebUI is a web-based user interface for exploring program capabilities identified by the capa tool. It provides an intuitive and interactive way to analyze and visualize the results of capa analysis. - -## Features - -- **Import capa Results**: Easily upload or import capa JSON result files. -- **Interactive Tree View**: Explore rule matches in a hierarchical structure. -- **Function Capabilities**: Group capabilities by function for static analysis. -- **Process Capabilities**: Group capabilities by process for dynamic analysis. -- **Toggeable Settings**: Toggle between different view modes and filter options. - -## Getting Started - -1. **Access the Application**: Open the Capa Explorer WebUI in your web browser. - -2. **Import capa Results**: - - - Click on "Upload from local" to select a capa JSON file from your computer (with a version higher than 7.0.0). - - Or, paste a URL to a capa JSON file and click the arrow button to load it. - - Alternatively, use the "Preview Static" or "Preview Dynamic" for sample data. - -3. **Explore the Results**: - - - Use the tree view to navigate through the identified capabilities. - - Toggle between different views using the checkboxes in the settings panel: - - "Show capabilities by function/process" for grouped analysis. - - "Show library rule matches" to include or exclude library rules. - -4. **Interact with the Data**: - - Expand/collapse nodes in the TreeTable to see more details. - - Use the search and filter options to find specific features or capabilities (rules). - - Right click on rule names to view their source code or additional information. - -## Feedback and Contributions - -We welcome your feedback and contributions to improve the web-based Capa Explorer. Please report any issues or suggest enhancements through the `capa` GitHub repository. - ---- - -For developers interested in building or contributing to Capa Explorer WebUI, please refer to our [Development Guide](CONTRIBUTION.md). diff --git a/webui/index.html b/webui/index.html deleted file mode 100644 index f2d13068..00000000 --- a/webui/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Capa Explorer - - -
- - - diff --git a/webui/jsconfig.json b/webui/jsconfig.json deleted file mode 100644 index 5a1f2d22..00000000 --- a/webui/jsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "paths": { - "@/*": ["./src/*"] - } - }, - "exclude": ["node_modules", "dist"] -} diff --git a/webui/package.json b/webui/package.json deleted file mode 100644 index 8502fd6f..00000000 --- a/webui/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "capa-webui", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "dev": "vite", - "build": "vite build", - "build:bundle": "vite build --mode bundle", - "preview": "vite preview", - "test": "vitest", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", - "format": "prettier --write src/" - }, - "dependencies": { - "@highlightjs/vue-plugin": "^2.1.0", - "@primevue/themes": "^4.0.0-rc.2", - "pako": "^2.1.0", - "plotly.js-dist": "^2.34.0", - "primeflex": "^3.3.1", - "primeicons": "^7.0.0", - "primevue": "^4.0.0-rc.2", - "vue": "^3.4.29", - "vue-router": "^4.3.3" - }, - "devDependencies": { - "@rushstack/eslint-patch": "^1.8.0", - "@vitejs/plugin-vue": "^5.0.5", - "@vue/eslint-config-prettier": "^9.0.0", - "@vue/test-utils": "^2.4.6", - "eslint": "^8.57.0", - "eslint-plugin-vue": "^9.23.0", - "jsdom": "^24.1.0", - "prettier": "^3.2.5", - "vite": "^5.3.1", - "vite-plugin-singlefile": "^2.0.2", - "vitest": "^1.6.0" - } -} diff --git a/webui/public/favicon.ico b/webui/public/favicon.ico deleted file mode 100644 index 9dcfdab8..00000000 Binary files a/webui/public/favicon.ico and /dev/null differ diff --git a/webui/src/App.vue b/webui/src/App.vue deleted file mode 100644 index a05399de..00000000 --- a/webui/src/App.vue +++ /dev/null @@ -1,15 +0,0 @@ - - - diff --git a/webui/src/assets/images/icon.png b/webui/src/assets/images/icon.png deleted file mode 100644 index 912d71b4..00000000 Binary files a/webui/src/assets/images/icon.png and /dev/null differ diff --git a/webui/src/assets/images/logo-full.png b/webui/src/assets/images/logo-full.png deleted file mode 100644 index eba3a834..00000000 Binary files a/webui/src/assets/images/logo-full.png and /dev/null differ diff --git a/webui/src/assets/main.css b/webui/src/assets/main.css deleted file mode 100644 index c167535c..00000000 --- a/webui/src/assets/main.css +++ /dev/null @@ -1,20 +0,0 @@ -body { - margin: 0 auto; - font-weight: normal; - font-family: Arial, Helvetica, sans-serif; -} - -a { - text-decoration: none; - color: inherit; - transition: color 0.15s ease-in-out; -} - -a:hover { - color: var(--primary-color); -} - -/* remove the border from rows other than rule names */ -.p-treetable-tbody > tr:not(:is([aria-level="1"])) > td { - border: none !important; -} diff --git a/webui/src/components/BannerHeader.vue b/webui/src/components/BannerHeader.vue deleted file mode 100644 index 9d4e10cf..00000000 --- a/webui/src/components/BannerHeader.vue +++ /dev/null @@ -1,45 +0,0 @@ - - - diff --git a/webui/src/components/DescriptionPanel.vue b/webui/src/components/DescriptionPanel.vue deleted file mode 100644 index da33cde0..00000000 --- a/webui/src/components/DescriptionPanel.vue +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/webui/src/components/FunctionCapabilities.vue b/webui/src/components/FunctionCapabilities.vue deleted file mode 100644 index 214c498d..00000000 --- a/webui/src/components/FunctionCapabilities.vue +++ /dev/null @@ -1,81 +0,0 @@ - - - - - diff --git a/webui/src/components/MetadataPanel.vue b/webui/src/components/MetadataPanel.vue deleted file mode 100644 index 686a0ab6..00000000 --- a/webui/src/components/MetadataPanel.vue +++ /dev/null @@ -1,109 +0,0 @@ - - - diff --git a/webui/src/components/NamespaceChart.vue b/webui/src/components/NamespaceChart.vue deleted file mode 100644 index b0ce4100..00000000 --- a/webui/src/components/NamespaceChart.vue +++ /dev/null @@ -1,126 +0,0 @@ - - - diff --git a/webui/src/components/NavBar.vue b/webui/src/components/NavBar.vue deleted file mode 100644 index 5e6616de..00000000 --- a/webui/src/components/NavBar.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/webui/src/components/ProcessCapabilities.vue b/webui/src/components/ProcessCapabilities.vue deleted file mode 100644 index 206dc79a..00000000 --- a/webui/src/components/ProcessCapabilities.vue +++ /dev/null @@ -1,223 +0,0 @@ - - - diff --git a/webui/src/components/RuleMatchesTable.vue b/webui/src/components/RuleMatchesTable.vue deleted file mode 100644 index d7dcee1b..00000000 --- a/webui/src/components/RuleMatchesTable.vue +++ /dev/null @@ -1,289 +0,0 @@ - - - - - diff --git a/webui/src/components/SettingsPanel.vue b/webui/src/components/SettingsPanel.vue deleted file mode 100644 index ca5f4fa8..00000000 --- a/webui/src/components/SettingsPanel.vue +++ /dev/null @@ -1,77 +0,0 @@ - - - diff --git a/webui/src/components/UploadOptions.vue b/webui/src/components/UploadOptions.vue deleted file mode 100644 index d3735c7d..00000000 --- a/webui/src/components/UploadOptions.vue +++ /dev/null @@ -1,91 +0,0 @@ - - - - - diff --git a/webui/src/components/columns/RuleColumn.vue b/webui/src/components/columns/RuleColumn.vue deleted file mode 100644 index e47f1d41..00000000 --- a/webui/src/components/columns/RuleColumn.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/webui/src/components/misc/LibraryTag.vue b/webui/src/components/misc/LibraryTag.vue deleted file mode 100644 index 52d414dc..00000000 --- a/webui/src/components/misc/LibraryTag.vue +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/webui/src/components/misc/VTIcon.vue b/webui/src/components/misc/VTIcon.vue deleted file mode 100644 index d57b0e39..00000000 --- a/webui/src/components/misc/VTIcon.vue +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/webui/src/composables/useRdocLoader.js b/webui/src/composables/useRdocLoader.js deleted file mode 100644 index 5eee5be5..00000000 --- a/webui/src/composables/useRdocLoader.js +++ /dev/null @@ -1,89 +0,0 @@ -// useDataLoader.js -import { ref, readonly } from "vue"; -import { useToast } from "primevue/usetoast"; - -export function useRdocLoader() { - const toast = useToast(); - const rdocData = ref(null); - const isValidVersion = ref(false); - - const MIN_SUPPORTED_VERSION = "7.0.0"; - - /** - * Checks if the loaded rdoc version is supported - * @param {Object} rdoc - The loaded JSON rdoc data - * @returns {boolean} - True if version is supported, false otherwise - */ - const checkVersion = (rdoc) => { - const version = rdoc.meta.version; - if (version < MIN_SUPPORTED_VERSION) { - console.error( - `Version ${version} is not supported. Please use version ${MIN_SUPPORTED_VERSION} or higher.` - ); - toast.add({ - severity: "error", - summary: "Unsupported Version", - detail: `Version ${version} is not supported. Please use version ${MIN_SUPPORTED_VERSION} or higher.`, - life: 5000, - group: "bc" // bottom-center - }); - return false; - } - return true; - }; - - /** - * Loads JSON rdoc data from various sources - * @param {File|string|Object} source - File object, URL string, or JSON object - * @returns {Promise} - */ - const loadRdoc = async (source) => { - try { - let data; - - if (typeof source === "string") { - // Load from URL - const response = await fetch(source); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - data = await response.json(); - } else if (typeof source === "object") { - // Direct JSON object (Preview options) - data = source; - } else { - throw new Error("Invalid source type"); - } - - if (checkVersion(data)) { - rdocData.value = data; - isValidVersion.value = true; - toast.add({ - severity: "success", - summary: "Success", - detail: "JSON data loaded successfully", - life: 3000, - group: "bc" // bottom-center - }); - } else { - rdocData.value = null; - isValidVersion.value = false; - } - } catch (error) { - console.error("Error loading JSON:", error); - toast.add({ - severity: "error", - summary: "Error", - detail: "Failed to process the file. Please ensure it's a valid JSON or gzipped JSON file.", - life: 3000, - group: "bc" // bottom-center - }); - } - }; - - return { - rdocData: readonly(rdocData), - isValidVersion: readonly(isValidVersion), - loadRdoc - }; -} diff --git a/webui/src/main.js b/webui/src/main.js deleted file mode 100644 index 0e0cd00d..00000000 --- a/webui/src/main.js +++ /dev/null @@ -1,88 +0,0 @@ -import "primeicons/primeicons.css"; -import "./assets/main.css"; - -import "highlight.js/styles/default.css"; -import "primeflex/primeflex.css"; -import "primeflex/themes/primeone-light.css"; - -import "highlight.js/lib/common"; -import hljsVuePlugin from "@highlightjs/vue-plugin"; - -import { createApp } from "vue"; -import PrimeVue from "primevue/config"; -import Ripple from "primevue/ripple"; -import Aura from "@primevue/themes/aura"; -import App from "./App.vue"; -import MenuBar from "primevue/menubar"; -import Card from "primevue/card"; -import Panel from "primevue/panel"; -import Column from "primevue/column"; -import Checkbox from "primevue/checkbox"; -import FloatLabel from "primevue/floatlabel"; -import Tooltip from "primevue/tooltip"; -import Divider from "primevue/divider"; -import ContextMenu from "primevue/contextmenu"; -import ToastService from "primevue/toastservice"; -import Toast from "primevue/toast"; -import router from "./router"; - -import { definePreset } from "@primevue/themes"; - -const Noir = definePreset(Aura, { - semantic: { - primary: { - 50: "{zinc.50}", - 100: "{zinc.100}", - 200: "{zinc.200}", - 300: "{zinc.300}", - 400: "{zinc.400}", - 500: "{zinc.500}", - 600: "{zinc.600}", - 700: "{zinc.700}", - 800: "{zinc.800}", - 900: "{zinc.900}", - 950: "{zinc.950}" - }, - colorScheme: { - light: { - primary: { - color: "{slate.800}", - inverseColor: "#ffffff", - hoverColor: "{zinc.900}", - activeColor: "{zinc.800}" - } - } - } - } -}); - -const app = createApp(App); - -app.use(router); -app.use(hljsVuePlugin); - -app.use(PrimeVue, { - theme: { - preset: Noir, - options: { - darkModeSelector: "light" - } - }, - ripple: true -}); -app.use(ToastService); - -app.directive("tooltip", Tooltip); -app.directive("ripple", Ripple); - -app.component("Card", Card); -app.component("Divider", Divider); -app.component("Toast", Toast); -app.component("Panel", Panel); -app.component("MenuBar", MenuBar); -app.component("Checkbox", Checkbox); -app.component("FloatLabel", FloatLabel); -app.component("Column", Column); -app.component("ContextMenu", ContextMenu); - -app.mount("#app"); diff --git a/webui/src/router/index.js b/webui/src/router/index.js deleted file mode 100644 index e0187aac..00000000 --- a/webui/src/router/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import { createRouter, createWebHashHistory } from "vue-router"; -import ImportView from "../views/ImportView.vue"; -import NotFoundView from "../views/NotFoundView.vue"; - -const router = createRouter({ - history: createWebHashHistory(import.meta.env.BASE_URL), - routes: [ - { - path: "/", - name: "home", - component: ImportView - }, - // 404 Route - This should be the last route - { - path: "/:pathMatch(.*)*", - name: "NotFound", - component: NotFoundView - } - ] -}); - -export default router; diff --git a/webui/src/tests/rdocParser.test.js b/webui/src/tests/rdocParser.test.js deleted file mode 100644 index 2ea034df..00000000 --- a/webui/src/tests/rdocParser.test.js +++ /dev/null @@ -1,301 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { parseRules, parseFunctionCapabilities } 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/fileUtils.js b/webui/src/utils/fileUtils.js deleted file mode 100644 index 44a8f86f..00000000 --- a/webui/src/utils/fileUtils.js +++ /dev/null @@ -1,38 +0,0 @@ -import pako from "pako"; - -/** - * Checks if the given file is gzipped - * @param {File} file - The file to check - * @returns {Promise} - True if the file is gzipped, false otherwise - */ -export const isGzipped = async (file) => { - const arrayBuffer = await file.arrayBuffer(); - const uint8Array = new Uint8Array(arrayBuffer); - return uint8Array[0] === 0x1f && uint8Array[1] === 0x8b; -}; - -/** - * Decompresses a gzipped file - * @param {File} file - The gzipped file to decompress - * @returns {Promise} - The decompressed file content as a string - */ -export const decompressGzip = async (file) => { - const arrayBuffer = await file.arrayBuffer(); - const uint8Array = new Uint8Array(arrayBuffer); - const decompressed = pako.inflate(uint8Array, { to: "string" }); - return decompressed; -}; - -/** - * Reads a file as text - * @param {File} file - The file to read - * @returns {Promise} - The file content as a string - */ -export const readFileAsText = (file) => { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (event) => resolve(event.target.result); - reader.onerror = (error) => reject(error); - reader.readAsText(file); - }); -}; diff --git a/webui/src/utils/rdocParser.js b/webui/src/utils/rdocParser.js deleted file mode 100644 index f45652ff..00000000 --- a/webui/src/utils/rdocParser.js +++ /dev/null @@ -1,631 +0,0 @@ -/** - * Parses rules data for the CapaTreeTable component - * @param {Object} rules - The rules object from the rodc JSON data - * @param {string} flavor - The flavor of the analysis (static or dynamic) - * @param {Object} layout - The layout object from the rdoc JSON data - * @param {number} [maxMatches=500] - Maximum number of matches to parse per rule - * @returns {Array} - Parsed tree data for the TreeTable component - */ -export function parseRules(rules, flavor, layout, maxMatches = 1) { - return Object.entries(rules).map(([, rule], index) => { - const ruleNode = { - key: `${index}`, - data: { - type: "rule", - name: rule.meta.name, - lib: rule.meta.lib, - matchCount: rule.matches.length, - namespace: rule.meta.namespace, - mbc: rule.meta.mbc, - source: rule.source, - tactic: JSON.stringify(rule.meta.attack), - attack: rule.meta.attack - ? rule.meta.attack.map((attack) => ({ - tactic: attack.tactic, - technique: attack.technique, - id: attack.id.includes(".") ? attack.id.split(".")[0] : attack.id, - techniques: attack.subtechnique ? [{ technique: attack.subtechnique, id: attack.id }] : [] - })) - : null - } - }; - - // Is this a static rule with a file-level scope? - const isFileScope = rule.meta.scopes && rule.meta.scopes.static === "file"; - - // Limit the number of matches to process - // Dynamic matches can have thousands of matches, only show `maxMatches` for performance reasons - const limitedMatches = flavor === "dynamic" ? rule.matches.slice(0, maxMatches) : rule.matches; - - if (isFileScope) { - // The scope for the rule is a file, so we don't need to show the match location address - ruleNode.children = limitedMatches.map((match, matchIndex) => { - return parseNode(match[1], `${index}-${matchIndex}`, rules, rule.meta.lib, layout); - }); - } else { - // This is not a file-level match scope, we need to create intermediate nodes for each match - ruleNode.children = limitedMatches.map((match, matchIndex) => { - const matchKey = `${index}-${matchIndex}`; - const matchNode = { - key: matchKey, - data: { - type: "match location", - name: - flavor === "static" - ? `${rule.meta.scopes.static} @ ` + formatAddress(match[0]) - : getProcessName(layout, match[0]) - }, - children: [parseNode(match[1], `${matchKey}`, rules, rule.meta.lib, layout)] - }; - return matchNode; - }); - } - - // Add a note if there are more matches than the limit - if (rule.matches.length > limitedMatches.length) { - ruleNode.children.push({ - key: `${index}`, - data: { - type: "match location", - name: `... and ${rule.matches.length - maxMatches} more matches` - } - }); - } - - return ruleNode; - }); -} - -/** - * 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 data for the CapasByFunction DataTable component - */ -export function parseFunctionCapabilities(data, showLibraryRules) { - const result = []; - const matchesByFunction = new Map(); - - // Create a map of basic blocks to functions - const functionsByBB = new Map(); - for (const func of data.meta.analysis.layout.functions) { - const funcAddress = func.address.value; - for (const bb of func.matched_basic_blocks) { - functionsByBB.set(bb.address.value, funcAddress); - } - } - - // 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; - } - - if (rule.meta.scopes.static === "function") { - // Function scope - for (const [addr] of rule.matches) { - const funcAddr = addr.value; - if (!matchesByFunction.has(funcAddr)) { - matchesByFunction.set(funcAddr, new Map()); - } - const funcMatches = matchesByFunction.get(funcAddr); - funcMatches.set(rule.meta.name, { - count: (funcMatches.get(rule.meta.name)?.count || 0) + 1, - namespace: rule.meta.namespace, - lib: rule.meta.lib - }); - } - } else if (rule.meta.scopes.static === "basic block") { - // Basic block scope - for (const [addr] of rule.matches) { - const bbAddr = addr.value; - const funcAddr = functionsByBB.get(bbAddr); - if (funcAddr) { - if (!matchesByFunction.has(funcAddr)) { - matchesByFunction.set(funcAddr, new Map()); - } - const funcMatches = matchesByFunction.get(funcAddr); - funcMatches.set(rule.meta.name, { - count: (funcMatches.get(rule.meta.name)?.count || 0) + 1, - namespace: rule.meta.namespace, - lib: rule.meta.lib - }); - } - } - } - } - - // Convert the matchesByFunction map to the intermediate result array - for (const [funcAddr, matches] of matchesByFunction) { - const functionAddress = funcAddr.toString(16).toUpperCase(); - const matchingRules = Array.from(matches, ([ruleName, data]) => ({ - ruleName, - matchCount: data.count, - namespace: data.namespace, - lib: data.lib - })); - - result.push({ - funcaddr: `0x${functionAddress}`, - matchCount: matchingRules.length, - capabilities: matchingRules, - lib: data.lib - }); - } - - // Transform the intermediate result into the final format - const finalResult = result.flatMap((func) => - func.capabilities.map((cap) => ({ - funcaddr: func.funcaddr, - matchCount: func.matchCount, - ruleName: cap.ruleName, - ruleMatchCount: cap.matchCount, - namespace: cap.namespace, - lib: cap.lib - })) - ); - - return finalResult; -} - -// Helper functions - -/** - * Parses a single `node` object (i.e. statement or feature) in each rule - * @param {Object} node - The node to parse - * @param {string} key - The key for this node - * @param {Object} rules - The full rules object - * @param {boolean} lib - Whether this is a library rule - * @returns {Object} - Parsed node data - */ -function parseNode(node, key, rules, lib, layout) { - if (!node) return null; - - const isNotStatement = node.node.statement && node.node.statement.type === "not"; - const processedNode = isNotStatement ? invertNotStatementSuccess(node) : node; - - if (!processedNode.success) { - return null; - } - - 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, - address: getNodeAddress(processedNode), - description: getNodeDescription(processedNode), - namespace: null, - matchCount: null, - source: null - }, - children: [] - }; - // Recursively parse children - if (processedNode.children && Array.isArray(processedNode.children)) { - result.children = processedNode.children - .map((child) => { - const childNode = parseNode(child, `${key}`, rules, lib, layout); - return childNode; - }) - .filter((child) => child !== null); - } - // If this is a match node, add the rule's source code to the result.data.source object - if (processedNode.node.feature && processedNode.node.feature.type === "match") { - const ruleName = processedNode.node.feature.match; - const rule = rules[ruleName]; - if (rule) { - result.data.source = rule.source; - } - result.children = []; - } - // If this is an optional node, check if it has children. If not, return null (optional statement always evaluate to true) - // we only render them, if they have at least one child node where node.success is true. - if (processedNode.node.statement && processedNode.node.statement.type === "optional") { - if (result.children.length === 0) return null; - } - - if (processedNode.node.feature && processedNode.node.feature.type === "regex") { - result.children = processRegexCaptures(processedNode, key); - } - - // Add call information for dynamic sandbox traces - if (processedNode.node.feature && processedNode.node.feature.type === "api") { - const callInfo = getCallInfo(node, layout); - if (callInfo) { - result.children.push({ - key: key, - data: { - type: "call-info", - name: callInfo - }, - children: [] - }); - } - } - - return result; -} - -// TODO(s-ff): decide if we want to show call info or not -// e.g. explorer.exe{id:0,tid:10,pid:100,ppid:1000} -function getCallInfo(node, layout) { - if (!node.locations || node.locations.length === 0) return null; - - const location = node.locations[0]; - if (location.type !== "call") return null; - - // eslint-disable-next-line no-unused-vars - const [ppid, pid, tid, callId] = location.value; - // eslint-disable-next-line no-unused-vars - const callName = node.node.feature.api; - - const pname = getProcessName(layout, location); - const cname = getCallName(layout, location); - // eslint-disable-next-line no-unused-vars - const [fname, separator, restWithArgs] = partition(cname, "("); - const [args, , returnValueWithParen] = rpartition(restWithArgs, ")"); - - const s = []; - s.push(`${fname}(`); - for (const arg of args.split(", ")) { - s.push(` ${arg},`); - } - s.push(`)${returnValueWithParen}`); - - //const callInfo = `${pname}{pid:${pid},tid:${tid},call:${callId}}\n${s.join('\n')}`; - - return { processName: pname, callInfo: s.join("\n") }; -} - -/** - * Splits a string into three parts based on the first occurrence of a separator. - * This function mimics Python's str.partition() method. - * - * @param {string} str - The input string to be partitioned. - * @param {string} separator - The separator to use for partitioning. - * @returns {Array} An array containing three elements: - * 1. The part of the string before the separator. - * 2. The separator itself. - * 3. The part of the string after the separator. - * If the separator is not found, returns [str, '', '']. - * - * @example - * // Returns ["hello", ",", "world"] - * partition("hello,world", ","); - * - * @example - * // Returns ["hello world", "", ""] - * partition("hello world", ":"); - */ -function partition(str, separator) { - const index = str.indexOf(separator); - if (index === -1) { - // Separator not found, return original string and two empty strings - return [str, "", ""]; - } - return [str.slice(0, index), separator, str.slice(index + separator.length)]; -} - -/** - * Get the process name from the layout - * @param {Object} layout - The layout object - * @param {Object} address - The address object containing process information - * @returns {string} The process name - */ -function getProcessName(layout, address) { - if (!layout || !layout.processes || !Array.isArray(layout.processes)) { - console.error("Invalid layout structure"); - return "Unknown Process"; - } - - const [ppid, pid] = address.value; - - for (const process of layout.processes) { - if ( - process.address && - process.address.type === "process" && - process.address.value && - process.address.value[0] === ppid && - process.address.value[1] === pid - ) { - return process.name || "Unnamed Process"; - } - } - - return "Unknown Process"; -} - -/** - * Splits a string into three parts based on the last occurrence of a separator. - * This function mimics Python's str.rpartition() method. - * - * @param {string} str - The input string to be partitioned. - * @param {string} separator - The separator to use for partitioning. - * @returns {Array} An array containing three elements: - * 1. The part of the string before the last occurrence of the separator. - * 2. The separator itself. - * 3. The part of the string after the last occurrence of the separator. - * If the separator is not found, returns ['', '', str]. - * - * @example - * // Returns ["hello,", ",", "world"] - * rpartition("hello,world,", ","); - * - * @example - * // Returns ["", "", "hello world"] - * rpartition("hello world", ":"); - */ -function rpartition(str, separator) { - const index = str.lastIndexOf(separator); - if (index === -1) { - // Separator not found, return two empty strings and the original string - return ["", "", str]; - } - return [ - str.slice(0, index), // Part before the last separator - separator, // The separator itself - str.slice(index + separator.length) // Part after the last separator - ]; -} - -/** - * Get the call name from the layout - * @param {Object} layout - The layout object - * @param {Object} address - The address object containing call information - * @returns {string} The call name with arguments - */ -function getCallName(layout, address) { - if (!layout || !layout.processes || !Array.isArray(layout.processes)) { - console.error("Invalid layout structure"); - return "Unknown Call"; - } - - const [ppid, pid, tid, callId] = address.value; - - for (const process of layout.processes) { - if ( - process.address && - process.address.type === "process" && - process.address.value && - process.address.value[0] === ppid && - process.address.value[1] === pid - ) { - for (const thread of process.matched_threads) { - if ( - thread.address && - thread.address.type === "thread" && - thread.address.value && - thread.address.value[2] === tid - ) { - for (const call of thread.matched_calls) { - if ( - call.address && - call.address.type === "call" && - call.address.value && - call.address.value[3] === callId - ) { - return call.name || "Unnamed Call"; - } - } - } - } - } - } - - return "Unknown Call"; -} - -function processRegexCaptures(node, key) { - if (!node.captures) return []; - - return Object.entries(node.captures).map(([capture, locations]) => ({ - key: key, - data: { - type: "regex-capture", - name: `"${escape(capture)}"`, - address: formatAddress(locations[0]) - } - })); -} - -function formatAddress(address) { - switch (address.type) { - case "absolute": - return formatHex(address.value); - case "relative": - return `base address+${formatHex(address.value)}`; - case "file": - return `file+${formatHex(address.value)}`; - case "dn_token": - return `token(${formatHex(address.value)})`; - case "dn_token_offset": { - const [token, offset] = address.value; - return `token(${formatHex(token)})+${formatHex(offset)}`; - } - case "process": - //const [ppid, pid] = address.value; - //return `process{pid:${pid}}`; - return formatDynamicAddress(address.value); - case "thread": - //const [threadPpid, threadPid, tid] = address.value; - //return `process{pid:${threadPid},tid:${tid}}`; - return formatDynamicAddress(address.value); - case "call": - //const [callPpid, callPid, callTid, id] = address.value; - //return `process{pid:${callPid},tid:${callTid},call:${id}}`; - return formatDynamicAddress(address.value); - case "no address": - return ""; - default: - throw new Error("Unexpected address type"); - } -} - -function escape(str) { - return str.replace(/"/g, '\\"'); -} - -/** - * Inverts the success values for children of a 'not' statement - * @param {Object} node - The node to invert - * @returns {Object} The inverted node - */ -function invertNotStatementSuccess(node) { - if (!node) return null; - - return { - ...node, - children: node.children - ? node.children.map((child) => ({ - ...child, - success: !child.success, - children: child.children ? invertNotStatementSuccess(child).children : [] - })) - : [] - }; -} - -/** - * Gets the description of a node - * @param {Object} node - The node to get the description from - * @returns {string|null} The description or null if not found - */ -function getNodeDescription(node) { - if (node.node.statement) { - return node.node.statement.description; - } else if (node.node.feature) { - return node.node.feature.description; - } else { - return null; - } -} - -/** - * Gets the name of a node - * @param {Object} node - The node to get the name from - * @returns {string} The name of the node - */ -function getNodeName(node) { - if (node.node.statement) { - return getStatementName(node.node.statement); - } else if (node.node.feature) { - return getFeatureName(node.node.feature); - } - return null; -} - -/** - * Gets the name for a statement node - * @param {Object} statement - The statement object - * @returns {string} The name of the statement - */ -function getStatementName(statement) { - switch (statement.type) { - case "subscope": - // for example, "basic block: " - return `${statement.scope}:`; - case "range": - return getRangeName(statement); - case "some": - return `${statement.count} or more`; - default: - // statement (e.g. "and: ", "or: ", "optional:", ... etc) - return `${statement.type}:`; - } -} - -/** - * Gets the name for a feature node - * @param {Object} feature - The feature object - * @returns {string} The name of the feature - */ -function getFeatureName(feature) { - switch (feature.type) { - case "number": - case "offset": - // example: "number: 0x1234", "offset: 0x3C" - // 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[feature.type]}`; - } -} - -/** - * Formats the name for a range statement - * @param {Object} statement - The range statement object - * @returns {string} The formatted range name - */ -function getRangeName(statement) { - const { child, min, max } = statement; - const { type, [type]: value } = child; - const rangeType = value || value === 0 ? `count(${type}(${value}))` : `count(${type})`; - let rangeValue; - - if (min === max) { - rangeValue = `${min}`; - } else if (max >= Number.MAX_SAFE_INTEGER) { - rangeValue = `${min} or more`; - } else { - rangeValue = `between ${min} and ${max}`; - } - - // for example: count(mnemonic(xor)): 2 or more - return `${rangeType}: ${rangeValue} `; -} - -/** - * Gets the address of a node - * @param {Object} node - The node to get the address from - * @returns {string|null} The formatted address or null if not found - */ -function getNodeAddress(node) { - if (node.node.feature && node.node.feature.type === "regex") return null; - if (node.locations && node.locations.length > 0) { - return formatAddress(node.locations[0]); - } - 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 - * @returns {string} - Formatted address string - */ -function formatDynamicAddress(value) { - const parts = ["ppid", "pid", "tid", "id"]; - return value - .map((item, index) => `${parts[index]}:${item}`) - .reverse() - .join(","); -} - -function formatHex(address) { - return `0x${address.toString(16).toUpperCase()}`; -} diff --git a/webui/src/utils/urlHelpers.js b/webui/src/utils/urlHelpers.js deleted file mode 100644 index 3e8cca81..00000000 --- a/webui/src/utils/urlHelpers.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Creates an MBC (Malware Behavior Catalog) URL from an MBC object. - * - * @param {Object} mbc - The MBC object to format. - * @param {string} mbc.id - The ID of the MBC entry. - * @param {string} mbc.objective - The objective of the malware behavior. - * @param {string} mbc.behavior - The specific behavior of the malware. - * @returns {string|null} The MBC URL or null if the ID is invalid. - */ -export 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; - } - - // Convert the objective and behavior to lowercase and replace spaces with hyphens - const objectivePath = mbc.objective.toLowerCase().replace(/\s+/g, "-"); - const behaviorPath = mbc.behavior.toLowerCase().replace(/\s+/g, "-"); - - // Construct the final URL - return `${baseUrl}/${objectivePath}/${behaviorPath}.md`; -} - -/** - * Creates a MITRE ATT&CK URL for a specific technique or sub-technique. - * - * @param {Object} attack - The ATT&CK object containing information about the technique. - * @param {string} attack.id - The ID of the ATT&CK technique or sub-technique. - * @returns {string|null} The formatted MITRE ATT&CK URL for the technique or null if the ID is invalid. - */ -export function createATTACKHref(attack) { - const baseUrl = "https://attack.mitre.org/techniques/"; - const idParts = attack.id.split("."); - - if (idParts.length === 1) { - // It's a technique - return `${baseUrl}${idParts[0]}`; - } else if (idParts.length === 2) { - // It's a sub-technique - return `${baseUrl}${idParts[0]}/${idParts[1]}`; - } else { - return null; - } -} diff --git a/webui/src/views/ImportView.vue b/webui/src/views/ImportView.vue deleted file mode 100644 index 978a3983..00000000 --- a/webui/src/views/ImportView.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - diff --git a/webui/src/views/NotFoundView.vue b/webui/src/views/NotFoundView.vue deleted file mode 100644 index 3520ebd5..00000000 --- a/webui/src/views/NotFoundView.vue +++ /dev/null @@ -1,19 +0,0 @@ - - - diff --git a/webui/vite.config.js b/webui/vite.config.js deleted file mode 100644 index f80858ef..00000000 --- a/webui/vite.config.js +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig } from 'vite' -import vue from '@vitejs/plugin-vue' -import { viteSingleFile } from 'vite-plugin-singlefile' - -// eslint-disable-next-line no-unused-vars -export default defineConfig(({ command, mode }) => { - const isBundle = mode === 'bundle' - - return { - base: isBundle ? '/' : '/capa/', - plugins: isBundle ? [vue(), viteSingleFile()] : [vue()] - } -}) diff --git a/webui/vitest.config.js b/webui/vitest.config.js deleted file mode 100644 index abe5bba0..00000000 --- a/webui/vitest.config.js +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from 'vitest/config' -import vue from '@vitejs/plugin-vue' - -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