linter: validate dynamic example offsets

closes #3058
This commit is contained in:
Willi Ballenthin
2026-05-07 12:10:59 +02:00
committed by Willi Ballenthin
parent 8e464e6041
commit 8fca21f808
3 changed files with 91 additions and 9 deletions
+1
View File
@@ -60,6 +60,7 @@
- fix: FeatureRegexRegistryControlSetMatchIncomplete now checks all Regex features instead of returning after the first @williballenthin (SURF-84)
- fix: MissingStaticScope and MissingDynamicScope lint checks guard against absent scopes dict to prevent TypeError @williballenthin (SURF-83)
- fix: MissingExampleOffset lint now reads scopes.static instead of obsolete scope key @williballenthin (SURF-82)
- fix: extend MissingExampleOffset lint to validate dynamic examples using (pid:N,tid:N,call:N) format @williballenthin #3058
- fix: invert scope filter in import-to-ida.py so function-scope rules are annotated instead of skipped @williballenthin (SURF-81)
- fix: remove dead string literal in test_detect_duplicate_features @williballenthin (SURF-80)
- fix: remove duplicate Rule.from_yaml call in test_scope_instruction_description @williballenthin (SURF-79)
+32 -8
View File
@@ -247,17 +247,41 @@ class MissingExamples(Lint):
class MissingExampleOffset(Lint):
name = "missing example offset"
recommendation = "Add offset of example function"
recommendation = "Add offset of example (static: hash:0xADDR, dynamic: hash:(pid:N,tid:N,call:N))"
STATIC_SCOPES_NEEDING_OFFSET = ("function", "basic block")
DYNAMIC_SCOPES_NEEDING_OFFSET = ("process", "thread", "call", "span of calls")
def check_rule(self, ctx: Context, rule: Rule):
static_scope = rule.meta.get("scopes", {}).get("static")
if static_scope in ("function", "basic block"):
examples = rule.meta.get("examples")
if isinstance(examples, list):
for example in examples:
if example and ":" not in example:
logger.debug("example: %s", example)
scopes = rule.meta.get("scopes", {})
static_scope = scopes.get("static")
dynamic_scope = scopes.get("dynamic")
examples = rule.meta.get("examples")
if not isinstance(examples, list):
return False
for example in examples:
if not example:
continue
example_id, _, offset = example.partition(":")
sample_path = ctx.samples.get(example_id)
is_dynamic_sample = sample_path is not None and "dynamic" in sample_path.parts
if is_dynamic_sample:
if dynamic_scope in self.DYNAMIC_SCOPES_NEEDING_OFFSET:
if not offset or not offset.startswith("("):
logger.debug("example: %s (missing dynamic offset)", example)
return True
else:
if static_scope in self.STATIC_SCOPES_NEEDING_OFFSET:
if not offset:
logger.debug("example: %s (missing static offset)", example)
return True
return False
class ExampleFileDNE(Lint):
+58 -1
View File
@@ -282,7 +282,6 @@ def test_missing_example_offset_uses_scopes():
import lint as lint_module
lint_instance = lint_module.MissingExampleOffset()
ctx = lint_module.Context(samples={}, rules=capa.rules.RuleSet([]), is_thorough=False)
function_scope_rule_missing_offset = capa.rules.Rule.from_yaml(
textwrap.dedent("""
@@ -298,6 +297,10 @@ def test_missing_example_offset_uses_scopes():
- api: CreateFile
""")
)
rules = capa.rules.RuleSet([function_scope_rule_missing_offset])
ctx = lint_module.Context(samples={}, rules=rules, is_thorough=False)
assert lint_instance.check_rule(ctx, function_scope_rule_missing_offset) is True
function_scope_rule_with_offset = capa.rules.Rule.from_yaml(
@@ -332,6 +335,60 @@ def test_missing_example_offset_uses_scopes():
)
assert lint_instance.check_rule(ctx, file_scope_rule_no_offset) is not True
ctx_with_dynamic = lint_module.Context(
samples={"abc123_min_archive.zip": Path("tests/data/dynamic/vmray/abc123_min_archive.zip")},
rules=rules,
is_thorough=False,
)
dynamic_example_missing_offset = capa.rules.Rule.from_yaml(
textwrap.dedent("""
rule:
meta:
name: test rule dynamic example missing offset
scopes:
static: basic block
dynamic: call
examples:
- abc123_min_archive.zip
features:
- api: CreateFile
""")
)
assert lint_instance.check_rule(ctx_with_dynamic, dynamic_example_missing_offset) is True
dynamic_example_with_offset = capa.rules.Rule.from_yaml(
textwrap.dedent("""
rule:
meta:
name: test rule dynamic example with offset
scopes:
static: basic block
dynamic: call
examples:
- abc123_min_archive.zip:(pid:2932,tid:2928,call:354)
features:
- api: CreateFile
""")
)
assert lint_instance.check_rule(ctx_with_dynamic, dynamic_example_with_offset) is not True
dynamic_file_scope_no_offset = capa.rules.Rule.from_yaml(
textwrap.dedent("""
rule:
meta:
name: test rule dynamic file scope no offset
scopes:
static: file
dynamic: file
examples:
- abc123_min_archive.zip
features:
- string: test
""")
)
assert lint_instance.check_rule(ctx_with_dynamic, dynamic_file_scope_no_offset) is not True
def test_feature_regex_registry_control_set_checks_all_features():
sys.path.insert(0, str(CD / ".." / "scripts"))