mal-toolbox 0.2.0__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,36 @@ def disaggregate_attack_step_full_name(
32
32
  return attack_step_full_name.split(':')
33
33
 
34
34
 
35
+ @dataclass
36
+ class Detector:
37
+ context: Context
38
+ name: Optional[str]
39
+ type: Optional[str]
40
+ tprate: Optional[dict]
41
+
42
+ def to_dict(self) -> dict:
43
+ return {
44
+ "context": self.context.to_dict(),
45
+ "name": self.name,
46
+ "type": self.type,
47
+ "tprate": self.tprate,
48
+ }
49
+
50
+
51
+ class Context(dict):
52
+ def __init__(self, context) -> None:
53
+ super().__init__(context)
54
+ self._context_dict = context
55
+ for label, asset in context.items():
56
+ setattr(self, label, asset)
57
+
58
+ def to_dict(self) -> dict:
59
+ return {label: asset.name for label, asset in self.items()}
60
+
61
+ def __str__(self) -> str:
62
+ return str({label: asset.name for label, asset in self._context_dict.items()})
63
+
64
+
35
65
  @dataclass
36
66
  class LanguageGraphAsset:
37
67
  name: str
@@ -60,8 +90,8 @@ class LanguageGraphAsset:
60
90
  'is_abstract': self.is_abstract
61
91
  }
62
92
 
63
- for assoc in self.own_associations.values():
64
- node_dict['associations'][assoc.full_name] = assoc.to_dict()
93
+ for fieldname, assoc in self.own_associations.items():
94
+ node_dict['associations'][fieldname] = assoc.to_dict()
65
95
  for attack_step in self.attack_steps.values():
66
96
  node_dict['attack_steps'][attack_step.name] = \
67
97
  attack_step.to_dict()
@@ -75,7 +105,7 @@ class LanguageGraphAsset:
75
105
 
76
106
 
77
107
  def __repr__(self) -> str:
78
- return str(self.to_dict())
108
+ return f'LanguageGraphAsset(name: "{self.name}")'
79
109
 
80
110
 
81
111
  def __hash__(self):
@@ -101,6 +131,7 @@ class LanguageGraphAsset:
101
131
  current_asset = current_asset.own_super_asset
102
132
  return False
103
133
 
134
+
104
135
  @cached_property
105
136
  def sub_assets(self) -> set[LanguageGraphAsset]:
106
137
  """
@@ -244,7 +275,9 @@ class LanguageGraphAssociation:
244
275
 
245
276
 
246
277
  def __repr__(self) -> str:
247
- return str(self.to_dict())
278
+ return (f'LanguageGraphAssociation(name: "{self.name}", '
279
+ f'left_field: {self.left_field}, '
280
+ f'right_field: {self.right_field})')
248
281
 
249
282
 
250
283
  @property
@@ -262,6 +295,15 @@ class LanguageGraphAssociation:
262
295
  return full_name
263
296
 
264
297
 
298
+ def get_field(self, fieldname: str) -> LanguageGraphAssociationField:
299
+ """
300
+ Return the field that matches the `fieldname` given as parameter.
301
+ """
302
+ if self.right_field.fieldname == fieldname:
303
+ return self.right_field
304
+ return self.left_field
305
+
306
+
265
307
  def contains_fieldname(self, fieldname: str) -> bool:
266
308
  """
267
309
  Check if the association contains the field name given as a parameter.
@@ -317,35 +359,6 @@ class LanguageGraphAssociation:
317
359
  raise LanguageGraphAssociationError(msg % (fieldname, self.name))
318
360
 
319
361
 
320
- def get_opposite_asset(
321
- self, asset: LanguageGraphAsset
322
- ) -> Optional[LanguageGraphAsset]:
323
- """
324
- Return the opposite asset if the association matches the asset given
325
- as a parameter. A match can either be an explicit one or if the asset
326
- given subassets either of the two assets that are part of the
327
- association.
328
-
329
- Arguments:
330
- asset - the asset to look for
331
- Return the other asset if the parameter matched either of the
332
- two. None, otherwise.
333
- """
334
- #TODO Should check to see which one is the tightest fit for
335
- # associations between assets on different levels of the same
336
- # inheritance chain.
337
- if asset.is_subasset_of(self.left_field.asset):
338
- return self.right_field.asset
339
- if asset.is_subasset_of(self.right_field.asset):
340
- return self.left_field.asset
341
-
342
- logger.warning(
343
- 'Requested asset "%s" from association %s'
344
- 'which did not contain it!', asset.name, self.name
345
- )
346
- return None
347
-
348
-
349
362
  @dataclass
350
363
  class LanguageGraphAttackStep:
