@@ -122,7 +122,16 @@ def __init__(
122122 # }
123123 # software...
124124 }
125+ self .stixIDToName = {} # stixID to object name
126+ self .new_subtechnique_of_rels = [] # all subtechnique-of relationships in the new data
127+ self .old_subtechnique_of_rels = [] # all subtechnique-of relationships in the old data
128+ self .new_id_to_technique = {} # stixID => technique for every technique in the new data
129+ self .old_id_to_technique = {} # stixID => technique for every technique in the old data
130+ # build the bove data structures
125131 self .load_data ()
132+ # remove duplicate relationships
133+ self .new_subtechnique_of_rels = [i for n , i in enumerate (self .new_subtechnique_of_rels ) if i not in self .new_subtechnique_of_rels [n + 1 :]]
134+ self .old_subtechnique_of_rels = [i for n , i in enumerate (self .old_subtechnique_of_rels ) if i not in self .old_subtechnique_of_rels [n + 1 :]]
126135
127136
128137 def verboseprint (self , * args , ** kwargs ):
@@ -186,26 +195,43 @@ def load_datastore(data_store):
186195 "data_store" : data_store
187196 }
188197
198+ def parse_subtechniques (data_store , new = False ):
199+ # parse dataStore sub-technique-of relationships
200+ if new :
201+ for technique in list (data_store .query (attackTypeToStixFilter ["technique" ])):
202+ self .new_id_to_technique [technique ["id" ]] = technique
203+ self .new_subtechnique_of_rels += list (data_store .query ([
204+ Filter ("type" , "=" , "relationship" ),
205+ Filter ("relationship_type" , "=" , "subtechnique-of" )
206+ ]))
207+ else :
208+ for technique in list (data_store .query (attackTypeToStixFilter ["technique" ])):
209+ self .old_id_to_technique [technique ["id" ]] = technique
210+ self .old_subtechnique_of_rels += list (data_store .query ([
211+ Filter ("type" , "=" , "relationship" ),
212+ Filter ("relationship_type" , "=" , "subtechnique-of" )
213+ ]))
214+
189215 # load data from directory according to domain
190- def load_dir (dir ):
216+ def load_dir (dir , new = False ):
191217 data_store = MemoryStore ()
192218 datafile = os .path .join (dir , domain + ".json" )
193219 data_store .load_from_file (datafile )
194-
220+ parse_subtechniques ( data_store , new )
195221 return load_datastore (data_store )
196222
197223 # load data from TAXII server according to domain
198- def load_taxii ():
224+ def load_taxii (new = False ):
199225 collection = Collection ("https://cti-taxii.mitre.org/stix/collections/" + domainToTaxiiCollectionId [domain ])
200226 data_store = TAXIICollectionSource (collection )
201-
227+ parse_subtechniques ( data_store , new )
202228 return load_datastore (data_store )
203229
204230 if self .use_taxii :
205- old = load_taxii ()
231+ old = load_taxii (False )
206232 else :
207- old = load_dir (self .old )
208- new = load_dir (self .new )
233+ old = load_dir (self .old , False )
234+ new = load_dir (self .new , True )
209235
210236 intersection = old ["keys" ] & new ["keys" ]
211237 additions = new ["keys" ] - old ["keys" ]
@@ -226,7 +252,12 @@ def load_taxii():
226252 Filter ('type' , '=' , 'relationship' ),
227253 Filter ('relationship_type' , '=' , 'revoked-by' ),
228254 Filter ('source_ref' , '=' , key )
229- ])[0 ]["target_ref" ]
255+ ])
256+ if (len (revoked_by_key ) == 0 ):
257+ print ("WARNING: revoked object" , key , "has no revoked-by relationship" )
258+ continue
259+ else : revoked_by_key = revoked_by_key [0 ]["target_ref" ]
260+
230261 new ["id_to_obj" ][key ]["revoked_by" ] = new ["id_to_obj" ][revoked_by_key ]
231262
232263 revocations .add (key )
@@ -240,11 +271,11 @@ def load_taxii():
240271 try :
241272 old_version = float (old ["id_to_obj" ][key ]["x_mitre_version" ])
242273 except :
243- print ("old\n \t " + key )
274+ print ("ERROR: cannot get old version for object: " + key )
244275 try :
245276 new_version = float (new ["id_to_obj" ][key ]["x_mitre_version" ])
246277 except :
247- print ("new\n \t " + key )
278+ print ("ERROR: cannot get new version for object: " + key )
248279
249280 # check for changes
250281 if new_version > old_version :
@@ -307,12 +338,91 @@ def get_md_key(self):
307338 key += "\n " + "* Object deletions: " + statusDescriptions ['deletions' ]
308339 return f"{ key } "
309340
341+ def has_subtechniques (self , sdo , new = False ):
342+ """return true or false depending on whether the SDO has sub-techniques. new determines whether to parse from the new or old data"""
343+ if new : return len (list (filter (lambda rel : rel ["target_ref" ] == sdo ["id" ], self .new_subtechnique_of_rels ))) > 0
344+ else : return len (list (filter (lambda rel : rel ["target_ref" ] == sdo ["id" ], self .old_subtechnique_of_rels ))) > 0
310345
311346 def get_markdown_string (self ):
312347 """
313348 Return a markdown string summarizing detected differences.
314349 """
315350
351+ def getSectionList (items , obj_type , section ):
352+ """
353+ parse a list of items in a section and return a string for the items
354+ """
355+
356+ # get parents which have children
357+ childless = list (filter (lambda item : not self .has_subtechniques (item , True ) and not ("x_mitre_is_subtechnique" in item and item ["x_mitre_is_subtechnique" ]), items ))
358+ parents = list (filter (lambda item : self .has_subtechniques (item , True ) and not ("x_mitre_is_subtechnique" in item and item ["x_mitre_is_subtechnique" ]), items ))
359+ children = { item ["id" ]: item for item in filter (lambda item : "x_mitre_is_subtechnique" in item and item ["x_mitre_is_subtechnique" ], items ) }
360+
361+
362+
363+ parentToChildren = {} # stixID => [ children ]
364+ for relationship in self .new_subtechnique_of_rels :
365+ if relationship ["target_ref" ] in parentToChildren :
366+ if relationship ["source_ref" ] in children :
367+ parentToChildren [relationship ["target_ref" ]].append (children [relationship ["source_ref" ]])
368+ else :
369+ if relationship ["source_ref" ] in children :
370+ parentToChildren [relationship ["target_ref" ]] = children [relationship ["source_ref" ]]
371+ parentToChildren [relationship ["target_ref" ]] = [ children [relationship ["source_ref" ]] ]
372+
373+
374+ # now group parents and children
375+ groupings = []
376+
377+ for parent in childless + parents :
378+ parent_children = parentToChildren .pop (parent ["id" ]) if parent ["id" ] in parentToChildren else []
379+ groupings .append ({
380+ "parent" : parent ,
381+ "parentInSection" : True ,
382+ "children" : parent_children
383+ })
384+
385+ for parentID in parentToChildren :
386+ groupings .append ({
387+ "parent" : self .new_id_to_technique [parentID ],
388+ "parentInSection" : False ,
389+ "children" : parentToChildren [parentID ]
390+ })
391+
392+ groupings = sorted (groupings , key = lambda grouping : grouping ["parent" ]["name" ])
393+
394+ def placard (item ):
395+ """get a section list item for the given SDO according to section type"""
396+ if section == "revocations" :
397+ revoker = item ['revoked_by' ]
398+ if "x_mitre_is_subtechnique" in revoker and revoker ["x_mitre_is_subtechnique" ]:
399+ # get revoking technique's parent for display
400+ parentID = list (filter (lambda rel : rel ["source_ref" ] == revoker ["id" ], self .new_subtechnique_of_rels ))[0 ]["target_ref" ]
401+ parentName = self .new_id_to_technique [parentID ]["name" ] if parentID in self .new_id_to_technique else "ERROR NO PARENT"
402+ return f"{ item ['name' ]} (revoked by { parentName } : [{ revoker ['name' ]} ]({ self .site_prefix } /{ self .getUrlFromStix (revoker )} ))"
403+ else :
404+ return f"{ item ['name' ]} (revoked by [{ revoker ['name' ]} ]({ self .site_prefix } /{ self .getUrlFromStix (revoker )} ))"
405+ if section == "deletions" :
406+ return f"{ item ['name' ]} "
407+ else :
408+ return f"[{ item ['name' ]} ]({ self .site_prefix } /{ self .getUrlFromStix (item )} )"
409+
410+
411+ # build sectionList string
412+ sectionString = ""
413+ for grouping in groupings :
414+ if grouping ["parentInSection" ]:
415+ sectionString += f"* { placard (grouping ['parent' ]) } \n "
416+ # else:
417+ # sectionString += f"* _{grouping['parent']['name']}_\n"
418+ for child in sorted (grouping ["children" ], key = lambda child : child ["name" ]):
419+ if grouping ["parentInSection" ]:
420+ sectionString += f"\t * { placard (child ) } \n "
421+ else :
422+ sectionString += f"* { grouping ['parent' ]['name' ] } : { placard (child ) } \n "
423+
424+ return sectionString
425+
316426 self .verboseprint ("generating markdown string... " , end = "" , flush = "true" )
317427
318428 content = ""
@@ -321,17 +431,9 @@ def get_markdown_string(self):
321431 for domain in self .data [obj_type ]:
322432 domain_sections = ""
323433 for section in self .data [obj_type ][domain ]:
324- if section == "revocations" :
325- # handle revoked by
326- section_items = list (map (lambda d : f"* { d ['name' ]} (revoked by [{ d ['revoked_by' ]['name' ]} ]({ self .site_prefix } /{ self .getUrlFromStix (d ['revoked_by' ])} ))" , self .data [obj_type ][domain ][section ]))
327- elif section == "deletions" :
328- section_items = list (map (lambda d : f"* { d ['name' ]} " , self .data [obj_type ][domain ][section ]))
329- else :
330- section_items = list (map (lambda d : f"* [{ d ['name' ]} ]({ self .site_prefix } /{ self .getUrlFromStix (d )} )" , self .data [obj_type ][domain ][section ]))
331-
332- if len (section_items ) > 0 :
333- section_items = "\n " .join (sorted (section_items ))
334- else :
434+ if len (self .data [obj_type ][domain ][section ]) > 0 : # if there are items in the section
435+ section_items = getSectionList (self .data [obj_type ][domain ][section ], obj_type , section )
436+ else : # no items in section
335437 section_items = "No changes"
336438 header = sectionNameToSectionHeaders [section ] + ":"
337439 if "{obj_type}" in header :
0 commit comments