Compare commits

...

44 Commits

Author SHA1 Message Date
Yacine
f6b7582606 bump to v7.2.0 (#2297)
* update CHANGELOG.md and version.py

---------

Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2024-08-20 20:12:46 +02:00
Yacine
791f5e2359 Add the ability to select which functions or processes you which to extract capabilities from (#2156) 2024-08-20 14:09:46 +02:00
Moritz
c409b2b7ed Merge pull request #2300 from s-ff/add-file-scope-rules 2024-08-17 09:09:08 +02:00
Soufiane Fariss
4501955728 remove octal repr for hex values 2024-08-16 23:37:30 +02:00
Capa Bot
6b4591de14 Sync capa rules submodule 2024-08-16 18:57:36 +00:00
Soufiane Fariss
00cce585d6 remove sorting from columns 2024-08-16 18:52:53 +02:00
Soufiane Fariss
19e2097f79 change placeholder text 2024-08-16 18:52:02 +02:00
Soufiane Fariss
b67bd4d084 add file-level rules to capabilities by function 2024-08-16 18:23:44 +02:00
Soufiane Fariss
854759cb43 add tooltip to show decimal/octal rep 2024-08-16 18:17:34 +02:00
Moritz
348e0b3203 Merge pull request #2299 from s-ff/issue/2236
web: add copy rule name and description to VT to right click menu
2024-08-16 17:21:31 +02:00
Soufiane Fariss
03e2195582 add copy rule name and description to VT 2024-08-16 16:49:51 +02:00
Capa Bot
076bb13e2d Sync capa rules submodule 2024-08-16 14:05:19 +00:00
Moritz
76bd1460ba Merge pull request #2298 from s-ff/fixes-2288-2289-2290
web: fix global search and add UI tweaks
2024-08-16 15:02:59 +02:00
Capa Bot
14a7bab890 Sync capa rules submodule 2024-08-16 12:18:34 +00:00
Soufiane Fariss
8ca88d94d5 disable show lib rules button if none 2024-08-16 14:14:29 +02:00
Capa Bot
9d3f732b33 Sync capa rules submodule 2024-08-16 11:25:22 +00:00
Soufiane Fariss
d3e3c966d6 web: introduce column filters and UI tweaks 2024-08-16 12:57:44 +02:00
Capa Bot
e402aab41d Sync capa-testfiles submodule 2024-08-15 20:03:31 +00:00
Soufiane Fariss
c73abb8855 add 'distinct' keyword to clarify count is distinct 2024-08-15 17:05:47 +02:00
Soufiane Fariss
04071606cd fix global search in shhow capabilities by function 2024-08-15 17:03:02 +02:00
Moritz
19698b1ba1 Merge pull request #2296 from s-ff/rearrange-navbar-icons
rearrange navbar icons
2024-08-15 16:58:31 +02:00
Soufiane Fariss
25e9e18097 rearrange navbar icons
moves FLARE logo to the right left side, and make a link to /
2024-08-15 16:48:54 +02:00
Moritz
3a21648e78 Merge pull request #2294 from s-ff/render-results-in-analysis
web: diplay results in new /analysis route
2024-08-15 16:28:20 +02:00
Soufiane Fariss
8dcb7a473e web: diplay results in new /analysis route 2024-08-15 16:10:41 +02:00
Capa Bot
cf91503dc3 Sync capa rules submodule 2024-08-15 12:33:40 +00:00
Moritz
d8691edd15 Merge pull request #2282 from mandiant/dependabot/pip/types-psutil-6.0.0.20240621
build(deps): bump types-psutil from 5.8.23 to 6.0.0.20240621
2024-08-15 14:30:57 +02:00
Moritz
56a6f9c83e Merge pull request #2281 from mandiant/dependabot/pip/pip-24.2
build(deps): bump pip from 24.1.2 to 24.2
2024-08-15 11:40:59 +02:00
Moritz
e25e68e169 Merge pull request #2280 from mandiant/dependabot/pip/black-24.8.0
build(deps): bump black from 24.4.2 to 24.8.0
2024-08-15 11:40:41 +02:00
dependabot[bot]
728742a1ad build(deps): bump types-psutil from 5.8.23 to 6.0.0.20240621
Bumps [types-psutil](https://github.com/python/typeshed) from 5.8.23 to 6.0.0.20240621.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-psutil
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-15 09:31:06 +00:00
Moritz
da273824d1 Merge pull request #2279 from mandiant/dependabot/pip/pyinstaller-6.10.0
build(deps): bump pyinstaller from 6.9.0 to 6.10.0
2024-08-15 11:30:05 +02:00
Moritz
7a6f63cf2b Merge pull request #2278 from mandiant/dependabot/pip/types-requests-2.32.0.20240712
build(deps): bump types-requests from 2.32.0.20240602 to 2.32.0.20240712
2024-08-15 11:29:52 +02:00
Capa Bot
d62734ecc2 Sync capa-testfiles submodule 2024-08-14 12:20:36 +00:00
Capa Bot
5ccb642929 Sync capa rules submodule 2024-08-14 08:48:33 +00:00
Moritz
8d5fcdf287 Merge pull request #2201 from Ana06/ida_apis
ida extractor: extract APIs from renamed globals
2024-08-13 17:59:11 +02:00
Ana Maria Martinez Gomez
be8499238c ida extractor: extract APIs from renamed globals
Add support to extract dynamically resolved APIs stored in global
variables that have been renamed (for example using the `renimp.idc`
script included with IDA Pro).
2024-08-13 17:15:14 +02:00
Capa Bot
40c7714c48 Sync capa-testfiles submodule 2024-08-13 14:59:22 +00:00
Capa Bot
460590cec0 Sync capa-testfiles submodule 2024-08-13 14:59:00 +00:00
Capa Bot
25d2ef30e7 Sync capa-testfiles submodule 2024-08-13 14:58:53 +00:00
Moritz
71ae51ef69 Merge pull request #2284 from s-ff/move-release-to-public
use relative path for zip release asset
2024-08-12 17:45:51 +02:00
Soufiane Fariss
216bfb968d fix typo, and move release asset to public dir
This commit -
- fixes a a typo in package.json (outDir)
- sets the href of the zip file to ./
- moves the zip asset to the public dir.

Note: public dir is a special dir which hosts files that would be served
as is, so it makes sense to put the release for download there.
2024-08-12 17:26:50 +02:00
dependabot[bot]
32cb0365f8 build(deps): bump pip from 24.1.2 to 24.2
Bumps [pip](https://github.com/pypa/pip) from 24.1.2 to 24.2.
- [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/pip/compare/24.1.2...24.2)

---
updated-dependencies:
- dependency-name: pip
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 14:33:31 +00:00
dependabot[bot]
b299e4bc1f build(deps): bump black from 24.4.2 to 24.8.0
Bumps [black](https://github.com/psf/black) from 24.4.2 to 24.8.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/24.4.2...24.8.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 14:33:26 +00:00
dependabot[bot]
bc2802fd72 build(deps): bump pyinstaller from 6.9.0 to 6.10.0
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 6.9.0 to 6.10.0.
- [Release notes](https://github.com/pyinstaller/pyinstaller/releases)
- [Changelog](https://github.com/pyinstaller/pyinstaller/blob/develop/doc/CHANGES.rst)
- [Commits](https://github.com/pyinstaller/pyinstaller/compare/v6.9.0...v6.10.0)

---
updated-dependencies:
- dependency-name: pyinstaller
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 14:33:21 +00:00
dependabot[bot]
81a14838bd build(deps): bump types-requests from 2.32.0.20240602 to 2.32.0.20240712
Bumps [types-requests](https://github.com/python/typeshed) from 2.32.0.20240602 to 2.32.0.20240712.
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-12 14:33:17 +00:00
29 changed files with 539 additions and 221 deletions

View File

@@ -53,7 +53,7 @@ jobs:
run: npm run build:bundle
working-directory: ./web/explorer
- name: Zip release bundle
run: zip -r capa-explorer-web.zip capa-explorer-web
run: zip -r public/capa-explorer-web.zip capa-explorer-web
working-directory: ./web/explorer
- name: Build
run: npm run build

View File

@@ -4,17 +4,50 @@
### New Features
### Breaking Changes
### New Rules (0)
-
### Bug Fixes
### capa explorer IDA Pro plugin
### Development
### Raw diffs
- [capa v7.2.0...master](https://github.com/mandiant/capa/compare/v7.2.0...master)
- [capa-rules v7.2.0...master](https://github.com/mandiant/capa-rules/compare/v7.2.0...master)
### v7.2.0
capa v7.2.0 introduces a first version of capa explorer web: a web-based user interface to inspect capa results using your browser. Users can inspect capa result JSON documents in an online web instance or a standalone HTML page for offline usage. capa explorer supports interactive exploring of capa results to make it easier to understand them. Users can filter, sort, and see the details of all identified capabilities. capa explorer web was worked on by @s-ff as part of a [GSoC project](https://summerofcode.withgoogle.com/programs/2024/projects/cR3hjbsq), and it is available at https://mandiant.github.io/capa/explorer/#/.
This release also adds a feature extractor for output from the DRAKVUF sandbox. Now, analysts can pass the resulting `drakmon.log` file to capa and extract capabilities from the artifacts captured by the sandbox. This feature extractor will also be added to the DRAKVUF sandbox as a post-processing script, and it was worked on by @yelhamer as part of a [GSoC project](https://summerofcode.withgoogle.com/programs/2024/projects/fCnBGuEC).
Additionally, we fixed several bugs handling ELF files, and added the ability to filter capa analysis by functions or processes. We also added support to the IDA Pro extractor to leverage analyst recovered API names.
Special thanks to our repeat and new contributors:
* @lakshayletsgo for their first contribution in https://github.com/mandiant/capa/pull/2248
* @msm-cert for their first contribution in https://github.com/mandiant/capa/pull/2143
* @VascoSch92 for their first contribution in https://github.com/mandiant/capa/pull/2143
### New Features
- webui: explore capa analysis results in a web-based UI online and offline #2224 @s-ff
- support analyzing DRAKVUF traces #2143 @yelhamer
- IDA extractor: extract names from dynamically resolved APIs stored in renamed global variables #2201 @Ana06
- cli: add the ability to select which specific functions or processes to analyze @yelhamer
### 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
@@ -30,8 +63,8 @@
- 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)
- [capa-rules v7.1.0...master](https://github.com/mandiant/capa-rules/compare/v7.1.0...master)
- [capa v7.1.0...7.2.0](https://github.com/mandiant/capa/compare/v7.1.0...7.2.0)
- [capa-rules v7.1.0...7.2.0](https://github.com/mandiant/capa-rules/compare/v7.1.0...7.2.0)
## v7.1.0
The v7.1.0 release brings large performance improvements to capa's rule matching engine.

View File

@@ -269,6 +269,7 @@ Please learn to write rules and contribute new entries as you find interesting t
# 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)

View File

@@ -23,3 +23,15 @@ class UnsupportedOSError(ValueError):
class EmptyReportError(ValueError):
pass
class InvalidArgument(ValueError):
pass
class NonExistantFunctionError(ValueError):
pass
class NonExistantProcessError(ValueError):
pass

View File

@@ -9,7 +9,9 @@
import abc
import hashlib
import dataclasses
from typing import Any, Dict, Tuple, Union, Iterator
from copy import copy
from types import MethodType
from typing import Any, Set, Dict, Tuple, Union, Iterator
from dataclasses import dataclass
# TODO(williballenthin): use typing.TypeAlias directly when Python 3.9 is deprecated
@@ -296,6 +298,22 @@ class StaticFeatureExtractor:
raise NotImplementedError()
def FunctionFilter(extractor: StaticFeatureExtractor, functions: Set) -> StaticFeatureExtractor:
original_get_functions = extractor.get_functions
def filtered_get_functions(self):
yield from (f for f in original_get_functions() if f.address in functions)
# we make a copy of the original extractor object and then update its get_functions() method with the decorated filter one.
# this is in order to preserve the original extractor object's get_functions() method, in case it is used elsewhere in the code.
# an example where this is important is in our testfiles where we may use the same extractor object with different tests,
# with some of these tests needing to install a functions filter on the extractor object.
new_extractor = copy(extractor)
new_extractor.get_functions = MethodType(filtered_get_functions, extractor) # type: ignore
return new_extractor
@dataclass
class ProcessHandle:
"""
@@ -467,4 +485,20 @@ class DynamicFeatureExtractor:
raise NotImplementedError()
def ProcessFilter(extractor: DynamicFeatureExtractor, processes: Set) -> DynamicFeatureExtractor:
original_get_processes = extractor.get_processes
def filtered_get_processes(self):
yield from (f for f in original_get_processes() if f.address.pid in processes)
# we make a copy of the original extractor object and then update its get_processes() method with the decorated filter one.
# this is in order to preserve the original extractor object's get_processes() method, in case it is used elsewhere in the code.
# an example where this is important is in our testfiles where we may use the same extractor object with different tests,
# with some of these tests needing to install a processes filter on the extractor object.
new_extractor = copy(extractor)
new_extractor.get_processes = MethodType(filtered_get_processes, extractor) # type: ignore
return new_extractor
FeatureExtractor: TypeAlias = Union[StaticFeatureExtractor, DynamicFeatureExtractor]

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

View File

@@ -17,7 +17,7 @@ import argparse
import textwrap
import contextlib
from types import TracebackType
from typing import Any, Dict, List, Optional
from typing import Any, Set, Dict, List, Optional, TypedDict
from pathlib import Path
import colorama
@@ -62,6 +62,7 @@ from capa.helpers import (
log_unsupported_drakvuf_report_error,
)
from capa.exceptions import (
InvalidArgument,
EmptyReportError,
UnsupportedOSError,
UnsupportedArchError,
@@ -83,9 +84,17 @@ from capa.features.common import (
FORMAT_FREEZE,
FORMAT_RESULT,
FORMAT_DRAKVUF,
STATIC_FORMATS,
DYNAMIC_FORMATS,
)
from capa.capabilities.common import find_capabilities, has_file_limitation, find_file_capabilities
from capa.features.extractors.base_extractor import FeatureExtractor, StaticFeatureExtractor, DynamicFeatureExtractor
from capa.features.extractors.base_extractor import (
ProcessFilter,
FunctionFilter,
FeatureExtractor,
StaticFeatureExtractor,
DynamicFeatureExtractor,
)
RULES_PATH_DEFAULT_STRING = "(embedded rules)"
SIGNATURES_PATH_DEFAULT_STRING = "(embedded signatures)"
@@ -106,10 +115,17 @@ E_MISSING_CAPE_STATIC_ANALYSIS = 21
E_MISSING_CAPE_DYNAMIC_ANALYSIS = 22
E_EMPTY_REPORT = 23
E_UNSUPPORTED_GHIDRA_EXECUTION_MODE = 24
E_INVALID_INPUT_FORMAT = 25
E_INVALID_FEATURE_EXTRACTOR = 26
logger = logging.getLogger("capa")
class FilterConfig(TypedDict, total=False):
processes: Set[int]
functions: Set[int]
@contextlib.contextmanager
def timing(msg: str):
t0 = time.time()
@@ -276,6 +292,22 @@ def install_common_args(parser, wanted=None):
help=f"select backend, {backend_help}",
)
if "restrict-to-functions" in wanted:
parser.add_argument(
"--restrict-to-functions",
type=lambda s: s.replace(" ", "").split(","),
default=[],
help="provide a list of comma-separated function virtual addresses to analyze (static analysis).",
)
if "restrict-to-processes" in wanted:
parser.add_argument(
"--restrict-to-processes",
type=lambda s: s.replace(" ", "").split(","),
default=[],
help="provide a list of comma-separated process IDs to analyze (dynamic analysis).",
)
if "os" in wanted:
oses = [
(OS_AUTO, "detect OS automatically - default"),
@@ -749,9 +781,10 @@ def get_extractor_from_cli(args, input_format: str, backend: str) -> FeatureExtr
os_ = get_os_from_cli(args, backend)
sample_path = get_sample_path_from_cli(args, backend)
extractor_filters = get_extractor_filters_from_cli(args, input_format)
try:
return capa.loader.get_extractor(
extractor = capa.loader.get_extractor(
args.input_file,
input_format,
os_,
@@ -761,6 +794,7 @@ def get_extractor_from_cli(args, input_format: str, backend: str) -> FeatureExtr
disable_progress=args.quiet or args.debug,
sample_path=sample_path,
)
return apply_extractor_filters(extractor, extractor_filters)
except UnsupportedFormatError as e:
if input_format == FORMAT_CAPE:
log_unsupported_cape_report_error(str(e))
@@ -780,6 +814,38 @@ def get_extractor_from_cli(args, input_format: str, backend: str) -> FeatureExtr
raise ShouldExitError(E_CORRUPT_FILE) from e
def get_extractor_filters_from_cli(args, input_format) -> FilterConfig:
if not hasattr(args, "restrict_to_processes") and not hasattr(args, "restrict_to_functions"):
# no processes or function filters were installed in the args
return {}
if input_format in STATIC_FORMATS:
if args.restrict_to_processes:
raise InvalidArgument("Cannot filter processes with static analysis.")
return {"functions": {int(addr, 0) for addr in args.restrict_to_functions}}
elif input_format in DYNAMIC_FORMATS:
if args.restrict_to_functions:
raise InvalidArgument("Cannot filter functions with dynamic analysis.")
return {"processes": {int(pid, 0) for pid in args.restrict_to_processes}}
else:
raise ShouldExitError(E_INVALID_INPUT_FORMAT)
def apply_extractor_filters(extractor: FeatureExtractor, extractor_filters: FilterConfig):
if not any(extractor_filters.values()):
return extractor
# if the user specified extractor filters, then apply them here
if isinstance(extractor, StaticFeatureExtractor):
assert extractor_filters["functions"]
return FunctionFilter(extractor, extractor_filters["functions"])
elif isinstance(extractor, DynamicFeatureExtractor):
assert extractor_filters["processes"]
return ProcessFilter(extractor, extractor_filters["processes"])
else:
raise ShouldExitError(E_INVALID_FEATURE_EXTRACTOR)
def main(argv: Optional[List[str]] = None):
if sys.version_info < (3, 8):
raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.8+")
@@ -819,7 +885,20 @@ def main(argv: Optional[List[str]] = None):
parser = argparse.ArgumentParser(
description=desc, epilog=epilog, formatter_class=argparse.RawDescriptionHelpFormatter
)
install_common_args(parser, {"input_file", "format", "backend", "os", "signatures", "rules", "tag"})
install_common_args(
parser,
{
"input_file",
"format",
"backend",
"os",
"signatures",
"rules",
"tag",
"restrict-to-functions",
"restrict-to-processes",
},
)
parser.add_argument("-j", "--json", action="store_true", help="emit JSON instead of text")
args = parser.parse_args(args=argv)

View File

@@ -5,7 +5,7 @@
# 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.
__version__ = "7.1.0"
__version__ = "7.2.0"
def get_major_version():

View File

@@ -9,6 +9,22 @@ Use the `-t` option to run rules with the given metadata value (see the rule fie
For example, `capa -t william.ballenthin@mandiant.com` runs rules that reference Willi's email address (probably as the author), or
`capa -t communication` runs rules with the namespace `communication`.
### only analyze selected functions
Use the `--restrict-to-functions` option to extract capabilities from only a selected set of functions. This is useful for analyzing
large functions and figuring out their capabilities and their address of occurance; for example: PEB access, RC4 encryption, etc.
To use this, you can copy the virtual addresses from your favorite disassembler and pass them to capa as follows:
`capa sample.exe --restrict-to-functions 0x4019C0,0x401CD0`. If you add the `-v` option then capa will extract the interesting parts of a function for you.
### only analyze selected processes
Use the `--restrict-to-processes` option to extract capabilities from only a selected set of processes. This is useful for filtering the noise
generated from analyzing non-malicious processes that can be reported by some sandboxes, as well as reduce the execution time
by not analyzing such processes in the first place.
To use this, you can pick the PIDs of the processes you are interested in from the sandbox-generated process tree (or from the sandbox-reported malware PID)
and pass that to capa as follows: `capa report.log --restrict-to-processes 3888,3214,4299`. If you add the `-v` option then capa will tell you
which threads perform what actions (encrypt/decrypt data, initiate a connection, etc.).
### IDA Pro plugin: capa explorer
Please check out the [capa explorer documentation](/capa/ida/plugin/README.md).
@@ -16,4 +32,4 @@ Please check out the [capa explorer documentation](/capa/ida/plugin/README.md).
Set the environment variable `CAPA_SAVE_WORKSPACE` to instruct the underlying analysis engine to
cache its intermediate results to the file system. For example, vivisect will create `.viv` files.
Subsequently, capa may run faster when reprocessing the same input file.
This is particularly useful during rule development as you repeatedly test a rule against a known sample.
This is particularly useful during rule development as you repeatedly test a rule against a known sample.

View File

@@ -136,7 +136,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",
@@ -147,8 +147,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"
]
@@ -157,7 +157,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"
]
@@ -188,6 +188,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

@@ -9,6 +9,7 @@
import textwrap
import capa.capabilities.common
from capa.features.extractors.base_extractor import FunctionFilter
def test_match_across_scopes_file_function(z9324d_extractor):
@@ -174,6 +175,37 @@ def test_subscope_bb_rules(z9324d_extractor):
assert "test rule" in capabilities
def test_match_specific_functions(z9324d_extractor):
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: receive data
scopes:
static: function
dynamic: call
examples:
- 9324d1a8ae37a36ae560c37448c9705a:0x401CD0
features:
- or:
- api: recv
"""
)
)
]
)
extractor = FunctionFilter(z9324d_extractor, {0x4019C0})
capabilities, meta = capa.capabilities.common.find_capabilities(rules, extractor)
matches = capabilities["receive data"]
# test that we received only one match
assert len(matches) == 1
# and that this match is from the specified function
assert matches[0][0] == 0x4019C0
def test_byte_matching(z9324d_extractor):
rules = capa.rules.RuleSet(
[

View File

@@ -6,7 +6,7 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"build:bundle": "vite build --mode bundle --outDir=capa-exlorer-web",
"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 function address" />
</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" header="Rule Matches" class="w-min" :showFilterMenu="false" :showClearButton="false">
<template #filter v-if="props.showColumnFilters">
<InputText v-model="filters['rule'].value" placeholder="Filter by rule" />
</template>
<template #body="{ data }">
{{ data.rule }}
<LibraryTag v-if="data.lib" />
</template>
</Column>
<Column field="namespace" sortable header="Namespace"></Column>
<Column field="namespace" header="Namespace" :showFilterMenu="false" :showClearButton="false">
<template #filter v-if="props.showColumnFilters">
<InputText v-model="filters['namespace'].value" placeholder="Filter by namespace" />
</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,15 +1,21 @@
<script setup>
import Menubar from "primevue/menubar";
import { RouterLink } from "vue-router";
</script>
<template>
<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
v-tooltip.right="'Download capa Explorer Web for offline usage'"
href="/capa-explorer-web.zip"
href="./capa-explorer-web.zip"
download="capa-explorer-web.zip"
aria-label="Download capa Explorer Web release"
>
@@ -18,7 +24,6 @@ import Menubar from "primevue/menubar";
<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

@@ -31,7 +31,15 @@
<template v-else-if="node.data.type === 'feature'">
<span>
- {{ node.data.typeValue }}:
<span :class="{ 'text-green-700': node.data.typeValue !== 'regex' }" class="font-monospace">
<span
:class="{ 'text-green-700': node.data.typeValue !== 'regex' }"
class="font-monospace"
v-tooltip.top="{
value: getTooltipContent(node.data),
showDelay: 1000,
hideDelay: 300
}"
>
{{ node.data.name }}
</span>
</span>
@@ -63,4 +71,12 @@ defineProps({
required: true
}
});
const getTooltipContent = (data) => {
if (data.typeValue === "number" || data.typeValue === "offset") {
const decimalValue = parseInt(data.name, 16);
return `Decimal: ${decimalValue}`;
}
return null;
};
</script>

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

@@ -108,6 +108,9 @@ export function parseFunctionCapabilities(doc) {
// Map to store capabilities matched to each function
const matchesByFunction = new Map();
// Add a special entry for file-level matches
matchesByFunction.set("file", new Set());
// Iterate through all rules in the document
for (const [, rule] of Object.entries(doc.rules)) {
if (rule.meta.scopes.static === "function") {
@@ -133,12 +136,26 @@ export function parseFunctionCapabilities(doc) {
.add({ name: rule.meta.name, namespace: rule.meta.namespace, lib: rule.meta.lib });
}
}
} else if (rule.meta.scopes.static === "file") {
// Add file-level matches to the special 'file' entry
matchesByFunction.get("file").add({
name: rule.meta.name,
namespace: rule.meta.namespace,
lib: rule.meta.lib
});
}
// (else) Ignoring file scope rules
}
const result = [];
// Add file-level matches if there are any
if (matchesByFunction.get("file").size > 0) {
result.push({
address: "file",
capabilities: Array.from(matchesByFunction.get("file"))
});
}
// Iterate through all functions in the document
for (const f of doc.meta.analysis.feature_counts.functions) {
const addr = formatAddress(f.address);

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>