mirror of
https://github.com/mandiant/capa.git
synced 2026-02-04 11:07:53 -08:00
update .prettierrc.json, and reformat code
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": false,
|
||||
"printWidth": 120,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<template>
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<BannerHeader />
|
||||
<NavBar />
|
||||
</div>
|
||||
</header>
|
||||
<RouterView />
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<BannerHeader />
|
||||
<NavBar />
|
||||
</div>
|
||||
</header>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
import NavBar from './components/NavBar.vue'
|
||||
import BannerHeader from './components/BannerHeader.vue'
|
||||
import { RouterView } from "vue-router";
|
||||
import NavBar from "./components/NavBar.vue";
|
||||
import BannerHeader from "./components/BannerHeader.vue";
|
||||
</script>
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
body {
|
||||
margin: 0 auto;
|
||||
font-weight: normal;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
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;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: color 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--primary-color);
|
||||
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;
|
||||
}
|
||||
.p-treetable-tbody > tr:not(:is([aria-level="1"])) > td {
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="showBanner"
|
||||
class="bg-bluegray-900 text-gray-100 flex justify-content-between lg:justify-content-center align-items-center flex-wrap"
|
||||
>
|
||||
<div class="font-bold mr-8">This is an early release</div>
|
||||
<div class="align-items-center hidden lg:flex">
|
||||
<span class="line-height-3">Please report any bugs, enhancements or features in the </span>
|
||||
<a v-ripple href="https://github.com/mandiant/capa/issues" class="flex align-items-center ml-2 mr-8 text-white">
|
||||
<span class="no-underline font-bold">Github issues</span>
|
||||
<i class="pi pi-github ml-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
v-ripple
|
||||
@click="closeBanner"
|
||||
class="flex align-items-center no-underline justify-content-center border-circle text-gray-50 hover:bg-bluegray-700 cursor-pointer transition-colors transition-duration-150"
|
||||
style="width: 2rem; height: 2rem"
|
||||
<div
|
||||
v-if="showBanner"
|
||||
class="bg-bluegray-900 text-gray-100 flex justify-content-between lg:justify-content-center align-items-center flex-wrap"
|
||||
>
|
||||
<i class="pi pi-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="font-bold mr-8">This is an early release</div>
|
||||
<div class="align-items-center hidden lg:flex">
|
||||
<span class="line-height-3">Please report any bugs, enhancements or features in the </span>
|
||||
<a
|
||||
v-ripple
|
||||
href="https://github.com/mandiant/capa/issues"
|
||||
class="flex align-items-center ml-2 mr-8 text-white"
|
||||
>
|
||||
<span class="no-underline font-bold">Github issues</span>
|
||||
<i class="pi pi-github ml-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
v-ripple
|
||||
@click="closeBanner"
|
||||
class="flex align-items-center no-underline justify-content-center border-circle text-gray-50 hover:bg-bluegray-700 cursor-pointer transition-colors transition-duration-150"
|
||||
style="width: 2rem; height: 2rem"
|
||||
>
|
||||
<i class="pi pi-times"></i>
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, onMounted } from "vue";
|
||||
|
||||
const showBanner = ref(true)
|
||||
const showBanner = ref(true);
|
||||
|
||||
onMounted(() => {
|
||||
const bannerHidden = localStorage.getItem('bannerHidden')
|
||||
if (bannerHidden === 'true') {
|
||||
showBanner.value = false
|
||||
}
|
||||
})
|
||||
const bannerHidden = localStorage.getItem("bannerHidden");
|
||||
if (bannerHidden === "true") {
|
||||
showBanner.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
const closeBanner = () => {
|
||||
showBanner.value = false
|
||||
localStorage.setItem('bannerHidden', 'true')
|
||||
}
|
||||
showBanner.value = false;
|
||||
localStorage.setItem("bannerHidden", "true");
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,47 +1,45 @@
|
||||
<template>
|
||||
<div class="flex flex-column align-items-center">
|
||||
<div class="text-center">
|
||||
<h1>
|
||||
<img src="../assets/images/logo-full.png" alt="Capa: identify program capabilities" />
|
||||
<h6 class="font-medium" style="color: rgb(176, 26, 26)">
|
||||
capa: identify program capabilities
|
||||
</h6>
|
||||
</h1>
|
||||
<div class="flex flex-column align-items-center">
|
||||
<div class="text-center">
|
||||
<h1>
|
||||
<img src="../assets/images/logo-full.png" alt="Capa: identify program capabilities" />
|
||||
<h6 class="font-medium" style="color: rgb(176, 26, 26)">capa: identify program capabilities</h6>
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xl max-w-75rem" style="max-width: 75ch">
|
||||
Capa-WebUI is a web-based tool for exploring the capabilities identified in a program. It can be used to
|
||||
search and display the rule matches in different viewing modes.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-content-center w-full mb-3 gap-4">
|
||||
<Button label="Help" @click="openDialog('help')" icon="pi pi-info-circle" outlined />
|
||||
<Button label="Changelog" @click="openDialog('changelog')" icon="pi pi-history" outlined />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xl max-w-75rem" style="max-width: 75ch">
|
||||
Capa-WebUI is a web-based tool for exploring the capabilities identified in a program. It
|
||||
can be used to search and display the rule matches in different viewing modes.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex justify-content-center w-full mb-3 gap-4">
|
||||
<Button label="Help" @click="openDialog('help')" icon="pi pi-info-circle" outlined />
|
||||
<Button label="Changelog" @click="openDialog('changelog')" icon="pi pi-history" outlined />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog v-model:visible="helpDialogVisible" header="Help" modal>
|
||||
<p>TODO: Help instruction to walk through users how generate a capa report.</p>
|
||||
</Dialog>
|
||||
<Dialog v-model:visible="helpDialogVisible" header="Help" modal>
|
||||
<p>TODO: Help instruction to walk through users how generate a capa report.</p>
|
||||
</Dialog>
|
||||
|
||||
<Dialog v-model:visible="changelogDialogVisible" header="Changelog" modal>
|
||||
<p>TODO: Add major changes to the tool here.</p>
|
||||
</Dialog>
|
||||
<Dialog v-model:visible="changelogDialogVisible" header="Changelog" modal>
|
||||
<p>TODO: Add major changes to the tool here.</p>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import Button from 'primevue/button'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import { ref } from "vue";
|
||||
import Button from "primevue/button";
|
||||
import Dialog from "primevue/dialog";
|
||||
|
||||
const helpDialogVisible = ref(false)
|
||||
const changelogDialogVisible = ref(false)
|
||||
const helpDialogVisible = ref(false);
|
||||
const changelogDialogVisible = ref(false);
|
||||
|
||||
const openDialog = (type) => {
|
||||
if (type === 'help') {
|
||||
helpDialogVisible.value = true
|
||||
} else if (type === 'changelog') {
|
||||
changelogDialogVisible.value = true
|
||||
}
|
||||
}
|
||||
if (type === "help") {
|
||||
helpDialogVisible.value = true;
|
||||
} else if (type === "changelog") {
|
||||
changelogDialogVisible.value = true;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,81 +1,81 @@
|
||||
<template>
|
||||
<div>
|
||||
<DataTable
|
||||
:value="tableData"
|
||||
rowGroupMode="rowspan"
|
||||
groupRowsBy="funcaddr"
|
||||
sortMode="single"
|
||||
removableSort
|
||||
size="small"
|
||||
:filters="filters"
|
||||
:rowHover="true"
|
||||
:filterMode="filterMode"
|
||||
filterDisplay="menu"
|
||||
:globalFilterFields="['funcaddr', 'ruleName', 'namespace']"
|
||||
>
|
||||
<template #header>
|
||||
<InputText v-model="filters['global'].value" placeholder="Global Search" />
|
||||
</template>
|
||||
<div>
|
||||
<DataTable
|
||||
:value="tableData"
|
||||
rowGroupMode="rowspan"
|
||||
groupRowsBy="funcaddr"
|
||||
sortMode="single"
|
||||
removableSort
|
||||
size="small"
|
||||
:filters="filters"
|
||||
:rowHover="true"
|
||||
:filterMode="filterMode"
|
||||
filterDisplay="menu"
|
||||
:globalFilterFields="['funcaddr', 'ruleName', 'namespace']"
|
||||
>
|
||||
<template #header>
|
||||
<InputText v-model="filters['global'].value" placeholder="Global Search" />
|
||||
</template>
|
||||
|
||||
<Column field="funcaddr" sortable header="Function Address" :rowspan="3" class="w-min">
|
||||
<template #body="slotProps">
|
||||
<span style="font-family: monospace">{{ slotProps.data.funcaddr }}</span>
|
||||
<span v-if="slotProps.data.matchCount > 1" class="font-italic">
|
||||
({{ slotProps.data.matchCount }} matches)
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="funcaddr" sortable header="Function Address" :rowspan="3" class="w-min">
|
||||
<template #body="slotProps">
|
||||
<span style="font-family: monospace">{{ slotProps.data.funcaddr }}</span>
|
||||
<span v-if="slotProps.data.matchCount > 1" class="font-italic">
|
||||
({{ slotProps.data.matchCount }} matches)
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="ruleName" header="Matches" class="w-min">
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.ruleName }}
|
||||
<LibraryTag v-if="slotProps.data.lib" />
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="ruleName" header="Matches" class="w-min">
|
||||
<template #body="slotProps">
|
||||
{{ slotProps.data.ruleName }}
|
||||
<LibraryTag v-if="slotProps.data.lib" />
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column field="namespace" header="Namespace"></Column>
|
||||
</DataTable>
|
||||
<Column field="namespace" header="Namespace"></Column>
|
||||
</DataTable>
|
||||
|
||||
<Dialog v-model:visible="sourceDialogVisible" :style="{ width: '50vw' }">
|
||||
<highlightjs lang="yml" :code="currentSource" class="bg-white" />
|
||||
</Dialog>
|
||||
</div>
|
||||
<Dialog v-model:visible="sourceDialogVisible" :style="{ width: '50vw' }">
|
||||
<highlightjs lang="yml" :code="currentSource" class="bg-white" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import DataTable from 'primevue/datatable'
|
||||
import Column from 'primevue/column'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import LibraryTag from './misc/LibraryTag.vue'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import { ref, computed } from "vue";
|
||||
import DataTable from "primevue/datatable";
|
||||
import Column from "primevue/column";
|
||||
import Dialog from "primevue/dialog";
|
||||
import LibraryTag from "./misc/LibraryTag.vue";
|
||||
import InputText from "primevue/inputtext";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showLibraryRules: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showLibraryRules: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const filters = ref({
|
||||
global: { value: null, matchMode: 'contains' }
|
||||
})
|
||||
const filterMode = ref('lenient')
|
||||
const sourceDialogVisible = ref(false)
|
||||
const currentSource = ref('')
|
||||
global: { value: null, matchMode: "contains" }
|
||||
});
|
||||
const filterMode = ref("lenient");
|
||||
const sourceDialogVisible = ref(false);
|
||||
const currentSource = ref("");
|
||||
|
||||
import { parseFunctionCapabilities } from '../utils/rdocParser'
|
||||
import { parseFunctionCapabilities } from "../utils/rdocParser";
|
||||
|
||||
const tableData = computed(() => parseFunctionCapabilities(props.data, props.showLibraryRules))
|
||||
const tableData = computed(() => parseFunctionCapabilities(props.data, props.showLibraryRules));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* tighten up the spacing between rows */
|
||||
:deep(.p-datatable.p-datatable-sm .p-datatable-tbody > tr > td) {
|
||||
padding: 0.1rem 0.5rem !important;
|
||||
padding: 0.1rem 0.5rem !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,114 +1,111 @@
|
||||
<template>
|
||||
<!-- Main container with gradient background -->
|
||||
<div
|
||||
class="flex align-items-center justify-content-between w-full p-3 shadow-1"
|
||||
:style="{ background: 'linear-gradient(to right, #2c3e50, #3498db)' }"
|
||||
>
|
||||
<!-- File information section -->
|
||||
<div class="flex-grow-1 mr-3">
|
||||
<h1 class="text-xl m-0 text-overflow-ellipsis overflow-hidden white-space-nowrap text-white">
|
||||
{{ fileName }}
|
||||
</h1>
|
||||
<p class="text-xs mt-1 mb-0 text-white-alpha-70">SHA256:
|
||||
<a :href="`https://www.virustotal.com/gui/file/${sha256}`" target="_blank">{{ sha256 }}
|
||||
</a>
|
||||
</p>
|
||||
<!-- Main container with gradient background -->
|
||||
<div
|
||||
class="flex align-items-center justify-content-between w-full p-3 shadow-1"
|
||||
:style="{ background: 'linear-gradient(to right, #2c3e50, #3498db)' }"
|
||||
>
|
||||
<!-- File information section -->
|
||||
<div class="flex-grow-1 mr-3">
|
||||
<h1 class="text-xl m-0 text-overflow-ellipsis overflow-hidden white-space-nowrap text-white">
|
||||
{{ fileName }}
|
||||
</h1>
|
||||
<p class="text-xs mt-1 mb-0 text-white-alpha-70">
|
||||
SHA256:
|
||||
<a :href="`https://www.virustotal.com/gui/file/${sha256}`" target="_blank">{{ sha256 }} </a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Vertical divider -->
|
||||
<div class="mx-3 bg-white-alpha-30" style="width: 1px; height: 30px"></div>
|
||||
|
||||
<!-- Analysis information section -->
|
||||
<div class="flex-grow-1 mr-3">
|
||||
<!-- OS • Program Format • Arch -->
|
||||
<div class="flex align-items-center text-sm m-0 line-height-3 text-white">
|
||||
<span class="capitalize">{{ data.meta.analysis.os }}</span>
|
||||
<span class="ml-2 mr-2 text-white-alpha-30"> • </span>
|
||||
<span class="uppercase">{{ data.meta.analysis.format }}</span>
|
||||
<span class="ml-2 mr-2 text-white-alpha-30"> • </span>
|
||||
<span class="uppercase">{{ data.meta.analysis.arch }}</span>
|
||||
</div>
|
||||
<!-- Flavor • Extractor • CAPA Version • Timestamp -->
|
||||
<div class="flex align-items-center text-sm m-0 line-height-3 text-white">
|
||||
<span class="inline-flex">
|
||||
<span class="capitalize">{{ flavor }}</span>
|
||||
</span>
|
||||
<span class="ml-1">analysis using</span>
|
||||
<span class="ml-1">{{ data.meta.analysis.extractor.split(/(Feature)?Extractor/)[0] }}</span>
|
||||
<span class="mx-2 text-white-alpha-30"> • </span>
|
||||
<span>CAPA v{{ data.meta.version }}</span>
|
||||
<span class="mx-2 text-white-alpha-30"> • </span>
|
||||
<span>{{ new Date(data.meta.timestamp).toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vertical divider -->
|
||||
<div class="mx-3 bg-white-alpha-30" style="width: 1px; height: 30px"></div>
|
||||
|
||||
<!-- Key metrics section -->
|
||||
<div class="flex justify-content-around flex-grow-1">
|
||||
<!-- Rules count -->
|
||||
<div class="text-center">
|
||||
<span class="block text-xl font-bold text-white">{{ keyMetrics.ruleCount }}</span>
|
||||
<span class="block text-xs uppercase text-white-alpha-70">Rules</span>
|
||||
</div>
|
||||
<!-- Namespaces count -->
|
||||
<div class="text-center">
|
||||
<span class="block text-xl font-bold text-white">{{ keyMetrics.namespaceCount }}</span>
|
||||
<span class="block text-xs uppercase text-white-alpha-70">Namespaces</span>
|
||||
</div>
|
||||
<!-- Functions or Processes count -->
|
||||
<div class="text-center">
|
||||
<span class="block text-xl font-bold text-white">{{ keyMetrics.functionOrProcessCount }}</span>
|
||||
<span class="block text-xs uppercase text-white-alpha-70">
|
||||
{{ flavor === "static" ? "Functions" : "Processes" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vertical divider -->
|
||||
<div class="mx-3 bg-white-alpha-30" style="width: 1px; height: 30px"></div>
|
||||
|
||||
<!-- Analysis information section -->
|
||||
<div class="flex-grow-1 mr-3">
|
||||
<!-- OS • Program Format • Arch -->
|
||||
<div class="flex align-items-center text-sm m-0 line-height-3 text-white">
|
||||
<span class="capitalize">{{ data.meta.analysis.os }}</span>
|
||||
<span class="ml-2 mr-2 text-white-alpha-30"> • </span>
|
||||
<span class="uppercase">{{ data.meta.analysis.format }}</span>
|
||||
<span class="ml-2 mr-2 text-white-alpha-30"> • </span>
|
||||
<span class="uppercase">{{ data.meta.analysis.arch }}</span>
|
||||
</div>
|
||||
<!-- Flavor • Extractor • CAPA Version • Timestamp -->
|
||||
<div class="flex align-items-center text-sm m-0 line-height-3 text-white">
|
||||
<span class="inline-flex">
|
||||
<span class="capitalize">{{ flavor }}</span>
|
||||
</span>
|
||||
<span class="ml-1">analysis using</span>
|
||||
<span class="ml-1">{{ data.meta.analysis.extractor.split(/(Feature)?Extractor/)[0] }}</span>
|
||||
<span class="mx-2 text-white-alpha-30"> • </span>
|
||||
<span>CAPA v{{ data.meta.version }}</span>
|
||||
<span class="mx-2 text-white-alpha-30"> • </span>
|
||||
<span>{{ new Date(data.meta.timestamp).toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vertical divider -->
|
||||
<div class="mx-3 bg-white-alpha-30" style="width: 1px; height: 30px"></div>
|
||||
|
||||
<!-- Key metrics section -->
|
||||
<div class="flex justify-content-around flex-grow-1">
|
||||
<!-- Rules count -->
|
||||
<div class="text-center">
|
||||
<span class="block text-xl font-bold text-white">{{ keyMetrics.ruleCount }}</span>
|
||||
<span class="block text-xs uppercase text-white-alpha-70">Rules</span>
|
||||
</div>
|
||||
<!-- Namespaces count -->
|
||||
<div class="text-center">
|
||||
<span class="block text-xl font-bold text-white">{{ keyMetrics.namespaceCount }}</span>
|
||||
<span class="block text-xs uppercase text-white-alpha-70">Namespaces</span>
|
||||
</div>
|
||||
<!-- Functions or Processes count -->
|
||||
<div class="text-center">
|
||||
<span class="block text-xl font-bold text-white">{{
|
||||
keyMetrics.functionOrProcessCount
|
||||
}}</span>
|
||||
<span class="block text-xs uppercase text-white-alpha-70">
|
||||
{{ flavor === 'static' ? 'Functions' : 'Processes' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const keyMetrics = ref({
|
||||
ruleCount: 0,
|
||||
namespaceCount: 0,
|
||||
functionOrProcessCount: 0
|
||||
})
|
||||
ruleCount: 0,
|
||||
namespaceCount: 0,
|
||||
functionOrProcessCount: 0
|
||||
});
|
||||
|
||||
// get the filename from the path, e.g. "malware.exe" from "/home/user/malware.exe"
|
||||
const fileName = computed(() => props.data.meta.sample.path.split('/').pop())
|
||||
const fileName = computed(() => props.data.meta.sample.path.split("/").pop());
|
||||
// get the flavor from the metadata, e.g. "dynamic" or "static"
|
||||
const flavor = computed(() => props.data.meta.flavor)
|
||||
const flavor = computed(() => props.data.meta.flavor);
|
||||
// get the SHA256 hash from the metadata
|
||||
const sha256 = computed(() => props.data.meta.sample.sha256.toUpperCase())
|
||||
const sha256 = computed(() => props.data.meta.sample.sha256.toUpperCase());
|
||||
|
||||
// Function to parse metadata and update key metrics
|
||||
const parseMetadata = () => {
|
||||
if (props.data) {
|
||||
keyMetrics.value = {
|
||||
ruleCount: Object.keys(props.data.rules).length,
|
||||
namespaceCount: new Set(Object.values(props.data.rules).map((rule) => rule.meta.namespace))
|
||||
.size,
|
||||
functionOrProcessCount:
|
||||
flavor.value === 'static'
|
||||
? props.data.meta.analysis.feature_counts.functions.length
|
||||
: props.data.meta.analysis.feature_counts.processes.length
|
||||
if (props.data) {
|
||||
keyMetrics.value = {
|
||||
ruleCount: Object.keys(props.data.rules).length,
|
||||
namespaceCount: new Set(Object.values(props.data.rules).map((rule) => rule.meta.namespace)).size,
|
||||
functionOrProcessCount:
|
||||
flavor.value === "static"
|
||||
? props.data.meta.analysis.feature_counts.functions.length
|
||||
: props.data.meta.analysis.feature_counts.processes.length
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Call parseMetadata when the component is mounted
|
||||
onMounted(() => {
|
||||
parseMetadata()
|
||||
})
|
||||
parseMetadata();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,126 +1,126 @@
|
||||
<template>
|
||||
<div ref="chartRef" class="w-screen h-screen"></div>
|
||||
<div ref="chartRef" class="w-screen h-screen"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import Plotly from 'plotly.js-dist'
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import Plotly from "plotly.js-dist";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const chartRef = ref(null)
|
||||
const chartRef = ref(null);
|
||||
|
||||
const createSunburstData = (rules) => {
|
||||
const data = {
|
||||
ids: [],
|
||||
labels: [],
|
||||
parents: [],
|
||||
values: []
|
||||
}
|
||||
const data = {
|
||||
ids: [],
|
||||
labels: [],
|
||||
parents: [],
|
||||
values: []
|
||||
};
|
||||
|
||||
const addNamespace = (namespace, value) => {
|
||||
const parts = namespace.split('/')
|
||||
let currentId = ''
|
||||
let parent = ''
|
||||
const addNamespace = (namespace, value) => {
|
||||
const parts = namespace.split("/");
|
||||
let currentId = "";
|
||||
let parent = "";
|
||||
|
||||
parts.forEach((part) => {
|
||||
currentId = currentId ? `${currentId}/${part}` : part
|
||||
parts.forEach((part) => {
|
||||
currentId = currentId ? `${currentId}/${part}` : part;
|
||||
|
||||
if (!data.ids.includes(currentId)) {
|
||||
data.ids.push(currentId)
|
||||
data.labels.push(part)
|
||||
data.parents.push(parent)
|
||||
data.values.push(0)
|
||||
}
|
||||
if (!data.ids.includes(currentId)) {
|
||||
data.ids.push(currentId);
|
||||
data.labels.push(part);
|
||||
data.parents.push(parent);
|
||||
data.values.push(0);
|
||||
}
|
||||
|
||||
const valueIndex = data.ids.indexOf(currentId)
|
||||
data.values[valueIndex] += value
|
||||
const valueIndex = data.ids.indexOf(currentId);
|
||||
data.values[valueIndex] += value;
|
||||
|
||||
parent = currentId
|
||||
})
|
||||
parent = currentId;
|
||||
});
|
||||
|
||||
return parent
|
||||
}
|
||||
return parent;
|
||||
};
|
||||
|
||||
Object.entries(rules).forEach(([ruleName, rule]) => {
|
||||
if (rule.meta.lib) return // Skip library rules
|
||||
Object.entries(rules).forEach(([ruleName, rule]) => {
|
||||
if (rule.meta.lib) return; // Skip library rules
|
||||
|
||||
const namespace = rule.meta.namespace || 'root'
|
||||
const parent = addNamespace(namespace, rule.matches.length)
|
||||
const namespace = rule.meta.namespace || "root";
|
||||
const parent = addNamespace(namespace, rule.matches.length);
|
||||
|
||||
// Add the rule itself
|
||||
data.ids.push(ruleName)
|
||||
data.labels.push(rule.meta.name)
|
||||
data.parents.push(parent)
|
||||
data.values.push(rule.matches.length)
|
||||
})
|
||||
// Add the rule itself
|
||||
data.ids.push(ruleName);
|
||||
data.labels.push(rule.meta.name);
|
||||
data.parents.push(parent);
|
||||
data.values.push(rule.matches.length);
|
||||
});
|
||||
|
||||
return data
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const renderChart = () => {
|
||||
if (!chartRef.value) return
|
||||
if (!chartRef.value) return;
|
||||
|
||||
const sunburstData = createSunburstData(props.data.rules)
|
||||
const sunburstData = createSunburstData(props.data.rules);
|
||||
|
||||
const layout = {
|
||||
margin: { l: 0, r: 0, b: 0, t: 0 },
|
||||
sunburstcolorway: [
|
||||
'#636efa',
|
||||
'#EF553B',
|
||||
'#00cc96',
|
||||
'#ab63fa',
|
||||
'#19d3f3',
|
||||
'#e763fa',
|
||||
'#FECB52',
|
||||
'#FFA15A',
|
||||
'#FF6692',
|
||||
'#B6E880'
|
||||
],
|
||||
extendsunburstcolorway: true
|
||||
}
|
||||
const layout = {
|
||||
margin: { l: 0, r: 0, b: 0, t: 0 },
|
||||
sunburstcolorway: [
|
||||
"#636efa",
|
||||
"#EF553B",
|
||||
"#00cc96",
|
||||
"#ab63fa",
|
||||
"#19d3f3",
|
||||
"#e763fa",
|
||||
"#FECB52",
|
||||
"#FFA15A",
|
||||
"#FF6692",
|
||||
"#B6E880"
|
||||
],
|
||||
extendsunburstcolorway: true
|
||||
};
|
||||
|
||||
const config = {
|
||||
responsive: true
|
||||
}
|
||||
const config = {
|
||||
responsive: true
|
||||
};
|
||||
|
||||
Plotly.newPlot(
|
||||
chartRef.value,
|
||||
[
|
||||
{
|
||||
type: 'sunburst',
|
||||
ids: sunburstData.ids,
|
||||
labels: sunburstData.labels,
|
||||
parents: sunburstData.parents,
|
||||
values: sunburstData.values,
|
||||
outsidetextfont: { size: 20, color: '#377eb8' },
|
||||
leaf: { opacity: 0.6 },
|
||||
marker: { line: { width: 2 } },
|
||||
branchvalues: 'total'
|
||||
}
|
||||
],
|
||||
layout,
|
||||
config
|
||||
)
|
||||
Plotly.newPlot(
|
||||
chartRef.value,
|
||||
[
|
||||
{
|
||||
type: "sunburst",
|
||||
ids: sunburstData.ids,
|
||||
labels: sunburstData.labels,
|
||||
parents: sunburstData.parents,
|
||||
values: sunburstData.values,
|
||||
outsidetextfont: { size: 20, color: "#377eb8" },
|
||||
leaf: { opacity: 0.6 },
|
||||
marker: { line: { width: 2 } },
|
||||
branchvalues: "total"
|
||||
}
|
||||
],
|
||||
layout,
|
||||
config
|
||||
);
|
||||
|
||||
return sunburstData
|
||||
}
|
||||
return sunburstData;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const data = renderChart()
|
||||
console.log(data)
|
||||
})
|
||||
const data = renderChart();
|
||||
console.log(data);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
renderChart()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
() => props.data,
|
||||
() => {
|
||||
renderChart();
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref } from "vue";
|
||||
|
||||
import Menubar from 'primevue/menubar'
|
||||
import Menubar from "primevue/menubar";
|
||||
|
||||
const items = ref([
|
||||
{
|
||||
label: 'Import Analysis',
|
||||
icon: 'pi pi-file-import',
|
||||
// TODO(s-ff): This is not the conventinal way of navigating to a new page.
|
||||
command: () => window.location.replace(window.location.origin + '/capa/') // reload the page
|
||||
}
|
||||
])
|
||||
{
|
||||
label: "Import Analysis",
|
||||
icon: "pi pi-file-import",
|
||||
// TODO(s-ff): This is not the conventinal way of navigating to a new page.
|
||||
command: () => window.location.replace(window.location.origin + "/capa/") // reload the page
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Menubar :model="items" class="p-1">
|
||||
<template #end>
|
||||
<div class="flex align-items-center gap-3">
|
||||
<a
|
||||
v-ripple
|
||||
href="https://github.com/mandiant/capa"
|
||||
class="flex align-items-center justify-content-center text-color w-2rem"
|
||||
>
|
||||
<i id="github-icon" class="pi pi-github text-2xl"></i>
|
||||
</a>
|
||||
<img src="../assets/images/icon.png" alt="Logo" class="w-2rem" />
|
||||
</div>
|
||||
</template>
|
||||
</Menubar>
|
||||
<Menubar :model="items" class="p-1">
|
||||
<template #end>
|
||||
<div class="flex align-items-center gap-3">
|
||||
<a
|
||||
v-ripple
|
||||
href="https://github.com/mandiant/capa"
|
||||
class="flex align-items-center justify-content-center text-color w-2rem"
|
||||
>
|
||||
<i id="github-icon" class="pi pi-github text-2xl"></i>
|
||||
</a>
|
||||
<img src="../assets/images/icon.png" alt="Logo" class="w-2rem" />
|
||||
</div>
|
||||
</template>
|
||||
</Menubar>
|
||||
</template>
|
||||
|
||||
@@ -1,223 +1,223 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<TreeTable
|
||||
:value="processTree"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
:filters="filters"
|
||||
filterMode="lenient"
|
||||
sortField="pid"
|
||||
:sortOrder="1"
|
||||
rowHover="true"
|
||||
>
|
||||
<Column field="processname" header="Process" expander>
|
||||
<template #body="slotProps">
|
||||
<span
|
||||
:id="'process-' + slotProps.node.key"
|
||||
class="cursor-pointer flex align-items-center"
|
||||
@mouseenter="showTooltip($event, slotProps.node)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<span
|
||||
class="text-lg text-overflow-ellipsis overflow-hidden white-space-nowrap inline-block max-w-20rem"
|
||||
style="font-family: monospace"
|
||||
>
|
||||
{{ slotProps.node.data.processname }}
|
||||
</span>
|
||||
<span class="ml-2"> - PID: {{ slotProps.node.data.pid }} </span>
|
||||
<span v-if="slotProps.node.data.uniqueMatchCount > 0" class="font-italic ml-2">
|
||||
({{ slotProps.node.data.uniqueMatchCount }} unique
|
||||
{{ slotProps.node.data.uniqueMatchCount > 1 ? 'matches' : 'match' }})
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="pid" header="PID" sortable>
|
||||
<template #body="slotProps">
|
||||
<span :style="{ color: getColorForId(slotProps.node.data.pid) }">
|
||||
{{ slotProps.node.data.pid }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="ppid" header="PPID" sortable>
|
||||
<template #body="slotProps">
|
||||
<span :style="{ color: getColorForId(slotProps.node.data.ppid) }">
|
||||
{{ slotProps.node.data.ppid }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
</TreeTable>
|
||||
|
||||
<div
|
||||
v-if="tooltipVisible"
|
||||
class="fixed bg-gray-800 text-white p-3 border-round-sm z-5 max-w-50rem shadow-2"
|
||||
:style="tooltipStyle"
|
||||
>
|
||||
<div v-for="rule in currentNode.data.uniqueRules" :key="rule.name">
|
||||
• {{ rule.name }}
|
||||
<span class="font-italic"
|
||||
>({{ rule.matchCount }} {{ rule.scope }} {{ rule.matchCount > 1 ? 'matches' : 'match' }})</span
|
||||
<div class="card">
|
||||
<TreeTable
|
||||
:value="processTree"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
:filters="filters"
|
||||
filterMode="lenient"
|
||||
sortField="pid"
|
||||
:sortOrder="1"
|
||||
rowHover="true"
|
||||
>
|
||||
<LibraryTag v-if="rule.lib" />
|
||||
</div>
|
||||
<Column field="processname" header="Process" expander>
|
||||
<template #body="slotProps">
|
||||
<span
|
||||
:id="'process-' + slotProps.node.key"
|
||||
class="cursor-pointer flex align-items-center"
|
||||
@mouseenter="showTooltip($event, slotProps.node)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<span
|
||||
class="text-lg text-overflow-ellipsis overflow-hidden white-space-nowrap inline-block max-w-20rem"
|
||||
style="font-family: monospace"
|
||||
>
|
||||
{{ slotProps.node.data.processname }}
|
||||
</span>
|
||||
<span class="ml-2"> - PID: {{ slotProps.node.data.pid }} </span>
|
||||
<span v-if="slotProps.node.data.uniqueMatchCount > 0" class="font-italic ml-2">
|
||||
({{ slotProps.node.data.uniqueMatchCount }} unique
|
||||
{{ slotProps.node.data.uniqueMatchCount > 1 ? "matches" : "match" }})
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="pid" header="PID" sortable>
|
||||
<template #body="slotProps">
|
||||
<span :style="{ color: getColorForId(slotProps.node.data.pid) }">
|
||||
{{ slotProps.node.data.pid }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
<Column field="ppid" header="PPID" sortable>
|
||||
<template #body="slotProps">
|
||||
<span :style="{ color: getColorForId(slotProps.node.data.ppid) }">
|
||||
{{ slotProps.node.data.ppid }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
</TreeTable>
|
||||
|
||||
<div
|
||||
v-if="tooltipVisible"
|
||||
class="fixed bg-gray-800 text-white p-3 border-round-sm z-5 max-w-50rem shadow-2"
|
||||
:style="tooltipStyle"
|
||||
>
|
||||
<div v-for="rule in currentNode.data.uniqueRules" :key="rule.name">
|
||||
• {{ rule.name }}
|
||||
<span class="font-italic"
|
||||
>({{ rule.matchCount }} {{ rule.scope }} {{ rule.matchCount > 1 ? "matches" : "match" }})</span
|
||||
>
|
||||
<LibraryTag v-if="rule.lib" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import TreeTable from 'primevue/treetable'
|
||||
import Column from 'primevue/column'
|
||||
import LibraryTag from './misc/LibraryTag.vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||
import TreeTable from "primevue/treetable";
|
||||
import Column from "primevue/column";
|
||||
import LibraryTag from "./misc/LibraryTag.vue";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showLibraryRules: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showLibraryRules: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const filters = ref({})
|
||||
const expandedKeys = ref({})
|
||||
const tooltipVisible = ref(false)
|
||||
const currentNode = ref(null)
|
||||
const filters = ref({});
|
||||
const expandedKeys = ref({});
|
||||
const tooltipVisible = ref(false);
|
||||
const currentNode = ref(null);
|
||||
const tooltipStyle = ref({
|
||||
position: 'fixed',
|
||||
top: '0px',
|
||||
left: '0px'
|
||||
})
|
||||
position: "fixed",
|
||||
top: "0px",
|
||||
left: "0px"
|
||||
});
|
||||
|
||||
const getProcessIds = (location) => {
|
||||
if (!location || location.type === 'no address') {
|
||||
return null
|
||||
}
|
||||
if (Array.isArray(location.value) && location.value.length >= 2) {
|
||||
return {
|
||||
ppid: location.value[0],
|
||||
pid: location.value[1]
|
||||
if (!location || location.type === "no address") {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (Array.isArray(location.value) && location.value.length >= 2) {
|
||||
return {
|
||||
ppid: location.value[0],
|
||||
pid: location.value[1]
|
||||
};
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const processTree = computed(() => {
|
||||
if (
|
||||
!props.data ||
|
||||
!props.data.meta ||
|
||||
!props.data.meta.analysis ||
|
||||
!props.data.meta.analysis.layout ||
|
||||
!props.data.meta.analysis.layout.processes
|
||||
) {
|
||||
console.error('Invalid data structure')
|
||||
return []
|
||||
}
|
||||
|
||||
const processes = props.data.meta.analysis.layout.processes
|
||||
const rules = props.data.rules || {}
|
||||
const processMap = new Map()
|
||||
|
||||
// create all process nodes
|
||||
processes.forEach((process) => {
|
||||
if (!process.address || !Array.isArray(process.address.value) || process.address.value.length < 2) {
|
||||
console.warn('Invalid process structure', process)
|
||||
return
|
||||
if (
|
||||
!props.data ||
|
||||
!props.data.meta ||
|
||||
!props.data.meta.analysis ||
|
||||
!props.data.meta.analysis.layout ||
|
||||
!props.data.meta.analysis.layout.processes
|
||||
) {
|
||||
console.error("Invalid data structure");
|
||||
return [];
|
||||
}
|
||||
const [ppid, pid] = process.address.value
|
||||
processMap.set(pid, {
|
||||
key: `process-${pid}`,
|
||||
data: {
|
||||
processname: process.name || '<Unknown Process>',
|
||||
pid,
|
||||
ppid,
|
||||
uniqueMatchCount: 0,
|
||||
uniqueRules: new Map()
|
||||
},
|
||||
children: []
|
||||
})
|
||||
})
|
||||
|
||||
// build the tree structure and add rule matches
|
||||
Object.entries(rules).forEach(([ruleName, rule]) => {
|
||||
if (!props.showLibraryRules && rule.meta && rule.meta.lib) return
|
||||
if (!rule.matches || !Array.isArray(rule.matches)) return
|
||||
const processes = props.data.meta.analysis.layout.processes;
|
||||
const rules = props.data.rules || {};
|
||||
const processMap = new Map();
|
||||
|
||||
rule.matches.forEach((match) => {
|
||||
if (!Array.isArray(match) || match.length === 0) return
|
||||
const [location] = match
|
||||
const ids = getProcessIds(location)
|
||||
if (ids && processMap.has(ids.pid)) {
|
||||
const processNode = processMap.get(ids.pid)
|
||||
if (!processNode.data.uniqueRules.has(ruleName)) {
|
||||
processNode.data.uniqueMatchCount++
|
||||
processNode.data.uniqueRules.set(ruleName, {
|
||||
name: ruleName,
|
||||
lib: rule.meta && rule.meta.lib,
|
||||
matchCount: 0,
|
||||
scope: location.type
|
||||
})
|
||||
// create all process nodes
|
||||
processes.forEach((process) => {
|
||||
if (!process.address || !Array.isArray(process.address.value) || process.address.value.length < 2) {
|
||||
console.warn("Invalid process structure", process);
|
||||
return;
|
||||
}
|
||||
processNode.data.uniqueRules.get(ruleName).matchCount++
|
||||
}
|
||||
})
|
||||
})
|
||||
// build the final tree structure
|
||||
const rootProcesses = []
|
||||
processMap.forEach((processNode) => {
|
||||
console.log(processNode)
|
||||
processNode.data.uniqueRules = Array.from(processNode.data.uniqueRules.values())
|
||||
const parentProcess = processMap.get(processNode.data.ppid)
|
||||
if (parentProcess) {
|
||||
parentProcess.children.push(processNode)
|
||||
} else {
|
||||
rootProcesses.push(processNode)
|
||||
}
|
||||
})
|
||||
const [ppid, pid] = process.address.value;
|
||||
processMap.set(pid, {
|
||||
key: `process-${pid}`,
|
||||
data: {
|
||||
processname: process.name || "<Unknown Process>",
|
||||
pid,
|
||||
ppid,
|
||||
uniqueMatchCount: 0,
|
||||
uniqueRules: new Map()
|
||||
},
|
||||
children: []
|
||||
});
|
||||
});
|
||||
|
||||
return rootProcesses
|
||||
})
|
||||
// build the tree structure and add rule matches
|
||||
Object.entries(rules).forEach(([ruleName, rule]) => {
|
||||
if (!props.showLibraryRules && rule.meta && rule.meta.lib) return;
|
||||
if (!rule.matches || !Array.isArray(rule.matches)) return;
|
||||
|
||||
rule.matches.forEach((match) => {
|
||||
if (!Array.isArray(match) || match.length === 0) return;
|
||||
const [location] = match;
|
||||
const ids = getProcessIds(location);
|
||||
if (ids && processMap.has(ids.pid)) {
|
||||
const processNode = processMap.get(ids.pid);
|
||||
if (!processNode.data.uniqueRules.has(ruleName)) {
|
||||
processNode.data.uniqueMatchCount++;
|
||||
processNode.data.uniqueRules.set(ruleName, {
|
||||
name: ruleName,
|
||||
lib: rule.meta && rule.meta.lib,
|
||||
matchCount: 0,
|
||||
scope: location.type
|
||||
});
|
||||
}
|
||||
processNode.data.uniqueRules.get(ruleName).matchCount++;
|
||||
}
|
||||
});
|
||||
});
|
||||
// build the final tree structure
|
||||
const rootProcesses = [];
|
||||
processMap.forEach((processNode) => {
|
||||
console.log(processNode);
|
||||
processNode.data.uniqueRules = Array.from(processNode.data.uniqueRules.values());
|
||||
const parentProcess = processMap.get(processNode.data.ppid);
|
||||
if (parentProcess) {
|
||||
parentProcess.children.push(processNode);
|
||||
} else {
|
||||
rootProcesses.push(processNode);
|
||||
}
|
||||
});
|
||||
|
||||
return rootProcesses;
|
||||
});
|
||||
|
||||
const getColorForId = (id) => {
|
||||
if (id === undefined || id === null) return 'black'
|
||||
const hue = Math.abs((id * 41) % 360)
|
||||
return `hsl(${hue}, 70%, 40%)`
|
||||
}
|
||||
if (id === undefined || id === null) return "black";
|
||||
const hue = Math.abs((id * 41) % 360);
|
||||
return `hsl(${hue}, 70%, 40%)`;
|
||||
};
|
||||
|
||||
const showTooltip = (event, node) => {
|
||||
if (node.data.uniqueMatchCount > 0) {
|
||||
currentNode.value = node
|
||||
tooltipVisible.value = true
|
||||
updateTooltipPosition(event)
|
||||
}
|
||||
}
|
||||
if (node.data.uniqueMatchCount > 0) {
|
||||
currentNode.value = node;
|
||||
tooltipVisible.value = true;
|
||||
updateTooltipPosition(event);
|
||||
}
|
||||
};
|
||||
|
||||
const hideTooltip = () => {
|
||||
tooltipVisible.value = false
|
||||
currentNode.value = null
|
||||
}
|
||||
tooltipVisible.value = false;
|
||||
currentNode.value = null;
|
||||
};
|
||||
|
||||
const updateTooltipPosition = (event) => {
|
||||
const offset = 10
|
||||
tooltipStyle.value = {
|
||||
position: 'fixed',
|
||||
top: `${event.clientY + offset}px`,
|
||||
left: `${event.clientX + offset}px`
|
||||
}
|
||||
}
|
||||
const offset = 10;
|
||||
tooltipStyle.value = {
|
||||
position: "fixed",
|
||||
top: `${event.clientY + offset}px`,
|
||||
left: `${event.clientX + offset}px`
|
||||
};
|
||||
};
|
||||
|
||||
const handleMouseMove = (event) => {
|
||||
if (tooltipVisible.value) {
|
||||
updateTooltipPosition(event)
|
||||
}
|
||||
}
|
||||
if (tooltipVisible.value) {
|
||||
updateTooltipPosition(event);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
})
|
||||
document.addEventListener("mousemove", handleMouseMove);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mousemove', handleMouseMove)
|
||||
})
|
||||
document.removeEventListener("mousemove", handleMouseMove);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,284 +1,289 @@
|
||||
<template>
|
||||
<div class="card">
|
||||
<TreeTable
|
||||
:value="filteredTreeData"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
size="small"
|
||||
:filters="filters"
|
||||
:filterMode="filterMode"
|
||||
sortField="namespace"
|
||||
:sortOrder="-1"
|
||||
removableSort
|
||||
:rowHover="true"
|
||||
:indentation="1.3"
|
||||
selectionMode="single"
|
||||
@node-select="onNodeSelect"
|
||||
:pt="{
|
||||
row: ({ instance }) => ({
|
||||
oncontextmenu: (event) => onRightClick(event, instance)
|
||||
})
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-content-end w-full">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="filters['global']" placeholder="Global search" />
|
||||
</IconField>
|
||||
</div>
|
||||
</template>
|
||||
<div class="card">
|
||||
<TreeTable
|
||||
:value="filteredTreeData"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
size="small"
|
||||
:filters="filters"
|
||||
:filterMode="filterMode"
|
||||
sortField="namespace"
|
||||
:sortOrder="-1"
|
||||
removableSort
|
||||
:rowHover="true"
|
||||
:indentation="1.3"
|
||||
selectionMode="single"
|
||||
@node-select="onNodeSelect"
|
||||
:pt="{
|
||||
row: ({ instance }) => ({
|
||||
oncontextmenu: (event) => onRightClick(event, instance)
|
||||
})
|
||||
}"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex justify-content-end w-full">
|
||||
<IconField>
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText v-model="filters['global']" placeholder="Global search" />
|
||||
</IconField>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Rule column (always visible) -->
|
||||
<Column field="name" header="Rule" :sortable="true" :expander="true" filterMatchMode="contains">
|
||||
<template #filter>
|
||||
<InputText v-model="filters['name']" type="text" placeholder="Filter by Rule or Feature" />
|
||||
</template>
|
||||
<template #body="{ node }">
|
||||
<RuleColumn :node="node" />
|
||||
</template>
|
||||
</Column>
|
||||
<!-- Rule column (always visible) -->
|
||||
<Column field="name" header="Rule" :sortable="true" :expander="true" filterMatchMode="contains">
|
||||
<template #filter>
|
||||
<InputText v-model="filters['name']" type="text" placeholder="Filter by Rule or Feature" />
|
||||
</template>
|
||||
<template #body="{ node }">
|
||||
<RuleColumn :node="node" />
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
<Column
|
||||
v-for="col in visibleColumns"
|
||||
:key="col.field"
|
||||
:field="col.field"
|
||||
:header="props.data.meta.flavor === 'dynamic' && col.field === 'address' ? 'Process' : col.header"
|
||||
:sortable="col.field !== 'source'"
|
||||
:class="{ 'w-3': col.field === 'mbc', 'w-full': col.field === 'name' }"
|
||||
filterMatchMode="contains"
|
||||
>
|
||||
<template #filter>
|
||||
<InputText v-model="filters[col.field]" type="text" :placeholder="`Filter by ${col.header}`" />
|
||||
</template>
|
||||
<template #body="slotProps">
|
||||
<!-- Address column -->
|
||||
<span v-if="col.field === 'address'" class="text-sm" style="font-family: monospace">
|
||||
{{ slotProps.node.data.address }}
|
||||
</span>
|
||||
<Column
|
||||
v-for="col in visibleColumns"
|
||||
:key="col.field"
|
||||
:field="col.field"
|
||||
:header="props.data.meta.flavor === 'dynamic' && col.field === 'address' ? 'Process' : col.header"
|
||||
:sortable="col.field !== 'source'"
|
||||
:class="{ 'w-3': col.field === 'mbc', 'w-full': col.field === 'name' }"
|
||||
filterMatchMode="contains"
|
||||
>
|
||||
<template #filter>
|
||||
<InputText v-model="filters[col.field]" type="text" :placeholder="`Filter by ${col.header}`" />
|
||||
</template>
|
||||
<template #body="slotProps">
|
||||
<!-- Address column -->
|
||||
<span v-if="col.field === 'address'" class="text-sm" style="font-family: monospace">
|
||||
{{ slotProps.node.data.address }}
|
||||
</span>
|
||||
|
||||
<!-- Tactic column -->
|
||||
<div v-else-if="col.field === 'tactic' && slotProps.node.data.attack">
|
||||
<div v-for="(attack, index) in slotProps.node.data.attack" :key="index">
|
||||
<a :href="createATTACKHref(attack)" target="_blank">
|
||||
{{ attack.technique }} <span class="text-500 text-sm font-normal ml-1">({{ attack.id }})</span>
|
||||
</a>
|
||||
<div
|
||||
v-for="(technique, techIndex) in attack.techniques"
|
||||
:key="techIndex"
|
||||
style="font-size: 0.8em; margin-left: 1em"
|
||||
>
|
||||
<a :href="createATTACKHref(technique)" target="_blank">
|
||||
↳ {{ technique.technique }}
|
||||
<span class="text-500 text-xs font-normal ml-1">({{ technique.id }})</span>
|
||||
<!-- Tactic column -->
|
||||
<div v-else-if="col.field === 'tactic' && slotProps.node.data.attack">
|
||||
<div v-for="(attack, index) in slotProps.node.data.attack" :key="index">
|
||||
<a :href="createATTACKHref(attack)" target="_blank">
|
||||
{{ attack.technique }}
|
||||
<span class="text-500 text-sm font-normal ml-1">({{ attack.id }})</span>
|
||||
</a>
|
||||
<div
|
||||
v-for="(technique, techIndex) in attack.techniques"
|
||||
:key="techIndex"
|
||||
style="font-size: 0.8em; margin-left: 1em"
|
||||
>
|
||||
<a :href="createATTACKHref(technique)" target="_blank">
|
||||
↳ {{ technique.technique }}
|
||||
<span class="text-500 text-xs font-normal ml-1">({{ technique.id }})</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MBC column -->
|
||||
<div v-else-if="col.field === 'mbc' && slotProps.node.data.mbc">
|
||||
<div v-for="(mbc, index) in slotProps.node.data.mbc" :key="index">
|
||||
<a :href="createMBCHref(mbc)" target="_blank">
|
||||
{{ mbc.parts.join("::") }}
|
||||
<span class="text-500 text-sm font-normal opacity-80 ml-1">[{{ mbc.id }}]</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Namespace column -->
|
||||
<span v-else-if="col.field === 'namespace' && !slotProps.node.data.lib">
|
||||
{{ slotProps.node.data.namespace }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
</TreeTable>
|
||||
|
||||
<ContextMenu ref="menu" :model="contextMenuItems">
|
||||
<template #item="{ item, props }">
|
||||
<a v-ripple v-bind="props.action" :href="item.url" :target="item.target">
|
||||
<span v-if="item.icon !== 'vt-icon'" :class="item.icon" />
|
||||
<VTIcon v-else-if="item.icon === 'vt-icon'" />
|
||||
<span>{{ item.label }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
|
||||
<!-- MBC column -->
|
||||
<div v-else-if="col.field === 'mbc' && slotProps.node.data.mbc">
|
||||
<div v-for="(mbc, index) in slotProps.node.data.mbc" :key="index">
|
||||
<a :href="createMBCHref(mbc)" target="_blank">
|
||||
{{ mbc.parts.join('::') }}
|
||||
<span class="text-500 text-sm font-normal opacity-80 ml-1">[{{ mbc.id }}]</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<Toast />
|
||||
|
||||
<!-- Namespace column -->
|
||||
<span v-else-if="col.field === 'namespace' && !slotProps.node.data.lib">
|
||||
{{ slotProps.node.data.namespace }}
|
||||
</span>
|
||||
</template>
|
||||
</Column>
|
||||
</TreeTable>
|
||||
|
||||
<ContextMenu ref="menu" :model="contextMenuItems">
|
||||
<template #item="{ item, props }">
|
||||
<a v-ripple v-bind="props.action" :href="item.url" :target="item.target">
|
||||
<span v-if="item.icon !== 'vt-icon'" :class="item.icon" />
|
||||
<VTIcon v-else-if="item.icon === 'vt-icon'" />
|
||||
<span>{{ item.label }}</span>
|
||||
</a>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
|
||||
<Toast />
|
||||
|
||||
<Dialog v-model:visible="sourceDialogVisible" :style="{ width: '50vw' }">
|
||||
<highlightjs autodetect :code="currentSource" />
|
||||
</Dialog>
|
||||
</div>
|
||||
<Dialog v-model:visible="sourceDialogVisible" :style="{ width: '50vw' }">
|
||||
<highlightjs autodetect :code="currentSource" />
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import 'highlight.js/styles/stackoverflow-light.css'
|
||||
import "highlight.js/styles/stackoverflow-light.css";
|
||||
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import TreeTable from 'primevue/treetable'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import Column from 'primevue/column'
|
||||
import IconField from 'primevue/iconfield'
|
||||
import InputIcon from 'primevue/inputicon'
|
||||
import ContextMenu from 'primevue/contextmenu'
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import TreeTable from "primevue/treetable";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Dialog from "primevue/dialog";
|
||||
import Column from "primevue/column";
|
||||
import IconField from "primevue/iconfield";
|
||||
import InputIcon from "primevue/inputicon";
|
||||
import ContextMenu from "primevue/contextmenu";
|
||||
|
||||
import RuleColumn from './columns/RuleColumn.vue'
|
||||
import VTIcon from './misc/VTIcon.vue'
|
||||
import RuleColumn from "./columns/RuleColumn.vue";
|
||||
import VTIcon from "./misc/VTIcon.vue";
|
||||
|
||||
import { parseRules } from '../utils/rdocParser'
|
||||
import { createMBCHref, createATTACKHref } from '../utils/urlHelpers'
|
||||
import { parseRules } from "../utils/rdocParser";
|
||||
import { createMBCHref, createATTACKHref } from "../utils/urlHelpers";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showLibraryRules: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
data: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showLibraryRules: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
const treeData = ref([])
|
||||
const filters = ref({})
|
||||
const filterMode = ref('lenient')
|
||||
const sourceDialogVisible = ref(false)
|
||||
const currentSource = ref('')
|
||||
const expandedKeys = ref({})
|
||||
const treeData = ref([]);
|
||||
const filters = ref({});
|
||||
const filterMode = ref("lenient");
|
||||
const sourceDialogVisible = ref(false);
|
||||
const currentSource = ref("");
|
||||
const expandedKeys = ref({});
|
||||
|
||||
const menu = ref()
|
||||
const selectedNode = ref({})
|
||||
const menu = ref();
|
||||
const selectedNode = ref({});
|
||||
|
||||
const contextMenuItems = computed(() => [
|
||||
{
|
||||
label: 'View source',
|
||||
icon: 'pi pi-eye',
|
||||
command: () => {
|
||||
showSource(selectedNode.value.data.source)
|
||||
{
|
||||
label: "View source",
|
||||
icon: "pi pi-eye",
|
||||
command: () => {
|
||||
showSource(selectedNode.value.data.source);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: "View rule in capa-rules",
|
||||
icon: "pi pi-external-link",
|
||||
target: "_blank",
|
||||
url: selectedNode.value.url
|
||||
},
|
||||
{
|
||||
label: "Lookup rule in VirusTotal",
|
||||
icon: "vt-icon",
|
||||
target: "_blank",
|
||||
url: selectedNode.value.vturl
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'View rule in capa-rules',
|
||||
icon: 'pi pi-external-link',
|
||||
target: '_blank',
|
||||
url: selectedNode.value.url
|
||||
},
|
||||
{
|
||||
label: 'Lookup rule in VirusTotal',
|
||||
icon: 'vt-icon',
|
||||
target: '_blank',
|
||||
url: selectedNode.value.vturl
|
||||
}
|
||||
])
|
||||
]);
|
||||
|
||||
const onRightClick = (event, instance) => {
|
||||
if (instance.node.data.source) {
|
||||
selectedNode.value = instance.node
|
||||
// contrust capa-rules url
|
||||
selectedNode.value.url = `https://github.com/mandiant/capa-rules/blob/master/${instance.node.data.namespace || 'lib'}/${instance.node.data.name.toLowerCase().replace(/\s+/g, '-')}.yml`
|
||||
// construct VirusTotal deep link
|
||||
const behaviourSignature = `behaviour_signature:"${instance.node.data.name}"`
|
||||
selectedNode.value.vturl = `https://www.virustotal.com/gui/search/${encodeURIComponent(behaviourSignature)}/files`
|
||||
if (instance.node.data.source) {
|
||||
selectedNode.value = instance.node;
|
||||
// contrust capa-rules url
|
||||
selectedNode.value.url = `https://github.com/mandiant/capa-rules/blob/master/${instance.node.data.namespace || "lib"}/${instance.node.data.name.toLowerCase().replace(/\s+/g, "-")}.yml`;
|
||||
// construct VirusTotal deep link
|
||||
const behaviourSignature = `behaviour_signature:"${instance.node.data.name}"`;
|
||||
selectedNode.value.vturl = `https://www.virustotal.com/gui/search/${encodeURIComponent(behaviourSignature)}/files`;
|
||||
|
||||
menu.value.show(event)
|
||||
}
|
||||
}
|
||||
menu.value.show(event);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Expand node on click
|
||||
*/
|
||||
const onNodeSelect = (node) => {
|
||||
const nodeKey = node.key
|
||||
const nodeType = node.data.type
|
||||
const nodeKey = node.key;
|
||||
const nodeType = node.data.type;
|
||||
|
||||
// We only expand rule and match locations, if not return
|
||||
if (nodeType !== 'rule' && nodeType !== 'match location') return
|
||||
// We only expand rule and match locations, if not return
|
||||
if (nodeType !== "rule" && nodeType !== "match location") return;
|
||||
|
||||
// If the node is already expanded, collapse it
|
||||
if (expandedKeys.value[nodeKey]) {
|
||||
delete expandedKeys.value[nodeKey]
|
||||
return
|
||||
}
|
||||
// If the node is already expanded, collapse it
|
||||
if (expandedKeys.value[nodeKey]) {
|
||||
delete expandedKeys.value[nodeKey];
|
||||
return;
|
||||
}
|
||||
|
||||
if (nodeType === 'rule') {
|
||||
// For rule nodes, clear existing expanded keys and set the clicked rule as expanded
|
||||
// and expand the first (child) match by default
|
||||
expandedKeys.value = { [nodeKey]: true, [`${nodeKey}-0`]: true }
|
||||
} else if (nodeType === 'match location') {
|
||||
// For match location nodes, we need to keep the parent expanded
|
||||
// and toggle the clicked node while collapsing siblings
|
||||
const [parentKey] = nodeKey.split('-')
|
||||
expandedKeys.value = { [parentKey]: true, [`${nodeKey}`]: true }
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (nodeType === "rule") {
|
||||
// For rule nodes, clear existing expanded keys and set the clicked rule as expanded
|
||||
// and expand the first (child) match by default
|
||||
expandedKeys.value = { [nodeKey]: true, [`${nodeKey}-0`]: true };
|
||||
} else if (nodeType === "match location") {
|
||||
// For match location nodes, we need to keep the parent expanded
|
||||
// and toggle the clicked node while collapsing siblings
|
||||
const [parentKey] = nodeKey.split("-");
|
||||
expandedKeys.value = { [parentKey]: true, [`${nodeKey}`]: true };
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// All available columns
|
||||
const visibleColumns = ref([
|
||||
{ field: 'address', header: 'Address' },
|
||||
{ field: 'namespace', header: 'Namespace' },
|
||||
{ field: 'tactic', header: 'ATT&CK Tactic' },
|
||||
{ field: 'mbc', header: 'Malware Behaviour Catalogue' }
|
||||
])
|
||||
{ field: "address", header: "Address" },
|
||||
{ field: "namespace", header: "Namespace" },
|
||||
{ field: "tactic", header: "ATT&CK Tactic" },
|
||||
{ field: "mbc", header: "Malware Behaviour Catalogue" }
|
||||
]);
|
||||
|
||||
// Filter out the treeData for showing/hiding lib rules
|
||||
const filteredTreeData = computed(() => {
|
||||
if (props.showLibraryRules) {
|
||||
return treeData.value // Return all data when showLibraryRules is true
|
||||
} else {
|
||||
// Filter out library rules when showLibraryRules is false
|
||||
const filterNode = (node) => {
|
||||
if (node.data && node.data.lib) {
|
||||
return false
|
||||
}
|
||||
if (node.children) {
|
||||
node.children = node.children.filter(filterNode)
|
||||
}
|
||||
return true
|
||||
if (props.showLibraryRules) {
|
||||
return treeData.value; // Return all data when showLibraryRules is true
|
||||
} else {
|
||||
// Filter out library rules when showLibraryRules is false
|
||||
const filterNode = (node) => {
|
||||
if (node.data && node.data.lib) {
|
||||
return false;
|
||||
}
|
||||
if (node.children) {
|
||||
node.children = node.children.filter(filterNode);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return treeData.value.filter(filterNode);
|
||||
}
|
||||
return treeData.value.filter(filterNode)
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
const showSource = (source) => {
|
||||
currentSource.value = source
|
||||
sourceDialogVisible.value = true
|
||||
}
|
||||
currentSource.value = source;
|
||||
sourceDialogVisible.value = true;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (props.data && props.data.rules) {
|
||||
treeData.value = parseRules(props.data.rules, props.data.meta.flavor, props.data.meta.analysis.layout)
|
||||
} else {
|
||||
console.error('Invalid data prop:', props.data)
|
||||
}
|
||||
})
|
||||
if (props.data && props.data.rules) {
|
||||
treeData.value = parseRules(props.data.rules, props.data.meta.flavor, props.data.meta.analysis.layout);
|
||||
} else {
|
||||
console.error("Invalid data prop:", props.data);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Disable the toggle button for statement and features */
|
||||
:deep(
|
||||
.p-treetable-tbody > tr:not(:is([aria-level='1'], [aria-level='2'])) > td > div > .p-treetable-node-toggle-button
|
||||
) {
|
||||
visibility: hidden !important;
|
||||
height: 1.3rem;
|
||||
.p-treetable-tbody
|
||||
> tr:not(:is([aria-level="1"], [aria-level="2"]))
|
||||
> td
|
||||
> div
|
||||
> .p-treetable-node-toggle-button
|
||||
) {
|
||||
visibility: hidden !important;
|
||||
height: 1.3rem;
|
||||
}
|
||||
/* Disable the toggle button for rules */
|
||||
:deep(.p-treetable-tbody > tr:is([aria-level='1']) > td > div > .p-treetable-node-toggle-button) {
|
||||
visibility: collapse !important;
|
||||
height: 1.3rem;
|
||||
:deep(.p-treetable-tbody > tr:is([aria-level="1"]) > td > div > .p-treetable-node-toggle-button) {
|
||||
visibility: collapse !important;
|
||||
height: 1.3rem;
|
||||
}
|
||||
|
||||
/* Make all matches nodes (i.e. not rule names) slightly smaller,
|
||||
and tighten up the spacing between the rows */
|
||||
:deep(.p-treetable-tbody > tr:not([aria-level='1']) > td) {
|
||||
font-size: 0.95rem;
|
||||
padding: 0rem 0.5rem !important;
|
||||
:deep(.p-treetable-tbody > tr:not([aria-level="1"]) > td) {
|
||||
font-size: 0.95rem;
|
||||
padding: 0rem 0.5rem !important;
|
||||
}
|
||||
|
||||
/* Optional: Add a subtle background to root-level rows for better distinction */
|
||||
:deep(.p-treetable-tbody > tr[aria-level='1']) {
|
||||
background-color: #f9f9f9;
|
||||
:deep(.p-treetable-tbody > tr[aria-level="1"]) {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,75 +1,77 @@
|
||||
<template>
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="flex align-items-center flex-row gap-3">
|
||||
<div class="flex flex-row align-items-center gap-2">
|
||||
<Checkbox
|
||||
v-model="showCapabilitiesByFunctionOrProcess"
|
||||
inputId="showCapabilitiesByFunctionOrProcess"
|
||||
:binary="true"
|
||||
:disabled="showNamespaceChart"
|
||||
/>
|
||||
<label for="showCapabilitiesByFunctionOrProcess">{{ capabilitiesLabel }}</label>
|
||||
</div>
|
||||
<div class="flex flex-row align-items-center gap-2">
|
||||
<Checkbox
|
||||
v-model="showLibraryRules"
|
||||
inputId="showLibraryRules"
|
||||
:binary="true"
|
||||
:disabled="showNamespaceChart"
|
||||
/>
|
||||
<label for="showLibraryRules">
|
||||
<span v-if="libraryRuleMatchesCount > 1"> Show {{ libraryRuleMatchesCount }} library rule matches </span>
|
||||
<span v-else>Show 1 library rule match</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-row align-items-center gap-2">
|
||||
<Checkbox v-model="showNamespaceChart" inputId="showNamespaceChart" :binary="true" />
|
||||
<label for="showNamespaceChart"> Show namespace chart </label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="flex align-items-center flex-row gap-3">
|
||||
<div class="flex flex-row align-items-center gap-2">
|
||||
<Checkbox
|
||||
v-model="showCapabilitiesByFunctionOrProcess"
|
||||
inputId="showCapabilitiesByFunctionOrProcess"
|
||||
:binary="true"
|
||||
:disabled="showNamespaceChart"
|
||||
/>
|
||||
<label for="showCapabilitiesByFunctionOrProcess">{{ capabilitiesLabel }}</label>
|
||||
</div>
|
||||
<div class="flex flex-row align-items-center gap-2">
|
||||
<Checkbox
|
||||
v-model="showLibraryRules"
|
||||
inputId="showLibraryRules"
|
||||
:binary="true"
|
||||
:disabled="showNamespaceChart"
|
||||
/>
|
||||
<label for="showLibraryRules">
|
||||
<span v-if="libraryRuleMatchesCount > 1">
|
||||
Show {{ libraryRuleMatchesCount }} library rule matches
|
||||
</span>
|
||||
<span v-else>Show 1 library rule match</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex flex-row align-items-center gap-2">
|
||||
<Checkbox v-model="showNamespaceChart" inputId="showNamespaceChart" :binary="true" />
|
||||
<label for="showNamespaceChart"> Show namespace chart </label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import Checkbox from 'primevue/checkbox'
|
||||
import { ref, computed, watch } from "vue";
|
||||
import Checkbox from "primevue/checkbox";
|
||||
|
||||
const props = defineProps({
|
||||
flavor: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
libraryRuleMatchesCount: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
flavor: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
libraryRuleMatchesCount: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const showCapabilitiesByFunctionOrProcess = ref(false)
|
||||
const showLibraryRules = ref(false)
|
||||
const showNamespaceChart = ref(false)
|
||||
const showCapabilitiesByFunctionOrProcess = ref(false);
|
||||
const showLibraryRules = ref(false);
|
||||
const showNamespaceChart = ref(false);
|
||||
|
||||
const emit = defineEmits([
|
||||
'update:show-capabilities-by-function-or-process',
|
||||
'update:show-library-rules',
|
||||
'update:show-namespace-chart'
|
||||
])
|
||||
"update:show-capabilities-by-function-or-process",
|
||||
"update:show-library-rules",
|
||||
"update:show-namespace-chart"
|
||||
]);
|
||||
|
||||
const capabilitiesLabel = computed(() => {
|
||||
return props.flavor === 'static' ? 'Show capabilities by function' : 'Show capabilities by process'
|
||||
})
|
||||
return props.flavor === "static" ? "Show capabilities by function" : "Show capabilities by process";
|
||||
});
|
||||
|
||||
watch(showCapabilitiesByFunctionOrProcess, (newValue) => {
|
||||
emit('update:show-capabilities-by-function-or-process', newValue)
|
||||
})
|
||||
emit("update:show-capabilities-by-function-or-process", newValue);
|
||||
});
|
||||
|
||||
watch(showLibraryRules, (newValue) => {
|
||||
emit('update:show-library-rules', newValue)
|
||||
})
|
||||
emit("update:show-library-rules", newValue);
|
||||
});
|
||||
|
||||
watch(showNamespaceChart, (newValue) => {
|
||||
emit('update:show-namespace-chart', newValue)
|
||||
})
|
||||
emit("update:show-namespace-chart", newValue);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,91 +1,91 @@
|
||||
<template>
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="flex flex-wrap align-items-center justify-content-center gap-3">
|
||||
<div class="flex-grow-1 flex align-items-center justify-content-center">
|
||||
<FileUpload
|
||||
mode="basic"
|
||||
name="model[]"
|
||||
accept=".json,.gz"
|
||||
:max-file-size="10000000"
|
||||
:auto="true"
|
||||
:custom-upload="true"
|
||||
choose-label="Upload from local"
|
||||
@uploader="$emit('load-from-local', $event)"
|
||||
/>
|
||||
</div>
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="flex flex-wrap align-items-center justify-content-center gap-3">
|
||||
<div class="flex-grow-1 flex align-items-center justify-content-center">
|
||||
<FileUpload
|
||||
mode="basic"
|
||||
name="model[]"
|
||||
accept=".json,.gz"
|
||||
:max-file-size="10000000"
|
||||
:auto="true"
|
||||
:custom-upload="true"
|
||||
choose-label="Upload from local"
|
||||
@uploader="$emit('load-from-local', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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 gap-2">
|
||||
<FloatLabel>
|
||||
<InputText id="url" type="text" v-model="loadURL" />
|
||||
<label for="url">Load from URL</label>
|
||||
</FloatLabel>
|
||||
<Button icon="pi pi-arrow-right" @click="$emit('load-from-url', loadURL)" :disabled="!loadURL" />
|
||||
</div>
|
||||
<div class="flex-grow-1 flex align-items-center justify-content-center gap-2">
|
||||
<FloatLabel>
|
||||
<InputText id="url" type="text" v-model="loadURL" />
|
||||
<label for="url">Load from URL</label>
|
||||
</FloatLabel>
|
||||
<Button icon="pi pi-arrow-right" @click="$emit('load-from-url', loadURL)" :disabled="!loadURL" />
|
||||
</div>
|
||||
|
||||
<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="$emit('load-demo-static')" class="p-button" />
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import Card from 'primevue/card'
|
||||
import FileUpload from 'primevue/fileupload'
|
||||
import Divider from 'primevue/divider'
|
||||
import FloatLabel from 'primevue/floatlabel'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Button from 'primevue/button'
|
||||
import { ref } from "vue";
|
||||
import Card from "primevue/card";
|
||||
import FileUpload from "primevue/fileupload";
|
||||
import Divider from "primevue/divider";
|
||||
import FloatLabel from "primevue/floatlabel";
|
||||
import InputText from "primevue/inputtext";
|
||||
import Button from "primevue/button";
|
||||
|
||||
const loadURL = ref('')
|
||||
const loadURL = ref("");
|
||||
|
||||
defineEmits(['load-from-local', 'load-from-url', 'load-demo-static', 'load-demo-dynamic'])
|
||||
defineEmits(["load-from-local", "load-from-url", "load-demo-static", "load-demo-dynamic"]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@media screen and (min-width: 769px) {
|
||||
.hidden-mobile {
|
||||
display: flex !important;
|
||||
}
|
||||
.visible-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
.hidden-mobile {
|
||||
display: flex !important;
|
||||
}
|
||||
.visible-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.hidden-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
.visible-mobile {
|
||||
display: flex !important;
|
||||
}
|
||||
.hidden-mobile {
|
||||
display: none !important;
|
||||
}
|
||||
.visible-mobile {
|
||||
display: flex !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,49 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="node.data.type === 'rule'">
|
||||
{{ node.data.name }}
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'match location'">
|
||||
<span class="text-sm font-italic">{{ node.data.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'statement'"
|
||||
>-
|
||||
<span
|
||||
:class="{ 'text-green-700': node.data.typeValue === 'range', 'font-semibold': node.data.typeValue !== 'range' }"
|
||||
>
|
||||
{{ node.data.name }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'feature'">
|
||||
<span
|
||||
>- {{ node.data.typeValue }}:
|
||||
<span :class="{ 'text-green-700': node.data.typeValue !== 'regex' }" style="font-family: monospace">{{
|
||||
node.data.name
|
||||
}}</span></span
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'regex-capture'">
|
||||
- <span class="text-green-700" style="font-family: monospace">{{ node.data.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'call-info'">
|
||||
<highlightjs lang="c" :code="node.data.name.callInfo" style="background-color: #f0f0f0" />
|
||||
</template>
|
||||
<span v-if="node.data.description" class="text-gray-500 text-sm" style="font-size: 90%">
|
||||
= {{ node.data.description }}
|
||||
</span>
|
||||
<span v-if="node.data.matchCount > 1" class="font-italic"> ({{ node.data.matchCount }} matches) </span>
|
||||
<LibraryTag v-if="node.data.lib && node.data.matchCount" />
|
||||
</div>
|
||||
<div>
|
||||
<template v-if="node.data.type === 'rule'">
|
||||
{{ node.data.name }}
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'match location'">
|
||||
<span class="text-sm font-italic">{{ node.data.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'statement'"
|
||||
>-
|
||||
<span
|
||||
:class="{
|
||||
'text-green-700': node.data.typeValue === 'range',
|
||||
'font-semibold': node.data.typeValue !== 'range'
|
||||
}"
|
||||
>
|
||||
{{ node.data.name }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'feature'">
|
||||
<span
|
||||
>- {{ node.data.typeValue }}:
|
||||
<span :class="{ 'text-green-700': node.data.typeValue !== 'regex' }" style="font-family: monospace">{{
|
||||
node.data.name
|
||||
}}</span></span
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'regex-capture'">
|
||||
- <span class="text-green-700" style="font-family: monospace">{{ node.data.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="node.data.type === 'call-info'">
|
||||
<highlightjs lang="c" :code="node.data.name.callInfo" style="background-color: #f0f0f0" />
|
||||
</template>
|
||||
<span v-if="node.data.description" class="text-gray-500 text-sm" style="font-size: 90%">
|
||||
= {{ node.data.description }}
|
||||
</span>
|
||||
<span v-if="node.data.matchCount > 1" class="font-italic"> ({{ node.data.matchCount }} matches) </span>
|
||||
<LibraryTag v-if="node.data.lib && node.data.matchCount" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps } from 'vue'
|
||||
import LibraryTag from '../misc/LibraryTag.vue'
|
||||
import { defineProps } from "vue";
|
||||
import LibraryTag from "../misc/LibraryTag.vue";
|
||||
|
||||
defineProps({
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
node: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Tag from 'primevue/tag';
|
||||
</script>
|
||||
import Tag from "primevue/tag";
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
<template>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.87 12L0 22.68h24V1.32H0zm10.73 8.52H5.28l8.637-8.448L5.28 3.48H21.6z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.87 12L0 22.68h24V1.32H0zm10.73 8.52H5.28l8.637-8.448L5.28 3.48H21.6z" fill="currentColor" />
|
||||
</svg>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -1,87 +1,89 @@
|
||||
// useDataLoader.js
|
||||
import { ref, readonly } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
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 toast = useToast();
|
||||
const rdocData = ref(null);
|
||||
const isValidVersion = ref(false);
|
||||
|
||||
const MIN_SUPPORTED_VERSION = '7.0.0'
|
||||
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<void>}
|
||||
*/
|
||||
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}`)
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
data = await response.json()
|
||||
} else if (typeof source === 'object') {
|
||||
// Direct JSON object (Preview options)
|
||||
data = source
|
||||
} else {
|
||||
throw new Error('Invalid source type')
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Loads JSON rdoc data from various sources
|
||||
* @param {File|string|Object} source - File object, URL string, or JSON object
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const loadRdoc = async (source) => {
|
||||
try {
|
||||
let data;
|
||||
|
||||
return {
|
||||
rdocData: readonly(rdocData),
|
||||
isValidVersion: readonly(isValidVersion),
|
||||
loadRdoc
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,88 +1,88 @@
|
||||
import 'primeicons/primeicons.css'
|
||||
import './assets/main.css'
|
||||
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/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 "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 { 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'
|
||||
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: {
|
||||
semantic: {
|
||||
primary: {
|
||||
color: '{slate.800}',
|
||||
inverseColor: '#ffffff',
|
||||
hoverColor: '{zinc.900}',
|
||||
activeColor: '{zinc.800}'
|
||||
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)
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router)
|
||||
app.use(hljsVuePlugin)
|
||||
app.use(router);
|
||||
app.use(hljsVuePlugin);
|
||||
|
||||
app.use(PrimeVue, {
|
||||
theme: {
|
||||
preset: Noir,
|
||||
options: {
|
||||
darkModeSelector: 'light'
|
||||
}
|
||||
},
|
||||
ripple: true
|
||||
})
|
||||
app.use(ToastService)
|
||||
theme: {
|
||||
preset: Noir,
|
||||
options: {
|
||||
darkModeSelector: "light"
|
||||
}
|
||||
},
|
||||
ripple: true
|
||||
});
|
||||
app.use(ToastService);
|
||||
|
||||
app.directive('tooltip', Tooltip)
|
||||
app.directive('ripple', Ripple)
|
||||
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.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')
|
||||
app.mount("#app");
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import ImportView from '../views/ImportView.vue'
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import ImportView from "../views/ImportView.vue";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: ImportView
|
||||
}
|
||||
]
|
||||
})
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "home",
|
||||
component: ImportView
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
@@ -1,299 +1,301 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { parseRules, parseFunctionCapabilities } from '../utils/rdocParser'
|
||||
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([])
|
||||
})
|
||||
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
|
||||
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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
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'])
|
||||
})
|
||||
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 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)
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
const resultWithoutLib = parseFunctionCapabilities(mockData, false)
|
||||
expect(resultWithoutLib).toHaveLength(0)
|
||||
})
|
||||
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 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 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 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')
|
||||
})
|
||||
})
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import pako from 'pako'
|
||||
import pako from "pako";
|
||||
|
||||
/**
|
||||
* Checks if the given file is gzipped
|
||||
@@ -6,10 +6,10 @@ import pako from 'pako'
|
||||
* @returns {Promise<boolean>} - 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
|
||||
}
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
return uint8Array[0] === 0x1f && uint8Array[1] === 0x8b;
|
||||
};
|
||||
|
||||
/**
|
||||
* Decompresses a gzipped file
|
||||
@@ -17,11 +17,11 @@ export const isGzipped = async (file) => {
|
||||
* @returns {Promise<string>} - 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
|
||||
}
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
const decompressed = pako.inflate(uint8Array, { to: "string" });
|
||||
return decompressed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reads a file as text
|
||||
@@ -29,10 +29,10 @@ export const decompressGzip = async (file) => {
|
||||
* @returns {Promise<string>} - 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)
|
||||
})
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => resolve(event.target.result);
|
||||
reader.onerror = (error) => reject(error);
|
||||
reader.readAsText(file);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,73 +7,73 @@
|
||||
* @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
|
||||
}
|
||||
}
|
||||
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'
|
||||
// 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
|
||||
// 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)]
|
||||
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;
|
||||
});
|
||||
}
|
||||
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`
|
||||
// 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
|
||||
})
|
||||
return ruleNode;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,92 +83,92 @@ export function parseRules(rules, flavor, layout, maxMatches = 1) {
|
||||
* @returns {Array} - Parsed data for the CapasByFunction DataTable component
|
||||
*/
|
||||
export function parseFunctionCapabilities(data, showLibraryRules) {
|
||||
const result = []
|
||||
const matchesByFunction = new Map()
|
||||
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())
|
||||
// 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);
|
||||
}
|
||||
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
|
||||
}))
|
||||
// Iterate through all rules in the data
|
||||
for (const ruleId in data.rules) {
|
||||
const rule = data.rules[ruleId];
|
||||
|
||||
result.push({
|
||||
funcaddr: `0x${functionAddress}`,
|
||||
matchCount: matchingRules.length,
|
||||
capabilities: matchingRules,
|
||||
lib: data.lib
|
||||
})
|
||||
}
|
||||
// Skip library rules if showLibraryRules is false
|
||||
if (!showLibraryRules && rule.meta.lib) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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
|
||||
}))
|
||||
)
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return finalResult
|
||||
// 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
|
||||
@@ -182,106 +182,108 @@ export function parseFunctionCapabilities(data, showLibraryRules) {
|
||||
* @returns {Object} - Parsed node data
|
||||
*/
|
||||
function parseNode(node, key, rules, lib, layout) {
|
||||
if (!node) return null
|
||||
if (!node) return null;
|
||||
|
||||
const isNotStatement = node.node.statement && node.node.statement.type === 'not'
|
||||
const processedNode = isNotStatement ? invertNotStatementSuccess(node) : node
|
||||
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
|
||||
if (!processedNode.success) {
|
||||
return null;
|
||||
}
|
||||
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({
|
||||
const result = {
|
||||
key: key,
|
||||
data: {
|
||||
type: 'call-info',
|
||||
name: callInfo
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
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
|
||||
if (!node.locations || node.locations.length === 0) return null;
|
||||
|
||||
const location = node.locations[0]
|
||||
if (location.type !== 'call') 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
|
||||
// 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 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 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')}`;
|
||||
//const callInfo = `${pname}{pid:${pid},tid:${tid},call:${callId}}\n${s.join('\n')}`;
|
||||
|
||||
return { processName: pname, callInfo: s.join('\n') }
|
||||
return { processName: pname, callInfo: s.join("\n") };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -305,12 +307,12 @@ function getCallInfo(node, layout) {
|
||||
* 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)]
|
||||
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)];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,26 +322,26 @@ function partition(str, separator) {
|
||||
* @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'
|
||||
if (!layout || !layout.processes || !Array.isArray(layout.processes)) {
|
||||
console.error("Invalid layout structure");
|
||||
return "Unknown Process";
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -363,16 +365,16 @@ function getProcessName(layout, address) {
|
||||
* 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
|
||||
]
|
||||
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
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,94 +384,94 @@ function rpartition(str, separator) {
|
||||
* @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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!layout || !layout.processes || !Array.isArray(layout.processes)) {
|
||||
console.error("Invalid layout structure");
|
||||
return "Unknown Call";
|
||||
}
|
||||
}
|
||||
|
||||
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 []
|
||||
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])
|
||||
}
|
||||
}))
|
||||
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)}`
|
||||
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");
|
||||
}
|
||||
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, '\\"')
|
||||
return str.replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -478,18 +480,18 @@ function escape(str) {
|
||||
* @returns {Object} The inverted node
|
||||
*/
|
||||
function invertNotStatementSuccess(node) {
|
||||
if (!node) return null
|
||||
if (!node) return null;
|
||||
|
||||
return {
|
||||
...node,
|
||||
children: node.children
|
||||
? node.children.map((child) => ({
|
||||
...child,
|
||||
success: !child.success,
|
||||
children: child.children ? invertNotStatementSuccess(child).children : []
|
||||
}))
|
||||
: []
|
||||
}
|
||||
return {
|
||||
...node,
|
||||
children: node.children
|
||||
? node.children.map((child) => ({
|
||||
...child,
|
||||
success: !child.success,
|
||||
children: child.children ? invertNotStatementSuccess(child).children : []
|
||||
}))
|
||||
: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -498,13 +500,13 @@ function invertNotStatementSuccess(node) {
|
||||
* @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
|
||||
}
|
||||
if (node.node.statement) {
|
||||
return node.node.statement.description;
|
||||
} else if (node.node.feature) {
|
||||
return node.node.feature.description;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -513,12 +515,12 @@ function getNodeDescription(node) {
|
||||
* @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
|
||||
if (node.node.statement) {
|
||||
return getStatementName(node.node.statement);
|
||||
} else if (node.node.feature) {
|
||||
return getFeatureName(node.node.feature);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -527,18 +529,18 @@ function getNodeName(node) {
|
||||
* @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}:`
|
||||
}
|
||||
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}:`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -547,19 +549,19 @@ function getStatementName(statement) {
|
||||
* @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]}`
|
||||
}
|
||||
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]}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -568,21 +570,21 @@ function getFeatureName(feature) {
|
||||
* @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
|
||||
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}`
|
||||
}
|
||||
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} `
|
||||
// for example: count(mnemonic(xor)): 2 or more
|
||||
return `${rangeType}: ${rangeValue} `;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -591,11 +593,11 @@ function getRangeName(statement) {
|
||||
* @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
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -605,10 +607,10 @@ function getNodeAddress(node) {
|
||||
*/
|
||||
|
||||
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()
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -617,13 +619,13 @@ function formatBytes(byteString) {
|
||||
* @returns {string} - Formatted address string
|
||||
*/
|
||||
function formatDynamicAddress(value) {
|
||||
const parts = ['ppid', 'pid', 'tid', 'id']
|
||||
return value
|
||||
.map((item, index) => `${parts[index]}:${item}`)
|
||||
.reverse()
|
||||
.join(',')
|
||||
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()}`
|
||||
return `0x${address.toString(16).toUpperCase()}`;
|
||||
}
|
||||
|
||||
@@ -8,25 +8,25 @@
|
||||
* @returns {string|null} The MBC URL or null if the ID is invalid.
|
||||
*/
|
||||
export function createMBCHref(mbc) {
|
||||
let baseUrl
|
||||
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
|
||||
}
|
||||
// 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, '-')
|
||||
// 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`
|
||||
// Construct the final URL
|
||||
return `${baseUrl}/${objectivePath}/${behaviorPath}.md`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,16 +37,16 @@ export function createMBCHref(mbc) {
|
||||
* @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('.')
|
||||
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
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,120 +1,120 @@
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
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 { ref, computed, onMounted } from "vue";
|
||||
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 demoRdocStatic from '../../../tests/data/rd/al-khaser_x64.exe_.json'
|
||||
import demoRdocDynamic from '../../../tests/data/rd/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json'
|
||||
import demoRdocStatic from "../../../tests/data/rd/al-khaser_x64.exe_.json";
|
||||
import demoRdocDynamic from "../../../tests/data/rd/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json";
|
||||
|
||||
import { useRdocLoader } from '../composables/useRdocLoader'
|
||||
const { rdocData, isValidVersion, loadRdoc } = useRdocLoader()
|
||||
import { useRdocLoader } from "../composables/useRdocLoader";
|
||||
const { rdocData, isValidVersion, loadRdoc } = useRdocLoader();
|
||||
|
||||
import { isGzipped, decompressGzip, readFileAsText } from '../utils/fileUtils'
|
||||
import { isGzipped, decompressGzip, readFileAsText } from "../utils/fileUtils";
|
||||
|
||||
const showCapabilitiesByFunctionOrProcess = ref(false)
|
||||
const showLibraryRules = ref(false)
|
||||
const showNamespaceChart = ref(false)
|
||||
const showCapabilitiesByFunctionOrProcess = ref(false);
|
||||
const showLibraryRules = ref(false);
|
||||
const showNamespaceChart = ref(false);
|
||||
|
||||
const flavor = computed(() => rdocData.value?.meta.flavor)
|
||||
const flavor = computed(() => rdocData.value?.meta.flavor);
|
||||
|
||||
const libraryRuleMatchesCount = computed(() => {
|
||||
if (!rdocData.value || !rdocData.value.rules) return 0
|
||||
return Object.values(rdocData.value.rules).filter((rule) => rule.meta.lib).length
|
||||
})
|
||||
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
|
||||
}
|
||||
showCapabilitiesByFunctionOrProcess.value = value;
|
||||
};
|
||||
|
||||
const updateShowLibraryRules = (value) => {
|
||||
showLibraryRules.value = value
|
||||
}
|
||||
showLibraryRules.value = value;
|
||||
};
|
||||
|
||||
const updateShowNamespaceChart = (value) => {
|
||||
showNamespaceChart.value = value
|
||||
}
|
||||
showNamespaceChart.value = value;
|
||||
};
|
||||
|
||||
const loadFromLocal = async (event) => {
|
||||
const file = event.files[0]
|
||||
const file = event.files[0];
|
||||
|
||||
let fileContent
|
||||
if (await isGzipped(file)) {
|
||||
fileContent = await decompressGzip(file)
|
||||
} else {
|
||||
fileContent = await readFileAsText(file)
|
||||
}
|
||||
let fileContent;
|
||||
if (await isGzipped(file)) {
|
||||
fileContent = await decompressGzip(file);
|
||||
} else {
|
||||
fileContent = await readFileAsText(file);
|
||||
}
|
||||
|
||||
const jsonData = JSON.parse(fileContent)
|
||||
const jsonData = JSON.parse(fileContent);
|
||||
|
||||
loadRdoc(jsonData)
|
||||
}
|
||||
loadRdoc(jsonData);
|
||||
};
|
||||
|
||||
const loadFromURL = (url) => {
|
||||
loadRdoc(url)
|
||||
}
|
||||
loadRdoc(url);
|
||||
};
|
||||
|
||||
const loadDemoDataStatic = () => {
|
||||
loadRdoc(demoRdocStatic)
|
||||
}
|
||||
loadRdoc(demoRdocStatic);
|
||||
};
|
||||
|
||||
const loadDemoDataDynamic = () => {
|
||||
loadRdoc(demoRdocDynamic)
|
||||
}
|
||||
loadRdoc(demoRdocDynamic);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const rdocURL = urlParams.get('rdoc')
|
||||
if (rdocURL) {
|
||||
loadFromURL(rdocURL)
|
||||
}
|
||||
})
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const rdocURL = urlParams.get("rdoc");
|
||||
if (rdocURL) {
|
||||
loadFromURL(rdocURL);
|
||||
}
|
||||
});
|
||||
</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>
|
||||
<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="flavor"
|
||||
:library-rule-matches-count="libraryRuleMatchesCount"
|
||||
@update:show-capabilities-by-function-or-process="updateShowCapabilitiesByFunctionOrProcess"
|
||||
@update:show-library-rules="updateShowLibraryRules"
|
||||
@update:show-namespace-chart="updateShowNamespaceChart"
|
||||
/>
|
||||
<Toast position="bottom-center" group="bc" />
|
||||
<template v-if="rdocData && isValidVersion">
|
||||
<MetadataPanel :data="rdocData" />
|
||||
<SettingsPanel
|
||||
:flavor="flavor"
|
||||
:library-rule-matches-count="libraryRuleMatchesCount"
|
||||
@update:show-capabilities-by-function-or-process="updateShowCapabilitiesByFunctionOrProcess"
|
||||
@update:show-library-rules="updateShowLibraryRules"
|
||||
@update:show-namespace-chart="updateShowNamespaceChart"
|
||||
/>
|
||||
|
||||
<RuleMatchesTable
|
||||
v-if="!showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
|
||||
:data="rdocData"
|
||||
:show-library-rules="showLibraryRules"
|
||||
/>
|
||||
<FunctionCapabilities
|
||||
v-if="flavor === 'static' && showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
|
||||
:data="rdocData"
|
||||
:show-library-rules="showLibraryRules"
|
||||
/>
|
||||
<ProcessCapabilities
|
||||
v-else-if="flavor === 'dynamic' && showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
|
||||
:data="rdocData"
|
||||
:show-capabilities-by-process="showCapabilitiesByFunctionOrProcess"
|
||||
:show-library-rules="showLibraryRules"
|
||||
/>
|
||||
<NamespaceChart v-else-if="showNamespaceChart" :data="rdocData" />
|
||||
</template>
|
||||
<RuleMatchesTable
|
||||
v-if="!showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
|
||||
:data="rdocData"
|
||||
:show-library-rules="showLibraryRules"
|
||||
/>
|
||||
<FunctionCapabilities
|
||||
v-if="flavor === 'static' && showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
|
||||
:data="rdocData"
|
||||
:show-library-rules="showLibraryRules"
|
||||
/>
|
||||
<ProcessCapabilities
|
||||
v-else-if="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