Merge pull request #2301 from s-ff/use-gzipped-preview

web: don't bundle preview data in build and release
This commit is contained in:
Fariss
2024-08-21 18:06:28 +02:00
committed by GitHub
8 changed files with 311 additions and 377 deletions

View File

@@ -1,6 +1,8 @@
<script setup>
import Menubar from "primevue/menubar";
import { RouterLink } from "vue-router";
const isBundle = import.meta.env.MODE === "bundle";
</script>
<template>
@@ -13,8 +15,9 @@ import { RouterLink } from "vue-router";
<template #end>
<div class="flex align-items-center gap-3">
<a
v-if="!isBundle"
v-ripple
v-tooltip.right="'Download capa Explorer Web for offline usage'"
v-tooltip.bottom="'Download capa Explorer Web for offline usage'"
href="./capa-explorer-web.zip"
download="capa-explorer-web.zip"
aria-label="Download capa Explorer Web release"

View File

@@ -3,7 +3,7 @@
:value="filteredTreeData"
v-model:expandedKeys="expandedKeys"
size="small"
scrollable
:scrollable="true"
:filters="filters"
:filterMode="filterMode"
sortField="namespace"
@@ -49,10 +49,11 @@
</template>
</Column>
<!-- Address/Process column -->
<!-- Address column (only shown for static flavor) -->
<Column
v-if="props.data.meta.flavor === 'static'"
field="address"
:header="props.data.meta.flavor === 'dynamic' ? 'Process' : 'Address'"
header="Address"
filterMatchMode="contains"
style="width: 8.5%"
class="cursor-default"
@@ -252,7 +253,6 @@ const onRightClick = (event, instance) => {
selectedNode.value = instance.node;
// show the context menu
console.log(menu);
menu.value.show(event);
}
};

View File

@@ -29,28 +29,34 @@
</FloatLabel>
<Button icon="pi pi-arrow-right" @click="$emit('load-from-url', loadURL)" :disabled="!loadURL" />
</div>
<template v-if="!isBundle">
<Divider layout="vertical" class="hidden-mobile">
<b>OR</b>
</Divider>
<Divider layout="horizontal" class="visible-mobile" align="center">
<b>OR</b>
</Divider>
<Divider layout="vertical" class="hidden-mobile">
<b>OR</b>
</Divider>
<Divider layout="horizontal" class="visible-mobile" align="center">
<b>OR</b>
</Divider>
<div class="flex-grow-1 flex align-items-center justify-content-center">
<Button
label="Preview Static"
@click="router.push({ path: '/', query: { rdoc: staticURL } })"
/>
</div>
<div class="flex-grow-1 flex align-items-center justify-content-center">
<Button label="Preview Static" @click="$emit('load-demo-static')" class="p-button" />
</div>
<Divider layout="vertical" class="hidden-mobile">
<b>OR</b>
</Divider>
<Divider layout="horizontal" class="visible-mobile" align="center">
<b>OR</b>
</Divider>
<div class="flex-grow-1 flex align-items-center justify-content-center">
<Button label="Preview Dynamic" @click="$emit('load-demo-dynamic')" class="p-button" />
</div>
<Divider layout="vertical" class="hidden-mobile">
<b>OR</b>
</Divider>
<Divider layout="horizontal" class="visible-mobile" align="center">
<b>OR</b>
</Divider>
<div class="flex-grow-1 flex align-items-center justify-content-center">
<Button
label="Preview Dynamic"
@click="router.push({ path: '/', query: { rdoc: dynamicURL } })"
/>
</div>
</template>
</div>
</template>
</Card>
@@ -65,9 +71,17 @@ import FloatLabel from "primevue/floatlabel";
import InputText from "primevue/inputtext";
import Button from "primevue/button";
const loadURL = ref("");
import { useRouter } from "vue-router";
const router = useRouter();
defineEmits(["load-from-local", "load-from-url", "load-demo-static", "load-demo-dynamic"]);
const loadURL = ref("");
const isBundle = import.meta.env.MODE === "bundle";
defineEmits(["load-from-local", "load-from-url"]);
const dynamicURL =
"https://raw.githubusercontent.com/mandiant/capa-testfiles/master/rd/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json.gz";
const staticURL = "https://raw.githubusercontent.com/mandiant/capa-testfiles/master/rd/al-khaser_x64.exe_.json";
</script>
<style scoped>

