Skip to content

Commit e1735f0

Browse files
authored
update pydantic models to guarantee type coercion (#1176)
* add CompoundStatement to fix Pydantic typing bug * explorer: fix #1151 * explorer: support rendering operand number/offset
1 parent 8521f85 commit e1735f0

File tree

7 files changed

+240
-50
lines changed

7 files changed

+240
-50
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
### Bug Fixes
2121
- render: convert feature attributes to aliased dictionary for vverbose #1152 @mike-hunhoff
2222
- decouple Token dependency / extractor and features #1139 @mr-tz
23+
- update pydantic model to guarantee type coercion #1176 @mike-hunhoff
2324
- do not overwrite version in version.py during PyInstaller build #1169 @mr-tz
2425

2526
### capa explorer IDA Pro plugin

capa/features/freeze/__init__.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,13 @@ class BasicBlockFeature(HashableModel):
145145
versus right at its starting address.
146146
"""
147147

148-
basic_block: Address
148+
basic_block: Address = Field(alias="basic block")
149149
address: Address
150150
feature: Feature
151151

152+
class Config:
153+
allow_population_by_field_name = True
154+
152155

153156
class InstructionFeature(HashableModel):
154157
"""
@@ -179,7 +182,7 @@ class BasicBlockFeatures(BaseModel):
179182
class FunctionFeatures(BaseModel):
180183
address: Address
181184
features: Tuple[FunctionFeature, ...]
182-
basic_blocks: Tuple[BasicBlockFeatures, ...] = Field(alias="basic block")
185+
basic_blocks: Tuple[BasicBlockFeatures, ...] = Field(alias="basic blocks")
183186

184187
class Config:
185188
allow_population_by_field_name = True

capa/features/freeze/features.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -340,7 +340,6 @@ class OperandOffsetFeature(FeatureModel):
340340
MnemonicFeature,
341341
OperandNumberFeature,
342342
OperandOffsetFeature,
343-
# this has to go last because...? pydantic fails to serialize correctly otherwise.
344-
# possibly because this feature has no associated value?
343+
# Note! this must be last, see #1161
345344
BasicBlockFeature,
346345
]

capa/ida/plugin/model.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -365,12 +365,13 @@ def render_capa_doc_statement_node(
365365
@param doc: result doc
366366
"""
367367

368-
if isinstance(statement, (rd.AndStatement, rd.OrStatement, rd.OptionalStatement)):
369-
display = statement.type
370-
if statement.description:
371-
display += " (%s)" % statement.description
372-
return CapaExplorerDefaultItem(parent, display)
373-
elif isinstance(statement, rd.NotStatement):
368+
if isinstance(statement, rd.CompoundStatement):
369+
if statement.type != rd.CompoundStatementType.NOT:
370+
display = statement.type
371+
if statement.description:
372+
display += " (%s)" % statement.description
373+
return CapaExplorerDefaultItem(parent, display)
374+
elif isinstance(statement, rd.CompoundStatement) and statement.type == rd.CompoundStatementType.NOT:
374375
# TODO: do we display 'not'
375376
pass
376377
elif isinstance(statement, rd.SomeStatement):
@@ -424,7 +425,7 @@ def render_capa_doc_match(self, parent: CapaExplorerDataItem, match: rd.Match, d
424425
return
425426

426427
# optional statement with no successful children is empty
427-
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
428+
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.OPTIONAL:
428429
if not any(map(lambda m: m.success, match.children)):
429430
return
430431

@@ -524,7 +525,7 @@ def capa_doc_feature_to_display(self, feature: frzf.Feature):
524525
@param feature: capa feature read from doc
525526
"""
526527
key = feature.type
527-
value = getattr(feature, feature.type)
528+
value = feature.dict(by_alias=True).get(feature.type)
528529

529530
if value:
530531
if isinstance(feature, frzf.StringFeature):
@@ -638,6 +639,8 @@ def render_capa_doc_feature(
638639
frzf.MnemonicFeature,
639640
frzf.NumberFeature,
640641
frzf.OffsetFeature,
642+
frzf.OperandNumberFeature,
643+
frzf.OperandOffsetFeature,
641644
),
642645
):
643646
# display instruction preview

capa/render/result_document.py

+16-32
Original file line numberDiff line numberDiff line change
@@ -124,22 +124,19 @@ def from_capa(cls, meta: Any) -> "Metadata":
124124
)
125125

126126

127-
class StatementModel(FrozenModel):
128-
...
129-
130-
131-
class AndStatement(StatementModel):
132-
type = "and"
133-
description: Optional[str]
127+
class CompoundStatementType:
128+
AND = "and"
129+
OR = "or"
130+
NOT = "not"
131+
OPTIONAL = "optional"
134132

