diff --git a/CHANGELOG.md b/CHANGELOG.md index 09dfb040bb..4a64144989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - ### Bug Fixes +- fix: prevent capafmt from corrupting rules whose namespace contains `features` @SkxOverKill #3143 - fix lots of linter errors identified by pyright @williballenthin #3052 - fix: render_default always returns empty string @williballenthin #3012 - fix: elf.py vdso_guess exception handler clobbers symtab_guess @williballenthin #3013 diff --git a/capa/rules/__init__.py b/capa/rules/__init__.py index ef4e372c70..434ee79186 100644 --- a/capa/rules/__init__.py +++ b/capa/rules/__init__.py @@ -1374,7 +1374,10 @@ def move_to_end(m, k): # see #263 # only do this for the features section, so the meta description doesn't get reformatted # assumes features section always exists - features_offset = doc.find("features") + # anchor on the section header rather than the bare word "features", + # since a rule's namespace (e.g. `impact/features/persistence`) + # may contain that substring and mislead the offset. see #3134 + features_offset = doc.find("\n features:") doc = doc[:features_offset] + doc[features_offset:].replace(" description:", " description:") # for negative hex numbers, yaml dump outputs: diff --git a/tests/test_fmt.py b/tests/test_fmt.py index 05b1fafcd7..20587b4766 100644 --- a/tests/test_fmt.py +++ b/tests/test_fmt.py @@ -148,3 +148,29 @@ def test_rule_reformat_string_description(): rule = capa.rules.Rule.from_yaml(src) assert rule.to_yaml() == src + + +def test_rule_reformat_namespace_with_features(): + # regression test for #3134 + # a namespace containing the substring "features" + # (e.g. `impact/features/persistence`) must not be mistaken for the + # `features:` section; otherwise the meta `description` gets + # incorrectly re-indented and the emitted YAML becomes invalid. + src = textwrap.dedent(""" + rule: + meta: + name: test rule + namespace: impact/features/persistence + authors: + - user@domain.com + description: this is a description + scopes: + static: function + dynamic: process + features: + - and: + - string: foo + description: bar + """).lstrip() + + assert capa.rules.Rule.from_yaml(src).to_yaml() == src