View File

@@ -11,7 +11,7 @@
<!-- example node: "basic block @ 0x401000" or "explorer.exe" -->
<template v-else-if="node.data.type === 'match location'">
<span class="text-sm font-italic">{{ node.data.name }}</span>
<span class="text-sm font-monospace text-xs">{{ node.data.name }}</span>
</template>
<!-- example node: "- or", "- and" -->
@@ -52,7 +52,7 @@
<!-- example node: "exit(0) -> 0" (if the node type is call-info, we highlight node.data.name.callInfo) -->
<template v-else-if="node.data.type === 'call-info'">
<highlightjs lang="c" :code="node.data.name.callInfo" />
<highlightjs lang="c" :code="node.data.name.callInfo" class="text-xs" />
</template>
<!-- example node: " = IMAGE_NT_SIGNATURE (PE)" -->

View File

@@ -6,32 +6,63 @@ export function useRdocLoader() {
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
* Displays a toast notification.
* @param {string} severity - The severity level of the notification
* @param {string} summary - The title of the notification.
* @param {string} detail - The detailed message of the notification.
* @returns {void}
*/
const showToast = (severity, summary, detail) => {
toast.add({ severity, summary, detail, life: 3000, group: "bc" }); // bc: bottom-center
};
/**
* Checks if the version of the loaded data is supported.
* @param {Object} rdoc - The loaded JSON data containing version information.
* @returns {boolean} True if the version is supported, false otherwise.
*/
const checkVersion = (rdoc) => {
const version = rdoc.meta.version;
if (version < MIN_SUPPORTED_VERSION) {
console.error(
showToast(
"error",
"Unsupported Version",
`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<void>}
* Processes the content of a file or blob.
* @param {File|Blob} blob - The file or blob to process.
* @returns {Promise<Object>} A promise that resolves to the parsed JSON data.
* @throws {Error} If the content cannot be parsed as JSON.
*/
const processBlob = async (blob) => {
const content = (await isGzipped(blob)) ? await decompressGzip(blob) : await readFileAsText(blob);
return JSON.parse(content);
};
/**
* Fetches data from a URL.
* @param {string} url - The URL to fetch data from.
* @returns {Promise<Blob>} A promise that resolves to the fetched data as a Blob.
* @throws {Error} If the fetch request fails.
*/
const fetchFromUrl = async (url) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.blob();
};
/**
* Loads and processes RDOC data from various sources.
* @param {string|File|Object} source - The source of the RDOC data. Can be a URL string, a File object, or a JSON object.
* @returns {Promise<Object|null>} A promise that resolves to the processed RDOC data, or null if processing fails.
*/
const loadRdoc = async (source) => {
try {
@@ -39,50 +70,25 @@ export function useRdocLoader() {
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();
const blob = await fetchFromUrl(source);
data = await processBlob(blob);
} else if (source instanceof File) {
let fileContent;
if (await isGzipped(source)) {
fileContent = await decompressGzip(source);
} else {
fileContent = await readFileAsText(source);
}
data = JSON.parse(fileContent);
} else if (typeof source === "object") {
// Direct JSON object (Preview options)
data = source;
// Load from local
data = await processBlob(source);
} else {
throw new Error("Invalid source type");
}
if (checkVersion(data)) {
toast.add({
severity: "success",
summary: "Success",
detail: "JSON data loaded successfully",
life: 3000,
group: "bc" // bottom-center
});
showToast("success", "Success", "JSON data loaded successfully");
return data;
}
} 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
});
showToast("error", "Failed to process the file", error.message);
}
return null;
};
return {
loadRdoc
};
return { loadRdoc };
}

View File

@@ -3,69 +3,219 @@
* @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=1] - Maximum number of matches to parse per rule
* @param {number} [maxMatches=300] - Maximum number of matches to parse per rule (used for optimized rendering in dynamic analysis)
* @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,
attack: rule.meta.attack
}
};
export function parseRules(rules, flavor, _layout, maxMatches = 300) {
const layout = preprocessLayout(_layout);
const treeData = [];
let index = 0;
for (const [, rule] of Object.entries(rules)) {
const ruleNode = createRuleNode(rule, index, flavor);
// 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;
// Dynamic matches can have thousands of matches, only show `maxMatches` for rendering optimization
const matchesToProcess = flavor === "dynamic" ? rule.matches.slice(0, maxMatches) : rule.matches;
// Is this a static rule with a file-level scope?
const isFileScope = rule.meta.scopes && rule.meta.scopes.static === "file";
for (let matchIndex = 0; matchIndex < matchesToProcess.length; matchIndex++) {
const match = matchesToProcess[matchIndex];
const matchKey = `${index}-${matchIndex}`;
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;
});
// Check if the rule has a file-level scope
if (rule.meta.scopes && rule.meta.scopes.static === "file") {
// The scope for the rule is a file, so we don't need to show the match location address
ruleNode.children.push(parseNode(match[1], matchKey, rules, rule.meta.lib, layout));
} else {
// This is not a file-level match scope, we need to create an intermediate node for each match
const matchNode = createMatchNode(rule.meta.scopes.static, match, matchKey, flavor, layout);
matchNode.children.push(parseNode(match[1], matchKey, rules, rule.meta.lib, layout));
ruleNode.children.push(matchNode);
}
}
// Finally, add a note if there are more matches than the limit (only applicable in dynamic mode)
if (rule.matches.length > limitedMatches.length) {
ruleNode.children.push({
key: `${index}`,
data: {
type: "match location",
name: `... and ${rule.matches.length - maxMatches} more matches`
// Add note for additional non-covered matches in dynamic mode
if (flavor === "dynamic" && rule.matches.length > maxMatches) {
ruleNode.children.push(createAdditionalMatchesNode(index, rule.matches.length - maxMatches));
}
treeData.push(ruleNode);
index++;
}
return treeData;
}
/**
* Preprocesses the layout to create efficient lookup maps
* @param {Object} layout - The layout object from rdoc JSON data
* @returns {Object} An object containing lookup maps for calls, threads, and processes
*/
function preprocessLayout(layout) {
const processMap = new Map();
const threadMap = new Map();
const callMap = new Map();
if (layout && layout.processes) {
for (const process of layout.processes) {
if (process.address && process.address.type === "process" && process.address.value) {
const [ppid, pid] = process.address.value;
processMap.set(`${ppid}-${pid}`, process);
if (process.matched_threads) {
for (const thread of process.matched_threads) {
if (thread.address && thread.address.type === "thread" && thread.address.value) {
const [, , tid] = thread.address.value;
threadMap.set(`${ppid}-${pid}-${tid}`, thread);
if (thread.matched_calls) {
for (const call of thread.matched_calls) {
if (call.address && call.address.type === "call" && call.address.value) {
const [, , , callId] = call.address.value;
callMap.set(`${ppid}-${pid}-${tid}-${callId}`, call);
}
}
}
}
}
}
}
}
}
return { processMap, threadMap, callMap };
}
// Creates a node for a rule
function createRuleNode(rule, index) {
return {
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,
attack: rule.meta.attack
},
children: []
};
}
// Creates a match location (e.g. basic block @ 0x1000 or explorer.exe (ppid: 1234, pid: 5678)) node
function createMatchNode(scope, match, matchKey, flavor, layout) {
const [location] = match;
const name = flavor === "static" ? `${scope} @ ${formatAddress(location)}` : getProcessName(layout, location);
return {
key: matchKey,
data: {
type: "match location",
name: name
},
children: []
};
}
// Creates a note node for additional non-covered matches in dynamic mode
function createAdditionalMatchesNode(index, additionalMatchCount) {
return {
key: `${index}`,
data: {
type: "match location",
name: `... and ${additionalMatchCount} more matches`
}
};
}
/**
* 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, // feature or statement
typeValue: processedNode.node.statement?.type || processedNode.node.feature?.type,
success: processedNode.success,
name: getNodeName(processedNode),
lib: lib,
address: getNodeAddress(processedNode),
description: getNodeDescription(processedNode)
},
children: []
};
if (processedNode.children && Array.isArray(processedNode.children)) {
result.children = processedNode.children
.map((child) => parseNode(child, `${key}`, rules, lib, layout))
.filter((child) => child !== null);
}
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 (
processedNode.node.statement &&
processedNode.node.statement.type === "optional" &&
result.children.length === 0
) {
return null;
}
if (processedNode.node.feature && processedNode.node.feature.type === "regex") {
result.children = processRegexCaptures(processedNode, key);
}
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 ruleNode;
});
return result;
}
/**
* Get the process name using the optimized processNames Map
* @param {Map} layout - The layout object containing maps
* @param {Object} address - The address object containing process information
* @returns {string} The process name
*/
function getProcessName(layout, address) {
const [ppid, pid] = address.value;
const processKey = `${ppid}-${pid}`;
const process = layout.processMap.get(processKey);
return process.name + ` (ppid:${ppid}, pid:${pid})`;
}
/**
@@ -175,249 +325,34 @@ export function parseFunctionCapabilities(doc) {
// 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?.type || processedNode.node.feature?.type, // e.g., number, regex, api, or, and, optional ... etc
success: processedNode.success,
name: getNodeName(processedNode),
lib: lib,
address: getNodeAddress(processedNode),
description: getNodeDescription(processedNode)
},
children: []
};
// Recursively parse node children (i.e., nested statements or features)
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;
}
// regex features have captures, which we need to process and add as children
if (processedNode.node.feature && processedNode.node.feature.type === "regex") {
result.children = processRegexCaptures(processedNode, key);
}
// Add call information for dynamic sandbox traces when the feature is `api`
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;
}
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") };
return { processName: pname, callInfo: cname };
}
/**
* 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<string>} 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<string>} 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
* Get the call name from the preprocessed layout maps
* @param {Object} layoutMaps - The preprocessed layout maps
* @param {Object} address - The address object containing call information
* @returns {string} The call name with arguments
* @returns {string} The call name or "Unknown Call" if not found
*/
function getCallName(layout, address) {
if (!layout || !layout.processes || !Array.isArray(layout.processes)) {
console.error("Invalid layout structure");
function getCallName(layoutMaps, address) {
if (!address || !address.value || address.value.length < 4) {
return "Unknown Call";
}
const [ppid, pid, tid, callId] = address.value;
const callKey = `${ppid}-${pid}-${tid}-${callId}`;
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";
const call = layoutMaps.callMap.get(callKey);
return call.name;
}
function processRegexCaptures(node, key) {

View File

@@ -1,11 +1,6 @@
<template>
<DescriptionPanel />
<UploadOptions
@load-from-local="loadFromLocal"
@load-from-url="loadFromURL"
@load-demo-static="loadDemoDataStatic"
@load-demo-dynamic="loadDemoDataDynamic"
/>
<UploadOptions @load-from-local="loadFromLocal" @load-from-url="loadFromURL" />
</template>
<script setup>
@@ -15,10 +10,6 @@ import { watch } from "vue";
import DescriptionPanel from "@/components/DescriptionPanel.vue";
import UploadOptions from "@/components/UploadOptions.vue";
// import demo data
import demoRdocStatic from "@testfiles/rd/al-khaser_x64.exe_.json";
import demoRdocDynamic from "@testfiles/rd/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json";
// import router utils
import { useRouter, useRoute } from "vue-router";
const router = useRouter();
@@ -47,22 +38,6 @@ const loadFromURL = async (url) => {
}
};
const loadDemoDataStatic = async () => {
const result = await loadRdoc(demoRdocStatic);
if (result) {
rdocStore.setData(demoRdocStatic);
router.push("/analysis");
}
};
const loadDemoDataDynamic = async () => {
const result = await loadRdoc(demoRdocDynamic);
if (result) {
rdocStore.setData(demoRdocDynamic);
router.push("/analysis");
}
};
// Watch for changes in the rdoc query parameter
watch(
() => route.query.rdoc,

View File

@@ -14,6 +14,7 @@ export default defineConfig(({ mode }) => {
"@": fileURLToPath(new URL("src", import.meta.url)),
"@testfiles": fileURLToPath(new URL("../../tests/data", import.meta.url))
}
}
},
assetsInclude: ["**/*.gz"]
};
});