Compare commits

...

130 Commits

Author SHA1 Message Date
Mike Hunhoff
0ba5f9664a remove deprecated APIs 2025-12-10 21:20:56 +00:00
Mike Hunhoff
98873c8570 support Ghidra v12 2025-12-10 19:58:21 +00:00
Mike Hunhoff
3687bb95e9 fix black errors 2025-12-09 00:43:44 +00:00
Mike Hunhoff
7175714f9e Update capa/features/extractors/ghidra/helpers.py
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-12-08 17:37:04 -07:00
Mike Hunhoff
32c201d9b1 update CHANGELOG and PyGhidra version requirements 2025-12-09 00:36:10 +00:00
Mike Hunhoff
784e0346d9 merge upstream 2025-12-09 00:24:41 +00:00
Mike Hunhoff
be1ccb0776 ghidra: init commit switch to PyGhidra 2025-12-09 00:24:03 +00:00
dependabot[bot]
9b42b45d21 build(deps-dev): bump flake8-bugbear from 24.12.12 to 25.10.21 (#2773)
* build(deps-dev): bump flake8-bugbear from 24.12.12 to 25.10.21

Bumps [flake8-bugbear](https://github.com/PyCQA/flake8-bugbear) from 24.12.12 to 25.10.21.
- [Release notes](https://github.com/PyCQA/flake8-bugbear/releases)
- [Commits](https://github.com/PyCQA/flake8-bugbear/compare/24.12.12...25.10.21)

---
updated-dependencies:
- dependency-name: flake8-bugbear
  dependency-version: 25.10.21
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix flake8 raised bugs

* use super

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: mr-tz <moritz.raabe@mandiant.com>
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2025-12-04 10:19:16 -07:00
Capa Bot
d17264c928 Sync capa rules submodule 2025-12-04 17:17:51 +00:00
Capa Bot
f313852e70 Sync capa rules submodule 2025-12-04 12:11:09 +00:00
Capa Bot
c0ae1352c6 Sync capa-testfiles submodule 2025-12-03 21:00:48 +00:00
dependabot[bot]
ccb3e6de74 build(deps-dev): bump flake8-comprehensions from 3.16.0 to 3.17.0 (#2782)
Bumps [flake8-comprehensions](https://github.com/adamchainz/flake8-comprehensions) from 3.16.0 to 3.17.0.
- [Changelog](https://github.com/adamchainz/flake8-comprehensions/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/adamchainz/flake8-comprehensions/compare/3.16.0...3.17.0)

---
updated-dependencies:
- dependency-name: flake8-comprehensions
  dependency-version: 3.17.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 11:27:08 -07:00
dependabot[bot]
26c6ffd62d build(deps-dev): bump ruff from 0.12.0 to 0.14.7 (#2781)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.12.0 to 0.14.7.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.12.0...0.14.7)

---
updated-dependencies:
- dependency-name: ruff
  dependency-version: 0.14.7
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 11:26:34 -07:00
Capa Bot
18923601c7 Sync capa rules submodule 2025-11-25 20:39:18 +00:00
0x1622
1568ce4832 Use SafeLoader for YAML (#2776) 2025-11-25 07:01:23 -07:00
Mike Hunhoff
ffce77b13d ci: deprecate macos-13 runner and use Python v3.13 for testing (#2777) 2025-11-24 19:53:39 -07:00
dependabot[bot]
895b2440c0 build(deps-dev): bump pre-commit from 4.2.0 to 4.5.0 (#2772)
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.2.0 to 4.5.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.2.0...v4.5.0)

---
updated-dependencies:
- dependency-name: pre-commit
  dependency-version: 4.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 08:40:13 -07:00
dependabot[bot]
c901f809a2 build(deps-dev): bump black from 25.1.0 to 25.11.0 (#2771)
Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.11.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/25.1.0...25.11.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 08:39:17 -07:00
dependabot[bot]
308b3e5c1c build(deps): bump xmltodict from 0.14.2 to 1.0.2 (#2774)
Bumps [xmltodict](https://github.com/martinblech/xmltodict) from 0.14.2 to 1.0.2.
- [Release notes](https://github.com/martinblech/xmltodict/releases)
- [Changelog](https://github.com/martinblech/xmltodict/blob/master/CHANGELOG.md)
- [Commits](https://github.com/martinblech/xmltodict/compare/v0.14.2...v1.0.2)

---
updated-dependencies:
- dependency-name: xmltodict
  dependency-version: 1.0.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 08:30:16 -07:00
Mike Hunhoff
7844ebb144 v9.3.1 (#2769) 2025-11-20 08:37:49 -07:00
dependabot[bot]
e393cff0e1 build(deps): bump glob from 10.4.2 to 10.5.0 in /web/explorer (#2766)
Bumps [glob](https://github.com/isaacs/node-glob) from 10.4.2 to 10.5.0.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.4.2...v10.5.0)

---
updated-dependencies:
- dependency-name: glob
  dependency-version: 10.5.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-19 08:34:25 -07:00
Mike Hunhoff
7780b9e8a8 explorer: add missing ida-netnode dependency to project.toml (#2765) 2025-11-18 08:55:57 -07:00
Mike Hunhoff
8d39765e7b ci: bump binja minor version (#2763) 2025-11-17 11:10:46 -07:00
dependabot[bot]
dec0bcfe79 build(deps-dev): bump js-yaml from 4.1.0 to 4.1.1 in /web/explorer (#2758)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 10:02:54 -07:00
dependabot[bot]
99ccecba4e build(deps): bump humanize from 4.13.0 to 4.14.0 (#2762)
Bumps [humanize](https://github.com/python-humanize/humanize) from 4.13.0 to 4.14.0.
- [Release notes](https://github.com/python-humanize/humanize/releases)
- [Commits](https://github.com/python-humanize/humanize/compare/4.13.0...4.14.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 09:10:40 -07:00
dependabot[bot]
af27463c37 build(deps-dev): bump pyinstaller from 6.14.1 to 6.16.0 (#2761)
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 6.14.1 to 6.16.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.14.1...v6.16.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 09:10:30 -07:00
dependabot[bot]
f4f47b4d55 build(deps): bump protobuf from 6.31.1 to 6.33.1 (#2760)
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 6.31.1 to 6.33.1.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 09:10:19 -07:00
dependabot[bot]
adc2401136 build(deps): bump pycparser from 2.22 to 2.23 (#2759)
Bumps [pycparser](https://github.com/eliben/pycparser) from 2.22 to 2.23.
- [Release notes](https://github.com/eliben/pycparser/releases)
- [Changelog](https://github.com/eliben/pycparser/blob/main/CHANGES)
- [Commits](https://github.com/eliben/pycparser/compare/release_v2.22...release_v2.23)

---
updated-dependencies:
- dependency-name: pycparser
  dependency-version: '2.23'
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-17 09:10:09 -07:00
Moritz
0ff7855467 Release v9.3.0 2025-11-12 17:30:07 +01:00
Capa Bot
d5411cadad Sync capa rules submodule 2025-11-12 10:26:46 +00:00
dependabot[bot]
cbd6d2a189 build(deps): bump pydantic from 2.11.4 to 2.12.4 (#2755)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.11.4 to 2.12.4.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/v2.12.4/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.11.4...v2.12.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 09:24:02 -07:00
dependabot[bot]
29af6dc875 build(deps-dev): bump build from 1.2.2 to 1.3.0 (#2754)
Bumps [build](https://github.com/pypa/build) from 1.2.2 to 1.3.0.
- [Release notes](https://github.com/pypa/build/releases)
- [Changelog](https://github.com/pypa/build/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pypa/build/compare/1.2.2...1.3.0)

---
updated-dependencies:
- dependency-name: build
  dependency-version: 1.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-10 09:23:14 -07:00
Moritz
66a3aac815 Merge pull request #2753 from mandiant/fix/capa-explorer-qt-load
delay import to not load Qt* when running under idalib
2025-11-10 16:51:14 +01:00
mr-tz
7525de7bbd delay import to not load Qt* when running under idalib
closes #2752
2025-11-10 12:13:35 +00:00
Capa Bot
cbd1cb2b7d Sync capa rules submodule 2025-11-07 07:40:05 +00:00
Matthew Haigh
503c34b8f9 added mailinglist cta (#2744)
* added mailinglist cta

* Update README.md

Added mailto: link for better user experience

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: Matt Haigh <matthaigh@google.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-11-03 09:11:14 -07:00
dependabot[bot]
888295b37a build(deps): bump types-protobuf from 6.30.2.20250516 to 6.32.1.20250918 (#2733)
Bumps [types-protobuf](https://github.com/typeshed-internal/stub_uploader) from 6.30.2.20250516 to 6.32.1.20250918.
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

---
updated-dependencies:
- dependency-name: types-protobuf
  dependency-version: 6.32.1.20250918
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-03 09:07:26 -07:00
dependabot[bot]
5f9c908315 build(deps): bump pip from 25.2 to 25.3 (#2741)
Bumps [pip](https://github.com/pypa/pip) from 25.2 to 25.3.
- [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/pip/compare/25.2...25.3)

---
updated-dependencies:
- dependency-name: pip
  dependency-version: '25.3'
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2025-11-03 09:07:02 -07:00
Willi Ballenthin
cb2e2323f9 explorer: add support for IDA 9.2 (#2723)
* ida: add Qt compatibility layer for PyQt5 and PySide6

Introduce a new module `qt_compat.py` providing a unified import
interface and API compatibility for Qt modules. It handles differences between
PyQt5 (used in IDA <9.2) and PySide6 (used in IDA >=9.2). Update all
plugin modules to import Qt components via this compatibility layer
instead of directly importing from PyQt5. This enhances plugin
compatibility across different IDA versions.

thanks @mike-hunhoff!

changelog

* qt_compat: use __all__ rather than noqa

---------

Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
2025-11-03 13:29:06 +01:00
Willi Ballenthin
5ea63770ba Merge pull request #2724 from HexRays-plugin-contributions/ida-plugin-json
add `ida-plugin.json`
2025-10-29 17:55:49 +01:00
Capa Bot
6795813fbe Sync capa rules submodule 2025-10-28 15:21:05 +00:00
Capa Bot
ca708ca52e Sync capa-testfiles submodule 2025-10-28 15:15:42 +00:00
Capa Bot
68cf74d60c Sync capa rules submodule 2025-10-28 13:12:29 +00:00
Moritz
5a0c47419f Merge pull request #2735 from mandiant/dependabot/npm_and_yarn/web/explorer/vite-6.4.1
build(deps-dev): bump vite from 6.4.0 to 6.4.1 in /web/explorer
2025-10-24 12:32:50 +02:00
Moritz
4dbdd9dcfa Merge branch 'master' into dependabot/npm_and_yarn/web/explorer/vite-6.4.1 2025-10-24 12:30:15 +02:00
Moritz
82cbfd33db Merge pull request #2732 from xusheng6/test_fix_binja_crash
binja: fix crash in binja feature extraction when MLIL is unavailable…
2025-10-24 12:29:51 +02:00
dependabot[bot]
5906bb3ecf build(deps-dev): bump vite from 6.4.0 to 6.4.1 in /web/explorer
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.4.0 to 6.4.1.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.1/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-21 04:19:00 +00:00
Moritz
08319f598f Merge pull request #2730 from mandiant/dependabot/npm_and_yarn/web/explorer/vite-6.4.0
build(deps-dev): bump vite from 6.3.4 to 6.4.0 in /web/explorer
2025-10-20 17:28:58 +02:00
Capa Bot
e6df6ad0cd Sync capa rules submodule 2025-10-20 15:27:46 +00:00
Capa Bot
add09df061 Sync capa-testfiles submodule 2025-10-20 15:18:32 +00:00
Mike Hunhoff
acb34e88d6 Update CHANGELOG.md
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-10-20 09:05:30 -06:00
Xusheng
0099e75704 binja: fix crash in binja feature extraction when MLIL is unavailable. Fix https://github.com/mandiant/capa/issues/2714 2025-10-20 18:46:53 +08:00
dependabot[bot]
da0803b671 build(deps-dev): bump vite from 6.3.4 to 6.4.0 in /web/explorer
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.3.4 to 6.4.0.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@6.4.0/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-16 10:58:41 +00:00
Moritz
789747282d Merge pull request #2728 from mandiant/dependabot/pip/rich-14.2.0
build(deps): bump rich from 14.0.0 to 14.2.0
2025-10-16 12:57:18 +02:00
Capa Bot
3bc2d9915c Sync capa-testfiles submodule 2025-10-13 18:52:26 +00:00
dependabot[bot]
5974440ab7 build(deps): bump rich from 14.0.0 to 14.2.0
Bumps [rich](https://github.com/Textualize/rich) from 14.0.0 to 14.2.0.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v14.0.0...v14.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-13 14:25:06 +00:00
dependabot[bot]
b9d517a70b build(deps): bump pip from 25.1.1 to 25.2 (#2717)
Bumps [pip](https://github.com/pypa/pip) from 25.1.1 to 25.2.
- [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/pip/compare/25.1.1...25.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-10-06 08:32:13 -06:00
dependabot[bot]
e5b8788620 build(deps): bump humanize from 4.12.0 to 4.13.0 (#2716)
Bumps [humanize](https://github.com/python-humanize/humanize) from 4.12.0 to 4.13.0.
- [Release notes](https://github.com/python-humanize/humanize/releases)
- [Commits](https://github.com/python-humanize/humanize/compare/4.12.0...4.13.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-10-06 08:31:46 -06:00
axelmierczuk
ec411f1552 Update pyproject.toml
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2025-10-01 19:00:26 +02:00
axelmierczuk
6871adc9dc Pin ida-settings version to 2.1.0 2025-10-01 19:00:26 +02:00
Capa Bot
07880c1418 Sync capa rules submodule 2025-09-23 20:18:16 +00:00
Capa Bot
5a6c8ca7c1 Sync capa rules submodule 2025-09-09 19:22:11 +00:00
Capa Bot
3bd8371d0c Sync capa rules submodule 2025-09-03 16:27:26 +00:00
dependabot[bot]
d0c87ef32c build(deps): bump markdown-it-py from 3.0.0 to 4.0.0 (#2711)
Bumps [markdown-it-py](https://github.com/executablebooks/markdown-it-py) from 3.0.0 to 4.0.0.
- [Release notes](https://github.com/executablebooks/markdown-it-py/releases)
- [Changelog](https://github.com/executablebooks/markdown-it-py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/executablebooks/markdown-it-py/compare/v3.0.0...v4.0.0)

---
updated-dependencies:
- dependency-name: markdown-it-py
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-03 10:11:25 -06:00
dependabot[bot]
bd2731f87f build(deps): bump pytest-sugar from 1.0.0 to 1.1.1 (#2710)
Bumps [pytest-sugar](https://github.com/Teemu/pytest-sugar) from 1.0.0 to 1.1.1.
- [Release notes](https://github.com/Teemu/pytest-sugar/releases)
- [Changelog](https://github.com/Teemu/pytest-sugar/blob/main/CHANGES.rst)
- [Commits](https://github.com/Teemu/pytest-sugar/compare/v1.0.0...v1.1.1)

---
updated-dependencies:
- dependency-name: pytest-sugar
  dependency-version: 1.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-03 10:10:32 -06:00
Capa Bot
4a167d7188 Sync capa rules submodule 2025-09-03 16:08:58 +00:00
Capa Bot
c01bc346fc Sync capa rules submodule 2025-09-03 16:05:36 +00:00
Capa Bot
826330f511 Sync capa-testfiles submodule 2025-09-03 15:58:45 +00:00
Capa Bot
40e5095577 Sync capa-testfiles submodule 2025-09-03 15:55:29 +00:00
Capa Bot
c7eede3c53 Sync capa-testfiles submodule 2025-09-03 15:51:51 +00:00
Capa Bot
1a5f50195a Sync capa rules submodule 2025-08-25 19:08:17 +00:00
Capa Bot
aafca2e00a Sync capa-testfiles submodule 2025-08-25 18:59:27 +00:00
Capa Bot
3a24fabeb6 Sync capa rules submodule 2025-08-22 14:58:24 +00:00
Capa Bot
2f81bb79f9 Sync capa rules submodule 2025-08-21 14:57:07 +00:00
Capa Bot
fc83b7b0a1 Sync capa rules submodule 2025-08-21 14:56:48 +00:00
dependabot[bot]
d430aea04e build(deps): bump dnfile from 0.15.0 to 0.16.4 (#2700)
---
updated-dependencies:
- dependency-name: dnfile
  dependency-version: 0.16.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-08-20 15:11:17 -06:00
dependabot[bot]
1eb42599cf build(deps): bump mypy from 1.16.0 to 1.17.1 (#2704)
Bumps [mypy](https://github.com/python/mypy) from 1.16.0 to 1.17.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.16.0...v1.17.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-08-20 15:10:52 -06:00
dependabot[bot]
618ae2111b build(deps): bump form-data from 4.0.0 to 4.0.4 in /web/explorer (#2702)
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.0 to 4.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.0...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-08-20 12:11:46 -06:00
Mike Hunhoff
42b6d8106a binja: update core version info check (#2709) 2025-08-20 11:56:56 -06:00
Capa Bot
78a020e1ac Sync capa rules submodule 2025-08-20 16:04:49 +00:00
Capa Bot
a80f85aab4 Sync capa-testfiles submodule 2025-08-20 15:57:15 +00:00
Capa Bot
f94f554d15 Sync capa-testfiles submodule 2025-08-20 15:32:08 +00:00
Capa Bot
d456d52e81 Sync capa rules submodule 2025-08-14 20:59:31 +00:00
Capa Bot
2a18b08a80 Sync capa rules submodule 2025-08-14 15:11:56 +00:00
Capa Bot
dd2e350a1a Sync capa-testfiles submodule 2025-08-14 15:08:18 +00:00
Capa Bot
164a7bdfb5 Sync capa rules submodule 2025-08-13 14:40:23 +00:00
Capa Bot
d7c896bbc6 Sync capa rules submodule 2025-08-12 16:21:29 +00:00
Capa Bot
8185ac4dde Sync capa rules submodule 2025-08-12 15:43:50 +00:00
Capa Bot
92a6ddff99 Sync capa rules submodule 2025-08-12 15:42:57 +00:00
Capa Bot
af87fae036 Sync capa-testfiles submodule 2025-08-12 15:38:12 +00:00
Capa Bot
c774db26f0 Sync capa-testfiles submodule 2025-08-12 15:37:46 +00:00
Capa Bot
eb0afc806e Sync capa rules submodule 2025-08-04 18:41:07 +00:00
dependabot[bot]
9a09f667bf build(deps): bump flake8 from 7.2.0 to 7.3.0 (#2697)
Bumps [flake8](https://github.com/pycqa/flake8) from 7.2.0 to 7.3.0.
- [Commits](https://github.com/pycqa/flake8/compare/7.2.0...7.3.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 09:30:33 -06:00
dependabot[bot]
15a1dc3409 build(deps): bump ruff from 0.11.0 to 0.12.0 (#2698)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.0 to 0.12.0.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/0.11.0...0.12.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-24 09:30:20 -06:00
Capa Bot
a18fe34d01 Sync capa rules submodule 2025-06-20 16:16:27 +00:00
Capa Bot
edcea18c52 Sync capa-testfiles submodule 2025-06-17 19:17:09 +00:00
dependabot[bot]
92f0306f96 build(deps): bump mypy from 1.15.0 to 1.16.0 (#2696)
Bumps [mypy](https://github.com/python/mypy) from 1.15.0 to 1.16.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.15.0...v1.16.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 12:00:21 -06:00
dependabot[bot]
f2ed75c339 build(deps): bump protobuf from 6.30.1 to 6.31.1 (#2686)
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 6.30.1 to 6.31.1.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/protobuf_release.bzl)
- [Commits](https://github.com/protocolbuffers/protobuf/compare/v6.30.1...v6.31.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-06-11 16:28:23 -06:00
dependabot[bot]
6e18657ca7 build(deps): bump types-protobuf from 5.29.1.20241207 to 6.30.2.20250516 (#2687)
Bumps [types-protobuf](https://github.com/typeshed-internal/stub_uploader) from 5.29.1.20241207 to 6.30.2.20250516.
- [Commits](https://github.com/typeshed-internal/stub_uploader/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-06-11 16:28:11 -06:00
dependabot[bot]
8ba48d11d0 build(deps): bump pyinstaller from 6.12.0 to 6.14.1 (#2688)
Bumps [pyinstaller](https://github.com/pyinstaller/pyinstaller) from 6.12.0 to 6.14.1.
- [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.12.0...v6.14.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-06-11 16:27:52 -06:00
Mike Hunhoff
d6f442b5bd ci: remove redundant "test_run" action (#2692)
* ci: remove redundant "test_run" action

* update CHANGELOG
2025-06-10 16:43:10 -06:00
Spencer Heywood
0da5d7c5b5 add support for arm64 binary releases (#2691) 2025-06-10 15:52:55 -06:00
Mike Hunhoff
fa5d9a9302 v9.2.1 (#2685) 2025-06-06 14:16:25 -06:00
Mike Hunhoff
30fb4751f6 fix build issues discovered during and after v9.2.0 release (#2684)
* ci: downgrade Ubuntu version to accommodate older GLIBC versions

* ci: upgrade Windows version to avoid deprecation

* ci: exclude pkg_resources from PyInstaller build

* update CHANGELOG

* update spec file

* ci: check if build runs without warnings or errors

* update CHANGELOG

* update build commands

* update build commands

* update build commands

* update build commands

* update build commands
2025-06-06 12:02:06 -06:00
Mike Hunhoff
a8eab7ddf0 update "publish package" worflow to 1.12.4 (#2680) 2025-06-05 13:44:30 -06:00
Mike Hunhoff
5ad1dda918 v9.2.0 (#2677) 2025-06-05 09:03:56 -06:00
Capa Bot
eabb2cc809 Sync capa rules submodule 2025-06-02 19:16:57 +00:00
Capa Bot
a34c3ecc57 Sync capa rules submodule 2025-06-02 18:47:17 +00:00
dependabot[bot]
d22de5cf7f build(deps): bump rich from 13.9.2 to 14.0.0 (#2675)
Bumps [rich](https://github.com/Textualize/rich) from 13.9.2 to 14.0.0.
- [Release notes](https://github.com/Textualize/rich/releases)
- [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Textualize/rich/compare/v13.9.2...v14.0.0)

---
updated-dependencies:
- dependency-name: rich
  dependency-version: 14.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-02 12:39:31 -06:00
dependabot[bot]
8f78834cae build(deps): bump setuptools from 78.1.1 to 80.9.0 (#2674)
Bumps [setuptools](https://github.com/pypa/setuptools) from 78.1.1 to 80.9.0.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v78.1.1...v80.9.0)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 80.9.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-02 12:39:08 -06:00
dependabot[bot]
08dbb0e02d build(deps): bump flake8-simplify from 0.21.0 to 0.22.0 (#2673)
Bumps [flake8-simplify](https://github.com/MartinThoma/flake8-simplify) from 0.21.0 to 0.22.0.
- [Release notes](https://github.com/MartinThoma/flake8-simplify/releases)
- [Changelog](https://github.com/MartinThoma/flake8-simplify/blob/main/CHANGELOG.md)
- [Commits](https://github.com/MartinThoma/flake8-simplify/commits)

---
updated-dependencies:
- dependency-name: flake8-simplify
  dependency-version: 0.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-02 12:38:41 -06:00
Mike Hunhoff
98725c52dc vmray: do not restrict analysis to PE and ELF files, e.g. docx (#2672)
* vmray: do not restrict analysis to PE and ELF files, e.g. docx

* update CHANGELOG
2025-05-30 13:53:12 -06:00
dependabot[bot]
eb87153064 build(deps): bump setuptools from 76.0.0 to 78.1.1 (#2667)
Bumps [setuptools](https://github.com/pypa/setuptools) from 76.0.0 to 78.1.1.
- [Release notes](https://github.com/pypa/setuptools/releases)
- [Changelog](https://github.com/pypa/setuptools/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/setuptools/compare/v76.0.0...v78.1.1)

---
updated-dependencies:
- dependency-name: setuptools
  dependency-version: 78.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-05-30 12:35:35 -06:00
dependabot[bot]
56aa7176b0 build(deps): bump pip from 25.0 to 25.1.1 (#2666)
Bumps [pip](https://github.com/pypa/pip) from 25.0 to 25.1.1.
- [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst)
- [Commits](https://github.com/pypa/pip/compare/25.0...25.1.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Moritz <mr-tz@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-05-30 12:35:15 -06:00
dependabot[bot]
8b41671409 build(deps): bump flake8 from 7.1.1 to 7.2.0 (#2648)
* build(deps): bump flake8 from 7.1.1 to 7.2.0

Bumps [flake8](https://github.com/pycqa/flake8) from 7.1.1 to 7.2.0.
- [Commits](https://github.com/pycqa/flake8/compare/7.1.1...7.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>

* fix lints (#2671)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-05-30 12:09:16 -06:00
dependabot[bot]
5dbbc2b468 build(deps): bump pre-commit from 4.1.0 to 4.2.0 (#2646)
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/pre-commit/pre-commit/releases)
- [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md)
- [Commits](https://github.com/pre-commit/pre-commit/compare/v4.1.0...v4.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-05-30 11:28:19 -06:00
Mike Hunhoff
96d1eb64c3 update binja core version (#2670)
* update binja core version

* update CHANGELOG
2025-05-30 10:52:56 -06:00
Capa Bot
9234b33051 Sync capa rules submodule 2025-05-28 20:44:44 +00:00
Capa Bot
51f5114ad7 Sync capa rules submodule 2025-05-22 18:53:54 +00:00
Capa Bot
4b72f8a872 Sync capa-testfiles submodule 2025-05-22 17:48:58 +00:00
Moritz
8206a97b0f Merge pull request #2659 from mandiant/dependabot/npm_and_yarn/web/explorer/vite-6.3.4
build(deps-dev): bump vite from 6.2.3 to 6.3.4 in /web/explorer
2025-05-20 16:47:50 +02:00
Capa Bot
5a33b4b2a8 Sync capa rules submodule 2025-05-19 18:21:38 +00:00
Capa Bot
fcfdeec377 Sync capa rules submodule 2025-05-19 18:21:13 +00:00
Capa Bot
37a63a751c Sync capa-testfiles submodule 2025-05-19 18:12:00 +00:00
zdw@
3a9f2136bb lint: log the failed example+rule (#2661)
* lint: log the failed example+rule

* Update scripts/lint.py

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>

* fix lint

---------

Co-authored-by: Mike Hunhoff <mike.hunhoff@gmail.com>
2025-05-12 15:11:22 -06:00
Capa Bot
390e2a6315 Sync capa-testfiles submodule 2025-05-12 16:17:27 +00:00
Capa Bot
6a43084915 Sync capa-testfiles submodule 2025-05-12 16:06:51 +00:00
dependabot[bot]
6d7ca57fa9 build(deps): bump pydantic from 2.10.1 to 2.11.4 (#2660)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.10.1 to 2.11.4.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.10.1...v2.11.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-12 09:48:46 -06:00
Ana María Martínez Gómez
d1090e8391 ci: Update Ubuntu version in actions (#2656)
ubuntu-20.04 has been deprecated causing several GH actions to fail:
https://github.com/actions/runner-images/issues/11101
2025-05-09 15:40:59 -06:00
dependabot[bot]
b07efe773b build(deps-dev): bump vite from 6.2.3 to 6.3.4 in /web/explorer
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.2.3 to 6.3.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.3.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.3.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-30 18:53:28 +00:00
48 changed files with 1072 additions and 1039 deletions

27
.bumpversion.toml Normal file
View File

@@ -0,0 +1,27 @@
[tool.bumpversion]
current_version = "9.3.1"
[[tool.bumpversion.files]]
filename = "capa/version.py"
search = '__version__ = "{current_version}"'
replace = '__version__ = "{new_version}"'
[[tool.bumpversion.files]]
filename = "capa/ida/plugin/ida-plugin.json"
search = '"version": "{current_version}"'
replace = '"version": "{new_version}"'
[[tool.bumpversion.files]]
filename = "capa/ida/plugin/ida-plugin.json"
search = '"flare-capa=={current_version}"'
replace = '"flare-capa=={new_version}"'
[[tool.bumpversion.files]]
filename = "CHANGELOG.md"
search = "v{current_version}...master"
replace = "v{current_version}...{new_version}"
[[tool.bumpversion.files]]
filename = "CHANGELOG.md"
search = "master (unreleased)"
replace = "v{new_version}"

View File

@@ -74,6 +74,9 @@ a = Analysis(
# only be installed locally. # only be installed locally.
"binaryninja", "binaryninja",
"ida", "ida",
# remove once https://github.com/mandiant/capa/issues/2681 has
# been addressed by PyInstaller
"pkg_resources",
], ],
) )

View File

@@ -9,6 +9,7 @@ on:
- '**.md' - '**.md'
release: release:
types: [edited, published] types: [edited, published]
workflow_dispatch: # manual trigger for testing
permissions: permissions:
contents: write contents: write
@@ -22,24 +23,38 @@ jobs:
fail-fast: true fail-fast: true
matrix: matrix:
include: include:
- os: ubuntu-20.04 - os: ubuntu-22.04
# use old linux so that the shared library versioning is more portable # use old linux so that the shared library versioning is more portable
artifact_name: capa artifact_name: capa
asset_name: linux asset_name: linux
python_version: '3.10' python_version: '3.10'
- os: ubuntu-20.04 - os: ubuntu-22.04-arm
artifact_name: capa
asset_name: linux-arm64
python_version: '3.10'
- os: ubuntu-22.04
artifact_name: capa artifact_name: capa
asset_name: linux-py312 asset_name: linux-py312
python_version: '3.12' python_version: '3.12'
- os: windows-2019 - os: windows-2022
artifact_name: capa.exe artifact_name: capa.exe
asset_name: windows asset_name: windows
python_version: '3.10' python_version: '3.10'
- os: macos-13 # Windows 11 ARM64 complains of conflicting package version
# use older macOS for assumed better portability # Additionally, there is no ARM64 build of Python for Python 3.10 on Windows 11 ARM: https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json
#- os: windows-11-arm
# artifact_name: capa.exe
# asset_name: windows-arm64
# python_version: '3.12'
- os: macos-15-intel
# macos-15-intel is the lowest native intel build
artifact_name: capa artifact_name: capa
asset_name: macos asset_name: macos
python_version: '3.10' python_version: '3.10'
- os: macos-14
artifact_name: capa
asset_name: macos-arm64
python_version: '3.10'
steps: steps:
- name: Checkout capa - name: Checkout capa
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@@ -49,7 +64,7 @@ jobs:
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
with: with:
python-version: ${{ matrix.python_version }} python-version: ${{ matrix.python_version }}
- if: matrix.os == 'ubuntu-20.04' - if: matrix.os == 'ubuntu-22.04' || matrix.os == 'ubuntu-22.04-arm'
run: sudo apt-get install -y libyaml-dev run: sudo apt-get install -y libyaml-dev
- name: Upgrade pip, setuptools - name: Upgrade pip, setuptools
run: python -m pip install --upgrade pip setuptools run: python -m pip install --upgrade pip setuptools
@@ -59,6 +74,28 @@ jobs:
pip install -e .[build] pip install -e .[build]
- name: Build standalone executable - name: Build standalone executable
run: pyinstaller --log-level DEBUG .github/pyinstaller/pyinstaller.spec run: pyinstaller --log-level DEBUG .github/pyinstaller/pyinstaller.spec
- name: Does it run without warnings or errors?
shell: bash
run: |
if [[ "${{ matrix.os }}" == "windows-2022" ]] || [[ "${{ matrix.os }}" == "windows-11-arm" ]]; then
EXECUTABLE=".\\dist\\capa"
else
EXECUTABLE="./dist/capa"
fi
output=$(${EXECUTABLE} --version 2>&1)
exit_code=$?
echo "${output}"
echo "${exit_code}"
if echo "${output}" | grep -iE 'error|warning'; then
exit 1
fi
if [[ "${exit_code}" -ne 0 ]]; then
exit 1
fi
- name: Does it run (PE)? - name: Does it run (PE)?
run: dist/capa -d "tests/data/Practical Malware Analysis Lab 01-01.dll_" run: dist/capa -d "tests/data/Practical Malware Analysis Lab 01-01.dll_"
- name: Does it run (Shellcode)? - name: Does it run (Shellcode)?
@@ -74,34 +111,6 @@ jobs:
name: ${{ matrix.asset_name }} name: ${{ matrix.asset_name }}
path: dist/${{ matrix.artifact_name }} path: dist/${{ matrix.artifact_name }}
test_run:
name: Test run on ${{ matrix.os }} / ${{ matrix.asset_name }}
runs-on: ${{ matrix.os }}
needs: [build]
strategy:
matrix:
include:
# OSs not already tested above
- os: ubuntu-22.04
artifact_name: capa
asset_name: linux
- os: ubuntu-22.04
artifact_name: capa
asset_name: linux-py312
- os: windows-2022
artifact_name: capa.exe
asset_name: windows
steps:
- name: Download ${{ matrix.asset_name }}
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2
with:
name: ${{ matrix.asset_name }}
- name: Set executable flag
if: matrix.os != 'windows-2022'
run: chmod +x ${{ matrix.artifact_name }}
- name: Run capa
run: ./${{ matrix.artifact_name }} -h
zip_and_upload: zip_and_upload:
# upload zipped binaries to Release page # upload zipped binaries to Release page
if: github.event_name == 'release' if: github.event_name == 'release'
@@ -113,12 +122,18 @@ jobs:
include: include:
- asset_name: linux - asset_name: linux
artifact_name: capa artifact_name: capa
- asset_name: linux-arm64
artifact_name: capa
- asset_name: linux-py312 - asset_name: linux-py312
artifact_name: capa artifact_name: capa
- asset_name: windows - asset_name: windows
artifact_name: capa.exe artifact_name: capa.exe
#- asset_name: windows-arm64
# artifact_name: capa.exe
- asset_name: macos - asset_name: macos
artifact_name: capa artifact_name: capa
- asset_name: macos-arm64
artifact_name: capa
steps: steps:
- name: Download ${{ matrix.asset_name }} - name: Download ${{ matrix.asset_name }}
uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2 uses: actions/download-artifact@eaceaf801fd36c7dee90939fad912460b18a1ffe # v4.1.2

View File

@@ -35,7 +35,7 @@ jobs:
with: with:
path: dist/* path: dist/*
- name: publish package - name: publish package
uses: pypa/gh-action-pypi-publish@f5622bde02b04381239da3573277701ceca8f6a0 # release/v1 uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # release/v1.12.4
with: with:
skip-existing: true skip-existing: true
verbose: true verbose: true

View File

@@ -42,10 +42,10 @@ jobs:
- name: Checkout capa - name: Checkout capa
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
# use latest available python to take advantage of best performance # use latest available python to take advantage of best performance
- name: Set up Python 3.12 - name: Set up Python 3.13
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
with: with:
python-version: "3.12" python-version: "3.13"
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install -r requirements.txt pip install -r requirements.txt
@@ -70,10 +70,10 @@ jobs:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
submodules: recursive submodules: recursive
- name: Set up Python 3.12 - name: Set up Python 3.13
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0
with: with:
python-version: "3.12" python-version: "3.13"
- name: Install capa - name: Install capa
run: | run: |
pip install -r requirements.txt pip install -r requirements.txt
@@ -88,16 +88,14 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-20.04, windows-2019, macos-13] os: [ubuntu-22.04, ubuntu-22.04-arm, windows-2022, macos-15-intel, macos-14]
# across all operating systems # across all operating systems
python-version: ["3.10", "3.11"] python-version: ["3.10", "3.13"]
include: include:
# on Ubuntu run these as well # on Ubuntu run these as well
- os: ubuntu-20.04 - os: ubuntu-22.04
python-version: "3.10"
- os: ubuntu-20.04
python-version: "3.11" python-version: "3.11"
- os: ubuntu-20.04 - os: ubuntu-22.04
python-version: "3.12" python-version: "3.12"
steps: steps:
- name: Checkout capa with submodules - name: Checkout capa with submodules
@@ -109,7 +107,7 @@ jobs:
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install pyyaml - name: Install pyyaml
if: matrix.os == 'ubuntu-20.04' if: matrix.os == 'ubuntu-22.04'
run: sudo apt-get install -y libyaml-dev run: sudo apt-get install -y libyaml-dev
- name: Install capa - name: Install capa
run: | run: |
@@ -131,7 +129,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.10", "3.11"] python-version: ["3.10", "3.13"]
steps: steps:
- name: Checkout capa with submodules - name: Checkout capa with submodules
# do only run if BN_SERIAL is available, have to do this in every step, see https://github.com/orgs/community/discussions/26726#discussioncomment-3253118 # do only run if BN_SERIAL is available, have to do this in every step, see https://github.com/orgs/community/discussions/26726#discussioncomment-3253118
@@ -168,16 +166,15 @@ jobs:
ghidra-tests: ghidra-tests:
name: Ghidra tests for ${{ matrix.python-version }} name: Ghidra tests for ${{ matrix.python-version }}
runs-on: ubuntu-20.04 runs-on: ubuntu-22.04
needs: [tests] needs: [tests]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.10", "3.11"] python-version: ["3.10", "3.13"]
java-version: ["17"] java-version: ["21"]
ghidra-version: ["11.0.1"] ghidra-version: ["12.0"]
public-version: ["PUBLIC_20240130"] # for ghidra releases public-version: ["PUBLIC_20251205"] # for ghidra releases
ghidrathon-version: ["4.0.0"]
steps: steps:
- name: Checkout capa with submodules - name: Checkout capa with submodules
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
@@ -197,25 +194,13 @@ jobs:
mkdir ./.github/ghidra mkdir ./.github/ghidra
wget "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${{ matrix.ghidra-version }}_build/ghidra_${{ matrix.ghidra-version }}_${{ matrix.public-version }}.zip" -O ./.github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC.zip wget "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_${{ matrix.ghidra-version }}_build/ghidra_${{ matrix.ghidra-version }}_${{ matrix.public-version }}.zip" -O ./.github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC.zip
unzip .github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC.zip -d .github/ghidra/ unzip .github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC.zip -d .github/ghidra/
- name: Install Ghidrathon
run : |
mkdir ./.github/ghidrathon
wget "https://github.com/mandiant/Ghidrathon/releases/download/v${{ matrix.ghidrathon-version }}/Ghidrathon-v${{ matrix.ghidrathon-version}}.zip" -O ./.github/ghidrathon/ghidrathon-v${{ matrix.ghidrathon-version }}.zip
unzip .github/ghidrathon/ghidrathon-v${{ matrix.ghidrathon-version }}.zip -d .github/ghidrathon/
python -m pip install -r .github/ghidrathon/requirements.txt
python .github/ghidrathon/ghidrathon_configure.py $(pwd)/.github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC
unzip .github/ghidrathon/Ghidrathon-v${{ matrix.ghidrathon-version }}.zip -d .github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC/Ghidra/Extensions
- name: Install pyyaml - name: Install pyyaml
run: sudo apt-get install -y libyaml-dev run: sudo apt-get install -y libyaml-dev
- name: Install capa - name: Install capa
run: | run: |
pip install -r requirements.txt pip install -e .[dev]
pip install -e .[dev,scripts]
- name: Run tests - name: Run tests
run: | env:
mkdir ./.github/ghidra/project GHIDRA_INSTALL_DIR: ${{ github.workspace }}/.github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC
.github/ghidra/ghidra_${{ matrix.ghidra-version }}_PUBLIC/support/analyzeHeadless .github/ghidra/project ghidra_test -Import ./tests/data/mimikatz.exe_ -ScriptPath ./tests/ -PostScript test_ghidra_features.py > ../output.log run: pytest -v tests/test_ghidra_features.py
cat ../output.log
exit_code=$(cat ../output.log | grep exit | awk '{print $NF}')
exit $exit_code

1
.gitignore vendored
View File

@@ -122,6 +122,7 @@ scripts/perf/*.zip
*/.DS_Store */.DS_Store
Pipfile Pipfile
Pipfile.lock Pipfile.lock
uv.lock
/cache/ /cache/
.github/binja/binaryninja .github/binja/binaryninja
.github/binja/download_headless.py .github/binja/download_headless.py

View File

@@ -4,9 +4,137 @@
### New Features ### New Features
- ghidra: support PyGhidra @mike-hunhoff #2788
### Breaking Changes ### Breaking Changes
### New Rules (15) ### New Rules (4)
- nursery/run-as-nodejs-native-module mehunhoff@google.com
- nursery/inject-shellcode-using-thread-pool-work-insertion-with-tp_io still@teamt5.org
- nursery/inject-shellcode-using-thread-pool-work-insertion-with-tp_timer still@teamt5.org
- nursery/inject-shellcode-using-thread-pool-work-insertion-with-tp_work still@teamt5.org
-
### Bug Fixes
- Fixed insecure deserialization vulnerability in YAML loading @0x1622 (#2770)
### capa Explorer Web
### capa Explorer IDA Pro plugin
### Development
- ci: deprecate macos-13 runner and use Python v3.13 for testing @mike-hunhoff #2777
### Raw diffs
- [capa v9.3.1...master](https://github.com/mandiant/capa/compare/v9.3.1...master)
- [capa-rules v9.3.1...master](https://github.com/mandiant/capa-rules/compare/v9.3.1...master)
## v9.3.1
This patch release fixes a missing import for the capa explorer plugin for IDA Pro.
### Bug Fixes
- add missing ida-netnode dependency to project.toml @mike-hunhoff #2765
### Development
- ci: bump binja min version @mike-hunhoff #2763
### Raw diffs
- [capa v9.3.0...master](https://github.com/mandiant/capa/compare/v9.3.0...master)
- [capa-rules v9.3.0...master](https://github.com/mandiant/capa-rules/compare/v9.3.0...master)
## v9.3.0
capa v9.3.0 comes with over 20 new and/or impoved rules.
For IDA users the capa explorer plugin is now available via the IDA Pro plugin repository and contains Qt compatibility layer for PyQt5 and PySide6 support.
Additionally a Binary Ninja bug has been fixed. Released binaries now include ARM64 binaries (Linux and macOS).
### New Features
- ci: add support for arm64 binary releases
### Breaking Changes
### New Rules (24)
- anti-analysis/anti-vm/vm-detection/detect-mouse-movement-via-activity-checks-on-windows tevajdr@gmail.com
- nursery/create-executable-heap moritz.raabe@mandiant.com
- anti-analysis/packer/dxpack/packed-with-dxpack jakubjozwiak@google.com
- anti-analysis/anti-av/patch-bitdefender-hooking-dll-function jakubjozwiak@google.com
- nursery/acquire-load-driver-privileges mehunhoff@google.com
- nursery/communicate-using-ftp mehunhoff@google.com
- linking/static/eclipse-paho-mqtt-c/linked-against-eclipse-paho-mqtt-c jakubjozwiak@google.com
- linking/static/qmqtt/linked-against-qmqtt jakubjozwiak@google.com
- anti-analysis/anti-forensic/disable-powershell-transcription jakubjozwiak@google.com
- host-interaction/powershell/bypass-powershell-constrained-language-mode-via-getsystemlockdownpolicy-patch jakubjozwiak@google.com
- linking/static/grpc/linked-against-grpc jakubjozwiak@google.com
- linking/static/hp-socket/linked-against-hp-socket jakubjozwiak@google.com
- load-code/execute-jscript-via-vsaengine-in-dotnet jakubjozwiak@google.com
- linking/static/funchook/linked-against-funchook jakubjozwiak@google.com
- linking/static/plthook/linked-against-plthook jakubjozwiak@google.com
- host-interaction/network/enumerate-tcp-connections-via-wmi-com-api jakubjozwiak@google.com
- host-interaction/network/routing-table/create-routing-table-entry jakubjozwiak@google.com
- host-interaction/network/routing-table/get-routing-table michael.hunhoff@mandiant.com
- host-interaction/file-system/use-io_uring-io-interface-on-linux jakubjozwiak@google.com
- collection/keylog/log-keystrokes-via-direct-input zeze-zeze
- nursery/compiled-from-fsharp mehunhoff@google.com
- nursery/decrypt-data-using-aes-via-dotnet mehunhoff@google.com
- nursery/get-dotnet-assembly-entry-point mehunhoff@google.com
### Bug Fixes
- binja: fix a crash during feature extraction when the MLIL is unavailable @xusheng6 #2714
### capa Explorer Web
### capa Explorer IDA Pro plugin
- add `ida-plugin.json` for inclusion in the IDA Pro plugin repository @williballenthin
- ida plugin: add Qt compatibility layer for PyQt5 and PySide6 support @williballenthin #2707
- delay import to not load Qt* when running under idalib @mr-tz #2752
### Development
- ci: remove redundant "test_run" action from build workflow @mike-hunhoff #2692
- dev: add bumpmyversion to bump and sync versions across the project @mr-tz
### Raw diffs
- [capa v9.2.1...9.3.0](https://github.com/mandiant/capa/compare/v9.2.1...9.3.0)
- [capa-rules v9.2.1...9.3.0](https://github.com/mandiant/capa-rules/compare/v9.2.1...9.3.0)
## v9.2.1
This point release fixes bugs including removing an unnecessary PyInstaller warning message and enabling the standalone binary to execute on systems running older versions of glibc.
### Bug Fixes
- ci: exclude pkg_resources from PyInstaller build @mike-hunhoff #2684
- ci: downgrade Ubuntu version to accommodate older glibc versions @mike-hunhoff #2684
### Development
- ci: upgrade Windows version to avoid deprecation @mike-hunhoff #2684
- ci: check if build runs without warnings or errors @mike-hunhoff #2684
### Raw diffs
- [capa v9.2.0...v9.2.1](https://github.com/mandiant/capa/compare/v9.2.0...v9.2.1)
- [capa-rules v9.2.0...v9.2.1](https://github.com/mandiant/capa-rules/compare/v9.2.0...v9.2.1)
## v9.2.0
This release improves a few aspects of dynamic analysis, including relaxing our validation on fields across many CAPE versions and processing additional VMRay submission file types, for example.
It also includes an updated rule pack containing new rules and rule fixes.
### New Features
- vmray: do not restrict analysis to PE and ELF files, e.g. docx @mike-hunhoff #2672
### Breaking Changes
### New Rules (22)
- communication/socket/connect-socket moritz.raabe@mandiant.com joakim@intezer.com mrhafizfarhad@gmail.com - communication/socket/connect-socket moritz.raabe@mandiant.com joakim@intezer.com mrhafizfarhad@gmail.com
- communication/socket/udp/connect-udp-socket mrhafizfarhad@gmail.com - communication/socket/udp/connect-udp-socket mrhafizfarhad@gmail.com
@@ -22,22 +150,23 @@
- nursery/disable-firewall-features-via-registry-on-windows mehunhoff@google.com - nursery/disable-firewall-features-via-registry-on-windows mehunhoff@google.com
- nursery/disable-system-restore-features-via-registry-on-windows mehunhoff@google.com - nursery/disable-system-restore-features-via-registry-on-windows mehunhoff@google.com
- nursery/disable-windows-defender-features-via-registry-on-windows mehunhoff@google.com - nursery/disable-windows-defender-features-via-registry-on-windows mehunhoff@google.com
- - host-interaction/file-system/write/clear-file-content jakeperalta7
- host-interaction/filter/unload-minifilter-driver JakePeralta7
- exploitation/enumeration/make-suspicious-ntquerysysteminformation-call zdw@google.com
- exploitation/gadgets/load-ntoskrnl zdw@google.com
- exploitation/gadgets/resolve-ntoskrnl-gadgets zdw@google.com
- exploitation/spraying/make-suspicious-ntfscontrolfile-call zdw@google.com
- anti-analysis/anti-forensic/unload-sysmon JakePeralta7
### Bug Fixes ### Bug Fixes
- cape: make some fields optional @williballenthin #2631 #2632 - cape: make some fields optional @williballenthin #2631 #2632
- lint: add WARN for regex features that contain unescaped dot #2635 - lint: add WARN for regex features that contain unescaped dot #2635
- lint: add ERROR for incomplete registry control set regex #2643 - lint: add ERROR for incomplete registry control set regex #2643
- binja: update unit test core version #2670
### capa Explorer Web
### capa Explorer IDA Pro plugin
### Development
### Raw diffs ### Raw diffs
- [capa v9.1.0...master](https://github.com/mandiant/capa/compare/v9.1.0...master) - [capa v9.1.0...v9.2.0](https://github.com/mandiant/capa/compare/v9.1.0...v9.2.0)
- [capa-rules v9.1.0...master](https://github.com/mandiant/capa-rules/compare/v9.1.0...master) - [capa-rules v9.1.0...v9.2.0](https://github.com/mandiant/capa-rules/compare/v9.1.0...v9.2.0)
## v9.1.0 ## v9.1.0

View File

@@ -315,3 +315,6 @@ If you use Ghidra, then you can use the [capa + Ghidra integration](/capa/ghidra
## capa testfiles ## capa testfiles
The [capa-testfiles repository](https://github.com/mandiant/capa-testfiles) contains the data we use to test capa's code and rules The [capa-testfiles repository](https://github.com/mandiant/capa-testfiles) contains the data we use to test capa's code and rules
## mailing list
Subscribe to the FLARE mailing list for community announcements! Email "subscribe" to [flare-external@google.com](mailto:flare-external@google.com?subject=subscribe).

View File

@@ -19,7 +19,6 @@ from binaryninja import (
Function, Function,
BinaryView, BinaryView,
SymbolType, SymbolType,
ILException,
RegisterValueType, RegisterValueType,
VariableSourceType, VariableSourceType,
LowLevelILOperation, LowLevelILOperation,
@@ -192,9 +191,8 @@ def extract_stackstring(fh: FunctionHandle):
if bv is None: if bv is None:
return return
try: mlil = func.mlil
mlil = func.mlil if mlil is None:
except ILException:
return return
for block in mlil.basic_blocks: for block in mlil.basic_blocks:

View File

@@ -83,7 +83,7 @@ def bb_contains_stackstring(bb: ghidra.program.model.block.CodeBlock) -> bool:
true if basic block contains enough moves of constant bytes to the stack true if basic block contains enough moves of constant bytes to the stack
""" """
count = 0 count = 0
for insn in currentProgram().getListing().getInstructions(bb, True): # type: ignore [name-defined] # noqa: F821 for insn in capa.features.extractors.ghidra.helpers.get_current_program().getListing().getInstructions(bb, True):
if is_mov_imm_to_stack(insn): if is_mov_imm_to_stack(insn):
count += get_printable_len(insn.getScalar(1)) count += get_printable_len(insn.getScalar(1))
if count > MIN_STACKSTRING_LEN: if count > MIN_STACKSTRING_LEN:
@@ -96,7 +96,9 @@ def _bb_has_tight_loop(bb: ghidra.program.model.block.CodeBlock):
parse tight loops, true if last instruction in basic block branches to bb start parse tight loops, true if last instruction in basic block branches to bb start
""" """
# Reverse Ordered, first InstructionDB # Reverse Ordered, first InstructionDB
last_insn = currentProgram().getListing().getInstructions(bb, False).next() # type: ignore [name-defined] # noqa: F821 last_insn = (
capa.features.extractors.ghidra.helpers.get_current_program().getListing().getInstructions(bb, False).next()
)
if last_insn.getFlowType().isJump(): if last_insn.getFlowType().isJump():
return last_insn.getAddress(0) == bb.getMinAddress() return last_insn.getAddress(0) == bb.getMinAddress()
@@ -140,20 +142,3 @@ def extract_features(fh: FunctionHandle, bbh: BBHandle) -> Iterator[tuple[Featur
for bb_handler in BASIC_BLOCK_HANDLERS: for bb_handler in BASIC_BLOCK_HANDLERS:
for feature, addr in bb_handler(fh, bbh): for feature, addr in bb_handler(fh, bbh):
yield feature, addr yield feature, addr
def main():
features = []
from capa.features.extractors.ghidra.extractor import GhidraFeatureExtractor
for fh in GhidraFeatureExtractor().get_functions():
for bbh in capa.features.extractors.ghidra.helpers.get_function_blocks(fh):
features.extend(list(extract_features(fh, bbh)))
import pprint
pprint.pprint(features) # noqa: T203
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,36 @@
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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 Optional
class GhidraContext:
def __init__(self, program, flat_api, monitor):
self.program = program
self.flat_api = flat_api
self.monitor = monitor
_context: Optional[GhidraContext] = None
def set_context(program, flat_api, monitor):
global _context
_context = GhidraContext(program, flat_api, monitor)
def get_context() -> GhidraContext:
if _context is None:
raise RuntimeError("GhidraContext not initialized")
return _context

View File

@@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import weakref
import contextlib
from typing import Iterator from typing import Iterator
import capa.features.extractors.ghidra.file import capa.features.extractors.ghidra.file
@@ -31,19 +33,21 @@ from capa.features.extractors.base_extractor import (
class GhidraFeatureExtractor(StaticFeatureExtractor): class GhidraFeatureExtractor(StaticFeatureExtractor):
def __init__(self): def __init__(self, ctx_manager=None, tmpdir=None):
self.ctx_manager = ctx_manager
self.tmpdir = tmpdir
import capa.features.extractors.ghidra.helpers as ghidra_helpers import capa.features.extractors.ghidra.helpers as ghidra_helpers
super().__init__( super().__init__(
SampleHashes( SampleHashes(
md5=capa.ghidra.helpers.get_file_md5(), md5=ghidra_helpers.get_current_program().getExecutableMD5(),
# ghidra doesn't expose this hash. # ghidra doesn't expose this hash.
# https://ghidra.re/ghidra_docs/api/ghidra/program/model/listing/Program.html # https://ghidra.re/ghidra_docs/api/ghidra/program/model/listing/Program.html
# #
# the hashes are stored in the database, not computed on the fly, # the hashes are stored in the database, not computed on the fly,
# so it's probably not trivial to add SHA1. # so it's probably not trivial to add SHA1.
sha1="", sha1="",
sha256=capa.ghidra.helpers.get_file_sha256(), sha256=ghidra_helpers.get_current_program().getExecutableSHA256(),
) )
) )
@@ -55,8 +59,16 @@ class GhidraFeatureExtractor(StaticFeatureExtractor):
self.externs = ghidra_helpers.get_file_externs() self.externs = ghidra_helpers.get_file_externs()
self.fakes = ghidra_helpers.map_fake_import_addrs() self.fakes = ghidra_helpers.map_fake_import_addrs()
# Register cleanup to run when the extractor is garbage collected or when the program exits.
# We use weakref.finalize instead of __del__ to avoid issues with reference cycles and
# to ensure deterministic cleanup on interpreter shutdown.
if self.ctx_manager or self.tmpdir:
weakref.finalize(self, cleanup, self.ctx_manager, self.tmpdir)
def get_base_address(self): def get_base_address(self):
return AbsoluteVirtualAddress(currentProgram().getImageBase().getOffset()) # type: ignore [name-defined] # noqa: F821 import capa.features.extractors.ghidra.helpers as ghidra_helpers
return AbsoluteVirtualAddress(ghidra_helpers.get_current_program().getImageBase().getOffset())
def extract_global_features(self): def extract_global_features(self):
yield from self.global_features yield from self.global_features
@@ -77,7 +89,9 @@ class GhidraFeatureExtractor(StaticFeatureExtractor):
@staticmethod @staticmethod
def get_function(addr: int) -> FunctionHandle: def get_function(addr: int) -> FunctionHandle:
func = getFunctionContaining(toAddr(addr)) # type: ignore [name-defined] # noqa: F821 import capa.features.extractors.ghidra.helpers as ghidra_helpers
func = ghidra_helpers.get_flat_api().getFunctionContaining(ghidra_helpers.get_flat_api().toAddr(addr))
return FunctionHandle(address=AbsoluteVirtualAddress(func.getEntryPoint().getOffset()), inner=func) return FunctionHandle(address=AbsoluteVirtualAddress(func.getEntryPoint().getOffset()), inner=func)
def extract_function_features(self, fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]: def extract_function_features(self, fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]:
@@ -98,3 +112,12 @@ class GhidraFeatureExtractor(StaticFeatureExtractor):
def extract_insn_features(self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle): def extract_insn_features(self, fh: FunctionHandle, bbh: BBHandle, ih: InsnHandle):
yield from capa.features.extractors.ghidra.insn.extract_features(fh, bbh, ih) yield from capa.features.extractors.ghidra.insn.extract_features(fh, bbh, ih)
def cleanup(ctx_manager, tmpdir):
if ctx_manager:
with contextlib.suppress(Exception):
ctx_manager.__exit__(None, None, None)
if tmpdir:
with contextlib.suppress(Exception):
tmpdir.cleanup()

View File

@@ -80,7 +80,7 @@ def extract_file_embedded_pe() -> Iterator[tuple[Feature, Address]]:
for i in range(256) for i in range(256)
] ]
for block in currentProgram().getMemory().getBlocks(): # type: ignore [name-defined] # noqa: F821 for block in capa.features.extractors.ghidra.helpers.get_current_program().getMemory().getBlocks():
if not all((block.isLoaded(), block.isInitialized(), "Headers" not in block.getName())): if not all((block.isLoaded(), block.isInitialized(), "Headers" not in block.getName())):
continue continue
@@ -93,9 +93,37 @@ def extract_file_embedded_pe() -> Iterator[tuple[Feature, Address]]:
def extract_file_export_names() -> Iterator[tuple[Feature, Address]]: def extract_file_export_names() -> Iterator[tuple[Feature, Address]]:
"""extract function exports""" """extract function exports"""
st = currentProgram().getSymbolTable() # type: ignore [name-defined] # noqa: F821 program = capa.features.extractors.ghidra.helpers.get_current_program()
st = program.getSymbolTable()
for addr in st.getExternalEntryPointIterator(): for addr in st.getExternalEntryPointIterator():
yield Export(st.getPrimarySymbol(addr).getName()), AbsoluteVirtualAddress(addr.getOffset()) sym = st.getPrimarySymbol(addr)
name = sym.getName()
# Check for forwarded export
is_forwarded = False
refs = program.getReferenceManager().getReferencesFrom(addr)
for ref in refs:
if ref.getToAddress().isExternalAddress():
ext_sym = st.getPrimarySymbol(ref.getToAddress())
if ext_sym:
ext_loc = program.getExternalManager().getExternalLocation(ext_sym)
if ext_loc:
# It is a forwarded export
libname = ext_loc.getLibraryName()
if libname.lower().endswith(".dll"):
libname = libname[:-4]
forwarded_name = f"{libname}.{ext_loc.getLabel()}"
forwarded_name = capa.features.extractors.helpers.reformat_forwarded_export_name(forwarded_name)
yield Export(forwarded_name), AbsoluteVirtualAddress(addr.getOffset())
yield Characteristic("forwarded export"), AbsoluteVirtualAddress(addr.getOffset())
is_forwarded = True
break
if not is_forwarded:
yield Export(name), AbsoluteVirtualAddress(addr.getOffset())
def extract_file_import_names() -> Iterator[tuple[Feature, Address]]: def extract_file_import_names() -> Iterator[tuple[Feature, Address]]:
@@ -110,7 +138,7 @@ def extract_file_import_names() -> Iterator[tuple[Feature, Address]]:
- importname - importname
""" """
for f in currentProgram().getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821 for f in capa.features.extractors.ghidra.helpers.get_current_program().getFunctionManager().getExternalFunctions():
for r in f.getSymbol().getReferences(): for r in f.getSymbol().getReferences():
if r.getReferenceType().isData(): if r.getReferenceType().isData():
addr = r.getFromAddress().getOffset() # gets pointer to fake external addr addr = r.getFromAddress().getOffset() # gets pointer to fake external addr
@@ -126,14 +154,14 @@ def extract_file_import_names() -> Iterator[tuple[Feature, Address]]:
def extract_file_section_names() -> Iterator[tuple[Feature, Address]]: def extract_file_section_names() -> Iterator[tuple[Feature, Address]]:
"""extract section names""" """extract section names"""
for block in currentProgram().getMemory().getBlocks(): # type: ignore [name-defined] # noqa: F821 for block in capa.features.extractors.ghidra.helpers.get_current_program().getMemory().getBlocks():
yield Section(block.getName()), AbsoluteVirtualAddress(block.getStart().getOffset()) yield Section(block.getName()), AbsoluteVirtualAddress(block.getStart().getOffset())
def extract_file_strings() -> Iterator[tuple[Feature, Address]]: def extract_file_strings() -> Iterator[tuple[Feature, Address]]:
"""extract ASCII and UTF-16 LE strings""" """extract ASCII and UTF-16 LE strings"""
for block in currentProgram().getMemory().getBlocks(): # type: ignore [name-defined] # noqa: F821 for block in capa.features.extractors.ghidra.helpers.get_current_program().getMemory().getBlocks():
if not block.isInitialized(): if not block.isInitialized():
continue continue
@@ -153,7 +181,8 @@ def extract_file_function_names() -> Iterator[tuple[Feature, Address]]:
extract the names of statically-linked library functions. extract the names of statically-linked library functions.
""" """
for sym in currentProgram().getSymbolTable().getAllSymbols(True): # type: ignore [name-defined] # noqa: F821 for sym in capa.features.extractors.ghidra.helpers.get_current_program().getSymbolTable().getAllSymbols(True):
# .isExternal() misses more than this config for the function symbols # .isExternal() misses more than this config for the function symbols
if sym.getSymbolType() == SymbolType.FUNCTION and sym.getSource() == SourceType.ANALYSIS and sym.isGlobal(): if sym.getSymbolType() == SymbolType.FUNCTION and sym.getSource() == SourceType.ANALYSIS and sym.isGlobal():
name = sym.getName() # starts to resolve names based on Ghidra's FidDB name = sym.getName() # starts to resolve names based on Ghidra's FidDB
@@ -170,7 +199,7 @@ def extract_file_function_names() -> Iterator[tuple[Feature, Address]]:
def extract_file_format() -> Iterator[tuple[Feature, Address]]: def extract_file_format() -> Iterator[tuple[Feature, Address]]:
ef = currentProgram().getExecutableFormat() # type: ignore [name-defined] # noqa: F821 ef = capa.features.extractors.ghidra.helpers.get_current_program().getExecutableFormat()
if "PE" in ef: if "PE" in ef:
yield Format(FORMAT_PE), NO_ADDRESS yield Format(FORMAT_PE), NO_ADDRESS
elif "ELF" in ef: elif "ELF" in ef:

View File

@@ -26,18 +26,22 @@ from capa.features.extractors.base_extractor import FunctionHandle
def extract_function_calls_to(fh: FunctionHandle): def extract_function_calls_to(fh: FunctionHandle):
"""extract callers to a function""" """extract callers to a function"""
f: ghidra.program.database.function.FunctionDB = fh.inner f: "ghidra.program.database.function.FunctionDB" = fh.inner
for ref in f.getSymbol().getReferences(): for ref in f.getSymbol().getReferences():
if ref.getReferenceType().isCall(): if ref.getReferenceType().isCall():
yield Characteristic("calls to"), AbsoluteVirtualAddress(ref.getFromAddress().getOffset()) yield Characteristic("calls to"), AbsoluteVirtualAddress(ref.getFromAddress().getOffset())
def extract_function_loop(fh: FunctionHandle): def extract_function_loop(fh: FunctionHandle):
f: ghidra.program.database.function.FunctionDB = fh.inner f: "ghidra.program.database.function.FunctionDB" = fh.inner
edges = [] edges = []
for block in SimpleBlockIterator(BasicBlockModel(currentProgram()), f.getBody(), monitor()): # type: ignore [name-defined] # noqa: F821 for block in SimpleBlockIterator(
dests = block.getDestinations(monitor()) # type: ignore [name-defined] # noqa: F821 BasicBlockModel(capa.features.extractors.ghidra.helpers.get_current_program()),
f.getBody(),
capa.features.extractors.ghidra.helpers.get_monitor(),
):
dests = block.getDestinations(capa.features.extractors.ghidra.helpers.get_monitor())
s_addrs = block.getStartAddresses() s_addrs = block.getStartAddresses()
while dests.hasNext(): # For loop throws Python TypeError while dests.hasNext(): # For loop throws Python TypeError
@@ -49,16 +53,17 @@ def extract_function_loop(fh: FunctionHandle):
def extract_recursive_call(fh: FunctionHandle): def extract_recursive_call(fh: FunctionHandle):
f: ghidra.program.database.function.FunctionDB = fh.inner f: "ghidra.program.database.function.FunctionDB" = fh.inner
for func in f.getCalledFunctions(monitor()): # type: ignore [name-defined] # noqa: F821 for func in f.getCalledFunctions(capa.features.extractors.ghidra.helpers.get_monitor()):
if func.getEntryPoint().getOffset() == f.getEntryPoint().getOffset(): if func.getEntryPoint().getOffset() == f.getEntryPoint().getOffset():
yield Characteristic("recursive call"), AbsoluteVirtualAddress(f.getEntryPoint().getOffset()) yield Characteristic("recursive call"), AbsoluteVirtualAddress(f.getEntryPoint().getOffset())
def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]: def extract_features(fh: FunctionHandle) -> Iterator[tuple[Feature, Address]]:
for func_handler in FUNCTION_HANDLERS: """extract function features"""
for feature, addr in func_handler(fh): for function_handler in FUNCTION_HANDLERS:
for feature, addr in function_handler(fh):
yield feature, addr yield feature, addr

View File

@@ -26,7 +26,7 @@ logger = logging.getLogger(__name__)
def extract_os() -> Iterator[tuple[Feature, Address]]: def extract_os() -> Iterator[tuple[Feature, Address]]:
format_name: str = currentProgram().getExecutableFormat() # type: ignore [name-defined] # noqa: F821 format_name: str = capa.features.extractors.ghidra.helpers.get_current_program().getExecutableFormat()
if "PE" in format_name: if "PE" in format_name:
yield OS(OS_WINDOWS), NO_ADDRESS yield OS(OS_WINDOWS), NO_ADDRESS
@@ -53,7 +53,7 @@ def extract_os() -> Iterator[tuple[Feature, Address]]:
def extract_arch() -> Iterator[tuple[Feature, Address]]: def extract_arch() -> Iterator[tuple[Feature, Address]]:
lang_id = currentProgram().getMetadata().get("Language ID") # type: ignore [name-defined] # noqa: F821 lang_id = capa.features.extractors.ghidra.helpers.get_current_program().getMetadata().get("Language ID")
if "x86" in lang_id and "64" in lang_id: if "x86" in lang_id and "64" in lang_id:
yield Arch(ARCH_AMD64), NO_ADDRESS yield Arch(ARCH_AMD64), NO_ADDRESS

View File

@@ -22,9 +22,22 @@ from ghidra.program.model.symbol import SourceType, SymbolType
from ghidra.program.model.address import AddressSpace from ghidra.program.model.address import AddressSpace
import capa.features.extractors.helpers import capa.features.extractors.helpers
import capa.features.extractors.ghidra.context as ghidra_context
from capa.features.common import THUNK_CHAIN_DEPTH_DELTA from capa.features.common import THUNK_CHAIN_DEPTH_DELTA
from capa.features.address import AbsoluteVirtualAddress from capa.features.address import AbsoluteVirtualAddress
from capa.features.extractors.base_extractor import BBHandle, InsnHandle, FunctionHandle from capa.features.extractors.base_extractor import BBHandle, InsnHandle
def get_current_program():
return ghidra_context.get_context().program
def get_monitor():
return ghidra_context.get_context().monitor
def get_flat_api():
return ghidra_context.get_context().flat_api
def ints_to_bytes(bytez: list[int]) -> bytes: def ints_to_bytes(bytez: list[int]) -> bytes:
@@ -36,7 +49,7 @@ def ints_to_bytes(bytez: list[int]) -> bytes:
return bytes([b & 0xFF for b in bytez]) return bytes([b & 0xFF for b in bytez])
def find_byte_sequence(addr: ghidra.program.model.address.Address, seq: bytes) -> Iterator[int]: def find_byte_sequence(addr: "ghidra.program.model.address.Address", seq: bytes) -> Iterator[int]:
"""yield all ea of a given byte sequence """yield all ea of a given byte sequence
args: args:
@@ -44,12 +57,12 @@ def find_byte_sequence(addr: ghidra.program.model.address.Address, seq: bytes) -
seq: bytes to search e.g. b"\x01\x03" seq: bytes to search e.g. b"\x01\x03"
""" """
seqstr = "".join([f"\\x{b:02x}" for b in seq]) seqstr = "".join([f"\\x{b:02x}" for b in seq])
eas = findBytes(addr, seqstr, java.lang.Integer.MAX_VALUE, 1) # type: ignore [name-defined] # noqa: F821 eas = get_flat_api().findBytes(addr, seqstr, java.lang.Integer.MAX_VALUE, 1)
yield from eas yield from eas
def get_bytes(addr: ghidra.program.model.address.Address, length: int) -> bytes: def get_bytes(addr: "ghidra.program.model.address.Address", length: int) -> bytes:
"""yield length bytes at addr """yield length bytes at addr
args: args:
@@ -57,12 +70,12 @@ def get_bytes(addr: ghidra.program.model.address.Address, length: int) -> bytes:
length: length of bytes to pull length: length of bytes to pull
""" """
try: try:
return ints_to_bytes(getBytes(addr, length)) # type: ignore [name-defined] # noqa: F821 return ints_to_bytes(get_flat_api().getBytes(addr, int(length)))
except RuntimeError: except Exception:
return b"" return b""
def get_block_bytes(block: ghidra.program.model.mem.MemoryBlock) -> bytes: def get_block_bytes(block: "ghidra.program.model.mem.MemoryBlock") -> bytes:
"""yield all bytes in a given block """yield all bytes in a given block
args: args:
@@ -73,20 +86,21 @@ def get_block_bytes(block: ghidra.program.model.mem.MemoryBlock) -> bytes:
def get_function_symbols(): def get_function_symbols():
"""yield all non-external function symbols""" """yield all non-external function symbols"""
yield from currentProgram().getFunctionManager().getFunctionsNoStubs(True) # type: ignore [name-defined] # noqa: F821 yield from get_current_program().getFunctionManager().getFunctionsNoStubs(True)
def get_function_blocks(fh: FunctionHandle) -> Iterator[BBHandle]: def get_function_blocks(fh: "capa.features.extractors.base_extractor.FunctionHandle") -> Iterator[BBHandle]:
"""yield BBHandle for each bb in a given function""" """
yield the basic blocks of the function
"""
func: ghidra.program.database.function.FunctionDB = fh.inner for block in SimpleBlockIterator(BasicBlockModel(get_current_program()), fh.inner.getBody(), get_monitor()):
for bb in SimpleBlockIterator(BasicBlockModel(currentProgram()), func.getBody(), monitor()): # type: ignore [name-defined] # noqa: F821 yield BBHandle(address=AbsoluteVirtualAddress(block.getMinAddress().getOffset()), inner=block)
yield BBHandle(address=AbsoluteVirtualAddress(bb.getMinAddress().getOffset()), inner=bb)
def get_insn_in_range(bbh: BBHandle) -> Iterator[InsnHandle]: def get_insn_in_range(bbh: BBHandle) -> Iterator[InsnHandle]:
"""yield InshHandle for each insn in a given basicblock""" """yield InshHandle for each insn in a given basicblock"""
for insn in currentProgram().getListing().getInstructions(bbh.inner, True): # type: ignore [name-defined] # noqa: F821 for insn in get_current_program().getListing().getInstructions(bbh.inner, True):
yield InsnHandle(address=AbsoluteVirtualAddress(insn.getAddress().getOffset()), inner=insn) yield InsnHandle(address=AbsoluteVirtualAddress(insn.getAddress().getOffset()), inner=insn)
@@ -95,7 +109,7 @@ def get_file_imports() -> dict[int, list[str]]:
import_dict: dict[int, list[str]] = {} import_dict: dict[int, list[str]] = {}
for f in currentProgram().getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821 for f in get_current_program().getFunctionManager().getExternalFunctions():
for r in f.getSymbol().getReferences(): for r in f.getSymbol().getReferences():
if r.getReferenceType().isData(): if r.getReferenceType().isData():
addr = r.getFromAddress().getOffset() # gets pointer to fake external addr addr = r.getFromAddress().getOffset() # gets pointer to fake external addr
@@ -133,7 +147,7 @@ def get_file_externs() -> dict[int, list[str]]:
extern_dict: dict[int, list[str]] = {} extern_dict: dict[int, list[str]] = {}
for sym in currentProgram().getSymbolTable().getAllSymbols(True): # type: ignore [name-defined] # noqa: F821 for sym in get_current_program().getSymbolTable().getAllSymbols(True):
# .isExternal() misses more than this config for the function symbols # .isExternal() misses more than this config for the function symbols
if sym.getSymbolType() == SymbolType.FUNCTION and sym.getSource() == SourceType.ANALYSIS and sym.isGlobal(): if sym.getSymbolType() == SymbolType.FUNCTION and sym.getSource() == SourceType.ANALYSIS and sym.isGlobal():
name = sym.getName() # starts to resolve names based on Ghidra's FidDB name = sym.getName() # starts to resolve names based on Ghidra's FidDB
@@ -171,7 +185,7 @@ def map_fake_import_addrs() -> dict[int, list[int]]:
""" """
fake_dict: dict[int, list[int]] = {} fake_dict: dict[int, list[int]] = {}
for f in currentProgram().getFunctionManager().getExternalFunctions(): # type: ignore [name-defined] # noqa: F821 for f in get_current_program().getFunctionManager().getExternalFunctions():
for r in f.getSymbol().getReferences(): for r in f.getSymbol().getReferences():
if r.getReferenceType().isData(): if r.getReferenceType().isData():
fake_dict.setdefault(f.getEntryPoint().getOffset(), []).append(r.getFromAddress().getOffset()) fake_dict.setdefault(f.getEntryPoint().getOffset(), []).append(r.getFromAddress().getOffset())
@@ -180,7 +194,7 @@ def map_fake_import_addrs() -> dict[int, list[int]]:
def check_addr_for_api( def check_addr_for_api(
addr: ghidra.program.model.address.Address, addr: "ghidra.program.model.address.Address",
fakes: dict[int, list[int]], fakes: dict[int, list[int]],
imports: dict[int, list[str]], imports: dict[int, list[str]],
externs: dict[int, list[str]], externs: dict[int, list[str]],
@@ -202,18 +216,18 @@ def check_addr_for_api(
return False return False
def is_call_or_jmp(insn: ghidra.program.database.code.InstructionDB) -> bool: def is_call_or_jmp(insn: "ghidra.program.database.code.InstructionDB") -> bool:
return any(mnem in insn.getMnemonicString() for mnem in ["CALL", "J"]) # JMP, JNE, JNZ, etc return any(mnem in insn.getMnemonicString() for mnem in ["CALL", "J"]) # JMP, JNE, JNZ, etc
def is_sp_modified(insn: ghidra.program.database.code.InstructionDB) -> bool: def is_sp_modified(insn: "ghidra.program.database.code.InstructionDB") -> bool:
for i in range(insn.getNumOperands()): for i in range(insn.getNumOperands()):
if insn.getOperandType(i) == OperandType.REGISTER: if insn.getOperandType(i) == OperandType.REGISTER:
return "SP" in insn.getRegister(i).getName() and insn.getOperandRefType(i).isWrite() return "SP" in insn.getRegister(i).getName() and insn.getOperandRefType(i).isWrite()
return False return False
def is_stack_referenced(insn: ghidra.program.database.code.InstructionDB) -> bool: def is_stack_referenced(insn: "ghidra.program.database.code.InstructionDB") -> bool:
"""generic catch-all for stack references""" """generic catch-all for stack references"""
for i in range(insn.getNumOperands()): for i in range(insn.getNumOperands()):
if insn.getOperandType(i) == OperandType.REGISTER: if insn.getOperandType(i) == OperandType.REGISTER:
@@ -225,7 +239,7 @@ def is_stack_referenced(insn: ghidra.program.database.code.InstructionDB) -> boo
return any(ref.isStackReference() for ref in insn.getReferencesFrom()) return any(ref.isStackReference() for ref in insn.getReferencesFrom())
def is_zxor(insn: ghidra.program.database.code.InstructionDB) -> bool: def is_zxor(insn: "ghidra.program.database.code.InstructionDB") -> bool:
# assume XOR insn # assume XOR insn
# XOR's against the same operand zero out # XOR's against the same operand zero out
ops = [] ops = []
@@ -241,29 +255,29 @@ def is_zxor(insn: ghidra.program.database.code.InstructionDB) -> bool:
return all(n == operands[0] for n in operands) return all(n == operands[0] for n in operands)
def handle_thunk(addr: ghidra.program.model.address.Address): def handle_thunk(addr: "ghidra.program.model.address.Address"):
"""Follow thunk chains down to a reasonable depth""" """Follow thunk chains down to a reasonable depth"""
ref = addr ref = addr
for _ in range(THUNK_CHAIN_DEPTH_DELTA): for _ in range(THUNK_CHAIN_DEPTH_DELTA):
thunk_jmp = getInstructionAt(ref) # type: ignore [name-defined] # noqa: F821 thunk_jmp = get_flat_api().getInstructionAt(ref)
if thunk_jmp and is_call_or_jmp(thunk_jmp): if thunk_jmp and is_call_or_jmp(thunk_jmp):
if OperandType.isAddress(thunk_jmp.getOperandType(0)): if OperandType.isAddress(thunk_jmp.getOperandType(0)):
ref = thunk_jmp.getAddress(0) ref = thunk_jmp.getAddress(0)
else: else:
thunk_dat = getDataContaining(ref) # type: ignore [name-defined] # noqa: F821 thunk_dat = get_flat_api().getDataContaining(ref)
if thunk_dat and thunk_dat.isDefined() and thunk_dat.isPointer(): if thunk_dat and thunk_dat.isDefined() and thunk_dat.isPointer():
ref = thunk_dat.getValue() ref = thunk_dat.getValue()
break # end of thunk chain reached break # end of thunk chain reached
return ref return ref
def dereference_ptr(insn: ghidra.program.database.code.InstructionDB): def dereference_ptr(insn: "ghidra.program.database.code.InstructionDB"):
addr_code = OperandType.ADDRESS | OperandType.CODE addr_code = OperandType.ADDRESS | OperandType.CODE
to_deref = insn.getAddress(0) to_deref = insn.getAddress(0)
dat = getDataContaining(to_deref) # type: ignore [name-defined] # noqa: F821 dat = get_flat_api().getDataContaining(to_deref)
if insn.getOperandType(0) == addr_code: if insn.getOperandType(0) == addr_code:
thfunc = getFunctionContaining(to_deref) # type: ignore [name-defined] # noqa: F821 thfunc = get_flat_api().getFunctionContaining(to_deref)
if thfunc and thfunc.isThunk(): if thfunc and thfunc.isThunk():
return handle_thunk(to_deref) return handle_thunk(to_deref)
else: else:
@@ -294,7 +308,7 @@ def find_data_references_from_insn(insn, max_depth: int = 10):
to_addr = reference.getToAddress() to_addr = reference.getToAddress()
for _ in range(max_depth - 1): for _ in range(max_depth - 1):
data = getDataAt(to_addr) # type: ignore [name-defined] # noqa: F821 data = get_flat_api().getDataAt(to_addr)
if data and data.isPointer(): if data and data.isPointer():
ptr_value = data.getValue() ptr_value = data.getValue()

View File

@@ -234,7 +234,7 @@ def extract_insn_bytes_features(fh: FunctionHandle, bb: BBHandle, ih: InsnHandle
push offset iid_004118d4_IShellLinkA ; riid push offset iid_004118d4_IShellLinkA ; riid
""" """
for addr in capa.features.extractors.ghidra.helpers.find_data_references_from_insn(ih.inner): for addr in capa.features.extractors.ghidra.helpers.find_data_references_from_insn(ih.inner):
data = getDataAt(addr) # type: ignore [name-defined] # noqa: F821 data = capa.features.extractors.ghidra.helpers.get_flat_api().getDataAt(addr)
if data and not data.hasStringValue(): if data and not data.hasStringValue():
extracted_bytes = capa.features.extractors.ghidra.helpers.get_bytes(addr, MAX_BYTES_FEATURE_SIZE) extracted_bytes = capa.features.extractors.ghidra.helpers.get_bytes(addr, MAX_BYTES_FEATURE_SIZE)
if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes): if extracted_bytes and not capa.features.extractors.helpers.all_zeros(extracted_bytes):
@@ -249,9 +249,9 @@ def extract_insn_string_features(fh: FunctionHandle, bb: BBHandle, ih: InsnHandl
push offset aAcr ; "ACR > " push offset aAcr ; "ACR > "
""" """
for addr in capa.features.extractors.ghidra.helpers.find_data_references_from_insn(ih.inner): for addr in capa.features.extractors.ghidra.helpers.find_data_references_from_insn(ih.inner):
data = getDataAt(addr) # type: ignore [name-defined] # noqa: F821 data = capa.features.extractors.ghidra.helpers.get_flat_api().getDataAt(addr)
if data and data.hasStringValue(): if data and data.hasStringValue():
yield String(data.getValue()), ih.address yield String(str(data.getValue())), ih.address
def extract_insn_mnemonic_features( def extract_insn_mnemonic_features(
@@ -361,8 +361,8 @@ def extract_insn_cross_section_cflow(
if capa.features.extractors.ghidra.helpers.check_addr_for_api(ref, fakes, imports, externs): if capa.features.extractors.ghidra.helpers.check_addr_for_api(ref, fakes, imports, externs):
return return
this_mem_block = getMemoryBlock(insn.getAddress()) # type: ignore [name-defined] # noqa: F821 this_mem_block = capa.features.extractors.ghidra.helpers.get_flat_api().getMemoryBlock(insn.getAddress())
ref_block = getMemoryBlock(ref) # type: ignore [name-defined] # noqa: F821 ref_block = capa.features.extractors.ghidra.helpers.get_flat_api().getMemoryBlock(ref)
if ref_block != this_mem_block: if ref_block != this_mem_block:
yield Characteristic("cross section flow"), ih.address yield Characteristic("cross section flow"), ih.address
@@ -425,19 +425,19 @@ def check_nzxor_security_cookie_delta(
Check if insn within last addr of last bb - delta Check if insn within last addr of last bb - delta
""" """
model = SimpleBlockModel(currentProgram()) # type: ignore [name-defined] # noqa: F821 model = SimpleBlockModel(capa.features.extractors.ghidra.helpers.get_current_program())
insn_addr = insn.getAddress() insn_addr = insn.getAddress()
func_asv = fh.getBody() func_asv = fh.getBody()
first_addr = func_asv.getMinAddress() first_addr = func_asv.getMinAddress()
if insn_addr < first_addr.add(SECURITY_COOKIE_BYTES_DELTA): if insn_addr < first_addr.add(SECURITY_COOKIE_BYTES_DELTA):
first_bb = model.getFirstCodeBlockContaining(first_addr, monitor()) # type: ignore [name-defined] # noqa: F821 first_bb = model.getFirstCodeBlockContaining(first_addr, capa.features.extractors.ghidra.helpers.get_monitor())
if first_bb.contains(insn_addr): if first_bb.contains(insn_addr):
return True return True
last_addr = func_asv.getMaxAddress() last_addr = func_asv.getMaxAddress()
if insn_addr > last_addr.add(SECURITY_COOKIE_BYTES_DELTA * -1): if insn_addr > last_addr.add(SECURITY_COOKIE_BYTES_DELTA * -1):
last_bb = model.getFirstCodeBlockContaining(last_addr, monitor()) # type: ignore [name-defined] # noqa: F821 last_bb = model.getFirstCodeBlockContaining(last_addr, capa.features.extractors.ghidra.helpers.get_monitor())
if last_bb.contains(insn_addr): if last_bb.contains(insn_addr):
return True return True

View File

@@ -96,14 +96,7 @@ class VMRayAnalysis:
% (self.submission_name, self.submission_type) % (self.submission_name, self.submission_type)
) )
if self.submission_static is not None: if self.submission_static is None:
if self.submission_static.pe is None and self.submission_static.elf is None:
# we only support static analysis for PE and ELF files for now
raise UnsupportedFormatError(
"archive does not contain a supported file format (submission_name: %s, submission_type: %s)"
% (self.submission_name, self.submission_type)
)
else:
# VMRay may not record static analysis for certain file types, e.g. MSI, but we'd still like to match dynamic # VMRay may not record static analysis for certain file types, e.g. MSI, but we'd still like to match dynamic
# execution so we continue without and accept that the results may be incomplete # execution so we continue without and accept that the results may be incomplete
logger.warning( logger.warning(

View File

@@ -4,104 +4,14 @@
# capa + Ghidra # capa + Ghidra
[capa](https://github.com/mandiant/capa) is the FLARE teams open-source tool that detects capabilities in executable files. [Ghidra](https://github.com/NationalSecurityAgency/ghidra) is an open-source software reverse engineering framework created and maintained by the National Security Agency Research Directorate. capa + Ghidra brings capas detection capabilities directly to Ghidras user interface helping speed up your reverse engineering tasks by identifying what parts of a program suggest interesting behavior, such as setting a registry value. You can execute the included Python 3 scripts [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) or [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) to run capas analysis and view the results in Ghidra. You may be asking yourself, “Python 3 scripts in Ghidra?”. You read that correctly. This integration is written entirely in Python 3 and relies on [Ghidrathon]( https://github.com/mandiant/ghidrathon), an open source Ghidra extension that adds Python 3 scripting to Ghidra. [capa](https://github.com/mandiant/capa) is the FLARE teams open-source tool that detects capabilities in executable files. [Ghidra](https://github.com/NationalSecurityAgency/ghidra) is an open-source software reverse engineering framework. capa + Ghidra brings capas detection capabilities to Ghidra using [PyGhidra](https://github.com/NationalSecurityAgency/ghidra/tree/master/Ghidra/Features/PyGhidra).
Check out our capa + Ghidra blog posts: ## Prerequisites
* [Riding Dragons: capa Harnesses Ghidra](https://www.mandiant.com/resources/blog/capa-harnesses-ghidra)
## UI Integration - Ghidra >= 12.0 must be installed and available to PyGhidra (e.g. set `GHIDRA_INSTALL_DIR` environment variable)
[capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) renders capa results in Ghidra's UI to help you quickly navigate them. This includes adding matched functions to Ghidras Symbol Tree and Bookmarks windows and adding comments to functions that indicate matched capabilities and features. You can execute this script using Ghidras Script Manager window.
### Symbol Tree Window
Matched functions are added to Ghidra's Symbol Tree window under a custom namespace that maps to the capabilities' [capa namespace](https://github.com/mandiant/capa-rules/blob/master/doc/format.md#rule-namespace).
<div align="center">
<img src="https://github.com/mandiant/capa/assets/66766340/eeae33f4-99d4-42dc-a5e8-4c1b8c661492" width=300>
</div>
### Comments
Comments are added at the beginning of matched functions indicating matched capabilities and inline comments are added to functions indicating matched features. You can view these comments in Ghidras Disassembly Listing and Decompile windows.
<div align="center">
<img src="https://github.com/mandiant/capa/assets/66766340/bb2b4170-7fd4-45fc-8c7b-ff8f2e2f101b" width=1000>
</div>
### Bookmarks
Bookmarks are added to functions that matched a capability that is mapped to a MITRE ATT&CK and/or Malware Behavior Catalog (MBC) technique. You can view these bookmarks in Ghidra's Bookmarks window.
<div align="center">
<img src="https://github.com/mandiant/capa/assets/66766340/7f9a66a9-7be7-4223-91c6-4b8fc4651336" width=825>
</div>
## Text-based Integration
[capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) outputs text-based capa results that mirror the output of capas standalone tool. You can execute this script using Ghidras Script Manager and view its output in Ghidras Console window.
<div align="center">
<img src="../../doc/img/ghidra_script_mngr_output.png" width=700>
</div>
You can also execute [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using Ghidra's Headless Analyzer to view its output in a terminal window.
<div align="center">
<img src="../../doc/img/ghidra_headless_analyzer.png">
</div>
# Getting Started
## Requirements
| Tool | Version | Source |
|------------|---------|--------|
| capa | `>= 7.0.0` | https://github.com/mandiant/capa/releases |
| Ghidrathon | `>= 3.0.0` | https://github.com/mandiant/Ghidrathon/releases |
| Ghidra | `>= 10.3.2` | https://github.com/NationalSecurityAgency/ghidra/releases |
| Python | `>= 3.10.0` | https://www.python.org/downloads |
## Installation
**Note**: capa + Ghidra relies on [Ghidrathon]( https://github.com/mandiant/ghidrathon) to execute Python 3 code in Ghidra. You must first install and configure Ghidrathon using the [steps outlined in its README]( https://github.com/mandiant/ghidrathon?tab=readme-ov-file#installing-ghidrathon). Then, you must use the Python 3 interpreter that you configured with Ghidrathon to complete the following steps:
1. Install capa and its dependencies from PyPI using the following command:
```bash
$ pip install flare-capa
```
2. Download and extract the [official capa rules](https://github.com/mandiant/capa-rules/releases) that match the capa version you have installed. You can use the following command to view the version of capa you have installed:
```bash
$ pip show flare-capa
OR
$ capa --version
```
3. Copy [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) to your `ghidra_scripts` directory or manually add the parent directory of each script using Ghidras Script Manager.
## Usage ## Usage
You can execute [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using Ghidras Script Manager. [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) can also be executed using Ghidra's Headless Analyzer.
### Execution using Ghidras Script Manager
You can execute [capa_explorer.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_explorer.py) and [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using Ghidra's Script Manager as follows:
1. Navigate to `Window > Script Manager`
2. Expand the `Python 3 > capa` category
3. Double-click a script to execute it
Both scripts ask you to provide the path of your capa rules directory (see installation step 2). [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) also has you choose one of `default`, `verbose`, and `vverbose` output formats which mirror the output formats of capas standalone tool.
### Execution using Ghidras Headless Analyzer
You can execute [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) using Ghidras Headless Analyzer by invoking the `analyzeHeadless` script included with Ghidra in its `support` directory. The following arguments must be provided:
| Argument | Description |
|----|----|
|`<project_path>`| Path to Ghidra project|
| `<project_name>`| Name of Ghidra Project|
| `-Process <sample_name>` OR `-Import <sample_path>`| Name of sample `<sample_name>` already imported into `<project_name>` OR absolute path of sample `<sample_path>` to import into `<project_name>`|
| `-ScriptPath <script_path>`| OPTIONAL parent directory `<script_path>` of [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py)|
| `-PostScript capa_ghidra.py`| Execute [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) after Ghidra analysis|
| `"<script_args>"`| Quoted string `"<script_args>"` containing script arguments passed to [capa_ghidra.py](https://raw.githubusercontent.com/mandiant/capa/master/capa/ghidra/capa_ghidra.py) that must specify a capa rules path and optionally the output format (`--verbose`, `--vverbose`, `--json`) you can specify `”help”` to view the scripts help message |
The following is an example of combining these arguments into a single `analyzeHeadless` script command:
```bash ```bash
$ analyzeHeadless /home/wumbo/demo demo -Import /home/wumbo/capa/tests/data/Practical\ Malware\ Analysis\ Lab\ 01-01.dll_ -PostScript capa_ghidra.py "/home/wumbo/capa/rules --verbose" $ capa -b ghidra /path/to/sample
``` ```

View File

@@ -1,385 +0,0 @@
# Run capa against loaded Ghidra database and render results in Ghidra UI
# @author Colton Gabertan (gabertan.colton@gmail.com)
# @category Python 3.capa
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
import sys
import json
import logging
import pathlib
from typing import Any
from ghidra.app.cmd.label import AddLabelCmd, CreateNamespacesCmd
from ghidra.program.model.symbol import Namespace, SourceType, SymbolType
import capa
import capa.main
import capa.rules
import capa.render.json
import capa.ghidra.helpers
import capa.capabilities.common
import capa.features.extractors.ghidra.extractor
logger = logging.getLogger("capa_explorer")
def add_bookmark(addr, txt, category="CapaExplorer"):
"""create bookmark at addr"""
currentProgram().getBookmarkManager().setBookmark(addr, "Info", category, txt) # type: ignore [name-defined] # noqa: F821
def create_namespace(namespace_str):
"""create new Ghidra namespace for each capa namespace"""
cmd = CreateNamespacesCmd(namespace_str, SourceType.USER_DEFINED)
cmd.applyTo(currentProgram()) # type: ignore [name-defined] # noqa: F821
return cmd.getNamespace()
def create_label(ghidra_addr, name, capa_namespace):
"""custom label cmd to overlay symbols under capa-generated namespaces"""
# prevent duplicate labels under the same capa-generated namespace
symbol_table = currentProgram().getSymbolTable() # type: ignore [name-defined] # noqa: F821
for sym in symbol_table.getSymbols(ghidra_addr):
if sym.getName(True) == capa_namespace.getName(True) + Namespace.DELIMITER + name:
return
# create SymbolType.LABEL at addr
# prioritize capa-generated namespace (duplicate match @ new addr), else put under global Ghidra one (new match)
cmd = AddLabelCmd(ghidra_addr, name, True, SourceType.USER_DEFINED)
cmd.applyTo(currentProgram()) # type: ignore [name-defined] # noqa: F821
# assign new match overlay label to capa-generated namespace
cmd.getSymbol().setNamespace(capa_namespace)
return
class CapaMatchData:
def __init__(
self,
namespace,
scope,
capability,
matches,
attack: list[dict[Any, Any]],
mbc: list[dict[Any, Any]],
):
self.namespace = namespace
self.scope = scope
self.capability = capability
self.matches = matches
self.attack = attack
self.mbc = mbc
def bookmark_functions(self):
"""create bookmarks for MITRE ATT&CK & MBC mappings"""
if self.attack == [] and self.mbc == []:
return
for key in self.matches.keys():
addr = toAddr(hex(key)) # type: ignore [name-defined] # noqa: F821
func = getFunctionContaining(addr) # type: ignore [name-defined] # noqa: F821
# bookmark & tag MITRE ATT&CK tactics & MBC @ function scope
if func is not None:
func_addr = func.getEntryPoint()
if self.attack != []:
for item in self.attack:
attack_txt = ""
for part in item.get("parts", {}):
attack_txt = attack_txt + part + Namespace.DELIMITER
attack_txt = attack_txt + item.get("id", {})
add_bookmark(func_addr, attack_txt, "CapaExplorer::MITRE ATT&CK")
if self.mbc != []:
for item in self.mbc:
mbc_txt = ""
for part in item.get("parts", {}):
mbc_txt = mbc_txt + part + Namespace.DELIMITER
mbc_txt = mbc_txt + item.get("id", {})
add_bookmark(func_addr, mbc_txt, "CapaExplorer::MBC")
def set_plate_comment(self, ghidra_addr):
"""set plate comments at matched functions"""
comment = getPlateComment(ghidra_addr) # type: ignore [name-defined] # noqa: F821
rule_path = self.namespace.replace(Namespace.DELIMITER, "/")
# 2 calls to avoid duplicate comments via subsequent script runs
if comment is None:
# first comment @ function
comment = rule_path + "\n"
setPlateComment(ghidra_addr, comment) # type: ignore [name-defined] # noqa: F821
elif rule_path not in comment:
comment = comment + rule_path + "\n"
setPlateComment(ghidra_addr, comment) # type: ignore [name-defined] # noqa: F821
else:
return
def set_pre_comment(self, ghidra_addr, sub_type, description):
"""set pre comments at subscoped matches of main rules"""
comment = getPreComment(ghidra_addr) # type: ignore [name-defined] # noqa: F821
if comment is None:
comment = "capa: " + sub_type + "(" + description + ")" + ' matched in "' + self.capability + '"\n'
setPreComment(ghidra_addr, comment) # type: ignore [name-defined] # noqa: F821
elif self.capability not in comment:
comment = (
comment + "capa: " + sub_type + "(" + description + ")" + ' matched in "' + self.capability + '"\n'
)
setPreComment(ghidra_addr, comment) # type: ignore [name-defined] # noqa: F821
else:
return
def label_matches(self):
"""label findings at function scopes and comment on subscope matches"""
capa_namespace = create_namespace(self.namespace)
symbol_table = currentProgram().getSymbolTable() # type: ignore [name-defined] # noqa: F821
# handle function main scope of matched rule
# these will typically contain further matches within
if self.scope == "function":
for addr in self.matches.keys():
ghidra_addr = toAddr(hex(addr)) # type: ignore [name-defined] # noqa: F821
# classify new function label under capa-generated namespace
sym = symbol_table.getPrimarySymbol(ghidra_addr)
if sym is not None:
if sym.getSymbolType() == SymbolType.FUNCTION:
create_label(ghidra_addr, sym.getName(), capa_namespace)
self.set_plate_comment(ghidra_addr)
# parse the corresponding nodes, and pre-comment subscope matched features
# under the encompassing function(s)
for sub_match in self.matches.get(addr):
for loc, node in sub_match.items():
sub_ghidra_addr = toAddr(hex(loc)) # type: ignore [name-defined] # noqa: F821
if sub_ghidra_addr == ghidra_addr:
# skip duplicates
continue
# precomment subscope matches under the function
if node != {}:
for sub_type, description in parse_node(node):
self.set_pre_comment(sub_ghidra_addr, sub_type, description)
else:
# resolve the encompassing function for the capa namespace
# of non-function scoped main matches
for addr in self.matches.keys():
ghidra_addr = toAddr(hex(addr)) # type: ignore [name-defined] # noqa: F821
# basic block / insn scoped main matches
# Ex. See "Create Process on Windows" Rule
func = getFunctionContaining(ghidra_addr) # type: ignore [name-defined] # noqa: F821
if func is not None:
func_addr = func.getEntryPoint()
create_label(func_addr, func.getName(), capa_namespace)
self.set_plate_comment(func_addr)
# create subscope match precomments
for sub_match in self.matches.get(addr):
for loc, node in sub_match.items():
sub_ghidra_addr = toAddr(hex(loc)) # type: ignore [name-defined] # noqa: F821
if node != {}:
if func is not None:
# basic block/ insn scope under resolved function
for sub_type, description in parse_node(node):
self.set_pre_comment(sub_ghidra_addr, sub_type, description)
else:
# this would be a global/file scoped main match
# try to resolve the encompassing function via the subscope match, instead
# Ex. "run as service" rule
sub_func = getFunctionContaining(sub_ghidra_addr) # type: ignore [name-defined] # noqa: F821
if sub_func is not None:
sub_func_addr = sub_func.getEntryPoint()
# place function in capa namespace & create the subscope match label in Ghidra's global namespace
create_label(sub_func_addr, sub_func.getName(), capa_namespace)
self.set_plate_comment(sub_func_addr)
for sub_type, description in parse_node(node):
self.set_pre_comment(sub_ghidra_addr, sub_type, description)
else:
# addr is in some other file section like .data
# represent this location with a label symbol under the capa namespace
# Ex. See "Reference Base64 String" rule
for sub_type, description in parse_node(node):
# in many cases, these will be ghidra-labeled data, so just add the existing
# label symbol to the capa namespace
for sym in symbol_table.getSymbols(sub_ghidra_addr):
if sym.getSymbolType() == SymbolType.LABEL:
sym.setNamespace(capa_namespace)
self.set_pre_comment(sub_ghidra_addr, sub_type, description)
def get_capabilities():
rules_dir: str = ""
try:
selected_dir = askDirectory("Choose capa rules directory", "Ok") # type: ignore [name-defined] # noqa: F821
if selected_dir:
rules_dir = selected_dir.getPath()
except RuntimeError:
# RuntimeError thrown when user selects "Cancel"
pass
if not rules_dir:
logger.info("You must choose a capa rules directory before running capa.")
return "" # return empty str to avoid handling both int and str types
rules_path: pathlib.Path = pathlib.Path(rules_dir)
logger.info("running capa using rules from %s", str(rules_path))
rules = capa.rules.get_rules([rules_path])
meta = capa.ghidra.helpers.collect_metadata([rules_path])
extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor()
capabilities = capa.capabilities.common.find_capabilities(rules, extractor, True)
if capa.capabilities.common.has_static_limitation(rules, capabilities, is_standalone=False):
popup("capa explorer encountered warnings during analysis. Please check the console output for more information.") # type: ignore [name-defined] # noqa: F821
logger.info("capa encountered warnings during analysis")
return capa.render.json.render(meta, rules, capabilities.matches)
def get_locations(match_dict):
"""recursively collect match addresses and associated nodes"""
for loc in match_dict.get("locations", {}):
# either an rva (absolute)
# or an offset into a file (file)
if loc.get("type", "") in ("absolute", "file"):
yield loc.get("value"), match_dict.get("node")
for child in match_dict.get("children", {}):
yield from get_locations(child)
def parse_node(node_data):
"""pull match descriptions and sub features by parsing node dicts"""
node = node_data.get(node_data.get("type"))
if "description" in node:
yield "description", node.get("description")
data = node.get(node.get("type"))
if isinstance(data, (str, int)):
feat_type = node.get("type")
if isinstance(data, int):
data = hex(data)
yield feat_type, data
def parse_json(capa_data):
"""Parse json produced by capa"""
for rule, capability in capa_data.get("rules", {}).items():
# structure to contain rule match address & supporting feature data
# {rule match addr:[{feature addr:{node_data}}]}
rule_matches: dict[Any, list[Any]] = {}
for i in range(len(capability.get("matches"))):
# grab rule match location
match_loc = capability.get("matches")[i][0].get("value")
if match_loc is None:
# Ex. See "Reference Base64 string"
# {'type':'no address'}
match_loc = i
rule_matches[match_loc] = []
# grab extracted feature locations & corresponding node data
# feature[0]: location
# feature[1]: node
features = capability.get("matches")[i][1]
feat_dict = {}
for feature in get_locations(features):
feat_dict[feature[0]] = feature[1]
rule_matches[match_loc].append(feat_dict)
# dict data of currently matched rule
meta = capability["meta"]
# get MITRE ATT&CK and MBC
attack = meta.get("attack")
if attack is None:
attack = []
mbc = meta.get("mbc")
if mbc is None:
mbc = []
# scope match for the rule
scope = meta["scopes"].get("static")
fmt_rule = Namespace.DELIMITER + rule.replace(" ", "-")
if "namespace" in meta:
# split into list to help define child namespaces
# this requires the correct delimiter used by Ghidra
# Ex. 'communication/named-pipe/create/create pipe' -> capa::communication::named-pipe::create::create-pipe
namespace_str = Namespace.DELIMITER.join(meta["namespace"].split("/"))
namespace = "capa" + Namespace.DELIMITER + namespace_str + fmt_rule
else:
# lib rules via the official rules repo will not contain data
# for the "namespaces" key, so format using rule itself
# Ex. 'contain loop' -> capa::lib::contain-loop
namespace = "capa" + Namespace.DELIMITER + "lib" + fmt_rule
yield CapaMatchData(namespace, scope, rule, rule_matches, attack, mbc)
def main():
logging.basicConfig(level=logging.INFO)
logging.getLogger().setLevel(logging.INFO)
if isRunningHeadless(): # type: ignore [name-defined] # noqa: F821
logger.error("unsupported Ghidra execution mode")
return capa.main.E_UNSUPPORTED_GHIDRA_EXECUTION_MODE
if not capa.ghidra.helpers.is_supported_ghidra_version():
logger.error("unsupported Ghidra version")
return capa.main.E_UNSUPPORTED_GHIDRA_VERSION
if not capa.ghidra.helpers.is_supported_file_type():
logger.error("unsupported file type")
return capa.main.E_INVALID_FILE_TYPE
if not capa.ghidra.helpers.is_supported_arch_type():
logger.error("unsupported file architecture")
return capa.main.E_INVALID_FILE_ARCH
# capa_data will always contain {'meta':..., 'rules':...}
# if the 'rules' key contains no values, then there were no matches
capa_data = json.loads(get_capabilities())
if capa_data.get("rules") is None:
logger.info("capa explorer found no matches")
popup("capa explorer found no matches.") # type: ignore [name-defined] # noqa: F821
return capa.main.E_EMPTY_REPORT
for item in parse_json(capa_data):
item.bookmark_functions()
item.label_matches()
logger.info("capa explorer analysis complete")
popup("capa explorer analysis complete.\nPlease see results in the Bookmarks Window and Namespaces section of the Symbol Tree Window.") # type: ignore [name-defined] # noqa: F821
return 0
if __name__ == "__main__":
if sys.version_info < (3, 10):
from capa.exceptions import UnsupportedRuntimeError
raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.10+")
exit_code = main()
if exit_code != 0:
popup("capa explorer encountered errors during analysis. Please check the console output for more information.") # type: ignore [name-defined] # noqa: F821
sys.exit(exit_code)

View File

@@ -1,174 +0,0 @@
# Run capa against loaded Ghidra database and render results in Ghidra Console window
# @author Mike Hunhoff (mehunhoff@google.com)
# @category Python 3.capa
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
import sys
import logging
import pathlib
import argparse
import capa
import capa.main
import capa.rules
import capa.ghidra.helpers
import capa.render.default
import capa.capabilities.common
import capa.features.extractors.ghidra.extractor
logger = logging.getLogger("capa_ghidra")
def run_headless():
parser = argparse.ArgumentParser(description="The FLARE team's open-source tool to integrate capa with Ghidra.")
parser.add_argument(
"rules",
type=str,
help="path to rule file or directory",
)
parser.add_argument(
"-v", "--verbose", action="store_true", help="enable verbose result document (no effect with --json)"
)
parser.add_argument(
"-vv", "--vverbose", action="store_true", help="enable very verbose result document (no effect with --json)"
)
parser.add_argument("-d", "--debug", action="store_true", help="enable debugging output on STDERR")
parser.add_argument("-q", "--quiet", action="store_true", help="disable all output but errors")
parser.add_argument("-j", "--json", action="store_true", help="emit JSON instead of text")
script_args = list(getScriptArgs()) # type: ignore [name-defined] # noqa: F821
if not script_args or len(script_args) > 1:
script_args = []
else:
script_args = script_args[0].split()
for idx, arg in enumerate(script_args):
if arg.lower() == "help":
script_args[idx] = "--help"
args = parser.parse_args(args=script_args)
if args.quiet:
logging.basicConfig(level=logging.WARNING)
logging.getLogger().setLevel(logging.WARNING)
elif args.debug:
logging.basicConfig(level=logging.DEBUG)
logging.getLogger().setLevel(logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)
logging.getLogger().setLevel(logging.INFO)
logger.debug("running in Ghidra headless mode")
rules_path = pathlib.Path(args.rules)
logger.debug("rule path: %s", rules_path)
rules = capa.rules.get_rules([rules_path])
meta = capa.ghidra.helpers.collect_metadata([rules_path])
extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor()
capabilities = capa.capabilities.common.find_capabilities(rules, extractor, False)
meta.analysis.feature_counts = capabilities.feature_counts
meta.analysis.library_functions = capabilities.library_functions
meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities.matches)
if capa.capabilities.common.has_static_limitation(rules, capabilities, is_standalone=True):
logger.info("capa encountered warnings during analysis")
if args.json:
print(capa.render.json.render(meta, rules, capabilities.matches)) # noqa: T201
elif args.vverbose:
print(capa.render.vverbose.render(meta, rules, capabilities.matches)) # noqa: T201
elif args.verbose:
print(capa.render.verbose.render(meta, rules, capabilities.matches)) # noqa: T201
else:
print(capa.render.default.render(meta, rules, capabilities.matches)) # noqa: T201
return 0
def run_ui():
logging.basicConfig(level=logging.INFO)
logging.getLogger().setLevel(logging.INFO)
rules_dir: str = ""
try:
selected_dir = askDirectory("Choose capa rules directory", "Ok") # type: ignore [name-defined] # noqa: F821
if selected_dir:
rules_dir = selected_dir.getPath()
except RuntimeError:
# RuntimeError thrown when user selects "Cancel"
pass
if not rules_dir:
logger.info("You must choose a capa rules directory before running capa.")
return capa.main.E_MISSING_RULES
verbose = askChoice( # type: ignore [name-defined] # noqa: F821
"capa output verbosity", "Choose capa output verbosity", ["default", "verbose", "vverbose"], "default"
)
rules_path: pathlib.Path = pathlib.Path(rules_dir)
logger.info("running capa using rules from %s", str(rules_path))
rules = capa.rules.get_rules([rules_path])
meta = capa.ghidra.helpers.collect_metadata([rules_path])
extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor()
capabilities = capa.capabilities.common.find_capabilities(rules, extractor, True)
meta.analysis.feature_counts = capabilities.feature_counts
meta.analysis.library_functions = capabilities.library_functions
meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities.matches)
if capa.capabilities.common.has_static_limitation(rules, capabilities, is_standalone=False):
logger.info("capa encountered warnings during analysis")
if verbose == "vverbose":
print(capa.render.vverbose.render(meta, rules, capabilities.matches)) # noqa: T201
elif verbose == "verbose":
print(capa.render.verbose.render(meta, rules, capabilities.matches)) # noqa: T201
else:
print(capa.render.default.render(meta, rules, capabilities.matches)) # noqa: T201
return 0
def main():
if not capa.ghidra.helpers.is_supported_ghidra_version():
return capa.main.E_UNSUPPORTED_GHIDRA_VERSION
if not capa.ghidra.helpers.is_supported_file_type():
return capa.main.E_INVALID_FILE_TYPE
if not capa.ghidra.helpers.is_supported_arch_type():
return capa.main.E_INVALID_FILE_ARCH
if isRunningHeadless(): # type: ignore [name-defined] # noqa: F821
return run_headless()
else:
return run_ui()
if __name__ == "__main__":
if sys.version_info < (3, 10):
from capa.exceptions import UnsupportedRuntimeError
raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.10+")
sys.exit(main())

View File

@@ -22,6 +22,7 @@ import capa.version
import capa.features.common import capa.features.common
import capa.features.freeze import capa.features.freeze
import capa.render.result_document as rdoc import capa.render.result_document as rdoc
import capa.features.extractors.ghidra.context as ghidra_context
import capa.features.extractors.ghidra.helpers import capa.features.extractors.ghidra.helpers
from capa.features.address import AbsoluteVirtualAddress from capa.features.address import AbsoluteVirtualAddress
@@ -31,6 +32,14 @@ logger = logging.getLogger("capa")
SUPPORTED_FILE_TYPES = ("Executable and Linking Format (ELF)", "Portable Executable (PE)", "Raw Binary") SUPPORTED_FILE_TYPES = ("Executable and Linking Format (ELF)", "Portable Executable (PE)", "Raw Binary")
def get_current_program():
return ghidra_context.get_context().program
def get_flat_api():
return ghidra_context.get_context().flat_api
class GHIDRAIO: class GHIDRAIO:
""" """
An object that acts as a file-like object, An object that acts as a file-like object,
@@ -48,7 +57,12 @@ class GHIDRAIO:
self.offset = offset self.offset = offset
def read(self, size): def read(self, size):
logger.debug("reading 0x%x bytes at 0x%x (ea: 0x%x)", size, self.offset, currentProgram().getImageBase().add(self.offset).getOffset()) # type: ignore [name-defined] # noqa: F821 logger.debug(
"reading 0x%x bytes at 0x%x (ea: 0x%x)",
size,
self.offset,
get_current_program().getImageBase().add(self.offset).getOffset(),
)
if size > len(self.bytes_) - self.offset: if size > len(self.bytes_) - self.offset:
logger.debug("cannot read 0x%x bytes at 0x%x (ea: BADADDR)", size, self.offset) logger.debug("cannot read 0x%x bytes at 0x%x (ea: BADADDR)", size, self.offset)
@@ -60,7 +74,7 @@ class GHIDRAIO:
return return
def get_bytes(self): def get_bytes(self):
file_bytes = currentProgram().getMemory().getAllFileBytes()[0] # type: ignore [name-defined] # noqa: F821 file_bytes = get_current_program().getMemory().getAllFileBytes()[0]
# getOriginalByte() allows for raw file parsing on the Ghidra side # getOriginalByte() allows for raw file parsing on the Ghidra side
# other functions will fail as Ghidra will think that it's reading uninitialized memory # other functions will fail as Ghidra will think that it's reading uninitialized memory
@@ -70,21 +84,32 @@ class GHIDRAIO:
def is_supported_ghidra_version(): def is_supported_ghidra_version():
version = float(getGhidraVersion()[:4]) # type: ignore [name-defined] # noqa: F821 import ghidra.framework
if version < 10.2:
warning_msg = "capa does not support this Ghidra version" version = ghidra.framework.Application.getApplicationVersion()
logger.warning(warning_msg) try:
logger.warning("Your Ghidra version is: %s. Supported versions are: Ghidra >= 10.2", version) # version format example: "11.1.2" or "11.4"
major, minor = map(int, version.split(".")[:2])
if major < 12:
logger.error("-" * 80)
logger.error(" Ghidra version %s is not supported.", version)
logger.error(" ")
logger.error(" capa requires Ghidra 12.0 or higher.")
logger.error("-" * 80)
return False
except ValueError:
logger.warning("could not parse Ghidra version: %s", version)
return False return False
return True return True
def is_running_headless(): def is_running_headless():
return isRunningHeadless() # type: ignore [name-defined] # noqa: F821 return True # PyGhidra is always headless in this context
def is_supported_file_type(): def is_supported_file_type():
file_info = currentProgram().getExecutableFormat() # type: ignore [name-defined] # noqa: F821 file_info = get_current_program().getExecutableFormat()
if file_info not in SUPPORTED_FILE_TYPES: if file_info not in SUPPORTED_FILE_TYPES:
logger.error("-" * 80) logger.error("-" * 80)
logger.error(" Input file does not appear to be a supported file type.") logger.error(" Input file does not appear to be a supported file type.")
@@ -99,7 +124,7 @@ def is_supported_file_type():
def is_supported_arch_type(): def is_supported_arch_type():
lang_id = str(currentProgram().getLanguageID()).lower() # type: ignore [name-defined] # noqa: F821 lang_id = str(get_current_program().getLanguageID()).lower()
if not all((lang_id.startswith("x86"), any(arch in lang_id for arch in ("32", "64")))): if not all((lang_id.startswith("x86"), any(arch in lang_id for arch in ("32", "64")))):
logger.error("-" * 80) logger.error("-" * 80)
@@ -112,18 +137,18 @@ def is_supported_arch_type():
def get_file_md5(): def get_file_md5():
return currentProgram().getExecutableMD5() # type: ignore [name-defined] # noqa: F821 return get_current_program().getExecutableMD5()
def get_file_sha256(): def get_file_sha256():
return currentProgram().getExecutableSHA256() # type: ignore [name-defined] # noqa: F821 return get_current_program().getExecutableSHA256()
def collect_metadata(rules: list[Path]): def collect_metadata(rules: list[Path]):
md5 = get_file_md5() md5 = get_file_md5()
sha256 = get_file_sha256() sha256 = get_file_sha256()
info = currentProgram().getLanguageID().toString() # type: ignore [name-defined] # noqa: F821 info = get_current_program().getLanguageID().toString()
if "x86" in info and "64" in info: if "x86" in info and "64" in info:
arch = "x86_64" arch = "x86_64"
elif "x86" in info and "32" in info: elif "x86" in info and "32" in info:
@@ -131,11 +156,11 @@ def collect_metadata(rules: list[Path]):
else: else:
arch = "unknown arch" arch = "unknown arch"
format_name: str = currentProgram().getExecutableFormat() # type: ignore [name-defined] # noqa: F821 format_name: str = get_current_program().getExecutableFormat()
if "PE" in format_name: if "PE" in format_name:
os = "windows" os = "windows"
elif "ELF" in format_name: elif "ELF" in format_name:
with contextlib.closing(capa.ghidra.helpers.GHIDRAIO()) as f: with contextlib.closing(GHIDRAIO()) as f:
os = capa.features.extractors.elf.detect_elf_os(f) os = capa.features.extractors.elf.detect_elf_os(f)
else: else:
os = "unknown os" os = "unknown os"
@@ -148,16 +173,18 @@ def collect_metadata(rules: list[Path]):
md5=md5, md5=md5,
sha1="", sha1="",
sha256=sha256, sha256=sha256,
path=currentProgram().getExecutablePath(), # type: ignore [name-defined] # noqa: F821 path=get_current_program().getExecutablePath(),
), ),
flavor=rdoc.Flavor.STATIC, flavor=rdoc.Flavor.STATIC,
analysis=rdoc.StaticAnalysis( analysis=rdoc.StaticAnalysis(
format=currentProgram().getExecutableFormat(), # type: ignore [name-defined] # noqa: F821 format=get_current_program().getExecutableFormat(),
arch=arch, arch=arch,
os=os, os=os,
extractor="ghidra", extractor="ghidra",
rules=tuple(r.resolve().absolute().as_posix() for r in rules), rules=tuple(r.resolve().absolute().as_posix() for r in rules),
base_address=capa.features.freeze.Address.from_capa(AbsoluteVirtualAddress(currentProgram().getImageBase().getOffset())), # type: ignore [name-defined] # noqa: F821 base_address=capa.features.freeze.Address.from_capa(
AbsoluteVirtualAddress(get_current_program().getImageBase().getOffset())
),
layout=rdoc.StaticLayout( layout=rdoc.StaticLayout(
functions=(), functions=(),
), ),

View File

@@ -17,7 +17,6 @@ import logging
import idaapi import idaapi
import ida_kernwin import ida_kernwin
from capa.ida.plugin.form import CapaExplorerForm
from capa.ida.plugin.icon import ICON from capa.ida.plugin.icon import ICON
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -74,6 +73,9 @@ class CapaExplorerPlugin(idaapi.plugin_t):
arg (int): bitflag. Setting LSB enables automatic analysis upon arg (int): bitflag. Setting LSB enables automatic analysis upon
loading. The other bits are currently undefined. See `form.Options`. loading. The other bits are currently undefined. See `form.Options`.
""" """
# delay import to not trigger load of Qt components when not running in idaq, i.e., in idalib
from capa.ida.plugin.form import CapaExplorerForm
if not self.form: if not self.form:
self.form = CapaExplorerForm(self.PLUGIN_NAME, arg) self.form = CapaExplorerForm(self.PLUGIN_NAME, arg)
else: else:

View File

@@ -14,9 +14,9 @@
import ida_kernwin import ida_kernwin
from PyQt5 import QtCore
from capa.ida.plugin.error import UserCancelledError from capa.ida.plugin.error import UserCancelledError
from capa.ida.plugin.qt_compat import QtCore, Signal
from capa.features.extractors.ida.extractor import IdaFeatureExtractor from capa.features.extractors.ida.extractor import IdaFeatureExtractor
from capa.features.extractors.base_extractor import FunctionHandle from capa.features.extractors.base_extractor import FunctionHandle
@@ -24,7 +24,7 @@ from capa.features.extractors.base_extractor import FunctionHandle
class CapaExplorerProgressIndicator(QtCore.QObject): class CapaExplorerProgressIndicator(QtCore.QObject):
"""implement progress signal, used during feature extraction""" """implement progress signal, used during feature extraction"""
progress = QtCore.pyqtSignal(str) progress = Signal(str)
def update(self, text): def update(self, text):
"""emit progress update """emit progress update

View File

@@ -23,7 +23,6 @@ from pathlib import Path
import idaapi import idaapi
import ida_kernwin import ida_kernwin
import ida_settings import ida_settings
from PyQt5 import QtGui, QtCore, QtWidgets
import capa.main import capa.main
import capa.rules import capa.rules
@@ -51,6 +50,7 @@ from capa.ida.plugin.hooks import CapaExplorerIdaHooks
from capa.ida.plugin.model import CapaExplorerDataModel from capa.ida.plugin.model import CapaExplorerDataModel
from capa.ida.plugin.proxy import CapaExplorerRangeProxyModel, CapaExplorerSearchProxyModel from capa.ida.plugin.proxy import CapaExplorerRangeProxyModel, CapaExplorerSearchProxyModel
from capa.ida.plugin.extractor import CapaExplorerFeatureExtractor from capa.ida.plugin.extractor import CapaExplorerFeatureExtractor
from capa.ida.plugin.qt_compat import QtGui, QtCore, QtWidgets
from capa.features.extractors.base_extractor import FunctionHandle from capa.features.extractors.base_extractor import FunctionHandle
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -1358,7 +1358,7 @@ class CapaExplorerForm(idaapi.PluginForm):
@param state: checked state @param state: checked state
""" """
if state == QtCore.Qt.Checked: if state:
self.limit_results_to_function(idaapi.get_func(idaapi.get_screen_ea())) self.limit_results_to_function(idaapi.get_func(idaapi.get_screen_ea()))
else: else:
self.range_model_proxy.reset_address_range_filter() self.range_model_proxy.reset_address_range_filter()
@@ -1367,7 +1367,7 @@ class CapaExplorerForm(idaapi.PluginForm):
def slot_checkbox_limit_features_by_ea(self, state): def slot_checkbox_limit_features_by_ea(self, state):
""" """ """ """
if state == QtCore.Qt.Checked: if state:
self.view_rulegen_features.filter_items_by_ea(idaapi.get_screen_ea()) self.view_rulegen_features.filter_items_by_ea(idaapi.get_screen_ea())
else: else:
self.view_rulegen_features.show_all_items() self.view_rulegen_features.show_all_items()

View File

@@ -0,0 +1,38 @@
{
"IDAMetadataDescriptorVersion": 1,
"plugin": {
"name": "capa",
"entryPoint": "capa_explorer.py",
"version": "9.3.1",
"idaVersions": ">=7.4",
"description": "Identify capabilities in executable files using FLARE's capa framework",
"license": "Apache-2.0",
"categories": [
"malware-analysis",
"api-scripting-and-automation",
"ui-ux-and-visualization"
],
"pythonDependencies": ["flare-capa==9.3.1"],
"urls": {
"repository": "https://github.com/mandiant/capa"
},
"authors": [
{"name": "Willi Ballenthin", "email": "wballenthin@hex-rays.com"},
{"name": "Moritz Raabe", "email": "moritzraabe@google.com"},
{"name": "Mike Hunhoff", "email": "mike.hunhoff@gmail.com"},
{"name": "Yacine Elhamer", "email": "elhamer.yacine@gmail.com"}
],
"keywords": [
"capability-detection",
"malware-analysis",
"behavior-analysis",
"reverse-engineering",
"att&ck",
"rule-engine",
"feature-extraction",
"yara-like-rules",
"static-analysis",
"dynamic-analysis"
]
}
}

View File

@@ -18,10 +18,10 @@ from typing import Iterator, Optional
import idc import idc
import idaapi import idaapi
from PyQt5 import QtCore
import capa.ida.helpers import capa.ida.helpers
from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress
from capa.ida.plugin.qt_compat import QtCore, qt_get_item_flag_tristate
def info_to_name(display): def info_to_name(display):
@@ -55,7 +55,7 @@ class CapaExplorerDataItem:
self.flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable self.flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
if self._can_check: if self._can_check:
self.flags = self.flags | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsTristate self.flags = self.flags | QtCore.Qt.ItemIsUserCheckable | qt_get_item_flag_tristate()
if self.pred: if self.pred:
self.pred.appendChild(self) self.pred.appendChild(self)

View File

@@ -18,7 +18,6 @@ from collections import deque
import idc import idc
import idaapi import idaapi
from PyQt5 import QtGui, QtCore
import capa.rules import capa.rules
import capa.ida.helpers import capa.ida.helpers
@@ -42,6 +41,7 @@ from capa.ida.plugin.item import (
CapaExplorerInstructionViewItem, CapaExplorerInstructionViewItem,
) )
from capa.features.address import Address, AbsoluteVirtualAddress from capa.features.address import Address, AbsoluteVirtualAddress
from capa.ida.plugin.qt_compat import QtGui, QtCore
# default highlight color used in IDA window # default highlight color used in IDA window
DEFAULT_HIGHLIGHT = 0xE6C700 DEFAULT_HIGHLIGHT = 0xE6C700
@@ -269,7 +269,7 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
visited.add(child_index) visited.add(child_index)
for idx in range(self.rowCount(child_index)): for idx in range(self.rowCount(child_index)):
stack.append(child_index.child(idx, 0)) stack.append(self.index(idx, 0, child_index))
def reset_ida_highlighting(self, item, checked): def reset_ida_highlighting(self, item, checked):
"""reset IDA highlight for item """reset IDA highlight for item

View File

@@ -12,10 +12,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from PyQt5 import QtCore
from PyQt5.QtCore import Qt
from capa.ida.plugin.model import CapaExplorerDataModel from capa.ida.plugin.model import CapaExplorerDataModel
from capa.ida.plugin.qt_compat import Qt, QtCore
class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel): class CapaExplorerRangeProxyModel(QtCore.QSortFilterProxyModel):

View File

@@ -0,0 +1,79 @@
# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# 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.
"""
Qt compatibility layer for capa IDA Pro plugin.
Handles PyQt5 (IDA < 9.2) vs PySide6 (IDA >= 9.2) differences.
This module provides a unified import interface for Qt modules and handles
API changes between Qt5 and Qt6.
"""
try:
# IDA 9.2+ uses PySide6
from PySide6 import QtGui, QtCore, QtWidgets
from PySide6.QtGui import QAction
QT_LIBRARY = "PySide6"
Signal = QtCore.Signal
except ImportError:
# Older IDA versions use PyQt5
try:
from PyQt5 import QtGui, QtCore, QtWidgets
from PyQt5.QtWidgets import QAction
QT_LIBRARY = "PyQt5"
Signal = QtCore.pyqtSignal
except ImportError:
raise ImportError("Neither PySide6 nor PyQt5 is available. Cannot initialize capa IDA plugin.")
Qt = QtCore.Qt
def qt_get_item_flag_tristate():
"""
Get the tristate item flag compatible with Qt5 and Qt6.
Qt5 (PyQt5): Uses Qt.ItemIsTristate
Qt6 (PySide6): Qt.ItemIsTristate was removed, uses Qt.ItemIsAutoTristate
ItemIsAutoTristate automatically manages tristate based on child checkboxes,
matching the original ItemIsTristate behavior where parent checkboxes reflect
the check state of their children.
Returns:
int: The appropriate flag value for the Qt version
Raises:
AttributeError: If the tristate flag cannot be found in the Qt library
"""
if QT_LIBRARY == "PySide6":
# Qt6: ItemIsTristate was removed, replaced with ItemIsAutoTristate
# Try different possible locations (API varies slightly across PySide6 versions)
if hasattr(Qt, "ItemIsAutoTristate"):
return Qt.ItemIsAutoTristate
elif hasattr(Qt, "ItemFlag") and hasattr(Qt.ItemFlag, "ItemIsAutoTristate"):
return Qt.ItemFlag.ItemIsAutoTristate
else:
raise AttributeError(
"Cannot find ItemIsAutoTristate in PySide6. "
+ "Your PySide6 version may be incompatible with capa. "
+ f"Available Qt attributes: {[attr for attr in dir(Qt) if 'Item' in attr]}"
)
else:
# Qt5: Use the original ItemIsTristate flag
return Qt.ItemIsTristate
__all__ = ["qt_get_item_flag_tristate", "Signal", "QAction", "QtGui", "QtCore", "QtWidgets"]

View File

@@ -18,7 +18,6 @@ from collections import Counter
import idc import idc
import idaapi import idaapi
from PyQt5 import QtGui, QtCore, QtWidgets
import capa.rules import capa.rules
import capa.engine import capa.engine
@@ -28,6 +27,7 @@ import capa.features.basicblock
from capa.ida.plugin.item import CapaExplorerFunctionItem from capa.ida.plugin.item import CapaExplorerFunctionItem
from capa.features.address import AbsoluteVirtualAddress, _NoAddress from capa.features.address import AbsoluteVirtualAddress, _NoAddress
from capa.ida.plugin.model import CapaExplorerDataModel from capa.ida.plugin.model import CapaExplorerDataModel
from capa.ida.plugin.qt_compat import QtGui, QtCore, Signal, QAction, QtWidgets
MAX_SECTION_SIZE = 750 MAX_SECTION_SIZE = 750
@@ -147,7 +147,7 @@ def calc_item_depth(o):
def build_action(o, display, data, slot): def build_action(o, display, data, slot):
""" """ """ """
action = QtWidgets.QAction(display, o) action = QAction(display, o)
action.setData(data) action.setData(data)
action.triggered.connect(lambda checked: slot(action)) action.triggered.connect(lambda checked: slot(action))
@@ -312,7 +312,7 @@ class CapaExplorerRulegenPreview(QtWidgets.QTextEdit):
class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget): class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget):
updated = QtCore.pyqtSignal() updated = Signal()
def __init__(self, preview, parent=None): def __init__(self, preview, parent=None):
""" """ """ """

View File

@@ -79,6 +79,7 @@ BACKEND_VMRAY = "vmray"
BACKEND_FREEZE = "freeze" BACKEND_FREEZE = "freeze"
BACKEND_BINEXPORT2 = "binexport2" BACKEND_BINEXPORT2 = "binexport2"
BACKEND_IDA = "ida" BACKEND_IDA = "ida"
BACKEND_GHIDRA = "ghidra"
class CorruptFile(ValueError): class CorruptFile(ValueError):
@@ -351,6 +352,69 @@ def get_extractor(
return capa.features.extractors.ida.extractor.IdaFeatureExtractor() return capa.features.extractors.ida.extractor.IdaFeatureExtractor()
elif backend == BACKEND_GHIDRA:
import pyghidra
with console.status("analyzing program...", spinner="dots"):
if not pyghidra.started():
pyghidra.start()
import capa.ghidra.helpers
if not capa.ghidra.helpers.is_supported_ghidra_version():
raise RuntimeError("unsupported Ghidra version")
import tempfile
tmpdir = tempfile.TemporaryDirectory()
project_cm = pyghidra.open_project(tmpdir.name, "CapaProject", create=True)
project = project_cm.__enter__()
try:
from ghidra.util.task import TaskMonitor
monitor = TaskMonitor.DUMMY
# Import file
loader = pyghidra.program_loader().project(project).source(str(input_path)).name(input_path.name)
with loader.load() as load_results:
load_results.save(monitor)
# Open program
program, consumer = pyghidra.consume_program(project, "/" + input_path.name)
# Analyze
pyghidra.analyze(program, monitor)
from ghidra.program.flatapi import FlatProgramAPI
flat_api = FlatProgramAPI(program)
import capa.features.extractors.ghidra.context as ghidra_context
ghidra_context.set_context(program, flat_api, monitor)
# Wrapper to handle cleanup of program (consumer) and project
class GhidraContextWrapper:
def __init__(self, project_cm, program, consumer):
self.project_cm = project_cm
self.program = program
self.consumer = consumer
def __exit__(self, exc_type, exc_val, exc_tb):
self.program.release(self.consumer)
self.project_cm.__exit__(exc_type, exc_val, exc_tb)
cm = GhidraContextWrapper(project_cm, program, consumer)
except Exception:
project_cm.__exit__(None, None, None)
tmpdir.cleanup()
raise
import capa.features.extractors.ghidra.extractor
return capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor(ctx_manager=cm, tmpdir=tmpdir)
else: else:
raise ValueError("unexpected backend: " + backend) raise ValueError("unexpected backend: " + backend)

View File

@@ -55,6 +55,7 @@ from capa.loader import (
BACKEND_VMRAY, BACKEND_VMRAY,
BACKEND_DOTNET, BACKEND_DOTNET,
BACKEND_FREEZE, BACKEND_FREEZE,
BACKEND_GHIDRA,
BACKEND_PEFILE, BACKEND_PEFILE,
BACKEND_DRAKVUF, BACKEND_DRAKVUF,
BACKEND_BINEXPORT2, BACKEND_BINEXPORT2,
@@ -298,6 +299,7 @@ def install_common_args(parser, wanted=None):
(BACKEND_BINJA, "Binary Ninja"), (BACKEND_BINJA, "Binary Ninja"),
(BACKEND_DOTNET, ".NET"), (BACKEND_DOTNET, ".NET"),
(BACKEND_BINEXPORT2, "BinExport2"), (BACKEND_BINEXPORT2, "BinExport2"),
(BACKEND_GHIDRA, "Ghidra"),
(BACKEND_FREEZE, "capa freeze"), (BACKEND_FREEZE, "capa freeze"),
(BACKEND_CAPE, "CAPE"), (BACKEND_CAPE, "CAPE"),
(BACKEND_DRAKVUF, "DRAKVUF"), (BACKEND_DRAKVUF, "DRAKVUF"),
@@ -392,6 +394,7 @@ class ShouldExitError(Exception):
"""raised when a main-related routine indicates the program should exit.""" """raised when a main-related routine indicates the program should exit."""
def __init__(self, status_code: int): def __init__(self, status_code: int):
super().__init__(status_code)
self.status_code = status_code self.status_code = status_code

View File

@@ -274,12 +274,8 @@ SUPPORTED_FEATURES[Scope.FUNCTION].update(SUPPORTED_FEATURES[Scope.BASIC_BLOCK])
class InvalidRule(ValueError): class InvalidRule(ValueError):
def __init__(self, msg):
super().__init__()
self.msg = msg
def __str__(self): def __str__(self):
return f"invalid rule: {self.msg}" return f"invalid rule: {super().__str__()}"
def __repr__(self): def __repr__(self):
return str(self) return str(self)
@@ -289,20 +285,15 @@ class InvalidRuleWithPath(InvalidRule):
def __init__(self, path, msg): def __init__(self, path, msg):
super().__init__(msg) super().__init__(msg)
self.path = path self.path = path
self.msg = msg
self.__cause__ = None self.__cause__ = None
def __str__(self): def __str__(self):
return f"invalid rule: {self.path}: {self.msg}" return f"invalid rule: {self.path}: {super(InvalidRule, self).__str__()}"
class InvalidRuleSet(ValueError): class InvalidRuleSet(ValueError):
def __init__(self, msg):
super().__init__()
self.msg = msg
def __str__(self): def __str__(self):
return f"invalid rule set: {self.msg}" return f"invalid rule set: {super().__str__()}"
def __repr__(self): def __repr__(self):
return str(self) return str(self)
@@ -1102,15 +1093,15 @@ class Rule:
@lru_cache() @lru_cache()
def _get_yaml_loader(): def _get_yaml_loader():
try: try:
# prefer to use CLoader to be fast, see #306 # prefer to use CLoader to be fast, see #306 / CSafeLoader is the same as CLoader but with safe loading
# on Linux, make sure you install libyaml-dev or similar # on Linux, make sure you install libyaml-dev or similar
# on Windows, get WHLs from pyyaml.org/pypi # on Windows, get WHLs from pyyaml.org/pypi
logger.debug("using libyaml CLoader.") logger.debug("using libyaml CSafeLoader.")
return yaml.CLoader return yaml.CSafeLoader
except Exception: except Exception:
logger.debug("unable to import libyaml CLoader, falling back to Python yaml parser.") logger.debug("unable to import libyaml CSafeLoader, falling back to Python yaml parser.")
logger.debug("this will be slower to load rules.") logger.debug("this will be slower to load rules.")
return yaml.Loader return yaml.SafeLoader
@staticmethod @staticmethod
def _get_ruamel_yaml_parser(): def _get_ruamel_yaml_parser():

View File

@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
__version__ = "9.1.0" __version__ = "9.3.1"
def get_major_version(): def get_major_version():

View File

@@ -7,6 +7,7 @@
- [ ] Review changes - [ ] Review changes
- capa https://github.com/mandiant/capa/compare/\<last-release\>...master - capa https://github.com/mandiant/capa/compare/\<last-release\>...master
- capa-rules https://github.com/mandiant/capa-rules/compare/\<last-release>\...master - capa-rules https://github.com/mandiant/capa-rules/compare/\<last-release>\...master
- [ ] Run `$ bump-my-version bump {patch/minor/major} [--allow-dirty]` to update [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py) and other version files
- [ ] Update [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md) - [ ] Update [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md)
- Do not forget to add a nice introduction thanking contributors - Do not forget to add a nice introduction thanking contributors
- Remember that we need a major release if we introduce breaking changes - Remember that we need a major release if we introduce breaking changes
@@ -36,7 +37,6 @@
- [capa <release>...master](https://github.com/mandiant/capa/compare/<release>...master) - [capa <release>...master](https://github.com/mandiant/capa/compare/<release>...master)
- [capa-rules <release>...master](https://github.com/mandiant/capa-rules/compare/<release>...master) - [capa-rules <release>...master](https://github.com/mandiant/capa-rules/compare/<release>...master)
``` ```
- [ ] Update [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py)
- [ ] Create a PR with the updated [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md) and [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py). Copy this checklist in the PR description. - [ ] Create a PR with the updated [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md) and [capa/version.py](https://github.com/mandiant/capa/blob/master/capa/version.py). Copy this checklist in the PR description.
- [ ] Update the [homepage](https://github.com/mandiant/capa/blob/master/web/public/index.html) (i.e. What's New section) - [ ] Update the [homepage](https://github.com/mandiant/capa/blob/master/web/public/index.html) (i.e. What's New section)
- [ ] After PR review, merge the PR and [create the release in GH](https://github.com/mandiant/capa/releases/new) using text from the [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md). - [ ] After PR review, merge the PR and [create the release in GH](https://github.com/mandiant/capa/releases/new) using text from the [CHANGELOG.md](https://github.com/mandiant/capa/blob/master/CHANGELOG.md).

View File

@@ -74,10 +74,12 @@ dependencies = [
# comments and context. # comments and context.
"pyyaml>=6", "pyyaml>=6",
"colorama>=0.4", "colorama>=0.4",
"ida-settings>=2", "ida-netnode>=3.0",
"ida-settings>=3.1.0",
"ruamel.yaml>=0.18", "ruamel.yaml>=0.18",
"pefile>=2023.2.7", "pefile>=2023.2.7",
"pyelftools>=0.31", "pyelftools>=0.31",
"pyghidra>=3.0.0",
"pydantic>=2", "pydantic>=2",
"rich>=13", "rich>=13",
"humanize>=4", "humanize>=4",
@@ -104,7 +106,7 @@ dependencies = [
"networkx>=3", "networkx>=3",
"dnfile>=0.15.0", "dnfile>=0.17.0",
] ]
dynamic = ["version"] dynamic = ["version"]
@@ -121,34 +123,35 @@ dev = [
# we want all developer environments to be consistent. # we want all developer environments to be consistent.
# These dependencies are not used in production environments # These dependencies are not used in production environments
# and should not conflict with other libraries/tooling. # and should not conflict with other libraries/tooling.
"pre-commit==4.1.0", "pre-commit==4.5.0",
"pytest==8.0.0", "pytest==8.0.0",
"pytest-sugar==1.0.0", "pytest-sugar==1.1.1",
"pytest-instafail==0.5.0", "pytest-instafail==0.5.0",
"flake8==7.1.1", "flake8==7.3.0",
"flake8-bugbear==24.12.12", "flake8-bugbear==25.10.21",
"flake8-encodings==0.5.1", "flake8-encodings==0.5.1",
"flake8-comprehensions==3.16.0", "flake8-comprehensions==3.17.0",
"flake8-logging-format==0.9.0", "flake8-logging-format==0.9.0",
"flake8-no-implicit-concat==0.3.5", "flake8-no-implicit-concat==0.3.5",
"flake8-print==5.0.0", "flake8-print==5.0.0",
"flake8-todos==0.3.1", "flake8-todos==0.3.1",
"flake8-simplify==0.21.0", "flake8-simplify==0.22.0",
"flake8-use-pathlib==0.3.0", "flake8-use-pathlib==0.3.0",
"flake8-copyright==0.2.4", "flake8-copyright==0.2.4",
"ruff==0.11.0", "ruff==0.14.7",
"black==25.1.0", "black==25.11.0",
"isort==6.0.0", "isort==6.0.0",
"mypy==1.15.0", "mypy==1.17.1",
"mypy-protobuf==3.6.0", "mypy-protobuf==3.6.0",
"PyGithub==2.6.0", "PyGithub==2.6.0",
"bump-my-version==1.2.4",
# type stubs for mypy # type stubs for mypy
"types-backports==0.1.3", "types-backports==0.1.3",
"types-colorama==0.4.15.11", "types-colorama==0.4.15.11",
"types-PyYAML==6.0.8", "types-PyYAML==6.0.8",
"types-psutil==7.0.0.20250218", "types-psutil==7.0.0.20250218",
"types_requests==2.32.0.20240712", "types_requests==2.32.0.20240712",
"types-protobuf==5.29.1.20241207", "types-protobuf==6.32.1.20250918",
"deptry==0.23.0" "deptry==0.23.0"
] ]
build = [ build = [
@@ -156,16 +159,18 @@ build = [
# we want all developer environments to be consistent. # we want all developer environments to be consistent.
# These dependencies are not used in production environments # These dependencies are not used in production environments
# and should not conflict with other libraries/tooling. # and should not conflict with other libraries/tooling.
"pyinstaller==6.12.0", "pyinstaller==6.16.0",
"setuptools==76.0.0", "setuptools==80.9.0",
"build==1.2.2" "build==1.3.0"
] ]
scripts = [ scripts = [
# can (optionally) be more lenient on dependencies here
# see comment on dependencies for more context
"jschema_to_python==1.2.3", "jschema_to_python==1.2.3",
"psutil==7.0.0", "psutil==7.1.2",
"stix2==3.0.1", "stix2==3.0.1",
"sarif_om==1.0.4", "sarif_om==1.0.4",
"requests==2.32.3", "requests>=2.32.4",
] ]
[tool.deptry] [tool.deptry]
@@ -197,7 +202,8 @@ known_first_party = [
"idc", "idc",
"java", "java",
"netnode", "netnode",
"PyQt5" "PyQt5",
"PySide6"
] ]
[tool.deptry.per_rule_ignores] [tool.deptry.per_rule_ignores]
@@ -205,6 +211,7 @@ known_first_party = [
DEP002 = [ DEP002 = [
"black", "black",
"build", "build",
"bump-my-version",
"deptry", "deptry",
"flake8", "flake8",
"flake8-bugbear", "flake8-bugbear",

View File

@@ -10,38 +10,40 @@ annotated-types==0.7.0
colorama==0.4.6 colorama==0.4.6
cxxfilt==0.3.0 cxxfilt==0.3.0
dncil==1.0.2 dncil==1.0.2
dnfile==0.15.0 dnfile==0.17.0
funcy==2.0 funcy==2.0
humanize==4.12.0 humanize==4.14.0
ida-netnode==3.0 ida-netnode==3.0
ida-settings==2.1.0 ida-settings==3.2.2
intervaltree==3.1.0 intervaltree==3.1.0
markdown-it-py==3.0.0 markdown-it-py==4.0.0
mdurl==0.1.2 mdurl==0.1.2
msgpack==1.0.8 msgpack==1.0.8
networkx==3.4.2 networkx==3.4.2
pefile==2024.8.26 pefile==2024.8.26
pip==25.0 pip==25.3
protobuf==6.30.1 protobuf==6.33.1
pyasn1==0.5.1 pyasn1==0.5.1
pyasn1-modules==0.3.0 pyasn1-modules==0.3.0
pycparser==2.22 pycparser==2.23
pydantic==2.10.1 pydantic==2.12.4
# pydantic pins pydantic-core, # pydantic pins pydantic-core,
# but dependabot updates these separately (which is broken) and is annoying, # but dependabot updates these separately (which is broken) and is annoying,
# so we rely on pydantic to pull in the right version of pydantic-core. # so we rely on pydantic to pull in the right version of pydantic-core.
# pydantic-core==2.23.4 # pydantic-core==2.23.4
xmltodict==0.14.2 xmltodict==1.0.2
pyelftools==0.32 pyelftools==0.32
pygments==2.19.1 pygments==2.19.1
pyghidra==3.0.0
python-flirt==0.9.2 python-flirt==0.9.2
pyyaml==6.0.2 pyyaml==6.0.2
rich==13.9.2 rich==14.2.0
ruamel-yaml==0.18.6 ruamel-yaml==0.18.6
ruamel-yaml-clib==0.2.8 ruamel-yaml-clib==0.2.14
setuptools==76.0.0 setuptools==80.9.0
six==1.17.0 six==1.17.0
sortedcontainers==2.4.0 sortedcontainers==2.4.0
viv-utils==0.8.0 viv-utils==0.8.0
vivisect==1.2.1 vivisect==1.2.1
msgspec==0.19.0 msgspec==0.19.0
bump-my-version==1.2.4

2
rules

Submodule rules updated: d64c2c91ea...6120dfb6e0

View File

@@ -175,8 +175,6 @@ def convert_rule(rule, rulename, cround, depth):
depth += 1 depth += 1
logger.info("recursion depth: %d", depth) logger.info("recursion depth: %d", depth)
global var_names
def do_statement(s_type, kid): def do_statement(s_type, kid):
yara_strings = "" yara_strings = ""
yara_condition = "" yara_condition = ""

View File

@@ -406,6 +406,7 @@ class DoesntMatchExample(Lint):
return True return True
if rule.name not in capabilities: if rule.name not in capabilities:
logger.info('rule "%s" does not match for sample %s', rule.name, example_id)
return True return True

View File

@@ -227,13 +227,33 @@ def get_vmray_extractor(path):
return VMRayExtractor.from_zipfile(path) return VMRayExtractor.from_zipfile(path)
@lru_cache(maxsize=1) GHIDRA_CACHE: dict[Path, tuple] = {}
def get_ghidra_extractor(path: Path): def get_ghidra_extractor(path: Path):
# we need to start PyGhidra before importing the extractor
# because the extractor imports Ghidra modules that are only available after PyGhidra is started
import pyghidra
if not pyghidra.started():
pyghidra.start()
import capa.features.extractors.ghidra.context
import capa.features.extractors.ghidra.extractor import capa.features.extractors.ghidra.extractor
extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor() if path in GHIDRA_CACHE:
setattr(extractor, "path", path.as_posix()) extractor, program, flat_api, monitor = GHIDRA_CACHE[path]
capa.features.extractors.ghidra.context.set_context(program, flat_api, monitor)
return extractor
# We use a larger cache size to avoid re-opening the same file multiple times
# which is very slow with Ghidra.
extractor = capa.loader.get_extractor(
path, FORMAT_AUTO, OS_AUTO, capa.loader.BACKEND_GHIDRA, [], disable_progress=True
)
ctx = capa.features.extractors.ghidra.context.get_context()
GHIDRA_CACHE[path] = (extractor, ctx.program, ctx.flat_api, ctx.monitor)
return extractor return extractor

View File

@@ -70,4 +70,4 @@ def test_standalone_binja_backend():
@pytest.mark.skipif(binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed") @pytest.mark.skipif(binja_present is False, reason="Skip binja tests if the binaryninja Python API is not installed")
def test_binja_version(): def test_binja_version():
version = binaryninja.core_version_info() version = binaryninja.core_version_info()
assert version.major == 4 and version.minor == 2 assert version.major == 5 and version.minor == 2

View File

@@ -11,95 +11,35 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import os
""" import importlib.util
Must invoke this script from within the Ghidra Runtime Environment
"""
import sys
import logging
from pathlib import Path
import pytest import pytest
import fixtures
try: import capa.features.common
sys.path.append(str(Path(__file__).parent))
import fixtures ghidra_present = importlib.util.find_spec("pyghidra") is not None and "GHIDRA_INSTALL_DIR" in os.environ
finally:
sys.path.pop()
logger = logging.getLogger("test_ghidra_features") @pytest.mark.skipif(ghidra_present is False, reason="PyGhidra not installed")
@fixtures.parametrize(
ghidra_present: bool = False "sample,scope,feature,expected",
try: [
import ghidra # noqa: F401 t
for t in fixtures.FEATURE_PRESENCE_TESTS
ghidra_present = True # this test case is specific to Vivisect and its basic blocks do not align with Ghidra's analysis
except ImportError: if t[0] != "294b8d..." or t[2] != capa.features.common.String("\r\n\x00:ht")
pass ],
indirect=["sample", "scope"],
)
def standardize_posix_str(psx_str):
"""fixture test passes the PosixPath to the test data
params: psx_str - PosixPath() to the test data
return: string that matches test-id sample name
"""
if "Practical Malware Analysis Lab" in str(psx_str):
# <PosixPath>/'Practical Malware Analysis Lab 16-01.exe_' -> 'pma16-01'
wanted_str = "pma" + str(psx_str).split("/")[-1][len("Practical Malware Analysis Lab ") : -5]
else:
# <PosixPath>/mimikatz.exe_ -> mimikatz
wanted_str = str(psx_str).split("/")[-1][:-5]
if "_" in wanted_str:
# al-khaser_x86 -> al-khaser x86
wanted_str = wanted_str.replace("_", " ")
return wanted_str
def check_input_file(wanted):
"""check that test is running on the loaded sample
params: wanted - PosixPath() passed from test arg
"""
import capa.ghidra.helpers as ghidra_helpers
found = ghidra_helpers.get_file_md5()
sample_name = standardize_posix_str(wanted)
if not found.startswith(fixtures.get_sample_md5_by_name(sample_name)):
raise RuntimeError(f"please run the tests against sample with MD5: `{found}`")
@pytest.mark.skipif(ghidra_present is False, reason="Ghidra tests must be ran within Ghidra")
@fixtures.parametrize("sample,scope,feature,expected", fixtures.FEATURE_PRESENCE_TESTS, indirect=["sample", "scope"])
def test_ghidra_features(sample, scope, feature, expected): def test_ghidra_features(sample, scope, feature, expected):
try:
check_input_file(sample)
except RuntimeError:
pytest.skip(reason="Test must be ran against sample loaded in Ghidra")
fixtures.do_test_feature_presence(fixtures.get_ghidra_extractor, sample, scope, feature, expected) fixtures.do_test_feature_presence(fixtures.get_ghidra_extractor, sample, scope, feature, expected)
@pytest.mark.skipif(ghidra_present is False, reason="Ghidra tests must be ran within Ghidra") @pytest.mark.skipif(ghidra_present is False, reason="PyGhidra not installed")
@fixtures.parametrize( @fixtures.parametrize(
"sample,scope,feature,expected", fixtures.FEATURE_COUNT_TESTS_GHIDRA, indirect=["sample", "scope"] "sample,scope,feature,expected", fixtures.FEATURE_COUNT_TESTS_GHIDRA, indirect=["sample", "scope"]
) )
def test_ghidra_feature_counts(sample, scope, feature, expected): def test_ghidra_feature_counts(sample, scope, feature, expected):
try:
check_input_file(sample)
except RuntimeError:
pytest.skip(reason="Test must be ran against sample loaded in Ghidra")
fixtures.do_test_feature_count(fixtures.get_ghidra_extractor, sample, scope, feature, expected) fixtures.do_test_feature_count(fixtures.get_ghidra_extractor, sample, scope, feature, expected)
if __name__ == "__main__":
# No support for faulthandler module in Ghidrathon, see:
# https://github.com/mandiant/Ghidrathon/issues/70
sys.exit(pytest.main(["--pyargs", "-p no:faulthandler", "test_ghidra_features"]))

View File

@@ -27,7 +27,7 @@
"eslint-plugin-vue": "^9.23.0", "eslint-plugin-vue": "^9.23.0",
"jsdom": "^24.1.0", "jsdom": "^24.1.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"vite": "^6.2.3", "vite": "^6.4.1",
"vite-plugin-singlefile": "^2.2.0", "vite-plugin-singlefile": "^2.2.0",
"vitest": "^3.0.9" "vitest": "^3.0.9"
} }
@@ -1416,6 +1416,20 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": { "node_modules/callsites": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -1646,6 +1660,21 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/eastasianwidth": { "node_modules/eastasianwidth": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -1711,6 +1740,26 @@
"url": "https://github.com/fb55/entities?sponsor=1" "url": "https://github.com/fb55/entities?sponsor=1"
} }
}, },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
@@ -1718,6 +1767,35 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.1", "version": "0.25.1",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz",
@@ -2108,13 +2186,16 @@
} }
}, },
"node_modules/form-data": { "node_modules/form-data": {
"version": "4.0.0", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"asynckit": "^0.4.0", "asynckit": "^0.4.0",
"combined-stream": "^1.0.8", "combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12" "mime-types": "^2.1.12"
}, },
"engines": { "engines": {
@@ -2141,11 +2222,61 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/glob": { "node_modules/function-bind": {
"version": "10.4.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-GwMlUF6PkPo3Gk21UxkCohOv0PLcIXVtKyLlpEI28R/cO/4eNOdmLk3CMW1wROV/WR/EsZOWAfBbBOqYvs88/w==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true, "dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"foreground-child": "^3.1.0", "foreground-child": "^3.1.0",
"jackspeak": "^3.1.2", "jackspeak": "^3.1.2",
@@ -2157,9 +2288,6 @@
"bin": { "bin": {
"glob": "dist/esm/bin.mjs" "glob": "dist/esm/bin.mjs"
}, },
"engines": {
"node": ">=16 || 14 >=14.18"
},
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
@@ -2215,6 +2343,19 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/graphemer": { "node_modules/graphemer": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -2230,6 +2371,48 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/highlight.js": { "node_modules/highlight.js": {
"version": "11.9.0", "version": "11.9.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.9.0.tgz",
@@ -2456,10 +2639,11 @@
} }
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"argparse": "^2.0.1" "argparse": "^2.0.1"
}, },
@@ -2608,6 +2792,16 @@
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
}, },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -3426,6 +3620,51 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tinyglobby/node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tinypool": { "node_modules/tinypool": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz",
@@ -3561,15 +3800,18 @@
"dev": true "dev": true
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.2.3", "version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==", "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"rollup": "^4.30.1" "rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
}, },
"bin": { "bin": {
"vite": "bin/vite.js" "vite": "bin/vite.js"
@@ -3672,6 +3914,34 @@
"vite": "^5.4.11 || ^6.0.0" "vite": "^5.4.11 || ^6.0.0"
} }
}, },
"node_modules/vite/node_modules/fdir": {
"version": "6.4.4",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz",
"integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/vitest": { "node_modules/vitest": {
"version": "3.0.9", "version": "3.0.9",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.9.tgz",

View File

@@ -33,7 +33,7 @@
"eslint-plugin-vue": "^9.23.0", "eslint-plugin-vue": "^9.23.0",
"jsdom": "^24.1.0", "jsdom": "^24.1.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"vite": "^6.2.3", "vite": "^6.4.1",
"vite-plugin-singlefile": "^2.2.0", "vite-plugin-singlefile": "^2.2.0",
"vitest": "^3.0.9" "vitest": "^3.0.9"
} }

View File

@@ -210,35 +210,31 @@
<div class="row flex-lg-row-reverse align-items-center g-5"> <div class="row flex-lg-row-reverse align-items-center g-5">
<h1>What's New</h1> <h1>What's New</h1>
<h2 class="mt-3">Rule Updates</h2>
<ul class="mt-2 ps-5">
<!-- TODO(williballenthin): add date -->
<li>
added:
<a href="./rules/change registry key timestamp/">
change registry key timestamp
</a>
</li>
<li>
added:
<a href="./rules/check mutex and terminate process on windows/">
check mutex and terminate process on Windows
</a>
</li>
<li>
added:
<a href="./rules/clear windows event logs remotely/">
clear windows event logs remotely
</a>
</li>
</ul>
<h2 class="mt-3">Tool Updates</h2> <h2 class="mt-3">Tool Updates</h2>
<h3 class="mt-2">v9.3.1 (<em>2025-11-19</em>)</h3>
<p class="mt-0">
This patch release fixes a missing import for the capa explorer plugin for IDA Pro.
</p>
<h3 class="mt-2">v9.3.0 (<em>2025-11-12</em>)</h3>
<p class="mt-0">
capa v9.3.0 comes with over 20 new and/or impoved rules.
For IDA users the capa explorer plugin is now available via the IDA Pro plugin repository and contains Qt compatibility layer for PyQt5 and PySide6 support.
Additionally a Binary Ninja bug has been fixed. Released binaries now include ARM64 binaries (Linux and macOS).
</p>
<h3 class="mt-2">v9.2.1 (<em>2025-06-06</em>)</h3>
<p class="mt-0">
This point release fixes bugs including removing an unnecessary PyInstaller warning message and enabling the standalone binary to execute on systems running older versions of glibc.
</p>
<h3 class="mt-2">v9.2.0 (<em>2025-06-03</em>)</h3>
<p class="mt-0">
This release improves a few aspects of dynamic analysis, including relaxing our validation on fields across many CAPE versions and processing additional VMRay submission file types, for example.
It also includes an updated rule pack containing new rules and rule fixes.
</p>
<h3 class="mt-2">v9.1.0 (<em>2025-03-02</em>)</h3> <h3 class="mt-2">v9.1.0 (<em>2025-03-02</em>)</h3>
<p class="mt-0"> <p class="mt-0">
This release improves a few aspects of dynamic analysis, relaxing our validation on fields across many CAPE versions, for example. This release improves a few aspects of dynamic analysis, relaxing our validation on fields across many CAPE versions, for example.