From ebb3e723534ba0d553aa1853bf36d8f915af575c Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Fri, 3 Apr 2026 19:57:20 +0000 Subject: [PATCH 01/16] lint: replace isort/flake8 with ruff --- .devcontainer/devcontainer.json | 2 +- .github/CONTRIBUTING.md | 2 +- .github/flake8.ini | 41 ------------------ .github/ruff.toml | 69 ++++++++++++++++++++++++------ .github/workflows/black-format.yml | 8 ++-- .github/workflows/tests.yml | 4 -- .justfile | 8 ---- .pre-commit-config.yaml | 42 +----------------- doc/installation.md | 10 ++--- pyproject.toml | 27 +----------- requirements.txt | 2 +- scripts/profile-memory.py | 2 +- 12 files changed, 70 insertions(+), 147 deletions(-) delete mode 100644 .github/flake8.ini diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cbecb5603f..706a816dde 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -24,7 +24,7 @@ "python.formatting.blackPath": "/usr/local/py-utils/bin/black", "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", - "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", + "python.linting.ruffPath": "/usr/local/py-utils/bin/ruff", "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ce96a6d80f..509a24ade4 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -195,7 +195,7 @@ All Python code must adhere to the style guide used by capa: 1. [PEP8](https://www.python.org/dev/peps/pep-0008/), with clarifications from 2. [Willi's style guide](https://docs.google.com/document/d/1iRpeg-w4DtibwytUyC_dDT7IGhNGBP25-nQfuBa-Fyk/edit?usp=sharing), formatted with - 3. [isort](https://pypi.org/project/isort/) (with line width 120 and ordered by line length), and formatted with + 3. [ruff](https://beta.ruff.rs/docs/) (with line length 120 and imports ordered by line length), and formatted with 4. [black](https://github.com/psf/black) (with line width 120), and formatted with 5. [dos2unix](https://linux.die.net/man/1/dos2unix) diff --git a/.github/flake8.ini b/.github/flake8.ini deleted file mode 100644 index e7ca409041..0000000000 --- a/.github/flake8.ini +++ /dev/null @@ -1,41 +0,0 @@ -[flake8] -max-line-length = 120 - -extend-ignore = - # E203: whitespace before ':' (black does this) - E203, - # F401: `foo` imported but unused (prefer ruff) - F401, - # F811 Redefinition of unused `foo` (prefer ruff) - F811, - # E501 line too long (prefer black) - E501, - # E701 multiple statements on one line (colon) (prefer black, see https://github.com/psf/black/issues/4173) - E701, - # B010 Do not call setattr with a constant attribute value - B010, - # G200 Logging statement uses exception in arguments - G200, - # SIM102 Use a single if-statement instead of nested if-statements - # doesn't provide a space for commenting or logical separation of conditions - SIM102, - # SIM114 Use logical or and a single body - # makes logic trees too complex - SIM114, - # SIM117 Use 'with Foo, Bar:' instead of multiple with statements - # makes lines too long - SIM117 - -per-file-ignores = - # T201 print found. - # - # scripts are meant to print output - scripts/*: T201 - # capa.exe is meant to print output - capa/main.py: T201 - # utility used to find the Binary Ninja API via invoking python.exe - capa/features/extractors/binja/find_binja_api.py: T201 - -copyright-check = True -copyright-min-file-size = 1 -copyright-regexp = Copyright \d{4} Google LLC diff --git a/.github/ruff.toml b/.github/ruff.toml index c3a1de6d99..0038c2929b 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -1,17 +1,3 @@ -# Enable the pycodestyle (`E`) and Pyflakes (`F`) rules by default. -# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or -# McCabe complexity (`C901`) by default. -lint.select = ["E", "F"] - -# Allow autofix for all enabled rules (when `--fix`) is provided. -lint.fixable = ["ALL"] -lint.unfixable = [] - -# E402 module level import not at top of file -# E722 do not use bare 'except' -# E501 line too long -lint.ignore = ["E402", "E722", "E501"] - line-length = 120 exclude = [ @@ -41,3 +27,58 @@ exclude = [ "*_pb2.py", "*_pb2.pyi" ] + +# Enable pycodestyle (`E`), Pyflakes (`F`), isort (`I`), Bugbear (`B`), +# Comprehensions (`C4`), Implicit String Concat (`ISC`), Print (`T20`), +# Simplify (`SIM`), and Copyright (`CPY`) for strict parity. +lint.select = [ + "E", + "F", + "I", + "B", + "C4", + "ISC", + "T20", + "SIM", + "CPY" +] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +lint.fixable = ["ALL"] +lint.unfixable = [] + +# Map existing flake8 ignores to maintain strict formatting parity +lint.ignore = [ + "E402", + "E722", + "E501", + "E203", + "E701", + "B010", + "SIM102", + "SIM114", + "SIM117", + "B905", + "SIM300", + "ISC003", + "SIM103", + "SIM108", + "I001", + "SIM118", + "SIM401", + "SIM115", + "B904" +] + +[lint.per-file-ignores] +# T201 print found schemas for scripts and entrypoints +"scripts/*" = ["T201"] +"capa/main.py" = ["T201"] +"capa/features/extractors/binja/find_binja_api.py" = ["T201"] + +[lint.flake8-copyright] +notice-rgx = "Copyright \\d{4} Google LLC" +min-file-size = 1 + +[lint.isort] +length-sort = true diff --git a/.github/workflows/black-format.yml b/.github/workflows/black-format.yml index c8cf204219..1ad7daf2bc 100644 --- a/.github/workflows/black-format.yml +++ b/.github/workflows/black-format.yml @@ -35,8 +35,10 @@ jobs: pip install -r requirements.txt pip install -e .[dev,scripts] - - name: Run isort - run: pre-commit run isort --all-files + - name: Run ruff/continue + # ruff returns non-zero error code after formatting, which is what we expect + continue-on-error: true + run: pre-commit run ruff --all-files - name: Run black/continue # black returns non-zero error code after formatting, which is what we expect @@ -58,5 +60,5 @@ jobs: git config user.name "${GITHUB_ACTOR}" git config user.email "${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com" git add -A - git commit -m "style: auto-format with black and isort" + git commit -m "style: auto-format with ruff and black" git push diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2b157abe8c..a0c748f9cc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,12 +52,8 @@ jobs: pip install -e .[dev,scripts] - name: Lint with ruff run: pre-commit run ruff - - name: Lint with isort - run: pre-commit run isort --show-diff-on-failure - name: Lint with black run: pre-commit run black --show-diff-on-failure - - name: Lint with flake8 - run: pre-commit run flake8 --hook-stage manual - name: Check types with mypy run: pre-commit run mypy --hook-stage manual - name: Check imports against dependencies diff --git a/.justfile b/.justfile index 91953dd1f9..80218745b0 100644 --- a/.justfile +++ b/.justfile @@ -1,15 +1,9 @@ -@isort: - pre-commit run isort --show-diff-on-failure --all-files - @black: pre-commit run black --show-diff-on-failure --all-files @ruff: pre-commit run ruff --all-files -@flake8: - pre-commit run flake8 --hook-stage manual --all-files - @mypy: pre-commit run mypy --hook-stage manual --all-files @@ -17,9 +11,7 @@ pre-commit run deptry --hook-stage manual --all-files @lint: - -just isort -just black -just ruff - -just flake8 -just mypy -just deptry diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0cf0bddac..b26fb0f2e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,38 +9,16 @@ # run all linters liks: # # ❯ pre-commit run --all-files -# isort....................................................................Passed # black....................................................................Passed # ruff.....................................................................Passed -# flake8...................................................................Passed # mypy.....................................................................Passed # # run a single linter like: # -# ❯ pre-commit run --all-files isort +# ❯ pre-commit run --all-files ruff # isort....................................................................Passed repos: -- repo: local - hooks: - - id: isort - name: isort - stages: [pre-commit, pre-push, manual] - language: system - entry: isort - args: - - "--length-sort" - - "--profile" - - "black" - - "--line-length=120" - - "--skip-glob" - - "*_pb2.py" - - "capa/" - - "scripts/" - - "tests/" - - "web/rules/scripts/" - always_run: true - pass_filenames: false - repo: local hooks: @@ -78,24 +56,6 @@ repos: always_run: true pass_filenames: false -- repo: local - hooks: - - id: flake8 - name: flake8 - stages: [pre-push, manual] - language: system - entry: flake8 - args: - - "--config" - - ".github/flake8.ini" - - "--extend-exclude" - - "capa/render/proto/capa_pb2.py,capa/features/extractors/binexport2/binexport2_pb2.py" - - "capa/" - - "scripts/" - - "tests/" - - "web/rules/scripts/" - always_run: true - pass_filenames: false - repo: local hooks: diff --git a/doc/installation.md b/doc/installation.md index e3645b50d7..fc8ef9c3af 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -99,9 +99,7 @@ Please install these dependencies before install capa (from source or from PyPI) We use the following tools to ensure consistent code style and formatting: - [black](https://github.com/psf/black) code formatter - - [isort](https://pypi.org/project/isort/) code formatter - - [ruff](https://beta.ruff.rs/docs/) code linter - - [flake8](https://flake8.pycqa.org/en/latest/) code linter + - [ruff](https://beta.ruff.rs/docs/) code linter and formatter - [mypy](https://mypy-lang.org/) type checking - [capafmt](https://github.com/mandiant/capa/blob/master/scripts/capafmt.py) rule formatter @@ -115,17 +113,15 @@ We use [pre-commit](https://pre-commit.com/) so that its trivial to run the same Run all linters like: ❯ pre-commit run --hook-stage=manual --all-files - isort....................................................................Passed black....................................................................Passed ruff.....................................................................Passed - flake8...................................................................Passed mypy.....................................................................Passed pytest (fast)............................................................Passed Or run a single linter like: - ❯ pre-commit run --all-files --hook-stage=manual isort - isort....................................................................Passed + ❯ pre-commit run --all-files --hook-stage=manual ruff + ruff.....................................................................Passed Importantly, you can configure pre-commit to run automatically before every commit by running: diff --git a/pyproject.toml b/pyproject.toml index 3041fa5530..e8d932910e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,20 +133,8 @@ dev = [ "pytest==9.0.2", "pytest-sugar==1.1.1", "pytest-instafail==0.5.0", - "flake8==7.3.0", - "flake8-bugbear==25.11.29", - "flake8-encodings==0.5.1", - "flake8-comprehensions==3.17.0", - "flake8-logging-format==0.9.0", - "flake8-no-implicit-concat==0.3.5", - "flake8-print==5.0.0", - "flake8-todos==0.3.1", - "flake8-simplify==0.30.0", - "flake8-use-pathlib==0.3.0", - "flake8-copyright==0.2.4", "ruff==0.15.0", "black==26.3.0", - "isort==8.0.0", "mypy==1.19.1", "mypy-protobuf==5.0.0", "PyGithub==2.9.0", @@ -166,7 +154,7 @@ build = [ # These dependencies are not used in production environments # and should not conflict with other libraries/tooling. "pyinstaller==6.19.0", - "setuptools==80.10.1", + "setuptools==82.0.1", "build==1.4.0" ] scripts = [ @@ -222,18 +210,7 @@ DEP002 = [ "build", "bump-my-version", "deptry", - "flake8", - "flake8-bugbear", - "flake8-comprehensions", - "flake8-copyright", - "flake8-encodings", - "flake8-logging-format", - "flake8-no-implicit-concat", - "flake8-print", - "flake8-simplify", - "flake8-todos", - "flake8-use-pathlib", - "isort", + "mypy", "mypy-protobuf", "pre-commit", diff --git a/requirements.txt b/requirements.txt index 2ee2115a82..fefe32c825 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,7 @@ pyyaml==6.0.2 rich==14.3.2 ruamel-yaml==0.19.1 ruamel-yaml-clib==0.2.14 -setuptools==80.10.1 +setuptools==82.0.1 six==1.17.0 sortedcontainers==2.4.0 viv-utils==0.8.0 diff --git a/scripts/profile-memory.py b/scripts/profile-memory.py index 85515699d1..3ab8afeeb8 100644 --- a/scripts/profile-memory.py +++ b/scripts/profile-memory.py @@ -47,7 +47,7 @@ def display_top(snapshot, key_type="lineno", limit=10): def main(): - # import within main to keep isort happy + # import within main to keep ruff happy # while also invoking tracemalloc.start() immediately upon start. import io import os From 4204e4d37c79e467bc03b219ad929463f930c8ab Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Fri, 3 Apr 2026 20:01:51 +0000 Subject: [PATCH 02/16] update ruff links --- .github/CONTRIBUTING.md | 2 +- doc/installation.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 509a24ade4..dde68a38d7 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -195,7 +195,7 @@ All Python code must adhere to the style guide used by capa: 1. [PEP8](https://www.python.org/dev/peps/pep-0008/), with clarifications from 2. [Willi's style guide](https://docs.google.com/document/d/1iRpeg-w4DtibwytUyC_dDT7IGhNGBP25-nQfuBa-Fyk/edit?usp=sharing), formatted with - 3. [ruff](https://beta.ruff.rs/docs/) (with line length 120 and imports ordered by line length), and formatted with + 3. [ruff](https://docs.astral.sh/ruff/) (with line length 120 and imports ordered by line length), and formatted with 4. [black](https://github.com/psf/black) (with line width 120), and formatted with 5. [dos2unix](https://linux.die.net/man/1/dos2unix) diff --git a/doc/installation.md b/doc/installation.md index fc8ef9c3af..c7d7717a9b 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -99,7 +99,7 @@ Please install these dependencies before install capa (from source or from PyPI) We use the following tools to ensure consistent code style and formatting: - [black](https://github.com/psf/black) code formatter - - [ruff](https://beta.ruff.rs/docs/) code linter and formatter + - [ruff](https://docs.astral.sh/ruff/) code linter and formatter - [mypy](https://mypy-lang.org/) type checking - [capafmt](https://github.com/mandiant/capa/blob/master/scripts/capafmt.py) rule formatter From 416a116751ed43c33344c37d9ac82bcd96fdf825 Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Fri, 3 Apr 2026 20:04:19 +0000 Subject: [PATCH 03/16] remove stale isort reference --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b26fb0f2e2..e9ae69e4b7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ # run a single linter like: # # ❯ pre-commit run --all-files ruff -# isort....................................................................Passed +# ruff.....................................................................Passed repos: From 01d3d17a08f9faff9b0c38a53e33008431aedf04 Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Fri, 3 Apr 2026 20:09:40 +0000 Subject: [PATCH 04/16] update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1cc6d2365..26c84999ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### capa Explorer IDA Pro plugin ### Development +- replace isort/flake8 with ruff @mike-hunhoff #2992 ### Raw diffs - [capa v9.4.0...master](https://github.com/mandiant/capa/compare/v9.4.0...master) From b9a9dd57f1a5527f9a47d9728436cf5593226b50 Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Fri, 3 Apr 2026 21:11:17 +0000 Subject: [PATCH 05/16] address review --- .github/ruff.toml | 84 ++++++++++++------- .pre-commit-config.yaml | 1 + .../extractors/binexport2/arch/intel/insn.py | 4 +- capa/helpers.py | 4 +- scripts/show-features.py | 2 +- scripts/show-unused-features.py | 2 +- tests/fixtures.py | 2 +- web/rules/scripts/build_rules.py | 2 +- 8 files changed, 61 insertions(+), 40 deletions(-) diff --git a/.github/ruff.toml b/.github/ruff.toml index 0038c2929b..20765f419d 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -1,4 +1,5 @@ line-length = 120 +preview = true # Required to enable pre-release copyright header checks (CPY001) exclude = [ # Exclude a variety of commonly ignored directories. @@ -28,46 +29,63 @@ exclude = [ "*_pb2.pyi" ] -# Enable pycodestyle (`E`), Pyflakes (`F`), isort (`I`), Bugbear (`B`), -# Comprehensions (`C4`), Implicit String Concat (`ISC`), Print (`T20`), -# Simplify (`SIM`), and Copyright (`CPY`) for strict parity. lint.select = [ - "E", - "F", - "I", - "B", - "C4", - "ISC", - "T20", - "SIM", - "CPY" + "E", # pycodestyle (base style rules) + "F", # Pyflakes (logical/syntax errors) + "I", # isort (import sorting) + "B", # flake8-bugbear (common bugs/design problems) + "C4", # flake8-comprehensions (simplify list/dict comprehensions) + "ISC", # flake8-implicit-str-concat (detect accidental multi-line string issues) + "T20", # flake8-print (prevent leftover print/pprint statements) + "SIM", # flake8-simplify (code simplification upgrades) + "CPY", # flake8-copyright (header requirement enforcement) + "G", # flake8-logging-format (logging statement validation) + "TD", # flake8-todos (TODO formatting requirements) + "PTH", # flake8-use-pathlib (migration from os.path to Pathlib) + "UP" # pyupgrade (modern Python syntax upgrades) ] # Allow autofix for all enabled rules (when `--fix`) is provided. lint.fixable = ["ALL"] lint.unfixable = [] -# Map existing flake8 ignores to maintain strict formatting parity +# Map existing flake8 ignores to maintain strict parity lint.ignore = [ - "E402", - "E722", - "E501", - "E203", - "E701", - "B010", - "SIM102", - "SIM114", - "SIM117", - "B905", - "SIM300", - "ISC003", - "SIM103", - "SIM108", - "I001", - "SIM118", - "SIM401", - "SIM115", - "B904" + # Legacy flake8 ignores + "E402", # Module level import not at top of file + "E722", # Do not use bare except + "E501", # Line too long + "E203", # Whitespace before ':' + "E701", # Multiple statements on one line + "B010", # Do not call setattr with a constant attribute value + "SIM102", # Use a single if statement instead of nested if statements + "SIM114", # Combine if branches using logical or operator + + # Newly surfaced Ruff strictness ignores + "B905", # zip() without an explicit strict= parameter + "UP032", # Use f-string instead of format call + "UP031", # Use format specifiers instead of percent format + "SIM300", # Yoda condition detected (constant before variable) + "SIM108", # Use ternary operator instead of if-else block + "ISC003", # Explicitly concatenated string should be implicitly concatenated + "UP009", # UTF-8 encoding declaration is unnecessary + "UP035", # Deprecated typing alias usage + "UP006", # Use type instead of Type for type annotation + "SIM115", # Use a context manager for opening files + "SIM118", # Use key not in dict instead of key not in dict.keys() + "UP024", # Replace aliased errors with OSError + "UP045", # Use X | None for optional type annotations + "SIM103", # Return negated condition directly + "UP007", # Use X | Y for union type annotations + "B904", # Raise exceptions within except clause using raise from + "SIM401", # Use dict.get instead of an if-else block + "UP028", # Replace yield over for loop with yield from + "UP037", # Remove quotes from type annotation + "UP036", # Outdated python version block for sys.version_info + "F401", # Module imported but unused + "C409", # Unnecessary list comprehension passed to tuple() + "E226", # Missing whitespace around arithmetic operator + "C419" # Unnecessary list comprehension ] [lint.per-file-ignores] @@ -75,6 +93,8 @@ lint.ignore = [ "scripts/*" = ["T201"] "capa/main.py" = ["T201"] "capa/features/extractors/binja/find_binja_api.py" = ["T201"] +"tests/conftest.py" = ["I001"] # Suppress import sorting to preserve explicit legacy fixture loading order +"*_pb2.py" = ["ALL"] # Completely disable all formatting for auto-generated protocol buffer files [lint.flake8-copyright] notice-rgx = "Copyright \\d{4} Google LLC" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9ae69e4b7..017a869e86 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,6 +53,7 @@ repos: - "scripts/" - "tests/" - "web/rules/scripts/" + exclude: '.*_pb2\.py$' always_run: true pass_filenames: false diff --git a/capa/features/extractors/binexport2/arch/intel/insn.py b/capa/features/extractors/binexport2/arch/intel/insn.py index ed0f186343..36fd8cc6dd 100644 --- a/capa/features/extractors/binexport2/arch/intel/insn.py +++ b/capa/features/extractors/binexport2/arch/intel/insn.py @@ -170,12 +170,12 @@ def is_security_cookie( basic_block_index: int = bbi.basic_block_index bb: BinExport2.BasicBlock = be2.basic_block[basic_block_index] if flow_graph.entry_basic_block_index == basic_block_index: - first_addr: int = min((idx.insn_address_by_index[ir.begin_index] for ir in bb.instruction_index)) + first_addr: int = min(idx.insn_address_by_index[ir.begin_index] for ir in bb.instruction_index) if instruction_address < first_addr + SECURITY_COOKIE_BYTES_DELTA: return True # or insn falls at the end before return in a terminal basic block. if basic_block_index not in (e.source_basic_block_index for e in flow_graph.edge): - last_addr: int = max((idx.insn_address_by_index[ir.end_index - 1] for ir in bb.instruction_index)) + last_addr: int = max(idx.insn_address_by_index[ir.end_index - 1] for ir in bb.instruction_index) if instruction_address > last_addr - SECURITY_COOKIE_BYTES_DELTA: return True return False diff --git a/capa/helpers.py b/capa/helpers.py index 27c757dcc6..503d4ca6a2 100644 --- a/capa/helpers.py +++ b/capa/helpers.py @@ -390,7 +390,7 @@ def is_cache_newer_than_rule_code(cache_dir: Path) -> bool: return False latest_cache_file = max(cache_files, key=os.path.getmtime) - cache_timestamp = os.path.getmtime(latest_cache_file) + cache_timestamp = Path(latest_cache_file).stat().st_mtime # these are the relevant rules code files that could conflict with using an outdated cache # delayed import due to circular dependencies @@ -398,7 +398,7 @@ def is_cache_newer_than_rule_code(cache_dir: Path) -> bool: import capa.rules.cache latest_rule_code_file = max([Path(capa.rules.__file__), Path(capa.rules.cache.__file__)], key=os.path.getmtime) - rule_code_timestamp = os.path.getmtime(latest_rule_code_file) + rule_code_timestamp = Path(latest_rule_code_file).stat().st_mtime if rule_code_timestamp > cache_timestamp: diff --git a/scripts/show-features.py b/scripts/show-features.py index 0004f6c79e..8858f72d35 100644 --- a/scripts/show-features.py +++ b/scripts/show-features.py @@ -95,7 +95,7 @@ def format_address(addr: capa.features.address.Address) -> str: - return v.format_address(capa.features.freeze.Address.from_capa((addr))) + return v.format_address(capa.features.freeze.Address.from_capa(addr)) def main(argv=None): diff --git a/scripts/show-unused-features.py b/scripts/show-unused-features.py index 966f56d854..45277ccbc5 100644 --- a/scripts/show-unused-features.py +++ b/scripts/show-unused-features.py @@ -40,7 +40,7 @@ def format_address(addr: capa.features.address.Address) -> str: - return v.format_address(capa.features.freeze.Address.from_capa((addr))) + return v.format_address(capa.features.freeze.Address.from_capa(addr)) def get_rules_feature_set(rules: capa.rules.RuleSet) -> set[Feature]: diff --git a/tests/fixtures.py b/tests/fixtures.py index 6f15d03655..2b8c29180c 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -345,7 +345,7 @@ def extract_global_features(extractor): return features -@lru_cache() +@lru_cache def extract_file_features(extractor): features = collections.defaultdict(set) for feature, va in extractor.extract_file_features(): diff --git a/web/rules/scripts/build_rules.py b/web/rules/scripts/build_rules.py index e1c5093686..8aa3961a55 100644 --- a/web/rules/scripts/build_rules.py +++ b/web/rules/scripts/build_rules.py @@ -127,7 +127,7 @@ def render_rule(timestamps, path: Path) -> str: return html_content -yaml_files = glob(os.path.join(input_directory, "**/*.yml"), recursive=True) +yaml_files = [str(p) for p in input_directory.glob("**/*.yml")] timestamps = {} for line in txt_file_path.read_text(encoding="utf-8").splitlines(): From 18ba50a22bce3f3a0aff56e9a48d1091ccfdd6e5 Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Fri, 3 Apr 2026 21:27:48 +0000 Subject: [PATCH 06/16] remove unused imports --- .github/ruff.toml | 2 -- capa/capabilities/dynamic.py | 1 - capa/features/extractors/binexport2/arch/intel/insn.py | 1 - capa/features/extractors/binexport2/extractor.py | 1 - capa/features/extractors/binexport2/file.py | 1 - capa/features/extractors/binexport2/helpers.py | 1 - capa/features/extractors/binja/extractor.py | 1 - capa/features/extractors/binja/file.py | 2 -- capa/features/extractors/dnfile/extractor.py | 1 - capa/features/extractors/ghidra/file.py | 1 - capa/features/extractors/ida/extractor.py | 1 - capa/features/extractors/ida/file.py | 1 - capa/features/extractors/pefile.py | 3 --- capa/features/extractors/viv/file.py | 2 -- capa/features/extractors/vmray/extractor.py | 1 - capa/features/freeze/__init__.py | 6 ------ capa/ghidra/helpers.py | 2 -- capa/ghidra/plugin/capa_explorer.py | 1 - capa/ida/plugin/form.py | 3 --- capa/ida/plugin/view.py | 2 -- capa/loader.py | 1 - capa/main.py | 9 --------- capa/render/default.py | 1 - capa/rules/__init__.py | 1 - capa/rules/cache.py | 1 - scripts/bulk-process.py | 3 --- scripts/cache-ruleset.py | 3 --- scripts/capa-as-library.py | 2 -- scripts/capa2yara.py | 3 --- scripts/detect-binexport2-capabilities.py | 7 ------- scripts/detect_duplicate_features.py | 1 - scripts/match-function-id.py | 5 ----- scripts/profile-time.py | 6 ------ scripts/proto-to-results.py | 2 -- scripts/show-capabilities-by-function.py | 5 ----- scripts/show-features.py | 5 ----- scripts/show-unused-features.py | 2 -- tests/fixtures.py | 1 - tests/test_binexport_features.py | 1 - tests/test_binja_features.py | 2 -- tests/test_cape_features.py | 2 -- tests/test_drakvuf_features.py | 1 - tests/test_dynamic_span_of_calls_scope.py | 1 - tests/test_freeze_dynamic.py | 4 ---- tests/test_freeze_static.py | 2 -- tests/test_main.py | 2 -- tests/test_optimizer.py | 2 -- tests/test_proto.py | 2 -- tests/test_render.py | 1 - tests/test_rules.py | 1 - tests/test_vmray_features.py | 1 - web/rules/scripts/build_rules.py | 2 -- 52 files changed, 116 deletions(-) diff --git a/.github/ruff.toml b/.github/ruff.toml index 20765f419d..7cf1a92c5a 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -81,8 +81,6 @@ lint.ignore = [ "SIM401", # Use dict.get instead of an if-else block "UP028", # Replace yield over for loop with yield from "UP037", # Remove quotes from type annotation - "UP036", # Outdated python version block for sys.version_info - "F401", # Module imported but unused "C409", # Unnecessary list comprehension passed to tuple() "E226", # Missing whitespace around arithmetic operator "C419" # Unnecessary list comprehension diff --git a/capa/capabilities/dynamic.py b/capa/capabilities/dynamic.py index 3c98a16caa..e9f687883a 100644 --- a/capa/capabilities/dynamic.py +++ b/capa/capabilities/dynamic.py @@ -18,7 +18,6 @@ import collections from dataclasses import dataclass -import capa.perf import capa.engine import capa.helpers import capa.features.freeze as frz diff --git a/capa/features/extractors/binexport2/arch/intel/insn.py b/capa/features/extractors/binexport2/arch/intel/insn.py index 36fd8cc6dd..74fd9c9cba 100644 --- a/capa/features/extractors/binexport2/arch/intel/insn.py +++ b/capa/features/extractors/binexport2/arch/intel/insn.py @@ -15,7 +15,6 @@ import logging from typing import Iterator -import capa.features.extractors.strings import capa.features.extractors.binexport2.helpers from capa.features.insn import MAX_STRUCTURE_SIZE, Number, Offset, OperandNumber, OperandOffset from capa.features.common import Feature, Characteristic diff --git a/capa/features/extractors/binexport2/extractor.py b/capa/features/extractors/binexport2/extractor.py index 49b8fad70e..29ec5ea828 100644 --- a/capa/features/extractors/binexport2/extractor.py +++ b/capa/features/extractors/binexport2/extractor.py @@ -15,7 +15,6 @@ import logging from typing import Iterator -import capa.features.extractors.elf import capa.features.extractors.common import capa.features.extractors.binexport2.file import capa.features.extractors.binexport2.insn diff --git a/capa/features/extractors/binexport2/file.py b/capa/features/extractors/binexport2/file.py index 6b2aa8a73f..7d2e96c518 100644 --- a/capa/features/extractors/binexport2/file.py +++ b/capa/features/extractors/binexport2/file.py @@ -19,7 +19,6 @@ import pefile from elftools.elf.elffile import ELFFile -import capa.features.common import capa.features.extractors.common import capa.features.extractors.pefile import capa.features.extractors.elffile diff --git a/capa/features/extractors/binexport2/helpers.py b/capa/features/extractors/binexport2/helpers.py index c359ecb146..cfba8c486d 100644 --- a/capa/features/extractors/binexport2/helpers.py +++ b/capa/features/extractors/binexport2/helpers.py @@ -18,7 +18,6 @@ from dataclasses import dataclass import capa.features.extractors.helpers -import capa.features.extractors.binexport2.helpers from capa.features.common import ARCH_I386, ARCH_AMD64, ARCH_AARCH64 from capa.features.extractors.binexport2.binexport2_pb2 import BinExport2 diff --git a/capa/features/extractors/binja/extractor.py b/capa/features/extractors/binja/extractor.py index 100526f119..3101be66d3 100644 --- a/capa/features/extractors/binja/extractor.py +++ b/capa/features/extractors/binja/extractor.py @@ -16,7 +16,6 @@ import binaryninja as binja -import capa.features.extractors.elf import capa.features.extractors.binja.file import capa.features.extractors.binja.insn import capa.features.extractors.binja.global_ diff --git a/capa/features/extractors/binja/file.py b/capa/features/extractors/binja/file.py index 64d67cf6fa..c4e9b73caf 100644 --- a/capa/features/extractors/binja/file.py +++ b/capa/features/extractors/binja/file.py @@ -16,9 +16,7 @@ from binaryninja import Segment, BinaryView, SymbolType, SymbolBinding -import capa.features.extractors.common import capa.features.extractors.helpers -import capa.features.extractors.strings from capa.features.file import Export, Import, Section, FunctionName from capa.features.common import ( FORMAT_PE, diff --git a/capa/features/extractors/dnfile/extractor.py b/capa/features/extractors/dnfile/extractor.py index 84fc886974..caf6b31925 100644 --- a/capa/features/extractors/dnfile/extractor.py +++ b/capa/features/extractors/dnfile/extractor.py @@ -21,7 +21,6 @@ import dnfile from dncil.cil.opcode import OpCodes -import capa.features.extractors import capa.features.extractors.dotnetfile import capa.features.extractors.dnfile.file import capa.features.extractors.dnfile.insn diff --git a/capa/features/extractors/ghidra/file.py b/capa/features/extractors/ghidra/file.py index 42656e4776..c06a90462a 100644 --- a/capa/features/extractors/ghidra/file.py +++ b/capa/features/extractors/ghidra/file.py @@ -18,7 +18,6 @@ from ghidra.program.model.symbol import SourceType, SymbolType -import capa.features.extractors.common import capa.features.extractors.helpers import capa.features.extractors.strings import capa.features.extractors.ghidra.helpers diff --git a/capa/features/extractors/ida/extractor.py b/capa/features/extractors/ida/extractor.py index b139f2f38f..463f4876fd 100644 --- a/capa/features/extractors/ida/extractor.py +++ b/capa/features/extractors/ida/extractor.py @@ -17,7 +17,6 @@ import idaapi import capa.ida.helpers -import capa.features.extractors.elf import capa.features.extractors.ida.file import capa.features.extractors.ida.insn import capa.features.extractors.ida.global_ diff --git a/capa/features/extractors/ida/file.py b/capa/features/extractors/ida/file.py index a47f1524c5..0cc0f8bffc 100644 --- a/capa/features/extractors/ida/file.py +++ b/capa/features/extractors/ida/file.py @@ -22,7 +22,6 @@ import ida_entry import capa.ida.helpers -import capa.features.extractors.common import capa.features.extractors.helpers import capa.features.extractors.strings import capa.features.extractors.ida.helpers diff --git a/capa/features/extractors/pefile.py b/capa/features/extractors/pefile.py index 8b76e1d8ab..0070dabb84 100644 --- a/capa/features/extractors/pefile.py +++ b/capa/features/extractors/pefile.py @@ -18,11 +18,8 @@ import pefile -import capa.features.common -import capa.features.extractors import capa.features.extractors.common import capa.features.extractors.helpers -import capa.features.extractors.strings from capa.features.file import Export, Import, Section from capa.features.common import OS, ARCH_I386, FORMAT_PE, ARCH_AMD64, OS_WINDOWS, Arch, Format, Characteristic from capa.features.address import NO_ADDRESS, FileOffsetAddress, AbsoluteVirtualAddress diff --git a/capa/features/extractors/viv/file.py b/capa/features/extractors/viv/file.py index 5f9df620a5..ae72c44c4f 100644 --- a/capa/features/extractors/viv/file.py +++ b/capa/features/extractors/viv/file.py @@ -19,10 +19,8 @@ import viv_utils import viv_utils.flirt -import capa.features.insn import capa.features.extractors.common import capa.features.extractors.helpers -import capa.features.extractors.strings from capa.features.file import Export, Import, Section, FunctionName from capa.features.common import Feature, Characteristic from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress diff --git a/capa/features/extractors/vmray/extractor.py b/capa/features/extractors/vmray/extractor.py index 27eeed4819..471a62651c 100644 --- a/capa/features/extractors/vmray/extractor.py +++ b/capa/features/extractors/vmray/extractor.py @@ -16,7 +16,6 @@ from typing import Iterator from pathlib import Path -import capa.helpers import capa.features.extractors.vmray.call import capa.features.extractors.vmray.file import capa.features.extractors.vmray.global_ diff --git a/capa/features/freeze/__init__.py b/capa/features/freeze/__init__.py index 3afd0290ff..e25550c2ca 100644 --- a/capa/features/freeze/__init__.py +++ b/capa/features/freeze/__init__.py @@ -24,13 +24,7 @@ from pydantic import Field, BaseModel, ConfigDict -import capa.helpers -import capa.version -import capa.features.file -import capa.features.insn -import capa.features.common import capa.features.address -import capa.features.basicblock import capa.features.extractors.null as null from capa.helpers import assert_never from capa.features.freeze.features import Feature, feature_from_capa diff --git a/capa/ghidra/helpers.py b/capa/ghidra/helpers.py index 61b275d1f0..3c722ed365 100644 --- a/capa/ghidra/helpers.py +++ b/capa/ghidra/helpers.py @@ -17,9 +17,7 @@ import contextlib from pathlib import Path -import capa import capa.version -import capa.features.common import capa.features.freeze import capa.render.result_document as rdoc import capa.features.extractors.ghidra.context as ghidra_context diff --git a/capa/ghidra/plugin/capa_explorer.py b/capa/ghidra/plugin/capa_explorer.py index 974a81508e..86f1832526 100644 --- a/capa/ghidra/plugin/capa_explorer.py +++ b/capa/ghidra/plugin/capa_explorer.py @@ -30,7 +30,6 @@ from ghidra.program.flatapi import FlatProgramAPI from ghidra.program.model.symbol import Namespace, SourceType, SymbolType -import capa import capa.main import capa.rules import capa.version diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index 800453bbfa..3e0ef75677 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -26,14 +26,11 @@ import capa.main import capa.rules -import capa.engine import capa.version import capa.ida.helpers -import capa.render.json import capa.features.common import capa.capabilities.common import capa.render.result_document -import capa.features.extractors.ida.extractor from capa.rules import Rule from capa.engine import FeatureSet from capa.rules.cache import compute_ruleset_cache_identifier diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index a442f4d1e9..a108b7286d 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -19,8 +19,6 @@ import idc import idaapi -import capa.rules -import capa.engine import capa.ida.helpers import capa.features.common import capa.features.basicblock diff --git a/capa/loader.py b/capa/loader.py index 939680ab7d..55813b998c 100644 --- a/capa/loader.py +++ b/capa/loader.py @@ -26,7 +26,6 @@ import capa.version import capa.features.common import capa.features.freeze as frz -import capa.features.extractors import capa.render.result_document as rdoc import capa.features.extractors.common from capa.rules import RuleSet diff --git a/capa/main.py b/capa/main.py index 368d3ecd15..6c3203c64d 100644 --- a/capa/main.py +++ b/capa/main.py @@ -30,9 +30,7 @@ from rich.logging import RichHandler from elftools.common.exceptions import ELFError -import capa.perf import capa.rules -import capa.engine import capa.loader import capa.helpers import capa.version @@ -40,12 +38,9 @@ import capa.rules.cache import capa.render.default import capa.render.verbose -import capa.features.common import capa.render.vverbose -import capa.features.extractors import capa.render.result_document import capa.render.result_document as rdoc -import capa.features.extractors.common from capa.rules import RuleSet from capa.loader import ( BACKEND_IDA, @@ -77,7 +72,6 @@ UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError, - UnsupportedRuntimeError, ) from capa.features.common import ( OS_AUTO, @@ -938,9 +932,6 @@ def apply_extractor_filters(extractor: FeatureExtractor, extractor_filters: Filt def main(argv: Optional[list[str]] = None): - if sys.version_info < (3, 10): - raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.10+") - if argv is None: argv = sys.argv[1:] diff --git a/capa/render/default.py b/capa/render/default.py index ff273b5161..2e7fba3cc8 100644 --- a/capa/render/default.py +++ b/capa/render/default.py @@ -17,7 +17,6 @@ import collections import urllib.parse -import rich import rich.table import rich.console from rich.console import Console diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index 1eca880423..57b2fdd9b6 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -43,7 +43,6 @@ import capa.perf import capa.engine as ceng -import capa.features import capa.optimizer import capa.features.com import capa.features.file diff --git a/capa/rules/cache.py b/capa/rules/cache.py index f23e61212f..7bae9f6cd6 100644 --- a/capa/rules/cache.py +++ b/capa/rules/cache.py @@ -23,7 +23,6 @@ from dataclasses import dataclass import capa.rules -import capa.helpers import capa.version logger = logging.getLogger(__name__) diff --git a/scripts/bulk-process.py b/scripts/bulk-process.py index 12d64fede5..24b064cd5d 100644 --- a/scripts/bulk-process.py +++ b/scripts/bulk-process.py @@ -70,11 +70,8 @@ import multiprocessing.pool from pathlib import Path -import capa import capa.main -import capa.rules import capa.loader -import capa.render.json import capa.capabilities.common import capa.render.result_document as rd diff --git a/scripts/cache-ruleset.py b/scripts/cache-ruleset.py index a6fe907472..0ed48d47ce 100644 --- a/scripts/cache-ruleset.py +++ b/scripts/cache-ruleset.py @@ -29,10 +29,7 @@ import capa.main import capa.rules -import capa.engine -import capa.helpers import capa.rules.cache -import capa.features.insn logger = logging.getLogger("cache-ruleset") diff --git a/scripts/capa-as-library.py b/scripts/capa-as-library.py index 1d935a8e80..cbf2fb50f5 100644 --- a/scripts/capa-as-library.py +++ b/scripts/capa-as-library.py @@ -21,9 +21,7 @@ import capa.main import capa.rules -import capa.engine import capa.loader -import capa.features import capa.render.json import capa.render.utils as rutils import capa.render.default diff --git a/scripts/capa2yara.py b/scripts/capa2yara.py index 0315d60e11..47d21763ac 100644 --- a/scripts/capa2yara.py +++ b/scripts/capa2yara.py @@ -46,9 +46,6 @@ import capa.main import capa.rules -import capa.engine -import capa.features -import capa.features.insn logger = logging.getLogger("capa2yara") diff --git a/scripts/detect-binexport2-capabilities.py b/scripts/detect-binexport2-capabilities.py index 2261578611..8ad2914807 100644 --- a/scripts/detect-binexport2-capabilities.py +++ b/scripts/detect-binexport2-capabilities.py @@ -38,15 +38,8 @@ import argparse import capa.main -import capa.rules -import capa.engine import capa.loader -import capa.helpers -import capa.features -import capa.exceptions import capa.render.proto -import capa.render.verbose -import capa.features.freeze import capa.capabilities.common import capa.render.result_document as rd from capa.loader import FORMAT_BINEXPORT2, BACKEND_BINEXPORT2 diff --git a/scripts/detect_duplicate_features.py b/scripts/detect_duplicate_features.py index 6d41eab1e7..3ecc12dbd0 100644 --- a/scripts/detect_duplicate_features.py +++ b/scripts/detect_duplicate_features.py @@ -17,7 +17,6 @@ import argparse from pathlib import Path -import capa.main import capa.rules from capa.features.common import Feature diff --git a/scripts/match-function-id.py b/scripts/match-function-id.py index dfcfb72aa4..9c679a6038 100644 --- a/scripts/match-function-id.py +++ b/scripts/match-function-id.py @@ -64,11 +64,6 @@ import viv_utils.flirt import capa.main -import capa.rules -import capa.engine -import capa.helpers -import capa.features -import capa.features.freeze from capa.loader import BACKEND_VIV logger = logging.getLogger("capa.match-function-id") diff --git a/scripts/profile-time.py b/scripts/profile-time.py index 095604506c..5159d0375f 100644 --- a/scripts/profile-time.py +++ b/scripts/profile-time.py @@ -57,13 +57,7 @@ import capa.main import capa.perf -import capa.rules -import capa.engine -import capa.loader import capa.helpers -import capa.features -import capa.features.common -import capa.features.freeze import capa.capabilities.common logger = logging.getLogger("capa.profile") diff --git a/scripts/proto-to-results.py b/scripts/proto-to-results.py index 4888fdb3ce..27b869940c 100644 --- a/scripts/proto-to-results.py +++ b/scripts/proto-to-results.py @@ -44,10 +44,8 @@ from pathlib import Path import capa.main -import capa.render.json import capa.render.proto import capa.render.proto.capa_pb2 -import capa.render.result_document logger = logging.getLogger("capa.proto-to-results-json") diff --git a/scripts/show-capabilities-by-function.py b/scripts/show-capabilities-by-function.py index 1c69302e3f..0cace2a23d 100644 --- a/scripts/show-capabilities-by-function.py +++ b/scripts/show-capabilities-by-function.py @@ -64,13 +64,8 @@ import capa.main import capa.rules -import capa.engine -import capa.helpers -import capa.features -import capa.exceptions import capa.render.utils as rutils import capa.render.verbose -import capa.features.freeze import capa.capabilities.common import capa.render.result_document as rd from capa.features.freeze import Address diff --git a/scripts/show-features.py b/scripts/show-features.py index 8858f72d35..a5ad8f2c4f 100644 --- a/scripts/show-features.py +++ b/scripts/show-features.py @@ -76,12 +76,7 @@ import argparse import capa.main -import capa.rules -import capa.engine -import capa.loader import capa.helpers -import capa.features -import capa.exceptions import capa.render.verbose as v import capa.features.freeze import capa.features.address diff --git a/scripts/show-unused-features.py b/scripts/show-unused-features.py index 45277ccbc5..25086a2612 100644 --- a/scripts/show-unused-features.py +++ b/scripts/show-unused-features.py @@ -25,8 +25,6 @@ import capa.main import capa.rules import capa.helpers -import capa.features -import capa.exceptions import capa.render.verbose as v import capa.features.common import capa.features.freeze diff --git a/tests/fixtures.py b/tests/fixtures.py index 2b8c29180c..ecd8de8c85 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -307,7 +307,6 @@ def get_ghidra_extractor(path: Path): pyghidra.start() import capa.features.extractors.ghidra.context - import capa.features.extractors.ghidra.extractor if path in GHIDRA_CACHE: extractor, program, flat_api, monitor = GHIDRA_CACHE[path] diff --git a/tests/test_binexport_features.py b/tests/test_binexport_features.py index 2695230c0e..e41a1cc983 100644 --- a/tests/test_binexport_features.py +++ b/tests/test_binexport_features.py @@ -20,7 +20,6 @@ import capa.features.file import capa.features.insn import capa.features.common -import capa.features.basicblock from capa.features.common import ( OS, OS_LINUX, diff --git a/tests/test_binja_features.py b/tests/test_binja_features.py index c97a8d26c6..3f00d6a759 100644 --- a/tests/test_binja_features.py +++ b/tests/test_binja_features.py @@ -19,8 +19,6 @@ import fixtures import capa.main -import capa.features.file -import capa.features.common logger = logging.getLogger(__file__) diff --git a/tests/test_cape_features.py b/tests/test_cape_features.py index d3cc4bdd67..a93b9bc7fc 100644 --- a/tests/test_cape_features.py +++ b/tests/test_cape_features.py @@ -15,11 +15,9 @@ import fixtures -import capa.main import capa.features.file import capa.features.insn import capa.features.common -import capa.features.basicblock DYNAMIC_CAPE_FEATURE_PRESENCE_TESTS = sorted( [ diff --git a/tests/test_drakvuf_features.py b/tests/test_drakvuf_features.py index 9f5115ae37..8fc89d63ad 100644 --- a/tests/test_drakvuf_features.py +++ b/tests/test_drakvuf_features.py @@ -15,7 +15,6 @@ import fixtures -import capa.main import capa.features.file import capa.features.insn import capa.features.common diff --git a/tests/test_dynamic_span_of_calls_scope.py b/tests/test_dynamic_span_of_calls_scope.py index d024e5ba59..24782ea8ce 100644 --- a/tests/test_dynamic_span_of_calls_scope.py +++ b/tests/test_dynamic_span_of_calls_scope.py @@ -38,7 +38,6 @@ import pytest import fixtures -import capa.main import capa.rules import capa.capabilities.dynamic from capa.features.extractors.base_extractor import ThreadFilter, DynamicFeatureExtractor diff --git a/tests/test_freeze_dynamic.py b/tests/test_freeze_dynamic.py index 74129f4fae..d6595df8dd 100644 --- a/tests/test_freeze_dynamic.py +++ b/tests/test_freeze_dynamic.py @@ -19,14 +19,10 @@ import capa.main import capa.rules -import capa.helpers -import capa.features.file import capa.features.insn import capa.features.common import capa.features.freeze -import capa.features.basicblock import capa.features.extractors.null -import capa.features.extractors.base_extractor from capa.features.address import Address, AbsoluteVirtualAddress from capa.features.extractors.base_extractor import ( SampleHashes, diff --git a/tests/test_freeze_static.py b/tests/test_freeze_static.py index 9c171f2e95..b4c5754024 100644 --- a/tests/test_freeze_static.py +++ b/tests/test_freeze_static.py @@ -19,14 +19,12 @@ import capa.main import capa.rules -import capa.helpers import capa.features.file import capa.features.insn import capa.features.common import capa.features.freeze import capa.features.basicblock import capa.features.extractors.null -import capa.features.extractors.base_extractor from capa.features.address import Address, AbsoluteVirtualAddress from capa.features.extractors.base_extractor import BBHandle, SampleHashes, FunctionHandle diff --git a/tests/test_main.py b/tests/test_main.py index 72ebd76293..bab650a065 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -22,8 +22,6 @@ import capa.main import capa.rules -import capa.engine -import capa.features def test_main(z9324d_extractor): diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index 1c3d559826..e868d8e7b1 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -16,9 +16,7 @@ import textwrap import capa.rules -import capa.engine import capa.optimizer -import capa.features.common from capa.engine import Or, And from capa.features.insn import Mnemonic from capa.features.common import Arch, Substring diff --git a/tests/test_proto.py b/tests/test_proto.py index b0dc106040..e3592555cb 100644 --- a/tests/test_proto.py +++ b/tests/test_proto.py @@ -18,9 +18,7 @@ import pytest import capa.rules -import capa.render import capa.render.proto -import capa.render.utils import capa.features.freeze import capa.features.address import capa.render.proto.capa_pb2 as capa_pb2 diff --git a/tests/test_render.py b/tests/test_render.py index 5fb3b3b20e..fcae0dcf4f 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -27,7 +27,6 @@ import capa.features.freeze import capa.render.vverbose import capa.features.address -import capa.features.basicblock import capa.render.result_document import capa.render.result_document as rd import capa.features.freeze.features diff --git a/tests/test_rules.py b/tests/test_rules.py index 60d3e4e7cb..aaa66a5d1e 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -19,7 +19,6 @@ import capa.rules import capa.engine -import capa.rules.cache import capa.features.common import capa.features.address from capa.engine import Or diff --git a/tests/test_vmray_features.py b/tests/test_vmray_features.py index bf035b83a9..7638f2e423 100644 --- a/tests/test_vmray_features.py +++ b/tests/test_vmray_features.py @@ -15,7 +15,6 @@ import fixtures -import capa.main import capa.features.file import capa.features.insn import capa.features.common diff --git a/web/rules/scripts/build_rules.py b/web/rules/scripts/build_rules.py index 8aa3961a55..084631f153 100644 --- a/web/rules/scripts/build_rules.py +++ b/web/rules/scripts/build_rules.py @@ -13,11 +13,9 @@ # limitations under the License. -import os import sys import logging import urllib.parse -from glob import glob from pathlib import Path import pygments From 2b31d8225a246921cf3eb611f5922c18be5d6ca6 Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Fri, 3 Apr 2026 21:31:04 +0000 Subject: [PATCH 07/16] remove unnecessary list comprehension --- .github/ruff.toml | 1 - capa/engine.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ruff.toml b/.github/ruff.toml index 7cf1a92c5a..8443aba988 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -83,7 +83,6 @@ lint.ignore = [ "UP037", # Remove quotes from type annotation "C409", # Unnecessary list comprehension passed to tuple() "E226", # Missing whitespace around arithmetic operator - "C419" # Unnecessary list comprehension ] [lint.per-file-ignores] diff --git a/capa/engine.py b/capa/engine.py index fc1919fa30..1296198cae 100644 --- a/capa/engine.py +++ b/capa/engine.py @@ -216,7 +216,7 @@ def evaluate(self, features: FeatureSet, short_circuit=True): # because we've overridden `__bool__` above. # # we can't use `if child is True` because the instance is not True. - success = sum([1 for child in results if bool(child) is True]) >= self.count + success = sum(1 for child in results if bool(child) is True) >= self.count return Result(success, self, results) From 6347bdf4da2dbea3d27995ac8c88fc7c434918fe Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Fri, 3 Apr 2026 21:33:47 +0000 Subject: [PATCH 08/16] remove quotes from type annotation --- .github/ruff.toml | 1 - capa/features/extractors/ghidra/function.py | 6 +++--- capa/ida/plugin/item.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/ruff.toml b/.github/ruff.toml index 8443aba988..0761c3ca63 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -80,7 +80,6 @@ lint.ignore = [ "B904", # Raise exceptions within except clause using raise from "SIM401", # Use dict.get instead of an if-else block "UP028", # Replace yield over for loop with yield from - "UP037", # Remove quotes from type annotation "C409", # Unnecessary list comprehension passed to tuple() "E226", # Missing whitespace around arithmetic operator ] diff --git a/capa/features/extractors/ghidra/function.py b/capa/features/extractors/ghidra/function.py index 8fa8cc71bb..a46d8b15b5 100644 --- a/capa/features/extractors/ghidra/function.py +++ b/capa/features/extractors/ghidra/function.py @@ -26,14 +26,14 @@ def extract_function_calls_to(fh: FunctionHandle): """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(): if ref.getReferenceType().isCall(): yield Characteristic("calls to"), AbsoluteVirtualAddress(ref.getFromAddress().getOffset()) def extract_function_loop(fh: FunctionHandle): - f: "ghidra.program.database.function.FunctionDB" = fh.inner + f: ghidra.program.database.function.FunctionDB = fh.inner edges = [] for block in SimpleBlockIterator( @@ -53,7 +53,7 @@ def extract_function_loop(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(capa.features.extractors.ghidra.helpers.get_monitor()): if func.getEntryPoint().getOffset() == f.getEntryPoint().getOffset(): diff --git a/capa/ida/plugin/item.py b/capa/ida/plugin/item.py index 0510d5f971..3292de1c0a 100644 --- a/capa/ida/plugin/item.py +++ b/capa/ida/plugin/item.py @@ -47,7 +47,7 @@ def __init__(self, parent: Optional["CapaExplorerDataItem"], data: list[str], ca """initialize item""" self.pred = parent self._data = data - self._children: list["CapaExplorerDataItem"] = [] + self._children: list[CapaExplorerDataItem] = [] self._checked = False self._can_check = can_check From 18e7faf6e429012cf46dbabbc7948c6bd6d8a9dc Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Fri, 3 Apr 2026 21:35:47 +0000 Subject: [PATCH 09/16] use dict.get instead of if-else block --- .github/ruff.toml | 1 - scripts/capa2sarif.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/ruff.toml b/.github/ruff.toml index 0761c3ca63..2780dad504 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -78,7 +78,6 @@ lint.ignore = [ "SIM103", # Return negated condition directly "UP007", # Use X | Y for union type annotations "B904", # Raise exceptions within except clause using raise from - "SIM401", # Use dict.get instead of an if-else block "UP028", # Replace yield over for loop with yield from "C409", # Unnecessary list comprehension passed to tuple() "E226", # Missing whitespace around arithmetic operator diff --git a/scripts/capa2sarif.py b/scripts/capa2sarif.py index b16d9f088b..348d5485fc 100644 --- a/scripts/capa2sarif.py +++ b/scripts/capa2sarif.py @@ -169,7 +169,7 @@ def _sarif_boilerplate(data_meta: dict, data_rules: dict) -> Optional[dict]: "shortDescription": {"text": data_rules[key]["meta"]["name"]}, "messageStrings": {"default": {"text": data_rules[key]["meta"]["name"]}}, "properties": { - "namespace": data_rules[key]["meta"]["namespace"] if "namespace" in data_rules[key]["meta"] else [], + "namespace": data_rules[key]["meta"].get("namespace", []), "scopes": data_rules[key]["meta"]["scopes"], "references": data_rules[key]["meta"]["references"], "lib": data_rules[key]["meta"]["lib"], From 06a3a9a0204b4c72df31d2a350f59d9040b5be00 Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Fri, 3 Apr 2026 21:38:56 +0000 Subject: [PATCH 10/16] remove unnecessary utf-8 encoding declaration --- .github/ruff.toml | 1 - capa/capabilities/common.py | 1 - capa/capabilities/dynamic.py | 1 - capa/capabilities/static.py | 1 - tests/test_capabilities.py | 1 - tests/test_main.py | 1 - tests/test_os_detection.py | 1 - 7 files changed, 7 deletions(-) diff --git a/.github/ruff.toml b/.github/ruff.toml index 2780dad504..ec4e129caf 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -68,7 +68,6 @@ lint.ignore = [ "SIM300", # Yoda condition detected (constant before variable) "SIM108", # Use ternary operator instead of if-else block "ISC003", # Explicitly concatenated string should be implicitly concatenated - "UP009", # UTF-8 encoding declaration is unnecessary "UP035", # Deprecated typing alias usage "UP006", # Use type instead of Type for type annotation "SIM115", # Use a context manager for opening files diff --git a/capa/capabilities/common.py b/capa/capabilities/common.py index 9a6e13b052..6575fe7b96 100644 --- a/capa/capabilities/common.py +++ b/capa/capabilities/common.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/capa/capabilities/dynamic.py b/capa/capabilities/dynamic.py index e9f687883a..80a04e7be8 100644 --- a/capa/capabilities/dynamic.py +++ b/capa/capabilities/dynamic.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/capa/capabilities/static.py b/capa/capabilities/static.py index 893887f77b..1047713b50 100644 --- a/capa/capabilities/static.py +++ b/capa/capabilities/static.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 9a68956822..4ee5e83cec 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_main.py b/tests/test_main.py index bab650a065..2d94def240 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2020 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/test_os_detection.py b/tests/test_os_detection.py index be4762f235..4467b8b225 100644 --- a/tests/test_os_detection.py +++ b/tests/test_os_detection.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); From d1016173e1b822fef0b1b014aa0ba71457a0e7b0 Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Mon, 6 Apr 2026 16:19:15 +0000 Subject: [PATCH 11/16] Revert "remove unused imports" This reverts commit 18ba50a22bce3f3a0aff56e9a48d1091ccfdd6e5. --- capa/capabilities/dynamic.py | 1 + capa/features/extractors/binexport2/arch/intel/insn.py | 1 + capa/features/extractors/binexport2/extractor.py | 1 + capa/features/extractors/binexport2/file.py | 1 + capa/features/extractors/binexport2/helpers.py | 1 + capa/features/extractors/binja/extractor.py | 1 + capa/features/extractors/binja/file.py | 2 ++ capa/features/extractors/dnfile/extractor.py | 1 + capa/features/extractors/ghidra/file.py | 1 + capa/features/extractors/ida/extractor.py | 1 + capa/features/extractors/ida/file.py | 1 + capa/features/extractors/pefile.py | 3 +++ capa/features/extractors/viv/file.py | 2 ++ capa/features/extractors/vmray/extractor.py | 1 + capa/features/freeze/__init__.py | 6 ++++++ capa/ghidra/helpers.py | 2 ++ capa/ghidra/plugin/capa_explorer.py | 1 + capa/ida/plugin/form.py | 3 +++ capa/ida/plugin/view.py | 2 ++ capa/loader.py | 1 + capa/main.py | 9 +++++++++ capa/render/default.py | 1 + capa/rules/__init__.py | 1 + capa/rules/cache.py | 1 + scripts/bulk-process.py | 3 +++ scripts/cache-ruleset.py | 3 +++ scripts/capa-as-library.py | 2 ++ scripts/capa2yara.py | 3 +++ scripts/detect-binexport2-capabilities.py | 7 +++++++ scripts/detect_duplicate_features.py | 1 + scripts/match-function-id.py | 5 +++++ scripts/profile-time.py | 6 ++++++ scripts/proto-to-results.py | 2 ++ scripts/show-capabilities-by-function.py | 5 +++++ scripts/show-features.py | 5 +++++ scripts/show-unused-features.py | 2 ++ tests/fixtures.py | 1 + tests/test_binexport_features.py | 1 + tests/test_binja_features.py | 2 ++ tests/test_cape_features.py | 2 ++ tests/test_drakvuf_features.py | 1 + tests/test_dynamic_span_of_calls_scope.py | 1 + tests/test_freeze_dynamic.py | 4 ++++ tests/test_freeze_static.py | 2 ++ tests/test_main.py | 2 ++ tests/test_optimizer.py | 2 ++ tests/test_proto.py | 2 ++ tests/test_render.py | 1 + tests/test_rules.py | 1 + tests/test_vmray_features.py | 1 + web/rules/scripts/build_rules.py | 2 ++ 51 files changed, 114 insertions(+) diff --git a/capa/capabilities/dynamic.py b/capa/capabilities/dynamic.py index 80a04e7be8..e7e6594a5e 100644 --- a/capa/capabilities/dynamic.py +++ b/capa/capabilities/dynamic.py @@ -17,6 +17,7 @@ import collections from dataclasses import dataclass +import capa.perf import capa.engine import capa.helpers import capa.features.freeze as frz diff --git a/capa/features/extractors/binexport2/arch/intel/insn.py b/capa/features/extractors/binexport2/arch/intel/insn.py index 74fd9c9cba..36fd8cc6dd 100644 --- a/capa/features/extractors/binexport2/arch/intel/insn.py +++ b/capa/features/extractors/binexport2/arch/intel/insn.py @@ -15,6 +15,7 @@ import logging from typing import Iterator +import capa.features.extractors.strings import capa.features.extractors.binexport2.helpers from capa.features.insn import MAX_STRUCTURE_SIZE, Number, Offset, OperandNumber, OperandOffset from capa.features.common import Feature, Characteristic diff --git a/capa/features/extractors/binexport2/extractor.py b/capa/features/extractors/binexport2/extractor.py index 29ec5ea828..49b8fad70e 100644 --- a/capa/features/extractors/binexport2/extractor.py +++ b/capa/features/extractors/binexport2/extractor.py @@ -15,6 +15,7 @@ import logging from typing import Iterator +import capa.features.extractors.elf import capa.features.extractors.common import capa.features.extractors.binexport2.file import capa.features.extractors.binexport2.insn diff --git a/capa/features/extractors/binexport2/file.py b/capa/features/extractors/binexport2/file.py index 7d2e96c518..6b2aa8a73f 100644 --- a/capa/features/extractors/binexport2/file.py +++ b/capa/features/extractors/binexport2/file.py @@ -19,6 +19,7 @@ import pefile from elftools.elf.elffile import ELFFile +import capa.features.common import capa.features.extractors.common import capa.features.extractors.pefile import capa.features.extractors.elffile diff --git a/capa/features/extractors/binexport2/helpers.py b/capa/features/extractors/binexport2/helpers.py index cfba8c486d..c359ecb146 100644 --- a/capa/features/extractors/binexport2/helpers.py +++ b/capa/features/extractors/binexport2/helpers.py @@ -18,6 +18,7 @@ from dataclasses import dataclass import capa.features.extractors.helpers +import capa.features.extractors.binexport2.helpers from capa.features.common import ARCH_I386, ARCH_AMD64, ARCH_AARCH64 from capa.features.extractors.binexport2.binexport2_pb2 import BinExport2 diff --git a/capa/features/extractors/binja/extractor.py b/capa/features/extractors/binja/extractor.py index 3101be66d3..100526f119 100644 --- a/capa/features/extractors/binja/extractor.py +++ b/capa/features/extractors/binja/extractor.py @@ -16,6 +16,7 @@ import binaryninja as binja +import capa.features.extractors.elf import capa.features.extractors.binja.file import capa.features.extractors.binja.insn import capa.features.extractors.binja.global_ diff --git a/capa/features/extractors/binja/file.py b/capa/features/extractors/binja/file.py index c4e9b73caf..64d67cf6fa 100644 --- a/capa/features/extractors/binja/file.py +++ b/capa/features/extractors/binja/file.py @@ -16,7 +16,9 @@ from binaryninja import Segment, BinaryView, SymbolType, SymbolBinding +import capa.features.extractors.common import capa.features.extractors.helpers +import capa.features.extractors.strings from capa.features.file import Export, Import, Section, FunctionName from capa.features.common import ( FORMAT_PE, diff --git a/capa/features/extractors/dnfile/extractor.py b/capa/features/extractors/dnfile/extractor.py index caf6b31925..84fc886974 100644 --- a/capa/features/extractors/dnfile/extractor.py +++ b/capa/features/extractors/dnfile/extractor.py @@ -21,6 +21,7 @@ import dnfile from dncil.cil.opcode import OpCodes +import capa.features.extractors import capa.features.extractors.dotnetfile import capa.features.extractors.dnfile.file import capa.features.extractors.dnfile.insn diff --git a/capa/features/extractors/ghidra/file.py b/capa/features/extractors/ghidra/file.py index c06a90462a..42656e4776 100644 --- a/capa/features/extractors/ghidra/file.py +++ b/capa/features/extractors/ghidra/file.py @@ -18,6 +18,7 @@ from ghidra.program.model.symbol import SourceType, SymbolType +import capa.features.extractors.common import capa.features.extractors.helpers import capa.features.extractors.strings import capa.features.extractors.ghidra.helpers diff --git a/capa/features/extractors/ida/extractor.py b/capa/features/extractors/ida/extractor.py index 463f4876fd..b139f2f38f 100644 --- a/capa/features/extractors/ida/extractor.py +++ b/capa/features/extractors/ida/extractor.py @@ -17,6 +17,7 @@ import idaapi import capa.ida.helpers +import capa.features.extractors.elf import capa.features.extractors.ida.file import capa.features.extractors.ida.insn import capa.features.extractors.ida.global_ diff --git a/capa/features/extractors/ida/file.py b/capa/features/extractors/ida/file.py index 0cc0f8bffc..a47f1524c5 100644 --- a/capa/features/extractors/ida/file.py +++ b/capa/features/extractors/ida/file.py @@ -22,6 +22,7 @@ import ida_entry import capa.ida.helpers +import capa.features.extractors.common import capa.features.extractors.helpers import capa.features.extractors.strings import capa.features.extractors.ida.helpers diff --git a/capa/features/extractors/pefile.py b/capa/features/extractors/pefile.py index 0070dabb84..8b76e1d8ab 100644 --- a/capa/features/extractors/pefile.py +++ b/capa/features/extractors/pefile.py @@ -18,8 +18,11 @@ import pefile +import capa.features.common +import capa.features.extractors import capa.features.extractors.common import capa.features.extractors.helpers +import capa.features.extractors.strings from capa.features.file import Export, Import, Section from capa.features.common import OS, ARCH_I386, FORMAT_PE, ARCH_AMD64, OS_WINDOWS, Arch, Format, Characteristic from capa.features.address import NO_ADDRESS, FileOffsetAddress, AbsoluteVirtualAddress diff --git a/capa/features/extractors/viv/file.py b/capa/features/extractors/viv/file.py index ae72c44c4f..5f9df620a5 100644 --- a/capa/features/extractors/viv/file.py +++ b/capa/features/extractors/viv/file.py @@ -19,8 +19,10 @@ import viv_utils import viv_utils.flirt +import capa.features.insn import capa.features.extractors.common import capa.features.extractors.helpers +import capa.features.extractors.strings from capa.features.file import Export, Import, Section, FunctionName from capa.features.common import Feature, Characteristic from capa.features.address import Address, FileOffsetAddress, AbsoluteVirtualAddress diff --git a/capa/features/extractors/vmray/extractor.py b/capa/features/extractors/vmray/extractor.py index 471a62651c..27eeed4819 100644 --- a/capa/features/extractors/vmray/extractor.py +++ b/capa/features/extractors/vmray/extractor.py @@ -16,6 +16,7 @@ from typing import Iterator from pathlib import Path +import capa.helpers import capa.features.extractors.vmray.call import capa.features.extractors.vmray.file import capa.features.extractors.vmray.global_ diff --git a/capa/features/freeze/__init__.py b/capa/features/freeze/__init__.py index e25550c2ca..3afd0290ff 100644 --- a/capa/features/freeze/__init__.py +++ b/capa/features/freeze/__init__.py @@ -24,7 +24,13 @@ from pydantic import Field, BaseModel, ConfigDict +import capa.helpers +import capa.version +import capa.features.file +import capa.features.insn +import capa.features.common import capa.features.address +import capa.features.basicblock import capa.features.extractors.null as null from capa.helpers import assert_never from capa.features.freeze.features import Feature, feature_from_capa diff --git a/capa/ghidra/helpers.py b/capa/ghidra/helpers.py index 3c722ed365..61b275d1f0 100644 --- a/capa/ghidra/helpers.py +++ b/capa/ghidra/helpers.py @@ -17,7 +17,9 @@ import contextlib from pathlib import Path +import capa import capa.version +import capa.features.common import capa.features.freeze import capa.render.result_document as rdoc import capa.features.extractors.ghidra.context as ghidra_context diff --git a/capa/ghidra/plugin/capa_explorer.py b/capa/ghidra/plugin/capa_explorer.py index 86f1832526..974a81508e 100644 --- a/capa/ghidra/plugin/capa_explorer.py +++ b/capa/ghidra/plugin/capa_explorer.py @@ -30,6 +30,7 @@ from ghidra.program.flatapi import FlatProgramAPI from ghidra.program.model.symbol import Namespace, SourceType, SymbolType +import capa import capa.main import capa.rules import capa.version diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index 3e0ef75677..800453bbfa 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -26,11 +26,14 @@ import capa.main import capa.rules +import capa.engine import capa.version import capa.ida.helpers +import capa.render.json import capa.features.common import capa.capabilities.common import capa.render.result_document +import capa.features.extractors.ida.extractor from capa.rules import Rule from capa.engine import FeatureSet from capa.rules.cache import compute_ruleset_cache_identifier diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index a108b7286d..a442f4d1e9 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -19,6 +19,8 @@ import idc import idaapi +import capa.rules +import capa.engine import capa.ida.helpers import capa.features.common import capa.features.basicblock diff --git a/capa/loader.py b/capa/loader.py index 55813b998c..939680ab7d 100644 --- a/capa/loader.py +++ b/capa/loader.py @@ -26,6 +26,7 @@ import capa.version import capa.features.common import capa.features.freeze as frz +import capa.features.extractors import capa.render.result_document as rdoc import capa.features.extractors.common from capa.rules import RuleSet diff --git a/capa/main.py b/capa/main.py index 6c3203c64d..368d3ecd15 100644 --- a/capa/main.py +++ b/capa/main.py @@ -30,7 +30,9 @@ from rich.logging import RichHandler from elftools.common.exceptions import ELFError +import capa.perf import capa.rules +import capa.engine import capa.loader import capa.helpers import capa.version @@ -38,9 +40,12 @@ import capa.rules.cache import capa.render.default import capa.render.verbose +import capa.features.common import capa.render.vverbose +import capa.features.extractors import capa.render.result_document import capa.render.result_document as rdoc +import capa.features.extractors.common from capa.rules import RuleSet from capa.loader import ( BACKEND_IDA, @@ -72,6 +77,7 @@ UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError, + UnsupportedRuntimeError, ) from capa.features.common import ( OS_AUTO, @@ -932,6 +938,9 @@ def apply_extractor_filters(extractor: FeatureExtractor, extractor_filters: Filt def main(argv: Optional[list[str]] = None): + if sys.version_info < (3, 10): + raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.10+") + if argv is None: argv = sys.argv[1:] diff --git a/capa/render/default.py b/capa/render/default.py index 2e7fba3cc8..ff273b5161 100644 --- a/capa/render/default.py +++ b/capa/render/default.py @@ -17,6 +17,7 @@ import collections import urllib.parse +import rich import rich.table import rich.console from rich.console import Console diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index 57b2fdd9b6..1eca880423 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -43,6 +43,7 @@ import capa.perf import capa.engine as ceng +import capa.features import capa.optimizer import capa.features.com import capa.features.file diff --git a/capa/rules/cache.py b/capa/rules/cache.py index 7bae9f6cd6..f23e61212f 100644 --- a/capa/rules/cache.py +++ b/capa/rules/cache.py @@ -23,6 +23,7 @@ from dataclasses import dataclass import capa.rules +import capa.helpers import capa.version logger = logging.getLogger(__name__) diff --git a/scripts/bulk-process.py b/scripts/bulk-process.py index 24b064cd5d..12d64fede5 100644 --- a/scripts/bulk-process.py +++ b/scripts/bulk-process.py @@ -70,8 +70,11 @@ import multiprocessing.pool from pathlib import Path +import capa import capa.main +import capa.rules import capa.loader +import capa.render.json import capa.capabilities.common import capa.render.result_document as rd diff --git a/scripts/cache-ruleset.py b/scripts/cache-ruleset.py index 0ed48d47ce..a6fe907472 100644 --- a/scripts/cache-ruleset.py +++ b/scripts/cache-ruleset.py @@ -29,7 +29,10 @@ import capa.main import capa.rules +import capa.engine +import capa.helpers import capa.rules.cache +import capa.features.insn logger = logging.getLogger("cache-ruleset") diff --git a/scripts/capa-as-library.py b/scripts/capa-as-library.py index cbf2fb50f5..1d935a8e80 100644 --- a/scripts/capa-as-library.py +++ b/scripts/capa-as-library.py @@ -21,7 +21,9 @@ import capa.main import capa.rules +import capa.engine import capa.loader +import capa.features import capa.render.json import capa.render.utils as rutils import capa.render.default diff --git a/scripts/capa2yara.py b/scripts/capa2yara.py index 47d21763ac..0315d60e11 100644 --- a/scripts/capa2yara.py +++ b/scripts/capa2yara.py @@ -46,6 +46,9 @@ import capa.main import capa.rules +import capa.engine +import capa.features +import capa.features.insn logger = logging.getLogger("capa2yara") diff --git a/scripts/detect-binexport2-capabilities.py b/scripts/detect-binexport2-capabilities.py index 8ad2914807..2261578611 100644 --- a/scripts/detect-binexport2-capabilities.py +++ b/scripts/detect-binexport2-capabilities.py @@ -38,8 +38,15 @@ import argparse import capa.main +import capa.rules +import capa.engine import capa.loader +import capa.helpers +import capa.features +import capa.exceptions import capa.render.proto +import capa.render.verbose +import capa.features.freeze import capa.capabilities.common import capa.render.result_document as rd from capa.loader import FORMAT_BINEXPORT2, BACKEND_BINEXPORT2 diff --git a/scripts/detect_duplicate_features.py b/scripts/detect_duplicate_features.py index 3ecc12dbd0..6d41eab1e7 100644 --- a/scripts/detect_duplicate_features.py +++ b/scripts/detect_duplicate_features.py @@ -17,6 +17,7 @@ import argparse from pathlib import Path +import capa.main import capa.rules from capa.features.common import Feature diff --git a/scripts/match-function-id.py b/scripts/match-function-id.py index 9c679a6038..dfcfb72aa4 100644 --- a/scripts/match-function-id.py +++ b/scripts/match-function-id.py @@ -64,6 +64,11 @@ import viv_utils.flirt import capa.main +import capa.rules +import capa.engine +import capa.helpers +import capa.features +import capa.features.freeze from capa.loader import BACKEND_VIV logger = logging.getLogger("capa.match-function-id") diff --git a/scripts/profile-time.py b/scripts/profile-time.py index 5159d0375f..095604506c 100644 --- a/scripts/profile-time.py +++ b/scripts/profile-time.py @@ -57,7 +57,13 @@ import capa.main import capa.perf +import capa.rules +import capa.engine +import capa.loader import capa.helpers +import capa.features +import capa.features.common +import capa.features.freeze import capa.capabilities.common logger = logging.getLogger("capa.profile") diff --git a/scripts/proto-to-results.py b/scripts/proto-to-results.py index 27b869940c..4888fdb3ce 100644 --- a/scripts/proto-to-results.py +++ b/scripts/proto-to-results.py @@ -44,8 +44,10 @@ from pathlib import Path import capa.main +import capa.render.json import capa.render.proto import capa.render.proto.capa_pb2 +import capa.render.result_document logger = logging.getLogger("capa.proto-to-results-json") diff --git a/scripts/show-capabilities-by-function.py b/scripts/show-capabilities-by-function.py index 0cace2a23d..1c69302e3f 100644 --- a/scripts/show-capabilities-by-function.py +++ b/scripts/show-capabilities-by-function.py @@ -64,8 +64,13 @@ import capa.main import capa.rules +import capa.engine +import capa.helpers +import capa.features +import capa.exceptions import capa.render.utils as rutils import capa.render.verbose +import capa.features.freeze import capa.capabilities.common import capa.render.result_document as rd from capa.features.freeze import Address diff --git a/scripts/show-features.py b/scripts/show-features.py index a5ad8f2c4f..8858f72d35 100644 --- a/scripts/show-features.py +++ b/scripts/show-features.py @@ -76,7 +76,12 @@ import argparse import capa.main +import capa.rules +import capa.engine +import capa.loader import capa.helpers +import capa.features +import capa.exceptions import capa.render.verbose as v import capa.features.freeze import capa.features.address diff --git a/scripts/show-unused-features.py b/scripts/show-unused-features.py index 25086a2612..45277ccbc5 100644 --- a/scripts/show-unused-features.py +++ b/scripts/show-unused-features.py @@ -25,6 +25,8 @@ import capa.main import capa.rules import capa.helpers +import capa.features +import capa.exceptions import capa.render.verbose as v import capa.features.common import capa.features.freeze diff --git a/tests/fixtures.py b/tests/fixtures.py index ecd8de8c85..2b8c29180c 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -307,6 +307,7 @@ def get_ghidra_extractor(path: Path): pyghidra.start() import capa.features.extractors.ghidra.context + import capa.features.extractors.ghidra.extractor if path in GHIDRA_CACHE: extractor, program, flat_api, monitor = GHIDRA_CACHE[path] diff --git a/tests/test_binexport_features.py b/tests/test_binexport_features.py index e41a1cc983..2695230c0e 100644 --- a/tests/test_binexport_features.py +++ b/tests/test_binexport_features.py @@ -20,6 +20,7 @@ import capa.features.file import capa.features.insn import capa.features.common +import capa.features.basicblock from capa.features.common import ( OS, OS_LINUX, diff --git a/tests/test_binja_features.py b/tests/test_binja_features.py index 3f00d6a759..c97a8d26c6 100644 --- a/tests/test_binja_features.py +++ b/tests/test_binja_features.py @@ -19,6 +19,8 @@ import fixtures import capa.main +import capa.features.file +import capa.features.common logger = logging.getLogger(__file__) diff --git a/tests/test_cape_features.py b/tests/test_cape_features.py index a93b9bc7fc..d3cc4bdd67 100644 --- a/tests/test_cape_features.py +++ b/tests/test_cape_features.py @@ -15,9 +15,11 @@ import fixtures +import capa.main import capa.features.file import capa.features.insn import capa.features.common +import capa.features.basicblock DYNAMIC_CAPE_FEATURE_PRESENCE_TESTS = sorted( [ diff --git a/tests/test_drakvuf_features.py b/tests/test_drakvuf_features.py index 8fc89d63ad..9f5115ae37 100644 --- a/tests/test_drakvuf_features.py +++ b/tests/test_drakvuf_features.py @@ -15,6 +15,7 @@ import fixtures +import capa.main import capa.features.file import capa.features.insn import capa.features.common diff --git a/tests/test_dynamic_span_of_calls_scope.py b/tests/test_dynamic_span_of_calls_scope.py index 24782ea8ce..d024e5ba59 100644 --- a/tests/test_dynamic_span_of_calls_scope.py +++ b/tests/test_dynamic_span_of_calls_scope.py @@ -38,6 +38,7 @@ import pytest import fixtures +import capa.main import capa.rules import capa.capabilities.dynamic from capa.features.extractors.base_extractor import ThreadFilter, DynamicFeatureExtractor diff --git a/tests/test_freeze_dynamic.py b/tests/test_freeze_dynamic.py index d6595df8dd..74129f4fae 100644 --- a/tests/test_freeze_dynamic.py +++ b/tests/test_freeze_dynamic.py @@ -19,10 +19,14 @@ import capa.main import capa.rules +import capa.helpers +import capa.features.file import capa.features.insn import capa.features.common import capa.features.freeze +import capa.features.basicblock import capa.features.extractors.null +import capa.features.extractors.base_extractor from capa.features.address import Address, AbsoluteVirtualAddress from capa.features.extractors.base_extractor import ( SampleHashes, diff --git a/tests/test_freeze_static.py b/tests/test_freeze_static.py index b4c5754024..9c171f2e95 100644 --- a/tests/test_freeze_static.py +++ b/tests/test_freeze_static.py @@ -19,12 +19,14 @@ import capa.main import capa.rules +import capa.helpers import capa.features.file import capa.features.insn import capa.features.common import capa.features.freeze import capa.features.basicblock import capa.features.extractors.null +import capa.features.extractors.base_extractor from capa.features.address import Address, AbsoluteVirtualAddress from capa.features.extractors.base_extractor import BBHandle, SampleHashes, FunctionHandle diff --git a/tests/test_main.py b/tests/test_main.py index 2d94def240..70097988e9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -21,6 +21,8 @@ import capa.main import capa.rules +import capa.engine +import capa.features def test_main(z9324d_extractor): diff --git a/tests/test_optimizer.py b/tests/test_optimizer.py index e868d8e7b1..1c3d559826 100644 --- a/tests/test_optimizer.py +++ b/tests/test_optimizer.py @@ -16,7 +16,9 @@ import textwrap import capa.rules +import capa.engine import capa.optimizer +import capa.features.common from capa.engine import Or, And from capa.features.insn import Mnemonic from capa.features.common import Arch, Substring diff --git a/tests/test_proto.py b/tests/test_proto.py index e3592555cb..b0dc106040 100644 --- a/tests/test_proto.py +++ b/tests/test_proto.py @@ -18,7 +18,9 @@ import pytest import capa.rules +import capa.render import capa.render.proto +import capa.render.utils import capa.features.freeze import capa.features.address import capa.render.proto.capa_pb2 as capa_pb2 diff --git a/tests/test_render.py b/tests/test_render.py index fcae0dcf4f..5fb3b3b20e 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -27,6 +27,7 @@ import capa.features.freeze import capa.render.vverbose import capa.features.address +import capa.features.basicblock import capa.render.result_document import capa.render.result_document as rd import capa.features.freeze.features diff --git a/tests/test_rules.py b/tests/test_rules.py index aaa66a5d1e..60d3e4e7cb 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -19,6 +19,7 @@ import capa.rules import capa.engine +import capa.rules.cache import capa.features.common import capa.features.address from capa.engine import Or diff --git a/tests/test_vmray_features.py b/tests/test_vmray_features.py index 7638f2e423..bf035b83a9 100644 --- a/tests/test_vmray_features.py +++ b/tests/test_vmray_features.py @@ -15,6 +15,7 @@ import fixtures +import capa.main import capa.features.file import capa.features.insn import capa.features.common diff --git a/web/rules/scripts/build_rules.py b/web/rules/scripts/build_rules.py index 084631f153..8aa3961a55 100644 --- a/web/rules/scripts/build_rules.py +++ b/web/rules/scripts/build_rules.py @@ -13,9 +13,11 @@ # limitations under the License. +import os import sys import logging import urllib.parse +from glob import glob from pathlib import Path import pygments From eb69536c08e445f7a60c1a3d09aaf523c84f0039 Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Mon, 6 Apr 2026 16:21:27 +0000 Subject: [PATCH 12/16] skip check for unused imports --- .github/ruff.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ruff.toml b/.github/ruff.toml index ec4e129caf..6f54898633 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -80,6 +80,7 @@ lint.ignore = [ "UP028", # Replace yield over for loop with yield from "C409", # Unnecessary list comprehension passed to tuple() "E226", # Missing whitespace around arithmetic operator + "F401", # Unused imports ] [lint.per-file-ignores] From 8ee47bc56650fff78ef07290175f96fea69c6307 Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Mon, 6 Apr 2026 16:23:02 +0000 Subject: [PATCH 13/16] fix UP036 Version block is outdated for minimum Python version --- capa/main.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/capa/main.py b/capa/main.py index 368d3ecd15..9114735318 100644 --- a/capa/main.py +++ b/capa/main.py @@ -77,7 +77,6 @@ UnsupportedOSError, UnsupportedArchError, UnsupportedFormatError, - UnsupportedRuntimeError, ) from capa.features.common import ( OS_AUTO, @@ -938,9 +937,6 @@ def apply_extractor_filters(extractor: FeatureExtractor, extractor_filters: Filt def main(argv: Optional[list[str]] = None): - if sys.version_info < (3, 10): - raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.10+") - if argv is None: argv = sys.argv[1:] From c277657386da160b7d733b2686ff3e4c925d2c6e Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Mon, 6 Apr 2026 17:31:51 +0000 Subject: [PATCH 14/16] add TODO comment for unused imports --- .github/ruff.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/ruff.toml b/.github/ruff.toml index 6f54898633..cf6c5a96f6 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -80,6 +80,8 @@ lint.ignore = [ "UP028", # Replace yield over for loop with yield from "C409", # Unnecessary list comprehension passed to tuple() "E226", # Missing whitespace around arithmetic operator + # TODO(mike-hunhoff): address circular dependencies + # https://github.com/mandiant/capa/issues/2996 "F401", # Unused imports ] From c54cf81988f778138c44ce0061aba9e8a98df7e9 Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Mon, 6 Apr 2026 18:07:46 +0000 Subject: [PATCH 15/16] replace black with ruff --- .devcontainer/devcontainer.json | 1 - .github/CONTRIBUTING.md | 7 +- .github/ruff.toml | 1 - .../{black-format.yml => ruff-format.yml} | 12 +- .github/workflows/tests.yml | 4 +- .justfile | 6 +- .pre-commit-config.yaml | 17 +- CHANGELOG.md | 2 +- .../features/extractors/binexport2/helpers.py | 12 +- capa/features/extractors/ghidra/file.py | 1 - capa/features/extractors/vmray/call.py | 14 +- capa/ida/plugin/form.py | 2 +- capa/ida/plugin/view.py | 29 +- capa/render/proto/__init__.py | 85 ++-- capa/render/result_document.py | 33 +- capa/render/utils.py | 16 +- capa/render/vverbose.py | 17 +- doc/installation.md | 3 +- pyproject.toml | 2 - scripts/capa2sarif.py | 40 +- scripts/lint.py | 41 +- scripts/profile-memory.py | 22 +- scripts/setup-linter-dependencies.py | 30 +- tests/test_binexport_accessors.py | 7 +- tests/test_capabilities.py | 122 +++-- tests/test_dynamic_span_of_calls_scope.py | 30 +- tests/test_engine.py | 45 +- tests/test_freeze_dynamic.py | 12 +- tests/test_freeze_static.py | 12 +- tests/test_main.py | 68 +-- tests/test_match.py | 78 ++-- tests/test_os_detection.py | 142 +++--- tests/test_render.py | 18 +- tests/test_rule_cache.py | 12 +- tests/test_rules.py | 418 +++++++++++------- tests/test_rules_insn_scope.py | 38 +- web/rules/scripts/build_root.py | 6 +- 37 files changed, 793 insertions(+), 612 deletions(-) rename .github/workflows/{black-format.yml => ruff-format.yml} (86%) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 706a816dde..d78af45b23 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,7 +21,6 @@ "python.linting.enabled": true, "python.linting.pylintEnabled": true, "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", - "python.formatting.blackPath": "/usr/local/py-utils/bin/black", "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", "python.linting.ruffPath": "/usr/local/py-utils/bin/ruff", diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index dde68a38d7..056be94ae8 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -194,10 +194,9 @@ sign a new one. All Python code must adhere to the style guide used by capa: 1. [PEP8](https://www.python.org/dev/peps/pep-0008/), with clarifications from - 2. [Willi's style guide](https://docs.google.com/document/d/1iRpeg-w4DtibwytUyC_dDT7IGhNGBP25-nQfuBa-Fyk/edit?usp=sharing), formatted with - 3. [ruff](https://docs.astral.sh/ruff/) (with line length 120 and imports ordered by line length), and formatted with - 4. [black](https://github.com/psf/black) (with line width 120), and formatted with - 5. [dos2unix](https://linux.die.net/man/1/dos2unix) + 2. [Willi's style guide](https://docs.google.com/document/d/1iRpeg-w4DtibwytUyC_dDT7IGhNGBP25-nQfuBa-Fyk/edit?usp=sharing), and checked/formatted with + 3. [ruff](https://docs.astral.sh/ruff/) (with line length 120), and + 4. [dos2unix](https://linux.die.net/man/1/dos2unix) Our CI pipeline will reformat and enforce the Python styleguide. diff --git a/.github/ruff.toml b/.github/ruff.toml index cf6c5a96f6..ba68383251 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -79,7 +79,6 @@ lint.ignore = [ "B904", # Raise exceptions within except clause using raise from "UP028", # Replace yield over for loop with yield from "C409", # Unnecessary list comprehension passed to tuple() - "E226", # Missing whitespace around arithmetic operator # TODO(mike-hunhoff): address circular dependencies # https://github.com/mandiant/capa/issues/2996 "F401", # Unused imports diff --git a/.github/workflows/black-format.yml b/.github/workflows/ruff-format.yml similarity index 86% rename from .github/workflows/black-format.yml rename to .github/workflows/ruff-format.yml index 1ad7daf2bc..9c5eed2565 100644 --- a/.github/workflows/black-format.yml +++ b/.github/workflows/ruff-format.yml @@ -1,4 +1,4 @@ -name: black auto-format +name: ruff auto-format on: pull_request: @@ -13,7 +13,7 @@ permissions: contents: write jobs: - black-format: + ruff-format: # only run on dependabot PRs or manual trigger if: github.actor == 'dependabot[bot]' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-22.04 @@ -40,10 +40,10 @@ jobs: continue-on-error: true run: pre-commit run ruff --all-files - - name: Run black/continue - # black returns non-zero error code after formatting, which is what we expect + - name: Run ruff format/continue + # ruff format returns non-zero error code after formatting, which is what we expect continue-on-error: true - run: pre-commit run black --all-files + run: pre-commit run ruff-format --all-files - name: Check for changes id: changes @@ -60,5 +60,5 @@ jobs: git config user.name "${GITHUB_ACTOR}" git config user.email "${GITHUB_ACTOR_ID}+${GITHUB_ACTOR}@users.noreply.github.com" git add -A - git commit -m "style: auto-format with ruff and black" + git commit -m "style: auto-format with ruff" git push diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a0c748f9cc..b4a9953de8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -52,8 +52,8 @@ jobs: pip install -e .[dev,scripts] - name: Lint with ruff run: pre-commit run ruff - - name: Lint with black - run: pre-commit run black --show-diff-on-failure + - name: Check formatting with ruff + run: pre-commit run ruff-format --show-diff-on-failure - name: Check types with mypy run: pre-commit run mypy --hook-stage manual - name: Check imports against dependencies diff --git a/.justfile b/.justfile index 80218745b0..f8d017fb25 100644 --- a/.justfile +++ b/.justfile @@ -1,5 +1,5 @@ -@black: - pre-commit run black --show-diff-on-failure --all-files +@ruff-format: + pre-commit run ruff-format --show-diff-on-failure --all-files @ruff: pre-commit run ruff --all-files @@ -11,7 +11,7 @@ pre-commit run deptry --hook-stage manual --all-files @lint: - -just black + -just ruff-format -just ruff -just mypy -just deptry diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 017a869e86..acebaad7b6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,10 +6,10 @@ # ❯ pre-commit install --hook-type pre-push # pre-commit installed at .git/hooks/pre-push # -# run all linters liks: +# run all linters like: # # ❯ pre-commit run --all-files -# black....................................................................Passed +# ruff-format..............................................................Passed # ruff.....................................................................Passed # mypy.....................................................................Passed # @@ -22,19 +22,20 @@ repos: - repo: local hooks: - - id: black - name: black + - id: ruff-format + name: ruff format stages: [pre-commit, pre-push, manual] language: system - entry: black + entry: ruff args: - - "--line-length=120" - - "--extend-exclude" - - ".*_pb2.py" + - "format" + - "--config" + - ".github/ruff.toml" - "capa/" - "scripts/" - "tests/" - "web/rules/scripts/" + exclude: '.*_pb2\.py$' always_run: true pass_filenames: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 26c84999ac..d613005bb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ ### capa Explorer IDA Pro plugin ### Development -- replace isort/flake8 with ruff @mike-hunhoff #2992 +- replace black/isort/flake8 with ruff @mike-hunhoff #2992 ### Raw diffs - [capa v9.4.0...master](https://github.com/mandiant/capa/compare/v9.4.0...master) diff --git a/capa/features/extractors/binexport2/helpers.py b/capa/features/extractors/binexport2/helpers.py index c359ecb146..9eab3972c8 100644 --- a/capa/features/extractors/binexport2/helpers.py +++ b/capa/features/extractors/binexport2/helpers.py @@ -662,14 +662,10 @@ def __init__(self, queries: list[BinExport2InstructionPattern]): @classmethod def from_str(cls, patterns: str): - return cls( - [ - BinExport2InstructionPattern.from_str(line) - for line in filter( - lambda line: not line.startswith("#"), (line.strip() for line in patterns.split("\n")) - ) - ] - ) + return cls([ + BinExport2InstructionPattern.from_str(line) + for line in filter(lambda line: not line.startswith("#"), (line.strip() for line in patterns.split("\n"))) + ]) def match( self, mnemonic: str, operand_expressions: list[list[BinExport2.Expression]] diff --git a/capa/features/extractors/ghidra/file.py b/capa/features/extractors/ghidra/file.py index 42656e4776..4bfbb7b299 100644 --- a/capa/features/extractors/ghidra/file.py +++ b/capa/features/extractors/ghidra/file.py @@ -186,7 +186,6 @@ def extract_file_function_names() -> Iterator[tuple[Feature, Address]]: """ for sym in capa.features.extractors.ghidra.helpers.get_current_program().getSymbolTable().getAllSymbols(True): - # .isExternal() misses more than this config for the function symbols 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 diff --git a/capa/features/extractors/vmray/call.py b/capa/features/extractors/vmray/call.py index 888e1b3937..e1f928a333 100644 --- a/capa/features/extractors/vmray/call.py +++ b/capa/features/extractors/vmray/call.py @@ -26,14 +26,12 @@ logger = logging.getLogger(__name__) -VOID_PTR_NUMBER_PARAMS = frozenset( - { - "hKey", - "hKeyRoot", - "hkResult", - "samDesired", - } -) +VOID_PTR_NUMBER_PARAMS = frozenset({ + "hKey", + "hKeyRoot", + "hkResult", + "samDesired", +}) def get_call_param_features(param: Param, ch: CallHandle) -> Iterator[tuple[Feature, Address]]: diff --git a/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index 800453bbfa..8c076cc688 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -639,7 +639,7 @@ def load_capa_rules(self): try: def on_load_rule(_, i, total): - update_wait_box(f"loading capa rules from {rule_path} ({i+1} of {total})") + update_wait_box(f"loading capa rules from {rule_path} ({i + 1} of {total})") if ida_kernwin.user_cancelled(): raise UserCancelledError("user cancelled") diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index a442f4d1e9..4c21de1c9d 100644 --- a/capa/ida/plugin/view.py +++ b/capa/ida/plugin/view.py @@ -80,18 +80,18 @@ def parse_node_for_feature(feature, description, comment, depth): display = "" if feature.startswith("#"): - display += f"{' '*depth}{feature}\n" + display += f"{' ' * depth}{feature}\n" elif description: if feature.startswith(("- and", "- or", "- optional", "- basic block", "- not", "- instruction:")): - display += f"{' '*depth}{feature}\n" + display += f"{' ' * depth}{feature}\n" if comment: display += f" # {comment}" - display += f"\n{' '*(depth+2)}- description: {description}\n" + display += f"\n{' ' * (depth + 2)}- description: {description}\n" elif feature.startswith("- string"): - display += f"{' '*depth}{feature}\n" + display += f"{' ' * depth}{feature}\n" if comment: display += f" # {comment}" - display += f"\n{' '*(depth+2)}description: {description}\n" + display += f"\n{' ' * (depth + 2)}description: {description}\n" elif feature.startswith("- count"): # count is weird, we need to format description based on feature type, so we parse with regex # assume format - count(()): @@ -99,20 +99,20 @@ def parse_node_for_feature(feature, description, comment, depth): if m: name, value, count = m.groups() if name in ("string",): - display += f"{' '*depth}{feature}" + display += f"{' ' * depth}{feature}" if comment: display += f" # {comment}" - display += f"\n{' '*(depth+2)}description: {description}\n" + display += f"\n{' ' * (depth + 2)}description: {description}\n" else: - display += f"{' '*depth}- count({name}({value} = {description})): {count}" + display += f"{' ' * depth}- count({name}({value} = {description})): {count}" if comment: display += f" # {comment}\n" else: - display += f"{' '*depth}{feature} = {description}" + display += f"{' ' * depth}{feature} = {description}" if comment: display += f" # {comment}\n" else: - display += f"{' '*depth}{feature}" + display += f"{' ' * depth}{feature}" if comment: display += f" # {comment}\n" @@ -785,8 +785,13 @@ def load_features_from_yaml(self, rule_text, update_preview=False): def get_features(self, selected=False, ignore=()): """ """ for feature in filter( - lambda o: o.capa_type - in (CapaExplorerRulegenEditor.get_node_type_feature(), CapaExplorerRulegenEditor.get_node_type_comment()), + lambda o: ( + o.capa_type + in ( + CapaExplorerRulegenEditor.get_node_type_feature(), + CapaExplorerRulegenEditor.get_node_type_comment(), + ) + ), tuple(iterate_tree(self)), ): if feature in ignore: diff --git a/capa/render/proto/__init__.py b/capa/render/proto/__init__.py index 53f942c546..8c204fcaea 100644 --- a/capa/render/proto/__init__.py +++ b/capa/render/proto/__init__.py @@ -691,30 +691,26 @@ def static_analysis_from_pb2(analysis: capa_pb2.StaticAnalysis) -> rd.StaticAnal rules=tuple(analysis.rules), base_address=addr_from_pb2(analysis.base_address), layout=rd.StaticLayout( - functions=tuple( - [ - rd.FunctionLayout( - address=addr_from_pb2(f.address), - matched_basic_blocks=tuple( - [rd.BasicBlockLayout(address=addr_from_pb2(bb.address)) for bb in f.matched_basic_blocks] - ), - ) - for f in analysis.layout.functions - ] - ) + functions=tuple([ + rd.FunctionLayout( + address=addr_from_pb2(f.address), + matched_basic_blocks=tuple([ + rd.BasicBlockLayout(address=addr_from_pb2(bb.address)) for bb in f.matched_basic_blocks + ]), + ) + for f in analysis.layout.functions + ]) ), feature_counts=rd.StaticFeatureCounts( file=analysis.feature_counts.file, - functions=tuple( - [ - rd.FunctionFeatureCount(address=addr_from_pb2(f.address), count=f.count) - for f in analysis.feature_counts.functions - ] - ), - ), - library_functions=tuple( - [rd.LibraryFunction(address=addr_from_pb2(lf.address), name=lf.name) for lf in analysis.library_functions] + functions=tuple([ + rd.FunctionFeatureCount(address=addr_from_pb2(f.address), count=f.count) + for f in analysis.feature_counts.functions + ]), ), + library_functions=tuple([ + rd.LibraryFunction(address=addr_from_pb2(lf.address), name=lf.name) for lf in analysis.library_functions + ]), ) @@ -726,38 +722,29 @@ def dynamic_analysis_from_pb2(analysis: capa_pb2.DynamicAnalysis) -> rd.DynamicA extractor=analysis.extractor, rules=tuple(analysis.rules), layout=rd.DynamicLayout( - processes=tuple( - [ - rd.ProcessLayout( - address=addr_from_pb2(p.address), - name=p.name, - matched_threads=tuple( - [ - rd.ThreadLayout( - address=addr_from_pb2(t.address), - matched_calls=tuple( - [ - rd.CallLayout(address=addr_from_pb2(c.address), name=c.name) - for c in t.matched_calls - ] - ), - ) - for t in p.matched_threads - ] - ), - ) - for p in analysis.layout.processes - ] - ) + processes=tuple([ + rd.ProcessLayout( + address=addr_from_pb2(p.address), + name=p.name, + matched_threads=tuple([ + rd.ThreadLayout( + address=addr_from_pb2(t.address), + matched_calls=tuple([ + rd.CallLayout(address=addr_from_pb2(c.address), name=c.name) for c in t.matched_calls + ]), + ) + for t in p.matched_threads + ]), + ) + for p in analysis.layout.processes + ]) ), feature_counts=rd.DynamicFeatureCounts( file=analysis.feature_counts.file, - processes=tuple( - [ - rd.ProcessFeatureCount(address=addr_from_pb2(p.address), count=p.count) - for p in analysis.feature_counts.processes - ] - ), + processes=tuple([ + rd.ProcessFeatureCount(address=addr_from_pb2(p.address), count=p.count) + for p in analysis.feature_counts.processes + ]), ), ) diff --git a/capa/render/result_document.py b/capa/render/result_document.py index 3ad71752dc..c8d194d2a3 100644 --- a/capa/render/result_document.py +++ b/capa/render/result_document.py @@ -393,7 +393,6 @@ def from_capa( ) for location in result.locations: - # keep this in sync with the copy below if isinstance(location, DynamicCallAddress): if location in rule_matches: @@ -409,15 +408,13 @@ def from_capa( # # Despite the edge cases (like API hammering), this turns out to be pretty easy: # collect the most recent match (with the given name) prior to the wanted location. - matches_in_thread = sorted( - [ - (a.id, m) - for a, m in rule_matches.items() - if isinstance(a, DynamicCallAddress) - and a.thread == location.thread - and a.id <= location.id - ] - ) + matches_in_thread = sorted([ + (a.id, m) + for a, m in rule_matches.items() + if isinstance(a, DynamicCallAddress) + and a.thread == location.thread + and a.id <= location.id + ]) if matches_in_thread: _, most_recent_match = matches_in_thread[-1] children.append(Match.from_capa(rules, capabilities, most_recent_match)) @@ -470,15 +467,13 @@ def from_capa( if location in rule_matches: children.append(Match.from_capa(rules, capabilities, rule_matches[location])) else: - matches_in_thread = sorted( - [ - (a.id, m) - for a, m in rule_matches.items() - if isinstance(a, DynamicCallAddress) - and a.thread == location.thread - and a.id <= location.id - ] - ) + matches_in_thread = sorted([ + (a.id, m) + for a, m in rule_matches.items() + if isinstance(a, DynamicCallAddress) + and a.thread == location.thread + and a.id <= location.id + ]) # namespace matches may not occur within the same thread as the result, so only # proceed if a match within the same thread is found if matches_in_thread: diff --git a/capa/render/utils.py b/capa/render/utils.py index 903d0cb780..e61b69616e 100644 --- a/capa/render/utils.py +++ b/capa/render/utils.py @@ -80,15 +80,13 @@ def capability_rules(doc: rd.ResultDocument) -> Iterator[rd.RuleMatches]: def maec_rules(doc: rd.ResultDocument) -> Iterator[rd.RuleMatches]: """enumerate 'maec' rules.""" for rule in doc.rules.values(): - if any( - [ - rule.meta.maec.analysis_conclusion, - rule.meta.maec.analysis_conclusion_ov, - rule.meta.maec.malware_family, - rule.meta.maec.malware_category, - rule.meta.maec.malware_category_ov, - ] - ): + if any([ + rule.meta.maec.analysis_conclusion, + rule.meta.maec.analysis_conclusion_ov, + rule.meta.maec.malware_family, + rule.meta.maec.malware_category, + rule.meta.maec.malware_category_ov, + ]): yield rule diff --git a/capa/render/vverbose.py b/capa/render/vverbose.py index fe378a8099..491fbbf8e6 100644 --- a/capa/render/vverbose.py +++ b/capa/render/vverbose.py @@ -424,20 +424,19 @@ def render_rules(console: Console, doc: rd.ResultDocument): rows.append(("namespace", rule.meta.namespace)) if rule.meta.maec.analysis_conclusion or rule.meta.maec.analysis_conclusion_ov: - rows.append( - ( - "maec/analysis-conclusion", - rule.meta.maec.analysis_conclusion or rule.meta.maec.analysis_conclusion_ov, - ) - ) + rows.append(( + "maec/analysis-conclusion", + rule.meta.maec.analysis_conclusion or rule.meta.maec.analysis_conclusion_ov, + )) if rule.meta.maec.malware_family: rows.append(("maec/malware-family", rule.meta.maec.malware_family)) if rule.meta.maec.malware_category or rule.meta.maec.malware_category_ov: - rows.append( - ("maec/malware-category", rule.meta.maec.malware_category or rule.meta.maec.malware_category_ov) - ) + rows.append(( + "maec/malware-category", + rule.meta.maec.malware_category or rule.meta.maec.malware_category_ov, + )) rows.append(("author", ", ".join(rule.meta.authors))) diff --git a/doc/installation.md b/doc/installation.md index c7d7717a9b..3e4062beaf 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -98,7 +98,6 @@ Please install these dependencies before install capa (from source or from PyPI) `$ pip install -r requirements.txt` We use the following tools to ensure consistent code style and formatting: - - [black](https://github.com/psf/black) code formatter - [ruff](https://docs.astral.sh/ruff/) code linter and formatter - [mypy](https://mypy-lang.org/) type checking - [capafmt](https://github.com/mandiant/capa/blob/master/scripts/capafmt.py) rule formatter @@ -113,7 +112,7 @@ We use [pre-commit](https://pre-commit.com/) so that its trivial to run the same Run all linters like: ❯ pre-commit run --hook-stage=manual --all-files - black....................................................................Passed + ruff-format..............................................................Passed ruff.....................................................................Passed mypy.....................................................................Passed pytest (fast)............................................................Passed diff --git a/pyproject.toml b/pyproject.toml index 8bb82bacdc..faeb2027d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,7 +134,6 @@ dev = [ "pytest-sugar==1.1.1", "pytest-instafail==0.5.0", "ruff==0.15.0", - "black==26.3.0", "mypy==1.20.0", "mypy-protobuf==5.0.0", "PyGithub==2.9.0", @@ -206,7 +205,6 @@ known_first_party = [ [tool.deptry.per_rule_ignores] # dependencies defined but not used in the codebase DEP002 = [ - "black", "build", "bump-my-version", "deptry", diff --git a/scripts/capa2sarif.py b/scripts/capa2sarif.py index 348d5485fc..be4c815caf 100644 --- a/scripts/capa2sarif.py +++ b/scripts/capa2sarif.py @@ -161,21 +161,19 @@ def _sarif_boilerplate(data_meta: dict, data_rules: dict) -> Optional[dict]: id = data_rules[key]["meta"]["name"] # Append current rule - rules.append( - { - # Default to attack identifier, fall back to MBC, mainly relevant if both are present - "id": id, - "name": data_rules[key]["meta"]["name"], - "shortDescription": {"text": data_rules[key]["meta"]["name"]}, - "messageStrings": {"default": {"text": data_rules[key]["meta"]["name"]}}, - "properties": { - "namespace": data_rules[key]["meta"].get("namespace", []), - "scopes": data_rules[key]["meta"]["scopes"], - "references": data_rules[key]["meta"]["references"], - "lib": data_rules[key]["meta"]["lib"], - }, - } - ) + rules.append({ + # Default to attack identifier, fall back to MBC, mainly relevant if both are present + "id": id, + "name": data_rules[key]["meta"]["name"], + "shortDescription": {"text": data_rules[key]["meta"]["name"]}, + "messageStrings": {"default": {"text": data_rules[key]["meta"]["name"]}}, + "properties": { + "namespace": data_rules[key]["meta"].get("namespace", []), + "scopes": data_rules[key]["meta"]["scopes"], + "references": data_rules[key]["meta"]["references"], + "lib": data_rules[key]["meta"]["lib"], + }, + }) tool = Tool( driver=ToolComponent( @@ -284,13 +282,11 @@ def _enumerate_evidence(node: dict, related_count: int) -> list[dict]: if loc["type"] != "absolute": continue - related_locations.append( - { - "id": related_count, - "message": {"text": label}, - "physicalLocation": {"address": {"absoluteAddress": loc["value"]}}, - } - ) + related_locations.append({ + "id": related_count, + "message": {"text": label}, + "physicalLocation": {"address": {"absoluteAddress": loc["value"]}}, + }) related_count += 1 if node.get("success") and node.get("node", {}).get("type") == "statement": diff --git a/scripts/lint.py b/scripts/lint.py index c489c8d5e0..c5953cd70e 100644 --- a/scripts/lint.py +++ b/scripts/lint.py @@ -593,20 +593,18 @@ class DuplicateFeatureUnderStatement(Lint): def check_rule(self, ctx: Context, rule: Rule) -> bool: self.violation = False self.recommendation = "" - STATEMENTS = frozenset( - { - "or", - "and", - "not", - "optional", - "some", - "basic block", - "function", - "instruction", - "call", - " or more", - } - ) + STATEMENTS = frozenset({ + "or", + "and", + "not", + "optional", + "some", + "basic block", + "function", + "instruction", + "call", + " or more", + }) # rule.statement discards the duplicate features by default so # need to use the rule definition to check for duplicates data = rule._get_ruamel_yaml_parser().load(rule.definition) @@ -1099,7 +1097,7 @@ def lint_rule(ctx: Context, rule: Rule): # and ends up just producing a lot of noise. if not (is_nursery_rule(rule) and len(violations) == 1 and violations[0].name == "missing examples"): print("") - print(f'{" (nursery) " if is_nursery_rule(rule) else ""} {rule.name}') + print(f"{' (nursery) ' if is_nursery_rule(rule) else ''} {rule.name}") for violation in violations: print( @@ -1112,8 +1110,10 @@ def lint_rule(ctx: Context, rule: Rule): lints_failed = len( tuple( filter( - lambda v: v.level == Lint.FAIL - and not (v.name == "missing examples" or v.name == "referenced example doesn't exist"), + lambda v: ( + v.level == Lint.FAIL + and not (v.name == "missing examples" or v.name == "referenced example doesn't exist") + ), violations, ) ) @@ -1121,8 +1121,9 @@ def lint_rule(ctx: Context, rule: Rule): lints_warned = len( tuple( filter( - lambda v: v.level == Lint.WARN - or (v.level == Lint.FAIL and v.name == "referenced example doesn't exist"), + lambda v: ( + v.level == Lint.WARN or (v.level == Lint.FAIL and v.name == "referenced example doesn't exist") + ), violations, ) ) @@ -1130,7 +1131,7 @@ def lint_rule(ctx: Context, rule: Rule): if (not lints_failed) and (not lints_warned) and has_examples: print("") - print(f'{" (nursery) " if is_nursery_rule(rule) else ""} {rule.name}') + print(f"{' (nursery) ' if is_nursery_rule(rule) else ''} {rule.name}") print(f" {Lint.WARN}: '[green]no lint failures[/green]': Graduate the rule") print("") else: diff --git a/scripts/profile-memory.py b/scripts/profile-memory.py index 3ab8afeeb8..02cfb29713 100644 --- a/scripts/profile-memory.py +++ b/scripts/profile-memory.py @@ -21,19 +21,17 @@ def display_top(snapshot, key_type="lineno", limit=10): # via: https://docs.python.org/3/library/tracemalloc.html#pretty-top - snapshot = snapshot.filter_traces( - ( - tracemalloc.Filter(False, ""), - tracemalloc.Filter(False, ""), - tracemalloc.Filter(False, ""), - ) - ) + snapshot = snapshot.filter_traces(( + tracemalloc.Filter(False, ""), + tracemalloc.Filter(False, ""), + tracemalloc.Filter(False, ""), + )) top_stats = snapshot.statistics(key_type) print(f"Top {limit} lines") for index, stat in enumerate(top_stats[:limit], 1): frame = stat.traceback[0] - print(f"#{index}: {frame.filename}:{frame.lineno}: {(stat.size/1024):.1f} KiB") + print(f"#{index}: {frame.filename}:{frame.lineno}: {(stat.size / 1024):.1f} KiB") line = linecache.getline(frame.filename, frame.lineno).strip() if line: print(f" {line}") @@ -41,9 +39,9 @@ def display_top(snapshot, key_type="lineno", limit=10): other = top_stats[limit:] if other: size = sum(stat.size for stat in other) - print(f"{len(other)} other: {(size/1024):.1f} KiB") + print(f"{len(other)} other: {(size / 1024):.1f} KiB") total = sum(stat.size for stat in top_stats) - print(f"Total allocated size: {(total/1024):.1f} KiB") + print(f"Total allocated size: {(total / 1024):.1f} KiB") def main(): @@ -63,7 +61,7 @@ def main(): print() for i in range(count): - print(f"iteration {i+1}/{count}...") + print(f"iteration {i + 1}/{count}...") with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()): t0 = time.time() capa.main.main() @@ -72,7 +70,7 @@ def main(): gc.collect() process = psutil.Process(os.getpid()) - print(f" duration: {(t1-t0):.2f}") + print(f" duration: {(t1 - t0):.2f}") print(f" rss: {(process.memory_info().rss / 1024 / 1024):.1f} MiB") print(f" vms: {(process.memory_info().vms / 1024 / 1024):.1f} MiB") diff --git a/scripts/setup-linter-dependencies.py b/scripts/setup-linter-dependencies.py index 01e15acc17..bdadc292ce 100644 --- a/scripts/setup-linter-dependencies.py +++ b/scripts/setup-linter-dependencies.py @@ -97,24 +97,20 @@ def _get_tactics(self) -> list[dict]: """Get tactics IDs from Mitre matrix.""" # Only one matrix for enterprise att&ck framework matrix = self._remove_deprecated_objects( - self._memory_store.query( - [ - Filter("type", "=", "x-mitre-matrix"), - ] - ) + self._memory_store.query([ + Filter("type", "=", "x-mitre-matrix"), + ]) )[0] return list(map(self._memory_store.get, matrix["tactic_refs"])) def _get_techniques_from_tactic(self, tactic: str) -> list[AttackPattern]: """Get techniques and sub techniques from a Mitre tactic (kill_chain_phases->phase_name)""" techniques = self._remove_deprecated_objects( - self._memory_store.query( - [ - Filter("type", "=", "attack-pattern"), - Filter("kill_chain_phases.phase_name", "=", tactic), - Filter("kill_chain_phases.kill_chain_name", "=", self.kill_chain_name), - ] - ) + self._memory_store.query([ + Filter("type", "=", "attack-pattern"), + Filter("kill_chain_phases.phase_name", "=", tactic), + Filter("kill_chain_phases.kill_chain_name", "=", self.kill_chain_name), + ]) ) return techniques @@ -122,12 +118,10 @@ def _get_parent_technique_from_subtechnique(self, technique: AttackPattern) -> A """Get parent technique of a sub technique using the technique ID TXXXX.YYY""" sub_id = technique["external_references"][0]["external_id"].split(".")[0] parent_technique = self._remove_deprecated_objects( - self._memory_store.query( - [ - Filter("type", "=", "attack-pattern"), - Filter("external_references.external_id", "=", sub_id), - ] - ) + self._memory_store.query([ + Filter("type", "=", "attack-pattern"), + Filter("external_references.external_id", "=", sub_id), + ]) )[0] return parent_technique diff --git a/tests/test_binexport_accessors.py b/tests/test_binexport_accessors.py index 4c5362c438..7cdb9bc545 100644 --- a/tests/test_binexport_accessors.py +++ b/tests/test_binexport_accessors.py @@ -458,7 +458,8 @@ def test_pattern_parsing(): capture="#int", ) - assert BinExport2InstructionPatternMatcher.from_str(""" + assert ( + BinExport2InstructionPatternMatcher.from_str(""" # comment br reg br reg(not-stack) @@ -479,7 +480,9 @@ def test_pattern_parsing(): call [reg * #int + #int] call [reg + reg + #int] call [reg + #int] - """).queries is not None + """).queries + is not None + ) def match_address(extractor: BinExport2FeatureExtractor, queries: BinExport2InstructionPatternMatcher, address: int): diff --git a/tests/test_capabilities.py b/tests/test_capabilities.py index 4ee5e83cec..56406d62b4 100644 --- a/tests/test_capabilities.py +++ b/tests/test_capabilities.py @@ -19,10 +19,10 @@ def test_match_across_scopes_file_function(z9324d_extractor): - rules = capa.rules.RuleSet( - [ - # this rule should match on a function (0x4073F0) - capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + # this rule should match on a function (0x4073F0) + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: install service @@ -36,9 +36,11 @@ def test_match_across_scopes_file_function(z9324d_extractor): - api: advapi32.OpenSCManagerA - api: advapi32.CreateServiceA - api: advapi32.StartServiceA - """)), - # this rule should match on a file feature - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + # this rule should match on a file feature + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: .text section @@ -49,11 +51,13 @@ def test_match_across_scopes_file_function(z9324d_extractor): - 9324d1a8ae37a36ae560c37448c9705a features: - section: .text - """)), - # this rule should match on earlier rule matches: - # - install service, with function scope - # - .text section, with file scope - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + # this rule should match on earlier rule matches: + # - install service, with function scope + # - .text section, with file scope + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: .text section and install service @@ -66,9 +70,9 @@ def test_match_across_scopes_file_function(z9324d_extractor): - and: - match: install service - match: .text section - """)), - ] - ) + """) + ), + ]) capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) assert "install service" in capabilities.matches assert ".text section" in capabilities.matches @@ -76,10 +80,10 @@ def test_match_across_scopes_file_function(z9324d_extractor): def test_match_across_scopes(z9324d_extractor): - rules = capa.rules.RuleSet( - [ - # this rule should match on a basic block (including at least 0x403685) - capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + # this rule should match on a basic block (including at least 0x403685) + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: tight loop @@ -90,10 +94,12 @@ def test_match_across_scopes(z9324d_extractor): - 9324d1a8ae37a36ae560c37448c9705a:0x403685 features: - characteristic: tight loop - """)), - # this rule should match on a function (0x403660) - # based on API, as well as prior basic block rule match - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + # this rule should match on a function (0x403660) + # based on API, as well as prior basic block rule match + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: kill thread loop @@ -107,9 +113,11 @@ def test_match_across_scopes(z9324d_extractor): - api: kernel32.TerminateThread - api: kernel32.CloseHandle - match: tight loop - """)), - # this rule should match on a file feature and a prior function rule match - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + # this rule should match on a file feature and a prior function rule match + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: kill thread program @@ -122,9 +130,9 @@ def test_match_across_scopes(z9324d_extractor): - and: - section: .text - match: kill thread loop - """)), - ] - ) + """) + ), + ]) capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) assert "tight loop" in capabilities.matches assert "kill thread loop" in capabilities.matches @@ -132,7 +140,9 @@ def test_match_across_scopes(z9324d_extractor): def test_subscope_bb_rules(z9324d_extractor): - rules = capa.rules.RuleSet([capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -143,14 +153,18 @@ def test_subscope_bb_rules(z9324d_extractor): - and: - basic block: - characteristic: tight loop - """))]) + """) + ) + ]) # tight loop at 0x403685 capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) assert "test rule" in capabilities.matches def test_match_specific_functions(z9324d_extractor): - rules = capa.rules.RuleSet([capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: receive data @@ -162,7 +176,9 @@ def test_match_specific_functions(z9324d_extractor): features: - or: - api: recv - """))]) + """) + ) + ]) extractor = FunctionFilter(z9324d_extractor, {0x4019C0}) capabilities = capa.capabilities.common.find_capabilities(rules, extractor) matches = capabilities.matches["receive data"] @@ -173,7 +189,9 @@ def test_match_specific_functions(z9324d_extractor): def test_byte_matching(z9324d_extractor): - rules = capa.rules.RuleSet([capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: byte match test @@ -183,13 +201,17 @@ def test_byte_matching(z9324d_extractor): features: - and: - bytes: ED 24 9E F4 52 A9 07 47 55 8E E1 AB 30 8E 23 61 - """))]) + """) + ) + ]) capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) assert "byte match test" in capabilities.matches def test_com_feature_matching(z395eb_extractor): - rules = capa.rules.RuleSet([capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: initialize IWebBrowser2 @@ -201,13 +223,17 @@ def test_com_feature_matching(z395eb_extractor): - api: ole32.CoCreateInstance - com/class: InternetExplorer #bytes: 01 DF 02 00 00 00 00 00 C0 00 00 00 00 00 00 46 = CLSID_InternetExplorer - com/interface: IWebBrowser2 #bytes: 61 16 0C D3 AF CD D0 11 8A 3E 00 C0 4F C9 E2 6E = IID_IWebBrowser2 - """))]) + """) + ) + ]) capabilities = capa.main.find_capabilities(rules, z395eb_extractor) assert "initialize IWebBrowser2" in capabilities.matches def test_count_bb(z9324d_extractor): - rules = capa.rules.RuleSet([capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: count bb @@ -218,14 +244,18 @@ def test_count_bb(z9324d_extractor): features: - and: - count(basic blocks): 1 or more - """))]) + """) + ) + ]) capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) assert "count bb" in capabilities.matches def test_instruction_scope(z9324d_extractor): # .text:004071A4 68 E8 03 00 00 push 3E8h - rules = capa.rules.RuleSet([capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: push 1000 @@ -237,7 +267,9 @@ def test_instruction_scope(z9324d_extractor): - and: - mnemonic: push - number: 1000 - """))]) + """) + ) + ]) capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) assert "push 1000" in capabilities.matches assert 0x4071A4 in {result[0] for result in capabilities.matches["push 1000"]} @@ -247,7 +279,9 @@ def test_instruction_subscope(z9324d_extractor): # .text:00406F60 sub_406F60 proc near # [...] # .text:004071A4 68 E8 03 00 00 push 3E8h - rules = capa.rules.RuleSet([capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: push 1000 on i386 @@ -261,7 +295,9 @@ def test_instruction_subscope(z9324d_extractor): - instruction: - mnemonic: push - number: 1000 - """))]) + """) + ) + ]) capabilities = capa.capabilities.common.find_capabilities(rules, z9324d_extractor) assert "push 1000 on i386" in capabilities.matches assert 0x406F60 in {result[0] for result in capabilities.matches["push 1000 on i386"]} diff --git a/tests/test_dynamic_span_of_calls_scope.py b/tests/test_dynamic_span_of_calls_scope.py index d024e5ba59..29cadb50ff 100644 --- a/tests/test_dynamic_span_of_calls_scope.py +++ b/tests/test_dynamic_span_of_calls_scope.py @@ -368,9 +368,9 @@ def test_dynamic_span_multiple_spans_overlapping_single_event(): def test_dynamic_span_scope_match_statements(): extractor = get_0000a657_thread3064() - ruleset = capa.rules.RuleSet( - [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + ruleset = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: resolve add VEH @@ -383,8 +383,10 @@ def test_dynamic_span_scope_match_statements(): - api: LdrGetDllHandle - api: LdrGetProcedureAddress - string: AddVectoredExceptionHandler - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: resolve remove VEH @@ -397,8 +399,10 @@ def test_dynamic_span_scope_match_statements(): - api: LdrGetDllHandle - api: LdrGetProcedureAddress - string: RemoveVectoredExceptionHandler - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: resolve add and remove VEH @@ -409,8 +413,10 @@ def test_dynamic_span_scope_match_statements(): - and: - match: resolve add VEH - match: resolve remove VEH - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: has VEH runtime linking @@ -420,9 +426,9 @@ def test_dynamic_span_scope_match_statements(): features: - and: - match: linking/runtime-linking/veh - """)), - ] - ) + """) + ), + ]) capabilities = capa.capabilities.dynamic.find_dynamic_capabilities(ruleset, extractor, disable_progress=True) diff --git a/tests/test_engine.py b/tests/test_engine.py index 6db2a9fa74..0236cd936b 100644 --- a/tests/test_engine.py +++ b/tests/test_engine.py @@ -59,25 +59,34 @@ def test_some(): assert bool(Some(2, [Number(1), Number(2), Number(3)]).evaluate({Number(0): {ADDR1}, Number(1): {ADDR1}})) is False assert ( bool( - Some(2, [Number(1), Number(2), Number(3)]).evaluate( - {Number(0): {ADDR1}, Number(1): {ADDR1}, Number(2): {ADDR1}} - ) + Some(2, [Number(1), Number(2), Number(3)]).evaluate({ + Number(0): {ADDR1}, + Number(1): {ADDR1}, + Number(2): {ADDR1}, + }) ) is True ) assert ( bool( - Some(2, [Number(1), Number(2), Number(3)]).evaluate( - {Number(0): {ADDR1}, Number(1): {ADDR1}, Number(2): {ADDR1}, Number(3): {ADDR1}} - ) + Some(2, [Number(1), Number(2), Number(3)]).evaluate({ + Number(0): {ADDR1}, + Number(1): {ADDR1}, + Number(2): {ADDR1}, + Number(3): {ADDR1}, + }) ) is True ) assert ( bool( - Some(2, [Number(1), Number(2), Number(3)]).evaluate( - {Number(0): {ADDR1}, Number(1): {ADDR1}, Number(2): {ADDR1}, Number(3): {ADDR1}, Number(4): {ADDR1}} - ) + Some(2, [Number(1), Number(2), Number(3)]).evaluate({ + Number(0): {ADDR1}, + Number(1): {ADDR1}, + Number(2): {ADDR1}, + Number(3): {ADDR1}, + Number(4): {ADDR1}, + }) ) is True ) @@ -85,15 +94,21 @@ def test_some(): def test_complex(): assert True is bool( - Or([And([Number(1), Number(2)]), Or([Number(3), Some(2, [Number(4), Number(5), Number(6)])])]).evaluate( - {Number(5): {ADDR1}, Number(6): {ADDR1}, Number(7): {ADDR1}, Number(8): {ADDR1}} - ) + Or([And([Number(1), Number(2)]), Or([Number(3), Some(2, [Number(4), Number(5), Number(6)])])]).evaluate({ + Number(5): {ADDR1}, + Number(6): {ADDR1}, + Number(7): {ADDR1}, + Number(8): {ADDR1}, + }) ) assert False is bool( - Or([And([Number(1), Number(2)]), Or([Number(3), Some(2, [Number(4), Number(5)])])]).evaluate( - {Number(5): {ADDR1}, Number(6): {ADDR1}, Number(7): {ADDR1}, Number(8): {ADDR1}} - ) + Or([And([Number(1), Number(2)]), Or([Number(3), Some(2, [Number(4), Number(5)])])]).evaluate({ + Number(5): {ADDR1}, + Number(6): {ADDR1}, + Number(7): {ADDR1}, + Number(8): {ADDR1}, + }) ) diff --git a/tests/test_freeze_dynamic.py b/tests/test_freeze_dynamic.py index 74129f4fae..5fc319d15e 100644 --- a/tests/test_freeze_dynamic.py +++ b/tests/test_freeze_dynamic.py @@ -106,9 +106,9 @@ def test_null_feature_extractor(): DynamicCallAddress(thread=ThreadAddress(ProcessAddress(pid=1), tid=1), id=2), ] - rules = capa.rules.RuleSet( - [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: create file @@ -118,9 +118,9 @@ def test_null_feature_extractor(): features: - and: - api: CreateFile - """)), - ] - ) + """) + ), + ]) capabilities = capa.main.find_capabilities(rules, EXTRACTOR) assert "create file" in capabilities.matches diff --git a/tests/test_freeze_static.py b/tests/test_freeze_static.py index 9c171f2e95..28d5e0c42c 100644 --- a/tests/test_freeze_static.py +++ b/tests/test_freeze_static.py @@ -86,9 +86,9 @@ def test_null_feature_extractor(): AbsoluteVirtualAddress(0x401002), ] - rules = capa.rules.RuleSet( - [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: xor loop @@ -100,9 +100,9 @@ def test_null_feature_extractor(): - characteristic: tight loop - mnemonic: xor - characteristic: nzxor - """)), - ] - ) + """) + ), + ]) capabilities = capa.main.find_capabilities(rules, EXTRACTOR) assert "xor loop" in capabilities.matches diff --git a/tests/test_main.py b/tests/test_main.py index 70097988e9..6bd1011769 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -53,14 +53,12 @@ def test_main_single_rule(z9324d_extractor, tmpdir): rule_file = tmpdir.mkdir("capa").join("rule.yml") rule_file.write(RULE_CONTENT) assert ( - capa.main.main( - [ - path, - "-v", - "-r", - rule_file.strpath, - ] - ) + capa.main.main([ + path, + "-v", + "-r", + rule_file.strpath, + ]) == 0 ) @@ -95,9 +93,9 @@ def test_main_shellcode(z499c2_extractor): def test_ruleset(): - rules = capa.rules.RuleSet( - [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: file rule @@ -106,8 +104,10 @@ def test_ruleset(): dynamic: process features: - characteristic: embedded pe - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: function rule @@ -116,8 +116,10 @@ def test_ruleset(): dynamic: process features: - characteristic: tight loop - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: basic block rule @@ -126,8 +128,10 @@ def test_ruleset(): dynamic: process features: - characteristic: nzxor - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: process rule @@ -136,8 +140,10 @@ def test_ruleset(): dynamic: process features: - string: "explorer.exe" - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: thread rule @@ -146,8 +152,10 @@ def test_ruleset(): dynamic: thread features: - api: RegDeleteKey - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test call subscope @@ -159,8 +167,10 @@ def test_ruleset(): - string: "explorer.exe" - call: - api: HttpOpenRequestW - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -178,9 +188,9 @@ def test_ruleset(): - number: 6 = IPPROTO_TCP - number: 1 = SOCK_STREAM - number: 2 = AF_INET - """)), - ] - ) + """) + ), + ]) assert len(rules.file_rules) == 2 assert len(rules.function_rules) == 2 assert len(rules.basic_block_rules) == 2 @@ -291,7 +301,8 @@ def test_main_cape1(tmp_path): # https://github.com/mandiant/capa/pull/1696 rules = tmp_path / "rules" rules.mkdir() - (rules / "create-or-open-registry-key.yml").write_text(textwrap.dedent(""" + (rules / "create-or-open-registry-key.yml").write_text( + textwrap.dedent(""" rule: meta: name: create or open registry key @@ -321,7 +332,8 @@ def test_main_cape1(tmp_path): - api: SHRegOpenUSKey - api: SHRegCreateUSKey - api: RtlCreateRegistryKey - """)) + """) + ) assert capa.main.main([str(path), "-r", str(rules)]) == 0 assert capa.main.main([str(path), "-q", "-r", str(rules)]) == 0 diff --git a/tests/test_match.py b/tests/test_match.py index 139e2434a6..674b71b3ae 100644 --- a/tests/test_match.py +++ b/tests/test_match.py @@ -208,7 +208,8 @@ def test_match_adds_matched_rule_feature(): def test_match_matched_rules(): """show that using `match` adds a feature for matched rules.""" rules = [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule1 @@ -217,8 +218,10 @@ def test_match_matched_rules(): dynamic: process features: - number: 100 - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule2 @@ -227,7 +230,8 @@ def test_match_matched_rules(): dynamic: process features: - match: test rule1 - """)), + """) + ), ] features, _ = match( @@ -251,7 +255,8 @@ def test_match_matched_rules(): def test_match_namespace(): rules = [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: CreateFile API @@ -261,8 +266,10 @@ def test_match_namespace(): namespace: file/create/CreateFile features: - api: CreateFile - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: WriteFile API @@ -272,8 +279,10 @@ def test_match_namespace(): namespace: file/write features: - api: WriteFile - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: file-create @@ -282,8 +291,10 @@ def test_match_namespace(): dynamic: process features: - match: file/create - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: filesystem-any @@ -292,7 +303,8 @@ def test_match_namespace(): dynamic: process features: - match: file - """)), + """) + ), ] features, matches = match( @@ -319,7 +331,8 @@ def test_match_namespace(): def test_match_substring(): rules = [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -329,7 +342,8 @@ def test_match_substring(): features: - and: - substring: abc - """)), + """) + ), ] features, _ = match( capa.rules.topologically_order_rules(rules), @@ -369,7 +383,8 @@ def test_match_substring(): def test_match_regex(): rules = [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -379,8 +394,10 @@ def test_match_regex(): features: - and: - string: /.*bbbb.*/ - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule with implied wildcards @@ -390,8 +407,10 @@ def test_match_regex(): features: - and: - string: /bbbb/ - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule with anchor @@ -401,7 +420,8 @@ def test_match_regex(): features: - and: - string: /^bbbb/ - """)), + """) + ), ] features, _ = match( capa.rules.topologically_order_rules(rules), @@ -436,7 +456,8 @@ def test_match_regex(): def test_match_regex_ignorecase(): rules = [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -446,7 +467,8 @@ def test_match_regex_ignorecase(): features: - and: - string: /.*bbbb.*/i - """)), + """) + ), ] features, _ = match( capa.rules.topologically_order_rules(rules), @@ -458,7 +480,8 @@ def test_match_regex_ignorecase(): def test_match_regex_complex(): rules = [ - capa.rules.Rule.from_yaml(textwrap.dedent(r""" + capa.rules.Rule.from_yaml( + textwrap.dedent(r""" rule: meta: name: test rule @@ -468,7 +491,8 @@ def test_match_regex_complex(): features: - or: - string: /.*HARDWARE\\Key\\key with spaces\\.*/i - """)), + """) + ), ] features, _ = match( capa.rules.topologically_order_rules(rules), @@ -480,7 +504,8 @@ def test_match_regex_complex(): def test_match_regex_values_always_string(): rules = [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -491,7 +516,8 @@ def test_match_regex_values_always_string(): - or: - string: /123/ - string: /0x123/ - """)), + """) + ), ] features, _ = match( capa.rules.topologically_order_rules(rules), diff --git a/tests/test_os_detection.py b/tests/test_os_detection.py index 4467b8b225..4d71c67b36 100644 --- a/tests/test_os_detection.py +++ b/tests/test_os_detection.py @@ -109,77 +109,75 @@ def test_elf_parse_capa_pyinstaller_header(): # compressed ELF header of capa-v5.1.0-linux # SHA256 e16974994914466647e24cdcfb6a6f8710297a4def21525e53f73c72c4b52fcf elf_header = zlib.decompress( - b"".join( - [ - b"\x78\x9c\x8d\x56\x4f\x88\x1c\xd5\x13\xae\x1d\x35\x0a\x7a\x58\x65", - b"\xd1\xa0\x9b\xb0\x82\x11\x14\x67\x63\xd6\xcd\x26\xf1\xf0\x63\x49", - b"\xdc\xc4\xc8\x26\x98\x7f\x07\x89\xa4\xed\xe9\x7e\x6f\xa6\x99\xd7", - b"\xaf\xdb\xee\x37\xbb\x13\x3d\xb8\x78\x8a\x28\x28\x1e\xbc\x09\x7b", - b"\xf0\xcf\x82\xa0\x41\x10\x23\xa8\x07\x89\x17\x41\x85\x88\x07\x2f", - b"\xe2\x25\xb0\x17\x51\x7e\x07\xd9\x5b\x52\xf5\xfe\xcc\x36\x71\x0a", - b"\x6c\x98\xa9\xf7\xbe\xf9\xea\xab\xaf\xea\x35\x3d\xfd\xda\xd2\xf2", - b"\xd1\xd6\xc4\x04\x84\xab\x05\xff\x03\xda\x2d\xed\x59\xb4\x7b\xf7", - b"\x8d\xf1\xef\x57\x5b\x81\xb3\x08\x07\xe1\x6e\xfc\x9e\x86\x87\x60", - b"\x07\xee\x6f\x6f\xf2\xfc\x2a\xc4\x9e\xcf\x0a\xf1\x2e\xcf\xbb\xcd", - b"\xe7\x6d\x78\x7c\xa3\xe5\xf8\x21\x4e\x7b\x5e\x88\xc1\x21\x45\xca", - b"\xdb\xbe\xb6\x2b\xd3\x75\xe9\x01\xb7\x0b\x11\x26\xb7\xf3\xee\xa0", - b"\xc5\x8c\xc7\x67\x7c\x9e\x8f\xf9\x8b\x6e\x1b\x62\x33\xcf\xd6\x5b", - b"\xf3\xf8\x9a\xcf\xf3\xf1\xca\x7e\xb7\x0d\xb1\x99\x47\xb3\xd9\xfc", - b"\xc6\xed\x37\x7f\x74\xfc\x10\xaf\xf8\x26\x36\x86\xbe\x33\x9f\x47", - b"\xe3\xa0\xbc\x2d\x9f\xb7\xe5\xf9\x21\x5a\x42\x23\x86\x79\x92\x1c", - b"\x7d\xae\x7a\xfc\xaa\x9f\x63\x88\xcf\x78\x5e\x88\x61\x86\xcf\x5f", - b"\x37\x29\xad\x37\xd7\xbd\xcf\x75\xef\xd3\xc7\x27\xe8\xa0\x1a\x31", - b"\xe4\x9d\xc2\x3c\xf2\xf9\x5f\x2f\xdf\x1e\x9c\x0e\xf5\x98\xb9\xec", - b"\xf4\xfe\x43\x0c\xe7\xbe\x57\x65\x9d\x85\xf9\xbd\x2a\x6d\xab\x4c", - b"\x0f\x86\xed\xe1\xc1\x85\xf6\xc2\xfc\x6c\x5d\xcc\xce\x59\x4f\x53", - b"\xfe\x9e\x3a\x76\xf2\x1c\x9c\xfd\xe5\xd9\x33\xe2\xcc\x4b\x17\x76", - b"\x7f\xfd\xd1\xd4\xe6\xe5\xa9\xe5\xf3\x4f\xff\x7c\x82\x38\xe4\x81", - b"\xf4\x88\xd3\x9c\x35\xdd\x12\x94\x7b\xaa\x71\x6e\x30\x31\x03\x6b", - b"\x13\x93\x2d\xc2\x4e\x7b\x0f\x8f\xed\x7a\x6b\x5a\x9e\x8b\x27\x0f", - b"\xfd\xff\xcd\x70\x5b\xfe\xeb\xd2\x28\x7a\xdf\x18\xfc\x1a\x0a\x8f", - b"\xc3\xff\x60\xf0\x0b\xf8\x19\x87\x7f\xc7\xe8\x3f\xc7\xe0\x27\x18", - b"\x9d\x1b\x0c\x7e\x9d\xc1\xe9\xb8\xc6\xe1\x6f\x33\xf8\x97\x4c\x5f", - b"\x6d\x86\xbf\x83\xe1\x7f\xc0\xf4\xf5\x08\x83\xff\xc9\xe8\xff\xc5", - b"\xe8\x4b\x06\x7f\x90\xc1\xdf\x60\xf0\x1f\x18\xfc\x13\xc6\xe7\x49", - b"\x86\x7f\x88\xc1\xff\x61\xfa\xa2\xa7\xf2\x38\xfc\x08\x83\x7f\xc5", - b"\xe0\x47\x99\xba\x37\x18\xfc\x4e\x46\xe7\x57\xc6\xe7\xf7\x0c\xfe", - b"\x2e\xa3\x73\x96\xa9\xfb\x05\xa3\x33\xc7\xf0\x5f\x67\xf4\x7f\x67", - b"\x74\x1e\x67\xf8\x6d\x46\x9f\x9e\x17\xe1\x2f\xa5\x79\x9d\x67\xf8", - b"\x9f\x72\x3e\x19\x7c\x91\xc1\x0f\x33\xfe\x1f\x66\xf8\x2f\x33\xfc", - b"\xfb\x99\x7e\x25\x83\xbf\xcf\xe8\xec\x66\xf0\x75\xc6\xcf\x25\x86", - b"\xff\x2d\xc3\x7f\x87\xc1\xe9\x99\x3e\x0e\xbf\x87\xe1\xbf\xc0\xf4", - b"\x45\x7f\xef\xe3\xf0\x0f\x19\xfc\x3d\x06\xa7\xd7\x80\x71\xf8\x01", - b"\xa6\x6e\xc6\xf0\x3f\x67\xf8\x9f\x31\xfc\x9f\x18\xfc\x51\x66\x0e", - b"\x29\x83\x6f\x31\xf8\xc7\x0c\xbe\x8b\xf1\x99\x73\xcf\x4f\x86\xbf", - b"\x93\xf1\x9f\x32\xf8\x1e\xee\x79\x88\x75\xef\x45\xb5\xf5\x6b\xee", - b"\x7d\x22\xbc\x1f\x4d\x79\x7c\xe3\x16\xfc\x37\x8f\x5f\xbe\x05\x87", - b"\x28\xea\xe6\x85\x8e\x6a\x13\x57\x26\x8a\x20\x55\x89\x2a\x6a\x81", - b"\xb1\xbe\x98\xe3\x77\x51\x0a\x8d\x41\x54\x55\x51\x41\xa6\xa5\x8a", - b"\x8d\x08\xf1\xb8\xce\x4c\x14\x36\x4b\x3a\x45\x2d\xe4\xe9\x22\x52", - b"\x45\x12\x9b\xac\xd0\x50\xc5\x19\x6a\xc9\xa2\xea\xc3\x6a\x9c\x99", - b"\x32\x23\xce\xb0\xec\x46\x9d\xb8\x16\x3a\xce\x05\xe4\xfd\xd4\x88", - b"\xbc\x04\x29\xd5\xa0\xee\x41\x6d\xaa\xa4\xbc\x08\x32\xe9\xe5\x45", - b"\x0a\x95\x88\xd3\x34\xab\xa0\x16\x86\x24\x15\x49\x91\x9f\xd5\xa4", - b"\xd6\x44\x43\xb6\x4e\x30\x39\x42\xfb\x55\x3a\x28\xa1\x74\x3e\x6d", - b"\x0b\x36\x31\xeb\xea\x58\x39\x1e\xf2\xf3\x4e\x6d\x0a\x4c\xc6\x04", - b"\x35\xc4\x8e\x0d\x0c\x34\xbe\x66\xf5\xc9\x05\xb2\xb1\x9c\xc2\x3a", - b"\x48\x4f\x33\x0d\x5d\x61\xfd\xf6\x33\x65\x05\x4c\xd1\x07\x29\x0a", - b"\x09\xe8\xc3\x91\x2a\x85\x56\xca\x2a\x31\x0a\x30\xdb\x76\x53\xe5", - b"\xa4\x93\x8b\x9c\x5c\x25\x4a\xc4\x15\x1a\xc2\x22\xd8\x80\xd0\x2b", - b"\x58\x56\x96\x55\xa6\x8d\x8c\x92\x5e\x9f\xca\x14\x03\x63\xd9\xb6", - b"\x65\x3b\xf7\x28\x5a\xa9\x75\x83\x94\x8f\xaa\xe1\x48\xad\xc3\x32", - b"\x36\x3d\x90\x46\x20\x0e\x5a\x45\x2a\xd6\x5d\x3c\x82\x02\x68\x32", - b"\x54\x1d\x7d\x53\x2d\x54\xa7\xda\x38\x9a\xa6\x1c\x4d\xd4\x76\x2c", - b"\x86\x22\x59\x29\xdd\x64\x50\x38\x8a\x82\xb4\xa5\xc9\x0c\x7b\x2b", - b"\x40\xae\x56\x19\x1e\xb7\xa4\x2c\xa4\x38\xa7\x96\x80\x9d\x10\xe8", - b"\xfb\xa8\x92\x1e\x55\x5a\x69\x76\x67\xcf\x64\x9b\xee\xc6\xed\xc0", - b"\xd8\xb8\x3c\x61\x3a\x03\x69\xd3\x71\x5a\x18\xdc\xe1\xe1\xd9\x64", - b"\x9d\xc4\xdf\x90\x79\x8c\x27\x21\xdd\x0f\xb5\x29\xed\xa0\x6a\x21", - b"\xfa\x05\x84\xb6\xc8\x9d\x00\x4c\x49\x95\x7b\x4b\xc6\xe5\x2b\xb4", - b"\xda\x47\xab\xd2\xf4\xc8\x27\xed\x9f\xa4\x7d\x42\xab\x05\x38\xb6", - b"\x7c\xfc\xf0\x91\x68\x6e\x76\x6e\x76\xff\x68\x7d\x60\xb4\xda\x37", - b"\x3f\x5a\x3e\x35\x5a\x35\x30\x5c\xc3\x4d\x95\x6e\xa4\x60", - ] - ) + b"".join([ + b"\x78\x9c\x8d\x56\x4f\x88\x1c\xd5\x13\xae\x1d\x35\x0a\x7a\x58\x65", + b"\xd1\xa0\x9b\xb0\x82\x11\x14\x67\x63\xd6\xcd\x26\xf1\xf0\x63\x49", + b"\xdc\xc4\xc8\x26\x98\x7f\x07\x89\xa4\xed\xe9\x7e\x6f\xa6\x99\xd7", + b"\xaf\xdb\xee\x37\xbb\x13\x3d\xb8\x78\x8a\x28\x28\x1e\xbc\x09\x7b", + b"\xf0\xcf\x82\xa0\x41\x10\x23\xa8\x07\x89\x17\x41\x85\x88\x07\x2f", + b"\xe2\x25\xb0\x17\x51\x7e\x07\xd9\x5b\x52\xf5\xfe\xcc\x36\x71\x0a", + b"\x6c\x98\xa9\xf7\xbe\xf9\xea\xab\xaf\xea\x35\x3d\xfd\xda\xd2\xf2", + b"\xd1\xd6\xc4\x04\x84\xab\x05\xff\x03\xda\x2d\xed\x59\xb4\x7b\xf7", + b"\x8d\xf1\xef\x57\x5b\x81\xb3\x08\x07\xe1\x6e\xfc\x9e\x86\x87\x60", + b"\x07\xee\x6f\x6f\xf2\xfc\x2a\xc4\x9e\xcf\x0a\xf1\x2e\xcf\xbb\xcd", + b"\xe7\x6d\x78\x7c\xa3\xe5\xf8\x21\x4e\x7b\x5e\x88\xc1\x21\x45\xca", + b"\xdb\xbe\xb6\x2b\xd3\x75\xe9\x01\xb7\x0b\x11\x26\xb7\xf3\xee\xa0", + b"\xc5\x8c\xc7\x67\x7c\x9e\x8f\xf9\x8b\x6e\x1b\x62\x33\xcf\xd6\x5b", + b"\xf3\xf8\x9a\xcf\xf3\xf1\xca\x7e\xb7\x0d\xb1\x99\x47\xb3\xd9\xfc", + b"\xc6\xed\x37\x7f\x74\xfc\x10\xaf\xf8\x26\x36\x86\xbe\x33\x9f\x47", + b"\xe3\xa0\xbc\x2d\x9f\xb7\xe5\xf9\x21\x5a\x42\x23\x86\x79\x92\x1c", + b"\x7d\xae\x7a\xfc\xaa\x9f\x63\x88\xcf\x78\x5e\x88\x61\x86\xcf\x5f", + b"\x37\x29\xad\x37\xd7\xbd\xcf\x75\xef\xd3\xc7\x27\xe8\xa0\x1a\x31", + b"\xe4\x9d\xc2\x3c\xf2\xf9\x5f\x2f\xdf\x1e\x9c\x0e\xf5\x98\xb9\xec", + b"\xf4\xfe\x43\x0c\xe7\xbe\x57\x65\x9d\x85\xf9\xbd\x2a\x6d\xab\x4c", + b"\x0f\x86\xed\xe1\xc1\x85\xf6\xc2\xfc\x6c\x5d\xcc\xce\x59\x4f\x53", + b"\xfe\x9e\x3a\x76\xf2\x1c\x9c\xfd\xe5\xd9\x33\xe2\xcc\x4b\x17\x76", + b"\x7f\xfd\xd1\xd4\xe6\xe5\xa9\xe5\xf3\x4f\xff\x7c\x82\x38\xe4\x81", + b"\xf4\x88\xd3\x9c\x35\xdd\x12\x94\x7b\xaa\x71\x6e\x30\x31\x03\x6b", + b"\x13\x93\x2d\xc2\x4e\x7b\x0f\x8f\xed\x7a\x6b\x5a\x9e\x8b\x27\x0f", + b"\xfd\xff\xcd\x70\x5b\xfe\xeb\xd2\x28\x7a\xdf\x18\xfc\x1a\x0a\x8f", + b"\xc3\xff\x60\xf0\x0b\xf8\x19\x87\x7f\xc7\xe8\x3f\xc7\xe0\x27\x18", + b"\x9d\x1b\x0c\x7e\x9d\xc1\xe9\xb8\xc6\xe1\x6f\x33\xf8\x97\x4c\x5f", + b"\x6d\x86\xbf\x83\xe1\x7f\xc0\xf4\xf5\x08\x83\xff\xc9\xe8\xff\xc5", + b"\xe8\x4b\x06\x7f\x90\xc1\xdf\x60\xf0\x1f\x18\xfc\x13\xc6\xe7\x49", + b"\x86\x7f\x88\xc1\xff\x61\xfa\xa2\xa7\xf2\x38\xfc\x08\x83\x7f\xc5", + b"\xe0\x47\x99\xba\x37\x18\xfc\x4e\x46\xe7\x57\xc6\xe7\xf7\x0c\xfe", + b"\x2e\xa3\x73\x96\xa9\xfb\x05\xa3\x33\xc7\xf0\x5f\x67\xf4\x7f\x67", + b"\x74\x1e\x67\xf8\x6d\x46\x9f\x9e\x17\xe1\x2f\xa5\x79\x9d\x67\xf8", + b"\x9f\x72\x3e\x19\x7c\x91\xc1\x0f\x33\xfe\x1f\x66\xf8\x2f\x33\xfc", + b"\xfb\x99\x7e\x25\x83\xbf\xcf\xe8\xec\x66\xf0\x75\xc6\xcf\x25\x86", + b"\xff\x2d\xc3\x7f\x87\xc1\xe9\x99\x3e\x0e\xbf\x87\xe1\xbf\xc0\xf4", + b"\x45\x7f\xef\xe3\xf0\x0f\x19\xfc\x3d\x06\xa7\xd7\x80\x71\xf8\x01", + b"\xa6\x6e\xc6\xf0\x3f\x67\xf8\x9f\x31\xfc\x9f\x18\xfc\x51\x66\x0e", + b"\x29\x83\x6f\x31\xf8\xc7\x0c\xbe\x8b\xf1\x99\x73\xcf\x4f\x86\xbf", + b"\x93\xf1\x9f\x32\xf8\x1e\xee\x79\x88\x75\xef\x45\xb5\xf5\x6b\xee", + b"\x7d\x22\xbc\x1f\x4d\x79\x7c\xe3\x16\xfc\x37\x8f\x5f\xbe\x05\x87", + b"\x28\xea\xe6\x85\x8e\x6a\x13\x57\x26\x8a\x20\x55\x89\x2a\x6a\x81", + b"\xb1\xbe\x98\xe3\x77\x51\x0a\x8d\x41\x54\x55\x51\x41\xa6\xa5\x8a", + b"\x8d\x08\xf1\xb8\xce\x4c\x14\x36\x4b\x3a\x45\x2d\xe4\xe9\x22\x52", + b"\x45\x12\x9b\xac\xd0\x50\xc5\x19\x6a\xc9\xa2\xea\xc3\x6a\x9c\x99", + b"\x32\x23\xce\xb0\xec\x46\x9d\xb8\x16\x3a\xce\x05\xe4\xfd\xd4\x88", + b"\xbc\x04\x29\xd5\xa0\xee\x41\x6d\xaa\xa4\xbc\x08\x32\xe9\xe5\x45", + b"\x0a\x95\x88\xd3\x34\xab\xa0\x16\x86\x24\x15\x49\x91\x9f\xd5\xa4", + b"\xd6\x44\x43\xb6\x4e\x30\x39\x42\xfb\x55\x3a\x28\xa1\x74\x3e\x6d", + b"\x0b\x36\x31\xeb\xea\x58\x39\x1e\xf2\xf3\x4e\x6d\x0a\x4c\xc6\x04", + b"\x35\xc4\x8e\x0d\x0c\x34\xbe\x66\xf5\xc9\x05\xb2\xb1\x9c\xc2\x3a", + b"\x48\x4f\x33\x0d\x5d\x61\xfd\xf6\x33\x65\x05\x4c\xd1\x07\x29\x0a", + b"\x09\xe8\xc3\x91\x2a\x85\x56\xca\x2a\x31\x0a\x30\xdb\x76\x53\xe5", + b"\xa4\x93\x8b\x9c\x5c\x25\x4a\xc4\x15\x1a\xc2\x22\xd8\x80\xd0\x2b", + b"\x58\x56\x96\x55\xa6\x8d\x8c\x92\x5e\x9f\xca\x14\x03\x63\xd9\xb6", + b"\x65\x3b\xf7\x28\x5a\xa9\x75\x83\x94\x8f\xaa\xe1\x48\xad\xc3\x32", + b"\x36\x3d\x90\x46\x20\x0e\x5a\x45\x2a\xd6\x5d\x3c\x82\x02\x68\x32", + b"\x54\x1d\x7d\x53\x2d\x54\xa7\xda\x38\x9a\xa6\x1c\x4d\xd4\x76\x2c", + b"\x86\x22\x59\x29\xdd\x64\x50\x38\x8a\x82\xb4\xa5\xc9\x0c\x7b\x2b", + b"\x40\xae\x56\x19\x1e\xb7\xa4\x2c\xa4\x38\xa7\x96\x80\x9d\x10\xe8", + b"\xfb\xa8\x92\x1e\x55\x5a\x69\x76\x67\xcf\x64\x9b\xee\xc6\xed\xc0", + b"\xd8\xb8\x3c\x61\x3a\x03\x69\xd3\x71\x5a\x18\xdc\xe1\xe1\xd9\x64", + b"\x9d\xc4\xdf\x90\x79\x8c\x27\x21\xdd\x0f\xb5\x29\xed\xa0\x6a\x21", + b"\xfa\x05\x84\xb6\xc8\x9d\x00\x4c\x49\x95\x7b\x4b\xc6\xe5\x2b\xb4", + b"\xda\x47\xab\xd2\xf4\xc8\x27\xed\x9f\xa4\x7d\x42\xab\x05\x38\xb6", + b"\x7c\xfc\xf0\x91\x68\x6e\x76\x6e\x76\xff\x68\x7d\x60\xb4\xda\x37", + b"\x3f\x5a\x3e\x35\x5a\x35\x30\x5c\xc3\x4d\x95\x6e\xa4\x60", + ]) ) assert capa.features.extractors.elf.detect_elf_os(io.BytesIO(elf_header)) == "linux" diff --git a/tests/test_render.py b/tests/test_render.py index 5fb3b3b20e..a7e8590194 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -57,7 +57,8 @@ def test_render_meta_attack(): subtechnique = "Windows Service" canonical = "{:s}::{:s}::{:s} [{:s}]".format(tactic, technique, subtechnique, id) - rule = textwrap.dedent(""" + rule = textwrap.dedent( + """ rule: meta: name: test rule @@ -70,7 +71,8 @@ def test_render_meta_attack(): - {:s} features: - number: 1 - """.format(canonical)) + """.format(canonical) + ) r = capa.rules.Rule.from_yaml(rule) rule_meta = capa.render.result_document.RuleMetadata.from_capa(r) attack = rule_meta.attack[0] @@ -91,7 +93,8 @@ def test_render_meta_mbc(): method = "Heavens Gate" canonical = "{:s}::{:s}::{:s} [{:s}]".format(objective, behavior, method, id) - rule = textwrap.dedent(""" + rule = textwrap.dedent( + """ rule: meta: name: test rule @@ -104,7 +107,8 @@ def test_render_meta_mbc(): - {:s} features: - number: 1 - """.format(canonical)) + """.format(canonical) + ) r = capa.rules.Rule.from_yaml(rule) rule_meta = capa.render.result_document.RuleMetadata.from_capa(r) mbc = rule_meta.mbc[0] @@ -122,7 +126,8 @@ def test_render_meta_maec(): malware_category = "downloader" analysis_conclusion = "malicious" - rule_yaml = textwrap.dedent(""" + rule_yaml = textwrap.dedent( + """ rule: meta: name: test rule @@ -136,7 +141,8 @@ def test_render_meta_maec(): maec/analysis-conclusion: {:s} features: - number: 1 - """.format(malware_family, malware_category, analysis_conclusion)) + """.format(malware_family, malware_category, analysis_conclusion) + ) rule = capa.rules.Rule.from_yaml(rule_yaml) rm = capa.render.result_document.RuleMatches( meta=capa.render.result_document.RuleMetadata.from_capa(rule), diff --git a/tests/test_rule_cache.py b/tests/test_rule_cache.py index 59f6ff7d1f..3aa0c97df9 100644 --- a/tests/test_rule_cache.py +++ b/tests/test_rule_cache.py @@ -22,7 +22,8 @@ import capa.helpers import capa.rules.cache -R1 = capa.rules.Rule.from_yaml(textwrap.dedent(""" +R1 = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -38,9 +39,11 @@ - and: - number: 1 - number: 2 - """)) + """) +) -R2 = capa.rules.Rule.from_yaml(textwrap.dedent(""" +R2 = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule 2 @@ -56,7 +59,8 @@ - and: - number: 3 - number: 4 - """)) + """) +) def test_ruleset_cache_ids(): diff --git a/tests/test_rules.py b/tests/test_rules.py index 60d3e4e7cb..689659df9c 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -149,7 +149,8 @@ def rec(statement): def test_invalid_rule_statement_descriptions(): # statements can only have one description with pytest.raises(capa.rules.InvalidRule): - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -161,7 +162,8 @@ def test_invalid_rule_statement_descriptions(): - number: 1 = This is the number 1 - description: description - description: another description (invalid) - """)) + """) + ) def test_empty_yaml_raises_invalid_rule(): @@ -267,7 +269,8 @@ def test_rule_yaml_count_string(): def test_invalid_rule_feature(): with pytest.raises(capa.rules.InvalidRule): - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -276,10 +279,12 @@ def test_invalid_rule_feature(): dynamic: process features: - foo: true - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -288,10 +293,12 @@ def test_invalid_rule_feature(): dynamic: process features: - characteristic: nzxor - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -300,10 +307,12 @@ def test_invalid_rule_feature(): dynamic: thread features: - characteristic: embedded pe - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -312,11 +321,13 @@ def test_invalid_rule_feature(): dynamic: thread features: - characteristic: embedded pe - """)) + """) + ) def test_multi_scope_rules_features(): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -330,9 +341,11 @@ def test_multi_scope_rules_features(): - os: linux - mnemonic: syscall - number: 1 = write - """)) + """) + ) - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -346,9 +359,11 @@ def test_multi_scope_rules_features(): - os: linux - mnemonic: syscall - number: 0 = read - """)) + """) + ) - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -366,12 +381,14 @@ def test_multi_scope_rules_features(): - number: 6 = IPPROTO_TCP - number: 1 = SOCK_STREAM - number: 2 = AF_INET - """)) + """) + ) def test_rules_flavor_filtering(): rules = [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: static rule @@ -380,8 +397,10 @@ def test_rules_flavor_filtering(): dynamic: unsupported features: - api: CreateFileA - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: dynamic rule @@ -390,7 +409,8 @@ def test_rules_flavor_filtering(): dynamic: thread features: - api: CreateFileA - """)), + """) + ), ] static_rules = capa.rules.RuleSet([r for r in rules if r.scopes.static is not None]) @@ -408,7 +428,8 @@ def test_meta_scope_keywords(): for static_scope in static_scopes: for dynamic_scope in dynamic_scopes: - _ = capa.rules.Rule.from_yaml(textwrap.dedent(f""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(f""" rule: meta: name: test rule @@ -418,11 +439,13 @@ def test_meta_scope_keywords(): features: - or: - format: pe - """)) + """) + ) # its also ok to specify "unsupported" for static_scope in static_scopes: - _ = capa.rules.Rule.from_yaml(textwrap.dedent(f""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(f""" rule: meta: name: test rule @@ -432,9 +455,11 @@ def test_meta_scope_keywords(): features: - or: - format: pe - """)) + """) + ) for dynamic_scope in dynamic_scopes: - _ = capa.rules.Rule.from_yaml(textwrap.dedent(f""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(f""" rule: meta: name: test rule @@ -444,11 +469,13 @@ def test_meta_scope_keywords(): features: - or: - format: pe - """)) + """) + ) # but at least one scope must be specified with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -456,9 +483,11 @@ def test_meta_scope_keywords(): features: - or: - format: pe - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -468,20 +497,22 @@ def test_meta_scope_keywords(): features: - or: - format: pe - """)) + """) + ) def test_subscope_same_as_scope(): - static_scopes = sorted( - [e.value for e in capa.rules.STATIC_SCOPES if e not in (capa.rules.Scope.FILE, capa.rules.Scope.GLOBAL)] - ) - dynamic_scopes = sorted( - [e.value for e in capa.rules.DYNAMIC_SCOPES if e not in (capa.rules.Scope.FILE, capa.rules.Scope.GLOBAL)] - ) + static_scopes = sorted([ + e.value for e in capa.rules.STATIC_SCOPES if e not in (capa.rules.Scope.FILE, capa.rules.Scope.GLOBAL) + ]) + dynamic_scopes = sorted([ + e.value for e in capa.rules.DYNAMIC_SCOPES if e not in (capa.rules.Scope.FILE, capa.rules.Scope.GLOBAL) + ]) for static_scope in static_scopes: for dynamic_scope in dynamic_scopes: - _ = capa.rules.Rule.from_yaml(textwrap.dedent(f""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(f""" rule: meta: name: test rule @@ -494,13 +525,14 @@ def test_subscope_same_as_scope(): - format: pe - {dynamic_scope}: - format: pe - """)) + """) + ) def test_lib_rules(): - rules = capa.rules.RuleSet( - [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: a lib rule @@ -510,8 +542,10 @@ def test_lib_rules(): lib: true features: - api: CreateFileA - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: a standard rule @@ -521,17 +555,17 @@ def test_lib_rules(): lib: false features: - api: CreateFileW - """)), - ] - ) + """) + ), + ]) # lib rules are added to the rule set assert len(rules.function_rules) == 2 def test_subscope_rules(): - rules = capa.rules.RuleSet( - [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test function subscope @@ -545,8 +579,10 @@ def test_subscope_rules(): - and: - characteristic: nzxor - characteristic: loop - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test process subscope @@ -559,8 +595,10 @@ def test_subscope_rules(): - process: - and: - substring: "http://" - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test thread subscope @@ -572,8 +610,10 @@ def test_subscope_rules(): - string: "explorer.exe" - thread: - api: HttpOpenRequestW - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test call subscope @@ -585,9 +625,9 @@ def test_subscope_rules(): - string: "explorer.exe" - call: - api: HttpOpenRequestW - """)), - ] - ) + """) + ), + ]) # the file rule scope will have four rules: # - `test function subscope`, `test process subscope` and # `test thread subscope` for the static scope @@ -614,9 +654,9 @@ def test_subscope_rules(): def test_duplicate_rules(): with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.RuleSet( - [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule-name @@ -625,8 +665,10 @@ def test_duplicate_rules(): dynamic: process features: - api: CreateFileA - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule-name @@ -635,16 +677,16 @@ def test_duplicate_rules(): dynamic: process features: - api: CreateFileW - """)), - ] - ) + """) + ), + ]) def test_missing_dependency(): with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.RuleSet( - [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: dependent rule @@ -653,14 +695,15 @@ def test_missing_dependency(): dynamic: process features: - match: missing rule - """)), - ] - ) + """) + ), + ]) def test_invalid_rules(): with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -669,10 +712,12 @@ def test_invalid_rules(): dynamic: process features: - characteristic: number(1) - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -681,11 +726,13 @@ def test_invalid_rules(): dynamic: process features: - characteristic: count(number(100)) - """)) + """) + ) # att&ck and mbc must be lists with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -695,9 +742,11 @@ def test_invalid_rules(): att&ck: Tactic::Technique::Subtechnique [Identifier] features: - number: 1 - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -707,9 +756,11 @@ def test_invalid_rules(): mbc: Objective::Behavior::Method [Identifier] features: - number: 1 - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -718,9 +769,11 @@ def test_invalid_rules(): behavior: process features: - number: 1 - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -729,9 +782,11 @@ def test_invalid_rules(): dynamic: process features: - number: 1 - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -740,9 +795,11 @@ def test_invalid_rules(): dynamic: process features: - number: 1 - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -751,7 +808,8 @@ def test_invalid_rules(): dynamic: function features: - number: 1 - """)) + """) + ) def test_number_symbol(): @@ -828,7 +886,8 @@ def test_count_api(): def test_invalid_number(): with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -837,10 +896,12 @@ def test_invalid_number(): dynamic: process features: - number: "this is a string" - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -849,10 +910,12 @@ def test_invalid_number(): dynamic: process features: - number: 2= - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -861,7 +924,8 @@ def test_invalid_number(): dynamic: process features: - number: symbol name = 2 - """)) + """) + ) def test_offset_symbol(): @@ -913,7 +977,8 @@ def test_count_offset_symbol(): def test_invalid_offset(): with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -922,10 +987,12 @@ def test_invalid_offset(): dynamic: process features: - offset: "this is a string" - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -934,10 +1001,12 @@ def test_invalid_offset(): dynamic: process features: - offset: 2= - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -946,12 +1015,14 @@ def test_invalid_offset(): dynamic: process features: - offset: symbol name = 2 - """)) + """) + ) def test_invalid_string_values_int(): with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -960,10 +1031,12 @@ def test_invalid_string_values_int(): dynamic: process features: - string: 123 - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -972,7 +1045,8 @@ def test_invalid_string_values_int(): dynamic: process features: - string: 0x123 - """)) + """) + ) def test_explicit_string_values_int(): @@ -1054,9 +1128,9 @@ def test_substring_description(): def test_filter_rules(): - rules = capa.rules.RuleSet( - [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule 1 @@ -1067,8 +1141,10 @@ def test_filter_rules(): - joe features: - api: CreateFile - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule 2 @@ -1077,18 +1153,18 @@ def test_filter_rules(): dynamic: process features: - string: joe - """)), - ] - ) + """) + ), + ]) rules = rules.filter_rules_by_meta("joe") assert len(rules) == 1 assert "rule 1" in rules.rules def test_filter_rules_dependencies(): - rules = capa.rules.RuleSet( - [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule 1 @@ -1097,8 +1173,10 @@ def test_filter_rules_dependencies(): dynamic: process features: - match: rule 2 - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule 2 @@ -1107,8 +1185,10 @@ def test_filter_rules_dependencies(): dynamic: process features: - match: rule 3 - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule 3 @@ -1117,9 +1197,9 @@ def test_filter_rules_dependencies(): dynamic: process features: - api: CreateFile - """)), - ] - ) + """) + ), + ]) rules = rules.filter_rules_by_meta("rule 1") assert len(rules.rules) == 3 assert "rule 1" in rules.rules @@ -1129,9 +1209,9 @@ def test_filter_rules_dependencies(): def test_filter_rules_missing_dependency(): with pytest.raises(capa.rules.InvalidRule): - capa.rules.RuleSet( - [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule 1 @@ -1142,14 +1222,15 @@ def test_filter_rules_missing_dependency(): - joe features: - match: rule 2 - """)), - ] - ) + """) + ), + ]) def test_rules_namespace_dependencies(): rules = [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule 1 @@ -1159,8 +1240,10 @@ def test_rules_namespace_dependencies(): namespace: ns1/nsA features: - api: CreateFile - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule 2 @@ -1170,8 +1253,10 @@ def test_rules_namespace_dependencies(): namespace: ns1/nsB features: - api: CreateFile - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule 3 @@ -1180,8 +1265,10 @@ def test_rules_namespace_dependencies(): dynamic: process features: - match: ns1/nsA - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: rule 4 @@ -1190,7 +1277,8 @@ def test_rules_namespace_dependencies(): dynamic: process features: - match: ns1 - """)), + """) + ), ] r3 = {r.name for r in capa.rules.get_rules_and_dependencies(rules, "rule 3")} @@ -1281,7 +1369,8 @@ def test_arch_features(): def test_property_access(): - r = capa.rules.Rule.from_yaml(textwrap.dedent(""" + r = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -1290,7 +1379,8 @@ def test_property_access(): dynamic: process features: - property/read: System.IO.FileInfo::Length - """)) + """) + ) assert bool(r.evaluate({Property("System.IO.FileInfo::Length", access=FeatureAccess.READ): {ADDR1}})) is True assert bool(r.evaluate({Property("System.IO.FileInfo::Length"): {ADDR1}})) is False @@ -1298,7 +1388,8 @@ def test_property_access(): def test_property_access_symbol(): - r = capa.rules.Rule.from_yaml(textwrap.dedent(""" + r = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -1307,23 +1398,21 @@ def test_property_access_symbol(): dynamic: process features: - property/read: System.IO.FileInfo::Length = some property - """)) + """) + ) assert ( bool( - r.evaluate( - { - Property("System.IO.FileInfo::Length", access=FeatureAccess.READ, description="some property"): { - ADDR1 - } - } - ) + r.evaluate({ + Property("System.IO.FileInfo::Length", access=FeatureAccess.READ, description="some property"): {ADDR1} + }) ) is True ) def test_translate_com_features(): - r = capa.rules.Rule.from_yaml(textwrap.dedent(""" + r = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -1334,7 +1423,8 @@ def test_translate_com_features(): - com/class: WICPngDecoder # 389ea17b-5078-4cde-b6ef-25c15175c751 WICPngDecoder # e018945b-aa86-4008-9bd4-6777a1e40c11 WICPngDecoder - """)) + """) + ) com_name = "WICPngDecoder" com_features = [ capa.features.common.Bytes(b"{\xa1\x9e8xP\xdeL\xb6\xef%\xc1Qu\xc7Q", f"CLSID_{com_name} as bytes"), @@ -1348,39 +1438,46 @@ def test_translate_com_features(): def test_invalid_com_features(): # test for unknown COM class with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule features: - com/class: invalid_com - """)) + """) + ) # test for unknown COM interface with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule features: - com/interface: invalid_com - """)) + """) + ) # test for invalid COM type # valid_com_types = "class", "interface" with pytest.raises(capa.rules.InvalidRule): - _ = capa.rules.Rule.from_yaml(textwrap.dedent(""" + _ = capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule features: - com/invalid_COM_type: WICPngDecoder - """)) + """) + ) def test_circular_dependency(): rules = [ - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule 1 @@ -1392,8 +1489,10 @@ def test_circular_dependency(): - or: - match: test rule 2 - api: kernel32.VirtualAlloc - """)), - capa.rules.Rule.from_yaml(textwrap.dedent(""" + """) + ), + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule 2 @@ -1403,7 +1502,8 @@ def test_circular_dependency(): lib: true features: - match: test rule 1 - """)), + """) + ), ] with pytest.raises(capa.rules.InvalidRule): list(capa.rules.get_rules_and_dependencies(rules, rules[0].name)) diff --git a/tests/test_rules_insn_scope.py b/tests/test_rules_insn_scope.py index 1b393dc560..9db9c69950 100644 --- a/tests/test_rules_insn_scope.py +++ b/tests/test_rules_insn_scope.py @@ -21,7 +21,8 @@ def test_rule_scope_instruction(): - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -33,10 +34,12 @@ def test_rule_scope_instruction(): - mnemonic: mov - arch: i386 - os: windows - """)) + """) + ) with pytest.raises(capa.rules.InvalidRule): - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -45,11 +48,14 @@ def test_rule_scope_instruction(): dynamic: unsupported features: - characteristic: embedded pe - """)) + """) + ) def test_rule_subscope_instruction(): - rules = capa.rules.RuleSet([capa.rules.Rule.from_yaml(textwrap.dedent(""" + rules = capa.rules.RuleSet([ + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -63,7 +69,9 @@ def test_rule_subscope_instruction(): - mnemonic: mov - arch: i386 - os: windows - """))]) + """) + ) + ]) # the function rule scope will have one rules: # - `test rule` assert len(rules.function_rules) == 1 @@ -74,7 +82,8 @@ def test_rule_subscope_instruction(): def test_scope_instruction_implied_and(): - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -87,11 +96,13 @@ def test_scope_instruction_implied_and(): - mnemonic: mov - arch: i386 - os: windows - """)) + """) + ) def test_scope_instruction_description(): - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -105,9 +116,11 @@ def test_scope_instruction_description(): - mnemonic: mov - arch: i386 - os: windows - """)) + """) + ) - capa.rules.Rule.from_yaml(textwrap.dedent(""" + capa.rules.Rule.from_yaml( + textwrap.dedent(""" rule: meta: name: test rule @@ -121,4 +134,5 @@ def test_scope_instruction_description(): - mnemonic: mov - arch: i386 - os: windows - """)) + """) + ) diff --git a/web/rules/scripts/build_root.py b/web/rules/scripts/build_root.py index 7239d3219e..48736a3741 100644 --- a/web/rules/scripts/build_root.py +++ b/web/rules/scripts/build_root.py @@ -268,9 +268,9 @@ def generate_html(categories_data, color_map):
-
{card['namespace']}
- -
{', '.join(card['authors'])}
+
{card["namespace"]}
+ +
{", ".join(card["authors"])}
""" From 7c4940cd3aaa3ab8e65953feb73ff47943a604d4 Mon Sep 17 00:00:00 2001 From: Mike Hunhoff Date: Tue, 7 Apr 2026 16:59:39 +0000 Subject: [PATCH 16/16] address review comments --- .github/ruff.toml | 7 +++++-- .github/workflows/ruff-format.yml | 2 +- .justfile | 2 +- .pre-commit-config.yaml | 3 +-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/ruff.toml b/.github/ruff.toml index ba68383251..5ca01d9941 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -1,5 +1,6 @@ line-length = 120 preview = true # Required to enable pre-release copyright header checks (CPY001) +explicit-preview-rules = true exclude = [ # Exclude a variety of commonly ignored directories. @@ -26,7 +27,8 @@ exclude = [ "venv", # protobuf generated files "*_pb2.py", - "*_pb2.pyi" + "*_pb2.pyi", + "rules" ] lint.select = [ @@ -42,7 +44,8 @@ lint.select = [ "G", # flake8-logging-format (logging statement validation) "TD", # flake8-todos (TODO formatting requirements) "PTH", # flake8-use-pathlib (migration from os.path to Pathlib) - "UP" # pyupgrade (modern Python syntax upgrades) + "UP", # pyupgrade (modern Python syntax upgrades) + "CPY001", # flake8-copyright ] # Allow autofix for all enabled rules (when `--fix`) is provided. diff --git a/.github/workflows/ruff-format.yml b/.github/workflows/ruff-format.yml index 9c5eed2565..51a4a342f8 100644 --- a/.github/workflows/ruff-format.yml +++ b/.github/workflows/ruff-format.yml @@ -35,7 +35,7 @@ jobs: pip install -r requirements.txt pip install -e .[dev,scripts] - - name: Run ruff/continue + - name: Run ruff check --fix/continue # ruff returns non-zero error code after formatting, which is what we expect continue-on-error: true run: pre-commit run ruff --all-files diff --git a/.justfile b/.justfile index f8d017fb25..3b0983048f 100644 --- a/.justfile +++ b/.justfile @@ -11,7 +11,7 @@ pre-commit run deptry --hook-stage manual --all-files @lint: - -just ruff-format -just ruff + -just ruff-format -just mypy -just deptry diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index acebaad7b6..734fdf1e5b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,7 +35,6 @@ repos: - "scripts/" - "tests/" - "web/rules/scripts/" - exclude: '.*_pb2\.py$' always_run: true pass_filenames: false @@ -48,13 +47,13 @@ repos: entry: ruff args: - "check" + - "--fix" - "--config" - ".github/ruff.toml" - "capa/" - "scripts/" - "tests/" - "web/rules/scripts/" - exclude: '.*_pb2\.py$' always_run: true pass_filenames: false