mirror of
https://github.com/mandiant/capa.git
synced 2025-12-12 15:49:46 -08:00
Merge branch 'master' into vmray-extractor
This commit is contained in:
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -3,6 +3,10 @@ name: build
|
||||
on:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
paths-ignore:
|
||||
- 'web/**'
|
||||
- 'doc/**'
|
||||
- '**.md'
|
||||
release:
|
||||
types: [edited, published]
|
||||
|
||||
|
||||
12
.github/workflows/tests.yml
vendored
12
.github/workflows/tests.yml
vendored
@@ -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
|
||||
|
||||
|
||||
@@ -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
4
.gitmodules
vendored
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
39
README.md
39
README.md
@@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
# 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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
BIN
doc/img/capa_web_explorer.png
Normal file
BIN
doc/img/capa_web_explorer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 603 KiB |
@@ -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",
|
||||
|
||||
@@ -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
2
rules
Submodule rules updated: 0e2500fa8a...5b8c8a63a2
Submodule tests/data updated: 245d1dbfed...ad887bbed9
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<Toast position="bottom-center" group="bc" />
|
||||
<header>
|
||||
<div class="wrapper">
|
||||
<BannerHeader />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
filterMode="lenient"
|
||||
sortField="pid"
|
||||
:sortOrder="1"
|
||||
rowHover="true"
|
||||
:rowHover="true"
|
||||
>
|
||||
<Column field="processname" header="Process" expander>
|
||||
<template #body="slotProps">
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import { ref, readonly } from "vue";
|
||||
import { useToast } from "primevue/usetoast";
|
||||
import { isGzipped, decompressGzip, readFileAsText } from "@/utils/fileUtils";
|
||||
|
||||
export function useRdocLoader() {
|
||||
const toast = useToast();
|
||||
const rdocData = ref(null);
|
||||
const isValidVersion = ref(false);
|
||||
|
||||
const MIN_SUPPORTED_VERSION = "7.0.0";
|
||||
|
||||
/**
|
||||
@@ -47,6 +44,14 @@ export function useRdocLoader() {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
data = await response.json();
|
||||
} else if (source instanceof File) {
|
||||
let fileContent;
|
||||
if (await isGzipped(source)) {
|
||||
fileContent = await decompressGzip(source);
|
||||
} else {
|
||||
fileContent = await readFileAsText(source);
|
||||
}
|
||||
data = JSON.parse(fileContent);
|
||||
} else if (typeof source === "object") {
|
||||
// Direct JSON object (Preview options)
|
||||
data = source;
|
||||
@@ -55,8 +60,6 @@ export function useRdocLoader() {
|
||||
}
|
||||
|
||||
if (checkVersion(data)) {
|
||||
rdocData.value = data;
|
||||
isValidVersion.value = true;
|
||||
toast.add({
|
||||
severity: "success",
|
||||
summary: "Success",
|
||||
@@ -64,9 +67,7 @@ export function useRdocLoader() {
|
||||
life: 3000,
|
||||
group: "bc" // bottom-center
|
||||
});
|
||||
} else {
|
||||
rdocData.value = null;
|
||||
isValidVersion.value = false;
|
||||
return data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading JSON:", error);
|
||||
@@ -78,11 +79,10 @@ export function useRdocLoader() {
|
||||
group: "bc" // bottom-center
|
||||
});
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return {
|
||||
rdocData: readonly(rdocData),
|
||||
isValidVersion: readonly(isValidVersion),
|
||||
loadRdoc
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import ImportView from "../views/ImportView.vue";
|
||||
import NotFoundView from "../views/NotFoundView.vue";
|
||||
import ImportView from "@/views/ImportView.vue";
|
||||
import NotFoundView from "@/views/NotFoundView.vue";
|
||||
import AnalysisView from "@/views/AnalysisView.vue";
|
||||
|
||||
import { rdocStore } from "@/store/rdocStore";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
@@ -10,6 +13,20 @@ const router = createRouter({
|
||||
name: "home",
|
||||
component: ImportView
|
||||
},
|
||||
{
|
||||
path: "/analysis",
|
||||
name: "analysis",
|
||||
component: AnalysisView,
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (rdocStore.data.value === null) {
|
||||
// No rdoc loaded, redirect to home page
|
||||
next({ name: "home" });
|
||||
} else {
|
||||
// rdoc is loaded, proceed to analysis page
|
||||
next();
|
||||
}
|
||||
}
|
||||
},
|
||||
// 404 Route - This should be the last route
|
||||
{
|
||||
path: "/:pathMatch(.*)*",
|
||||
|
||||
11
web/explorer/src/store/rdocStore.js
Normal file
11
web/explorer/src/store/rdocStore.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { ref } from "vue";
|
||||
|
||||
export const rdocStore = {
|
||||
data: ref(null),
|
||||
setData(newData) {
|
||||
this.data.value = newData;
|
||||
},
|
||||
clearData() {
|
||||
this.data.value = null;
|
||||
}
|
||||
};
|
||||
76
web/explorer/src/views/AnalysisView.vue
Normal file
76
web/explorer/src/views/AnalysisView.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
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
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
49
web/public/index.html
Normal 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>
|
||||
Reference in New Issue
Block a user