From e9e5d2bb122feeefc9f760b5b65bbdf786bb95a5 Mon Sep 17 00:00:00 2001 From: Soufiane Fariss Date: Mon, 5 Aug 2024 15:20:40 +0200 Subject: [PATCH] delete webui --- webui/.eslintrc.cjs | 13 - webui/.gitignore | 28 - webui/.prettierrc.json | 8 - webui/README.md | 41 -- webui/index.html | 13 - webui/jsconfig.json | 8 - webui/package.json | 39 -- webui/public/favicon.ico | Bin 15406 -> 0 bytes webui/src/App.vue | 15 - webui/src/assets/images/icon.png | Bin 6576 -> 0 bytes webui/src/assets/images/logo-full.png | Bin 10054 -> 0 bytes webui/src/assets/main.css | 20 - webui/src/components/BannerHeader.vue | 45 -- webui/src/components/DescriptionPanel.vue | 16 - webui/src/components/FunctionCapabilities.vue | 81 --- webui/src/components/MetadataPanel.vue | 109 --- webui/src/components/NamespaceChart.vue | 126 ---- webui/src/components/NavBar.vue | 31 - webui/src/components/ProcessCapabilities.vue | 223 ------- webui/src/components/RuleMatchesTable.vue | 289 -------- webui/src/components/SettingsPanel.vue | 77 --- webui/src/components/UploadOptions.vue | 91 --- webui/src/components/columns/RuleColumn.vue | 52 -- webui/src/components/misc/LibraryTag.vue | 13 - webui/src/components/misc/VTIcon.vue | 5 - webui/src/composables/useRdocLoader.js | 89 --- webui/src/main.js | 88 --- webui/src/router/index.js | 22 - webui/src/tests/rdocParser.test.js | 301 --------- webui/src/utils/fileUtils.js | 38 -- webui/src/utils/rdocParser.js | 631 ------------------ webui/src/utils/urlHelpers.js | 52 -- webui/src/views/ImportView.vue | 120 ---- webui/src/views/NotFoundView.vue | 19 - webui/vite.config.js | 13 - webui/vitest.config.js | 12 - 36 files changed, 2728 deletions(-) delete mode 100644 webui/.eslintrc.cjs delete mode 100644 webui/.gitignore delete mode 100644 webui/.prettierrc.json delete mode 100644 webui/README.md delete mode 100644 webui/index.html delete mode 100644 webui/jsconfig.json delete mode 100644 webui/package.json delete mode 100644 webui/public/favicon.ico delete mode 100644 webui/src/App.vue delete mode 100644 webui/src/assets/images/icon.png delete mode 100644 webui/src/assets/images/logo-full.png delete mode 100644 webui/src/assets/main.css delete mode 100644 webui/src/components/BannerHeader.vue delete mode 100644 webui/src/components/DescriptionPanel.vue delete mode 100644 webui/src/components/FunctionCapabilities.vue delete mode 100644 webui/src/components/MetadataPanel.vue delete mode 100644 webui/src/components/NamespaceChart.vue delete mode 100644 webui/src/components/NavBar.vue delete mode 100644 webui/src/components/ProcessCapabilities.vue delete mode 100644 webui/src/components/RuleMatchesTable.vue delete mode 100644 webui/src/components/SettingsPanel.vue delete mode 100644 webui/src/components/UploadOptions.vue delete mode 100644 webui/src/components/columns/RuleColumn.vue delete mode 100644 webui/src/components/misc/LibraryTag.vue delete mode 100644 webui/src/components/misc/VTIcon.vue delete mode 100644 webui/src/composables/useRdocLoader.js delete mode 100644 webui/src/main.js delete mode 100644 webui/src/router/index.js delete mode 100644 webui/src/tests/rdocParser.test.js delete mode 100644 webui/src/utils/fileUtils.js delete mode 100644 webui/src/utils/rdocParser.js delete mode 100644 webui/src/utils/urlHelpers.js delete mode 100644 webui/src/views/ImportView.vue delete mode 100644 webui/src/views/NotFoundView.vue delete mode 100644 webui/vite.config.js delete mode 100644 webui/vitest.config.js 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 9dcfdab8d631e1c3adc2030cbb7567d40951d0c1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15406 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 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 912d71b47b5080f6e574d161d5c621cd4196fa53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6576 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 OD82h6_gls&;C}!jHciX` 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