mirror of
https://github.com/mandiant/capa.git
synced 2025-12-28 21:53:29 -08:00
web: diplay results in new /analysis route
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<Toast position="bottom-center" group="bc" />
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<BannerHeader />
|
||||
|
||||
@@ -71,22 +71,7 @@ const currentSource = ref("");
|
||||
const functionCapabilities = ref([]);
|
||||
|
||||
onMounted(() => {
|
||||
const cacheKey = "functionCapabilities";
|
||||
let cachedData = sessionStorage.getItem(cacheKey);
|
||||
|
||||
if (cachedData) {
|
||||
// If the data is already in sessionStorage, parse it and use it
|
||||
functionCapabilities.value = JSON.parse(cachedData);
|
||||
} else {
|
||||
// Parse function capabilities and cache the result in sessionStorage
|
||||
functionCapabilities.value = parseFunctionCapabilities(props.data);
|
||||
try {
|
||||
sessionStorage.setItem(cacheKey, JSON.stringify(functionCapabilities.value));
|
||||
} catch (e) {
|
||||
console.warn("Failed to store parsed data in sessionStorage:", e);
|
||||
// If storing fails (e.g., due to storage limits), we can still continue with the parsed data
|
||||
}
|
||||
}
|
||||
functionCapabilities.value = parseFunctionCapabilities(props.data);
|
||||
});
|
||||
|
||||
/*
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
filterMode="lenient"
|
||||
sortField="pid"
|
||||
:sortOrder="1"
|
||||
rowHover="true"
|
||||
:rowHover="true"
|
||||
>
|
||||
<Column field="processname" header="Process" expander>
|
||||
<template #body="slotProps">
|
||||
|
||||
@@ -156,8 +156,6 @@
|
||||
</template>
|
||||
</ContextMenu>
|
||||
|
||||
<Toast />
|
||||
|
||||
<!-- Source code dialog -->
|
||||
<Dialog v-model:visible="sourceDialogVisible" style="width: 50vw">
|
||||
<highlightjs autodetect :code="currentSource" />
|
||||
@@ -325,23 +323,7 @@ const showSource = (source) => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const cacheKey = "ruleMatches";
|
||||
const cachedData = sessionStorage.getItem(cacheKey);
|
||||
|
||||
if (cachedData) {
|
||||
// If cached data exists, parse and use it
|
||||
treeData.value = JSON.parse(cachedData);
|
||||
} else {
|
||||
// If no cached data, parse the rules and store in sessionStorage
|
||||
treeData.value = parseRules(props.data.rules, props.data.meta.flavor, props.data.meta.analysis.layout);
|
||||
// Store the parsed data in sessionStorage
|
||||
try {
|
||||
sessionStorage.setItem(cacheKey, JSON.stringify(treeData.value));
|
||||
} catch (e) {
|
||||
console.warn("Failed to store parsed data in sessionStorage:", e);
|
||||
// If storing fails (e.g., due to storage limits), we can still continue with the parsed data
|
||||
}
|
||||
}
|
||||
treeData.value = parseRules(props.data.rules, props.data.meta.flavor, props.data.meta.analysis.layout);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { ref, readonly } from "vue";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { isGzipped, decompressGzip, readFileAsText } from "@/utils/fileUtils";
|
||||
|
||||
export function useRdocLoader() {
|
||||
const toast = useToast();
|
||||
const rdocData = ref(null);
|
||||
const isValidVersion = ref(false);
|
||||
|
||||
const MIN_SUPPORTED_VERSION = "7.0.0";
|
||||
|
||||
/**
|
||||
@@ -47,6 +44,14 @@ export function useRdocLoader() {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
data = await response.json();
|
||||
} 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;
|
||||
@@ -55,8 +60,6 @@ export function useRdocLoader() {
|
||||
}
|
||||
|
||||
if (checkVersion(data)) {
|
||||
rdocData.value = data;
|
||||
isValidVersion.value = true;
|
||||
toast.add({
|
||||
severity: "success",
|
||||
summary: "Success",
|
||||
@@ -64,9 +67,7 @@ export function useRdocLoader() {
|
||||
life: 3000,
|
||||
group: "bc" // bottom-center
|
||||
});
|
||||
} else {
|
||||
rdocData.value = null;
|
||||
isValidVersion.value = false;
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading JSON:", error);
|
||||
@@ -78,11 +79,10 @@ export function useRdocLoader() {
|
||||
group: "bc" // bottom-center
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return {
|
||||
rdocData: readonly(rdocData),
|
||||
isValidVersion: readonly(isValidVersion),
|
||||
loadRdoc
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import ImportView from "../views/ImportView.vue";
|
||||
import NotFoundView from "../views/NotFoundView.vue";
|
||||
import ImportView from "@/views/ImportView.vue";
|
||||
import NotFoundView from "@/views/NotFoundView.vue";
|
||||
import AnalysisView from "@/views/AnalysisView.vue";
|
||||
|
||||
import { rdocStore } from "@/store/rdocStore";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
@@ -10,6 +13,20 @@ const router = createRouter({
|
||||
name: "home",
|
||||
component: ImportView
|
||||
},
|
||||
{
|
||||
path: "/analysis",
|
||||
name: "analysis",
|
||||
component: AnalysisView,
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (rdocStore.data.value === null) {
|
||||
// No rdoc loaded, redirect to home page
|
||||
next({ name: "home" });
|
||||
} else {
|
||||
// rdoc is loaded, proceed to analysis page
|
||||
next();
|
||||
}
|
||||
}
|
||||
},
|
||||
// 404 Route - This should be the last route
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
|
||||
11
web/explorer/src/store/rdocStore.js
Normal file
11
web/explorer/src/store/rdocStore.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ref } from "vue";
|
||||
|
||||
export const rdocStore = {
|
||||
data: ref(null),
|
||||
setData(newData) {
|
||||
this.data.value = newData;
|
||||
},
|
||||
clearData() {
|
||||
this.data.value = null;
|
||||
}
|
||||
};
|
||||
74
web/explorer/src/views/AnalysisView.vue
Normal file
74
web/explorer/src/views/AnalysisView.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<MetadataPanel :data="doc" />
|
||||
<SettingsPanel
|
||||
:flavor="doc.meta.flavor"
|
||||
:library-rule-matches-count="libraryRuleMatchesCount"
|
||||
@update:show-capabilities-by-function-or-process="updateShowCapabilitiesByFunctionOrProcess"
|
||||
@update:show-library-rules="updateShowLibraryRules"
|
||||
@update:show-namespace-chart="updateShowNamespaceChart"
|
||||
@update:show-column-filters="updateShowColumnFilters"
|
||||
/>
|
||||
<RuleMatchesTable
|
||||
v-if="!showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
|
||||
:data="doc"
|
||||
:show-library-rules="showLibraryRules"
|
||||
:show-column-filters="showColumnFilters"
|
||||
/>
|
||||
<FunctionCapabilities
|
||||
v-if="doc.meta.flavor === 'static' && showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
|
||||
:data="doc"
|
||||
:show-library-rules="showLibraryRules"
|
||||
/>
|
||||
<ProcessCapabilities
|
||||
v-else-if="doc.meta.flavor === 'dynamic' && showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
|
||||
:data="doc"
|
||||
:show-capabilities-by-process="showCapabilitiesByFunctionOrProcess"
|
||||
:show-library-rules="showLibraryRules"
|
||||
/>
|
||||
<NamespaceChart v-else-if="showNamespaceChart" :data="doc" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from "vue";
|
||||
|
||||
// Componenets
|
||||
import MetadataPanel from "@/components/MetadataPanel.vue";
|
||||
import SettingsPanel from "@/components/SettingsPanel.vue";
|
||||
import RuleMatchesTable from "@/components/RuleMatchesTable.vue";
|
||||
import FunctionCapabilities from "@/components/FunctionCapabilities.vue";
|
||||
import ProcessCapabilities from "@/components/ProcessCapabilities.vue";
|
||||
import NamespaceChart from "@/components/NamespaceChart.vue";
|
||||
|
||||
// Import loaded rdoc
|
||||
import { rdocStore } from "@/store/rdocStore";
|
||||
const doc = rdocStore.data.value;
|
||||
|
||||
// Viewing options
|
||||
const showCapabilitiesByFunctionOrProcess = ref(false);
|
||||
const showLibraryRules = ref(false);
|
||||
const showNamespaceChart = ref(false);
|
||||
const showColumnFilters = ref(false);
|
||||
|
||||
// Count library rules
|
||||
const libraryRuleMatchesCount = computed(() => {
|
||||
if (!doc || !doc.rules) return 0;
|
||||
return Object.values(rdocStore.data.value.rules).filter((rule) => rule.meta.lib).length;
|
||||
});
|
||||
|
||||
// Event handlers to update variables
|
||||
const updateShowCapabilitiesByFunctionOrProcess = (value) => {
|
||||
showCapabilitiesByFunctionOrProcess.value = value;
|
||||
};
|
||||
|
||||
const updateShowLibraryRules = (value) => {
|
||||
showLibraryRules.value = value;
|
||||
};
|
||||
|
||||
const updateShowNamespaceChart = (value) => {
|
||||
showNamespaceChart.value = value;
|
||||
};
|
||||
|
||||
const updateShowColumnFilters = (value) => {
|
||||
showColumnFilters.value = value;
|
||||
};
|
||||
</script>
|
||||
@@ -1,130 +1,78 @@
|
||||
<template>
|
||||
<DescriptionPanel />
|
||||
<UploadOptions
|
||||
@load-from-local="loadFromLocal"
|
||||
@load-from-url="loadFromURL"
|
||||
@load-demo-static="loadDemoDataStatic"
|
||||
@load-demo-dynamic="loadDemoDataDynamic"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { watch } from "vue";
|
||||
|
||||
// componenets
|
||||
import DescriptionPanel from "@/components/DescriptionPanel.vue";
|
||||
import UploadOptions from "@/components/UploadOptions.vue";
|
||||
import MetadataPanel from "@/components/MetadataPanel.vue";
|
||||
import RuleMatchesTable from "@/components/RuleMatchesTable.vue";
|
||||
import FunctionCapabilities from "@/components/FunctionCapabilities.vue";
|
||||
import ProcessCapabilities from "@/components/ProcessCapabilities.vue";
|
||||
import SettingsPanel from "@/components/SettingsPanel.vue";
|
||||
import NamespaceChart from "@/components/NamespaceChart.vue";
|
||||
import Toast from "primevue/toast";
|
||||
|
||||
// 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();
|
||||
const route = useRoute();
|
||||
|
||||
// import rdoc loader function
|
||||
import { useRdocLoader } from "@/composables/useRdocLoader";
|
||||
const { rdocData, isValidVersion, loadRdoc } = useRdocLoader();
|
||||
const { loadRdoc } = useRdocLoader();
|
||||
|
||||
import { isGzipped, decompressGzip, readFileAsText } from "@/utils/fileUtils";
|
||||
|
||||
const showCapabilitiesByFunctionOrProcess = ref(false);
|
||||
const showLibraryRules = ref(false);
|
||||
const showNamespaceChart = ref(false);
|
||||
const showColumnFilters = ref(false);
|
||||
|
||||
const libraryRuleMatchesCount = computed(() => {
|
||||
if (!rdocData.value || !rdocData.value.rules) return 0;
|
||||
return Object.values(rdocData.value.rules).filter((rule) => rule.meta.lib).length;
|
||||
});
|
||||
|
||||
const updateShowCapabilitiesByFunctionOrProcess = (value) => {
|
||||
showCapabilitiesByFunctionOrProcess.value = value;
|
||||
};
|
||||
|
||||
const updateShowLibraryRules = (value) => {
|
||||
showLibraryRules.value = value;
|
||||
};
|
||||
|
||||
const updateShowNamespaceChart = (value) => {
|
||||
showNamespaceChart.value = value;
|
||||
};
|
||||
|
||||
const updateShowColumnFilters = (value) => {
|
||||
showColumnFilters.value = value;
|
||||
};
|
||||
// import rdoc store
|
||||
import { rdocStore } from "@/store/rdocStore";
|
||||
|
||||
const loadFromLocal = async (event) => {
|
||||
const file = event.files[0];
|
||||
|
||||
let fileContent;
|
||||
if (await isGzipped(file)) {
|
||||
fileContent = await decompressGzip(file);
|
||||
} else {
|
||||
fileContent = await readFileAsText(file);
|
||||
const result = await loadRdoc(event.files[0]);
|
||||
if (result) {
|
||||
rdocStore.setData(result);
|
||||
router.push("/analysis");
|
||||
}
|
||||
|
||||
const jsonData = JSON.parse(fileContent);
|
||||
|
||||
loadRdoc(jsonData);
|
||||
};
|
||||
|
||||
const loadFromURL = (url) => {
|
||||
loadRdoc(url);
|
||||
};
|
||||
|
||||
const loadDemoDataStatic = () => {
|
||||
loadRdoc(demoRdocStatic);
|
||||
};
|
||||
|
||||
const loadDemoDataDynamic = () => {
|
||||
loadRdoc(demoRdocDynamic);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// Clear out sessionStorage to prevent stale data from being used
|
||||
sessionStorage.clear();
|
||||
|
||||
// Check if the URL contains a rdoc parameter and load the data from that URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const encodedRdocURL = urlParams.get("rdoc");
|
||||
if (encodedRdocURL) {
|
||||
const rdocURL = decodeURIComponent(encodedRdocURL);
|
||||
loadFromURL(rdocURL);
|
||||
const loadFromURL = async (url) => {
|
||||
const result = await loadRdoc(url);
|
||||
if (result) {
|
||||
rdocStore.setData(result);
|
||||
router.push({ name: "analysis", query: { rdoc: 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,
|
||||
(rdocURL) => {
|
||||
if (rdocURL) {
|
||||
// Clear the query parameter
|
||||
router.replace({ query: {} });
|
||||
loadFromURL(decodeURIComponent(rdocURL));
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Panel v-if="!rdocData || !isValidVersion">
|
||||
<DescriptionPanel />
|
||||
<UploadOptions
|
||||
@load-from-local="loadFromLocal"
|
||||
@load-from-url="loadFromURL"
|
||||
@load-demo-static="loadDemoDataStatic"
|
||||
@load-demo-dynamic="loadDemoDataDynamic"
|
||||
/>
|
||||
</Panel>
|
||||
|
||||
<Toast position="bottom-center" group="bc" />
|
||||
<template v-if="rdocData && isValidVersion">
|
||||
<MetadataPanel :data="rdocData" />
|
||||
<SettingsPanel
|
||||
:flavor="rdocData.meta.flavor"
|
||||
:library-rule-matches-count="libraryRuleMatchesCount"
|
||||
@update:show-capabilities-by-function-or-process="updateShowCapabilitiesByFunctionOrProcess"
|
||||
@update:show-library-rules="updateShowLibraryRules"
|
||||
@update:show-namespace-chart="updateShowNamespaceChart"
|
||||
@update:show-column-filters="updateShowColumnFilters"
|
||||
/>
|
||||
|
||||
<RuleMatchesTable
|
||||
v-if="!showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
|
||||
:data="rdocData"
|
||||
:show-library-rules="showLibraryRules"
|
||||
:show-column-filters="showColumnFilters"
|
||||
/>
|
||||
<FunctionCapabilities
|
||||
v-if="rdocData.meta.flavor === 'static' && showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
|
||||
:data="rdocData"
|
||||
:show-library-rules="showLibraryRules"
|
||||
/>
|
||||
<ProcessCapabilities
|
||||
v-else-if="rdocData.meta.flavor === 'dynamic' && showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
|
||||
:data="rdocData"
|
||||
:show-capabilities-by-process="showCapabilitiesByFunctionOrProcess"
|
||||
:show-library-rules="showLibraryRules"
|
||||
/>
|
||||
<NamespaceChart v-else-if="showNamespaceChart" :data="rdocData" />
|
||||
</template>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user