6
6
# is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
7
7
# See the License for the specific language governing permissions and limitations under the License.
8
8
9
+ import io
9
10
import collections
11
+ import urllib .parse
10
12
11
- import tabulate
13
+ import rich
14
+ import rich .table
15
+ import rich .console
16
+ from rich .console import Console
12
17
13
18
import capa .render .utils as rutils
14
19
import capa .render .result_document as rd
15
20
import capa .features .freeze .features as frzf
16
21
from capa .rules import RuleSet
17
22
from capa .engine import MatchResults
18
- from capa .render .utils import StringIO
19
23
20
- tabulate .PRESERVE_WHITESPACE = True
24
+
25
+ def bold_markup (s ) -> str :
26
+ """
27
+ Generate Rich markup in a bold style.
28
+
29
+ The resulting string should be passed to a Rich renderable
30
+ and/or printed via Rich or the markup will be visible to the user.
31
+ """
32
+ return f"[cyan]{ s } [/cyan]"
33
+
34
+
35
+ def link_markup (s : str , href : str ) -> str :
36
+ """
37
+ Generate Rich markup for a clickable hyperlink.
38
+ This works in many modern terminals.
39
+ When it doesn't work, the fallback is just to show the link name (s),
40
+ as if it was not a link.
41
+
42
+ The resulting string should be passed to a Rich renderable
43
+ and/or printed via Rich or the markup will be visible to the user.
44
+ """
45
+ return f"[link={ href } ]{ s } [/link]"
21
46
22
47
23
48
def width (s : str , character_count : int ) -> str :
@@ -28,20 +53,31 @@ def width(s: str, character_count: int) -> str:
28
53
return s
29
54
30
55
31
- def render_meta (doc : rd .ResultDocument , ostream : StringIO ):
56
+ def render_sample_link (hash : str ) -> str :
57
+ url = "https://www.virustotal.com/gui/file/" + hash
58
+ return link_markup (hash , url )
59
+
60
+
61
+ def render_meta (doc : rd .ResultDocument , console : Console ):
32
62
rows = [
33
- (width ( "md5" , 22 ), width (doc .meta .sample .md5 , 82 )),
34
- ("sha1" , doc .meta .sample .sha1 ),
35
- ("sha256" , doc .meta .sample .sha256 ),
63
+ ("md5" , render_sample_link (doc .meta .sample .md5 )),
64
+ ("sha1" , render_sample_link ( doc .meta .sample .sha1 ) ),
65
+ ("sha256" , render_sample_link ( doc .meta .sample .sha256 ) ),
36
66
("analysis" , doc .meta .flavor .value ),
37
67
("os" , doc .meta .analysis .os ),
38
68
("format" , doc .meta .analysis .format ),
39
69
("arch" , doc .meta .analysis .arch ),
40
70
("path" , doc .meta .sample .path ),
41
71
]
42
72
43
- ostream .write (tabulate .tabulate (rows , tablefmt = "mixed_outline" ))
44
- ostream .write ("\n " )
73
+ table = rich .table .Table (show_header = False , min_width = 100 )
74
+ table .add_column ()
75
+ table .add_column ()
76
+
77
+ for row in rows :
78
+ table .add_row (* row )
79
+
80
+ console .print (table )
45
81
46
82
47
83
def find_subrule_matches (doc : rd .ResultDocument ):
@@ -71,7 +107,12 @@ def rec(match: rd.Match):
71
107
return matches
72
108
73
109
74
- def render_capabilities (doc : rd .ResultDocument , ostream : StringIO ):
110
+ def render_rule_name (name : str ) -> str :
111
+ url = f"https://mandiant.github.io/capa/rules/{ urllib .parse .quote (name )} /"
112
+ return bold_markup (link_markup (name , url ))
113
+
114
+
115
+ def render_capabilities (doc : rd .ResultDocument , console : Console ):
75
116
"""
76
117
example::
77
118
@@ -95,25 +136,30 @@ def render_capabilities(doc: rd.ResultDocument, ostream: StringIO):
95
136
96
137
count = len (rule .matches )
97
138
if count == 1 :
98
- capability = rutils . bold (rule .meta .name )
139
+ capability = render_rule_name (rule .meta .name )
99
140
else :
100
- capability = f" { rutils . bold (rule .meta .name )} ({ count } matches)"
141
+ capability = render_rule_name (rule .meta .name ) + f" ({ count } matches)"
101
142
rows .append ((capability , rule .meta .namespace ))
102
143
103
144
if rows :
104
- ostream . write (
105
- tabulate . tabulate (
106
- rows ,
107
- headers = [ width ( "Capability" , 50 ), width ( "Namespace" , 50 )],
108
- tablefmt = "mixed_outline" ,
109
- )
110
- )
111
- ostream . write ( " \n " )
145
+ table = rich . table . Table ( min_width = 100 )
146
+ table . add_column ( width ( "Capability" , 20 ))
147
+ table . add_column ( "Namespace" )
148
+
149
+ for row in rows :
150
+ table . add_row ( * row )
151
+
152
+ console . print ( table )
112
153
else :
113
- ostream .writeln (rutils .bold ("no capabilities found" ))
154
+ console .print (bold_markup ("no capabilities found" ))
155
+
156
+
157
+ def render_attack_link (id : str ) -> str :
158
+ url = f"https://attack.mitre.org/techniques/{ id .replace ('.' , '/' )} /"
159
+ return rf"\[{ link_markup (id , url )} ]"
114
160
115
161
116
- def render_attack (doc : rd .ResultDocument , ostream : StringIO ):
162
+ def render_attack (doc : rd .ResultDocument , console : Console ):
117
163
"""
118
164
example::
119
165
@@ -132,35 +178,36 @@ def render_attack(doc: rd.ResultDocument, ostream: StringIO):
132
178
tactics = collections .defaultdict (set )
133
179
for rule in rutils .capability_rules (doc ):
134
180
for attack in rule .meta .attack :
135
- tactics [attack .tactic ].add ((attack .technique , attack .subtechnique , attack .id ))
181
+ tactics [attack .tactic ].add ((attack .technique , attack .subtechnique , attack .id . strip ( "[" ). strip ( "]" ) ))
136
182
137
183
rows = []
138
184
for tactic , techniques in sorted (tactics .items ()):
139
185
inner_rows = []
140
186
for technique , subtechnique , id in sorted (techniques ):
141
187
if not subtechnique :
142
- inner_rows .append (f"{ rutils .bold (technique )} { id } " )
188
+ # example: File and Directory Discovery [T1083]
189
+ inner_rows .append (f"{ bold_markup (technique )} { render_attack_link (id )} " )
143
190
else :
144
- inner_rows . append ( f" { rutils . bold ( technique ) } :: { subtechnique } { id } " )
145
- rows .append (
146
- (
147
- rutils . bold (tactic .upper ()),
148
- "\n " .join (inner_rows ),
149
- )
150
- )
191
+ # example: Code Discovery::Enumerate PE Sections [T1084.001]
192
+ inner_rows .append (f" { bold_markup ( technique ) } :: { subtechnique } { render_attack_link ( id ) } " )
193
+
194
+ tactic = bold_markup (tactic .upper ())
195
+ technique = "\n " .join (inner_rows )
196
+
197
+ rows . append (( tactic , technique ) )
151
198
152
199
if rows :
153
- ostream .write (
154
- tabulate .tabulate (
155
- rows ,
156
- headers = [width ("ATT&CK Tactic" , 20 ), width ("ATT&CK Technique" , 80 )],
157
- tablefmt = "mixed_grid" ,
158
- )
159
- )
160
- ostream .write ("\n " )
200
+ table = rich .table .Table (min_width = 100 )
201
+ table .add_column (width ("ATT&CK Tactic" , 20 ))
202
+ table .add_column ("ATT&CK Technique" )
203
+
204
+ for row in rows :
205
+ table .add_row (* row )
161
206
207
+ console .print (table )
162
208
163
- def render_maec (doc : rd .ResultDocument , ostream : StringIO ):
209
+
210
+ def render_maec (doc : rd .ResultDocument , console : Console ):
164
211
"""
165
212
example::
166
213
@@ -193,20 +240,37 @@ def render_maec(doc: rd.ResultDocument, ostream: StringIO):
193
240
for category in sorted (maec_categories ):
194
241
values = maec_table .get (category , set ())
195
242
if values :
196
- rows .append ((rutils . bold (category .replace ("_" , "-" )), "\n " .join (sorted (values ))))
243
+ rows .append ((bold_markup (category .replace ("_" , "-" )), "\n " .join (sorted (values ))))
197
244
198
245
if rows :
199
- ostream .write (
200
- tabulate .tabulate (
201
- rows ,
202
- headers = [width ("MAEC Category" , 25 ), width ("MAEC Value" , 75 )],
203
- tablefmt = "mixed_grid" ,
204
- )
205
- )
206
- ostream .write ("\n " )
246
+ table = rich .table .Table (min_width = 100 )
247
+ table .add_column (width ("MAEC Category" , 20 ))
248
+ table .add_column ("MAEC Value" )
249
+
250
+ for row in rows :
251
+ table .add_row (* row )
252
+
253
+ console .print (table )
254
+
255
+
256
+ def render_mbc_link (id : str , objective : str , behavior : str ) -> str :
257
+ if id [0 ] in {"B" , "T" , "E" , "F" }:
258
+ # behavior
259
+ base_url = "https://github.com/MBCProject/mbc-markdown/blob/main"
260
+ elif id [0 ] == "C" :
261
+ # micro-behavior
262
+ base_url = "https://github.com/MBCProject/mbc-markdown/blob/main/micro-behaviors"
263
+ else :
264
+ raise ValueError ("unexpected MBC prefix" )
207
265
266
+ objective_fragment = objective .lower ().replace (" " , "-" )
267
+ behavior_fragment = behavior .lower ().replace (" " , "-" )
208
268
209
- def render_mbc (doc : rd .ResultDocument , ostream : StringIO ):
269
+ url = f"{ base_url } /{ objective_fragment } /{ behavior_fragment } .md"
270
+ return rf"\[{ link_markup (id , url )} ]"
271
+
272
+
273
+ def render_mbc (doc : rd .ResultDocument , console : Console ):
210
274
"""
211
275
example::
212
276
@@ -223,48 +287,48 @@ def render_mbc(doc: rd.ResultDocument, ostream: StringIO):
223
287
objectives = collections .defaultdict (set )
224
288
for rule in rutils .capability_rules (doc ):
225
289
for mbc in rule .meta .mbc :
226
- objectives [mbc .objective ].add ((mbc .behavior , mbc .method , mbc .id ))
290
+ objectives [mbc .objective ].add ((mbc .behavior , mbc .method , mbc .id . strip ( "[" ). strip ( "]" ) ))
227
291
228
292
rows = []
229
293
for objective , behaviors in sorted (objectives .items ()):
230
294
inner_rows = []
231
- for behavior , method , id in sorted (behaviors ):
232
- if not method :
233
- inner_rows .append (f"{ rutils .bold (behavior )} [{ id } ]" )
295
+ for technique , subtechnique , id in sorted (behaviors ):
296
+ if not subtechnique :
297
+ # example: File and Directory Discovery [T1083]
298
+ inner_rows .append (f"{ bold_markup (technique )} { render_mbc_link (id , objective , technique )} " )
234
299
else :
235
- inner_rows .append (f"{ rutils .bold (behavior )} ::{ method } [{ id } ]" )
236
- rows .append (
237
- (
238
- rutils .bold (objective .upper ()),
239
- "\n " .join (inner_rows ),
240
- )
241
- )
300
+ # example: Code Discovery::Enumerate PE Sections [T1084.001]
301
+ inner_rows .append (
302
+ f"{ bold_markup (technique )} ::{ subtechnique } { render_mbc_link (id , objective , technique )} "
303
+ )
304
+
305
+ objective = bold_markup (objective .upper ())
306
+ technique = "\n " .join (inner_rows )
307
+
308
+ rows .append ((objective , technique ))
242
309
243
310
if rows :
244
- ostream . write (
245
- tabulate . tabulate (
246
- rows ,
247
- headers = [ width ( "MBC Objective" , 25 ), width ( "MBC Behavior" , 75 )],
248
- tablefmt = "mixed_grid" ,
249
- )
250
- )
251
- ostream . write ( " \n " )
311
+ table = rich . table . Table ( min_width = 100 )
312
+ table . add_column ( width ( "MBC Objective" , 20 ))
313
+ table . add_column ( "MBC Behavior" )
314
+
315
+ for row in rows :
316
+ table . add_row ( * row )
317
+
318
+ console . print ( table )
252
319
253
320
254
321
def render_default (doc : rd .ResultDocument ):
255
- ostream = rutils .StringIO ()
256
-
257
- render_meta (doc , ostream )
258
- ostream .write ("\n " )
259
- render_attack (doc , ostream )
260
- ostream .write ("\n " )
261
- render_maec (doc , ostream )
262
- ostream .write ("\n " )
263
- render_mbc (doc , ostream )
264
- ostream .write ("\n " )
265
- render_capabilities (doc , ostream )
266
-
267
- return ostream .getvalue ()
322
+ f = io .StringIO ()
323
+ console = rich .console .Console ()
324
+
325
+ render_meta (doc , console )
326
+ render_attack (doc , console )
327
+ render_maec (doc , console )
328
+ render_mbc (doc , console )
329
+ render_capabilities (doc , console )
330
+
331
+ return f .getvalue ()
268
332
269
333
270
334
def render (meta , rules : RuleSet , capabilities : MatchResults ) -> str :
0 commit comments