Files
capa/tests/test_rules.py
Ana Maria Martinez Gomez 3cd97ae9f2 [copyright + license] Fix headers
Replace the header from source code files using the following script:
```Python
for dir_path, dir_names, file_names in os.walk("capa"):
    for file_name in file_names:
        # header are only in `.py` and `.toml` files
        if file_name[-3:] not in (".py", "oml"):
            continue
        file_path = f"{dir_path}/{file_name}"
        f = open(file_path, "rb+")
        content = f.read()
        m = re.search(OLD_HEADER, content)
        if not m:
            continue
        print(f"{file_path}: {m.group('year')}")
        content = content.replace(m.group(0), NEW_HEADER % m.group("year"))
        f.seek(0)
        f.write(content)
```

Some files had the copyright headers inside a `"""` comment and needed
manual changes before applying the script. `hook-vivisect.py` and
`pyinstaller.spec` didn't include the license in the header and also
needed manual changes.

The old header had the confusing sentence `All rights reserved`, which
does not make sense for an open source license. Replace the header by
the default Google header that corrects this issue and keep capa
consistent with other Google projects.

Adapt the linter to work with the new header.

Replace also the copyright text in the `web/public/index.html` file for
consistency.
2025-01-15 08:52:42 -07:00

1626 lines
49 KiB
Python

