Merge branch 'master' into vmray-extractor

This commit is contained in:
Yacine
2024-08-17 02:15:01 +01:00
committed by GitHub
31 changed files with 444 additions and 260 deletions

View File

@@ -3,6 +3,10 @@ name: build
on:
pull_request:
branches: [ master ]
paths-ignore:
- 'web/**'
- 'doc/**'
- '**.md'
release:
types: [edited, published]

View File

@@ -1,10 +1,22 @@
name: CI
# tests.yml workflow will run for all changes except:
# any file or directory under web/ or doc/
# any Markdown (.md) file anywhere in the repository
on:
push:
branches: [ master ]
paths-ignore:
- 'web/**'
- 'doc/**'
- '**.md'
pull_request:
branches: [ master ]
paths-ignore:
- 'web/**'
- 'doc/**'
- '**.md'
permissions: read-all

View File

@@ -1,11 +1,10 @@
name: deploy Capa Explorer Web to Github Pages
name: deploy web to GitHub Pages
on:
# Runs on pushes targeting the webui branch
push:
branches: [ master ]
branches: [ master, "wb/webui-actions-1" ]
paths:
- 'web/explorer/**'
- 'web/**'
# Allows to run this workflow manually from the Actions tab
workflow_dispatch:
@@ -22,10 +21,17 @@ concurrency:
cancel-in-progress: true
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
build-landing-page:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/upload-artifact@v4
with:
name: landing-page
path: './web/public'
build-explorer:
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -43,24 +49,41 @@ jobs:
- name: Install dependencies
run: npm ci
working-directory: ./web/explorer
- name: Lint
run: npm run lint
- name: Generate release bundle
run: npm run build:bundle
working-directory: ./web/explorer
- name: Format
run: npm run format:check
working-directory: ./web/explorer
- name: Run unit tests
run: npm run test
- name: Zip release bundle
run: zip -r public/capa-explorer-web.zip capa-explorer-web
working-directory: ./web/explorer
- name: Build
run: npm run build
working-directory: ./web/explorer
- uses: actions/upload-artifact@v4
with:
name: explorer
path: './web/explorer/dist'
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: [build-landing-page, build-explorer]
steps:
- uses: actions/download-artifact@v4
with:
name: landing-page
path: './public/'
- uses: actions/download-artifact@v4
with:
name: explorer
path: './public/explorer'
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './web/explorer/dist'
path: './public'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

4
.gitmodules vendored
View File

@@ -1,6 +1,6 @@
[submodule "rules"]
path = rules
url = ../capa-rules.git
url = ../../mandiant/capa-rules.git
[submodule "tests/data"]
path = tests/data
url = ../capa-testfiles.git
url = ../../mandiant/capa-testfiles.git

View File

