Global Sub-rule Suppression Causes Silent Loss of Independent Capability Matches in Default CLI Output
- Dominant language
- Python
- Stars
- 6.2k
- Forks
- 726
- Avg merge
- 11d 11h
- Merged PRs (30d)
- 7
Description
### 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:
```python
# 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:
```python
# 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:
```text
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?
Contributor guide
Research direction
Start in capa/render/default.py by reading find_subrule_matches and render_capabilities, then compare the default summary with verbose (-v) output using the Rule A/Rule B scenario described in the issue. Trace how matches are associated with functions and how sub-rule names enter the suppression set. Done means independently matched sub-rules remain visible in the default summary while covered sub-rule matches stay suppressed.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- cli, reverse-engineering
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 52/100