# Copyright 2020 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import textwrap
import pytest
import capa.rules
import capa.engine
import capa.features.common
import capa.features.address
from capa.engine import Or
from capa.features.file import FunctionName
from capa.features.insn import API, Number, Offset, Property
from capa.features.common import (
OS,
OS_LINUX,
ARCH_I386,
FORMAT_PE,
ARCH_AMD64,
FORMAT_ELF,
OS_WINDOWS,
Arch,
Format,
String,
Substring,
FeatureAccess,
)
ADDR1 = capa.features.address.AbsoluteVirtualAddress(0x401001)
ADDR2 = capa.features.address.AbsoluteVirtualAddress(0x401002)
ADDR3 = capa.features.address.AbsoluteVirtualAddress(0x401003)
ADDR4 = capa.features.address.AbsoluteVirtualAddress(0x401004)
def test_rule_ctor():
r = capa.rules.Rule(
"test rule", capa.rules.Scopes(capa.rules.Scope.FUNCTION, capa.rules.Scope.FILE), Or([Number(1)]), {}
)
assert bool(r.evaluate({Number(0): {ADDR1}})) is False
assert bool(r.evaluate({Number(1): {ADDR2}})) is True
def test_rule_yaml():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
authors:
- user@domain.com
scopes:
static: function
dynamic: process
examples:
- foo1234
- bar5678
features:
- and:
- number: 1
- number: 2
"""
)
r = capa.rules.Rule.from_yaml(rule)
assert bool(r.evaluate({Number(0): {ADDR1}})) is False
assert bool(r.evaluate({Number(0): {ADDR1}, Number(1): {ADDR1}})) is False
assert bool(r.evaluate({Number(0): {ADDR1}, Number(1): {ADDR1}, Number(2): {ADDR1}})) is True
assert bool(r.evaluate({Number(0): {ADDR1}, Number(1): {ADDR1}, Number(2): {ADDR1}, Number(3): {ADDR1}})) is True
def test_rule_yaml_complex():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- and:
- number: 1
- number: 2
- or:
- number: 3
- 2 or more:
- number: 4
- number: 5
- number: 6
"""
)
r = capa.rules.Rule.from_yaml(rule)
assert bool(r.evaluate({Number(5): {ADDR1}, Number(6): {ADDR1}, Number(7): {ADDR1}, Number(8): {ADDR1}})) is True
assert bool(r.evaluate({Number(6): {ADDR1}, Number(7): {ADDR1}, Number(8): {ADDR1}})) is False
def test_rule_descriptions():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- description: and description
- number: 1 = number description
- string: mystring
description: string description
- string: '/myregex/'
description: regex description
- mnemonic: inc = mnemonic description
# TODO - count(number(2 = number description)): 2
- or:
- description: or description
- and:
- offset: 0x50 = offset description
- offset: 0x34 = offset description
- description: and description
- and:
- description: and description
"""
)
r = capa.rules.Rule.from_yaml(rule)
def rec(statement):
if isinstance(statement, capa.engine.Statement):
assert statement.description == statement.name.lower() + " description"
for child in statement.get_children():
rec(child)
else:
if isinstance(statement.value, str):
assert "description" not in statement.value
assert statement.description == statement.name + " description"
rec(r.statement)
def test_invalid_rule_statement_descriptions():
# statements can only have one description
with pytest.raises(capa.rules.InvalidRule):
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- number: 1 = This is the number 1
- description: description
- description: another description (invalid)
"""
)
)
def test_rule_yaml_not():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- number: 1
- not:
- number: 2
"""
)
r = capa.rules.Rule.from_yaml(rule)
assert bool(r.evaluate({Number(1): {ADDR1}})) is True
assert bool(r.evaluate({Number(1): {ADDR1}, Number(2): {ADDR1}})) is False
def test_rule_yaml_count():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): 1
"""
)
r = capa.rules.Rule.from_yaml(rule)
assert bool(r.evaluate({Number(100): set()})) is False
assert bool(r.evaluate({Number(100): {ADDR1}})) is True
assert bool(r.evaluate({Number(100): {ADDR1, ADDR2}})) is False
def test_rule_yaml_count_range():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- count(number(100)): (1, 2)
"""
)
r = capa.rules.Rule.from_yaml(rule)
assert bool(r.evaluate({Number(100): set()})) is False
assert bool(r.evaluate({Number(100): {ADDR1}})) is True
assert bool(r.evaluate({Number(100): {ADDR1, ADDR2}})) is True
assert bool(r.evaluate({Number(100): {ADDR1, ADDR2, ADDR3}})) is False
def test_rule_yaml_count_string():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- count(string(foo)): 2
"""
)
r = capa.rules.Rule.from_yaml(rule)
assert bool(r.evaluate({String("foo"): set()})) is False
assert bool(r.evaluate({String("foo"): {ADDR1}})) is False
assert bool(r.evaluate({String("foo"): {ADDR1, ADDR2}})) is True
assert bool(r.evaluate({String("foo"): {ADDR1, ADDR2, ADDR3}})) is False
def test_invalid_rule_feature():
with pytest.raises(capa.rules.InvalidRule):
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- foo: true
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: file
dynamic: process
features:
- characteristic: nzxor
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: thread
features:
- characteristic: embedded pe
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: basic block
dynamic: thread
features:
- characteristic: embedded pe
"""
)
)
def test_multi_scope_rules_features():
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- api: write
- and:
- os: linux
- mnemonic: syscall
- number: 1 = write
"""
)
)
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- api: read
- and:
- os: linux
- mnemonic: syscall
- number: 0 = read
"""
)
)
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: instruction
dynamic: call
features:
- and:
- or:
- api: socket
- and:
- os: linux
- mnemonic: syscall
- number: 41 = socket()
- number: 6 = IPPROTO_TCP
- number: 1 = SOCK_STREAM
- number: 2 = AF_INET
"""
)
)
def test_rules_flavor_filtering():
rules = [
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: static rule
scopes:
static: function
dynamic: unsupported
features:
- api: CreateFileA
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: dynamic rule
scopes:
static: unsupported
dynamic: thread
features:
- api: CreateFileA
"""
)
),
]
static_rules = capa.rules.RuleSet([r for r in rules if r.scopes.static is not None])
dynamic_rules = capa.rules.RuleSet([r for r in rules if r.scopes.dynamic is not None])
# only static rule
assert len(static_rules) == 1
# only dynamic rule
assert len(dynamic_rules) == 1
def test_meta_scope_keywords():
static_scopes = sorted([e.value for e in capa.rules.STATIC_SCOPES])
dynamic_scopes = sorted([e.value for e in capa.rules.DYNAMIC_SCOPES])
for static_scope in static_scopes:
for dynamic_scope in dynamic_scopes:
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
f"""
rule:
meta:
name: test rule
scopes:
static: {static_scope}
dynamic: {dynamic_scope}
features:
- or:
- format: pe
"""
)
)
# its also ok to specify "unsupported"
for static_scope in static_scopes:
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
f"""
rule:
meta:
name: test rule
scopes:
static: {static_scope}
dynamic: unsupported
features:
- or:
- format: pe
"""
)
)
for dynamic_scope in dynamic_scopes:
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
f"""
rule:
meta:
name: test rule
scopes:
static: unsupported
dynamic: {dynamic_scope}
features:
- or:
- format: pe
"""
)
)
# but at least one scope must be specified
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes: {}
features:
- or:
- format: pe
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: unsupported
dynamic: unsupported
features:
- or:
- format: pe
"""
)
)
def test_lib_rules():
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: a lib rule
scopes:
static: function
dynamic: process
lib: true
features:
- api: CreateFileA
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: a standard rule
scopes:
static: function
dynamic: process
lib: false
features:
- api: CreateFileW
"""
)
),
]
)
# lib rules are added to the rule set
assert len(rules.function_rules) == 2
def test_subscope_rules():
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test function subscope
scopes:
static: file
dynamic: process
features:
- and:
- characteristic: embedded pe
- function:
- and:
- characteristic: nzxor
- characteristic: loop
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test process subscope
scopes:
static: file
dynamic: file
features:
- and:
- import: WININET.dll.HttpOpenRequestW
- process:
- and:
- substring: "http://"
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test thread subscope
scopes:
static: file
dynamic: process
features:
- and:
- string: "explorer.exe"
- thread:
- api: HttpOpenRequestW
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test call subscope
scopes:
static: basic block
dynamic: thread
features:
- and:
- string: "explorer.exe"
- call:
- api: HttpOpenRequestW
"""
)
),
]
)
# the file rule scope will have four rules:
# - `test function subscope`, `test process subscope` and
# `test thread subscope` for the static scope
# - and `test process subscope` for both scopes
assert len(rules.file_rules) == 3
# the function rule scope have two rule:
# - the rule on which `test function subscope` depends
assert len(rules.function_rules) == 1
# the process rule scope has three rules:
# - the rule on which `test process subscope` depends,
assert len(rules.process_rules) == 3
# the thread rule scope has two rule:
# - the rule on which `test thread subscope` depends
# - the `test call subscope` rule
assert len(rules.thread_rules) == 2
# the call rule scope has one rule:
# - the rule on which `test call subcsope` depends
assert len(rules.call_rules) == 1
def test_duplicate_rules():
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule-name
scopes:
static: function
dynamic: process
features:
- api: CreateFileA
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule-name
scopes:
static: function
dynamic: process
features:
- api: CreateFileW
"""
)
),
]
)
def test_missing_dependency():
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: dependent rule
scopes:
static: function
dynamic: process
features:
- match: missing rule
"""
)
),
]
)
def test_invalid_rules():
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- characteristic: number(1)
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- characteristic: count(number(100))
"""
)
)
# att&ck and mbc must be lists
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
att&ck: Tactic::Technique::Subtechnique [Identifier]
features:
- number: 1
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
mbc: Objective::Behavior::Method [Identifier]
features:
- number: 1
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: basic block
behavior: process
features:
- number: 1
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
legacy: basic block
dynamic: process
features:
- number: 1
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: process
dynamic: process
features:
- number: 1
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: basic block
dynamic: function
features:
- number: 1
"""
)
)
def test_number_symbol():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- number: 1
- number: 0xFFFFFFFF
- number: 2 = symbol name
- number: 3 = symbol name
- number: 4 = symbol name = another name
- number: 0x100 = symbol name
- number: 0x11 = (FLAG_A | FLAG_B)
"""
)
r = capa.rules.Rule.from_yaml(rule)
children = list(r.statement.get_children())
assert (Number(1) in children) is True
assert (Number(0xFFFFFFFF) in children) is True
assert (Number(2, description="symbol name") in children) is True
assert (Number(3, description="symbol name") in children) is True
assert (Number(4, description="symbol name = another name") in children) is True
assert (Number(0x100, description="symbol name") in children) is True
def test_count_number_symbol():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- count(number(2 = symbol name)): 1
- count(number(0x100 = symbol name)): 2 or more
- count(number(0x11 = (FLAG_A | FLAG_B))): 2 or more
"""
)
r = capa.rules.Rule.from_yaml(rule)
assert bool(r.evaluate({Number(2): set()})) is False
assert bool(r.evaluate({Number(2): {ADDR1}})) is True
assert bool(r.evaluate({Number(2): {ADDR1, ADDR2}})) is False
assert bool(r.evaluate({Number(0x100, description="symbol name"): {ADDR1}})) is False
assert bool(r.evaluate({Number(0x100, description="symbol name"): {ADDR1, ADDR2, ADDR3}})) is True
def test_count_api():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: thread
features:
- or:
- count(api(kernel32.CreateFileA)): 1
- count(api(System.Convert::FromBase64String)): 1
"""
)
r = capa.rules.Rule.from_yaml(rule)
# apis including their DLL names are not extracted anymore
assert bool(r.evaluate({API("kernel32.CreateFileA"): set()})) is False
assert bool(r.evaluate({API("kernel32.CreateFile"): set()})) is False
assert bool(r.evaluate({API("CreateFile"): {ADDR1}})) is False
assert bool(r.evaluate({API("CreateFileA"): {ADDR1}})) is True
assert bool(r.evaluate({API("System.Convert::FromBase64String"): {ADDR1}})) is True
def test_invalid_number():
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- number: "this is a string"
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- number: 2=
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- number: symbol name = 2
"""
)
)
def test_offset_symbol():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- and:
- offset: 1
- offset: 2 = symbol name
- offset: 3 = symbol name
- offset: 4 = symbol name = another name
- offset: 0x100 = symbol name
"""
)
r = capa.rules.Rule.from_yaml(rule)
children = list(r.statement.get_children())
assert (Offset(1) in children) is True
assert (Offset(2, description="symbol name") in children) is True
assert (Offset(3, description="symbol name") in children) is True
assert (Offset(4, description="symbol name = another name") in children) is True
assert (Offset(0x100, description="symbol name") in children) is True
def test_count_offset_symbol():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- count(offset(2 = symbol name)): 1
- count(offset(0x100 = symbol name)): 2 or more
- count(offset(0x11 = (FLAG_A | FLAG_B))): 2 or more
"""
)
r = capa.rules.Rule.from_yaml(rule)
assert bool(r.evaluate({Offset(2): set()})) is False
assert bool(r.evaluate({Offset(2): {ADDR1}})) is True
assert bool(r.evaluate({Offset(2): {ADDR1, ADDR2}})) is False
assert bool(r.evaluate({Offset(0x100, description="symbol name"): {ADDR1}})) is False
assert bool(r.evaluate({Offset(0x100, description="symbol name"): {ADDR1, ADDR2, ADDR3}})) is True
def test_invalid_offset():
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- offset: "this is a string"
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- offset: 2=
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- offset: symbol name = 2
"""
)
)
def test_invalid_string_values_int():
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- string: 123
"""
)
)
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- string: 0x123
"""
)
)
def test_explicit_string_values_int():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- string: "123"
- string: "0x123"
"""
)
r = capa.rules.Rule.from_yaml(rule)
children = list(r.statement.get_children())
assert (String("123") in children) is True
assert (String("0x123") in children) is True
def test_string_values_special_characters():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- string: "hello\\r\\nworld"
- string: "bye\\nbye"
description: "test description"
"""
)
r = capa.rules.Rule.from_yaml(rule)
children = list(r.statement.get_children())
assert (String("hello\r\nworld") in children) is True
assert (String("bye\nbye") in children) is True
def test_substring_feature():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- substring: abc
- substring: "def"
- substring: "gh\\ni"
"""
)
r = capa.rules.Rule.from_yaml(rule)
children = list(r.statement.get_children())
assert (Substring("abc") in children) is True
assert (Substring("def") in children) is True
assert (Substring("gh\ni") in children) is True
def test_substring_description():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- or:
- substring: abc
description: the start of the alphabet
"""
)
r = capa.rules.Rule.from_yaml(rule)
children = list(r.statement.get_children())
assert (Substring("abc") in children) is True
def test_filter_rules():
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule 1
scopes:
static: function
dynamic: process
authors:
- joe
features:
- api: CreateFile
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule 2
scopes:
static: function
dynamic: process
features:
- string: joe
"""
)
),
]
)
rules = rules.filter_rules_by_meta("joe")
assert len(rules) == 1
assert "rule 1" in rules.rules
def test_filter_rules_dependencies():
rules = capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule 1
scopes:
static: function
dynamic: process
features:
- match: rule 2
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule 2
scopes:
static: function
dynamic: process
features:
- match: rule 3
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule 3
scopes:
static: function
dynamic: process
features:
- api: CreateFile
"""
)
),
]
)
rules = rules.filter_rules_by_meta("rule 1")
assert len(rules.rules) == 3
assert "rule 1" in rules.rules
assert "rule 2" in rules.rules
assert "rule 3" in rules.rules
def test_filter_rules_missing_dependency():
with pytest.raises(capa.rules.InvalidRule):
capa.rules.RuleSet(
[
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule 1
scopes:
static: function
dynamic: process
authors:
- joe
features:
- match: rule 2
"""
)
),
]
)
def test_rules_namespace_dependencies():
rules = [
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule 1
scopes:
static: function
dynamic: process
namespace: ns1/nsA
features:
- api: CreateFile
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule 2
scopes:
static: function
dynamic: process
namespace: ns1/nsB
features:
- api: CreateFile
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule 3
scopes:
static: function
dynamic: process
features:
- match: ns1/nsA
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: rule 4
scopes:
static: function
dynamic: process
features:
- match: ns1
"""
)
),
]
r3 = {r.name for r in capa.rules.get_rules_and_dependencies(rules, "rule 3")}
assert "rule 1" in r3
assert "rule 2" not in r3
assert "rule 4" not in r3
r4 = {r.name for r in capa.rules.get_rules_and_dependencies(rules, "rule 4")}
assert "rule 1" in r4
assert "rule 2" in r4
assert "rule 3" not in r4
def test_function_name_features():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: file
dynamic: process
features:
- and:
- function-name: strcpy
- function-name: strcmp = copy from here to there
- function-name: strdup
description: duplicate a string
"""
)
r = capa.rules.Rule.from_yaml(rule)
children = list(r.statement.get_children())
assert (FunctionName("strcpy") in children) is True
assert (FunctionName("strcmp", description="copy from here to there") in children) is True
assert (FunctionName("strdup", description="duplicate a string") in children) is True
def test_os_features():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: file
dynamic: process
features:
- and:
- os: windows
"""
)
r = capa.rules.Rule.from_yaml(rule)
children = list(r.statement.get_children())
assert (OS(OS_WINDOWS) in children) is True
assert (OS(OS_LINUX) not in children) is True
def test_format_features():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: file
dynamic: process
features:
- and:
- format: pe
"""
)
r = capa.rules.Rule.from_yaml(rule)
children = list(r.statement.get_children())
assert (Format(FORMAT_PE) in children) is True
assert (Format(FORMAT_ELF) not in children) is True
def test_arch_features():
rule = textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: file
dynamic: process
features:
- and:
- arch: amd64
"""
)
r = capa.rules.Rule.from_yaml(rule)
children = list(r.statement.get_children())
assert (Arch(ARCH_AMD64) in children) is True
assert (Arch(ARCH_I386) not in children) is True
def test_property_access():
r = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- property/read: System.IO.FileInfo::Length
"""
)
)
assert bool(r.evaluate({Property("System.IO.FileInfo::Length", access=FeatureAccess.READ): {ADDR1}})) is True
assert bool(r.evaluate({Property("System.IO.FileInfo::Length"): {ADDR1}})) is False
assert bool(r.evaluate({Property("System.IO.FileInfo::Length", access=FeatureAccess.WRITE): {ADDR1}})) is False
def test_property_access_symbol():
r = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: function
dynamic: process
features:
- property/read: System.IO.FileInfo::Length = some property
"""
)
)
assert (
bool(
r.evaluate(
{
Property("System.IO.FileInfo::Length", access=FeatureAccess.READ, description="some property"): {
ADDR1
}
}
)
)
is True
)
def test_translate_com_features():
r = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
scopes:
static: basic block
dynamic: call
features:
- com/class: WICPngDecoder
# 389ea17b-5078-4cde-b6ef-25c15175c751 WICPngDecoder
# e018945b-aa86-4008-9bd4-6777a1e40c11 WICPngDecoder
"""
)
)
com_name = "WICPngDecoder"
com_features = [
capa.features.common.Bytes(b"{\xa1\x9e8xP\xdeL\xb6\xef%\xc1Qu\xc7Q", f"CLSID_{com_name} as bytes"),
capa.features.common.StringFactory("389ea17b-5078-4cde-b6ef-25c15175c751", f"CLSID_{com_name} as GUID string"),
capa.features.common.Bytes(b"[\x94\x18\xe0\x86\xaa\x08@\x9b\xd4gw\xa1\xe4\x0c\x11", f"IID_{com_name} as bytes"),
capa.features.common.StringFactory("e018945b-aa86-4008-9bd4-6777a1e40c11", f"IID_{com_name} as GUID string"),
]
assert set(com_features) == set(r.statement.get_children())
def test_invalid_com_features():
# test for unknown COM class
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
features:
- com/class: invalid_com
"""
)
)
# test for unknown COM interface
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
features:
- com/interface: invalid_com
"""
)
)
# test for invalid COM type
# valid_com_types = "class", "interface"
with pytest.raises(capa.rules.InvalidRule):
_ = capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule
features:
- com/invalid_COM_type: WICPngDecoder
"""
)
)
def test_circular_dependency():
rules = [
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule 1
scopes:
static: function
dynamic: process
lib: true
features:
- or:
- match: test rule 2
- api: kernel32.VirtualAlloc
"""
)
),
capa.rules.Rule.from_yaml(
textwrap.dedent(
"""
rule:
meta:
name: test rule 2
scopes:
static: function
dynamic: process
lib: true
features:
- match: test rule 1
"""
)
),
]
with pytest.raises(capa.rules.InvalidRule):
list(capa.rules.get_rules_and_dependencies(rules, rules[0].name))