351
364
  name: str
@@ -359,6 +372,7 @@ class LanguageGraphAttackStep:
359
372
  inherits: Optional[LanguageGraphAttackStep] = None
360
373
  tags: set = field(default_factory = set)
361
374
  _attributes: Optional[dict] = None
375
+ detectors: dict = field(default_factory = lambda: {})
362
376
 
363
377
 
364
378
  def __hash__(self):
@@ -386,7 +400,9 @@ class LanguageGraphAttackStep:
386
400
  'info': self.info,
387
401
  'overrides': self.overrides,
388
402
  'inherits': self.inherits.full_name if self.inherits else None,
389
- 'tags': list(self.tags)
403
+ 'tags': list(self.tags),
404
+ 'detectors': {label: detector.to_dict() for label, detector in
405
+ self.detectors.items()},
390
406
  }
391
407
 
392
408
  for child in self.children:
@@ -484,7 +500,7 @@ class ExpressionsChain:
484
500
  )
485
501
 
486
502
  return {
487
- self.association.full_name:
503
+ self.association.name:
488
504
  {
489
505
  'fieldname': self.fieldname,
490
506
  'asset type': asset_type
@@ -527,7 +543,7 @@ class ExpressionsChain:
527
543
  serialized_expr_chain: dict,
528
544
  lang_graph: LanguageGraph,
529
545
  ) -> Optional[ExpressionsChain]:
530
- """Create LanguageGraph from dict
546
+ """Create ExpressionsChain from dict
531
547
  Args:
532
548
  serialized_expr_chain - expressions chain in dict format
533
549
  lang_graph - the LanguageGraph that contains the assets,
@@ -567,10 +583,26 @@ class ExpressionsChain:
567
583
  assoc_name = list(serialized_expr_chain.keys())[0]
568
584
  target_asset = lang_graph.assets[\
569
585
  serialized_expr_chain[assoc_name]['asset type']]
586
+ fieldname = serialized_expr_chain[assoc_name]['fieldname']
587
+
588
+ association = None
589
+ for assoc in target_asset.associations.values():
590
+ if assoc.contains_fieldname(fieldname) and \
591
+ assoc.name == assoc_name:
592
+ association = assoc
593
+ break
594
+
595
+ if association is None:
596
+ msg = 'Failed to find association "%s" with '\
597
+ 'fieldname "%s"'
598
+ logger.error(msg % (assoc_name, fieldname))
599
+ raise LanguageGraphException(msg % (assoc_name,
600
+ fieldname))
601
+
570
602
  new_expr_chain = ExpressionsChain(
571
603
  type = 'field',
572
- association = target_asset.associations[assoc_name],
573
- fieldname = serialized_expr_chain[assoc_name]['fieldname']
604
+ association = association,
605
+ fieldname = fieldname
574
606
  )
575
607
  return new_expr_chain
576
608
 
@@ -629,6 +661,11 @@ class LanguageGraph():
629
661
  self._generate_graph()
630
662
 
631
663
 
664
+ def __repr__(self) -> str:
665
+ return (f'LanguageGraph(id: "{self.metadata.get("id", "N/A")}", '
666
+ f'version: "{self.metadata.get("version", "N/A")}")')
667
+
668
+
632
669
  @classmethod
633
670
  def from_mal_spec(cls, mal_spec_file: str) -> LanguageGraph:
634
671
  """
@@ -663,12 +700,18 @@ class LanguageGraph():
663
700
  'Serializing %s assets.', len(self.assets.items())
664
701
  )
665
702
 
666
- serialized_graph = {}
703
+ serialized_graph = {'metadata': self.metadata}
667
704
  for asset in self.assets.values():
668
705
  serialized_graph[asset.name] = asset.to_dict()
669
706
 
670
707
  return serialized_graph
671
708
 
709
+ def _link_association_to_assets(cls,
710
+ assoc: LanguageGraphAssociation,
711
+ left_asset: LanguageGraphAsset,
712
+ right_asset: LanguageGraphAsset):
713
+ left_asset.own_associations[assoc.right_field.fieldname] = assoc
714
+ right_asset.own_associations[assoc.left_field.fieldname] = assoc
672
715
 
673
716
  def save_to_file(self, filename: str) -> None:
674
717
  """Save to json/yml depending on extension"""
@@ -684,6 +727,8 @@ class LanguageGraph():
684
727
 
685
728
  logger.debug('Create language graph from dictionary.')
686
729
  lang_graph = LanguageGraph()
730
+ lang_graph.metadata = serialized_graph.pop('metadata')
731
+
687
732
  # Recreate all of the assets
688
733
  for asset_dict in serialized_graph.values():
689
734
  logger.debug(
@@ -766,11 +811,8 @@ class LanguageGraph():
766
811
  )
767
812
 
768
813
  # Add the association to the left and right asset
769
- associated_assets = [left_asset, right_asset]
770
- while associated_assets != []:
771
- asset = associated_assets.pop()
772
- if assoc_node.full_name not in asset.own_associations:
773
- asset.own_associations[assoc_node.full_name] = assoc_node
814
+ lang_graph._link_association_to_assets(assoc_node,
815
+ left_asset, right_asset)
774
816
 
775
817
  # Recreate the variables
776
818
  for asset_dict in serialized_graph.values():
@@ -1360,11 +1402,8 @@ class LanguageGraph():
1360
1402
  )
1361
1403
 
1362
1404
  # Add the association to the left and right asset
1363
- associated_assets = [left_asset, right_asset]
1364
- while associated_assets != []:
1365
- asset = associated_assets.pop()
1366
- if assoc_node.full_name not in asset.own_associations:
1367
- asset.own_associations[assoc_node.full_name] = assoc_node
1405
+ self._link_association_to_assets(assoc_node,
1406
+ left_asset, right_asset)
1368
1407
 
1369
1408
  # Set the variables
1370
1409
  for asset in self.assets.values():
@@ -1407,6 +1446,20 @@ class LanguageGraph():
1407
1446
  asset.attack_steps[attack_step_attribs['name']] = \
1408
1447
  attack_step_node
1409
1448
 
1449
+ for detector in attack_step_attribs.get("detectors",
1450
+ {}).values():
1451
+ attack_step_node.detectors[detector["name"]] = Detector(
1452
+ context=Context(
1453
+ {
1454
+ label: self.assets[asset]
1455
+ for label, asset in detector["context"].items()
1456
+ }
1457
+ ),
1458
+ name=detector.get("name"),
1459
+ type=detector.get("type"),
1460
+ tprate=detector.get("tprate"),
1461
+ )
1462
+
1410
1463
  # Create the inherited attack steps
1411
1464
  assets = list(self.assets.values())
1412
1465
  while len(assets) > 0:
@@ -1540,8 +1593,7 @@ class LanguageGraph():
1540
1593
  None,
1541
1594
  step_expression
1542
1595
  )
1543
- attack_step.own_requires.append(
1544
- self.reverse_expr_chain(result_expr_chain, None))
1596
+ attack_step.own_requires.append(result_expr_chain)
1545
1597
 
1546
1598
  def _get_attacks_for_asset_type(self, asset_type: str) -> dict:
1547
1599
  """
@@ -1681,51 +1733,3 @@ class LanguageGraph():
1681
1733
  self._generate_graph()
1682
1734
 
1683
1735
 
1684
- def get_association_by_fields_and_assets(
1685
- self,
1686
- first_field: str,
1687
- second_field: str,
1688
- first_asset_name: str,
1689
- second_asset_name: str
1690
- ) -> Optional[LanguageGraphAssociation]:
1691
- """
1692
- Get an association based on its field names and asset types
1693
-
1694
- Arguments:
1695
- first_field - a string containing the first field
1696
- second_field - a string containing the second field
1697
- first_asset_name - a string representing the first asset type
1698
- second_asset_name - a string representing the second asset type
1699
-
1700
- Return:
1701
- The association matching the fieldnames and asset types.
1702
- None if there is no match.
1703
- """
1704
- first_asset = self.assets[first_asset_name]
1705
- second_asset = self.assets[second_asset_name]
1706
-
1707
- for assoc in first_asset.associations.values():
1708
- logger.debug(
1709
- 'Compare ("%s", "%s", "%s", "%s") to '
1710
- '("%s", "%s", "%s", "%s").',
1711
- first_asset_name, first_field,
1712
- second_asset_name, second_field,
1713
- assoc.left_field.asset.name, assoc.left_field.fieldname,
1714
- assoc.right_field.asset.name, assoc.right_field.fieldname
1715
- )
1716
-
1717
- # If the asset and fields match either way we accept it as a
1718
- # match.
1719
- if assoc.left_field.fieldname == first_field and \
1720
- assoc.right_field.fieldname == second_field and \
1721
- first_asset.is_subasset_of(assoc.left_field.asset) and \
1722
- second_asset.is_subasset_of(assoc.right_field.asset):
1723
- return assoc
1724
-
1725
- if assoc.left_field.fieldname == second_field and \
1726
- assoc.right_field.fieldname == first_field and \
1727
- second_asset.is_subasset_of(assoc.left_field.asset) and \
1728
- first_asset.is_subasset_of(assoc.right_field.asset):
1729
- return assoc
1730
-
1731
- return None