3737 "minor_changes" : "Minor {obj_type} changes" ,
3838 "deprecations" : "{obj_type} deprecations" ,
3939 "revocations" : "{obj_type} revocations" ,
40- "deletions" : "{obj_type} deletions"
40+ "deletions" : "{obj_type} deletions" ,
41+ "unchanged" : "Unchanged {obj_type}"
4142}
4243statusToColor = { # color key for layers
4344 "additions" : "#a1d99b" ,
4445 "changes" : "#fcf3a2" ,
4546 "minor_changes" : "#c7c4e0" ,
4647 "deletions" : "#ff00e1" , # this will probably never show up but just in case
4748 "revocations" : "#ff9000" ,
48- "deprecations" : "#ff6363"
49+ "deprecations" : "#ff6363" ,
50+ "unchanged" : "#ffffff"
4951}
5052statusDescriptions = { # explanation of modification types to data objects for legend in layer files
5153 "additions" : "objects which are present in the new data and not the old" ,
5254 "changes" : "objects which have a newer version number in the new data compared to the old" ,
5355 "minor_changes" : "objects which have a newer last edit date in the new data than in the old, but the same version number" ,
5456 "revocations" : "objects which are revoked in the new data but not in the old" ,
5557 "deprecations" : "objects which are deprecated in the new data but not in the old" ,
56- "deletions" : "objects which are present in the old data but not the new"
58+ "deletions" : "objects which are present in the old data but not the new" ,
59+ "unchanged" : "objects which did not change between the two versions"
5760}
5861
5962class DiffStix (object ):
@@ -66,6 +69,7 @@ def __init__(
6669 layers = None ,
6770 markdown = None ,
6871 minor_changes = False ,
72+ unchanged = False ,
6973 new = 'new' ,
7074 old = 'old' ,
7175 show_key = False ,
@@ -93,6 +97,7 @@ def __init__(
9397 self .layers = layers
9498 self .markdown = markdown
9599 self .minor_changes = minor_changes
100+ self .unchanged = unchanged
96101 self .new = new
97102 self .old = old
98103 self .show_key = show_key
@@ -109,7 +114,8 @@ def __init__(
109114 # changes: [],
110115 # minor_changes: [],
111116 # revocations: [],
112- # deprecations: []
117+ # deprecations: [],
118+ # unchanged: []
113119 # }
114120 # mobile-attack...
115121 # }
@@ -236,6 +242,7 @@ def load_taxii(new=False):
236242 minor_changes = set ()
237243 revocations = set ()
238244 deprecations = set ()
245+ unchanged = set ()
239246
240247 # find changes, revocations and deprecations
241248 for key in intersection :
@@ -281,6 +288,8 @@ def load_taxii(new=False):
281288 new_date = dateparser .parse (new ["id_to_obj" ][key ]["modified" ])
282289 if new_date > old_date :
283290 minor_changes .add (key )
291+ else :
292+ unchanged .add (key )
284293
285294 # set data
286295 if obj_type not in self .data : self .data [obj_type ] = {}
@@ -291,6 +300,11 @@ def load_taxii(new=False):
291300 # only create minor_changes data if we want to display it later
292301 if self .minor_changes :
293302 self .data [obj_type ][domain ]["minor_changes" ] = [new ["id_to_obj" ][key ] for key in minor_changes ]
303+
304+ # ditto for unchanged
305+ if self .unchanged :
306+ self .data [obj_type ][domain ]["unchanged" ] = [new ["id_to_obj" ][key ] for key in unchanged ]
307+
294308 self .data [obj_type ][domain ]["revocations" ] = [new ["id_to_obj" ][key ] for key in revocations ]
295309 self .data [obj_type ][domain ]["deprecations" ] = [new ["id_to_obj" ][key ] for key in deprecations ]
296310 # only show deletions if objects were deleted
@@ -324,6 +338,8 @@ def get_md_key(self):
324338 )
325339 if self .minor_changes :
326340 key += "* Minor object changes: " + statusDescriptions ['minor_changes' ] + "\n "
341+ if self .unchanged :
342+ key += "* Unchanged objects: " + statusDescriptions ['unchanged' ] + "\n "
327343 key += (
328344 "* Object revocations: " + statusDescriptions ['revocations' ] + "\n "
329345 "* Object deprecations: " + statusDescriptions ['deprecations' ]
@@ -474,7 +490,7 @@ def get_layers_dict(self):
474490 "tactic" : phase ['phase_name' ],
475491 "enabled" : True ,
476492 "color" : statusToColor [status ],
477- "comment" : status [:- 1 ] # trim s off end of word
493+ "comment" : status [:- 1 ] if status != "unchanged" else status # trim s off end of word
478494 })
479495 used_statuses .add (status )
480496
@@ -528,8 +544,8 @@ def layers_dict_to_files(outfiles, layers):
528544 verboseprint ("writing layers dict to layer files... " , end = "" , flush = "true" )
529545
530546 # write each layer to separate files
531- json .dump (layers ['enterprise-attack' ], open (outfiles [0 ], "w" ), indent = 4 )
532- json .dump (layers ['mobile-attack' ], open (outfiles [1 ], "w" ), indent = 4 )
547+ if 'enterprise-attack' in layers : json .dump (layers ['enterprise-attack' ], open (outfiles [0 ], "w" ), indent = 4 )
548+ if 'mobile-attack' in layers : json .dump (layers ['mobile-attack' ], open (outfiles [1 ], "w" ), indent = 4 )
533549
534550 verboseprint ("done" )
535551
@@ -621,6 +637,11 @@ def layers_dict_to_files(outfiles, layers):
621637 help = "show changes to objects which didn't increment the version number"
622638 )
623639
640+ parser .add_argument ("--unchanged" ,
641+ action = "store_true" ,
642+ help = "show objects without changes in the markdown output"
643+ )
644+
624645 parser .add_argument ("--use-taxii" ,
625646 action = "store_true" ,
626647 help = "Use content from the ATT&CK TAXII server for the -old data"
@@ -648,6 +669,7 @@ def layers_dict_to_files(outfiles, layers):
648669 layers = args .layers ,
649670 markdown = args .markdown ,
650671 minor_changes = args .minor_changes ,
672+ unchanged = args .unchanged ,
651673 new = args .new ,
652674 old = args .old ,
653675 show_key = args .show_key ,
0 commit comments