mandiant / mandiant/capa

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

Open
#3,084 1 comment 0 reactions 0 assignees View on GitHub
bug question
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.