From eb81901d715afcbc6f0f0eb5cb3c13fe7e0a6ef5 Mon Sep 17 00:00:00 2001 From: Willi Ballenthin Date: Wed, 22 Apr 2026 17:29:38 +0300 Subject: [PATCH] fix: correct capa/subscope-rule key in RuleMetadata.from_capa `RuleMetadata.from_capa` used `rule.meta.get("capa/subscope", False)` and `Field(False, alias="capa/subscope")`, but the actual key set by `_extract_subscope_rules_rec` is `"capa/subscope-rule"`. This caused `is_subscope_rule` to always be `False` in every `RuleMetadata` instance, making downstream filters in `render/utils.py`, `render/vverbose.py`, and `scripts/import-to-ida.py` ineffective (though subscope rules are already excluded from `ResultDocument` before reaching those callers). --- CHANGELOG.md | 1 + capa/render/proto/__init__.py | 2 +- capa/render/result_document.py | 4 ++-- scripts/import-to-bn.py | 2 +- tests/test_result_document.py | 28 ++++++++++++++++++++++++++++ 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea46b6e2..1b5554f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - fix: loader.py reads entire file for magic byte check @williballenthin #3029 - fix: freeze/__init__.py: logically impossible condition @williballenthin #3030 - fix: EXTENSIONS_ELF never referenced @williballenthin #3031 +- fix: correct capa/subscope-rule key in RuleMetadata so is_subscope_rule is no longer always False @williballenthin - fix: remove unreachable backports.functools_lru_cache fallback and dead dependency @williballenthin - fix: Scopes.from_dict uses cls instead of self so subclasses return the correct type @williballenthin diff --git a/capa/render/proto/__init__.py b/capa/render/proto/__init__.py index ae25992e..c3908e66 100644 --- a/capa/render/proto/__init__.py +++ b/capa/render/proto/__init__.py @@ -971,7 +971,7 @@ def rule_metadata_from_pb2(pb: capa_pb2.RuleMetadata) -> rd.RuleMetadata: examples=tuple(pb.examples), description=pb.description, lib=pb.lib, - is_subscope_rule=pb.is_subscope_rule, # type: ignore # Pydantic alias capa/subscope; populate_by_name=True + is_subscope_rule=pb.is_subscope_rule, # type: ignore # Pydantic alias capa/subscope-rule; populate_by_name=True maec=maec_from_pb2(pb.maec), ) diff --git a/capa/render/result_document.py b/capa/render/result_document.py index 55e0cdbc..d1b8005e 100644 --- a/capa/render/result_document.py +++ b/capa/render/result_document.py @@ -660,7 +660,7 @@ class RuleMetadata(FrozenModel): description: str lib: bool = Field(False, alias="lib") - is_subscope_rule: bool = Field(False, alias="capa/subscope") + is_subscope_rule: bool = Field(False, alias="capa/subscope-rule") maec: MaecMetadata @classmethod @@ -676,7 +676,7 @@ class RuleMetadata(FrozenModel): examples=rule.meta.get("examples", []), description=rule.meta.get("description", ""), lib=rule.meta.get("lib", False), - is_subscope_rule=rule.meta.get("capa/subscope", False), # type: ignore # Pydantic alias capa/subscope; populate_by_name=True + is_subscope_rule=rule.meta.get("capa/subscope-rule", False), # type: ignore # Pydantic alias capa/subscope-rule; populate_by_name=True maec=MaecMetadata( analysis_conclusion=rule.meta.get("maec/analysis-conclusion"), # type: ignore # Pydantic alias analysis-conclusion analysis_conclusion_ov=rule.meta.get("maec/analysis-conclusion-ov"), # type: ignore # Pydantic alias diff --git a/scripts/import-to-bn.py b/scripts/import-to-bn.py index 5496f895..d87924d4 100644 --- a/scripts/import-to-bn.py +++ b/scripts/import-to-bn.py @@ -102,7 +102,7 @@ def load_analysis(bv): for rule in doc["rules"].values(): if rule["meta"].get("lib"): continue - if rule["meta"].get("capa/subscope"): + if rule["meta"].get("capa/subscope-rule"): continue if rule["meta"]["scopes"].get("static") != "function": continue diff --git a/tests/test_result_document.py b/tests/test_result_document.py index 10a33981..0dab9f0f 100644 --- a/tests/test_result_document.py +++ b/tests/test_result_document.py @@ -13,6 +13,7 @@ # limitations under the License. import copy +import textwrap import pytest import fixtures @@ -296,3 +297,30 @@ def test_rdoc_to_capa(): meta, capabilites = rd.to_capa() assert isinstance(meta, rdoc.Metadata) assert isinstance(capabilites, Capabilities) + + +def test_rule_metadata_is_subscope_rule_alias(): + rule = capa.rules.Rule.from_yaml( + textwrap.dedent(""" + rule: + meta: + name: test rule + scopes: + static: function + dynamic: process + authors: + - test + features: + - api: CreateFile + """) + ) + meta = rdoc.RuleMetadata.from_capa(rule) + assert meta.is_subscope_rule is False + + raw = meta.model_dump(by_alias=True) + assert "capa/subscope-rule" in raw + assert raw["capa/subscope-rule"] is False + + raw["capa/subscope-rule"] = True + meta_true = rdoc.RuleMetadata.model_validate(raw) + assert meta_true.is_subscope_rule is True