diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cbecb560..d78af45b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -21,10 +21,9 @@ "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.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 ce96a6d8..056be94a 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. [isort](https://pypi.org/project/isort/) (with line width 120 and 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/flake8.ini b/.github/flake8.ini deleted file mode 100644 index e7ca4090..00000000 --- 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 c3a1de6d..5ca01d99 100644 --- a/.github/ruff.toml +++ b/.github/ruff.toml @@ -1,18 +1,6 @@ -# 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 +preview = true # Required to enable pre-release copyright header checks (CPY001) +explicit-preview-rules = true exclude = [ # Exclude a variety of commonly ignored directories. @@ -39,5 +27,77 @@ exclude = [ "venv", # protobuf generated files "*_pb2.py", - "*_pb2.pyi" + "*_pb2.pyi", + "rules" ] + +lint.select = [ + "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) + "CPY001", # flake8-copyright +] + +# Allow autofix for all enabled rules (when `--fix`) is provided. +lint.fixable = ["ALL"] +lint.unfixable = [] + +# Map existing flake8 ignores to maintain strict parity +lint.ignore = [ + # 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 + "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 + "UP028", # Replace yield over for loop with yield from + "C409", # Unnecessary list comprehension passed to tuple() + # TODO(mike-hunhoff): address circular dependencies + # https://github.com/mandiant/capa/issues/2996 + "F401", # Unused imports +] + +[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"] +"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" +min-file-size = 1 + +[lint.isort] +length-sort = true diff --git a/.github/workflows/black-format.yml b/.github/workflows/ruff-format.yml similarity index 76% rename from .github/workflows/black-format.yml rename to .github/workflows/ruff-format.yml index c8cf2042..51a4a342 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 @@ -35,13 +35,15 @@ jobs: pip install -r requirements.txt pip install -e .[dev,scripts] - - name: Run isort - run: pre-commit run isort --all-files - - - name: Run black/continue - # black returns non-zero error code after formatting, which is what we expect + - 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 black --all-files + run: pre-commit run ruff --all-files + + - 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 ruff-format --all-files - name: Check for changes id: changes @@ -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" git push diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2b157abe..b4a9953d 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 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 91953dd1..3b098304 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-format: + pre-commit run ruff-format --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 ruff-format -just mypy -just deptry diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0cf0bdd..734fdf1e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,53 +6,31 @@ # ❯ 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 -# isort....................................................................Passed -# black....................................................................Passed +# ruff-format..............................................................Passed # ruff.....................................................................Passed -# flake8...................................................................Passed # mypy.....................................................................Passed # # run a single linter like: # -# ❯ pre-commit run --all-files isort -# isort....................................................................Passed +# ❯ pre-commit run --all-files ruff +# ruff.....................................................................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: - - 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/" @@ -69,6 +47,7 @@ repos: entry: ruff args: - "check" + - "--fix" - "--config" - ".github/ruff.toml" - "capa/" @@ -78,24 +57,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/CHANGELOG.md b/CHANGELOG.md index e1cc6d23..d613005b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ ### capa Explorer IDA Pro plugin ### Development +- 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/capabilities/common.py b/capa/capabilities/common.py index 9a6e13b0..6575fe7b 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 3c98a16c..e7e6594a 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 893887f7..1047713b 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/capa/engine.py b/capa/engine.py index fc1919fa..1296198c 100644 --- a/capa/engine.py +++ b/capa/engine.py @@ -216,7 +216,7 @@ class Some(Statement): # 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) diff --git a/capa/features/extractors/binexport2/arch/intel/insn.py b/capa/features/extractors/binexport2/arch/intel/insn.py index ed0f1863..36fd8cc6 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/features/extractors/binexport2/helpers.py b/capa/features/extractors/binexport2/helpers.py index c359ecb1..9eab3972 100644 --- a/capa/features/extractors/binexport2/helpers.py +++ b/capa/features/extractors/binexport2/helpers.py @@ -662,14 +662,10 @@ class BinExport2InstructionPatternMatcher: @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 42656e47..4bfbb7b2 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/ghidra/function.py b/capa/features/extractors/ghidra/function.py index 8fa8cc71..a46d8b15 100644 --- a/capa/features/extractors/ghidra/function.py +++ b/capa/features/extractors/ghidra/function.py @@ -26,14 +26,14 @@ from capa.features.extractors.base_extractor import FunctionHandle 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/features/extractors/vmray/call.py b/capa/features/extractors/vmray/call.py index 888e1b39..e1f928a3 100644 --- a/capa/features/extractors/vmray/call.py +++ b/capa/features/extractors/vmray/call.py @@ -26,14 +26,12 @@ from capa.features.extractors.base_extractor import CallHandle, ThreadHandle, Pr 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/helpers.py b/capa/helpers.py index 27c757dc..503d4ca6 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/capa/ida/plugin/form.py b/capa/ida/plugin/form.py index 800453bb..8c076cc6 100644 --- a/capa/ida/plugin/form.py +++ b/capa/ida/plugin/form.py @@ -639,7 +639,7 @@ class CapaExplorerForm(idaapi.PluginForm): 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/item.py b/capa/ida/plugin/item.py index 0510d5f9..3292de1c 100644 --- a/capa/ida/plugin/item.py +++ b/capa/ida/plugin/item.py @@ -47,7 +47,7 @@ class CapaExplorerDataItem: """initialize item""" self.pred = parent self._data = data - self._children: list["CapaExplorerDataItem"] = [] + self._children: list[CapaExplorerDataItem] = [] self._checked = False self._can_check = can_check diff --git a/capa/ida/plugin/view.py b/capa/ida/plugin/view.py index a442f4d1..4c21de1c 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 @@ class CapaExplorerRulegenEditor(QtWidgets.QTreeWidget): 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/main.py b/capa/main.py index 368d3ecd..91147353 100644 --- a/capa/main.py +++ b/capa/main.py @@ -77,7 +77,6 @@ from capa.exceptions import ( 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:] diff --git a/capa/render/proto/__init__.py b/capa/render/proto/__init__.py index 53f942c5..8c204fca 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 3ad71752..c8d194d2 100644 --- a/capa/render/result_document.py +++ b/capa/render/result_document.py @@ -393,7 +393,6 @@ class Match(FrozenModel): ) 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 @@ class Match(FrozenModel): # # 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 @@ class Match(FrozenModel): 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 903d0cb7..e61b6961 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 fe378a80..491fbbf8 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 e3645b50..3e4062be 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -98,10 +98,7 @@ 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 - - [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://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 @@ -115,17 +112,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-format..............................................................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 8e2e336f..b7d869d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -133,20 +133,7 @@ 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.20.0", "mypy-protobuf==5.0.0", "PyGithub==2.9.0", @@ -166,7 +153,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 = [ @@ -218,22 +205,10 @@ known_first_party = [ [tool.deptry.per_rule_ignores] # dependencies defined but not used in the codebase DEP002 = [ - "black", "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 851c34ad..080ddcd5 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/capa2sarif.py b/scripts/capa2sarif.py index b16d9f08..be4c815c 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"]["namespace"] if "namespace" in data_rules[key]["meta"] else [], - "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 c489c8d5..c5953cd7 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 85515699..02cfb297 100644 --- a/scripts/profile-memory.py +++ b/scripts/profile-memory.py @@ -21,19 +21,17 @@ tracemalloc.start() 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,13 +39,13 @@ 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(): - # 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 @@ -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 01e15acc..bdadc292 100644 --- a/scripts/setup-linter-dependencies.py +++ b/scripts/setup-linter-dependencies.py @@ -97,24 +97,20 @@ class MitreExtractor: """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 @@ class MitreExtractor: """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/scripts/show-features.py b/scripts/show-features.py index 0004f6c7..8858f72d 100644 --- a/scripts/show-features.py +++ b/scripts/show-features.py @@ -95,7 +95,7 @@ logger = logging.getLogger("capa.show-features") 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 966f56d8..45277ccb 100644 --- a/scripts/show-unused-features.py +++ b/scripts/show-unused-features.py @@ -40,7 +40,7 @@ logger = logging.getLogger("show-unused-features") 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 6f15d036..2b8c2918 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/tests/test_binexport_accessors.py b/tests/test_binexport_accessors.py index 4c5362c4..7cdb9bc5 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 9a689568..56406d62 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"); @@ -20,10 +19,10 @@ from capa.features.extractors.base_extractor import FunctionFilter 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 @@ -37,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 @@ -50,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 @@ -67,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 @@ -77,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 @@ -91,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 @@ -108,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 @@ -123,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 @@ -133,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 @@ -144,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 @@ -163,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"] @@ -174,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 @@ -184,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 @@ -202,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 @@ -219,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 @@ -238,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"]} @@ -248,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 @@ -262,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 d024e5ba..29cadb50 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 6db2a9fa..0236cd93 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 74129f4f..5fc319d1 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 9c171f2e..28d5e0c4 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 72ebd762..6bd10117 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"); @@ -54,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 ) @@ -96,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 @@ -107,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 @@ -117,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 @@ -127,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 @@ -137,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 @@ -147,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 @@ -160,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 @@ -179,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 @@ -292,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 @@ -322,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 139e2434..674b71b3 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 be4762f2..4d71c67b 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"); @@ -110,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 5fb3b3b2..a7e85901 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 59f6ff7d..3aa0c97d 100644 --- a/tests/test_rule_cache.py +++ b/tests/test_rule_cache.py @@ -22,7 +22,8 @@ import capa.rules 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 @@ R1 = capa.rules.Rule.from_yaml(textwrap.dedent(""" - 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 @@ R2 = capa.rules.Rule.from_yaml(textwrap.dedent(""" - and: - number: 3 - number: 4 - """)) + """) +) def test_ruleset_cache_ids(): diff --git a/tests/test_rules.py b/tests/test_rules.py index 60d3e4e7..689659df 100644 --- a/tests/test_rules.py +++ b/tests/test_rules.py @@ -149,7 +149,8 @@ def test_rule_descriptions(): 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 1b393dc5..9db9c699 100644 --- a/tests/test_rules_insn_scope.py +++ b/tests/test_rules_insn_scope.py @@ -21,7 +21,8 @@ import capa.rules 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 7239d321..48736a37 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"])}
""" diff --git a/web/rules/scripts/build_rules.py b/web/rules/scripts/build_rules.py index e1c50936..8aa3961a 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():