135133

136-
class OrStatement(StatementModel):
137-
type = "or"
138-
description: Optional[str]
134+
class StatementModel(FrozenModel):
135+
...
139136

140137

141-
class NotStatement(StatementModel):
142-
type = "not"
138+
class CompoundStatement(StatementModel):
139+
type: str
143140
description: Optional[str]
144141

145142

@@ -149,11 +146,6 @@ class SomeStatement(StatementModel):
149146
count: int
150147

151148

152-
class OptionalStatement(StatementModel):
153-
type = "optional"
154-
description: Optional[str]
155-
156-
157149
class RangeStatement(StatementModel):
158150
type = "range"
159151
description: Optional[str]
@@ -165,17 +157,15 @@ class RangeStatement(StatementModel):
165157
class SubscopeStatement(StatementModel):
166158
type = "subscope"
167159
description: Optional[str]
168-
scope = capa.rules.Scope
160+
scope: capa.rules.Scope
169161

170162

171163
Statement = Union[
172-
OptionalStatement,
173-
AndStatement,
174-
OrStatement,
175-
NotStatement,
176-
SomeStatement,
164+
# Note! order matters, see #1161
177165
RangeStatement,
166+
SomeStatement,
178167
SubscopeStatement,
168+
CompoundStatement,
179169
]
180170

181171

@@ -185,18 +175,12 @@ class StatementNode(FrozenModel):
185175

186176

187177
def statement_from_capa(node: capa.engine.Statement) -> Statement:
188-
if isinstance(node, capa.engine.And):
189-
return AndStatement(description=node.description)
190-
191-
elif isinstance(node, capa.engine.Or):
192-
return OrStatement(description=node.description)
193-
194-
elif isinstance(node, capa.engine.Not):
195-
return NotStatement(description=node.description)
178+
if isinstance(node, (capa.engine.And, capa.engine.Or, capa.engine.Not)):
179+
return CompoundStatement(type=node.__class__.__name__.lower(), description=node.description)
196180

197181
elif isinstance(node, capa.engine.Some):
198182
if node.count == 0:
199-
return OptionalStatement(description=node.description)
183+
return CompoundStatement(type=CompoundStatementType.OPTIONAL, description=node.description)
200184

201185
else:
202186
return SomeStatement(

capa/render/vverbose.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
6464
ostream.write(" = %s" % statement.description)
6565
ostream.writeln("")
6666

67-
elif isinstance(statement, (rd.AndStatement, rd.OrStatement, rd.OptionalStatement, rd.NotStatement)):
67+
elif isinstance(statement, (rd.CompoundStatement)):
6868
# emit `and:` `or:` `optional:` `not:`
6969
ostream.write(statement.type)
7070

@@ -87,7 +87,7 @@ def render_statement(ostream, match: rd.Match, statement: rd.Statement, indent=0
8787
# so, we have to inline some of the feature rendering here.
8888

8989
child = statement.child
90-
value = getattr(child, child.type)
90+
value = child.dict(by_alias=True).get(child.type)
9191

9292
if value:
9393
if isinstance(child, frzf.StringFeature):
@@ -211,12 +211,12 @@ def render_match(ostream, match: rd.Match, indent=0, mode=MODE_SUCCESS):
211211
return
212212

213213
# optional statement with no successful children is empty
214-
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
214+
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.OPTIONAL:
215215
if not any(map(lambda m: m.success, match.children)):
216216
return
217217

218218
# not statement, so invert the child mode to show failed evaluations
219-
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.NotStatement):
219+
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.NOT:
220220
child_mode = MODE_FAILURE
221221

222222
elif mode == MODE_FAILURE:
@@ -225,12 +225,12 @@ def render_match(ostream, match: rd.Match, indent=0, mode=MODE_SUCCESS):
225225
return
226226

227227
# optional statement with successful children is not relevant
228-
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.OptionalStatement):
228+
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.OPTIONAL:
229229
if any(map(lambda m: m.success, match.children)):
230230
return
231231

232232
# not statement, so invert the child mode to show successful evaluations
233-
if isinstance(match.node, rd.StatementNode) and isinstance(match.node.statement, rd.NotStatement):
233+
if isinstance(match.node, rd.StatementNode) and match.node.statement.type == rd.CompoundStatementType.NOT:
234234
child_mode = MODE_SUCCESS
235235
else:
236236
raise RuntimeError("unexpected mode: " + mode)

0 commit comments

Comments
 (0)