@@ -9,13 +9,17 @@ Unlock powerful malware analysis with capa's new [VMRay sandbox](https://www.vmr
- webui: explore capa analysis results in a web-based UI online and offline #2224 @s-ff
- support analyzing DRAKVUF traces #2143 @yelhamer
- dynamic: add support for VMRay dynamic sandbox traces #2208 @mike-hunhoff @r-sm2024 @mr-tz
- IDA extractor: extract names from dynamically resolved APIs stored in renamed global variables #2201 @Ana06
### Breaking Changes
### New Rules (2)
### New Rules (5)
- nursery/upload-file-to-onedrive jaredswilson@google.com ervinocampo@google.com
- data-manipulation/encoding/base64/decode-data-using-base64-via-vbmi-lookup-table still@teamt5.org
- communication/socket/attach-bpf-to-socket-on-linux jakub.jozwiak@mandiant.com
- anti-analysis/anti-av/overwrite-dll-text-section-to-remove-hooks jakub.jozwiak@mandiant.com
- nursery/delete-file-on-linux mehunhoff@google.com
-
### Bug Fixes
@@ -28,6 +32,8 @@ Unlock powerful malware analysis with capa's new [VMRay sandbox](https://www.vmr
### Development
- CI: use macos-12 since macos-11 is deprecated and will be removed on June 28th, 2024 #2173 @mr-tz
- CI: update Binary Ninja version to 4.1 and use Python 3.9 to test it #2211 @xusheng6
- CI: update tests.yml workflow to exclude web and documentation files #2263 @s-ff
- CI: update build.yml workflow to exclude web and documentation files #2270 @s-ff
### Raw diffs
- [capa v7.1.0...master](https://github.com/mandiant/capa/compare/v7.1.0...master)

View File

@@ -11,13 +11,13 @@ capa detects capabilities in executable files.
You run it against a PE, ELF, .NET module, shellcode file, or a sandbox report and it tells you what it thinks the program can do.
For example, it might suggest that the file is a backdoor, is capable of installing services, or relies on HTTP to communicate.
Check out our capa blog posts:
- [Dynamic capa: Exploring Executable Run-Time Behavior with the CAPE Sandbox](https://www.mandiant.com/resources/blog/dynamic-capa-executable-behavior-cape-sandbox)
- [capa v4: casting a wider .NET](https://www.mandiant.com/resources/blog/capa-v4-casting-wider-net) (.NET support)
- [ELFant in the Room capa v3](https://www.mandiant.com/resources/elfant-in-the-room-capa-v3) (ELF support)
- [capa 2.0: Better, Stronger, Faster](https://www.mandiant.com/resources/capa-2-better-stronger-faster)
- [capa: Automatically Identify Malware Capabilities](https://www.mandiant.com/resources/capa-automatically-identify-malware-capabilities)
To interactively inspect capa results in your browser use the [capa web explorer](https://mandiant.github.io/capa/explorer/).
If you want to inspect or write capa rules, head on over to the [capa-rules repository](https://github.com/mandiant/capa-rules). Otherwise, keep reading.
Below you find a list of [our capa blog posts with more details.](#blog-posts)
# example capa output
```
$ capa.exe suspicious.exe
@@ -72,16 +72,23 @@ Download stable releases of the standalone capa binaries [here](https://github.c
To use capa as a library or integrate with another tool, see [doc/installation.md](https://github.com/mandiant/capa/blob/master/doc/installation.md) for further setup instructions.
For more information about how to use capa, see [doc/usage.md](https://github.com/mandiant/capa/blob/master/doc/usage.md).
# web explorer
The [capa web explorer](https://mandiant.github.io/capa/explorer/) enables you to interactively explore capa results in your web browser. Besides the online version you can download a standalone HTML file for local offline usage.
![capa web explorer screenshot](https://github.com/mandiant/capa/blob/master/doc/img/capa_web_explorer.png)
More details on the web UI is available in the [capa web explorer README](https://github.com/mandiant/capa/blob/master/web/explorer/README.md).
# example
In the above sample output, we ran capa against an unknown binary (`suspicious.exe`),
and the tool reported that the program can send HTTP requests, decode data via XOR and Base64,
In the above sample output, we run capa against an unknown binary (`suspicious.exe`),
and the tool reports that the program can send HTTP requests, decode data via XOR and Base64,
install services, and spawn new processes.
Taken together, this makes us think that `suspicious.exe` could be a persistent backdoor.
Therefore, our next analysis step might be to run `suspicious.exe` in a sandbox and try to recover the command and control server.
## detailed results
By passing the `-vv` flag (for very verbose), capa reports exactly where it found evidence of these capabilities.
This is useful for at least two reasons:
@@ -131,6 +138,7 @@ capa also supports dynamic capabilities detection for multiple sandboxes includi
* [DRAKVUF](https://github.com/CERT-Polska/drakvuf-sandbox/) (supported report formats: `.log`, `.log.gz`)
* [VMRay](https://www.vmray.com/) (supported report formats: analysis archive `.zip`)
To use this feature, submit your file to a supported sandbox and then download and run capa against the generated report file. This feature enables capa to match capabilities against dynamic and static features that the sandbox captured during execution.
Here's an example of running capa against a packed file, and then running capa against the CAPE report generated for the same packed file:
@@ -220,6 +228,7 @@ $ capa 05be49819139a3fdcdbddbdefd298398779521f3d68daa25275cc77508e42310.json
┕━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┷━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┙
```
# capa rules
capa uses a collection of rules to identify capabilities within a program.
These rules are easy to write, even for those new to reverse engineering.
By authoring rules, you can extend the capabilities that capa recognizes.
@@ -256,18 +265,28 @@ rule:
- property/read: System.Net.Sockets.TcpClient::Client
```
The [github.com/mandiant/capa-rules](https://github.com/mandiant/capa-rules) repository contains hundreds of standard library rules that are distributed with capa.
The [github.com/mandiant/capa-rules](https://github.com/mandiant/capa-rules) repository contains hundreds of standard rules that are distributed with capa.
Please learn to write rules and contribute new entries as you find interesting techniques in malware.
# IDA Pro plugin: capa explorer
If you use IDA Pro, then you can use the [capa explorer](https://github.com/mandiant/capa/tree/master/capa/ida/plugin) plugin.
capa explorer helps you identify interesting areas of a program and build new capa rules using features extracted directly from your IDA Pro database.
It also uses your local changes to the .idb to extract better features, such as when you rename a global variable that contains a dynamically resolved API address.
![capa + IDA Pro integration](https://github.com/mandiant/capa/blob/master/doc/img/explorer_expanded.png)
# Ghidra integration
If you use Ghidra, then you can use the [capa + Ghidra integration](/capa/ghidra/) to run capa's analysis directly on your Ghidra database and render the results in Ghidra's user interface.
<img src="https://github.com/mandiant/capa/assets/66766340/eeae33f4-99d4-42dc-a5e8-4c1b8c661492" width=300>
# blog posts
- [Dynamic capa: Exploring Executable Run-Time Behavior with the CAPE Sandbox](https://www.mandiant.com/resources/blog/dynamic-capa-executable-behavior-cape-sandbox)
- [capa v4: casting a wider .NET](https://www.mandiant.com/resources/blog/capa-v4-casting-wider-net) (.NET support)
- [ELFant in the Room capa v3](https://www.mandiant.com/resources/elfant-in-the-room-capa-v3) (ELF support)
- [capa 2.0: Better, Stronger, Faster](https://www.mandiant.com/resources/capa-2-better-stronger-faster)
- [capa: Automatically Identify Malware Capabilities](https://www.mandiant.com/resources/capa-automatically-identify-malware-capabilities)
# further information
## capa
- [Installation](https://github.com/mandiant/capa/blob/master/doc/installation.md)

View File

@@ -5,9 +5,11 @@
# Unless required by applicable law or agreed to in writing, software distributed under the License
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and limitations under the License.
from typing import Any, Dict, Tuple, Iterator
import re
from typing import Any, Dict, Tuple, Iterator, Optional
import idc
import ida_ua
import idaapi
import idautils
@@ -35,9 +37,9 @@ def get_externs(ctx: Dict[str, Any]) -> Dict[int, Any]:
return ctx["externs_cache"]
def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[Any]:
def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Optional[Tuple[str, str]]:
"""check instruction for API call"""
info = ()
info = None
ref = insn.ea
# attempt to resolve API calls by following chained thunks to a reasonable depth
@@ -52,7 +54,7 @@ def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[A
except IndexError:
break
info = funcs.get(ref, ())
info = funcs.get(ref)
if info:
break
@@ -60,8 +62,7 @@ def check_for_api_call(insn: idaapi.insn_t, funcs: Dict[int, Any]) -> Iterator[A
if not f or not (f.flags & idaapi.FUNC_THUNK):
break
if info:
yield info
return info
def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle) -> Iterator[Tuple[Feature, Address]]:
@@ -76,16 +77,39 @@ def extract_insn_api_features(fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle)
if insn.get_canon_mnem() not in ("call", "jmp"):
return
# check calls to imported functions
for api in check_for_api_call(insn, get_imports(fh.ctx)):
# check call to imported functions
api = check_for_api_call(insn, get_imports(fh.ctx))
if api:
# tuple (<module>, <function>, <ordinal>)
for name in capa.features.extractors.helpers.generate_symbols(api[0], api[1]):
yield API(name), ih.address
# a call instruction should only call one function, stop if a call to an import is extracted
return
# check calls to extern functions
for api in check_for_api_call(insn, get_externs(fh.ctx)):
# check call to extern functions
api = check_for_api_call(insn, get_externs(fh.ctx))
if api:
# tuple (<module>, <function>, <ordinal>)
yield API(api[1]), ih.address
# a call instruction should only call one function, stop if a call to an extern is extracted
return
# extract dynamically resolved APIs stored in renamed globals (renamed for example using `renimp.idc`)
# examples: `CreateProcessA`, `HttpSendRequestA`
if insn.Op1.type == ida_ua.o_mem:
op_addr = insn.Op1.addr
op_name = idaapi.get_name(op_addr)
# when renaming a global using an API name, IDA assigns it the function type
# ensure we do not extract something wrong by checking that the address has a name and a type
# we could check that the type is a function definition, but that complicates the code
if (not op_name.startswith("off_")) and idc.get_type(op_addr):
# Remove suffix used in repeated names, for example _0 in VirtualFree_0
match = re.match(r"(.+)_\d+", op_name)
if match:
op_name = match.group(1)
# the global name does not include the DLL name, so we can't extract it
for name in capa.features.extractors.helpers.generate_symbols("", op_name):
yield API(name), ih.address
# extract IDA/FLIRT recognized API functions
targets = tuple(idautils.CodeRefsFrom(insn.ea, False))

View File

@@ -81,6 +81,7 @@ can update using the `Settings` button.
* Double-click the `Address` column to navigate your Disassembly view to the address of the associated feature
* Double-click a result in the `Rule Information` column to expand its children
* Select a checkbox in the `Rule Information` column to highlight the address of the associated feature in your Disassembly view
* Reanalyze if you renamed global variables that store dynamically resolved APIs. capa will use these to improve its analysis.
#### Tips for Rule Generator

Binary file not shown.

After

Width:  |  Height:  |  Size: 603 KiB

View File

@@ -137,7 +137,7 @@ dev = [
"flake8-use-pathlib==0.3.0",
"flake8-copyright==0.2.4",
"ruff==0.5.6",
"black==24.4.2",
"black==24.8.0",
"isort==5.13.2",
"mypy==1.11.1",
"mypy-protobuf==3.6.0",
@@ -148,8 +148,8 @@ dev = [
"types-PyYAML==6.0.8",
"types-tabulate==0.9.0.20240106",
"types-termcolor==1.1.4",
"types-psutil==5.8.23",
"types_requests==2.32.0.20240602",
"types-psutil==6.0.0.20240621",
"types_requests==2.32.0.20240712",
"types-protobuf==5.27.0.20240626",
"deptry==0.17.0"
]
@@ -158,7 +158,7 @@ build = [
# we want all developer environments to be consistent.
# These dependencies are not used in production environments
# and should not conflict with other libraries/tooling.
"pyinstaller==6.9.0",
"pyinstaller==6.10.0",
"setuptools==70.0.0",
"build==1.2.1"
]
@@ -189,6 +189,7 @@ known_first_party = [
"ida_loader",
"ida_nalt",
"ida_segment",
"ida_ua",
"idaapi",
"idautils",
"idc",

View File

@@ -21,7 +21,7 @@ mdurl==0.1.2
msgpack==1.0.8
networkx==3.1
pefile==2023.2.7
pip==24.1.2
pip==24.2
protobuf==5.27.3
pyasn1==0.4.8
pyasn1-modules==0.2.8

2
rules

Submodule rules updated: 0e2500fa8a...5b8c8a63a2

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/public/favicon.ico" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Capa Explorer</title>
</head>

View File

@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"build:bundle": "vite build --mode bundle",
"build:bundle": "vite build --mode bundle --outDir=capa-explorer-web",
"preview": "vite preview",
"test": "vitest",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",

View File

@@ -1,4 +1,5 @@
<template>
<Toast position="bottom-center" group="bc" />
<header>
<div class="wrapper">
<BannerHeader />

View File

@@ -7,7 +7,8 @@
size="small"
:filters="filters"
:filterMode="filterMode"
:globalFilterFields="['funcaddr', 'ruleName', 'namespace']"
filterDisplay="row"
:globalFilterFields="['address', 'rule', 'namespace']"
>
<template #header>
<IconField>
@@ -16,35 +17,47 @@
</IconField>
</template>
<Column field="address" sortable header="Function Address" :rowspan="3" class="w-min">
<Column
field="address"
sortable
header="Function Address"
class="w-min"
:showFilterMenu="false"
:showClearButton="false"
>
<template #filter v-if="props.showColumnFilters">
<InputText v-model="filters['address'].value" placeholder="Filter by name" />
</template>
<template #body="{ data }">
<span class="font-monospace">{{ data.address }}</span>
<span class="font-monospace text-base">{{ data.address }}</span>
<span v-if="data.matchCount > 1" class="font-italic">
({{ data.matchCount }} match{{ data.matchCount > 1 ? "es" : "" }})
</span>
</template>
</Column>
<Column field="rule" sortable header="Matches" class="w-min">
<Column field="rule" sortable header="Matches" class="w-min" :showFilterMenu="false" :showClearButton="false">
<template #filter v-if="props.showColumnFilters">
<InputText v-model="filters['rule'].value" placeholder="Filter by name" />
</template>
<template #body="{ data }">
{{ data.rule }}
<LibraryTag v-if="data.lib" />
</template>
</Column>
<Column field="namespace" sortable header="Namespace"></Column>
<Column field="namespace" sortable header="Namespace" :showFilterMenu="false" :showClearButton="false">
<template #filter v-if="props.showColumnFilters">
<InputText v-model="filters['namespace'].value" placeholder="Filter by name" />
</template>
</Column>
</DataTable>
<Dialog v-model:visible="sourceDialogVisible" :style="{ width: '50vw' }">
<highlightjs lang="yml" :code="currentSource" class="bg-white" />
</Dialog>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import DataTable from "primevue/datatable";
import Column from "primevue/column";
import Dialog from "primevue/dialog";
import IconField from "primevue/iconfield";
import InputIcon from "primevue/inputicon";
import InputText from "primevue/inputtext";
@@ -60,33 +73,25 @@ const props = defineProps({
showLibraryRules: {
type: Boolean,
default: false
},
showColumnFilters: {
type: Boolean,
default: false
}
});
const filters = ref({ global: { value: null, matchMode: "contains" } });
const filters = ref({
global: { value: null, matchMode: "contains" },
address: { value: null, matchMode: "contains" },
rule: { value: null, matchMode: "contains" },
namespace: { value: null, matchMode: "contains" }
});
const filterMode = ref("lenient");
const sourceDialogVisible = ref(false);
const currentSource = ref("");
const functionCapabilities = ref([]);
onMounted(() => {
const cacheKey = "functionCapabilities";
let cachedData = sessionStorage.getItem(cacheKey);
if (cachedData) {
// If the data is already in sessionStorage, parse it and use it
functionCapabilities.value = JSON.parse(cachedData);
} else {
// Parse function capabilities and cache the result in sessionStorage
functionCapabilities.value = parseFunctionCapabilities(props.data);
try {
sessionStorage.setItem(cacheKey, JSON.stringify(functionCapabilities.value));
} catch (e) {
console.warn("Failed to store parsed data in sessionStorage:", e);
// If storing fails (e.g., due to storage limits), we can still continue with the parsed data
}
}
functionCapabilities.value = parseFunctionCapabilities(props.data);
});
/*
@@ -116,8 +121,10 @@ const tableData = computed(() => {
</script>
<style scoped>
/* tighten up the spacing between rows */
:deep(.p-datatable.p-datatable-sm .p-datatable-tbody > tr > td) {
/* tighten up the spacing between rows, and change border color */
:deep(.p-datatable-tbody > tr > td) {
padding: 0.2rem 0.5rem !important;
border-width: 0 0 1px 0;
border-color: #97a0ab;
}
</style>

View File

@@ -1,29 +1,29 @@
<script setup>
import { ref } from "vue";
import Menubar from "primevue/menubar";
const items = ref([
{
label: "Import Analysis",
icon: "pi pi-file-import",
command: () => (window.location.href = window.location.origin + "/capa/")
}
]);
import { RouterLink } from "vue-router";
</script>
<template>
<Menubar :model="items" class="p-1">
<Menubar class="p-1">
<template #start>
<RouterLink to="/">
<img src="@/assets/images/icon.png" alt="Logo" class="w-2rem" />
</RouterLink>
</template>
<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"
v-tooltip.right="'Download capa Explorer Web for offline usage'"
href="./capa-explorer-web.zip"
download="capa-explorer-web.zip"
aria-label="Download capa Explorer Web release"
>
<i id="gitsub-icon" class="pi pi-github text-2xl"></i>
<i class="pi pi-download text-xl"></i>
</a>
<a v-ripple href="https://github.com/mandiant/capa" class="flex justify-content-center w-2rem">
<i class="pi pi-github text-2xl"></i>
</a>
<img src="@/assets/images/icon.png" alt="Logo" class="w-2rem" />
</div>
</template>
</Menubar>

View File

@@ -7,7 +7,7 @@
filterMode="lenient"
sortField="pid"
:sortOrder="1"
rowHover="true"
:rowHover="true"
>
<Column field="processname" header="Process" expander>
<template #body="slotProps">

View File

@@ -152,12 +152,11 @@
<span v-if="item.icon !== 'vt-icon'" :class="item.icon" />
<VTIcon v-else-if="item.icon === 'vt-icon'" />
<span>{{ item.label }}</span>
<i v-if="item.description" class="pi pi-info-circle text-xs" v-tooltip.right="item.description" />
</a>
</template>
</ContextMenu>
<Toast />
<!-- Source code dialog -->
<Dialog v-model:visible="sourceDialogVisible" style="width: 50vw">
<highlightjs autodetect :code="currentSource" />
@@ -217,6 +216,13 @@ const expandedKeys = ref({});
const menu = ref();
const selectedNode = ref({});
const contextMenuItems = computed(() => [
{
label: "Copy rule name",
icon: "pi pi-copy",
command: () => {
navigator.clipboard.writeText(selectedNode.value.data?.name);
}
},
{
label: "View source",
icon: "pi pi-eye",
@@ -234,6 +240,7 @@ const contextMenuItems = computed(() => [
label: "Lookup rule in VirusTotal",
icon: "vt-icon",
target: "_blank",
description: "Requires VirusTotal Premium account",
url: createVirusTotalUrl(selectedNode.value.data?.name)
}
]);
@@ -325,23 +332,7 @@ const showSource = (source) => {
};
onMounted(() => {
const cacheKey = "ruleMatches";
const cachedData = sessionStorage.getItem(cacheKey);
if (cachedData) {
// If cached data exists, parse and use it
treeData.value = JSON.parse(cachedData);
} else {
// If no cached data, parse the rules and store in sessionStorage
treeData.value = parseRules(props.data.rules, props.data.meta.flavor, props.data.meta.analysis.layout);
// Store the parsed data in sessionStorage
try {
sessionStorage.setItem(cacheKey, JSON.stringify(treeData.value));
} catch (e) {
console.warn("Failed to store parsed data in sessionStorage:", e);
// If storing fails (e.g., due to storage limits), we can still continue with the parsed data
}
}
treeData.value = parseRules(props.data.rules, props.data.meta.flavor, props.data.meta.analysis.layout);
});
</script>
@@ -357,11 +348,6 @@ onMounted(() => {
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;
}
/* Make all matches nodes (i.e. not rule names) slightly smaller,
and tighten up the spacing between the rows */

View File

@@ -16,13 +16,14 @@
v-model="showLibraryRules"
inputId="showLibraryRules"
:binary="true"
:disabled="showNamespaceChart"
:disabled="showNamespaceChart || libraryRuleMatchesCount === 0"
/>
<label for="showLibraryRules">
<span v-if="libraryRuleMatchesCount > 1">
Show {{ libraryRuleMatchesCount }} library rule matches
Show {{ libraryRuleMatchesCount }} distinct library rules
</span>
<span v-else>Show 1 library rule match</span>
<span v-else-if="libraryRuleMatchesCount === 1">Show 1 distinct library rule</span>
<span v-else>No library rules matched</span>
</label>
</div>
<div class="flex flex-row align-items-center gap-2">

View File

@@ -1,6 +1,6 @@
<template>
<div class="cursor-default">
<!--- example node: "parse PE headers (2 matches) lib" --->
<!-- example node: "parse PE headers (2 matches) lib" -->
<template v-if="node.data.type === 'rule'">
<div>
<span>{{ node.data.name }}</span>
@@ -9,12 +9,12 @@
</div>
</template>
<!--- example node: "basic block @ 0x401000" or "explorer.exe" --->
<!-- example node: "basic block @ 0x401000" or "explorer.exe" -->
<template v-else-if="node.data.type === 'match location'">
<span class="text-sm font-italic">{{ node.data.name }}</span>
</template>
<!--- example node: "- or", "- and" --->
<!-- example node: "- or", "- and" -->
<template v-else-if="node.data.type === 'statement'"
>-
<span
@@ -27,7 +27,7 @@
</span>
</template>
<!--- example node: "- api: GetProcAddress", "- regex: .*\\.exe" --->
<!-- example node: "- api: GetProcAddress", "- regex: .*\\.exe" -->
<template v-else-if="node.data.type === 'feature'">
<span>
- {{ node.data.typeValue }}:
@@ -37,17 +37,17 @@
</span>
</template>
<!--- example node: "- malware.exe" (these are the captures (i.e. children nodes) of regex nodes) --->
<!-- example node: "- malware.exe" (these are the captures (i.e. children nodes) of regex nodes) -->
<template v-else-if="node.data.type === 'regex-capture'">
- <span class="text-green-700 font-monospace">{{ node.data.name }}</span>
</template>
<!--- example node: "exit(0) -> 0" (if the node type is call-info, we highlight node.data.name.callInfo) --->
<!-- example node: "exit(0) -> 0" (if the node type is call-info, we highlight node.data.name.callInfo) -->
<template v-else-if="node.data.type === 'call-info'">
<highlightjs lang="c" :code="node.data.name.callInfo" />
</template>
<!-- example node: " = IMAGE_NT_SIGNATURE (PE)" --->
<!-- example node: " = IMAGE_NT_SIGNATURE (PE)" -->
<span v-if="node.data.description" class="text-gray-500 text-sm" style="font-size: 90%">
= {{ node.data.description }}
</span>
@@ -55,7 +55,6 @@
</template>
<script setup>
import { defineProps } from "vue";
import LibraryTag from "@/components/misc/LibraryTag.vue";
defineProps({

View File

@@ -1,11 +1,8 @@
import { ref, readonly } from "vue";
import { useToast } from "primevue/usetoast";
import { isGzipped, decompressGzip, readFileAsText } from "@/utils/fileUtils";
export function useRdocLoader() {
const toast = useToast();
const rdocData = ref(null);
const isValidVersion = ref(false);
const MIN_SUPPORTED_VERSION = "7.0.0";
/**
@@ -47,6 +44,14 @@ export function useRdocLoader() {
throw new Error(`HTTP error! status: ${response.status}`);
}
data = await response.json();
} else if (source instanceof File) {
let fileContent;
if (await isGzipped(source)) {
fileContent = await decompressGzip(source);
} else {
fileContent = await readFileAsText(source);
}
data = JSON.parse(fileContent);
} else if (typeof source === "object") {
// Direct JSON object (Preview options)
data = source;
@@ -55,8 +60,6 @@ export function useRdocLoader() {
}
if (checkVersion(data)) {
rdocData.value = data;
isValidVersion.value = true;
toast.add({
severity: "success",
summary: "Success",
@@ -64,9 +67,7 @@ export function useRdocLoader() {
life: 3000,
group: "bc" // bottom-center
});
} else {
rdocData.value = null;
isValidVersion.value = false;
return data;
}
} catch (error) {
console.error("Error loading JSON:", error);
@@ -78,11 +79,10 @@ export function useRdocLoader() {
group: "bc" // bottom-center
});
}
return null;
};
return {
rdocData: readonly(rdocData),
isValidVersion: readonly(isValidVersion),
loadRdoc
};
}

View File

@@ -1,6 +1,9 @@
import { createRouter, createWebHashHistory } from "vue-router";
import ImportView from "../views/ImportView.vue";
import NotFoundView from "../views/NotFoundView.vue";
import ImportView from "@/views/ImportView.vue";
import NotFoundView from "@/views/NotFoundView.vue";
import AnalysisView from "@/views/AnalysisView.vue";
import { rdocStore } from "@/store/rdocStore";
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
@@ -10,6 +13,20 @@ const router = createRouter({
name: "home",
component: ImportView
},
{
path: "/analysis",
name: "analysis",
component: AnalysisView,
beforeEnter: (to, from, next) => {
if (rdocStore.data.value === null) {
// No rdoc loaded, redirect to home page
next({ name: "home" });
} else {
// rdoc is loaded, proceed to analysis page
next();
}
}
},
// 404 Route - This should be the last route
{
path: "/:pathMatch(.*)*",

View File

@@ -0,0 +1,11 @@
import { ref } from "vue";
export const rdocStore = {
data: ref(null),
setData(newData) {
this.data.value = newData;
},
clearData() {
this.data.value = null;
}
};

View File

@@ -0,0 +1,76 @@
<template>
<MetadataPanel :data="doc" />
<SettingsPanel
:flavor="doc.meta.flavor"
:library-rule-matches-count="libraryRuleMatchesCount"
@update:show-capabilities-by-function-or-process="updateShowCapabilitiesByFunctionOrProcess"
@update:show-library-rules="updateShowLibraryRules"
@update:show-namespace-chart="updateShowNamespaceChart"
@update:show-column-filters="updateShowColumnFilters"
/>
<RuleMatchesTable
v-if="!showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
:data="doc"
:show-library-rules="showLibraryRules"
:show-column-filters="showColumnFilters"
/>
<FunctionCapabilities
v-if="doc.meta.flavor === 'static' && showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
:data="doc"
:show-library-rules="showLibraryRules"
:show-column-filters="showColumnFilters"
/>
<ProcessCapabilities
v-else-if="doc.meta.flavor === 'dynamic' && showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
:data="doc"
:show-capabilities-by-process="showCapabilitiesByFunctionOrProcess"
:show-library-rules="showLibraryRules"
:show-column-filters="showColumnFilters"
/>
<NamespaceChart v-else-if="showNamespaceChart" :data="doc" />
</template>
<script setup>
import { ref, computed } from "vue";
// Componenets
import MetadataPanel from "@/components/MetadataPanel.vue";
import SettingsPanel from "@/components/SettingsPanel.vue";
import RuleMatchesTable from "@/components/RuleMatchesTable.vue";
import FunctionCapabilities from "@/components/FunctionCapabilities.vue";
import ProcessCapabilities from "@/components/ProcessCapabilities.vue";
import NamespaceChart from "@/components/NamespaceChart.vue";
// Import loaded rdoc
import { rdocStore } from "@/store/rdocStore";
const doc = rdocStore.data.value;
// Viewing options
const showCapabilitiesByFunctionOrProcess = ref(false);
const showLibraryRules = ref(false);
const showNamespaceChart = ref(false);
const showColumnFilters = ref(false);
// Count library rules
const libraryRuleMatchesCount = computed(() => {
if (!doc || !doc.rules) return 0;
return Object.values(rdocStore.data.value.rules).filter((rule) => rule.meta.lib).length;
});
// Event handlers to update variables
const updateShowCapabilitiesByFunctionOrProcess = (value) => {
showCapabilitiesByFunctionOrProcess.value = value;
};
const updateShowLibraryRules = (value) => {
showLibraryRules.value = value;
};
const updateShowNamespaceChart = (value) => {
showNamespaceChart.value = value;
};
const updateShowColumnFilters = (value) => {
showColumnFilters.value = value;
};
</script>

View File

@@ -1,130 +1,78 @@
<template>
<DescriptionPanel />
<UploadOptions
@load-from-local="loadFromLocal"
@load-from-url="loadFromURL"
@load-demo-static="loadDemoDataStatic"
@load-demo-dynamic="loadDemoDataDynamic"
/>
</template>
<script setup>
import { ref, computed, onMounted } from "vue";
import { watch } from "vue";
// componenets
import DescriptionPanel from "@/components/DescriptionPanel.vue";
import UploadOptions from "@/components/UploadOptions.vue";
import MetadataPanel from "@/components/MetadataPanel.vue";
import RuleMatchesTable from "@/components/RuleMatchesTable.vue";
import FunctionCapabilities from "@/components/FunctionCapabilities.vue";
import ProcessCapabilities from "@/components/ProcessCapabilities.vue";
import SettingsPanel from "@/components/SettingsPanel.vue";
import NamespaceChart from "@/components/NamespaceChart.vue";
import Toast from "primevue/toast";
// import demo data
import demoRdocStatic from "@testfiles/rd/al-khaser_x64.exe_.json";
import demoRdocDynamic from "@testfiles/rd/0000a65749f5902c4d82ffa701198038f0b4870b00a27cfca109f8f933476d82.json";
// import router utils
import { useRouter, useRoute } from "vue-router";
const router = useRouter();
const route = useRoute();
// import rdoc loader function
import { useRdocLoader } from "@/composables/useRdocLoader";
const { rdocData, isValidVersion, loadRdoc } = useRdocLoader();
const { loadRdoc } = useRdocLoader();
import { isGzipped, decompressGzip, readFileAsText } from "@/utils/fileUtils";
const showCapabilitiesByFunctionOrProcess = ref(false);
const showLibraryRules = ref(false);
const showNamespaceChart = ref(false);
const showColumnFilters = ref(false);
const libraryRuleMatchesCount = computed(() => {
if (!rdocData.value || !rdocData.value.rules) return 0;
return Object.values(rdocData.value.rules).filter((rule) => rule.meta.lib).length;
});
const updateShowCapabilitiesByFunctionOrProcess = (value) => {
showCapabilitiesByFunctionOrProcess.value = value;
};
const updateShowLibraryRules = (value) => {
showLibraryRules.value = value;
};
const updateShowNamespaceChart = (value) => {
showNamespaceChart.value = value;
};
const updateShowColumnFilters = (value) => {
showColumnFilters.value = value;
};
// import rdoc store
import { rdocStore } from "@/store/rdocStore";
const loadFromLocal = async (event) => {
const file = event.files[0];
let fileContent;
if (await isGzipped(file)) {
fileContent = await decompressGzip(file);
} else {
fileContent = await readFileAsText(file);
const result = await loadRdoc(event.files[0]);
if (result) {
rdocStore.setData(result);
router.push("/analysis");
}
const jsonData = JSON.parse(fileContent);
loadRdoc(jsonData);
};
const loadFromURL = (url) => {
loadRdoc(url);
};
const loadDemoDataStatic = () => {
loadRdoc(demoRdocStatic);
};
const loadDemoDataDynamic = () => {
loadRdoc(demoRdocDynamic);
};
onMounted(() => {
// Clear out sessionStorage to prevent stale data from being used
sessionStorage.clear();
// Check if the URL contains a rdoc parameter and load the data from that URL
const urlParams = new URLSearchParams(window.location.search);
const encodedRdocURL = urlParams.get("rdoc");
if (encodedRdocURL) {
const rdocURL = decodeURIComponent(encodedRdocURL);
loadFromURL(rdocURL);
const loadFromURL = async (url) => {
const result = await loadRdoc(url);
if (result) {
rdocStore.setData(result);
router.push({ name: "analysis", query: { rdoc: url } });
}
});
};
const loadDemoDataStatic = async () => {
const result = await loadRdoc(demoRdocStatic);
if (result) {
rdocStore.setData(demoRdocStatic);
router.push("/analysis");
}
};
const loadDemoDataDynamic = async () => {
const result = await loadRdoc(demoRdocDynamic);
if (result) {
rdocStore.setData(demoRdocDynamic);
router.push("/analysis");
}
};
// Watch for changes in the rdoc query parameter
watch(
() => route.query.rdoc,
(rdocURL) => {
if (rdocURL) {
// Clear the query parameter
router.replace({ query: {} });
loadFromURL(decodeURIComponent(rdocURL));
}
},
{ immediate: true }
);
</script>
<template>
<Panel v-if="!rdocData || !isValidVersion">
<DescriptionPanel />
<UploadOptions
@load-from-local="loadFromLocal"
@load-from-url="loadFromURL"
@load-demo-static="loadDemoDataStatic"
@load-demo-dynamic="loadDemoDataDynamic"
/>
</Panel>
<Toast position="bottom-center" group="bc" />
<template v-if="rdocData && isValidVersion">
<MetadataPanel :data="rdocData" />
<SettingsPanel
:flavor="rdocData.meta.flavor"
:library-rule-matches-count="libraryRuleMatchesCount"
@update:show-capabilities-by-function-or-process="updateShowCapabilitiesByFunctionOrProcess"
@update:show-library-rules="updateShowLibraryRules"
@update:show-namespace-chart="updateShowNamespaceChart"
@update:show-column-filters="updateShowColumnFilters"
/>
<RuleMatchesTable
v-if="!showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
:data="rdocData"
:show-library-rules="showLibraryRules"
:show-column-filters="showColumnFilters"
/>
<FunctionCapabilities
v-if="rdocData.meta.flavor === 'static' && showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
:data="rdocData"
:show-library-rules="showLibraryRules"
/>
<ProcessCapabilities
v-else-if="rdocData.meta.flavor === 'dynamic' && showCapabilitiesByFunctionOrProcess && !showNamespaceChart"
:data="rdocData"
:show-capabilities-by-process="showCapabilitiesByFunctionOrProcess"
:show-library-rules="showLibraryRules"
/>
<NamespaceChart v-else-if="showNamespaceChart" :data="rdocData" />
</template>
</template>

View File

@@ -3,12 +3,11 @@ import vue from "@vitejs/plugin-vue";
import { viteSingleFile } from "vite-plugin-singlefile";
import { fileURLToPath, URL } from "node:url";
// eslint-disable-next-line no-unused-vars
export default defineConfig(({ command, mode }) => {
export default defineConfig(({ mode }) => {
const isBundle = mode === "bundle";
return {
base: isBundle ? "/" : "/capa/",
base: "./",
plugins: isBundle ? [vue(), viteSingleFile()] : [vue()],
resolve: {
alias: {

BIN
web/public/img/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
web/public/img/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

49
web/public/index.html Normal file
View File

@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<link rel="icon" href="img/icon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>capa</title>
<style>
/*
Josh's Custom CSS Reset
https://www.joshwcomeau.com/css/custom-css-reset/
*/
*, *::before, *::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
input, button, textarea, select {
font: inherit;
}
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
#root, #__next {
isolation: isolate;
}
</style>
</head>
<body>
<div style="height: 100%; display: flex; align-items: center; justify-content: center;">
<div>
<!-- this is centered -->
<img src="./img/icon.png" />
<br />
<a href="./explorer/">capa Explorer Web<a>
</div>
</div>
</body>
</html>