Skip to content

Commit c1fbb27

Browse files
author
Willi Ballenthin
committed
Merge branch 'master' into dynamic-feature-extraction
2 parents 3cf748a + e5efc15 commit c1fbb27

21 files changed

+341
-136
lines changed

CHANGELOG.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,21 @@
44

55
### New Features
66
- ELF: implement file import and export name extractor #1607 @Aayush-Goel-04
7-
- Add a dynamic feature extractor for the CAPE sandbox @yelhamer [#1535](https://github.com/mandiant/capa/issues/1535)
8-
- Add unit tests for the new CAPE extractor #1563 @yelhamer
9-
- Add a CAPE file format and CAPE-based dynamic feature extraction to scripts/show-features.py #1566 @yelhamer
10-
- Add a new process scope for the dynamic analysis flavor #1517 @yelhamer
11-
- Add a new thread scope for the dynamic analysis flavor #1517 @yelhamer
12-
- Add support for flavor-based rule scopes @yelhamer
13-
- Add ProcessesAddress and ThreadAddress #1612 @yelhamer
14-
- Add dynamic capability extraction @yelhamer
15-
- Add support for mixed-scopes rules @yelhamer
16-
- Add a call scope @yelhamer
7+
- bump pydantic from 1.10.9 to 2.1.1 #1582 @Aayush-Goel-04
8+
- develop script to highlight the features that are not used during matching #331 @Aayush-Goel-04
9+
- implement dynamic analysis via CAPE sandbox #48 #1535 @yelhamer
10+
- add call scope #771 @yelhamer
11+
- add process scope for the dynamic analysis flavor #1517 @yelhamer
12+
- Add thread scope for the dynamic analysis flavor #1517 @yelhamer
1713

1814
### Breaking Changes
1915

2016
### New Rules (4)
17+
2118
- executable/pe/export/forwarded-export [email protected]
2219
- host-interaction/bootloader/get-uefi-variable [email protected]
2320
- host-interaction/bootloader/set-uefi-variable [email protected]
21+
- nursery/enumerate-device-drivers-on-linux @mr-tz
2422
-
2523

2624
### Bug Fixes
@@ -29,6 +27,7 @@
2927
- linter: skip native API check for NtProtectVirtualMemory #1675 @williballenthin
3028

3129
### capa explorer IDA Pro plugin
30+
- fix unhandled exception when resolving rule path #1693 @mike-hunhoff
3231

3332
### Development
3433

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/flare-capa)](https://pypi.org/project/flare-capa)
44
[![Last release](https://img.shields.io/github/v/release/mandiant/capa)](https://github.com/mandiant/capa/releases)
5-
[![Number of rules](https://img.shields.io/badge/rules-826-blue.svg)](https://github.com/mandiant/capa-rules)
5+
[![Number of rules](https://img.shields.io/badge/rules-828-blue.svg)](https://github.com/mandiant/capa-rules)
66
[![CI status](https://github.com/mandiant/capa/workflows/CI/badge.svg)](https://github.com/mandiant/capa/actions?query=workflow%3ACI+event%3Apush+branch%3Amaster)
77
[![Downloads](https://img.shields.io/github/downloads/mandiant/capa/total)](https://github.com/mandiant/capa/releases)
88
[![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](LICENSE.txt)

capa/features/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,8 @@ def __lt__(self, other):
136136
import capa.features.freeze.features
137137

138138
return (
139-
capa.features.freeze.features.feature_from_capa(self).json()
140-
< capa.features.freeze.features.feature_from_capa(other).json()
139+
capa.features.freeze.features.feature_from_capa(self).model_dump_json()
140+
< capa.features.freeze.features.feature_from_capa(other).model_dump_json()
141141
)
142142

143143
def get_name_str(self) -> str:

capa/features/extractors/base_extractor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from dataclasses import dataclass
1414

1515
# TODO(williballenthin): use typing.TypeAlias directly when Python 3.9 is deprecated
16+
# https://github.com/mandiant/capa/issues/1699
1617
from typing_extensions import TypeAlias
1718

1819
import capa.features.address

capa/features/freeze/__init__.py

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from enum import Enum
1515
from typing import List, Tuple, Union
1616

17-
from pydantic import Field, BaseModel
17+
from pydantic import Field, BaseModel, ConfigDict
1818
from typing_extensions import TypeAlias
1919

2020
import capa.helpers
@@ -38,8 +38,7 @@
3838

3939

4040
class HashableModel(BaseModel):
41-
class Config:
42-
frozen = True
41+
model_config = ConfigDict(frozen=True)
4342

4443

4544
class AddressType(str, Enum):
@@ -57,7 +56,7 @@ class AddressType(str, Enum):
5756

5857
class Address(HashableModel):
5958
type: AddressType
60-
value: Union[int, Tuple[int, ...], None]
59+
value: Union[int, Tuple[int, ...], None] = None # None default value to support deserialization of NO_ADDRESS
6160

6261
@classmethod
6362
def from_capa(cls, a: capa.features.address.Address) -> "Address":
@@ -271,9 +270,7 @@ class BasicBlockFeature(HashableModel):
271270
basic_block: Address = Field(alias="basic block")
272271
address: Address
273272
feature: Feature
274-
275-
class Config:
276-
allow_population_by_field_name = True
273+
model_config = ConfigDict(populate_by_name=True)
277274

278275

279276
class InstructionFeature(HashableModel):
@@ -306,9 +303,7 @@ class FunctionFeatures(BaseModel):
306303
address: Address
307304
features: Tuple[FunctionFeature, ...]
308305
basic_blocks: Tuple[BasicBlockFeatures, ...] = Field(alias="basic blocks")
309-
310-
class Config:
311-
allow_population_by_field_name = True
306+
model_config = ConfigDict(populate_by_name=True)
312307

313308

314309
class CallFeatures(BaseModel):
@@ -332,9 +327,7 @@ class StaticFeatures(BaseModel):
332327
global_: Tuple[GlobalFeature, ...] = Field(alias="global")
333328
file: Tuple[FileFeature, ...]
334329
functions: Tuple[FunctionFeatures, ...]
335-
336-
class Config:
337-
allow_population_by_field_name = True
330+
model_config = ConfigDict(populate_by_name=True)
338331

339332

340333
class DynamicFeatures(BaseModel):
@@ -352,9 +345,7 @@ class Config:
352345
class Extractor(BaseModel):
353346
name: str
354347
version: str = capa.version.__version__
355-
356-
class Config:
357-
allow_population_by_field_name = True
348+
model_config = ConfigDict(populate_by_name=True)
358349

359350

360351
class Freeze(BaseModel):
@@ -363,9 +354,7 @@ class Freeze(BaseModel):
363354
sample_hashes: SampleHashes
364355
extractor: Extractor
365356
features: Features
366-
367-
class Config:
368-
allow_population_by_field_name = True
357+
model_config = ConfigDict(populate_by_name=True)
369358

370359

371360
def dumps_static(extractor: StaticFeatureExtractor) -> str:
@@ -467,7 +456,7 @@ def dumps_static(extractor: StaticFeatureExtractor) -> str:
467456
) # type: ignore
468457
# Mypy is unable to recognise `base_address` as a argument due to alias
469458

470-
return freeze.json()
459+
return freeze.model_dump_json()
471460

472461

473462
def dumps_dynamic(extractor: DynamicFeatureExtractor) -> str:

capa/features/freeze/features.py

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import binascii
99
from typing import Union, Optional
1010

11-
from pydantic import Field, BaseModel
11+
from pydantic import Field, BaseModel, ConfigDict
1212

1313
import capa.features.file
1414
import capa.features.insn
@@ -17,9 +17,7 @@
1717

1818

1919
class FeatureModel(BaseModel):
20-
class Config:
21-
frozen = True
22-
allow_population_by_field_name = True
20+
model_config = ConfigDict(frozen=True, populate_by_name=True)
2321

2422
def to_capa(self) -> capa.features.common.Feature:
2523
if isinstance(self, OSFeature):
@@ -213,141 +211,141 @@ def feature_from_capa(f: capa.features.common.Feature) -> "Feature":
213211
class OSFeature(FeatureModel):
214212
type: str = "os"
215213
os: str
216-
description: Optional[str]
214+
description: Optional[str] = None
217215

218216

219217
class ArchFeature(FeatureModel):
220218
type: str = "arch"
221219
arch: str
222-
description: Optional[str]
220+
description: Optional[str] = None
223221

224222

225223
class FormatFeature(FeatureModel):
226224
type: str = "format"
227225
format: str
228-
description: Optional[str]
226+
description: Optional[str] = None
229227

230228

231229
class MatchFeature(FeatureModel):
232230
type: str = "match"
233231
match: str
234-
description: Optional[str]
232+
description: Optional[str] = None
235233

236234

237235
class CharacteristicFeature(FeatureModel):
238236
type: str = "characteristic"
239237
characteristic: str
240-
description: Optional[str]
238+
description: Optional[str] = None
241239

242240

243241
class ExportFeature(FeatureModel):
244242
type: str = "export"
245243
export: str
246-
description: Optional[str]
244+
description: Optional[str] = None
247245

248246

249247
class ImportFeature(FeatureModel):
250248
type: str = "import"
251249
import_: str = Field(alias="import")
252-
description: Optional[str]
250+
description: Optional[str] = None
253251

254252

255253
class SectionFeature(FeatureModel):
256254
type: str = "section"
257255
section: str
258-
description: Optional[str]
256+
description: Optional[str] = None
259257

260258

261259
class FunctionNameFeature(FeatureModel):
262260
type: str = "function name"
263261
function_name: str = Field(alias="function name")
264-
description: Optional[str]
262+
description: Optional[str] = None
265263

266264

267265
class SubstringFeature(FeatureModel):
268266
type: str = "substring"
269267
substring: str
270-
description: Optional[str]
268+
description: Optional[str] = None
271269

272270

273271
class RegexFeature(FeatureModel):
274272
type: str = "regex"
275273
regex: str
276-
description: Optional[str]
274+
description: Optional[str] = None
277275

278276

279277
class StringFeature(FeatureModel):
280278
type: str = "string"
281279
string: str
282-
description: Optional[str]
280+
description: Optional[str] = None
283281

284282

285283
class ClassFeature(FeatureModel):
286284
type: str = "class"
287285
class_: str = Field(alias="class")
288-
description: Optional[str]
286+
description: Optional[str] = None
289287

290288

291289
class NamespaceFeature(FeatureModel):
292290
type: str = "namespace"
293291
namespace: str
294-
description: Optional[str]
292+
description: Optional[str] = None
295293

296294

297295
class BasicBlockFeature(FeatureModel):
298296
type: str = "basic block"
299-
description: Optional[str]
297+
description: Optional[str] = None
300298

301299

302300
class APIFeature(FeatureModel):
303301
type: str = "api"
304302
api: str
305-
description: Optional[str]
303+
description: Optional[str] = None
306304

307305

308306
class PropertyFeature(FeatureModel):
309307
type: str = "property"
310-
access: Optional[str]
308+
access: Optional[str] = None
311309
property: str
312-
description: Optional[str]
310+
description: Optional[str] = None
313311

314312

315313
class NumberFeature(FeatureModel):
316314
type: str = "number"
317315
number: Union[int, float]
318-
description: Optional[str]
316+
description: Optional[str] = None
319317

320318

321319
class BytesFeature(FeatureModel):
322320
type: str = "bytes"
323321
bytes: str
324-
description: Optional[str]
322+
description: Optional[str] = None
325323

326324

327325
class OffsetFeature(FeatureModel):
328326
type: str = "offset"
329327
offset: int
330-
description: Optional[str]
328+
description: Optional[str] = None
331329

332330

333331
class MnemonicFeature(FeatureModel):
334332
type: str = "mnemonic"
335333
mnemonic: str
336-
description: Optional[str]
334+
description: Optional[str] = None
337335

338336

339337
class OperandNumberFeature(FeatureModel):
340338
type: str = "operand number"
341339
index: int
342340
operand_number: int = Field(alias="operand number")
343-
description: Optional[str]
341+
description: Optional[str] = None
344342

345343

346344
class OperandOffsetFeature(FeatureModel):
347345
type: str = "operand offset"
348346
index: int
349347
operand_offset: int = Field(alias="operand offset")
350-
description: Optional[str]
348+
description: Optional[str] = None
351349

352350

353351
Feature = Union[

capa/ida/plugin/form.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -573,10 +573,11 @@ def ida_hook_rebase(self, meta, post=False):
573573

574574
def ensure_capa_settings_rule_path(self):
575575
try:
576-
path: Path = Path(settings.user.get(CAPA_SETTINGS_RULE_PATH, ""))
576+
path: str = settings.user.get(CAPA_SETTINGS_RULE_PATH, "")
577577

578578
# resolve rules directory - check self and settings first, then ask user
579-
if not path.exists():
579+
# pathlib.Path considers "" equivalent to "." so we first check if rule path is an empty string
580+
if not path or not Path(path).exists():
580581
# configure rules selection messagebox
581582
rules_message = QtWidgets.QMessageBox()
582583
rules_message.setIcon(QtWidgets.QMessageBox.Information)
@@ -594,15 +595,15 @@ def ensure_capa_settings_rule_path(self):
594595
if pressed == QtWidgets.QMessageBox.Cancel:
595596
raise UserCancelledError()
596597

597-
path = Path(self.ask_user_directory())
598+
path = self.ask_user_directory()
598599
if not path:
599600
raise UserCancelledError()
600601

601-
if not path.exists():
602+
if not Path(path).exists():
602603
logger.error("rule path %s does not exist or cannot be accessed", path)
603604
return False
604605

605-
settings.user[CAPA_SETTINGS_RULE_PATH] = str(path)
606+
settings.user[CAPA_SETTINGS_RULE_PATH] = path
606607
except UserCancelledError:
607608
capa.ida.helpers.inform_user_ida_ui("Analysis requires capa rules")
608609
logger.warning(
@@ -1307,7 +1308,7 @@ def save_program_analysis(self):
13071308
idaapi.info("No program analysis to save.")
13081309
return
13091310

1310-
s = self.resdoc_cache.json().encode("utf-8")
1311+
s = self.resdoc_cache.model_dump_json().encode("utf-8")
13111312

13121313
path = Path(self.ask_user_capa_json_file())
13131314
if not path.exists():

capa/render/json.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@
1111

1212

1313
def render(meta, rules: RuleSet, capabilities: MatchResults) -> str:
14-
return rd.ResultDocument.from_capa(meta, rules, capabilities).json(exclude_none=True)
14+
return rd.ResultDocument.from_capa(meta, rules, capabilities).model_dump_json(exclude_none=True)

0 commit comments

Comments
 (0)