Skip to content

Global Sub-rule Suppression Causes Silent Loss of Independent Capability Matches in Default CLI Output #3084

@mike-hunhoff

Description

@mike-hunhoff

Summary

There is a silent information-loss correctness bug in capa's default summary renderer (capa/render/default.py).

To reduce clutter in the default summary table, capa suppresses low-level "sub-rules" (rules that are matched as dependencies by higher-level parent rules). However, the current suppression logic is global and strictly name-based.

If a sub-rule Rule B is matched by a parent rule Rule A in Function 1, "Rule B" is added to the global suppression set. If "Rule B" also matches completely independently in Function 2 (where Rule A does not match), Rule B is still completely suppressed. As a result, the independent match at Function 2 is silently hidden from the default CLI summary output, leading to incomplete capability reports.


1. Technical Analysis & The Defect

In capa/render/default.py, the function find_subrule_matches traverses the match trees of all capability rules and adds the names of all successfully matched sub-rules to a global set:

# From capa/render/default.py
        elif isinstance(match.node, rd.FeatureNode) and isinstance(match.node.feature, frzf.MatchFeature):
            # Adds the sub-rule name to a global set
            matches.add(match.node.feature.match)

Then, in render_capabilities, capa completely skips rendering any rule whose name exists in that global set:

# From capa/render/default.py
    for rule in rutils.capability_rules(doc):
        if rule.meta.name in subrule_matches:
            # Sub-rule name matches the global set; skip rendering entirely!
            continue

Because the check is global and based only on the name, all independent matches of the sub-rule elsewhere in the binary are silently discarded in the default CLI output view.


2. Proof of Concept (PoC)

This bug can be demonstrated using a simple ruleset with two mock rules:

  • rule_b (Low-level, e.g. encrypt data using RC4)
  • rule_a (High-level, e.g. Ransomware behavior, which depends on match: rule_b)

Consider a mock binary with two functions:

  • Function 1 (0x401000): Contains features triggering both rule_a and rule_b (so rule_b acts as a sub-rule).
  • Function 2 (0x402000): Contains features triggering only rule_b (so rule_b matches independently).

Expected Behavior (CLI Summary)

  • rule_a should show 1 match (at 0x401000).
  • rule_b should show 1 match (at 0x402000, representing the independent match that is not covered by rule_a).

Actual Behavior (Current CLI Summary)

  • rule_a shows 1 match (at 0x401000).
  • rule_b is completely hidden from the summary table! The independent match at 0x402000 is completely lost in the default view.

Verification in Verbose Mode (-v)

In verbose mode (which does not use suppression), both matches are correctly shown:

rule_a
scope    function
matches  0x401000

rule_b (2 matches)
scope    function
matches  0x401000
         0x402000

This confirms that rule_b did indeed match at 0x402000 independently, yet was silently suppressed from the default user summary.


Is this how we want capa to work?

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingquestionFurther information is requested

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions